@genex-ai/embed-sdk 0.16.0 → 0.21.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.
@@ -1,3 +1,15 @@
1
+ import {
2
+ NATIVE_CHANNEL,
3
+ NATIVE_PROTOCOL_VERSION,
4
+ NATIVE_RECEIVER,
5
+ encodeBridgeMessage,
6
+ hasNativeMarker,
7
+ isGenerationId,
8
+ isGenerationTerminal,
9
+ readBridgeNativeCommand,
10
+ readNativeEntry
11
+ } from "./chunk-Q476GJHI.js";
12
+
1
13
  // src/commerce.ts
2
14
  var cfg = null;
3
15
  function _initCommerce(c) {
@@ -105,234 +117,179 @@ async function buy(opts) {
105
117
  return awaitOutcome(view.id, opts.timeoutMs ?? 18e4);
106
118
  }
107
119
 
108
- // ../embed-protocol/src/constants.ts
109
- var NATIVE_CHANNEL = "genex-native";
110
- var NATIVE_PROTOCOL_VERSION = 1;
111
- var NATIVE_MARKER_PARAM = "genex_native";
112
- var NATIVE_MARKER_VALUE = "1";
113
- var NATIVE_PROTOCOL_PARAM = "genex_protocol";
114
- var NATIVE_ATTEMPT_PARAM = "genex_attempt";
115
- var NATIVE_NONCE_PARAM = "genex_nonce";
116
- var NATIVE_RECEIVER = "__genexNative";
117
- var MAX_BRIDGE_MESSAGE_BYTES = 4096;
118
- var BRIDGE_ID_RE = /^[0-9a-f]{32}$/;
119
- var SLUG_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
120
- function isBridgeId(value) {
121
- return typeof value === "string" && BRIDGE_ID_RE.test(value);
122
- }
123
- function isBridgeSlug(value) {
124
- return typeof value === "string" && SLUG_RE.test(value);
120
+ // src/generation.ts
121
+ var config = null;
122
+ function _initGeneration(value) {
123
+ config = value;
124
+ }
125
+ function must2() {
126
+ if (!config) throw new Error("genex generation: call initEmbed() first");
127
+ return config;
128
+ }
129
+ async function request(path, body) {
130
+ const c = must2();
131
+ const token = c.getToken();
132
+ if (!token) throw new Error("no_player_session");
133
+ const response = await fetch(`${c.apiUrl}/api/runtime${path}`, {
134
+ method: body === void 0 ? "GET" : "POST",
135
+ credentials: "omit",
136
+ cache: "no-store",
137
+ headers: { Authorization: `Bearer ${token}`, ...body === void 0 ? {} : { "Content-Type": "application/json" } },
138
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
139
+ });
140
+ const result = await response.json().catch(() => ({}));
141
+ if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : "generation_unavailable");
142
+ return result;
125
143
  }
126
-
127
- // ../embed-protocol/src/base64url.ts
128
- var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
129
- var LOOKUP = (() => {
130
- const table = new Array(128).fill(-1);
131
- for (let i = 0; i < ALPHABET.length; i++) table[ALPHABET.charCodeAt(i)] = i;
132
- return table;
133
- })();
134
- function utf8Bytes(text) {
135
- const out = [];
136
- for (let i = 0; i < text.length; i++) {
137
- let code = text.charCodeAt(i);
138
- if (code >= 55296 && code <= 56319) {
139
- const next = i + 1 < text.length ? text.charCodeAt(i + 1) : 0;
140
- if (next >= 56320 && next <= 57343) {
141
- code = 65536 + (code - 55296 << 10) + (next - 56320);
142
- i++;
143
- } else {
144
- code = 65533;
145
- }
146
- } else if (code >= 56320 && code <= 57343) {
147
- code = 65533;
148
- }
149
- if (code < 128) out.push(code);
150
- else if (code < 2048) out.push(192 | code >> 6, 128 | code & 63);
151
- else if (code < 65536) {
152
- out.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
153
- } else {
154
- out.push(
155
- 240 | code >> 18,
156
- 128 | code >> 12 & 63,
157
- 128 | code >> 6 & 63,
158
- 128 | code & 63
159
- );
160
- }
161
- }
162
- return out;
144
+ function validEstimateCoins(value) {
145
+ return Number.isSafeInteger(value) && value >= 0 && value <= 1e6;
163
146
  }
164
- function utf8Text(bytes) {
165
- let out = "";
166
- for (let i = 0; i < bytes.length; ) {
167
- const b0 = bytes[i];
168
- let code;
169
- let size;
170
- if (b0 < 128) {
171
- code = b0;
172
- size = 1;
173
- } else if ((b0 & 224) === 192) {
174
- code = b0 & 31;
175
- size = 2;
176
- } else if ((b0 & 240) === 224) {
177
- code = b0 & 15;
178
- size = 3;
179
- } else if ((b0 & 248) === 240) {
180
- code = b0 & 7;
181
- size = 4;
182
- } else return null;
183
- if (i + size > bytes.length) return null;
184
- for (let k = 1; k < size; k++) {
185
- const b = bytes[i + k];
186
- if ((b & 192) !== 128) return null;
187
- code = code << 6 | b & 63;
188
- }
189
- if (size === 2 && code < 128) return null;
190
- if (size === 3 && (code < 2048 || code >= 55296 && code <= 57343)) return null;
191
- if (size === 4 && (code < 65536 || code > 1114111)) return null;
192
- if (code > 65535) {
193
- const v = code - 65536;
194
- out += String.fromCharCode(55296 + (v >> 10), 56320 + (v & 1023));
195
- } else {
196
- out += String.fromCharCode(code);
197
- }
198
- i += size;
199
- }
200
- return out;
147
+ function getGenerationModels() {
148
+ return request("/models");
201
149
  }
202
- function utf8ByteLength(text) {
203
- return utf8Bytes(text).length;
150
+ function isWorkflowIdentifier(value) {
151
+ return typeof value === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(value) && value !== "." && value !== "..";
204
152
  }
205
- function encodeBase64UrlJson(value) {
206
- const bytes = utf8Bytes(JSON.stringify(value));
207
- let out = "";
208
- for (let i = 0; i < bytes.length; i += 3) {
209
- const b0 = bytes[i];
210
- const b1 = bytes[i + 1];
211
- const b2 = bytes[i + 2];
212
- out += ALPHABET[b0 >> 2];
213
- out += ALPHABET[(b0 & 3) << 4 | (b1 ?? 0) >> 4];
214
- if (b1 === void 0) break;
215
- out += ALPHABET[(b1 & 15) << 2 | (b2 ?? 0) >> 6];
216
- if (b2 === void 0) break;
217
- out += ALPHABET[b2 & 63];
218
- }
219
- return out;
153
+ function getWorkflowOfferings(workflowId) {
154
+ if (!isWorkflowIdentifier(workflowId)) return Promise.reject(new Error("invalid_workflow_id"));
155
+ return request(`/workflows/${encodeURIComponent(workflowId)}/offerings`);
220
156
  }
221
- function decodeBase64UrlJson(encoded) {
222
- if (encoded.length % 4 === 1) return void 0;
223
- const bytes = [];
224
- let buffer = 0;
225
- let bits = 0;
226
- for (let i = 0; i < encoded.length; i++) {
227
- const code = encoded.charCodeAt(i);
228
- const value = code < 128 ? LOOKUP[code] : -1;
229
- if (value < 0) return void 0;
230
- buffer = buffer << 6 | value;
231
- bits += 6;
232
- if (bits >= 8) {
233
- bits -= 8;
234
- bytes.push(buffer >> bits & 255);
235
- }
236
- }
237
- if (bits > 0 && (buffer & (1 << bits) - 1) !== 0) return void 0;
238
- const text = utf8Text(bytes);
239
- if (text === null) return void 0;
240
- try {
241
- return JSON.parse(text);
242
- } catch {
243
- return void 0;
244
- }
157
+ function getGeneration(generationId) {
158
+ if (!isGenerationId(generationId)) return Promise.reject(new Error("invalid_generation_id"));
159
+ return request(`/generations/${encodeURIComponent(generationId)}`);
245
160
  }
246
-
247
- // ../embed-protocol/src/messages.ts
248
- var MAX_TICKET_LENGTH = 2048;
249
- var MAX_EXPIRES_AT_LENGTH = 32;
250
- var ISO_INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
251
- function isRecord(value) {
252
- return typeof value === "object" && value !== null && !Array.isArray(value);
253
- }
254
- function hasExactKeys(value, keys) {
255
- const own = Object.keys(value);
256
- if (own.length !== keys.length) return false;
257
- for (const key of keys) {
258
- if (!Object.prototype.hasOwnProperty.call(value, key)) return false;
259
- }
260
- return true;
261
- }
262
- var BASE_KEYS = ["channel", "v", "type", "slug", "attemptId", "nonce"];
263
- function envelopeBaseIsValid(value) {
264
- return value.channel === NATIVE_CHANNEL && value.v === NATIVE_PROTOCOL_VERSION && isBridgeSlug(value.slug) && isBridgeId(value.attemptId) && isBridgeId(value.nonce);
265
- }
266
- function parseBridgeNativeCommand(value) {
267
- if (!isRecord(value) || !envelopeBaseIsValid(value)) return void 0;
268
- switch (value.type) {
269
- case "ticket":
270
- if (!hasExactKeys(value, [...BASE_KEYS, "ticket", "expiresAt"])) return void 0;
271
- if (typeof value.ticket !== "string" || value.ticket.length === 0 || value.ticket.length > MAX_TICKET_LENGTH) {
272
- return void 0;
273
- }
274
- if (typeof value.expiresAt !== "string" || value.expiresAt.length > MAX_EXPIRES_AT_LENGTH || !ISO_INSTANT_RE.test(value.expiresAt)) {
275
- return void 0;
276
- }
277
- return value;
278
- case "guest":
279
- case "cancel":
280
- if (!hasExactKeys(value, BASE_KEYS)) return void 0;
281
- return value;
282
- case "foreground":
283
- if (!hasExactKeys(value, [...BASE_KEYS, "active"])) return void 0;
284
- if (typeof value.active !== "boolean") return void 0;
285
- return value;
286
- case "network":
287
- if (!hasExactKeys(value, [...BASE_KEYS, "online"])) return void 0;
288
- if (typeof value.online !== "boolean") return void 0;
289
- return value;
290
- default:
291
- return void 0;
292
- }
161
+ function outcome(view) {
162
+ if (!isGenerationTerminal(view.status)) return void 0;
163
+ return {
164
+ status: view.status,
165
+ generationId: view.id,
166
+ ...billingReceipt(view),
167
+ ...view.status === "succeeded" ? {
168
+ output: view.output,
169
+ source: view.source,
170
+ ...view.modelProvenance ? { modelProvenance: view.modelProvenance } : {}
171
+ } : {},
172
+ ...view.error ? { error: view.error } : {}
173
+ };
293
174
  }
294
-
295
- // ../embed-protocol/src/transport.ts
296
- function read(raw, parse) {
297
- if (typeof raw !== "string" || raw.length === 0) return { ok: false, reason: "undecodable" };
298
- if (utf8ByteLength(raw) > MAX_BRIDGE_MESSAGE_BYTES) return { ok: false, reason: "too-large" };
299
- const decoded = decodeBase64UrlJson(raw);
300
- if (decoded === void 0) return { ok: false, reason: "undecodable" };
301
- const message = parse(decoded);
302
- if (message === void 0) return { ok: false, reason: "invalid" };
303
- return { ok: true, message };
304
- }
305
- function readBridgeNativeCommand(raw) {
306
- return read(raw, parseBridgeNativeCommand);
307
- }
308
- function encodeBridgeMessage(message) {
309
- const encoded = encodeBase64UrlJson(message);
310
- if (encoded.length > MAX_BRIDGE_MESSAGE_BYTES) return void 0;
311
- return encoded;
175
+ function billingReceipt(view) {
176
+ return {
177
+ ...view.billingStatus !== void 0 ? { billingStatus: view.billingStatus } : {},
178
+ ...view.reservedCoins !== void 0 ? { reservedCoins: view.reservedCoins } : {},
179
+ ...view.reservedDisplayUsdCents !== void 0 ? { reservedDisplayUsdCents: view.reservedDisplayUsdCents } : {},
180
+ ...view.chargedCoins !== void 0 ? { chargedCoins: view.chargedCoins } : {},
181
+ ...view.chargedDisplayUsdCents !== void 0 ? { chargedDisplayUsdCents: view.chargedDisplayUsdCents } : {}
182
+ };
312
183
  }
313
-
314
- // ../embed-protocol/src/entry-url.ts
315
- function readParam(search, key) {
316
- const query = search.charAt(0) === "?" ? search.slice(1) : search;
317
- if (query.length === 0) return null;
318
- for (const pair of query.split("&")) {
319
- const eq = pair.indexOf("=");
320
- if (eq === -1) continue;
321
- if (pair.slice(0, eq) === key) return pair.slice(eq + 1);
184
+ function authorizationOutcome(view) {
185
+ if (["queued", "dispatching", "awaiting_external", "succeeded"].includes(view.status)) {
186
+ return { status: "authorized", generationId: view.id, ...billingReceipt(view), ...view.funding ? { funding: view.funding } : {} };
322
187
  }
323
- return null;
188
+ if (!isGenerationTerminal(view.status)) return void 0;
189
+ return {
190
+ status: view.status === "canceled" || view.status === "expired" ? view.status : "failed",
191
+ generationId: view.id,
192
+ ...billingReceipt(view),
193
+ ...view.error ? { error: view.error } : view.status === "unknown" ? { error: "unknown_execution" } : {}
194
+ };
324
195
  }
325
- function hasNativeMarker(search) {
326
- return readParam(search, NATIVE_MARKER_PARAM) === NATIVE_MARKER_VALUE;
196
+ async function poll(generationId, timeoutMs, readOutcome, popup) {
197
+ const deadline = Date.now() + timeoutMs;
198
+ while (Date.now() < deadline) {
199
+ try {
200
+ const view = await getGeneration(generationId);
201
+ const result = readOutcome(view);
202
+ if (result) return result;
203
+ if (popup?.closed && view.status === "requires_confirmation") {
204
+ const canceled = await request(`/generations/${generationId}/cancel`, {});
205
+ const cancelResult = readOutcome(canceled);
206
+ if (cancelResult) return cancelResult;
207
+ }
208
+ } catch {
209
+ }
210
+ await new Promise((resolve) => setTimeout(resolve, 750));
211
+ }
212
+ return { status: "pending", generationId, error: "wait_timeout" };
213
+ }
214
+ function waitForGeneration(generationId, timeoutMs = 6e5) {
215
+ if (!isGenerationId(generationId)) return Promise.resolve({ status: "failed", error: "invalid_generation_id" });
216
+ return poll(generationId, timeoutMs, outcome);
217
+ }
218
+ async function generate(options) {
219
+ if (!validEstimateCoins(options.estimateCoins)) return { status: "failed", error: "invalid_estimate_coins" };
220
+ const { timeoutMs, idempotencyKey, ...input } = options;
221
+ return startGeneration(
222
+ "/generations",
223
+ { ...input, idempotencyKey },
224
+ timeoutMs ?? 6e5,
225
+ outcome
226
+ );
327
227
  }
328
- function readNativeEntry(search) {
329
- if (!hasNativeMarker(search)) return null;
330
- const attemptId = readParam(search, NATIVE_ATTEMPT_PARAM);
331
- const nonce = readParam(search, NATIVE_NONCE_PARAM);
332
- const protocol = Number(readParam(search, NATIVE_PROTOCOL_PARAM));
333
- if (!isBridgeId(attemptId) || !isBridgeId(nonce)) return null;
334
- if (protocol !== NATIVE_PROTOCOL_VERSION) return null;
335
- return { attemptId, nonce, protocol };
228
+ async function requestWorkflow(options) {
229
+ if (!validEstimateCoins(options.estimateCoins)) return { status: "failed", error: "invalid_estimate_coins" };
230
+ if (!isWorkflowIdentifier(options.workflowId)) return { status: "failed", error: "invalid_workflow_id" };
231
+ if (!isWorkflowIdentifier(options.offeringId)) return { status: "failed", error: "invalid_offering_id" };
232
+ return startGeneration(`/workflows/${encodeURIComponent(options.workflowId)}/requests`, {
233
+ offeringId: options.offeringId,
234
+ input: options.input,
235
+ estimateCoins: options.estimateCoins,
236
+ ...options.allowExternal !== void 0 ? { allowExternal: options.allowExternal } : {},
237
+ idempotencyKey: options.idempotencyKey
238
+ }, options.timeoutMs ?? 6e5, authorizationOutcome, options);
239
+ }
240
+ async function resumeWorkflow(generationId, options = {}) {
241
+ if (!isGenerationId(generationId)) return { status: "failed", error: "invalid_generation_id" };
242
+ return startGeneration("", {}, options.timeoutMs ?? 6e5, authorizationOutcome, void 0, generationId);
243
+ }
244
+ async function startGeneration(path, body, timeoutMs, readOutcome, workflow, resumeId) {
245
+ const c = must2();
246
+ if (c.isLocalTest()) return { status: "failed", error: "local_test_unsupported" };
247
+ if (c.isNative()) return { status: "failed", error: "native_unsupported" };
248
+ if (typeof window === "undefined") return { status: "failed", error: "no_window" };
249
+ if (!c.getToken()) return { status: "failed", error: "no_player_session" };
250
+ const parentOrigin2 = c.getParentOrigin();
251
+ if (c.isEmbedded() && (!parentOrigin2 || !c.dashboardOrigins.includes(parentOrigin2))) {
252
+ return { status: "failed", error: "untrusted_parent" };
253
+ }
254
+ let popup = null;
255
+ let dashboard;
256
+ if (!c.isEmbedded()) {
257
+ try {
258
+ dashboard = new URL(c.dashboardOrigins[0]);
259
+ if (dashboard.protocol !== "https:" && !(dashboard.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(dashboard.hostname))) throw new Error();
260
+ } catch {
261
+ return { status: "failed", error: "invalid_dashboard_origin" };
262
+ }
263
+ popup = window.open(`${dashboard.origin}/play/generate`, "_blank", "popup,width=480,height=760");
264
+ if (!popup) return { status: "failed", error: "popup_blocked" };
265
+ try {
266
+ popup.opener = null;
267
+ } catch {
268
+ }
269
+ }
270
+ try {
271
+ const view = resumeId ? await getGeneration(resumeId) : await request(path, { ...body, idempotencyKey: body.idempotencyKey ?? crypto.randomUUID() });
272
+ if (resumeId && (view.id !== resumeId || view.kind !== "workflow")) throw new Error("invalid_generation_response");
273
+ if (!isGenerationId(view.id) || workflow && (view.kind !== "workflow" || view.workflow?.id !== workflow.workflowId || view.workflow.offeringId !== workflow.offeringId)) {
274
+ throw new Error("invalid_generation_response");
275
+ }
276
+ const result = readOutcome(view);
277
+ const needsReceipt = view.billingStatus === "pending" || view.quote?.billingPolicy !== void 0 && (view.chargedCoins ?? 0) > 0;
278
+ if (result && result.status !== "authorized" && !needsReceipt) {
279
+ popup?.close();
280
+ return result;
281
+ }
282
+ if (popup && dashboard) {
283
+ if (!popup.closed) popup.location.href = `${dashboard.origin}/play/generate/${encodeURIComponent(view.id)}`;
284
+ } else if (parentOrigin2) {
285
+ window.parent.postMessage({ type: "genex:generation:confirm", v: 1, generationId: view.id }, parentOrigin2);
286
+ }
287
+ if (result) return result;
288
+ return poll(view.id, timeoutMs, readOutcome, popup);
289
+ } catch (error) {
290
+ popup?.close();
291
+ return { status: "failed", error: error instanceof Error ? error.message : "generation_unavailable" };
292
+ }
336
293
  }
337
294
 
338
295
  // src/index.ts
@@ -341,8 +298,8 @@ var RETRY_FLAG = "genex:embed:retry";
341
298
  var POPOVER_DISMISSED_FLAG = "genex:guest:popover-dismissed";
342
299
  var LOCAL_AUTH_FLAG = "genex:embed:local-auth";
343
300
  var PLAYER_ID_KEY = "genex:player";
344
- var SDK_VERSION = "0.16.0";
345
- var config = null;
301
+ var SDK_VERSION = "0.21.0";
302
+ var config2 = null;
346
303
  var state = "pending";
347
304
  var user = null;
348
305
  var localTestMode = false;
@@ -366,6 +323,8 @@ var nativeReauthTimeoutMs = 3e4;
366
323
  var heartbeatIntervalMs = 3e4;
367
324
  var handshakeTimer;
368
325
  var refreshTimer;
326
+ var unattendedWaiterTimer;
327
+ var earlyWaiterWarned = false;
369
328
  var messageHandler;
370
329
  var listeners = /* @__PURE__ */ new Map();
371
330
  var authWaiters = [];
@@ -382,8 +341,16 @@ function win() {
382
341
  function doc() {
383
342
  return globalThis.document;
384
343
  }
385
- function doFetch(input, init) {
386
- return globalThis.fetch(input, init);
344
+ function doFetch(input, init, timeoutMs) {
345
+ let signal;
346
+ if (timeoutMs !== void 0) {
347
+ try {
348
+ const ctor = globalThis.AbortSignal;
349
+ if (ctor && typeof ctor.timeout === "function") signal = ctor.timeout(timeoutMs);
350
+ } catch {
351
+ }
352
+ }
353
+ return globalThis.fetch(input, signal ? { ...init, signal } : init);
387
354
  }
388
355
  function readRetryFlag() {
389
356
  try {
@@ -449,16 +416,29 @@ function initEmbed(cfg2) {
449
416
  if (!w) return;
450
417
  if (initialized) return;
451
418
  initialized = true;
452
- config = {
419
+ if (unattendedWaiterTimer !== void 0) {
420
+ clearTimeout(unattendedWaiterTimer);
421
+ unattendedWaiterTimer = void 0;
422
+ }
423
+ config2 = {
453
424
  slug: cfg2.slug,
454
425
  apiUrl: cfg2.apiUrl.replace(/\/$/, ""),
455
426
  dashboardOrigins: [...cfg2.dashboardOrigins]
456
427
  };
457
428
  state = "pending";
458
429
  _initCommerce({
459
- apiUrl: config.apiUrl,
460
- dashboardOrigins: config.dashboardOrigins,
430
+ apiUrl: config2.apiUrl,
431
+ dashboardOrigins: config2.dashboardOrigins,
432
+ getToken: () => embedToken,
433
+ isEmbedded,
434
+ isNative: inNativeMode,
435
+ isLocalTest: () => localTestMode
436
+ });
437
+ _initGeneration({
438
+ apiUrl: config2.apiUrl,
439
+ dashboardOrigins: config2.dashboardOrigins,
461
440
  getToken: () => embedToken,
441
+ getParentOrigin: () => parentOrigin,
462
442
  isEmbedded,
463
443
  isNative: inNativeMode,
464
444
  isLocalTest: () => localTestMode
@@ -506,6 +486,7 @@ function getColyseusUrls() {
506
486
  function waitForAuth() {
507
487
  if (state === "authenticated" && user) return Promise.resolve({ user });
508
488
  if (state === "blocked") return Promise.reject(new Error("genex embed auth: blocked"));
489
+ noteWaiterBeforeInit("waitForAuth");
509
490
  return new Promise((resolve, reject) => {
510
491
  authWaiters.push({ resolve, reject });
511
492
  });
@@ -515,10 +496,29 @@ function waitForPlayer() {
515
496
  return Promise.resolve({ user, guest: state === "guest" });
516
497
  }
517
498
  if (state === "blocked") return Promise.reject(new Error("genex embed auth: blocked"));
499
+ noteWaiterBeforeInit("waitForPlayer");
518
500
  return new Promise((resolve, reject) => {
519
501
  playerWaiters.push({ resolve, reject });
520
502
  });
521
503
  }
504
+ function noteWaiterBeforeInit(fn) {
505
+ if (initialized) return;
506
+ if (!earlyWaiterWarned) {
507
+ earlyWaiterWarned = true;
508
+ try {
509
+ console.error(
510
+ `[genex] ${fn}() called before initEmbed() \u2014 identity can never resolve and the game will sit black. Call initEmbed({ slug, apiUrl, dashboardOrigins }) first (genex-threejs-embed-auth), and draw a frame before you await identity.`
511
+ );
512
+ } catch {
513
+ }
514
+ }
515
+ if (unattendedWaiterTimer === void 0) {
516
+ unattendedWaiterTimer = setTimeout(() => {
517
+ unattendedWaiterTimer = void 0;
518
+ if (!initialized) enterBlocked("no-identity");
519
+ }, handshakeTimeoutMs);
520
+ }
521
+ }
522
522
  function on(event, cb) {
523
523
  let set = listeners.get(event);
524
524
  if (!set) {
@@ -532,7 +532,7 @@ function on(event, cb) {
532
532
  }
533
533
  var KEEPALIVE_MAX_BYTES = 32 * 1024;
534
534
  async function ensurePlayer() {
535
- if (!config) throw new Error("genex embed: call initEmbed() first");
535
+ if (!config2) throw new Error("genex embed: call initEmbed() first");
536
536
  if (state === "pending") await waitForPlayer();
537
537
  if (state === "blocked") throw new Error("genex embed auth: blocked");
538
538
  }
@@ -541,7 +541,7 @@ function stateFetch(path, init = {}) {
541
541
  const headers = { Authorization: `Bearer ${embedToken}` };
542
542
  if (init.body !== void 0) headers["Content-Type"] = "application/json";
543
543
  if (init.ifVersion !== void 0) headers["If-Match"] = String(init.ifVersion);
544
- return doFetch(`${config.apiUrl}/api/projects/${encodeURIComponent(config.slug)}${path}`, {
544
+ return doFetch(`${config2.apiUrl}/api/projects/${encodeURIComponent(config2.slug)}${path}`, {
545
545
  method: init.method ?? "GET",
546
546
  headers,
547
547
  body: init.body,
@@ -631,7 +631,7 @@ async function submitScore(score, opts) {
631
631
  return { submitted: true, best: body.best, improved: body.improved };
632
632
  }
633
633
  async function getLeaderboard(opts) {
634
- if (!config) throw new Error("genex embed: call initEmbed() first");
634
+ if (!config2) throw new Error("genex embed: call initEmbed() first");
635
635
  if (localTestMode) return { items: [], me: null };
636
636
  if (state === "pending") {
637
637
  try {
@@ -647,7 +647,7 @@ async function getLeaderboard(opts) {
647
647
  const headers = {};
648
648
  if (embedToken) headers.Authorization = `Bearer ${embedToken}`;
649
649
  const res = await doFetch(
650
- `${config.apiUrl}/api/projects/${encodeURIComponent(config.slug)}/leaderboard${qs ? `?${qs}` : ""}`,
650
+ `${config2.apiUrl}/api/projects/${encodeURIComponent(config2.slug)}/leaderboard${qs ? `?${qs}` : ""}`,
651
651
  { headers }
652
652
  );
653
653
  if (!res.ok) throw new Error(`genex leaderboard failed (${res.status})`);
@@ -694,7 +694,7 @@ function acceptsTicket() {
694
694
  return (state === "pending" || state === "guest") && !redeeming;
695
695
  }
696
696
  async function handleParentMessage(event) {
697
- if (!config) return;
697
+ if (!config2) return;
698
698
  const data = event.data;
699
699
  if (!data || data.v !== PROTOCOL_VERSION) return;
700
700
  const isTicket = data.type === "genex:embed:ticket";
@@ -702,7 +702,7 @@ async function handleParentMessage(event) {
702
702
  if (!isTicket && !isGuest) return;
703
703
  if (isTicket && (!acceptsTicket() || typeof data.ticket !== "string" || !data.ticket)) return;
704
704
  if (isGuest && (state !== "pending" || redeeming || requestingGuest)) return;
705
- if (!config.dashboardOrigins.includes(event.origin)) {
705
+ if (!config2.dashboardOrigins.includes(event.origin)) {
706
706
  const fresh = await fetchDashboardOrigins();
707
707
  if (isTicket && !acceptsTicket()) return;
708
708
  if (isGuest && (state !== "pending" || redeeming || requestingGuest)) return;
@@ -739,13 +739,13 @@ function inNativeMode() {
739
739
  }
740
740
  function postToNative(message) {
741
741
  const w = win();
742
- if (!w || !config || !nativeEntry) return;
742
+ if (!w || !config2 || !nativeEntry) return;
743
743
  const bridge = nativeBridge(w);
744
744
  if (!bridge) return;
745
745
  const encoded = encodeBridgeMessage({
746
746
  channel: NATIVE_CHANNEL,
747
747
  v: NATIVE_PROTOCOL_VERSION,
748
- slug: config.slug,
748
+ slug: config2.slug,
749
749
  attemptId: nativeEntry.attemptId,
750
750
  nonce: nativeEntry.nonce,
751
751
  ...message
@@ -772,11 +772,11 @@ function startNativeHandshake(w, entry) {
772
772
  }, handshakeTimeoutMs);
773
773
  }
774
774
  async function handleNativeCommand(payload) {
775
- if (!config || !nativeEntry) return;
776
- const read2 = readBridgeNativeCommand(payload);
777
- if (!read2.ok) return;
778
- const command = read2.message;
779
- if (command.slug !== config.slug || command.attemptId !== nativeEntry.attemptId || command.nonce !== nativeEntry.nonce) {
775
+ if (!config2 || !nativeEntry) return;
776
+ const read = readBridgeNativeCommand(payload);
777
+ if (!read.ok) return;
778
+ const command = read.message;
779
+ if (command.slug !== config2.slug || command.attemptId !== nativeEntry.attemptId || command.nonce !== nativeEntry.nonce) {
780
780
  return;
781
781
  }
782
782
  switch (command.type) {
@@ -841,16 +841,25 @@ function postToParent(message, targetOrigin) {
841
841
  async function startStandaloneFlow(w) {
842
842
  const frag = readFragment(w);
843
843
  if (frag.ticket) {
844
+ armStandaloneWatchdog();
844
845
  await redeemTicket(frag.ticket);
845
846
  return;
846
847
  }
847
848
  if (frag.guest) {
849
+ armStandaloneWatchdog();
848
850
  await requestGuestSession();
849
851
  return;
850
852
  }
851
853
  showOverlay("redirecting");
852
854
  await redirectToAuthorize(w, { guestOk: true });
853
855
  }
856
+ function armStandaloneWatchdog() {
857
+ if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
858
+ handshakeTimer = setTimeout(() => {
859
+ handshakeTimer = void 0;
860
+ if (state === "pending") enterBlocked("no-identity");
861
+ }, handshakeTimeoutMs);
862
+ }
854
863
  var stashedTicketHash = null;
855
864
  function _stashTicketFromUrl() {
856
865
  const w = win();
@@ -896,44 +905,48 @@ function readFragment(w) {
896
905
  return { ticket, guest };
897
906
  }
898
907
  async function redirectToAuthorize(w, opts) {
899
- if (!config || localTestMode) return;
908
+ if (!config2 || localTestMode) return;
900
909
  if (inNativeMode()) return;
901
- const origin = opts?.guestOk ? config.dashboardOrigins[0] ?? (await fetchDashboardOrigins())[0] : (await fetchDashboardOrigins())[0] ?? config.dashboardOrigins[0];
910
+ const origin = opts?.guestOk ? config2.dashboardOrigins[0] ?? (await fetchDashboardOrigins())[0] : (await fetchDashboardOrigins())[0] ?? config2.dashboardOrigins[0];
902
911
  if (!origin) {
903
912
  enterBlocked();
904
913
  return;
905
914
  }
906
915
  const returnTo = w.location.origin + w.location.pathname;
907
916
  const guestParam = opts?.guestOk ? "&guest=ok" : "";
908
- const url = `${origin}/play/authorize?returnTo=${encodeURIComponent(returnTo)}&slug=${encodeURIComponent(config.slug)}${guestParam}`;
917
+ const url = `${origin}/play/authorize?returnTo=${encodeURIComponent(returnTo)}&slug=${encodeURIComponent(config2.slug)}${guestParam}`;
909
918
  w.location.replace(url);
910
919
  }
911
920
  async function fetchDashboardOrigins() {
912
- if (!config) return [];
921
+ if (!config2) return [];
913
922
  try {
914
- const res = await doFetch(`${config.apiUrl}/api/embed/dashboard-origins`);
915
- if (!res.ok) return config.dashboardOrigins;
923
+ const res = await doFetch(`${config2.apiUrl}/api/embed/dashboard-origins`);
924
+ if (!res.ok) return config2.dashboardOrigins;
916
925
  const body = await res.json();
917
926
  if (Array.isArray(body.origins) && body.origins.every((o) => typeof o === "string")) {
918
927
  return body.origins;
919
928
  }
920
929
  } catch {
921
930
  }
922
- return config.dashboardOrigins;
931
+ return config2.dashboardOrigins;
923
932
  }
924
933
  async function redeemTicket(ticket) {
925
- if (!config || localTestMode || redeeming) return;
934
+ if (!config2 || localTestMode || redeeming) return;
926
935
  const reauthing = nativeReauthPending && state === "authenticated";
927
936
  if (state !== "pending" && state !== "guest" && !reauthing) return;
928
937
  const upgradingFromGuest = state === "guest";
929
938
  const epoch = ++identityEpoch;
930
939
  redeeming = true;
931
940
  try {
932
- const res = await doFetch(`${config.apiUrl}/api/embed/session`, {
933
- method: "POST",
934
- headers: { "Content-Type": "application/json" },
935
- body: JSON.stringify({ ticket })
936
- });
941
+ const res = await doFetch(
942
+ `${config2.apiUrl}/api/embed/session`,
943
+ {
944
+ method: "POST",
945
+ headers: { "Content-Type": "application/json" },
946
+ body: JSON.stringify({ ticket })
947
+ },
948
+ handshakeTimeoutMs
949
+ );
937
950
  if (epoch !== identityEpoch) return;
938
951
  if (!res.ok) {
939
952
  emit("error", { error: new Error(`ticket redemption failed (${res.status})`) });
@@ -946,6 +959,8 @@ async function redeemTicket(ticket) {
946
959
  user = body.user;
947
960
  colyseusUrls = body.colyseus?.urls;
948
961
  state = "authenticated";
962
+ if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
963
+ handshakeTimer = void 0;
949
964
  clearRetryFlag();
950
965
  removeOverlay();
951
966
  removeGuestPopover();
@@ -972,19 +987,19 @@ async function redeemTicket(ticket) {
972
987
  }
973
988
  }
974
989
  async function requestGuestSession() {
975
- if (!config || localTestMode || requestingGuest || redeeming) return;
990
+ if (!config2 || localTestMode || requestingGuest || redeeming) return;
976
991
  if (state !== "pending" && state !== "guest") return;
977
992
  const epoch = ++identityEpoch;
978
993
  requestingGuest = true;
979
994
  try {
980
- const res = await doFetch(`${config.apiUrl}/api/embed/guest-session`, {
995
+ const res = await doFetch(`${config2.apiUrl}/api/embed/guest-session`, {
981
996
  method: "POST",
982
997
  headers: { "Content-Type": "application/json" },
983
998
  // playerId is analytics-only: the SERVER still mints the session's own
984
999
  // guest id and the token is unchanged — this just lets the play event
985
1000
  // key on something that survives the tab.
986
- body: JSON.stringify({ slug: config.slug, playerId: playerId() })
987
- });
1001
+ body: JSON.stringify({ slug: config2.slug, playerId: playerId() })
1002
+ }, handshakeTimeoutMs);
988
1003
  if (epoch !== identityEpoch) return;
989
1004
  if (!res.ok) {
990
1005
  emit("error", { error: new Error(`guest session failed (${res.status})`) });
@@ -1076,6 +1091,8 @@ function enterLocalTestMode(w) {
1076
1091
  }
1077
1092
  async function handleRedeemFailure() {
1078
1093
  const w = win();
1094
+ if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
1095
+ handshakeTimer = void 0;
1079
1096
  if (inNativeMode()) {
1080
1097
  clearNativeReauth();
1081
1098
  postNativeError("ticket-rejected");
@@ -1097,12 +1114,12 @@ function scheduleRefresh() {
1097
1114
  }, refreshDelayMs);
1098
1115
  }
1099
1116
  async function refreshToken() {
1100
- if (!config || state !== "authenticated" && state !== "guest" || !embedToken) return;
1117
+ if (!config2 || state !== "authenticated" && state !== "guest" || !embedToken) return;
1101
1118
  const epoch = identityEpoch;
1102
1119
  const tokenAtStart = embedToken;
1103
1120
  let status;
1104
1121
  try {
1105
- const res = await doFetch(`${config.apiUrl}/api/embed/session/refresh`, {
1122
+ const res = await doFetch(`${config2.apiUrl}/api/embed/session/refresh`, {
1106
1123
  method: "POST",
1107
1124
  headers: { Authorization: `Bearer ${tokenAtStart}` },
1108
1125
  // Best-effort completion if the tab starts unloading mid-refresh; zero
@@ -1469,8 +1486,11 @@ function __resetForTests(overrides) {
1469
1486
  messageHandler = void 0;
1470
1487
  if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
1471
1488
  if (refreshTimer !== void 0) clearTimeout(refreshTimer);
1489
+ if (unattendedWaiterTimer !== void 0) clearTimeout(unattendedWaiterTimer);
1472
1490
  handshakeTimer = void 0;
1473
1491
  refreshTimer = void 0;
1492
+ unattendedWaiterTimer = void 0;
1493
+ earlyWaiterWarned = false;
1474
1494
  stopNativeTimers();
1475
1495
  if (w) delete w[NATIVE_RECEIVER];
1476
1496
  nativeEntry = null;
@@ -1479,7 +1499,7 @@ function __resetForTests(overrides) {
1479
1499
  removeOverlay(true);
1480
1500
  removeGuestPopover();
1481
1501
  overlayFontRequested = false;
1482
- config = null;
1502
+ config2 = null;
1483
1503
  state = "pending";
1484
1504
  user = null;
1485
1505
  localTestMode = false;
@@ -1510,6 +1530,13 @@ export {
1510
1530
  getEntitlements,
1511
1531
  consumeEntitlement,
1512
1532
  buy,
1533
+ getGenerationModels,
1534
+ getWorkflowOfferings,
1535
+ getGeneration,
1536
+ waitForGeneration,
1537
+ generate,
1538
+ requestWorkflow,
1539
+ resumeWorkflow,
1513
1540
  initEmbed,
1514
1541
  isEmbedded,
1515
1542
  getAuthState,