@memizy/protocol 1.0.0-rc.5 → 1.0.0-rc.7
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 +30 -0
- package/LICENSE +21 -0
- package/README.md +6 -4
- package/SPEC.md +90 -54
- 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 +107 -44
- package/dist/limits.cjs +1 -1
- package/dist/limits.js +9 -1
- package/dist/manifestSchema.cjs +1 -1
- package/dist/manifestSchema.js +1 -1
- package/dist/pluginRuntime.cjs +1 -1
- package/dist/pluginRuntime.js +26 -24
- package/dist/relay.cjs +1 -1
- package/dist/relay.js +2 -1
- package/dist/schemas.cjs +1 -1
- package/dist/schemas.js +34 -36
- package/dist/settings.cjs +1 -1
- package/dist/settings.js +24 -14
- package/dist/settingsSchema.cjs +1 -1
- package/dist/settingsSchema.js +55 -39
- 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/items.d.ts +6 -3
- package/dist/src/limits.d.ts +17 -1
- package/dist/src/manifestSchema.d.ts +43 -50
- package/dist/src/pluginRuntime.d.ts +8 -2
- package/dist/src/relay.d.ts +23 -5
- package/dist/src/schemas.d.ts +3 -10
- package/dist/src/security.d.ts +1 -1
- package/dist/src/settings.d.ts +24 -3
- package/dist/src/settingsSchema.d.ts +63 -0
- package/dist/src/types.d.ts +33 -31
- package/dist/types.cjs +1 -1
- package/dist/types.js +3 -7
- package/package.json +13 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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.7 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
* `InitPayload.seed`: a secret seed for the game's random numbers, only for the authority (SPEC 4.2, 8.4). Before, the SDK seeded `ctx.random` with the session id, which every device knows – a player could compute a shuffled deck.
|
|
8
|
+
* Public items lose media that only hidden fields use (also in the public set's `meta.assets`); choice IDs are opaque to games, and a host may give per-session IDs (SPEC 4.4).
|
|
9
|
+
* `prepareDisplaySet` shuffles each item with its own seed (the seed and the item ID): editing, adding or removing an item, or a part of a set, never changes the order of the other items (SPEC 4.4).
|
|
10
|
+
* `recordAnswer` is an *authority* method (SPEC 5). The display order uses a secret, random seed (SPEC 4.4; not the session id).
|
|
11
|
+
* SPEC: the Penpal 8 wire format (4); a host may give a game a part of a large set (4.2); answers to `open-ended` items do not change repetition, games record the first attempt (5.1); Zod: only `parse` / `safeParse` of the exported schemas are covered (9).
|
|
12
|
+
* Public items keep only the fields the OQSE schema of their type lists (fail closed): unknown fields – also of choices, zones and other nested objects – and free-form `appSpecific` / `customData` are removed, so a field of a later OQSE version can never reveal an answer on players' devices (SPEC 4.4). Custom `x-` types keep their fields except `solution`.
|
|
13
|
+
* Settings of a type from a later protocol version: a manifest may declare them (`UnknownSettingSchema`, needs `id`, `label`, `default`); `PluginRuntime.unknownSettings` lists them, `resolveSettings(definitions, input, unknownSettings)` gives them their default; `isKnownSetting`, `SETTING_TYPES`. `SettingValue` may be any JSON value.
|
|
14
|
+
* `AnswerRecord.score` (partial credit 0–1).
|
|
15
|
+
* SPEC: `InitPayload.set` holds the questions of this instance (4.2); the two axes of questions (10, decided); reserved: personal sets (`modes.multiplayer.sets`, feature `personal-sets`, `Player.set`), `pickItems`, `openSource`, `markItem` (10).
|
|
16
|
+
|
|
17
|
+
## 1.0.0-rc.6 (2026-10-14)
|
|
18
|
+
|
|
19
|
+
The contract of protocol 1.0 is final: within MAJOR 1 it changes only additively (SPEC 9).
|
|
20
|
+
|
|
21
|
+
* SPEC status 1.0; the "RC4" labels are gone.
|
|
22
|
+
* The address `"settings"` (`SETTINGS_ADDRESS`, the plugin's settings screen in the lobby) is defined in SPEC 3.1 and on the `Address` type.
|
|
23
|
+
* `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.
|
|
24
|
+
* 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).
|
|
25
|
+
* `RELAY_LIMITS.frameBytes` (in `@memizy/protocol/internal`) is 3 MB, so that a 2 MB asset fits a relay frame as base64.
|
|
26
|
+
* 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.
|
|
27
|
+
|
|
28
|
+
## 1.0.0-rc.5
|
|
29
|
+
|
|
30
|
+
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 schemas: `parse` / `safeParse` are covered by semver, their Zod-specific API (`.extend`, `.shape`, the Zod major version) is not (SPEC 9). The functions (`safeParsePluginManifest`, `resolveSettings`, …) are the stable way to validate.
|
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
|
|
|
@@ -97,6 +95,8 @@ The OQSEM part describes **content** (item types, assets, features) and is used
|
|
|
97
95
|
|
|
98
96
|
`label`, `description` and option labels are either a string or a map of BCP 47 language → string (e.g. `{ "cs": "Čas", "en": "Time" }`).
|
|
99
97
|
|
|
98
|
+
**Setting types from later versions:** a later 1.x version may add setting types. A host that does not know a type does not show the setting, ignores values sent for it (`updateSettings`) and passes its `default` (any JSON value) to the plugin. So a plugin may use a new setting type without requiring the newer protocol version, as long as it works with the default; the `type` is a lowercase name (`^[a-z][a-z0-9-]*$`), and the setting still needs `id`, `label` and `default`.
|
|
99
|
+
|
|
100
100
|
Reserved for later minor versions: `teams`, editing permissions.
|
|
101
101
|
|
|
102
102
|
---
|
|
@@ -107,18 +107,19 @@ Reserved for later minor versions: `teams`, editing permissions.
|
|
|
107
107
|
|
|
108
108
|
| Term | Meaning |
|
|
109
109
|
| :--- | :--- |
|
|
110
|
+
| **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
111
|
| **Session** | One run of a game, from start to end (e.g., one quiz in a lesson). |
|
|
111
112
|
| **Mode** | `solo` (one person plays alone) or `multiplayer` (several devices together). |
|
|
112
113
|
| **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
114
|
| **Instance** | One running plugin iframe on one device. 30 phones + 1 projector = 31 instances. |
|
|
114
115
|
| **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
116
|
| **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. |
|
|
117
|
+
| **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
118
|
| **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
119
|
| **State** | The whole state of the game: one JSON value owned by the authority; its content is defined by the plugin. |
|
|
119
120
|
| **Action** | An intent of a player or a timer (e.g., "answered B"). Only actions change the state. |
|
|
120
121
|
| **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](
|
|
122
|
+
| **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
123
|
| **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
124
|
|
|
124
125
|
| Situation | Instances (views) | Authority |
|
|
@@ -144,7 +145,7 @@ A plugin does not declare views. They follow from what it declares, so the two c
|
|
|
144
145
|
|
|
145
146
|
**Multiplayer:**
|
|
146
147
|
|
|
147
|
-
1. **Lobby** (host UI). The host chooses the plugin and the set; players join and may
|
|
148
|
+
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
149
|
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
150
|
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
151
|
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 +153,8 @@ A plugin does not declare views. They follow from what it declares, so the two c
|
|
|
152
153
|
|
|
153
154
|
**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
155
|
|
|
156
|
+
**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). A continued game uses **the same set** (and the same display-order seed): the game state refers to its items. Continuing with another or an edited set is a set change that only games supporting it get (planned, section 10, `setChanged`).
|
|
157
|
+
|
|
155
158
|
---
|
|
156
159
|
|
|
157
160
|
## 4. Handshake
|
|
@@ -160,6 +163,19 @@ The connection uses [Penpal](https://github.com/Aaronius/penpal) over `postMessa
|
|
|
160
163
|
|
|
161
164
|
**Transport.** Within MAJOR version 1 the connection is Penpal 8 over `postMessage`, with the method names of sections 5 and 7. Implementations may update Penpal (including security fixes) only to versions that speak the same wire protocol: an older host must still connect to a newer plugin and the other way round. Penpal 8 needs `Promise.withResolvers` and `Object.hasOwn`; on older browsers the host and the SDK add them before connecting.
|
|
162
165
|
|
|
166
|
+
**Wire format** (Penpal 8, for implementations without the library). Every message is a plain object with `namespace: "penpal"` (and `channel`, which Memizy does not set):
|
|
167
|
+
|
|
168
|
+
| Message | Fields | Meaning |
|
|
169
|
+
| :--- | :--- | :--- |
|
|
170
|
+
| `SYN` | `participantId` (a random string) | Both sides send it with `window.postMessage(message, "*")` until they have received the other's `SYN`. |
|
|
171
|
+
| `ACK1` | – | Sent by the side whose `participantId` is greater (string comparison). |
|
|
172
|
+
| `ACK2` | – | The answer to `ACK1`; it transfers a `MessagePort` (`postMessage(message, origin, [port])`). From now on both sides use only this port. |
|
|
173
|
+
| `CALL` | `id` (string, unique per sender), `methodPath` (`["hello"]`, a one-element array with the method name of section 5 or 7), `args` (array) | A method call. |
|
|
174
|
+
| `REPLY` | `callId` (= the `id` of the call), `value` | The result. On failure also `isError: true` and, for an `Error`, `isSerializedErrorInstance: true` with `value: { name, message, stack, penpalCode }` (`penpalCode` `METHOD_NOT_FOUND` = the method does not exist, section 8.2). |
|
|
175
|
+
| `DESTROY` | – | The other side closed the connection. |
|
|
176
|
+
|
|
177
|
+
Values are copied with the structured clone algorithm (JSON values, and `Blob` for `getAsset`). A rejected call's error message starts with the error code in brackets, `"[CODE] text"` (section 8.2). A receiver accepts window messages only from the expected window (the iframe's `contentWindow`, or the parent).
|
|
178
|
+
|
|
163
179
|
1. The host creates the iframe and starts listening.
|
|
164
180
|
2. The plugin (SDK) connects and calls `hello(handshake)` within **10 s**.
|
|
165
181
|
3. The host validates the handshake and returns an `InitPayload` or rejects with a `ProtocolError`.
|
|
@@ -196,25 +212,26 @@ interface InitPayload {
|
|
|
196
212
|
};
|
|
197
213
|
|
|
198
214
|
players: Player[]; // current roster (presenter is not a player)
|
|
199
|
-
set: {
|
|
215
|
+
set: { // the questions of THIS instance (see below)
|
|
200
216
|
meta: OQSEMeta;
|
|
201
217
|
items: OQSEAnyItem[]; // only types the plugin declared, in display order (4.4); public items for non-authority instances in multiplayer
|
|
202
218
|
};
|
|
203
219
|
settings: Record<string, unknown>; // current values for every declared setting (defaults applied)
|
|
204
220
|
config: { locale: string; theme: 'light' | 'dark' };
|
|
205
221
|
clock: SessionClock; // session clock and pauses (below)
|
|
206
|
-
services
|
|
222
|
+
services: string[]; // services declared by the plugin AND offered by this host
|
|
207
223
|
progress: Record<string, ProgressRecord>; // learning progress of `self` (empty for board/settings)
|
|
208
224
|
data: { plugin: unknown; set: unknown }; // plugin data of `self` (null when nothing saved; null for board/settings)
|
|
209
225
|
snapshot: unknown | null; // last saved snapshot, only for the authority (resume)
|
|
226
|
+
seed?: string; // only for the authority: the secret seed of the game's random numbers
|
|
210
227
|
}
|
|
211
228
|
|
|
212
|
-
type Address = string; // player ID, "board"; "server" is reserved
|
|
229
|
+
type Address = string; // player ID, "board", "settings"; "server" is reserved
|
|
213
230
|
|
|
214
231
|
interface SessionClock {
|
|
215
232
|
offsetMs: number; // Date.now() + offsetMs = session time
|
|
216
|
-
pausedMs
|
|
217
|
-
pausedAt
|
|
233
|
+
pausedMs: number; // total of finished pauses
|
|
234
|
+
pausedAt: number | null; // session time when the current pause began (null = running)
|
|
218
235
|
}
|
|
219
236
|
// Game time = (pausedAt ?? session time) - pausedMs. The SDK uses game time for ctx.now,
|
|
220
237
|
// timers and countdowns, so a pause stops them everywhere and nothing jumps afterwards.
|
|
@@ -227,23 +244,28 @@ interface Player {
|
|
|
227
244
|
}
|
|
228
245
|
```
|
|
229
246
|
|
|
247
|
+
**The seed of the game rules:** the host gives the authority a random `seed` (at least 128 bits of entropy, e.g. 32 hex digits) that no other instance gets. The SDK seeds `ctx.random` / `ctx.shuffle` with it, so players cannot compute a shuffled deck or the next draw from data they have (the session `id` is known to every instance). A resumed authority continues from the generator state in its snapshot; a host that records a game for replay keeps the seed with it.
|
|
248
|
+
|
|
249
|
+
**The set of an instance:** `set` holds the questions this instance plays with. In 1.0 every instance of a session gets the same set (the authority and solo with answers, the others as public items, section 4.4). A host MAY give a game only a part of a large set (e.g., a lesson's items, or the items due for review); the game plays with what it gets. Plugins MUST NOT assume more than that: a later version may let players bring their own sets (planned in section 10, "Personal sets"), and then the set of each player's instance can differ – a plugin opts in to that in its manifest, so 1.0 plugins never see it.
|
|
250
|
+
|
|
230
251
|
**Assets:** every `MediaObject.value` in `set` is either an absolute `https:` URL, or a host-relative key that the SDK resolves with `getAsset` (section 5). The host never passes `blob:` URLs, because an opaque-origin iframe cannot load the host's blob URLs.
|
|
231
252
|
|
|
232
253
|
### 4.3 Version negotiation
|
|
233
254
|
|
|
234
255
|
* 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
256
|
* 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
|
-
* A plugin that uses a field, value
|
|
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](
|
|
257
|
+
* A plugin that uses a field, value or method introduced in protocol 1.*x* declares `protocol: "1.x"` in its manifest (the minimum version it needs). Setting types are the exception: older hosts pass their default (section 2). A host that supports only an older minor version then rejects it before loading, instead of misreading what it does not know.
|
|
258
|
+
* `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
259
|
* 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
260
|
|
|
240
261
|
### 4.4 Display order and hidden answers
|
|
241
262
|
|
|
242
263
|
The host prepares the set once per session (`prepareDisplaySet` in `@memizy/protocol`; a future server authority does the same):
|
|
243
264
|
|
|
244
|
-
1. **Display order.** With a seed (
|
|
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)`, `
|
|
246
|
-
3. **
|
|
265
|
+
1. **Display order.** With a **secret** seed (random per game, kept by the host), 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. **Each item is shuffled on its own**, with a generator seeded by the seed and the item's ID, so editing, adding or removing an item – or giving a game only part of a set – never changes the order of the other items. The seed MUST NOT be derived from anything the instances know (such as the session id): authors often write a set in the answer's order (the correct order of `sort-items` or `timeline`, prompt *i* with match *i*), so a player who could repeat the shuffle could undo it and read the answer. Instances get the shuffled items and never need the seed; a host that continues a saved game uses the same seed again.
|
|
266
|
+
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. Media (`assets`) that only removed fields refer to (the back of a flashcard, an explanation) are removed too, from the item and from the set's `meta.assets`, and a non-authority instance can fetch only media of its own items (`getAsset`). **Public items of official types keep only the fields the host knows are not part of the answer** (OQSE "Hiding answers safely"): every other field – a field from a newer OQSE version, a vendor key, an unknown field of a choice or another nested object, free-form `appSpecific` and `customData` – is removed, so that unknown data never reveals an answer. A host that sends the game to other devices sends only public items.
|
|
267
|
+
3. **Choice IDs are opaque to games.** A host MAY give the instances of a session per-session IDs for choices instead of the set's IDs (the same on every instance; it maps recorded answers back to the set's IDs), e.g. because a set's IDs give the answer away (`"correct"`, or `"a"` for an answer written first). Games MUST NOT compare choice IDs with IDs written in their code; they compare answers only through `checkAnswer` and the items they get.
|
|
268
|
+
4. **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
269
|
|
|
248
270
|
Solo instances and the authority get the full items in display order.
|
|
249
271
|
|
|
@@ -255,32 +277,30 @@ All methods return Promises and reject with a `ProtocolError` (section 8.2). Met
|
|
|
255
277
|
|
|
256
278
|
| Method | Description |
|
|
257
279
|
| :--- | :--- |
|
|
258
|
-
| `hello(handshake): InitPayload` | Handshake (section 4). Called exactly once
|
|
280
|
+
| `hello(handshake): InitPayload` | Handshake (section 4). Called exactly once per instance; a second call rejects with `INVALID_ARGUMENT`. |
|
|
259
281
|
| `ready(): void` | The plugin is rendered and interactive. |
|
|
260
282
|
| `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
|
-
| `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). |
|
|
283
|
+
| `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. |
|
|
284
|
+
| `recordAnswer(answer: AnswerRecord): void` | *authority.* 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). Only the authority records: it has the answers and checks them – another instance could not prove an answer, and a modified player's device must not be able to write "correct" into a teacher's statistics. |
|
|
264
285
|
| `saveData(scope: 'plugin' \| 'set', value: unknown): void` | *views `solo`, `controller`.* Replaces the plugin data of `self` in the given scope (section 5.2). |
|
|
265
286
|
| `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
287
|
| `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. |
|
|
288
|
+
| `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. |
|
|
289
|
+
| `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. |
|
|
290
|
+
| `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
291
|
|
|
273
292
|
```ts
|
|
274
293
|
interface AnswerRecord {
|
|
275
|
-
playerId?: string; // default: self
|
|
294
|
+
playerId?: string; // default: self (the authority as a player: solo, or the host playing along); required on the board
|
|
276
295
|
itemId: string;
|
|
277
296
|
isCorrect: boolean;
|
|
278
297
|
confidence?: 1 | 2 | 3 | 4;
|
|
279
298
|
timeSpentMs?: number;
|
|
280
299
|
hintsUsed?: number;
|
|
281
300
|
isSkipped?: boolean;
|
|
282
|
-
|
|
283
|
-
|
|
301
|
+
score?: number; // partial credit 0–1 (e.g. 2 of 3 pairs); isCorrect stays the pass/fail outcome
|
|
302
|
+
answer?: unknown; // what the player answered (checkAnswer format, choice IDs; max 4 KB)
|
|
303
|
+
item?: { id: string; type: string; [key: string]: unknown }; // required for an item that is not in the set
|
|
284
304
|
}
|
|
285
305
|
|
|
286
306
|
interface SessionResult {
|
|
@@ -291,14 +311,14 @@ interface SessionResult {
|
|
|
291
311
|
|
|
292
312
|
### 5.1 Learning progress
|
|
293
313
|
|
|
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.
|
|
314
|
+
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
315
|
|
|
299
|
-
|
|
316
|
+
What `recordAnswer` carries besides the result:
|
|
300
317
|
|
|
301
|
-
* `answer` – what the player answered, in the format of the item type defined in [OQSE Answer Formats](
|
|
318
|
+
* `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.
|
|
319
|
+
* `score` – partial credit between 0 and 1 for items that allow it (OQSE `pedagogy.partialCredit`, e.g. 2 of 3 correct pairs = `0.67`). `isCorrect` stays the outcome used for repetition; `score` is for statistics. The plugin computes it (`checkAnswer` returns only right or wrong).
|
|
320
|
+
* **Answers that are not checked automatically** (`open-ended`, OQSE Answer Formats): the plugin records the `answer`; `isCorrect` is ignored. The host keeps the answer for a teacher (or an evaluator) and does not change the repetition schedule.
|
|
321
|
+
* **One answer per question and player:** a game that lets a player try again records the **first** attempt (that is what repetition needs); later attempts are part of the game.
|
|
302
322
|
* **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
323
|
* 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
324
|
|
|
@@ -354,7 +374,7 @@ If the authority instance loses its connection or is being recreated (e.g., the
|
|
|
354
374
|
|
|
355
375
|
* 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
376
|
* 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('
|
|
377
|
+
* 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
378
|
|
|
359
379
|
---
|
|
360
380
|
|
|
@@ -366,10 +386,9 @@ If the authority instance loses its connection or is being recreated (e.g., the
|
|
|
366
386
|
| `deliver(message: IncomingMessage): void` | A game message from another instance. |
|
|
367
387
|
| `playersChanged(players: Player[]): void` | The roster changed (join, leave, rename, connection status). Always the full list. |
|
|
368
388
|
| `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
389
|
| `configChanged(config: { locale, theme }): void` | Locale or theme changed. |
|
|
371
|
-
| `clockChanged(clock: SessionClock): void` | Clock synchronization updated, or
|
|
372
|
-
| `sessionEnded(reason: 'finished' \| '
|
|
390
|
+
| `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. |
|
|
391
|
+
| `sessionEnded(reason: 'finished' \| 'host-left' \| 'kicked' \| 'closed' \| 'error'): void` | The host is closing this instance. |
|
|
373
392
|
|
|
374
393
|
---
|
|
375
394
|
|
|
@@ -382,13 +401,17 @@ The values are **guaranteed minimums**: a host MUST accept at least this much an
|
|
|
382
401
|
| Limit | Guaranteed minimum | When exceeded |
|
|
383
402
|
| :--- | :--- | :--- |
|
|
384
403
|
| `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** (
|
|
404
|
+
| `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
405
|
| `saveSnapshot` size | 1 MB | `SNAPSHOT_TOO_LARGE` |
|
|
387
406
|
| `saveSnapshot` rate | 2 per second (the host keeps the latest) | coalesced, no error |
|
|
388
407
|
| `saveData` size per scope | 256 KB | `DATA_TOO_LARGE` |
|
|
389
408
|
| `saveData` rate | 1 per second per scope (the host keeps the latest) | coalesced, no error |
|
|
390
409
|
| `hello` deadline | 10 s after iframe load | the host shows an error |
|
|
391
410
|
| `recordAnswer` answer / generated item | 4 KB / 32 KB | `DATA_TOO_LARGE` |
|
|
411
|
+
| `recordAnswer` rate per instance | 10 per second (burst 20); the authority: 10 per second **per player** (it records for everyone) | `RATE_LIMITED` |
|
|
412
|
+
| `getAsset` size | 2 MB per asset | `DATA_TOO_LARGE` |
|
|
413
|
+
| `getAsset` rate per instance | 10 per second (burst 20) | `RATE_LIMITED` |
|
|
414
|
+
| `reportError` rate per instance | 5 per second (burst 10) | dropped, no error |
|
|
392
415
|
| `service` request / result | 64 KB / 256 KB | `MESSAGE_TOO_LARGE` |
|
|
393
416
|
| `service` rate per instance | 30 calls/min | `RATE_LIMITED` |
|
|
394
417
|
|
|
@@ -415,7 +438,8 @@ interface ProtocolError {
|
|
|
415
438
|
| `ASSET_NOT_FOUND` | `getAsset` with an unknown key. |
|
|
416
439
|
| `SESSION_ENDED` | A call after `sessionEnded`. |
|
|
417
440
|
| `INTERNAL_ERROR` | A host-side failure; the plugin may retry. |
|
|
418
|
-
| `SERVICE_UNAVAILABLE` |
|
|
441
|
+
| `SERVICE_UNAVAILABLE` | The service is not declared by the plugin or not offered by this host. |
|
|
442
|
+
| `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
443
|
|
|
420
444
|
New codes may be added in minor versions; plugins MUST treat unknown codes like `INTERNAL_ERROR`.
|
|
421
445
|
|
|
@@ -425,12 +449,12 @@ New codes may be added in minor versions; plugins MUST treat unknown codes like
|
|
|
425
449
|
|
|
426
450
|
### 8.4 Security
|
|
427
451
|
|
|
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).
|
|
452
|
+
* 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
453
|
* The host validates every incoming call against the schemas in `@memizy/protocol` and the limits above.
|
|
430
454
|
* The host never passes credentials, user e-mail or other private data. `Player.name` is the display name chosen for the session.
|
|
431
|
-
* 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
|
|
455
|
+
* Answers are known only to the authority (section 4.4), and so are the seed of the game's random numbers (section 4.2) and the seed of the display order (the host's, 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.
|
|
456
|
+
* **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").
|
|
457
|
+
* **Devices.** The iframe `allow` attribute grants only `permissions.devices`; the browser still asks the user.
|
|
434
458
|
|
|
435
459
|
---
|
|
436
460
|
|
|
@@ -446,7 +470,9 @@ From `1.0` on, the following MUST NOT change in a backward-incompatible way with
|
|
|
446
470
|
|
|
447
471
|
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
472
|
|
|
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.
|
|
473
|
+
* 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.
|
|
474
|
+
* **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.
|
|
475
|
+
* 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. The functions (`safeParsePluginManifest`, `resolveSettings`, … and in `@memizy/oqse` `loadOQSEFile`, `safeValidateOQSEFile`, …) and the published JSON Schemas are the stable way to validate. The exported `*Schema` objects are Zod schemas whose `parse` / `safeParse` behaviour is covered; their Zod-specific API (`.extend`, `.shape`, `.pick`, the Zod major version) is not – a 1.x release may move to a newer Zod (noted in the changelog).
|
|
450
476
|
* 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
477
|
* 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
478
|
|
|
@@ -464,12 +490,17 @@ What was decided and why, and what is **reserved** (name and intent fixed, not i
|
|
|
464
490
|
4. **"Always new" exercises come from generators** that return ordinary OQSE items (in the plugin with `ctx.random()`, or as a service with a database or AI behind it). Everything else – rendering, `checkAnswer`, recording – works the same; answers are recorded as generated items.
|
|
465
491
|
5. **Progress for teachers** = `recordAnswer` + OQSE `skills` from the content (hierarchical IDs from a shared vocabulary); a game's own model of the player stays in plugin data.
|
|
466
492
|
6. **Choices have IDs** (OQSE): answers and statistics survive edits of a set; the host shuffles freely.
|
|
467
|
-
7.
|
|
493
|
+
7. **Questions have two independent axes.** *Whose content*: one shared set for everyone (the authority checks the answers, which stay hidden – fair, competitive) or each player's own set (planned, "Personal sets": the player's device checks – relaxed, based on trust). *How questions are chosen*: the same for everyone (a fair race) or different for each player (randomly, at their own pace, or by their own progress – planned `pickItems`). The second axis is the game's rule, not a manifest declaration. Learning progress always belongs to each player; a class view is an aggregate of the players' progress.
|
|
494
|
+
8. **`playerView`** (per-player views of the state) is an optional SDK feature, not a protocol requirement: games without secrets need nothing.
|
|
468
495
|
|
|
469
496
|
**Reserved**
|
|
470
497
|
|
|
471
498
|
| Name | Intent |
|
|
472
499
|
| :--- | :--- |
|
|
500
|
+
| Manifest `modes.multiplayer.sets`, feature `personal-sets`, `Player.set` | **Personal sets** ("everyone their own questions"): in multiplayer each player brings their own set (e.g., Spanish for one, chemistry for another, HSK 1 and HSK 3) and plays on one shared map. Values `"shared"` (today, the default) and `"personal"`. In `personal`, a player's instance gets that player's own full set and checks the answers itself on the player's device (trust, not fairness – a relaxed game among friends); the other instances and the authority get only the outcomes through the game's messages, and the board gets an empty set. `Player.set` (`{ id, title, language }`) tells the board who learns what. The player's set never travels through the relay. Plugins opt in; 1.0 plugins stay `"shared"`. |
|
|
501
|
+
| Host method `pickItems(playerId, count)` | **Questions by each player's progress**: the host chooses the next items for a player with its learning algorithm (due items first), so a shared-set game can give every player different questions by their own progress without knowing it. Guarded by a feature. |
|
|
502
|
+
| Host method `openSource({ setId, itemId, quote })` | "Where to learn this" from inside a game: the host opens the lesson an item's `sources` point to (OQSE `SourceReference.itemId` and `quote`) over the game, without leaving it. Hosts already show these links in their own review of mistakes after a game. |
|
|
503
|
+
| Host method `markItem(itemId, { starred })` | A star on an item from inside a game (OQSEP `starred`); hosts offer "practise only starred" by filtering the set before the game. |
|
|
473
504
|
| Address `"server"` | Server authority: official games run their rules on a server (graded homework, larger games). The display order and public items of section 4.4 already work the same there. |
|
|
474
505
|
| Item field `generated: { by, seed }` | Where a generated item comes from (service or generator) and how to recreate it. |
|
|
475
506
|
| Host method `uploadAsset` | Files created by players (drawings, photos) stored by the host and referenced like set assets. |
|
|
@@ -479,12 +510,17 @@ What was decided and why, and what is **reserved** (name and intent fixed, not i
|
|
|
479
510
|
| Feature `teams` | Teams managed by the host (today every game handles teams itself). |
|
|
480
511
|
| Hot-seat | Several players sharing one device; v1 assumes one player per instance. |
|
|
481
512
|
| Host migration | Moving the authority to another device when its device is gone for good; v1 resumes the same authority from the snapshot. |
|
|
513
|
+
| 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. |
|
|
514
|
+
| 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). |
|
|
515
|
+
| Plugin method `setChanged`, host method `chooseSet`, manifest key `appSpecific.memizy.setChanges` | **Changing the set of a running game**: the author edits it (in the Lab, or reloads it with changes), the teacher switches the class set, a game is continued with another set, or – with personal sets – a player picks another set of their own. A game opts in with `setChanges` (a feature in the handshake); other games never get a change. The host calls `setChanged` with the new set of each instance (full or public items, the progress and the `set` plugin data that belong to it). The SDK compares the items by ID and calls a hook where the items live: for a shared set on the authority, `itemsChanged(state, ctx, { added, removed, changed })`, as a system action (deterministic, recorded for replays); for a personal set on the player's device (its own question flow, e.g. the queue in `ui.local`), while the authority only sees the new `Player.set`; an edited item keeps its ID, so the game state stays valid, and its display order does not move (4.4). `chooseSet()` lets a game open the host's set picker from its own UI (a game never sees the player's library). With personal sets a change concerns only that player's instance: the authority is not involved. |
|
|
516
|
+
| Host method `saveProgress` | Writing learning progress directly, e.g. a placement test ("these items are known"); the host may refuse or limit it. |
|
|
517
|
+
| 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
518
|
|
|
483
519
|
---
|
|
484
520
|
|
|
485
521
|
## Changelog
|
|
486
522
|
|
|
487
|
-
* **1.0 (
|
|
523
|
+
* **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. Forward compatibility: public items keep only fields known not to be part of the answer (4.4); `InitPayload.set` holds the questions of this instance (4.2); unknown setting types are hidden and get their default, so new setting types need no newer protocol (2, 4.3); `AnswerRecord.score` (partial credit). Decided: the two axes of questions (10). Reserved: personal sets (`modes.multiplayer.sets`, feature `personal-sets`, `Player.set`), `pickItems`, `openSource`, `markItem` (10). `recordAnswer` is an *authority* method (5): other instances cannot prove an answer. The seed of the display order is secret and random (4.4; it was the session id by default, so a player could undo the shuffle). Public items lose media that only hidden fields use; `getAsset` gives an instance only media of its own set (4.4). Choice IDs are opaque to games; a host may give per-session IDs (4.4). `InitPayload.seed`: a secret seed for the game's random numbers, only for the authority (4.2, 8.4) – before, the SDK seeded them with the session id that every instance knows. The Penpal 8 wire format is described (4). A host may give a game a part of a large set (4.2). Answers to `open-ended` items do not change repetition; games record the first attempt (5.1). Each item is shuffled with its own seed (4.4); a continued game uses the same set (3.3); planned set changes (`setChanged`, `chooseSet`, `setChanges`, 10). Zod: only `parse` / `safeParse` of the exported schemas are covered (9). 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
524
|
* **RC1 (2026-10-04):** first release candidate.
|
|
489
525
|
* **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
526
|
* **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("./
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./version.cjs"),t=require("./errors.cjs"),n=require("./limits.cjs"),r=require("./settings.cjs"),i=require("./settingsSchema.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=i.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.SETTING_TYPES=r.SETTING_TYPES,exports.ServiceNameSchema=o.ServiceNameSchema,exports.SessionResultSchema=c.SessionResultSchema,exports.SettingDefinitionSchema=i.SettingDefinitionSchema,exports.SettingsUpdateSchema=c.SettingsUpdateSchema,exports.UnknownSettingSchema=i.UnknownSettingSchema,exports.compareProtocolVersions=e.compareProtocolVersions,exports.extractManifestFromHtml=a.extractManifestFromHtml,exports.isKnownSetting=r.isKnownSetting,exports.isProtocolErrorCode=t.isProtocolErrorCode,exports.isProtocolSupported=e.isProtocolSupported,exports.localize=r.localize,exports.negotiateProtocolVersion=e.negotiateProtocolVersion,exports.parsePluginManifest=o.parsePluginManifest,exports.parseProtocolVersion=e.parseProtocolVersion,exports.prepareDisplaySet=u.prepareDisplaySet,exports.readPluginManifestFromHtml=o.readPluginManifestFromHtml,exports.resolveSettings=r.resolveSettings,exports.safeParsePluginManifest=o.safeParsePluginManifest,exports.settingsForMode=r.settingsForMode,exports.toProtocolError=t.toProtocolError,exports.validateSettingValue=r.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 {
|
|
5
|
-
import {
|
|
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 { SETTING_TYPES as u, isKnownSetting as d, localize as f, resolveSettings as p, settingsForMode as m, validateSettingValue as h } from "./settings.js";
|
|
5
|
+
import { LocalizedTextSchema as g, SettingDefinitionSchema as _, UnknownSettingSchema as v } from "./settingsSchema.js";
|
|
6
|
+
import { extractManifestFromHtml as y } from "./pluginRuntime.js";
|
|
7
|
+
import { HostAsSchema as b, MemizyRuntimeSchema as x, MultiplayerModeSchema as S, PluginManifestError as C, PluginPermissionsSchema as w, ServiceNameSchema as T, parsePluginManifest as E, readPluginManifestFromHtml as D, safeParsePluginManifest as O } from "./manifestSchema.js";
|
|
8
|
+
import { BOARD_ADDRESS as k, HOST_API_METHODS as A, PLUGIN_API_METHODS as j, SERVER_ADDRESS as M, SETTINGS_ADDRESS as N } from "./types.js";
|
|
9
|
+
import { AddressSchema as P, AnswerRecordSchema as F, DataScopeSchema as I, ErrorReportSchema as L, HandshakeSchema as R, OutgoingMessageSchema as z, SessionResultSchema as B, SettingsUpdateSchema as V } from "./schemas.js";
|
|
10
|
+
import { RELAY_PROTOCOL as H } from "./relay.js";
|
|
11
|
+
import { prepareDisplaySet as U } from "./items.js";
|
|
12
|
+
import { PLUGIN_DEVICES as W } from "./security.js";
|
|
13
|
+
export { P as AddressSchema, F as AnswerRecordSchema, k as BOARD_ADDRESS, I as DataScopeSchema, L as ErrorReportSchema, A as HOST_API_METHODS, R as HandshakeSchema, b as HostAsSchema, l as LIMITS, g as LocalizedTextSchema, x as MemizyRuntimeSchema, S as MultiplayerModeSchema, z as OutgoingMessageSchema, j as PLUGIN_API_METHODS, W as PLUGIN_DEVICES, a as PROTOCOL_ERROR_CODES, e as PROTOCOL_VERSION, C as PluginManifestError, w as PluginPermissionsSchema, o as ProtocolError, H as RELAY_PROTOCOL, M as SERVER_ADDRESS, N as SETTINGS_ADDRESS, u as SETTING_TYPES, T as ServiceNameSchema, B as SessionResultSchema, _ as SettingDefinitionSchema, V as SettingsUpdateSchema, v as UnknownSettingSchema, t as compareProtocolVersions, y as extractManifestFromHtml, d as isKnownSetting, s as isProtocolErrorCode, n as isProtocolSupported, f as localize, r as negotiateProtocolVersion, E as parsePluginManifest, i as parseProtocolVersion, U as prepareDisplaySet, D as readPluginManifestFromHtml, p as resolveSettings, O 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;
|