wowdump 0.2.1 → 0.3.2

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 (63) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +17 -111
  3. package/dist/adapters/reader.js +33 -0
  4. package/dist/analysis/disassemble.js +77 -0
  5. package/dist/{frida-runtime.js → analysis/frida-runtime.js} +3 -25
  6. package/dist/analysis/runtime-script.js +36 -0
  7. package/dist/cli.js +563 -0
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +99 -0
  10. package/dist/reader/broker.js +475 -0
  11. package/dist/reader/client.js +1 -0
  12. package/dist/reader/launcher.js +219 -0
  13. package/dist/reader/main.js +100 -0
  14. package/dist/reader/protocol.js +1 -0
  15. package/dist/reader/windows.js +242 -0
  16. package/dist/reader-main.js +2 -0
  17. package/dist/toolchain.js +123 -0
  18. package/package.json +19 -37
  19. package/skills/wowdump/SKILL.md +22 -0
  20. package/skills/wowdump/references/commands.md +63 -0
  21. package/skills/wowdump/references/disassemble.md +18 -0
  22. package/skills/wowdump/references/dynamic.md +54 -0
  23. package/skills/wowdump/references/evidence-workflow.md +41 -0
  24. package/skills/wowdump/references/profiles.md +34 -0
  25. package/skills/wowdump/references/request-schema.md +28 -0
  26. package/skills/wowdump/references/workflow.md +44 -0
  27. package/skills/wowdump/scripts/dynamic-session.js +133 -0
  28. package/dist/agent.js +0 -1335
  29. package/dist/analysis-path.js +0 -38
  30. package/dist/analysis-process-log.js +0 -146
  31. package/dist/broker-client.js +0 -411
  32. package/dist/broker-codec.js +0 -148
  33. package/dist/broker-core.js +0 -1045
  34. package/dist/broker-gateway.js +0 -447
  35. package/dist/broker-ledger.js +0 -196
  36. package/dist/broker-main.js +0 -291
  37. package/dist/broker-protocol.js +0 -119
  38. package/dist/broker-runtime.js +0 -1283
  39. package/dist/broker-server.js +0 -466
  40. package/dist/build-bundle-loader.js +0 -183
  41. package/dist/build-bundle.js +0 -11
  42. package/dist/discovery.js +0 -59
  43. package/dist/dry-run.js +0 -38
  44. package/dist/error-log.js +0 -71
  45. package/dist/focus-errors.js +0 -63
  46. package/dist/focus-service.js +0 -1855
  47. package/dist/focused-session.js +0 -1357
  48. package/dist/mcp-main.js +0 -51
  49. package/dist/mcp.js +0 -924
  50. package/dist/observability.js +0 -41
  51. package/dist/process-log-lock.js +0 -195
  52. package/dist/processes.js +0 -47
  53. package/dist/runtime-config.js +0 -399
  54. package/dist/session.js +0 -145
  55. package/dist/storage.js +0 -12
  56. package/dist/wow-analysis.js +0 -1430
  57. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  58. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  59. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  60. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  61. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
  62. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  63. /package/dist/{types.js → core/types.js} +0 -0
@@ -1,466 +0,0 @@
1
- import { createConnection, createServer } from "node:net";
2
- import { spawn } from "node:child_process";
3
- import { createHash } from "node:crypto";
4
- import { mkdir, rename, writeFile } from "node:fs/promises";
5
- import { open, readFile, unlink } from "node:fs/promises";
6
- import { join } from "node:path";
7
- import { BROKER_MAX_INLINE_BYTES, BROKER_PROTOCOL_VERSION, FrameDecoder, BrokerProtocolError, encodeFrame, sidHash } from "./broker-protocol.js";
8
- export class FileBrokerArtifactStore {
9
- directory;
10
- constructor(directory) {
11
- this.directory = directory;
12
- }
13
- async write(requestId, value) {
14
- const body = Buffer.from(JSON.stringify(value), "utf8");
15
- const sha256 = createHash("sha256").update(body).digest("hex");
16
- await mkdir(this.directory, { recursive: true });
17
- const name = `broker-response-${requestId.replace(/[^A-Za-z0-9_.-]/g, "_")}-${sha256.slice(0, 16)}.json`;
18
- const path = join(this.directory, name);
19
- const temporary = `${path}.${process.pid}.tmp`;
20
- await writeFile(temporary, body);
21
- await rename(temporary, path);
22
- return { kind: "artifact", path, bytes: body.length, sha256, encoding: "json" };
23
- }
24
- }
25
- /** Cross-process singleton guard. Windows deployments can replace this with a native mutex guard. */
26
- export class FileBrokerSingletonGuard {
27
- lockPath;
28
- expectedPipeName;
29
- handle;
30
- aclInspector;
31
- aclConfigurator;
32
- expectedSidHash;
33
- constructor(lockPath, expectedPipeName, options = {}) {
34
- this.lockPath = lockPath;
35
- this.expectedPipeName = expectedPipeName;
36
- this.aclInspector = options.aclInspector ?? new WindowsNamedPipeAclInspector();
37
- this.aclConfigurator = options.aclConfigurator ?? new WindowsNamedPipeAclConfigurator();
38
- this.expectedSidHash = options.expectedSidHash ?? pipeSidHash(expectedPipeName);
39
- }
40
- async acquire(pipeName) {
41
- if (pipeName !== this.expectedPipeName)
42
- return null;
43
- try {
44
- this.handle = await open(this.lockPath, "wx");
45
- await this.handle.writeFile(JSON.stringify({ pid: process.pid, pipeName, acquiredAt: new Date().toISOString() }), "utf8");
46
- }
47
- catch {
48
- if (await brokerOwnerAlive(this.lockPath, pipeName))
49
- return null;
50
- await unlink(this.lockPath).catch(() => undefined);
51
- try {
52
- this.handle = await open(this.lockPath, "wx");
53
- await this.handle.writeFile(JSON.stringify({ pid: process.pid, pipeName, acquiredAt: new Date().toISOString() }), "utf8");
54
- }
55
- catch {
56
- return null;
57
- }
58
- }
59
- return { release: async () => { await this.handle?.close().catch(() => undefined); this.handle = undefined; await unlink(this.lockPath).catch(() => undefined); } };
60
- }
61
- async secureAcl(pipeName) {
62
- if (pipeName !== this.expectedPipeName || !this.expectedSidHash)
63
- throw new BrokerProtocolError("ACL_MISMATCH", "named-pipe SID namespace mismatch");
64
- await this.aclConfigurator.configure(pipeName);
65
- }
66
- async verifyAcl(pipeName) {
67
- if (pipeName !== this.expectedPipeName || !this.expectedSidHash)
68
- return false;
69
- try {
70
- const acl = await this.aclInspector.inspect(pipeName);
71
- if (sidHash(acl.currentSid) !== this.expectedSidHash)
72
- return false;
73
- const currentSid = normalizeSid(acl.currentSid);
74
- const trustedOwners = new Set([currentSid, "S-1-5-18", "S-1-5-32-544"]);
75
- if (!trustedOwners.has(normalizeSid(acl.ownerSid)))
76
- return false;
77
- const broadSids = new Set(["S-1-1-0", "S-1-5-7", "S-1-5-11", "S-1-5-32-545"]);
78
- if (acl.access.some(entry => entry.type === "allow" && broadSids.has(normalizeSid(entry.sid)) && grantsMutationRights(entry.rights)))
79
- return false;
80
- if (acl.access.some(entry => entry.type === "deny" && normalizeSid(entry.sid) === currentSid))
81
- return false;
82
- return acl.access.some(entry => entry.type === "allow" && normalizeSid(entry.sid) === currentSid);
83
- }
84
- catch {
85
- return false;
86
- }
87
- }
88
- }
89
- export class WindowsNamedPipeAclConfigurator {
90
- async configure(pipeName) {
91
- if (process.platform !== "win32")
92
- throw new Error("Windows named-pipe ACL configuration requires Windows");
93
- const script = [
94
- "$ErrorActionPreference='Stop'",
95
- "$source=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($env:WOW_BROKER_ACL_SOURCE))",
96
- "Add-Type -TypeDefinition $source -Language CSharp",
97
- "[WowDumpPipeAcl]::Configure($env:WOW_BROKER_ACL_PIPE)|Out-Null"
98
- ].join(";");
99
- const result = await runAclProcess("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script], pipeName.replace(/^\\\\\.\\pipe\\/i, ""));
100
- if (result.code !== 0)
101
- throw new Error(result.stderr || "named-pipe ACL configuration failed");
102
- }
103
- }
104
- export class WindowsNamedPipeAclInspector {
105
- async inspect(pipeName) {
106
- if (process.platform !== "win32")
107
- throw new Error("Windows named-pipe ACL inspection requires Windows");
108
- const script = [
109
- "$ErrorActionPreference='Stop'",
110
- "$source=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($env:WOW_BROKER_ACL_SOURCE))",
111
- "Add-Type -TypeDefinition $source -Language CSharp",
112
- "[WowDumpPipeAcl]::Inspect($env:WOW_BROKER_ACL_PIPE)|ConvertTo-Json -Depth 4 -Compress"
113
- ].join(";");
114
- const result = await runAclProcess("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script], pipeName.replace(/^\\\\\.\\pipe\\/i, ""));
115
- if (result.code !== 0)
116
- throw new Error(result.stderr || "named-pipe ACL query failed");
117
- const parsed = JSON.parse(result.stdout);
118
- if (!parsed || typeof parsed.currentSid !== "string" || typeof parsed.ownerSid !== "string" || !Array.isArray(parsed.access))
119
- throw new Error("named-pipe ACL response is invalid");
120
- return parsed;
121
- }
122
- }
123
- const PIPE_ACL_INSPECTOR_SOURCE = String.raw `
124
- using System;
125
- using System.Collections.Generic;
126
- using System.ComponentModel;
127
- using System.IO.Pipes;
128
- using System.Runtime.InteropServices;
129
- using System.Security.AccessControl;
130
- using System.Security.Principal;
131
-
132
- public static class WowDumpPipeAcl {
133
- public sealed class Entry {
134
- public string sid;
135
- public string type;
136
- public string rights;
137
- }
138
-
139
- public sealed class Snapshot {
140
- public string currentSid;
141
- public string ownerSid;
142
- public Entry[] access;
143
- }
144
-
145
- [DllImport("advapi32.dll", SetLastError = true)]
146
- private static extern uint GetSecurityInfo(
147
- IntPtr handle,
148
- int objectType,
149
- uint securityInfo,
150
- out IntPtr owner,
151
- out IntPtr group,
152
- out IntPtr dacl,
153
- out IntPtr sacl,
154
- out IntPtr descriptor);
155
-
156
- [DllImport("advapi32.dll")]
157
- private static extern uint GetSecurityDescriptorLength(IntPtr descriptor);
158
-
159
- [DllImport("kernel32.dll")]
160
- private static extern IntPtr LocalFree(IntPtr value);
161
-
162
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
163
- private static extern IntPtr CreateFile(
164
- string name,
165
- uint access,
166
- uint share,
167
- IntPtr security,
168
- uint creation,
169
- uint flags,
170
- IntPtr template);
171
-
172
- [DllImport("kernel32.dll", SetLastError = true)]
173
- private static extern bool CloseHandle(IntPtr handle);
174
-
175
- [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
176
- private static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(
177
- string sddl,
178
- uint revision,
179
- out IntPtr descriptor,
180
- out uint size);
181
-
182
- [DllImport("advapi32.dll", SetLastError = true)]
183
- private static extern bool GetSecurityDescriptorDacl(
184
- IntPtr descriptor,
185
- out bool present,
186
- out IntPtr dacl,
187
- out bool defaulted);
188
-
189
- [DllImport("advapi32.dll", SetLastError = true)]
190
- private static extern uint SetSecurityInfo(
191
- IntPtr handle,
192
- int objectType,
193
- uint securityInfo,
194
- IntPtr owner,
195
- IntPtr group,
196
- IntPtr dacl,
197
- IntPtr sacl);
198
-
199
- public static void Configure(string pipeName) {
200
- const uint ReadControl = 0x00020000;
201
- const uint WriteDac = 0x00040000;
202
- const uint OpenExisting = 3;
203
- const int SeKernelObject = 6;
204
- const uint DaclSecurityInformation = 4;
205
- const uint ProtectedDaclSecurityInformation = 0x80000000;
206
- string fullName = @"\\.\pipe\" + pipeName;
207
- IntPtr handle = CreateFile(
208
- fullName,
209
- ReadControl | WriteDac,
210
- 0,
211
- IntPtr.Zero,
212
- OpenExisting,
213
- 0,
214
- IntPtr.Zero);
215
- if (handle == new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
216
- try {
217
- string currentSid = WindowsIdentity.GetCurrent().User.Value;
218
- string sddl = "D:P(A;;GA;;;" + currentSid + ")(A;;GA;;;BA)(A;;GA;;;SY)";
219
- IntPtr descriptor;
220
- uint descriptorSize;
221
- if (!ConvertStringSecurityDescriptorToSecurityDescriptor(sddl, 1, out descriptor, out descriptorSize)) {
222
- throw new Win32Exception(Marshal.GetLastWin32Error());
223
- }
224
- try {
225
- bool present, defaulted;
226
- IntPtr dacl;
227
- if (!GetSecurityDescriptorDacl(descriptor, out present, out dacl, out defaulted) || !present) {
228
- throw new Win32Exception(Marshal.GetLastWin32Error());
229
- }
230
- uint code = SetSecurityInfo(
231
- handle,
232
- SeKernelObject,
233
- DaclSecurityInformation | ProtectedDaclSecurityInformation,
234
- IntPtr.Zero,
235
- IntPtr.Zero,
236
- dacl,
237
- IntPtr.Zero);
238
- if (code != 0) throw new Win32Exception((int)code);
239
- } finally {
240
- LocalFree(descriptor);
241
- }
242
- } finally {
243
- CloseHandle(handle);
244
- }
245
- }
246
-
247
- public static Snapshot Inspect(string pipeName) {
248
- using (var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut)) {
249
- pipe.Connect(1000);
250
- IntPtr owner, group, dacl, sacl, descriptor;
251
- const int SeKernelObject = 6;
252
- const uint OwnerAndDacl = 1u | 4u;
253
- uint code = GetSecurityInfo(
254
- pipe.SafePipeHandle.DangerousGetHandle(),
255
- SeKernelObject,
256
- OwnerAndDacl,
257
- out owner,
258
- out group,
259
- out dacl,
260
- out sacl,
261
- out descriptor);
262
- if (code != 0) throw new Win32Exception((int)code);
263
- try {
264
- int length = checked((int)GetSecurityDescriptorLength(descriptor));
265
- var bytes = new byte[length];
266
- Marshal.Copy(descriptor, bytes, 0, length);
267
- var security = new RawSecurityDescriptor(bytes, 0);
268
- var entries = new List<Entry>();
269
- if (security.DiscretionaryAcl != null) {
270
- foreach (GenericAce ace in security.DiscretionaryAcl) {
271
- var known = ace as KnownAce;
272
- var qualified = ace as QualifiedAce;
273
- if (known == null || qualified == null || qualified.SecurityIdentifier == null) continue;
274
- string type = qualified.AceQualifier == AceQualifier.AccessAllowed
275
- ? "allow"
276
- : qualified.AceQualifier == AceQualifier.AccessDenied ? "deny" : "other";
277
- entries.Add(new Entry {
278
- sid = qualified.SecurityIdentifier.Value,
279
- type = type,
280
- rights = known.AccessMask.ToString()
281
- });
282
- }
283
- }
284
- return new Snapshot {
285
- currentSid = WindowsIdentity.GetCurrent().User.Value,
286
- ownerSid = security.Owner == null ? null : security.Owner.Value,
287
- access = entries.ToArray()
288
- };
289
- } finally {
290
- if (descriptor != IntPtr.Zero) LocalFree(descriptor);
291
- }
292
- }
293
- }
294
- }`;
295
- function pipeSidHash(pipeName) {
296
- return /(?:^|\\)wowdump-frida-([a-f0-9]{24})$/i.exec(pipeName)?.[1]?.toLowerCase();
297
- }
298
- function normalizeSid(value) { return value.trim().toUpperCase(); }
299
- function grantsMutationRights(rights) {
300
- const numeric = /^\d+$/.test(rights.trim()) ? Number(rights.trim()) : undefined;
301
- if (numeric !== undefined)
302
- return (numeric & (2 | 4 | 16 | 256 | 262_144 | 524_288)) !== 0;
303
- return /write|create.?new.?instance|change.?permissions|take.?ownership|full.?control/i.test(rights);
304
- }
305
- function runAclProcess(file, args, pipeName) {
306
- return new Promise(resolve => {
307
- const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? "C:\\Windows";
308
- const env = { SystemRoot: systemRoot, WINDIR: systemRoot, PATH: process.env.PATH, PATHEXT: process.env.PATHEXT, PSModulePath: join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "Modules"), WOW_BROKER_ACL_PIPE: pipeName, WOW_BROKER_ACL_SOURCE: Buffer.from(PIPE_ACL_INSPECTOR_SOURCE, "utf16le").toString("base64") };
309
- const child = spawn(file, args, { env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
310
- let stdout = "";
311
- let stderr = "";
312
- child.stdout.setEncoding("utf8");
313
- child.stderr.setEncoding("utf8");
314
- child.stdout.on("data", chunk => { stdout += chunk; });
315
- child.stderr.on("data", chunk => { stderr += chunk; });
316
- child.on("error", error => resolve({ code: null, stdout, stderr: `${stderr}${error.message}` }));
317
- child.on("exit", code => resolve({ code, stdout, stderr }));
318
- });
319
- }
320
- async function brokerOwnerAlive(lockPath, pipeName) {
321
- let owner;
322
- try {
323
- owner = JSON.parse(await readFile(lockPath, "utf8"));
324
- }
325
- catch {
326
- owner = undefined;
327
- }
328
- if (owner && typeof owner === "object" && typeof owner.pid === "number") {
329
- try {
330
- process.kill(owner.pid, 0);
331
- return true;
332
- }
333
- catch { /* stale PID */ }
334
- }
335
- return pipeReachable(pipeName);
336
- }
337
- function pipeReachable(pipeName) {
338
- return new Promise(resolve => {
339
- const socket = createConnection(pipeName);
340
- const finish = (value) => { socket.destroy(); resolve(value); };
341
- const timer = setTimeout(() => finish(false), 100);
342
- socket.once("connect", () => { clearTimeout(timer); finish(true); });
343
- socket.once("error", () => { clearTimeout(timer); finish(false); });
344
- });
345
- }
346
- export class BrokerServer {
347
- options;
348
- server;
349
- sockets = new Set();
350
- preVerificationSockets = new Set();
351
- guardLease;
352
- aclVerified = false;
353
- closePromise;
354
- serverClosePromise;
355
- guardReleasePromise;
356
- constructor(options) {
357
- this.options = options;
358
- }
359
- async listen() {
360
- if (this.server)
361
- return;
362
- if (this.options.guard) {
363
- this.guardLease = await this.options.guard.acquire(this.options.pipeName) ?? undefined;
364
- if (!this.guardLease)
365
- throw new BrokerProtocolError("STALE_LOCK_UNSAFE", "Broker singleton is already owned", "Connect to the existing Broker instance.");
366
- }
367
- this.server = createServer(socket => this.acceptSocket(socket));
368
- try {
369
- await new Promise((resolve, reject) => { this.server.once("error", reject); this.server.listen(this.options.pipeName, () => { this.server.off("error", reject); resolve(); }); });
370
- await this.options.guard?.secureAcl?.(this.options.pipeName);
371
- if (this.options.guard && !await this.options.guard.verifyAcl(this.options.pipeName))
372
- throw new BrokerProtocolError("ACL_MISMATCH", "named-pipe ACL or SID namespace mismatch");
373
- this.aclVerified = true;
374
- for (const socket of this.preVerificationSockets)
375
- socket.destroy();
376
- this.preVerificationSockets.clear();
377
- }
378
- catch (error) {
379
- await this.closeInternal().catch(() => undefined);
380
- throw error;
381
- }
382
- }
383
- async close() {
384
- if (this.closePromise)
385
- return this.closePromise;
386
- this.closePromise = (async () => {
387
- await this.closeServer();
388
- await this.releaseGuard();
389
- })().finally(() => { this.closePromise = undefined; });
390
- return this.closePromise;
391
- }
392
- async closeServer() {
393
- if (this.serverClosePromise)
394
- return this.serverClosePromise;
395
- this.serverClosePromise = this.closeServerInternal().finally(() => { this.serverClosePromise = undefined; });
396
- return this.serverClosePromise;
397
- }
398
- async releaseGuard() {
399
- if (this.guardReleasePromise)
400
- return this.guardReleasePromise;
401
- this.guardReleasePromise = (async () => {
402
- const lease = this.guardLease;
403
- this.guardLease = undefined;
404
- await lease?.release();
405
- })().finally(() => { this.guardReleasePromise = undefined; });
406
- return this.guardReleasePromise;
407
- }
408
- async closeInternal() {
409
- try {
410
- await this.closeServer();
411
- }
412
- finally {
413
- await this.releaseGuard();
414
- }
415
- }
416
- async closeServerInternal() {
417
- for (const socket of this.preVerificationSockets)
418
- socket.destroy();
419
- this.preVerificationSockets.clear();
420
- for (const socket of this.sockets)
421
- socket.destroy();
422
- const server = this.server;
423
- this.server = undefined;
424
- this.aclVerified = false;
425
- if (server?.listening)
426
- await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
427
- }
428
- acceptSocket(socket) {
429
- if (!this.aclVerified) {
430
- this.preVerificationSockets.add(socket);
431
- socket.once("close", () => this.preVerificationSockets.delete(socket));
432
- return;
433
- }
434
- this.sockets.add(socket);
435
- const decoder = new FrameDecoder();
436
- let queued = 0;
437
- socket.on("close", () => this.sockets.delete(socket));
438
- socket.on("data", async (chunk) => {
439
- try {
440
- for (const request of decoder.push(chunk)) {
441
- if (queued >= (this.options.maxQueuedPerSocket ?? 128))
442
- throw new BrokerProtocolError("REQUEST_CANCELLED", "socket queue limit reached", "Wait for pending Broker requests.");
443
- queued += 1;
444
- try {
445
- socket.write(encodeFrame(await this.externalize(await this.options.core.accept(request))));
446
- }
447
- finally {
448
- queued -= 1;
449
- }
450
- }
451
- }
452
- catch (error) {
453
- const protocolError = error instanceof BrokerProtocolError ? error : new BrokerProtocolError("FRAME_INVALID", error instanceof Error ? error.message : String(error));
454
- socket.write(encodeFrame({ schemaVersion: 1, protocolVersion: BROKER_PROTOCOL_VERSION, requestId: "unknown", ok: false, error: { code: protocolError.code, message: protocolError.message, nextAction: protocolError.nextAction } }));
455
- }
456
- });
457
- }
458
- async externalize(response) {
459
- if (!response.ok || !this.options.artifactStore)
460
- return response;
461
- if (Buffer.byteLength(JSON.stringify(response.result), "utf8") <= BROKER_MAX_INLINE_BYTES)
462
- return response;
463
- const reference = await this.options.artifactStore.write(response.requestId, response.result);
464
- return { ...response, result: { ...reference } };
465
- }
466
- }
@@ -1,183 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { readFile } from "node:fs/promises";
3
- import { basename, isAbsolute, relative, resolve } from "node:path";
4
- import { buildBundleDirectory } from "./build-bundle.js";
5
- export const BUILD_BUNDLE_SCHEMA = "wowdump.build-bundle.v1";
6
- export const BUILD_BUNDLE_PAYLOADS = [
7
- "build-profile.json",
8
- "lua-targets.jsonl",
9
- "data-sources.json",
10
- "signatures.json"
11
- ];
12
- const REQUIRED_PAYLOADS = new Set(BUILD_BUNDLE_PAYLOADS);
13
- export class BuildBundleStore {
14
- profileRoot;
15
- cache = new Map();
16
- constructor(profileRoot) {
17
- if (typeof profileRoot !== "string" || !profileRoot.trim())
18
- invalid("profileRoot must be a non-empty string");
19
- this.profileRoot = resolve(profileRoot);
20
- }
21
- async manifest(buildKey) {
22
- return (await this.load(buildKey)).manifest;
23
- }
24
- async readText(buildKey, file) {
25
- const bytes = await this.payload(buildKey, file);
26
- const text = bytes.toString("utf8");
27
- if (file === "lua-targets.jsonl")
28
- parseJsonlWithBuildKey(text, buildKey, file);
29
- else
30
- parseJsonWithBuildKey(text, buildKey, file);
31
- return text;
32
- }
33
- async readJson(buildKey, file) {
34
- return parseJsonWithBuildKey((await this.payload(buildKey, file)).toString("utf8"), buildKey, file);
35
- }
36
- async readJsonl(buildKey, file) {
37
- return parseJsonlWithBuildKey((await this.payload(buildKey, file)).toString("utf8"), buildKey, file);
38
- }
39
- async payload(buildKey, file) {
40
- if (!REQUIRED_PAYLOADS.has(file))
41
- invalid(`unsupported payload ${String(file)}`);
42
- const bytes = (await this.load(buildKey)).payloads.get(file);
43
- if (!bytes)
44
- invalid(`${buildKey}/${file} is not present in the verified bundle`);
45
- return bytes;
46
- }
47
- load(buildKey) {
48
- const cached = this.cache.get(buildKey);
49
- if (cached)
50
- return cached;
51
- const pending = this.loadUncached(buildKey);
52
- this.cache.set(buildKey, pending);
53
- void pending.catch(() => {
54
- if (this.cache.get(buildKey) === pending)
55
- this.cache.delete(buildKey);
56
- });
57
- return pending;
58
- }
59
- async loadUncached(buildKey) {
60
- let directory;
61
- try {
62
- directory = buildBundleDirectory(this.profileRoot, buildKey);
63
- }
64
- catch (error) {
65
- invalid(`${buildKey}: ${errorText(error)}`);
66
- }
67
- const manifestPath = containedPath(directory, "manifest.json");
68
- let manifestBytes;
69
- try {
70
- manifestBytes = await readFile(manifestPath);
71
- }
72
- catch (error) {
73
- invalid(`${buildKey}/manifest.json: ${errorText(error)}`);
74
- }
75
- const manifest = validateManifest(parseJson(manifestBytes.toString("utf8"), "manifest.json"), buildKey);
76
- const entries = new Map(manifest.outputs.map(output => [output.file, output]));
77
- const payloadPairs = await Promise.all(BUILD_BUNDLE_PAYLOADS.map(async (file) => {
78
- const entry = entries.get(file);
79
- if (!entry)
80
- invalid(`${buildKey}/manifest.json: missing output ${file}`);
81
- const path = containedPath(directory, file);
82
- let bytes;
83
- try {
84
- bytes = await readFile(path);
85
- }
86
- catch (error) {
87
- invalid(`${buildKey}/${file}: ${errorText(error)}`);
88
- }
89
- if (bytes.length !== entry.size)
90
- invalid(`${buildKey}/${file}: size mismatch, expected ${entry.size}, got ${bytes.length}`);
91
- const actualHash = createHash("sha256").update(bytes).digest("hex");
92
- if (actualHash !== entry.sha256)
93
- invalid(`${buildKey}/${file}: sha256 mismatch, expected ${entry.sha256}, got ${actualHash}`);
94
- return [file, bytes];
95
- }));
96
- return { manifest, payloads: new Map(payloadPairs) };
97
- }
98
- }
99
- function validateManifest(value, buildKey) {
100
- if (!isRecord(value))
101
- invalid(`${buildKey}/manifest.json: root must be an object`);
102
- if (value.schema !== BUILD_BUNDLE_SCHEMA)
103
- invalid(`${buildKey}/manifest.json: schema must be ${BUILD_BUNDLE_SCHEMA}`);
104
- if (value.buildKey !== buildKey)
105
- invalid(`${buildKey}/manifest.json: buildKey mismatch`);
106
- const match = /^([a-z0-9_]+)@([0-9A-Za-z._-]+)$/.exec(buildKey);
107
- if (!match)
108
- invalid(`${buildKey}/manifest.json: invalid requested buildKey`);
109
- if (value.flavor !== match[1])
110
- invalid(`${buildKey}/manifest.json: flavor mismatch`);
111
- if (value.version !== match[2])
112
- invalid(`${buildKey}/manifest.json: version mismatch`);
113
- if (!Array.isArray(value.outputs) || value.outputs.length !== BUILD_BUNDLE_PAYLOADS.length) {
114
- invalid(`${buildKey}/manifest.json: outputs must contain exactly ${BUILD_BUNDLE_PAYLOADS.length} required payloads`);
115
- }
116
- const seen = new Set();
117
- const outputs = value.outputs.map((candidate, index) => {
118
- if (!isRecord(candidate))
119
- invalid(`${buildKey}/manifest.json: outputs[${index}] must be an object`);
120
- const file = candidate.file;
121
- if (typeof file !== "string" || !REQUIRED_PAYLOADS.has(file))
122
- invalid(`${buildKey}/manifest.json: invalid output file ${String(file)}`);
123
- if (file !== basename(file) || isAbsolute(file) || file === "." || file === "..")
124
- invalid(`${buildKey}/manifest.json: output path must be a basename: ${file}`);
125
- if (seen.has(file))
126
- invalid(`${buildKey}/manifest.json: duplicate output ${file}`);
127
- seen.add(file);
128
- if (!Number.isSafeInteger(candidate.size) || Number(candidate.size) < 0)
129
- invalid(`${buildKey}/manifest.json: invalid size for ${file}`);
130
- if (typeof candidate.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(candidate.sha256))
131
- invalid(`${buildKey}/manifest.json: invalid sha256 for ${file}`);
132
- return { file: file, size: Number(candidate.size), sha256: candidate.sha256 };
133
- });
134
- for (const file of BUILD_BUNDLE_PAYLOADS)
135
- if (!seen.has(file))
136
- invalid(`${buildKey}/manifest.json: missing output ${file}`);
137
- return { ...value, schema: BUILD_BUNDLE_SCHEMA, buildKey, flavor: match[1], version: match[2], outputs };
138
- }
139
- function containedPath(directory, file) {
140
- if (file !== basename(file) || isAbsolute(file))
141
- invalid(`payload path must be a basename: ${file}`);
142
- const path = resolve(directory, file);
143
- const child = relative(directory, path);
144
- if (!child || child.startsWith("..") || isAbsolute(child))
145
- invalid(`payload path escapes bundle directory: ${file}`);
146
- return path;
147
- }
148
- function parseJsonWithBuildKey(text, buildKey, file) {
149
- const value = parseJson(text, file);
150
- if (!isRecord(value))
151
- invalid(`${buildKey}/${file}: root must be an object`);
152
- if (value.buildKey !== buildKey)
153
- invalid(`${buildKey}/${file}: semantic buildKey mismatch`);
154
- return value;
155
- }
156
- function parseJsonlWithBuildKey(text, buildKey, file) {
157
- const records = text.split(/\r?\n/).filter(Boolean).map((line, index) => {
158
- const value = parseJson(line, `${file}:${index + 1}`);
159
- if (!isRecord(value))
160
- invalid(`${buildKey}/${file}:${index + 1}: record must be an object`);
161
- if (value.buildKey !== buildKey)
162
- invalid(`${buildKey}/${file}:${index + 1}: semantic buildKey mismatch`);
163
- return value;
164
- });
165
- return records;
166
- }
167
- function parseJson(text, label) {
168
- try {
169
- return JSON.parse(text);
170
- }
171
- catch (error) {
172
- invalid(`${label}: invalid JSON: ${errorText(error)}`);
173
- }
174
- }
175
- function isRecord(value) {
176
- return typeof value === "object" && value !== null && !Array.isArray(value);
177
- }
178
- function errorText(error) {
179
- return error instanceof Error ? error.message : String(error);
180
- }
181
- function invalid(message) {
182
- throw new Error(`BUILD_BUNDLE_INVALID: ${message}`);
183
- }
@@ -1,11 +0,0 @@
1
- import { resolve } from "node:path";
2
- export function buildBundleDirectory(profileRoot, buildKey) {
3
- const match = /^([a-z0-9_]+)@([0-9A-Za-z._-]+)$/.exec(buildKey);
4
- if (!match)
5
- throw new Error(`Invalid buildKey: ${buildKey}`);
6
- const root = resolve(profileRoot);
7
- const directory = resolve(root, match[1], match[2]);
8
- if (!directory.startsWith(root + "\\") && directory !== root)
9
- throw new Error(`Invalid buildKey path: ${buildKey}`);
10
- return directory;
11
- }