@genex-ai/embed-sdk 0.22.3 → 0.24.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.
- package/README.md +130 -2
- package/dist/{chunk-IFI2NYBC.js → chunk-GR56KHWN.js} +315 -20
- package/dist/development.js +1 -1
- package/dist/index.d.ts +103 -6
- package/dist/index.js +10 -2
- package/dist/sentry.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-Q476GJHI.js → chunk-VPIQVRVS.js} +0 -0
package/README.md
CHANGED
|
@@ -48,8 +48,9 @@ return `native_unsupported` and `local_test_unsupported` until they have support
|
|
|
48
48
|
confirmation surfaces.
|
|
49
49
|
|
|
50
50
|
`GenerateOptions` requires `estimateCoins` (an integer from 1 to 1,000,000 for an API-backed selection), `modelId`, `prompt`, `outputFormat: 'text' | 'json'`,
|
|
51
|
-
optional bounded `schema`, `allowExternal` (default false), `idempotencyKey
|
|
52
|
-
`timeoutMs` (default ten minutes)
|
|
51
|
+
optional bounded `schema`, `allowExternal` (default false), `idempotencyKey`,
|
|
52
|
+
`timeoutMs` (default ten minutes) and `grantId` — a standing budget the player
|
|
53
|
+
already approved, which removes the popup entirely (see *Standing budgets*). Reuse an idempotency key only for the same
|
|
53
54
|
operation; store it if you need retries to survive a reload. Only models returned
|
|
54
55
|
by Genex are accepted. This first adapter generates text and JSON, including
|
|
55
56
|
creature descriptions, dialogue, rules and other structured game data.
|
|
@@ -107,6 +108,128 @@ nor hosts subscription credentials.
|
|
|
107
108
|
generated it; `modelProvenance: 'unverified'` makes that explicit. Treat every generated output as data. Never execute returned code
|
|
108
109
|
or use a claimed model/source as authority to mint coins, rewards or items.
|
|
109
110
|
|
|
111
|
+
## Standing budgets
|
|
112
|
+
|
|
113
|
+
SDK **0.24.0+**. A one-time `generate()` asks the player once per call. When a
|
|
114
|
+
feature calls the model continuously — a thinking NPC, a director rewriting
|
|
115
|
+
the next encounter — ask for a **standing budget** instead: the player approves
|
|
116
|
+
once, sets their own limit on a Genex slider, and the game then calls the model
|
|
117
|
+
with no popup at all until the limit, a stop, or the budget's expiry.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { requestSpendGrant, generate, getSpendGrant, stopSpendGrant, generationErrorMessage } from '@genex-ai/embed-sdk';
|
|
121
|
+
|
|
122
|
+
let grantId: string | undefined;
|
|
123
|
+
|
|
124
|
+
// Call it from the click, before awaiting: the approval surface is reserved
|
|
125
|
+
// synchronously, exactly like generate().
|
|
126
|
+
talkButton.addEventListener('click', async () => {
|
|
127
|
+
if (!grantId) {
|
|
128
|
+
const budget = await requestSpendGrant({
|
|
129
|
+
models: [model.id],
|
|
130
|
+
perCallMaxCoins: 8, // Hard ceiling per call. The server refuses more.
|
|
131
|
+
perCallEstimateCoins: 5, // The honest per-call price, benchmarked first.
|
|
132
|
+
disclosure: { periodLabel: 'minute', estimatedCallsPerPeriod: 4, estimatedCoinsPerPeriod: 20 },
|
|
133
|
+
maxConcurrent: 2,
|
|
134
|
+
idempotencyKey: 'npc-conversation-budget',
|
|
135
|
+
});
|
|
136
|
+
if (budget.status !== 'active') return showNotice(generationErrorMessage(budget.error));
|
|
137
|
+
grantId = budget.grantId;
|
|
138
|
+
}
|
|
139
|
+
const reply = await generate({ ...request, grantId });
|
|
140
|
+
if (reply.status === 'succeeded') applyGeneration(reply);
|
|
141
|
+
else showNotice(generationErrorMessage(reply.error));
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`requestSpendGrant()` resolves `active` once the budget can serve calls,
|
|
146
|
+
`canceled` when the player closes the sheet or stops it, `expired` when the
|
|
147
|
+
approval window passes, `pending` with `error: 'wait_timeout'` when the SDK
|
|
148
|
+
stopped waiting (the sheet may still be open), and `failed` with a server code.
|
|
149
|
+
The successful result carries `grantId` and the full server `grant` view.
|
|
150
|
+
|
|
151
|
+
`generate({ grantId })` runs one call under that budget. It opens **no window,
|
|
152
|
+
posts no parent message and needs no user gesture**, so it works from a game
|
|
153
|
+
loop or a timer. Every other guard is unchanged: production play, a signed-in
|
|
154
|
+
player, a trusted parent origin, and no native or local-test surface. The
|
|
155
|
+
funding door was chosen once in the sheet, so `allowExternal` is ignored on a
|
|
156
|
+
budget call. Everything else is the ordinary generation result, including the
|
|
157
|
+
receipt: a started attempt is charged its fixed price whether it succeeds,
|
|
158
|
+
fails, or is stopped.
|
|
159
|
+
|
|
160
|
+
`getSpendGrant(id)` is the readout to draw in the game: `limitCoins`,
|
|
161
|
+
`spentCoins`, `heldCoins` (admitted, not settled yet), `remainingCoins`,
|
|
162
|
+
`limitCalls`, `callCount`, `status` and the frozen `terms`. Poll it on the
|
|
163
|
+
cadence your HUD needs — never derive the remaining budget from your own count
|
|
164
|
+
of calls, and never show a figure the server did not send. Store `grantId` the
|
|
165
|
+
way you store a `generationId`; `getSpendGrant()` recovers the budget after a
|
|
166
|
+
reload, and a budget whose status is no longer live simply needs a new
|
|
167
|
+
approval. `stopSpendGrant(id)` releases it when your feature ends. Stopping is
|
|
168
|
+
prospective: calls already started finish and are charged. The player has the
|
|
169
|
+
same lever on Genex, on every page, and theirs always outranks the game's.
|
|
170
|
+
|
|
171
|
+
**The disclosure is a promise, not a guess.** `disclosure` is what the Genex
|
|
172
|
+
sheet shows the player, attributed to your game — Genex prints a server-computed
|
|
173
|
+
worst case beside it, so an optimistic number is visible as one. Declare the
|
|
174
|
+
rate you actually expect, benchmark `perCallEstimateCoins` before you declare it
|
|
175
|
+
(see *Benchmark your own coin costs* below), and keep `perCallMaxCoins` a real
|
|
176
|
+
ceiling. The server refuses a call whose declared price is out of proportion to
|
|
177
|
+
the model's own cost (`grant_price_unreasonable`) — that check is what replaces
|
|
178
|
+
the popup, so a budget is never a blank cheque. The slider's bounds, the coin
|
|
179
|
+
value and the recommended headroom all come from the server; read them from
|
|
180
|
+
`getGenerationModels()` (`standingGrant`, `recommendedDeclaredHeadroomBps`)
|
|
181
|
+
rather than typing any of them into the game.
|
|
182
|
+
|
|
183
|
+
**Handle the refusals as game states.** A budget ends for ordinary reasons and
|
|
184
|
+
the game should have believable copy for each:
|
|
185
|
+
`grant_limit_reached` (the player's limit is spent), `grant_stopped`,
|
|
186
|
+
`grant_expired`, `grant_not_active`, `grant_concurrency` and
|
|
187
|
+
`grant_rate_limited` (your own declared ceilings), `grant_insufficient_funds`
|
|
188
|
+
(the wallet ran low — the budget resumes when it is topped up),
|
|
189
|
+
`waiting_for_plan` (a personal plan's own rate limit; it resumes by itself),
|
|
190
|
+
`grant_price_unreasonable`, `grant_already_active`, `external_grant_active` and
|
|
191
|
+
`invalid_grant_limit`. `generationErrorMessage(code)` returns a player-facing
|
|
192
|
+
sentence for every one of these and for every one-time code as well; show it, or
|
|
193
|
+
write your own in-fiction line keyed off the same code. Never show the raw code,
|
|
194
|
+
and never treat `unknown` as failure — it means the outcome is not settled yet.
|
|
195
|
+
|
|
196
|
+
**The receiver pattern.** Persistence is the game's job, and it is what decides
|
|
197
|
+
whether a generated thing survives a reload:
|
|
198
|
+
|
|
199
|
+
- Write exactly one function, `applyGeneration(result)`, that turns an output
|
|
200
|
+
into game state and saves it through the SDK's player-state API.
|
|
201
|
+
- In the click path, save the `generationId` (and `grantId`) into player state
|
|
202
|
+
**before** awaiting, then call `applyGeneration`.
|
|
203
|
+
- On boot, call `waitForGeneration(savedId)` for any stored id and call the
|
|
204
|
+
*same* `applyGeneration`.
|
|
205
|
+
|
|
206
|
+
One writer, two entry points: the difference between something that works once
|
|
207
|
+
and something that survives a reload. Validate `result.output` against the shape
|
|
208
|
+
your game expects before using it, always. It is data, never authority for
|
|
209
|
+
coins, rewards or items.
|
|
210
|
+
|
|
211
|
+
## Your subscription and the Genex player watcher
|
|
212
|
+
|
|
213
|
+
Both approval sheets can offer the player their own Claude or ChatGPT plan
|
|
214
|
+
beside coins, when the selected model's provider matches. That choice lives in
|
|
215
|
+
the trusted Genex surface only — never add a plan row to the game's own model
|
|
216
|
+
picker, and never ask a player for a subscription credential. Genex neither
|
|
217
|
+
collects nor hosts them.
|
|
218
|
+
|
|
219
|
+
A personal-plan request is answered on the player's own computer by the **Genex
|
|
220
|
+
player watcher**, a small program that runs the official `claude` / `codex` CLIs
|
|
221
|
+
they signed into themselves. If a player picks their plan and has not installed
|
|
222
|
+
it, Genex shows them the one-time prompt: *"Install the Genex player watcher:
|
|
223
|
+
run `npx @genex-ai/cli-demo@latest player install` and approve it on Genex."*
|
|
224
|
+
The game does not install, start or speak to the watcher; it only reads
|
|
225
|
+
`watcherOnline` on a grant or generation view when Genex includes it. A
|
|
226
|
+
personal-plan result costs zero coin, is metered in requests rather than coin,
|
|
227
|
+
and arrives with `source: 'external'` and `modelProvenance: 'unverified'`:
|
|
228
|
+
user-supplied output, never proof that a particular model produced it, and
|
|
229
|
+
never authority for money or rewards. When the player's own plan hits its rate limit
|
|
230
|
+
the budget reports `waiting_for_plan` and resumes on its own; it never falls back
|
|
231
|
+
to paid work.
|
|
232
|
+
|
|
110
233
|
## Registered generation workflows
|
|
111
234
|
|
|
112
235
|
SDK **0.21.0+** supports multi-step generation and usage receipts through an
|
|
@@ -207,6 +330,11 @@ describes both APIs, executor authorization, recovery and connector endpoints.
|
|
|
207
330
|
|
|
208
331
|
## Benchmark your own coin costs on the server
|
|
209
332
|
|
|
333
|
+
`npx genex llm bench` is the CLI door onto this lane: it runs the real model
|
|
334
|
+
against your own coin wallet, reports what it actually charged, and recommends
|
|
335
|
+
the price to declare. Reach for the API below when you want the samples in your
|
|
336
|
+
own script.
|
|
337
|
+
|
|
210
338
|
SDK **0.21.0+** provides `@genex-ai/embed-sdk/development`. Use it only in a
|
|
211
339
|
local Node script or trusted server with your own full creator bearer credential
|
|
212
340
|
and an owned `projectId`. It needs no production play token and cannot choose a
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isGenerationTerminal,
|
|
9
9
|
readBridgeNativeCommand,
|
|
10
10
|
readNativeEntry
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-VPIQVRVS.js";
|
|
12
12
|
|
|
13
13
|
// src/commerce.ts
|
|
14
14
|
var cfg = null;
|
|
@@ -217,7 +217,8 @@ function waitForGeneration(generationId, timeoutMs = 6e5) {
|
|
|
217
217
|
}
|
|
218
218
|
async function generate(options) {
|
|
219
219
|
if (!validEstimateCoins(options.estimateCoins)) return { status: "failed", error: "invalid_estimate_coins" };
|
|
220
|
-
const { timeoutMs, idempotencyKey, ...input } = options;
|
|
220
|
+
const { timeoutMs, idempotencyKey, grantId, ...input } = options;
|
|
221
|
+
if (grantId !== void 0) return generateUnderGrant(grantId, input, idempotencyKey, timeoutMs ?? 6e5);
|
|
221
222
|
return startGeneration(
|
|
222
223
|
"/generations",
|
|
223
224
|
{ ...input, idempotencyKey },
|
|
@@ -241,7 +242,7 @@ async function resumeWorkflow(generationId, options = {}) {
|
|
|
241
242
|
if (!isGenerationId(generationId)) return { status: "failed", error: "invalid_generation_id" };
|
|
242
243
|
return startGeneration("", {}, options.timeoutMs ?? 6e5, authorizationOutcome, void 0, generationId);
|
|
243
244
|
}
|
|
244
|
-
|
|
245
|
+
function approvalGuards() {
|
|
245
246
|
const c = must2();
|
|
246
247
|
if (c.isLocalTest()) return { status: "failed", error: "local_test_unsupported" };
|
|
247
248
|
if (c.isNative()) return { status: "failed", error: "native_unsupported" };
|
|
@@ -251,22 +252,32 @@ async function startGeneration(path, body, timeoutMs, readOutcome, workflow, res
|
|
|
251
252
|
if (c.isEmbedded() && (!parentOrigin2 || !c.dashboardOrigins.includes(parentOrigin2))) {
|
|
252
253
|
return { status: "failed", error: "untrusted_parent" };
|
|
253
254
|
}
|
|
254
|
-
|
|
255
|
+
return { parentOrigin: parentOrigin2 };
|
|
256
|
+
}
|
|
257
|
+
function reserveApprovalSurface(placeholder) {
|
|
258
|
+
const guarded = approvalGuards();
|
|
259
|
+
if ("status" in guarded) return guarded;
|
|
260
|
+
const c = must2();
|
|
261
|
+
if (c.isEmbedded()) return { popup: null, parentOrigin: guarded.parentOrigin };
|
|
255
262
|
let dashboard;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
}
|
|
263
|
+
try {
|
|
264
|
+
dashboard = new URL(c.dashboardOrigins[0]);
|
|
265
|
+
if (dashboard.protocol !== "https:" && !(dashboard.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(dashboard.hostname))) throw new Error();
|
|
266
|
+
} catch {
|
|
267
|
+
return { status: "failed", error: "invalid_dashboard_origin" };
|
|
269
268
|
}
|
|
269
|
+
const popup = window.open(`${dashboard.origin}${placeholder}`, "_blank", "popup,width=480,height=760");
|
|
270
|
+
if (!popup) return { status: "failed", error: "popup_blocked" };
|
|
271
|
+
try {
|
|
272
|
+
popup.opener = null;
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
return { popup, dashboard, parentOrigin: guarded.parentOrigin };
|
|
276
|
+
}
|
|
277
|
+
async function startGeneration(path, body, timeoutMs, readOutcome, workflow, resumeId) {
|
|
278
|
+
const surface = reserveApprovalSurface("/play/generate");
|
|
279
|
+
if ("status" in surface) return surface;
|
|
280
|
+
const { popup, dashboard, parentOrigin: parentOrigin2 } = surface;
|
|
270
281
|
try {
|
|
271
282
|
const view = resumeId ? await getGeneration(resumeId) : await request(path, { ...body, idempotencyKey: body.idempotencyKey ?? crypto.randomUUID() });
|
|
272
283
|
if (resumeId && (view.id !== resumeId || view.kind !== "workflow")) throw new Error("invalid_generation_response");
|
|
@@ -291,6 +302,274 @@ async function startGeneration(path, body, timeoutMs, readOutcome, workflow, res
|
|
|
291
302
|
return { status: "failed", error: error instanceof Error ? error.message : "generation_unavailable" };
|
|
292
303
|
}
|
|
293
304
|
}
|
|
305
|
+
var LIVE_GRANT_STATUSES = ["active", "suspended_insufficient_funds", "waiting_for_plan"];
|
|
306
|
+
function isGrantId(value) {
|
|
307
|
+
return isGenerationId(value);
|
|
308
|
+
}
|
|
309
|
+
function validCount(value, min, max) {
|
|
310
|
+
return Number.isSafeInteger(value) && value >= min && value <= max;
|
|
311
|
+
}
|
|
312
|
+
function invalidGrantOptions(options) {
|
|
313
|
+
if (!Array.isArray(options.models) || options.models.length < 1 || options.models.length > 8 || options.models.some((id) => typeof id !== "string" || !id || id.length > 150) || new Set(options.models).size !== options.models.length) return "invalid_models";
|
|
314
|
+
if (!validCount(options.perCallMaxCoins, 1, 1e6) || !validCount(options.perCallEstimateCoins, 1, 1e6) || options.perCallEstimateCoins > options.perCallMaxCoins) return "invalid_declared_price";
|
|
315
|
+
const d = options.disclosure;
|
|
316
|
+
if (!d || typeof d !== "object" || !["minute", "hour", "session"].includes(d.periodLabel) || !validCount(d.estimatedCallsPerPeriod, 1, 1e4) || !validCount(d.estimatedCoinsPerPeriod, 0, 1e6)) return "invalid_disclosure";
|
|
317
|
+
if (options.maxConcurrent !== void 0 && !validCount(options.maxConcurrent, 1, 8)) return "invalid_grant_terms";
|
|
318
|
+
if (options.maxCallsPerMinute !== void 0 && !validCount(options.maxCallsPerMinute, 1, 120)) return "invalid_grant_terms";
|
|
319
|
+
if (options.maxPendingExternal !== void 0 && !validCount(options.maxPendingExternal, 1, 8)) return "invalid_grant_terms";
|
|
320
|
+
if (options.allowExternal !== void 0 && typeof options.allowExternal !== "boolean") return "invalid_grant_terms";
|
|
321
|
+
if (options.idempotencyKey !== void 0 && (typeof options.idempotencyKey !== "string" || options.idempotencyKey.length < 8 || options.idempotencyKey.length > 100)) return "invalid_idempotency_key";
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
324
|
+
function grantOutcome(view) {
|
|
325
|
+
if (LIVE_GRANT_STATUSES.includes(view.status)) return { status: "active", grantId: view.id, grant: view };
|
|
326
|
+
switch (view.status) {
|
|
327
|
+
// The player closing the sheet, or stopping later, is a normal outcome.
|
|
328
|
+
case "stopped":
|
|
329
|
+
return { status: "canceled", grantId: view.id, grant: view };
|
|
330
|
+
case "expired":
|
|
331
|
+
return { status: "expired", grantId: view.id, grant: view };
|
|
332
|
+
// Spent out before it could serve this caller — a real answer, with the
|
|
333
|
+
// same code a call under an exhausted budget reports.
|
|
334
|
+
case "exhausted":
|
|
335
|
+
return { status: "failed", grantId: view.id, grant: view, error: "grant_limit_reached" };
|
|
336
|
+
default:
|
|
337
|
+
return void 0;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function getSpendGrant(grantId) {
|
|
341
|
+
if (!isGrantId(grantId)) return Promise.reject(new Error("invalid_grant_id"));
|
|
342
|
+
return request(`/grants/${encodeURIComponent(grantId)}`);
|
|
343
|
+
}
|
|
344
|
+
function stopSpendGrant(grantId) {
|
|
345
|
+
if (!isGrantId(grantId)) return Promise.reject(new Error("invalid_grant_id"));
|
|
346
|
+
return request(`/grants/${encodeURIComponent(grantId)}/stop`, {});
|
|
347
|
+
}
|
|
348
|
+
async function pollGrant(grantId, timeoutMs, popup) {
|
|
349
|
+
const deadline = Date.now() + timeoutMs;
|
|
350
|
+
while (Date.now() < deadline) {
|
|
351
|
+
try {
|
|
352
|
+
const view = await getSpendGrant(grantId);
|
|
353
|
+
const result = grantOutcome(view);
|
|
354
|
+
if (result) return result;
|
|
355
|
+
if (popup?.closed && view.status === "requires_confirmation") {
|
|
356
|
+
const stopped = await request(`/grants/${encodeURIComponent(grantId)}/stop`, {});
|
|
357
|
+
const stopResult = grantOutcome(stopped);
|
|
358
|
+
if (stopResult) return stopResult;
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
363
|
+
}
|
|
364
|
+
return { status: "pending", grantId, error: "wait_timeout" };
|
|
365
|
+
}
|
|
366
|
+
async function requestSpendGrant(options) {
|
|
367
|
+
const invalid = invalidGrantOptions(options);
|
|
368
|
+
if (invalid) return { status: "failed", error: invalid };
|
|
369
|
+
const surface = reserveApprovalSurface("/play/grant");
|
|
370
|
+
if ("status" in surface) return surface;
|
|
371
|
+
const { popup, dashboard, parentOrigin: parentOrigin2 } = surface;
|
|
372
|
+
try {
|
|
373
|
+
const view = await request("/grants", {
|
|
374
|
+
idempotencyKey: options.idempotencyKey ?? crypto.randomUUID(),
|
|
375
|
+
models: options.models,
|
|
376
|
+
perCallMaxCoins: options.perCallMaxCoins,
|
|
377
|
+
perCallEstimateCoins: options.perCallEstimateCoins,
|
|
378
|
+
disclosure: {
|
|
379
|
+
periodLabel: options.disclosure.periodLabel,
|
|
380
|
+
estimatedCallsPerPeriod: options.disclosure.estimatedCallsPerPeriod,
|
|
381
|
+
estimatedCoinsPerPeriod: options.disclosure.estimatedCoinsPerPeriod
|
|
382
|
+
},
|
|
383
|
+
...options.maxConcurrent !== void 0 ? { maxConcurrent: options.maxConcurrent } : {},
|
|
384
|
+
...options.maxCallsPerMinute !== void 0 ? { maxCallsPerMinute: options.maxCallsPerMinute } : {},
|
|
385
|
+
...options.maxPendingExternal !== void 0 ? { maxPendingExternal: options.maxPendingExternal } : {},
|
|
386
|
+
...options.allowExternal !== void 0 ? { allowExternal: options.allowExternal } : {}
|
|
387
|
+
});
|
|
388
|
+
if (!isGrantId(view.id)) throw new Error("invalid_grant_response");
|
|
389
|
+
const settled = grantOutcome(view);
|
|
390
|
+
if (settled) {
|
|
391
|
+
popup?.close();
|
|
392
|
+
return settled;
|
|
393
|
+
}
|
|
394
|
+
if (popup && dashboard) {
|
|
395
|
+
if (!popup.closed) popup.location.href = `${dashboard.origin}/play/grant/${encodeURIComponent(view.id)}`;
|
|
396
|
+
} else if (parentOrigin2) {
|
|
397
|
+
window.parent.postMessage({ type: "genex:grant:confirm", v: 1, grantId: view.id }, parentOrigin2);
|
|
398
|
+
}
|
|
399
|
+
return pollGrant(view.id, options.timeoutMs ?? 6e5, popup);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
popup?.close();
|
|
402
|
+
return { status: "failed", error: error instanceof Error ? error.message : "grant_unavailable" };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function generateUnderGrant(grantId, input, idempotencyKey, timeoutMs) {
|
|
406
|
+
if (!isGrantId(grantId)) return { status: "failed", error: "invalid_grant_id" };
|
|
407
|
+
const guarded = approvalGuards();
|
|
408
|
+
if ("status" in guarded) return guarded;
|
|
409
|
+
const { allowExternal: _unused, ...body } = input;
|
|
410
|
+
try {
|
|
411
|
+
const view = await request(
|
|
412
|
+
`/grants/${encodeURIComponent(grantId)}/generations`,
|
|
413
|
+
{ ...body, idempotencyKey: idempotencyKey ?? crypto.randomUUID() }
|
|
414
|
+
);
|
|
415
|
+
if (!isGenerationId(view.id) || view.grantId !== grantId) throw new Error("invalid_generation_response");
|
|
416
|
+
const result = outcome(view);
|
|
417
|
+
if (result) return result;
|
|
418
|
+
return poll(view.id, timeoutMs, outcome);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return { status: "failed", error: error instanceof Error ? error.message : "generation_unavailable" };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function generationErrorMessage(code) {
|
|
424
|
+
switch (code) {
|
|
425
|
+
// --- the outcome is not known yet. Never a failure sentence. ------------
|
|
426
|
+
case "unknown":
|
|
427
|
+
case "unknown_execution":
|
|
428
|
+
case "provider_outcome_unknown":
|
|
429
|
+
return "We don\u2019t know how this one ended yet \u2014 it is still being settled. Check back in a moment.";
|
|
430
|
+
case "wait_timeout":
|
|
431
|
+
return "This is taking longer than usual. It is still running \u2014 check back in a moment.";
|
|
432
|
+
// --- the player's wallet ------------------------------------------------
|
|
433
|
+
case "insufficient_balance":
|
|
434
|
+
return "There isn\u2019t enough coin in your Genex wallet for this request.";
|
|
435
|
+
case "grant_insufficient_funds":
|
|
436
|
+
return "Your budget paused because your Genex wallet ran low. Top it up and it picks up again.";
|
|
437
|
+
// --- standing budgets ---------------------------------------------------
|
|
438
|
+
case "grant_limit_reached":
|
|
439
|
+
return "This budget has reached the limit you set. Set up a new one to keep going.";
|
|
440
|
+
case "grant_stopped":
|
|
441
|
+
return "This budget was stopped. Set up a new one to keep going.";
|
|
442
|
+
case "grant_expired":
|
|
443
|
+
return "This budget has ended. Set up a new one to keep going.";
|
|
444
|
+
case "grant_not_active":
|
|
445
|
+
case "grant_not_found":
|
|
446
|
+
return "There is no budget running for this. Set one up to keep going.";
|
|
447
|
+
case "grant_price_unreasonable":
|
|
448
|
+
return "This asked to charge more for one request than your approval allows, so nothing was charged.";
|
|
449
|
+
case "grant_concurrency":
|
|
450
|
+
return "As many requests are running as your budget allows at once. Wait for one to finish.";
|
|
451
|
+
case "grant_rate_limited":
|
|
452
|
+
return "Requests are coming too quickly for this budget. Wait a moment and try again.";
|
|
453
|
+
case "grant_already_active":
|
|
454
|
+
return "A budget is already running here. Stop it on Genex before setting up another.";
|
|
455
|
+
case "external_grant_active":
|
|
456
|
+
return "A budget on your own plan is already running. Stop it on Genex before setting up another.";
|
|
457
|
+
case "invalid_grant_limit":
|
|
458
|
+
return "That limit is outside the range Genex offers. Choose one on the slider.";
|
|
459
|
+
case "waiting_for_plan":
|
|
460
|
+
return "Your own plan is at its rate limit right now. This resumes on its own once the limit resets.";
|
|
461
|
+
// --- personal plans and the player watcher ------------------------------
|
|
462
|
+
case "external_request_active":
|
|
463
|
+
return "Finish your current request on your own plan before starting another.";
|
|
464
|
+
case "external_not_allowed":
|
|
465
|
+
return "This game only generates through Genex.";
|
|
466
|
+
case "external_unavailable":
|
|
467
|
+
return "Your own plan can\u2019t answer this right now.";
|
|
468
|
+
case "player_update_required":
|
|
469
|
+
return "The Genex player watcher on your computer is out of date. Update it and try again.";
|
|
470
|
+
// --- approval, identity and the trusted surface -------------------------
|
|
471
|
+
case "popup_blocked":
|
|
472
|
+
return "Your browser blocked the Genex approval window. Allow pop-ups for this page and try again.";
|
|
473
|
+
case "no_player_session":
|
|
474
|
+
case "unauthorized":
|
|
475
|
+
case "guest_no_wallet":
|
|
476
|
+
case "player_wallet_required":
|
|
477
|
+
return "Sign in to Genex to use this.";
|
|
478
|
+
case "session_revoked":
|
|
479
|
+
return "Your Genex session ended. Sign in again to continue.";
|
|
480
|
+
case "untrusted_parent":
|
|
481
|
+
case "invalid_dashboard_origin":
|
|
482
|
+
case "forbidden_origin":
|
|
483
|
+
case "credential_scope":
|
|
484
|
+
return "This page can\u2019t reach Genex safely, so nothing was requested.";
|
|
485
|
+
case "native_unsupported":
|
|
486
|
+
return "In-game generation isn\u2019t available in the app yet.";
|
|
487
|
+
case "local_test_unsupported":
|
|
488
|
+
return "In-game generation doesn\u2019t run in local test mode.";
|
|
489
|
+
case "staging_no_generation":
|
|
490
|
+
return "Generation only works in the published game, not in a preview build.";
|
|
491
|
+
// --- quotes and terms ---------------------------------------------------
|
|
492
|
+
case "quote_expired":
|
|
493
|
+
case "generation_expired":
|
|
494
|
+
case "expired":
|
|
495
|
+
return "This request expired before it was approved. Try again.";
|
|
496
|
+
case "quote_changed":
|
|
497
|
+
case "quote_mismatch":
|
|
498
|
+
case "offering_changed":
|
|
499
|
+
return "The price changed. Start this again to see the new offer.";
|
|
500
|
+
case "billing_policy_mismatch":
|
|
501
|
+
case "billing_policy_required":
|
|
502
|
+
case "billing_policy_unsupported":
|
|
503
|
+
return "These terms need a fresh review on Genex. Reload the page and try again.";
|
|
504
|
+
case "funding_mismatch":
|
|
505
|
+
case "funding_conflict":
|
|
506
|
+
return "That payment option doesn\u2019t match this request. Start it again.";
|
|
507
|
+
case "invalid_declared_price":
|
|
508
|
+
case "invalid_estimate_coins":
|
|
509
|
+
return "This request\u2019s price isn\u2019t valid, so nothing was charged.";
|
|
510
|
+
// --- what the game asked for --------------------------------------------
|
|
511
|
+
case "model_not_allowed":
|
|
512
|
+
return "This budget doesn\u2019t cover that model. Choose one it does.";
|
|
513
|
+
case "model_not_available":
|
|
514
|
+
case "offering_unavailable":
|
|
515
|
+
case "workflow_unavailable":
|
|
516
|
+
case "runtime_models_unavailable":
|
|
517
|
+
return "That model isn\u2019t available here. Choose another.";
|
|
518
|
+
case "provider_budget_insufficient":
|
|
519
|
+
case "estimate_exceeds_provider_budget":
|
|
520
|
+
return "This request is too large for its approved budget. Make it smaller and try again.";
|
|
521
|
+
case "provider_budget_exceeded":
|
|
522
|
+
case "provider_budget_exhausted":
|
|
523
|
+
case "workflow_provider_budget_exhausted":
|
|
524
|
+
case "budget_exhausted":
|
|
525
|
+
case "budget_limit":
|
|
526
|
+
case "runtime_budget_exhausted":
|
|
527
|
+
case "provider_token_limit":
|
|
528
|
+
return "The model ran out of room before a usable answer was ready. The attempt is still charged.";
|
|
529
|
+
case "request_too_large":
|
|
530
|
+
return "That request is too long. Shorten it and try again.";
|
|
531
|
+
case "invalid_request":
|
|
532
|
+
case "invalid_schema":
|
|
533
|
+
return "This request wasn\u2019t something Genex could accept.";
|
|
534
|
+
case "generation_limit":
|
|
535
|
+
return "As many requests are running as Genex allows at once. Wait for one to finish.";
|
|
536
|
+
case "generation_not_found":
|
|
537
|
+
return "That request no longer exists.";
|
|
538
|
+
case "idempotency_conflict":
|
|
539
|
+
return "That request was already started with different details.";
|
|
540
|
+
// --- the result -----------------------------------------------------------
|
|
541
|
+
case "rejected":
|
|
542
|
+
case "invalid_output":
|
|
543
|
+
case "validation_failed":
|
|
544
|
+
case "invalid_provider_response":
|
|
545
|
+
return "The answer didn\u2019t match what this game asked for.";
|
|
546
|
+
// TWO DIFFERENT ENDINGS, and the difference is money. A budget that was
|
|
547
|
+
// stopped, expired or revoked cancels what it admitted but never
|
|
548
|
+
// dispatched, at zero cost — that is the one case where "before it ran" is
|
|
549
|
+
// true. A `canceled` row that carries this code instead comes from the
|
|
550
|
+
// cancel door closing on work already dispatched, or a workflow stopped
|
|
551
|
+
// after steps ran: a fixed-price attempt is charged in full and a metered
|
|
552
|
+
// one is charged for what it consumed. Denying the charge in the sentence a
|
|
553
|
+
// game shows was the SDK telling the player the opposite of their receipt.
|
|
554
|
+
case "player_stopped":
|
|
555
|
+
case "game_stopped":
|
|
556
|
+
case "operator_stopped":
|
|
557
|
+
return "This request was stopped before it ran, so nothing was charged for it.";
|
|
558
|
+
case "admission_revoked":
|
|
559
|
+
case "canceled":
|
|
560
|
+
return "This request was stopped. Work that had already started is still charged.";
|
|
561
|
+
case "account_erased":
|
|
562
|
+
return "This request was stopped because the account was erased.";
|
|
563
|
+
case "provider_failed":
|
|
564
|
+
case "provider_unavailable":
|
|
565
|
+
case "provider_response_unavailable":
|
|
566
|
+
case "provider_response_too_large":
|
|
567
|
+
case "context_unavailable":
|
|
568
|
+
return "The model couldn\u2019t finish this request.";
|
|
569
|
+
default:
|
|
570
|
+
return "Generation isn\u2019t available right now. Try again in a moment.";
|
|
571
|
+
}
|
|
572
|
+
}
|
|
294
573
|
|
|
295
574
|
// src/index.ts
|
|
296
575
|
var PROTOCOL_VERSION = 1;
|
|
@@ -298,7 +577,7 @@ var RETRY_FLAG = "genex:embed:retry";
|
|
|
298
577
|
var POPOVER_DISMISSED_FLAG = "genex:guest:popover-dismissed";
|
|
299
578
|
var LOCAL_AUTH_FLAG = "genex:embed:local-auth";
|
|
300
579
|
var PLAYER_ID_KEY = "genex:player";
|
|
301
|
-
var SDK_VERSION = "0.
|
|
580
|
+
var SDK_VERSION = "0.24.0";
|
|
302
581
|
var config2 = null;
|
|
303
582
|
var state = "pending";
|
|
304
583
|
var user = null;
|
|
@@ -548,12 +827,18 @@ function stateFetch(path, init = {}) {
|
|
|
548
827
|
keepalive: init.body !== void 0 && init.body.length <= KEEPALIVE_MAX_BYTES ? true : void 0
|
|
549
828
|
});
|
|
550
829
|
}
|
|
830
|
+
async function refusedAsStaging(res) {
|
|
831
|
+
if (res.status !== 403) return false;
|
|
832
|
+
const body = await res.json().catch(() => null);
|
|
833
|
+
return body?.error === "staging_no_save";
|
|
834
|
+
}
|
|
551
835
|
async function decodeSave(res) {
|
|
552
836
|
if (res.status === 409) {
|
|
553
837
|
const body2 = await res.json().catch(() => ({}));
|
|
554
838
|
return { saved: false, conflict: true, version: body2.version };
|
|
555
839
|
}
|
|
556
840
|
if (!res.ok) {
|
|
841
|
+
if (await refusedAsStaging(res)) return { saved: false, staging: true };
|
|
557
842
|
emit("error", { error: new Error(`genex state save failed (${res.status})`) });
|
|
558
843
|
return { saved: false };
|
|
559
844
|
}
|
|
@@ -566,7 +851,10 @@ async function loadPlayerState() {
|
|
|
566
851
|
return { data: hasPendingPlayerSave ? pendingPlayerSave : null, version: 0, guest: true };
|
|
567
852
|
}
|
|
568
853
|
const res = await stateFetch("/state/me");
|
|
569
|
-
if (!res.ok)
|
|
854
|
+
if (!res.ok) {
|
|
855
|
+
if (await refusedAsStaging(res)) return { data: null, version: 0, staging: true };
|
|
856
|
+
throw new Error(`genex load player state failed (${res.status})`);
|
|
857
|
+
}
|
|
570
858
|
const body = await res.json();
|
|
571
859
|
return { data: body.data, version: body.version };
|
|
572
860
|
}
|
|
@@ -589,7 +877,10 @@ async function loadWorldState() {
|
|
|
589
877
|
await ensurePlayer();
|
|
590
878
|
if (state === "guest") return { data: null, version: 0, guest: true };
|
|
591
879
|
const res = await stateFetch("/state");
|
|
592
|
-
if (!res.ok)
|
|
880
|
+
if (!res.ok) {
|
|
881
|
+
if (await refusedAsStaging(res)) return { data: null, version: 0, staging: true };
|
|
882
|
+
throw new Error(`genex load world state failed (${res.status})`);
|
|
883
|
+
}
|
|
593
884
|
const body = await res.json();
|
|
594
885
|
lastWorldVersion = body.version;
|
|
595
886
|
return { data: body.data, version: body.version };
|
|
@@ -1537,6 +1828,10 @@ export {
|
|
|
1537
1828
|
generate,
|
|
1538
1829
|
requestWorkflow,
|
|
1539
1830
|
resumeWorkflow,
|
|
1831
|
+
getSpendGrant,
|
|
1832
|
+
stopSpendGrant,
|
|
1833
|
+
requestSpendGrant,
|
|
1834
|
+
generationErrorMessage,
|
|
1540
1835
|
initEmbed,
|
|
1541
1836
|
isEmbedded,
|
|
1542
1837
|
getAuthState,
|
package/dist/development.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RuntimeWorkflowInput, RuntimeGenerationBilling, RuntimeGenerationFunding, RuntimeGenerationView, RuntimeGenerationModels, RuntimeWorkflowOfferings } from '@genex/embed-protocol';
|
|
2
|
-
export { RuntimeBillingPolicy, RuntimeExternalProvider, RuntimeGenerationBilling, RuntimeGenerationFunding, RuntimeGenerationModels, RuntimeGenerationStatus, RuntimeGenerationView, RuntimeWorkflowInput, RuntimeWorkflowOfferings } from '@genex/embed-protocol';
|
|
1
|
+
import { RuntimeWorkflowInput, RuntimeGenerationBilling, RuntimeGenerationFunding, RuntimeGrantView, RuntimeGenerationView, RuntimeGenerationModels, RuntimeWorkflowOfferings } from '@genex/embed-protocol';
|
|
2
|
+
export { RuntimeBillingPolicy, RuntimeExternalProvider, RuntimeGenerationBilling, RuntimeGenerationFunding, RuntimeGenerationModels, RuntimeGenerationStatus, RuntimeGenerationView, RuntimeGrantStatus, RuntimeGrantTerms, RuntimeGrantView, RuntimeWorkflowInput, RuntimeWorkflowOfferings } from '@genex/embed-protocol';
|
|
3
3
|
|
|
4
4
|
/** A thing the game sells. Resolved server-side; the game never sets a price. */
|
|
5
5
|
interface ShopItem {
|
|
@@ -134,6 +134,13 @@ interface GenerateOptions {
|
|
|
134
134
|
idempotencyKey?: string;
|
|
135
135
|
/** Waiting time only. Reaching it does not cancel an accepted generation. */
|
|
136
136
|
timeoutMs?: number;
|
|
137
|
+
/**
|
|
138
|
+
* A standing budget from requestSpendGrant(). With one, the call is admitted
|
|
139
|
+
* by the budget the player already approved: no popup, no parent modal and no
|
|
140
|
+
* user gesture. Every other guard still applies, and `allowExternal` is
|
|
141
|
+
* ignored because the funding door was chosen once, in the sheet.
|
|
142
|
+
*/
|
|
143
|
+
grantId?: string;
|
|
137
144
|
}
|
|
138
145
|
interface RuntimeGenerationResult extends RuntimeGenerationBilling {
|
|
139
146
|
status: 'succeeded' | 'failed' | 'unknown' | 'canceled' | 'expired' | 'pending';
|
|
@@ -176,6 +183,9 @@ declare function waitForGeneration(generationId: string, timeoutMs?: number): Pr
|
|
|
176
183
|
* asynchronous work, so standalone games retain the browser's user gesture.
|
|
177
184
|
* The player approves on Genex. Cancellation resolves normally; only API state
|
|
178
185
|
* can return an output. Store generationId to recover a pending result on boot.
|
|
186
|
+
*
|
|
187
|
+
* With `grantId`, the player already approved a standing budget: the call needs
|
|
188
|
+
* no popup and no user gesture, and may run from a loop or a timer.
|
|
179
189
|
*/
|
|
180
190
|
declare function generate(options: GenerateOptions): Promise<RuntimeGenerationResult>;
|
|
181
191
|
/**
|
|
@@ -189,6 +199,71 @@ declare function requestWorkflow(options: RequestWorkflowOptions): Promise<Runti
|
|
|
189
199
|
declare function resumeWorkflow(generationId: string, options?: {
|
|
190
200
|
timeoutMs?: number;
|
|
191
201
|
}): Promise<RuntimeWorkflowAuthorization>;
|
|
202
|
+
interface SpendGrantDisclosure {
|
|
203
|
+
/** The period the estimate below is expressed in. */
|
|
204
|
+
periodLabel: 'minute' | 'hour' | 'session';
|
|
205
|
+
/** Honest expected calls per period. The sheet shows the player a worst case beside it. */
|
|
206
|
+
estimatedCallsPerPeriod: number;
|
|
207
|
+
/** Honest expected coin per period, attributed to the game, never as a Genex figure. */
|
|
208
|
+
estimatedCoinsPerPeriod: number;
|
|
209
|
+
}
|
|
210
|
+
interface SpendGrantOptions {
|
|
211
|
+
/** Models this budget may spend on, from getGenerationModels(). One to eight. */
|
|
212
|
+
models: string[];
|
|
213
|
+
/** Hard per-call ceiling. The server refuses a call declaring more. */
|
|
214
|
+
perCallMaxCoins: number;
|
|
215
|
+
/** The honest per-call price the disclosure is built on. At most perCallMaxCoins. */
|
|
216
|
+
perCallEstimateCoins: number;
|
|
217
|
+
disclosure: SpendGrantDisclosure;
|
|
218
|
+
/** Calls this budget may have in flight at once. Server default applies when omitted. */
|
|
219
|
+
maxConcurrent?: number;
|
|
220
|
+
/** Calls this budget may start per minute. Server default applies when omitted. */
|
|
221
|
+
maxCallsPerMinute?: number;
|
|
222
|
+
/** Personal-plan jobs that may be queued at once. Server default applies when omitted. */
|
|
223
|
+
maxPendingExternal?: number;
|
|
224
|
+
/** Offer the player's own Claude/ChatGPT plan beside coins. Defaults off. */
|
|
225
|
+
allowExternal?: boolean;
|
|
226
|
+
/** Reuse for the same budget request across retries and reloads. */
|
|
227
|
+
idempotencyKey?: string;
|
|
228
|
+
/** Approval waiting time only. A timeout neither cancels nor charges. */
|
|
229
|
+
timeoutMs?: number;
|
|
230
|
+
}
|
|
231
|
+
interface SpendGrantResult {
|
|
232
|
+
/** `active` means the budget is approved and generate({ grantId }) may run. */
|
|
233
|
+
status: 'active' | 'canceled' | 'expired' | 'failed' | 'pending';
|
|
234
|
+
grantId?: string;
|
|
235
|
+
/** The authoritative server view: limit, spend, counts and the frozen terms. */
|
|
236
|
+
grant?: RuntimeGrantView;
|
|
237
|
+
error?: string;
|
|
238
|
+
}
|
|
239
|
+
/** Read a budget: limit, spend, calls and status. Scoped to this player AND this game. */
|
|
240
|
+
declare function getSpendGrant(grantId: string): Promise<RuntimeGrantView>;
|
|
241
|
+
/**
|
|
242
|
+
* Release a budget when the feature that needed it ends. Stopping is
|
|
243
|
+
* PROSPECTIVE: calls already started finish and are charged. The player has the
|
|
244
|
+
* same lever on Genex and it always outranks the game's.
|
|
245
|
+
*/
|
|
246
|
+
declare function stopSpendGrant(grantId: string): Promise<RuntimeGrantView>;
|
|
247
|
+
/**
|
|
248
|
+
* Ask the player for a standing budget. Call it directly from a click handler,
|
|
249
|
+
* before awaiting anything: the trusted surface is reserved synchronously, just
|
|
250
|
+
* like generate(). The player picks the limit and the funding door on Genex —
|
|
251
|
+
* the game never sees or chooses either.
|
|
252
|
+
*
|
|
253
|
+
* Resolves `active` once the budget can serve calls; pass its `grantId` to
|
|
254
|
+
* generate() from then on. A player who closes the sheet resolves `canceled`.
|
|
255
|
+
* Store the id: getSpendGrant(id) recovers the budget after a reload.
|
|
256
|
+
*/
|
|
257
|
+
declare function requestSpendGrant(options: SpendGrantOptions): Promise<SpendGrantResult>;
|
|
258
|
+
/**
|
|
259
|
+
* A player-facing sentence for any code this lane can put in `result.error`.
|
|
260
|
+
*
|
|
261
|
+
* Show it, or write your own in-fiction line keyed off the same code — never
|
|
262
|
+
* the raw code. Two rules the copy keeps: an uncertain outcome reads as
|
|
263
|
+
* uncertain rather than as failure, and no sentence here states a price, an
|
|
264
|
+
* allowance or a plan name. Those are the server's to show, in the Genex sheet.
|
|
265
|
+
*/
|
|
266
|
+
declare function generationErrorMessage(code: string | undefined): string;
|
|
192
267
|
|
|
193
268
|
interface EmbedConfig {
|
|
194
269
|
/** This game's own slug (GENEX.slug) — identifies the project to /play/authorize. */
|
|
@@ -222,11 +297,20 @@ interface PlayerStateResult {
|
|
|
222
297
|
version: number;
|
|
223
298
|
/** True when the current identity is a guest (server storage untouched). */
|
|
224
299
|
guest?: boolean;
|
|
300
|
+
/**
|
|
301
|
+
* True when the build is running on the PREVIEW channel — the owner's draft
|
|
302
|
+
* workspace, any `preview--<slug>` URL. Server saves are off there by design,
|
|
303
|
+
* so nothing was read: keep progress in localStorage and say "preview build",
|
|
304
|
+
* never "offline".
|
|
305
|
+
*/
|
|
306
|
+
staging?: boolean;
|
|
225
307
|
}
|
|
226
308
|
interface WorldStateResult {
|
|
227
309
|
data: unknown;
|
|
228
310
|
version: number;
|
|
229
311
|
guest?: boolean;
|
|
312
|
+
/** True on the preview channel — see PlayerStateResult.staging. */
|
|
313
|
+
staging?: boolean;
|
|
230
314
|
}
|
|
231
315
|
interface SaveStateResult {
|
|
232
316
|
/** True when the write landed on the server. */
|
|
@@ -239,6 +323,13 @@ interface SaveStateResult {
|
|
|
239
323
|
guest?: boolean;
|
|
240
324
|
/** True when the value was queued to auto-flush if this guest signs in mid-game. */
|
|
241
325
|
queued?: boolean;
|
|
326
|
+
/**
|
|
327
|
+
* True when the build is running on the PREVIEW channel (the owner's draft
|
|
328
|
+
* workspace, `preview--<slug>`): the server refused the write by policy, not
|
|
329
|
+
* by failure — nothing is queued, no `error` event fires. Keep the value in
|
|
330
|
+
* localStorage and tell the player it is a preview build.
|
|
331
|
+
*/
|
|
332
|
+
staging?: boolean;
|
|
242
333
|
}
|
|
243
334
|
interface SubmitScoreResult {
|
|
244
335
|
submitted: boolean;
|
|
@@ -355,14 +446,19 @@ declare function on(event: EmbedEvent, cb: (ctx?: EventContext) => void): () =>
|
|
|
355
446
|
* Load THIS player's own save slot (per-player, per-game — another player can
|
|
356
447
|
* never read or clobber it). Resolves { data: null, version: 0 } when they
|
|
357
448
|
* never saved. Guests get their queued pending save (if any) so a round-trip
|
|
358
|
-
* works mid-session; the server is untouched.
|
|
359
|
-
*
|
|
449
|
+
* works mid-session; the server is untouched. On the preview channel (the
|
|
450
|
+
* owner's draft workspace) resolves { data: null, version: 0, staging: true }
|
|
451
|
+
* — server saves are off there by design, and that is not an error. Await-safe
|
|
452
|
+
* from boot: resolves after identity exists, rejects only when the session is
|
|
453
|
+
* blocked.
|
|
360
454
|
*/
|
|
361
455
|
declare function loadPlayerState(): Promise<PlayerStateResult>;
|
|
362
456
|
/**
|
|
363
457
|
* Save THIS player's own slot (any JSON ≤ 256KB; last-write-wins on their own
|
|
364
458
|
* row). Guests: the value is QUEUED in memory and auto-flushed if they sign in
|
|
365
459
|
* mid-game ({ saved: false, guest: true, queued: true }) — no extra game code.
|
|
460
|
+
* On the preview channel: { saved: false, staging: true }, nothing queued
|
|
461
|
+
* (nothing would ever flush it) and no `error` event — keep it in localStorage.
|
|
366
462
|
* Pass { ifVersion } (from the last load/save) to detect same-account
|
|
367
463
|
* two-device races: a losing write resolves { conflict: true, version } —
|
|
368
464
|
* reload, merge, retry. Small saves survive tab close (fetch keepalive).
|
|
@@ -374,7 +470,8 @@ declare function savePlayerState(data: unknown, opts?: {
|
|
|
374
470
|
* Load the game's SHARED world slot (one blob per game — every player reads
|
|
375
471
|
* the same world). Guests resolve { data: null, guest: true } — the server
|
|
376
472
|
* refuses guest reads; in multiplayer, guests receive the live world through
|
|
377
|
-
* the room instead.
|
|
473
|
+
* the room instead. The preview channel resolves { data: null, version: 0,
|
|
474
|
+
* staging: true } for the same reason the player slot does.
|
|
378
475
|
*/
|
|
379
476
|
declare function loadWorldState(): Promise<WorldStateResult>;
|
|
380
477
|
/**
|
|
@@ -429,4 +526,4 @@ declare function __resetForTests(overrides?: {
|
|
|
429
526
|
heartbeatIntervalMs?: number;
|
|
430
527
|
}): void;
|
|
431
528
|
|
|
432
|
-
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, type Entitlement, type GenerateOptions, type Leaderboard, type LeaderboardEntry, type PlayerStateResult, type PurchaseResult, type PurchaseStatus, type RequestWorkflowOptions, type RuntimeGenerationResult, type RuntimeWorkflowAuthorization, type SaveStateResult, type ShopItem, type SubmitScoreResult, type WorldStateResult, __resetForTests, _stashTicketFromUrl, buy, consumeEntitlement, generate, getAuthState, getColyseusAuth, getColyseusUrls, getEmbedToken, getEntitlements, getGeneration, getGenerationModels, getLeaderboard, getShop, getUser, getWorkflowOfferings, initEmbed, isEmbedded, loadPlayerState, loadWorldState, on, requestWorkflow, resumeWorkflow, savePlayerState, saveWorldState, submitScore, waitForAuth, waitForGeneration, waitForPlayer };
|
|
529
|
+
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, type Entitlement, type GenerateOptions, type Leaderboard, type LeaderboardEntry, type PlayerStateResult, type PurchaseResult, type PurchaseStatus, type RequestWorkflowOptions, type RuntimeGenerationResult, type RuntimeWorkflowAuthorization, type SaveStateResult, type ShopItem, type SpendGrantDisclosure, type SpendGrantOptions, type SpendGrantResult, type SubmitScoreResult, type WorldStateResult, __resetForTests, _stashTicketFromUrl, buy, consumeEntitlement, generate, generationErrorMessage, getAuthState, getColyseusAuth, getColyseusUrls, getEmbedToken, getEntitlements, getGeneration, getGenerationModels, getLeaderboard, getShop, getSpendGrant, getUser, getWorkflowOfferings, initEmbed, isEmbedded, loadPlayerState, loadWorldState, on, requestSpendGrant, requestWorkflow, resumeWorkflow, savePlayerState, saveWorldState, stopSpendGrant, submitScore, waitForAuth, waitForGeneration, waitForPlayer };
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
buy,
|
|
5
5
|
consumeEntitlement,
|
|
6
6
|
generate,
|
|
7
|
+
generationErrorMessage,
|
|
7
8
|
getAuthState,
|
|
8
9
|
getColyseusAuth,
|
|
9
10
|
getColyseusUrls,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
getGenerationModels,
|
|
14
15
|
getLeaderboard,
|
|
15
16
|
getShop,
|
|
17
|
+
getSpendGrant,
|
|
16
18
|
getUser,
|
|
17
19
|
getWorkflowOfferings,
|
|
18
20
|
initEmbed,
|
|
@@ -20,22 +22,25 @@ import {
|
|
|
20
22
|
loadPlayerState,
|
|
21
23
|
loadWorldState,
|
|
22
24
|
on,
|
|
25
|
+
requestSpendGrant,
|
|
23
26
|
requestWorkflow,
|
|
24
27
|
resumeWorkflow,
|
|
25
28
|
savePlayerState,
|
|
26
29
|
saveWorldState,
|
|
30
|
+
stopSpendGrant,
|
|
27
31
|
submitScore,
|
|
28
32
|
waitForAuth,
|
|
29
33
|
waitForGeneration,
|
|
30
34
|
waitForPlayer
|
|
31
|
-
} from "./chunk-
|
|
32
|
-
import "./chunk-
|
|
35
|
+
} from "./chunk-GR56KHWN.js";
|
|
36
|
+
import "./chunk-VPIQVRVS.js";
|
|
33
37
|
export {
|
|
34
38
|
__resetForTests,
|
|
35
39
|
_stashTicketFromUrl,
|
|
36
40
|
buy,
|
|
37
41
|
consumeEntitlement,
|
|
38
42
|
generate,
|
|
43
|
+
generationErrorMessage,
|
|
39
44
|
getAuthState,
|
|
40
45
|
getColyseusAuth,
|
|
41
46
|
getColyseusUrls,
|
|
@@ -45,6 +50,7 @@ export {
|
|
|
45
50
|
getGenerationModels,
|
|
46
51
|
getLeaderboard,
|
|
47
52
|
getShop,
|
|
53
|
+
getSpendGrant,
|
|
48
54
|
getUser,
|
|
49
55
|
getWorkflowOfferings,
|
|
50
56
|
initEmbed,
|
|
@@ -52,10 +58,12 @@ export {
|
|
|
52
58
|
loadPlayerState,
|
|
53
59
|
loadWorldState,
|
|
54
60
|
on,
|
|
61
|
+
requestSpendGrant,
|
|
55
62
|
requestWorkflow,
|
|
56
63
|
resumeWorkflow,
|
|
57
64
|
savePlayerState,
|
|
58
65
|
saveWorldState,
|
|
66
|
+
stopSpendGrant,
|
|
59
67
|
submitScore,
|
|
60
68
|
waitForAuth,
|
|
61
69
|
waitForGeneration,
|
package/dist/sentry.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/embed-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Player identity + durable game state for genex games \u2014 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",
|
|
File without changes
|