@hraness/ghostget 0.18.0 → 0.18.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 (76) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +7 -7
  3. package/dist/apple-photos-client.js +2 -1
  4. package/dist/beeper-client.js +2 -1
  5. package/dist/client.js +1 -0
  6. package/dist/imessage-direct-install-2gm3kge7.js +324 -0
  7. package/dist/index-01eeae9e.js +5 -0
  8. package/dist/index-2ymnp8xv.js +145 -0
  9. package/dist/index-en5hycxp.js +175 -0
  10. package/dist/{index-9wca02er.js → index-hfbygww8.js} +1 -1
  11. package/dist/index-n4szk3nw.js +50 -0
  12. package/dist/index-yq6maz71.js +1105 -0
  13. package/dist/index-z1w83f81.js +4 -0
  14. package/dist/index.js +4 -47
  15. package/dist/messaging-automation-api.js +21 -0
  16. package/dist/messaging-automation-j4274hvc.js +609 -0
  17. package/dist/messaging-native-install-8w1j36ah.js +16 -0
  18. package/dist/messaging.js +1 -0
  19. package/dist/omni-client.js +1 -0
  20. package/dist/whatsapp-automation-runtime-hgh1e9rm.js +845 -0
  21. package/dist/whatsapp-client.js +1 -0
  22. package/docs/control-panel.md +2 -2
  23. package/docs/imessage-direct-provider.md +31 -7
  24. package/docs/messaging-automation.md +103 -0
  25. package/package.json +22 -2
  26. package/skills/ghostget/references/control-panel.md +1 -1
  27. package/skills/ghostget/references/install.md +5 -5
  28. package/skills/ghostget/references/linkedin-adapter.md +209 -13
  29. package/skills/ghostget/references/platform-patterns.md +1 -1
  30. package/src/args.ts +18 -5
  31. package/src/assets/adapters/imessage/wrench-web-adapter.json +126 -0
  32. package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
  33. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +144 -0
  34. package/src/assets/messaging-runtime/NOTICE.txt +2580 -0
  35. package/src/assets/messaging-runtime/imsg-darwin-arm64.gz +0 -0
  36. package/src/assets/messaging-runtime/phone-number-info.plist.gz +0 -0
  37. package/src/assets/messaging-runtime/phone-number-metadata.json.gz +0 -0
  38. package/src/assets/messaging-runtime/phone-number-privacy.plist.gz +0 -0
  39. package/src/assets/messaging-runtime/wacli-darwin-arm64.gz +0 -0
  40. package/src/beeper-client-types.ts +1 -1
  41. package/src/ghostget.ts +16 -4
  42. package/src/messaging-automation-api.ts +12 -0
  43. package/src/messaging-automation-descriptors.ts +29 -0
  44. package/src/messaging-automation-factory.ts +148 -0
  45. package/src/messaging-automation-server.ts +183 -0
  46. package/src/messaging-automation-types.ts +155 -0
  47. package/src/messaging-automation-validation.ts +110 -0
  48. package/src/messaging-automation.ts +356 -0
  49. package/src/plugins/imessage-direct/plugin.ts +12 -1
  50. package/src/plugins/imessage-direct/vendor/0003-feat-rpc-add-no-fetch-rich-cards.patch +276 -0
  51. package/src/plugins/imessage-direct/vendor/provenance.json +84 -10
  52. package/src/plugins/linkedin-web/plugin.ts +1 -1
  53. package/src/plugins/whatsapp-linked-device/plugin.ts +7 -2
  54. package/src/plugins/whatsapp-linked-device/vendor/0001-ghostget-private-messaging.patch +1093 -0
  55. package/src/plugins/whatsapp-linked-device/vendor/README.md +39 -0
  56. package/src/plugins/whatsapp-linked-device/vendor/provenance.json +45 -0
  57. package/src/provider-plugin.ts +14 -0
  58. package/src/providers/imessage-automation.ts +250 -0
  59. package/src/providers/imessage-direct-install.ts +7 -0
  60. package/src/providers/imessage-direct-runtime.ts +32 -2
  61. package/src/providers/imessage-direct.ts +13 -3
  62. package/src/providers/linkedin-contact-failure.ts +21 -0
  63. package/src/providers/linkedin-contact-platform.ts +8 -1
  64. package/src/providers/linkedin-contact-program.ts +34 -5
  65. package/src/providers/linkedin-web-contact.ts +1023 -51
  66. package/src/providers/linkedin-web-profile-browser.ts +659 -45
  67. package/src/providers/linkedin-web-runtime.ts +1 -0
  68. package/src/providers/linkedin-web.ts +46 -2
  69. package/src/providers/messaging-native-artifacts.ts +38 -0
  70. package/src/providers/messaging-native-install.ts +75 -0
  71. package/src/providers/whatsapp-automation-runtime.ts +240 -0
  72. package/src/providers/whatsapp-automation.ts +153 -0
  73. package/src/usage.ts +3 -1
  74. package/src/version.ts +1 -1
  75. package/src/web-session-contract-definitions.ts +1 -1
  76. package/tsconfig.json +3 -0
@@ -0,0 +1,845 @@
1
+ // @bun
2
+ import {
3
+ automationDigest,
4
+ automationRecord,
5
+ automationText
6
+ } from "./index-2ymnp8xv.js";
7
+ import {
8
+ assertSafeStatePath,
9
+ captureProcessOwnerIdentity,
10
+ ensurePrivateStateDirectory,
11
+ ghostgetStateHome,
12
+ processOwnerStatus
13
+ } from "./index-yq6maz71.js";
14
+ import {
15
+ startProviderPluginCleanupTrackedOperation
16
+ } from "./index-n4szk3nw.js";
17
+ import {
18
+ canonicalJson
19
+ } from "./index-gwk7rbyj.js";
20
+ import"./index-z1w83f81.js";
21
+
22
+ // src/providers/whatsapp-automation-runtime.ts
23
+ import { Database } from "bun:sqlite";
24
+ import { createHash, randomBytes } from "crypto";
25
+ import { constants } from "fs";
26
+ import { chmod as chmod2, link, lstat as lstat2, mkdtemp as mkdtemp2, open as open2, realpath as realpath2, rmdir as rmdir2, unlink as unlink2 } from "fs/promises";
27
+ import { createConnection } from "net";
28
+ import { tmpdir as tmpdir2 } from "os";
29
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3 } from "path";
30
+
31
+ // src/provider-plugin-cleanup-resource.ts
32
+ import {
33
+ chmodSync,
34
+ lstatSync,
35
+ mkdtempSync,
36
+ realpathSync,
37
+ rmdirSync
38
+ } from "fs";
39
+ import { tmpdir } from "os";
40
+ import { basename, dirname, isAbsolute, join, resolve } from "path";
41
+ import { types as nodeTypes } from "util";
42
+ var decimalPattern = /^(?:0|[1-9][0-9]{0,39})$/u;
43
+ var privateRootNamePattern = /^wrench-[a-z0-9][a-z0-9._-]{0,126}$/u;
44
+ function hasWellFormedUnicode(value) {
45
+ for (let index = 0;index < value.length; index += 1) {
46
+ const code = value.charCodeAt(index);
47
+ if (code >= 55296 && code <= 56319) {
48
+ if (index + 1 >= value.length)
49
+ return false;
50
+ const next = value.charCodeAt(index + 1);
51
+ if (next < 56320 || next > 57343)
52
+ return false;
53
+ index += 1;
54
+ } else if (code >= 56320 && code <= 57343)
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ function recoverablePrivateRootStats(stats, currentUid) {
60
+ return stats.isDirectory() && !stats.isSymbolicLink() && currentUid !== undefined && stats.uid === BigInt(currentUid) && (stats.mode & 0o777n) === 0o700n && stats.birthtimeNs > 0n;
61
+ }
62
+ function record(value, label) {
63
+ if (typeof value !== "object" || value === null || Array.isArray(value) || nodeTypes.isProxy(value)) {
64
+ throw new Error(`${label} must be a plain data object`);
65
+ }
66
+ const prototype = Object.getPrototypeOf(value);
67
+ if (prototype !== Object.prototype && prototype !== null) {
68
+ throw new Error(`${label} must be a plain data object`);
69
+ }
70
+ const descriptors = Object.getOwnPropertyDescriptors(value);
71
+ const result = {};
72
+ for (const key of Reflect.ownKeys(descriptors)) {
73
+ if (typeof key !== "string") {
74
+ throw new Error(`${label} has unsupported symbol fields`);
75
+ }
76
+ const descriptor = descriptors[key];
77
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor) || !hasWellFormedUnicode(key) || /[\u0000-\u001f\u007f-\u009f]/u.test(key)) {
78
+ throw new Error(`${label} has unsupported accessor fields`);
79
+ }
80
+ result[key] = descriptor.value;
81
+ }
82
+ return result;
83
+ }
84
+ function exactKeys(value, required, optional, label) {
85
+ const allowed = new Set([...required, ...optional]);
86
+ if (required.some((key) => !Object.hasOwn(value, key)) || Object.keys(value).some((key) => !allowed.has(key))) {
87
+ throw new Error(`${label} has unsupported fields`);
88
+ }
89
+ }
90
+ function decimal(value, label) {
91
+ if (typeof value !== "string" || !decimalPattern.test(value)) {
92
+ throw new Error(`${label} is malformed`);
93
+ }
94
+ return value;
95
+ }
96
+ function pid(value, label) {
97
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
98
+ throw new Error(`${label} is malformed`);
99
+ }
100
+ return value;
101
+ }
102
+ function digest(value, label) {
103
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
104
+ throw new Error(`${label} is malformed`);
105
+ }
106
+ return value;
107
+ }
108
+ function parseProcessOwner(value) {
109
+ const owner = record(value, "local CLI cleanup process owner");
110
+ exactKeys(owner, ["pid", "bootId", "processStartId"], [], "local CLI cleanup process owner");
111
+ return Object.freeze({
112
+ pid: pid(owner.pid, "local CLI cleanup process owner PID"),
113
+ bootId: digest(owner.bootId, "local CLI cleanup process boot identity"),
114
+ processStartId: digest(owner.processStartId, "local CLI cleanup process start identity")
115
+ });
116
+ }
117
+ function temporaryRootPath(value) {
118
+ if (typeof value !== "string" || value.length < 1 || value.length > 4096 || !isAbsolute(value) || resolve(value) !== value || dirname(value) !== realpathSync(tmpdir()) || !privateRootNamePattern.test(basename(value))) {
119
+ throw new Error("local CLI cleanup private root path is malformed");
120
+ }
121
+ return value;
122
+ }
123
+ function parsePrivateRoot(value) {
124
+ const root = record(value, "local CLI cleanup private root");
125
+ exactKeys(root, ["path", "device", "inode", "birthtimeNs", "mode", "uid"], [], "local CLI cleanup private root");
126
+ const mode = decimal(root.mode, "local CLI cleanup private root mode");
127
+ if (mode !== "448") {
128
+ throw new Error("local CLI cleanup private root must have mode 0700");
129
+ }
130
+ const birthtimeNs = decimal(root.birthtimeNs, "local CLI cleanup private root birth time");
131
+ if (birthtimeNs === "0") {
132
+ throw new Error("local CLI cleanup private root birth time is malformed");
133
+ }
134
+ return Object.freeze({
135
+ path: temporaryRootPath(root.path),
136
+ device: decimal(root.device, "local CLI cleanup private root device"),
137
+ inode: decimal(root.inode, "local CLI cleanup private root inode"),
138
+ birthtimeNs,
139
+ mode: "448",
140
+ uid: decimal(root.uid, "local CLI cleanup private root owner")
141
+ });
142
+ }
143
+ function parseProcessGroup(value) {
144
+ const group = record(value, "local CLI cleanup process group");
145
+ exactKeys(group, ["kind", "platform", "processGroupId", "leader"], [], "local CLI cleanup process group");
146
+ if (group.kind !== "posix-process-group-v1" || group.platform !== "darwin" && group.platform !== "linux") {
147
+ throw new Error("local CLI cleanup process group is malformed");
148
+ }
149
+ const processGroupId = pid(group.processGroupId, "local CLI cleanup process group ID");
150
+ const leader = parseProcessOwner(group.leader);
151
+ if (leader.pid !== processGroupId) {
152
+ throw new Error("local CLI cleanup process group does not match its leader");
153
+ }
154
+ return Object.freeze({
155
+ kind: "posix-process-group-v1",
156
+ platform: group.platform,
157
+ processGroupId,
158
+ leader
159
+ });
160
+ }
161
+ function parseLocalCliCleanupResourceIdentityV1(value) {
162
+ const resource = record(value, "local CLI cleanup resource identity");
163
+ exactKeys(resource, ["kind", "root"], ["processGroups"], "local CLI cleanup resource identity");
164
+ if (resource.kind !== "local-cli-private-root-v1") {
165
+ throw new Error("local CLI cleanup resource identity kind is unsupported");
166
+ }
167
+ const root = parsePrivateRoot(resource.root);
168
+ let processGroups;
169
+ if (resource.processGroups !== undefined) {
170
+ const rawGroups = resource.processGroups;
171
+ if (!Array.isArray(rawGroups) || nodeTypes.isProxy(rawGroups) || Object.getPrototypeOf(rawGroups) !== Array.prototype || rawGroups.length < 1 || rawGroups.length > 64) {
172
+ throw new Error("local CLI cleanup process group history is malformed");
173
+ }
174
+ const descriptors = Object.getOwnPropertyDescriptors(rawGroups);
175
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key !== "string") || Object.keys(descriptors).length !== rawGroups.length + 1 || !Object.entries(descriptors).every(([key, descriptor]) => key === "length" ? !descriptor.enumerable && "value" in descriptor && descriptor.value === rawGroups.length : descriptor.enumerable && ("value" in descriptor))) {
176
+ throw new Error("local CLI cleanup process group history is malformed");
177
+ }
178
+ processGroups = Object.freeze(Array.from({ length: rawGroups.length }, (_unused, index) => {
179
+ const descriptor = descriptors[String(index)];
180
+ if (descriptor === undefined || !("value" in descriptor)) {
181
+ throw new Error("local CLI cleanup process group history is sparse");
182
+ }
183
+ return parseProcessGroup(descriptor.value);
184
+ }));
185
+ }
186
+ return Object.freeze({
187
+ kind: "local-cli-private-root-v1",
188
+ root,
189
+ ...processGroups === undefined ? {} : { processGroups }
190
+ });
191
+ }
192
+ function captureLocalCliCleanupResource(path) {
193
+ const canonical = realpathSync(path);
194
+ const checkedPath = temporaryRootPath(canonical);
195
+ const stats = lstatSync(checkedPath, { bigint: true });
196
+ const currentUid = process.getuid?.();
197
+ if (!recoverablePrivateRootStats(stats, currentUid)) {
198
+ throw new Error("local CLI cleanup resource must be one owned mode-0700 temporary directory");
199
+ }
200
+ return parseLocalCliCleanupResourceIdentityV1({
201
+ kind: "local-cli-private-root-v1",
202
+ root: {
203
+ path: checkedPath,
204
+ device: stats.dev.toString(),
205
+ inode: stats.ino.toString(),
206
+ birthtimeNs: stats.birthtimeNs.toString(),
207
+ mode: "448",
208
+ uid: stats.uid.toString()
209
+ }
210
+ });
211
+ }
212
+ function processGroupIsPresent(processGroupId) {
213
+ try {
214
+ process.kill(-processGroupId, 0);
215
+ return true;
216
+ } catch (error) {
217
+ if (typeof error !== "object" || error === null || !("code" in error)) {
218
+ return null;
219
+ }
220
+ if (error.code === "ESRCH")
221
+ return false;
222
+ if (error.code === "EPERM")
223
+ return true;
224
+ return null;
225
+ }
226
+ }
227
+ function attachLocalCliCleanupProcessGroup(value, childPid) {
228
+ const resource = parseLocalCliCleanupResourceIdentityV1(value);
229
+ if ((resource.processGroups?.length ?? 0) >= 64) {
230
+ throw new Error("local CLI cleanup process group history exceeded its bound");
231
+ }
232
+ if (process.platform !== "darwin" && process.platform !== "linux") {
233
+ throw new Error("local CLI cleanup process groups require a POSIX runtime");
234
+ }
235
+ const leader = captureProcessOwnerIdentity(childPid);
236
+ if (processGroupIsPresent(childPid) !== true) {
237
+ throw new Error("local CLI cleanup child does not own a live process group");
238
+ }
239
+ return parseLocalCliCleanupResourceIdentityV1({
240
+ ...resource,
241
+ processGroups: [
242
+ ...resource.processGroups ?? [],
243
+ {
244
+ kind: "posix-process-group-v1",
245
+ platform: process.platform,
246
+ processGroupId: childPid,
247
+ leader
248
+ }
249
+ ]
250
+ });
251
+ }
252
+ function localCliCleanupProcessGroupStatus(resource, inspectOwner = processOwnerStatus) {
253
+ const checked = parseLocalCliCleanupResourceIdentityV1(resource);
254
+ const groups = checked.processGroups;
255
+ if (groups === undefined) {
256
+ return "unknown";
257
+ }
258
+ let unknown = false;
259
+ for (const group of groups) {
260
+ if (group.platform !== process.platform)
261
+ return "unknown";
262
+ const leader = inspectOwner(group.leader);
263
+ if (leader === "exact-live-owner")
264
+ return "active";
265
+ if (leader === "unknown") {
266
+ unknown = true;
267
+ continue;
268
+ }
269
+ const present = processGroupIsPresent(group.processGroupId);
270
+ if (present === true)
271
+ return "active";
272
+ if (present === null)
273
+ unknown = true;
274
+ }
275
+ return unknown ? "unknown" : "quiescent";
276
+ }
277
+
278
+ // src/providers/whatsapp-web-runtime.ts
279
+ import {
280
+ chmod,
281
+ lstat,
282
+ mkdtemp,
283
+ mkdir,
284
+ open,
285
+ readdir,
286
+ realpath,
287
+ rmdir,
288
+ unlink
289
+ } from "fs/promises";
290
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
291
+ import { BoundedByteBuffer } from "@hraness/kb/clip/bounded-byte-buffer";
292
+ var MAX_STDERR_BYTES = 64 * 1024;
293
+ var MAX_STORE_ENTRIES = 1e4;
294
+ var MAX_CONTACT_PROJECTION_STDERR_BYTES = 16 * 1024;
295
+ var MESSAGE_EXPORT_SESSION_MAX_TOTAL_STDOUT_BYTES = 512 * 1024 * 1024;
296
+ var MESSAGE_EXPORT_SESSION_MAX_FRAMES = 1001;
297
+ var MESSAGE_EXPORT_SESSION_SPOOL_CHUNK_BYTES = 64 * 1024;
298
+ function ownedByCurrentUser(stats) {
299
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
300
+ return uid === null || stats.uid === uid;
301
+ }
302
+ function assertPrivateOwned(stats, label, kind) {
303
+ const matchesKind = kind === "directory" ? stats.isDirectory() : kind === "file" ? stats.isFile() : stats.isSocket();
304
+ if (!matchesKind || !ownedByCurrentUser(stats)) {
305
+ throw new Error(`${label} must be an owned ${kind}`);
306
+ }
307
+ if ((stats.mode & 63) !== 0) {
308
+ throw new Error(`${label} must not grant group or world access`);
309
+ }
310
+ }
311
+ async function validateWhatsAppStoreDirectory(pathValue, purpose) {
312
+ if (!isAbsolute2(pathValue)) {
313
+ throw new Error("WhatsApp linked-device store path must be absolute");
314
+ }
315
+ const lexical = resolve2(pathValue);
316
+ let directoryStats;
317
+ try {
318
+ directoryStats = await lstat(lexical);
319
+ } catch (error) {
320
+ if (purpose === "pair" && typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
321
+ await mkdir(lexical, { recursive: true, mode: 448 });
322
+ await chmod(lexical, 448);
323
+ directoryStats = await lstat(lexical);
324
+ } else {
325
+ throw new Error("WhatsApp linked-device store is unavailable");
326
+ }
327
+ }
328
+ if (directoryStats.isSymbolicLink()) {
329
+ throw new Error("WhatsApp linked-device store must not be a symbolic link");
330
+ }
331
+ assertPrivateOwned(directoryStats, "WhatsApp linked-device store", "directory");
332
+ const canonical = await realpath(lexical);
333
+ if (canonical !== lexical) {
334
+ throw new Error("WhatsApp linked-device store path must be canonical");
335
+ }
336
+ const entries = await readdir(canonical, { withFileTypes: true });
337
+ if (entries.length > MAX_STORE_ENTRIES) {
338
+ throw new Error("WhatsApp linked-device store has too many entries");
339
+ }
340
+ for (const entry of entries) {
341
+ if (entry.name.length < 1 || entry.name.length > 255 || entry.name.includes("\x00") || entry.isSymbolicLink())
342
+ throw new Error("WhatsApp linked-device store contains an unsafe entry");
343
+ const entryPath = join2(canonical, entry.name);
344
+ const stats = await lstat(entryPath);
345
+ if (stats.isSymbolicLink()) {
346
+ throw new Error("WhatsApp linked-device store contains a symbolic link");
347
+ }
348
+ if (stats.isDirectory()) {
349
+ assertPrivateOwned(stats, "WhatsApp linked-device store entry", "directory");
350
+ } else if (stats.isFile()) {
351
+ assertPrivateOwned(stats, "WhatsApp linked-device store entry", "file");
352
+ } else if (stats.isSocket()) {
353
+ assertPrivateOwned(stats, "WhatsApp linked-device store entry", "socket");
354
+ } else {
355
+ throw new Error("WhatsApp linked-device store contains an unsupported entry");
356
+ }
357
+ }
358
+ const requireRegular = async (name) => {
359
+ let stats;
360
+ try {
361
+ stats = await lstat(join2(canonical, name));
362
+ } catch {
363
+ throw new Error(`WhatsApp linked-device store omitted ${name}`);
364
+ }
365
+ assertPrivateOwned(stats, `WhatsApp ${name}`, "file");
366
+ };
367
+ if (purpose === "sync" || purpose === "projection" || purpose === "contact-projection") {
368
+ await requireRegular("session.db");
369
+ }
370
+ if (purpose === "projection") {
371
+ await requireRegular("wacli.db");
372
+ }
373
+ return canonical;
374
+ }
375
+ class CanonicalSessionFrameDecoder {
376
+ #maximumFrameBytes;
377
+ #onFrame;
378
+ #chunks = [];
379
+ #pendingBytes = 0;
380
+ #frameCount = 0;
381
+ constructor(maximumFrameBytes, onFrame) {
382
+ this.#maximumFrameBytes = maximumFrameBytes;
383
+ this.#onFrame = onFrame;
384
+ }
385
+ get frameCount() {
386
+ return this.#frameCount;
387
+ }
388
+ #append(bytes) {
389
+ if (bytes.byteLength === 0)
390
+ return;
391
+ this.#pendingBytes += bytes.byteLength;
392
+ if (this.#pendingBytes + 1 > this.#maximumFrameBytes) {
393
+ throw new Error("WhatsApp projection session frame exceeded its bound");
394
+ }
395
+ this.#chunks.push(Buffer.from(bytes));
396
+ }
397
+ #finishLine() {
398
+ if (this.#pendingBytes < 2 || this.#pendingBytes + 1 > this.#maximumFrameBytes || this.#frameCount >= MESSAGE_EXPORT_SESSION_MAX_FRAMES)
399
+ throw new Error("WhatsApp projection session frame exceeded its bound");
400
+ const bytes = this.#chunks.length === 1 ? this.#chunks[0] : Buffer.concat(this.#chunks, this.#pendingBytes);
401
+ this.#chunks = [];
402
+ this.#pendingBytes = 0;
403
+ let line;
404
+ try {
405
+ line = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
406
+ } catch {
407
+ throw new Error("WhatsApp projection session frame was malformed");
408
+ }
409
+ if (line.includes("\r")) {
410
+ throw new Error("WhatsApp projection session frame exceeded its bound");
411
+ }
412
+ let value;
413
+ try {
414
+ value = JSON.parse(line);
415
+ } catch {
416
+ throw new Error("WhatsApp projection session frame was malformed");
417
+ }
418
+ const canonical = canonicalJson(value);
419
+ if (canonical !== line || !Buffer.from(canonical, "utf8").equals(bytes)) {
420
+ throw new Error("WhatsApp projection session frame was not canonical");
421
+ }
422
+ this.#frameCount += 1;
423
+ this.#onFrame(Object.freeze({
424
+ index: this.#frameCount,
425
+ canonical,
426
+ value
427
+ }));
428
+ }
429
+ push(bytes) {
430
+ let start = 0;
431
+ for (let index = 0;index < bytes.byteLength; index += 1) {
432
+ if (bytes[index] !== 10)
433
+ continue;
434
+ this.#append(bytes.subarray(start, index));
435
+ this.#finishLine();
436
+ start = index + 1;
437
+ }
438
+ this.#append(bytes.subarray(start));
439
+ }
440
+ finish() {
441
+ if (this.#pendingBytes !== 0 || this.#chunks.length !== 0) {
442
+ throw new Error("WhatsApp projection session ended inside a frame");
443
+ }
444
+ }
445
+ }
446
+
447
+ // src/providers/whatsapp-automation-runtime.ts
448
+ var WHATSAPP_AUTOMATION_PROTOCOL = "ghostget.whatsapp-private/1";
449
+ var WHATSAPP_AUTOMATION_VERSION = "0.15.0+ghostget-private.1";
450
+ var WHATSAPP_AUTOMATION_BINARY_SHA256 = "9b77ffb810d028fde725ca02b1451f1725b5ff5312a46a54468a9a38533d4cea";
451
+ var directJid = /^(?:[1-9][0-9]{4,14}@s\.whatsapp\.net|[1-9][0-9]{4,19}@lid)$/u;
452
+ var sha = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
453
+ function linked(auth) {
454
+ if (auth.kind !== "linked-device-store" || auth.provider !== "whatsapp" || !auth.subject)
455
+ throw new Error("A bound WhatsApp linked-device account is required");
456
+ return auth;
457
+ }
458
+ function accountSubject(jid) {
459
+ if (!directJid.test(jid))
460
+ throw new Error("Unsupported WhatsApp account identity");
461
+ return `whatsapp:${jid.endsWith("@lid") ? "lid" : "pn"}:${jid.split("@")[0]}`;
462
+ }
463
+ function accountJid(value) {
464
+ const jid = automationText(value, 128);
465
+ const normalized = jid.replace(/:[0-9]{1,5}(?=@(?:s\.whatsapp\.net|lid)$)/u, "");
466
+ accountSubject(normalized);
467
+ return normalized;
468
+ }
469
+ async function privateFile(path) {
470
+ const info = await lstat2(path, { bigint: true });
471
+ if (!info.isFile() || info.isSymbolicLink() || info.uid !== BigInt(process.getuid()) || info.nlink !== 1n || (info.mode & 0o777n) !== 0o600n || info.size < 1n || info.size > 4294967296n || info.birthtimeNs <= 0n || await realpath2(path) !== path)
472
+ throw new Error("Unsafe WhatsApp source file");
473
+ return { dev: String(info.dev), ino: String(info.ino), birthtimeNs: String(info.birthtimeNs) };
474
+ }
475
+ async function binaryBytes(path) {
476
+ const file = await open2(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
477
+ try {
478
+ const before = await file.stat();
479
+ if (!before.isFile() || before.nlink !== 1 || before.size < 1 || before.size > 128 * 1024 * 1024 || (before.mode & 18) !== 0 || (before.mode & 73) === 0 || ![0, process.getuid()].includes(before.uid))
480
+ throw new Error("Unsafe WhatsApp automation executable");
481
+ const bytes = await file.readFile(), after = await file.stat();
482
+ if (bytes.length !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
483
+ throw new Error("WhatsApp automation executable changed");
484
+ return bytes;
485
+ } finally {
486
+ await file.close();
487
+ }
488
+ }
489
+ async function binaryDigest(path) {
490
+ return createHash("sha256").update(await binaryBytes(path)).digest("hex");
491
+ }
492
+ function whatsappAutomationBinaryPath(environment = process.env) {
493
+ if (process.platform !== "darwin" || process.arch !== "arm64")
494
+ throw new Error("The reviewed WhatsApp automation build requires Apple silicon macOS");
495
+ return join3(ghostgetStateHome(environment), "tools", "wacli-private", WHATSAPP_AUTOMATION_VERSION, "darwin-arm64", "wacli");
496
+ }
497
+ async function resolveWhatsAppAutomationBinary(environment = process.env) {
498
+ const path = whatsappAutomationBinaryPath(environment);
499
+ if (await realpath2(path) !== path || await binaryDigest(path) !== WHATSAPP_AUTOMATION_BINARY_SHA256)
500
+ throw new Error("The reviewed one-attempt WhatsApp automation runtime is not installed");
501
+ return path;
502
+ }
503
+ async function installReviewedWhatsAppAutomationBinary(source, environment = process.env) {
504
+ if (!isAbsolute3(source))
505
+ throw new Error("WhatsApp automation install source must be absolute");
506
+ const bytes = await binaryBytes(source);
507
+ if (createHash("sha256").update(bytes).digest("hex") !== WHATSAPP_AUTOMATION_BINARY_SHA256)
508
+ throw new Error("WhatsApp automation install source does not match the reviewed build");
509
+ const destination = whatsappAutomationBinaryPath(environment);
510
+ ensurePrivateStateDirectory(dirname3(destination), environment);
511
+ assertSafeStatePath(destination, environment, false);
512
+ try {
513
+ await lstat2(destination);
514
+ if (await binaryDigest(destination) !== WHATSAPP_AUTOMATION_BINARY_SHA256)
515
+ throw new Error("WhatsApp automation install destination contains different bytes");
516
+ return { version: WHATSAPP_AUTOMATION_VERSION, sha256: WHATSAPP_AUTOMATION_BINARY_SHA256 };
517
+ } catch (error) {
518
+ if (error.code !== "ENOENT")
519
+ throw error;
520
+ }
521
+ const temporary = join3(dirname3(destination), `.wacli-install-${randomBytes(16).toString("hex")}`);
522
+ const file = await open2(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 320);
523
+ try {
524
+ try {
525
+ await file.writeFile(bytes);
526
+ await file.sync();
527
+ } finally {
528
+ await file.close();
529
+ }
530
+ try {
531
+ await link(temporary, destination);
532
+ } catch (error) {
533
+ if (error.code !== "EEXIST")
534
+ throw error;
535
+ }
536
+ } finally {
537
+ await unlink2(temporary);
538
+ }
539
+ const directory = await open2(dirname3(destination), constants.O_RDONLY);
540
+ try {
541
+ await directory.sync();
542
+ } finally {
543
+ await directory.close();
544
+ }
545
+ await resolveWhatsAppAutomationBinary(environment);
546
+ return { version: WHATSAPP_AUTOMATION_VERSION, sha256: WHATSAPP_AUTOMATION_BINARY_SHA256 };
547
+ }
548
+ function parseWhatsAppPrivateResponse(value) {
549
+ const row = automationRecord(value, ["protocol", "requestId", "generation", "account", "state", "to", "messageId", "connected"]);
550
+ if (row.protocol !== WHATSAPP_AUTOMATION_PROTOCOL || !["ready", "accepted", "not-started", "indeterminate"].includes(String(row.state)) || typeof row.connected !== "boolean")
551
+ throw new Error("Unsupported WhatsApp transport response");
552
+ for (const field of ["requestId", "to", "messageId"])
553
+ if (typeof row[field] !== "string" || Buffer.byteLength(row[field]) > 256 || /[\u0000-\u001f\u007f]/u.test(row[field]))
554
+ throw new Error("Invalid WhatsApp receipt field");
555
+ const generation = automationDigest(row.generation), account = automationText(row.account, 128);
556
+ if (!directJid.test(account) || row.requestId !== "" && !/^[a-f0-9]{64}$/u.test(row.requestId) || row.to !== "" && !directJid.test(row.to) || row.state === "accepted" && (row.messageId === "" || row.requestId === "" || row.to === ""))
557
+ throw new Error("Invalid WhatsApp receipt identity");
558
+ return { protocol: WHATSAPP_AUTOMATION_PROTOCOL, requestId: row.requestId, generation, account, state: row.state, to: row.to, messageId: row.messageId, connected: row.connected };
559
+ }
560
+ var blankStatus = () => ({ protocol: WHATSAPP_AUTOMATION_PROTOCOL, kind: "status", requestId: "", generation: "", account: "", to: "", message: "", file: "", filename: "", mime: "", id: "", reaction: "", question: "", options: [], selectable: 0 });
561
+ async function socketRequest(path, identity, request, signal, beforeWrite) {
562
+ signal?.throwIfAborted();
563
+ const info = await lstat2(path);
564
+ if (!info.isSocket() || info.isSymbolicLink() || info.uid !== process.getuid() || (info.mode & 511) !== 384 || info.dev !== identity.dev || info.ino !== identity.ino)
565
+ throw new Error("WhatsApp transport ownership changed");
566
+ return new Promise((resolve3, reject) => {
567
+ const socket = createConnection({ path });
568
+ let bytes = Buffer.alloc(0), settled = false;
569
+ const finish = (error, response) => {
570
+ if (settled)
571
+ return;
572
+ settled = true;
573
+ clearTimeout(timer);
574
+ signal?.removeEventListener("abort", abort);
575
+ socket.destroy();
576
+ error ? reject(error) : resolve3(response);
577
+ };
578
+ const abort = () => finish(new Error("WhatsApp transport request was interrupted"));
579
+ const timer = setTimeout(() => finish(new Error("WhatsApp transport response deadline exceeded")), 55000);
580
+ signal?.addEventListener("abort", abort, { once: true });
581
+ if (signal?.aborted) {
582
+ abort();
583
+ return;
584
+ }
585
+ socket.on("error", () => finish(new Error("WhatsApp transport connection failed")));
586
+ socket.on("close", () => {
587
+ if (!settled)
588
+ finish(new Error("WhatsApp transport closed without a receipt"));
589
+ });
590
+ socket.on("connect", () => {
591
+ (async () => {
592
+ await beforeWrite?.();
593
+ signal?.throwIfAborted();
594
+ if (settled)
595
+ return;
596
+ const current = await lstat2(path);
597
+ if (!current.isSocket() || current.isSymbolicLink() || current.dev !== identity.dev || current.ino !== identity.ino || current.uid !== process.getuid() || (current.mode & 511) !== 384)
598
+ throw new Error("WhatsApp socket changed after connection");
599
+ signal?.throwIfAborted();
600
+ if (settled)
601
+ return;
602
+ const payload = Buffer.from(JSON.stringify(request) + `
603
+ `);
604
+ if (payload.length > 1048576)
605
+ finish(new Error("WhatsApp request exceeds its byte bound"));
606
+ else
607
+ socket.write(payload);
608
+ })().catch(() => finish(new Error("WhatsApp request admission changed before writing")));
609
+ });
610
+ socket.on("data", (chunk) => {
611
+ if (!Buffer.isBuffer(chunk)) {
612
+ finish(new Error("WhatsApp response was not bytes"));
613
+ return;
614
+ }
615
+ if (bytes.length + chunk.length > 4096) {
616
+ finish(new Error("WhatsApp response exceeds its byte bound"));
617
+ return;
618
+ }
619
+ bytes = Buffer.concat([bytes, chunk]);
620
+ const newline = bytes.indexOf(10);
621
+ if (newline < 0)
622
+ return;
623
+ try {
624
+ if (newline !== bytes.length - 1)
625
+ throw new Error("Trailing WhatsApp response data");
626
+ const response = parseWhatsAppPrivateResponse(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, newline))));
627
+ if (request.kind === "status" ? response.state !== "ready" || response.requestId !== "" || response.to !== "" || response.messageId !== "" : response.requestId !== request.requestId || response.to !== request.to || response.account !== request.account || response.generation !== request.generation || response.state === "ready")
628
+ throw new Error("WhatsApp response did not bind its request");
629
+ finish(undefined, response);
630
+ } catch {
631
+ finish(new Error("WhatsApp transport returned a malformed receipt"));
632
+ }
633
+ });
634
+ });
635
+ }
636
+ var sleep = (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds));
637
+ function createWhatsAppAutomationRuntime(execution) {
638
+ let child;
639
+ let resource, cleanup;
640
+ let socket;
641
+ let authHash, root, closed = false, closing;
642
+ const staged = new Set;
643
+ const runtime = {
644
+ async read(auth, work, signal) {
645
+ signal?.throwIfAborted();
646
+ if (closed)
647
+ throw new Error("WhatsApp automation runtime is closed");
648
+ const selected = linked(auth), store = await validateWhatsAppStoreDirectory(selected.path, "projection");
649
+ const messagesPath = join3(store, "wacli.db"), sessionPath = join3(store, "session.db");
650
+ const before = [await privateFile(messagesPath), await privateFile(sessionPath)];
651
+ const session = new Database(sessionPath, { readonly: true, strict: true });
652
+ let account;
653
+ try {
654
+ const devices = session.query("SELECT jid FROM whatsmeow_device LIMIT 2").all();
655
+ if (devices.length !== 1)
656
+ throw new Error("WhatsApp session must select exactly one account");
657
+ account = accountJid(automationRecord(devices[0], ["jid"]).jid);
658
+ } finally {
659
+ session.close();
660
+ }
661
+ const subject = accountSubject(account);
662
+ if (selected.subject !== subject)
663
+ throw new Error("WhatsApp linked-device account changed");
664
+ let connected = false, generation = null;
665
+ if (socket && child && child.exitCode === null && authHash === sha(auth)) {
666
+ const status = await runtime.request(blankStatus(), signal);
667
+ if (status.account !== account)
668
+ throw new Error("WhatsApp connection account changed");
669
+ connected = status.connected;
670
+ generation = status.generation;
671
+ }
672
+ const database = new Database(messagesPath, { readonly: true, strict: true });
673
+ try {
674
+ database.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN");
675
+ const available = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='ghostget_automation_state'").get();
676
+ let ledger = null;
677
+ if (available) {
678
+ const state = automationRecord(database.query("SELECT version,generation FROM ghostget_automation_state WHERE singleton=1").get(), ["version", "generation"]);
679
+ if (state.version !== 1)
680
+ throw new Error("Unsupported WhatsApp event schema");
681
+ ledger = automationDigest(state.generation);
682
+ }
683
+ const snapshot = { account, subject, sourceGeneration: sha({ files: before, ledger }), ledgerReady: ledger !== null, connected, generation };
684
+ const result = work(database, snapshot);
685
+ database.exec("COMMIT");
686
+ if (sha(before) !== sha([await privateFile(messagesPath), await privateFile(sessionPath)]))
687
+ throw new Error("WhatsApp database generation changed during the read");
688
+ signal?.throwIfAborted();
689
+ return result;
690
+ } finally {
691
+ database.close();
692
+ }
693
+ },
694
+ async start(auth, beforeSpawn, signal) {
695
+ if (closed || child || root)
696
+ throw new Error("WhatsApp automation has already been started or closed");
697
+ if (!execution.registerCleanupBarrier)
698
+ throw new Error("WhatsApp automation requires durable process custody");
699
+ const selected = linked(auth), store = await validateWhatsAppStoreDirectory(selected.path, "projection");
700
+ await runtime.read(auth, () => {
701
+ return;
702
+ }, signal);
703
+ const binary = await resolveWhatsAppAutomationBinary(execution.environment);
704
+ await startProviderPluginCleanupTrackedOperation(execution.registerCleanupBarrier, async (publish, controller) => {
705
+ cleanup = controller;
706
+ try {
707
+ root = await mkdtemp2(join3(await realpath2(tmpdir2()), "wrench-whatsapp-automation-"));
708
+ await chmod2(root, 448);
709
+ resource = captureLocalCliCleanupResource(root);
710
+ publish?.(resource);
711
+ const socketPath = join3(root, ".s");
712
+ if (Buffer.byteLength(socketPath) > 100)
713
+ throw new Error("WhatsApp private runtime directory exceeds the Unix socket path bound");
714
+ await beforeSpawn();
715
+ signal?.throwIfAborted();
716
+ if (await binaryDigest(binary) !== WHATSAPP_AUTOMATION_BINARY_SHA256)
717
+ throw new Error("WhatsApp automation binary changed before launch");
718
+ child = Bun.spawn([binary, "--store", store, "--json", "--full", "sync", "--follow", "--presence-mode", "quiet", "--max-reconnect", "1m", "--max-messages", "200000", "--max-db-size", "2GB", "--ghostget-private-transport", "--ghostget-private-socket", socketPath], { stdin: "ignore", stdout: "ignore", stderr: "ignore", detached: true, env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", LANG: "C.UTF-8", LC_ALL: "C.UTF-8" } });
719
+ resource = attachLocalCliCleanupProcessGroup(resource, child.pid);
720
+ publish?.(resource);
721
+ authHash = sha(auth);
722
+ const until = Date.now() + 30000;
723
+ while (Date.now() < until) {
724
+ signal?.throwIfAborted();
725
+ if (child.exitCode !== null)
726
+ throw new Error("WhatsApp connection exited during startup");
727
+ try {
728
+ const info = await lstat2(socketPath);
729
+ if (!info.isSocket() || info.uid !== process.getuid() || (info.mode & 511) !== 384)
730
+ throw new Error("Unsafe WhatsApp private socket");
731
+ socket = { path: socketPath, dev: info.dev, ino: info.ino };
732
+ const status = await runtime.request(blankStatus(), signal);
733
+ if (status.connected && accountSubject(status.account) === selected.subject)
734
+ return;
735
+ throw new Error("WhatsApp connection did not match the selected account");
736
+ } catch (error) {
737
+ if (error.code !== "ENOENT")
738
+ throw error;
739
+ }
740
+ await sleep(50);
741
+ }
742
+ throw new Error("WhatsApp connection did not become ready within its deadline");
743
+ } catch (error) {
744
+ await runtime.close();
745
+ throw error;
746
+ }
747
+ });
748
+ },
749
+ async request(request, signal, beforeWrite) {
750
+ if (closed || !socket || !child || child.exitCode !== null)
751
+ throw new Error("The owned WhatsApp connection is unavailable");
752
+ return socketRequest(socket.path, socket, request, signal, beforeWrite);
753
+ },
754
+ async stage(bytes, digest2) {
755
+ const snapshot = Buffer.from(bytes);
756
+ if (closed || !root || !child || child.exitCode !== null || snapshot.byteLength < 1 || snapshot.byteLength > 20 * 1024 * 1024 || createHash("sha256").update(snapshot).digest("hex") !== automationDigest(digest2))
757
+ throw new Error("WhatsApp asset is unavailable or changed");
758
+ const path = join3(root, `asset-${randomBytes(16).toString("hex")}`), file = await open2(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 384);
759
+ try {
760
+ await file.writeFile(snapshot);
761
+ await file.sync();
762
+ } finally {
763
+ await file.close();
764
+ }
765
+ const identity = await lstat2(path);
766
+ staged.add(path);
767
+ return { path, async close() {
768
+ if (!staged.has(path))
769
+ return;
770
+ const current = await lstat2(path);
771
+ if (current.dev !== identity.dev || current.ino !== identity.ino || !current.isFile() || current.nlink !== 1)
772
+ throw new Error("WhatsApp staged asset changed");
773
+ await unlink2(path);
774
+ staged.delete(path);
775
+ } };
776
+ },
777
+ close() {
778
+ if (closing)
779
+ return closing;
780
+ closed = true;
781
+ closing = (async () => {
782
+ try {
783
+ if (child) {
784
+ if (child.exitCode === null) {
785
+ try {
786
+ process.kill(-child.pid, "SIGTERM");
787
+ } catch {
788
+ child.kill("SIGTERM");
789
+ }
790
+ }
791
+ const until = Date.now() + 55000;
792
+ while (child.exitCode === null && Date.now() < until)
793
+ await sleep(20);
794
+ if (child.exitCode === null) {
795
+ try {
796
+ process.kill(-child.pid, "SIGKILL");
797
+ } catch {
798
+ child.kill("SIGKILL");
799
+ }
800
+ }
801
+ const reapUntil = Date.now() + 5000;
802
+ while (child.exitCode === null && Date.now() < reapUntil)
803
+ await sleep(20);
804
+ if (child.exitCode === null)
805
+ throw new Error("WhatsApp process did not join");
806
+ await child.exited;
807
+ if (!resource || localCliCleanupProcessGroupStatus(resource) !== "quiescent")
808
+ throw new Error("WhatsApp process group cleanup is unverified");
809
+ }
810
+ if (socket) {
811
+ try {
812
+ const current = await lstat2(socket.path);
813
+ if (!current.isSocket() || current.dev !== socket.dev || current.ino !== socket.ino || current.uid !== process.getuid())
814
+ throw new Error("WhatsApp private socket changed before cleanup");
815
+ await unlink2(socket.path);
816
+ } catch (error) {
817
+ if (error.code !== "ENOENT")
818
+ throw error;
819
+ }
820
+ }
821
+ if (staged.size > 0)
822
+ throw new Error("WhatsApp assets are still in use");
823
+ if (root)
824
+ await rmdir2(root);
825
+ cleanup?.verified();
826
+ } catch (error) {
827
+ cleanup?.unsafe(error);
828
+ throw error;
829
+ }
830
+ })();
831
+ return closing;
832
+ }
833
+ };
834
+ return runtime;
835
+ }
836
+ export {
837
+ whatsappAutomationBinaryPath,
838
+ resolveWhatsAppAutomationBinary,
839
+ parseWhatsAppPrivateResponse,
840
+ installReviewedWhatsAppAutomationBinary,
841
+ createWhatsAppAutomationRuntime,
842
+ WHATSAPP_AUTOMATION_VERSION,
843
+ WHATSAPP_AUTOMATION_PROTOCOL,
844
+ WHATSAPP_AUTOMATION_BINARY_SHA256
845
+ };