@memizy/protocol 1.0.0-rc.6 → 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 +12 -0
- package/README.md +1 -1
- package/SPEC.md +43 -13
- package/dist/index.cjs +1 -1
- package/dist/index.js +10 -10
- package/dist/items.cjs +1 -1
- package/dist/items.js +106 -41
- 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/schemas.cjs +1 -1
- package/dist/schemas.js +1 -0
- 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/index.d.ts +2 -2
- package/dist/src/items.d.ts +6 -3
- package/dist/src/manifestSchema.d.ts +42 -49
- package/dist/src/pluginRuntime.d.ts +6 -0
- package/dist/src/schemas.d.ts +2 -0
- package/dist/src/settings.d.ts +23 -2
- package/dist/src/settingsSchema.d.ts +63 -0
- package/dist/src/types.d.ts +11 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
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
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
|
+
|
|
5
17
|
## 1.0.0-rc.6 (2026-10-14)
|
|
6
18
|
|
|
7
19
|
The contract of protocol 1.0 is final: within MAJOR 1 it changes only additively (SPEC 9).
|
package/README.md
CHANGED
|
@@ -20,4 +20,4 @@ if (result.success) console.log(result.runtime.views); // e.g. ['solo', 'board',
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
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
|
|
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
|
@@ -95,6 +95,8 @@ The OQSEM part describes **content** (item types, assets, features) and is used
|
|
|
95
95
|
|
|
96
96
|
`label`, `description` and option labels are either a string or a map of BCP 47 language → string (e.g. `{ "cs": "Čas", "en": "Time" }`).
|
|
97
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
|
+
|
|
98
100
|
Reserved for later minor versions: `teams`, editing permissions.
|
|
99
101
|
|
|
100
102
|
---
|
|
@@ -151,7 +153,7 @@ A plugin does not declare views. They follow from what it declares, so the two c
|
|
|
151
153
|
|
|
152
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.
|
|
153
155
|
|
|
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).
|
|
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`).
|
|
155
157
|
|
|
156
158
|
---
|
|
157
159
|
|
|
@@ -161,6 +163,19 @@ The connection uses [Penpal](https://github.com/Aaronius/penpal) over `postMessa
|
|
|
161
163
|
|
|
162
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.
|
|
163
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
|
+
|
|
164
179
|
1. The host creates the iframe and starts listening.
|
|
165
180
|
2. The plugin (SDK) connects and calls `hello(handshake)` within **10 s**.
|
|
166
181
|
3. The host validates the handshake and returns an `InitPayload` or rejects with a `ProtocolError`.
|
|
@@ -197,7 +212,7 @@ interface InitPayload {
|
|
|
197
212
|
};
|
|
198
213
|
|
|
199
214
|
players: Player[]; // current roster (presenter is not a player)
|
|
200
|
-
set: {
|
|
215
|
+
set: { // the questions of THIS instance (see below)
|
|
201
216
|
meta: OQSEMeta;
|
|
202
217
|
items: OQSEAnyItem[]; // only types the plugin declared, in display order (4.4); public items for non-authority instances in multiplayer
|
|
203
218
|
};
|
|
@@ -208,6 +223,7 @@ interface InitPayload {
|
|
|
208
223
|
progress: Record<string, ProgressRecord>; // learning progress of `self` (empty for board/settings)
|
|
209
224
|
data: { plugin: unknown; set: unknown }; // plugin data of `self` (null when nothing saved; null for board/settings)
|
|
210
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
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
type Address = string; // player ID, "board", "settings"; "server" is reserved
|
|
@@ -228,13 +244,17 @@ interface Player {
|
|
|
228
244
|
}
|
|
229
245
|
```
|
|
230
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
|
+
|
|
231
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.
|
|
232
252
|
|
|
233
253
|
### 4.3 Version negotiation
|
|
234
254
|
|
|
235
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").
|
|
236
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.
|
|
237
|
-
* A plugin that uses a field, value
|
|
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.
|
|
238
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.
|
|
239
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.
|
|
240
260
|
|
|
@@ -242,9 +262,10 @@ interface Player {
|
|
|
242
262
|
|
|
243
263
|
The host prepares the set once per session (`prepareDisplaySet` in `@memizy/protocol`; a future server authority does the same):
|
|
244
264
|
|
|
245
|
-
1. **Display order.** With a seed (
|
|
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.
|
|
247
|
-
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.
|
|
248
269
|
|
|
249
270
|
Solo instances and the authority get the full items in display order.
|
|
250
271
|
|
|
@@ -260,7 +281,7 @@ All methods return Promises and reject with a `ProtocolError` (section 8.2). Met
|
|
|
260
281
|
| `ready(): void` | The plugin is rendered and interactive. |
|
|
261
282
|
| `send(message: OutgoingMessage): void` | Sends a game message to other instances (section 6). |
|
|
262
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. |
|
|
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). |
|
|
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). |
|
|
@@ -270,13 +291,14 @@ All methods return Promises and reject with a `ProtocolError` (section 8.2). Met
|
|
|
270
291
|
|
|
271
292
|
```ts
|
|
272
293
|
interface AnswerRecord {
|
|
273
|
-
playerId?: string; // default: self
|
|
294
|
+
playerId?: string; // default: self (the authority as a player: solo, or the host playing along); required on the board
|
|
274
295
|
itemId: string;
|
|
275
296
|
isCorrect: boolean;
|
|
276
297
|
confidence?: 1 | 2 | 3 | 4;
|
|
277
298
|
timeSpentMs?: number;
|
|
278
299
|
hintsUsed?: number;
|
|
279
300
|
isSkipped?: boolean;
|
|
301
|
+
score?: number; // partial credit 0–1 (e.g. 2 of 3 pairs); isCorrect stays the pass/fail outcome
|
|
280
302
|
answer?: unknown; // what the player answered (checkAnswer format, choice IDs; max 4 KB)
|
|
281
303
|
item?: { id: string; type: string; [key: string]: unknown }; // required for an item that is not in the set
|
|
282
304
|
}
|
|
@@ -294,6 +316,9 @@ Plugins write it with **`recordAnswer`**: the plugin reports what happened ("pla
|
|
|
294
316
|
What `recordAnswer` carries besides the result:
|
|
295
317
|
|
|
296
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.
|
|
297
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.
|
|
298
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).
|
|
299
324
|
|
|
@@ -427,7 +452,7 @@ New codes may be added in minor versions; plugins MUST treat unknown codes like
|
|
|
427
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).
|
|
428
453
|
* The host validates every incoming call against the schemas in `@memizy/protocol` and the limits above.
|
|
429
454
|
* The host never passes credentials, user e-mail or other private data. `Player.name` is the display name chosen for the session.
|
|
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.
|
|
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.
|
|
431
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").
|
|
432
457
|
* **Devices.** The iframe `allow` attribute grants only `permissions.devices`; the browser still asks the user.
|
|
433
458
|
|
|
@@ -447,7 +472,7 @@ Allowed in `1.x`: new optional fields, new methods guarded by a feature, new set
|
|
|
447
472
|
|
|
448
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.
|
|
449
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.
|
|
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.
|
|
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).
|
|
451
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.
|
|
452
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.
|
|
453
478
|
|
|
@@ -465,12 +490,17 @@ What was decided and why, and what is **reserved** (name and intent fixed, not i
|
|
|
465
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.
|
|
466
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.
|
|
467
492
|
6. **Choices have IDs** (OQSE): answers and statistics survive edits of a set; the host shuffles freely.
|
|
468
|
-
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.
|
|
469
495
|
|
|
470
496
|
**Reserved**
|
|
471
497
|
|
|
472
498
|
| Name | Intent |
|
|
473
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. |
|
|
474
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. |
|
|
475
505
|
| Item field `generated: { by, seed }` | Where a generated item comes from (service or generator) and how to recreate it. |
|
|
476
506
|
| Host method `uploadAsset` | Files created by players (drawings, photos) stored by the host and referenced like set assets. |
|
|
@@ -482,7 +512,7 @@ What was decided and why, and what is **reserved** (name and intent fixed, not i
|
|
|
482
512
|
| Host migration | Moving the authority to another device when its device is gone for good; v1 resumes the same authority from the snapshot. |
|
|
483
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. |
|
|
484
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). |
|
|
485
|
-
| Plugin method `setChanged` |
|
|
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. |
|
|
486
516
|
| Host method `saveProgress` | Writing learning progress directly, e.g. a placement test ("these items are known"); the host may refuse or limit it. |
|
|
487
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. |
|
|
488
518
|
|
|
@@ -490,7 +520,7 @@ What was decided and why, and what is **reserved** (name and intent fixed, not i
|
|
|
490
520
|
|
|
491
521
|
## Changelog
|
|
492
522
|
|
|
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).
|
|
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).
|
|
494
524
|
* **RC1 (2026-10-04):** first release candidate.
|
|
495
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.
|
|
496
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/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,13 +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
3
|
import { LIMITS as l } from "./limits.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { extractManifestFromHtml as
|
|
7
|
-
import { HostAsSchema as
|
|
8
|
-
import { BOARD_ADDRESS as
|
|
9
|
-
import { AddressSchema as
|
|
10
|
-
import { RELAY_PROTOCOL as
|
|
11
|
-
import { prepareDisplaySet as
|
|
12
|
-
import { PLUGIN_DEVICES as
|
|
13
|
-
export {
|
|
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 };
|
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.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`],
|
|
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=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function o(e,t){return e.find(e=>{let n=e._zod.def.shape?.type;return n?._zod.def.type===`literal`&&n._zod.def.values.includes(t.type)})}function s(e,t){let n=t._zod.def;switch(n.type){case`optional`:case`nullable`:case`default`:case`prefault`:case`nonoptional`:case`readonly`:case`catch`:return e==null?e:s(e,n.innerType);case`pipe`:return s(e,n.in);case`lazy`:return s(e,n.getter());case`object`:{if(!a(e))return e;let t={};for(let[r,i]of Object.entries(n.shape)){if(!(r in e))continue;let n=s(e[r],i);n!==void 0&&(t[r]=n)}return t}case`array`:return Array.isArray(e)?e.map(e=>s(e,n.element)):e;case`record`:return a(e)?[`unknown`,`any`].includes(n.valueType._zod.def.type)?void 0:Object.fromEntries(Object.entries(e).map(([e,t])=>[e,s(t,n.valueType)])):e;case`intersection`:return a(e)?{...s(e,n.left),...s(e,n.right)}:e;case`union`:{if(!a(e)&&!Array.isArray(e))return e;let t=n.options,r=a(e)&&o(t,e)||t.find(t=>t.safeParse(e).success);return r?s(e,r):Array.isArray(e)?[]:{}}case`unknown`:case`any`:return;default:return e}}var c=[`explanation`,`incorrectFeedback`],l={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`]},u=[`options`,`items`,`prompts`,`matches`,`categories`,`rows`,`columns`,`labels`];function d(t){let n=t.type,r=n.startsWith(`x-`)?structuredClone(t):s(structuredClone(t),e.OQSEItemSchema);for(let e of c)delete r[e];if(Array.isArray(r.sources))for(let e of r.sources)e&&typeof e==`object`&&delete e.quote;if(n.startsWith(`x-`))return delete r.solution,r.answerHidden=!0,r;for(let e of l[n]??[])delete r[e];for(let e of u)if(Array.isArray(r[e]))for(let t of r[e])t&&typeof t==`object`&&delete t.explanation;switch(n){case`fill-in-blanks`:r.blanks=Object.fromEntries(Object.keys(r.blanks??{}).map(e=>[e,[]]));break;case`fill-in-select`:for(let e of Object.values(r.blanks??{})){delete e.correctId;for(let t of e.options??[])delete t.explanation}break;case`categorize`:for(let e of r.items??[])delete e.correctCategoryId;break;case`timeline`:for(let e of r.events??[])delete e.date,delete e.precision;break;case`diagram-label`:for(let e of r.zones??[])delete e.correctLabelId;r.requireTyping&&(r.labels=[]);break;case`pin-on-image`:case`pin-on-model`:r.hotspots=[]}if(r.assets&&typeof r.assets==`object`){let e=f(r);r.assets=Object.fromEntries(Object.entries(r.assets).filter(([t])=>e.has(t.toLowerCase())))}return r.answerHidden=!0,r}function f(t){let n=new Set;for(let[r,i]of(0,e.itemRichText)(t))if(r[0]!==`assets`)for(let t of(0,e.findAssetKeys)(i))n.add(t);let r=t.targetAsset;return typeof r==`string`&&n.add(r.toLowerCase()),n}function p(t,n){if(!t.assets)return t;let r=new Set;for(let e of n)for(let t of f(e))r.add(t);for(let[n,i]of(0,e.metaRichText)(t))if(n[1]!==`assets`)for(let t of(0,e.findAssetKeys)(i))r.add(t);return t.thumbnail&&r.add(t.thumbnail.toLowerCase()),{...t,assets:Object.fromEntries(Object.entries(t.assets).filter(([e])=>r.has(e.toLowerCase())))}}function m(e,n){let r=e.items.map(e=>n===void 0?structuredClone(e):i(e,t(`${n}:${e.id}`))),a=r.map(d);return{set:{meta:e.meta,items:r},publicSet:{meta:p(e.meta,a),items:a}}}exports.displayItem=i,exports.prepareDisplaySet=m,exports.publicItem=d,exports.seededRandom=t;
|
package/dist/items.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OQSEItemSchema as e, findAssetKeys as t, isTimelineOrderCorrect as n, itemRichText as r, metaRichText as i } from "@memizy/oqse";
|
|
2
2
|
//#region src/items.ts
|
|
3
|
-
function
|
|
3
|
+
function a(e) {
|
|
4
4
|
let t = 2166136261;
|
|
5
5
|
for (let n = 0; n < e.length; n++) t = Math.imul(t ^ e.charCodeAt(n), 16777619);
|
|
6
6
|
let n = t >>> 0;
|
|
@@ -10,7 +10,7 @@ function t(e) {
|
|
|
10
10
|
return e = Math.imul(e ^ e >>> 15, e | 1), e ^= e + Math.imul(e ^ e >>> 7, e | 61), ((e ^ e >>> 14) >>> 0) / 4294967296;
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function o(e, t, n) {
|
|
14
14
|
for (let r = 0; r < 6; r++) {
|
|
15
15
|
let r = [...e];
|
|
16
16
|
for (let e = r.length - 1; e > 0; e--) {
|
|
@@ -21,25 +21,70 @@ function n(e, t, n) {
|
|
|
21
21
|
}
|
|
22
22
|
return [...e.slice(1), e[0]];
|
|
23
23
|
}
|
|
24
|
-
var
|
|
25
|
-
function
|
|
26
|
-
let
|
|
27
|
-
switch (
|
|
24
|
+
var s = (e) => (t) => t.every((t, n) => t.id === e[n]);
|
|
25
|
+
function c(e, t) {
|
|
26
|
+
let r = structuredClone(e);
|
|
27
|
+
switch (r.type) {
|
|
28
28
|
case "mcq-single":
|
|
29
|
-
case "mcq-multi": return
|
|
29
|
+
case "mcq-multi": return r.shuffle !== !1 && (r.options = o(r.options, t)), r;
|
|
30
30
|
case "fill-in-select":
|
|
31
|
-
for (let e of Object.values(
|
|
32
|
-
return
|
|
33
|
-
case "sort-items": return
|
|
34
|
-
case "match-pairs": return
|
|
35
|
-
case "match-complex": return
|
|
36
|
-
case "timeline": return
|
|
37
|
-
case "categorize": return
|
|
38
|
-
case "diagram-label": return
|
|
39
|
-
default: return
|
|
31
|
+
for (let e of Object.values(r.blanks ?? {})) e.shuffle !== !1 && (e.options = o(e.options, t));
|
|
32
|
+
return r;
|
|
33
|
+
case "sort-items": return r.items = o(r.items, t, s(r.correctOrder)), r;
|
|
34
|
+
case "match-pairs": return r.prompts = o(r.prompts, t), r.matches = o(r.matches, t), r;
|
|
35
|
+
case "match-complex": return r.prompts = o(r.prompts, t), r.matches = o(r.matches, t), r;
|
|
36
|
+
case "timeline": return r.shuffle !== !1 && (r.events = o(r.events, t, (e) => n(e))), r;
|
|
37
|
+
case "categorize": return r.items = o(r.items, t), r;
|
|
38
|
+
case "diagram-label": return r.requireTyping || (r.labels = o(r.labels, t)), r;
|
|
39
|
+
default: return r;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
var
|
|
42
|
+
var l = (e) => typeof e == "object" && !!e && !Array.isArray(e);
|
|
43
|
+
function u(e, t) {
|
|
44
|
+
return e.find((e) => {
|
|
45
|
+
let n = e._zod.def.shape?.type;
|
|
46
|
+
return n?._zod.def.type === "literal" && n._zod.def.values.includes(t.type);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function d(e, t) {
|
|
50
|
+
let n = t._zod.def;
|
|
51
|
+
switch (n.type) {
|
|
52
|
+
case "optional":
|
|
53
|
+
case "nullable":
|
|
54
|
+
case "default":
|
|
55
|
+
case "prefault":
|
|
56
|
+
case "nonoptional":
|
|
57
|
+
case "readonly":
|
|
58
|
+
case "catch": return e == null ? e : d(e, n.innerType);
|
|
59
|
+
case "pipe": return d(e, n.in);
|
|
60
|
+
case "lazy": return d(e, n.getter());
|
|
61
|
+
case "object": {
|
|
62
|
+
if (!l(e)) return e;
|
|
63
|
+
let t = {};
|
|
64
|
+
for (let [r, i] of Object.entries(n.shape)) {
|
|
65
|
+
if (!(r in e)) continue;
|
|
66
|
+
let n = d(e[r], i);
|
|
67
|
+
n !== void 0 && (t[r] = n);
|
|
68
|
+
}
|
|
69
|
+
return t;
|
|
70
|
+
}
|
|
71
|
+
case "array": return Array.isArray(e) ? e.map((e) => d(e, n.element)) : e;
|
|
72
|
+
case "record": return l(e) ? ["unknown", "any"].includes(n.valueType._zod.def.type) ? void 0 : Object.fromEntries(Object.entries(e).map(([e, t]) => [e, d(t, n.valueType)])) : e;
|
|
73
|
+
case "intersection": return l(e) ? {
|
|
74
|
+
...d(e, n.left),
|
|
75
|
+
...d(e, n.right)
|
|
76
|
+
} : e;
|
|
77
|
+
case "union": {
|
|
78
|
+
if (!l(e) && !Array.isArray(e)) return e;
|
|
79
|
+
let t = n.options, r = l(e) && u(t, e) || t.find((t) => t.safeParse(e).success);
|
|
80
|
+
return r ? d(e, r) : Array.isArray(e) ? [] : {};
|
|
81
|
+
}
|
|
82
|
+
case "unknown":
|
|
83
|
+
case "any": return;
|
|
84
|
+
default: return e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
var f = ["explanation", "incorrectFeedback"], p = {
|
|
43
88
|
note: ["hiddenContent"],
|
|
44
89
|
flashcard: ["back"],
|
|
45
90
|
"true-false": ["correctAnswer"],
|
|
@@ -55,7 +100,7 @@ var a = ["explanation", "incorrectFeedback"], o = {
|
|
|
55
100
|
matrix: ["correctCells"],
|
|
56
101
|
"open-ended": ["sampleAnswer", "rubric"],
|
|
57
102
|
"chess-puzzle": ["correctAnswers"]
|
|
58
|
-
},
|
|
103
|
+
}, m = [
|
|
59
104
|
"options",
|
|
60
105
|
"items",
|
|
61
106
|
"prompts",
|
|
@@ -65,50 +110,70 @@ var a = ["explanation", "incorrectFeedback"], o = {
|
|
|
65
110
|
"columns",
|
|
66
111
|
"labels"
|
|
67
112
|
];
|
|
68
|
-
function
|
|
69
|
-
let
|
|
70
|
-
for (let e of
|
|
71
|
-
if (Array.isArray(
|
|
72
|
-
if (
|
|
73
|
-
for (let e of
|
|
74
|
-
for (let e of
|
|
75
|
-
switch (
|
|
113
|
+
function h(t) {
|
|
114
|
+
let n = t.type, r = n.startsWith("x-") ? structuredClone(t) : d(structuredClone(t), e);
|
|
115
|
+
for (let e of f) delete r[e];
|
|
116
|
+
if (Array.isArray(r.sources)) for (let e of r.sources) e && typeof e == "object" && delete e.quote;
|
|
117
|
+
if (n.startsWith("x-")) return delete r.solution, r.answerHidden = !0, r;
|
|
118
|
+
for (let e of p[n] ?? []) delete r[e];
|
|
119
|
+
for (let e of m) if (Array.isArray(r[e])) for (let t of r[e]) t && typeof t == "object" && delete t.explanation;
|
|
120
|
+
switch (n) {
|
|
76
121
|
case "fill-in-blanks":
|
|
77
|
-
|
|
122
|
+
r.blanks = Object.fromEntries(Object.keys(r.blanks ?? {}).map((e) => [e, []]));
|
|
78
123
|
break;
|
|
79
124
|
case "fill-in-select":
|
|
80
|
-
for (let e of Object.values(
|
|
125
|
+
for (let e of Object.values(r.blanks ?? {})) {
|
|
81
126
|
delete e.correctId;
|
|
82
127
|
for (let t of e.options ?? []) delete t.explanation;
|
|
83
128
|
}
|
|
84
129
|
break;
|
|
85
130
|
case "categorize":
|
|
86
|
-
for (let e of
|
|
131
|
+
for (let e of r.items ?? []) delete e.correctCategoryId;
|
|
87
132
|
break;
|
|
88
133
|
case "timeline":
|
|
89
|
-
for (let e of
|
|
134
|
+
for (let e of r.events ?? []) delete e.date, delete e.precision;
|
|
90
135
|
break;
|
|
91
136
|
case "diagram-label":
|
|
92
|
-
for (let e of
|
|
93
|
-
|
|
137
|
+
for (let e of r.zones ?? []) delete e.correctLabelId;
|
|
138
|
+
r.requireTyping && (r.labels = []);
|
|
94
139
|
break;
|
|
95
140
|
case "pin-on-image":
|
|
96
|
-
case "pin-on-model":
|
|
141
|
+
case "pin-on-model": r.hotspots = [];
|
|
97
142
|
}
|
|
98
|
-
|
|
143
|
+
if (r.assets && typeof r.assets == "object") {
|
|
144
|
+
let e = g(r);
|
|
145
|
+
r.assets = Object.fromEntries(Object.entries(r.assets).filter(([t]) => e.has(t.toLowerCase())));
|
|
146
|
+
}
|
|
147
|
+
return r.answerHidden = !0, r;
|
|
148
|
+
}
|
|
149
|
+
function g(e) {
|
|
150
|
+
let n = /* @__PURE__ */ new Set();
|
|
151
|
+
for (let [i, a] of r(e)) if (i[0] !== "assets") for (let e of t(a)) n.add(e);
|
|
152
|
+
let i = e.targetAsset;
|
|
153
|
+
return typeof i == "string" && n.add(i.toLowerCase()), n;
|
|
99
154
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
155
|
+
function _(e, n) {
|
|
156
|
+
if (!e.assets) return e;
|
|
157
|
+
let r = /* @__PURE__ */ new Set();
|
|
158
|
+
for (let e of n) for (let t of g(e)) r.add(t);
|
|
159
|
+
for (let [n, a] of i(e)) if (n[1] !== "assets") for (let e of t(a)) r.add(e);
|
|
160
|
+
return e.thumbnail && r.add(e.thumbnail.toLowerCase()), {
|
|
161
|
+
...e,
|
|
162
|
+
assets: Object.fromEntries(Object.entries(e.assets).filter(([e]) => r.has(e.toLowerCase())))
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function v(e, t) {
|
|
166
|
+
let n = e.items.map((e) => t === void 0 ? structuredClone(e) : c(e, a(`${t}:${e.id}`))), r = n.map(h);
|
|
102
167
|
return {
|
|
103
168
|
set: {
|
|
104
169
|
meta: e.meta,
|
|
105
|
-
items:
|
|
170
|
+
items: n
|
|
106
171
|
},
|
|
107
172
|
publicSet: {
|
|
108
|
-
meta: e.meta,
|
|
109
|
-
items:
|
|
173
|
+
meta: _(e.meta, r),
|
|
174
|
+
items: r
|
|
110
175
|
}
|
|
111
176
|
};
|
|
112
177
|
}
|
|
113
178
|
//#endregion
|
|
114
|
-
export {
|
|
179
|
+
export { c as displayItem, v as prepareDisplaySet, h as publicItem, a as seededRandom };
|
package/dist/manifestSchema.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./settingsSchema.cjs"),t=require("./pluginRuntime.cjs");let n=require("zod"),r=require("@memizy/oqse");var i=n.z.string().regex(/^\d+\.\d+$/,`Protocol version must be in MAJOR.MINOR format (e.g. "1.0")`),a=n.z.enum([`presenter`,`player`]),o=n.z.looseObject({players:n.z.looseObject({min:n.z.number().int().min(1,`players.min must be at least 1`),max:n.z.number().int().max(200,`players.max must be at most 200`),recommended:n.z.number().int().positive().optional()}).refine(e=>e.min<=e.max,{message:`players.min must not be greater than players.max`,path:[`min`]}).refine(e=>e.recommended===void 0||e.recommended>=e.min&&e.recommended<=e.max,{message:`players.recommended must be between min and max`,path:[`recommended`]}),hostAs:n.z.array(a).min(1,`hostAs must contain "presenter" and/or "player"`).refine(e=>new Set(e).size===e.length,`hostAs must not contain duplicates`),lateJoin:n.z.boolean().optional()}),s=n.z.string().regex(/^(https|wss):\/\/[a-z0-9.-]+(:\d{1,5})?$/i,`A network origin is "https://host" or "wss://host" (no path)`),c=n.z.string().max(100).regex(/^[a-z][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$/,`A service name is lowercase segments joined by dots (e.g. "chess.puzzles")`),l=n.z.looseObject({network:n.z.array(s).max(20).optional(),devices:n.z.array(n.z.enum([`camera`,`microphone`,`geolocation`,`serial`,`bluetooth`])).max(5).optional()}),u=n.z.looseObject({protocol:i,set:n.z.enum([`required`,`optional`,`none`]).optional(),modes:n.z.looseObject({solo:n.z.looseObject({}).optional(),multiplayer:o.optional()}),settings:n.z.array(e.
|
|
1
|
+
const e=require("./settingsSchema.cjs"),t=require("./pluginRuntime.cjs");let n=require("zod"),r=require("@memizy/oqse");var i=n.z.string().regex(/^\d+\.\d+$/,`Protocol version must be in MAJOR.MINOR format (e.g. "1.0")`),a=n.z.enum([`presenter`,`player`]),o=n.z.looseObject({players:n.z.looseObject({min:n.z.number().int().min(1,`players.min must be at least 1`),max:n.z.number().int().max(200,`players.max must be at most 200`),recommended:n.z.number().int().positive().optional()}).refine(e=>e.min<=e.max,{message:`players.min must not be greater than players.max`,path:[`min`]}).refine(e=>e.recommended===void 0||e.recommended>=e.min&&e.recommended<=e.max,{message:`players.recommended must be between min and max`,path:[`recommended`]}),hostAs:n.z.array(a).min(1,`hostAs must contain "presenter" and/or "player"`).refine(e=>new Set(e).size===e.length,`hostAs must not contain duplicates`),lateJoin:n.z.boolean().optional()}),s=n.z.string().regex(/^(https|wss):\/\/[a-z0-9.-]+(:\d{1,5})?$/i,`A network origin is "https://host" or "wss://host" (no path)`),c=n.z.string().max(100).regex(/^[a-z][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$/,`A service name is lowercase segments joined by dots (e.g. "chess.puzzles")`),l=n.z.looseObject({network:n.z.array(s).max(20).optional(),devices:n.z.array(n.z.enum([`camera`,`microphone`,`geolocation`,`serial`,`bluetooth`])).max(5).optional()}),u=n.z.looseObject({protocol:i,set:n.z.enum([`required`,`optional`,`none`]).optional(),modes:n.z.looseObject({solo:n.z.looseObject({}).optional(),multiplayer:o.optional()}),settings:n.z.array(e.SettingEntrySchema).optional(),settingsScreen:n.z.looseObject({size:n.z.enum([`compact`,`large`])}).optional(),display:n.z.looseObject({orientation:n.z.enum([`any`,`portrait`,`landscape`]).optional()}).optional(),permissions:l.optional(),services:n.z.array(c).max(20).optional()}).refine(e=>e.modes.solo!==void 0||e.modes.multiplayer!==void 0,{message:`modes must declare "solo" and/or "multiplayer"`,path:[`modes`]}).refine(e=>e.settingsScreen===void 0||e.modes.multiplayer!==void 0,{message:`settingsScreen is only used in the multiplayer lobby; declare modes.multiplayer or remove it`,path:[`settingsScreen`]}).refine(e=>{let t=(e.settings??[]).map(e=>e.id);return new Set(t).size===t.length},{message:`Setting ids must be unique`,path:[`settings`]});function d(e){let n=[],i=r.OQSEManifestSchema.safeParse(e);i.success||n.push(...(0,r.formatOQSEErrors)(i.error));let a=e?.appSpecific?.memizy;if(a===void 0)n.push(`appSpecific.memizy: Missing the Memizy runtime block (protocol, modes, …)`);else{let e=u.safeParse(a);e.success||n.push(...(0,r.formatOQSEErrors)(e.error).map(e=>`appSpecific.memizy.${e}`))}if(n.length>0)return{success:!1,errors:n};let o=(0,r.withManifestDefaults)(e),s=t.toRuntime(o.appSpecific.memizy),c=o.capabilities.types;return s.set!==`none`&&(!Array.isArray(c)||c.length===0)?{success:!1,errors:[`capabilities.types: Declare the item types the game shows, or "set": "none" in appSpecific.memizy for a game without questions.`]}:{success:!0,manifest:o,runtime:s}}var f=class extends Error{errors;constructor(e){super(`Invalid plugin manifest:\n${e.join(`
|
|
2
2
|
`)}`),this.name=`PluginManifestError`,this.errors=e}};function p(e){let t=d(e);if(!t.success)throw new f(t.errors);return{manifest:t.manifest,runtime:t.runtime}}function m(e){let n=t.extractManifestFromHtml(e);return n.success?d(n.data):{success:!1,errors:[n.error]}}exports.HostAsSchema=a,exports.MemizyRuntimeSchema=u,exports.MultiplayerModeSchema=o,exports.PluginManifestError=f,exports.PluginPermissionsSchema=l,exports.ServiceNameSchema=c,exports.parsePluginManifest=p,exports.readPluginManifestFromHtml=m,exports.safeParsePluginManifest=d;
|
package/dist/manifestSchema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SettingEntrySchema as e } from "./settingsSchema.js";
|
|
2
2
|
import { extractManifestFromHtml as t, toRuntime as n } from "./pluginRuntime.js";
|
|
3
3
|
import { z as r } from "zod";
|
|
4
4
|
import { OQSEManifestSchema as i, formatOQSEErrors as a, withManifestDefaults as o } from "@memizy/oqse";
|
package/dist/pluginRuntime.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
const e=require("./settings.cjs");var t=/<script\b[^>]*\btype\s*=\s*["']application\/oqse-manifest\+json["'][^>]*>([\s\S]*?)<\/script\s*>/i;function n(e){let n=t.exec(e);if(!n)return{success:!1,error:`No <script type="application/oqse-manifest+json"> data island found.`};try{return{success:!0,data:JSON.parse(n[1])}}catch(e){return{success:!1,error:`The manifest data island is not valid JSON: ${e.message}`}}}function r(t){let n=t.modes.multiplayer,r=n?{players:{min:n.players.min,max:n.players.max,...n.players.recommended!==void 0&&{recommended:n.players.recommended}},hostAs:[...n.hostAs],lateJoin:n.lateJoin??!0}:null,i=[];return t.modes.solo&&i.push(`solo`),r?.hostAs.includes(`presenter`)&&i.push(`board`),r&&i.push(`controller`),r&&t.settingsScreen&&i.push(`settings`),{protocol:t.protocol,set:t.set??`required`,solo:t.modes.solo!==void 0,multiplayer:r,settings:(t.settings??[]).filter(e.isKnownSetting),unknownSettings:(t.settings??[]).filter(t=>!e.isKnownSetting(t)),settingsScreen:t.settingsScreen?{size:t.settingsScreen.size}:null,orientation:t.display?.orientation??`any`,views:i,permissions:{network:[...new Set(t.permissions?.network??[])],devices:[...new Set(t.permissions?.devices??[])]},services:[...new Set(t.services??[])]}}exports.extractManifestFromHtml=n,exports.toRuntime=r;
|
package/dist/pluginRuntime.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { isKnownSetting as e } from "./settings.js";
|
|
1
2
|
//#region src/pluginRuntime.ts
|
|
2
|
-
var
|
|
3
|
-
function
|
|
4
|
-
let n =
|
|
3
|
+
var t = /<script\b[^>]*\btype\s*=\s*["']application\/oqse-manifest\+json["'][^>]*>([\s\S]*?)<\/script\s*>/i;
|
|
4
|
+
function n(e) {
|
|
5
|
+
let n = t.exec(e);
|
|
5
6
|
if (!n) return {
|
|
6
7
|
success: !1,
|
|
7
8
|
error: "No <script type=\"application/oqse-manifest+json\"> data island found."
|
|
@@ -18,31 +19,32 @@ function t(t) {
|
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
function
|
|
22
|
-
let
|
|
22
|
+
function r(t) {
|
|
23
|
+
let n = t.modes.multiplayer, r = n ? {
|
|
23
24
|
players: {
|
|
24
|
-
min:
|
|
25
|
-
max:
|
|
26
|
-
...
|
|
25
|
+
min: n.players.min,
|
|
26
|
+
max: n.players.max,
|
|
27
|
+
...n.players.recommended !== void 0 && { recommended: n.players.recommended }
|
|
27
28
|
},
|
|
28
|
-
hostAs: [...
|
|
29
|
-
lateJoin:
|
|
30
|
-
} : null,
|
|
31
|
-
return
|
|
32
|
-
protocol:
|
|
33
|
-
set:
|
|
34
|
-
solo:
|
|
35
|
-
multiplayer:
|
|
36
|
-
settings:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
hostAs: [...n.hostAs],
|
|
30
|
+
lateJoin: n.lateJoin ?? !0
|
|
31
|
+
} : null, i = [];
|
|
32
|
+
return t.modes.solo && i.push("solo"), r?.hostAs.includes("presenter") && i.push("board"), r && i.push("controller"), r && t.settingsScreen && i.push("settings"), {
|
|
33
|
+
protocol: t.protocol,
|
|
34
|
+
set: t.set ?? "required",
|
|
35
|
+
solo: t.modes.solo !== void 0,
|
|
36
|
+
multiplayer: r,
|
|
37
|
+
settings: (t.settings ?? []).filter(e),
|
|
38
|
+
unknownSettings: (t.settings ?? []).filter((t) => !e(t)),
|
|
39
|
+
settingsScreen: t.settingsScreen ? { size: t.settingsScreen.size } : null,
|
|
40
|
+
orientation: t.display?.orientation ?? "any",
|
|
41
|
+
views: i,
|
|
40
42
|
permissions: {
|
|
41
|
-
network: [...new Set(
|
|
42
|
-
devices: [...new Set(
|
|
43
|
+
network: [...new Set(t.permissions?.network ?? [])],
|
|
44
|
+
devices: [...new Set(t.permissions?.devices ?? [])]
|
|
43
45
|
},
|
|
44
|
-
services: [...new Set(
|
|
46
|
+
services: [...new Set(t.services ?? [])]
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
//#endregion
|
|
48
|
-
export {
|
|
50
|
+
export { n as extractManifestFromHtml, r as toRuntime };
|
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.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;
|
|
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(),score:t.z.number().min(0).max(1).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
|
@@ -33,6 +33,7 @@ var r = (e) => t.string().min(1).max(e), i = t.string().min(1).max(128), a = t.l
|
|
|
33
33
|
timeSpentMs: t.number().int().nonnegative().optional(),
|
|
34
34
|
hintsUsed: t.number().int().nonnegative().optional(),
|
|
35
35
|
isSkipped: t.boolean().optional(),
|
|
36
|
+
score: t.number().min(0).max(1).optional(),
|
|
36
37
|
answer: t.unknown().optional(),
|
|
37
38
|
item: t.looseObject({
|
|
38
39
|
id: r(128),
|
package/dist/settings.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e,t){switch(e.type){case`number`:return typeof t!=`number`||!Number.isFinite(t)?`must be a number`:e.min!==void 0&&t<e.min?`must be at least ${e.min}`:e.max!==void 0&&t>e.max?`must be at most ${e.max}`:null;case`boolean`:return typeof t==`boolean`?null:`must be true or false`;case`select`:return e.options.some(e=>e.value===t)?null:`must be one of the options`;case`text`:return typeof t==`string`?e.maxLength!==void 0&&t.length>e.maxLength?`must be at most ${e.maxLength} characters`:null:`must be text`}}function
|
|
1
|
+
var e=[`number`,`boolean`,`select`,`text`];function t(t){return e.includes(t.type)}function n(e,t){switch(e.type){case`number`:return typeof t!=`number`||!Number.isFinite(t)?`must be a number`:e.min!==void 0&&t<e.min?`must be at least ${e.min}`:e.max!==void 0&&t>e.max?`must be at most ${e.max}`:null;case`boolean`:return typeof t==`boolean`?null:`must be true or false`;case`select`:return e.options.some(e=>e.value===t)?null:`must be one of the options`;case`text`:return typeof t==`string`?e.maxLength!==void 0&&t.length>e.maxLength?`must be at most ${e.maxLength} characters`:null:`must be text`}}function r(e,t={},r=[]){let i={},a=[];for(let r of e){if(!(r.id in t)){i[r.id]=r.default;continue}let e=n(r,t[r.id]);e?(a.push(`${r.id}: ${e}`),i[r.id]=r.default):i[r.id]=t[r.id]}for(let e of r)i[e.id]=structuredClone(e.default);for(let n of Object.keys(t))!e.some(e=>e.id===n)&&!r.some(e=>e.id===n)&&a.push(`${n}: unknown setting`);return{values:i,errors:a}}function i(e,t){return e.filter(e=>!e.modes||e.modes.includes(t))}function a(e,t){if(typeof e==`string`)return e;let n=t.split(`-`)[0];return e[t]??e[n]??e.en??Object.values(e)[0]}exports.SETTING_TYPES=e,exports.isKnownSetting=t,exports.localize=a,exports.resolveSettings=r,exports.settingsForMode=i,exports.validateSettingValue=n;
|
package/dist/settings.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
//#region src/settings.ts
|
|
2
|
-
|
|
2
|
+
var e = [
|
|
3
|
+
"number",
|
|
4
|
+
"boolean",
|
|
5
|
+
"select",
|
|
6
|
+
"text"
|
|
7
|
+
];
|
|
8
|
+
function t(t) {
|
|
9
|
+
return e.includes(t.type);
|
|
10
|
+
}
|
|
11
|
+
function n(e, t) {
|
|
3
12
|
switch (e.type) {
|
|
4
13
|
case "number": return typeof t != "number" || !Number.isFinite(t) ? "must be a number" : e.min !== void 0 && t < e.min ? `must be at least ${e.min}` : e.max !== void 0 && t > e.max ? `must be at most ${e.max}` : null;
|
|
5
14
|
case "boolean": return typeof t == "boolean" ? null : "must be true or false";
|
|
@@ -7,29 +16,30 @@ function e(e, t) {
|
|
|
7
16
|
case "text": return typeof t == "string" ? e.maxLength !== void 0 && t.length > e.maxLength ? `must be at most ${e.maxLength} characters` : null : "must be text";
|
|
8
17
|
}
|
|
9
18
|
}
|
|
10
|
-
function
|
|
11
|
-
let
|
|
12
|
-
for (let
|
|
13
|
-
if (!(
|
|
14
|
-
r
|
|
19
|
+
function r(e, t = {}, r = []) {
|
|
20
|
+
let i = {}, a = [];
|
|
21
|
+
for (let r of e) {
|
|
22
|
+
if (!(r.id in t)) {
|
|
23
|
+
i[r.id] = r.default;
|
|
15
24
|
continue;
|
|
16
25
|
}
|
|
17
|
-
let
|
|
18
|
-
|
|
26
|
+
let e = n(r, t[r.id]);
|
|
27
|
+
e ? (a.push(`${r.id}: ${e}`), i[r.id] = r.default) : i[r.id] = t[r.id];
|
|
19
28
|
}
|
|
20
|
-
for (let e of
|
|
29
|
+
for (let e of r) i[e.id] = structuredClone(e.default);
|
|
30
|
+
for (let n of Object.keys(t)) !e.some((e) => e.id === n) && !r.some((e) => e.id === n) && a.push(`${n}: unknown setting`);
|
|
21
31
|
return {
|
|
22
|
-
values:
|
|
23
|
-
errors:
|
|
32
|
+
values: i,
|
|
33
|
+
errors: a
|
|
24
34
|
};
|
|
25
35
|
}
|
|
26
|
-
function
|
|
36
|
+
function i(e, t) {
|
|
27
37
|
return e.filter((e) => !e.modes || e.modes.includes(t));
|
|
28
38
|
}
|
|
29
|
-
function
|
|
39
|
+
function a(e, t) {
|
|
30
40
|
if (typeof e == "string") return e;
|
|
31
41
|
let n = t.split("-")[0];
|
|
32
42
|
return e[t] ?? e[n] ?? e.en ?? Object.values(e)[0];
|
|
33
43
|
}
|
|
34
44
|
//#endregion
|
|
35
|
-
export {
|
|
45
|
+
export { e as SETTING_TYPES, t as isKnownSetting, a as localize, r as resolveSettings, i as settingsForMode, n as validateSettingValue };
|
package/dist/settingsSchema.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("./settings.cjs");let t=require("zod");var n=t.z.union([t.z.string().min(1,`Text must not be empty`),t.z.record(t.z.string().min(2),t.z.string().min(1)).refine(e=>Object.keys(e).length>0,`Localized text needs at least one language`)]),r={id:t.z.string().regex(/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/,`Setting id must start with a letter and contain only letters, digits, "_" and "-"`),label:n,description:n.optional(),modes:t.z.array(t.z.enum([`solo`,`multiplayer`])).min(1,`modes needs at least one mode`).optional()},i=t.z.looseObject({...r,type:t.z.literal(`number`),default:t.z.number().finite(),min:t.z.number().finite().optional(),max:t.z.number().finite().optional(),step:t.z.number().positive().optional()}).refine(e=>e.min===void 0||e.max===void 0||e.min<=e.max,{message:`min must not be greater than max`,path:[`min`]}).refine(e=>(e.min===void 0||e.default>=e.min)&&(e.max===void 0||e.default<=e.max),{message:`default is outside min/max`,path:[`default`]}),a=t.z.looseObject({...r,type:t.z.literal(`boolean`),default:t.z.boolean()}),o=t.z.union([t.z.string(),t.z.number().finite(),t.z.boolean()]),s=t.z.looseObject({...r,type:t.z.literal(`select`),options:t.z.array(t.z.looseObject({value:o,label:n})).min(1,`A select setting needs at least one option`),default:o}).refine(e=>new Set(e.options.map(e=>e.value)).size===e.options.length,{message:`Option values must be unique`,path:[`options`]}).refine(e=>e.options.some(t=>t.value===e.default),{message:`default must be one of the option values`,path:[`default`]}),c=t.z.looseObject({...r,type:t.z.literal(`text`),default:t.z.string(),maxLength:t.z.number().int().positive().optional()}).refine(e=>e.maxLength===void 0||e.default.length<=e.maxLength,{message:`default is longer than maxLength`,path:[`default`]}),l=t.z.discriminatedUnion(`type`,[i,a,s,c]),u=t.z.looseObject({...r,type:t.z.string().regex(/^[a-z][a-z0-9-]*$/,`A setting type is a lowercase name`).refine(t=>!e.SETTING_TYPES.includes(t),`A known setting type`),default:t.z.unknown().refine(e=>e!==void 0,`A setting needs a default`)}),d=t.z.unknown().transform((n,r)=>{let i=typeof n==`object`&&n?n.type:void 0,a=(typeof i==`string`&&e.SETTING_TYPES.includes(i)?l:u).safeParse(n);if(!a.success){for(let e of a.error.issues)r.addIssue({code:`custom`,message:e.message,path:e.path});return t.z.NEVER}return a.data});exports.LocalizedTextSchema=n,exports.SettingDefinitionSchema=l,exports.SettingEntrySchema=d,exports.UnknownSettingSchema=u;
|
package/dist/settingsSchema.js
CHANGED
|
@@ -1,58 +1,74 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SETTING_TYPES as e } from "./settings.js";
|
|
2
|
+
import { z as t } from "zod";
|
|
2
3
|
//#region src/settingsSchema.ts
|
|
3
|
-
var
|
|
4
|
-
id:
|
|
5
|
-
label:
|
|
6
|
-
description:
|
|
7
|
-
modes:
|
|
8
|
-
},
|
|
9
|
-
...
|
|
10
|
-
type:
|
|
11
|
-
default:
|
|
12
|
-
min:
|
|
13
|
-
max:
|
|
14
|
-
step:
|
|
4
|
+
var n = t.union([t.string().min(1, "Text must not be empty"), t.record(t.string().min(2), t.string().min(1)).refine((e) => Object.keys(e).length > 0, "Localized text needs at least one language")]), r = {
|
|
5
|
+
id: t.string().regex(/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/, "Setting id must start with a letter and contain only letters, digits, \"_\" and \"-\""),
|
|
6
|
+
label: n,
|
|
7
|
+
description: n.optional(),
|
|
8
|
+
modes: t.array(t.enum(["solo", "multiplayer"])).min(1, "modes needs at least one mode").optional()
|
|
9
|
+
}, i = t.looseObject({
|
|
10
|
+
...r,
|
|
11
|
+
type: t.literal("number"),
|
|
12
|
+
default: t.number().finite(),
|
|
13
|
+
min: t.number().finite().optional(),
|
|
14
|
+
max: t.number().finite().optional(),
|
|
15
|
+
step: t.number().positive().optional()
|
|
15
16
|
}).refine((e) => e.min === void 0 || e.max === void 0 || e.min <= e.max, {
|
|
16
17
|
message: "min must not be greater than max",
|
|
17
18
|
path: ["min"]
|
|
18
19
|
}).refine((e) => (e.min === void 0 || e.default >= e.min) && (e.max === void 0 || e.default <= e.max), {
|
|
19
20
|
message: "default is outside min/max",
|
|
20
21
|
path: ["default"]
|
|
21
|
-
}),
|
|
22
|
-
...
|
|
23
|
-
type:
|
|
24
|
-
default:
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
]),
|
|
30
|
-
...
|
|
31
|
-
type:
|
|
32
|
-
options:
|
|
33
|
-
value:
|
|
34
|
-
label:
|
|
22
|
+
}), a = t.looseObject({
|
|
23
|
+
...r,
|
|
24
|
+
type: t.literal("boolean"),
|
|
25
|
+
default: t.boolean()
|
|
26
|
+
}), o = t.union([
|
|
27
|
+
t.string(),
|
|
28
|
+
t.number().finite(),
|
|
29
|
+
t.boolean()
|
|
30
|
+
]), s = t.looseObject({
|
|
31
|
+
...r,
|
|
32
|
+
type: t.literal("select"),
|
|
33
|
+
options: t.array(t.looseObject({
|
|
34
|
+
value: o,
|
|
35
|
+
label: n
|
|
35
36
|
})).min(1, "A select setting needs at least one option"),
|
|
36
|
-
default:
|
|
37
|
+
default: o
|
|
37
38
|
}).refine((e) => new Set(e.options.map((e) => e.value)).size === e.options.length, {
|
|
38
39
|
message: "Option values must be unique",
|
|
39
40
|
path: ["options"]
|
|
40
41
|
}).refine((e) => e.options.some((t) => t.value === e.default), {
|
|
41
42
|
message: "default must be one of the option values",
|
|
42
43
|
path: ["default"]
|
|
43
|
-
}),
|
|
44
|
-
...
|
|
45
|
-
type:
|
|
46
|
-
default:
|
|
47
|
-
maxLength:
|
|
44
|
+
}), c = t.looseObject({
|
|
45
|
+
...r,
|
|
46
|
+
type: t.literal("text"),
|
|
47
|
+
default: t.string(),
|
|
48
|
+
maxLength: t.number().int().positive().optional()
|
|
48
49
|
}).refine((e) => e.maxLength === void 0 || e.default.length <= e.maxLength, {
|
|
49
50
|
message: "default is longer than maxLength",
|
|
50
51
|
path: ["default"]
|
|
51
|
-
}),
|
|
52
|
-
r,
|
|
52
|
+
}), l = t.discriminatedUnion("type", [
|
|
53
53
|
i,
|
|
54
|
-
|
|
55
|
-
s
|
|
56
|
-
|
|
54
|
+
a,
|
|
55
|
+
s,
|
|
56
|
+
c
|
|
57
|
+
]), u = t.looseObject({
|
|
58
|
+
...r,
|
|
59
|
+
type: t.string().regex(/^[a-z][a-z0-9-]*$/, "A setting type is a lowercase name").refine((t) => !e.includes(t), "A known setting type"),
|
|
60
|
+
default: t.unknown().refine((e) => e !== void 0, "A setting needs a default")
|
|
61
|
+
}), d = t.unknown().transform((n, r) => {
|
|
62
|
+
let i = typeof n == "object" && n ? n.type : void 0, a = (typeof i == "string" && e.includes(i) ? l : u).safeParse(n);
|
|
63
|
+
if (!a.success) {
|
|
64
|
+
for (let e of a.error.issues) r.addIssue({
|
|
65
|
+
code: "custom",
|
|
66
|
+
message: e.message,
|
|
67
|
+
path: e.path
|
|
68
|
+
});
|
|
69
|
+
return t.NEVER;
|
|
70
|
+
}
|
|
71
|
+
return a.data;
|
|
72
|
+
});
|
|
57
73
|
//#endregion
|
|
58
|
-
export {
|
|
74
|
+
export { n as LocalizedTextSchema, l as SettingDefinitionSchema, d as SettingEntrySchema, u as UnknownSettingSchema };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
export { PROTOCOL_VERSION, parseProtocolVersion, compareProtocolVersions, negotiateProtocolVersion, isProtocolSupported, type ProtocolVersion, type HostProtocolVersions, } from './version.js';
|
|
10
10
|
export { PROTOCOL_ERROR_CODES, ProtocolError, isProtocolErrorCode, toProtocolError, type ProtocolErrorCode } from './errors.js';
|
|
11
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';
|
|
12
|
+
export { LocalizedTextSchema, SettingDefinitionSchema, UnknownSettingSchema, type LocalizedText, type SettingDefinition } from './settingsSchema.js';
|
|
13
|
+
export { validateSettingValue, resolveSettings, settingsForMode, localize, isKnownSetting, SETTING_TYPES, type SettingValue, type UnknownSettingDefinition } from './settings.js';
|
|
14
14
|
export { HostAsSchema, MultiplayerModeSchema, ServiceNameSchema, PluginPermissionsSchema, MemizyRuntimeSchema, PluginManifestError, safeParsePluginManifest, parsePluginManifest, readPluginManifestFromHtml, type MemizyRuntimeManifest, type PluginManifest, type PluginManifestResult, } from './manifestSchema.js';
|
|
15
15
|
export { extractManifestFromHtml, type HostAs, type PluginView, type PluginRuntime } from './pluginRuntime.js';
|
|
16
16
|
export * from './types.js';
|
package/dist/src/items.d.ts
CHANGED
|
@@ -11,9 +11,12 @@ export declare function seededRandom(seed: string): () => number;
|
|
|
11
11
|
/** The item in display order. Answers refer to IDs, so only the lists move. */
|
|
12
12
|
export declare function displayItem(item: OQSEAnyItem, random: () => number): OQSEAnyItem;
|
|
13
13
|
/**
|
|
14
|
-
* The item without its answer.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* The item without its answer. An official type keeps only the fields its OQSE schema lists
|
|
15
|
+
* (fail closed: unknown fields, also nested, are removed, as are free-form `appSpecific` and
|
|
16
|
+
* `customData`), then loses the fields that give the answer away. Custom (`x-`) types keep
|
|
17
|
+
* their own fields and lose only `solution` (where OQSE puts everything that gives their
|
|
18
|
+
* answer away) and the common fields. `hints` stay: a game may show them on any device while
|
|
19
|
+
* the player answers.
|
|
17
20
|
*/
|
|
18
21
|
export declare function publicItem(item: OQSEAnyItem): OQSEAnyItem;
|
|
19
22
|
/**
|
|
@@ -51,55 +51,48 @@ export declare const MemizyRuntimeSchema: z.ZodObject<{
|
|
|
51
51
|
lateJoin: z.ZodOptional<z.ZodBoolean>;
|
|
52
52
|
}, z.core.$loose>>;
|
|
53
53
|
}, z.core.$loose>;
|
|
54
|
-
settings: z.ZodOptional<z.ZodArray<z.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
97
|
-
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
98
|
-
modes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
99
|
-
solo: "solo";
|
|
100
|
-
multiplayer: "multiplayer";
|
|
101
|
-
}>>>;
|
|
102
|
-
}, z.core.$loose>], "type">>>;
|
|
54
|
+
settings: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodUnknown, z.ZodTransform<import('./settings.js').UnknownSettingDefinition | {
|
|
55
|
+
[x: string]: unknown;
|
|
56
|
+
type: "number";
|
|
57
|
+
default: number;
|
|
58
|
+
id: string;
|
|
59
|
+
label: string | Record<string, string>;
|
|
60
|
+
min?: number | undefined;
|
|
61
|
+
max?: number | undefined;
|
|
62
|
+
step?: number | undefined;
|
|
63
|
+
description?: string | Record<string, string> | undefined;
|
|
64
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
65
|
+
} | {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
type: "select";
|
|
68
|
+
options: {
|
|
69
|
+
[x: string]: unknown;
|
|
70
|
+
value: string | number | boolean;
|
|
71
|
+
label: string | Record<string, string>;
|
|
72
|
+
}[];
|
|
73
|
+
default: string | number | boolean;
|
|
74
|
+
id: string;
|
|
75
|
+
label: string | Record<string, string>;
|
|
76
|
+
description?: string | Record<string, string> | undefined;
|
|
77
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
78
|
+
} | {
|
|
79
|
+
[x: string]: unknown;
|
|
80
|
+
type: "text";
|
|
81
|
+
default: string;
|
|
82
|
+
id: string;
|
|
83
|
+
label: string | Record<string, string>;
|
|
84
|
+
maxLength?: number | undefined;
|
|
85
|
+
description?: string | Record<string, string> | undefined;
|
|
86
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
87
|
+
} | {
|
|
88
|
+
[x: string]: unknown;
|
|
89
|
+
type: "boolean";
|
|
90
|
+
default: boolean;
|
|
91
|
+
id: string;
|
|
92
|
+
label: string | Record<string, string>;
|
|
93
|
+
description?: string | Record<string, string> | undefined;
|
|
94
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
95
|
+
}, unknown>>>>;
|
|
103
96
|
settingsScreen: z.ZodOptional<z.ZodObject<{
|
|
104
97
|
size: z.ZodEnum<{
|
|
105
98
|
compact: "compact";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MemizyRuntimeManifest } from './manifestSchema.js';
|
|
2
2
|
import { SettingDefinition } from './settingsSchema.js';
|
|
3
|
+
import { UnknownSettingDefinition } from './settings.js';
|
|
3
4
|
export type HostAs = 'presenter' | 'player';
|
|
4
5
|
export type PluginView = 'solo' | 'board' | 'controller' | 'settings';
|
|
5
6
|
/** The runtime part with all defaults applied – what hosts work with. */
|
|
@@ -18,6 +19,11 @@ export interface PluginRuntime {
|
|
|
18
19
|
lateJoin: boolean;
|
|
19
20
|
};
|
|
20
21
|
settings: SettingDefinition[];
|
|
22
|
+
/**
|
|
23
|
+
* Settings of types from a later protocol version (SPEC 2): not shown; the plugin gets
|
|
24
|
+
* their `default` (pass them to `resolveSettings`).
|
|
25
|
+
*/
|
|
26
|
+
unknownSettings: UnknownSettingDefinition[];
|
|
21
27
|
settingsScreen: null | {
|
|
22
28
|
size: 'compact' | 'large';
|
|
23
29
|
};
|
package/dist/src/schemas.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare const AnswerRecordSchema: z.ZodObject<{
|
|
|
25
25
|
timeSpentMs: z.ZodOptional<z.ZodNumber>;
|
|
26
26
|
hintsUsed: z.ZodOptional<z.ZodNumber>;
|
|
27
27
|
isSkipped: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
score: z.ZodOptional<z.ZodNumber>;
|
|
28
29
|
answer: z.ZodOptional<z.ZodUnknown>;
|
|
29
30
|
item: z.ZodOptional<z.ZodObject<{
|
|
30
31
|
id: z.ZodString;
|
|
@@ -77,6 +78,7 @@ export declare const HOST_API_ARGS: {
|
|
|
77
78
|
timeSpentMs: z.ZodOptional<z.ZodNumber>;
|
|
78
79
|
hintsUsed: z.ZodOptional<z.ZodNumber>;
|
|
79
80
|
isSkipped: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
score: z.ZodOptional<z.ZodNumber>;
|
|
80
82
|
answer: z.ZodOptional<z.ZodUnknown>;
|
|
81
83
|
item: z.ZodOptional<z.ZodObject<{
|
|
82
84
|
id: z.ZodString;
|
package/dist/src/settings.d.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { LocalizedText, SettingDefinition } from './settingsSchema.js';
|
|
2
|
-
|
|
2
|
+
/** Setting types of this protocol version. */
|
|
3
|
+
export declare const SETTING_TYPES: readonly ["number", "boolean", "select", "text"];
|
|
4
|
+
/**
|
|
5
|
+
* A setting of a type from a later protocol version (SPEC 2): hosts do not show it and pass
|
|
6
|
+
* its `default` to the plugin.
|
|
7
|
+
*/
|
|
8
|
+
export interface UnknownSettingDefinition {
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
default: unknown;
|
|
12
|
+
label: LocalizedText;
|
|
13
|
+
description?: LocalizedText;
|
|
14
|
+
modes?: ('solo' | 'multiplayer')[];
|
|
15
|
+
}
|
|
16
|
+
/** Whether a manifest setting is of a type this protocol version defines. */
|
|
17
|
+
export declare function isKnownSetting(def: SettingDefinition | UnknownSettingDefinition): def is SettingDefinition;
|
|
18
|
+
/** A setting value: a number, boolean or text, or any JSON (a setting type of a later version). */
|
|
19
|
+
export type SettingValue = string | number | boolean | null | SettingValue[] | {
|
|
20
|
+
[key: string]: SettingValue;
|
|
21
|
+
};
|
|
3
22
|
/** Returns an error description if `value` is not valid for `def`, otherwise `null`. */
|
|
4
23
|
export declare function validateSettingValue(def: SettingDefinition, value: unknown): string | null;
|
|
5
24
|
/**
|
|
@@ -7,7 +26,9 @@ export declare function validateSettingValue(def: SettingDefinition, value: unkn
|
|
|
7
26
|
* value or its default. Invalid values and unknown keys are reported in `errors`
|
|
8
27
|
* (and replaced by defaults / dropped).
|
|
9
28
|
*/
|
|
10
|
-
export declare function resolveSettings(definitions: SettingDefinition[], input?: Record<string, unknown
|
|
29
|
+
export declare function resolveSettings(definitions: SettingDefinition[], input?: Record<string, unknown>,
|
|
30
|
+
/** Settings of types from a later version: always their default; input values are ignored. */
|
|
31
|
+
unknownSettings?: UnknownSettingDefinition[]): {
|
|
11
32
|
values: Record<string, SettingValue>;
|
|
12
33
|
errors: string[];
|
|
13
34
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { UnknownSettingDefinition } from './settings.js';
|
|
2
3
|
/** A string, or a map of BCP 47 language → string (e.g. `{ "cs": "Čas", "en": "Time" }`). */
|
|
3
4
|
export declare const LocalizedTextSchema: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
4
5
|
export type LocalizedText = z.infer<typeof LocalizedTextSchema>;
|
|
6
|
+
/** A setting of a type this protocol version defines (SPEC 2). */
|
|
5
7
|
export declare const SettingDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
6
8
|
type: z.ZodLiteral<"number">;
|
|
7
9
|
default: z.ZodNumber;
|
|
@@ -52,3 +54,64 @@ export declare const SettingDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
52
54
|
}>>>;
|
|
53
55
|
}, z.core.$loose>], "type">;
|
|
54
56
|
export type SettingDefinition = z.infer<typeof SettingDefinitionSchema>;
|
|
57
|
+
/**
|
|
58
|
+
* A setting of a type from a later protocol version (SPEC 2): hosts do not show it and
|
|
59
|
+
* pass its `default` (any JSON value) to the plugin.
|
|
60
|
+
*/
|
|
61
|
+
export declare const UnknownSettingSchema: z.ZodObject<{
|
|
62
|
+
type: z.ZodString;
|
|
63
|
+
default: z.ZodUnknown & z.ZodType<{} | null, unknown, z.core.$ZodTypeInternals<{} | null, unknown>>;
|
|
64
|
+
id: z.ZodString;
|
|
65
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
66
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
67
|
+
modes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
68
|
+
solo: "solo";
|
|
69
|
+
multiplayer: "multiplayer";
|
|
70
|
+
}>>>;
|
|
71
|
+
}, z.core.$loose>;
|
|
72
|
+
/**
|
|
73
|
+
* One entry of `settings` in a manifest: a known setting (validated by its type, with clear
|
|
74
|
+
* messages) or a setting of a later version (only `id`, `label`, `default`).
|
|
75
|
+
*/
|
|
76
|
+
export declare const SettingEntrySchema: z.ZodPipe<z.ZodUnknown, z.ZodTransform<UnknownSettingDefinition | {
|
|
77
|
+
[x: string]: unknown;
|
|
78
|
+
type: "number";
|
|
79
|
+
default: number;
|
|
80
|
+
id: string;
|
|
81
|
+
label: string | Record<string, string>;
|
|
82
|
+
min?: number | undefined;
|
|
83
|
+
max?: number | undefined;
|
|
84
|
+
step?: number | undefined;
|
|
85
|
+
description?: string | Record<string, string> | undefined;
|
|
86
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
87
|
+
} | {
|
|
88
|
+
[x: string]: unknown;
|
|
89
|
+
type: "select";
|
|
90
|
+
options: {
|
|
91
|
+
[x: string]: unknown;
|
|
92
|
+
value: string | number | boolean;
|
|
93
|
+
label: string | Record<string, string>;
|
|
94
|
+
}[];
|
|
95
|
+
default: string | number | boolean;
|
|
96
|
+
id: string;
|
|
97
|
+
label: string | Record<string, string>;
|
|
98
|
+
description?: string | Record<string, string> | undefined;
|
|
99
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
100
|
+
} | {
|
|
101
|
+
[x: string]: unknown;
|
|
102
|
+
type: "text";
|
|
103
|
+
default: string;
|
|
104
|
+
id: string;
|
|
105
|
+
label: string | Record<string, string>;
|
|
106
|
+
maxLength?: number | undefined;
|
|
107
|
+
description?: string | Record<string, string> | undefined;
|
|
108
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
109
|
+
} | {
|
|
110
|
+
[x: string]: unknown;
|
|
111
|
+
type: "boolean";
|
|
112
|
+
default: boolean;
|
|
113
|
+
id: string;
|
|
114
|
+
label: string | Record<string, string>;
|
|
115
|
+
description?: string | Record<string, string> | undefined;
|
|
116
|
+
modes?: ("solo" | "multiplayer")[] | undefined;
|
|
117
|
+
}, unknown>>;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -82,6 +82,11 @@ export interface InitPayload {
|
|
|
82
82
|
data: PluginData;
|
|
83
83
|
/** Last saved snapshot, only for the authority (resume). */
|
|
84
84
|
snapshot: unknown | null;
|
|
85
|
+
/**
|
|
86
|
+
* Only for the authority: the secret seed of the game's random numbers (at least 128 bits,
|
|
87
|
+
* e.g. 32 hex digits). Other instances never get it (SPEC 4.2).
|
|
88
|
+
*/
|
|
89
|
+
seed?: string;
|
|
85
90
|
}
|
|
86
91
|
/**
|
|
87
92
|
* The session clock. `Date.now() + offsetMs` is the session time. Pauses: the
|
|
@@ -97,7 +102,7 @@ export interface SessionClock {
|
|
|
97
102
|
pausedAt: number | null;
|
|
98
103
|
}
|
|
99
104
|
export interface AnswerRecord {
|
|
100
|
-
/** Default: self
|
|
105
|
+
/** Default: self (the authority as a player); required when the board records. Only the authority records (SPEC 5). */
|
|
101
106
|
playerId?: string;
|
|
102
107
|
itemId: string;
|
|
103
108
|
isCorrect: boolean;
|
|
@@ -105,6 +110,11 @@ export interface AnswerRecord {
|
|
|
105
110
|
timeSpentMs?: number;
|
|
106
111
|
hintsUsed?: number;
|
|
107
112
|
isSkipped?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Partial credit between 0 and 1 (e.g. 2 of 3 correct pairs = 0.67), for items that allow
|
|
115
|
+
* it (OQSE `pedagogy.partialCredit`). `isCorrect` stays the outcome used for repetition.
|
|
116
|
+
*/
|
|
117
|
+
score?: number;
|
|
108
118
|
/**
|
|
109
119
|
* What the player answered, in the `checkAnswer` format of the item type
|
|
110
120
|
* (choice IDs, text, numbers…). Lets teachers see which wrong answers were chosen.
|
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.7",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepublishOnly": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@memizy/oqse": "^1.0.0-rc.
|
|
37
|
+
"@memizy/oqse": "^1.0.0-rc.7",
|
|
38
38
|
"zod": "^4.3.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|