@anweather/game-plugin-ssdk 1.16.0 → 1.17.0
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/dist-cjs/runtime/auth.js +8 -0
- package/dist-cjs/runtime/conformance.js +6 -0
- package/dist-cjs/runtime/index.js +15 -1
- package/dist-cjs/runtime/live/checkpoint.js +31 -0
- package/dist-cjs/runtime/live/conformance.js +215 -0
- package/dist-cjs/runtime/live/harness.js +144 -0
- package/dist-cjs/runtime/live/outbox.js +72 -0
- package/dist-cjs/runtime/live/protocol.js +227 -0
- package/dist-cjs/runtime/live/rooms.js +579 -0
- package/dist-cjs/runtime/live/server.js +105 -0
- package/dist-cjs/runtime/servePlugin.js +8 -2
- package/dist-cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist-es/runtime/auth.js +5 -0
- package/dist-es/runtime/conformance.js +6 -0
- package/dist-es/runtime/index.js +5 -0
- package/dist-es/runtime/live/checkpoint.js +28 -0
- package/dist-es/runtime/live/conformance.js +210 -0
- package/dist-es/runtime/live/harness.js +141 -0
- package/dist-es/runtime/live/outbox.js +68 -0
- package/dist-es/runtime/live/protocol.js +217 -0
- package/dist-es/runtime/live/rooms.js +575 -0
- package/dist-es/runtime/live/server.js +102 -0
- package/dist-es/runtime/servePlugin.js +8 -2
- package/dist-es/tsconfig.es.tsbuildinfo +1 -1
- package/dist-types/runtime/auth.d.ts +3 -0
- package/dist-types/runtime/conformance.d.ts +6 -0
- package/dist-types/runtime/index.d.ts +11 -0
- package/dist-types/runtime/live/checkpoint.d.ts +15 -0
- package/dist-types/runtime/live/conformance.d.ts +24 -0
- package/dist-types/runtime/live/harness.d.ts +71 -0
- package/dist-types/runtime/live/outbox.d.ts +39 -0
- package/dist-types/runtime/live/protocol.d.ts +169 -0
- package/dist-types/runtime/live/rooms.d.ts +302 -0
- package/dist-types/runtime/live/server.d.ts +36 -0
- package/dist-types/runtime/servePlugin.d.ts +8 -0
- package/package.json +6 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { bearerMatches } from "../auth";
|
|
2
|
+
export const LIVE_PROTOCOL_VERSION = 1;
|
|
3
|
+
export const LIVE_PATH = "/v1/live";
|
|
4
|
+
export const LIVE_HEADERS = {
|
|
5
|
+
auth: "x-plugin-auth",
|
|
6
|
+
scope: "x-live-scope",
|
|
7
|
+
epoch: "x-live-epoch",
|
|
8
|
+
gameType: "x-game-type",
|
|
9
|
+
protocol: "x-live-protocol",
|
|
10
|
+
};
|
|
11
|
+
export const MAX_LINK_FRAME_BYTES = 64 * 1024;
|
|
12
|
+
export const LinkCloseCode = {
|
|
13
|
+
NORMAL: 1000,
|
|
14
|
+
GOING_AWAY: 1001,
|
|
15
|
+
POLICY: 1008,
|
|
16
|
+
TOO_BIG: 1009,
|
|
17
|
+
INTERNAL_ERROR: 1011,
|
|
18
|
+
RESTART: 1012,
|
|
19
|
+
TOO_SLOW: 4008,
|
|
20
|
+
SUPERSEDED: 4409,
|
|
21
|
+
GONE: 4410,
|
|
22
|
+
};
|
|
23
|
+
function parseObject(text) {
|
|
24
|
+
let value;
|
|
25
|
+
try {
|
|
26
|
+
value = JSON.parse(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "not JSON";
|
|
30
|
+
}
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
32
|
+
return "not a JSON object";
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
const isStr = (v) => typeof v === "string";
|
|
36
|
+
const isNonEmpty = (v) => typeof v === "string" && v.length > 0;
|
|
37
|
+
class Malformed extends Error {
|
|
38
|
+
}
|
|
39
|
+
function need(o, field, check, what) {
|
|
40
|
+
const v = o[field];
|
|
41
|
+
if (!check(v))
|
|
42
|
+
throw new Malformed(`${o.t}: ${field} must be ${what}`);
|
|
43
|
+
return v;
|
|
44
|
+
}
|
|
45
|
+
const isRole = (v) => v === "PARTICIPANT" || v === "SPECTATOR";
|
|
46
|
+
const isState = (v) => v === "ACTIVE" || v === "AWAY";
|
|
47
|
+
const isRevision = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
48
|
+
const isMember = (v) => {
|
|
49
|
+
if (typeof v !== "object" || v === null)
|
|
50
|
+
return false;
|
|
51
|
+
const m = v;
|
|
52
|
+
return isNonEmpty(m.playerId) && isStr(m.displayName) && isStr(m.role) && isStr(m.status);
|
|
53
|
+
};
|
|
54
|
+
const isMembers = (v) => Array.isArray(v) && v.every(isMember);
|
|
55
|
+
const isBool = (v) => typeof v === "boolean";
|
|
56
|
+
const isAnything = (v) => v !== undefined;
|
|
57
|
+
export function decodePlatformFrame(text) {
|
|
58
|
+
const o = parseObject(text);
|
|
59
|
+
if (typeof o === "string")
|
|
60
|
+
return { ok: false, unknown: false, error: o };
|
|
61
|
+
if (!isStr(o.t))
|
|
62
|
+
return { ok: false, unknown: false, error: "t must be a string" };
|
|
63
|
+
try {
|
|
64
|
+
switch (o.t) {
|
|
65
|
+
case "join":
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
frame: {
|
|
69
|
+
t: "join",
|
|
70
|
+
conn: need(o, "conn", isNonEmpty, "a non-empty string"),
|
|
71
|
+
participantId: need(o, "participantId", isNonEmpty, "a non-empty string"),
|
|
72
|
+
playerId: need(o, "playerId", isNonEmpty, "a non-empty string"),
|
|
73
|
+
displayName: need(o, "displayName", isStr, "a string"),
|
|
74
|
+
role: need(o, "role", isRole, "PARTICIPANT or SPECTATOR"),
|
|
75
|
+
gameId: need(o, "gameId", isNonEmpty, "a non-empty string"),
|
|
76
|
+
hidden: o.hidden === undefined ? false : need(o, "hidden", isBool, "a boolean"),
|
|
77
|
+
state: o.state === undefined ? "ACTIVE" : need(o, "state", isState, "ACTIVE or AWAY"),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
case "leave":
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
frame: {
|
|
84
|
+
t: "leave",
|
|
85
|
+
conn: need(o, "conn", isNonEmpty, "a non-empty string"),
|
|
86
|
+
reason: o.reason === undefined ? "closed" : need(o, "reason", isStr, "a string"),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
case "sig":
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
frame: {
|
|
93
|
+
t: "sig",
|
|
94
|
+
conn: need(o, "conn", isNonEmpty, "a non-empty string"),
|
|
95
|
+
participantId: need(o, "participantId", isNonEmpty, "a non-empty string"),
|
|
96
|
+
d: need(o, "d", isAnything, "present"),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
case "presence":
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
frame: {
|
|
103
|
+
t: "presence",
|
|
104
|
+
conn: need(o, "conn", isNonEmpty, "a non-empty string"),
|
|
105
|
+
state: need(o, "state", isState, "ACTIVE or AWAY"),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
case "game":
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
frame: {
|
|
112
|
+
t: "game",
|
|
113
|
+
gameId: need(o, "gameId", isNonEmpty, "a non-empty string"),
|
|
114
|
+
revision: need(o, "revision", isRevision, "a non-negative integer"),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
case "members":
|
|
118
|
+
return { ok: true, frame: { t: "members", members: need(o, "members", isMembers, "an array of members") } };
|
|
119
|
+
default:
|
|
120
|
+
return { ok: false, unknown: true, t: o.t };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e instanceof Malformed)
|
|
125
|
+
return { ok: false, unknown: false, error: e.message };
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function decodePluginFrame(text) {
|
|
130
|
+
const o = parseObject(text);
|
|
131
|
+
if (typeof o === "string")
|
|
132
|
+
return { ok: false, unknown: false, error: o };
|
|
133
|
+
if (!isStr(o.t))
|
|
134
|
+
return { ok: false, unknown: false, error: "t must be a string" };
|
|
135
|
+
try {
|
|
136
|
+
switch (o.t) {
|
|
137
|
+
case "send": {
|
|
138
|
+
const to = o.to;
|
|
139
|
+
if (!(to === "all" || (Array.isArray(to) && to.every(isNonEmpty)))) {
|
|
140
|
+
throw new Malformed(`send: to must be "all" or an array of conn ids`);
|
|
141
|
+
}
|
|
142
|
+
const frame = { t: "send", to: to, d: need(o, "d", isAnything, "present") };
|
|
143
|
+
if (o.k !== undefined)
|
|
144
|
+
frame.k = need(o, "k", isNonEmpty, "a non-empty string");
|
|
145
|
+
return { ok: true, frame };
|
|
146
|
+
}
|
|
147
|
+
case "kick": {
|
|
148
|
+
const frame = {
|
|
149
|
+
t: "kick",
|
|
150
|
+
conn: need(o, "conn", isNonEmpty, "a non-empty string"),
|
|
151
|
+
code: need(o, "code", isKickCode, "4403 or an integer from 4900 to 4999"),
|
|
152
|
+
};
|
|
153
|
+
if (o.reason !== undefined)
|
|
154
|
+
frame.reason = need(o, "reason", isCloseReason, "a string of at most 120 characters");
|
|
155
|
+
return { ok: true, frame };
|
|
156
|
+
}
|
|
157
|
+
case "ready":
|
|
158
|
+
return { ok: true, frame: { t: "ready" } };
|
|
159
|
+
default:
|
|
160
|
+
return { ok: false, unknown: true, t: o.t };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
if (e instanceof Malformed)
|
|
165
|
+
return { ok: false, unknown: false, error: e.message };
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export const MAX_KICK_REASON = 120;
|
|
170
|
+
const isCloseReason = (v) => typeof v === "string" && v.length <= MAX_KICK_REASON;
|
|
171
|
+
export function isKickCode(v) {
|
|
172
|
+
return typeof v === "number" && Number.isInteger(v) && (v === 4403 || (v >= 4900 && v <= 4999));
|
|
173
|
+
}
|
|
174
|
+
export function encodeFrame(frame) {
|
|
175
|
+
return JSON.stringify(frame);
|
|
176
|
+
}
|
|
177
|
+
export function byteLength(text) {
|
|
178
|
+
return Buffer.byteLength(text, "utf8");
|
|
179
|
+
}
|
|
180
|
+
export function parseScope(header) {
|
|
181
|
+
if (!header)
|
|
182
|
+
return null;
|
|
183
|
+
const colon = header.indexOf(":");
|
|
184
|
+
if (colon <= 0)
|
|
185
|
+
return null;
|
|
186
|
+
const kind = header.slice(0, colon);
|
|
187
|
+
const id = header.slice(colon + 1);
|
|
188
|
+
if (!id || /\s/.test(id))
|
|
189
|
+
return null;
|
|
190
|
+
if (kind !== "game" && kind !== "space")
|
|
191
|
+
return null;
|
|
192
|
+
return { kind, id, key: `${kind}:${id}` };
|
|
193
|
+
}
|
|
194
|
+
const header = (h, name) => {
|
|
195
|
+
const v = h[name];
|
|
196
|
+
return Array.isArray(v) ? v[0] : v;
|
|
197
|
+
};
|
|
198
|
+
export function checkHandshake(headers, expect) {
|
|
199
|
+
if (expect.authToken && !bearerMatches(header(headers, LIVE_HEADERS.auth), expect.authToken)) {
|
|
200
|
+
return { ok: false, status: 401, message: "missing or invalid X-Plugin-Auth" };
|
|
201
|
+
}
|
|
202
|
+
const protocol = header(headers, LIVE_HEADERS.protocol);
|
|
203
|
+
if (protocol !== String(LIVE_PROTOCOL_VERSION)) {
|
|
204
|
+
return { ok: false, status: 400, message: `unsupported X-Live-Protocol ${protocol ?? "(absent)"}; this plugin speaks ${LIVE_PROTOCOL_VERSION}` };
|
|
205
|
+
}
|
|
206
|
+
const gameType = header(headers, LIVE_HEADERS.gameType);
|
|
207
|
+
if (gameType !== expect.gameType) {
|
|
208
|
+
return { ok: false, status: 404, message: `this plugin hosts ${expect.gameType}, not ${gameType ?? "(absent)"}` };
|
|
209
|
+
}
|
|
210
|
+
const scope = parseScope(header(headers, LIVE_HEADERS.scope));
|
|
211
|
+
if (!scope)
|
|
212
|
+
return { ok: false, status: 400, message: "X-Live-Scope must be game:{id} or space:{id}" };
|
|
213
|
+
const epoch = header(headers, LIVE_HEADERS.epoch);
|
|
214
|
+
if (!epoch)
|
|
215
|
+
return { ok: false, status: 400, message: "X-Live-Epoch is required" };
|
|
216
|
+
return { ok: true, scope, epoch, protocol: LIVE_PROTOCOL_VERSION };
|
|
217
|
+
}
|