@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.
- package/CHANGELOG.md +283 -0
- package/README.md +7 -7
- 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-9wca02er.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 +2 -2
- package/docs/imessage-direct-provider.md +31 -7
- package/docs/messaging-automation.md +103 -0
- package/package.json +22 -2
- package/skills/ghostget/references/control-panel.md +1 -1
- package/skills/ghostget/references/install.md +5 -5
- package/skills/ghostget/references/linkedin-adapter.md +209 -13
- 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/beeper-client-types.ts +1 -1
- package/src/ghostget.ts +16 -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/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-plugin.ts +14 -0
- 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 +1023 -51
- 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/usage.ts +3 -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-plugin.ts
CHANGED
|
@@ -3485,6 +3485,20 @@ export function bindProviderPluginRuntimeLoadIdentity(
|
|
|
3485
3485
|
);
|
|
3486
3486
|
}
|
|
3487
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
|
+
|
|
3488
3502
|
export function lazyProviderApiRuntime(
|
|
3489
3503
|
loader: () => Promise<ProviderApiPluginRuntimeV1>,
|
|
3490
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");
|
|
@@ -891,7 +891,7 @@ function parseMessages(
|
|
|
891
891
|
|
|
892
892
|
function parseSendAccepted(
|
|
893
893
|
value: unknown,
|
|
894
|
-
input:
|
|
894
|
+
input: ImsgChatCoordinate,
|
|
895
895
|
): Readonly<Record<string, unknown>> {
|
|
896
896
|
const source = record(value, "imsg send result");
|
|
897
897
|
exactKeys(source, [
|
|
@@ -1033,6 +1033,9 @@ async function withRuntime<T>(
|
|
|
1033
1033
|
durableCleanupAdmissionRequired: boolean,
|
|
1034
1034
|
operation: (context: Readonly<{
|
|
1035
1035
|
subject: string;
|
|
1036
|
+
sourceGeneration: string;
|
|
1037
|
+
status: unknown;
|
|
1038
|
+
operationRoot: string;
|
|
1036
1039
|
run: (
|
|
1037
1040
|
requests: readonly ImsgRpcRequest[],
|
|
1038
1041
|
beforeSpawn?: () => Promise<void>,
|
|
@@ -1053,6 +1056,11 @@ async function withRuntime<T>(
|
|
|
1053
1056
|
?? join(homedir(), "Library", "Messages");
|
|
1054
1057
|
const store = await validateMessagesStore(auth.path, expectedStorePath);
|
|
1055
1058
|
const subject = subjectForStore(store.storePath);
|
|
1059
|
+
const databaseIdentity = await lstat(store.databasePath, { bigint: true });
|
|
1060
|
+
const sourceGeneration = createHash("sha256").update(canonicalJson({
|
|
1061
|
+
subject, device: String(databaseIdentity.dev), inode: String(databaseIdentity.ino),
|
|
1062
|
+
birthtime: String(databaseIdentity.birthtimeNs),
|
|
1063
|
+
})).digest("hex");
|
|
1056
1064
|
if (auth.subject !== undefined && auth.subject !== subject) {
|
|
1057
1065
|
throw new Error("current Messages device-default realm does not match the bound auth subject");
|
|
1058
1066
|
}
|
|
@@ -1107,7 +1115,12 @@ async function withRuntime<T>(
|
|
|
1107
1115
|
await realpath(reportedDatabasePath) !== reportedDatabasePath
|
|
1108
1116
|
|| reportedDatabasePath !== store.databasePath
|
|
1109
1117
|
) throw new Error("imsg status reported a different Messages database than the bound subject");
|
|
1110
|
-
|
|
1118
|
+
const result = await operation(Object.freeze({ subject, sourceGeneration, status: status.get("status"), operationRoot, run }));
|
|
1119
|
+
const after = await lstat(store.databasePath, { bigint: true });
|
|
1120
|
+
if (after.dev !== databaseIdentity.dev || after.ino !== databaseIdentity.ino || after.birthtimeNs !== databaseIdentity.birthtimeNs || after.isSymbolicLink()) {
|
|
1121
|
+
throw new Error("Messages database generation changed during the operation");
|
|
1122
|
+
}
|
|
1123
|
+
return result;
|
|
1111
1124
|
} finally {
|
|
1112
1125
|
try {
|
|
1113
1126
|
if (operationRoot === undefined) {
|
|
@@ -1137,6 +1150,23 @@ async function withRuntime<T>(
|
|
|
1137
1150
|
}
|
|
1138
1151
|
}
|
|
1139
1152
|
|
|
1153
|
+
/** Internal trusted host seam: retains the pinned executable, exact database,
|
|
1154
|
+
* durable process cleanup and deadline used by the existing provider runtime.
|
|
1155
|
+
* Callers build only reviewed semantic requests; this is never an agent tool. */
|
|
1156
|
+
export async function withImsgAutomationRuntime<T>(
|
|
1157
|
+
auth: GhostgetAuth,
|
|
1158
|
+
options: LocalCliExecutionOptions & Readonly<{ dependencies?: ImsgDirectRuntimeDependencies }>,
|
|
1159
|
+
operation: Parameters<typeof withRuntime<T>>[9],
|
|
1160
|
+
): Promise<T> {
|
|
1161
|
+
if (options.registerCleanupBarrier === undefined) throw new Error("Messaging automation requires durable provider cleanup custody");
|
|
1162
|
+
return startProviderPluginCleanupTrackedOperation(options.registerCleanupBarrier, (publish, cleanup) => withRuntime(
|
|
1163
|
+
requireImsgAuth(auth), 30_000, 10 * 1024 * 1024, options.dependencies,
|
|
1164
|
+
options.environment ?? process.env, options.operationDeadline, publish, cleanup, true, operation,
|
|
1165
|
+
));
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
export const imsgAutomationProjection = Object.freeze({ parseChats, parseChat, parseMessages, parseMessage, exactChat, parseSendAccepted });
|
|
1169
|
+
|
|
1140
1170
|
export async function inspectImsgDirectRuntime(
|
|
1141
1171
|
environment: Readonly<Record<string, string | undefined>>,
|
|
1142
1172
|
): Promise<LocalCliPluginRuntimeStatusV1> {
|
|
@@ -13,7 +13,11 @@ export const IMSG_EXACT_CHAT_PATCH_COMMIT =
|
|
|
13
13
|
"c5994f00d17969fd7772fd2772e7b3591089513a" as const;
|
|
14
14
|
export const IMSG_EXACT_CHAT_PATCH_SHA256 =
|
|
15
15
|
"b05aa92a078930f96fda611c674436638843f7b148d7bfa3b65ed1ccb0885c13" as const;
|
|
16
|
-
export const
|
|
16
|
+
export const IMSG_NO_FETCH_RICH_CARDS_PATCH_COMMIT =
|
|
17
|
+
"520b82ab025c1d4d57333b552aa65c9ae3fdb5fd" as const;
|
|
18
|
+
export const IMSG_NO_FETCH_RICH_CARDS_PATCH_SHA256 =
|
|
19
|
+
"c2346afca4dd0f9721c235db4f692d6a2d6dece36481e074d94b5ce78f2e019b" as const;
|
|
20
|
+
export const IMSG_REVIEWED_PATCH_COMMIT = IMSG_NO_FETCH_RICH_CARDS_PATCH_COMMIT;
|
|
17
21
|
export const IMSG_REVIEWED_PATCHES = Object.freeze([
|
|
18
22
|
Object.freeze({
|
|
19
23
|
commit: IMSG_PRIVATE_TRANSPORT_PATCH_COMMIT,
|
|
@@ -23,11 +27,17 @@ export const IMSG_REVIEWED_PATCHES = Object.freeze([
|
|
|
23
27
|
commit: IMSG_EXACT_CHAT_PATCH_COMMIT,
|
|
24
28
|
sha256: IMSG_EXACT_CHAT_PATCH_SHA256,
|
|
25
29
|
}),
|
|
30
|
+
Object.freeze({
|
|
31
|
+
commit: IMSG_NO_FETCH_RICH_CARDS_PATCH_COMMIT,
|
|
32
|
+
sha256: IMSG_NO_FETCH_RICH_CARDS_PATCH_SHA256,
|
|
33
|
+
}),
|
|
26
34
|
]);
|
|
27
35
|
export const IMSG_REVIEWED_VERSION =
|
|
28
|
-
"0.14.1+private-transport.
|
|
36
|
+
"0.14.1+private-transport.3" as const;
|
|
29
37
|
export const IMSG_DARWIN_ARM64_EXECUTABLE_SHA256 =
|
|
30
|
-
"
|
|
38
|
+
"46c4c73c81c7db2d516c2d467c66aff03c73de196996d646bc8afce0ea85cff6" as const;
|
|
39
|
+
// The exact .3 source and binary pins include the tested no-fetch RPC mode.
|
|
40
|
+
export const IMSG_NO_FETCH_RICH_CARDS_AVAILABLE = true;
|
|
31
41
|
|
|
32
42
|
export const IMSG_TOOL_PIN = Object.freeze({
|
|
33
43
|
id: "imsg-private-transport",
|
|
@@ -16,6 +16,27 @@ export class LinkedInContactIdentityMismatch extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export function isLinkedInContactGraphqlUnavailable(error: unknown): boolean {
|
|
20
|
+
let current: unknown = error;
|
|
21
|
+
for (let depth = 0; depth < 8 && current !== undefined; depth += 1) {
|
|
22
|
+
if (current instanceof LinkedInProfileBrowserResponseRejectedError) {
|
|
23
|
+
return current.status === 403
|
|
24
|
+
|| current.contentType === "text/html"
|
|
25
|
+
|| current.contentType === "missing";
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
current instanceof Error
|
|
29
|
+
&& "cause" in current
|
|
30
|
+
&& current.cause !== current
|
|
31
|
+
) {
|
|
32
|
+
current = current.cause;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
export function linkedInContactReadFailure(
|
|
20
41
|
error: unknown,
|
|
21
42
|
stage: LinkedInContactStage,
|
|
@@ -4,7 +4,10 @@ import * as Layer from "effect/Layer";
|
|
|
4
4
|
import { ReadEffectFailure, readAttempt } from "../read-effect";
|
|
5
5
|
import { readNative } from "../read-effect-platform";
|
|
6
6
|
import { LinkedInContactIdentityMismatch } from "./linkedin-contact-failure";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
LinkedInContactInfoJsonInput,
|
|
9
|
+
LinkedInContactNavigationInput,
|
|
10
|
+
} from "./linkedin-web-contact";
|
|
8
11
|
import type { LinkedInProfileBrowserTransport } from "./linkedin-web-profile-browser";
|
|
9
12
|
|
|
10
13
|
export type LinkedInContactIdentity = { readonly subject: string };
|
|
@@ -30,6 +33,10 @@ function platform(ports: LinkedInContactNative) {
|
|
|
30
33
|
readNative(() => browser.readProfileHtml(profileUrl)).pipe(Effect.uninterruptible),
|
|
31
34
|
contactPayload: (browser: LinkedInProfileBrowserTransport, input: LinkedInContactInfoJsonInput) =>
|
|
32
35
|
readNative(() => browser.readContactInfoJson(input)).pipe(Effect.uninterruptible),
|
|
36
|
+
contactOverlay: (browser: LinkedInProfileBrowserTransport, input: LinkedInContactInfoJsonInput) =>
|
|
37
|
+
readNative(() => browser.readContactOverlayText(input)).pipe(Effect.uninterruptible),
|
|
38
|
+
contactNavigation: (browser: LinkedInProfileBrowserTransport, input: LinkedInContactNavigationInput) =>
|
|
39
|
+
readNative(() => browser.readContactNavigationText(input)).pipe(Effect.uninterruptible),
|
|
33
40
|
observedAt: readAttempt(ports.observedAt),
|
|
34
41
|
closeBrowser: (browser: LinkedInProfileBrowserTransport) =>
|
|
35
42
|
readNative(() => browser.close()).pipe(Effect.uninterruptible),
|
|
@@ -4,11 +4,14 @@ import { readAttempt, type ReadEffectFailure, withReadResource } from "../read-e
|
|
|
4
4
|
import type { WebSessionExecution } from "../web-session-execution";
|
|
5
5
|
import { LinkedInContactPlatform } from "./linkedin-contact-platform";
|
|
6
6
|
import {
|
|
7
|
+
isLinkedInContactGraphqlUnavailable,
|
|
7
8
|
linkedInContactDiagnostic,
|
|
8
9
|
linkedInContactReadFailure,
|
|
9
10
|
type LinkedInContactStage,
|
|
10
11
|
} from "./linkedin-contact-failure";
|
|
11
12
|
import {
|
|
13
|
+
extractLinkedInContactNavigationAction,
|
|
14
|
+
linkedInContactNavigationActionError,
|
|
12
15
|
projectLinkedInContactInfo,
|
|
13
16
|
projectLinkedInEmbeddedContactFields,
|
|
14
17
|
projectLinkedInProfileContactBinding,
|
|
@@ -50,12 +53,38 @@ export function linkedInContactReadProgram(
|
|
|
50
53
|
});
|
|
51
54
|
} else {
|
|
52
55
|
stage = "contact";
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
const navigation = yield* readAttempt(() => extractLinkedInContactNavigationAction({
|
|
57
|
+
profileHtml,
|
|
58
|
+
publicIdentifier: binding.vanity,
|
|
56
59
|
profileUrn: binding.profileUrn,
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
}));
|
|
61
|
+
if (navigation.kind === "action") {
|
|
62
|
+
contactPayload = yield* platform.contactNavigation(browser, {
|
|
63
|
+
profileUrl: target.url,
|
|
64
|
+
profileUrn: binding.profileUrn,
|
|
65
|
+
sduiid: navigation.action.sduiid,
|
|
66
|
+
clientArguments: navigation.action.clientArguments,
|
|
67
|
+
});
|
|
68
|
+
} else if (navigation.kind !== "absent") {
|
|
69
|
+
yield* readAttempt(() => {
|
|
70
|
+
throw linkedInContactNavigationActionError(navigation);
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
const queryId = resolveLinkedInProfileContactInfoQueryId(profileHtml);
|
|
74
|
+
const contactInput = {
|
|
75
|
+
profileUrl: target.url,
|
|
76
|
+
profileUrn: binding.profileUrn,
|
|
77
|
+
...(queryId === undefined ? {} : { queryId }),
|
|
78
|
+
};
|
|
79
|
+
contactPayload = queryId === undefined
|
|
80
|
+
? yield* platform.contactOverlay(browser, contactInput)
|
|
81
|
+
: yield* platform.contactPayload(browser, contactInput).pipe(
|
|
82
|
+
Effect.catchIf(
|
|
83
|
+
(error) => isLinkedInContactGraphqlUnavailable(error),
|
|
84
|
+
() => platform.contactOverlay(browser, contactInput),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
59
88
|
}
|
|
60
89
|
stage = "projection";
|
|
61
90
|
const observedAt = yield* platform.observedAt;
|