@genex-ai/embed-sdk 0.4.0 → 0.5.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.
@@ -5,6 +5,7 @@ var POPOVER_DISMISSED_FLAG = "genex:guest:popover-dismissed";
5
5
  var config = null;
6
6
  var state = "pending";
7
7
  var user = null;
8
+ var localTestMode = false;
8
9
  var embedToken;
9
10
  var colyseusUrl;
10
11
  var parentOrigin = null;
@@ -77,6 +78,10 @@ function initEmbed(cfg) {
77
78
  dashboardOrigins: [...cfg.dashboardOrigins]
78
79
  };
79
80
  state = "pending";
81
+ if (wantsLocalTest(w)) {
82
+ enterLocalTestMode(w);
83
+ return;
84
+ }
80
85
  showOverlay("connecting");
81
86
  if (isEmbedded()) {
82
87
  startEmbeddedHandshake(w);
@@ -138,6 +143,7 @@ async function ensurePlayer() {
138
143
  if (state === "pending") await waitForPlayer();
139
144
  if (state === "blocked") throw new Error("genex embed auth: blocked");
140
145
  }
146
+ var lastWorldVersion;
141
147
  function stateFetch(path, init = {}) {
142
148
  const headers = { Authorization: `Bearer ${embedToken}` };
143
149
  if (init.body !== void 0) headers["Content-Type"] = "application/json";
@@ -192,18 +198,22 @@ async function loadWorldState() {
192
198
  const res = await stateFetch("/state");
193
199
  if (!res.ok) throw new Error(`genex load world state failed (${res.status})`);
194
200
  const body = await res.json();
201
+ lastWorldVersion = body.version;
195
202
  return { data: body.data, version: body.version };
196
203
  }
197
204
  async function saveWorldState(data, opts) {
198
205
  await ensurePlayer();
199
206
  if (state === "guest") return { saved: false, guest: true };
200
- return decodeSave(
207
+ const guard = opts && "ifVersion" in opts ? opts.ifVersion : lastWorldVersion;
208
+ const result = await decodeSave(
201
209
  await stateFetch("/state", {
202
210
  method: "PUT",
203
211
  body: JSON.stringify(data),
204
- ifVersion: opts?.ifVersion
212
+ ifVersion: guard == null ? void 0 : guard
205
213
  })
206
214
  );
215
+ if (result.version !== void 0) lastWorldVersion = result.version;
216
+ return result;
207
217
  }
208
218
  async function submitScore(score, opts) {
209
219
  const board = opts?.board ?? "main";
@@ -229,6 +239,7 @@ async function submitScore(score, opts) {
229
239
  }
230
240
  async function getLeaderboard(opts) {
231
241
  if (!config) throw new Error("genex embed: call initEmbed() first");
242
+ if (localTestMode) return { items: [], me: null };
232
243
  if (state === "pending") {
233
244
  try {
234
245
  await waitForPlayer();
@@ -378,7 +389,7 @@ function readFragment(w) {
378
389
  return { ticket, guest };
379
390
  }
380
391
  async function redirectToAuthorize(w, opts) {
381
- if (!config) return;
392
+ if (!config || localTestMode) return;
382
393
  const origin = opts?.guestOk ? config.dashboardOrigins[0] ?? (await fetchDashboardOrigins())[0] : (await fetchDashboardOrigins())[0] ?? config.dashboardOrigins[0];
383
394
  if (!origin) {
384
395
  enterBlocked();
@@ -403,7 +414,7 @@ async function fetchDashboardOrigins() {
403
414
  return config.dashboardOrigins;
404
415
  }
405
416
  async function redeemTicket(ticket) {
406
- if (!config || redeeming || state !== "pending" && state !== "guest") return;
417
+ if (!config || localTestMode || redeeming || state !== "pending" && state !== "guest") return;
407
418
  const upgradingFromGuest = state === "guest";
408
419
  redeeming = true;
409
420
  try {
@@ -443,7 +454,7 @@ async function redeemTicket(ticket) {
443
454
  }
444
455
  }
445
456
  async function requestGuestSession() {
446
- if (!config || requestingGuest || redeeming) return;
457
+ if (!config || localTestMode || requestingGuest || redeeming) return;
447
458
  if (state !== "pending" && state !== "guest") return;
448
459
  requestingGuest = true;
449
460
  try {
@@ -489,6 +500,39 @@ function enterGuest() {
489
500
  emit("guest", ctx);
490
501
  }
491
502
  }
503
+ function isLoopbackLocation(w) {
504
+ try {
505
+ const u = new URL(w.location.origin);
506
+ return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]");
507
+ } catch {
508
+ return false;
509
+ }
510
+ }
511
+ function wantsLocalTest(w) {
512
+ if (isEmbedded()) return false;
513
+ if (!isLoopbackLocation(w)) return false;
514
+ try {
515
+ return new URLSearchParams(w.location.search).get("genex_local_test") === "1";
516
+ } catch {
517
+ return false;
518
+ }
519
+ }
520
+ function enterLocalTestMode(w) {
521
+ localTestMode = true;
522
+ readFragment(w);
523
+ const localUser = { id: "local:test", name: "Local Tester" };
524
+ user = localUser;
525
+ state = "guest";
526
+ removeOverlay();
527
+ try {
528
+ console.info(
529
+ "[genex] local test mode \u2014 local visuals and controls only. Auth, saves, leaderboards, and multiplayer are NOT validated here; no Genex credential exists."
530
+ );
531
+ } catch {
532
+ }
533
+ for (const waiter of playerWaiters.splice(0)) waiter.resolve({ user: localUser, guest: true });
534
+ emit("guest", { user: localUser });
535
+ }
492
536
  async function handleRedeemFailure() {
493
537
  const w = win();
494
538
  if (w && !isEmbedded() && !readRetryFlag()) {
@@ -678,6 +722,7 @@ function __resetForTests(overrides) {
678
722
  config = null;
679
723
  state = "pending";
680
724
  user = null;
725
+ localTestMode = false;
681
726
  embedToken = void 0;
682
727
  colyseusUrl = void 0;
683
728
  parentOrigin = null;
package/dist/index.d.ts CHANGED
@@ -98,6 +98,9 @@ declare function getAuthState(): AuthState;
98
98
  /**
99
99
  * The current player — non-null once 'authenticated' OR 'guest'. Guests have
100
100
  * an `id` prefixed `guest:` and a server-assigned name like "Guest-1234".
101
+ * Local test mode is the one guest-state exception: `id` is `local:test`
102
+ * (a prefix that can never collide with `guest:` or account ids) and the
103
+ * name is "Local Tester".
101
104
  */
102
105
  declare function getUser(): EmbedUser | null;
103
106
  /**
@@ -171,13 +174,16 @@ declare function loadWorldState(): Promise<WorldStateResult>;
171
174
  /**
172
175
  * Save the game's SHARED world slot (any JSON ≤ 1MB, any signed-in player may
173
176
  * write — let ONE authority do it, e.g. the multiplayer host, and debounce).
174
- * Strongly prefer { ifVersion } here: the slot is shared, so unconditional
175
- * writes silently lose races. Guests: { saved: false, guest: true } — world
176
- * saves are NEVER queued (flushing a stale world later would clobber the live
177
- * one; the eventual account host keeps saving instead).
177
+ * If-Match is sent BY DEFAULT (from the last version this SDK loaded/saved), so a
178
+ * shared write can't silently clobber a newer one — a lost race resolves
179
+ * { conflict: true, version }: reload, merge, retry. Pass { ifVersion: n } to
180
+ * guard against a specific version, or { ifVersion: null } to force an
181
+ * unconditional overwrite (the old last-write-wins behaviour). Guests:
182
+ * { saved: false, guest: true } — world saves are NEVER queued (flushing a stale
183
+ * world later would clobber the live one; the eventual account host keeps saving).
178
184
  */
179
185
  declare function saveWorldState(data: unknown, opts?: {
180
- ifVersion?: number;
186
+ ifVersion?: number | null;
181
187
  }): Promise<SaveStateResult>;
182
188
  /**
183
189
  * Submit a score (soft-trust leaderboards: identity is verified server-side,
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  submitScore,
17
17
  waitForAuth,
18
18
  waitForPlayer
19
- } from "./chunk-6E5DPEVG.js";
19
+ } from "./chunk-GDJLPL6W.js";
20
20
  export {
21
21
  __resetForTests,
22
22
  _stashTicketFromUrl,
package/dist/sentry.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  _stashTicketFromUrl,
3
3
  getUser,
4
4
  on
5
- } from "./chunk-6E5DPEVG.js";
5
+ } from "./chunk-GDJLPL6W.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.4.0",
3
+ "version": "0.5.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",