@memizy/protocol 1.0.0-rc.5 → 1.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +6 -4
- package/SPEC.md +53 -47
- package/dist/errors.cjs +1 -1
- package/dist/errors.js +5 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +11 -12
- package/dist/internal.cjs +1 -0
- package/dist/internal.js +8 -0
- package/dist/items.cjs +1 -1
- package/dist/items.js +2 -4
- package/dist/limits.cjs +1 -1
- package/dist/limits.js +9 -1
- package/dist/relay.cjs +1 -1
- package/dist/relay.js +2 -1
- package/dist/schemas.cjs +1 -1
- package/dist/schemas.js +33 -36
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/index.d.ts +16 -13
- package/dist/src/internal.d.ts +12 -0
- package/dist/src/limits.d.ts +17 -1
- package/dist/src/manifestSchema.d.ts +1 -1
- package/dist/src/pluginRuntime.d.ts +2 -2
- package/dist/src/relay.d.ts +23 -5
- package/dist/src/schemas.d.ts +1 -10
- package/dist/src/security.d.ts +1 -1
- package/dist/src/settings.d.ts +1 -1
- package/dist/src/types.d.ts +22 -30
- package/dist/types.cjs +1 -1
- package/dist/types.js +3 -7
- package/package.json +13 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog of @memizy/protocol
|
|
2
|
+
|
|
3
|
+
The protocol has its own changelog at the end of [SPEC.md](https://github.com/memizy/memizy/blob/main/packages/protocol/SPEC.md#changelog) (protocol 1.0, 2026-10-14). This file lists the releases of the npm package.
|
|
4
|
+
|
|
5
|
+
## 1.0.0-rc.6 (2026-10-14)
|
|
6
|
+
|
|
7
|
+
The contract of protocol 1.0 is final: within MAJOR 1 it changes only additively (SPEC 9).
|
|
8
|
+
|
|
9
|
+
* SPEC status 1.0; the "RC4" labels are gone.
|
|
10
|
+
* The address `"settings"` (`SETTINGS_ADDRESS`, the plugin's settings screen in the lobby) is defined in SPEC 3.1 and on the `Address` type.
|
|
11
|
+
* `hello` is called once per instance: a second call rejects with `INVALID_ARGUMENT`. Only the first `end(result)` counts; later calls (also from a recreated authority) resolve and are ignored.
|
|
12
|
+
* Guaranteed rates and sizes in SPEC 8.1 and `LIMITS`: `recordAnswer` 10 per second, burst 20 (`answersPerSecond`, `answerBurst`; the authority: per player), `getAsset` 10 per second, burst 20, and 2 MB per asset (`assetsPerSecond`, `assetBurst`, `assetBytes`), `reportError` 5 per second, burst 10, more dropped without an error (`errorReportsPerSecond`, `errorReportBurst`), and `serviceResultBytes` (256 KB, the service result limit that was already in the SPEC).
|
|
13
|
+
* `RELAY_LIMITS.frameBytes` (in `@memizy/protocol/internal`) is 3 MB, so that a 2 MB asset fits a relay frame as base64.
|
|
14
|
+
* Since 1.0.0-rc.5 (details in the SPEC changelog "1.0"): the term *host* (3.1) and `sessionEnded` before a player's instance is removed (`kicked`, `closed`, `host-left`); `exit`, `resize`, `setChanged` and `saveProgress` removed and reserved (SPEC 10); `InitPayload.services` and all `SessionClock` fields always present; the relay type `RoomPlayer`; state patches carry their format (`f`); reserved names `$…`, `data-mz-…`, `mz-…`; the `@memizy/protocol/internal` entry point (not public API); Zod 4 for the 1.x line; the error code `UNKNOWN_METHOD`; the sandbox allows popups that escape it (links in a new tab); `appSpecific.memizy.set`; OQSE 1.0 sets and OQSEM 1.0 manifests.
|
|
15
|
+
|
|
16
|
+
## 1.0.0-rc.5
|
|
17
|
+
|
|
18
|
+
Protocol RC4 as a package: handshake, views and authority, messages, learning progress, plugin data, services, pauses, permissions with a content policy, display order and public items, limits and errors.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 memizy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Types, Zod schemas and helpers of the **Memizy Plugin Protocol v1** – the contract between Memizy plugins (`@memizy/plugin-sdk`) and hosts (`@memizy/host-sdk`).
|
|
4
4
|
|
|
5
|
-
* Specification: [SPEC.md](./SPEC.md) (
|
|
6
|
-
* Plugin authors do not use this package directly – see the [AI plugin guide](
|
|
5
|
+
* Specification: [SPEC.md](./SPEC.md) (1.0); changes of the package: [CHANGELOG.md](./CHANGELOG.md)
|
|
6
|
+
* Plugin authors do not use this package directly – see the [AI plugin guide](https://github.com/memizy/memizy/blob/main/docs/ai-plugin-guide.md).
|
|
7
7
|
|
|
8
8
|
```ts
|
|
9
9
|
import {
|
|
10
10
|
readPluginManifestFromHtml, // validate a plugin's manifest data island without running it
|
|
11
11
|
resolveSettings, // apply defaults / validate setting values
|
|
12
12
|
negotiateProtocolVersion, // handshake version negotiation
|
|
13
|
-
|
|
14
|
-
assertJsonWithin, LIMITS, // size limits
|
|
13
|
+
LIMITS, // size and rate limits (SPEC 8.1)
|
|
15
14
|
ProtocolError, toProtocolError,
|
|
16
15
|
type HostApi, type PluginApi, type InitPayload,
|
|
17
16
|
} from '@memizy/protocol';
|
|
@@ -19,3 +18,6 @@ import {
|
|
|
19
18
|
const result = readPluginManifestFromHtml(html);
|
|
20
19
|
if (result.success) console.log(result.runtime.views); // e.g. ['solo', 'board', 'controller']
|
|
21
20
|
```
|
|
21
|
+
|
|
22
|
+
* **Public API** = what `@memizy/protocol` exports (semver within 1.x, SPEC 9). Helpers shared by the Memizy packages – call parsing (`parseHostCall`), size checks (`assertJsonWithin`), rate limiting, content policies of the iframe, relay limits – are in **`@memizy/protocol/internal`** and may change in any release.
|
|
23
|
+
* The exported `*Schema` objects are **Zod 4** schemas; the 1.x line stays on Zod 4.
|
package/SPEC.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Memizy Plugin Protocol v1
|
|
2
2
|
|
|
3
|
-
> Status: **
|
|
4
|
-
>
|
|
5
|
-
> RC rules: the design does not change unless the implementation or the acceptance test proves that something does not work. Every change until `1.0` is recorded in the changelog at the end.
|
|
3
|
+
> Status: **1.0** (2026-10-14; protocol version `1.0`, packages `1.0.0`). The contract is frozen: within MAJOR `1` it changes only additively (section 9). Every change is recorded in the changelog at the end.
|
|
6
4
|
|
|
7
5
|
This document defines the contract between a **Memizy plugin** (an HTML page running in a sandboxed iframe) and a **Memizy host** (the Memizy app, the Plugin Lab, or any other application embedding plugins through `@memizy/host-sdk`).
|
|
8
6
|
|
|
@@ -15,9 +13,9 @@ It covers:
|
|
|
15
13
|
5. **message routing** for multiplayer,
|
|
16
14
|
6. **limits, errors, security and compatibility rules**.
|
|
17
15
|
|
|
18
|
-
It does **not** cover the high-level game API (`defineGame`) – that lives in `@memizy/plugin-sdk`, which every plugin bundles (see [the AI plugin guide](
|
|
16
|
+
It does **not** cover the high-level game API (`defineGame`) – that lives in `@memizy/plugin-sdk`, which every plugin bundles (see [the AI plugin guide](https://github.com/memizy/memizy/blob/main/docs/ai-plugin-guide.md)). It also does not cover the host ↔ multiplayer-server wire protocol, which is internal to `@memizy/host-sdk`.
|
|
19
17
|
|
|
20
|
-
Data formats (sets, items, progress) are defined by [OQSE](
|
|
18
|
+
Data formats (sets, items, progress) are defined by [OQSE](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse.md).
|
|
21
19
|
|
|
22
20
|
---
|
|
23
21
|
|
|
@@ -32,7 +30,7 @@ Data formats (sets, items, progress) are defined by [OQSE](../oqse/oqse.md).
|
|
|
32
30
|
|
|
33
31
|
## 2. Plugin Manifest
|
|
34
32
|
|
|
35
|
-
The manifest is an [OQSEM](
|
|
33
|
+
The manifest is an [OQSEM](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse-manifest.md) document embedded in the plugin HTML as a data island. The host reads it **without executing the plugin** (registry, upload in the Lab).
|
|
36
34
|
|
|
37
35
|
```html
|
|
38
36
|
<script type="application/oqse-manifest+json">
|
|
@@ -66,7 +64,7 @@ The manifest is an [OQSEM](../oqse/oqse-manifest.md) document embedded in the pl
|
|
|
66
64
|
</script>
|
|
67
65
|
```
|
|
68
66
|
|
|
69
|
-
The OQSEM part describes **content** (item types, assets, features) and is used for the [OQSE handshake](
|
|
67
|
+
The OQSEM part describes **content** (item types, assets, features) and is used for the [OQSE handshake](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse-manifest.md#the-handshake-matching-process) (`checkCompatibility`). The Memizy part (`appSpecific.memizy`) describes the **runtime**:
|
|
70
68
|
|
|
71
69
|
| Key | Type | Required | Description |
|
|
72
70
|
| :--- | :--- | :--- | :--- |
|
|
@@ -80,9 +78,9 @@ The OQSEM part describes **content** (item types, assets, features) and is used
|
|
|
80
78
|
| `settings` | SettingDefinition[] | No | Settings of the game (see below). Default `[]`. They define types, defaults and limits; the host validates every value against them. Used in both modes (section 3.3). |
|
|
81
79
|
| `settingsScreen` | `{ size }` | No | Multiplayer lobby only. Present = the plugin renders its own settings screen (view `settings`) on the host's device; absent = the host generates a form from `settings`. `size: "compact"` – a panel next to the player list (the user may expand it to full screen); `size: "large"` – opened as a full-screen dialog from a "Game settings" button. |
|
|
82
80
|
| `display.orientation` | `"any"` \| `"portrait"` \| `"landscape"` | No | Preferred orientation of player screens. Default `"any"`. |
|
|
83
|
-
| `permissions.network` | string[] | No |
|
|
84
|
-
| `permissions.devices` | (`"camera"` \| `"microphone"` \| `"geolocation"` \| `"serial"` \| `"bluetooth"`)[] | No |
|
|
85
|
-
| `services` | string[] | No |
|
|
81
|
+
| `permissions.network` | string[] | No | Origins the plugin talks to with `fetch` / WebSocket, as `"https://host[:port]"` or `"wss://host[:port]"` (no path; max 20). Shown to users and enforced (section 8.4). |
|
|
82
|
+
| `permissions.devices` | (`"camera"` \| `"microphone"` \| `"geolocation"` \| `"serial"` \| `"bluetooth"`)[] | No | Devices the plugin uses; the host grants only these (section 8.4). |
|
|
83
|
+
| `services` | string[] | No | Memizy services the plugin calls (section 5.3), e.g. `"chess.puzzles"`: lowercase segments joined by dots. A host that does not offer one can still run the plugin; the call then fails with `SERVICE_UNAVAILABLE`. |
|
|
86
84
|
|
|
87
85
|
**SettingDefinition:** `{ id, type, label, default, description?, modes?, ...typeSpecific }`
|
|
88
86
|
|
|
@@ -107,18 +105,19 @@ Reserved for later minor versions: `teams`, editing permissions.
|
|
|
107
105
|
|
|
108
106
|
| Term | Meaning |
|
|
109
107
|
| :--- | :--- |
|
|
108
|
+
| **Host** | The application that embeds the plugin on a device and connects it to the session (the Memizy app, the Plugin Lab, on a player's device the player's app). A plugin only ever talks to its host; how a host connects the devices of a session is the host's business. |
|
|
110
109
|
| **Session** | One run of a game, from start to end (e.g., one quiz in a lesson). |
|
|
111
110
|
| **Mode** | `solo` (one person plays alone) or `multiplayer` (several devices together). |
|
|
112
111
|
| **Host role** (`hostAs`) | In multiplayer, the role of the person who created the session: `presenter` (shows a shared board, e.g. a teacher at a projector, and does not play) or `player` (plays like everyone else, no board). A plugin may support both; the host chooses in the lobby. |
|
|
113
112
|
| **Instance** | One running plugin iframe on one device. 30 phones + 1 projector = 31 instances. |
|
|
114
113
|
| **View** | What one instance shows: `solo`, `board` (the presenter's shared screen), `controller` (one player's own screen – a phone, tablet or computer) or `settings` (the plugin's settings screen on the host's device in the multiplayer lobby). Views are derived from the manifest (section 3.2), not declared separately. |
|
|
115
114
|
| **Player** | A person who plays and can score. The presenter is not a player. |
|
|
116
|
-
| **Address** | The "envelope address" of an instance, used to route messages (section 6): the player ID for a player's instance (stable across reconnects), `"board"` for the presenter's board. The address `"server"` is **reserved** for a future server-side authority. Plugin authors never see addresses; the SDK exposes only player IDs. |
|
|
115
|
+
| **Address** | The "envelope address" of an instance, used to route messages (section 6): the player ID for a player's instance (stable across reconnects), `"board"` for the presenter's board, `"settings"` (`SETTINGS_ADDRESS`) for the plugin's settings screen in the lobby. The address `"server"` is **reserved** for a future server-side authority. Plugin authors never see addresses; the SDK exposes only player IDs. |
|
|
117
116
|
| **Authority** | The one place that owns the game state and applies actions: the board (`presenter`), the host's controller (host as `player`) or the only instance (`solo`). Other instances send actions to it and receive the new state. In a future version the authority may be the server (address `"server"`); this is why game logic must be deterministic. |
|
|
118
117
|
| **State** | The whole state of the game: one JSON value owned by the authority; its content is defined by the plugin. |
|
|
119
118
|
| **Action** | An intent of a player or a timer (e.g., "answered B"). Only actions change the state. |
|
|
120
119
|
| **Snapshot** | A saved copy of the authority's state (including pending timers), used to resume the game when the authority instance is recreated (reload, crash). Late joiners and reconnecting players do not use it; they synchronize with the running authority. |
|
|
121
|
-
| **Learning progress** | Per-item learning records of a player in [OQSEP](
|
|
120
|
+
| **Learning progress** | Per-item learning records of a player in [OQSEP](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse-progress.md) (buckets, review dates). Understood and scheduled by the host. |
|
|
122
121
|
| **Plugin data** | Game progress of a player that only the plugin understands (unlocked levels, coins, best scores). Stored by the host, opaque to it (section 5.2). |
|
|
123
122
|
|
|
124
123
|
| Situation | Instances (views) | Authority |
|
|
@@ -144,7 +143,7 @@ A plugin does not declare views. They follow from what it declares, so the two c
|
|
|
144
143
|
|
|
145
144
|
**Multiplayer:**
|
|
146
145
|
|
|
147
|
-
1. **Lobby** (host UI). The host chooses the plugin and the set; players join and may
|
|
146
|
+
1. **Lobby** (host UI). The host chooses the plugin and the set; players join. Player names are display names chosen by the host app – Memizy Play gives every player a random nickname and does not ask for real names (privacy of students); a host may let players change it. Names may change during the game (`playersChanged`). Plugins MUST NOT ask players for their real names or other personal data; a game that needs a name uses `Player.name`. The settings are edited by the host: either in a form generated from `settings`, or – if the manifest declares `settingsScreen` – in the plugin itself running in view `settings` on the host's device (the plugin calls `updateSettings` and receives `playersChanged`). Start is enabled only while the settings are valid.
|
|
148
147
|
2. **Loading.** After Start, the host creates the game instances (board and/or controllers). Each instance connects, receives its `InitPayload` (without game state) and calls `ready()`.
|
|
149
148
|
3. **Countdown.** When all connected instances are ready (or after a host-defined timeout, e.g. 15 s), the host shows a countdown and then calls `start()` on the authority. The authority creates the initial state and the game runs. The barrier exists so that timed games do not start before slower devices have loaded.
|
|
150
149
|
4. **Running.** Late joiners (if `lateJoin`) and reconnecting players get a new instance, which synchronizes with the authority. A recreated authority receives the last `snapshot` and continues without `start()`. While the authority is unreachable, see section 6.1.
|
|
@@ -152,6 +151,8 @@ A plugin does not declare views. They follow from what it declares, so the two c
|
|
|
152
151
|
|
|
153
152
|
**Solo:** one instance in view `solo`. There is **no plugin settings screen and no countdown**. Before creating the instance, the host MAY show a form generated from the settings that apply to solo (`modes`); otherwise it uses the defaults or values it presets (e.g. a course step "practise this set with 10 questions"). It calls `start()` right after `ready()`. Choices that belong to the game itself (a level, a character) are made inside the game, typically as its first phase.
|
|
154
153
|
|
|
154
|
+
**Continuing a saved game** (a host feature; nothing changes for plugins). A host may save a game that has not ended – the authority's last snapshot, the settings, the display-order seed (4.4) and the players – and continue it later, in multiplayer also in a new room with other devices. The continued session keeps the same session ID and the same player IDs, so the game state stays valid: the authority is recreated from the snapshot (`InitPayload.snapshot`, no `start()`), the session starts paused (`SessionClock`, so the time between the lessons does not count), and each device takes the place of one earlier player (the host decides how; Memizy Play lets the player pick the earlier nickname and the teacher change it). A place without a device is a disconnected player (6). A game that ended with `end(result)` cannot be continued (its snapshot is discarded, 5).
|
|
155
|
+
|
|
155
156
|
---
|
|
156
157
|
|
|
157
158
|
## 4. Handshake
|
|
@@ -203,18 +204,18 @@ interface InitPayload {
|
|
|
203
204
|
settings: Record<string, unknown>; // current values for every declared setting (defaults applied)
|
|
204
205
|
config: { locale: string; theme: 'light' | 'dark' };
|
|
205
206
|
clock: SessionClock; // session clock and pauses (below)
|
|
206
|
-
services
|
|
207
|
+
services: string[]; // services declared by the plugin AND offered by this host
|
|
207
208
|
progress: Record<string, ProgressRecord>; // learning progress of `self` (empty for board/settings)
|
|
208
209
|
data: { plugin: unknown; set: unknown }; // plugin data of `self` (null when nothing saved; null for board/settings)
|
|
209
210
|
snapshot: unknown | null; // last saved snapshot, only for the authority (resume)
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
type Address = string; // player ID, "board"; "server" is reserved
|
|
213
|
+
type Address = string; // player ID, "board", "settings"; "server" is reserved
|
|
213
214
|
|
|
214
215
|
interface SessionClock {
|
|
215
216
|
offsetMs: number; // Date.now() + offsetMs = session time
|
|
216
|
-
pausedMs
|
|
217
|
-
pausedAt
|
|
217
|
+
pausedMs: number; // total of finished pauses
|
|
218
|
+
pausedAt: number | null; // session time when the current pause began (null = running)
|
|
218
219
|
}
|
|
219
220
|
// Game time = (pausedAt ?? session time) - pausedMs. The SDK uses game time for ctx.now,
|
|
220
221
|
// timers and countdowns, so a pause stops them everywhere and nothing jumps afterwards.
|
|
@@ -234,15 +235,15 @@ interface Player {
|
|
|
234
235
|
* A host supports a set of protocol versions, one per MAJOR version (e.g. `1.4` and `2.1`). The plugin's MAJOR version must be one of them. Otherwise the host rejects with `ProtocolError { code: 'UNSUPPORTED_PROTOCOL' }` and shows the user a clear message ("this plugin needs a newer Memizy" or "this plugin is too old").
|
|
235
236
|
* The negotiated version has the plugin's MAJOR and the lower of both MINOR versions. Neither side uses a method, field or feature introduced in a higher minor than the negotiated one.
|
|
236
237
|
* A plugin that uses a field, value, method or setting type introduced in protocol 1.*x* declares `protocol: "1.x"` in its manifest (the minimum version it needs). A host that supports only an older minor version then rejects it before loading, instead of misreading what it does not know.
|
|
237
|
-
* `manifest.appSpecific.memizy.protocol` is checked **before** loading the iframe (the host does not offer plugins it cannot run). The manifest itself (`version`, the OQSEM format) follows the [OQSE version rules](
|
|
238
|
+
* `manifest.appSpecific.memizy.protocol` is checked **before** loading the iframe (the host does not offer plugins it cannot run). The manifest itself (`version`, the OQSEM format) follows the [OQSE version rules](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse.md#versioning-and-compatibility): the host reads the OQSEM MAJOR versions it supports.
|
|
238
239
|
* A host SHOULD keep supporting older MAJOR versions as long as practical (e.g. an adapter in `@memizy/host-sdk`), so older plugins keep working.
|
|
239
240
|
|
|
240
241
|
### 4.4 Display order and hidden answers
|
|
241
242
|
|
|
242
243
|
The host prepares the set once per session (`prepareDisplaySet` in `@memizy/protocol`; a future server authority does the same):
|
|
243
244
|
|
|
244
|
-
1. **Display order.** With a seed (normally the session id), the host shuffles `options` (mcq, unless `shuffle: false`), the options of `fill-in-select` blanks (unless `shuffle: false`), `items` of `sort-items` (never left in the correct order), `prompts` and `matches` of `match-pairs
|
|
245
|
-
2. **Public items.** In multiplayer, every instance except the authority gets the items without the fields that give the answer away and with `answerHidden: true`: `explanation`, `incorrectFeedback`, the `explanation` of every choice, `correctId`, `correctIds`, `correctAnswer(s)`, `
|
|
245
|
+
1. **Display order.** With a seed (normally the session id), the host shuffles `options` (mcq, unless `shuffle: false`), the options of `fill-in-select` blanks (unless `shuffle: false`), `items` of `sort-items` (never left in the correct order), `prompts` and `matches` of `match-pairs` and `match-complex`, `events` of `timeline` (unless `shuffle: false`; never left in chronological order), the entries of `categorize` and the `labels` of `diagram-label` (unless `requireTyping`). OQSE answers refer to choice IDs, so shuffling never changes an answer. Every instance sees the same order.
|
|
246
|
+
2. **Public items.** In multiplayer, every instance except the authority gets the items without the fields that give the answer away and with `answerHidden: true`: `explanation`, `incorrectFeedback`, the `explanation` of every choice, `correctId`, `correctIds`, `correctAnswer(s)`, `correctOrder`, `pairs`, `connections`, `correctCells`, `correctCategoryId`, zone `correctLabelId` (and the labels when `requireTyping`), the accepted texts of `fill-in-blanks`, the `correctId` of select blanks, event `date`s, `hotspots` of pin items, `back` of a flashcard, `hiddenContent` of a note, `sampleAnswer`, `rubric` and the `quote` of every source (`sources[].quote`, which often states the answer). Custom `x-` types lose `solution` (OQSE puts everything that gives their answer away there), `explanation` and `incorrectFeedback`, and also get `answerHidden: true`. `hints` stay public: a game may show them on any device while the player answers. A host that sends the game to other devices sends only public items.
|
|
246
247
|
3. **Revealing.** The authority sends full items to the other instances in its own messages (the plugin SDK's `ctx.reveal`); the host does not take part.
|
|
247
248
|
|
|
248
249
|
Solo instances and the authority get the full items in display order.
|
|
@@ -255,20 +256,17 @@ All methods return Promises and reject with a `ProtocolError` (section 8.2). Met
|
|
|
255
256
|
|
|
256
257
|
| Method | Description |
|
|
257
258
|
| :--- | :--- |
|
|
258
|
-
| `hello(handshake): InitPayload` | Handshake (section 4). Called exactly once
|
|
259
|
+
| `hello(handshake): InitPayload` | Handshake (section 4). Called exactly once per instance; a second call rejects with `INVALID_ARGUMENT`. |
|
|
259
260
|
| `ready(): void` | The plugin is rendered and interactive. |
|
|
260
261
|
| `send(message: OutgoingMessage): void` | Sends a game message to other instances (section 6). |
|
|
261
|
-
| `saveSnapshot(snapshot: unknown): void` | *authority.* Stores the latest resumable game state (overwrites the previous one). Returned as `InitPayload.snapshot` when the authority instance is recreated (reload, crash, reconnect). |
|
|
262
|
+
| `saveSnapshot(snapshot: unknown): void` | *authority.* Stores the latest resumable game state (overwrites the previous one). Returned as `InitPayload.snapshot` when the authority instance is recreated (reload, crash, reconnect). After `end(result)` the host discards the snapshot: a finished game is not resumed. |
|
|
262
263
|
| `recordAnswer(answer: AnswerRecord): void` | Records a learning result. The host applies its learning algorithm and stores the result in the learning progress of the given player (routing it to that player's device in multiplayer). |
|
|
263
|
-
| `saveProgress(records: Record<string, ProgressRecord>): void` | *views `solo`, `controller`.* Overwrites learning progress records of `self` directly (plugins with their own rating, e.g. "traffic light" self-rating). |
|
|
264
264
|
| `saveData(scope: 'plugin' \| 'set', value: unknown): void` | *views `solo`, `controller`.* Replaces the plugin data of `self` in the given scope (section 5.2). |
|
|
265
265
|
| `updateSettings(update: { values: Record<string, unknown>; valid: boolean; message?: string }): void` | *view `settings`.* Replaces the settings values. The host validates them against the manifest schema; `valid: false` disables Start and shows `message`. |
|
|
266
266
|
| `getAsset(key: string, itemId?: string): Blob` | Raw data of an asset (resolution: item assets, then set assets). |
|
|
267
|
-
| `end(result: SessionResult): void` | *authority.* The game is over. The host shows/stores the result; instances stay open until the host closes them. |
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
270
|
-
| `exit(): void` | The user wants to leave (e.g. a "Close" button inside the plugin). |
|
|
271
|
-
| `service(name: string, payload: unknown): unknown` | RC4. Calls a Memizy service (section 5.3). Rejects with `SERVICE_UNAVAILABLE` unless the plugin declares it and `InitPayload.services` lists it. |
|
|
267
|
+
| `end(result: SessionResult): void` | *authority.* The game is over. The host shows/stores the result; instances stay open until the host closes them. Only the first call counts: later calls (also from a recreated authority) resolve and are ignored. |
|
|
268
|
+
| `reportError(error: { code: string; message: string; context?: object }): void` | Non-fatal error for logs and the Lab test report. Reports beyond the rate in section 8.1 are dropped without an error. |
|
|
269
|
+
| `service(name: string, payload: unknown): unknown` | Calls a Memizy service (section 5.3). Rejects with `SERVICE_UNAVAILABLE` unless the plugin declares it and `InitPayload.services` lists it. |
|
|
272
270
|
|
|
273
271
|
```ts
|
|
274
272
|
interface AnswerRecord {
|
|
@@ -279,8 +277,8 @@ interface AnswerRecord {
|
|
|
279
277
|
timeSpentMs?: number;
|
|
280
278
|
hintsUsed?: number;
|
|
281
279
|
isSkipped?: boolean;
|
|
282
|
-
answer?: unknown; //
|
|
283
|
-
item?: { id: string; type: string; [key: string]: unknown }; //
|
|
280
|
+
answer?: unknown; // what the player answered (checkAnswer format, choice IDs; max 4 KB)
|
|
281
|
+
item?: { id: string; type: string; [key: string]: unknown }; // required for an item that is not in the set
|
|
284
282
|
}
|
|
285
283
|
|
|
286
284
|
interface SessionResult {
|
|
@@ -291,14 +289,11 @@ interface SessionResult {
|
|
|
291
289
|
|
|
292
290
|
### 5.1 Learning progress
|
|
293
291
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
* **`recordAnswer`** – the plugin reports what happened ("player X answered item Y correctly"). The host decides the new bucket and review date with its own algorithm. This is what most plugins use, in solo and in multiplayer.
|
|
297
|
-
* **`saveProgress`** – the plugin writes records of its own player directly (e.g., self-rating sets bucket 3). Only for `self`, never for other players.
|
|
292
|
+
Plugins write it with **`recordAnswer`**: the plugin reports what happened ("player X answered item Y correctly"); the host decides the new bucket and review date with its own algorithm. A self-rating (a flashcard) is an answer too (OQSE Answer Formats). Writing progress records directly is reserved (`saveProgress`, section 10).
|
|
298
293
|
|
|
299
|
-
|
|
294
|
+
What `recordAnswer` carries besides the result:
|
|
300
295
|
|
|
301
|
-
* `answer` – what the player answered, in the format of the item type defined in [OQSE Answer Formats](
|
|
296
|
+
* `answer` – what the player answered, in the format of the item type defined in [OQSE Answer Formats](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse.md#answer-formats) (the format `checkAnswer` takes: choice IDs, text, numbers; a flashcard's self-rating 1–4). Teachers see which wrong answers were chosen; because answers use IDs, the statistics survive edits of the set.
|
|
302
297
|
* **Generated items** (from a service or a generator in the plugin) are not in the set. The plugin attaches the item (`item`, at least `id` and `type`; answer fields may be left out; max 32 KB). The host counts such answers in statistics (per `skills` and `tags`) but not in the repetition schedule, because the item never comes back.
|
|
303
298
|
* Progress for teachers is aggregated per OQSE `skills` (and their parents, `chess.tactics` for `chess.tactics.fork`), which come from the content, never from the plugin. A game's own model of the player (rating, level, unlocked chapters) belongs to plugin data (section 5.2).
|
|
304
299
|
|
|
@@ -354,7 +349,7 @@ If the authority instance loses its connection or is being recreated (e.g., the
|
|
|
354
349
|
|
|
355
350
|
* The host calls `authorityChanged({ connected: false })` on all other instances and shows them a "Waiting for the host…" overlay. Messages sent `to: 'authority'` meanwhile are rejected with `AUTHORITY_UNAVAILABLE` (the SDK does not queue them).
|
|
356
351
|
* When the authority is back (resumed from its snapshot), the host calls `authorityChanged({ connected: true })`; the other instances re-synchronize with it.
|
|
357
|
-
* If the authority does not come back within a host-defined time, the host ends the session (`sessionEnded('
|
|
352
|
+
* If the authority does not come back within a host-defined time, the host ends the session (`sessionEnded('host-left')`). Handing the game over to another player is a future feature (section 10).
|
|
358
353
|
|
|
359
354
|
---
|
|
360
355
|
|
|
@@ -366,10 +361,9 @@ If the authority instance loses its connection or is being recreated (e.g., the
|
|
|
366
361
|
| `deliver(message: IncomingMessage): void` | A game message from another instance. |
|
|
367
362
|
| `playersChanged(players: Player[]): void` | The roster changed (join, leave, rename, connection status). Always the full list. |
|
|
368
363
|
| `authorityChanged(status: { connected: boolean }): void` | The authority became unreachable / reachable again (section 6.1). Not called on the authority itself. |
|
|
369
|
-
| `setChanged(set: { meta, items }): void` | The set was edited in the host while the plugin runs (solo only). |
|
|
370
364
|
| `configChanged(config: { locale, theme }): void` | Locale or theme changed. |
|
|
371
|
-
| `clockChanged(clock: SessionClock): void` | Clock synchronization updated, or
|
|
372
|
-
| `sessionEnded(reason: 'finished' \| '
|
|
365
|
+
| `clockChanged(clock: SessionClock): void` | Clock synchronization updated, or the host paused / resumed the game. While `pausedAt` is set the SDK stops game time and timers, ignores actions and covers the game with a "paused" curtain. |
|
|
366
|
+
| `sessionEnded(reason: 'finished' \| 'host-left' \| 'kicked' \| 'closed' \| 'error'): void` | The host is closing this instance. |
|
|
373
367
|
|
|
374
368
|
---
|
|
375
369
|
|
|
@@ -382,13 +376,17 @@ The values are **guaranteed minimums**: a host MUST accept at least this much an
|
|
|
382
376
|
| Limit | Guaranteed minimum | When exceeded |
|
|
383
377
|
| :--- | :--- | :--- |
|
|
384
378
|
| `send` message size (serialized JSON) | 64 KB | `MESSAGE_TOO_LARGE` |
|
|
385
|
-
| `send` rate per instance | 30 messages/s (burst 60); the authority: 30 messages/s **per other instance** (
|
|
379
|
+
| `send` rate per instance | 30 messages/s (burst 60); the authority: 30 messages/s **per other instance** (it sends each device its own view) | `RATE_LIMITED` |
|
|
386
380
|
| `saveSnapshot` size | 1 MB | `SNAPSHOT_TOO_LARGE` |
|
|
387
381
|
| `saveSnapshot` rate | 2 per second (the host keeps the latest) | coalesced, no error |
|
|
388
382
|
| `saveData` size per scope | 256 KB | `DATA_TOO_LARGE` |
|
|
389
383
|
| `saveData` rate | 1 per second per scope (the host keeps the latest) | coalesced, no error |
|
|
390
384
|
| `hello` deadline | 10 s after iframe load | the host shows an error |
|
|
391
385
|
| `recordAnswer` answer / generated item | 4 KB / 32 KB | `DATA_TOO_LARGE` |
|
|
386
|
+
| `recordAnswer` rate per instance | 10 per second (burst 20); the authority: 10 per second **per player** (it records for everyone) | `RATE_LIMITED` |
|
|
387
|
+
| `getAsset` size | 2 MB per asset | `DATA_TOO_LARGE` |
|
|
388
|
+
| `getAsset` rate per instance | 10 per second (burst 20) | `RATE_LIMITED` |
|
|
389
|
+
| `reportError` rate per instance | 5 per second (burst 10) | dropped, no error |
|
|
392
390
|
| `service` request / result | 64 KB / 256 KB | `MESSAGE_TOO_LARGE` |
|
|
393
391
|
| `service` rate per instance | 30 calls/min | `RATE_LIMITED` |
|
|
394
392
|
|
|
@@ -415,7 +413,8 @@ interface ProtocolError {
|
|
|
415
413
|
| `ASSET_NOT_FOUND` | `getAsset` with an unknown key. |
|
|
416
414
|
| `SESSION_ENDED` | A call after `sessionEnded`. |
|
|
417
415
|
| `INTERNAL_ERROR` | A host-side failure; the plugin may retry. |
|
|
418
|
-
| `SERVICE_UNAVAILABLE` |
|
|
416
|
+
| `SERVICE_UNAVAILABLE` | The service is not declared by the plugin or not offered by this host. |
|
|
417
|
+
| `UNKNOWN_METHOD` | The other side does not have the method (an older or newer version). Methods added in 1.x are guarded by a feature (9), so a plugin sees this only by mistake. |
|
|
419
418
|
|
|
420
419
|
New codes may be added in minor versions; plugins MUST treat unknown codes like `INTERNAL_ERROR`.
|
|
421
420
|
|
|
@@ -425,12 +424,12 @@ New codes may be added in minor versions; plugins MUST treat unknown codes like
|
|
|
425
424
|
|
|
426
425
|
### 8.4 Security
|
|
427
426
|
|
|
428
|
-
* The iframe is sandboxed with `allow-scripts allow-forms allow-modals allow-pointer-lock` – **never** `allow-same-origin`. As a consequence, browser storage (`localStorage`, IndexedDB) is not available to plugins; they use plugin data (section 5.2).
|
|
427
|
+
* The iframe is sandboxed with `allow-scripts allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox` – **never** `allow-same-origin` and never `allow-top-navigation`. Popups are allowed so that links open in a new tab (the plugin SDK opens every external link there; inside the iframe a link would replace the game). As a consequence, browser storage (`localStorage`, IndexedDB) is not available to plugins; they use plugin data (section 5.2).
|
|
429
428
|
* The host validates every incoming call against the schemas in `@memizy/protocol` and the limits above.
|
|
430
429
|
* The host never passes credentials, user e-mail or other private data. `Player.name` is the display name chosen for the session.
|
|
431
430
|
* Answers are known only to the authority (section 4.4). Everything else a plugin puts in the game state or in messages is visible to every instance: do not put secrets there.
|
|
432
|
-
* **Network
|
|
433
|
-
* **Devices
|
|
431
|
+
* **Network.** The host puts a Content Security Policy into the plugin document (`pluginContentPolicy` in `@memizy/protocol`): scripts only inline, from the library CDNs (`cdn.jsdelivr.net`, `unpkg.com`, `cdnjs.cloudflare.com`, `esm.sh`) and from the host; `fetch`, WebSocket and similar only to these and to the origins in `permissions.network`. Images and media may come from any `https:` URL (sets link them), so the policy blocks data channels but not every possible leak; hosts show the declared origins to users ("this game talks to lichess.org").
|
|
432
|
+
* **Devices.** The iframe `allow` attribute grants only `permissions.devices`; the browser still asks the user.
|
|
434
433
|
|
|
435
434
|
---
|
|
436
435
|
|
|
@@ -446,7 +445,9 @@ From `1.0` on, the following MUST NOT change in a backward-incompatible way with
|
|
|
446
445
|
|
|
447
446
|
Allowed in `1.x`: new optional fields, new methods guarded by a feature, new setting types, new enum values only where the receiver is required to ignore unknown values (`sessionEnded` reasons, error codes, `Player` fields).
|
|
448
447
|
|
|
449
|
-
* The plugin SDK sends its own game messages inside `send` / `deliver` (actions, state and state patches, reveals). Their format is part of the SDK's contract: within SDK 1.x it only changes additively, because the devices of one game may run different 1.x versions (a cached copy). Receivers ignore message types and fields they do not know.
|
|
448
|
+
* The plugin SDK sends its own game messages inside `send` / `deliver` (actions, state and state patches, reveals). Their format is part of the SDK's contract: within SDK 1.x it only changes additively, because the devices of one game may run different 1.x versions (a cached copy). Receivers ignore message types and fields they do not know. State patches carry the version of their format (`f`, today `1`: the patches of mutative 2); a receiver that does not know the format asks for the full state instead of applying them, so the patch format can change within 1.x.
|
|
449
|
+
* **Reserved names** (so that a game never collides with a later SDK): action names and timer keys starting with `$` (the SDK rejects them in a game); HTML attributes starting with `data-mz-` (new SDK attributes use this prefix; the existing `data-act`, `data-local`, `data-payload`, `data-setting`, `data-keep`, `data-key` stay); CSS classes starting with `mz-`. Fields the SDK keeps in the game state exist only on request: today `phase`, `phaseSeq` and `phaseEndsAt`, only in games with `phases`. Any later field of the SDK will also be opt-in (a new option of the game definition), never added to the state of an existing game.
|
|
450
|
+
* The public API of `@memizy/protocol` is what its entry point exports; `@memizy/protocol/internal` (helpers shared by the Memizy packages) is not covered by these rules. Exported Zod schemas are Zod 4 schemas: the 1.x packages stay on Zod 4.
|
|
450
451
|
* Libraries inside the SDK and the host SDK (sanitizer, Markdown parser…) may be updated in any 1.x release, including security fixes; only the wire formats above are fixed.
|
|
451
452
|
* Plugins load the SDK as `https://cdn.jsdelivr.net/npm/@memizy/plugin-sdk@1` (the self-contained bundle of the package): every 1.x release reaches them without a change of the plugin.
|
|
452
453
|
|
|
@@ -479,12 +480,17 @@ What was decided and why, and what is **reserved** (name and intent fixed, not i
|
|
|
479
480
|
| Feature `teams` | Teams managed by the host (today every game handles teams itself). |
|
|
480
481
|
| Hot-seat | Several players sharing one device; v1 assumes one player per instance. |
|
|
481
482
|
| Host migration | Moving the authority to another device when its device is gone for good; v1 resumes the same authority from the snapshot. |
|
|
483
|
+
| Host method `exit` | Leaving the game from inside it: a "back to the menu" button of a full-screen game; the host closes the game or returns to its menu. |
|
|
484
|
+
| Host method `resize` | Auto height for games embedded in articles and course pages: the game reports the height of its content, the host sizes the frame (switched on in the manifest; the SDK measures it). |
|
|
485
|
+
| Plugin method `setChanged` | Live set editing: the host updates the set of a running game, e.g. when the author edits it in the Lab. |
|
|
486
|
+
| Host method `saveProgress` | Writing learning progress directly, e.g. a placement test ("these items are known"); the host may refuse or limit it. |
|
|
487
|
+
| Feature `replay` | Recording a game and replaying it: the host keeps the session's inputs (the `InitPayload` with the seed and the display order, every action with its sender and game time, pauses) and replays the game by running the same plugin version on them. This works because the rules are deterministic (`ctx.random`, `ctx.now`, no `Math.random` / `Date.now` in actions); keep that rule. |
|
|
482
488
|
|
|
483
489
|
---
|
|
484
490
|
|
|
485
491
|
## Changelog
|
|
486
492
|
|
|
487
|
-
* **1.0 (
|
|
493
|
+
* **1.0 (2026-10-14):** the term *host* (3.1): the application that embeds the plugin on a device, also on a player's device; the host ends a player's instance with `sessionEnded` (also `kicked`, `host-left`) before removing it. Removed before 1.0 and reserved (10): `exit`, `resize`, `setChanged`, `saveProgress` (a self-rating is an answer). `InitPayload.services` and the pause fields of `SessionClock` are always present; `sessionEnded` reason `host-left` (was `host_left`); `SETTINGS_ADDRESS` is defined here; the relay type of a room player is `RoomPlayer`. State patches carry their format (`f`); reserved names `$…`, `data-mz-…`, `mz-…` and opt-in state fields; `@memizy/protocol/internal`; Zod 4 (9). Renamed before 1.0: `ui.renderNote` → `ui.note` (SDK), the type `Theme` → `HostConfig`. Player names and personal data: plugins MUST NOT ask for real names or other personal data (3.3); `setChanged` is optional for hosts (7); `replay` reserved (10). The sandbox allows popups that escape it, links open in a new tab (8.4). Error code `UNKNOWN_METHOD` (8.2). Continuing a saved game is described (3.3; host feature, no protocol change). After `end(result)` the host discards the snapshot (5). The transport (Penpal 8), the SDK message format (actions carry the `phaseSeq` the player saw and the tap time `actedAt`; devices of one game may run different SDK 1.x versions) and the protocol declaration of plugins using 1.x additions are fixed rules (4, 4.3, 9); plugins load the SDK from `…/plugin-sdk@1`. `appSpecific.memizy.set` (`required` / `optional` / `none`): games without questions; OQSEM `capabilities` and `actions` may be omitted. Public items (4.4) also lose `sources[].quote`; custom `x-` items lose `solution`, `explanation` and `incorrectFeedback` and get `answerHidden: true` (they were passed unchanged); `hints` stay public. `recordAnswer.answer` follows OQSE Answer Formats (a flashcard answer is a self-rating 1–4; `ui.question` offers four ratings). OQSE 1.0 sets and OQSEM 1.0 manifests (`"version": "1.0"`; other MAJOR versions are not read as 1.x). A host may support several protocol MAJOR versions at once (4.3); supporting older ones is a SHOULD instead of a MUST. Final rules: a second `hello` of an instance rejects with `INVALID_ARGUMENT`, only the first `end(result)` counts (5); guaranteed rates of `recordAnswer` (per player for the authority), `getAsset` and `reportError` (dropped beyond it) and an asset size of 2 MB (8.1); the address `"settings"` (3.1).
|
|
488
494
|
* **RC1 (2026-10-04):** first release candidate.
|
|
489
495
|
* **RC2 (2026-10-06):** `SettingDefinition.modes`; the host may show solo settings before the start (section 3.3). Additive: RC1 plugins and hosts keep working.
|
|
490
496
|
* **RC4 (2026-10-07):** OQSE 0.3 sets (choices with IDs; the RC3 `correctOrder` / `correctMatches` display fields are gone because answers refer to IDs); `recordAnswer.answer` and generated items (5.1); services (5.3, `service`, `InitPayload.services`, `SERVICE_UNAVAILABLE`); pauses (`SessionClock`); `permissions` with a Content Security Policy and device grants (8.4); decisions and reservations (section 10). Additive for the protocol except the OQSE version: plugins must read choices (`option.text`, `option.id`).
|
package/dist/errors.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=[`UNSUPPORTED_PROTOCOL`,`INVALID_ARGUMENT`,`NOT_AUTHORITY`,`NOT_ALLOWED_IN_VIEW`,`AUTHORITY_UNAVAILABLE`,`MESSAGE_TOO_LARGE`,`SNAPSHOT_TOO_LARGE`,`DATA_TOO_LARGE`,`RATE_LIMITED`,`ASSET_NOT_FOUND`,`SESSION_ENDED`,`INTERNAL_ERROR`,`SERVICE_UNAVAILABLE`],t=/^\[([A-Z_]+)\]\s?/,n=class extends Error{code;constructor(e,t){super(`[${e}] ${t}`),this.name=`ProtocolError`,this.code=e}get detail(){return this.message.replace(t,``)}};function r(t){return typeof t==`string`&&e.includes(t)}function i(e){if(e instanceof n)return e;let i=e instanceof Error?e.message:String(e)
|
|
1
|
+
var e=[`UNSUPPORTED_PROTOCOL`,`INVALID_ARGUMENT`,`NOT_AUTHORITY`,`NOT_ALLOWED_IN_VIEW`,`AUTHORITY_UNAVAILABLE`,`MESSAGE_TOO_LARGE`,`SNAPSHOT_TOO_LARGE`,`DATA_TOO_LARGE`,`RATE_LIMITED`,`ASSET_NOT_FOUND`,`SESSION_ENDED`,`INTERNAL_ERROR`,`SERVICE_UNAVAILABLE`,`UNKNOWN_METHOD`],t=/^\[([A-Z_]+)\]\s?/,n=class extends Error{code;constructor(e,t){super(`[${e}] ${t}`),this.name=`ProtocolError`,this.code=e}get detail(){return this.message.replace(t,``)}};function r(t){return typeof t==`string`&&e.includes(t)}function i(e){if(e instanceof n)return e;let i=e instanceof Error?e.message:String(e);if(e?.code===`METHOD_NOT_FOUND`)return new n(`UNKNOWN_METHOD`,i);let a=t.exec(i);return new n(a&&r(a[1])?a[1]:`INTERNAL_ERROR`,a?i.slice(a[0].length):i)}exports.PROTOCOL_ERROR_CODES=e,exports.ProtocolError=n,exports.isProtocolErrorCode=r,exports.toProtocolError=i;
|
package/dist/errors.js
CHANGED
|
@@ -12,7 +12,8 @@ var e = [
|
|
|
12
12
|
"ASSET_NOT_FOUND",
|
|
13
13
|
"SESSION_ENDED",
|
|
14
14
|
"INTERNAL_ERROR",
|
|
15
|
-
"SERVICE_UNAVAILABLE"
|
|
15
|
+
"SERVICE_UNAVAILABLE",
|
|
16
|
+
"UNKNOWN_METHOD"
|
|
16
17
|
], t = /^\[([A-Z_]+)\]\s?/, n = class extends Error {
|
|
17
18
|
code;
|
|
18
19
|
constructor(e, t) {
|
|
@@ -27,7 +28,9 @@ function r(t) {
|
|
|
27
28
|
}
|
|
28
29
|
function i(e) {
|
|
29
30
|
if (e instanceof n) return e;
|
|
30
|
-
let i = e instanceof Error ? e.message : String(e)
|
|
31
|
+
let i = e instanceof Error ? e.message : String(e);
|
|
32
|
+
if (e?.code === "METHOD_NOT_FOUND") return new n("UNKNOWN_METHOD", i);
|
|
33
|
+
let a = t.exec(i);
|
|
31
34
|
return new n(a && r(a[1]) ? a[1] : "INTERNAL_ERROR", a ? i.slice(a[0].length) : i);
|
|
32
35
|
}
|
|
33
36
|
//#endregion
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./version.cjs"),t=require("./errors.cjs"),n=require("./limits.cjs"),r=require("./settingsSchema.cjs"),i=require("./settings.cjs"),a=require("./pluginRuntime.cjs"),o=require("./manifestSchema.cjs"),s=require("./types.cjs"),c=require("./schemas.cjs"),l=require("./relay.cjs"),u=require("./items.cjs"),d=require("./security.cjs")
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./version.cjs"),t=require("./errors.cjs"),n=require("./limits.cjs"),r=require("./settingsSchema.cjs"),i=require("./settings.cjs"),a=require("./pluginRuntime.cjs"),o=require("./manifestSchema.cjs"),s=require("./types.cjs"),c=require("./schemas.cjs"),l=require("./relay.cjs"),u=require("./items.cjs"),d=require("./security.cjs");exports.AddressSchema=c.AddressSchema,exports.AnswerRecordSchema=c.AnswerRecordSchema,exports.BOARD_ADDRESS=s.BOARD_ADDRESS,exports.DataScopeSchema=c.DataScopeSchema,exports.ErrorReportSchema=c.ErrorReportSchema,exports.HOST_API_METHODS=s.HOST_API_METHODS,exports.HandshakeSchema=c.HandshakeSchema,exports.HostAsSchema=o.HostAsSchema,exports.LIMITS=n.LIMITS,exports.LocalizedTextSchema=r.LocalizedTextSchema,exports.MemizyRuntimeSchema=o.MemizyRuntimeSchema,exports.MultiplayerModeSchema=o.MultiplayerModeSchema,exports.OutgoingMessageSchema=c.OutgoingMessageSchema,exports.PLUGIN_API_METHODS=s.PLUGIN_API_METHODS,exports.PLUGIN_DEVICES=d.PLUGIN_DEVICES,exports.PROTOCOL_ERROR_CODES=t.PROTOCOL_ERROR_CODES,exports.PROTOCOL_VERSION=e.PROTOCOL_VERSION,exports.PluginManifestError=o.PluginManifestError,exports.PluginPermissionsSchema=o.PluginPermissionsSchema,exports.ProtocolError=t.ProtocolError,exports.RELAY_PROTOCOL=l.RELAY_PROTOCOL,exports.SERVER_ADDRESS=s.SERVER_ADDRESS,exports.SETTINGS_ADDRESS=s.SETTINGS_ADDRESS,exports.ServiceNameSchema=o.ServiceNameSchema,exports.SessionResultSchema=c.SessionResultSchema,exports.SettingDefinitionSchema=r.SettingDefinitionSchema,exports.SettingsUpdateSchema=c.SettingsUpdateSchema,exports.compareProtocolVersions=e.compareProtocolVersions,exports.extractManifestFromHtml=a.extractManifestFromHtml,exports.isProtocolErrorCode=t.isProtocolErrorCode,exports.isProtocolSupported=e.isProtocolSupported,exports.localize=i.localize,exports.negotiateProtocolVersion=e.negotiateProtocolVersion,exports.parsePluginManifest=o.parsePluginManifest,exports.parseProtocolVersion=e.parseProtocolVersion,exports.prepareDisplaySet=u.prepareDisplaySet,exports.readPluginManifestFromHtml=o.readPluginManifestFromHtml,exports.resolveSettings=i.resolveSettings,exports.safeParsePluginManifest=o.safeParsePluginManifest,exports.settingsForMode=i.settingsForMode,exports.toProtocolError=t.toProtocolError,exports.validateSettingValue=i.validateSettingValue;
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION as e, compareProtocolVersions as t, isProtocolSupported as n, negotiateProtocolVersion as r, parseProtocolVersion as i } from "./version.js";
|
|
2
2
|
import { PROTOCOL_ERROR_CODES as a, ProtocolError as o, isProtocolErrorCode as s, toProtocolError as c } from "./errors.js";
|
|
3
|
-
import { LIMITS as l
|
|
4
|
-
import { LocalizedTextSchema as
|
|
5
|
-
import { localize as
|
|
6
|
-
import { extractManifestFromHtml as
|
|
7
|
-
import { HostAsSchema as
|
|
8
|
-
import { BOARD_ADDRESS as
|
|
9
|
-
import { AddressSchema as
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
export { F as AddressSchema, I as AnswerRecordSchema, j as BOARD_ADDRESS, L as DataScopeSchema, R as ErrorReportSchema, z as HOST_API_ARGS, M as HOST_API_METHODS, B as HandshakeSchema, S as HostAsSchema, te as LIBRARY_ORIGINS, l as LIMITS, m as LocalizedTextSchema, C as MemizyRuntimeSchema, w as MultiplayerModeSchema, V as OutgoingMessageSchema, N as PLUGIN_API_METHODS, ne as PLUGIN_DEVICES, a as PROTOCOL_ERROR_CODES, e as PROTOCOL_VERSION, T as PluginManifestError, E as PluginPermissionsSchema, H as ProgressRecordsSchema, o as ProtocolError, q as RELAY_LIMITS, J as RELAY_PROTOCOL, u as RateLimiter, U as ResizeRequestSchema, P as SERVER_ADDRESS, D as ServiceNameSchema, W as SessionResultSchema, h as SettingDefinitionSchema, G as SettingsUpdateSchema, d as assertJsonWithin, t as compareProtocolVersions, Z as displayItem, oe as ensureConnectionPolyfills, b as extractManifestFromHtml, f as findNonJson, re as injectContentPolicy, Y as isPin, s as isProtocolErrorCode, n as isProtocolSupported, p as jsonByteSize, g as localize, r as negotiateProtocolVersion, X as normalizePlayerName, K as parseHostCall, O as parsePluginManifest, i as parseProtocolVersion, ie as pluginAllowAttribute, ae as pluginContentPolicy, Q as prepareDisplaySet, $ as publicItem, k as readPluginManifestFromHtml, _ as resolveSettings, A as safeParsePluginManifest, ee as seededRandom, v as settingsForMode, c as toProtocolError, x as toRuntime, y as validateSettingValue };
|
|
3
|
+
import { LIMITS as l } from "./limits.js";
|
|
4
|
+
import { LocalizedTextSchema as u, SettingDefinitionSchema as d } from "./settingsSchema.js";
|
|
5
|
+
import { localize as f, resolveSettings as p, settingsForMode as m, validateSettingValue as h } from "./settings.js";
|
|
6
|
+
import { extractManifestFromHtml as g } from "./pluginRuntime.js";
|
|
7
|
+
import { HostAsSchema as _, MemizyRuntimeSchema as v, MultiplayerModeSchema as y, PluginManifestError as b, PluginPermissionsSchema as x, ServiceNameSchema as S, parsePluginManifest as C, readPluginManifestFromHtml as w, safeParsePluginManifest as T } from "./manifestSchema.js";
|
|
8
|
+
import { BOARD_ADDRESS as E, HOST_API_METHODS as D, PLUGIN_API_METHODS as O, SERVER_ADDRESS as k, SETTINGS_ADDRESS as A } from "./types.js";
|
|
9
|
+
import { AddressSchema as j, AnswerRecordSchema as M, DataScopeSchema as N, ErrorReportSchema as P, HandshakeSchema as F, OutgoingMessageSchema as I, SessionResultSchema as L, SettingsUpdateSchema as R } from "./schemas.js";
|
|
10
|
+
import { RELAY_PROTOCOL as z } from "./relay.js";
|
|
11
|
+
import { prepareDisplaySet as B } from "./items.js";
|
|
12
|
+
import { PLUGIN_DEVICES as V } from "./security.js";
|
|
13
|
+
export { j as AddressSchema, M as AnswerRecordSchema, E as BOARD_ADDRESS, N as DataScopeSchema, P as ErrorReportSchema, D as HOST_API_METHODS, F as HandshakeSchema, _ as HostAsSchema, l as LIMITS, u as LocalizedTextSchema, v as MemizyRuntimeSchema, y as MultiplayerModeSchema, I as OutgoingMessageSchema, O as PLUGIN_API_METHODS, V as PLUGIN_DEVICES, a as PROTOCOL_ERROR_CODES, e as PROTOCOL_VERSION, b as PluginManifestError, x as PluginPermissionsSchema, o as ProtocolError, z as RELAY_PROTOCOL, k as SERVER_ADDRESS, A as SETTINGS_ADDRESS, S as ServiceNameSchema, L as SessionResultSchema, d as SettingDefinitionSchema, R as SettingsUpdateSchema, t as compareProtocolVersions, g as extractManifestFromHtml, s as isProtocolErrorCode, n as isProtocolSupported, f as localize, r as negotiateProtocolVersion, C as parsePluginManifest, i as parseProtocolVersion, B as prepareDisplaySet, w as readPluginManifestFromHtml, p as resolveSettings, T as safeParsePluginManifest, m as settingsForMode, c as toProtocolError, h as validateSettingValue };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./limits.cjs"),t=require("./pluginRuntime.cjs"),n=require("./schemas.cjs"),r=require("./relay.cjs"),i=require("./items.cjs"),a=require("./security.cjs"),o=require("./polyfills.cjs");exports.HOST_API_ARGS=n.HOST_API_ARGS,exports.LIBRARY_ORIGINS=a.LIBRARY_ORIGINS,exports.RELAY_LIMITS=r.RELAY_LIMITS,exports.RateLimiter=e.RateLimiter,exports.assertJsonWithin=e.assertJsonWithin,exports.displayItem=i.displayItem,exports.ensureConnectionPolyfills=o.ensureConnectionPolyfills,exports.findNonJson=e.findNonJson,exports.injectContentPolicy=a.injectContentPolicy,exports.isPin=r.isPin,exports.jsonByteSize=e.jsonByteSize,exports.normalizePlayerName=r.normalizePlayerName,exports.parseHostCall=n.parseHostCall,exports.pluginAllowAttribute=a.pluginAllowAttribute,exports.pluginContentPolicy=a.pluginContentPolicy,exports.publicItem=i.publicItem,exports.seededRandom=i.seededRandom,exports.toRuntime=t.toRuntime;
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RateLimiter as e, assertJsonWithin as t, findNonJson as n, jsonByteSize as r } from "./limits.js";
|
|
2
|
+
import { toRuntime as i } from "./pluginRuntime.js";
|
|
3
|
+
import { HOST_API_ARGS as a, parseHostCall as o } from "./schemas.js";
|
|
4
|
+
import { RELAY_LIMITS as s, isPin as c, normalizePlayerName as l } from "./relay.js";
|
|
5
|
+
import { displayItem as u, publicItem as d, seededRandom as f } from "./items.js";
|
|
6
|
+
import { LIBRARY_ORIGINS as p, injectContentPolicy as m, pluginAllowAttribute as h, pluginContentPolicy as g } from "./security.js";
|
|
7
|
+
import { ensureConnectionPolyfills as _ } from "./polyfills.js";
|
|
8
|
+
export { a as HOST_API_ARGS, p as LIBRARY_ORIGINS, s as RELAY_LIMITS, e as RateLimiter, t as assertJsonWithin, u as displayItem, _ as ensureConnectionPolyfills, n as findNonJson, m as injectContentPolicy, c as isPin, r as jsonByteSize, l as normalizePlayerName, o as parseHostCall, h as pluginAllowAttribute, g as pluginContentPolicy, d as publicItem, f as seededRandom, i as toRuntime };
|
package/dist/items.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let e=require("@memizy/oqse");function t(e){let t=2166136261;for(let n=0;n<e.length;n++)t=Math.imul(t^e.charCodeAt(n),16777619);let n=t>>>0;return()=>{n=n+1831565813>>>0;let e=n;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}}function n(e,t,n){for(let r=0;r<6;r++){let r=[...e];for(let e=r.length-1;e>0;e--){let n=Math.floor(t()*(e+1));[r[e],r[n]]=[r[n],r[e]]}if(!n||r.length<2||!n(r))return r}return[...e.slice(1),e[0]]}var r=e=>t=>t.every((t,n)=>t.id===e[n]);function i(t,i){let a=structuredClone(t);switch(a.type){case`mcq-single`:case`mcq-multi`:return a.shuffle!==!1&&(a.options=n(a.options,i)),a;case`fill-in-select`:for(let e of Object.values(a.blanks??{}))e.shuffle!==!1&&(e.options=n(e.options,i));return a;case`sort-items`:return a.items=n(a.items,i,r(a.correctOrder)),a;case`match-pairs`:return a.prompts=n(a.prompts,i),a.matches=n(a.matches,i),a;case`match-complex`:return a.
|
|
1
|
+
let e=require("@memizy/oqse");function t(e){let t=2166136261;for(let n=0;n<e.length;n++)t=Math.imul(t^e.charCodeAt(n),16777619);let n=t>>>0;return()=>{n=n+1831565813>>>0;let e=n;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}}function n(e,t,n){for(let r=0;r<6;r++){let r=[...e];for(let e=r.length-1;e>0;e--){let n=Math.floor(t()*(e+1));[r[e],r[n]]=[r[n],r[e]]}if(!n||r.length<2||!n(r))return r}return[...e.slice(1),e[0]]}var r=e=>t=>t.every((t,n)=>t.id===e[n]);function i(t,i){let a=structuredClone(t);switch(a.type){case`mcq-single`:case`mcq-multi`:return a.shuffle!==!1&&(a.options=n(a.options,i)),a;case`fill-in-select`:for(let e of Object.values(a.blanks??{}))e.shuffle!==!1&&(e.options=n(e.options,i));return a;case`sort-items`:return a.items=n(a.items,i,r(a.correctOrder)),a;case`match-pairs`:return a.prompts=n(a.prompts,i),a.matches=n(a.matches,i),a;case`match-complex`:return a.prompts=n(a.prompts,i),a.matches=n(a.matches,i),a;case`timeline`:return a.shuffle!==!1&&(a.events=n(a.events,i,t=>(0,e.isTimelineOrderCorrect)(t))),a;case`categorize`:return a.items=n(a.items,i),a;case`diagram-label`:return a.requireTyping||(a.labels=n(a.labels,i)),a;default:return a}}var a=[`explanation`,`incorrectFeedback`],o={note:[`hiddenContent`],flashcard:[`back`],"true-false":[`correctAnswer`],"mcq-single":[`correctId`],"mcq-multi":[`correctIds`],"short-answer":[`correctAnswers`],"sort-items":[`correctOrder`],"match-pairs":[`pairs`],"match-complex":[`connections`],slider:[`correctAnswer`],"numeric-input":[`correctAnswer`],"math-input":[`correctAnswers`],matrix:[`correctCells`],"open-ended":[`sampleAnswer`,`rubric`],"chess-puzzle":[`correctAnswers`]},s=[`options`,`items`,`prompts`,`matches`,`categories`,`rows`,`columns`,`labels`];function c(e){let t=e.type,n=structuredClone(e);for(let e of a)delete n[e];if(Array.isArray(n.sources))for(let e of n.sources)e&&typeof e==`object`&&delete e.quote;if(t.startsWith(`x-`))return delete n.solution,n.answerHidden=!0,n;for(let e of o[t]??[])delete n[e];for(let e of s)if(Array.isArray(n[e]))for(let t of n[e])t&&typeof t==`object`&&delete t.explanation;switch(t){case`fill-in-blanks`:n.blanks=Object.fromEntries(Object.keys(n.blanks??{}).map(e=>[e,[]]));break;case`fill-in-select`:for(let e of Object.values(n.blanks??{})){delete e.correctId;for(let t of e.options??[])delete t.explanation}break;case`categorize`:for(let e of n.items??[])delete e.correctCategoryId;break;case`timeline`:for(let e of n.events??[])delete e.date,delete e.precision;break;case`diagram-label`:for(let e of n.zones??[])delete e.correctLabelId;n.requireTyping&&(n.labels=[]);break;case`pin-on-image`:case`pin-on-model`:n.hotspots=[]}return n.answerHidden=!0,n}function l(e,n){let r=n===void 0?null:t(n),a=e.items.map(e=>r?i(e,r):structuredClone(e));return{set:{meta:e.meta,items:a},publicSet:{meta:e.meta,items:a.map(c)}}}exports.displayItem=i,exports.prepareDisplaySet=l,exports.publicItem=c,exports.seededRandom=t;
|
package/dist/items.js
CHANGED
|
@@ -32,7 +32,7 @@ function i(t, i) {
|
|
|
32
32
|
return a;
|
|
33
33
|
case "sort-items": return a.items = n(a.items, i, r(a.correctOrder)), a;
|
|
34
34
|
case "match-pairs": return a.prompts = n(a.prompts, i), a.matches = n(a.matches, i), a;
|
|
35
|
-
case "match-complex": return a.
|
|
35
|
+
case "match-complex": return a.prompts = n(a.prompts, i), a.matches = n(a.matches, i), a;
|
|
36
36
|
case "timeline": return a.shuffle !== !1 && (a.events = n(a.events, i, (t) => e(t))), a;
|
|
37
37
|
case "categorize": return a.items = n(a.items, i), a;
|
|
38
38
|
case "diagram-label": return a.requireTyping || (a.labels = n(a.labels, i)), a;
|
|
@@ -51,7 +51,7 @@ var a = ["explanation", "incorrectFeedback"], o = {
|
|
|
51
51
|
"match-complex": ["connections"],
|
|
52
52
|
slider: ["correctAnswer"],
|
|
53
53
|
"numeric-input": ["correctAnswer"],
|
|
54
|
-
"math-input": ["
|
|
54
|
+
"math-input": ["correctAnswers"],
|
|
55
55
|
matrix: ["correctCells"],
|
|
56
56
|
"open-ended": ["sampleAnswer", "rubric"],
|
|
57
57
|
"chess-puzzle": ["correctAnswers"]
|
|
@@ -60,8 +60,6 @@ var a = ["explanation", "incorrectFeedback"], o = {
|
|
|
60
60
|
"items",
|
|
61
61
|
"prompts",
|
|
62
62
|
"matches",
|
|
63
|
-
"leftItems",
|
|
64
|
-
"rightItems",
|
|
65
63
|
"categories",
|
|
66
64
|
"rows",
|
|
67
65
|
"columns",
|
package/dist/limits.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=require("./errors.cjs");var t={messageBytes:65536,messagesPerSecond:30,messageBurst:60,snapshotBytes:1048576,snapshotsPerSecond:2,dataBytes:262144,dataWritesPerSecond:1,helloTimeoutMs:1e4,answerBytes:4096,generatedItemBytes:32768,serviceBytes:65536,servicesPerMinute:30},n=64;function r(e,t=`$`,i=0,a=new Set){if(e===null||typeof e==`string`||typeof e==`boolean`)return null;if(typeof e==`number`)return Number.isFinite(e)?null:`${t} is not a finite number`;if(typeof e!=`object`)return`${t} is a ${typeof e}`;if(i>=n)return`${t} is nested deeper than ${n} levels`;if(a.has(e))return`${t} contains a circular reference`;if(Array.isArray(e)){a.add(e);for(let n=0;n<e.length;n++){let o=r(e[n],`${t}[${n}]`,i+1,a);if(o)return o}return a.delete(e),null}let o=Object.getPrototypeOf(e);if(o!==Object.prototype&&o!==null)return`${t} is not a plain object`;a.add(e);for(let[n,o]of Object.entries(e)){let e=r(o,`${t}.${n}`,i+1,a);if(e)return e}return a.delete(e),null}function i(e){return new TextEncoder().encode(JSON.stringify(e)).length}function a(t,n,a,o){let s=r(t);if(s)throw new e.ProtocolError(`INVALID_ARGUMENT`,`${o} must be plain JSON: ${s}.`);let c=i(t);if(c>n)throw new e.ProtocolError(a,`${o} is ${c} bytes; the limit is ${n} bytes.`);return c}var o=class{rate;burst;now;tokens;last;constructor(e,t,n=()=>Date.now()){this.rate=e,this.burst=t,this.now=n,this.tokens=t,this.last=n()}tryTake(e=1){let t=this.now();return this.tokens=Math.min(this.burst,this.tokens+(t-this.last)/1e3*this.rate),this.last=t,this.tokens<e?!1:(this.tokens-=e,!0)}};exports.LIMITS=t,exports.RateLimiter=o,exports.assertJsonWithin=a,exports.findNonJson=r,exports.jsonByteSize=i;
|
|
1
|
+
const e=require("./errors.cjs");var t={messageBytes:65536,messagesPerSecond:30,messageBurst:60,snapshotBytes:1048576,snapshotsPerSecond:2,dataBytes:262144,dataWritesPerSecond:1,helloTimeoutMs:1e4,answerBytes:4096,generatedItemBytes:32768,answersPerSecond:10,answerBurst:20,serviceBytes:65536,serviceResultBytes:262144,servicesPerMinute:30,assetsPerSecond:10,assetBurst:20,assetBytes:2097152,errorReportsPerSecond:5,errorReportBurst:10},n=64;function r(e,t=`$`,i=0,a=new Set){if(e===null||typeof e==`string`||typeof e==`boolean`)return null;if(typeof e==`number`)return Number.isFinite(e)?null:`${t} is not a finite number`;if(typeof e!=`object`)return`${t} is a ${typeof e}`;if(i>=n)return`${t} is nested deeper than ${n} levels`;if(a.has(e))return`${t} contains a circular reference`;if(Array.isArray(e)){a.add(e);for(let n=0;n<e.length;n++){let o=r(e[n],`${t}[${n}]`,i+1,a);if(o)return o}return a.delete(e),null}let o=Object.getPrototypeOf(e);if(o!==Object.prototype&&o!==null)return`${t} is not a plain object`;a.add(e);for(let[n,o]of Object.entries(e)){let e=r(o,`${t}.${n}`,i+1,a);if(e)return e}return a.delete(e),null}function i(e){return new TextEncoder().encode(JSON.stringify(e)).length}function a(t,n,a,o){let s=r(t);if(s)throw new e.ProtocolError(`INVALID_ARGUMENT`,`${o} must be plain JSON: ${s}.`);let c=i(t);if(c>n)throw new e.ProtocolError(a,`${o} is ${c} bytes; the limit is ${n} bytes.`);return c}var o=class{rate;burst;now;tokens;last;constructor(e,t,n=()=>Date.now()){this.rate=e,this.burst=t,this.now=n,this.tokens=t,this.last=n()}tryTake(e=1){let t=this.now();return this.tokens=Math.min(this.burst,this.tokens+(t-this.last)/1e3*this.rate),this.last=t,this.tokens<e?!1:(this.tokens-=e,!0)}};exports.LIMITS=t,exports.RateLimiter=o,exports.assertJsonWithin=a,exports.findNonJson=r,exports.jsonByteSize=i;
|
package/dist/limits.js
CHANGED
|
@@ -11,8 +11,16 @@ var t = {
|
|
|
11
11
|
helloTimeoutMs: 1e4,
|
|
12
12
|
answerBytes: 4096,
|
|
13
13
|
generatedItemBytes: 32768,
|
|
14
|
+
answersPerSecond: 10,
|
|
15
|
+
answerBurst: 20,
|
|
14
16
|
serviceBytes: 65536,
|
|
15
|
-
|
|
17
|
+
serviceResultBytes: 262144,
|
|
18
|
+
servicesPerMinute: 30,
|
|
19
|
+
assetsPerSecond: 10,
|
|
20
|
+
assetBurst: 20,
|
|
21
|
+
assetBytes: 2097152,
|
|
22
|
+
errorReportsPerSecond: 5,
|
|
23
|
+
errorReportBurst: 10
|
|
16
24
|
}, n = 64;
|
|
17
25
|
function r(e, t = "$", i = 0, a = /* @__PURE__ */ new Set()) {
|
|
18
26
|
if (e === null || typeof e == "string" || typeof e == "boolean") return null;
|
package/dist/relay.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=1,t={frameBytes:
|
|
1
|
+
var e=1,t={frameBytes:3145728,bundleBytes:8388608,playersPerRoom:100,playerFramesPerSecond:60,playerFrameBurst:120,hostFramesPerSecond:3e3,hostFrameBurst:6e3,nameLength:32,authLength:4096,hostAwayMs:6e5,roomTtlMs:216e5};function n(e){return typeof e==`string`?e.replace(/\s+/g,` `).trim().slice(0,t.nameLength):``}function r(e){return typeof e==`string`&&/^\d{6}$/.test(e)}exports.RELAY_LIMITS=t,exports.RELAY_PROTOCOL=e,exports.isPin=r,exports.normalizePlayerName=n;
|
package/dist/relay.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/relay.ts
|
|
2
2
|
var e = 1, t = {
|
|
3
|
-
frameBytes:
|
|
3
|
+
frameBytes: 3145728,
|
|
4
4
|
bundleBytes: 8388608,
|
|
5
5
|
playersPerRoom: 100,
|
|
6
6
|
playerFramesPerSecond: 60,
|
|
@@ -8,6 +8,7 @@ var e = 1, t = {
|
|
|
8
8
|
hostFramesPerSecond: 3e3,
|
|
9
9
|
hostFrameBurst: 6e3,
|
|
10
10
|
nameLength: 32,
|
|
11
|
+
authLength: 4096,
|
|
11
12
|
hostAwayMs: 6e5,
|
|
12
13
|
roomTtlMs: 216e5
|
|
13
14
|
};
|
package/dist/schemas.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=require("./errors.cjs");let t=require("zod"),n=require("@memizy/oqse");var r=e=>t.z.string().min(1).max(e),i=t.z.string().min(1).max(128),a=t.z.looseObject({protocol:t.z.string().regex(/^\d+\.\d+$/,`protocol must be in MAJOR.MINOR format`),sdk:t.z.looseObject({name:r(200),version:r(100)}),plugin:t.z.looseObject({id:r(500),version:r(100)}),features:t.z.array(r(100)).max(100)}),o=t.z.looseObject({to:t.z.union([t.z.literal(`authority`),t.z.literal(`all`),t.z.array(i).min(1).max(500)]),data:t.z.unknown()}),s=t.z.looseObject({playerId:i.optional(),itemId:r(128),isCorrect:t.z.boolean(),confidence:t.z.union([t.z.literal(1),t.z.literal(2),t.z.literal(3),t.z.literal(4)]).optional(),timeSpentMs:t.z.number().int().nonnegative().optional(),hintsUsed:t.z.number().int().nonnegative().optional(),isSkipped:t.z.boolean().optional(),answer:t.z.unknown().optional(),item:t.z.looseObject({id:r(128),type:r(64)}).optional()}).refine(e=>!e.item||e.item.id===e.itemId,{message:`item.id must equal itemId`,path:[`item`,`id`]}),c=t.z.
|
|
1
|
+
const e=require("./errors.cjs");let t=require("zod"),n=require("@memizy/oqse");var r=e=>t.z.string().min(1).max(e),i=t.z.string().min(1).max(128),a=t.z.looseObject({protocol:t.z.string().regex(/^\d+\.\d+$/,`protocol must be in MAJOR.MINOR format`),sdk:t.z.looseObject({name:r(200),version:r(100)}),plugin:t.z.looseObject({id:r(500),version:r(100)}),features:t.z.array(r(100)).max(100)}),o=t.z.looseObject({to:t.z.union([t.z.literal(`authority`),t.z.literal(`all`),t.z.array(i).min(1).max(500)]),data:t.z.unknown()}),s=t.z.looseObject({playerId:i.optional(),itemId:r(128),isCorrect:t.z.boolean(),confidence:t.z.union([t.z.literal(1),t.z.literal(2),t.z.literal(3),t.z.literal(4)]).optional(),timeSpentMs:t.z.number().int().nonnegative().optional(),hintsUsed:t.z.number().int().nonnegative().optional(),isSkipped:t.z.boolean().optional(),answer:t.z.unknown().optional(),item:t.z.looseObject({id:r(128),type:r(64)}).optional()}).refine(e=>!e.item||e.item.id===e.itemId,{message:`item.id must equal itemId`,path:[`item`,`id`]}),c=t.z.enum([`plugin`,`set`]),l=t.z.looseObject({values:t.z.record(t.z.string(),t.z.unknown()),valid:t.z.boolean(),message:t.z.string().max(500).optional()}),u=t.z.looseObject({scores:t.z.record(i,t.z.number().finite()).optional(),summary:t.z.string().max(1e3).optional()}),d=t.z.looseObject({code:r(100),message:t.z.string().max(2e3),context:t.z.record(t.z.string(),t.z.unknown()).optional()}),f={hello:t.z.tuple([a]),ready:t.z.tuple([]),send:t.z.tuple([o]),saveSnapshot:t.z.tuple([t.z.unknown()]),recordAnswer:t.z.tuple([s]),saveData:t.z.tuple([c,t.z.unknown()]),updateSettings:t.z.tuple([l]),getAsset:t.z.tuple([r(200),r(128).optional()]),end:t.z.tuple([u]),reportError:t.z.tuple([d]),service:t.z.tuple([r(100),t.z.unknown()])};function p(t,r){let i=f[t].safeParse(r);if(!i.success)throw new e.ProtocolError(`INVALID_ARGUMENT`,`${t}: ${(0,n.formatOQSEErrors)(i.error).join(`; `)}`);return i.data}exports.AddressSchema=i,exports.AnswerRecordSchema=s,exports.DataScopeSchema=c,exports.ErrorReportSchema=d,exports.HOST_API_ARGS=f,exports.HandshakeSchema=a,exports.OutgoingMessageSchema=o,exports.SessionResultSchema=u,exports.SettingsUpdateSchema=l,exports.parseHostCall=p;
|
package/dist/schemas.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { ProtocolError as e } from "./errors.js";
|
|
2
2
|
import { z as t } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { formatOQSEErrors as n } from "@memizy/oqse";
|
|
4
4
|
//#region src/schemas.ts
|
|
5
|
-
var
|
|
5
|
+
var r = (e) => t.string().min(1).max(e), i = t.string().min(1).max(128), a = t.looseObject({
|
|
6
6
|
protocol: t.string().regex(/^\d+\.\d+$/, "protocol must be in MAJOR.MINOR format"),
|
|
7
7
|
sdk: t.looseObject({
|
|
8
|
-
name:
|
|
9
|
-
version:
|
|
8
|
+
name: r(200),
|
|
9
|
+
version: r(100)
|
|
10
10
|
}),
|
|
11
11
|
plugin: t.looseObject({
|
|
12
|
-
id:
|
|
13
|
-
version:
|
|
12
|
+
id: r(500),
|
|
13
|
+
version: r(100)
|
|
14
14
|
}),
|
|
15
|
-
features: t.array(
|
|
16
|
-
}),
|
|
15
|
+
features: t.array(r(100)).max(100)
|
|
16
|
+
}), o = t.looseObject({
|
|
17
17
|
to: t.union([
|
|
18
18
|
t.literal("authority"),
|
|
19
19
|
t.literal("all"),
|
|
20
|
-
t.array(
|
|
20
|
+
t.array(i).min(1).max(500)
|
|
21
21
|
]),
|
|
22
22
|
data: t.unknown()
|
|
23
|
-
}),
|
|
24
|
-
playerId:
|
|
25
|
-
itemId:
|
|
23
|
+
}), s = t.looseObject({
|
|
24
|
+
playerId: i.optional(),
|
|
25
|
+
itemId: r(128),
|
|
26
26
|
isCorrect: t.boolean(),
|
|
27
27
|
confidence: t.union([
|
|
28
28
|
t.literal(1),
|
|
@@ -35,43 +35,40 @@ var i = (e) => t.string().min(1).max(e), a = t.string().min(1).max(128), o = t.l
|
|
|
35
35
|
isSkipped: t.boolean().optional(),
|
|
36
36
|
answer: t.unknown().optional(),
|
|
37
37
|
item: t.looseObject({
|
|
38
|
-
id:
|
|
39
|
-
type:
|
|
38
|
+
id: r(128),
|
|
39
|
+
type: r(64)
|
|
40
40
|
}).optional()
|
|
41
41
|
}).refine((e) => !e.item || e.item.id === e.itemId, {
|
|
42
42
|
message: "item.id must equal itemId",
|
|
43
43
|
path: ["item", "id"]
|
|
44
|
-
}),
|
|
44
|
+
}), c = t.enum(["plugin", "set"]), l = t.looseObject({
|
|
45
45
|
values: t.record(t.string(), t.unknown()),
|
|
46
46
|
valid: t.boolean(),
|
|
47
47
|
message: t.string().max(500).optional()
|
|
48
|
-
}),
|
|
49
|
-
scores: t.record(
|
|
48
|
+
}), u = t.looseObject({
|
|
49
|
+
scores: t.record(i, t.number().finite()).optional(),
|
|
50
50
|
summary: t.string().max(1e3).optional()
|
|
51
|
-
}),
|
|
52
|
-
code:
|
|
51
|
+
}), d = t.looseObject({
|
|
52
|
+
code: r(100),
|
|
53
53
|
message: t.string().max(2e3),
|
|
54
54
|
context: t.record(t.string(), t.unknown()).optional()
|
|
55
|
-
}),
|
|
56
|
-
hello: t.tuple([
|
|
55
|
+
}), f = {
|
|
56
|
+
hello: t.tuple([a]),
|
|
57
57
|
ready: t.tuple([]),
|
|
58
|
-
send: t.tuple([
|
|
58
|
+
send: t.tuple([o]),
|
|
59
59
|
saveSnapshot: t.tuple([t.unknown()]),
|
|
60
|
-
recordAnswer: t.tuple([
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
reportError: t.tuple([m]),
|
|
68
|
-
exit: t.tuple([]),
|
|
69
|
-
service: t.tuple([i(100), t.unknown()])
|
|
60
|
+
recordAnswer: t.tuple([s]),
|
|
61
|
+
saveData: t.tuple([c, t.unknown()]),
|
|
62
|
+
updateSettings: t.tuple([l]),
|
|
63
|
+
getAsset: t.tuple([r(200), r(128).optional()]),
|
|
64
|
+
end: t.tuple([u]),
|
|
65
|
+
reportError: t.tuple([d]),
|
|
66
|
+
service: t.tuple([r(100), t.unknown()])
|
|
70
67
|
};
|
|
71
|
-
function
|
|
72
|
-
let i =
|
|
73
|
-
if (!i.success) throw new e("INVALID_ARGUMENT", `${t}: ${
|
|
68
|
+
function p(t, r) {
|
|
69
|
+
let i = f[t].safeParse(r);
|
|
70
|
+
if (!i.success) throw new e("INVALID_ARGUMENT", `${t}: ${n(i.error).join("; ")}`);
|
|
74
71
|
return i.data;
|
|
75
72
|
}
|
|
76
73
|
//#endregion
|
|
77
|
-
export {
|
|
74
|
+
export { i as AddressSchema, s as AnswerRecordSchema, c as DataScopeSchema, d as ErrorReportSchema, f as HOST_API_ARGS, a as HandshakeSchema, o as OutgoingMessageSchema, u as SessionResultSchema, l as SettingsUpdateSchema, p as parseHostCall };
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* error `name` and `message`. The code is therefore also encoded in the
|
|
6
6
|
* message as `[CODE] text`, and {@link toProtocolError} restores it.
|
|
7
7
|
*/
|
|
8
|
-
export declare const PROTOCOL_ERROR_CODES: readonly ["UNSUPPORTED_PROTOCOL", "INVALID_ARGUMENT", "NOT_AUTHORITY", "NOT_ALLOWED_IN_VIEW", "AUTHORITY_UNAVAILABLE", "MESSAGE_TOO_LARGE", "SNAPSHOT_TOO_LARGE", "DATA_TOO_LARGE", "RATE_LIMITED", "ASSET_NOT_FOUND", "SESSION_ENDED", "INTERNAL_ERROR", "SERVICE_UNAVAILABLE"];
|
|
8
|
+
export declare const PROTOCOL_ERROR_CODES: readonly ["UNSUPPORTED_PROTOCOL", "INVALID_ARGUMENT", "NOT_AUTHORITY", "NOT_ALLOWED_IN_VIEW", "AUTHORITY_UNAVAILABLE", "MESSAGE_TOO_LARGE", "SNAPSHOT_TOO_LARGE", "DATA_TOO_LARGE", "RATE_LIMITED", "ASSET_NOT_FOUND", "SESSION_ENDED", "INTERNAL_ERROR", "SERVICE_UNAVAILABLE", "UNKNOWN_METHOD"];
|
|
9
9
|
export type ProtocolErrorCode = (typeof PROTOCOL_ERROR_CODES)[number];
|
|
10
10
|
export declare class ProtocolError extends Error {
|
|
11
11
|
readonly code: ProtocolErrorCode;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @memizy/protocol – Memizy Plugin Protocol v1.
|
|
3
3
|
* The normative specification is SPEC.md in this package.
|
|
4
|
+
*
|
|
5
|
+
* The public API is listed explicitly: it follows semver within 1.x. Helpers shared by the
|
|
6
|
+
* Memizy packages (rate limiting, call parsing, content policies…) are in
|
|
7
|
+
* `@memizy/protocol/internal` and may change in any release.
|
|
4
8
|
*/
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export * from './types';
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export
|
|
17
|
-
export * from './polyfills';
|
|
9
|
+
export { PROTOCOL_VERSION, parseProtocolVersion, compareProtocolVersions, negotiateProtocolVersion, isProtocolSupported, type ProtocolVersion, type HostProtocolVersions, } from './version.js';
|
|
10
|
+
export { PROTOCOL_ERROR_CODES, ProtocolError, isProtocolErrorCode, toProtocolError, type ProtocolErrorCode } from './errors.js';
|
|
11
|
+
export { LIMITS, type Limits } from './limits.js';
|
|
12
|
+
export { LocalizedTextSchema, SettingDefinitionSchema, type LocalizedText, type SettingDefinition } from './settingsSchema.js';
|
|
13
|
+
export { validateSettingValue, resolveSettings, settingsForMode, localize, type SettingValue } from './settings.js';
|
|
14
|
+
export { HostAsSchema, MultiplayerModeSchema, ServiceNameSchema, PluginPermissionsSchema, MemizyRuntimeSchema, PluginManifestError, safeParsePluginManifest, parsePluginManifest, readPluginManifestFromHtml, type MemizyRuntimeManifest, type PluginManifest, type PluginManifestResult, } from './manifestSchema.js';
|
|
15
|
+
export { extractManifestFromHtml, type HostAs, type PluginView, type PluginRuntime } from './pluginRuntime.js';
|
|
16
|
+
export * from './types.js';
|
|
17
|
+
export { AddressSchema, HandshakeSchema, OutgoingMessageSchema, AnswerRecordSchema, DataScopeSchema, SettingsUpdateSchema, SessionResultSchema, ErrorReportSchema, } from './schemas.js';
|
|
18
|
+
export { RELAY_PROTOCOL, type RelayBundle, type CreateRoomResponse, type RoomInfo, type RoomPlayer, type RelayClientMessage, type RelayErrorCode, type RelayServerMessage, } from './relay.js';
|
|
19
|
+
export { prepareDisplaySet, type DisplaySet, type PublicItemMark } from './items.js';
|
|
20
|
+
export { PLUGIN_DEVICES, type PluginDevice } from './security.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @memizy/protocol/internal – helpers shared by the Memizy packages (host-sdk, plugin-sdk,
|
|
3
|
+
* the relay server, Memizy Play). Not part of the public API: they may change in any
|
|
4
|
+
* release. Use `@memizy/protocol` instead.
|
|
5
|
+
*/
|
|
6
|
+
export { findNonJson, jsonByteSize, assertJsonWithin, RateLimiter } from './limits.js';
|
|
7
|
+
export { toRuntime } from './pluginRuntime.js';
|
|
8
|
+
export { HOST_API_ARGS, parseHostCall } from './schemas.js';
|
|
9
|
+
export { RELAY_LIMITS, normalizePlayerName, isPin } from './relay.js';
|
|
10
|
+
export { seededRandom, displayItem, publicItem } from './items.js';
|
|
11
|
+
export { LIBRARY_ORIGINS, pluginContentPolicy, pluginAllowAttribute, injectContentPolicy } from './security.js';
|
|
12
|
+
export { ensureConnectionPolyfills } from './polyfills.js';
|
package/dist/src/limits.d.ts
CHANGED
|
@@ -25,10 +25,26 @@ export declare const LIMITS: {
|
|
|
25
25
|
readonly answerBytes: number;
|
|
26
26
|
/** Max serialized size of a generated item attached to `recordAnswer`. */
|
|
27
27
|
readonly generatedItemBytes: number;
|
|
28
|
-
/**
|
|
28
|
+
/** `recordAnswer` calls per second and instance (sustained); the authority: per player. */
|
|
29
|
+
readonly answersPerSecond: 10;
|
|
30
|
+
/** Burst size of the `recordAnswer` rate limiter. */
|
|
31
|
+
readonly answerBurst: 20;
|
|
32
|
+
/** Max serialized size of a service request. */
|
|
29
33
|
readonly serviceBytes: number;
|
|
34
|
+
/** Max serialized size of a service result. */
|
|
35
|
+
readonly serviceResultBytes: number;
|
|
30
36
|
/** Service calls per minute and instance (sustained). */
|
|
31
37
|
readonly servicesPerMinute: 30;
|
|
38
|
+
/** `getAsset` calls per second and instance (sustained). */
|
|
39
|
+
readonly assetsPerSecond: 10;
|
|
40
|
+
/** Burst size of the `getAsset` rate limiter. */
|
|
41
|
+
readonly assetBurst: 20;
|
|
42
|
+
/** Size of one asset every host can deliver through `getAsset`. */
|
|
43
|
+
readonly assetBytes: number;
|
|
44
|
+
/** `reportError` calls per second and instance the host keeps (more are dropped). */
|
|
45
|
+
readonly errorReportsPerSecond: 5;
|
|
46
|
+
/** Burst size of the `reportError` limiter. */
|
|
47
|
+
readonly errorReportBurst: 10;
|
|
32
48
|
};
|
|
33
49
|
export type Limits = {
|
|
34
50
|
[K in keyof typeof LIMITS]: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { OQSEManifest } from '@memizy/oqse';
|
|
3
|
-
import { PluginRuntime } from './pluginRuntime';
|
|
3
|
+
import { PluginRuntime } from './pluginRuntime.js';
|
|
4
4
|
export declare const HostAsSchema: z.ZodEnum<{
|
|
5
5
|
presenter: "presenter";
|
|
6
6
|
player: "player";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MemizyRuntimeManifest } from './manifestSchema';
|
|
2
|
-
import { SettingDefinition } from './settingsSchema';
|
|
1
|
+
import { MemizyRuntimeManifest } from './manifestSchema.js';
|
|
2
|
+
import { SettingDefinition } from './settingsSchema.js';
|
|
3
3
|
export type HostAs = 'presenter' | 'player';
|
|
4
4
|
export type PluginView = 'solo' | 'board' | 'controller' | 'settings';
|
|
5
5
|
/** The runtime part with all defaults applied – what hosts work with. */
|
package/dist/src/relay.d.ts
CHANGED
|
@@ -16,9 +16,15 @@
|
|
|
16
16
|
* - `GET /api/health`
|
|
17
17
|
* WebSocket: `/ws`.
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* Version of the relay wire protocol (client <-> server). Within version 1 messages only
|
|
21
|
+
* change additively: receivers ignore unknown fields; the server answers an unknown `t`
|
|
22
|
+
* with a non-fatal `NOT_ALLOWED` error. The messages between the host app and the player
|
|
23
|
+
* apps (inside `data`) have their own version (`RELAY_APP_VERSION` in @memizy/host-sdk).
|
|
24
|
+
*/
|
|
19
25
|
export declare const RELAY_PROTOCOL = 1;
|
|
20
26
|
export declare const RELAY_LIMITS: {
|
|
21
|
-
/** Max size of one WebSocket frame (UTF-8 JSON). */
|
|
27
|
+
/** Max size of one WebSocket frame (UTF-8 JSON); fits an asset of `LIMITS.assetBytes` as base64. */
|
|
22
28
|
readonly frameBytes: number;
|
|
23
29
|
/** Max size of an uploaded bundle (plugin + set). */
|
|
24
30
|
readonly bundleBytes: number;
|
|
@@ -32,6 +38,8 @@ export declare const RELAY_LIMITS: {
|
|
|
32
38
|
readonly hostFrameBurst: 6000;
|
|
33
39
|
/** Max player name length. */
|
|
34
40
|
readonly nameLength: 32;
|
|
41
|
+
/** Max length of the `auth` a player sends when joining. */
|
|
42
|
+
readonly authLength: 4096;
|
|
35
43
|
/** A room is closed when its host has been away this long. */
|
|
36
44
|
readonly hostAwayMs: number;
|
|
37
45
|
/** A room is closed after this time in any case. */
|
|
@@ -55,10 +63,15 @@ export interface RoomInfo {
|
|
|
55
63
|
appName: string | null;
|
|
56
64
|
players: number;
|
|
57
65
|
}
|
|
58
|
-
export interface
|
|
66
|
+
export interface RoomPlayer {
|
|
59
67
|
id: string;
|
|
60
68
|
name: string;
|
|
61
69
|
connected: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* What the player's app sent as `auth` when joining (e.g. a signed token of a signed-in
|
|
72
|
+
* student). Only the host gets it; the server does not check it – the host app does.
|
|
73
|
+
*/
|
|
74
|
+
auth?: string;
|
|
62
75
|
}
|
|
63
76
|
export type RelayClientMessage =
|
|
64
77
|
/** The host attaches to its room (also after a reconnect). */
|
|
@@ -68,13 +81,18 @@ export type RelayClientMessage =
|
|
|
68
81
|
pin: string;
|
|
69
82
|
hostToken: string;
|
|
70
83
|
}
|
|
71
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* A player joins (with `token`: rejoins as the same player). `auth` (optional, at most
|
|
86
|
+
* `RELAY_LIMITS.authLength` characters) is passed to the host only, for apps with
|
|
87
|
+
* signed-in players; a rejoin with `auth` replaces the earlier one.
|
|
88
|
+
*/
|
|
72
89
|
| {
|
|
73
90
|
t: 'join';
|
|
74
91
|
protocol: number;
|
|
75
92
|
pin: string;
|
|
76
93
|
name: string;
|
|
77
94
|
token?: string;
|
|
95
|
+
auth?: string;
|
|
78
96
|
}
|
|
79
97
|
/** Host → one player. */
|
|
80
98
|
| {
|
|
@@ -114,7 +132,7 @@ export type RelayServerMessage =
|
|
|
114
132
|
{
|
|
115
133
|
t: 'hosting';
|
|
116
134
|
pin: string;
|
|
117
|
-
players:
|
|
135
|
+
players: RoomPlayer[];
|
|
118
136
|
}
|
|
119
137
|
/** To a player after `join`. Keep `token` to rejoin as the same player. */
|
|
120
138
|
| {
|
|
@@ -128,7 +146,7 @@ export type RelayServerMessage =
|
|
|
128
146
|
/** To the host: a player joined, reconnected, disconnected or was renamed. */
|
|
129
147
|
| {
|
|
130
148
|
t: 'presence';
|
|
131
|
-
player:
|
|
149
|
+
player: RoomPlayer;
|
|
132
150
|
event: 'join' | 'connect' | 'disconnect' | 'rename' | 'leave';
|
|
133
151
|
}
|
|
134
152
|
/** To players: the host connection state. */
|
package/dist/src/schemas.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { HostApi } from './types';
|
|
2
|
+
import { HostApi } from './types.js';
|
|
3
3
|
export declare const AddressSchema: z.ZodString;
|
|
4
4
|
export declare const HandshakeSchema: z.ZodObject<{
|
|
5
5
|
protocol: z.ZodString;
|
|
@@ -31,7 +31,6 @@ export declare const AnswerRecordSchema: z.ZodObject<{
|
|
|
31
31
|
type: z.ZodString;
|
|
32
32
|
}, z.core.$loose>>;
|
|
33
33
|
}, z.core.$loose>;
|
|
34
|
-
export declare const ProgressRecordsSchema: z.ZodRecord<z.ZodString, z.ZodType<import('@memizy/oqse').ProgressRecord, unknown, z.core.$ZodTypeInternals<import('@memizy/oqse').ProgressRecord, unknown>>>;
|
|
35
34
|
export declare const DataScopeSchema: z.ZodEnum<{
|
|
36
35
|
set: "set";
|
|
37
36
|
plugin: "plugin";
|
|
@@ -45,9 +44,6 @@ export declare const SessionResultSchema: z.ZodObject<{
|
|
|
45
44
|
scores: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
46
45
|
summary: z.ZodOptional<z.ZodString>;
|
|
47
46
|
}, z.core.$loose>;
|
|
48
|
-
export declare const ResizeRequestSchema: z.ZodObject<{
|
|
49
|
-
height: z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"auto">]>;
|
|
50
|
-
}, z.core.$loose>;
|
|
51
47
|
export declare const ErrorReportSchema: z.ZodObject<{
|
|
52
48
|
code: z.ZodString;
|
|
53
49
|
message: z.ZodString;
|
|
@@ -87,7 +83,6 @@ export declare const HOST_API_ARGS: {
|
|
|
87
83
|
type: z.ZodString;
|
|
88
84
|
}, z.core.$loose>>;
|
|
89
85
|
}, z.core.$loose>], null>;
|
|
90
|
-
saveProgress: z.ZodTuple<[z.ZodRecord<z.ZodString, z.ZodType<import('@memizy/oqse').ProgressRecord, unknown, z.core.$ZodTypeInternals<import('@memizy/oqse').ProgressRecord, unknown>>>], null>;
|
|
91
86
|
saveData: z.ZodTuple<[z.ZodEnum<{
|
|
92
87
|
set: "set";
|
|
93
88
|
plugin: "plugin";
|
|
@@ -102,15 +97,11 @@ export declare const HOST_API_ARGS: {
|
|
|
102
97
|
scores: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
103
98
|
summary: z.ZodOptional<z.ZodString>;
|
|
104
99
|
}, z.core.$loose>], null>;
|
|
105
|
-
resize: z.ZodTuple<[z.ZodObject<{
|
|
106
|
-
height: z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"auto">]>;
|
|
107
|
-
}, z.core.$loose>], null>;
|
|
108
100
|
reportError: z.ZodTuple<[z.ZodObject<{
|
|
109
101
|
code: z.ZodString;
|
|
110
102
|
message: z.ZodString;
|
|
111
103
|
context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
112
104
|
}, z.core.$loose>], null>;
|
|
113
|
-
exit: z.ZodTuple<[], null>;
|
|
114
105
|
service: z.ZodTuple<[z.ZodString, z.ZodUnknown], null>;
|
|
115
106
|
};
|
|
116
107
|
/**
|
package/dist/src/security.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginRuntime } from './pluginRuntime';
|
|
1
|
+
import { PluginRuntime } from './pluginRuntime.js';
|
|
2
2
|
/** CDNs plugins load libraries from (scripts, styles, fonts, data files). */
|
|
3
3
|
export declare const LIBRARY_ORIGINS: string[];
|
|
4
4
|
/** Device permissions a plugin may declare, and their iframe `allow` names. */
|
package/dist/src/settings.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LocalizedText, SettingDefinition } from './settingsSchema';
|
|
1
|
+
import { LocalizedText, SettingDefinition } from './settingsSchema.js';
|
|
2
2
|
export type SettingValue = string | number | boolean;
|
|
3
3
|
/** Returns an error description if `value` is not valid for `def`, otherwise `null`. */
|
|
4
4
|
export declare function validateSettingValue(def: SettingDefinition, value: unknown): string | null;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { OQSEAnyItem, OQSEMeta, ProgressRecord } from '@memizy/oqse';
|
|
2
|
-
import { HostAs, PluginView } from './pluginRuntime';
|
|
3
|
-
/** Address of an instance: a player ID, `"board"
|
|
2
|
+
import { HostAs, PluginView } from './pluginRuntime.js';
|
|
3
|
+
/** Address of an instance: a player ID, `"board"`, `"settings"` (`SETTINGS_ADDRESS`); `"server"` is reserved. */
|
|
4
4
|
export type Address = string;
|
|
5
5
|
/** Address of the presenter's board. */
|
|
6
6
|
export declare const BOARD_ADDRESS = "board";
|
|
7
7
|
/** Reserved for a future server-side authority (SPEC 3.1). */
|
|
8
8
|
export declare const SERVER_ADDRESS = "server";
|
|
9
|
+
/** Address of the plugin's own settings screen in the lobby (view `settings`). */
|
|
10
|
+
export declare const SETTINGS_ADDRESS = "settings";
|
|
9
11
|
export type SessionMode = 'solo' | 'multiplayer';
|
|
10
12
|
export interface Player {
|
|
11
13
|
id: string;
|
|
@@ -39,7 +41,8 @@ export interface Handshake {
|
|
|
39
41
|
};
|
|
40
42
|
features: string[];
|
|
41
43
|
}
|
|
42
|
-
|
|
44
|
+
/** Language and theme of the app (`InitPayload.config`, `configChanged`). */
|
|
45
|
+
export interface HostConfig {
|
|
43
46
|
locale: string;
|
|
44
47
|
theme: 'light' | 'dark';
|
|
45
48
|
}
|
|
@@ -68,14 +71,11 @@ export interface InitPayload {
|
|
|
68
71
|
};
|
|
69
72
|
/** Values for every declared setting (defaults applied). */
|
|
70
73
|
settings: Record<string, unknown>;
|
|
71
|
-
config:
|
|
74
|
+
config: HostConfig;
|
|
72
75
|
/** The session clock and its pauses (see {@link SessionClock}). */
|
|
73
76
|
clock: SessionClock;
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
* available here); call them with `service()`. Absent on older hosts.
|
|
77
|
-
*/
|
|
78
|
-
services?: string[];
|
|
77
|
+
/** Memizy services this host offers to the plugin (declared in its manifest and available here); call them with `service()`. */
|
|
78
|
+
services: string[];
|
|
79
79
|
/** Learning progress of `self` (empty for board/settings). */
|
|
80
80
|
progress: Record<string, ProgressRecord>;
|
|
81
81
|
/** Plugin data of `self` (`null` values for board/settings). */
|
|
@@ -84,15 +84,17 @@ export interface InitPayload {
|
|
|
84
84
|
snapshot: unknown | null;
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* The session clock. `Date.now() + offsetMs` is the session time.
|
|
87
|
+
* The session clock. `Date.now() + offsetMs` is the session time. Pauses: the
|
|
88
88
|
* game time is `(pausedAt ?? sessionTime) - pausedMs` – it stands still while the host
|
|
89
89
|
* has paused the game (`pausedAt` = session time of the pause) and continues afterwards
|
|
90
90
|
* without the paused time (`pausedMs` = total of finished pauses).
|
|
91
91
|
*/
|
|
92
92
|
export interface SessionClock {
|
|
93
93
|
offsetMs: number;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
/** Total length of finished pauses (ms). */
|
|
95
|
+
pausedMs: number;
|
|
96
|
+
/** Session time when the current pause started, or `null` while the game runs. */
|
|
97
|
+
pausedAt: number | null;
|
|
96
98
|
}
|
|
97
99
|
export interface AnswerRecord {
|
|
98
100
|
/** Default: self. The authority may record for any player. */
|
|
@@ -104,12 +106,12 @@ export interface AnswerRecord {
|
|
|
104
106
|
hintsUsed?: number;
|
|
105
107
|
isSkipped?: boolean;
|
|
106
108
|
/**
|
|
107
|
-
*
|
|
109
|
+
* What the player answered, in the `checkAnswer` format of the item type
|
|
108
110
|
* (choice IDs, text, numbers…). Lets teachers see which wrong answers were chosen.
|
|
109
111
|
*/
|
|
110
112
|
answer?: unknown;
|
|
111
113
|
/**
|
|
112
|
-
*
|
|
114
|
+
* The item itself, required when `itemId` is not in the set (a generated item, e.g.
|
|
113
115
|
* from a service). It needs at least `id` (= `itemId`) and `type`; the answer fields may
|
|
114
116
|
* be left out. Such answers count for statistics (`skills`, `tags`), not for repetition.
|
|
115
117
|
*/
|
|
@@ -130,9 +132,6 @@ export interface SettingsUpdate {
|
|
|
130
132
|
valid: boolean;
|
|
131
133
|
message?: string;
|
|
132
134
|
}
|
|
133
|
-
export interface ResizeRequest {
|
|
134
|
-
height: number | 'auto';
|
|
135
|
-
}
|
|
136
135
|
export interface ErrorReport {
|
|
137
136
|
code: string;
|
|
138
137
|
message: string;
|
|
@@ -150,7 +149,7 @@ export interface IncomingMessage {
|
|
|
150
149
|
/** Session clock (ms). */
|
|
151
150
|
sentAt: number;
|
|
152
151
|
}
|
|
153
|
-
export type SessionEndReason = 'finished' | '
|
|
152
|
+
export type SessionEndReason = 'finished' | 'host-left' | 'kicked' | 'closed' | 'error';
|
|
154
153
|
/** Methods the host exposes to the plugin (plugin → host). SPEC section 5. */
|
|
155
154
|
export interface HostApi {
|
|
156
155
|
hello(handshake: Handshake): Promise<InitPayload>;
|
|
@@ -158,15 +157,12 @@ export interface HostApi {
|
|
|
158
157
|
send(message: OutgoingMessage): Promise<void>;
|
|
159
158
|
saveSnapshot(snapshot: unknown): Promise<void>;
|
|
160
159
|
recordAnswer(answer: AnswerRecord): Promise<void>;
|
|
161
|
-
saveProgress(records: Record<string, ProgressRecord>): Promise<void>;
|
|
162
160
|
saveData(scope: DataScope, value: unknown): Promise<void>;
|
|
163
161
|
updateSettings(update: SettingsUpdate): Promise<void>;
|
|
164
162
|
getAsset(key: string, itemId?: string): Promise<Blob>;
|
|
165
163
|
end(result: SessionResult): Promise<void>;
|
|
166
|
-
resize(request: ResizeRequest): Promise<void>;
|
|
167
164
|
reportError(error: ErrorReport): Promise<void>;
|
|
168
|
-
|
|
169
|
-
/** RC4: calls a Memizy service (one of `InitPayload.services`); returns its JSON result. */
|
|
165
|
+
/** Calls a Memizy service (one of `InitPayload.services`); returns its JSON result. */
|
|
170
166
|
service(name: string, payload: unknown): Promise<unknown>;
|
|
171
167
|
}
|
|
172
168
|
/** Methods the plugin exposes to the host (host → plugin). SPEC section 7. */
|
|
@@ -177,14 +173,10 @@ export interface PluginApi {
|
|
|
177
173
|
authorityChanged(status: {
|
|
178
174
|
connected: boolean;
|
|
179
175
|
}): Promise<void>;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
items: OQSEAnyItem[];
|
|
183
|
-
}): Promise<void>;
|
|
184
|
-
configChanged(config: Theme): Promise<void>;
|
|
185
|
-
/** The clock was corrected, or the game was paused / resumed (RC4). */
|
|
176
|
+
configChanged(config: HostConfig): Promise<void>;
|
|
177
|
+
/** The clock was corrected, or the game was paused / resumed. */
|
|
186
178
|
clockChanged(clock: SessionClock): Promise<void>;
|
|
187
179
|
sessionEnded(reason: SessionEndReason): Promise<void>;
|
|
188
180
|
}
|
|
189
|
-
export declare const HOST_API_METHODS: readonly ["hello", "ready", "send", "saveSnapshot", "recordAnswer", "
|
|
190
|
-
export declare const PLUGIN_API_METHODS: readonly ["start", "deliver", "playersChanged", "authorityChanged", "
|
|
181
|
+
export declare const HOST_API_METHODS: readonly ["hello", "ready", "send", "saveSnapshot", "recordAnswer", "saveData", "updateSettings", "getAsset", "end", "reportError", "service"];
|
|
182
|
+
export declare const PLUGIN_API_METHODS: readonly ["start", "deliver", "playersChanged", "authorityChanged", "configChanged", "clockChanged", "sessionEnded"];
|
package/dist/types.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=`board`,t=`server`,n=[`hello`,`ready`,`send`,`saveSnapshot`,`recordAnswer`,`
|
|
1
|
+
var e=`board`,t=`server`,n=`settings`,r=[`hello`,`ready`,`send`,`saveSnapshot`,`recordAnswer`,`saveData`,`updateSettings`,`getAsset`,`end`,`reportError`,`service`],i=[`start`,`deliver`,`playersChanged`,`authorityChanged`,`configChanged`,`clockChanged`,`sessionEnded`];exports.BOARD_ADDRESS=e,exports.HOST_API_METHODS=r,exports.PLUGIN_API_METHODS=i,exports.SERVER_ADDRESS=t,exports.SETTINGS_ADDRESS=n;
|
package/dist/types.js
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
//#region src/types.ts
|
|
2
|
-
var e = "board", t = "server", n = [
|
|
2
|
+
var e = "board", t = "server", n = "settings", r = [
|
|
3
3
|
"hello",
|
|
4
4
|
"ready",
|
|
5
5
|
"send",
|
|
6
6
|
"saveSnapshot",
|
|
7
7
|
"recordAnswer",
|
|
8
|
-
"saveProgress",
|
|
9
8
|
"saveData",
|
|
10
9
|
"updateSettings",
|
|
11
10
|
"getAsset",
|
|
12
11
|
"end",
|
|
13
|
-
"resize",
|
|
14
12
|
"reportError",
|
|
15
|
-
"exit",
|
|
16
13
|
"service"
|
|
17
|
-
],
|
|
14
|
+
], i = [
|
|
18
15
|
"start",
|
|
19
16
|
"deliver",
|
|
20
17
|
"playersChanged",
|
|
21
18
|
"authorityChanged",
|
|
22
|
-
"setChanged",
|
|
23
19
|
"configChanged",
|
|
24
20
|
"clockChanged",
|
|
25
21
|
"sessionEnded"
|
|
26
22
|
];
|
|
27
23
|
//#endregion
|
|
28
|
-
export { e as BOARD_ADDRESS,
|
|
24
|
+
export { e as BOARD_ADDRESS, r as HOST_API_METHODS, i as PLUGIN_API_METHODS, t as SERVER_ADDRESS, n as SETTINGS_ADDRESS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memizy/protocol",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.6",
|
|
4
4
|
"description": "Memizy Plugin Protocol: types, Zod schemas and helpers shared by @memizy/plugin-sdk and @memizy/host-sdk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -12,21 +12,29 @@
|
|
|
12
12
|
"types": "./dist/src/index.d.ts",
|
|
13
13
|
"import": "./dist/index.js",
|
|
14
14
|
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./internal": {
|
|
17
|
+
"types": "./dist/src/internal.d.ts",
|
|
18
|
+
"import": "./dist/internal.js",
|
|
19
|
+
"require": "./dist/internal.cjs"
|
|
15
20
|
}
|
|
16
21
|
},
|
|
17
22
|
"files": [
|
|
18
23
|
"dist",
|
|
19
24
|
"SPEC.md",
|
|
20
|
-
"README.md"
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"LICENSE"
|
|
21
28
|
],
|
|
22
29
|
"scripts": {
|
|
23
|
-
"build": "tsc --noEmit && vite build",
|
|
30
|
+
"build": "tsc --noEmit && vite build && node ../../scripts/dts-extensions.mjs dist",
|
|
24
31
|
"test": "vitest run",
|
|
25
32
|
"test:watch": "vitest",
|
|
26
|
-
"typecheck": "tsc --noEmit"
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
27
35
|
},
|
|
28
36
|
"dependencies": {
|
|
29
|
-
"@memizy/oqse": "^1.0.0-rc.
|
|
37
|
+
"@memizy/oqse": "^1.0.0-rc.6",
|
|
30
38
|
"zod": "^4.3.6"
|
|
31
39
|
},
|
|
32
40
|
"devDependencies": {
|