@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.
- package/README.md +20 -2
- package/SPEC.md +491 -0
- package/dist/errors.cjs +1 -0
- package/dist/errors.js +34 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +14 -0
- package/dist/items.cjs +1 -0
- package/dist/items.js +116 -0
- package/dist/limits.cjs +1 -0
- package/dist/limits.js +65 -0
- package/dist/manifestSchema.cjs +2 -0
- package/dist/manifestSchema.js +107 -0
- package/dist/pluginRuntime.cjs +1 -0
- package/dist/pluginRuntime.js +48 -0
- package/dist/polyfills.cjs +1 -0
- package/dist/polyfills.js +23 -0
- package/dist/relay.cjs +1 -0
- package/dist/relay.js +21 -0
- package/dist/schemas.cjs +1 -0
- package/dist/schemas.js +77 -0
- package/dist/security.cjs +1 -0
- package/dist/security.js +40 -0
- package/dist/settings.cjs +1 -0
- package/dist/settings.js +35 -0
- package/dist/settingsSchema.cjs +1 -0
- package/dist/settingsSchema.js +58 -0
- package/dist/src/errors.d.ts +21 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/items.d.ts +27 -0
- package/dist/src/limits.d.ts +62 -0
- package/dist/src/manifestSchema.d.ts +159 -0
- package/dist/src/pluginRuntime.d.ts +47 -0
- package/dist/src/polyfills.d.ts +6 -0
- package/dist/src/relay.d.ts +168 -0
- package/dist/src/schemas.d.ts +120 -0
- package/dist/src/security.d.ts +21 -0
- package/dist/src/settings.d.ts +22 -0
- package/dist/src/settingsSchema.d.ts +54 -0
- package/dist/src/types.d.ts +190 -0
- package/dist/src/version.d.ts +30 -0
- package/dist/types.cjs +1 -0
- package/dist/types.js +28 -0
- package/dist/version.cjs +1 -0
- package/dist/version.js +31 -0
- package/package.json +55 -4
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol errors (SPEC section 8.2).
|
|
3
|
+
*
|
|
4
|
+
* Errors cross the iframe boundary through Penpal, which keeps only the
|
|
5
|
+
* error `name` and `message`. The code is therefore also encoded in the
|
|
6
|
+
* message as `[CODE] text`, and {@link toProtocolError} restores it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PROTOCOL_ERROR_CODES: readonly ["UNSUPPORTED_PROTOCOL", "INVALID_ARGUMENT", "NOT_AUTHORITY", "NOT_ALLOWED_IN_VIEW", "AUTHORITY_UNAVAILABLE", "MESSAGE_TOO_LARGE", "SNAPSHOT_TOO_LARGE", "DATA_TOO_LARGE", "RATE_LIMITED", "ASSET_NOT_FOUND", "SESSION_ENDED", "INTERNAL_ERROR", "SERVICE_UNAVAILABLE"];
|
|
9
|
+
export type ProtocolErrorCode = (typeof PROTOCOL_ERROR_CODES)[number];
|
|
10
|
+
export declare class ProtocolError extends Error {
|
|
11
|
+
readonly code: ProtocolErrorCode;
|
|
12
|
+
constructor(code: ProtocolErrorCode, message: string);
|
|
13
|
+
/** The message without the `[CODE]` prefix. */
|
|
14
|
+
get detail(): string;
|
|
15
|
+
}
|
|
16
|
+
export declare function isProtocolErrorCode(value: unknown): value is ProtocolErrorCode;
|
|
17
|
+
/**
|
|
18
|
+
* Converts anything thrown by a remote call into a {@link ProtocolError}.
|
|
19
|
+
* Unknown codes and foreign errors become `INTERNAL_ERROR` (SPEC 8.2).
|
|
20
|
+
*/
|
|
21
|
+
export declare function toProtocolError(error: unknown): ProtocolError;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @memizy/protocol – Memizy Plugin Protocol v1.
|
|
3
|
+
* The normative specification is SPEC.md in this package.
|
|
4
|
+
*/
|
|
5
|
+
export * from './version';
|
|
6
|
+
export * from './errors';
|
|
7
|
+
export * from './limits';
|
|
8
|
+
export * from './settingsSchema';
|
|
9
|
+
export * from './settings';
|
|
10
|
+
export * from './manifestSchema';
|
|
11
|
+
export * from './pluginRuntime';
|
|
12
|
+
export * from './types';
|
|
13
|
+
export * from './schemas';
|
|
14
|
+
export * from './relay';
|
|
15
|
+
export * from './items';
|
|
16
|
+
export * from './security';
|
|
17
|
+
export * from './polyfills';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { OQSEAnyItem, OQSEMeta } from '@memizy/oqse';
|
|
2
|
+
/** Marks an item whose answer was removed (multiplayer, not the authority). */
|
|
3
|
+
export interface PublicItemMark {
|
|
4
|
+
answerHidden?: true;
|
|
5
|
+
}
|
|
6
|
+
export interface DisplaySet {
|
|
7
|
+
meta: OQSEMeta;
|
|
8
|
+
items: OQSEAnyItem[];
|
|
9
|
+
}
|
|
10
|
+
export declare function seededRandom(seed: string): () => number;
|
|
11
|
+
/** The item in display order. Answers refer to IDs, so only the lists move. */
|
|
12
|
+
export declare function displayItem(item: OQSEAnyItem, random: () => number): OQSEAnyItem;
|
|
13
|
+
/**
|
|
14
|
+
* The item without its answer. Custom (`x-`) types lose only `solution` (where OQSE
|
|
15
|
+
* puts everything that gives their answer away) and the common fields. `hints` stay:
|
|
16
|
+
* a game may show them on any device while the player answers.
|
|
17
|
+
*/
|
|
18
|
+
export declare function publicItem(item: OQSEAnyItem): OQSEAnyItem;
|
|
19
|
+
/**
|
|
20
|
+
* The set of a session: `set` in display order with answers (the authority,
|
|
21
|
+
* solo), `publicSet` without answers (the other instances in multiplayer).
|
|
22
|
+
* Without a seed nothing is shuffled.
|
|
23
|
+
*/
|
|
24
|
+
export declare function prepareDisplaySet(set: DisplaySet, seed?: string): {
|
|
25
|
+
set: DisplaySet;
|
|
26
|
+
publicSet: DisplaySet;
|
|
27
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Limits (SPEC section 8.1) and JSON value checks.
|
|
3
|
+
*
|
|
4
|
+
* The values are guaranteed minimums: a host MUST accept at least this much
|
|
5
|
+
* and MAY allow more. Plugins (and the SDK) MUST stay within them.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LIMITS: {
|
|
8
|
+
/** Max serialized size of one `send` message (bytes of UTF-8 JSON). */
|
|
9
|
+
readonly messageBytes: number;
|
|
10
|
+
/** Sustained `send` rate per instance (messages per second). */
|
|
11
|
+
readonly messagesPerSecond: 30;
|
|
12
|
+
/** Burst size of the `send` rate limiter. */
|
|
13
|
+
readonly messageBurst: 60;
|
|
14
|
+
/** Max serialized size of a snapshot. */
|
|
15
|
+
readonly snapshotBytes: number;
|
|
16
|
+
/** Snapshots per second the host stores (more are coalesced). */
|
|
17
|
+
readonly snapshotsPerSecond: 2;
|
|
18
|
+
/** Max serialized size of plugin data per scope. */
|
|
19
|
+
readonly dataBytes: number;
|
|
20
|
+
/** Plugin data writes per second and scope the host stores (more are coalesced). */
|
|
21
|
+
readonly dataWritesPerSecond: 1;
|
|
22
|
+
/** Max time between the iframe load and `hello`. */
|
|
23
|
+
readonly helloTimeoutMs: 10000;
|
|
24
|
+
/** Max serialized size of the player's answer in `recordAnswer`. */
|
|
25
|
+
readonly answerBytes: number;
|
|
26
|
+
/** Max serialized size of a generated item attached to `recordAnswer`. */
|
|
27
|
+
readonly generatedItemBytes: number;
|
|
28
|
+
/** Max serialized size of a service request (its result may be up to `messageBytes * 4`). */
|
|
29
|
+
readonly serviceBytes: number;
|
|
30
|
+
/** Service calls per minute and instance (sustained). */
|
|
31
|
+
readonly servicesPerMinute: 30;
|
|
32
|
+
};
|
|
33
|
+
export type Limits = {
|
|
34
|
+
[K in keyof typeof LIMITS]: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Checks that `value` is plain JSON (null, booleans, finite numbers, strings,
|
|
38
|
+
* arrays and plain objects; no cycles, no `undefined`, functions, Dates, Maps…).
|
|
39
|
+
* Returns an error description, or `null` when the value is valid.
|
|
40
|
+
*/
|
|
41
|
+
export declare function findNonJson(value: unknown, path?: string, depth?: number, seen?: Set<object>): string | null;
|
|
42
|
+
/** UTF-8 byte size of the JSON serialization of a (valid JSON) value. */
|
|
43
|
+
export declare function jsonByteSize(value: unknown): number;
|
|
44
|
+
/**
|
|
45
|
+
* Throws `INVALID_ARGUMENT` if `value` is not plain JSON, or `tooLargeCode`
|
|
46
|
+
* if its serialized size exceeds `maxBytes`. Returns the size in bytes.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertJsonWithin(value: unknown, maxBytes: number, tooLargeCode: 'MESSAGE_TOO_LARGE' | 'SNAPSHOT_TOO_LARGE' | 'DATA_TOO_LARGE', what: string): number;
|
|
49
|
+
/**
|
|
50
|
+
* Token bucket rate limiter (`rate` tokens per second, up to `burst` stored).
|
|
51
|
+
* Used by the host for `send`; also usable by the SDK to stay within limits.
|
|
52
|
+
*/
|
|
53
|
+
export declare class RateLimiter {
|
|
54
|
+
private readonly rate;
|
|
55
|
+
private readonly burst;
|
|
56
|
+
private readonly now;
|
|
57
|
+
private tokens;
|
|
58
|
+
private last;
|
|
59
|
+
constructor(rate: number, burst: number, now?: () => number);
|
|
60
|
+
/** Takes `cost` tokens (default one). Returns `false` when the limit is exceeded. */
|
|
61
|
+
tryTake(cost?: number): boolean;
|
|
62
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { OQSEManifest } from '@memizy/oqse';
|
|
3
|
+
import { PluginRuntime } from './pluginRuntime';
|
|
4
|
+
export declare const HostAsSchema: z.ZodEnum<{
|
|
5
|
+
presenter: "presenter";
|
|
6
|
+
player: "player";
|
|
7
|
+
}>;
|
|
8
|
+
export declare const MultiplayerModeSchema: z.ZodObject<{
|
|
9
|
+
players: z.ZodObject<{
|
|
10
|
+
min: z.ZodNumber;
|
|
11
|
+
max: z.ZodNumber;
|
|
12
|
+
recommended: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
}, z.core.$loose>;
|
|
14
|
+
hostAs: z.ZodArray<z.ZodEnum<{
|
|
15
|
+
presenter: "presenter";
|
|
16
|
+
player: "player";
|
|
17
|
+
}>>;
|
|
18
|
+
lateJoin: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
}, z.core.$loose>;
|
|
20
|
+
/** Service names are dot-separated lowercase segments, e.g. "ai.chat" or "chess.puzzles". */
|
|
21
|
+
export declare const ServiceNameSchema: z.ZodString;
|
|
22
|
+
export declare const PluginPermissionsSchema: z.ZodObject<{
|
|
23
|
+
network: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
24
|
+
devices: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
25
|
+
camera: "camera";
|
|
26
|
+
microphone: "microphone";
|
|
27
|
+
geolocation: "geolocation";
|
|
28
|
+
serial: "serial";
|
|
29
|
+
bluetooth: "bluetooth";
|
|
30
|
+
}>>>;
|
|
31
|
+
}, z.core.$loose>;
|
|
32
|
+
export declare const MemizyRuntimeSchema: z.ZodObject<{
|
|
33
|
+
protocol: z.ZodString;
|
|
34
|
+
set: z.ZodOptional<z.ZodEnum<{
|
|
35
|
+
optional: "optional";
|
|
36
|
+
required: "required";
|
|
37
|
+
none: "none";
|
|
38
|
+
}>>;
|
|
39
|
+
modes: z.ZodObject<{
|
|
40
|
+
solo: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
|
|
41
|
+
multiplayer: z.ZodOptional<z.ZodObject<{
|
|
42
|
+
players: z.ZodObject<{
|
|
43
|
+
min: z.ZodNumber;
|
|
44
|
+
max: z.ZodNumber;
|
|
45
|
+
recommended: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
}, z.core.$loose>;
|
|
47
|
+
hostAs: z.ZodArray<z.ZodEnum<{
|
|
48
|
+
presenter: "presenter";
|
|
49
|
+
player: "player";
|
|
50
|
+
}>>;
|
|
51
|
+
lateJoin: z.ZodOptional<z.ZodBoolean>;
|
|
52
|
+
}, z.core.$loose>>;
|
|
53
|
+
}, z.core.$loose>;
|
|
54
|
+
settings: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
55
|
+
type: z.ZodLiteral<"number">;
|
|
56
|
+
default: z.ZodNumber;
|
|
57
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
step: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
62
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
63
|
+
modes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
64
|
+
solo: "solo";
|
|
65
|
+
multiplayer: "multiplayer";
|
|
66
|
+
}>>>;
|
|
67
|
+
}, z.core.$loose>, z.ZodObject<{
|
|
68
|
+
type: z.ZodLiteral<"boolean">;
|
|
69
|
+
default: z.ZodBoolean;
|
|
70
|
+
id: z.ZodString;
|
|
71
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
72
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
73
|
+
modes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
74
|
+
solo: "solo";
|
|
75
|
+
multiplayer: "multiplayer";
|
|
76
|
+
}>>>;
|
|
77
|
+
}, z.core.$loose>, z.ZodObject<{
|
|
78
|
+
type: z.ZodLiteral<"select">;
|
|
79
|
+
options: z.ZodArray<z.ZodObject<{
|
|
80
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
81
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
82
|
+
}, z.core.$loose>>;
|
|
83
|
+
default: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
84
|
+
id: z.ZodString;
|
|
85
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
86
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
87
|
+
modes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
88
|
+
solo: "solo";
|
|
89
|
+
multiplayer: "multiplayer";
|
|
90
|
+
}>>>;
|
|
91
|
+
}, z.core.$loose>, z.ZodObject<{
|
|
92
|
+
type: z.ZodLiteral<"text">;
|
|
93
|
+
default: z.ZodString;
|
|
94
|
+
maxLength: z.ZodOptional<z.ZodNumber>;
|
|
95
|
+
id: z.ZodString;
|
|
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">>>;
|
|
103
|
+
settingsScreen: z.ZodOptional<z.ZodObject<{
|
|
104
|
+
size: z.ZodEnum<{
|
|
105
|
+
compact: "compact";
|
|
106
|
+
large: "large";
|
|
107
|
+
}>;
|
|
108
|
+
}, z.core.$loose>>;
|
|
109
|
+
display: z.ZodOptional<z.ZodObject<{
|
|
110
|
+
orientation: z.ZodOptional<z.ZodEnum<{
|
|
111
|
+
any: "any";
|
|
112
|
+
portrait: "portrait";
|
|
113
|
+
landscape: "landscape";
|
|
114
|
+
}>>;
|
|
115
|
+
}, z.core.$loose>>;
|
|
116
|
+
permissions: z.ZodOptional<z.ZodObject<{
|
|
117
|
+
network: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
118
|
+
devices: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
119
|
+
camera: "camera";
|
|
120
|
+
microphone: "microphone";
|
|
121
|
+
geolocation: "geolocation";
|
|
122
|
+
serial: "serial";
|
|
123
|
+
bluetooth: "bluetooth";
|
|
124
|
+
}>>>;
|
|
125
|
+
}, z.core.$loose>>;
|
|
126
|
+
services: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
127
|
+
}, z.core.$loose>;
|
|
128
|
+
export type MemizyRuntimeManifest = z.infer<typeof MemizyRuntimeSchema>;
|
|
129
|
+
/** A plugin manifest: OQSEM plus the required `appSpecific.memizy` block. */
|
|
130
|
+
export type PluginManifest = OQSEManifest & {
|
|
131
|
+
appSpecific: Record<string, unknown> & {
|
|
132
|
+
memizy: MemizyRuntimeManifest;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
export type PluginManifestResult = {
|
|
136
|
+
success: true;
|
|
137
|
+
manifest: PluginManifest;
|
|
138
|
+
runtime: PluginRuntime;
|
|
139
|
+
} | {
|
|
140
|
+
success: false;
|
|
141
|
+
errors: string[];
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Validates a plugin manifest (OQSEM part and Memizy runtime part).
|
|
145
|
+
* The returned `manifest` is the original object (unknown keys preserved).
|
|
146
|
+
*/
|
|
147
|
+
export declare function safeParsePluginManifest(data: unknown): PluginManifestResult;
|
|
148
|
+
/** Error thrown by {@link parsePluginManifest}. */
|
|
149
|
+
export declare class PluginManifestError extends Error {
|
|
150
|
+
readonly errors: string[];
|
|
151
|
+
constructor(errors: string[]);
|
|
152
|
+
}
|
|
153
|
+
/** Like {@link safeParsePluginManifest}, but throws {@link PluginManifestError}. */
|
|
154
|
+
export declare function parsePluginManifest(data: unknown): {
|
|
155
|
+
manifest: PluginManifest;
|
|
156
|
+
runtime: PluginRuntime;
|
|
157
|
+
};
|
|
158
|
+
/** Extracts and validates the manifest of a plugin HTML file. */
|
|
159
|
+
export declare function readPluginManifestFromHtml(html: string): PluginManifestResult;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MemizyRuntimeManifest } from './manifestSchema';
|
|
2
|
+
import { SettingDefinition } from './settingsSchema';
|
|
3
|
+
export type HostAs = 'presenter' | 'player';
|
|
4
|
+
export type PluginView = 'solo' | 'board' | 'controller' | 'settings';
|
|
5
|
+
/** The runtime part with all defaults applied – what hosts work with. */
|
|
6
|
+
export interface PluginRuntime {
|
|
7
|
+
protocol: string;
|
|
8
|
+
/** Whether the plugin uses a study set: `none` = a game without questions. */
|
|
9
|
+
set: 'required' | 'optional' | 'none';
|
|
10
|
+
solo: boolean;
|
|
11
|
+
multiplayer: null | {
|
|
12
|
+
players: {
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
recommended?: number;
|
|
16
|
+
};
|
|
17
|
+
hostAs: HostAs[];
|
|
18
|
+
lateJoin: boolean;
|
|
19
|
+
};
|
|
20
|
+
settings: SettingDefinition[];
|
|
21
|
+
settingsScreen: null | {
|
|
22
|
+
size: 'compact' | 'large';
|
|
23
|
+
};
|
|
24
|
+
orientation: 'any' | 'portrait' | 'landscape';
|
|
25
|
+
/** Views the plugin must render (SPEC 3.2). */
|
|
26
|
+
views: PluginView[];
|
|
27
|
+
/** Declared network origins and devices (SPEC 8.4). */
|
|
28
|
+
permissions: {
|
|
29
|
+
network: string[];
|
|
30
|
+
devices: ('camera' | 'microphone' | 'geolocation' | 'serial' | 'bluetooth')[];
|
|
31
|
+
};
|
|
32
|
+
/** Memizy services the plugin uses (SPEC 5.3). */
|
|
33
|
+
services: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads the manifest data island from plugin HTML **without executing it**
|
|
37
|
+
* (works in browsers and in Node). Returns the parsed JSON, or an error.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractManifestFromHtml(html: string): {
|
|
40
|
+
success: true;
|
|
41
|
+
data: unknown;
|
|
42
|
+
} | {
|
|
43
|
+
success: false;
|
|
44
|
+
error: string;
|
|
45
|
+
};
|
|
46
|
+
/** Applies defaults and derives the views (SPEC 3.2). */
|
|
47
|
+
export declare function toRuntime(memizy: MemizyRuntimeManifest): PluginRuntime;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small polyfills for what the connection library (Penpal 8) needs from the browser, so
|
|
3
|
+
* older school devices (Safari before 17.4) still run games. Called right before
|
|
4
|
+
* connecting; it only adds what is missing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ensureConnectionPolyfills(): void;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay wire protocol: the host app and player apps ↔ the Memizy multiplayer
|
|
3
|
+
* server (WebSocket, JSON text frames). Plugins never see it (SPEC D2).
|
|
4
|
+
*
|
|
5
|
+
* The server is a room relay: PINs, player identity (reconnect tokens),
|
|
6
|
+
* presence and forwarding between the host and the players. The game session
|
|
7
|
+
* itself (validation, limits, authority, state) runs in the host's browser.
|
|
8
|
+
* The plugin and the study set are uploaded once over HTTP and downloaded by
|
|
9
|
+
* the players over HTTP (`RelayBundle`), not sent through the WebSocket.
|
|
10
|
+
*
|
|
11
|
+
* HTTP API (JSON):
|
|
12
|
+
* - `POST /api/rooms` → `CreateRoomResponse`
|
|
13
|
+
* - `GET /api/rooms/:pin` → `RoomInfo` (404 if unknown)
|
|
14
|
+
* - `PUT /api/rooms/:pin/bundle` (header `Authorization: Bearer <hostToken>`, body `RelayBundle`) → `{ version }`
|
|
15
|
+
* - `GET /api/rooms/:pin/bundle` → `RelayBundle` (header `X-Bundle-Version`)
|
|
16
|
+
* - `GET /api/health`
|
|
17
|
+
* WebSocket: `/ws`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RELAY_PROTOCOL = 1;
|
|
20
|
+
export declare const RELAY_LIMITS: {
|
|
21
|
+
/** Max size of one WebSocket frame (UTF-8 JSON). */
|
|
22
|
+
readonly frameBytes: number;
|
|
23
|
+
/** Max size of an uploaded bundle (plugin + set). */
|
|
24
|
+
readonly bundleBytes: number;
|
|
25
|
+
/** Max players in a room (the plugin may allow fewer). */
|
|
26
|
+
readonly playersPerRoom: 100;
|
|
27
|
+
/** Player frames per second (sustained / burst). */
|
|
28
|
+
readonly playerFramesPerSecond: 60;
|
|
29
|
+
readonly playerFrameBurst: 120;
|
|
30
|
+
/** Host frames per second (it talks to every player). */
|
|
31
|
+
readonly hostFramesPerSecond: 3000;
|
|
32
|
+
readonly hostFrameBurst: 6000;
|
|
33
|
+
/** Max player name length. */
|
|
34
|
+
readonly nameLength: 32;
|
|
35
|
+
/** A room is closed when its host has been away this long. */
|
|
36
|
+
readonly hostAwayMs: number;
|
|
37
|
+
/** A room is closed after this time in any case. */
|
|
38
|
+
readonly roomTtlMs: number;
|
|
39
|
+
};
|
|
40
|
+
/** What the players download: the plugin HTML and the set prepared for it. */
|
|
41
|
+
export interface RelayBundle {
|
|
42
|
+
pluginHtml: string;
|
|
43
|
+
/** The set as delivered to plugins (`InitPayload.set`). */
|
|
44
|
+
set: unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface CreateRoomResponse {
|
|
47
|
+
pin: string;
|
|
48
|
+
hostToken: string;
|
|
49
|
+
}
|
|
50
|
+
export interface RoomInfo {
|
|
51
|
+
pin: string;
|
|
52
|
+
/** Whether new players can join right now. */
|
|
53
|
+
open: boolean;
|
|
54
|
+
/** Name of the game (once the host uploaded a bundle). */
|
|
55
|
+
appName: string | null;
|
|
56
|
+
players: number;
|
|
57
|
+
}
|
|
58
|
+
export interface RelayPlayer {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
connected: boolean;
|
|
62
|
+
}
|
|
63
|
+
export type RelayClientMessage =
|
|
64
|
+
/** The host attaches to its room (also after a reconnect). */
|
|
65
|
+
{
|
|
66
|
+
t: 'host';
|
|
67
|
+
protocol: number;
|
|
68
|
+
pin: string;
|
|
69
|
+
hostToken: string;
|
|
70
|
+
}
|
|
71
|
+
/** A player joins (with `token`: rejoins as the same player). */
|
|
72
|
+
| {
|
|
73
|
+
t: 'join';
|
|
74
|
+
protocol: number;
|
|
75
|
+
pin: string;
|
|
76
|
+
name: string;
|
|
77
|
+
token?: string;
|
|
78
|
+
}
|
|
79
|
+
/** Host → one player. */
|
|
80
|
+
| {
|
|
81
|
+
t: 'to';
|
|
82
|
+
to: string;
|
|
83
|
+
data: unknown;
|
|
84
|
+
}
|
|
85
|
+
/** Host: allow or stop new players joining. */
|
|
86
|
+
| {
|
|
87
|
+
t: 'open';
|
|
88
|
+
open: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** Host: remove a player (they cannot rejoin with their token). */
|
|
91
|
+
| {
|
|
92
|
+
t: 'kick';
|
|
93
|
+
playerId: string;
|
|
94
|
+
}
|
|
95
|
+
/** Host: close the room for everyone. */
|
|
96
|
+
| {
|
|
97
|
+
t: 'close';
|
|
98
|
+
}
|
|
99
|
+
/** Player → host. */
|
|
100
|
+
| {
|
|
101
|
+
t: 'up';
|
|
102
|
+
data: unknown;
|
|
103
|
+
}
|
|
104
|
+
/** Player: change the name. */
|
|
105
|
+
| {
|
|
106
|
+
t: 'rename';
|
|
107
|
+
name: string;
|
|
108
|
+
} | {
|
|
109
|
+
t: 'ping';
|
|
110
|
+
};
|
|
111
|
+
export type RelayErrorCode = 'BAD_MESSAGE' | 'UNSUPPORTED_PROTOCOL' | 'ROOM_NOT_FOUND' | 'ROOM_CLOSED' | 'ROOM_FULL' | 'NOT_ALLOWED' | 'KICKED' | 'RATE_LIMITED' | 'TOO_LARGE';
|
|
112
|
+
export type RelayServerMessage =
|
|
113
|
+
/** To the host after `host`: the current roster. */
|
|
114
|
+
{
|
|
115
|
+
t: 'hosting';
|
|
116
|
+
pin: string;
|
|
117
|
+
players: RelayPlayer[];
|
|
118
|
+
}
|
|
119
|
+
/** To a player after `join`. Keep `token` to rejoin as the same player. */
|
|
120
|
+
| {
|
|
121
|
+
t: 'joined';
|
|
122
|
+
pin: string;
|
|
123
|
+
playerId: string;
|
|
124
|
+
token: string;
|
|
125
|
+
name: string;
|
|
126
|
+
hostConnected: boolean;
|
|
127
|
+
}
|
|
128
|
+
/** To the host: a player joined, reconnected, disconnected or was renamed. */
|
|
129
|
+
| {
|
|
130
|
+
t: 'presence';
|
|
131
|
+
player: RelayPlayer;
|
|
132
|
+
event: 'join' | 'connect' | 'disconnect' | 'rename' | 'leave';
|
|
133
|
+
}
|
|
134
|
+
/** To players: the host connection state. */
|
|
135
|
+
| {
|
|
136
|
+
t: 'host-presence';
|
|
137
|
+
connected: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** Player → host. */
|
|
140
|
+
| {
|
|
141
|
+
t: 'from';
|
|
142
|
+
from: string;
|
|
143
|
+
data: unknown;
|
|
144
|
+
}
|
|
145
|
+
/** Host → player. */
|
|
146
|
+
| {
|
|
147
|
+
t: 'data';
|
|
148
|
+
data: unknown;
|
|
149
|
+
}
|
|
150
|
+
/** A request failed; fatal errors are followed by closing the socket. */
|
|
151
|
+
| {
|
|
152
|
+
t: 'error';
|
|
153
|
+
code: RelayErrorCode;
|
|
154
|
+
message: string;
|
|
155
|
+
fatal: boolean;
|
|
156
|
+
}
|
|
157
|
+
/** The room was closed by the host or expired. */
|
|
158
|
+
| {
|
|
159
|
+
t: 'closed';
|
|
160
|
+
reason: 'host' | 'expired' | 'kicked';
|
|
161
|
+
} | {
|
|
162
|
+
t: 'pong';
|
|
163
|
+
time: number;
|
|
164
|
+
};
|
|
165
|
+
/** Normalizes a player name (trimmed, collapsed spaces, max length). */
|
|
166
|
+
export declare function normalizePlayerName(name: unknown): string;
|
|
167
|
+
/** A 6-digit PIN. */
|
|
168
|
+
export declare function isPin(value: unknown): value is string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { HostApi } from './types';
|
|
3
|
+
export declare const AddressSchema: z.ZodString;
|
|
4
|
+
export declare const HandshakeSchema: z.ZodObject<{
|
|
5
|
+
protocol: z.ZodString;
|
|
6
|
+
sdk: z.ZodObject<{
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
version: z.ZodString;
|
|
9
|
+
}, z.core.$loose>;
|
|
10
|
+
plugin: z.ZodObject<{
|
|
11
|
+
id: z.ZodString;
|
|
12
|
+
version: z.ZodString;
|
|
13
|
+
}, z.core.$loose>;
|
|
14
|
+
features: z.ZodArray<z.ZodString>;
|
|
15
|
+
}, z.core.$loose>;
|
|
16
|
+
export declare const OutgoingMessageSchema: z.ZodObject<{
|
|
17
|
+
to: z.ZodUnion<readonly [z.ZodLiteral<"authority">, z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
|
|
18
|
+
data: z.ZodUnknown;
|
|
19
|
+
}, z.core.$loose>;
|
|
20
|
+
export declare const AnswerRecordSchema: z.ZodObject<{
|
|
21
|
+
playerId: z.ZodOptional<z.ZodString>;
|
|
22
|
+
itemId: z.ZodString;
|
|
23
|
+
isCorrect: z.ZodBoolean;
|
|
24
|
+
confidence: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>]>>;
|
|
25
|
+
timeSpentMs: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
hintsUsed: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
isSkipped: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
answer: z.ZodOptional<z.ZodUnknown>;
|
|
29
|
+
item: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
id: z.ZodString;
|
|
31
|
+
type: z.ZodString;
|
|
32
|
+
}, z.core.$loose>>;
|
|
33
|
+
}, z.core.$loose>;
|
|
34
|
+
export declare const ProgressRecordsSchema: z.ZodRecord<z.ZodString, z.ZodType<import('@memizy/oqse').ProgressRecord, unknown, z.core.$ZodTypeInternals<import('@memizy/oqse').ProgressRecord, unknown>>>;
|
|
35
|
+
export declare const DataScopeSchema: z.ZodEnum<{
|
|
36
|
+
set: "set";
|
|
37
|
+
plugin: "plugin";
|
|
38
|
+
}>;
|
|
39
|
+
export declare const SettingsUpdateSchema: z.ZodObject<{
|
|
40
|
+
values: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
41
|
+
valid: z.ZodBoolean;
|
|
42
|
+
message: z.ZodOptional<z.ZodString>;
|
|
43
|
+
}, z.core.$loose>;
|
|
44
|
+
export declare const SessionResultSchema: z.ZodObject<{
|
|
45
|
+
scores: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
46
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$loose>;
|
|
48
|
+
export declare const ResizeRequestSchema: z.ZodObject<{
|
|
49
|
+
height: z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"auto">]>;
|
|
50
|
+
}, z.core.$loose>;
|
|
51
|
+
export declare const ErrorReportSchema: z.ZodObject<{
|
|
52
|
+
code: z.ZodString;
|
|
53
|
+
message: z.ZodString;
|
|
54
|
+
context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
55
|
+
}, z.core.$loose>;
|
|
56
|
+
/** Argument schemas of every Host API method. */
|
|
57
|
+
export declare const HOST_API_ARGS: {
|
|
58
|
+
hello: z.ZodTuple<[z.ZodObject<{
|
|
59
|
+
protocol: z.ZodString;
|
|
60
|
+
sdk: z.ZodObject<{
|
|
61
|
+
name: z.ZodString;
|
|
62
|
+
version: z.ZodString;
|
|
63
|
+
}, z.core.$loose>;
|
|
64
|
+
plugin: z.ZodObject<{
|
|
65
|
+
id: z.ZodString;
|
|
66
|
+
version: z.ZodString;
|
|
67
|
+
}, z.core.$loose>;
|
|
68
|
+
features: z.ZodArray<z.ZodString>;
|
|
69
|
+
}, z.core.$loose>], null>;
|
|
70
|
+
ready: z.ZodTuple<[], null>;
|
|
71
|
+
send: z.ZodTuple<[z.ZodObject<{
|
|
72
|
+
to: z.ZodUnion<readonly [z.ZodLiteral<"authority">, z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
|
|
73
|
+
data: z.ZodUnknown;
|
|
74
|
+
}, z.core.$loose>], null>;
|
|
75
|
+
saveSnapshot: z.ZodTuple<[z.ZodUnknown], null>;
|
|
76
|
+
recordAnswer: z.ZodTuple<[z.ZodObject<{
|
|
77
|
+
playerId: z.ZodOptional<z.ZodString>;
|
|
78
|
+
itemId: z.ZodString;
|
|
79
|
+
isCorrect: z.ZodBoolean;
|
|
80
|
+
confidence: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>]>>;
|
|
81
|
+
timeSpentMs: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
hintsUsed: z.ZodOptional<z.ZodNumber>;
|
|
83
|
+
isSkipped: z.ZodOptional<z.ZodBoolean>;
|
|
84
|
+
answer: z.ZodOptional<z.ZodUnknown>;
|
|
85
|
+
item: z.ZodOptional<z.ZodObject<{
|
|
86
|
+
id: z.ZodString;
|
|
87
|
+
type: z.ZodString;
|
|
88
|
+
}, z.core.$loose>>;
|
|
89
|
+
}, z.core.$loose>], null>;
|
|
90
|
+
saveProgress: z.ZodTuple<[z.ZodRecord<z.ZodString, z.ZodType<import('@memizy/oqse').ProgressRecord, unknown, z.core.$ZodTypeInternals<import('@memizy/oqse').ProgressRecord, unknown>>>], null>;
|
|
91
|
+
saveData: z.ZodTuple<[z.ZodEnum<{
|
|
92
|
+
set: "set";
|
|
93
|
+
plugin: "plugin";
|
|
94
|
+
}>, z.ZodUnknown], null>;
|
|
95
|
+
updateSettings: z.ZodTuple<[z.ZodObject<{
|
|
96
|
+
values: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
97
|
+
valid: z.ZodBoolean;
|
|
98
|
+
message: z.ZodOptional<z.ZodString>;
|
|
99
|
+
}, z.core.$loose>], null>;
|
|
100
|
+
getAsset: z.ZodTuple<[z.ZodString, z.ZodOptional<z.ZodString>], null>;
|
|
101
|
+
end: z.ZodTuple<[z.ZodObject<{
|
|
102
|
+
scores: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
103
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
104
|
+
}, z.core.$loose>], null>;
|
|
105
|
+
resize: z.ZodTuple<[z.ZodObject<{
|
|
106
|
+
height: z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"auto">]>;
|
|
107
|
+
}, z.core.$loose>], null>;
|
|
108
|
+
reportError: z.ZodTuple<[z.ZodObject<{
|
|
109
|
+
code: z.ZodString;
|
|
110
|
+
message: z.ZodString;
|
|
111
|
+
context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
112
|
+
}, z.core.$loose>], null>;
|
|
113
|
+
exit: z.ZodTuple<[], null>;
|
|
114
|
+
service: z.ZodTuple<[z.ZodString, z.ZodUnknown], null>;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Validates the arguments of a Host API call.
|
|
118
|
+
* @throws {ProtocolError} `INVALID_ARGUMENT` with readable details.
|
|
119
|
+
*/
|
|
120
|
+
export declare function parseHostCall<M extends keyof HostApi>(method: M, args: unknown[]): Parameters<HostApi[M]>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PluginRuntime } from './pluginRuntime';
|
|
2
|
+
/** CDNs plugins load libraries from (scripts, styles, fonts, data files). */
|
|
3
|
+
export declare const LIBRARY_ORIGINS: string[];
|
|
4
|
+
/** Device permissions a plugin may declare, and their iframe `allow` names. */
|
|
5
|
+
export declare const PLUGIN_DEVICES: readonly ["camera", "microphone", "geolocation", "serial", "bluetooth"];
|
|
6
|
+
export type PluginDevice = (typeof PLUGIN_DEVICES)[number];
|
|
7
|
+
/**
|
|
8
|
+
* The Content Security Policy of a plugin iframe.
|
|
9
|
+
*
|
|
10
|
+
* - Scripts only inline (the plugin itself), from the library CDNs and from the host
|
|
11
|
+
* (`hostOrigin`, which serves the SDK in development and in Memizy Play).
|
|
12
|
+
* - Network requests (`fetch`, WebSocket…) only to the CDNs and the declared
|
|
13
|
+
* `permissions.network` origins.
|
|
14
|
+
* - Images and media may come from any https URL (study sets link them), so an image
|
|
15
|
+
* URL can still carry data out: the policy blocks data channels, not every leak.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pluginContentPolicy(runtime: Pick<PluginRuntime, 'permissions'> | null, hostOrigin: string | null): string;
|
|
18
|
+
/** The iframe `allow` attribute for the declared devices (empty when none). */
|
|
19
|
+
export declare function pluginAllowAttribute(runtime: Pick<PluginRuntime, 'permissions'> | null): string;
|
|
20
|
+
/** Puts the policy into the plugin HTML as the first element of `<head>` (before any script). */
|
|
21
|
+
export declare function injectContentPolicy(html: string, policy: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LocalizedText, SettingDefinition } from './settingsSchema';
|
|
2
|
+
export type SettingValue = string | number | boolean;
|
|
3
|
+
/** Returns an error description if `value` is not valid for `def`, otherwise `null`. */
|
|
4
|
+
export declare function validateSettingValue(def: SettingDefinition, value: unknown): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Builds the full settings object: every declared setting gets the valid input
|
|
7
|
+
* value or its default. Invalid values and unknown keys are reported in `errors`
|
|
8
|
+
* (and replaced by defaults / dropped).
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveSettings(definitions: SettingDefinition[], input?: Record<string, unknown>): {
|
|
11
|
+
values: Record<string, SettingValue>;
|
|
12
|
+
errors: string[];
|
|
13
|
+
};
|
|
14
|
+
/** The settings a host shows for a mode (`modes` absent = both modes). */
|
|
15
|
+
export declare function settingsForMode<T extends {
|
|
16
|
+
modes?: readonly string[];
|
|
17
|
+
}>(definitions: readonly T[], mode: 'solo' | 'multiplayer'): T[];
|
|
18
|
+
/**
|
|
19
|
+
* Picks the best text for a locale: exact match (`cs-CZ`), language (`cs`),
|
|
20
|
+
* English, then the first available.
|
|
21
|
+
*/
|
|
22
|
+
export declare function localize(text: LocalizedText, locale: string): string;
|