@indiegems/gem-web-sdk 21.0.0 → 21.1.1

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.
@@ -13,8 +13,8 @@
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
16
- "sha256": "0883e3c53f500f4d0554d7d62ef938bc53676800b11a285e0da508c6fcc4076d",
17
- "updated": "2026-11-02T22:24:57Z"
16
+ "sha256": "5814655859e3d6b223f94b71f1b064783b0f242d409c3ec3078089cb4b7c8ec5",
17
+ "updated": "2026-11-04T17:17:14Z"
18
18
  },
19
19
  {
20
20
  "file": "arcade-and-store.md",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  {
30
30
  "file": "arcade-economy.md",
31
- "sha256": "b72474b4694842a1fda030ab9df79987d2906f7a8c922e8630622d747a4b2c3c",
32
- "updated": "2026-10-30T13:36:37Z"
31
+ "sha256": "c0f25495a536d5facdbb63cbcd89acf558e2fe79d44f3a99d35284de1937fc42",
32
+ "updated": "2026-11-04T17:09:56Z"
33
33
  },
34
34
  {
35
35
  "file": "async-opponents.md",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  {
50
50
  "file": "changelog.md",
51
- "sha256": "ed2476deed910a61badb23023267b5a8292467d3812c211adf3baf23c42ae914",
52
- "updated": "2026-11-03T21:36:35Z"
51
+ "sha256": "e0cdfb5efc261694e3cca35e23a1e62768db192468cafeae9144835ea7ac99b3",
52
+ "updated": "2026-11-04T20:17:46Z"
53
53
  },
54
54
  {
55
55
  "file": "daily-challenges.md",
@@ -73,18 +73,23 @@
73
73
  },
74
74
  {
75
75
  "file": "game-economy-tutorial.md",
76
- "sha256": "c037a74f206f094392f09e0f7b292271145a4d72b631b93f878705171e52475a",
77
- "updated": "2026-10-25T19:46:22Z"
76
+ "sha256": "0ab25a9d69cf5e40351624355da5ef84e1431bf8a8a420e2c4d7350e20a5b1c5",
77
+ "updated": "2026-11-04T16:44:33Z"
78
78
  },
79
79
  {
80
80
  "file": "getting-started.md",
81
- "sha256": "dd7328fd2462efd7373904f1c14085fd75b046d76670113cd0762825d0c0f107",
82
- "updated": "2026-11-03T21:36:35Z"
81
+ "sha256": "7d2cbe5cf8e26e102f52c8dac279998d11a67d82042b7e6051d1e4fb1548b0a5",
82
+ "updated": "2026-11-04T20:17:46Z"
83
+ },
84
+ {
85
+ "file": "graphics-route.md",
86
+ "sha256": "c71c826502b415a89dd069f79d22b82f7a9fe49c9da7f5e7a1052763c826f41f",
87
+ "updated": "2026-11-04T17:45:35Z"
83
88
  },
84
89
  {
85
90
  "file": "inventory-and-catalogs.md",
86
- "sha256": "dab6e42f4ad5cc2a63c2adb11e85053f963cd3166d1c447394cd6f532bb6c006",
87
- "updated": "2026-11-02T18:42:37Z"
91
+ "sha256": "2094c555eb5a1a3d817a597ac07b2b193077f92176eb9596c2ba3ed461363bb6",
92
+ "updated": "2026-11-04T16:43:14Z"
88
93
  },
89
94
  {
90
95
  "file": "leaderboards.md",
@@ -138,8 +143,8 @@
138
143
  },
139
144
  {
140
145
  "file": "server-bundles.md",
141
- "sha256": "736e7b4ceb810944331274f940cb47e7d3d4a57f815340a9ade7ffc5f01cd0fa",
142
- "updated": "2026-11-03T21:36:35Z"
146
+ "sha256": "31eeb1f555faf35123c4f22d072960448ef03f5ac4e76cc8c09748e127a88524",
147
+ "updated": "2026-11-04T20:17:46Z"
143
148
  },
144
149
  {
145
150
  "file": "serverless-functions-js.md",
@@ -163,8 +168,8 @@
163
168
  },
164
169
  {
165
170
  "file": "what-a-game-must-do.md",
166
- "sha256": "059f3c8a31c0f6098d2c8e4836b79e866d0c5486443dc89039e160648946bd68",
167
- "updated": "2026-11-02T23:27:22Z"
171
+ "sha256": "523886cf799570ab3631bbc9db11e03554d2ad403a1083d94038a48acd4fb611",
172
+ "updated": "2026-11-04T18:23:48Z"
168
173
  }
169
174
  ]
170
175
  }
@@ -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/v21.0.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v21.0.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v21.1.1/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v21.1.1/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -550,7 +550,7 @@ document**, so the call shapes provably match the contract:
550
550
  | Loot | `getDropRates` |
551
551
  | Leaderboards | `getLeaderboard` (the whole board — seed a match from current ranks), `getEntityLeaderboard` (one entity's own runs, best first, with its personal best — the read for deciding a reward from what the player actually did). Not `getFriendLeaderboard`: that view is per viewer, so a credential with no player identity is refused |
552
552
  | Transfers | `listTransfers`, `initiateInventoryTransfer`, `getTransferDetails`, `acceptTransfer`, `cancelInventoryTransfer`, `rejectTransfer` |
553
- | Platform-funded purchases | `getPurchaseCatalog` — the catalog read only. A **player client** may use `gem.me.mutations.purchase` for a direct platform-currency order because its token fixes the player, game and channel. A dedicated server token names no player and cannot start one; have the player's client initiate it. |
553
+ | Platform-funded purchases | `getPurchaseCatalog` — the catalog read only. A player spends platform currency only through the arcade's purchase sheet, which their **client** opens with `gem.purchases.request` ([inventory-and-catalogs.md](inventory-and-catalogs.md#16-selling-for-a-platform-currency)). A dedicated server token names no player and cannot start one; have the player's client initiate it. |
554
554
  | Game rooms | `getGameRoomV2`, `updateGameRoomV2`, `getRoomArgs`, `putRoomArgs`, `listGameRooms`, `closeGameRoom`, `kickPlayer`, `transferHostV2` — the reads pre-bound to your own room as `gem.room.get()` / `gem.room.args()`. [Below](#what-your-server-reads-at-startup) |
555
555
  | Game config | `getChannelAssignment` — which config version your server's own channel is running, and a short-lived signed URL for that version's JSON. Pass `gem.identity.channel`: the read is fenced to your own game and channel |
556
556
  | Room templates | `listResolvedTemplates`, `getResolvedTemplate` |
@@ -1454,6 +1454,36 @@ function frame(dt) {
1454
1454
  kind draws that one and ignores the field.
1455
1455
  - **No camera, no call.** A game with no camera to turn never calls `look`.
1456
1456
 
1457
+ ## WebGL or WebGPU: ask before you make your renderer
1458
+
1459
+ Every game that renders with WebGL asks the SDK which renderer to make, just before it makes its
1460
+ first one, and follows the answer for every canvas on the page:
1461
+
1462
+ ```js
1463
+ const { route, listed } = await gem.graphics.preferredRoute();
1464
+ ```
1465
+
1466
+ `route` is `webgl` almost everywhere. It is `webgpu` only on a device the platform lists as broken
1467
+ on WebGL (today the Pixel 10's PowerVR D-Series GPU, on every route but Vulkan, where WebGL crashes
1468
+ the game in Chrome and the whole Arcade app), and only where WebGPU is available. `listed` is true
1469
+ on such a device. So:
1470
+
1471
+ - `route` is `webgpu`: make your WebGPU renderer, and check it really started on WebGPU (with
1472
+ three.js, `renderer.backend.isWebGPUBackend` after `await renderer.init()`). If it did not, draw
1473
+ your **safe WebGL recipe**.
1474
+ - `route` is `webgl` and `listed` is true: draw your safe WebGL recipe. For every listed device
1475
+ today that is no shadow maps.
1476
+ - Otherwise: your normal WebGL renderer.
1477
+
1478
+ That holds for every WebGL canvas the page makes, menus included: one crash takes every context on
1479
+ the page with it. Supporting both means `WebGLRenderer` by default plus `WebGPURenderer` only on
1480
+ listed devices, never `WebGPURenderer` everywhere, and custom GLSL needs a TSL port for WebGPU.
1481
+ The call is bounded: it resolves within 1 second by default, normally at once after
1482
+ `awaitBridge()`, and never rejects. Do not detect devices yourself; the list is the platform's and
1483
+ reaches your live game without a new build. [`graphics-route.md`](graphics-route.md) has the
1484
+ three.js pattern, what both renderers cost, and how to test each answer with the `gemGraphics`
1485
+ launch key.
1486
+
1457
1487
  ## Graphics quality: the Arcade picks the start, your game adapts
1458
1488
 
1459
1489
  Players open a lot of games they have never played. Each one should open at a quality that suits
@@ -2258,8 +2288,9 @@ with your game's card, and shares it through its own sheet, copy or Share panel.
2258
2288
  only where `gem.share.available` is true.
2259
2289
 
2260
2290
  Two things before you reach for it: **call it from the click**, synchronously, never from a timer
2261
- or after an `await`; and **never gate a reward on it**, because your game is told nothing back,
2262
- not even whether the player shared.
2291
+ or after an `await`; and **never gate a reward on it**, because nothing about the player comes
2292
+ back, not even whether they shared. Its promise rejects only when the Arcade refused the link,
2293
+ naming why, so catch it and log it.
2263
2294
  [Linking to your game](getting-started.md#linking-to-your-game) is the section.
2264
2295
 
2265
2296
  ## If your game vibrates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "21.0.0",
3
+ "version": "21.1.1",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -39,6 +39,10 @@
39
39
  "types": "./dist/quality.d.ts",
40
40
  "import": "./dist/gem-web-sdk.quality.js"
41
41
  },
42
+ "./dev-tools": {
43
+ "types": "./dist/dev-tools.d.ts",
44
+ "import": "./dist/gem-web-sdk.dev-tools.js"
45
+ },
42
46
  "./conformance": {
43
47
  "types": "./dist/conformance.d.ts",
44
48
  "import": "./dist/gem-web-sdk.conformance.js"
@@ -2106,6 +2106,80 @@
2106
2106
  "rateLimit": { "perMinute": 12 }
2107
2107
  },
2108
2108
 
2109
+ {
2110
+ "type": "gem.graphicsRoutes",
2111
+ "direction": "parentToFrame",
2112
+ "transport": "port",
2113
+ "$comment": [
2114
+ "The devices on which a game should draw with WebGPU rather than WebGL.",
2115
+ "Gated by the `graphicsRoutes` capability.",
2116
+ "",
2117
+ "WEBGL IS THE DEFAULT and stays so (owner decision 2026-11-04): WebGPU",
2118
+ "generally performs worse out of the box. An entry names a device whose",
2119
+ "WebGL route is broken — the Pixel 10's PowerVR D-Series crashes on",
2120
+ "every route but Vulkan, one entry a route — so a game that asks",
2121
+ "`gem.graphics.preferredRoute()` is told the device is listed and to",
2122
+ "make its WebGPU renderer there, drawing its safe WebGL recipe where",
2123
+ "WebGPU is unavailable.",
2124
+ "",
2125
+ "THE WHOLE LIST, REPLACING THE SDK'S BUILT-IN ONE. Every SDK build carries",
2126
+ "a default list; this message is how a new entry reaches games already",
2127
+ "live, and how one that turns out wrong is taken away, with no game",
2128
+ "rebuilt. An EMPTY `entries` means 'no device needs WebGPU', which is an",
2129
+ "answer; never receiving the message means only 'this embedder never",
2130
+ "said', and the frame then uses its built-in list.",
2131
+ "",
2132
+ "OPEN VOCABULARIES, matched exactly, never by pattern. `gpu`, `route` and",
2133
+ "`host` are the families the SDK's own `gem_graphics_context_lost`",
2134
+ "report reads off the WebGL renderer string:",
2135
+ "`gpu` e.g. `powervr_d_series`, `route` e.g. `angle_gles`, `host`",
2136
+ "`android_webview` or `other`. Strings rather than enums on purpose: a",
2137
+ "family a newer SDK learns must not fail an older frame's validation of",
2138
+ "the whole list. A value a frame does not know simply matches nothing.",
2139
+ "An absent `route` or `host` matches any.",
2140
+ "",
2141
+ "PUSHED ONCE, right after the handshake accept and before anything else",
2142
+ "the parent sends, because a game asks just before it makes its renderer",
2143
+ "— which is at boot. The frame waits a bounded moment for it and answers",
2144
+ "from its built-in list when it does not come, so a parent that",
2145
+ "advertises nothing costs a game nothing, and one that advertises and",
2146
+ "stays silent costs it that wait at boot.",
2147
+ "",
2148
+ "Public, like everything on the bridge: a device list, nothing about the",
2149
+ "player. The frame does not report back which entry matched."
2150
+ ],
2151
+ "fields": {
2152
+ "entries": {
2153
+ "kind": "array",
2154
+ "maxItems": 32,
2155
+ "items": {
2156
+ "kind": "object",
2157
+ "fields": {
2158
+ "gpu": {
2159
+ "kind": "string",
2160
+ "maxLength": 32,
2161
+ "$comment": "The GPU family, e.g. `powervr_d_series`."
2162
+ },
2163
+ "route": {
2164
+ "kind": "string",
2165
+ "maxLength": 32,
2166
+ "optional": true,
2167
+ "$comment": "The WebGL route, e.g. `angle_gles`. Absent matches any route."
2168
+ },
2169
+ "host": {
2170
+ "kind": "string",
2171
+ "maxLength": 32,
2172
+ "optional": true,
2173
+ "$comment": "`android_webview` or `other`. Absent matches either."
2174
+ }
2175
+ }
2176
+ },
2177
+ "$comment": "Every device that should draw with WebGPU. Empty means none."
2178
+ }
2179
+ },
2180
+ "rateLimit": { "perMinute": 12 }
2181
+ },
2182
+
2109
2183
  {
2110
2184
  "type": "gem.devtools",
2111
2185
  "direction": "parentToFrame",
@@ -3483,18 +3557,38 @@
3483
3557
  "whether to share, builds the URL, holds the token and renders every",
3484
3558
  "surface.",
3485
3559
  "",
3486
- "ONE WAY, AND NOTHING ANSWERS IT. No requestId, no text, no URL and no",
3487
- "game id travel in this message, and no message travels back. The game",
3488
- "never learns whether the player shared, cancelled or was refused, so it",
3489
- "has nothing to gate progress or a reward on. An empty `params` array is",
3490
- "valid: it shares the game with no deep link.",
3560
+ "NOTHING ABOUT THE PLAYER COMES BACK. No text, no URL and no game id",
3561
+ "travel in this message. The game never learns whether the player",
3562
+ "shared, cancelled or dismissed the sheet, so it has nothing to gate",
3563
+ "progress or a reward on. The one answer is `gem.shareResult`, and only",
3564
+ "where both ends advertised `shareResult` and the request carries a",
3565
+ "`requestId`: whether the parent TOOK the request or REFUSED it, and",
3566
+ "why, decided on receipt from the request alone. Without those, nothing",
3567
+ "travels back, as before. An empty `params` array is valid: it shares",
3568
+ "the game with no deep link.",
3491
3569
  "",
3492
3570
  "WHAT THE PARENT OWES, none of which this schema can enforce:",
3493
3571
  "",
3494
- " REFUSE RESERVED KEYS. A key the parent reads from the play URL itself",
3495
- " refuses the whole request: the reserved launch keys, the same list the",
3496
- " SDK refuses before sending, which includes `play`, `channel`, `_s`,",
3497
- " every `utm_` key, `fbclid`, `ttclid`, `gclid` and `msclkid`.",
3572
+ " RESERVED KEYS: REMAP THEM, OR REFUSE THE REQUEST. The reserved",
3573
+ " launch keys are the ones the parent reads from the play URL itself",
3574
+ " (gem-api's launch-query-rules.json: `play`, `channel`, `invite`,",
3575
+ " `_s`, every `utm_` key, the ad-click ids and more). A parent that",
3576
+ " advertises `shareRemap` writes each one into the link under",
3577
+ " launch-query-rules.json's `share_key_escape_prefix` (`gemEsc_`), with",
3578
+ " a game key that already begins with the prefix escaped the same way,",
3579
+ " and removes the prefix once before forwarding the play URL's query",
3580
+ " into the frame, so the game reads back exactly the key it shared. It",
3581
+ " never acts on an escaped key itself, and a raw reserved key is still",
3582
+ " stripped before the frame. It still refuses `share_refused_keys`",
3583
+ " (`_s`, `embedder`) and a key too long to stay a valid key once",
3584
+ " escaped. A parent that does not advertise `shareRemap` refuses the",
3585
+ " whole request on any reserved key; the SDK refuses those before",
3586
+ " sending to such a parent.",
3587
+ "",
3588
+ " A restored click id or `utm_` value is whatever the link says: anyone",
3589
+ " who edits a share link can forge one, and third-party tags in the",
3590
+ " game's frame then read it as attribution. Accepted by the owner's",
3591
+ " decision; a game must not trust it as attribution.",
3498
3592
  "",
3499
3593
  " BUILD FROM THE FRAMED GAME, NEVER FROM THE MESSAGE. The link is",
3500
3594
  " `/games/<id>?play=1&<params>&_s=<token>`, where `<id>` is the game the",
@@ -3528,13 +3622,26 @@
3528
3622
  " shared: copy a game authored, sent under the platform's name, is a",
3529
3623
  " phishing kit with the platform's name on it.",
3530
3624
  "",
3531
- " SEND NOTHING BACK. No outcome, no acknowledgement, and no error for a",
3532
- " refusal - not even for a malformed or reserved request.",
3625
+ " ANSWER ONLY WHAT THE REQUEST DECIDES. To a frame that advertised",
3626
+ " `shareResult`, a parent that advertised it answers a request carrying",
3627
+ " a `requestId` with exactly one `gem.shareResult`, on receipt, before",
3628
+ " any sheet or panel opens: `refused` with the reason for a request its",
3629
+ " key, round-trip or length checks refuse, and `accepted` for every",
3630
+ " other, including one it then drops under the rules above. Nothing",
3631
+ " that depends on the player - shared, cancelled, dismissed, a",
3632
+ " cooldown after a dismissal - is ever answered. To any other frame, or",
3633
+ " for a request with no `requestId`, send nothing back.",
3533
3634
  "",
3534
3635
  "Everything in `params` is public: it is written into a link the player",
3535
3636
  "hands to anyone. A game puts no personal identifier in it."
3536
3637
  ],
3537
3638
  "fields": {
3639
+ "requestId": {
3640
+ "kind": "string",
3641
+ "maxLength": 64,
3642
+ "optional": true,
3643
+ "$comment": "Opaque and frame-chosen. Echo-only. Sent only to a parent that advertised `shareResult`, which answers it with one `gem.shareResult`. Absent from every SDK before it."
3644
+ },
3538
3645
  "params": {
3539
3646
  "kind": "array",
3540
3647
  "maxItems": 16,
@@ -3553,6 +3660,54 @@
3553
3660
  }
3554
3661
  },
3555
3662
 
3663
+ {
3664
+ "type": "gem.shareResult",
3665
+ "direction": "parentToFrame",
3666
+ "transport": "port",
3667
+ "$comment": [
3668
+ "The answer to one gem.requestShare that carried a `requestId`. Gated by",
3669
+ "the `shareResult` capability: sent only by a parent that advertised it,",
3670
+ "to a frame that advertised it.",
3671
+ "",
3672
+ " accepted - the parent took the request. NOT A SHARE: it says nothing",
3673
+ " about whether a sheet opened, or whether the player shared,",
3674
+ " and a request the parent then drops under its pacing rules",
3675
+ " is `accepted` too.",
3676
+ " refused - the parent will not share this link; `reason` says why,",
3677
+ " and `key` names the param when one key caused it.",
3678
+ "",
3679
+ "ANSWERED ON RECEIPT, FROM THE REQUEST ALONE, before any sheet or panel",
3680
+ "opens, so neither the answer nor its timing says anything about the",
3681
+ "player. The game is told what to fix, and nothing else."
3682
+ ],
3683
+ "fields": {
3684
+ "requestId": {
3685
+ "kind": "string",
3686
+ "maxLength": 64,
3687
+ "$comment": "Echoes the gem.requestShare that asked. Echo-only."
3688
+ },
3689
+ "outcome": {
3690
+ "kind": "enum",
3691
+ "values": ["accepted", "refused"],
3692
+ "$comment": "Closed at two. Why a request was refused is `reason`, which is open."
3693
+ },
3694
+ "reason": {
3695
+ "kind": "string",
3696
+ "maxLength": 64,
3697
+ "format": "code",
3698
+ "optional": true,
3699
+ "$comment": "With `refused` only. An open code, from launch-query-rules.json's `share_refusal_codes`: `reserved_key` (a key the parent keeps for itself and does not carry), `round_trip` (the built link does not parse back to what was asked, or an escaped key would not be a valid key), `too_long` (the link is over 2 KB). A receiver must not fail on a code it does not know."
3700
+ },
3701
+ "key": {
3702
+ "kind": "string",
3703
+ "maxLength": 64,
3704
+ "optional": true,
3705
+ "$comment": "With `refused` only: the param key that caused it, exactly as the request named it, when one key did. Absent for a refusal of the whole link (`too_long`)."
3706
+ }
3707
+ },
3708
+ "rateLimit": { "perMinute": 30 }
3709
+ },
3710
+
3556
3711
  {
3557
3712
  "type": "gem.requestAvatarPicker",
3558
3713
  "direction": "frameToParent",
@@ -3991,6 +4146,35 @@
3991
4146
  "maxLength": 64,
3992
4147
  "optional": true,
3993
4148
  "$comment": "Required for recipes, which are addressed within a catalog. Comes from the game's published config."
4149
+ },
4150
+ "origin": {
4151
+ "kind": "enum",
4152
+ "values": ["sheet", "direct_refused"],
4153
+ "optional": true,
4154
+ "$comment": [
4155
+ "Why the frame is asking. ABSENT MEANS `sheet`: an SDK older than the field",
4156
+ "sends none.",
4157
+ "",
4158
+ " sheet - the game called `gem.purchases.request`: the player",
4159
+ " pressed the game's Buy and expects the parent's sheet.",
4160
+ " direct_refused - the game spent directly (`gem.me.mutations.purchase`)",
4161
+ " and gem-api refused it 403 `guest_upgrade_required`:",
4162
+ " the player has no account the platform lets spend.",
4163
+ " The SDK hands the attempt to the parent so it can offer",
4164
+ " the player an account, then rejects the game's call",
4165
+ " with that error at once, without waiting for this",
4166
+ " ask's answer. `currencySlug` is the platform's exact",
4167
+ " slug, from the player's balances, never the game's",
4168
+ " string.",
4169
+ "",
4170
+ "A HINT FROM UNTRUSTED CODE, NOT A FACT. A game can post either value on",
4171
+ "the port it holds. The parent decides what to draw from its own record of",
4172
+ "the player, never from this field, and every obligation above applies to",
4173
+ "a `direct_refused` ask unchanged: it is answered `gem.purchaseResult` like",
4174
+ "any other, and a sheet it opens spends only on the player's own press.",
4175
+ "A parent that does not read the field treats the ask as a sheet ask,",
4176
+ "which is the safe reading of either value."
4177
+ ]
3994
4178
  }
3995
4179
  },
3996
4180
  "rateLimit": {
@@ -4222,6 +4406,11 @@
4222
4406
  " recipe_max_uses_exceeded,",
4223
4407
  " item_max_stack_exceeded,",
4224
4408
  " recipe_costs_unmet, quantity_invalid",
4409
+ " the player is a guest the platform does declined guest_upgrade_required",
4410
+ " not let spend (gem-api 403",
4411
+ " guest_upgrade_required, or the parent",
4412
+ " knows it first), whether or not the",
4413
+ " parent offered them an account",
4225
4414
  " gem-api confirmation_required - NEVER REACHES THE",
4226
4415
  " GAME: the parent",
4227
4416
  " handles it itself,",
@@ -4229,7 +4418,8 @@
4229
4418
  " the player's age",
4230
4419
  " gem-api 429 (its limiter is per IP, so it declined error",
4231
4420
  " would describe other people's activity),",
4232
- " 403, nonce conflicts, and every account",
4421
+ " any other 403, nonce conflicts, and every",
4422
+ " other account",
4233
4423
  " state - sanction, age, region",
4234
4424
  " 5xx, a network failure, a timeout, an `ok` unavailable error, or absent",
4235
4425
  " the frame could not verify, and a charge",
@@ -4254,6 +4444,67 @@
4254
4444
  },
4255
4445
  "rateLimit": { "perMinute": 30 }
4256
4446
  },
4447
+ {
4448
+ "type": "gem.purchaseSettled",
4449
+ "direction": "parentToFrame",
4450
+ "transport": "port",
4451
+ "$comment": [
4452
+ "A platform-currency purchase of this game's own item or recipe has",
4453
+ "completed, and no gem.purchaseResult reports it. Gated by the",
4454
+ "`purchaseSettled` capability. Pushed, unasked, once per such purchase",
4455
+ "while the frame session lasts.",
4456
+ "",
4457
+ "WHEN. The parent finished a purchase the game asked for in an earlier",
4458
+ "session or on a promise already answered: the typical case is a guest",
4459
+ "whose spend was refused (`guest_upgrade_required`), who made an account",
4460
+ "and confirmed the parent's sheet after the game's ask had been answered,",
4461
+ "or after a reload had dropped it. A purchase answered `ok` through",
4462
+ "gem.purchaseResult is never ALSO reported here.",
4463
+ "",
4464
+ "A HINT, NOT A RECEIPT, modelled on gem.storePurchaseSettled. The SDK",
4465
+ "confirms `purchaseId` against the platform's own record with the",
4466
+ "frame's token before it tells the game, exactly as it confirms a",
4467
+ "gem.purchaseResult `ok`, and re-reads the player's inventory either way",
4468
+ "but on a definite `absent`. A game never grants on the strength of this",
4469
+ "message: it re-reads inventory, which is where the truth is.",
4470
+ "",
4471
+ "EVERY FIELD IS THE PARENT'S, AND PUBLIC. The ids are this game's own,",
4472
+ "from its published config; `purchaseId` follows",
4473
+ "gem.purchaseResult.purchaseId's rules: opaque, high-entropy, minted for",
4474
+ "this disclosure. No price, no currency and no balance cross here."
4475
+ ],
4476
+ "fields": {
4477
+ "purchaseKind": {
4478
+ "kind": "enum",
4479
+ "values": ["item", "recipe"],
4480
+ "$comment": "As on gem.requestPurchase."
4481
+ },
4482
+ "targetId": {
4483
+ "kind": "string",
4484
+ "maxLength": 64,
4485
+ "$comment": "The item-definition or recipe id that was bought, as on gem.requestPurchase."
4486
+ },
4487
+ "catalogId": {
4488
+ "kind": "string",
4489
+ "maxLength": 64,
4490
+ "optional": true,
4491
+ "$comment": "The recipe's catalog, for a recipe; absent for an item."
4492
+ },
4493
+ "quantity": {
4494
+ "kind": "int",
4495
+ "min": 1,
4496
+ "max": 10,
4497
+ "$comment": "How many were bought, 1..=10 as on gem.requestPurchase. Always 1 for a recipe."
4498
+ },
4499
+ "purchaseId": {
4500
+ "kind": "string",
4501
+ "maxLength": 64,
4502
+ "optional": true,
4503
+ "$comment": "The platform's own id for this purchase, under gem.purchaseResult.purchaseId's rules. The SDK confirms it before it tells the game; a hint without one is never reported to the game as a purchase, though the SDK still re-reads the player's inventory."
4504
+ }
4505
+ },
4506
+ "rateLimit": { "perMinute": 30 }
4507
+ },
4257
4508
  {
4258
4509
  "type": "gem.deviceSettings.put",
4259
4510
  "direction": "frameToParent",
@@ -27,6 +27,12 @@
27
27
  "gem.purchaseResult"
28
28
  ]
29
29
  },
30
+ "purchaseSettled": {
31
+ "implemented": true,
32
+ "gates": [
33
+ "gem.purchaseSettled"
34
+ ]
35
+ },
30
36
  "avatar3dPurchases": {
31
37
  "implemented": true,
32
38
  "gates": [
@@ -86,6 +92,12 @@
86
92
  "gem.clusterTarget"
87
93
  ]
88
94
  },
95
+ "graphicsRoutes": {
96
+ "implemented": true,
97
+ "gates": [
98
+ "gem.graphicsRoutes"
99
+ ]
100
+ },
89
101
  "parties": {
90
102
  "implemented": true,
91
103
  "gates": [
@@ -217,6 +229,20 @@
217
229
  "gem.requestShare"
218
230
  ]
219
231
  },
232
+ "shareResult": {
233
+ "implemented": true,
234
+ "gates": [
235
+ "gem.shareResult"
236
+ ]
237
+ },
238
+ "shareRemap": {
239
+ "implemented": true,
240
+ "gates": [
241
+ {
242
+ "namespace": "gem.share"
243
+ }
244
+ ]
245
+ },
220
246
  "avatarPicker": {
221
247
  "implemented": true,
222
248
  "gates": [