@hraness/ghostget 0.17.6 → 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.
- package/CHANGELOG.md +301 -0
- package/README.md +16 -9
- package/dist/apple-photos-client.js +2 -1
- package/dist/beeper-client.js +2 -1
- package/dist/client.js +1 -0
- package/dist/imessage-direct-install-2gm3kge7.js +324 -0
- package/dist/index-01eeae9e.js +5 -0
- package/dist/index-2ymnp8xv.js +145 -0
- package/dist/index-en5hycxp.js +175 -0
- package/dist/{index-cytf8d9p.js → index-hfbygww8.js} +1 -1
- package/dist/index-n4szk3nw.js +50 -0
- package/dist/index-yq6maz71.js +1105 -0
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index.js +4 -47
- package/dist/messaging-automation-api.js +21 -0
- package/dist/messaging-automation-j4274hvc.js +609 -0
- package/dist/messaging-native-install-8w1j36ah.js +16 -0
- package/dist/messaging.js +1 -0
- package/dist/omni-client.js +1 -0
- package/dist/whatsapp-automation-runtime-hgh1e9rm.js +845 -0
- package/dist/whatsapp-client.js +1 -0
- package/docs/control-panel.md +147 -0
- package/docs/imessage-direct-provider.md +31 -7
- package/docs/messaging-automation.md +103 -0
- package/package.json +66 -7
- package/skills/ghostget/SKILL.md +3 -1
- package/skills/ghostget/references/control-panel.md +43 -0
- package/skills/ghostget/references/install.md +5 -5
- package/skills/ghostget/references/linkedin-adapter.md +222 -12
- package/skills/ghostget/references/platform-patterns.md +1 -1
- package/src/args.ts +18 -5
- package/src/assets/adapters/imessage/wrench-web-adapter.json +126 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +144 -0
- package/src/assets/messaging-runtime/NOTICE.txt +2580 -0
- package/src/assets/messaging-runtime/imsg-darwin-arm64.gz +0 -0
- package/src/assets/messaging-runtime/phone-number-info.plist.gz +0 -0
- package/src/assets/messaging-runtime/phone-number-metadata.json.gz +0 -0
- package/src/assets/messaging-runtime/phone-number-privacy.plist.gz +0 -0
- package/src/assets/messaging-runtime/wacli-darwin-arm64.gz +0 -0
- package/src/auth.ts +35 -1
- package/src/beeper-client-types.ts +1 -1
- package/src/cli.ts +18 -0
- package/src/confirmed-write-platform.ts +16 -1
- package/src/control/account-revision.ts +16 -0
- package/src/control/activity.ts +104 -0
- package/src/control/approval-broker.ts +59 -0
- package/src/control/approval-client.ts +49 -0
- package/src/control/bundled-interfaces.ts +20 -0
- package/src/control/cli.ts +20 -0
- package/src/control/connections.ts +87 -0
- package/src/control/credential-helper.ts +152 -0
- package/src/control/helper.ts +79 -0
- package/src/control/interface-cli.ts +22 -0
- package/src/control/interface-json.ts +94 -0
- package/src/control/interface-schema.ts +120 -0
- package/src/control/interfaces.ts +438 -0
- package/src/control/protocol.ts +182 -0
- package/src/control/service.ts +104 -0
- package/src/control/validation.ts +103 -0
- package/src/control/vault.ts +105 -0
- package/src/control/web-gateway.ts +62 -0
- package/src/control/web-policy.ts +56 -0
- package/src/ghostget.ts +18 -4
- package/src/messaging-automation-api.ts +12 -0
- package/src/messaging-automation-descriptors.ts +29 -0
- package/src/messaging-automation-factory.ts +148 -0
- package/src/messaging-automation-server.ts +183 -0
- package/src/messaging-automation-types.ts +155 -0
- package/src/messaging-automation-validation.ts +110 -0
- package/src/messaging-automation.ts +356 -0
- package/src/messaging-runtime.ts +3 -0
- package/src/oauth-google.ts +11 -5
- package/src/omni-runtime.ts +18 -3
- package/src/operation-permission-store.ts +92 -0
- package/src/operation-permission.ts +308 -0
- package/src/pinned-https.ts +5 -0
- package/src/plugins/imessage-direct/plugin.ts +12 -1
- package/src/plugins/imessage-direct/vendor/0003-feat-rpc-add-no-fetch-rich-cards.patch +276 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +84 -10
- package/src/plugins/linkedin-web/plugin.ts +1 -1
- package/src/plugins/whatsapp-linked-device/plugin.ts +7 -2
- package/src/plugins/whatsapp-linked-device/vendor/0001-ghostget-private-messaging.patch +1093 -0
- package/src/plugins/whatsapp-linked-device/vendor/README.md +39 -0
- package/src/plugins/whatsapp-linked-device/vendor/provenance.json +45 -0
- package/src/provider-http.ts +11 -3
- package/src/provider-plugin-contract-identity.ts +2 -2
- package/src/provider-plugin-import-analysis.ts +52 -0
- package/src/provider-plugin-module-analysis.ts +21 -1
- package/src/provider-plugin-registry.ts +4 -8
- package/src/provider-plugin.ts +18 -8
- package/src/providers/imessage-automation.ts +250 -0
- package/src/providers/imessage-direct-install.ts +7 -0
- package/src/providers/imessage-direct-runtime.ts +32 -2
- package/src/providers/imessage-direct.ts +13 -3
- package/src/providers/linkedin-contact-failure.ts +21 -0
- package/src/providers/linkedin-contact-platform.ts +8 -1
- package/src/providers/linkedin-contact-program.ts +34 -5
- package/src/providers/linkedin-web-contact.ts +1223 -35
- package/src/providers/linkedin-web-profile-browser.ts +659 -45
- package/src/providers/linkedin-web-runtime.ts +1 -0
- package/src/providers/linkedin-web.ts +46 -2
- package/src/providers/messaging-native-artifacts.ts +38 -0
- package/src/providers/messaging-native-install.ts +75 -0
- package/src/providers/whatsapp-automation-runtime.ts +240 -0
- package/src/providers/whatsapp-automation.ts +153 -0
- package/src/read-client.ts +12 -2
- package/src/runtime.ts +81 -9
- package/src/state-helper.ts +2 -0
- package/src/storage.ts +71 -1
- package/src/usage.ts +9 -1
- package/src/version.ts +1 -1
- package/src/web-session-contract-definitions.ts +1 -1
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Reviewed WhatsApp automation transport
|
|
2
|
+
|
|
3
|
+
This patch applies to `openclaw/wacli` commit
|
|
4
|
+
`a020de724180d31eccfa5241d45443402d62fb06` (v0.15.0). It is opt-in through
|
|
5
|
+
`sync --ghostget-private-transport`. Ordinary wacli commands retain their
|
|
6
|
+
existing behavior. Ghostget does not admit stock auto-retrying sends for its
|
|
7
|
+
automation contract.
|
|
8
|
+
|
|
9
|
+
The private socket carries fixed-schema JSON, bound to an exact account,
|
|
10
|
+
connection generation, direct recipient JID, and durable request ID. A claimed
|
|
11
|
+
request is never sent again; only an exact prior accepted receipt is returned.
|
|
12
|
+
Connection loss or an unfinished claim remains indeterminate. Acceptance means
|
|
13
|
+
the provider acknowledged the operation, not that the recipient received it.
|
|
14
|
+
|
|
15
|
+
Private sends make one joined application-level attempt and omit recipient
|
|
16
|
+
alias rewriting and URL previews. Text, document attachments, WebP stickers,
|
|
17
|
+
reactions and polls use existing pinned wacli operations. The upstream protocol
|
|
18
|
+
library may retransmit protocol frames with its own message ID. No account,
|
|
19
|
+
pairing, private-message or live-send qualification is implied by offline tests.
|
|
20
|
+
|
|
21
|
+
The private runtime installs SQLite triggers that journal message insert,
|
|
22
|
+
update and delete projections in the same transaction. A persistent generation
|
|
23
|
+
and monotonic sequence support resumption; retention is 10,000 events and
|
|
24
|
+
expired or changed anchors require a new baseline. Historical backfill keeps
|
|
25
|
+
its original timestamp. Consumers must apply their freshness policy before
|
|
26
|
+
replying. A single writer connection uses `synchronous=FULL`.
|
|
27
|
+
|
|
28
|
+
Build with Go 1.25.12, the unchanged committed module graph, a macOS arm64
|
|
29
|
+
toolchain, and the exact commands in `provenance.json`. The native binary is
|
|
30
|
+
bundled as compressed pinned bytes inside Ghostget's existing package archive.
|
|
31
|
+
Owner setup installs it with the exact-hash installer; it is not an additional
|
|
32
|
+
canonical package release asset. The supervised socket lives in its owned
|
|
33
|
+
private cleanup directory so long managed account paths remain usable.
|
|
34
|
+
|
|
35
|
+
Tests in the patch exercise a real private Unix socket using a synthetic
|
|
36
|
+
runtime, joined cancellation, one-attempt failure, exact replay, identity and
|
|
37
|
+
target mismatch, transactional events, cursor expiration and bounded retention.
|
|
38
|
+
Both complete upstream suites (plain and `sqlite_fts5`) and `go vet ./...`
|
|
39
|
+
passed. Two builds with identical flags produced identical bytes.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"upstream": {
|
|
4
|
+
"repository": "https://github.com/openclaw/wacli",
|
|
5
|
+
"version": "0.15.0",
|
|
6
|
+
"baseCommit": "a020de724180d31eccfa5241d45443402d62fb06"
|
|
7
|
+
},
|
|
8
|
+
"reviewedPatchStack": {
|
|
9
|
+
"tipCommit": "16cf7557a1683eb18e7a6e1c1fde8ca9f05e0850",
|
|
10
|
+
"patches": [{
|
|
11
|
+
"file": "0001-ghostget-private-messaging.patch",
|
|
12
|
+
"sha256": "44235d0fcd070d895fbe2fa3fa4406b34b8f7c808216fd22a2417c4fc86f3255",
|
|
13
|
+
"purpose": "Bounded owner-only IPC, durable one-attempt request claims, exact direct JIDs, joined cancellation and atomic bounded message-change events."
|
|
14
|
+
}]
|
|
15
|
+
},
|
|
16
|
+
"artifact": {
|
|
17
|
+
"platform": "darwin",
|
|
18
|
+
"arch": "arm64",
|
|
19
|
+
"version": "0.15.0+ghostget-private.1",
|
|
20
|
+
"executableSha256": "9b77ffb810d028fde725ca02b1451f1725b5ff5312a46a54468a9a38533d4cea",
|
|
21
|
+
"executableBytes": 21963810,
|
|
22
|
+
"compression": "gzip",
|
|
23
|
+
"compressedSha256": "1c1650d6c79b74db7f8f335b4746398c802031260a90468312dcaac0374a5166",
|
|
24
|
+
"compressedBytes": 7682949,
|
|
25
|
+
"build": {
|
|
26
|
+
"go": "go1.25.12 darwin/arm64",
|
|
27
|
+
"goArchiveSha256": "fa2c88bbcf64bd3b2aef355f026cfec6d3a4a01c132f999c8f8c964eb767164f",
|
|
28
|
+
"clang": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
|
|
29
|
+
"macosSdk": "26.5",
|
|
30
|
+
"moduleGraphChanged": false,
|
|
31
|
+
"command": ["go", "build", "-trimpath", "-buildvcs=false", "-tags", "sqlite_fts5", "-ldflags", "-s -w -buildid= -X main.version=0.15.0+ghostget-private.1", "-o", "wacli-private", "./cmd/wacli"],
|
|
32
|
+
"secondIdenticalBuildVerified": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"validation": {
|
|
36
|
+
"goTestAll": "passed",
|
|
37
|
+
"goTestAllSqliteFts5": "passed",
|
|
38
|
+
"goVetAll": "passed",
|
|
39
|
+
"privateSocketUsesSyntheticRuntime": true,
|
|
40
|
+
"realAccountRead": false,
|
|
41
|
+
"pairingPerformed": false,
|
|
42
|
+
"liveSendPerformed": false,
|
|
43
|
+
"deliveryQualified": false
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/provider-http.ts
CHANGED
|
@@ -76,7 +76,7 @@ function canonicalTokenPath(path: string): string {
|
|
|
76
76
|
return current;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function readPrivateTokenFile(path: string): string {
|
|
79
|
+
function readPrivateTokenFile(path: string, expectedContent?: string): string {
|
|
80
80
|
if (!isAbsolute(path)) throw new Error("OAuth token file path must be absolute");
|
|
81
81
|
const canonical = canonicalTokenPath(path);
|
|
82
82
|
const descriptor = openSync(
|
|
@@ -112,6 +112,11 @@ function readPrivateTokenFile(path: string): string {
|
|
|
112
112
|
|| before.ctimeNs !== after.ctimeNs
|
|
113
113
|
|| before.mode !== after.mode
|
|
114
114
|
) throw new Error("OAuth token file changed while it was read");
|
|
115
|
+
// Bind write readback before decoding, including byte-order marks and
|
|
116
|
+
// formatting. Never derive cleanup authority from different stored bytes.
|
|
117
|
+
if (expectedContent !== undefined && !buffer.equals(Buffer.from(expectedContent, "utf8"))) {
|
|
118
|
+
throw new Error("OAuth credential file does not match its intended content");
|
|
119
|
+
}
|
|
115
120
|
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
116
121
|
} finally {
|
|
117
122
|
closeSync(descriptor);
|
|
@@ -190,11 +195,14 @@ function parseGoogleInstalledAppRefresh(value: unknown): GoogleInstalledAppRefre
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
/** Load and strictly bind a private token document without enforcing freshness. */
|
|
193
|
-
export function loadOAuthCredential(
|
|
198
|
+
export function loadOAuthCredential(
|
|
199
|
+
auth: OAuthTokenAuth,
|
|
200
|
+
options: Readonly<{ expectedContent?: string }> = {},
|
|
201
|
+
): LoadedOAuthCredential {
|
|
194
202
|
let content: string;
|
|
195
203
|
let parsed: unknown;
|
|
196
204
|
try {
|
|
197
|
-
content = readPrivateTokenFile(auth.path);
|
|
205
|
+
content = readPrivateTokenFile(auth.path, options.expectedContent);
|
|
198
206
|
parsed = JSON.parse(content) as unknown;
|
|
199
207
|
} catch (error) {
|
|
200
208
|
throw new Error(`could not load private ${auth.provider} OAuth token document`, { cause: error });
|
|
@@ -303,8 +303,9 @@ const identities = Object.freeze({
|
|
|
303
303
|
"linkedin-web": {
|
|
304
304
|
schemaVersion: 1,
|
|
305
305
|
pluginVersion: "1.6.0",
|
|
306
|
-
implementationSha256: "
|
|
306
|
+
implementationSha256: "95000a4d81f2fdbd222fa793b229704ba77ab9a23d02b26db5fa561bd5d6e491",
|
|
307
307
|
legacyCurrentReadImplementationSha256: [
|
|
308
|
+
"11b52ca3dd9cbafe4829eed9f27d59bdc1c312db943ea29b93cbab6417ac7025",
|
|
308
309
|
"e4614ab9c7733d7526b8157410e462f1bb48fdaa1c2319a6a4179c18b9e6839d",
|
|
309
310
|
"97194dc5a8ec3afc4f4a20152efe51dbdc2fa8a8dde0cd9484b54718c4767f45",
|
|
310
311
|
"64621493e09949fcb9973270140fec1d7398374cf48d3ef150a0c24b882fdeb8",
|
|
@@ -316,7 +317,6 @@ const identities = Object.freeze({
|
|
|
316
317
|
"dbda2aee2075a0a3726241fd24932113ae6c2d139ae1f7036ea827a335f7344c",
|
|
317
318
|
"9acb34f9ef5f59dd46b249766af236a3876a4975e0797e330f2975b3fd4ed643",
|
|
318
319
|
"951102310bd87f93ce0863c1a444371d62f412934cc805f82c9981f82edfe50e",
|
|
319
|
-
"4fd6c293984be688a9273acb09bccea64a75134daca745b322fa8d33dfe9f97c",
|
|
320
320
|
],
|
|
321
321
|
legacyReadImplementationSha256: {
|
|
322
322
|
test: "00a99426ec31182f8d37d5cceb819947a09e630b468bb6e3ed8827f5b0fa4628",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
type LiteralModuleImport = {
|
|
4
|
+
readonly kind: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const scanners = Object.freeze({
|
|
9
|
+
js: new Bun.Transpiler({ loader: "js" }),
|
|
10
|
+
ts: new Bun.Transpiler({ loader: "ts" }),
|
|
11
|
+
});
|
|
12
|
+
const MAX_MEMO_ENTRIES = 2_048;
|
|
13
|
+
const MAX_MEMO_TEXT_BYTES = 4 * 1024 * 1024;
|
|
14
|
+
const importsMemo = new Map<string, {
|
|
15
|
+
readonly imports: readonly LiteralModuleImport[];
|
|
16
|
+
readonly textBytes: number;
|
|
17
|
+
}>();
|
|
18
|
+
let memoTextBytes = 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pure syntax only: callers still read current bytes, enforce their bounds,
|
|
22
|
+
* resolve every edge from its current importer, and revalidate the filesystem.
|
|
23
|
+
* Keep only immutable scanner output; never retain source text or an AST.
|
|
24
|
+
*/
|
|
25
|
+
export function scanProviderPluginValueImports(
|
|
26
|
+
source: string,
|
|
27
|
+
loader: "js" | "ts",
|
|
28
|
+
): readonly LiteralModuleImport[] {
|
|
29
|
+
const key = `${loader}\0${createHash("sha256").update(source).digest("hex")}`;
|
|
30
|
+
const cached = importsMemo.get(key);
|
|
31
|
+
if (cached !== undefined) return cached.imports;
|
|
32
|
+
const imports = Object.freeze(scanners[loader].scanImports(source).map(
|
|
33
|
+
({ kind, path }) => Object.freeze({ kind, path }),
|
|
34
|
+
));
|
|
35
|
+
const textBytes = key.length + imports.reduce(
|
|
36
|
+
(total, entry) => total + Buffer.byteLength(entry.kind) + Buffer.byteLength(entry.path),
|
|
37
|
+
0,
|
|
38
|
+
);
|
|
39
|
+
// Count entries as well as returned text so empty modules stay bounded too.
|
|
40
|
+
// Oversized foreign input is still rejected by the caller's own import bound.
|
|
41
|
+
if (imports.length > 4_096 || textBytes > MAX_MEMO_TEXT_BYTES) return imports;
|
|
42
|
+
while (importsMemo.size >= MAX_MEMO_ENTRIES
|
|
43
|
+
|| memoTextBytes + textBytes > MAX_MEMO_TEXT_BYTES) {
|
|
44
|
+
const oldest = importsMemo.entries().next().value;
|
|
45
|
+
if (oldest === undefined) break;
|
|
46
|
+
importsMemo.delete(oldest[0]);
|
|
47
|
+
memoTextBytes -= oldest[1].textBytes;
|
|
48
|
+
}
|
|
49
|
+
importsMemo.set(key, { imports, textBytes });
|
|
50
|
+
memoTextBytes += textBytes;
|
|
51
|
+
return imports;
|
|
52
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { extname } from "node:path";
|
|
2
3
|
import ts from "typescript";
|
|
3
4
|
|
|
@@ -8,6 +9,12 @@ const opaqueModuleLoaderNames = new Set([
|
|
|
8
9
|
"getBuiltinModule",
|
|
9
10
|
]);
|
|
10
11
|
|
|
12
|
+
// Only a boolean is retained, keyed by the freshly supplied source and every
|
|
13
|
+
// parser input. Filename is included because the source-local binding proof
|
|
14
|
+
// creates a TypeScript program. No AST, path resolution, or authority is cached.
|
|
15
|
+
const nonLiteralLoadMemo = new Map<string, boolean>();
|
|
16
|
+
const MAX_NON_LITERAL_LOAD_MEMO_ENTRIES = 4_096;
|
|
17
|
+
|
|
11
18
|
function hasOneLiteralModuleArgument(node: ts.CallExpression): boolean {
|
|
12
19
|
const [argument] = node.arguments;
|
|
13
20
|
return node.arguments.length === 1
|
|
@@ -244,12 +251,20 @@ function localEvalCallbackProof(sourceFile: ts.SourceFile): (node: ts.Node) => b
|
|
|
244
251
|
* rather than attempting to sandbox deliberately obfuscated JavaScript.
|
|
245
252
|
*/
|
|
246
253
|
export function hasNonLiteralModuleLoad(source: string, path: string): boolean {
|
|
254
|
+
const scriptKind = providerPluginScriptKind(path);
|
|
255
|
+
const key = createHash("sha256")
|
|
256
|
+
.update(JSON.stringify([path, scriptKind]))
|
|
257
|
+
.update("\0")
|
|
258
|
+
.update(source)
|
|
259
|
+
.digest("hex");
|
|
260
|
+
const cached = nonLiteralLoadMemo.get(key);
|
|
261
|
+
if (cached !== undefined) return cached;
|
|
247
262
|
const sourceFile = ts.createSourceFile(
|
|
248
263
|
path,
|
|
249
264
|
source,
|
|
250
265
|
ts.ScriptTarget.ESNext,
|
|
251
266
|
false,
|
|
252
|
-
|
|
267
|
+
scriptKind,
|
|
253
268
|
);
|
|
254
269
|
let callbackProof: ((node: ts.Node) => boolean) | undefined;
|
|
255
270
|
const isLocalCallback = (node: ts.Node): boolean => {
|
|
@@ -332,5 +347,10 @@ export function hasNonLiteralModuleLoad(source: string, path: string): boolean {
|
|
|
332
347
|
node.forEachChild((child) => visit(child, node));
|
|
333
348
|
};
|
|
334
349
|
visit(sourceFile);
|
|
350
|
+
if (nonLiteralLoadMemo.size >= MAX_NON_LITERAL_LOAD_MEMO_ENTRIES) {
|
|
351
|
+
const oldest = nonLiteralLoadMemo.keys().next().value;
|
|
352
|
+
if (oldest !== undefined) nonLiteralLoadMemo.delete(oldest);
|
|
353
|
+
}
|
|
354
|
+
nonLiteralLoadMemo.set(key, found);
|
|
335
355
|
return found;
|
|
336
356
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
sep,
|
|
25
25
|
} from "node:path";
|
|
26
26
|
import ts from "typescript";
|
|
27
|
+
import { scanProviderPluginValueImports } from "./provider-plugin-import-analysis";
|
|
27
28
|
import {
|
|
28
29
|
hasNonLiteralModuleLoad,
|
|
29
30
|
providerPluginScriptKind,
|
|
@@ -540,10 +541,6 @@ function readDependencySourceFromDisk(path: string): Buffer {
|
|
|
540
541
|
);
|
|
541
542
|
}
|
|
542
543
|
|
|
543
|
-
const providerPluginImportScanners = Object.freeze({
|
|
544
|
-
js: new Bun.Transpiler({ loader: "js" }),
|
|
545
|
-
ts: new Bun.Transpiler({ loader: "ts" }),
|
|
546
|
-
});
|
|
547
544
|
const providerPluginModuleExtensions = new Set([
|
|
548
545
|
".cjs",
|
|
549
546
|
".cts",
|
|
@@ -698,11 +695,10 @@ function valueImports(source: string, path: string): readonly {
|
|
|
698
695
|
`provider plugin implementation module ${path} uses configuration-dependent JSX or TSX; publish deterministic JavaScript or TypeScript instead`,
|
|
699
696
|
);
|
|
700
697
|
}
|
|
701
|
-
const
|
|
698
|
+
const loader =
|
|
702
699
|
extension === ".ts" || extension === ".mts" || extension === ".cts"
|
|
703
|
-
?
|
|
704
|
-
|
|
705
|
-
return scanner.scanImports(moduleSource);
|
|
700
|
+
? "ts" : "js";
|
|
701
|
+
return scanProviderPluginValueImports(moduleSource, loader);
|
|
706
702
|
}
|
|
707
703
|
|
|
708
704
|
|
package/src/provider-plugin.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
sep,
|
|
26
26
|
} from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { scanProviderPluginValueImports } from "./provider-plugin-import-analysis";
|
|
28
29
|
|
|
29
30
|
import type { GhostgetAuth } from "./auth";
|
|
30
31
|
import type { MessageLikeMeSourceConversationCoordinateBindingV1 } from "./message-like-me-agentic-messaging";
|
|
@@ -1339,10 +1340,6 @@ const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DEPTH = 32;
|
|
|
1339
1340
|
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_PATH_BYTES = 1_024;
|
|
1340
1341
|
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_BYTES = 128 * 1024 * 1024;
|
|
1341
1342
|
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_CACHE_ENTRIES = 256;
|
|
1342
|
-
const providerPluginEvaluationImportScanners = Object.freeze({
|
|
1343
|
-
js: new Bun.Transpiler({ loader: "js" }),
|
|
1344
|
-
ts: new Bun.Transpiler({ loader: "ts" }),
|
|
1345
|
-
});
|
|
1346
1343
|
const providerPluginEvaluationModuleExtensions = new Set([
|
|
1347
1344
|
".cjs",
|
|
1348
1345
|
".cts",
|
|
@@ -2194,11 +2191,10 @@ function providerPluginEvaluationValueImports(
|
|
|
2194
2191
|
`provider plugin evaluation module ${path} must be valid UTF-8`,
|
|
2195
2192
|
);
|
|
2196
2193
|
}
|
|
2197
|
-
const
|
|
2194
|
+
const loader =
|
|
2198
2195
|
extension === ".ts" || extension === ".mts" || extension === ".cts"
|
|
2199
|
-
?
|
|
2200
|
-
|
|
2201
|
-
const imports = Object.freeze([...scanner.scanImports(source)]);
|
|
2196
|
+
? "ts" : "js";
|
|
2197
|
+
const imports = scanProviderPluginValueImports(source, loader);
|
|
2202
2198
|
if (imports.length > MAX_PROVIDER_PLUGIN_EVALUATION_IMPORTS_PER_MODULE) {
|
|
2203
2199
|
throw new Error(
|
|
2204
2200
|
`provider plugin evaluation module ${path} has too many static imports`,
|
|
@@ -3489,6 +3485,20 @@ export function bindProviderPluginRuntimeLoadIdentity(
|
|
|
3489
3485
|
);
|
|
3490
3486
|
}
|
|
3491
3487
|
|
|
3488
|
+
/** Trusted extensions share the binding's complete source snapshot. They cannot
|
|
3489
|
+
* load an independent provider runtime without the registry's pre/post checks. */
|
|
3490
|
+
export async function loadProviderPluginExtensionRuntime<T>(
|
|
3491
|
+
loadRuntime: () => Promise<unknown>, loader: () => Promise<T>,
|
|
3492
|
+
): Promise<T> {
|
|
3493
|
+
const identity = providerPluginRuntimeLoadIdentities.get(loadRuntime);
|
|
3494
|
+
if (identity === undefined) throw new Error("Provider extension has no registered implementation identity");
|
|
3495
|
+
providerPluginStartedRuntimeLoaders.add(loadRuntime);
|
|
3496
|
+
await identity.verify("before");
|
|
3497
|
+
const runtime = await loader();
|
|
3498
|
+
await identity.verify("after");
|
|
3499
|
+
return runtime;
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3492
3502
|
export function lazyProviderApiRuntime(
|
|
3493
3503
|
loader: () => Promise<ProviderApiPluginRuntimeV1>,
|
|
3494
3504
|
): ProviderApiPluginRuntimeHooksV1 {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { GhostgetAuth } from "../auth";
|
|
6
|
+
import { canonicalJson } from "../canonical-json";
|
|
7
|
+
import type { LocalCliExecutionOptions } from "../local-cli-execution";
|
|
8
|
+
import { OperationDeadline } from "../operation-deadline";
|
|
9
|
+
import type { AutomationAction, AutomationConversation, AutomationCoordinate, AutomationIdentity, AutomationMessage, AutomationProviderStatus, AutomationProviderSendResult, MessagingAutomationProvider } from "../messaging-automation-types";
|
|
10
|
+
import { AUTOMATION_ACTION_KINDS, automationArray, automationDigest, automationInteger, automationRecord, automationText, parseAutomationAction, parseAutomationCoordinate, parseAutomationIdentity } from "../messaging-automation-validation";
|
|
11
|
+
import { IMSG_NO_FETCH_RICH_CARDS_AVAILABLE, type ImsgChatCoordinate, type ImsgRpcRequest } from "./imessage-direct";
|
|
12
|
+
import { imsgAutomationProjection as project, withImsgAutomationRuntime, type ImsgChatProjection, type ImsgDirectRuntimeDependencies } from "./imessage-direct-runtime";
|
|
13
|
+
|
|
14
|
+
export type ImsgAutomationOperation = "inspect" | "conversations" | "resolve" | "history" | "events" | AutomationAction["kind"];
|
|
15
|
+
/** The Ghostget host supplies existing permission admission, auth lifetime and
|
|
16
|
+
* exact plugin closure. A contact grant only narrows this authority. */
|
|
17
|
+
export type ImsgAutomationAdmission = Readonly<{ auth: GhostgetAuth; accountIdentity: string; implementationIdentity: string }>;
|
|
18
|
+
export type ImsgAutomationOptions = Readonly<{
|
|
19
|
+
authorize(operation: ImsgAutomationOperation, signal?: AbortSignal): Promise<ImsgAutomationAdmission>;
|
|
20
|
+
execution: Pick<LocalCliExecutionOptions, "registerCleanupBarrier" | "environment">;
|
|
21
|
+
resolveAsset(assetId: string, signal?: AbortSignal): Promise<Readonly<{ bytes: Uint8Array; sha256: string }>>;
|
|
22
|
+
dependencies?: ImsgDirectRuntimeDependencies;
|
|
23
|
+
}>;
|
|
24
|
+
type Session = Parameters<Parameters<typeof withImsgAutomationRuntime>[2]>[0];
|
|
25
|
+
const sha = (value: unknown): string => createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
26
|
+
function request(method: string, params: Readonly<Record<string, unknown>> = {}, id = "operation"): ImsgRpcRequest { return { jsonrpc: "2.0", id, method, params }; }
|
|
27
|
+
function coordinate(value: unknown): Extract<AutomationCoordinate, { provider: "imessage" }> {
|
|
28
|
+
const result = parseAutomationCoordinate(value);
|
|
29
|
+
if (result.provider !== "imessage") throw new Error("iMessage cannot operate another messaging network");
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function target(value: AutomationCoordinate): ImsgChatCoordinate {
|
|
33
|
+
const item = coordinate(value); return { chatGuid: item.chatGuid, service: "iMessage", observedChatRowId: item.observedChatRowId };
|
|
34
|
+
}
|
|
35
|
+
function conversation(chat: ImsgChatProjection): AutomationConversation {
|
|
36
|
+
return { coordinate: coordinate({ provider: "imessage", chatGuid: chat.guid, service: "iMessage", observedChatRowId: chat.id }), title: chat.title, kind: chat.kind, participants: chat.participants };
|
|
37
|
+
}
|
|
38
|
+
async function exactConversation(session: Session, value: AutomationCoordinate): Promise<AutomationConversation> {
|
|
39
|
+
const selected = target(value);
|
|
40
|
+
const output = await session.run([request("chats.get", { chat_id: selected.observedChatRowId })]);
|
|
41
|
+
return conversation(project.exactChat(output.get("operation"), selected));
|
|
42
|
+
}
|
|
43
|
+
function methods(session: Session): ReadonlySet<string> {
|
|
44
|
+
const status = session.status as Record<string, unknown>; // already parsed by the pinned runtime
|
|
45
|
+
return new Set(automationArray(status.methods, 256).map(value => automationText(value, 128)));
|
|
46
|
+
}
|
|
47
|
+
function providerStatus(session: Session, identity: AutomationIdentity): AutomationProviderStatus {
|
|
48
|
+
const available = methods(session);
|
|
49
|
+
const rpc: Partial<Record<AutomationAction["kind"], string>> = { text: "send", attachment: "send", reaction: "tapback", sticker: "send.sticker", link: "send.rich", poll: "poll.send" };
|
|
50
|
+
const actions = Object.fromEntries(AUTOMATION_ACTION_KINDS.map(kind => {
|
|
51
|
+
if (kind === "link" && !IMSG_NO_FETCH_RICH_CARDS_AVAILABLE) return [kind, { available: false, reason: "The installed iMessage artifact predates no-fetch rich cards. The replacement native build has not been admitted." }];
|
|
52
|
+
const method = rpc[kind]; const ready = method !== undefined && available.has(method);
|
|
53
|
+
return [kind, { available: ready, reason: ready ? null : !method ? "The pinned iMessage helper has no reviewed operation for this action." : method === "send" ? "The native iMessage send operation is unavailable. Check Messages permissions in Ghostget." : `The private Messages bridge does not currently advertise ${method}. Its injection design requires owner-configured system support, including disabled SIP. Ghostget installation never changes SIP.` }];
|
|
54
|
+
})) as AutomationProviderStatus["actions"];
|
|
55
|
+
return { identity, connected: true, events: { available: available.has("messages.after"), reason: available.has("messages.after") ? null : "The pinned database cursor operation is unavailable." }, actions };
|
|
56
|
+
}
|
|
57
|
+
function message(value: unknown, selected: AutomationCoordinate): AutomationMessage {
|
|
58
|
+
const projected = project.parseMessage(value, "automation message", target(selected));
|
|
59
|
+
const raw = value as Record<string, unknown>;
|
|
60
|
+
if (raw.is_reaction !== undefined && typeof raw.is_reaction !== "boolean") throw new Error("Invalid reaction marker");
|
|
61
|
+
const related = raw.is_reaction === true ? automationText(raw.reacted_to_guid, 256) : projected.replyToGuid;
|
|
62
|
+
if (Buffer.byteLength(projected.text) > 65_536) throw new Error("Automation message exceeds its text bound");
|
|
63
|
+
return { id: automationText(projected.guid, 256), coordinate: selected, direction: projected.isFromMe ? "outgoing" : "incoming", occurredAt: projected.createdAt, text: projected.text, kind: raw.is_reaction === true ? "reaction" : "message", relatedMessageId: related, attachments: [] };
|
|
64
|
+
}
|
|
65
|
+
async function historyMessages(session: Session, selected: AutomationCoordinate, limit: number): Promise<readonly AutomationMessage[]> {
|
|
66
|
+
await exactConversation(session, selected);
|
|
67
|
+
const response = await session.run([request("messages.history", { chat_id: target(selected).observedChatRowId, limit, attachments: false })]);
|
|
68
|
+
const row = automationRecord(response.get("operation"), ["messages"]);
|
|
69
|
+
const messages = automationArray(row.messages, limit).map(raw => message(raw, selected));
|
|
70
|
+
if (new Set(messages.map(item => item.id)).size !== messages.length) throw new Error("Repeated message identity");
|
|
71
|
+
return messages;
|
|
72
|
+
}
|
|
73
|
+
type Cursor = { version: 1; identity: string; coordinates: string; rows: number[]; anchors: ({ row: number; id: string } | null)[] };
|
|
74
|
+
function cursor(value: string | null, identity: AutomationIdentity, coordinates: readonly AutomationCoordinate[]): Cursor {
|
|
75
|
+
if (value === null) return { version: 1, identity: sha(identity), coordinates: sha(coordinates), rows: coordinates.map(() => 0), anchors: coordinates.map(() => null) };
|
|
76
|
+
automationText(value, 16_384);
|
|
77
|
+
const decoded = Buffer.from(value, "base64url");
|
|
78
|
+
if (decoded.toString("base64url") !== value) throw new Error("Noncanonical iMessage cursor");
|
|
79
|
+
const parsed = automationRecord(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decoded)), ["version", "identity", "coordinates", "rows", "anchors"]);
|
|
80
|
+
if (parsed.version !== 1 || parsed.identity !== sha(identity) || parsed.coordinates !== sha(coordinates)) throw new Error("iMessage cursor generation or scope changed");
|
|
81
|
+
const rows = automationArray(parsed.rows, 50).map(value => automationInteger(value, 0, Number.MAX_SAFE_INTEGER));
|
|
82
|
+
if (rows.length !== coordinates.length) throw new Error("iMessage cursor scope changed");
|
|
83
|
+
const anchors = automationArray(parsed.anchors, 50).map((value, index) => {
|
|
84
|
+
if (value === null) return null;
|
|
85
|
+
const anchor = automationRecord(value, ["row", "id"]);
|
|
86
|
+
return { row: automationInteger(anchor.row, 1, rows[index] ?? 0), id: automationText(anchor.id, 256) };
|
|
87
|
+
});
|
|
88
|
+
if (anchors.length !== rows.length) throw new Error("iMessage cursor anchor scope changed");
|
|
89
|
+
return { version: 1, identity: parsed.identity as string, coordinates: parsed.coordinates as string, rows, anchors };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Concrete pinned imsg implementation. It never starts the private Messages
|
|
93
|
+
* bridge, changes OS permissions, or falls back to SMS or another transport. */
|
|
94
|
+
export function createImsgAutomationProvider(options: ImsgAutomationOptions): MessagingAutomationProvider {
|
|
95
|
+
let closed = false; let inFlight: Promise<unknown> | undefined;
|
|
96
|
+
async function run<T>(operation: ImsgAutomationOperation, signal: AbortSignal | undefined, work: (session: Session, identity: AutomationIdentity, reauthorize: () => Promise<void>) => Promise<T>): Promise<T> {
|
|
97
|
+
if (closed || inFlight) throw new Error("iMessage provider is closed or busy");
|
|
98
|
+
const pending = (async () => {
|
|
99
|
+
signal?.throwIfAborted();
|
|
100
|
+
const admission = await options.authorize(operation, signal);
|
|
101
|
+
if (admission.auth.kind !== "linked-device-store" || admission.auth.provider !== "imessage") throw new Error("iMessage account required");
|
|
102
|
+
const deadline = new OperationDeadline(30_000, signal ? { signal } : {});
|
|
103
|
+
try {
|
|
104
|
+
return await withImsgAutomationRuntime(admission.auth, { ...options.execution, operationDeadline: deadline, ...(options.dependencies ? { dependencies: options.dependencies } : {}) }, async session => {
|
|
105
|
+
const identity = parseAutomationIdentity({ provider: "imessage", authId: admission.auth.id, accountIdentity: admission.accountIdentity, accountSubject: session.subject, implementationIdentity: admission.implementationIdentity, sourceGeneration: session.sourceGeneration });
|
|
106
|
+
const reauthorize = async () => { signal?.throwIfAborted(); const after = await options.authorize(operation, signal); if (sha(after) !== sha(admission)) throw new Error("iMessage account or permission changed during the operation"); };
|
|
107
|
+
const result = await work(session, identity, reauthorize);
|
|
108
|
+
await reauthorize();
|
|
109
|
+
return result;
|
|
110
|
+
});
|
|
111
|
+
} finally { deadline.dispose(); }
|
|
112
|
+
})();
|
|
113
|
+
inFlight = pending;
|
|
114
|
+
try { return await pending; } finally { if (inFlight === pending) inFlight = undefined; }
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
provider: "imessage",
|
|
118
|
+
inspect(signal) { return run("inspect", signal, async (session, identity) => providerStatus(session, identity)); },
|
|
119
|
+
conversations(input, signal) {
|
|
120
|
+
const limit = automationInteger(input.limit, 1, 200);
|
|
121
|
+
return run("conversations", signal, async (session, identity) => {
|
|
122
|
+
const result = await session.run([request("chats.list", { limit })]);
|
|
123
|
+
return { identity, conversations: project.parseChats(result.get("operation")).map(conversation), complete: false };
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
resolve(value, signal) { const selected = coordinate(value); return run("resolve", signal, async (session, identity) => ({ identity, conversation: await exactConversation(session, selected) })); },
|
|
127
|
+
history(input, signal) {
|
|
128
|
+
const selected = coordinate(input.coordinate), limit = automationInteger(input.limit, 1, 200);
|
|
129
|
+
return run("history", signal, async (session, identity) => {
|
|
130
|
+
await exactConversation(session, selected);
|
|
131
|
+
const response = await session.run([request("messages.history", { chat_id: selected.observedChatRowId, limit, attachments: false })]);
|
|
132
|
+
const raw = automationArray(automationRecord(response.get("operation"), ["messages"]).messages, limit);
|
|
133
|
+
const messages = raw.map(value => message(value, selected));
|
|
134
|
+
if (new Set(messages.map(item => item.id)).size !== messages.length) throw new Error("Repeated history identity");
|
|
135
|
+
const next = cursor(null, identity, [selected]);
|
|
136
|
+
for (const [index, value] of raw.entries()) {
|
|
137
|
+
const row = automationInteger((value as Record<string, unknown>).id, 1, Number.MAX_SAFE_INTEGER);
|
|
138
|
+
if (row > next.rows[0]!) { next.rows[0] = row; next.anchors[0] = { row, id: messages[index]!.id }; }
|
|
139
|
+
}
|
|
140
|
+
// Start after the newest verified history row, then silently drain any
|
|
141
|
+
// remaining rows to establish the initial live boundary.
|
|
142
|
+
return { identity, messages, nextCursor: Buffer.from(canonicalJson(next)).toString("base64url"), caughtUp: false, gap: false };
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
events(input, signal) {
|
|
146
|
+
const coordinates = automationArray(input.coordinates, 50).map(coordinate).sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
|
|
147
|
+
if (coordinates.length === 0 || new Set(coordinates.map(sha)).size !== coordinates.length) throw new Error("Distinct iMessage event scopes required");
|
|
148
|
+
const limit = automationInteger(input.limit, 1, 500);
|
|
149
|
+
return run("events", signal, async (session, identity) => {
|
|
150
|
+
if (!methods(session).has("messages.after")) throw new Error("Database event cursor is unavailable");
|
|
151
|
+
const next = cursor(input.cursor, identity, coordinates);
|
|
152
|
+
const messages: AutomationMessage[] = []; let caughtUp = true;
|
|
153
|
+
for (const [index, selected] of coordinates.entries()) {
|
|
154
|
+
if (messages.length === limit) { caughtUp = false; break; }
|
|
155
|
+
await exactConversation(session, selected);
|
|
156
|
+
const anchor = next.anchors[index];
|
|
157
|
+
if (anchor) {
|
|
158
|
+
const proof = await session.run([request("messages.after", { chat_id: selected.observedChatRowId, since_rowid: anchor.row - 1, limit: 1, attachments: false, convert_attachments: false, include_reactions: true })]);
|
|
159
|
+
const proofPage = automationRecord(proof.get("operation"), ["messages", "next_rowid", "has_more"]);
|
|
160
|
+
const first = automationArray(proofPage.messages, 1)[0];
|
|
161
|
+
if (!first || (first as Record<string, unknown>).id !== anchor.row || message(first, selected).id !== anchor.id) throw new Error("iMessage cursor anchor changed; re-enrollment is required");
|
|
162
|
+
}
|
|
163
|
+
const remaining = limit - messages.length;
|
|
164
|
+
const result = await session.run([request("messages.after", { chat_id: selected.observedChatRowId, since_rowid: next.rows[index], limit: remaining, attachments: false, convert_attachments: false, include_reactions: true })]);
|
|
165
|
+
const page = automationRecord(result.get("operation"), ["messages", "next_rowid", "has_more"]);
|
|
166
|
+
const nextRow = automationInteger(page.next_rowid, next.rows[index]!, Number.MAX_SAFE_INTEGER);
|
|
167
|
+
if (typeof page.has_more !== "boolean" || page.has_more && nextRow === next.rows[index]) throw new Error("Nonprogressing iMessage cursor");
|
|
168
|
+
const rawMessages = automationArray(page.messages, remaining);
|
|
169
|
+
let previous = next.rows[index]!;
|
|
170
|
+
for (const raw of rawMessages) {
|
|
171
|
+
const parsed = message(raw, selected);
|
|
172
|
+
const rowId = automationInteger((raw as Record<string, unknown>).id, previous + 1, nextRow);
|
|
173
|
+
previous = rowId; messages.push(parsed); next.anchors[index] = { row: rowId, id: parsed.id };
|
|
174
|
+
}
|
|
175
|
+
next.rows[index] = nextRow;
|
|
176
|
+
if (page.has_more) caughtUp = false;
|
|
177
|
+
}
|
|
178
|
+
if (new Set(messages.map(item => item.id)).size !== messages.length) throw new Error("Repeated event identity");
|
|
179
|
+
return { identity, messages, nextCursor: Buffer.from(canonicalJson(next)).toString("base64url"), caughtUp, gap: false };
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
async send(input, signal): Promise<AutomationProviderSendResult> {
|
|
183
|
+
const selected = coordinate(input.coordinate), action = parseAutomationAction(input.action), expected = parseAutomationIdentity(input.identity);
|
|
184
|
+
let dispatched = false;
|
|
185
|
+
try {
|
|
186
|
+
return await run(action.kind, signal, async (session, identity, reauthorize) => {
|
|
187
|
+
if (sha(identity) !== sha(expected)) throw new Error("iMessage account or source changed before dispatch");
|
|
188
|
+
const live = await exactConversation(session, selected);
|
|
189
|
+
if (live.kind !== "single" || live.participants.length !== 1) throw new Error("Only direct participant-bound automation is supported");
|
|
190
|
+
if (!providerStatus(session, identity).actions[action.kind].available) throw new Error("Requested action is unavailable");
|
|
191
|
+
let method: string; const params: Record<string, unknown> = { chat_guid: selected.chatGuid };
|
|
192
|
+
if (action.kind === "text") { method = "send"; Object.assign(params, { text: action.text, service: "imessage", transport: "applescript", allow_sms_fallback: false }); }
|
|
193
|
+
else if (action.kind === "link") { method = "send.rich"; params.url = action.url; params.fetch_metadata = false; }
|
|
194
|
+
else if (action.kind === "poll") { if (action.maximumSelections !== null) throw new Error("Native iMessage polls use provider selection behavior"); method = "poll.send"; Object.assign(params, { question: action.question, options: action.options, suppress_comment: true }); }
|
|
195
|
+
else if (action.kind === "reaction" || action.kind === "sticker" || action.kind === "attachment") {
|
|
196
|
+
if ((action.kind === "reaction" || action.kind === "sticker") && action.messageId !== null) {
|
|
197
|
+
const current = await historyMessages(session, selected, 200);
|
|
198
|
+
if (!current.some(message => message.id === action.messageId && message.kind === "message")) throw new Error("Reaction or sticker target must belong to current conversation history");
|
|
199
|
+
}
|
|
200
|
+
if (action.kind === "reaction") {
|
|
201
|
+
if (!["❤️", "👍", "👎", "😂", "‼️", "❓"].includes(action.emoji)) throw new Error("This iMessage contract admits the six standard reactions");
|
|
202
|
+
method = "tapback"; Object.assign(params, { message_guid: action.messageId, emoji: action.emoji, remove: action.remove });
|
|
203
|
+
} else {
|
|
204
|
+
const asset = await options.resolveAsset(action.assetId, signal);
|
|
205
|
+
const maximum = action.kind === "sticker" ? 500 * 1024 : 16 * 1024 * 1024;
|
|
206
|
+
if (!(asset.bytes instanceof Uint8Array) || asset.bytes.byteLength < 1 || asset.bytes.byteLength > maximum) throw new Error("Attachment byte admission failed");
|
|
207
|
+
const bytes = Buffer.from(asset.bytes);
|
|
208
|
+
if (createHash("sha256").update(bytes).digest("hex") !== automationDigest(asset.sha256)) throw new Error("Attachment byte admission failed");
|
|
209
|
+
const path = join(session.operationRoot, action.kind === "attachment" ? action.name : "sticker");
|
|
210
|
+
const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
211
|
+
try { await file.writeFile(bytes); await file.sync(); } finally { await file.close(); }
|
|
212
|
+
params.file = path;
|
|
213
|
+
if (action.kind === "sticker") { method = "send.sticker"; if (action.messageId !== null) params.attach_to = action.messageId; }
|
|
214
|
+
else { method = "send"; Object.assign(params, { service: "imessage", transport: "applescript", allow_sms_fallback: false }); }
|
|
215
|
+
}
|
|
216
|
+
} else throw new Error("No reviewed iMessage implementation exists for this experience");
|
|
217
|
+
signal?.throwIfAborted();
|
|
218
|
+
const response = await session.run([request(method, params)], async () => {
|
|
219
|
+
await reauthorize();
|
|
220
|
+
if (sha(await exactConversation(session, selected)) !== sha(live)) throw new Error("iMessage participants changed before dispatch");
|
|
221
|
+
await reauthorize();
|
|
222
|
+
dispatched = true;
|
|
223
|
+
});
|
|
224
|
+
const raw = response.get("operation");
|
|
225
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid iMessage acceptance");
|
|
226
|
+
const result = raw as Record<string, unknown>;
|
|
227
|
+
if (action.kind === "text" || action.kind === "attachment") {
|
|
228
|
+
const accepted = project.parseSendAccepted(result, target(selected));
|
|
229
|
+
return { state: "accepted", messageId: automationText(accepted.messageGuid, 256), providerReceiptId: null, delivery: "unknown" };
|
|
230
|
+
}
|
|
231
|
+
const allowed = action.kind === "reaction" ? ["ok", "reaction"] : action.kind === "poll" ? ["ok", "event", "guid", "message_id", "poll"] : action.kind === "link" ? ["ok", "id", "guid", "message_id", "messageGuid", "chat_guid", "chatGuid", "queued", "service", "richLinkImageUsed"] : ["ok", "transfer_guid"];
|
|
232
|
+
if (result.ok !== true || Object.keys(result).some(key => !allowed.includes(key)) || result.chat_guid !== undefined && result.chat_guid !== selected.chatGuid || result.transport !== undefined && result.transport !== "applescript") throw new Error("iMessage acceptance changed");
|
|
233
|
+
const ids = [result.guid, result.message_id, result.messageGuid].filter(value => value !== undefined).map(value => automationText(value, 256));
|
|
234
|
+
const messageId = ids[0] ?? null;
|
|
235
|
+
if (ids.some(value => value !== messageId)) throw new Error("Ambiguous iMessage result identity");
|
|
236
|
+
if (result.chatGuid !== undefined && result.chatGuid !== selected.chatGuid || result.queued !== undefined && typeof result.queued !== "boolean") throw new Error("iMessage rich-link receipt changed");
|
|
237
|
+
if (action.kind === "link" && (result.service !== undefined && result.service !== "iMessage" || result.richLinkImageUsed !== undefined && result.richLinkImageUsed !== false)) throw new Error("iMessage rich-link transport or metadata mode changed");
|
|
238
|
+
if (result.id !== undefined) automationInteger(result.id, 1, Number.MAX_SAFE_INTEGER);
|
|
239
|
+
if (action.kind === "link" && messageId === null) throw new Error("Rich-link acceptance has no reconciled message identity");
|
|
240
|
+
const providerReceiptId = result.transfer_guid === undefined ? null : automationText(result.transfer_guid, 256);
|
|
241
|
+
if (action.kind === "reaction") { automationRecord(result, ["ok", "reaction"]); automationInteger(result.reaction, action.remove ? 3000 : 2000, action.remove ? 3005 : 2005); }
|
|
242
|
+
if (action.kind === "sticker") { automationRecord(result, ["ok", "transfer_guid"]); if (providerReceiptId === null) throw new Error("Sticker transfer receipt is missing"); }
|
|
243
|
+
if (action.kind === "poll" && (result.event !== "imessage.poll.created" || messageId === null)) throw new Error("Poll creation receipt is missing");
|
|
244
|
+
return { state: "accepted", messageId, providerReceiptId, delivery: "unknown" };
|
|
245
|
+
});
|
|
246
|
+
} catch { return { state: dispatched ? "indeterminate" : "not-started", reason: dispatched ? "iMessage dispatch or process cleanup could not be reconciled. Do not retry." : "iMessage admission failed before any send." }; }
|
|
247
|
+
},
|
|
248
|
+
async close() { closed = true; await inFlight; },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
unlink,
|
|
10
10
|
} from "node:fs/promises";
|
|
11
11
|
import { isAbsolute, join } from "node:path";
|
|
12
|
+
import { ensureImsgNativeResources, verifyImsgNativeResources } from "./messaging-native-install";
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
ensurePrivateStateDirectory,
|
|
@@ -136,6 +137,11 @@ export async function resolvePinnedImsgBinary(
|
|
|
136
137
|
`reviewed imsg transport ${IMSG_REVIEWED_VERSION} is unavailable or failed integrity verification`,
|
|
137
138
|
);
|
|
138
139
|
}
|
|
140
|
+
try {
|
|
141
|
+
await verifyImsgNativeResources(join(ghostgetStateHome(environment), "tools", "imsg", IMSG_REVIEWED_VERSION));
|
|
142
|
+
} catch {
|
|
143
|
+
throw installFailure("reviewed imsg resources are unavailable or failed integrity verification");
|
|
144
|
+
}
|
|
139
145
|
return resolved;
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -274,6 +280,7 @@ export async function installReviewedImsgBinary(
|
|
|
274
280
|
try {
|
|
275
281
|
ensurePrivateStateDirectory(installDirectory, environment);
|
|
276
282
|
await validateInstallDirectory(installDirectory);
|
|
283
|
+
await ensureImsgNativeResources(installDirectory, environment);
|
|
277
284
|
} catch (error) {
|
|
278
285
|
if (error instanceof ImsgInstallFailure) throw error;
|
|
279
286
|
throw installFailure("imsg install state directory is unavailable or unsafe");
|