wowdump 0.0.0 → 0.2.1

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/adapters.js +101 -0
  4. package/dist/agent.js +1335 -0
  5. package/dist/analysis-path.js +38 -0
  6. package/dist/analysis-process-log.js +146 -0
  7. package/dist/broker-client.js +411 -0
  8. package/dist/broker-codec.js +148 -0
  9. package/dist/broker-core.js +1045 -0
  10. package/dist/broker-gateway.js +447 -0
  11. package/dist/broker-ledger.js +196 -0
  12. package/dist/broker-main.js +291 -0
  13. package/dist/broker-protocol.js +119 -0
  14. package/dist/broker-runtime.js +1283 -0
  15. package/dist/broker-server.js +466 -0
  16. package/dist/build-bundle-loader.js +183 -0
  17. package/dist/build-bundle.js +11 -0
  18. package/dist/discovery.js +59 -0
  19. package/dist/dry-run.js +38 -0
  20. package/dist/error-log.js +71 -0
  21. package/dist/focus-errors.js +63 -0
  22. package/dist/focus-service.js +1855 -0
  23. package/dist/focused-session.js +1357 -0
  24. package/dist/frida-runtime.js +711 -0
  25. package/dist/mcp-main.js +51 -0
  26. package/dist/mcp.js +924 -0
  27. package/dist/observability.js +41 -0
  28. package/dist/process-log-lock.js +195 -0
  29. package/dist/processes.js +47 -0
  30. package/dist/runtime-config.js +399 -0
  31. package/dist/session.js +145 -0
  32. package/dist/storage.js +12 -0
  33. package/dist/types.js +26 -0
  34. package/dist/wow-analysis.js +1430 -0
  35. package/package.json +64 -13
  36. package/resources/builds/retail/12.0.7.68974/build-profile.json +290 -0
  37. package/resources/builds/retail/12.0.7.68974/data-sources.json +1633 -0
  38. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +5130 -0
  39. package/resources/builds/retail/12.0.7.68974/manifest.json +63 -0
  40. package/resources/builds/retail/12.0.7.68974/signatures.json +260 -0
  41. package/index.js +0 -1
@@ -0,0 +1,291 @@
1
+ import { resolve, join } from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { BrokerCore } from "./broker-core.js";
6
+ import { BrokerServer, FileBrokerArtifactStore, FileBrokerSingletonGuard } from "./broker-server.js";
7
+ import { BrokerFridaRuntime } from "./broker-runtime.js";
8
+ import { FridaCommandRuntime } from "./frida-runtime.js";
9
+ import { canonicalArtifactRoot, sidHash } from "./broker-protocol.js";
10
+ import { isWindowsProcessElevated } from "./broker-client.js";
11
+ import { discoverInstalls } from "./discovery.js";
12
+ import { listWowProcesses } from "./processes.js";
13
+ import { FocusAnalysisService } from "./focus-service.js";
14
+ import { AnalysisProcessLog } from "./analysis-process-log.js";
15
+ process.title = "wowdump-broker";
16
+ if (process.env.NODE_ENV === "test" && process.env.WOW_BROKER_LIFECYCLE_NORMALIZE_FIXTURE) {
17
+ const fixture = JSON.parse(process.env.WOW_BROKER_LIFECYCLE_NORMALIZE_FIXTURE);
18
+ const event = fixture.event && typeof fixture.event === "object" && !Array.isArray(fixture.event) ? fixture.event : {};
19
+ const seq = Number.isSafeInteger(fixture.seq) ? Number(fixture.seq) : 1;
20
+ const fixtureInstanceId = typeof fixture.instanceId === "string" ? fixture.instanceId : "fixture-instance";
21
+ const timestamp = typeof fixture.timestamp === "string" ? fixture.timestamp : new Date().toISOString();
22
+ process.stdout.write(`${JSON.stringify(normalizeLifecycleEvent(event, seq, fixtureInstanceId, timestamp))}\n`);
23
+ process.exit(0);
24
+ }
25
+ const configuredPipeName = argument("--pipe") ?? process.env.WOW_BROKER_PIPE;
26
+ if (!configuredPipeName)
27
+ throw new Error("Broker pipe name is required");
28
+ const pipeName = configuredPipeName;
29
+ const sid = process.env.WOW_BROKER_SID ?? process.env.USERNAME ?? "default";
30
+ const currentSidHash = argument("--sid-hash") ?? process.env.WOW_BROKER_SID_HASH ?? sidHash(sid);
31
+ if (!/^[a-f0-9]{24}$/i.test(currentSidHash))
32
+ throw new Error("Broker SID hash is invalid");
33
+ const bootstrapNonce = argument("--bootstrap-nonce") ?? process.env.WOW_BROKER_BOOTSTRAP_NONCE ?? randomUUID();
34
+ const instanceId = randomUUID();
35
+ const artifactDir = canonicalArtifactRoot(argument("--artifact-root") ?? process.env.WOW_BROKER_ARTIFACT_ROOT ?? process.env.WOWDUMP_RUNTIME_DIR ?? join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "wowdump"));
36
+ const profileRoot = resolve(process.env.WOWDUMP_PROFILE_DIR ?? artifactDir);
37
+ const gameRoots = (process.env.WOWDUMP_GAME_ROOTS ?? process.env.WOW_ROOT ?? "")
38
+ .split(";").map(value => value.trim()).filter(Boolean);
39
+ const executor = new FridaCommandRuntime({ artifactDir });
40
+ const processLog = new AnalysisProcessLog(artifactDir);
41
+ const focusService = new FocusAnalysisService({ executor, runtimeRoot: artifactDir, profileRoot, deferAttachmentDetach: true, lifecycle: event => appendLifecycle(event) });
42
+ const readyIdentity = { instanceId, protocolVersion: "2026-07-28", sidHash: currentSidHash, elevated: await isWindowsProcessElevated(), pipeName, bootstrapNonce, artifactRoot: artifactDir };
43
+ const processResolver = async (pid) => {
44
+ try {
45
+ const installs = gameRoots.flatMap(gameRoot => discoverInstalls(gameRoot));
46
+ const process = (await listWowProcesses(installs)).find(item => item.pid === pid);
47
+ return process ? {
48
+ pid: process.pid,
49
+ name: "Wow.exe",
50
+ executable: process.executable,
51
+ processStartTime: process.startTime,
52
+ buildKey: process.install.build.buildKey
53
+ } : undefined;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ };
59
+ const runtime = new BrokerFridaRuntime(executor, readyIdentity, undefined, join(artifactDir, "broker-evidence"), processResolver, focusService);
60
+ let server;
61
+ let core;
62
+ let closing;
63
+ async function start() {
64
+ core = new BrokerCore({
65
+ runtime,
66
+ ledgerPath: join(artifactDir, "frida-broker-request-ledger.jsonl"),
67
+ idleTimeoutMs: numericEnvironment("WOWDUMP_BROKER_IDLE_MS", 20 * 60 * 1000),
68
+ lifecycle: event => appendLifecycle(event),
69
+ closeServer: () => server?.closeServer(),
70
+ releaseSingleton: () => server?.releaseGuard()
71
+ });
72
+ const artifactStore = new FileBrokerArtifactStore(join(artifactDir, "broker-artifacts"));
73
+ const guard = new FileBrokerSingletonGuard(join(homedir(), `.wowdump-frida-${currentSidHash}.lock`), pipeName);
74
+ server = new BrokerServer({ pipeName, core, guard, artifactStore });
75
+ await server.listen();
76
+ }
77
+ async function stop(reason) {
78
+ if (closing)
79
+ return closing;
80
+ closing = (async () => {
81
+ if (core)
82
+ await core.shutdown(reason).catch(() => undefined);
83
+ else
84
+ await server?.close().catch(() => undefined);
85
+ })();
86
+ return closing;
87
+ }
88
+ process.once("SIGINT", () => void stop("signal"));
89
+ process.once("SIGTERM", () => void stop("signal"));
90
+ process.once("uncaughtException", error => {
91
+ console.error("broker:", error);
92
+ void stop("broker_fatal").finally(() => process.exit(1));
93
+ });
94
+ process.once("unhandledRejection", error => {
95
+ console.error("broker:", error);
96
+ void stop("broker_fatal").finally(() => process.exit(1));
97
+ });
98
+ try {
99
+ await start();
100
+ }
101
+ catch (error) {
102
+ await writeStartupFailure(error).catch(() => undefined);
103
+ throw error;
104
+ }
105
+ function argument(name) {
106
+ const index = process.argv.indexOf(name);
107
+ return index >= 0 ? process.argv[index + 1] : undefined;
108
+ }
109
+ function numericEnvironment(name, fallback) {
110
+ const value = Number(process.env[name]);
111
+ return Number.isSafeInteger(value) && value >= 0 ? value : fallback;
112
+ }
113
+ async function writeStartupFailure(error) {
114
+ await mkdir(artifactDir, { recursive: true });
115
+ const safeNonce = bootstrapNonce.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 128);
116
+ const path = join(artifactDir, `broker-startup-failure-${safeNonce}.json`);
117
+ const temporary = `${path}.${process.pid}.tmp`;
118
+ const protocolError = error;
119
+ const report = {
120
+ schema: "wow.broker-startup-failure.v1",
121
+ timestamp: new Date().toISOString(),
122
+ pid: process.pid,
123
+ pipeName,
124
+ sidHash: currentSidHash,
125
+ artifactRoot: artifactDir,
126
+ bootstrapNonce,
127
+ elevated: await isWindowsProcessElevated().catch(() => false),
128
+ error: {
129
+ name: error instanceof Error ? error.name : "Error",
130
+ code: typeof protocolError?.code === "string" ? protocolError.code : null,
131
+ message: error instanceof Error ? error.message : String(error),
132
+ stack: error instanceof Error ? error.stack ?? null : null,
133
+ nextAction: typeof protocolError?.nextAction === "string" ? protocolError.nextAction : null
134
+ }
135
+ };
136
+ await writeFile(temporary, `${JSON.stringify(report, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
137
+ await rename(temporary, path);
138
+ }
139
+ let lifecycleSeq = 0;
140
+ let lifecycleLoaded = false;
141
+ let lifecycleTail = Promise.resolve();
142
+ function appendLifecycle(event) {
143
+ const operation = lifecycleTail.then(() => appendLifecycleOwned(event));
144
+ lifecycleTail = operation.catch(() => undefined);
145
+ return operation;
146
+ }
147
+ async function appendLifecycleOwned(event) {
148
+ await mkdir(artifactDir, { recursive: true });
149
+ if (!lifecycleLoaded) {
150
+ lifecycleLoaded = true;
151
+ try {
152
+ const text = await readFile(join(artifactDir, "frida-broker-lifecycle.jsonl"), "utf8");
153
+ for (const line of text.split(/\r?\n/)) {
154
+ if (!line)
155
+ continue;
156
+ try {
157
+ const parsed = JSON.parse(line);
158
+ if (Number.isSafeInteger(parsed.seq))
159
+ lifecycleSeq = Math.max(lifecycleSeq, Number(parsed.seq));
160
+ }
161
+ catch { /* preserve malformed history; the process log remains append-only */ }
162
+ }
163
+ }
164
+ catch (error) {
165
+ if (error.code !== "ENOENT")
166
+ throw error;
167
+ }
168
+ }
169
+ const normalized = normalizeLifecycleEvent(event, ++lifecycleSeq, instanceId);
170
+ const lifecycleFile = join(artifactDir, "frida-broker-lifecycle.jsonl");
171
+ const lifecycleHandle = await open(lifecycleFile, "a", 0o600);
172
+ try {
173
+ await lifecycleHandle.write(JSON.stringify(normalized) + "\n", null, "utf8");
174
+ await lifecycleHandle.sync();
175
+ }
176
+ finally {
177
+ await lifecycleHandle.close();
178
+ }
179
+ const value = normalized;
180
+ const operation = typeof value.operation === "string" ? value.operation : null;
181
+ const focusOperation = operation?.startsWith("wow_focus_") || operation?.startsWith("wow_watch_") || operation === "wow_session_checkpoint";
182
+ const result = lifecycleResultProjection(value);
183
+ const sessionId = textValue(value.sessionId) ?? textValue(result.sessionId);
184
+ const resourceId = textValue(value.resourceId) ?? textValue(result.resourceId);
185
+ const pid = Number.isSafeInteger(value.pid) ? Number(value.pid) : null;
186
+ await processLog.append({
187
+ buildKey: typeof value.buildKey === "string" ? value.buildKey : null,
188
+ phase: focusOperation ? (operation?.startsWith("wow_watch_") ? "data-root" : "lua-trace") : "verify",
189
+ action: focusOperation ? `${operation}_${String(value.action ?? "lifecycle")}` : `broker_${String(value.action ?? "lifecycle")}`,
190
+ tool: "frida-mcp",
191
+ command: operation,
192
+ status: typeof value.status === "string" ? value.status : "partial",
193
+ target: {
194
+ name: sessionId ?? "Wow.exe",
195
+ kind: sessionId ? "focused_session" : "module",
196
+ pid,
197
+ sessionId: sessionId ?? null,
198
+ resourceId: resourceId ?? null,
199
+ va: null,
200
+ rva: null,
201
+ imageBase: "0x140000000",
202
+ moduleBase: null,
203
+ runtimeAddress: null
204
+ },
205
+ result: {
206
+ ...result,
207
+ requestId: textValue(value.requestId) ?? textValue(result.requestId) ?? null,
208
+ clientId: textValue(value.clientId) ?? textValue(result.clientId) ?? null,
209
+ sessionId: sessionId ?? null,
210
+ resourceId: resourceId ?? null,
211
+ pid,
212
+ buildKey: typeof value.buildKey === "string" ? value.buildKey : null
213
+ },
214
+ error: value.error ?? null,
215
+ nextAction: typeof value.nextAction === "string" ? value.nextAction : null
216
+ });
217
+ }
218
+ export function normalizeLifecycleEvent(event, seq, writerInstanceId, timestamp = new Date().toISOString()) {
219
+ const value = event;
220
+ const result = value.result && typeof value.result === "object" && !Array.isArray(value.result)
221
+ ? value.result
222
+ : {};
223
+ const durationCandidate = typeof value.durationMs === "number" ? value.durationMs : value.duration;
224
+ const durationMs = typeof durationCandidate === "number" && Number.isFinite(durationCandidate) && durationCandidate >= 0
225
+ ? durationCandidate
226
+ : 0;
227
+ const dropSummary = objectValue(value.dropSummary) ?? objectValue(result.dropSummary) ?? {
228
+ droppedEventCount: nonNegativeNumber(value.droppedEventCount) ?? nonNegativeNumber(result.droppedEventCount) ?? 0,
229
+ droppedByteCount: nonNegativeNumber(value.droppedByteCount) ?? nonNegativeNumber(result.droppedByteCount) ?? 0,
230
+ overflowCount: nonNegativeNumber(value.overflowCount) ?? nonNegativeNumber(result.overflowCount) ?? 0
231
+ };
232
+ const cleanupSummary = objectValue(value.cleanupSummary) ?? objectValue(result.cleanupSummary) ?? {
233
+ cleanupAttempted: booleanValue(value.cleanupAttempted) ?? booleanValue(result.cleanupAttempted) ?? false,
234
+ fullyReleased: booleanValue(value.fullyReleased) ?? booleanValue(result.fullyReleased) ?? null,
235
+ cleanupStatus: objectValue(value.cleanupStatus) ?? objectValue(result.cleanupStatus) ?? null,
236
+ residualResourceIds: arrayValue(value.residualResourceIds) ?? arrayValue(result.residualResourceIds) ?? [],
237
+ residualSessionId: textValue(value.residualSessionId) ?? textValue(result.residualSessionId) ?? null
238
+ };
239
+ const error = Object.hasOwn(value, "error") ? value.error : null;
240
+ return {
241
+ ...value,
242
+ timestamp: textValue(value.timestamp) ?? timestamp,
243
+ seq,
244
+ instanceId: writerInstanceId,
245
+ clientId: textValue(value.clientId) ?? textValue(result.clientId) ?? null,
246
+ requestId: textValue(value.requestId) ?? textValue(result.requestId) ?? null,
247
+ resourceId: textValue(value.resourceId) ?? textValue(result.resourceId) ?? null,
248
+ sessionId: textValue(value.sessionId) ?? textValue(result.sessionId) ?? null,
249
+ operation: textValue(value.operation) ?? textValue(value.action) ?? "broker_lifecycle",
250
+ pid: Number.isSafeInteger(value.pid) ? Number(value.pid) : Number.isSafeInteger(result.pid) ? Number(result.pid) : null,
251
+ buildKey: textValue(value.buildKey) ?? textValue(result.buildKey) ?? null,
252
+ duration: durationMs,
253
+ durationMs,
254
+ result: Object.hasOwn(value, "result") ? value.result : null,
255
+ status: textValue(value.status) ?? (error === null || error === undefined ? "partial" : "failed"),
256
+ dropSummary,
257
+ cleanupSummary,
258
+ error: error ?? null,
259
+ nextAction: textValue(value.nextAction) ?? null
260
+ };
261
+ }
262
+ function lifecycleResultProjection(value) {
263
+ const source = value.result && typeof value.result === "object" && !Array.isArray(value.result)
264
+ ? value.result
265
+ : {};
266
+ const projected = {};
267
+ for (const key of [
268
+ "requestId", "clientId", "sessionId", "resourceId", "pid", "buildKey",
269
+ "cleanupAttempted", "fullyReleased", "alreadyReleased", "cleanupStatus",
270
+ "residualSessionId", "residualResourceIds", "attachmentDetachStatus",
271
+ "seq", "eventKind", "diff", "reason", "cleanupSteps"
272
+ ])
273
+ if (Object.hasOwn(source, key))
274
+ projected[key] = source[key];
275
+ return projected;
276
+ }
277
+ function textValue(value) {
278
+ return typeof value === "string" && value.length > 0 ? value : undefined;
279
+ }
280
+ function objectValue(value) {
281
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
282
+ }
283
+ function arrayValue(value) {
284
+ return Array.isArray(value) ? value : undefined;
285
+ }
286
+ function booleanValue(value) {
287
+ return typeof value === "boolean" ? value : undefined;
288
+ }
289
+ function nonNegativeNumber(value) {
290
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
291
+ }
@@ -0,0 +1,119 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ export const BROKER_PROTOCOL_VERSION = "2026-07-28";
4
+ export const BROKER_SCHEMA_VERSION = 1;
5
+ export const BROKER_MAX_FRAME_BYTES = 4 * 1024 * 1024;
6
+ export const BROKER_MAX_INLINE_BYTES = 1024 * 1024;
7
+ export const BROKER_DEFAULT_TIMEOUT_MS = 30_000;
8
+ export function canonicalArtifactRoot(value) {
9
+ if (typeof value !== "string" || !value.trim())
10
+ throw new BrokerProtocolError("BROKER_ARTIFACT_ROOT_MISMATCH", "artifactRoot must be a non-empty absolute path");
11
+ return resolve(value);
12
+ }
13
+ export const BROKER_ERROR_CODES = [
14
+ "BROKER_STOPPED", "BOOTSTRAP_LOCK_BUSY", "BOOTSTRAP_TIMEOUT",
15
+ "BROKER_EXECUTABLE_INVALID", "BOOTSTRAP_SPAWN_FAILED", "UAC_CANCELLED",
16
+ "UAC_FAILED", "READY_IDENTITY_MISMATCH", "ACL_MISMATCH", "STALE_LOCK_UNSAFE",
17
+ "FRAME_INVALID", "FRAME_TOO_LARGE", "PROTOCOL_VERSION_MISMATCH", "REQUEST_TIMEOUT",
18
+ "REQUEST_CANCELLED", "REQUEST_NOT_FOUND", "REQUEST_ID_REPLAYED", "PID_REQUIRED",
19
+ "PID_NOT_WOW", "PID_BUILD_MISMATCH", "PID_REUSED", "MODULE_NOT_FOUND",
20
+ "SELECTOR_EMPTY", "SELECTOR_INVALID", "TARGETS_OVER_LIMIT", "RESOURCE_NOT_FOUND",
21
+ "RESOURCE_NOT_OWNER", "RESOURCE_ALREADY_RELEASED", "RESOURCE_REF_UNDERFLOW",
22
+ "SESSION_TARGET_MISMATCH", "CAPTURE_FATAL", "ATTACHMENT_FATAL", "BROKER_FATAL",
23
+ "CLEANUP_TIMEOUT", "ARTIFACT_MANIFEST_CONFLICT", "ARTIFACT_VERIFY_FAILED",
24
+ "MUTATION_PLAN_REQUIRED", "MUTATION_CONFIRMATION_REQUIRED", "MUTATION_EXPECTED_EFFECT_REQUIRED",
25
+ "MUTATION_ROLLBACK_REQUIRED", "MUTATION_ROLLBACK_INVALID", "MUTATION_IRREVERSIBLE_CONFIRMATION_REQUIRED",
26
+ "MUTATION_EVIDENCE_PLAN_REQUIRED", "MUTATION_AUDIT_REQUIRED", "MUTATION_CONFIRMATION_EXPIRED",
27
+ "MUTATION_CONFIRMATION_REPLAYED", "MUTATION_TARGET_MISMATCH", "MUTATION_EVIDENCE_MISMATCH",
28
+ "MUTATION_BEFORE_EVIDENCE_FAILED", "MUTATION_EXECUTION_FAILED", "MUTATION_AFTER_EVIDENCE_FAILED",
29
+ "MUTATION_ROLLBACK_UNAVAILABLE", "MUTATION_ROLLBACK_FAILED", "MUTATION_OUTCOME_UNKNOWN",
30
+ "SPAWN_CONTRACT_REQUIRED", "SPAWN_PID_MUST_BE_NULL", "SPAWN_BUILD_MISMATCH",
31
+ "HOST_CALL_BUILD_KEY_REQUIRED", "HOST_CALL_NOT_ALLOWLISTED", "HOST_CALL_SCHEMA_MISMATCH",
32
+ "BROKER_ARTIFACT_ROOT_MISMATCH"
33
+ ];
34
+ export class BrokerProtocolError extends Error {
35
+ code;
36
+ nextAction;
37
+ constructor(code, message, nextAction = "Inspect Broker status and retry with a valid request.") {
38
+ super(message);
39
+ this.code = code;
40
+ this.nextAction = nextAction;
41
+ this.name = "BrokerProtocolError";
42
+ }
43
+ }
44
+ export function sidHash(sid) {
45
+ return createHash("sha256").update(sid, "utf8").digest("hex").slice(0, 24);
46
+ }
47
+ export function pipeNameForSid(sid) {
48
+ return `\\\\.\\pipe\\wowdump-frida-${sidHash(sid)}`;
49
+ }
50
+ export function canonicalPayloadHash(request) {
51
+ return createHash("sha256").update(stableJson({
52
+ clientId: request.clientId,
53
+ operation: request.operation,
54
+ pid: request.pid,
55
+ buildKey: request.buildKey,
56
+ payload: request.payload
57
+ })).digest("hex");
58
+ }
59
+ export function validateRequest(value) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value))
61
+ fail("FRAME_INVALID", "request must be an object");
62
+ const request = value;
63
+ if (request.schemaVersion !== BROKER_SCHEMA_VERSION || request.protocolVersion !== BROKER_PROTOCOL_VERSION) {
64
+ fail("PROTOCOL_VERSION_MISMATCH", "unsupported Broker schema or protocol version");
65
+ }
66
+ for (const key of ["requestId", "clientId", "operation"]) {
67
+ if (typeof request[key] !== "string" || !request[key].trim())
68
+ fail("FRAME_INVALID", `${key} must be a non-empty string`);
69
+ }
70
+ if (request.pid !== null && (!Number.isSafeInteger(request.pid) || request.pid <= 0))
71
+ fail("FRAME_INVALID", "pid must be null or a positive integer");
72
+ if (request.buildKey !== null && typeof request.buildKey !== "string")
73
+ fail("FRAME_INVALID", "buildKey must be null or a string");
74
+ if (!request.payload || typeof request.payload !== "object" || Array.isArray(request.payload))
75
+ fail("FRAME_INVALID", "payload must be an object");
76
+ return request;
77
+ }
78
+ export function encodeFrame(value) {
79
+ const body = Buffer.from(JSON.stringify(value), "utf8");
80
+ if (body.length > BROKER_MAX_FRAME_BYTES)
81
+ fail("FRAME_TOO_LARGE", `frame exceeds ${BROKER_MAX_FRAME_BYTES} bytes`);
82
+ const frame = Buffer.allocUnsafe(body.length + 4);
83
+ frame.writeUInt32LE(body.length, 0);
84
+ body.copy(frame, 4);
85
+ return frame;
86
+ }
87
+ export class FrameDecoder {
88
+ buffered = Buffer.alloc(0);
89
+ push(chunk) {
90
+ this.buffered = Buffer.concat([this.buffered, chunk]);
91
+ const values = [];
92
+ while (this.buffered.length >= 4) {
93
+ const length = this.buffered.readUInt32LE(0);
94
+ if (length > BROKER_MAX_FRAME_BYTES)
95
+ fail("FRAME_TOO_LARGE", `frame exceeds ${BROKER_MAX_FRAME_BYTES} bytes`);
96
+ if (this.buffered.length < length + 4)
97
+ break;
98
+ const body = this.buffered.subarray(4, length + 4);
99
+ this.buffered = this.buffered.subarray(length + 4);
100
+ try {
101
+ values.push(JSON.parse(body.toString("utf8")));
102
+ }
103
+ catch {
104
+ fail("FRAME_INVALID", "frame body is not valid JSON");
105
+ }
106
+ }
107
+ return values;
108
+ }
109
+ }
110
+ function stableJson(value) {
111
+ if (Array.isArray(value))
112
+ return `[${value.map(stableJson).join(",")}]`;
113
+ if (value && typeof value === "object")
114
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`).join(",")}}`;
115
+ return JSON.stringify(value);
116
+ }
117
+ function fail(code, message) {
118
+ throw new BrokerProtocolError(code, message);
119
+ }