@indiegems/gem-web-sdk 15.3.2 → 16.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 +26 -0
- package/dist/conformance.d.ts +7 -1
- package/dist/game.d.ts +187 -1
- package/dist/gem-web-sdk.bridge.js +2 -2
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.esm.js +3 -3
- 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 +2 -2
- package/dist/index.d.ts +171 -7
- package/dist/sdk.d.ts +187 -1
- package/docs/api/api-reference-function.md +1 -1
- package/docs/api/api-reference-game.md +762 -3
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +1203 -190
- package/docs/arcade-and-store.md +289 -0
- package/docs/arcade-economy.md +13 -9
- package/docs/changelog.md +26 -0
- package/docs/getting-started.md +5 -3
- package/docs/inventory-and-catalogs.md +8 -4
- package/docs/manifest.json +17 -12
- package/docs/player-text-fields.json +45 -4
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +36 -3
- package/package.json +1 -1
- package/schema/bridge.v1.json +735 -5
- package/schema/capabilities.public.json +28 -0
package/schema/bridge.v1.json
CHANGED
|
@@ -3767,11 +3767,16 @@
|
|
|
3767
3767
|
" - A DISMISSAL BACKS OFF, and the confirm control is quiet after paint",
|
|
3768
3768
|
" (both below; the arcade holds the quiet period for 400 ms).",
|
|
3769
3769
|
" - A SHORTFALL IS NEVER A SALE. `insufficient_funds` gets no top-up, no",
|
|
3770
|
-
" upsell and no way into a store
|
|
3771
|
-
"
|
|
3772
|
-
"
|
|
3773
|
-
"
|
|
3774
|
-
"
|
|
3770
|
+
" upsell and no way into a store from THIS sheet. A game MAY open the",
|
|
3771
|
+
" arcade's store for one of the store's own SKUs -",
|
|
3772
|
+
" gem.requestStorePurchase, gated on `store` -",
|
|
3773
|
+
" but only from the player's press on a button of the game's; that",
|
|
3774
|
+
" sheet is the Arcade Store's, under its own rules, and nothing opens",
|
|
3775
|
+
" it by itself on a refusal here.",
|
|
3776
|
+
" - MONEY IS TAKEN ONLY ON THE PAYMENT PROVIDER'S PAGE OR THE OPERATING",
|
|
3777
|
+
" SYSTEM'S PURCHASE SHEET, which a player reaches through the parent's",
|
|
3778
|
+
" own chrome. Nothing a frame sends carries a price or a card field,",
|
|
3779
|
+
" and no payment starts without the player's own press there.",
|
|
3775
3780
|
"",
|
|
3776
3781
|
" THE FRAME CHOOSES THE AMOUNT. Not the unit price - but `targetId` x",
|
|
3777
3782
|
" `currencySlug` x `quantity` is nevertheless the total, up to ten times the",
|
|
@@ -4834,6 +4839,731 @@
|
|
|
4834
4839
|
}
|
|
4835
4840
|
},
|
|
4836
4841
|
"rateLimit": { "perMinute": 30 }
|
|
4842
|
+
},
|
|
4843
|
+
{
|
|
4844
|
+
"type": "gem.requestAppLinks",
|
|
4845
|
+
"direction": "frameToParent",
|
|
4846
|
+
"transport": "port",
|
|
4847
|
+
"$comment": [
|
|
4848
|
+
"Ask which app listings the player can be sent to from here: the Gem",
|
|
4849
|
+
"Arcade app's, and this game's own. Gated by the `appLinks` capability.",
|
|
4850
|
+
"Answered by `gem.appLinksResult`.",
|
|
4851
|
+
"",
|
|
4852
|
+
"A READ, NOT AN ACT. It renders nothing and opens nothing, so it needs",
|
|
4853
|
+
"no user activation and takes no part in the acting-message mutex. A",
|
|
4854
|
+
"game asks so it can draw the right store badge, or no badge at all,",
|
|
4855
|
+
"before the player presses anything; the press is `gem.requestOpenApp`.",
|
|
4856
|
+
"",
|
|
4857
|
+
"NOTHING BUT THE CORRELATOR IS SENT. The parent knows which game it",
|
|
4858
|
+
"framed and which platform the player is on; a game id or a platform",
|
|
4859
|
+
"the frame supplied would be one the frame chose."
|
|
4860
|
+
],
|
|
4861
|
+
"fields": {
|
|
4862
|
+
"requestId": {
|
|
4863
|
+
"kind": "string",
|
|
4864
|
+
"maxLength": 64,
|
|
4865
|
+
"$comment": "Opaque and frame-chosen. Echo-only, exactly as on gem.requestPurchase: not unique across tabs or reloads, never a key, and not safe to write into a log line unescaped."
|
|
4866
|
+
}
|
|
4867
|
+
},
|
|
4868
|
+
"rateLimit": { "perMinute": 12 }
|
|
4869
|
+
},
|
|
4870
|
+
{
|
|
4871
|
+
"type": "gem.appLinksResult",
|
|
4872
|
+
"direction": "parentToFrame",
|
|
4873
|
+
"transport": "port",
|
|
4874
|
+
"$comment": [
|
|
4875
|
+
"The answer to one gem.requestAppLinks. Gated by the `appLinks`",
|
|
4876
|
+
"capability.",
|
|
4877
|
+
"",
|
|
4878
|
+
"WHICH STORE, NEVER A URL. `arcade` is present when `gem.requestOpenApp`",
|
|
4879
|
+
"with target `arcade` would open a store listing from here; `game` when",
|
|
4880
|
+
"target `game` would. Each says which store it is, so a game can draw the",
|
|
4881
|
+
"matching badge. The link itself is built by the parent from a fixed",
|
|
4882
|
+
"template and never crosses: a URL the frame could read is a URL the",
|
|
4883
|
+
"frame could be tricked into trusting, and a URL the frame could send",
|
|
4884
|
+
"back is a redirect it chose.",
|
|
4885
|
+
"",
|
|
4886
|
+
"ABSENT MEANS NO LISTING HERE: a desktop browser, a platform without a",
|
|
4887
|
+
"listing yet, or the player already inside that app (the outcome is",
|
|
4888
|
+
"still `ok`; `reason` may say `already_in_app`). A game hides the button",
|
|
4889
|
+
"for an absent entry.",
|
|
4890
|
+
"",
|
|
4891
|
+
"OUTCOMES, closed at three as gem.purchaseResult's are:",
|
|
4892
|
+
" ok - the answer is in `arcade` and `game`, either or both",
|
|
4893
|
+
" of which may be absent.",
|
|
4894
|
+
" declined - the parent chose not to answer.",
|
|
4895
|
+
" unavailable - no reliable answer. A game hides both buttons.",
|
|
4896
|
+
"Everything finer is `reason`, an open code."
|
|
4897
|
+
],
|
|
4898
|
+
"fields": {
|
|
4899
|
+
"requestId": {
|
|
4900
|
+
"kind": "string",
|
|
4901
|
+
"maxLength": 64,
|
|
4902
|
+
"$comment": "Echoes the gem.requestAppLinks that asked. Echo-only."
|
|
4903
|
+
},
|
|
4904
|
+
"outcome": {
|
|
4905
|
+
"kind": "enum",
|
|
4906
|
+
"values": ["ok", "declined", "unavailable"],
|
|
4907
|
+
"$comment": "THREE VALUES, CLOSED for gem.purchaseResult's reason: `check-schema-freeze` treats a new enum value as breaking, so the vocabulary that may grow lives in `reason`."
|
|
4908
|
+
},
|
|
4909
|
+
"reason": {
|
|
4910
|
+
"kind": "string",
|
|
4911
|
+
"maxLength": 64,
|
|
4912
|
+
"format": "code",
|
|
4913
|
+
"optional": true,
|
|
4914
|
+
"$comment": "WHY, as an open machine code (`^[a-z_]{1,64}$`): `already_in_app`, `store_off`, `error` and whatever the parent adds later. Normalized to the code grammar before it is sent, as gem.storePurchaseResult's. A receiver maps a code it does not know to its own fallback and must not fail on one. Advisory: a game branches on `outcome` and on which entries are present, never on this."
|
|
4915
|
+
},
|
|
4916
|
+
"arcade": {
|
|
4917
|
+
"kind": "object",
|
|
4918
|
+
"optional": true,
|
|
4919
|
+
"$comment": "Present when the Gem Arcade app has a store listing the parent can open from here. Absent inside the Gem Arcade app itself.",
|
|
4920
|
+
"fields": {
|
|
4921
|
+
"store": {
|
|
4922
|
+
"kind": "enum",
|
|
4923
|
+
"values": ["app_store", "google_play"],
|
|
4924
|
+
"$comment": "Which store the listing is in, for the badge. A store that is not one of these is a new optional field behind a capability of its own, never a third value here: an N-1 frame would refuse the whole message."
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
},
|
|
4928
|
+
"game": {
|
|
4929
|
+
"kind": "object",
|
|
4930
|
+
"optional": true,
|
|
4931
|
+
"$comment": "Present when THIS game has its own app recorded for the player's platform (staff-recorded, a store id per platform, never a URL). Absent when it has none, or inside that app's own wrapper.",
|
|
4932
|
+
"fields": {
|
|
4933
|
+
"store": {
|
|
4934
|
+
"kind": "enum",
|
|
4935
|
+
"values": ["app_store", "google_play"],
|
|
4936
|
+
"$comment": "As `arcade.store`."
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
},
|
|
4941
|
+
"rateLimit": { "perMinute": 30 }
|
|
4942
|
+
},
|
|
4943
|
+
{
|
|
4944
|
+
"type": "gem.requestOpenApp",
|
|
4945
|
+
"direction": "frameToParent",
|
|
4946
|
+
"transport": "port",
|
|
4947
|
+
"$comment": [
|
|
4948
|
+
"Ask the parent to open a store listing: the Gem Arcade app's",
|
|
4949
|
+
"(`target: arcade`) or this game's own (`target: game`). Gated by the",
|
|
4950
|
+
"`appLinks` capability. Answered by `gem.openAppResult`.",
|
|
4951
|
+
"",
|
|
4952
|
+
"AN ACTING MESSAGE. It leaves the game, so the parent holds it to the",
|
|
4953
|
+
"rules every acting message shares (gem.requestOpenGame and",
|
|
4954
|
+
"gem.requestStorePurchase state them too):",
|
|
4955
|
+
"",
|
|
4956
|
+
" A PERSON MUST HAVE PRESSED. Read `navigator.userActivation.isActive`",
|
|
4957
|
+
" synchronously on receipt, before any `await`, or accept a controller",
|
|
4958
|
+
" press the PARENT PAGE observed itself. A pad press the frame reports is",
|
|
4959
|
+
" never trusted. A missing `userActivation` API FAILS CLOSED. Without a",
|
|
4960
|
+
" press, answer `declined` / `busy` and show nothing.",
|
|
4961
|
+
"",
|
|
4962
|
+
" ONE AT A TIME, ACROSS EVERY ACTING MESSAGE. While this, an open-game,",
|
|
4963
|
+
" a store purchase, a gem.requestPurchase, a share or a friend challenge",
|
|
4964
|
+
" is open, answer `declined` / `busy` and show nothing.",
|
|
4965
|
+
"",
|
|
4966
|
+
" BOTH BEFORE RESOLVING ANYTHING. Activation and the mutex are checked",
|
|
4967
|
+
" before the target is looked up, so a refusal says nothing about it.",
|
|
4968
|
+
"",
|
|
4969
|
+
" BOUND TO THE FRAME SESSION. An answer for a frame that has since gone",
|
|
4970
|
+
" is dropped, never delivered to whatever game is framed next.",
|
|
4971
|
+
"",
|
|
4972
|
+
"THE PARENT BUILDS THE URL from a fixed template and the store id it",
|
|
4973
|
+
"holds (never from anything in this message), rechecks it against the",
|
|
4974
|
+
"template's host and path, and opens it with `noopener,noreferrer` or the",
|
|
4975
|
+
"app shell's allowlisted external-link path. Where it cannot open one (a",
|
|
4976
|
+
"TV, a refused pop-up) it shows its own sheet with a QR code and the",
|
|
4977
|
+
"short URL as text, and answers `ok` / `handed_off`.",
|
|
4978
|
+
"",
|
|
4979
|
+
"ANSWERED AT THE HAND-OFF, never later: as the listing's window or the",
|
|
4980
|
+
"store app opens, or as the QR sheet goes up. The answer never waits on",
|
|
4981
|
+
"the player closing the sheet, so it always arrives well inside the",
|
|
4982
|
+
"game's two-minute wait. Closing the sheet is not reported; while it is",
|
|
4983
|
+
"up, it still holds the one acting slot, and a second acting ask is",
|
|
4984
|
+
"`declined` / `busy`."
|
|
4985
|
+
],
|
|
4986
|
+
"fields": {
|
|
4987
|
+
"requestId": {
|
|
4988
|
+
"kind": "string",
|
|
4989
|
+
"maxLength": 64,
|
|
4990
|
+
"$comment": "Opaque and frame-chosen. Echo-only."
|
|
4991
|
+
},
|
|
4992
|
+
"target": {
|
|
4993
|
+
"kind": "enum",
|
|
4994
|
+
"values": ["arcade", "game"],
|
|
4995
|
+
"$comment": "Which listing. A closed set, so the validator refuses anything else at the boundary. The parent resolves the listing itself; nothing here names a store, a platform or a URL."
|
|
4996
|
+
}
|
|
4997
|
+
},
|
|
4998
|
+
"rateLimit": {
|
|
4999
|
+
"perMinute": 6,
|
|
5000
|
+
"$comment": "gem.requestShare's limit: it bounds a BUGGY game. NOT a security control: a hostile game posts on the port it holds rather than calling the SDK. What bounds the player's experience is the parent's activation check and its one-at-a-time rule."
|
|
5001
|
+
}
|
|
5002
|
+
},
|
|
5003
|
+
{
|
|
5004
|
+
"type": "gem.openAppResult",
|
|
5005
|
+
"direction": "parentToFrame",
|
|
5006
|
+
"transport": "port",
|
|
5007
|
+
"$comment": [
|
|
5008
|
+
"The answer to one gem.requestOpenApp. Gated by the `appLinks`",
|
|
5009
|
+
"capability.",
|
|
5010
|
+
"",
|
|
5011
|
+
" ok - the listing opened, in a new tab or the store app, or",
|
|
5012
|
+
" (`reason: handed_off`) the parent showed its own QR",
|
|
5013
|
+
" sheet. The game carries on; nothing left the page.",
|
|
5014
|
+
" declined - nothing was opened: no press, or another acting",
|
|
5015
|
+
" message open (`busy`).",
|
|
5016
|
+
" unavailable - there is no such listing here (`already_in_app`,",
|
|
5017
|
+
" `not_found`), or no reliable answer.",
|
|
5018
|
+
"",
|
|
5019
|
+
"NEVER AUTO-RETRIED. Only a fresh press may ask again."
|
|
5020
|
+
],
|
|
5021
|
+
"fields": {
|
|
5022
|
+
"requestId": {
|
|
5023
|
+
"kind": "string",
|
|
5024
|
+
"maxLength": 64,
|
|
5025
|
+
"$comment": "Echoes the gem.requestOpenApp that asked. Echo-only."
|
|
5026
|
+
},
|
|
5027
|
+
"outcome": {
|
|
5028
|
+
"kind": "enum",
|
|
5029
|
+
"values": ["ok", "declined", "unavailable"],
|
|
5030
|
+
"$comment": "Closed at three, as gem.appLinksResult's."
|
|
5031
|
+
},
|
|
5032
|
+
"reason": {
|
|
5033
|
+
"kind": "string",
|
|
5034
|
+
"maxLength": 64,
|
|
5035
|
+
"format": "code",
|
|
5036
|
+
"optional": true,
|
|
5037
|
+
"$comment": "An open code, on any outcome: `handed_off` with `ok`; `busy` with `declined`; `already_in_app`, `not_found` or `error` with `unavailable`. Normalized to the code grammar before it is sent, as gem.storePurchaseResult's. A receiver must not fail on a code it does not know."
|
|
5038
|
+
}
|
|
5039
|
+
},
|
|
5040
|
+
"rateLimit": { "perMinute": 30 }
|
|
5041
|
+
},
|
|
5042
|
+
{
|
|
5043
|
+
"type": "gem.requestOpenGame",
|
|
5044
|
+
"direction": "frameToParent",
|
|
5045
|
+
"transport": "port",
|
|
5046
|
+
"$comment": [
|
|
5047
|
+
"Ask the parent to open ANOTHER game in place of this one. Gated by the",
|
|
5048
|
+
"`openGame` capability. Answered by `gem.openGameResult`.",
|
|
5049
|
+
"",
|
|
5050
|
+
"AN ACTING MESSAGE, under every rule gem.requestOpenApp states: a press",
|
|
5051
|
+
"the parent can see (a missing `userActivation` API fails closed), one",
|
|
5052
|
+
"acting message at a time, both checked before `ref` is resolved, and the",
|
|
5053
|
+
"answer bound to this frame session.",
|
|
5054
|
+
"",
|
|
5055
|
+
"WHAT THE PARENT DOES WITH IT. It resolves `ref` with the platform for",
|
|
5056
|
+
"the session's channel, exactly as its own `/play/<ref>` route would, and",
|
|
5057
|
+
"answers before it moves:",
|
|
5058
|
+
"",
|
|
5059
|
+
" - unknown, hidden, not on this channel, the wrong engine or the wrong",
|
|
5060
|
+
" form factor: `unavailable` / `not_found`, with no difference between",
|
|
5061
|
+
" \"does not exist\" and \"you may not see it\";",
|
|
5062
|
+
" - this same game: `declined` / `same_game`;",
|
|
5063
|
+
" - otherwise `ok`, then its own non-interactive \"Opening <game>\" card",
|
|
5064
|
+
" over this game's hidden frame, then `gem.willTerminate` with the",
|
|
5065
|
+
" ordinary save grace, then the other game. Back returns the player",
|
|
5066
|
+
" to this game's page, never relaunching it by itself.",
|
|
5067
|
+
"",
|
|
5068
|
+
"NO CONFIRMATION SHEET, by owner decision: games are developed in-house",
|
|
5069
|
+
"and trusted, and the parent's card still shows that the Arcade moved the",
|
|
5070
|
+
"player. If third-party games ever ship, this is revisited first."
|
|
5071
|
+
],
|
|
5072
|
+
"fields": {
|
|
5073
|
+
"requestId": {
|
|
5074
|
+
"kind": "string",
|
|
5075
|
+
"maxLength": 64,
|
|
5076
|
+
"$comment": "Opaque and frame-chosen. Echo-only."
|
|
5077
|
+
},
|
|
5078
|
+
"ref": {
|
|
5079
|
+
"kind": "string",
|
|
5080
|
+
"maxLength": 40,
|
|
5081
|
+
"format": "gameRef",
|
|
5082
|
+
"$comment": "The game to open: its id (a UUID) or its web address (a URL slug: 3 to 40 lower-case letters, digits and single hyphens, starting and ending with a letter or digit), exactly what the parent's `/play/<ref>` route accepts. The `gameRef` format holds it to that grammar at both ends, so nothing else reaches a lookup. Whether it names a game the player may open is the platform's to judge."
|
|
5083
|
+
}
|
|
5084
|
+
},
|
|
5085
|
+
"rateLimit": {
|
|
5086
|
+
"perMinute": 6,
|
|
5087
|
+
"$comment": "gem.requestOpenApp's limit and its argument: it bounds a BUGGY game, not a hostile one."
|
|
5088
|
+
}
|
|
5089
|
+
},
|
|
5090
|
+
{
|
|
5091
|
+
"type": "gem.openGameResult",
|
|
5092
|
+
"direction": "parentToFrame",
|
|
5093
|
+
"transport": "port",
|
|
5094
|
+
"$comment": [
|
|
5095
|
+
"The answer to one gem.requestOpenGame. Gated by the `openGame`",
|
|
5096
|
+
"capability.",
|
|
5097
|
+
"",
|
|
5098
|
+
" ok - the other game is opening. `gem.willTerminate` follows;",
|
|
5099
|
+
" save then, not after this.",
|
|
5100
|
+
" declined - nothing moved: no press or another acting message open",
|
|
5101
|
+
" (`busy`), or `ref` names this game (`same_game`).",
|
|
5102
|
+
" unavailable - no game the player can open here (`not_found`), or no",
|
|
5103
|
+
" reliable answer.",
|
|
5104
|
+
"",
|
|
5105
|
+
"NEVER AUTO-RETRIED. Only a fresh press may ask again."
|
|
5106
|
+
],
|
|
5107
|
+
"fields": {
|
|
5108
|
+
"requestId": {
|
|
5109
|
+
"kind": "string",
|
|
5110
|
+
"maxLength": 64,
|
|
5111
|
+
"$comment": "Echoes the gem.requestOpenGame that asked. Echo-only."
|
|
5112
|
+
},
|
|
5113
|
+
"outcome": {
|
|
5114
|
+
"kind": "enum",
|
|
5115
|
+
"values": ["ok", "declined", "unavailable"],
|
|
5116
|
+
"$comment": "Closed at three, as gem.appLinksResult's."
|
|
5117
|
+
},
|
|
5118
|
+
"reason": {
|
|
5119
|
+
"kind": "string",
|
|
5120
|
+
"maxLength": 64,
|
|
5121
|
+
"format": "code",
|
|
5122
|
+
"optional": true,
|
|
5123
|
+
"$comment": "An open code: `busy` or `same_game` with `declined`; `not_found` or `error` with `unavailable`. Normalized to the code grammar before it is sent, as gem.storePurchaseResult's. A receiver must not fail on a code it does not know."
|
|
5124
|
+
}
|
|
5125
|
+
},
|
|
5126
|
+
"rateLimit": { "perMinute": 30 }
|
|
5127
|
+
},
|
|
5128
|
+
{
|
|
5129
|
+
"type": "gem.requestSimilarGames",
|
|
5130
|
+
"direction": "frameToParent",
|
|
5131
|
+
"transport": "port",
|
|
5132
|
+
"$comment": [
|
|
5133
|
+
"Ask for games like this one, as the Arcade home page shows them. Gated",
|
|
5134
|
+
"by the `openGame` capability, because a list of games is useful only",
|
|
5135
|
+
"beside the message that opens one. Answered by `gem.similarGamesResult`.",
|
|
5136
|
+
"",
|
|
5137
|
+
"A READ, NOT AN ACT: no activation, no mutex. The parent asks the",
|
|
5138
|
+
"platform's similar-games read for the game it framed (never a game the",
|
|
5139
|
+
"frame names), on the release channel, browser games only, never",
|
|
5140
|
+
"personalized."
|
|
5141
|
+
],
|
|
5142
|
+
"fields": {
|
|
5143
|
+
"requestId": {
|
|
5144
|
+
"kind": "string",
|
|
5145
|
+
"maxLength": 64,
|
|
5146
|
+
"$comment": "Opaque and frame-chosen. Echo-only."
|
|
5147
|
+
},
|
|
5148
|
+
"limit": {
|
|
5149
|
+
"kind": "int",
|
|
5150
|
+
"min": 1,
|
|
5151
|
+
"max": 24,
|
|
5152
|
+
"optional": true,
|
|
5153
|
+
"$comment": "How many games at most. Absent means 12. The bound matches the platform's own, so a limit the SDK accepts is one the read honours."
|
|
5154
|
+
}
|
|
5155
|
+
},
|
|
5156
|
+
"rateLimit": { "perMinute": 12 }
|
|
5157
|
+
},
|
|
5158
|
+
{
|
|
5159
|
+
"type": "gem.similarGamesResult",
|
|
5160
|
+
"direction": "parentToFrame",
|
|
5161
|
+
"transport": "port",
|
|
5162
|
+
"maxBytes": 524288,
|
|
5163
|
+
"$comment": [
|
|
5164
|
+
"The answer to one gem.requestSimilarGames. Gated by the `openGame`",
|
|
5165
|
+
"capability.",
|
|
5166
|
+
"",
|
|
5167
|
+
"`games` holds up to 24 cards, best first, and is empty unless the",
|
|
5168
|
+
"outcome is `ok` (an `ok` may be empty too: hide the section). Each card",
|
|
5169
|
+
"is what an Arcade home tile shows. The parent leaves `previewVideoUrl`",
|
|
5170
|
+
"out under reduced motion, data saver or a phone form factor, which the",
|
|
5171
|
+
"frame cannot see for itself.",
|
|
5172
|
+
"",
|
|
5173
|
+
"ANOTHER DEVELOPER'S TEXT, AND MARKED SO. A card's name, tagline, short",
|
|
5174
|
+
"description and tags are written by the other game's team, so they sit",
|
|
5175
|
+
"under `untrusted`, as a player's name does on gem.player, and the SDK",
|
|
5176
|
+
"hands them to the game as `PlayerText`. Render them as text, never as",
|
|
5177
|
+
"markup. Outside `untrusted` a card carries only platform values: ids, a",
|
|
5178
|
+
"slug that has passed the platform's own grammar, and platform media",
|
|
5179
|
+
"URLs.",
|
|
5180
|
+
"",
|
|
5181
|
+
"ITS OWN BYTE CAP, `maxBytes`, above the protocol's `maxMessageBytes`.",
|
|
5182
|
+
"Twenty-four cards of five media URLs each (signed URLs run to several",
|
|
5183
|
+
"hundred characters) do not fit the protocol's 64 KB at the walk's three",
|
|
5184
|
+
"bytes a character. It is parent-to-frame only, so the receiver is the",
|
|
5185
|
+
"game, and a frame that predates it never advertised `openGame` and is",
|
|
5186
|
+
"never sent one."
|
|
5187
|
+
],
|
|
5188
|
+
"fields": {
|
|
5189
|
+
"requestId": {
|
|
5190
|
+
"kind": "string",
|
|
5191
|
+
"maxLength": 64,
|
|
5192
|
+
"$comment": "Echoes the gem.requestSimilarGames that asked. Echo-only."
|
|
5193
|
+
},
|
|
5194
|
+
"outcome": {
|
|
5195
|
+
"kind": "enum",
|
|
5196
|
+
"values": ["ok", "declined", "unavailable"],
|
|
5197
|
+
"$comment": "Closed at three, as gem.appLinksResult's."
|
|
5198
|
+
},
|
|
5199
|
+
"reason": {
|
|
5200
|
+
"kind": "string",
|
|
5201
|
+
"maxLength": 64,
|
|
5202
|
+
"format": "code",
|
|
5203
|
+
"optional": true,
|
|
5204
|
+
"$comment": "An open code, sent with `declined` or `unavailable`. Normalized to the code grammar before it is sent, as gem.storePurchaseResult's. A receiver must not fail on a code it does not know."
|
|
5205
|
+
},
|
|
5206
|
+
"games": {
|
|
5207
|
+
"kind": "array",
|
|
5208
|
+
"maxItems": 24,
|
|
5209
|
+
"$comment": "The cards, best first. Empty unless `ok`.",
|
|
5210
|
+
"items": {
|
|
5211
|
+
"kind": "object",
|
|
5212
|
+
"fields": {
|
|
5213
|
+
"gameId": {
|
|
5214
|
+
"kind": "string",
|
|
5215
|
+
"maxLength": 36,
|
|
5216
|
+
"format": "gameRef",
|
|
5217
|
+
"$comment": "The game's id, a UUID: what gem.requestOpenGame's `ref` takes."
|
|
5218
|
+
},
|
|
5219
|
+
"slug": {
|
|
5220
|
+
"kind": "string",
|
|
5221
|
+
"maxLength": 40,
|
|
5222
|
+
"format": "gameRef",
|
|
5223
|
+
"optional": true,
|
|
5224
|
+
"$comment": "The game's web address, when it has one. Absent for a game with none."
|
|
5225
|
+
},
|
|
5226
|
+
"iconUrl": {
|
|
5227
|
+
"kind": "string",
|
|
5228
|
+
"maxLength": 1024,
|
|
5229
|
+
"format": "url",
|
|
5230
|
+
"$comment": "The game's icon, on the platform's media host. Structurally validated only; see gem.player.avatarIconUrl for what the format does not do."
|
|
5231
|
+
},
|
|
5232
|
+
"capsuleLandscapeUrl": {
|
|
5233
|
+
"kind": "string",
|
|
5234
|
+
"maxLength": 1024,
|
|
5235
|
+
"format": "url",
|
|
5236
|
+
"$comment": "The landscape tile art, as the home page draws it."
|
|
5237
|
+
},
|
|
5238
|
+
"capsulePortraitUrl": {
|
|
5239
|
+
"kind": "string",
|
|
5240
|
+
"maxLength": 1024,
|
|
5241
|
+
"format": "url",
|
|
5242
|
+
"$comment": "The portrait tile art."
|
|
5243
|
+
},
|
|
5244
|
+
"previewVideoUrl": {
|
|
5245
|
+
"kind": "string",
|
|
5246
|
+
"maxLength": 1024,
|
|
5247
|
+
"format": "url",
|
|
5248
|
+
"optional": true,
|
|
5249
|
+
"$comment": "The tile's hover clip. Absent when the game has none, and whenever the parent judges a clip unwelcome: reduced motion, data saver, a phone."
|
|
5250
|
+
},
|
|
5251
|
+
"previewPosterUrl": {
|
|
5252
|
+
"kind": "string",
|
|
5253
|
+
"maxLength": 1024,
|
|
5254
|
+
"format": "url",
|
|
5255
|
+
"optional": true,
|
|
5256
|
+
"$comment": "The still shown before and instead of the clip."
|
|
5257
|
+
},
|
|
5258
|
+
"untrusted": {
|
|
5259
|
+
"kind": "object",
|
|
5260
|
+
"$comment": "Text the OTHER game's team wrote. Never markup; see the message comment.",
|
|
5261
|
+
"fields": {
|
|
5262
|
+
"name": {
|
|
5263
|
+
"kind": "string",
|
|
5264
|
+
"maxLength": 64,
|
|
5265
|
+
"$comment": "The game's public name."
|
|
5266
|
+
},
|
|
5267
|
+
"tagline": {
|
|
5268
|
+
"kind": "string",
|
|
5269
|
+
"maxLength": 64,
|
|
5270
|
+
"$comment": "One line under the name. May be empty."
|
|
5271
|
+
},
|
|
5272
|
+
"shortDescription": {
|
|
5273
|
+
"kind": "string",
|
|
5274
|
+
"maxLength": 320,
|
|
5275
|
+
"$comment": "A sentence or two. May be empty."
|
|
5276
|
+
},
|
|
5277
|
+
"tags": {
|
|
5278
|
+
"kind": "array",
|
|
5279
|
+
"maxItems": 10,
|
|
5280
|
+
"items": { "kind": "string", "maxLength": 32 },
|
|
5281
|
+
"$comment": "The game's store tags, as shown on the tile."
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
},
|
|
5289
|
+
"rateLimit": { "perMinute": 30 }
|
|
5290
|
+
},
|
|
5291
|
+
{
|
|
5292
|
+
"type": "gem.requestStoreOffers",
|
|
5293
|
+
"direction": "frameToParent",
|
|
5294
|
+
"transport": "port",
|
|
5295
|
+
"$comment": [
|
|
5296
|
+
"Ask what the Arcade Store sells to this player, here, for real money:",
|
|
5297
|
+
"Gem packs, Boosts, passes and bundles. Gated by the `store` capability.",
|
|
5298
|
+
"Answered by `gem.storeOffersResult`.",
|
|
5299
|
+
"",
|
|
5300
|
+
"WHY THE PARENT ANSWERS IT. The price a player pays depends on the",
|
|
5301
|
+
"payment rail the parent is on - the website's checkout or the app",
|
|
5302
|
+
"store's own price - which only the parent knows, and the platform's",
|
|
5303
|
+
"listing reads need the account session the frame never holds. So the",
|
|
5304
|
+
"parent reads the same listing its own Arcade Store shows, minus",
|
|
5305
|
+
"per-player offers (the daily deal, a first-purchase special), with",
|
|
5306
|
+
"names in the same localization the store shows and prices formatted by",
|
|
5307
|
+
"the active rail. A SKU the app store does not return is left out.",
|
|
5308
|
+
"",
|
|
5309
|
+
"A READ, NOT AN ACT: no activation, no mutex. Per-session state comes",
|
|
5310
|
+
"back as a reason: `store_off` where the store is withheld (an age or",
|
|
5311
|
+
"region policy, a TV, an in-app browser that takes no payment) and",
|
|
5312
|
+
"`sign_in_required` for a guest, who may then be offered",
|
|
5313
|
+
"gem.requestSignIn."
|
|
5314
|
+
],
|
|
5315
|
+
"fields": {
|
|
5316
|
+
"requestId": {
|
|
5317
|
+
"kind": "string",
|
|
5318
|
+
"maxLength": 64,
|
|
5319
|
+
"$comment": "Opaque and frame-chosen. Echo-only."
|
|
5320
|
+
}
|
|
5321
|
+
},
|
|
5322
|
+
"rateLimit": { "perMinute": 12 }
|
|
5323
|
+
},
|
|
5324
|
+
{
|
|
5325
|
+
"type": "gem.storeOffersResult",
|
|
5326
|
+
"direction": "parentToFrame",
|
|
5327
|
+
"transport": "port",
|
|
5328
|
+
"maxBytes": 262144,
|
|
5329
|
+
"$comment": [
|
|
5330
|
+
"The answer to one gem.requestStoreOffers. Gated by the `store`",
|
|
5331
|
+
"capability.",
|
|
5332
|
+
"",
|
|
5333
|
+
"`offers` is empty unless the outcome is `ok`. Each is one SKU the",
|
|
5334
|
+
"player could buy here right now through gem.requestStorePurchase.",
|
|
5335
|
+
"",
|
|
5336
|
+
"WHAT IS NOT HERE, deliberately. No numeric price: `priceDisplay` is the",
|
|
5337
|
+
"rail's own formatted string, and a game shows it exactly as given,",
|
|
5338
|
+
"never parsing it or computing a discount from it. No balance. And no",
|
|
5339
|
+
"per-offer availability flag: one would reveal a recent purchase through",
|
|
5340
|
+
"its cooldown, so the parent's sheet explains a cooldown instead.",
|
|
5341
|
+
"",
|
|
5342
|
+
"THE NAMES ARE THE PLATFORM'S, not a game developer's: the Arcade",
|
|
5343
|
+
"Store's own catalog copy, which is why `name` and `description` sit",
|
|
5344
|
+
"outside an `untrusted` object.",
|
|
5345
|
+
"",
|
|
5346
|
+
"ITS OWN BYTE CAP, `maxBytes`, on gem.similarGamesResult's argument: a",
|
|
5347
|
+
"full catalog of offers with art does not fit the protocol's 64 KB at",
|
|
5348
|
+
"the walk's arithmetic, and the receiver is the game."
|
|
5349
|
+
],
|
|
5350
|
+
"fields": {
|
|
5351
|
+
"requestId": {
|
|
5352
|
+
"kind": "string",
|
|
5353
|
+
"maxLength": 64,
|
|
5354
|
+
"$comment": "Echoes the gem.requestStoreOffers that asked. Echo-only."
|
|
5355
|
+
},
|
|
5356
|
+
"outcome": {
|
|
5357
|
+
"kind": "enum",
|
|
5358
|
+
"values": ["ok", "declined", "unavailable"],
|
|
5359
|
+
"$comment": "Closed at three, as gem.appLinksResult's."
|
|
5360
|
+
},
|
|
5361
|
+
"reason": {
|
|
5362
|
+
"kind": "string",
|
|
5363
|
+
"maxLength": 64,
|
|
5364
|
+
"format": "code",
|
|
5365
|
+
"optional": true,
|
|
5366
|
+
"$comment": "An open code, sent with `declined` or `unavailable`: `store_off`, `sign_in_required`, `error`. Normalized to the code grammar before it is sent, as gem.storePurchaseResult's. A receiver must not fail on a code it does not know."
|
|
5367
|
+
},
|
|
5368
|
+
"offers": {
|
|
5369
|
+
"kind": "array",
|
|
5370
|
+
"maxItems": 32,
|
|
5371
|
+
"$comment": "What the store sells here, in the store's own order. Empty unless `ok`.",
|
|
5372
|
+
"items": {
|
|
5373
|
+
"kind": "object",
|
|
5374
|
+
"fields": {
|
|
5375
|
+
"skuId": {
|
|
5376
|
+
"kind": "string",
|
|
5377
|
+
"maxLength": 64,
|
|
5378
|
+
"$comment": "The platform's SKU id: what gem.requestStorePurchase's `skuId` takes."
|
|
5379
|
+
},
|
|
5380
|
+
"kind": {
|
|
5381
|
+
"kind": "enum",
|
|
5382
|
+
"values": ["gem_pack", "boost", "pass", "bundle"],
|
|
5383
|
+
"$comment": "What sort of thing it is. A new sort of SKU is left out of this list by the parent, never sent as a fifth value: an N-1 frame would refuse the whole message."
|
|
5384
|
+
},
|
|
5385
|
+
"name": {
|
|
5386
|
+
"kind": "string",
|
|
5387
|
+
"maxLength": 80,
|
|
5388
|
+
"$comment": "The store's own name for it, localized as the store shows it."
|
|
5389
|
+
},
|
|
5390
|
+
"description": {
|
|
5391
|
+
"kind": "string",
|
|
5392
|
+
"maxLength": 400,
|
|
5393
|
+
"$comment": "What the player gets, in the store's words. May be empty."
|
|
5394
|
+
},
|
|
5395
|
+
"imageUrl": {
|
|
5396
|
+
"kind": "string",
|
|
5397
|
+
"maxLength": 1024,
|
|
5398
|
+
"format": "url",
|
|
5399
|
+
"optional": true,
|
|
5400
|
+
"$comment": "The SKU's art on the platform's media host. Absent when it has none; the offer is still listed."
|
|
5401
|
+
},
|
|
5402
|
+
"priceDisplay": {
|
|
5403
|
+
"kind": "string",
|
|
5404
|
+
"maxLength": 32,
|
|
5405
|
+
"$comment": "The price, formatted by the active payment rail in the player's storefront currency. Show it exactly as given."
|
|
5406
|
+
},
|
|
5407
|
+
"grant": {
|
|
5408
|
+
"kind": "object",
|
|
5409
|
+
"optional": true,
|
|
5410
|
+
"$comment": "For a Gem pack: the currency and amount it grants. Absent for every other kind.",
|
|
5411
|
+
"fields": {
|
|
5412
|
+
"currency": {
|
|
5413
|
+
"kind": "string",
|
|
5414
|
+
"maxLength": 32,
|
|
5415
|
+
"$comment": "The platform currency's exact slug, e.g. GEMS."
|
|
5416
|
+
},
|
|
5417
|
+
"amount": {
|
|
5418
|
+
"kind": "int",
|
|
5419
|
+
"min": 1,
|
|
5420
|
+
"max": 1000000000,
|
|
5421
|
+
"$comment": "How much of it."
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
},
|
|
5429
|
+
"rateLimit": { "perMinute": 30 }
|
|
5430
|
+
},
|
|
5431
|
+
{
|
|
5432
|
+
"type": "gem.requestStorePurchase",
|
|
5433
|
+
"direction": "frameToParent",
|
|
5434
|
+
"transport": "port",
|
|
5435
|
+
"$comment": [
|
|
5436
|
+
"Ask the parent to open the Arcade Store's purchase sheet for ONE SKU it",
|
|
5437
|
+
"listed in gem.storeOffersResult - real money, not Gems. Gated by the",
|
|
5438
|
+
"`store` capability. Answered by `gem.storePurchaseResult`.",
|
|
5439
|
+
"",
|
|
5440
|
+
"THE GAME ASKS; THE PLATFORM SELLS. Nothing in this message is a price,",
|
|
5441
|
+
"a card field, a URL or a token. The parent renders its own sheet from",
|
|
5442
|
+
"platform records, takes money only on the payment provider's page or",
|
|
5443
|
+
"the operating system's purchase sheet, and tells the game the outcome.",
|
|
5444
|
+
"",
|
|
5445
|
+
"AN ACTING MESSAGE, under every rule gem.requestOpenApp states: a press",
|
|
5446
|
+
"the parent can see (a missing `userActivation` API fails closed), one",
|
|
5447
|
+
"acting message at a time across all of them, both checked before",
|
|
5448
|
+
"`skuId` is resolved, and the answer bound to this frame session. And:",
|
|
5449
|
+
"",
|
|
5450
|
+
" ONLY A SKU IT WOULD LIST. `skuId` must be one gem.storeOffersResult",
|
|
5451
|
+
" would list to this player right now; anything else is `unavailable`",
|
|
5452
|
+
" with nothing shown.",
|
|
5453
|
+
"",
|
|
5454
|
+
" THE STORE'S OWN SHEET. Over the paused game, hidden not dimmed, with",
|
|
5455
|
+
" the parent's bar saying the Arcade Store is open, game chrome frozen,",
|
|
5456
|
+
" and focus never landing on a money control. \"Not now\" has the",
|
|
5457
|
+
" default focus. Buy arms visibly for a moment and then takes a FRESH",
|
|
5458
|
+
" press; input during arming restarts it.",
|
|
5459
|
+
"",
|
|
5460
|
+
" BACK OFF AFTER A DISMISSAL. `cancelled` is disclosed, so after the",
|
|
5461
|
+
" player dismisses a sheet a game started, further asks from that game",
|
|
5462
|
+
" are `declined` / `busy`, unrendered, for an interval that grows with",
|
|
5463
|
+
" each dismissal (the Arcade: 30 s, 2 min, 10 min, then the rest of the",
|
|
5464
|
+
" session).",
|
|
5465
|
+
"",
|
|
5466
|
+
" RESUME, THEN ANSWER. The result is sent after the game has been told",
|
|
5467
|
+
" it is resumed, so the game's own celebration is visible.",
|
|
5468
|
+
"",
|
|
5469
|
+
" EVIDENCE, NOT ACCOUNTING. The parent marks the checkout as started by",
|
|
5470
|
+
" a game, with the game it framed, for attribution only.",
|
|
5471
|
+
"",
|
|
5472
|
+
"Every field is untrusted text: match `skuId` against the offers you",
|
|
5473
|
+
"read, never interpolate it into a path, a query or a log line."
|
|
5474
|
+
],
|
|
5475
|
+
"fields": {
|
|
5476
|
+
"requestId": {
|
|
5477
|
+
"kind": "string",
|
|
5478
|
+
"maxLength": 64,
|
|
5479
|
+
"$comment": "Opaque and frame-chosen. Echo-only, and never a key on the money path: the parent derives its own request identity."
|
|
5480
|
+
},
|
|
5481
|
+
"skuId": {
|
|
5482
|
+
"kind": "string",
|
|
5483
|
+
"maxLength": 64,
|
|
5484
|
+
"$comment": "One `skuId` from gem.storeOffersResult. Frame-chosen: matched by the parent against the listing it would show now."
|
|
5485
|
+
}
|
|
5486
|
+
},
|
|
5487
|
+
"rateLimit": {
|
|
5488
|
+
"perMinute": 12,
|
|
5489
|
+
"$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."
|
|
5490
|
+
}
|
|
5491
|
+
},
|
|
5492
|
+
{
|
|
5493
|
+
"type": "gem.storePurchaseResult",
|
|
5494
|
+
"direction": "parentToFrame",
|
|
5495
|
+
"transport": "port",
|
|
5496
|
+
"$comment": [
|
|
5497
|
+
"The answer to one gem.requestStorePurchase. Gated by the `store`",
|
|
5498
|
+
"capability.",
|
|
5499
|
+
"",
|
|
5500
|
+
" ok - the player paid. With no reason, fulfilment has",
|
|
5501
|
+
" settled: re-read balances and inventory through",
|
|
5502
|
+
" `gem.me`; nothing here carries an amount. With",
|
|
5503
|
+
" `reason: pending`, the provider has not settled yet (or",
|
|
5504
|
+
" the player left before it did): `gem.storePurchaseSettled`",
|
|
5505
|
+
" follows when it does, and a game re-reads on resume too.",
|
|
5506
|
+
" declined - nothing was bought: the player dismissed the sheet",
|
|
5507
|
+
" (`cancelled`), or no press, another acting message open",
|
|
5508
|
+
" or a back-off running (`busy`).",
|
|
5509
|
+
" unavailable - no sale the parent can vouch for. With `store_off`",
|
|
5510
|
+
" (the store withheld), `sign_in_required` (a guest),",
|
|
5511
|
+
" `not_found` (an unknown SKU) or the provider's own",
|
|
5512
|
+
" failure code, nothing was bought. With `error`, the",
|
|
5513
|
+
" OUTCOME IS UNKNOWN: the parent has no reliable answer, and",
|
|
5514
|
+
" the player may have paid. The SDK resolves the same",
|
|
5515
|
+
" `unavailable` / `error` when the port closes under an open",
|
|
5516
|
+
" purchase, which can be after the player paid. A game",
|
|
5517
|
+
" re-reads balances and inventory through `gem.me` and",
|
|
5518
|
+
" keeps listening for gem.storePurchaseSettled.",
|
|
5519
|
+
"",
|
|
5520
|
+
"NO TIMEOUT ON THE GAME'S SIDE while the port is alive: the parent always",
|
|
5521
|
+
"answers, and settlement can take as long as the provider takes.",
|
|
5522
|
+
"NEVER AUTO-RETRIED. Only a fresh press may ask again."
|
|
5523
|
+
],
|
|
5524
|
+
"fields": {
|
|
5525
|
+
"requestId": {
|
|
5526
|
+
"kind": "string",
|
|
5527
|
+
"maxLength": 64,
|
|
5528
|
+
"$comment": "Echoes the gem.requestStorePurchase that asked. Echo-only."
|
|
5529
|
+
},
|
|
5530
|
+
"outcome": {
|
|
5531
|
+
"kind": "enum",
|
|
5532
|
+
"values": ["ok", "declined", "unavailable"],
|
|
5533
|
+
"$comment": "Closed at three, as gem.purchaseResult's."
|
|
5534
|
+
},
|
|
5535
|
+
"reason": {
|
|
5536
|
+
"kind": "string",
|
|
5537
|
+
"maxLength": 64,
|
|
5538
|
+
"format": "code",
|
|
5539
|
+
"optional": true,
|
|
5540
|
+
"$comment": "An open code, on any outcome: `pending` with `ok`; `cancelled` or `busy` with `declined`; `store_off`, `sign_in_required`, `not_found`, `error` or the payment provider's own failure code with `unavailable`. The parent normalizes every reason to the code grammar before it is sent, a provider's code included, and sends `error` when nothing is left — GemHost's answer methods lower-case it, map any other character to `_` and cut it to 64 — so a malformed code never fails outbound validation and leaves the purchase waiting. A receiver must not fail on a code it does not know."
|
|
5541
|
+
}
|
|
5542
|
+
},
|
|
5543
|
+
"rateLimit": { "perMinute": 30 }
|
|
5544
|
+
},
|
|
5545
|
+
{
|
|
5546
|
+
"type": "gem.storePurchaseSettled",
|
|
5547
|
+
"direction": "parentToFrame",
|
|
5548
|
+
"transport": "port",
|
|
5549
|
+
"$comment": [
|
|
5550
|
+
"A store purchase this game started, answered `ok` / `pending`, has",
|
|
5551
|
+
"since settled. Gated by the `store` capability. Pushed, unasked, once",
|
|
5552
|
+
"per such purchase while the frame session lasts; a purchase that",
|
|
5553
|
+
"settles after the frame has gone is not reported to the next one.",
|
|
5554
|
+
"",
|
|
5555
|
+
"A HINT, NOT A RECEIPT. It carries the SKU and nothing else: no amount,",
|
|
5556
|
+
"no order id. A game re-reads balances and inventory through `gem.me`,",
|
|
5557
|
+
"which is where the truth is."
|
|
5558
|
+
],
|
|
5559
|
+
"fields": {
|
|
5560
|
+
"skuId": {
|
|
5561
|
+
"kind": "string",
|
|
5562
|
+
"maxLength": 64,
|
|
5563
|
+
"$comment": "The SKU the settled purchase was for, as the game asked for it."
|
|
5564
|
+
}
|
|
5565
|
+
},
|
|
5566
|
+
"rateLimit": { "perMinute": 30 }
|
|
4837
5567
|
}
|
|
4838
5568
|
],
|
|
4839
5569
|
|