@crowdedkingdoms/crowdyjs 18.6.0-dev.1 → 18.8.0-dev.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.
- package/MIGRATION.md +104 -0
- package/README.md +108 -4
- package/dist/binary-wire.d.ts +30 -2
- package/dist/binary-wire.d.ts.map +1 -1
- package/dist/binary-wire.js +61 -3
- package/dist/domains/appAccess.d.ts +32 -1
- package/dist/domains/appAccess.d.ts.map +1 -1
- package/dist/domains/appAccess.js +49 -1
- package/dist/domains/channels.d.ts +8 -3
- package/dist/domains/channels.d.ts.map +1 -1
- package/dist/domains/channels.js +8 -3
- package/dist/domains/exec.d.ts +14 -0
- package/dist/domains/exec.d.ts.map +1 -1
- package/dist/domains/exec.js +13 -1
- package/dist/domains/grids.d.ts +1 -0
- package/dist/domains/grids.d.ts.map +1 -1
- package/dist/domains/grids.js +1 -0
- package/dist/domains/portal.d.ts +18 -0
- package/dist/domains/portal.d.ts.map +1 -1
- package/dist/domains/portal.js +1 -1
- package/dist/domains/udp.d.ts +27 -8
- package/dist/domains/udp.d.ts.map +1 -1
- package/dist/domains/udp.js +39 -9
- package/dist/domains/users.d.ts +23 -2
- package/dist/domains/users.d.ts.map +1 -1
- package/dist/domains/users.js +34 -2
- package/dist/domains/voxels.d.ts +4 -3
- package/dist/domains/voxels.d.ts.map +1 -1
- package/dist/domains/voxels.js +4 -3
- package/dist/errors.d.ts +52 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +74 -0
- package/dist/generated/graphql-schema.d.ts +163 -49
- package/dist/generated/graphql-schema.d.ts.map +1 -1
- package/dist/generated/graphql-schema.js +3 -1
- package/dist/generated/graphql.d.ts +159 -17
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +30 -23
- package/dist/grid-mods/grid-host-calls.d.ts +34 -0
- package/dist/grid-mods/grid-host-calls.d.ts.map +1 -1
- package/dist/grid-mods/grid-host-calls.js +28 -5
- package/dist/grid-mods/index.d.ts +1 -1
- package/dist/grid-mods/index.d.ts.map +1 -1
- package/dist/grid-mods/index.js +1 -1
- package/dist/grid-scope.d.ts +3 -0
- package/dist/grid-scope.d.ts.map +1 -1
- package/dist/grid-scope.js +6 -0
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/media/voice-frames.d.ts +258 -0
- package/dist/media/voice-frames.d.ts.map +1 -0
- package/dist/media/voice-frames.js +424 -0
- package/dist/metrics.d.ts +1 -1
- package/dist/realtime.d.ts +19 -3
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +8 -0
- package/dist/stores/actors.d.ts +3 -1
- package/dist/stores/actors.d.ts.map +1 -1
- package/dist/stores/chunks.d.ts +65 -6
- package/dist/stores/chunks.d.ts.map +1 -1
- package/dist/stores/chunks.js +161 -19
- package/dist/stores/index.d.ts +2 -2
- package/dist/stores/index.d.ts.map +1 -1
- package/dist/stores/index.js +1 -1
- package/dist/stores/keys.d.ts +5 -0
- package/dist/stores/keys.d.ts.map +1 -1
- package/dist/stores/keys.js +7 -0
- package/dist/stores/session.d.ts +1 -1
- package/dist/stores/session.d.ts.map +1 -1
- package/dist/stores/session.js +2 -0
- package/dist/types.d.ts +49 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/MIGRATION.md
CHANGED
|
@@ -120,6 +120,110 @@ super-admin's session). There is no SDK replacement.
|
|
|
120
120
|
`dev`'s SDL after #417 merged (`npm run schema:sync:paths -- --schema <that schema.gql>`, then
|
|
121
121
|
`npm run codegen`).
|
|
122
122
|
|
|
123
|
+
## 18.8.0: builds report the SDK version they compiled against
|
|
124
|
+
|
|
125
|
+
Additive, with one new floor: **the build documents select `ExecBuild.sdkVersion`, so 18.8.0
|
|
126
|
+
needs ck-api v2.40.2 or later** for `exec.build`, `buildStatus`, `waitForBuild`, `modBuild`,
|
|
127
|
+
`modClientBuild`, `modBuildStatus` and `waitForModBuild` (an older API refuses the selection).
|
|
128
|
+
|
|
129
|
+
- **`sdkVersion`** on every build: the version of `ckx-sdk` the platform compiled the build
|
|
130
|
+
against (`crowdy-client-sdk` for a CLIENT half), from the toolchain of the API instance that
|
|
131
|
+
ran it. The platform points a crate at its own copy of the SDK whatever version the crate
|
|
132
|
+
names, so this, not your manifest, says what your code was checked against. `null` until the
|
|
133
|
+
build starts, and for builds from before the platform recorded it. The build log's first line
|
|
134
|
+
names the same toolchain.
|
|
135
|
+
|
|
136
|
+
`schema.gql` and the generated types carry cks-game-api `dev`'s SDL at #494.
|
|
137
|
+
|
|
138
|
+
## 18.7.0: voice helpers, channel audio, opcode 140 on the relay, wide voxels, self-echo, pause and access refusals
|
|
139
|
+
|
|
140
|
+
Additive, with reads that change for values they used to get wrong (the wide-voxel item) and one
|
|
141
|
+
new floor: **the token mutations (`portal.mintAppToken`, `exchangeCode`, `refresh`) select
|
|
142
|
+
`runtimeGate` and the `udpNotifications` subscription selects `ChannelAudioNotification`, so
|
|
143
|
+
18.7.0 needs the ck-api release after v2.39.0 that carries both** (an older one refuses the
|
|
144
|
+
selections). Channel audio and the voxel echo need Buddy v0.37.0.
|
|
145
|
+
|
|
146
|
+
- **Voice helpers** (`media/voice-frames.ts`, exported from the package entry): an optional
|
|
147
|
+
convention for what an audio payload carries. A 10-byte header, little-endian, goes in front of
|
|
148
|
+
each codec frame: version 1, codec (0 raw, 1 Opus 48 kHz mono, 2 G.711 µ-law 8 kHz), `u16` seq,
|
|
149
|
+
`u32` timestamp in codec samples (milliseconds for raw), the frame's duration in ms, and flags
|
|
150
|
+
(bit 0 the first packet after silence, bit 1 the last before it). `encodeVoiceHeader` /
|
|
151
|
+
`encodeVoicePacket` write it; `decodeVoicePacket` returns `null` for a packet shorter than
|
|
152
|
+
10 bytes or of another version and never throws. `VoicePacketizer` numbers one sender's frames
|
|
153
|
+
across the seq and timestamp wraps and sets the flags (`packetize(frame, { last })`, `skip()`
|
|
154
|
+
for silence). `VoiceJitterBuffer` takes `push(key, packet, nowMs)` per sender key and returns
|
|
155
|
+
the due frames from `pull(key, nowMs)` / `poll(nowMs)`: in seq order, `targetDelayMs` (60) after
|
|
156
|
+
a talk spurt's first packet, a gap (`frame: null`) for each one that never came, late packets
|
|
157
|
+
dropped, at most `maxFrames` (64) a sender, starting over on a talk spurt or `resetAfterMs`
|
|
158
|
+
(200) of silence. A spurt that starts while the one before still has frames waiting drops
|
|
159
|
+
them. CrowdyCPP 0.60.0 has the same helpers and replays `test/unit/fixtures/voice-frames.json`.
|
|
160
|
+
No codec ships: a browser encodes Opus with WebCodecs (the module doc has the settings). A game
|
|
161
|
+
with a voice format of its own keeps it; positioning a voice stays the game's.
|
|
162
|
+
- **`genericSpatial`.** Opcode 140 (`GENERIC_SPATIAL_1`), an app-defined spatial payload such as
|
|
163
|
+
CrowdyCPP's `Connection::sendGenericSpatial`, reaches the new `genericSpatial` handler of
|
|
164
|
+
`udp.subscribe` (and `any`) as a `GenericSpatialNotification`: the spatial header fields and a
|
|
165
|
+
base64 `payload`. The World Stores bus has a `genericSpatial` key. **Binary relay only**
|
|
166
|
+
(`realtime: { binaryTransport: true }`): the GraphQL `udpNotifications` union has no member for
|
|
167
|
+
140, so the proxy drops it and a client on the GraphQL transport never sees one. `UdpNotification`
|
|
168
|
+
gains the member, so an exhaustive `switch` over `__typename` needs the new case.
|
|
169
|
+
- **`ChunkStore` keeps wide voxel types and other addresses.** Positions and types are the app's
|
|
170
|
+
signed 16-bit values, which the platform does not check. An edit the 16×16×16 one-byte grid
|
|
171
|
+
cannot hold (a type outside 0-255, a position outside 0-15) — a realtime update, a hydrated
|
|
172
|
+
`voxelStates` entry, or a local `setVoxel` — is kept whole in the new `CachedChunk.overlay`
|
|
173
|
+
(`ChunkOverlayVoxel`: `x`, `y`, `z`, `voxelType`, `state`; keyed by `voxelKey(x, y, z)`), and
|
|
174
|
+
`voxelTypeAt` / `voxelStateAt` return it. What changes: a type outside 0-255 is no longer stored
|
|
175
|
+
truncated (300 used to read as 44, -1 as 255); a position outside 0-15 no longer lands on
|
|
176
|
+
another voxel (`(16, 0, 0)` used to overwrite `(0, 1, 0)`), and reading one returns the overlay's
|
|
177
|
+
or nothing; and code that walks `chunk.voxels` itself sees 0 where an in-grid voxel's type is in
|
|
178
|
+
the overlay. The store is a 16×16×16 helper; a game with other addressing reads the raw
|
|
179
|
+
`voxelUpdate` events and `chunks.get`'s `voxelStates`.
|
|
180
|
+
- **`createGridHostCalls({ voxelBounds })`** sets the positions and types a CLIENT half's
|
|
181
|
+
`voxel_set` may write, for a world that uses other signed 16-bit values. The default,
|
|
182
|
+
`DEFAULT_GRID_VOXEL_BOUNDS`, is today's: positions 0-15, types 0-255. Bounds that are not
|
|
183
|
+
integer ranges within -32768 to 32767 throw `RangeError` when the host calls are created.
|
|
184
|
+
- **Channel audio** (Buddy v0.37.0). `udp.sendChannelAudio({ channelId, uuid, payload,
|
|
185
|
+
sequenceNumber? })` sends opcode 35 (opcode 17's layout and signing) on the binary relay, else
|
|
186
|
+
the `sendChannelAudio` mutation; `payload` is base64, at most 1,024 bytes, and the server
|
|
187
|
+
refuses it with `UNAUTHORIZED` (7) without the channel's `send_voice` and the app's
|
|
188
|
+
`use_voice_chat`. There is no echo to the sender. Other members' frames arrive as opcode 36,
|
|
189
|
+
standalone or bundled, as a `ChannelAudioNotification` (`channelId`, `uuid`, `audioData`
|
|
190
|
+
base64, `sequenceNumber`, `epochMillis`) on the new `channelAudio` handler of `udp.subscribe`
|
|
191
|
+
(and `any`) and the World Stores bus key `channelAudio`, on the binary relay and on the GraphQL
|
|
192
|
+
transport alike (the `udpNotifications` subscription selects the union's
|
|
193
|
+
`ChannelAudioNotification`, so it too needs that ck-api release). `channels.create` /
|
|
194
|
+
`grids.createChannel` take `membersCanSpeak` (default false: the member role gets
|
|
195
|
+
`send_voice`); `GridScope.channels` has `sendAudio(channelId, uuid, audioBase64)`;
|
|
196
|
+
`serializeChannelAudio` is exported. The voice helpers are the payload: see the README.
|
|
197
|
+
- **UDP error 33, `APP_PAUSED`**: the replication server refuses a paused app's sends.
|
|
198
|
+
`UDP_ERROR_NAMES[33]` and the generated `UdpErrorCode.AppPaused` name it.
|
|
199
|
+
- **Voxel edits are int16 and their state at most 1,024 bytes.** `udp.sendVoxelUpdate` and
|
|
200
|
+
`ChunkStore.setVoxel` throw `RangeError` (nothing sent) for a position or type outside
|
|
201
|
+
-32768..32767 or a state over 1,024 bytes (`assertVoxelEdit`, `VOXEL_STATE_MAX_BYTES`); the
|
|
202
|
+
server answers those with `INVALID_REQUEST` (15). The 0-15 / 0-255 ranges in the docs were
|
|
203
|
+
never enforced and are gone.
|
|
204
|
+
- **The echo of your own voxel edit.** Buddy v0.37.0 delivers every accepted edit back to its
|
|
205
|
+
sender as a `VoxelUpdateNotification`. `ChunkStore` records each `setVoxel` (uuid, sequence,
|
|
206
|
+
voxel) for 10 s and does not apply its echo again, so a local edit fires `onChunkChanged` once;
|
|
207
|
+
it applies the echo only when another client's edit of the voxel arrived in between (the server
|
|
208
|
+
ordered yours last) and no newer local edit of it is pending. A game reading `voxelUpdate`
|
|
209
|
+
itself sees its own edits there now: compare the `uuid` and `sequenceNumber` with the send's.
|
|
210
|
+
- **Pause and access refusals.** `isAppPaused(gate)` reads `AppTokenResponse.runtimeGate` and
|
|
211
|
+
`GameClientBootstrap.runtimeGate` (`{ status, reason }`; anything but `ACTIVE` is paused; a
|
|
212
|
+
paused app still mints, so check before entering). `appPausedOf(err)` reads `APP_PAUSED`
|
|
213
|
+
(`reason`), `accessRefusalOf(err)` `ACCESS_REVOKED` / `ACCESS_SUSPENDED` (`suspendedUntil`) /
|
|
214
|
+
`ACCESS_NOT_GRANTED`, `actorExistsOf(err)` `ACTOR_EXISTS` (`ownedByCaller`), with their
|
|
215
|
+
`*_CODE` constants.
|
|
216
|
+
- **New API wraps**: `users.playerProfile(userId)` and `users.playerProfiles(userIds)` (at most
|
|
217
|
+
`PLAYER_PROFILES_MAX`, 100; `RangeError` above) for public `{ userId, gamertag, disambiguation }`;
|
|
218
|
+
`users.get` documents that another user's private fields come back null.
|
|
219
|
+
`appAccess.suspend(appId, userId, until, idempotencyKey?)`, `unsuspend`,
|
|
220
|
+
`resyncTierGridPermissions` (`manage_access_tiers`); `suspendedUntil` on every access record.
|
|
221
|
+
`exec.restartType(appId, nodeType)` → `{ nodeType, stopped }` (`manage_compute`);
|
|
222
|
+
`exec.status` adds `budgetPauseReason`, `maxInstances`, `maxReservedMb`, `instanceLimit`,
|
|
223
|
+
`instances`, `reservedMb`. `apps.get` / `apps.update` carry `claimOwnerKeys`;
|
|
224
|
+
`chunks.get` / `getByDistance` carry `voxelStatesTruncated`; `gameClientBootstrap` carries
|
|
225
|
+
`runtimeGate` and `wildernessWritesOpen`.
|
|
226
|
+
|
|
123
227
|
## 18.6.0: the input log
|
|
124
228
|
|
|
125
229
|
Additive. An app with replay logging on has its client inputs recorded, and `client.inputLog`
|
package/README.md
CHANGED
|
@@ -234,7 +234,7 @@ never told about, so a native refresh without it is a re-placement.
|
|
|
234
234
|
| Sub-client | What it does |
|
|
235
235
|
|---|---|
|
|
236
236
|
| `client.auth` | Sign-in: `login` / `register` (email + password), magic link, social/OIDC. Passwords: `requestPasswordReset` / `resetPassword`, `changePassword`, and `setInitialPassword` for an account created by magic link or a social provider (added in 15.1.0). Log out, and linked identities (`myIdentities`, `linkIdentity`/`unlinkIdentity`). **No dev bypass** — `devLogin` was removed in 15.0.0 and `DEV_AUTH_BYPASS` is gone from every tier. |
|
|
237
|
-
| `client.users` | `me`, `updateGamertag`, profile reads
|
|
237
|
+
| `client.users` | `me`, `updateGamertag`, profile reads; `playerProfile` / `playerProfiles` (up to 100) for other players' public `{ userId, gamertag, disambiguation }`. |
|
|
238
238
|
| `client.session` | Token store, `restore()`, `getToken()`, manual `setToken()`. |
|
|
239
239
|
| `client.portal` | App-scoped token minting (`mintAppToken`) and the cross-origin PKCE entry flow (`beginEntry` / `handleAuthorizeRequest` / `completeEntry` / `refresh`). |
|
|
240
240
|
| `client.hosting` | Third-party hosting on Crowdy Games (17.2.0): `claim` a slug for an app, `beginPublish` / `completePublish` / `abandonPublish` a built bundle, `setEnabled`; public `game(slug)` / `listed()`. Mutations need an identity session with `manage_apps`. `@crowdedkingdoms/crowdyjs/hosting` exports `publishDirectory(client, { dir, slug })`, the Node helper that does the whole publish for a `dist/`. |
|
|
@@ -245,7 +245,7 @@ never told about, so a native refresh without it is a re-placement.
|
|
|
245
245
|
| `client.teleport` | Teleport requests. |
|
|
246
246
|
| `client.inputLog` | The input log (18.6.0): `sessions(appId)` and `messages(appId, gameTokenId)` read the client inputs recorded for an app with replay logging on (`apps.update(appId, { replayLoggingEnabled: true })`). A player reads their own; `manage_apps` reads every session. Keep paging while `hasNextPage` is true. |
|
|
247
247
|
| `client.channels`, `client.teams` | Messaging channels and app-scoped player teams (membership + roles). |
|
|
248
|
-
| `client.exec` | **ck-exec:** an app's server code as hubs and spokes. `connect(appId, { nodeType, key })` opens a player's connection to an execution host; the `ExecConnection` it returns has `call`, `callRaw`, `subscribe`, `ping`, `onReconnect` and `close`, with MessagePack payloads, and it reconnects and renews subscriptions by itself. `starters` / `build` / `deploy` build and deploy an app (`manage_compute`); `logs`, `instances`, `versions`, `activateVersion` and `
|
|
248
|
+
| `client.exec` | **ck-exec:** an app's server code as hubs and spokes. `connect(appId, { nodeType, key })` opens a player's connection to an execution host; the `ExecConnection` it returns has `call`, `callRaw`, `subscribe`, `ping`, `onReconnect` and `close`, with MessagePack payloads, and it reconnects and renews subscriptions by itself. `starters` / `build` / `deploy` build and deploy an app (`manage_compute`); `logs`, `instances`, `versions`, `activateVersion`, `setEnabled`, `restartType` and `status` operate it; `mod*` are players' mods on grids they own, and `modClient*`, `gridClientMods`, `consentClientMod`, `trustAuthor`, `revokeClientModConsent` and `revokeAuthorTrust` their CLIENT halves. See [ck-exec](#ck-exec). |
|
|
249
249
|
| `client.playerWallet` | Player spend: balance, ledger, hourly charges, spend caps, card setup, auto-recharge. A ck-exec mod's compute is billed here, to its owner. |
|
|
250
250
|
| `client.marketplace` | Player-authorized grid claims (`claimGridOwnership`, `claimGridChunk`, `releaseClaimedGrid`, requests and invites) and studio moderation of player code (admission queue, listing administration, claim policy). Mods publish and install through `client.exec`. |
|
|
251
251
|
| `client.crowdyStudio` | Cloud project, personal-library, and common-file APIs for Crowdy Studio: target-scoped files, metadata/module names, optimistic revisions, copy-by-value imports, atomic saves. |
|
|
@@ -264,7 +264,7 @@ grouped under `client.admin` and mirrored at the top level):
|
|
|
264
264
|
|---|---|
|
|
265
265
|
| `client.organizations` | Orgs, members, RBAC roles, org API tokens. |
|
|
266
266
|
| `client.apps` | App registry, discovery + routing (`create`, `routeFor`, `marketplace`), visibility, and player-code admission mode / allow-list administration. |
|
|
267
|
-
| `client.appAccess` | Access tiers + per-user grants, and the feature keys a tier grants (`defineFeature`, `grantTierFeature`, `tierFeatures`, …), which a hub checks with the node API's `players.features`. |
|
|
267
|
+
| `client.appAccess` | Access tiers + per-user grants, and the feature keys a tier grants (`defineFeature`, `grantTierFeature`, `tierFeatures`, …), which a hub checks with the node API's `players.features`; timed bans (`suspend`, `unsuspend`) and `resyncTierGridPermissions`. |
|
|
268
268
|
| `client.billing` | Org wallet + per-app spend budgets. |
|
|
269
269
|
| `client.payments` | Payment checkouts (wallet top-ups, plan purchases). |
|
|
270
270
|
| `client.quotas` | Usage quotas at the org/app scope. |
|
|
@@ -346,6 +346,10 @@ const unsubscribe = client.udp.subscribe(
|
|
|
346
346
|
},
|
|
347
347
|
clientEvent: (event) => { /* ... */ },
|
|
348
348
|
serverEvent: (event) => { /* ... */ },
|
|
349
|
+
genericSpatial: (event) => {
|
|
350
|
+
// An app-defined spatial payload (opcode 140): binary relay only, see below.
|
|
351
|
+
console.log(event.uuid, event.payload); // payload is base64
|
|
352
|
+
},
|
|
349
353
|
singleActorMessage: (event) => {
|
|
350
354
|
// A direct actor-to-actor message addressed to you.
|
|
351
355
|
console.log(event.uuid, event.payload); // payload is base64
|
|
@@ -386,6 +390,13 @@ while `AUTH_REQUIRED` / `APP_ID_REQUIRED` must be fixed by the caller first.
|
|
|
386
390
|
Unsubscribing stops delivery only; call `client.udp.disconnect()` to close the
|
|
387
391
|
UDP proxy session.
|
|
388
392
|
|
|
393
|
+
`genericSpatial` receives opcode 140 (`GENERIC_SPATIAL_1`), an app-defined spatial payload the
|
|
394
|
+
server fans out without reading — what CrowdyCPP's `Connection::sendGenericSpatial` sends. It
|
|
395
|
+
arrives only on the binary relay (`realtime: { binaryTransport: true }`): the GraphQL
|
|
396
|
+
`udpNotifications` union has no member for it, so the GraphQL proxy drops it and a client on that
|
|
397
|
+
transport never sees one. A World Stores session carries it on its bus too:
|
|
398
|
+
`session.context.on('genericSpatial', …)`.
|
|
399
|
+
|
|
389
400
|
### Surviving the loss of an instance (direct connect)
|
|
390
401
|
|
|
391
402
|
**If your environment uses direct connect, set `realtime.discoveryUrl`.** Under
|
|
@@ -450,6 +461,12 @@ with a live actor within `maxDistance` chunks (straight-line distance between
|
|
|
450
461
|
chunk coordinates) of `chunk` receive it; they get the same `channelMessage`
|
|
451
462
|
notification.
|
|
452
463
|
|
|
464
|
+
`sendVoxelUpdate` takes the app's signed 16-bit positions and type and a state of
|
|
465
|
+
at most 1,024 bytes (`RangeError` otherwise, nothing sent). Buddy v0.37.0 echoes
|
|
466
|
+
every accepted voxel edit back to its sender as a `voxelUpdate`, so a game reading
|
|
467
|
+
those sees its own edits; World Stores' `ChunkStore` applies that echo once. A
|
|
468
|
+
paused app's sends are refused with UDP error 33, `APP_PAUSED`.
|
|
469
|
+
|
|
453
470
|
### Bundled sends on the binary relay
|
|
454
471
|
|
|
455
472
|
With `realtime: { binaryTransport: true }` the SDK signs each message itself
|
|
@@ -477,6 +494,76 @@ await client.udp.sendSingleActorMessage({
|
|
|
477
494
|
});
|
|
478
495
|
```
|
|
479
496
|
|
|
497
|
+
### Voice payloads
|
|
498
|
+
|
|
499
|
+
`sendAudioPacket` carries whatever bytes it is given, and a game with a voice format of its own
|
|
500
|
+
keeps it. The voice helpers are an optional one that both SDKs share (CrowdyCPP's
|
|
501
|
+
`crowdy/media/voice_frames.hpp`, held to the same cases in `test/unit/fixtures/voice-frames.json`):
|
|
502
|
+
a 10-byte header in front of each codec frame, little-endian — the version (1), the codec (0 raw,
|
|
503
|
+
1 Opus 48 kHz mono, 2 G.711 µ-law 8 kHz), a `u16` seq, a `u32` timestamp in codec samples, the
|
|
504
|
+
frame's duration in milliseconds and two talk-spurt flags (bit 0 the first packet after silence,
|
|
505
|
+
bit 1 the last before it). A reader refuses a packet shorter than the header or of another
|
|
506
|
+
version. `VoicePacketizer` numbers one sender's frames; `VoiceJitterBuffer` puts each sender's
|
|
507
|
+
packets back in order, plays them out 60 ms (`targetDelayMs`) after the first packet of a talk
|
|
508
|
+
spurt arrived, reports a frame that never came as a gap, drops one that arrives after its
|
|
509
|
+
playout time, and holds at most 64 frames a sender.
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
import {
|
|
513
|
+
VoiceCodec, VoiceJitterBuffer, VoicePacketizer, decodeBase64, encodeBase64,
|
|
514
|
+
} from '@crowdedkingdoms/crowdyjs';
|
|
515
|
+
|
|
516
|
+
const packetizer = new VoicePacketizer({ codec: VoiceCodec.OPUS, frameMs: 20 });
|
|
517
|
+
// Every 20 ms while the player talks (`opusFrame` from a WebCodecs AudioEncoder):
|
|
518
|
+
await client.udp.sendAudioPacket({
|
|
519
|
+
appId, chunk, uuid: me.uuid,
|
|
520
|
+
audioData: encodeBase64(packetizer.packetize(opusFrame, { last: talkKeyReleased })),
|
|
521
|
+
});
|
|
522
|
+
// ...and packetizer.skip() for every 20 ms of silence that is not sent.
|
|
523
|
+
|
|
524
|
+
const voices = new VoiceJitterBuffer();
|
|
525
|
+
client.udp.subscribe({
|
|
526
|
+
audio: (n) => voices.push(n.uuid, decodeBase64(n.audioData), performance.now()),
|
|
527
|
+
actorLeft: (n) => voices.forget(n.uuid),
|
|
528
|
+
}, appId);
|
|
529
|
+
// From the audio clock, at least once a frame:
|
|
530
|
+
for (const slot of voices.poll(performance.now())) {
|
|
531
|
+
if (slot.gap) conceal(slot.key, slot.frameMs); // the frame never came
|
|
532
|
+
else play(slot.key, slot.codec, slot.frame!);
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The SDK ships no codec. A browser encodes and decodes Opus with WebCodecs (`AudioEncoder` /
|
|
537
|
+
`AudioDecoder`, `codec: 'opus'`, 48 kHz, one channel; check `AudioEncoder.isConfigSupported`
|
|
538
|
+
first) and falls back to G.711 µ-law where it has none; `media/voice-frames.ts` has the settings.
|
|
539
|
+
Where a voice sits in the world (panning, distance attenuation) stays the game's.
|
|
540
|
+
|
|
541
|
+
#### Channel audio (party and guild voice)
|
|
542
|
+
|
|
543
|
+
`sendAudioPacket` reaches players near a chunk. `sendChannelAudio` (Buddy v0.37.0) reaches every
|
|
544
|
+
active member of a channel wherever they are: opcode 35 on the binary relay, the
|
|
545
|
+
`sendChannelAudio` mutation otherwise, at most 1,024 payload bytes. The sender needs the channel's
|
|
546
|
+
`send_voice` (`channels.create({ ..., membersCanSpeak: true })` gives it to the member role) and the
|
|
547
|
+
app's `use_voice_chat`; without them the server answers `UNAUTHORIZED` on the subscription. There is
|
|
548
|
+
no echo. Members get a `channelAudio` notification; key the jitter buffer by channel and sender so
|
|
549
|
+
one player in two channels is two streams. It arrives on the binary relay and on the GraphQL
|
|
550
|
+
transport alike.
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
const party = new VoicePacketizer({ codec: VoiceCodec.OPUS, frameMs: 20 });
|
|
554
|
+
await client.udp.sendChannelAudio({
|
|
555
|
+
channelId, uuid: me.uuid,
|
|
556
|
+
payload: encodeBase64(party.packetize(opusFrame, { last: talkKeyReleased })),
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const partyVoices = new VoiceJitterBuffer();
|
|
560
|
+
client.udp.subscribe({
|
|
561
|
+
channelAudio: (n) =>
|
|
562
|
+
partyVoices.push(`${n.channelId}:${n.uuid}`, decodeBase64(n.audioData), performance.now()),
|
|
563
|
+
}, appId);
|
|
564
|
+
// ...poll(performance.now()) from the audio clock as above.
|
|
565
|
+
```
|
|
566
|
+
|
|
480
567
|
## World helpers
|
|
481
568
|
|
|
482
569
|
```ts
|
|
@@ -545,7 +632,9 @@ holds it to rules of its own (18.0.2): `grid_permission_check` answers only for
|
|
|
545
632
|
the player's four code-permission keys (`GRID_PERMISSION_CHECK_KEYS`) and
|
|
546
633
|
refuses any other key instead of answering false, because a game knows no other
|
|
547
634
|
key for a grid; `voxel_set` takes a voxel inside its chunk (0-15) and a type
|
|
548
|
-
0-255
|
|
635
|
+
0-255, or the ranges the game sets with `createGridHostCalls({ voxelBounds })` for a world
|
|
636
|
+
whose positions and types are other signed 16-bit values (18.7.0); and a half's spatial and
|
|
637
|
+
channel sends go out as an actor uuid the page
|
|
549
638
|
derives for the grid from the one the half names (`clientHalfActorUuid`), so a
|
|
550
639
|
half can never move the player's avatar or speak as another player. The broker
|
|
551
640
|
refuses a call that names its chunk a second way (`chunk`, `chunk_x`, …): route
|
|
@@ -614,6 +703,14 @@ another player's realtime voxel update) reaches a reload only through hydration:
|
|
|
614
703
|
returns it in `voxelStates`, and the bulk load's `voxels` never hold it. The store hydrates when
|
|
615
704
|
it has a `voxelStateCodec`; without one, set `chunks: { hydrateVoxelStates: true }`.
|
|
616
705
|
|
|
706
|
+
`chunks` is a helper for 16×16×16 chunks with one byte per voxel. Voxel positions and types are
|
|
707
|
+
the app's signed 16-bit values, which the platform does not check: an edit the dense grid cannot
|
|
708
|
+
hold (a type outside 0-255, a position outside 0-15), whether a realtime update, a hydrated
|
|
709
|
+
`voxelStates` entry or a local `setVoxel`, is kept whole in the chunk's `overlay` (keyed by
|
|
710
|
+
`voxelKey(x, y, z)`) instead of being truncated into `voxels`, and `voxelTypeAt` / `voxelStateAt`
|
|
711
|
+
return it (18.7.0). A game with other addressing reads the raw `voxelUpdate` events and
|
|
712
|
+
`chunks.get`'s `voxelStates`.
|
|
713
|
+
|
|
617
714
|
A chunk write-back the server refuses (a visitor on someone else's plot, a safe zone, a closed
|
|
618
715
|
wilderness) is dropped, not retried; one that fails for a reason that can clear is tried five
|
|
619
716
|
times. Listen with `session.chunks.onWriteBackFailed(...)` to undo or flag the edit (18.0.4).
|
|
@@ -986,6 +1083,13 @@ GraphQL errors carry a stable `extensions.code` (e.g. `UNAUTHENTICATED`,
|
|
|
986
1083
|
where applicable, `extensions.remediation` and `extensions.requiredPermission`.
|
|
987
1084
|
Branch on `error.extensions?.code` rather than parsing messages.
|
|
988
1085
|
|
|
1086
|
+
Readers for the refusals a player should be told about, each `null` for any other error:
|
|
1087
|
+
`accessRefusalOf(err)` (`ACCESS_REVOKED`, `ACCESS_SUSPENDED` with `suspendedUntil`,
|
|
1088
|
+
`ACCESS_NOT_GRANTED`), `appPausedOf(err)` (`APP_PAUSED`, with `reason`) and `actorExistsOf(err)`
|
|
1089
|
+
(`ACTOR_EXISTS`; `ownedByCaller: true` means a retried create of your own actor). A paused app
|
|
1090
|
+
still mints tokens: check `isAppPaused(token.runtimeGate)` (or the bootstrap's `runtimeGate`)
|
|
1091
|
+
before entering the world.
|
|
1092
|
+
|
|
989
1093
|
## Idempotent retries
|
|
990
1094
|
|
|
991
1095
|
Destructive game-client mutations accept an optional **idempotency key**. Pass
|
package/dist/binary-wire.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ export declare const WireMessageType: {
|
|
|
52
52
|
readonly CHANNEL_MESSAGE_REQUEST: 17;
|
|
53
53
|
readonly CHANNEL_MESSAGE_NOTIFICATION: 18;
|
|
54
54
|
readonly CHANNEL_MESSAGE_RANGED_REQUEST: 32;
|
|
55
|
+
readonly CHANNEL_AUDIO_REQUEST: 35;
|
|
56
|
+
readonly CHANNEL_AUDIO_NOTIFICATION: 36;
|
|
55
57
|
readonly ACTOR_UPDATE_REQUEST_2: 128;
|
|
56
58
|
readonly ACTOR_UPDATE_RESPONSE_2: 129;
|
|
57
59
|
readonly ACTOR_UPDATE_NOTIFICATION_2: 130;
|
|
@@ -64,6 +66,7 @@ export declare const WireMessageType: {
|
|
|
64
66
|
readonly CLIENT_TEXT_NOTIFICATION_2: 137;
|
|
65
67
|
readonly CLIENT_EVENT_NOTIFICATION_2: 138;
|
|
66
68
|
readonly SERVER_EVENT_NOTIFICATION_2: 139;
|
|
69
|
+
readonly GENERIC_SPATIAL_1: 140;
|
|
67
70
|
readonly SINGLE_ACTOR_MESSAGE: 142;
|
|
68
71
|
readonly CLIENT_VIDEO_PACKET_2: 143;
|
|
69
72
|
readonly CLIENT_VIDEO_NOTIFICATION_2: 144;
|
|
@@ -111,6 +114,23 @@ export interface SpatialSendBase {
|
|
|
111
114
|
export declare function serializeActorUpdate(ctx: RelaySignContext, input: SpatialSendBase & {
|
|
112
115
|
state: string;
|
|
113
116
|
}): Promise<Uint8Array>;
|
|
117
|
+
/** Most bytes a voxel edit's state may carry; the server refuses longer with INVALID_REQUEST. */
|
|
118
|
+
export declare const VOXEL_STATE_MAX_BYTES = 1024;
|
|
119
|
+
/**
|
|
120
|
+
* Check a voxel edit before it is sent: positions and type are app-defined
|
|
121
|
+
* signed 16-bit integers (no narrower range), and the state is at most
|
|
122
|
+
* {@link VOXEL_STATE_MAX_BYTES} decoded.
|
|
123
|
+
* @throws {RangeError} naming the field that does not fit.
|
|
124
|
+
*/
|
|
125
|
+
export declare function assertVoxelEdit(input: {
|
|
126
|
+
voxel: {
|
|
127
|
+
x: number;
|
|
128
|
+
y: number;
|
|
129
|
+
z: number;
|
|
130
|
+
};
|
|
131
|
+
voxelType: number;
|
|
132
|
+
voxelState?: string | null;
|
|
133
|
+
}): void;
|
|
114
134
|
export declare function serializeVoxelUpdate(ctx: RelaySignContext, input: SpatialSendBase & {
|
|
115
135
|
voxel: {
|
|
116
136
|
x: number;
|
|
@@ -153,12 +173,20 @@ export declare function serializeSingleActorMessage(ctx: RelaySignContext, input
|
|
|
153
173
|
* `[1B type=17][8B channelId][32B uuid][2B payloadLen][payload][1B containsAuth]
|
|
154
174
|
* [32B HMAC][8B gameTokenId][1B seq]` — HMAC over everything before it.
|
|
155
175
|
*/
|
|
156
|
-
export declare function serializeChannelMessage(ctx: RelaySignContext, input:
|
|
176
|
+
export declare function serializeChannelMessage(ctx: RelaySignContext, input: ChannelSendInput): Promise<Uint8Array>;
|
|
177
|
+
/**
|
|
178
|
+
* CHANNEL_AUDIO_REQUEST (Buddy v0.37.0): opcode 17's layout and signing with
|
|
179
|
+
* type byte 35. The payload is opaque (at most 1,024 bytes), typically one
|
|
180
|
+
* {@link VoicePacketizer} packet.
|
|
181
|
+
*/
|
|
182
|
+
export declare function serializeChannelAudio(ctx: RelaySignContext, input: ChannelSendInput): Promise<Uint8Array>;
|
|
183
|
+
/** Input shared by the channel message and channel audio uplinks (`ChannelMessageInput`). */
|
|
184
|
+
export interface ChannelSendInput {
|
|
157
185
|
channelId: string;
|
|
158
186
|
uuid: string;
|
|
159
187
|
payload?: string | null;
|
|
160
188
|
sequenceNumber?: number | null;
|
|
161
|
-
}
|
|
189
|
+
}
|
|
162
190
|
/** Largest `maxDistance` a ranged channel message may carry (the GraphQL `Int` ceiling). */
|
|
163
191
|
export declare const CHANNEL_RANGED_MAX_DISTANCE = 2147483647;
|
|
164
192
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"binary-wire.d.ts","sourceRoot":"","sources":["../src/binary-wire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,uEAAuE;AACvE,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,QACmC,CAAC;AAE9E;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAInE;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,CAwB5E;AAED,kEAAkE;AAClE,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"binary-wire.d.ts","sourceRoot":"","sources":["../src/binary-wire.ts"],"names":[],"mappings":"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"}
|
package/dist/binary-wire.js
CHANGED
|
@@ -89,6 +89,9 @@ export const WireMessageType = {
|
|
|
89
89
|
// Buddy v0.35.0: a channel publish only members near an origin chunk receive
|
|
90
90
|
// (they get an ordinary CHANNEL_MESSAGE_NOTIFICATION).
|
|
91
91
|
CHANNEL_MESSAGE_RANGED_REQUEST: 32,
|
|
92
|
+
// Buddy v0.37.0: channel audio, laid out exactly as 17 / 18 with their own type byte.
|
|
93
|
+
CHANNEL_AUDIO_REQUEST: 35,
|
|
94
|
+
CHANNEL_AUDIO_NOTIFICATION: 36,
|
|
92
95
|
ACTOR_UPDATE_REQUEST_2: 128,
|
|
93
96
|
ACTOR_UPDATE_RESPONSE_2: 129,
|
|
94
97
|
ACTOR_UPDATE_NOTIFICATION_2: 130,
|
|
@@ -101,6 +104,9 @@ export const WireMessageType = {
|
|
|
101
104
|
CLIENT_TEXT_NOTIFICATION_2: 137,
|
|
102
105
|
CLIENT_EVENT_NOTIFICATION_2: 138,
|
|
103
106
|
SERVER_EVENT_NOTIFICATION_2: 139,
|
|
107
|
+
// An app-defined spatial payload (CrowdyCPP `Connection::sendGenericSpatial`, a hub's
|
|
108
|
+
// emit). Parsed on the binary relay only: the GraphQL union has no member for it.
|
|
109
|
+
GENERIC_SPATIAL_1: 140,
|
|
104
110
|
SINGLE_ACTOR_MESSAGE: 142,
|
|
105
111
|
// Buddy v0.25.0: webcam video pair (audio's shape, payload = one SDK
|
|
106
112
|
// fragment, see media/video-frames.ts) and the server-only actor-left downlink.
|
|
@@ -153,6 +159,7 @@ const UDP_ERROR_NAMES = {
|
|
|
153
159
|
30: 'CANNOT_DELETE_DEFAULT_WORLD_GRID',
|
|
154
160
|
31: 'GRID_HAS_NESTED_CHILDREN',
|
|
155
161
|
32: 'TOKEN_EXPIRED',
|
|
162
|
+
33: 'APP_PAUSED',
|
|
156
163
|
};
|
|
157
164
|
const textEncoder = new TextEncoder();
|
|
158
165
|
const textDecoder = new TextDecoder();
|
|
@@ -252,6 +259,29 @@ export function serializeClientCapabilities(ctx, appId, flags = SDK_CAPABILITIES
|
|
|
252
259
|
export function serializeActorUpdate(ctx, input) {
|
|
253
260
|
return serializeSpatial(ctx, WireMessageType.ACTOR_UPDATE_REQUEST_2, input.appId, input.chunk, input.distance ?? 8, input.decayRate ?? 1, input.uuid, decodeBase64(input.state ?? ''), input.sequenceNumber ?? 0);
|
|
254
261
|
}
|
|
262
|
+
/** Most bytes a voxel edit's state may carry; the server refuses longer with INVALID_REQUEST. */
|
|
263
|
+
export const VOXEL_STATE_MAX_BYTES = 1024;
|
|
264
|
+
/**
|
|
265
|
+
* Check a voxel edit before it is sent: positions and type are app-defined
|
|
266
|
+
* signed 16-bit integers (no narrower range), and the state is at most
|
|
267
|
+
* {@link VOXEL_STATE_MAX_BYTES} decoded.
|
|
268
|
+
* @throws {RangeError} naming the field that does not fit.
|
|
269
|
+
*/
|
|
270
|
+
export function assertVoxelEdit(input) {
|
|
271
|
+
const int16 = (name, v) => {
|
|
272
|
+
if (!Number.isInteger(v) || v < -32768 || v > 32767) {
|
|
273
|
+
throw new RangeError(`${name} must be a signed 16-bit integer: ${v}`);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
int16('voxel.x', input.voxel.x);
|
|
277
|
+
int16('voxel.y', input.voxel.y);
|
|
278
|
+
int16('voxel.z', input.voxel.z);
|
|
279
|
+
int16('voxelType', input.voxelType);
|
|
280
|
+
const stateBytes = input.voxelState ? decodeBase64(input.voxelState).length : 0;
|
|
281
|
+
if (stateBytes > VOXEL_STATE_MAX_BYTES) {
|
|
282
|
+
throw new RangeError(`voxelState is ${stateBytes} bytes; at most ${VOXEL_STATE_MAX_BYTES}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
255
285
|
export function serializeVoxelUpdate(ctx, input) {
|
|
256
286
|
const voxelState = decodeBase64(input.voxelState ?? '');
|
|
257
287
|
const payload = new Uint8Array(10 + voxelState.length);
|
|
@@ -292,7 +322,18 @@ export function serializeSingleActorMessage(ctx, input) {
|
|
|
292
322
|
* `[1B type=17][8B channelId][32B uuid][2B payloadLen][payload][1B containsAuth]
|
|
293
323
|
* [32B HMAC][8B gameTokenId][1B seq]` — HMAC over everything before it.
|
|
294
324
|
*/
|
|
295
|
-
export
|
|
325
|
+
export function serializeChannelMessage(ctx, input) {
|
|
326
|
+
return serializeChannelFrame(ctx, WireMessageType.CHANNEL_MESSAGE_REQUEST, input);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* CHANNEL_AUDIO_REQUEST (Buddy v0.37.0): opcode 17's layout and signing with
|
|
330
|
+
* type byte 35. The payload is opaque (at most 1,024 bytes), typically one
|
|
331
|
+
* {@link VoicePacketizer} packet.
|
|
332
|
+
*/
|
|
333
|
+
export function serializeChannelAudio(ctx, input) {
|
|
334
|
+
return serializeChannelFrame(ctx, WireMessageType.CHANNEL_AUDIO_REQUEST, input);
|
|
335
|
+
}
|
|
336
|
+
async function serializeChannelFrame(ctx, messageType, input) {
|
|
296
337
|
const payload = input.payload ? decodeBase64(input.payload) : new Uint8Array(0);
|
|
297
338
|
if (payload.length > 1024) {
|
|
298
339
|
throw new Error(`Channel payload exceeds 1024 bytes: ${payload.length}`);
|
|
@@ -305,7 +346,7 @@ export async function serializeChannelMessage(ctx, input) {
|
|
|
305
346
|
const prefix = new Uint8Array(prefixLen);
|
|
306
347
|
const view = new DataView(prefix.buffer);
|
|
307
348
|
let off = 0;
|
|
308
|
-
view.setUint8(off,
|
|
349
|
+
view.setUint8(off, messageType);
|
|
309
350
|
off += 1;
|
|
310
351
|
view.setBigUint64(off, BigInt(input.channelId), true);
|
|
311
352
|
off += 8;
|
|
@@ -462,7 +503,8 @@ function parseOne(bytes) {
|
|
|
462
503
|
'UNKNOWN_ERROR'),
|
|
463
504
|
};
|
|
464
505
|
}
|
|
465
|
-
if (messageType === WireMessageType.CHANNEL_MESSAGE_NOTIFICATION
|
|
506
|
+
if (messageType === WireMessageType.CHANNEL_MESSAGE_NOTIFICATION ||
|
|
507
|
+
messageType === WireMessageType.CHANNEL_AUDIO_NOTIFICATION) {
|
|
466
508
|
// [1B type][8B channelId][32B senderUuid][2B payloadLen][payload][8B epochMillis][1B seq]
|
|
467
509
|
if (bytes.length < 1 + 8 + UUID_SIZE + 2)
|
|
468
510
|
return null;
|
|
@@ -480,6 +522,16 @@ function parseOne(bytes) {
|
|
|
480
522
|
const epochMillis = view.getBigUint64(off, true);
|
|
481
523
|
off += 8;
|
|
482
524
|
const sequenceNumber = view.getUint8(off);
|
|
525
|
+
if (messageType === WireMessageType.CHANNEL_AUDIO_NOTIFICATION) {
|
|
526
|
+
return {
|
|
527
|
+
__typename: 'ChannelAudioNotification',
|
|
528
|
+
channelId: channelId.toString(),
|
|
529
|
+
uuid,
|
|
530
|
+
audioData: encodeBase64(payload),
|
|
531
|
+
sequenceNumber,
|
|
532
|
+
epochMillis: epochMillis.toString(),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
483
535
|
return {
|
|
484
536
|
__typename: 'ChannelMessageNotification',
|
|
485
537
|
channelId: channelId.toString(),
|
|
@@ -574,6 +626,12 @@ function parseOne(bytes) {
|
|
|
574
626
|
payload: encodeBase64(p.payload),
|
|
575
627
|
};
|
|
576
628
|
}
|
|
629
|
+
case WireMessageType.GENERIC_SPATIAL_1:
|
|
630
|
+
return {
|
|
631
|
+
__typename: 'GenericSpatialNotification',
|
|
632
|
+
...spatialCommon(p),
|
|
633
|
+
payload: encodeBase64(p.payload),
|
|
634
|
+
};
|
|
577
635
|
default:
|
|
578
636
|
return null;
|
|
579
637
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphQLClient } from '../client.js';
|
|
2
|
-
import { type DefineAppFeatureInput, type DefineAppFeatureMutation, type AppFeaturesQuery, type GrantTierFeatureInput, type GrantTierFeatureMutation, type TierFeaturesQuery, type AppAccessTiersQuery, type MyAppAccessQuery, type AppUserAccessByAppQuery, type AppUserAccessConnectionQuery, type AppUserAccessConnectionQueryVariables, type RuntimePermissionsQuery, type AppGrantMemberCandidatesQuery, type ClaimFreeAppAccessMutation, type GrantMyAppAccessMutation, type CreateAccessTierMutation, type UpdateAccessTierMutation, type ArchiveAccessTierMutation, type GrantAppAccessMutation, type RevokeAppAccessMutation, type CreateAccessTierInput, type UpdateAccessTierInput, type GrantAppAccessInput } from '../generated/graphql.js';
|
|
2
|
+
import { type SuspendAppAccessMutation, type UnsuspendAppAccessMutation, type DefineAppFeatureInput, type DefineAppFeatureMutation, type AppFeaturesQuery, type GrantTierFeatureInput, type GrantTierFeatureMutation, type TierFeaturesQuery, type AppAccessTiersQuery, type MyAppAccessQuery, type AppUserAccessByAppQuery, type AppUserAccessConnectionQuery, type AppUserAccessConnectionQueryVariables, type RuntimePermissionsQuery, type AppGrantMemberCandidatesQuery, type ClaimFreeAppAccessMutation, type GrantMyAppAccessMutation, type CreateAccessTierMutation, type UpdateAccessTierMutation, type ArchiveAccessTierMutation, type GrantAppAccessMutation, type RevokeAppAccessMutation, type CreateAccessTierInput, type UpdateAccessTierInput, type GrantAppAccessInput } from '../generated/graphql.js';
|
|
3
3
|
/**
|
|
4
4
|
* App access tiers + per-user access grants — exposed as `client.appAccess`
|
|
5
5
|
* (and grouped under `client.admin`).
|
|
@@ -137,6 +137,37 @@ export declare class AppAccessAPI {
|
|
|
137
137
|
* @returns `true` on success.
|
|
138
138
|
*/
|
|
139
139
|
revoke(appId: string, userId: string): Promise<RevokeAppAccessMutation['revokeAppAccess']>;
|
|
140
|
+
/**
|
|
141
|
+
* Suspend a player's access to an app until `until`: a timed ban that lifts by
|
|
142
|
+
* itself. Until then their gameplay tokens are refused with `ACCESS_SUSPENDED`
|
|
143
|
+
* (carrying `suspendedUntil`, see `accessRefusalOf`), and the tokens they hold
|
|
144
|
+
* for the app are deleted and their realtime sessions ended at once. `until`
|
|
145
|
+
* must be in the future and at most 365 days away; the player must have an
|
|
146
|
+
* access record. Requires the `manage_access_tiers` app permission.
|
|
147
|
+
*
|
|
148
|
+
* @param appId - Numeric app id.
|
|
149
|
+
* @param userId - Numeric id of the player to suspend.
|
|
150
|
+
* @param until - When the suspension lapses (a `Date` or ISO-8601 string).
|
|
151
|
+
* @param idempotencyKey - Optional; a replay with the same key and arguments returns the first result.
|
|
152
|
+
* @returns The access record, with `suspendedUntil` set.
|
|
153
|
+
*/
|
|
154
|
+
suspend(appId: string, userId: string, until: Date | string, idempotencyKey?: string): Promise<SuspendAppAccessMutation['suspendAppAccess']>;
|
|
155
|
+
/**
|
|
156
|
+
* Lift a player's suspension before it lapses (a no-op for a player who is
|
|
157
|
+
* not suspended). Requires the `manage_access_tiers` app permission.
|
|
158
|
+
*
|
|
159
|
+
* @param appId - Numeric app id.
|
|
160
|
+
* @param userId - Numeric id of the player whose suspension to lift.
|
|
161
|
+
* @returns The access record, with `suspendedUntil` cleared.
|
|
162
|
+
*/
|
|
163
|
+
unsuspend(appId: string, userId: string): Promise<UnsuspendAppAccessMutation['unsuspendAppAccess']>;
|
|
164
|
+
/**
|
|
165
|
+
* Re-apply every player's tier keys that follow onto the app's world grid and
|
|
166
|
+
* rebuild the world grid's permissions. Tier changes made through this API
|
|
167
|
+
* already do it; call it after a change made another way. Requires the
|
|
168
|
+
* `manage_access_tiers` app permission.
|
|
169
|
+
*/
|
|
170
|
+
resyncTierGridPermissions(appId: string): Promise<boolean>;
|
|
140
171
|
/**
|
|
141
172
|
* Define (or re-describe) a feature key for an app. Idempotent on `(appId, featureKey)`.
|
|
142
173
|
* Requires the app-admin `manage_apps` permission.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appAccess.d.ts","sourceRoot":"","sources":["../../src/domains/appAccess.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,
|
|
1
|
+
{"version":3,"file":"appAccess.d.ts","sourceRoot":"","sources":["../../src/domains/appAccess.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppAccessTiersDocument, MyAppAccessDocument, AppUserAccessByAppDocument, AppUserAccessConnectionDocument, RuntimePermissionsDocument, AppGrantMemberCandidatesDocument, ClaimFreeAppAccessDocument, GrantMyAppAccessDocument, CreateAccessTierDocument, UpdateAccessTierDocument, ArchiveAccessTierDocument, GrantAppAccessDocument, RevokeAppAccessDocument, DefineAppFeatureDocument, AppFeaturesDocument, GrantTierFeatureDocument, RevokeTierFeatureDocument, TierFeaturesDocument, } from '../generated/graphql.js';
|
|
1
|
+
import { AppAccessTiersDocument, MyAppAccessDocument, AppUserAccessByAppDocument, AppUserAccessConnectionDocument, RuntimePermissionsDocument, AppGrantMemberCandidatesDocument, ClaimFreeAppAccessDocument, GrantMyAppAccessDocument, CreateAccessTierDocument, UpdateAccessTierDocument, ArchiveAccessTierDocument, GrantAppAccessDocument, RevokeAppAccessDocument, SuspendAppAccessDocument, UnsuspendAppAccessDocument, ResyncTierGridPermissionsDocument, DefineAppFeatureDocument, AppFeaturesDocument, GrantTierFeatureDocument, RevokeTierFeatureDocument, TierFeaturesDocument, } from '../generated/graphql.js';
|
|
2
2
|
/**
|
|
3
3
|
* App access tiers + per-user access grants — exposed as `client.appAccess`
|
|
4
4
|
* (and grouped under `client.admin`).
|
|
@@ -195,6 +195,54 @@ export class AppAccessAPI {
|
|
|
195
195
|
});
|
|
196
196
|
return data.revokeAppAccess;
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Suspend a player's access to an app until `until`: a timed ban that lifts by
|
|
200
|
+
* itself. Until then their gameplay tokens are refused with `ACCESS_SUSPENDED`
|
|
201
|
+
* (carrying `suspendedUntil`, see `accessRefusalOf`), and the tokens they hold
|
|
202
|
+
* for the app are deleted and their realtime sessions ended at once. `until`
|
|
203
|
+
* must be in the future and at most 365 days away; the player must have an
|
|
204
|
+
* access record. Requires the `manage_access_tiers` app permission.
|
|
205
|
+
*
|
|
206
|
+
* @param appId - Numeric app id.
|
|
207
|
+
* @param userId - Numeric id of the player to suspend.
|
|
208
|
+
* @param until - When the suspension lapses (a `Date` or ISO-8601 string).
|
|
209
|
+
* @param idempotencyKey - Optional; a replay with the same key and arguments returns the first result.
|
|
210
|
+
* @returns The access record, with `suspendedUntil` set.
|
|
211
|
+
*/
|
|
212
|
+
async suspend(appId, userId, until, idempotencyKey) {
|
|
213
|
+
const data = await this.api.request(SuspendAppAccessDocument, {
|
|
214
|
+
appId,
|
|
215
|
+
userId,
|
|
216
|
+
until: until instanceof Date ? until.toISOString() : until,
|
|
217
|
+
idempotencyKey,
|
|
218
|
+
});
|
|
219
|
+
return data.suspendAppAccess;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Lift a player's suspension before it lapses (a no-op for a player who is
|
|
223
|
+
* not suspended). Requires the `manage_access_tiers` app permission.
|
|
224
|
+
*
|
|
225
|
+
* @param appId - Numeric app id.
|
|
226
|
+
* @param userId - Numeric id of the player whose suspension to lift.
|
|
227
|
+
* @returns The access record, with `suspendedUntil` cleared.
|
|
228
|
+
*/
|
|
229
|
+
async unsuspend(appId, userId) {
|
|
230
|
+
const data = await this.api.request(UnsuspendAppAccessDocument, {
|
|
231
|
+
appId,
|
|
232
|
+
userId,
|
|
233
|
+
});
|
|
234
|
+
return data.unsuspendAppAccess;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Re-apply every player's tier keys that follow onto the app's world grid and
|
|
238
|
+
* rebuild the world grid's permissions. Tier changes made through this API
|
|
239
|
+
* already do it; call it after a change made another way. Requires the
|
|
240
|
+
* `manage_access_tiers` app permission.
|
|
241
|
+
*/
|
|
242
|
+
async resyncTierGridPermissions(appId) {
|
|
243
|
+
const data = await this.api.request(ResyncTierGridPermissionsDocument, { appId });
|
|
244
|
+
return data.resyncTierGridPermissions;
|
|
245
|
+
}
|
|
198
246
|
// -- Tier features -----------------------------------------------------------
|
|
199
247
|
//
|
|
200
248
|
// Feature keys an access tier grants. A ck-exec hub checks a player's with the node API's
|