@indiegems/gem-web-sdk 18.1.0 → 19.0.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/CHANGELOG.md +18 -0
- package/dist/function.d.ts +16 -2
- package/dist/game.d.ts +16 -2
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.esm.js +1 -1
- package/dist/gem-web-sdk.function.js +3 -3
- package/dist/gem-web-sdk.game.js +3 -3
- package/dist/gem-web-sdk.min.js +3 -3
- package/dist/gem-web-sdk.sdk.js +3 -3
- package/dist/gem-web-sdk.server.js +1 -1
- package/dist/sdk.d.ts +16 -2
- package/docs/api/api-reference-function.md +247 -2
- package/docs/api/api-reference-game.md +249 -3
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +660 -4
- package/docs/arcade-and-store.md +149 -6
- package/docs/changelog.md +18 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +8 -8
- package/docs/server-bundles.md +8 -2
- package/package.json +1 -1
package/docs/arcade-and-store.md
CHANGED
|
@@ -40,7 +40,8 @@ itself. You get an outcome.
|
|
|
40
40
|
- [8. The Arcade Store's offers](#8-the-arcade-stores-offers)
|
|
41
41
|
- [9. Buying from the Arcade Store](#9-buying-from-the-arcade-store)
|
|
42
42
|
- [10. Letting the Arcade show one offer](#10-letting-the-arcade-show-one-offer)
|
|
43
|
-
- [11.
|
|
43
|
+
- [11. What the player holds, and a VIP door](#11-what-the-player-holds-and-a-vip-door)
|
|
44
|
+
- [12. Outcomes and reasons](#12-outcomes-and-reasons)
|
|
44
45
|
|
|
45
46
|
## 1. Two surfaces, and which one is real money
|
|
46
47
|
|
|
@@ -67,7 +68,7 @@ three and never grows:
|
|
|
67
68
|
|
|
68
69
|
`reason` is a short code that says why. The set is open: a new Arcade build may send a reason your
|
|
69
70
|
game has never seen, so branch on `outcome` and treat `reason` as a hint. The codes in use today are
|
|
70
|
-
in [§
|
|
71
|
+
in [§12](#12-outcomes-and-reasons).
|
|
71
72
|
|
|
72
73
|
## 3. What is available here
|
|
73
74
|
|
|
@@ -316,9 +317,9 @@ Pick by who can say what the offer is:
|
|
|
316
317
|
- **Do not show a price on the button.** The sheet shows the right one for this player, which a
|
|
317
318
|
sale or a pass can change, and `priceDisplay` does not say either of those. If you want a price on
|
|
318
319
|
screen, draw the offer yourself and use `purchase`.
|
|
319
|
-
- **
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
- **The sheet does not tell you what the player holds.** It tells the player, so a button that
|
|
321
|
+
stays up for a holder is fine: they see what they already have, and choose from there. To change
|
|
322
|
+
your own screen for a holder, read `gem.me.passes` ([§11](#11-what-the-player-holds-and-a-vip-door)).
|
|
322
323
|
- **Hide the button when the SKU is not in `offers()`.** A SKU that is not on sale here answers
|
|
323
324
|
`unavailable` / `not_found`, and the player would have pressed a button that does nothing.
|
|
324
325
|
- **Read `showOfferAvailable`, not `available`.** An older Arcade can sell through `purchase` and
|
|
@@ -332,7 +333,149 @@ closed checkout, shared with `purchase`: the next ask of either kind is `decline
|
|
|
332
333
|
while. The press shows nothing during a back-off, so give the player something else to do on that
|
|
333
334
|
screen.
|
|
334
335
|
|
|
335
|
-
## 11.
|
|
336
|
+
## 11. What the player holds, and a VIP door
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
const passes = await gem.me.passes.list();
|
|
340
|
+
// [{ pass: 'vip', held: true, endsAt: '2026-11-29T12:34:56Z' }]
|
|
341
|
+
|
|
342
|
+
const vip = await gem.me.passes.get('vip'); // one pass by its slug
|
|
343
|
+
if (vip.held) drawVipBadge();
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`gem.me.passes` says which store passes the player holds. **VIP is the pass `vip`**, its offering
|
|
347
|
+
slug. Key on the slug, which is stable and not localized, and never on a display name.
|
|
348
|
+
|
|
349
|
+
- **It lists every pass games may see, held or not.** An admin marks a pass as visible to games, and
|
|
350
|
+
the list has one entry for each, so its shape never says what the player bought. A pass that is not
|
|
351
|
+
marked is left out, and `get` reads a pass that is not listed as not held.
|
|
352
|
+
- **A pass is a slug, whether it is held, and when it ends.** No price, no name, no perks, nothing
|
|
353
|
+
about how the player got it, and nothing about a pass that has ended.
|
|
354
|
+
- **`endsAt` is the exact end,** grace and any queued period included, and `null` when the pass is not
|
|
355
|
+
held or is held with no end, which support can grant. Read `held` first: `null` alone never means
|
|
356
|
+
ended.
|
|
357
|
+
- **A guest reads as not holding.** Every pass is `held: false`, and that is no error.
|
|
358
|
+
- **A read that fails rejects with a `GemApiError`, as every `gem.me` read does.** That is unknown,
|
|
359
|
+
not "not held", so keep what you drew. It is the one call on this page that rejects.
|
|
360
|
+
- **It is a read over your play token,** not an ask of the Arcade. It needs no press, and works
|
|
361
|
+
wherever your game has a token, including where the store is off: holding a pass is not buying one.
|
|
362
|
+
Each call goes to the platform, so read when you draw and do not poll.
|
|
363
|
+
|
|
364
|
+
### The browser draws, the server decides
|
|
365
|
+
|
|
366
|
+
**Never decide anything from `gem.me.passes`.** It runs in the player's browser, and a player can
|
|
367
|
+
make any answer come back. It is for drawing the door. What is behind the door comes from your
|
|
368
|
+
function, which asks the platform and answers with the contents. A level that ships in your build is
|
|
369
|
+
not behind a door at all, because a player can open it by editing the page.
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
// 1. DRAWING. Open or shut. Nothing here unlocks anything.
|
|
373
|
+
function drawVipDoor(passes) {
|
|
374
|
+
door.drawOpen(passes.some((p) => p.pass === 'vip' && p.held));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await gem.awaitBridge();
|
|
378
|
+
try {
|
|
379
|
+
drawVipDoor(await gem.me.passes.list());
|
|
380
|
+
} catch {
|
|
381
|
+
door.drawShut(); // unknown: the press below still asks the function
|
|
382
|
+
}
|
|
383
|
+
gem.me.passes.on('changed', drawVipDoor); // opens after a purchase, locks when VIP ends
|
|
384
|
+
|
|
385
|
+
// 2. DECIDING. The press asks the function. The browser's reading is not part of the call.
|
|
386
|
+
door.onPress = async () => {
|
|
387
|
+
let answer;
|
|
388
|
+
try {
|
|
389
|
+
answer = (await gem.functions.invoke({ door: 'vip-lounge' })).json();
|
|
390
|
+
} catch {
|
|
391
|
+
showDoorIsStuck(); // nothing decided: not "you need VIP"
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (answer.allowed) enterLounge(answer.lounge);
|
|
395
|
+
else showHowToGetVip();
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// 3. BUYING. From the press on your own "How to get VIP" button.
|
|
399
|
+
howToGetVipButton.onclick = () => gem.store.showOffer(VIP_SKU);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The function, in Rust, because only the platform's answer counts:
|
|
403
|
+
|
|
404
|
+
```rust
|
|
405
|
+
use gem::functions::host::{self, ApiRequest, Caller};
|
|
406
|
+
|
|
407
|
+
// The input says which door. It never says who is asking, or what they hold.
|
|
408
|
+
let context = host::get_context();
|
|
409
|
+
let Caller::Player(_) = context.caller else {
|
|
410
|
+
return Err("expected a player's call".into());
|
|
411
|
+
};
|
|
412
|
+
let read = host::call_api(&ApiRequest {
|
|
413
|
+
method: "GET".into(),
|
|
414
|
+
path: format!("/v1/games/{}/players/me/passes", context.game_id),
|
|
415
|
+
body: Vec::new(),
|
|
416
|
+
})?;
|
|
417
|
+
if read.status != 200 {
|
|
418
|
+
return Err(format!("the pass read answered {}", read.status)); // decides nothing
|
|
419
|
+
}
|
|
420
|
+
let body: serde_json::Value = serde_json::from_slice(&read.body).map_err(|e| e.to_string())?;
|
|
421
|
+
let vip = body["passes"]
|
|
422
|
+
.as_array()
|
|
423
|
+
.into_iter()
|
|
424
|
+
.flatten()
|
|
425
|
+
.any(|pass| pass["pass"] == "vip" && pass["held"] == true);
|
|
426
|
+
let answer = if vip {
|
|
427
|
+
serde_json::json!({ "allowed": true, "lounge": lounge_contents() })
|
|
428
|
+
} else {
|
|
429
|
+
serde_json::json!({ "allowed": false })
|
|
430
|
+
};
|
|
431
|
+
Ok(answer.to_string().into_bytes())
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
"Me" on that route is the player who invoked the function. A function your server invoked has no
|
|
435
|
+
player, and the route refuses it with `403`. The body is `{ pass, held, ends_at }` for each pass.
|
|
436
|
+
|
|
437
|
+
- **If the door and the function disagree, the function is right.** The player sees "How to get VIP"
|
|
438
|
+
or the lounge, and either way nobody got in by editing the page. Redraw the door when the function
|
|
439
|
+
says no.
|
|
440
|
+
- **A guest has no VIP and cannot buy it from here.** Their door is shut, and `offers()` answers
|
|
441
|
+
`unavailable` / `sign_in_required`, so offer `gem.account.requestSignIn` instead of the button.
|
|
442
|
+
|
|
443
|
+
### When the door moves by itself
|
|
444
|
+
|
|
445
|
+
While something listens to `on('changed')`, the SDK reads again at each of these, and calls you only
|
|
446
|
+
if the answer differs:
|
|
447
|
+
|
|
448
|
+
| Moment | Why you cannot rely on something else |
|
|
449
|
+
| --------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
450
|
+
| `store.purchase` or `store.showOffer` answers `ok` | `onSettled` does not fire after a plain `ok` |
|
|
451
|
+
| Either answers `unavailable` / `error` | The outcome is unknown, and the player may have paid |
|
|
452
|
+
| `gem.store.onSettled` fires | `ok` / `pending` still reads as not held |
|
|
453
|
+
| Your game resumes | A purchase in the Arcade's own store, from the Gem menu, sends no event at all |
|
|
454
|
+
| The player signs in | A guest who signs in may hold VIP |
|
|
455
|
+
| A held pass reaches `endsAt` | VIP does not renew, so the door has to lock by itself |
|
|
456
|
+
|
|
457
|
+
A read that fails changes nothing, and is tried again later. A read that was already on its way when
|
|
458
|
+
a purchase ended is never reused, so a stale answer cannot shut the door on the buyer. If you read
|
|
459
|
+
without a listener, call `list()` yourself at those moments; it is always fresh.
|
|
460
|
+
|
|
461
|
+
`on` never fires in a function, which has none of those moments.
|
|
462
|
+
|
|
463
|
+
### On a dedicated server
|
|
464
|
+
|
|
465
|
+
The platform has a read for this,
|
|
466
|
+
`GET /v1/games/{game_id}/matches/{match_id}/players/{player_id}/passes`, with the same body, for a
|
|
467
|
+
player in your live match. **A server bundle cannot call it yet**, and the SDK has no method for it,
|
|
468
|
+
for two reasons:
|
|
469
|
+
|
|
470
|
+
- **It needs the allocation nonce.** The route requires `X-Alloc-Nonce`, which the platform's runtime
|
|
471
|
+
holds and never hands to your bundle. The boost read has the same limit. Reaching it takes a hook in
|
|
472
|
+
the runtime, like the one behind `gem.match.end()`.
|
|
473
|
+
- **No game server is allowed to yet.** It answers `403` `client_permission_denied` to every caller
|
|
474
|
+
until the platform grants game servers `passes.read`, which ships after the route itself.
|
|
475
|
+
|
|
476
|
+
Until both land, decide in a function, or leave a VIP door out of a dedicated-server match.
|
|
477
|
+
|
|
478
|
+
## 12. Outcomes and reasons
|
|
336
479
|
|
|
337
480
|
| Call | `ok` | `declined` | `unavailable` |
|
|
338
481
|
| ---------------- | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
|
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,24 @@ Its entries are grouped under up to five headings:
|
|
|
16
16
|
- **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
|
|
17
17
|
- **New capability**: something a game can now do. Nothing changes unless you use it.
|
|
18
18
|
|
|
19
|
+
## 19.0.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 18.1.1. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Rebuild
|
|
24
|
+
|
|
25
|
+
- The player scope on the game object is now typed as PlayerScope instead of EntityScope. Recompile your game. If you annotated that value, or passed it to code that expects EntityScope, change the annotation to PlayerScope. Otherwise nothing else needs to change.
|
|
26
|
+
|
|
27
|
+
### New capability
|
|
28
|
+
|
|
29
|
+
- A game can now read which passes its player holds. Every pass the Arcade marks as visible to games is reported, held or not, with the time it ends. The read is exposed through PassesApi on PlayerScope, and it is not available on EntityScope because a character holds no pass. The result only tells you who holds a pass; your game still decides what the pass unlocks. PassesEvent lets you listen for changes, and the SDK re-reads after a store purchase completes, on resume, after a sign-in, and when a held pass ends, so a buyer's doors open without a reload. GamePass describes one entry in the result.
|
|
30
|
+
|
|
31
|
+
## 18.1.1
|
|
32
|
+
|
|
33
|
+
Upgrading from 18.1.0. Impact: none.
|
|
34
|
+
|
|
35
|
+
Nothing in this release asks anything of a game.
|
|
36
|
+
|
|
19
37
|
## 18.1.0
|
|
20
38
|
|
|
21
39
|
Upgrading from 18.0.0. Impact: recommended.
|
package/docs/getting-started.md
CHANGED
package/docs/manifest.json
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"file": "arcade-and-store.md",
|
|
21
|
-
"sha256": "
|
|
22
|
-
"updated": "2026-10-
|
|
21
|
+
"sha256": "f5c2d57fbd28f576a6653167f0e50d12b6e4269c5f55f6020fa40d7fe2f4f3f1",
|
|
22
|
+
"updated": "2026-10-30T21:39:33Z"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"file": "arcade-avatars.md",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"file": "changelog.md",
|
|
51
|
-
"sha256": "
|
|
52
|
-
"updated": "2026-10-
|
|
51
|
+
"sha256": "fb27142771fc12eda3370d5d2cd5128a3ed03adaa947f674c8d35e781edb55d1",
|
|
52
|
+
"updated": "2026-10-31T03:13:57Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"file": "daily-challenges.md",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"file": "getting-started.md",
|
|
81
|
-
"sha256": "
|
|
82
|
-
"updated": "2026-10-
|
|
81
|
+
"sha256": "271469a08140655896a5127f987c183dca5bff617357645ce7dedae68610a9b2",
|
|
82
|
+
"updated": "2026-10-31T03:13:57Z"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"file": "inventory-and-catalogs.md",
|
|
@@ -138,8 +138,8 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"file": "server-bundles.md",
|
|
141
|
-
"sha256": "
|
|
142
|
-
"updated": "2026-10-
|
|
141
|
+
"sha256": "1a71596efb1eca4783929912ba503d63cdb7d7e24346e836d55abcfaf751b347",
|
|
142
|
+
"updated": "2026-10-31T03:13:57Z"
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"file": "serverless-functions-js.md",
|
package/docs/server-bundles.md
CHANGED
|
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
|
|
|
80
80
|
also publishes on its download host, with the hash beside it:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
curl -fsSL -O https://downloads.gemarcade.com/sdk/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v19.0.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v19.0.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -829,6 +829,12 @@ The short version: the browser half is what a player's **session** can do, and t
|
|
|
829
829
|
what a **match** can do. `gem.rooms.join()` has no meaning to a process that exists because a match
|
|
830
830
|
was already allocated.
|
|
831
831
|
|
|
832
|
+
**`gem.me.passes` is browser and function only**, and that is a limit rather than a principle. The
|
|
833
|
+
platform has a match read of which passes a player in your match holds, but your bundle cannot call
|
|
834
|
+
it: it requires the allocation nonce, which the runtime never hands to a bundle, as the boost read
|
|
835
|
+
does, and it answers `403` to every caller until game servers are granted `passes.read`.
|
|
836
|
+
[arcade-and-store.md](arcade-and-store.md#on-a-dedicated-server) has what to do instead.
|
|
837
|
+
|
|
832
838
|
**Rooms are the case where those two answers differ**, so they are worth stating separately rather
|
|
833
839
|
than filed under "browser-only". `gem.rooms` — join, leave, ready up, quick-join — is a player
|
|
834
840
|
acting on their own membership, and your server has none. Reading and administering the room your
|