@genex-ai/embed-sdk 0.6.0 → 0.7.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.
@@ -12,9 +12,11 @@ var parentOrigin = null;
12
12
  var initialized = false;
13
13
  var redeeming = false;
14
14
  var requestingGuest = false;
15
+ var identityEpoch = 0;
15
16
  var handshakeTimeoutMs = 1e4;
16
17
  var refreshDelayMs = 10 * 6e4;
17
18
  var refreshRetryMs = 6e4;
19
+ var overlayTextDelayMs = 500;
18
20
  var handshakeTimer;
19
21
  var refreshTimer;
20
22
  var messageHandler;
@@ -419,6 +421,7 @@ async function fetchDashboardOrigins() {
419
421
  async function redeemTicket(ticket) {
420
422
  if (!config || localTestMode || redeeming || state !== "pending" && state !== "guest") return;
421
423
  const upgradingFromGuest = state === "guest";
424
+ const epoch = ++identityEpoch;
422
425
  redeeming = true;
423
426
  try {
424
427
  const res = await doFetch(`${config.apiUrl}/api/embed/session`, {
@@ -426,12 +429,14 @@ async function redeemTicket(ticket) {
426
429
  headers: { "Content-Type": "application/json" },
427
430
  body: JSON.stringify({ ticket })
428
431
  });
432
+ if (epoch !== identityEpoch) return;
429
433
  if (!res.ok) {
430
434
  emit("error", { error: new Error(`ticket redemption failed (${res.status})`) });
431
435
  if (!upgradingFromGuest) await handleRedeemFailure();
432
436
  return;
433
437
  }
434
438
  const body = await res.json();
439
+ if (epoch !== identityEpoch) return;
435
440
  embedToken = body.embedToken;
436
441
  user = body.user;
437
442
  colyseusUrls = body.colyseus?.urls;
@@ -449,6 +454,7 @@ async function redeemTicket(ticket) {
449
454
  for (const waiter of playerWaiters.splice(0)) waiter.resolve({ user, guest: false });
450
455
  emit("authenticated", ctx);
451
456
  } catch (error) {
457
+ if (epoch !== identityEpoch) return;
452
458
  emit("error", { error });
453
459
  if (!upgradingFromGuest) await handleRedeemFailure();
454
460
  } finally {
@@ -458,6 +464,7 @@ async function redeemTicket(ticket) {
458
464
  async function requestGuestSession() {
459
465
  if (!config || localTestMode || requestingGuest || redeeming) return;
460
466
  if (state !== "pending" && state !== "guest") return;
467
+ const epoch = ++identityEpoch;
461
468
  requestingGuest = true;
462
469
  try {
463
470
  const res = await doFetch(`${config.apiUrl}/api/embed/guest-session`, {
@@ -465,17 +472,20 @@ async function requestGuestSession() {
465
472
  headers: { "Content-Type": "application/json" },
466
473
  body: JSON.stringify({ slug: config.slug })
467
474
  });
475
+ if (epoch !== identityEpoch) return;
468
476
  if (!res.ok) {
469
477
  emit("error", { error: new Error(`guest session failed (${res.status})`) });
470
478
  enterBlocked();
471
479
  return;
472
480
  }
473
481
  const body = await res.json();
482
+ if (epoch !== identityEpoch) return;
474
483
  embedToken = body.embedToken;
475
484
  user = body.user;
476
485
  colyseusUrls = body.colyseus?.urls;
477
486
  enterGuest();
478
487
  } catch (error) {
488
+ if (epoch !== identityEpoch) return;
479
489
  emit("error", { error });
480
490
  enterBlocked();
481
491
  } finally {
@@ -553,18 +563,22 @@ function scheduleRefresh() {
553
563
  }
554
564
  async function refreshToken() {
555
565
  if (!config || state !== "authenticated" && state !== "guest" || !embedToken) return;
566
+ const epoch = identityEpoch;
567
+ const tokenAtStart = embedToken;
556
568
  let status;
557
569
  try {
558
570
  const res = await doFetch(`${config.apiUrl}/api/embed/session/refresh`, {
559
571
  method: "POST",
560
- headers: { Authorization: `Bearer ${embedToken}` },
572
+ headers: { Authorization: `Bearer ${tokenAtStart}` },
561
573
  // Best-effort completion if the tab starts unloading mid-refresh; zero
562
574
  // security consequence either way (worst case the token just expires).
563
575
  keepalive: true
564
576
  });
565
577
  status = res.status;
578
+ if (epoch !== identityEpoch || embedToken !== tokenAtStart) return;
566
579
  if (res.ok) {
567
580
  const body = await res.json();
581
+ if (epoch !== identityEpoch || embedToken !== tokenAtStart) return;
568
582
  embedToken = body.embedToken;
569
583
  scheduleRefresh();
570
584
  return;
@@ -572,6 +586,7 @@ async function refreshToken() {
572
586
  } catch {
573
587
  status = void 0;
574
588
  }
589
+ if (epoch !== identityEpoch || embedToken !== tokenAtStart) return;
575
590
  if (status === 401) {
576
591
  if (state === "guest") {
577
592
  void requestGuestSession();
@@ -588,6 +603,7 @@ async function refreshToken() {
588
603
  }
589
604
  function enterBlocked() {
590
605
  if (state === "blocked") return;
606
+ identityEpoch++;
591
607
  state = "blocked";
592
608
  user = null;
593
609
  embedToken = void 0;
@@ -604,13 +620,36 @@ function enterBlocked() {
604
620
  emit("blocked");
605
621
  }
606
622
  var OVERLAY_TEXT = {
607
- connecting: { title: "Connecting to Genex\u2026" },
608
- // Neutral on purpose: the automatic bounce precedes GUEST play as often as
609
- // sign-in "signing you in" would be wrong for most visitors.
610
- redirecting: { title: "Loading\u2026" },
611
- "blocked-standalone": { title: "Sign in to play", button: "Sign in" },
612
- "blocked-embedded": { title: "Sign in on the Genex dashboard to continue" }
623
+ // Delayed (text grace period): most connects finish before the text ever
624
+ // shows a sub-second text flash reads as a glitch.
625
+ connecting: { title: "Connecting\u2026" },
626
+ // Same copy as `connecting` so the whole standalone bounce (gate ->
627
+ // /play/authorize -> gate) reads as ONE continuous screen. Still neutral:
628
+ // the automatic bounce precedes GUEST play as often as sign-in.
629
+ redirecting: { title: "Connecting\u2026" },
630
+ // Terminal states need the visitor to act — text shows immediately.
631
+ "blocked-standalone": { title: "Sign in to play", button: "Sign in", instant: true },
632
+ "blocked-embedded": { title: "Sign in on the Genex dashboard to continue", instant: true }
613
633
  };
634
+ var OVERLAY_FONT_URL = "https://cdn.genex.technology/fonts/Geist-variable.woff2";
635
+ var OVERLAY_FONT_STACK = "'Geist',system-ui,-apple-system,sans-serif";
636
+ var overlayFontRequested = false;
637
+ function ensureOverlayFont(d) {
638
+ if (overlayFontRequested) return;
639
+ overlayFontRequested = true;
640
+ try {
641
+ const FF = win()?.FontFace;
642
+ const fonts = d.fonts;
643
+ if (!FF || !fonts?.add) return;
644
+ const face = new FF("Geist", `url(${OVERLAY_FONT_URL}) format('woff2')`, {
645
+ weight: "100 900",
646
+ display: "swap"
647
+ });
648
+ face.load().then((loaded) => fonts.add(loaded)).catch(() => {
649
+ });
650
+ } catch {
651
+ }
652
+ }
614
653
  function showOverlay(kind) {
615
654
  const d = doc();
616
655
  if (!d?.createElement) return;
@@ -620,23 +659,45 @@ function showOverlay(kind) {
620
659
  } catch {
621
660
  }
622
661
  }
623
- function removeOverlay() {
662
+ function removeOverlay(instant = false) {
624
663
  try {
625
- const root = overlayEl?.root;
626
- root?.remove?.();
664
+ const el = overlayEl;
665
+ overlayEl = null;
666
+ if (!el) return;
667
+ el.clearTextTimer();
668
+ const root = el.root;
669
+ if (instant || !root.style) {
670
+ root.remove?.();
671
+ return;
672
+ }
673
+ root.style.pointerEvents = "none";
674
+ root.style.transition = "opacity 180ms ease";
675
+ root.style.opacity = "0";
676
+ setTimeout(() => {
677
+ try {
678
+ root.remove?.();
679
+ } catch {
680
+ }
681
+ }, 220);
627
682
  } catch {
628
683
  }
629
- overlayEl = null;
630
684
  }
631
685
  function buildOverlay(d) {
686
+ ensureOverlayFont(d);
632
687
  const root = d.createElement("div");
633
688
  root.setAttribute("data-genex-embed-overlay", "");
634
- root.style.cssText = "position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px;background:#080a14;color:#fff;font-family:system-ui,-apple-system,sans-serif;text-align:center;pointer-events:auto;user-select:none";
689
+ root.style.cssText = `position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px;background:#000;color:#fff;font-family:${OVERLAY_FONT_STACK};text-align:center;pointer-events:auto;user-select:none`;
635
690
  const title = d.createElement("div");
636
- title.style.cssText = "font-size:18px;font-weight:600;padding:0 24px";
691
+ title.style.cssText = "font-size:18px;font-weight:600;padding:0 24px;opacity:0";
637
692
  root.appendChild(title);
638
693
  const button = d.createElement("button");
639
- button.style.cssText = "font-size:15px;font-weight:600;padding:10px 28px;border-radius:8px;border:none;cursor:pointer;background:#fff;color:#111;display:none";
694
+ button.style.cssText = "font-size:15px;font-weight:600;padding:10px 28px;border-radius:9px;border:none;cursor:pointer;background:#4b60bf;color:#fff;display:none";
695
+ button.addEventListener("mouseenter", () => {
696
+ button.style.background = "#3f51a8";
697
+ });
698
+ button.addEventListener("mouseleave", () => {
699
+ button.style.background = "#4b60bf";
700
+ });
640
701
  button.addEventListener("click", () => {
641
702
  const w = win();
642
703
  if (!w) return;
@@ -650,11 +711,32 @@ function buildOverlay(d) {
650
711
  };
651
712
  if (d.body) mount();
652
713
  else d.addEventListener?.("DOMContentLoaded", mount);
714
+ let textTimer;
715
+ const clearTextTimer = () => {
716
+ if (textTimer !== void 0) {
717
+ clearTimeout(textTimer);
718
+ textTimer = void 0;
719
+ }
720
+ };
653
721
  return {
654
722
  root,
723
+ clearTextTimer,
655
724
  setContent(kind) {
656
725
  const text = OVERLAY_TEXT[kind];
726
+ clearTextTimer();
657
727
  title.textContent = text.title;
728
+ if (text.instant || overlayTextDelayMs <= 0) {
729
+ title.style.transition = "";
730
+ title.style.opacity = "1";
731
+ } else {
732
+ title.style.transition = "";
733
+ title.style.opacity = "0";
734
+ textTimer = setTimeout(() => {
735
+ textTimer = void 0;
736
+ title.style.transition = "opacity 200ms ease";
737
+ title.style.opacity = "1";
738
+ }, overlayTextDelayMs);
739
+ }
658
740
  button.style.display = text.button ? "inline-block" : "none";
659
741
  if (text.button) button.textContent = text.button;
660
742
  mount();
@@ -678,15 +760,22 @@ function removeGuestPopover() {
678
760
  popoverEl = null;
679
761
  }
680
762
  function buildGuestPopover(d) {
763
+ ensureOverlayFont(d);
681
764
  const root = d.createElement("div");
682
765
  root.setAttribute("data-genex-guest-popover", "");
683
- 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";
766
+ 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(0,0,0,0.85);color:#fff;font-family:${OVERLAY_FONT_STACK};font-size:13px;box-shadow:0 4px 24px rgba(0,0,0,0.4);pointer-events:auto;user-select:none`;
684
767
  const text = d.createElement("span");
685
768
  text.textContent = user?.name ? `Playing as ${user.name} \u2014 sign in to save progress` : "Sign in to save progress";
686
769
  root.appendChild(text);
687
770
  const signIn = d.createElement("button");
688
771
  signIn.textContent = "Sign in";
689
- signIn.style.cssText = "font-size:13px;font-weight:600;padding:6px 16px;border-radius:999px;border:none;cursor:pointer;background:#fff;color:#111";
772
+ signIn.style.cssText = "font-size:13px;font-weight:600;padding:6px 16px;border-radius:9px;border:none;cursor:pointer;background:#4b60bf;color:#fff";
773
+ signIn.addEventListener("mouseenter", () => {
774
+ signIn.style.background = "#3f51a8";
775
+ });
776
+ signIn.addEventListener("mouseleave", () => {
777
+ signIn.style.background = "#4b60bf";
778
+ });
690
779
  signIn.addEventListener("click", () => {
691
780
  const w = win();
692
781
  if (!w) return;
@@ -719,8 +808,9 @@ function __resetForTests(overrides) {
719
808
  if (refreshTimer !== void 0) clearTimeout(refreshTimer);
720
809
  handshakeTimer = void 0;
721
810
  refreshTimer = void 0;
722
- removeOverlay();
811
+ removeOverlay(true);
723
812
  removeGuestPopover();
813
+ overlayFontRequested = false;
724
814
  config = null;
725
815
  state = "pending";
726
816
  user = null;
@@ -731,6 +821,7 @@ function __resetForTests(overrides) {
731
821
  initialized = false;
732
822
  redeeming = false;
733
823
  requestingGuest = false;
824
+ identityEpoch++;
734
825
  listeners.clear();
735
826
  authWaiters = [];
736
827
  playerWaiters = [];
@@ -741,6 +832,7 @@ function __resetForTests(overrides) {
741
832
  handshakeTimeoutMs = overrides?.handshakeTimeoutMs ?? 1e4;
742
833
  refreshDelayMs = overrides?.refreshDelayMs ?? 10 * 6e4;
743
834
  refreshRetryMs = overrides?.refreshRetryMs ?? 6e4;
835
+ overlayTextDelayMs = overrides?.overlayTextDelayMs ?? 500;
744
836
  }
745
837
 
746
838
  export {
package/dist/index.d.ts CHANGED
@@ -116,7 +116,7 @@ declare function getUser(): EmbedUser | null;
116
116
  */
117
117
  declare function getEmbedToken(): string | undefined;
118
118
  /**
119
- * The credential for multiplayer: pass as `connect({ ..., auth: getColyseusAuth() })`
119
+ * The credential for multiplayer: pass as `connect({ ..., auth: () => getColyseusAuth() })`
120
120
  * AFTER `await waitForPlayer()` — the relay rejects tokenless joins, but
121
121
  * accepts guest tokens. Read it fresh at every connect() call (tokens rotate
122
122
  * ~every 10 minutes). NEVER log this value.
@@ -221,6 +221,7 @@ declare function __resetForTests(overrides?: {
221
221
  handshakeTimeoutMs?: number;
222
222
  refreshDelayMs?: number;
223
223
  refreshRetryMs?: number;
224
+ overlayTextDelayMs?: number;
224
225
  }): void;
225
226
 
226
227
  export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, type Leaderboard, type LeaderboardEntry, type PlayerStateResult, type SaveStateResult, type SubmitScoreResult, type WorldStateResult, __resetForTests, _stashTicketFromUrl, getAuthState, getColyseusAuth, getColyseusUrls, getEmbedToken, getLeaderboard, getUser, initEmbed, isEmbedded, loadPlayerState, loadWorldState, on, savePlayerState, saveWorldState, submitScore, waitForAuth, waitForPlayer };
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  submitScore,
18
18
  waitForAuth,
19
19
  waitForPlayer
20
- } from "./chunk-5VYRFOFN.js";
20
+ } from "./chunk-BWJ2FVCH.js";
21
21
  export {
22
22
  __resetForTests,
23
23
  _stashTicketFromUrl,
package/dist/sentry.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  _stashTicketFromUrl,
3
3
  getUser,
4
4
  on
5
- } from "./chunk-5VYRFOFN.js";
5
+ } from "./chunk-BWJ2FVCH.js";
6
6
 
7
7
  // src/sentry.ts
8
8
  import * as Sentry from "@sentry/browser";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/embed-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Player identity + durable game state for genex games — signed-in or guest play, per-player save slots, shared world state, and soft-trust leaderboards.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",