@memizy/protocol 0.0.0-stage → 1.0.0-rc.5

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.
Files changed (45) hide show
  1. package/README.md +20 -2
  2. package/SPEC.md +491 -0
  3. package/dist/errors.cjs +1 -0
  4. package/dist/errors.js +34 -0
  5. package/dist/index.cjs +1 -0
  6. package/dist/index.js +14 -0
  7. package/dist/items.cjs +1 -0
  8. package/dist/items.js +116 -0
  9. package/dist/limits.cjs +1 -0
  10. package/dist/limits.js +65 -0
  11. package/dist/manifestSchema.cjs +2 -0
  12. package/dist/manifestSchema.js +107 -0
  13. package/dist/pluginRuntime.cjs +1 -0
  14. package/dist/pluginRuntime.js +48 -0
  15. package/dist/polyfills.cjs +1 -0
  16. package/dist/polyfills.js +23 -0
  17. package/dist/relay.cjs +1 -0
  18. package/dist/relay.js +21 -0
  19. package/dist/schemas.cjs +1 -0
  20. package/dist/schemas.js +77 -0
  21. package/dist/security.cjs +1 -0
  22. package/dist/security.js +40 -0
  23. package/dist/settings.cjs +1 -0
  24. package/dist/settings.js +35 -0
  25. package/dist/settingsSchema.cjs +1 -0
  26. package/dist/settingsSchema.js +58 -0
  27. package/dist/src/errors.d.ts +21 -0
  28. package/dist/src/index.d.ts +17 -0
  29. package/dist/src/items.d.ts +27 -0
  30. package/dist/src/limits.d.ts +62 -0
  31. package/dist/src/manifestSchema.d.ts +159 -0
  32. package/dist/src/pluginRuntime.d.ts +47 -0
  33. package/dist/src/polyfills.d.ts +6 -0
  34. package/dist/src/relay.d.ts +168 -0
  35. package/dist/src/schemas.d.ts +120 -0
  36. package/dist/src/security.d.ts +21 -0
  37. package/dist/src/settings.d.ts +22 -0
  38. package/dist/src/settingsSchema.d.ts +54 -0
  39. package/dist/src/types.d.ts +190 -0
  40. package/dist/src/version.d.ts +30 -0
  41. package/dist/types.cjs +1 -0
  42. package/dist/types.js +28 -0
  43. package/dist/version.cjs +1 -0
  44. package/dist/version.js +31 -0
  45. package/package.json +55 -4
package/dist/items.js ADDED
@@ -0,0 +1,116 @@
1
+ import { isTimelineOrderCorrect as e } from "@memizy/oqse";
2
+ //#region src/items.ts
3
+ function t(e) {
4
+ let t = 2166136261;
5
+ for (let n = 0; n < e.length; n++) t = Math.imul(t ^ e.charCodeAt(n), 16777619);
6
+ let n = t >>> 0;
7
+ return () => {
8
+ n = n + 1831565813 >>> 0;
9
+ let e = n;
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
+ };
12
+ }
13
+ function n(e, t, n) {
14
+ for (let r = 0; r < 6; r++) {
15
+ let r = [...e];
16
+ for (let e = r.length - 1; e > 0; e--) {
17
+ let n = Math.floor(t() * (e + 1));
18
+ [r[e], r[n]] = [r[n], r[e]];
19
+ }
20
+ if (!n || r.length < 2 || !n(r)) return r;
21
+ }
22
+ return [...e.slice(1), e[0]];
23
+ }
24
+ var r = (e) => (t) => t.every((t, n) => t.id === e[n]);
25
+ function i(t, i) {
26
+ let a = structuredClone(t);
27
+ switch (a.type) {
28
+ case "mcq-single":
29
+ case "mcq-multi": return a.shuffle !== !1 && (a.options = n(a.options, i)), a;
30
+ case "fill-in-select":
31
+ for (let e of Object.values(a.blanks ?? {})) e.shuffle !== !1 && (e.options = n(e.options, i));
32
+ return a;
33
+ case "sort-items": return a.items = n(a.items, i, r(a.correctOrder)), a;
34
+ case "match-pairs": return a.prompts = n(a.prompts, i), a.matches = n(a.matches, i), a;
35
+ case "match-complex": return a.leftItems = n(a.leftItems, i), a.rightItems = n(a.rightItems, i), a;
36
+ case "timeline": return a.shuffle !== !1 && (a.events = n(a.events, i, (t) => e(t))), a;
37
+ case "categorize": return a.items = n(a.items, i), a;
38
+ case "diagram-label": return a.requireTyping || (a.labels = n(a.labels, i)), a;
39
+ default: return a;
40
+ }
41
+ }
42
+ var a = ["explanation", "incorrectFeedback"], o = {
43
+ note: ["hiddenContent"],
44
+ flashcard: ["back"],
45
+ "true-false": ["correctAnswer"],
46
+ "mcq-single": ["correctId"],
47
+ "mcq-multi": ["correctIds"],
48
+ "short-answer": ["correctAnswers"],
49
+ "sort-items": ["correctOrder"],
50
+ "match-pairs": ["pairs"],
51
+ "match-complex": ["connections"],
52
+ slider: ["correctAnswer"],
53
+ "numeric-input": ["correctAnswer"],
54
+ "math-input": ["correctAnswer", "alternativeAnswers"],
55
+ matrix: ["correctCells"],
56
+ "open-ended": ["sampleAnswer", "rubric"],
57
+ "chess-puzzle": ["correctAnswers"]
58
+ }, s = [
59
+ "options",
60
+ "items",
61
+ "prompts",
62
+ "matches",
63
+ "leftItems",
64
+ "rightItems",
65
+ "categories",
66
+ "rows",
67
+ "columns",
68
+ "labels"
69
+ ];
70
+ function c(e) {
71
+ let t = e.type, n = structuredClone(e);
72
+ for (let e of a) delete n[e];
73
+ if (Array.isArray(n.sources)) for (let e of n.sources) e && typeof e == "object" && delete e.quote;
74
+ if (t.startsWith("x-")) return delete n.solution, n.answerHidden = !0, n;
75
+ for (let e of o[t] ?? []) delete n[e];
76
+ for (let e of s) if (Array.isArray(n[e])) for (let t of n[e]) t && typeof t == "object" && delete t.explanation;
77
+ switch (t) {
78
+ case "fill-in-blanks":
79
+ n.blanks = Object.fromEntries(Object.keys(n.blanks ?? {}).map((e) => [e, []]));
80
+ break;
81
+ case "fill-in-select":
82
+ for (let e of Object.values(n.blanks ?? {})) {
83
+ delete e.correctId;
84
+ for (let t of e.options ?? []) delete t.explanation;
85
+ }
86
+ break;
87
+ case "categorize":
88
+ for (let e of n.items ?? []) delete e.correctCategoryId;
89
+ break;
90
+ case "timeline":
91
+ for (let e of n.events ?? []) delete e.date, delete e.precision;
92
+ break;
93
+ case "diagram-label":
94
+ for (let e of n.zones ?? []) delete e.correctLabelId;
95
+ n.requireTyping && (n.labels = []);
96
+ break;
97
+ case "pin-on-image":
98
+ case "pin-on-model": n.hotspots = [];
99
+ }
100
+ return n.answerHidden = !0, n;
101
+ }
102
+ function l(e, n) {
103
+ let r = n === void 0 ? null : t(n), a = e.items.map((e) => r ? i(e, r) : structuredClone(e));
104
+ return {
105
+ set: {
106
+ meta: e.meta,
107
+ items: a
108
+ },
109
+ publicSet: {
110
+ meta: e.meta,
111
+ items: a.map(c)
112
+ }
113
+ };
114
+ }
115
+ //#endregion
116
+ export { i as displayItem, l as prepareDisplaySet, c as publicItem, t as seededRandom };
@@ -0,0 +1 @@
1
+ const e=require("./errors.cjs");var t={messageBytes:65536,messagesPerSecond:30,messageBurst:60,snapshotBytes:1048576,snapshotsPerSecond:2,dataBytes:262144,dataWritesPerSecond:1,helloTimeoutMs:1e4,answerBytes:4096,generatedItemBytes:32768,serviceBytes:65536,servicesPerMinute:30},n=64;function r(e,t=`$`,i=0,a=new Set){if(e===null||typeof e==`string`||typeof e==`boolean`)return null;if(typeof e==`number`)return Number.isFinite(e)?null:`${t} is not a finite number`;if(typeof e!=`object`)return`${t} is a ${typeof e}`;if(i>=n)return`${t} is nested deeper than ${n} levels`;if(a.has(e))return`${t} contains a circular reference`;if(Array.isArray(e)){a.add(e);for(let n=0;n<e.length;n++){let o=r(e[n],`${t}[${n}]`,i+1,a);if(o)return o}return a.delete(e),null}let o=Object.getPrototypeOf(e);if(o!==Object.prototype&&o!==null)return`${t} is not a plain object`;a.add(e);for(let[n,o]of Object.entries(e)){let e=r(o,`${t}.${n}`,i+1,a);if(e)return e}return a.delete(e),null}function i(e){return new TextEncoder().encode(JSON.stringify(e)).length}function a(t,n,a,o){let s=r(t);if(s)throw new e.ProtocolError(`INVALID_ARGUMENT`,`${o} must be plain JSON: ${s}.`);let c=i(t);if(c>n)throw new e.ProtocolError(a,`${o} is ${c} bytes; the limit is ${n} bytes.`);return c}var o=class{rate;burst;now;tokens;last;constructor(e,t,n=()=>Date.now()){this.rate=e,this.burst=t,this.now=n,this.tokens=t,this.last=n()}tryTake(e=1){let t=this.now();return this.tokens=Math.min(this.burst,this.tokens+(t-this.last)/1e3*this.rate),this.last=t,this.tokens<e?!1:(this.tokens-=e,!0)}};exports.LIMITS=t,exports.RateLimiter=o,exports.assertJsonWithin=a,exports.findNonJson=r,exports.jsonByteSize=i;
package/dist/limits.js ADDED
@@ -0,0 +1,65 @@
1
+ import { ProtocolError as e } from "./errors.js";
2
+ //#region src/limits.ts
3
+ var t = {
4
+ messageBytes: 65536,
5
+ messagesPerSecond: 30,
6
+ messageBurst: 60,
7
+ snapshotBytes: 1048576,
8
+ snapshotsPerSecond: 2,
9
+ dataBytes: 262144,
10
+ dataWritesPerSecond: 1,
11
+ helloTimeoutMs: 1e4,
12
+ answerBytes: 4096,
13
+ generatedItemBytes: 32768,
14
+ serviceBytes: 65536,
15
+ servicesPerMinute: 30
16
+ }, n = 64;
17
+ function r(e, t = "$", i = 0, a = /* @__PURE__ */ new Set()) {
18
+ if (e === null || typeof e == "string" || typeof e == "boolean") return null;
19
+ if (typeof e == "number") return Number.isFinite(e) ? null : `${t} is not a finite number`;
20
+ if (typeof e != "object") return `${t} is a ${typeof e}`;
21
+ if (i >= n) return `${t} is nested deeper than ${n} levels`;
22
+ if (a.has(e)) return `${t} contains a circular reference`;
23
+ if (Array.isArray(e)) {
24
+ a.add(e);
25
+ for (let n = 0; n < e.length; n++) {
26
+ let o = r(e[n], `${t}[${n}]`, i + 1, a);
27
+ if (o) return o;
28
+ }
29
+ return a.delete(e), null;
30
+ }
31
+ let o = Object.getPrototypeOf(e);
32
+ if (o !== Object.prototype && o !== null) return `${t} is not a plain object`;
33
+ a.add(e);
34
+ for (let [n, o] of Object.entries(e)) {
35
+ let e = r(o, `${t}.${n}`, i + 1, a);
36
+ if (e) return e;
37
+ }
38
+ return a.delete(e), null;
39
+ }
40
+ function i(e) {
41
+ return new TextEncoder().encode(JSON.stringify(e)).length;
42
+ }
43
+ function a(t, n, a, o) {
44
+ let s = r(t);
45
+ if (s) throw new e("INVALID_ARGUMENT", `${o} must be plain JSON: ${s}.`);
46
+ let c = i(t);
47
+ if (c > n) throw new e(a, `${o} is ${c} bytes; the limit is ${n} bytes.`);
48
+ return c;
49
+ }
50
+ var o = class {
51
+ rate;
52
+ burst;
53
+ now;
54
+ tokens;
55
+ last;
56
+ constructor(e, t, n = () => Date.now()) {
57
+ this.rate = e, this.burst = t, this.now = n, this.tokens = t, this.last = n();
58
+ }
59
+ tryTake(e = 1) {
60
+ let t = this.now();
61
+ return this.tokens = Math.min(this.burst, this.tokens + (t - this.last) / 1e3 * this.rate), this.last = t, this.tokens < e ? !1 : (this.tokens -= e, !0);
62
+ }
63
+ };
64
+ //#endregion
65
+ export { t as LIMITS, o as RateLimiter, a as assertJsonWithin, r as findNonJson, i as jsonByteSize };
@@ -0,0 +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.SettingDefinitionSchema).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
+ `)}`),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;
@@ -0,0 +1,107 @@
1
+ import { SettingDefinitionSchema as e } from "./settingsSchema.js";
2
+ import { extractManifestFromHtml as t, toRuntime as n } from "./pluginRuntime.js";
3
+ import { z as r } from "zod";
4
+ import { OQSEManifestSchema as i, formatOQSEErrors as a, withManifestDefaults as o } from "@memizy/oqse";
5
+ //#region src/manifestSchema.ts
6
+ var s = r.string().regex(/^\d+\.\d+$/, "Protocol version must be in MAJOR.MINOR format (e.g. \"1.0\")"), c = r.enum(["presenter", "player"]), l = r.looseObject({
7
+ players: r.looseObject({
8
+ min: r.number().int().min(1, "players.min must be at least 1"),
9
+ max: r.number().int().max(200, "players.max must be at most 200"),
10
+ recommended: r.number().int().positive().optional()
11
+ }).refine((e) => e.min <= e.max, {
12
+ message: "players.min must not be greater than players.max",
13
+ path: ["min"]
14
+ }).refine((e) => e.recommended === void 0 || e.recommended >= e.min && e.recommended <= e.max, {
15
+ message: "players.recommended must be between min and max",
16
+ path: ["recommended"]
17
+ }),
18
+ hostAs: r.array(c).min(1, "hostAs must contain \"presenter\" and/or \"player\"").refine((e) => new Set(e).size === e.length, "hostAs must not contain duplicates"),
19
+ lateJoin: r.boolean().optional()
20
+ }), u = r.string().regex(/^(https|wss):\/\/[a-z0-9.-]+(:\d{1,5})?$/i, "A network origin is \"https://host\" or \"wss://host\" (no path)"), d = r.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\")"), f = r.looseObject({
21
+ network: r.array(u).max(20).optional(),
22
+ devices: r.array(r.enum([
23
+ "camera",
24
+ "microphone",
25
+ "geolocation",
26
+ "serial",
27
+ "bluetooth"
28
+ ])).max(5).optional()
29
+ }), p = r.looseObject({
30
+ protocol: s,
31
+ set: r.enum([
32
+ "required",
33
+ "optional",
34
+ "none"
35
+ ]).optional(),
36
+ modes: r.looseObject({
37
+ solo: r.looseObject({}).optional(),
38
+ multiplayer: l.optional()
39
+ }),
40
+ settings: r.array(e).optional(),
41
+ settingsScreen: r.looseObject({ size: r.enum(["compact", "large"]) }).optional(),
42
+ display: r.looseObject({ orientation: r.enum([
43
+ "any",
44
+ "portrait",
45
+ "landscape"
46
+ ]).optional() }).optional(),
47
+ permissions: f.optional(),
48
+ services: r.array(d).max(20).optional()
49
+ }).refine((e) => e.modes.solo !== void 0 || e.modes.multiplayer !== void 0, {
50
+ message: "modes must declare \"solo\" and/or \"multiplayer\"",
51
+ path: ["modes"]
52
+ }).refine((e) => e.settingsScreen === void 0 || e.modes.multiplayer !== void 0, {
53
+ message: "settingsScreen is only used in the multiplayer lobby; declare modes.multiplayer or remove it",
54
+ path: ["settingsScreen"]
55
+ }).refine((e) => {
56
+ let t = (e.settings ?? []).map((e) => e.id);
57
+ return new Set(t).size === t.length;
58
+ }, {
59
+ message: "Setting ids must be unique",
60
+ path: ["settings"]
61
+ });
62
+ function m(e) {
63
+ let t = [], r = i.safeParse(e);
64
+ r.success || t.push(...a(r.error));
65
+ let s = e?.appSpecific?.memizy;
66
+ if (s === void 0) t.push("appSpecific.memizy: Missing the Memizy runtime block (protocol, modes, …)");
67
+ else {
68
+ let e = p.safeParse(s);
69
+ e.success || t.push(...a(e.error).map((e) => `appSpecific.memizy.${e}`));
70
+ }
71
+ if (t.length > 0) return {
72
+ success: !1,
73
+ errors: t
74
+ };
75
+ let c = o(e), l = n(c.appSpecific.memizy), u = c.capabilities.types;
76
+ return l.set !== "none" && (!Array.isArray(u) || u.length === 0) ? {
77
+ success: !1,
78
+ errors: ["capabilities.types: Declare the item types the game shows, or \"set\": \"none\" in appSpecific.memizy for a game without questions."]
79
+ } : {
80
+ success: !0,
81
+ manifest: c,
82
+ runtime: l
83
+ };
84
+ }
85
+ var h = class extends Error {
86
+ errors;
87
+ constructor(e) {
88
+ super(`Invalid plugin manifest:\n${e.join("\n")}`), this.name = "PluginManifestError", this.errors = e;
89
+ }
90
+ };
91
+ function g(e) {
92
+ let t = m(e);
93
+ if (!t.success) throw new h(t.errors);
94
+ return {
95
+ manifest: t.manifest,
96
+ runtime: t.runtime
97
+ };
98
+ }
99
+ function _(e) {
100
+ let n = t(e);
101
+ return n.success ? m(n.data) : {
102
+ success: !1,
103
+ errors: [n.error]
104
+ };
105
+ }
106
+ //#endregion
107
+ export { c as HostAsSchema, p as MemizyRuntimeSchema, l as MultiplayerModeSchema, h as PluginManifestError, f as PluginPermissionsSchema, d as ServiceNameSchema, g as parsePluginManifest, _ as readPluginManifestFromHtml, m as safeParsePluginManifest };
@@ -0,0 +1 @@
1
+ var e=/<script\b[^>]*\btype\s*=\s*["']application\/oqse-manifest\+json["'][^>]*>([\s\S]*?)<\/script\s*>/i;function t(t){let n=e.exec(t);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 n(e){let t=e.modes.multiplayer,n=t?{players:{min:t.players.min,max:t.players.max,...t.players.recommended!==void 0&&{recommended:t.players.recommended}},hostAs:[...t.hostAs],lateJoin:t.lateJoin??!0}:null,r=[];return e.modes.solo&&r.push(`solo`),n?.hostAs.includes(`presenter`)&&r.push(`board`),n&&r.push(`controller`),n&&e.settingsScreen&&r.push(`settings`),{protocol:e.protocol,set:e.set??`required`,solo:e.modes.solo!==void 0,multiplayer:n,settings:e.settings??[],settingsScreen:e.settingsScreen?{size:e.settingsScreen.size}:null,orientation:e.display?.orientation??`any`,views:r,permissions:{network:[...new Set(e.permissions?.network??[])],devices:[...new Set(e.permissions?.devices??[])]},services:[...new Set(e.services??[])]}}exports.extractManifestFromHtml=t,exports.toRuntime=n;
@@ -0,0 +1,48 @@
1
+ //#region src/pluginRuntime.ts
2
+ var e = /<script\b[^>]*\btype\s*=\s*["']application\/oqse-manifest\+json["'][^>]*>([\s\S]*?)<\/script\s*>/i;
3
+ function t(t) {
4
+ let n = e.exec(t);
5
+ if (!n) return {
6
+ success: !1,
7
+ error: "No <script type=\"application/oqse-manifest+json\"> data island found."
8
+ };
9
+ try {
10
+ return {
11
+ success: !0,
12
+ data: JSON.parse(n[1])
13
+ };
14
+ } catch (e) {
15
+ return {
16
+ success: !1,
17
+ error: `The manifest data island is not valid JSON: ${e.message}`
18
+ };
19
+ }
20
+ }
21
+ function n(e) {
22
+ let t = e.modes.multiplayer, n = t ? {
23
+ players: {
24
+ min: t.players.min,
25
+ max: t.players.max,
26
+ ...t.players.recommended !== void 0 && { recommended: t.players.recommended }
27
+ },
28
+ hostAs: [...t.hostAs],
29
+ lateJoin: t.lateJoin ?? !0
30
+ } : null, r = [];
31
+ return e.modes.solo && r.push("solo"), n?.hostAs.includes("presenter") && r.push("board"), n && r.push("controller"), n && e.settingsScreen && r.push("settings"), {
32
+ protocol: e.protocol,
33
+ set: e.set ?? "required",
34
+ solo: e.modes.solo !== void 0,
35
+ multiplayer: n,
36
+ settings: e.settings ?? [],
37
+ settingsScreen: e.settingsScreen ? { size: e.settingsScreen.size } : null,
38
+ orientation: e.display?.orientation ?? "any",
39
+ views: r,
40
+ permissions: {
41
+ network: [...new Set(e.permissions?.network ?? [])],
42
+ devices: [...new Set(e.permissions?.devices ?? [])]
43
+ },
44
+ services: [...new Set(e.services ?? [])]
45
+ };
46
+ }
47
+ //#endregion
48
+ export { t as extractManifestFromHtml, n as toRuntime };
@@ -0,0 +1 @@
1
+ function e(){typeof Promise.withResolvers!=`function`&&Object.defineProperty(Promise,"withResolvers",{configurable:!0,writable:!0,value:function(){let e,t;return{promise:new this((n,r)=>{e=n,t=r}),resolve:e,reject:t}}}),typeof Object.hasOwn!=`function`&&Object.defineProperty(Object,"hasOwn",{configurable:!0,writable:!0,value:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})}exports.ensureConnectionPolyfills=e;
@@ -0,0 +1,23 @@
1
+ //#region src/polyfills.ts
2
+ function e() {
3
+ typeof Promise.withResolvers != "function" && Object.defineProperty(Promise, "withResolvers", {
4
+ configurable: !0,
5
+ writable: !0,
6
+ value: function() {
7
+ let e, t;
8
+ return {
9
+ promise: new this((n, r) => {
10
+ e = n, t = r;
11
+ }),
12
+ resolve: e,
13
+ reject: t
14
+ };
15
+ }
16
+ }), typeof Object.hasOwn != "function" && Object.defineProperty(Object, "hasOwn", {
17
+ configurable: !0,
18
+ writable: !0,
19
+ value: (e, t) => Object.prototype.hasOwnProperty.call(e, t)
20
+ });
21
+ }
22
+ //#endregion
23
+ export { e as ensureConnectionPolyfills };
package/dist/relay.cjs ADDED
@@ -0,0 +1 @@
1
+ var e=1,t={frameBytes:2097152,bundleBytes:8388608,playersPerRoom:100,playerFramesPerSecond:60,playerFrameBurst:120,hostFramesPerSecond:3e3,hostFrameBurst:6e3,nameLength:32,hostAwayMs:6e5,roomTtlMs:216e5};function n(e){return typeof e==`string`?e.replace(/\s+/g,` `).trim().slice(0,t.nameLength):``}function r(e){return typeof e==`string`&&/^\d{6}$/.test(e)}exports.RELAY_LIMITS=t,exports.RELAY_PROTOCOL=e,exports.isPin=r,exports.normalizePlayerName=n;
package/dist/relay.js ADDED
@@ -0,0 +1,21 @@
1
+ //#region src/relay.ts
2
+ var e = 1, t = {
3
+ frameBytes: 2097152,
4
+ bundleBytes: 8388608,
5
+ playersPerRoom: 100,
6
+ playerFramesPerSecond: 60,
7
+ playerFrameBurst: 120,
8
+ hostFramesPerSecond: 3e3,
9
+ hostFrameBurst: 6e3,
10
+ nameLength: 32,
11
+ hostAwayMs: 6e5,
12
+ roomTtlMs: 216e5
13
+ };
14
+ function n(e) {
15
+ return typeof e == "string" ? e.replace(/\s+/g, " ").trim().slice(0, t.nameLength) : "";
16
+ }
17
+ function r(e) {
18
+ return typeof e == "string" && /^\d{6}$/.test(e);
19
+ }
20
+ //#endregion
21
+ export { t as RELAY_LIMITS, e as RELAY_PROTOCOL, r as isPin, n as normalizePlayerName };
@@ -0,0 +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.record(r(128),n.ProgressRecordSchema),l=t.z.enum([`plugin`,`set`]),u=t.z.looseObject({values:t.z.record(t.z.string(),t.z.unknown()),valid:t.z.boolean(),message:t.z.string().max(500).optional()}),d=t.z.looseObject({scores:t.z.record(i,t.z.number().finite()).optional(),summary:t.z.string().max(1e3).optional()}),f=t.z.looseObject({height:t.z.union([t.z.number().positive().max(1e5),t.z.literal(`auto`)])}),p=t.z.looseObject({code:r(100),message:t.z.string().max(2e3),context:t.z.record(t.z.string(),t.z.unknown()).optional()}),m={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]),saveProgress:t.z.tuple([c]),saveData:t.z.tuple([l,t.z.unknown()]),updateSettings:t.z.tuple([u]),getAsset:t.z.tuple([r(200),r(128).optional()]),end:t.z.tuple([d]),resize:t.z.tuple([f]),reportError:t.z.tuple([p]),exit:t.z.tuple([]),service:t.z.tuple([r(100),t.z.unknown()])};function h(t,r){let i=m[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=l,exports.ErrorReportSchema=p,exports.HOST_API_ARGS=m,exports.HandshakeSchema=a,exports.OutgoingMessageSchema=o,exports.ProgressRecordsSchema=c,exports.ResizeRequestSchema=f,exports.SessionResultSchema=d,exports.SettingsUpdateSchema=u,exports.parseHostCall=h;
@@ -0,0 +1,77 @@
1
+ import { ProtocolError as e } from "./errors.js";
2
+ import { z as t } from "zod";
3
+ import { ProgressRecordSchema as n, formatOQSEErrors as r } from "@memizy/oqse";
4
+ //#region src/schemas.ts
5
+ var i = (e) => t.string().min(1).max(e), a = t.string().min(1).max(128), o = t.looseObject({
6
+ protocol: t.string().regex(/^\d+\.\d+$/, "protocol must be in MAJOR.MINOR format"),
7
+ sdk: t.looseObject({
8
+ name: i(200),
9
+ version: i(100)
10
+ }),
11
+ plugin: t.looseObject({
12
+ id: i(500),
13
+ version: i(100)
14
+ }),
15
+ features: t.array(i(100)).max(100)
16
+ }), s = t.looseObject({
17
+ to: t.union([
18
+ t.literal("authority"),
19
+ t.literal("all"),
20
+ t.array(a).min(1).max(500)
21
+ ]),
22
+ data: t.unknown()
23
+ }), c = t.looseObject({
24
+ playerId: a.optional(),
25
+ itemId: i(128),
26
+ isCorrect: t.boolean(),
27
+ confidence: t.union([
28
+ t.literal(1),
29
+ t.literal(2),
30
+ t.literal(3),
31
+ t.literal(4)
32
+ ]).optional(),
33
+ timeSpentMs: t.number().int().nonnegative().optional(),
34
+ hintsUsed: t.number().int().nonnegative().optional(),
35
+ isSkipped: t.boolean().optional(),
36
+ answer: t.unknown().optional(),
37
+ item: t.looseObject({
38
+ id: i(128),
39
+ type: i(64)
40
+ }).optional()
41
+ }).refine((e) => !e.item || e.item.id === e.itemId, {
42
+ message: "item.id must equal itemId",
43
+ path: ["item", "id"]
44
+ }), l = t.record(i(128), n), u = t.enum(["plugin", "set"]), d = t.looseObject({
45
+ values: t.record(t.string(), t.unknown()),
46
+ valid: t.boolean(),
47
+ message: t.string().max(500).optional()
48
+ }), f = t.looseObject({
49
+ scores: t.record(a, t.number().finite()).optional(),
50
+ summary: t.string().max(1e3).optional()
51
+ }), p = t.looseObject({ height: t.union([t.number().positive().max(1e5), t.literal("auto")]) }), m = t.looseObject({
52
+ code: i(100),
53
+ message: t.string().max(2e3),
54
+ context: t.record(t.string(), t.unknown()).optional()
55
+ }), h = {
56
+ hello: t.tuple([o]),
57
+ ready: t.tuple([]),
58
+ send: t.tuple([s]),
59
+ saveSnapshot: t.tuple([t.unknown()]),
60
+ recordAnswer: t.tuple([c]),
61
+ saveProgress: t.tuple([l]),
62
+ saveData: t.tuple([u, t.unknown()]),
63
+ updateSettings: t.tuple([d]),
64
+ getAsset: t.tuple([i(200), i(128).optional()]),
65
+ end: t.tuple([f]),
66
+ resize: t.tuple([p]),
67
+ reportError: t.tuple([m]),
68
+ exit: t.tuple([]),
69
+ service: t.tuple([i(100), t.unknown()])
70
+ };
71
+ function g(t, n) {
72
+ let i = h[t].safeParse(n);
73
+ if (!i.success) throw new e("INVALID_ARGUMENT", `${t}: ${r(i.error).join("; ")}`);
74
+ return i.data;
75
+ }
76
+ //#endregion
77
+ export { a as AddressSchema, c as AnswerRecordSchema, u as DataScopeSchema, m as ErrorReportSchema, h as HOST_API_ARGS, o as HandshakeSchema, s as OutgoingMessageSchema, l as ProgressRecordsSchema, p as ResizeRequestSchema, f as SessionResultSchema, d as SettingsUpdateSchema, g as parseHostCall };
@@ -0,0 +1 @@
1
+ var e=[`https://cdn.jsdelivr.net`,`https://unpkg.com`,`https://cdnjs.cloudflare.com`,`https://esm.sh`],t=[`https://fonts.googleapis.com`,`https://fonts.gstatic.com`],n=[`camera`,`microphone`,`geolocation`,`serial`,`bluetooth`];function r(n,r){let i=r&&/^https?:\/\//.test(r)?[r]:[],a=n?.permissions.network??[],o=(...e)=>[...new Set(e.flat())].join(` `);return[`default-src 'none'`,`script-src 'unsafe-inline' 'wasm-unsafe-eval' blob: ${o(e,i)}`,`style-src 'unsafe-inline' ${o(e,t,i)}`,`font-src data: ${o(e,t,i)}`,`img-src data: blob: https:`,`media-src data: blob: https:`,`connect-src data: blob: ${o(e,i,a)}`,`worker-src blob: ${o(e,i)}`,`frame-src 'none'`,`form-action 'none'`,`base-uri 'none'`].join(`; `)}function i(e){return(e?.permissions.devices??[]).map(e=>`${e} *`).join(`; `)}function a(e,t){let n=`<meta http-equiv="Content-Security-Policy" content="${t.replace(/"/g,`&quot;`)}">`,r=/<head\b[^>]*>/i.exec(e);if(r)return e.slice(0,r.index+r[0].length)+n+e.slice(r.index+r[0].length);let i=/^\s*<!doctype[^>]*>/i.exec(e);return i?e.slice(0,i[0].length)+`<head>${n}</head>`+e.slice(i[0].length):`<head>${n}</head>${e}`}exports.LIBRARY_ORIGINS=e,exports.PLUGIN_DEVICES=n,exports.injectContentPolicy=a,exports.pluginAllowAttribute=i,exports.pluginContentPolicy=r;
@@ -0,0 +1,40 @@
1
+ //#region src/security.ts
2
+ var e = [
3
+ "https://cdn.jsdelivr.net",
4
+ "https://unpkg.com",
5
+ "https://cdnjs.cloudflare.com",
6
+ "https://esm.sh"
7
+ ], t = ["https://fonts.googleapis.com", "https://fonts.gstatic.com"], n = [
8
+ "camera",
9
+ "microphone",
10
+ "geolocation",
11
+ "serial",
12
+ "bluetooth"
13
+ ];
14
+ function r(n, r) {
15
+ let i = r && /^https?:\/\//.test(r) ? [r] : [], a = n?.permissions.network ?? [], o = (...e) => [...new Set(e.flat())].join(" ");
16
+ return [
17
+ "default-src 'none'",
18
+ `script-src 'unsafe-inline' 'wasm-unsafe-eval' blob: ${o(e, i)}`,
19
+ `style-src 'unsafe-inline' ${o(e, t, i)}`,
20
+ `font-src data: ${o(e, t, i)}`,
21
+ "img-src data: blob: https:",
22
+ "media-src data: blob: https:",
23
+ `connect-src data: blob: ${o(e, i, a)}`,
24
+ `worker-src blob: ${o(e, i)}`,
25
+ "frame-src 'none'",
26
+ "form-action 'none'",
27
+ "base-uri 'none'"
28
+ ].join("; ");
29
+ }
30
+ function i(e) {
31
+ return (e?.permissions.devices ?? []).map((e) => `${e} *`).join("; ");
32
+ }
33
+ function a(e, t) {
34
+ let n = `<meta http-equiv="Content-Security-Policy" content="${t.replace(/"/g, "&quot;")}">`, r = /<head\b[^>]*>/i.exec(e);
35
+ if (r) return e.slice(0, r.index + r[0].length) + n + e.slice(r.index + r[0].length);
36
+ let i = /^\s*<!doctype[^>]*>/i.exec(e);
37
+ return i ? e.slice(0, i[0].length) + `<head>${n}</head>` + e.slice(i[0].length) : `<head>${n}</head>${e}`;
38
+ }
39
+ //#endregion
40
+ export { e as LIBRARY_ORIGINS, n as PLUGIN_DEVICES, a as injectContentPolicy, i as pluginAllowAttribute, r as pluginContentPolicy };
@@ -0,0 +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 t(t,n={}){let r={},i=[];for(let a of t){if(!(a.id in n)){r[a.id]=a.default;continue}let t=e(a,n[a.id]);t?(i.push(`${a.id}: ${t}`),r[a.id]=a.default):r[a.id]=n[a.id]}for(let e of Object.keys(n))t.some(t=>t.id===e)||i.push(`${e}: unknown setting`);return{values:r,errors:i}}function n(e,t){return e.filter(e=>!e.modes||e.modes.includes(t))}function r(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.localize=r,exports.resolveSettings=t,exports.settingsForMode=n,exports.validateSettingValue=e;
@@ -0,0 +1,35 @@
1
+ //#region src/settings.ts
2
+ function e(e, t) {
3
+ switch (e.type) {
4
+ 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
+ case "boolean": return typeof t == "boolean" ? null : "must be true or false";
6
+ case "select": return e.options.some((e) => e.value === t) ? null : "must be one of the options";
7
+ 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
+ }
9
+ }
10
+ function t(t, n = {}) {
11
+ let r = {}, i = [];
12
+ for (let a of t) {
13
+ if (!(a.id in n)) {
14
+ r[a.id] = a.default;
15
+ continue;
16
+ }
17
+ let t = e(a, n[a.id]);
18
+ t ? (i.push(`${a.id}: ${t}`), r[a.id] = a.default) : r[a.id] = n[a.id];
19
+ }
20
+ for (let e of Object.keys(n)) t.some((t) => t.id === e) || i.push(`${e}: unknown setting`);
21
+ return {
22
+ values: r,
23
+ errors: i
24
+ };
25
+ }
26
+ function n(e, t) {
27
+ return e.filter((e) => !e.modes || e.modes.includes(t));
28
+ }
29
+ function r(e, t) {
30
+ if (typeof e == "string") return e;
31
+ let n = t.split("-")[0];
32
+ return e[t] ?? e[n] ?? e.en ?? Object.values(e)[0];
33
+ }
34
+ //#endregion
35
+ export { r as localize, t as resolveSettings, n as settingsForMode, e as validateSettingValue };
@@ -0,0 +1 @@
1
+ let e=require("zod");var t=e.z.union([e.z.string().min(1,`Text must not be empty`),e.z.record(e.z.string().min(2),e.z.string().min(1)).refine(e=>Object.keys(e).length>0,`Localized text needs at least one language`)]),n={id:e.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:t,description:t.optional(),modes:e.z.array(e.z.enum([`solo`,`multiplayer`])).min(1,`modes needs at least one mode`).optional()},r=e.z.looseObject({...n,type:e.z.literal(`number`),default:e.z.number().finite(),min:e.z.number().finite().optional(),max:e.z.number().finite().optional(),step:e.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`]}),i=e.z.looseObject({...n,type:e.z.literal(`boolean`),default:e.z.boolean()}),a=e.z.union([e.z.string(),e.z.number().finite(),e.z.boolean()]),o=e.z.looseObject({...n,type:e.z.literal(`select`),options:e.z.array(e.z.looseObject({value:a,label:t})).min(1,`A select setting needs at least one option`),default:a}).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`]}),s=e.z.looseObject({...n,type:e.z.literal(`text`),default:e.z.string(),maxLength:e.z.number().int().positive().optional()}).refine(e=>e.maxLength===void 0||e.default.length<=e.maxLength,{message:`default is longer than maxLength`,path:[`default`]}),c=e.z.discriminatedUnion(`type`,[r,i,o,s]);exports.LocalizedTextSchema=t,exports.SettingDefinitionSchema=c;
@@ -0,0 +1,58 @@
1
+ import { z as e } from "zod";
2
+ //#region src/settingsSchema.ts
3
+ var t = e.union([e.string().min(1, "Text must not be empty"), e.record(e.string().min(2), e.string().min(1)).refine((e) => Object.keys(e).length > 0, "Localized text needs at least one language")]), n = {
4
+ id: e.string().regex(/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/, "Setting id must start with a letter and contain only letters, digits, \"_\" and \"-\""),
5
+ label: t,
6
+ description: t.optional(),
7
+ modes: e.array(e.enum(["solo", "multiplayer"])).min(1, "modes needs at least one mode").optional()
8
+ }, r = e.looseObject({
9
+ ...n,
10
+ type: e.literal("number"),
11
+ default: e.number().finite(),
12
+ min: e.number().finite().optional(),
13
+ max: e.number().finite().optional(),
14
+ step: e.number().positive().optional()
15
+ }).refine((e) => e.min === void 0 || e.max === void 0 || e.min <= e.max, {
16
+ message: "min must not be greater than max",
17
+ path: ["min"]
18
+ }).refine((e) => (e.min === void 0 || e.default >= e.min) && (e.max === void 0 || e.default <= e.max), {
19
+ message: "default is outside min/max",
20
+ path: ["default"]
21
+ }), i = e.looseObject({
22
+ ...n,
23
+ type: e.literal("boolean"),
24
+ default: e.boolean()
25
+ }), a = e.union([
26
+ e.string(),
27
+ e.number().finite(),
28
+ e.boolean()
29
+ ]), o = e.looseObject({
30
+ ...n,
31
+ type: e.literal("select"),
32
+ options: e.array(e.looseObject({
33
+ value: a,
34
+ label: t
35
+ })).min(1, "A select setting needs at least one option"),
36
+ default: a
37
+ }).refine((e) => new Set(e.options.map((e) => e.value)).size === e.options.length, {
38
+ message: "Option values must be unique",
39
+ path: ["options"]
40
+ }).refine((e) => e.options.some((t) => t.value === e.default), {
41
+ message: "default must be one of the option values",
42
+ path: ["default"]
43
+ }), s = e.looseObject({
44
+ ...n,
45
+ type: e.literal("text"),
46
+ default: e.string(),
47
+ maxLength: e.number().int().positive().optional()
48
+ }).refine((e) => e.maxLength === void 0 || e.default.length <= e.maxLength, {
49
+ message: "default is longer than maxLength",
50
+ path: ["default"]
51
+ }), c = e.discriminatedUnion("type", [
52
+ r,
53
+ i,
54
+ o,
55
+ s
56
+ ]);
57
+ //#endregion
58
+ export { t as LocalizedTextSchema, c as SettingDefinitionSchema };