@indiegems/gem-web-sdk 19.0.2 → 20.1.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.
@@ -1790,8 +1790,11 @@
1790
1790
  "deliberately: a frame that took its JWKS endpoint from a relayed message",
1791
1791
  "would let the parent choose the keys it verifies peers against.",
1792
1792
  "",
1793
- "The nine events are the whole vocabulary; an unrecognised p2p_* type is",
1794
- "a drop at the parent, not a passthrough.",
1793
+ "The nine events are the whole vocabulary of this message; an",
1794
+ "unrecognised p2p_* type is a drop at the parent, not a passthrough. The",
1795
+ "relay drain notice is the one further push the parent relays, and it",
1796
+ "crosses as its own type, gem.relayDraining: this enum is closed, so a",
1797
+ "tenth value would be refused by every frame already shipped.",
1795
1798
  "",
1796
1799
  "PARENT TO FRAME ONLY. gem.signal is the other direction."
1797
1800
  ],
@@ -1824,6 +1827,68 @@
1824
1827
  "rateLimit": { "perMinute": 1800 }
1825
1828
  },
1826
1829
 
1830
+ {
1831
+ "type": "gem.relayDraining",
1832
+ "direction": "parentToFrame",
1833
+ "transport": "port",
1834
+ "$comment": [
1835
+ "A TURN relay this player's P2P match may be using is going away, relayed",
1836
+ "from gem-api's `p2p_relay_draining`. Gated by the `p2pRelayDrain`",
1837
+ "capability; the mapping lives in schema/capabilities.json and nowhere",
1838
+ "else.",
1839
+ "",
1840
+ "A HINT, AND ONE THE FRAME COULD SURVIVE WITHOUT. It tells the frame to",
1841
+ "re-lease and, where its selected candidate pair runs through one of",
1842
+ "these relays, to restart ICE on the connection it already has, so the",
1843
+ "match moves before the relay stops rather than after. The control is",
1844
+ "the relay's own: a draining relay refuses new allocations whatever",
1845
+ "this message says, and a connection on it fails over by the ordinary",
1846
+ "path when it stops. A parent that rewrites this can cost a frame a",
1847
+ "renegotiation; it cannot move a connection anywhere.",
1848
+ "",
1849
+ "ITS OWN TYPE, NOT A TENTH VALUE IN gem.signalEvent'S ENUM: the",
1850
+ "generated validator rejects an unknown enum value, and every frame",
1851
+ "already shipped would answer `invalid_message`. A parent sends this",
1852
+ "only to a frame that advertised the capability and drops it otherwise.",
1853
+ "",
1854
+ "SCOPED BY MATCH, NOT BY GAME. gem-api stamps the push with no game id,",
1855
+ "as it does `p2p_error`, so the parent relays it only for a match this",
1856
+ "player's socket or this frame has already named.",
1857
+ "",
1858
+ "PARENT TO FRAME ONLY. A frame-authored drain notice would let a game",
1859
+ "tell the parent a relay is going away."
1860
+ ],
1861
+ "fields": {
1862
+ "matchId": {
1863
+ "kind": "string",
1864
+ "maxLength": 64,
1865
+ "$comment": "The match whose relays are draining. The frame acts only for the live session on this match."
1866
+ },
1867
+ "clusterTargetId": {
1868
+ "kind": "string",
1869
+ "maxLength": 64,
1870
+ "$comment": "The cluster the draining relays belong to. It decides nothing on the frame: the re-lease asks the cluster the session already leases from."
1871
+ },
1872
+ "relayIps": {
1873
+ "kind": "array",
1874
+ "maxItems": 16,
1875
+ "items": { "kind": "string", "maxLength": 64 },
1876
+ "$comment": "The draining relays' public addresses — the hosts in the lease's `turn:` URLs, and what a selected candidate pair names. A lease names at most three relays."
1877
+ },
1878
+ "drainingSince": {
1879
+ "kind": "string",
1880
+ "maxLength": 40,
1881
+ "$comment": "RFC 3339. When the relays began draining."
1882
+ },
1883
+ "deadline": {
1884
+ "kind": "string",
1885
+ "maxLength": 40,
1886
+ "$comment": "RFC 3339. When the relays stop relaying. A link still on one by then is dropped and re-dialled; one this side cannot parse is read as the drain ceiling, thirty minutes."
1887
+ }
1888
+ },
1889
+ "rateLimit": { "perMinute": 60 }
1890
+ },
1891
+
1827
1892
  {
1828
1893
  "type": "gem.dedicatedEvent",
1829
1894
  "direction": "parentToFrame",
@@ -3819,11 +3884,11 @@
3819
3884
  " balance a player paid for is the rest of this list, and these four are",
3820
3885
  " the ones the old argument stood in for:",
3821
3886
  "",
3822
- " - THE CONFIRMATION IS THE PARENT'S, and it costs a gesture. The sheet is",
3823
- " drawn in the parent's own chrome from platform records, and only on",
3824
- " user activation (below).",
3825
- " - A DISMISSAL BACKS OFF, and the confirm control is quiet after paint",
3826
- " (both below; the arcade holds the quiet period for 400 ms).",
3887
+ " - THE CONFIRMATION IS THE PARENT'S. The sheet is drawn in the",
3888
+ " parent's own chrome from platform records, and only the player's own",
3889
+ " press on its confirm control spends.",
3890
+ " - THE CONFIRM CONTROL IS QUIET AFTER PAINT (below; the arcade holds",
3891
+ " the quiet period for 400 ms).",
3827
3892
  " - A SHORTFALL IS NEVER A SALE. `insufficient_funds` gets no top-up, no",
3828
3893
  " upsell and no way into a store from THIS sheet. A game MAY open the",
3829
3894
  " arcade's store for one of the store's own SKUs -",
@@ -3842,18 +3907,14 @@
3842
3907
  " price, quantity and total distinctly so a 10x multiplier is never hidden",
3843
3908
  " behind one figure.",
3844
3909
  "",
3845
- " ONLY ON USER ACTIVATION. Render nothing, and answer `declined` with",
3846
- " reason `busy`, unless `navigator.userActivation.isActive` is true when",
3847
- " the ask arrives. A click or key press in the game frame activates the",
3848
- " parent page too, so a Buy button works and a timer does not. It is the",
3849
- " browser's own signal, so keyboard and assistive-technology activation",
3850
- " count. This is the control that makes `cancelled` (gem.purchaseResult)",
3851
- " safe to disclose: a prompt costs a real player's gesture.",
3852
- "",
3853
- " BACK OFF AFTER A CANCEL. After a `cancelled`, answer further asks from",
3854
- " that game `declined` / `busy` without rendering, for an interval that",
3855
- " grows with each consecutive cancel. Without it, 'ask, learn cancelled,",
3856
- " ask again' is a re-prompt loop a game can write in three lines.",
3910
+ " NO PRESS, AND NO BACK-OFF (owner decision).",
3911
+ " Arcade games are developed in-house and trusted, and a limit between",
3912
+ " a player and a purchase refused players who wanted to buy. So the",
3913
+ " parent renders the sheet for an ask from a timer or after an await as",
3914
+ " for one from a click - `navigator.userActivation` is not read - and a",
3915
+ " sheet the player closed never makes the next ask wait: it is answered",
3916
+ " `cancelled`, and the game's next ask opens a sheet. The sheet itself",
3917
+ " is the confirmation; a game cannot press it.",
3857
3918
  "",
3858
3919
  " ONE PROMPT AT A TIME, and not instantly actionable. A gem.requestPurchase",
3859
3920
  " arriving while a prompt is open is answered `declined` / `busy` without",
@@ -3960,9 +4021,9 @@
3960
4021
  "EVERY OBLIGATION ON `gem.requestPurchase` APPLIES HERE, UNCHANGED:",
3961
4022
  "payable currencies only, resolved exactly; price it yourself from",
3962
4023
  "platform records, under the player's session (the offer's price in",
3963
- "`currencySlug`); the confirmation is the parent's own sheet, on user",
3964
- "activation only; back off after a cancel; one prompt at a time, quiet",
3965
- "after paint; one purchase per ask; meter it in money, server-side;",
4024
+ "`currencySlug`); the confirmation is the parent's own sheet, with no",
4025
+ "press required and no back-off; one prompt at a time, quiet after",
4026
+ "paint; one purchase per ask; meter it in money, server-side;",
3966
4027
  "treat every field as untrusted text. A shortfall is never a sale.",
3967
4028
  "",
3968
4029
  "AND THREE OF ITS OWN:",
@@ -4056,13 +4117,14 @@
4056
4117
  "a game nothing it cannot already read.",
4057
4118
  "",
4058
4119
  "THE ONE ADDED DISCLOSURE IS `cancelled`: what the player chose in",
4059
- "platform UI. It is behavioural data, and it is what makes an instant",
4060
- "re-prompt loop cheap - ask, learn 'cancelled', ask again. It ships with",
4061
- "two parent-side controls, stated as obligations on gem.requestPurchase:",
4062
- "ONLY ON USER ACTIVATION (no prompt without a real gesture) and BACK OFF",
4063
- "AFTER A CANCEL (further asks answered `busy`, unrendered, for a growing",
4064
- "interval). A parent that cannot enforce both must not send `cancelled`;",
4065
- "it sends `error` in its place.",
4120
+ "platform UI. It first shipped behind two parent-side controls - user",
4121
+ "activation, and a back-off after a cancel - against a game that asks,",
4122
+ "learns 'cancelled' and asks again. Both were removed by owner decision:",
4123
+ "Arcade games are developed in-house and trusted,",
4124
+ "and the back-off refused players who wanted to buy. A parent sends",
4125
+ "`cancelled` when the player dismissed a sheet they could afford, and",
4126
+ "answers the game's next ask on its merits. A parent older than that",
4127
+ "decision may send `error` in its place; read both as 'not bought'.",
4066
4128
  "",
4067
4129
  "EVERY OUTCOME RESOLVES. Only a protocol failure rejects. A player tapping",
4068
4130
  "Cancel is an ordinary thing that happens in every shop, and rejecting there",
@@ -4098,7 +4160,7 @@
4098
4160
  "outcome": {
4099
4161
  "kind": "enum",
4100
4162
  "values": ["ok", "declined", "unavailable"],
4101
- "$comment": "THREE VALUES, AND THAT IS THE COMPLETE VOCABULARY OF WHAT TO DO. The WHY is `reason`, a bounded string, so the explanations can grow without this enum growing - which matters because `check-schema-freeze` treats a new enum value as breaking (an N-1 receiver refuses it, and a refused `declined` becomes a two-minute timeout and an `unavailable`). A game branches its money logic on this field and on a re-read of inventory, never on `reason`. THE TIMING CHANNEL: a refusal the parent reaches without rendering anything is separable from a human Cancel by latency. That used to be the gap the no-reason rule left open; with `reason` sent, it tells a game nothing `reason` and `gem.platform.currencies.get()` do not already say. What still matters is how fast a game can ask, and user activation plus the cancel back-off bound that at human speed."
4163
+ "$comment": "THREE VALUES, AND THAT IS THE COMPLETE VOCABULARY OF WHAT TO DO. The WHY is `reason`, a bounded string, so the explanations can grow without this enum growing - which matters because `check-schema-freeze` treats a new enum value as breaking (an N-1 receiver refuses it, and a refused `declined` becomes a two-minute timeout and an `unavailable`). A game branches its money logic on this field and on a re-read of inventory, never on `reason`. THE TIMING CHANNEL: a refusal the parent reaches without rendering anything is separable from a human Cancel by latency. That used to be the gap the no-reason rule left open; with `reason` sent, it tells a game nothing `reason` and `gem.platform.currencies.get()` do not already say. What still matters is how fast a game can ask: games are trusted, so nothing paces a game's asks beyond one prompt at a time and the rate limit."
4102
4164
  },
4103
4165
  "reason": {
4104
4166
  "kind": "string",
@@ -4138,9 +4200,8 @@
4138
4200
  " the target is not in the purchase catalog, declined not_purchasable",
4139
4201
  " or is not purchasable",
4140
4202
  " the target has no price in that currency declined no_price_in_currency",
4141
- " a sheet is already open, a purchase is in declined busy",
4142
- " flight, a cancel back-off is running, or",
4143
- " the ask arrived without user activation",
4203
+ " a sheet is already open, or a purchase is declined busy",
4204
+ " in flight",
4144
4205
  " the player dismissed a sheet they could declined cancelled",
4145
4206
  " afford",
4146
4207
  " a shortfall shown on the sheet, or gem-api declined insufficient_funds",
@@ -5013,8 +5074,9 @@
5013
5074
  "`appLinks` capability. Answered by `gem.openAppResult`.",
5014
5075
  "",
5015
5076
  "AN ACTING MESSAGE. It leaves the game, so the parent holds it to the",
5016
- "rules every acting message shares (gem.requestOpenGame and",
5017
- "gem.requestStorePurchase state them too):",
5077
+ "rules every acting message shares (gem.requestOpenGame states them",
5078
+ "too; a purchase - gem.requestStorePurchase, gem.requestPurchase -",
5079
+ "shares all of them but the press):",
5018
5080
  "",
5019
5081
  " A PERSON MUST HAVE PRESSED. Read `navigator.userActivation.isActive`",
5020
5082
  " synchronously on receipt, before any `await`, or accept a controller",
@@ -5507,10 +5569,12 @@
5507
5569
  "listing, takes money only on the payment provider's page or the",
5508
5570
  "operating system's purchase sheet, and tells the game the outcome.",
5509
5571
  "",
5510
- "AN ACTING MESSAGE, under every rule gem.requestOpenApp states: a press",
5511
- "the parent can see (a missing `userActivation` API fails closed), one",
5512
- "acting message at a time across all of them, both checked before",
5513
- "`skuId` is resolved, and the answer bound to this frame session. And:",
5572
+ "AN ACTING MESSAGE: one acting message at a time across all of them,",
5573
+ "checked before `skuId` is resolved, and the answer bound to this frame",
5574
+ "session. UNLIKE gem.requestOpenApp AND gem.requestOpenGame IT NEEDS NO",
5575
+ "PRESS (owner decision): a purchase asked from a",
5576
+ "timer or after an await opens its checkout as one asked from a click",
5577
+ "does, because the checkout is the confirmation. And:",
5514
5578
  "",
5515
5579
  " ONLY A SKU IT WOULD LIST. `skuId` must be one gem.storeOffersResult",
5516
5580
  " would list to this player right now; anything else is `unavailable`",
@@ -5528,14 +5592,13 @@
5528
5592
  " bought again yet, the parent says when, in its own chrome, and",
5529
5593
  " answers `declined` / `cancelled` - the answer a closed checkout gets,",
5530
5594
  " so the cooldown, which would reveal a recent purchase, never crosses",
5531
- " the bridge. It starts no back-off: the player turned nothing down.",
5595
+ " the bridge.",
5532
5596
  "",
5533
- " BACK OFF AFTER A DISMISSAL. `cancelled` is disclosed, so after the",
5534
- " player closes a checkout a game started, further asks from that game",
5535
- " are `declined` / `busy`, unrendered, for an interval that grows with",
5536
- " each dismissal (the Arcade: 30 s, 2 min, 10 min, then the rest of the",
5537
- " session). Only the player closing it starts the back-off: a failed",
5538
- " payment, `error` and `ok` / `pending` do not.",
5597
+ " NO BACK-OFF (owner decision). A player who",
5598
+ " closed a checkout and asks again gets the checkout again, however many",
5599
+ " times; a player who paid and buys the same SKU again gets a new",
5600
+ " checkout. Only one checkout is on screen at a time, so an ask while",
5601
+ " one is open is `busy` - the moment it ends, the next ask opens.",
5539
5602
  "",
5540
5603
  " RESUME, THEN ANSWER. The result is sent after the game has been told",
5541
5604
  " it is resumed, so the game's own celebration is visible.",
@@ -5560,7 +5623,7 @@
5560
5623
  },
5561
5624
  "rateLimit": {
5562
5625
  "perMinute": 12,
5563
- "$comment": "gem.requestPurchase's limit and its argument: it bounds a BUGGY game, not a hostile one. What bounds the player's experience is the activation check, the one-at-a-time rule and the back-off."
5626
+ "$comment": "gem.requestPurchase's limit and its argument: it bounds a BUGGY game, not a hostile one, at a rate no player buying by hand reaches. What bounds the player's experience is the one-at-a-time rule."
5564
5627
  }
5565
5628
  },
5566
5629
  {
@@ -5578,9 +5641,9 @@
5578
5641
  " the player left before it did): `gem.storePurchaseSettled`",
5579
5642
  " follows when it does, and a game re-reads on resume too.",
5580
5643
  " declined - nothing was bought: the player closed the checkout,",
5581
- " or the offer is cooling down (`cancelled`); or no",
5582
- " press, another acting message open or a back-off",
5583
- " running (`busy`).",
5644
+ " or the offer is cooling down (`cancelled`); or another",
5645
+ " acting message was open (`busy`). Either way the",
5646
+ " game's next ask is answered on its merits.",
5584
5647
  " unavailable - no sale the parent can vouch for. With `store_off`",
5585
5648
  " (the store withheld), `sign_in_required` (a guest),",
5586
5649
  " `not_found` (an unknown SKU) or the provider's own",
@@ -5659,18 +5722,17 @@
5659
5722
  "gem.requestStorePurchase opens.",
5660
5723
  "",
5661
5724
  "EVERY RULE OF gem.requestStorePurchase HOLDS, and it shares them rather",
5662
- "than having its own: an acting message (a press the parent can see, one",
5663
- "at a time across all of them, both checked before `skuId` is resolved,",
5664
- "the answer bound to this frame session); only a SKU the parent would",
5665
- "list now; an offer cooling down opens nothing and answers `declined` /",
5666
- "`cancelled`; the SAME back-off, so a game cannot escape one by switching",
5667
- "message; resume, then answer; evidence, not accounting.",
5725
+ "than having its own: an acting message (one at a time across all of",
5726
+ "them, checked before `skuId` is resolved, no press needed, the answer",
5727
+ "bound to this frame session); only a SKU the parent would list now; an",
5728
+ "offer cooling down opens nothing and answers `declined` / `cancelled`;",
5729
+ "no back-off; resume, then answer; evidence, not accounting.",
5668
5730
  "",
5669
5731
  "THE SHEET IS NOT A SECOND CONFIRMATION, and it does not change",
5670
5732
  "gem.requestStorePurchase's straight-to-checkout rule. It is the pitch;",
5671
5733
  "the checkout is still the confirmation, with its own pay control. The",
5672
5734
  "player leaving the sheet without pressing Buy is a dismissal, exactly",
5673
- "as closing a checkout is, and starts the back-off.",
5735
+ "as closing a checkout is.",
5674
5736
  "",
5675
5737
  "A PARENT THAT ADVERTISES `storeOfferSheet` ADVERTISES `store`: the",
5676
5738
  "game's SKU comes from gem.storeOffersResult, and a purchase that answers",
@@ -5693,7 +5755,7 @@
5693
5755
  },
5694
5756
  "rateLimit": {
5695
5757
  "perMinute": 12,
5696
- "$comment": "gem.requestStorePurchase's limit and its argument: it bounds a BUGGY game, not a hostile one. What bounds the player's experience is the activation check, the one-at-a-time rule and the back-off."
5758
+ "$comment": "gem.requestStorePurchase's limit and its argument: it bounds a BUGGY game, not a hostile one, at a rate no player buying by hand reaches. What bounds the player's experience is the one-at-a-time rule."
5697
5759
  }
5698
5760
  },
5699
5761
  {
@@ -5710,8 +5772,8 @@
5710
5772
  " gem.storePurchaseSettled follows when it does.",
5711
5773
  " declined - nothing was bought: the player left the sheet or closed",
5712
5774
  " the checkout, or the offer is cooling down (`cancelled`);",
5713
- " or no press, another acting message open or a back-off",
5714
- " running (`busy`).",
5775
+ " or another acting message was open (`busy`). Either way",
5776
+ " the game's next ask is answered on its merits.",
5715
5777
  " unavailable - no sale the parent can vouch for. With `store_off`,",
5716
5778
  " `sign_in_required`, `not_found` or the provider's own",
5717
5779
  " failure code, nothing was bought. With `error`, the",
@@ -308,6 +308,12 @@
308
308
  "gates": [
309
309
  "gem.menuStyle"
310
310
  ]
311
+ },
312
+ "p2pRelayDrain": {
313
+ "implemented": true,
314
+ "gates": [
315
+ "gem.relayDraining"
316
+ ]
311
317
  }
312
318
  }
313
319
  }