@seekprotocol/game 0.0.0-stage → 0.1.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/seek-game.js ADDED
@@ -0,0 +1,982 @@
1
+ // @seekprotocol/game: the Seek GameKit bridge. Generated by packages/game/bundle.mjs; do not edit.
2
+
3
+ // src/diagnostics.ts
4
+ var LOG_LEVELS = ["debug", "info", "warn", "error"];
5
+ var isLogLevel = (v) => typeof v === "string" && LOG_LEVELS.includes(v);
6
+ var HOST_EVENTS = ["no_ready", "hang", "render_gone", "load_failed"];
7
+ var isHostEvent = (v) => typeof v === "string" && HOST_EVENTS.includes(v);
8
+ var HOST_READY_TIMEOUT_MS = 1e4;
9
+ var HOST_HANG_MS = 3e4;
10
+ var HOST_EVENT_TEXT = {
11
+ no_ready: "The game did not call ready() within 10 seconds.",
12
+ hang: "The game sent no heartbeat for 30 seconds while it was in the foreground.",
13
+ render_gone: "The WebView process running the game died.",
14
+ load_failed: "The game page failed to load."
15
+ };
16
+ var END_REASONS = ["exit", "pagehide", "closed", "host_exit", ...HOST_EVENTS];
17
+ var ERROR_SOURCES = ["error", "unhandledrejection", "console", "manual"];
18
+ var isFatalSource = (s) => s === "error" || s === "unhandledrejection";
19
+ var DIAG_LIMITS = {
20
+ /** Characters of a message (log or error). */
21
+ message: 500,
22
+ /** Characters of a stack, after scrubbing. */
23
+ stack: 4e3,
24
+ /** Stack frames kept. */
25
+ frames: 20,
26
+ /** Characters of a source file path. */
27
+ file: 200,
28
+ /** UTF-8 bytes of a log's data as JSON. */
29
+ data: 2048,
30
+ /** Characters of a host event's detail. */
31
+ detail: 200,
32
+ /** Entries in one batch. */
33
+ batchEntries: 50,
34
+ /** UTF-8 bytes of one batch as JSON (the proxy reads at most 64 KB). */
35
+ batchBytes: 48 * 1024,
36
+ /** Error entries per session; the proxy keeps the same limit. */
37
+ sessionErrors: 100,
38
+ /** Log entries per session. */
39
+ sessionLogs: 1e3,
40
+ /** One error entry stands for at most this many identical errors. */
41
+ repeat: 1e3
42
+ };
43
+ var DIAG_BODY_MAX_BYTES = 64 * 1024;
44
+ var PERF_RANGES = { fps: [0, 240], jank: [0, 1e5], heap_mb: [0, 65536], ready_ms: [0, 6e5] };
45
+ var DEVICE_CLASSES = ["phone", "tablet", "desktop", "unknown"];
46
+ var OS_NAMES = ["ios", "android", "web", "unknown"];
47
+ var OS_VERSION = /^\d{1,3}(?:\.\d{1,3}){0,2}$/;
48
+ var APP_VERSION = /^(?:\d{1,3}\.\d{1,3}\.\d{1,4}|simulator)$/;
49
+ function launchContextProblem(c) {
50
+ if (!c || typeof c !== "object" || Array.isArray(c)) return "ctx is an object.";
51
+ const x = c;
52
+ for (const k of Object.keys(x)) if (!["device", "os", "os_version", "app_version"].includes(k)) return `ctx has no field ${k}.`;
53
+ if (!DEVICE_CLASSES.includes(x.device)) return "ctx.device is phone, tablet, desktop or unknown.";
54
+ if (!OS_NAMES.includes(x.os)) return "ctx.os is ios, android, web or unknown.";
55
+ if (x.os_version !== void 0 && (typeof x.os_version !== "string" || !OS_VERSION.test(x.os_version))) return "ctx.os_version is digits and dots.";
56
+ if (x.app_version !== void 0 && (typeof x.app_version !== "string" || !APP_VERSION.test(x.app_version))) return "ctx.app_version is x.y.z or simulator.";
57
+ return null;
58
+ }
59
+ function normaliseContext(c) {
60
+ const x = c && typeof c === "object" ? c : {};
61
+ return {
62
+ device: DEVICE_CLASSES.includes(x.device) ? x.device : "unknown",
63
+ os: OS_NAMES.includes(x.os) ? x.os : "unknown",
64
+ os_version: typeof x.os_version === "string" && OS_VERSION.test(x.os_version) ? x.os_version : null,
65
+ app_version: typeof x.app_version === "string" && APP_VERSION.test(x.app_version) ? x.app_version : null
66
+ };
67
+ }
68
+ var SCRUBS = [
69
+ // A URL keeps its path but loses its query and fragment: those carry ids, tokens and positions.
70
+ [/\b((?:https?|wss?):\/\/[^\s?#"'<>`]*)[?#][^\s"'<>`)]*/gi, "$1?[query]"],
71
+ // Bearer and basic credentials.
72
+ [/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [token]"],
73
+ // JWTs and JWS: three base64url parts, the first starting like a JSON header.
74
+ [/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g, "[token]"],
75
+ // Emails.
76
+ [/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}/g, "[email]"],
77
+ // key=value and "key": "value" where the key names a secret.
78
+ [/((?<![A-Za-z])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|passwd|pwd|auth|authorization|session|cookie|sig|signature)["']?\s*[:=]\s*["']?)(?!(?:bearer|basic)\b|\[)[^\s"'&,;}]{4,}/gi, "$1[secret]"],
79
+ // A latitude, longitude pair: two decimals with at least three places, in range.
80
+ [/(?<![\d.])-?(?:[1-8]?\d(?:\.\d{3,})|90\.0{3,})\s*[,;/ ]\s*-?(?:1[0-7]\d|0?\d?\d)\.\d{3,}(?![\d.])/g, "[location]"],
81
+ // Long opaque strings: 24 or more base64url or hex characters with letters and digits mixed in.
82
+ [/(?<![A-Za-z0-9_-])(?=[A-Za-z0-9_-]{24,})(?=[A-Za-z_-]*\d)(?=[0-9_-]*[A-Za-z])[A-Za-z0-9_-]{24,}(?![A-Za-z0-9_-])/g, "[key]"],
83
+ // Long digit runs (phone, card and account numbers, timestamps), with spaces or dashes inside.
84
+ [/(?<![\d.])\+?\d(?:[\s-]?\d){6,}(?![\d.])/g, "[number]"]
85
+ ];
86
+ function scrubText(s, extra = []) {
87
+ let out = s;
88
+ for (const secret of extra) if (secret && secret.length >= 8) out = out.split(secret).join("[token]");
89
+ for (const [re, to] of SCRUBS) out = out.replace(re, to);
90
+ return out;
91
+ }
92
+ var SECRET_KEY = /^(?:.*(?:token|secret|password|passwd|pwd|auth|cookie|session|signature|apikey|api_key|email|phone|wallet|address)|lat|lng|lon|long|latitude|longitude|coords?|position|location|ip)$/i;
93
+ function scrubData(value, extra = [], depth = 0) {
94
+ if (depth > 6) return "[deep]";
95
+ if (value === null || typeof value === "boolean") return value;
96
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
97
+ if (typeof value === "string") return scrubText(value.length > DIAG_LIMITS.message ? value.slice(0, DIAG_LIMITS.message) : value, extra);
98
+ if (Array.isArray(value)) return value.slice(0, 50).map((v) => scrubData(v, extra, depth + 1));
99
+ if (typeof value === "object") {
100
+ const out = {};
101
+ for (const [k, v] of Object.entries(value).slice(0, 50)) {
102
+ const key = scrubText(k.slice(0, 64), extra);
103
+ out[key] = SECRET_KEY.test(k) ? "[removed]" : scrubData(v, extra, depth + 1);
104
+ }
105
+ return out;
106
+ }
107
+ return null;
108
+ }
109
+ function boundData(value, extra = []) {
110
+ if (value === void 0) return void 0;
111
+ const clean = scrubData(value, extra);
112
+ let bytes = Infinity;
113
+ try {
114
+ bytes = new TextEncoder().encode(JSON.stringify(clean)).byteLength;
115
+ } catch {
116
+ return { truncated: true };
117
+ }
118
+ return bytes > DIAG_LIMITS.data ? { truncated: true, bytes } : clean;
119
+ }
120
+ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
121
+ function shortenGameUrls(s) {
122
+ return s.replace(
123
+ /https:\/\/[a-z0-9][a-z0-9-]{1,28}[a-z0-9]-[0-9a-f]{16}\.play\.seekprotocol\.ai(?:\/@[0-9a-f]{64})?(\/[^\s?#"'()<>`]*)([?#][^\s"'()<>`]*?)?(?=:\d+:\d+|[\s"'()<>`]|$)/g,
124
+ (_m, path, query) => `${path}${query ? "?[query]" : ""}`
125
+ );
126
+ }
127
+ var CHROME_FRAME = /^\s*at\s+(?:(.*?)\s+\()?(.+?):(\d+):(\d+)\)?\s*$/;
128
+ var GECKO_FRAME = /^\s*(.*?)@(.+?):(\d+):(\d+)\s*$/;
129
+ function parseStack(stack) {
130
+ if (!stack) return [];
131
+ const frames = [];
132
+ for (const raw of stack.split("\n")) {
133
+ const m = CHROME_FRAME.exec(raw) ?? GECKO_FRAME.exec(raw);
134
+ if (!m) continue;
135
+ frames.push({ fn: (m[1] ?? "").trim(), file: m[2].trim(), line: Number(m[3]), col: Number(m[4]) });
136
+ if (frames.length >= DIAG_LIMITS.frames) break;
137
+ }
138
+ return frames;
139
+ }
140
+ function normaliseMessage(message) {
141
+ return message.replace(/\[(?:email|token|key|secret|location|number|query)\]/g, "<x>").replace(/(["'`])(?:(?!\1).){0,200}\1/g, "<s>").replace(/\b0x[0-9a-f]+\b/gi, "<n>").replace(/\b[0-9a-f]{8,}\b/gi, "<n>").replace(/-?\d+(?:\.\d+)?/g, "<n>").replace(/\s+/g, " ").trim().toLowerCase().slice(0, 300);
142
+ }
143
+ var FINGERPRINT_FRAMES = 3;
144
+ var frameKey = (f) => `${shortenGameUrls(f.file).replace(/[?#].*$/, "")}:${f.fn || `line ${f.line ?? "?"}`}`;
145
+ var hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
146
+ async function sha256HexOf(s) {
147
+ return hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)));
148
+ }
149
+ function fingerprintInput(kind, message, stack, file, line) {
150
+ const frames = parseStack(stack).slice(0, FINGERPRINT_FRAMES).map(frameKey);
151
+ if (!frames.length && file) frames.push(`${shortenGameUrls(file)}:line ${line ?? "?"}`);
152
+ return [kind, normaliseMessage(message), ...frames].join("\n");
153
+ }
154
+ async function fingerprint(kind, message, stack, file, line) {
155
+ return (await sha256HexOf(fingerprintInput(kind, message, stack, file, line))).slice(0, 32);
156
+ }
157
+ async function sessionIdOf(appId, runId) {
158
+ return (await sha256HexOf(`seek-diag-session:v1:${appId}:${runId.toLowerCase()}`)).slice(0, 32);
159
+ }
160
+ var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
161
+ var txt = (v, max) => typeof v === "string" && v.length > 0 ? clip(v, max) : null;
162
+ var int = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? v : void 0;
163
+ function parseDiagnosticsBatch(body, opts = { from: "game" }) {
164
+ if (!isObj(body) || !Array.isArray(body.entries)) return null;
165
+ const secrets = opts.secrets ?? [];
166
+ const out = { entries: [], invalid: 0, hostOnly: 0, dropped: int(body.dropped, 0, 1e6) ?? 0 };
167
+ const list = body.entries.slice(0, DIAG_LIMITS.batchEntries);
168
+ out.invalid += body.entries.length - list.length;
169
+ for (const e of list) {
170
+ const entry = cleanDiagnosticEntry(e, secrets);
171
+ if (!entry) out.invalid++;
172
+ else if (entry.kind === "host" && opts.from !== "host") out.hostOnly++;
173
+ else out.entries.push(entry);
174
+ }
175
+ return out;
176
+ }
177
+ function cleanDiagnosticEntry(e, secrets = []) {
178
+ if (!isObj(e)) return null;
179
+ const at = typeof e.at === "number" && Number.isFinite(e.at) ? Math.round(e.at) : NaN;
180
+ if (!Number.isFinite(at)) return null;
181
+ const scrub = (s, max) => clip(scrubText(shortenGameUrls(s), secrets), max);
182
+ switch (e.kind) {
183
+ case "heartbeat": {
184
+ const fps = int(e.fps, ...PERF_RANGES.fps);
185
+ const jank = int(e.jank, ...PERF_RANGES.jank);
186
+ const heap = int(e.heap_mb, ...PERF_RANGES.heap_mb);
187
+ return { kind: "heartbeat", at, ...fps !== void 0 ? { fps } : {}, ...jank !== void 0 ? { jank } : {}, ...heap !== void 0 ? { heap_mb: heap } : {} };
188
+ }
189
+ case "start": {
190
+ const ready = int(e.ready_ms, ...PERF_RANGES.ready_ms);
191
+ return { kind: "start", at, ...ready !== void 0 ? { ready_ms: ready } : {} };
192
+ }
193
+ case "end":
194
+ return END_REASONS.includes(e.reason) ? { kind: "end", at, reason: e.reason } : null;
195
+ case "host": {
196
+ if (!isHostEvent(e.event)) return null;
197
+ const detail = txt(e.detail, DIAG_LIMITS.detail);
198
+ return { kind: "host", at, event: e.event, ...detail ? { detail: scrub(detail, DIAG_LIMITS.detail) } : {} };
199
+ }
200
+ case "log": {
201
+ const message = txt(e.message, DIAG_LIMITS.message * 2);
202
+ if (!isLogLevel(e.level) || message === null) return null;
203
+ const data = e.data === void 0 ? void 0 : boundData(e.data, secrets);
204
+ return {
205
+ kind: "log",
206
+ at,
207
+ level: e.level,
208
+ message: scrub(message, DIAG_LIMITS.message),
209
+ ...data !== void 0 ? { data } : {},
210
+ source: e.source === "console" ? "console" : "log"
211
+ };
212
+ }
213
+ case "error": {
214
+ const message = txt(e.message, DIAG_LIMITS.message * 2) ?? "Unknown error";
215
+ if (!ERROR_SOURCES.includes(e.source)) return null;
216
+ const stack = txt(e.stack, DIAG_LIMITS.stack * 2);
217
+ const file = txt(e.file, DIAG_LIMITS.file * 2);
218
+ const line = int(e.line, 0, 1e7);
219
+ const col = int(e.col, 0, 1e7);
220
+ const count = int(e.count, 1, DIAG_LIMITS.repeat);
221
+ return {
222
+ kind: "error",
223
+ at,
224
+ source: e.source,
225
+ message: scrub(message, DIAG_LIMITS.message),
226
+ ...stack ? { stack: scrub(stack, DIAG_LIMITS.stack) } : {},
227
+ ...file ? { file: scrub(file, DIAG_LIMITS.file) } : {},
228
+ ...line !== void 0 ? { line } : {},
229
+ ...col !== void 0 ? { col } : {},
230
+ ...count !== void 0 && count > 1 ? { count } : {}
231
+ };
232
+ }
233
+ default:
234
+ return null;
235
+ }
236
+ }
237
+ var isCrashEntry = (e) => e.kind === "host" || e.kind === "error" && isFatalSource(e.source);
238
+ function createHostWatchdog(opts) {
239
+ const now = opts.now ?? (() => Date.now());
240
+ const setT = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
241
+ const clearT = opts.clearTimer ?? ((t) => clearTimeout(t));
242
+ const readyMs = opts.readyTimeoutMs ?? HOST_READY_TIMEOUT_MS;
243
+ const hangMs = opts.hangMs ?? HOST_HANG_MS;
244
+ let timer = null;
245
+ let isReady = false;
246
+ let paused = false;
247
+ let done = false;
248
+ const arm = (ms, event) => {
249
+ if (timer !== null) clearT(timer);
250
+ timer = done || paused ? null : setT(() => report(event), ms);
251
+ };
252
+ const report = (event, detail) => {
253
+ if (done) return;
254
+ done = true;
255
+ if (timer !== null) clearT(timer);
256
+ timer = null;
257
+ opts.onCrash({ kind: "host", at: now(), event, ...detail ? { detail: clip(scrubText(detail), DIAG_LIMITS.detail) } : {} });
258
+ };
259
+ arm(readyMs, "no_ready");
260
+ return {
261
+ ready() {
262
+ if (!isReady && !done) {
263
+ isReady = true;
264
+ arm(hangMs, "hang");
265
+ }
266
+ },
267
+ beat() {
268
+ if (isReady && !done) arm(hangMs, "hang");
269
+ },
270
+ pause() {
271
+ paused = true;
272
+ if (timer !== null) clearT(timer);
273
+ timer = null;
274
+ },
275
+ resume() {
276
+ paused = false;
277
+ if (!done) arm(isReady ? hangMs : readyMs, isReady ? "hang" : "no_ready");
278
+ },
279
+ fail(event, detail) {
280
+ report(event, detail);
281
+ },
282
+ stop() {
283
+ done = true;
284
+ if (timer !== null) clearT(timer);
285
+ timer = null;
286
+ }
287
+ };
288
+ }
289
+
290
+ // src/protocol.ts
291
+ var PROTOCOL = "seek-game";
292
+ var PROTOCOL_VERSION = 1;
293
+ var BRIDGE_VERSION = "0.1.0";
294
+ var MAX_MESSAGE_BYTES = 96 * 1024;
295
+ var STORAGE_MAX_BYTES = 64 * 1024;
296
+ var STORAGE_KEY = /^[A-Za-z0-9_.-]{1,64}$/;
297
+ var TRACK_EVENT = /^[a-z][a-z0-9_]{0,31}$/;
298
+ var TRACK_PROP_KEY = /^[a-z][a-z0-9_]{0,31}$/;
299
+ var TRACK_MAX_PROPS = 8;
300
+ var TRACK_MAX_STRING = 64;
301
+ var MAX_TOKEN_LENGTH = 4096;
302
+ var PROXY_ORIGIN = "https://api.play.seekprotocol.ai";
303
+ var DEFAULT_HOST_ORIGINS = ["https://storeconnect.seekprotocol.ai"];
304
+ var utf8Bytes = (s) => new TextEncoder().encode(s).byteLength;
305
+ function jsonBytes(value) {
306
+ try {
307
+ const s = JSON.stringify(value);
308
+ return s === void 0 ? Infinity : utf8Bytes(s);
309
+ } catch {
310
+ return Infinity;
311
+ }
312
+ }
313
+ function isJsonValue(v, depth = 0) {
314
+ if (depth > 32) return false;
315
+ if (v === null || typeof v === "string" || typeof v === "boolean") return true;
316
+ if (typeof v === "number") return Number.isFinite(v);
317
+ if (Array.isArray(v)) return v.every((x) => isJsonValue(x, depth + 1));
318
+ if (typeof v === "object") {
319
+ const proto = Object.getPrototypeOf(v);
320
+ return (proto === Object.prototype || proto === null) && Object.values(v).every((x) => isJsonValue(x, depth + 1));
321
+ }
322
+ return false;
323
+ }
324
+ var storageKeyProblem = (key) => typeof key === "string" && STORAGE_KEY.test(key) ? null : "A storage key is 1 to 64 letters, digits, dots, hyphens and underscores.";
325
+ function trackProblem(event, props) {
326
+ if (typeof event !== "string" || !TRACK_EVENT.test(event)) return "An event name is 1 to 32 lowercase letters, digits and underscores, starting with a letter.";
327
+ if (props === void 0) return null;
328
+ if (!props || typeof props !== "object" || Array.isArray(props)) return "Props are an object.";
329
+ const entries = Object.entries(props);
330
+ if (entries.length > TRACK_MAX_PROPS) return `At most ${TRACK_MAX_PROPS} props.`;
331
+ for (const [k, v] of entries) {
332
+ if (!TRACK_PROP_KEY.test(k)) return `Prop "${k}": names are lowercase letters, digits and underscores.`;
333
+ if (typeof v === "string") {
334
+ if (v.length > TRACK_MAX_STRING) return `Prop "${k}": text is at most ${TRACK_MAX_STRING} characters.`;
335
+ } else if (typeof v === "number") {
336
+ if (!Number.isFinite(v)) return `Prop "${k}": numbers are finite.`;
337
+ } else if (typeof v !== "boolean") {
338
+ return `Prop "${k}": a prop is text, a number or true/false.`;
339
+ }
340
+ }
341
+ return null;
342
+ }
343
+ function finishProblem(r) {
344
+ const x = r;
345
+ if (!x || typeof x !== "object") return "finish takes { score, seconds }.";
346
+ if (typeof x.score !== "number" || !Number.isFinite(x.score) || x.score < 0 || x.score > 1e9) return "score is a number from 0 to 1,000,000,000.";
347
+ if (typeof x.seconds !== "number" || !Number.isFinite(x.seconds) || x.seconds < 0 || x.seconds > 86400) return "seconds is a number from 0 to 86,400.";
348
+ return null;
349
+ }
350
+ var isObj2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
351
+ function parseHostMessage(data) {
352
+ let m = data;
353
+ if (typeof data === "string") {
354
+ if (data.length > MAX_MESSAGE_BYTES || utf8Bytes(data) > MAX_MESSAGE_BYTES) return null;
355
+ try {
356
+ m = JSON.parse(data);
357
+ } catch {
358
+ return null;
359
+ }
360
+ } else if (jsonBytes(data) > MAX_MESSAGE_BYTES) {
361
+ return null;
362
+ }
363
+ if (!isObj2(m) || m.seek !== PROTOCOL || m.v !== PROTOCOL_VERSION) return null;
364
+ if (typeof m.event === "string") {
365
+ return m.event === "pause" || m.event === "resume" || m.event === "exit" ? { seek: PROTOCOL, v: PROTOCOL_VERSION, event: m.event } : null;
366
+ }
367
+ if (typeof m.re !== "string" || m.re.length > 64 || typeof m.ok !== "boolean") return null;
368
+ if (m.ok) return { seek: PROTOCOL, v: PROTOCOL_VERSION, re: m.re, ok: true, payload: m.payload };
369
+ const e = isObj2(m.error) ? m.error : {};
370
+ return {
371
+ seek: PROTOCOL,
372
+ v: PROTOCOL_VERSION,
373
+ re: m.re,
374
+ ok: false,
375
+ error: { code: typeof e.code === "string" ? e.code.slice(0, 64) : "host_error", message: typeof e.message === "string" ? e.message.slice(0, 500) : "The host refused." }
376
+ };
377
+ }
378
+ function parseGameRequest(data) {
379
+ let m = data;
380
+ if (typeof data === "string") {
381
+ if (utf8Bytes(data) > MAX_MESSAGE_BYTES) return null;
382
+ try {
383
+ m = JSON.parse(data);
384
+ } catch {
385
+ return null;
386
+ }
387
+ } else if (jsonBytes(data) > MAX_MESSAGE_BYTES) {
388
+ return null;
389
+ }
390
+ if (!isObj2(m) || m.seek !== PROTOCOL || m.v !== PROTOCOL_VERSION || typeof m.id !== "string" || !m.id || m.id.length > 64) return null;
391
+ const types = ["ready", "finish", "storage.get", "storage.set", "storage.remove", "track", "heartbeat", "diagnostics"];
392
+ if (!types.includes(m.type)) return null;
393
+ const p = m.payload;
394
+ switch (m.type) {
395
+ case "finish":
396
+ if (finishProblem(p)) return null;
397
+ break;
398
+ case "storage.get":
399
+ case "storage.remove":
400
+ if (!isObj2(p) || storageKeyProblem(p.key)) return null;
401
+ break;
402
+ case "storage.set":
403
+ if (!isObj2(p) || storageKeyProblem(p.key) || !isJsonValue(p.value)) return null;
404
+ break;
405
+ case "track":
406
+ if (!isObj2(p) || trackProblem(p.event, p.props)) return null;
407
+ break;
408
+ case "heartbeat":
409
+ if (!isObj2(p) || p.ended !== void 0 && typeof p.ended !== "boolean") return null;
410
+ break;
411
+ case "diagnostics": {
412
+ const batch = parseDiagnosticsBatch(p, { from: "game" });
413
+ if (!batch || batch.hostOnly) return null;
414
+ return { seek: PROTOCOL, v: PROTOCOL_VERSION, id: m.id, type: "diagnostics", payload: { entries: batch.entries, ...batch.dropped ? { dropped: batch.dropped } : {} } };
415
+ }
416
+ case "ready":
417
+ break;
418
+ }
419
+ return { seek: PROTOCOL, v: PROTOCOL_VERSION, id: m.id, type: m.type, payload: p };
420
+ }
421
+ var fromB64url = (s) => {
422
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
423
+ return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)));
424
+ };
425
+ function decodeLaunchToken(token) {
426
+ if (typeof token !== "string" || token.length > MAX_TOKEN_LENGTH) return null;
427
+ const parts = token.split(".");
428
+ if (parts.length !== 3 || !parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p))) return null;
429
+ let c;
430
+ try {
431
+ c = JSON.parse(fromB64url(parts[1]));
432
+ } catch {
433
+ return null;
434
+ }
435
+ if (!isObj2(c)) return null;
436
+ const { run, seed, lang, sub, game, version, exp } = c;
437
+ if (typeof run !== "string" || typeof sub !== "string" || typeof game !== "string" || typeof version !== "string" || typeof lang !== "string") return null;
438
+ if (typeof seed !== "number" || !Number.isSafeInteger(seed) || typeof exp !== "number") return null;
439
+ return { runId: run, seed, lang, sub, game, version, expiresAt: exp * 1e3 };
440
+ }
441
+
442
+ // src/collector.ts
443
+ var PRE_READY_ENTRIES = 20;
444
+ function createCollector(opts) {
445
+ const now = opts.now ?? (() => Date.now());
446
+ const timers = opts.timers ?? {
447
+ setInterval: (fn, ms) => setInterval(fn, ms),
448
+ clearInterval: (t) => clearInterval(t)
449
+ };
450
+ const flushMs = Math.max(1e3, opts.flushMs ?? 5e3);
451
+ const heartbeatMs = Math.min(25e3, Math.max(5e3, opts.heartbeatMs ?? 1e4));
452
+ const rate = opts.rate ?? { perSecond: 5, burst: 30 };
453
+ const secrets = () => opts.secrets?.() ?? [];
454
+ let session = null;
455
+ let ended = false;
456
+ let queue = [];
457
+ let dropped = 0;
458
+ let droppedTotal = 0;
459
+ let sent = 0;
460
+ let batches = 0;
461
+ let seq = 0;
462
+ let errors = 0;
463
+ let logs = 0;
464
+ let tokens = rate.burst;
465
+ let lastRefill = now();
466
+ let flushTimer = null;
467
+ let beatTimer = null;
468
+ const drop = () => {
469
+ dropped++;
470
+ droppedTotal++;
471
+ return false;
472
+ };
473
+ const allow = () => {
474
+ const t = now();
475
+ tokens = Math.min(rate.burst, tokens + (t - lastRefill) / 1e3 * rate.perSecond);
476
+ lastRefill = t;
477
+ if (tokens < 1) return false;
478
+ tokens -= 1;
479
+ return true;
480
+ };
481
+ const push = (raw, limited = true) => {
482
+ if (ended) return false;
483
+ if (limited && !allow()) return drop();
484
+ const entry = cleanDiagnosticEntry(raw, secrets());
485
+ if (!entry) return drop();
486
+ if (!session && queue.length >= PRE_READY_ENTRIES) return drop();
487
+ queue.push(entry);
488
+ if (session && queue.length >= DIAG_LIMITS.batchEntries) void flush();
489
+ return true;
490
+ };
491
+ const bytes = (b) => new TextEncoder().encode(JSON.stringify(b)).byteLength;
492
+ async function flush(final = false) {
493
+ if (!session || !queue.length && !dropped) return;
494
+ const pending = queue;
495
+ queue = [];
496
+ while (pending.length || dropped) {
497
+ const batch = { session, seq: ++seq, entries: [], ...dropped ? { dropped } : {} };
498
+ dropped = 0;
499
+ while (pending.length && batch.entries.length < DIAG_LIMITS.batchEntries) {
500
+ batch.entries.push(pending[0]);
501
+ if (batch.entries.length > 1 && bytes(batch) > DIAG_LIMITS.batchBytes) {
502
+ batch.entries.pop();
503
+ break;
504
+ }
505
+ pending.shift();
506
+ }
507
+ batches++;
508
+ sent += batch.entries.length;
509
+ try {
510
+ await opts.send(batch, final && !pending.length);
511
+ } catch {
512
+ }
513
+ }
514
+ }
515
+ const collector = {
516
+ get session() {
517
+ return session;
518
+ },
519
+ get ended() {
520
+ return ended;
521
+ },
522
+ start(id, readyMs) {
523
+ if (session || ended) return;
524
+ session = id;
525
+ const start = cleanDiagnosticEntry({ kind: "start", at: now(), ...readyMs !== void 0 ? { ready_ms: readyMs } : {} });
526
+ queue.unshift(start ?? { kind: "start", at: now() });
527
+ void flush();
528
+ flushTimer = timers.setInterval(() => void flush(), flushMs);
529
+ beatTimer = timers.setInterval(() => {
530
+ if (ended) return;
531
+ let sample = {};
532
+ try {
533
+ sample = opts.sample?.() ?? {};
534
+ } catch {
535
+ }
536
+ queue.push(cleanDiagnosticEntry({ ...sample, kind: "heartbeat", at: now() }) ?? { kind: "heartbeat", at: now() });
537
+ try {
538
+ opts.beat?.(false);
539
+ } catch {
540
+ }
541
+ }, heartbeatMs);
542
+ try {
543
+ opts.beat?.(false);
544
+ } catch {
545
+ }
546
+ },
547
+ error(e) {
548
+ if (ended) return false;
549
+ if (errors >= DIAG_LIMITS.sessionErrors) return drop();
550
+ const entry = cleanDiagnosticEntry({ kind: "error", at: now(), ...e }, secrets());
551
+ if (!entry || entry.kind !== "error") return drop();
552
+ const same = queue.find((q) => q.kind === "error" && q.source === entry.source && q.message === entry.message && q.stack === entry.stack);
553
+ if (same && (same.count ?? 1) < DIAG_LIMITS.repeat) {
554
+ same.count = (same.count ?? 1) + 1;
555
+ return true;
556
+ }
557
+ if (!push(entry)) return false;
558
+ errors++;
559
+ return true;
560
+ },
561
+ log(level, message, data, source = "log") {
562
+ if (logs >= DIAG_LIMITS.sessionLogs) return drop();
563
+ if (!push({ kind: "log", at: now(), level, message: typeof message === "string" && message ? message : String(message || "(empty)"), ...data !== void 0 ? { data } : {}, source })) return false;
564
+ logs++;
565
+ return true;
566
+ },
567
+ async end(reason) {
568
+ if (ended) return;
569
+ if (session) queue.push({ kind: "end", at: now(), reason });
570
+ ended = true;
571
+ if (flushTimer !== null) timers.clearInterval(flushTimer);
572
+ if (beatTimer !== null) timers.clearInterval(beatTimer);
573
+ flushTimer = beatTimer = null;
574
+ try {
575
+ opts.beat?.(true);
576
+ } catch {
577
+ }
578
+ await flush(true);
579
+ },
580
+ flush,
581
+ stats: () => ({ sent, dropped: droppedTotal, errors, logs, batches })
582
+ };
583
+ return collector;
584
+ }
585
+
586
+ // src/perf.ts
587
+ var JANK_FRAME_MS = 50;
588
+ var PAUSE_MS = 1e3;
589
+ function createPerfSampler(win) {
590
+ const raf = win.requestAnimationFrame?.bind(win);
591
+ const perf = win.performance;
592
+ let frames = 0;
593
+ let jank = 0;
594
+ let visibleMs = 0;
595
+ let last = null;
596
+ let id = null;
597
+ let stopped = false;
598
+ const tick = (t) => {
599
+ if (stopped) return;
600
+ if (last !== null && !win.document?.hidden) {
601
+ const gap = t - last;
602
+ if (gap < PAUSE_MS) {
603
+ frames++;
604
+ visibleMs += gap;
605
+ if (gap > JANK_FRAME_MS) jank++;
606
+ }
607
+ }
608
+ last = t;
609
+ id = raf(tick);
610
+ };
611
+ if (raf) id = raf(tick);
612
+ return {
613
+ sample() {
614
+ const out = {};
615
+ if (visibleMs >= 500) {
616
+ out.fps = Math.min(240, Math.round(frames * 1e3 / visibleMs));
617
+ out.jank = jank;
618
+ }
619
+ const heap = perf?.memory?.usedJSHeapSize;
620
+ if (typeof heap === "number" && Number.isFinite(heap) && heap > 0) out.heap_mb = Math.round(heap / (1024 * 1024));
621
+ frames = 0;
622
+ jank = 0;
623
+ visibleMs = 0;
624
+ return out;
625
+ },
626
+ sinceLoad() {
627
+ const now = perf?.now();
628
+ return typeof now === "number" && Number.isFinite(now) ? Math.round(now) : void 0;
629
+ },
630
+ stop() {
631
+ stopped = true;
632
+ if (id !== null) win.cancelAnimationFrame?.(id);
633
+ }
634
+ };
635
+ }
636
+
637
+ // src/bridge.ts
638
+ var BridgeError = class extends Error {
639
+ constructor(code, message) {
640
+ super(message);
641
+ this.code = code;
642
+ this.name = "BridgeError";
643
+ }
644
+ };
645
+ var TRACK_PER_LAUNCH = 200;
646
+ function createBridge(opts = {}) {
647
+ const win = opts.window ?? globalThis;
648
+ const hostOrigins = [...opts.hostOrigins ?? DEFAULT_HOST_ORIGINS];
649
+ const doFetch = opts.fetch ?? ((...a) => fetch(...a));
650
+ const timeoutMs = opts.timeoutMs ?? 1e4;
651
+ const native = typeof win.ReactNativeWebView?.postMessage === "function";
652
+ const parent = !native && win.parent && win.parent !== win ? win.parent : null;
653
+ let seq = 0;
654
+ let lockedOrigin = null;
655
+ let readyPromise = null;
656
+ let launchInfo = null;
657
+ let token = "";
658
+ let transport = "proxy";
659
+ let finished = false;
660
+ let tracked = 0;
661
+ let closed = false;
662
+ const pending = /* @__PURE__ */ new Map();
663
+ const handlers = { pause: /* @__PURE__ */ new Set(), resume: /* @__PURE__ */ new Set(), exit: /* @__PURE__ */ new Set() };
664
+ const vopts = opts.vitals === false ? null : opts.vitals ?? {};
665
+ let vitals = null;
666
+ const trusted = (e) => {
667
+ if (native) return e.source === null || e.source === void 0;
668
+ if (!parent || e.source !== parent) return false;
669
+ return lockedOrigin ? e.origin === lockedOrigin : hostOrigins.includes(e.origin);
670
+ };
671
+ const onMessage = (e) => {
672
+ if (closed || !trusted(e)) return;
673
+ const m = parseHostMessage(e.data);
674
+ if (!m) return;
675
+ if ("event" in m) {
676
+ if (m.event === "exit") void vitals?.end("host_exit");
677
+ for (const fn of handlers[m.event]) {
678
+ try {
679
+ fn();
680
+ } catch {
681
+ }
682
+ }
683
+ return;
684
+ }
685
+ const p = pending.get(m.re);
686
+ if (!p) return;
687
+ if (!native && !lockedOrigin) lockedOrigin = e.origin;
688
+ pending.delete(m.re);
689
+ clearTimeout(p.timer);
690
+ if (m.ok) p.resolve(m.payload);
691
+ else p.reject(new BridgeError(m.error.code, m.error.message));
692
+ };
693
+ win.addEventListener("message", onMessage);
694
+ if (native) win.document?.addEventListener("message", onMessage);
695
+ function post(type, payload, wait = true, ms = timeoutMs) {
696
+ if (closed) return Promise.reject(new BridgeError("closed", "The bridge is closed."));
697
+ if (!native && !parent) return Promise.reject(new BridgeError("no_host", "Not running inside Seek or the simulator."));
698
+ const msg = { seek: PROTOCOL, v: PROTOCOL_VERSION, id: `g${++seq}-${Math.random().toString(36).slice(2, 10)}`, type, payload };
699
+ const json = JSON.stringify(msg);
700
+ if (utf8Bytes(json) > MAX_MESSAGE_BYTES) return Promise.reject(new BridgeError("too_big", `A message is at most ${MAX_MESSAGE_BYTES} bytes.`));
701
+ const sent = new Promise((resolve, reject) => {
702
+ if (!wait) return resolve(void 0);
703
+ const timer = setTimeout(() => {
704
+ pending.delete(msg.id);
705
+ reject(new BridgeError("timeout", `The host did not answer ${type}.`));
706
+ }, ms);
707
+ pending.set(msg.id, { resolve, reject, timer });
708
+ });
709
+ if (native) win.ReactNativeWebView.postMessage(json);
710
+ else if (lockedOrigin) parent.postMessage(msg, lockedOrigin);
711
+ else for (const origin of hostOrigins) parent.postMessage(msg, origin);
712
+ return sent;
713
+ }
714
+ const needLaunch = () => {
715
+ if (!launchInfo) throw new BridgeError("not_ready", "Call ready() first.");
716
+ return launchInfo;
717
+ };
718
+ async function proxy(method, path, body) {
719
+ let res;
720
+ try {
721
+ res = await doFetch(`${PROXY_ORIGIN}${path}`, {
722
+ method,
723
+ headers: { authorization: `Bearer ${token}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
724
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
725
+ credentials: "omit",
726
+ mode: "cors",
727
+ ...method === "POST" ? { keepalive: true } : {}
728
+ });
729
+ } catch {
730
+ throw new BridgeError("offline", "Could not reach Seek.");
731
+ }
732
+ let data = {};
733
+ try {
734
+ data = await res.json();
735
+ } catch {
736
+ }
737
+ if (!res.ok) throw new BridgeError(typeof data.error === "string" ? data.error : `http_${res.status}`, typeof data.message === "string" ? data.message : "Seek refused.");
738
+ return data;
739
+ }
740
+ const sendBatch = (batch) => transport === "host" ? post("diagnostics", batch, false) : proxy("POST", "/v1/diagnostics", batch);
741
+ const perf = vopts && vopts.performance !== false ? createPerfSampler(win) : null;
742
+ if (vopts) {
743
+ vitals = createCollector({
744
+ ...perf ? { sample: () => perf.sample() } : {},
745
+ send: (batch) => sendBatch(batch),
746
+ beat: (ended) => {
747
+ if (!closed) post("heartbeat", ended ? { ended: true } : {}, false).catch(() => void 0);
748
+ },
749
+ secrets: () => token ? [token] : [],
750
+ ...vopts.now ? { now: vopts.now } : {},
751
+ ...vopts.timers ? { timers: vopts.timers } : {},
752
+ ...vopts.heartbeatSeconds ? { heartbeatMs: vopts.heartbeatSeconds * 1e3 } : {},
753
+ ...vopts.flushSeconds ? { flushMs: vopts.flushSeconds * 1e3 } : {}
754
+ });
755
+ }
756
+ const errorOf = (v) => {
757
+ if (v && typeof v === "object" && typeof v.message === "string") {
758
+ const e = v;
759
+ return { message: `${e.name && e.name !== "Error" ? `${e.name}: ` : ""}${e.message || "Error"}`, ...typeof e.stack === "string" ? { stack: e.stack } : {} };
760
+ }
761
+ if (typeof v === "string") return { message: v || "Error" };
762
+ try {
763
+ return { message: JSON.stringify(v)?.slice(0, 500) ?? String(v) };
764
+ } catch {
765
+ return { message: String(v) };
766
+ }
767
+ };
768
+ const onError = (e) => {
769
+ if (!vitals || !e || "data" in e || typeof e.message !== "string") return;
770
+ const err = e.error ? errorOf(e.error) : { message: e.message || "Script error" };
771
+ vitals.error({
772
+ source: "error",
773
+ message: err.message || e.message,
774
+ ...err.stack ? { stack: err.stack } : {},
775
+ ...typeof e.filename === "string" && e.filename ? { file: e.filename } : {},
776
+ ...Number.isInteger(e.lineno) ? { line: e.lineno } : {},
777
+ ...Number.isInteger(e.colno) ? { col: e.colno } : {}
778
+ });
779
+ };
780
+ const onRejection = (e) => {
781
+ if (!vitals || !e || "data" in e || !("reason" in e)) return;
782
+ const err = errorOf(e.reason);
783
+ vitals.error({ source: "unhandledrejection", message: `Unhandled rejection: ${err.message}`, ...err.stack ? { stack: err.stack } : {} });
784
+ };
785
+ const onPageHide = (e) => {
786
+ if (!vitals || !e || "data" in e || e.type !== "pagehide") return;
787
+ void vitals.end("pagehide");
788
+ };
789
+ if (vitals && vopts?.errors !== false) {
790
+ win.addEventListener("error", onError);
791
+ win.addEventListener("unhandledrejection", onRejection);
792
+ }
793
+ if (vitals) win.addEventListener("pagehide", onPageHide);
794
+ const restoreConsole = [];
795
+ if (vitals && vopts?.console) {
796
+ const target = vopts.consoleObject ?? globalThis.console;
797
+ const wanted = new Set(vopts.console === true ? LOG_LEVELS : vopts.console.filter(isLogLevel));
798
+ const levelOf = { debug: "debug", log: "info", info: "info", warn: "warn", error: "error" };
799
+ let inside = false;
800
+ for (const method of Object.keys(levelOf)) {
801
+ const original = target?.[method];
802
+ if (!target || typeof original !== "function" || !wanted.has(levelOf[method])) continue;
803
+ target[method] = (...args) => {
804
+ original.apply(target, args);
805
+ if (inside) return;
806
+ inside = true;
807
+ try {
808
+ const parts = args.map((a) => typeof a === "string" ? a : errorOf(a).message);
809
+ const err = args.find((a) => a instanceof Error);
810
+ if (method === "error" && err) vitals.error({ source: "console", ...errorOf(err), message: parts.join(" ") });
811
+ else vitals.log(levelOf[method], parts.join(" "), void 0, "console");
812
+ } catch {
813
+ } finally {
814
+ inside = false;
815
+ }
816
+ };
817
+ restoreConsole.push(() => {
818
+ target[method] = original;
819
+ });
820
+ }
821
+ }
822
+ const bridge = {
823
+ ready() {
824
+ if (readyPromise) return readyPromise;
825
+ readyPromise = post("ready", { bridge: BRIDGE_VERSION, protocol: PROTOCOL_VERSION }).then((payload) => {
826
+ const p = payload;
827
+ const info = decodeLaunchToken(p?.token);
828
+ if (!info) throw new BridgeError("bad_launch", "The host sent no valid launch token.");
829
+ token = p.token;
830
+ transport = p.transport === "host" ? "host" : "proxy";
831
+ launchInfo = info;
832
+ if (vitals && !vitals.session) {
833
+ const readyMs = perf?.sinceLoad();
834
+ void sessionIdOf(info.game, info.runId).then((id) => vitals?.start(id, readyMs)).catch(() => void 0);
835
+ }
836
+ return info;
837
+ });
838
+ readyPromise.catch(() => {
839
+ readyPromise = null;
840
+ });
841
+ return readyPromise;
842
+ },
843
+ launch: () => ({ ...needLaunch() }),
844
+ async finish(result) {
845
+ needLaunch();
846
+ const problem = finishProblem(result);
847
+ if (problem) throw new BridgeError("bad_finish", problem);
848
+ if (finished) throw new BridgeError("already_finished", "A run is finished once.");
849
+ finished = true;
850
+ const answer = await post("finish", { score: result.score, seconds: result.seconds }, true, Math.max(timeoutMs, 3e4));
851
+ return { accepted: answer?.accepted === true };
852
+ },
853
+ storage: {
854
+ async get(key) {
855
+ needLaunch();
856
+ const problem = storageKeyProblem(key);
857
+ if (problem) throw new BridgeError("bad_key", problem);
858
+ const data = transport === "host" ? await post("storage.get", { key }) : await proxy("GET", `/v1/storage/${encodeURIComponent(key)}`);
859
+ return data?.value ?? null;
860
+ },
861
+ async set(key, value) {
862
+ needLaunch();
863
+ const problem = storageKeyProblem(key);
864
+ if (problem) throw new BridgeError("bad_key", problem);
865
+ if (!isJsonValue(value)) throw new BridgeError("bad_value", "Storage holds plain JSON: objects, arrays, text, finite numbers, true/false and null.");
866
+ if (jsonBytes(value) > STORAGE_MAX_BYTES) throw new BridgeError("storage_full", `A game keeps at most ${STORAGE_MAX_BYTES} bytes per player.`);
867
+ if (transport === "host") await post("storage.set", { key, value });
868
+ else await proxy("PUT", `/v1/storage/${encodeURIComponent(key)}`, { value });
869
+ },
870
+ async remove(key) {
871
+ needLaunch();
872
+ const problem = storageKeyProblem(key);
873
+ if (problem) throw new BridgeError("bad_key", problem);
874
+ if (transport === "host") await post("storage.remove", { key });
875
+ else await proxy("DELETE", `/v1/storage/${encodeURIComponent(key)}`);
876
+ }
877
+ },
878
+ track(event, props) {
879
+ if (!launchInfo || closed || tracked >= TRACK_PER_LAUNCH || trackProblem(event, props)) return false;
880
+ tracked++;
881
+ const body = { event, ...props ? { props } : {} };
882
+ const sent = transport === "host" ? post("track", body, false) : proxy("POST", "/v1/track", body);
883
+ sent.catch(() => {
884
+ });
885
+ return true;
886
+ },
887
+ log(level, message, data) {
888
+ if (!vitals || !isLogLevel(level)) return false;
889
+ return vitals.log(level, typeof message === "string" ? message : String(message), data);
890
+ },
891
+ async exit() {
892
+ await vitals?.end("exit");
893
+ },
894
+ sessionId: () => vitals?.session ?? null,
895
+ on(event, fn) {
896
+ handlers[event].add(fn);
897
+ return () => handlers[event].delete(fn);
898
+ },
899
+ close() {
900
+ if (closed) return;
901
+ void vitals?.end("closed");
902
+ perf?.stop();
903
+ closed = true;
904
+ win.removeEventListener("message", onMessage);
905
+ win.removeEventListener("error", onError);
906
+ win.removeEventListener("unhandledrejection", onRejection);
907
+ win.removeEventListener("pagehide", onPageHide);
908
+ for (const restore of restoreConsole) restore();
909
+ if (native) win.document?.removeEventListener("message", onMessage);
910
+ for (const [id, p] of pending) {
911
+ clearTimeout(p.timer);
912
+ p.reject(new BridgeError("closed", "The bridge is closed."));
913
+ pending.delete(id);
914
+ }
915
+ }
916
+ };
917
+ return bridge;
918
+ }
919
+ export {
920
+ BRIDGE_VERSION,
921
+ BridgeError,
922
+ DEFAULT_HOST_ORIGINS,
923
+ DEVICE_CLASSES,
924
+ DIAG_BODY_MAX_BYTES,
925
+ DIAG_LIMITS,
926
+ END_REASONS,
927
+ ERROR_SOURCES,
928
+ FINGERPRINT_FRAMES,
929
+ HOST_EVENTS,
930
+ HOST_EVENT_TEXT,
931
+ HOST_HANG_MS,
932
+ HOST_READY_TIMEOUT_MS,
933
+ JANK_FRAME_MS,
934
+ LOG_LEVELS,
935
+ MAX_MESSAGE_BYTES,
936
+ MAX_TOKEN_LENGTH,
937
+ OS_NAMES,
938
+ PERF_RANGES,
939
+ PRE_READY_ENTRIES,
940
+ PROTOCOL,
941
+ PROTOCOL_VERSION,
942
+ PROXY_ORIGIN,
943
+ STORAGE_KEY,
944
+ STORAGE_MAX_BYTES,
945
+ TRACK_EVENT,
946
+ TRACK_MAX_PROPS,
947
+ TRACK_MAX_STRING,
948
+ TRACK_PER_LAUNCH,
949
+ TRACK_PROP_KEY,
950
+ boundData,
951
+ cleanDiagnosticEntry,
952
+ createBridge,
953
+ createCollector,
954
+ createHostWatchdog,
955
+ createPerfSampler,
956
+ decodeLaunchToken,
957
+ fingerprint,
958
+ fingerprintInput,
959
+ finishProblem,
960
+ frameKey,
961
+ isCrashEntry,
962
+ isFatalSource,
963
+ isHostEvent,
964
+ isJsonValue,
965
+ isLogLevel,
966
+ jsonBytes,
967
+ launchContextProblem,
968
+ normaliseContext,
969
+ normaliseMessage,
970
+ parseDiagnosticsBatch,
971
+ parseGameRequest,
972
+ parseHostMessage,
973
+ parseStack,
974
+ scrubData,
975
+ scrubText,
976
+ sessionIdOf,
977
+ sha256HexOf,
978
+ shortenGameUrls,
979
+ storageKeyProblem,
980
+ trackProblem,
981
+ utf8Bytes
982
+ };