@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,609 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
AUTOMATION_ACTION_KINDS,
|
|
4
|
+
automationArray,
|
|
5
|
+
automationDate,
|
|
6
|
+
automationDigest,
|
|
7
|
+
automationId,
|
|
8
|
+
automationInteger,
|
|
9
|
+
automationRecord,
|
|
10
|
+
automationText,
|
|
11
|
+
parseAutomationAction,
|
|
12
|
+
parseAutomationActionKind,
|
|
13
|
+
parseAutomationCoordinate,
|
|
14
|
+
parseAutomationIdentity,
|
|
15
|
+
parseAutomationMessage
|
|
16
|
+
} from "./index-2ymnp8xv.js";
|
|
17
|
+
import {
|
|
18
|
+
ensurePrivateStateDirectory,
|
|
19
|
+
ghostgetStateHome,
|
|
20
|
+
snapshotPrivateStateDirectory
|
|
21
|
+
} from "./index-yq6maz71.js";
|
|
22
|
+
import {
|
|
23
|
+
canonicalJson,
|
|
24
|
+
sha256
|
|
25
|
+
} from "./index-gwk7rbyj.js";
|
|
26
|
+
import {
|
|
27
|
+
MESSAGING_AUTOMATION_PROTOCOL
|
|
28
|
+
} from "./index-01eeae9e.js";
|
|
29
|
+
import"./index-z1w83f81.js";
|
|
30
|
+
|
|
31
|
+
// src/messaging-automation.ts
|
|
32
|
+
import { Database, constants as sqlite } from "bun:sqlite";
|
|
33
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync } from "fs";
|
|
34
|
+
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto";
|
|
35
|
+
import { join } from "path";
|
|
36
|
+
var RUN_COLUMNS = "id,plan_id,intent_id,enrollment_id,state,accepted,total,reason";
|
|
37
|
+
var same = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
38
|
+
var authority = (identity, selected) => ({ identity, conversation: { ...selected, title: null } });
|
|
39
|
+
var sameConversation = (a, b) => same({ ...a, title: null }, { ...b, title: null });
|
|
40
|
+
var stopped = (signal) => {
|
|
41
|
+
if (signal?.aborted)
|
|
42
|
+
throw new Error("Messaging automation operation was cancelled.");
|
|
43
|
+
};
|
|
44
|
+
var CAPACITY = Object.freeze({ enrollments: 1000, grants: 1e4, plans: 20000, runs: 20000, messages: 50000, events: 50000 });
|
|
45
|
+
function grantData(value) {
|
|
46
|
+
const r = automationRecord(value, ["enrollmentId", "expectedBindingDigest", "actions", "expiresAt", "maximumActions", "minimumIntervalMs"]);
|
|
47
|
+
const actions = automationArray(r.actions, AUTOMATION_ACTION_KINDS.length).map(parseAutomationActionKind);
|
|
48
|
+
if (actions.length === 0 || new Set(actions).size !== actions.length)
|
|
49
|
+
throw new Error("Messaging grant action list is invalid.");
|
|
50
|
+
return Object.freeze({ enrollmentId: automationId(r.enrollmentId), expectedBindingDigest: automationDigest(r.expectedBindingDigest), actions: Object.freeze(actions), expiresAt: automationDate(r.expiresAt), maximumActions: automationInteger(r.maximumActions, 1, 1e5), minimumIntervalMs: automationInteger(r.minimumIntervalMs, 0, 86400000) });
|
|
51
|
+
}
|
|
52
|
+
function planData(value) {
|
|
53
|
+
const r = automationRecord(value, ["enrollmentId", "expectedRevision", "intentId", "actions", "bindingDigest", "expiresAt", "id", "digest"]);
|
|
54
|
+
const actions = automationArray(r.actions, 8).map(parseAutomationAction);
|
|
55
|
+
if (actions.length === 0 || Buffer.byteLength(canonicalJson(actions)) > 256 * 1024)
|
|
56
|
+
throw new Error("Messaging plan exceeds its bound.");
|
|
57
|
+
const binding = { enrollmentId: automationId(r.enrollmentId), expectedRevision: automationInteger(r.expectedRevision, 0, Number.MAX_SAFE_INTEGER), intentId: automationId(r.intentId), actions: Object.freeze(actions), bindingDigest: automationDigest(r.bindingDigest), expiresAt: automationDate(r.expiresAt) };
|
|
58
|
+
const digest = automationDigest(r.digest);
|
|
59
|
+
const id = automationId(r.id);
|
|
60
|
+
if (sha256(canonicalJson(binding)) !== digest || id !== `plan:${digest}`)
|
|
61
|
+
throw new Error("Messaging plan binding is invalid.");
|
|
62
|
+
return Object.freeze({ ...binding, id, digest });
|
|
63
|
+
}
|
|
64
|
+
function conversation(value) {
|
|
65
|
+
const r = automationRecord(value, ["coordinate", "title", "kind", "participants"]);
|
|
66
|
+
const coordinate = parseAutomationCoordinate(r.coordinate);
|
|
67
|
+
const participants = automationArray(r.participants, 2).map((value2) => automationText(value2, 512));
|
|
68
|
+
if (r.kind !== "single" || participants.length < 1 || new Set(participants).size !== participants.length)
|
|
69
|
+
throw new Error("Messaging automation requires one verified individual conversation.");
|
|
70
|
+
return Object.freeze({ coordinate, title: r.title === null ? null : automationText(r.title, 512), kind: "single", participants: Object.freeze([...participants].sort()) });
|
|
71
|
+
}
|
|
72
|
+
function checkedPage(value, expected, coordinate) {
|
|
73
|
+
const r = automationRecord(value, ["identity", "messages", "nextCursor", "caughtUp", "gap"]);
|
|
74
|
+
const identity = parseAutomationIdentity(r.identity);
|
|
75
|
+
if (!same(identity, expected) || typeof r.caughtUp !== "boolean" || typeof r.gap !== "boolean")
|
|
76
|
+
throw new Error("Messaging provider identity or cursor state changed.");
|
|
77
|
+
const messages = automationArray(r.messages, 500).map(parseAutomationMessage);
|
|
78
|
+
if (messages.some((message) => !same(message.coordinate, coordinate)))
|
|
79
|
+
throw new Error("Messaging provider returned another conversation.");
|
|
80
|
+
return Object.freeze({ identity, messages: Object.freeze(messages), nextCursor: automationText(r.nextCursor, 4096), caughtUp: r.caughtUp, gap: r.gap });
|
|
81
|
+
}
|
|
82
|
+
function checkedResult(value) {
|
|
83
|
+
if (typeof value !== "object" || value === null)
|
|
84
|
+
throw new Error("Messaging provider result is invalid.");
|
|
85
|
+
const state = Object.getOwnPropertyDescriptor(value, "state")?.value;
|
|
86
|
+
if (state === "accepted") {
|
|
87
|
+
const r2 = automationRecord(value, ["state", "messageId", "providerReceiptId", "delivery"]);
|
|
88
|
+
if (r2.delivery !== "unknown")
|
|
89
|
+
throw new Error("Messaging provider delivery claim is unsupported.");
|
|
90
|
+
return { state, messageId: r2.messageId === null ? null : automationId(r2.messageId), providerReceiptId: r2.providerReceiptId === null ? null : automationId(r2.providerReceiptId), delivery: "unknown" };
|
|
91
|
+
}
|
|
92
|
+
const r = automationRecord(value, ["state", "reason"]);
|
|
93
|
+
if (state !== "not-started" && state !== "indeterminate")
|
|
94
|
+
throw new Error("Messaging provider result is invalid.");
|
|
95
|
+
return { state, reason: automationText(r.reason, 1024) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class MessagingAutomationHost {
|
|
99
|
+
environment;
|
|
100
|
+
now;
|
|
101
|
+
db;
|
|
102
|
+
directory;
|
|
103
|
+
directoryIdentity;
|
|
104
|
+
fileIdentity;
|
|
105
|
+
providers = new Map;
|
|
106
|
+
cursorKey;
|
|
107
|
+
controllers = new Set;
|
|
108
|
+
planControllers = new Map;
|
|
109
|
+
pending = new Set;
|
|
110
|
+
closing = false;
|
|
111
|
+
closed = false;
|
|
112
|
+
constructor(providers, environment = process.env, now = Date.now) {
|
|
113
|
+
this.environment = environment;
|
|
114
|
+
this.now = now;
|
|
115
|
+
if (providers.length > 16)
|
|
116
|
+
throw new Error("Too many messaging providers.");
|
|
117
|
+
for (const provider of providers) {
|
|
118
|
+
if (this.providers.has(provider.provider))
|
|
119
|
+
throw new Error("Duplicate messaging provider.");
|
|
120
|
+
this.providers.set(provider.provider, provider);
|
|
121
|
+
}
|
|
122
|
+
this.directory = join(ghostgetStateHome(environment), "messaging", "automation");
|
|
123
|
+
this.directoryIdentity = ensurePrivateStateDirectory(this.directory, environment);
|
|
124
|
+
const path = join(this.directory, "host.sqlite");
|
|
125
|
+
const fd = openSync(path, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 384);
|
|
126
|
+
try {
|
|
127
|
+
const stat = fstatSync(fd);
|
|
128
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 511) !== 384 || stat.nlink !== 1)
|
|
129
|
+
throw new Error("Unsafe messaging journal.");
|
|
130
|
+
this.fileIdentity = { dev: stat.dev, ino: stat.ino };
|
|
131
|
+
} finally {
|
|
132
|
+
closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
this.checkFiles();
|
|
135
|
+
this.db = new Database(path, sqlite.SQLITE_OPEN_READWRITE | sqlite.SQLITE_OPEN_NOFOLLOW);
|
|
136
|
+
try {
|
|
137
|
+
this.db.exec("PRAGMA busy_timeout=250; PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL; PRAGMA trusted_schema=OFF; PRAGMA secure_delete=ON; PRAGMA max_page_count=65536;");
|
|
138
|
+
const version = this.db.query("PRAGMA user_version").get()?.user_version;
|
|
139
|
+
if (version !== 0 && version !== 1 && version !== 2)
|
|
140
|
+
throw new Error("Unsupported messaging journal schema.");
|
|
141
|
+
this.db.exec("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY,value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS enrollments (id TEXT PRIMARY KEY,data TEXT NOT NULL,cursor TEXT NOT NULL,revision INTEGER NOT NULL,ready INTEGER NOT NULL,reason TEXT); CREATE TABLE IF NOT EXISTS grants (id TEXT PRIMARY KEY,data TEXT NOT NULL,revoked INTEGER NOT NULL,consumed INTEGER NOT NULL,last_dispatch INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS plans (id TEXT PRIMARY KEY,data TEXT NOT NULL); CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY,plan_id TEXT NOT NULL,intent_id TEXT NOT NULL UNIQUE,enrollment_id TEXT NOT NULL,state TEXT NOT NULL,accepted TEXT NOT NULL,total INTEGER NOT NULL,reason TEXT); CREATE INDEX IF NOT EXISTS runs_contact ON runs(enrollment_id,state); CREATE TABLE IF NOT EXISTS messages (enrollment_id TEXT NOT NULL,event_key TEXT NOT NULL,data TEXT NOT NULL,PRIMARY KEY(enrollment_id,event_key)); CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT,enrollment_id TEXT NOT NULL,revision INTEGER NOT NULL,data TEXT NOT NULL);");
|
|
142
|
+
if (version !== 2)
|
|
143
|
+
this.db.transaction(() => {
|
|
144
|
+
this.db.exec("ALTER TABLE enrollments ADD COLUMN baselining INTEGER NOT NULL DEFAULT 1; ALTER TABLE enrollments ADD COLUMN gap INTEGER NOT NULL DEFAULT 0; UPDATE enrollments SET gap=CASE WHEN reason LIKE '%unresolved gap%' THEN 1 ELSE 0 END,ready=0; PRAGMA user_version=2;");
|
|
145
|
+
}).immediate();
|
|
146
|
+
else
|
|
147
|
+
this.db.exec("PRAGMA user_version=2;");
|
|
148
|
+
this.db.exec("CREATE TABLE IF NOT EXISTS grant_intents (id TEXT PRIMARY KEY,request_digest TEXT NOT NULL,grant_id TEXT NOT NULL UNIQUE REFERENCES grants(id));");
|
|
149
|
+
this.db.query("INSERT OR IGNORE INTO metadata(key,value) VALUES('cursor-key',?)").run(randomBytes(32).toString("hex"));
|
|
150
|
+
this.cursorKey = Buffer.from(automationDigest(this.db.query("SELECT value FROM metadata WHERE key='cursor-key'").get()?.value), "hex");
|
|
151
|
+
this.checkFiles();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.db.close();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
checkFiles() {
|
|
158
|
+
snapshotPrivateStateDirectory(this.directory, this.environment, this.directoryIdentity);
|
|
159
|
+
for (const name of ["host.sqlite", "host.sqlite-journal", "host.sqlite-wal", "host.sqlite-shm"]) {
|
|
160
|
+
let stat;
|
|
161
|
+
try {
|
|
162
|
+
stat = lstatSync(join(this.directory, name));
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error.code === "ENOENT" && name !== "host.sqlite")
|
|
165
|
+
continue;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 511) !== 384 || stat.nlink !== 1 || name.endsWith("-wal") || name.endsWith("-shm") || name === "host.sqlite" && (stat.dev !== this.fileIdentity.dev || stat.ino !== this.fileIdentity.ino))
|
|
169
|
+
throw new Error("Messaging journal filesystem identity changed.");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
ready() {
|
|
173
|
+
if (this.closing || this.closed)
|
|
174
|
+
throw new Error("Messaging automation host is closed.");
|
|
175
|
+
this.checkFiles();
|
|
176
|
+
}
|
|
177
|
+
capacity(table, additional = 1) {
|
|
178
|
+
const count = this.db.query(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
|
|
179
|
+
const pages = this.db.query("PRAGMA page_count").get().page_count;
|
|
180
|
+
const size = this.db.query("PRAGMA page_size").get().page_size;
|
|
181
|
+
if (count + additional > CAPACITY[table] || pages * size >= 192 * 1024 * 1024)
|
|
182
|
+
throw new Error("Messaging journal capacity reached; owner maintenance is required before further dispatch.");
|
|
183
|
+
}
|
|
184
|
+
provider(id) {
|
|
185
|
+
const provider = this.providers.get(id);
|
|
186
|
+
if (provider === undefined)
|
|
187
|
+
throw new Error("Messaging provider is unavailable.");
|
|
188
|
+
return provider;
|
|
189
|
+
}
|
|
190
|
+
row(id) {
|
|
191
|
+
this.ready();
|
|
192
|
+
const row = this.db.query("SELECT * FROM enrollments WHERE id=?").get(automationId(id));
|
|
193
|
+
if (row === null)
|
|
194
|
+
throw new Error("Messaging enrollment does not exist.");
|
|
195
|
+
return row;
|
|
196
|
+
}
|
|
197
|
+
enrollment(row) {
|
|
198
|
+
const r = automationRecord(JSON.parse(row.data), ["identity", "conversation", "bindingDigest"]);
|
|
199
|
+
const identity = parseAutomationIdentity(r.identity);
|
|
200
|
+
const selected = conversation(r.conversation);
|
|
201
|
+
const bindingDigest = automationDigest(r.bindingDigest);
|
|
202
|
+
if (sha256(canonicalJson(authority(identity, selected))) !== bindingDigest)
|
|
203
|
+
throw new Error("Messaging enrollment binding is invalid.");
|
|
204
|
+
const ready = automationInteger(row.ready, 0, 1);
|
|
205
|
+
const baselining = automationInteger(row.baselining, 0, 1);
|
|
206
|
+
const gap = automationInteger(row.gap, 0, 1);
|
|
207
|
+
if (ready && (baselining || gap))
|
|
208
|
+
throw new Error("Messaging enrollment readiness is inconsistent.");
|
|
209
|
+
return Object.freeze({ id: automationId(row.id), identity, conversation: selected, bindingDigest, revision: automationInteger(row.revision, 0, Number.MAX_SAFE_INTEGER), ready: ready === 1, reason: row.reason === null ? null : automationText(row.reason, 1024) });
|
|
210
|
+
}
|
|
211
|
+
enrollments() {
|
|
212
|
+
this.ready();
|
|
213
|
+
return this.db.query("SELECT * FROM enrollments ORDER BY id LIMIT 1001").all().map((row) => this.enrollment(row));
|
|
214
|
+
}
|
|
215
|
+
async status(provider, signal) {
|
|
216
|
+
stopped(signal);
|
|
217
|
+
const value = await provider.inspect(signal);
|
|
218
|
+
stopped(signal);
|
|
219
|
+
const r = automationRecord(value, ["identity", "connected", "events", "actions"]);
|
|
220
|
+
const identity = parseAutomationIdentity(r.identity);
|
|
221
|
+
if (identity.provider !== provider.provider || typeof r.connected !== "boolean")
|
|
222
|
+
throw new Error("Messaging provider identity is invalid.");
|
|
223
|
+
const actions = automationRecord(r.actions, AUTOMATION_ACTION_KINDS);
|
|
224
|
+
for (const capability of [r.events, ...Object.values(actions)]) {
|
|
225
|
+
const c = automationRecord(capability, ["available", "reason"]);
|
|
226
|
+
if (typeof c.available !== "boolean")
|
|
227
|
+
throw new Error("Messaging capability is invalid.");
|
|
228
|
+
if (c.reason !== null)
|
|
229
|
+
automationText(c.reason, 1024);
|
|
230
|
+
}
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
async providerStatus(id, signal) {
|
|
234
|
+
this.ready();
|
|
235
|
+
return this.status(this.provider(automationId(id)), signal);
|
|
236
|
+
}
|
|
237
|
+
async conversations(raw, signal) {
|
|
238
|
+
this.ready();
|
|
239
|
+
const r = automationRecord(raw, ["provider", "limit"]);
|
|
240
|
+
const provider = this.provider(automationId(r.provider)), limit = automationInteger(r.limit, 1, 200);
|
|
241
|
+
const status = await this.status(provider, signal);
|
|
242
|
+
const result = automationRecord(await provider.conversations({ limit }, signal), ["identity", "conversations", "complete"]);
|
|
243
|
+
stopped(signal);
|
|
244
|
+
const identity = parseAutomationIdentity(result.identity);
|
|
245
|
+
if (!same(identity, status.identity) || typeof result.complete !== "boolean")
|
|
246
|
+
throw new Error("Messaging discovery identity changed.");
|
|
247
|
+
const rows = automationArray(result.conversations, limit).filter((value) => automationRecord(value, ["coordinate", "title", "kind", "participants"]).kind === "single").map(conversation);
|
|
248
|
+
if (rows.some((row) => row.coordinate.provider !== identity.provider) || new Set(rows.map((row) => canonicalJson(row.coordinate))).size !== rows.length)
|
|
249
|
+
throw new Error("Messaging discovery contains conflicting conversations.");
|
|
250
|
+
return Object.freeze({ identity, conversations: Object.freeze(rows), complete: result.complete });
|
|
251
|
+
}
|
|
252
|
+
history(raw) {
|
|
253
|
+
this.ready();
|
|
254
|
+
const r = automationRecord(raw, ["enrollmentId", "limit"]), limit = automationInteger(r.limit, 1, 200);
|
|
255
|
+
const enrollment = this.enrollment(this.row(automationId(r.enrollmentId)));
|
|
256
|
+
const rows = this.db.query("SELECT data FROM messages WHERE rowid IN (SELECT MAX(rowid) FROM messages WHERE enrollment_id=? GROUP BY json_extract(data,'$.id')) ORDER BY json_extract(data,'$.occurredAt') DESC,rowid DESC LIMIT ?").all(enrollment.id, limit);
|
|
257
|
+
const messages = rows.map((row) => parseAutomationMessage(JSON.parse(row.data))).reverse();
|
|
258
|
+
if (messages.some((message) => !same(message.coordinate, enrollment.conversation.coordinate)))
|
|
259
|
+
throw new Error("Messaging stored history has another conversation.");
|
|
260
|
+
return Object.freeze({ enrollment, messages: Object.freeze(messages) });
|
|
261
|
+
}
|
|
262
|
+
async enroll(raw, signal) {
|
|
263
|
+
this.ready();
|
|
264
|
+
const request = automationRecord(raw, ["provider", "coordinate"]);
|
|
265
|
+
const coordinate = parseAutomationCoordinate(request.coordinate);
|
|
266
|
+
if (request.provider !== coordinate.provider)
|
|
267
|
+
throw new Error("Messaging provider and coordinate disagree.");
|
|
268
|
+
const provider = this.provider(coordinate.provider);
|
|
269
|
+
const status = await this.status(provider, signal);
|
|
270
|
+
const resolved = await provider.resolve(coordinate, signal);
|
|
271
|
+
stopped(signal);
|
|
272
|
+
if (!same(parseAutomationIdentity(resolved.identity), status.identity))
|
|
273
|
+
throw new Error("Messaging identity changed during enrollment.");
|
|
274
|
+
const selected = conversation(resolved.conversation);
|
|
275
|
+
if (!same(selected.coordinate, coordinate))
|
|
276
|
+
throw new Error("Messaging route resolution changed target.");
|
|
277
|
+
const history = checkedPage(await provider.history({ coordinate, limit: 200 }, signal), status.identity, coordinate);
|
|
278
|
+
stopped(signal);
|
|
279
|
+
const binding = { identity: status.identity, conversation: selected };
|
|
280
|
+
const bindingDigest = sha256(canonicalJson(authority(status.identity, selected)));
|
|
281
|
+
const id = `enrollment:${randomUUID()}`;
|
|
282
|
+
this.ready();
|
|
283
|
+
this.db.transaction(() => {
|
|
284
|
+
this.capacity("enrollments");
|
|
285
|
+
this.capacity("messages", history.messages.length);
|
|
286
|
+
if (this.enrollments().some((enrollment) => same(enrollment.identity, status.identity) && same(enrollment.conversation.coordinate, coordinate)))
|
|
287
|
+
throw new Error("Conversation is already enrolled.");
|
|
288
|
+
this.db.query("INSERT INTO enrollments(id,data,cursor,revision,ready,reason,baselining,gap) VALUES(?,?,?,0,?,?,?,?)").run(id, canonicalJson({ ...binding, bindingDigest }), history.nextCursor, !history.gap && history.caughtUp ? 1 : 0, history.gap ? "Provider history has an unresolved gap." : history.caughtUp ? null : "Initial history catchup is incomplete.", history.caughtUp ? 0 : 1, history.gap ? 1 : 0);
|
|
289
|
+
for (const message of history.messages)
|
|
290
|
+
this.db.query("INSERT OR IGNORE INTO messages VALUES(?,?,?)").run(id, sha256(canonicalJson(message)), canonicalJson(message));
|
|
291
|
+
}).immediate();
|
|
292
|
+
this.checkFiles();
|
|
293
|
+
return this.enrollment(this.row(id));
|
|
294
|
+
}
|
|
295
|
+
grant(raw, intent) {
|
|
296
|
+
this.ready();
|
|
297
|
+
const r = automationRecord(raw, ["enrollmentId", "expectedBindingDigest", "actions", "expiresAt", "maximumActions", "minimumIntervalMs"]);
|
|
298
|
+
const enrollment = this.enrollment(this.row(automationId(r.enrollmentId)));
|
|
299
|
+
const expectedBindingDigest = automationDigest(r.expectedBindingDigest);
|
|
300
|
+
if (enrollment.bindingDigest !== expectedBindingDigest)
|
|
301
|
+
throw new Error("Messaging enrollment changed before grant issuance.");
|
|
302
|
+
const actions = automationArray(r.actions, AUTOMATION_ACTION_KINDS.length).map(parseAutomationActionKind);
|
|
303
|
+
if (actions.length === 0 || new Set(actions).size !== actions.length)
|
|
304
|
+
throw new Error("Messaging grant action list is invalid.");
|
|
305
|
+
const expiresAt = automationDate(r.expiresAt);
|
|
306
|
+
const grant = { enrollmentId: enrollment.id, expectedBindingDigest, actions: [...actions].sort(), expiresAt, maximumActions: automationInteger(r.maximumActions, 1, 1e5), minimumIntervalMs: automationInteger(r.minimumIntervalMs, 0, 86400000) };
|
|
307
|
+
const intentId = intent === undefined ? null : automationId(intent), digest = sha256(canonicalJson(grant));
|
|
308
|
+
const result = this.db.transaction(() => {
|
|
309
|
+
if (intentId !== null) {
|
|
310
|
+
const previous = this.db.query("SELECT request_digest,grant_id FROM grant_intents WHERE id=?").get(intentId);
|
|
311
|
+
if (previous) {
|
|
312
|
+
if (automationDigest(previous.request_digest) !== digest)
|
|
313
|
+
throw new Error("Messaging grant intent changed its request.");
|
|
314
|
+
return this.grantStatus(previous.grant_id);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (Date.parse(expiresAt) <= this.now() || Date.parse(expiresAt) > this.now() + 30 * 86400000)
|
|
318
|
+
throw new Error("Messaging grant must expire within 30 days.");
|
|
319
|
+
const id = `grant:${randomUUID()}`;
|
|
320
|
+
this.capacity("grants");
|
|
321
|
+
this.db.query("INSERT INTO grants VALUES(?,?,0,0,0)").run(id, canonicalJson(grant));
|
|
322
|
+
if (intentId !== null)
|
|
323
|
+
this.db.query("INSERT INTO grant_intents VALUES(?,?,?)").run(intentId, digest, id);
|
|
324
|
+
return Object.freeze({ ...grant, id, revoked: false, consumedActions: 0 });
|
|
325
|
+
}).immediate();
|
|
326
|
+
this.checkFiles();
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
grantByIntent(intentId) {
|
|
330
|
+
this.ready();
|
|
331
|
+
const row = this.db.query("SELECT grant_id FROM grant_intents WHERE id=?").get(automationId(intentId));
|
|
332
|
+
return row === null ? null : this.grantStatus(row.grant_id);
|
|
333
|
+
}
|
|
334
|
+
revoke(id) {
|
|
335
|
+
this.ready();
|
|
336
|
+
if (this.db.query("UPDATE grants SET revoked=1 WHERE id=?").run(automationId(id)).changes !== 1)
|
|
337
|
+
throw new Error("Messaging grant does not exist.");
|
|
338
|
+
this.checkFiles();
|
|
339
|
+
}
|
|
340
|
+
grantStatus(id) {
|
|
341
|
+
this.ready();
|
|
342
|
+
const row = this.db.query("SELECT * FROM grants WHERE id=?").get(automationId(id));
|
|
343
|
+
if (row === null)
|
|
344
|
+
throw new Error("Messaging grant does not exist.");
|
|
345
|
+
const grant = grantData(JSON.parse(row.data));
|
|
346
|
+
return Object.freeze({ ...grant, id: automationId(row.id), revoked: automationInteger(row.revoked, 0, 1) === 1, consumedActions: automationInteger(row.consumed, 0, grant.maximumActions) });
|
|
347
|
+
}
|
|
348
|
+
activeRun(enrollmentId) {
|
|
349
|
+
return this.db.query("SELECT COUNT(*) AS n FROM runs WHERE enrollment_id=? AND state IN ('started','partial','indeterminate')").get(enrollmentId).n > 0;
|
|
350
|
+
}
|
|
351
|
+
async poll(enrollmentId, signal) {
|
|
352
|
+
const initial = this.row(enrollmentId);
|
|
353
|
+
const enrollment = this.enrollment(initial);
|
|
354
|
+
if (this.activeRun(enrollmentId))
|
|
355
|
+
return enrollment;
|
|
356
|
+
const provider = this.provider(enrollment.identity.provider);
|
|
357
|
+
const status = await this.status(provider, signal);
|
|
358
|
+
if (!same(status.identity, enrollment.identity)) {
|
|
359
|
+
this.db.query("UPDATE enrollments SET ready=0,reason=? WHERE id=?").run("Provider identity changed; enroll the conversation again.", enrollmentId);
|
|
360
|
+
throw new Error("Messaging provider identity changed.");
|
|
361
|
+
}
|
|
362
|
+
if (!status.connected || !status.events.available) {
|
|
363
|
+
this.db.query("UPDATE enrollments SET ready=0,reason=? WHERE id=?").run(status.events.reason ?? "Provider events are unavailable.", enrollmentId);
|
|
364
|
+
return this.enrollment(this.row(enrollmentId));
|
|
365
|
+
}
|
|
366
|
+
const page = checkedPage(await provider.events({ coordinates: [enrollment.conversation.coordinate], cursor: initial.cursor, limit: 200 }, signal), enrollment.identity, enrollment.conversation.coordinate);
|
|
367
|
+
stopped(signal);
|
|
368
|
+
this.ready();
|
|
369
|
+
this.db.transaction(() => {
|
|
370
|
+
const current = this.row(enrollmentId);
|
|
371
|
+
if (current.cursor !== initial.cursor || current.revision !== initial.revision || current.baselining !== initial.baselining || current.gap !== initial.gap || this.activeRun(enrollmentId))
|
|
372
|
+
throw new Error("Messaging poll lost its concurrent cursor claim.");
|
|
373
|
+
this.capacity("messages", page.messages.length);
|
|
374
|
+
if (!initial.baselining)
|
|
375
|
+
this.capacity("events", page.messages.length);
|
|
376
|
+
let revision = initial.revision;
|
|
377
|
+
for (const [index, message] of page.messages.entries()) {
|
|
378
|
+
const data = canonicalJson(message);
|
|
379
|
+
const previous = this.db.query("SELECT data FROM messages WHERE enrollment_id=? AND json_extract(data,'$.id')=? ORDER BY rowid DESC LIMIT 1").get(enrollmentId, message.id);
|
|
380
|
+
if (previous?.data === data)
|
|
381
|
+
continue;
|
|
382
|
+
this.db.query("INSERT INTO messages VALUES(?,?,?)").run(enrollmentId, sha256(canonicalJson({ cursor: page.nextCursor, index, message })), data);
|
|
383
|
+
if (initial.baselining === 1)
|
|
384
|
+
continue;
|
|
385
|
+
revision = automationInteger(revision + 1, 1, Number.MAX_SAFE_INTEGER);
|
|
386
|
+
this.db.query("INSERT INTO events(enrollment_id,revision,data) VALUES(?,?,?)").run(enrollmentId, revision, data);
|
|
387
|
+
}
|
|
388
|
+
const gap = initial.gap === 1 || page.gap;
|
|
389
|
+
this.db.query("UPDATE enrollments SET cursor=?,revision=?,ready=?,reason=?,baselining=?,gap=? WHERE id=?").run(page.nextCursor, revision, !gap && page.caughtUp ? 1 : 0, gap ? "Provider event stream has an unresolved gap." : page.caughtUp ? null : "Provider catchup is incomplete.", initial.baselining && !page.caughtUp ? 1 : 0, gap ? 1 : 0, enrollmentId);
|
|
390
|
+
}).immediate();
|
|
391
|
+
this.checkFiles();
|
|
392
|
+
return this.enrollment(this.row(enrollmentId));
|
|
393
|
+
}
|
|
394
|
+
events(raw) {
|
|
395
|
+
this.ready();
|
|
396
|
+
const r = automationRecord(raw, ["enrollmentIds", "cursor", "limit"]);
|
|
397
|
+
const ids = automationArray(r.enrollmentIds, 100).map(automationId).sort();
|
|
398
|
+
if (ids.length === 0 || new Set(ids).size !== ids.length)
|
|
399
|
+
throw new Error("Messaging event selection is invalid.");
|
|
400
|
+
for (const id of ids)
|
|
401
|
+
this.row(id);
|
|
402
|
+
const limit = automationInteger(r.limit, 1, 500);
|
|
403
|
+
const fingerprint = sha256(canonicalJson(ids));
|
|
404
|
+
let after = 0;
|
|
405
|
+
if (r.cursor !== null) {
|
|
406
|
+
const cursor = automationText(r.cursor, 4096);
|
|
407
|
+
const [payload2, signature, extra] = cursor.split(".");
|
|
408
|
+
if (payload2 === undefined || signature === undefined || extra !== undefined || !/^[a-f0-9]{64}$/u.test(signature))
|
|
409
|
+
throw new Error("Messaging cursor is invalid.");
|
|
410
|
+
const mac = createHmac("sha256", this.cursorKey).update(payload2).digest();
|
|
411
|
+
if (!timingSafeEqual(Buffer.from(signature, "hex"), mac))
|
|
412
|
+
throw new Error("Messaging cursor is invalid.");
|
|
413
|
+
const decoded = automationRecord(JSON.parse(Buffer.from(payload2, "base64url").toString()), ["fingerprint", "after"]);
|
|
414
|
+
if (decoded.fingerprint !== fingerprint)
|
|
415
|
+
throw new Error("Messaging cursor belongs to a different contact selection.");
|
|
416
|
+
after = automationInteger(decoded.after, 0, Number.MAX_SAFE_INTEGER);
|
|
417
|
+
}
|
|
418
|
+
const rows = this.db.query(`SELECT seq,enrollment_id,revision,data FROM events WHERE seq>? AND enrollment_id IN (${ids.map(() => "?").join(",")}) ORDER BY seq LIMIT ?`).all(after, ...ids, limit + 1);
|
|
419
|
+
const selected = rows.slice(0, limit);
|
|
420
|
+
const payload = Buffer.from(canonicalJson({ fingerprint, after: selected.at(-1)?.seq ?? after })).toString("base64url");
|
|
421
|
+
return Object.freeze({ events: Object.freeze(selected.map((row) => Object.freeze({ sequence: row.seq, enrollmentId: row.enrollment_id, revision: row.revision, message: parseAutomationMessage(JSON.parse(row.data)) }))), nextCursor: `${payload}.${createHmac("sha256", this.cursorKey).update(payload).digest("hex")}`, caughtUp: rows.length <= limit });
|
|
422
|
+
}
|
|
423
|
+
prepare(raw) {
|
|
424
|
+
this.ready();
|
|
425
|
+
const r = automationRecord(raw, ["enrollmentId", "expectedRevision", "intentId", "actions"]);
|
|
426
|
+
const enrollment = this.enrollment(this.row(automationId(r.enrollmentId)));
|
|
427
|
+
const expectedRevision = automationInteger(r.expectedRevision, 0, Number.MAX_SAFE_INTEGER);
|
|
428
|
+
if (!enrollment.ready || enrollment.revision !== expectedRevision)
|
|
429
|
+
throw new Error("Messaging context is stale or unavailable.");
|
|
430
|
+
const actions = automationArray(r.actions, 8).map(parseAutomationAction);
|
|
431
|
+
if (actions.length === 0)
|
|
432
|
+
throw new Error("Messaging plan is empty.");
|
|
433
|
+
if (Buffer.byteLength(canonicalJson(actions)) > 256 * 1024)
|
|
434
|
+
throw new Error("Messaging plan exceeds its byte bound.");
|
|
435
|
+
const intentId = automationId(r.intentId);
|
|
436
|
+
const binding = { enrollmentId: enrollment.id, expectedRevision, intentId, actions, bindingDigest: enrollment.bindingDigest, expiresAt: new Date(this.now() + 120000).toISOString() };
|
|
437
|
+
const digest = sha256(canonicalJson(binding));
|
|
438
|
+
const plan = Object.freeze({ ...binding, id: `plan:${digest}`, digest });
|
|
439
|
+
this.db.transaction(() => {
|
|
440
|
+
this.capacity("plans");
|
|
441
|
+
this.db.query("INSERT OR IGNORE INTO plans VALUES(?,?)").run(plan.id, canonicalJson(plan));
|
|
442
|
+
}).immediate();
|
|
443
|
+
this.checkFiles();
|
|
444
|
+
return plan;
|
|
445
|
+
}
|
|
446
|
+
runProjection(row) {
|
|
447
|
+
const accepted = automationArray(JSON.parse(row.accepted), 8).map((value) => {
|
|
448
|
+
const r = automationRecord(value, ["messageId", "providerReceiptId"]);
|
|
449
|
+
return Object.freeze({ messageId: r.messageId === null ? null : automationId(r.messageId), providerReceiptId: r.providerReceiptId === null ? null : automationId(r.providerReceiptId) });
|
|
450
|
+
});
|
|
451
|
+
const totalActions = automationInteger(row.total, 1, 8);
|
|
452
|
+
if (!["started", "accepted", "failed", "partial", "indeterminate"].includes(row.state) || accepted.length > totalActions || row.state === "accepted" && accepted.length !== totalActions || row.state === "failed" && accepted.length !== 0)
|
|
453
|
+
throw new Error("Messaging run state is inconsistent.");
|
|
454
|
+
return Object.freeze({ id: automationId(row.id), planId: automationId(row.plan_id), intentId: automationId(row.intent_id), enrollmentId: automationId(row.enrollment_id), state: row.state, accepted: Object.freeze(accepted), totalActions, reason: row.reason === null ? null : automationText(row.reason, 1024), retryable: false });
|
|
455
|
+
}
|
|
456
|
+
run(id) {
|
|
457
|
+
this.ready();
|
|
458
|
+
const row = this.db.query(`SELECT ${RUN_COLUMNS} FROM runs WHERE id=?`).get(automationId(id));
|
|
459
|
+
if (row === null)
|
|
460
|
+
throw new Error("Messaging run does not exist.");
|
|
461
|
+
return this.runProjection(row);
|
|
462
|
+
}
|
|
463
|
+
cancel(planId) {
|
|
464
|
+
this.ready();
|
|
465
|
+
const controllers = this.planControllers.get(automationId(planId));
|
|
466
|
+
if (controllers === undefined)
|
|
467
|
+
return false;
|
|
468
|
+
for (const controller of controllers)
|
|
469
|
+
controller.abort();
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
submit(raw, signal) {
|
|
473
|
+
this.ready();
|
|
474
|
+
const r = automationRecord(raw, ["planId", "grantId"]);
|
|
475
|
+
const controller = new AbortController;
|
|
476
|
+
const abort = () => controller.abort();
|
|
477
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
478
|
+
if (signal?.aborted)
|
|
479
|
+
controller.abort();
|
|
480
|
+
this.controllers.add(controller);
|
|
481
|
+
const planId = automationId(r.planId), grantId = automationId(r.grantId);
|
|
482
|
+
const owned = this.planControllers.get(planId) ?? new Set;
|
|
483
|
+
owned.add(controller);
|
|
484
|
+
this.planControllers.set(planId, owned);
|
|
485
|
+
const operation = this.dispatch(planId, grantId, controller.signal);
|
|
486
|
+
this.pending.add(operation);
|
|
487
|
+
operation.finally(() => {
|
|
488
|
+
signal?.removeEventListener("abort", abort);
|
|
489
|
+
this.controllers.delete(controller);
|
|
490
|
+
owned.delete(controller);
|
|
491
|
+
if (!owned.size)
|
|
492
|
+
this.planControllers.delete(planId);
|
|
493
|
+
this.pending.delete(operation);
|
|
494
|
+
}).catch(() => {
|
|
495
|
+
return;
|
|
496
|
+
});
|
|
497
|
+
return operation;
|
|
498
|
+
}
|
|
499
|
+
async dispatch(planId, grantId, signal) {
|
|
500
|
+
stopped(signal);
|
|
501
|
+
const record = this.db.query("SELECT data FROM plans WHERE id=?").get(planId);
|
|
502
|
+
if (record === null)
|
|
503
|
+
throw new Error("Messaging plan does not exist.");
|
|
504
|
+
const plan = planData(JSON.parse(record.data));
|
|
505
|
+
if (plan.id !== planId)
|
|
506
|
+
throw new Error("Messaging plan binding is invalid.");
|
|
507
|
+
const existing = this.db.query(`SELECT ${RUN_COLUMNS} FROM runs WHERE intent_id=?`).get(plan.intentId);
|
|
508
|
+
if (existing !== null) {
|
|
509
|
+
if (existing.plan_id !== plan.id)
|
|
510
|
+
throw new Error("Messaging intent already belongs to another plan.");
|
|
511
|
+
return this.runProjection(existing);
|
|
512
|
+
}
|
|
513
|
+
const enrollment = await this.poll(plan.enrollmentId, signal);
|
|
514
|
+
const provider = this.provider(enrollment.identity.provider);
|
|
515
|
+
const status = await this.status(provider, signal);
|
|
516
|
+
const route = await provider.resolve(enrollment.conversation.coordinate, signal);
|
|
517
|
+
stopped(signal);
|
|
518
|
+
if (!same(parseAutomationIdentity(route.identity), enrollment.identity) || !sameConversation(conversation(route.conversation), enrollment.conversation) || !same(status.identity, enrollment.identity) || !status.connected)
|
|
519
|
+
throw new Error("Messaging route or identity changed before dispatch.");
|
|
520
|
+
for (const action of plan.actions)
|
|
521
|
+
if (!status.actions[parseAutomationAction(action).kind].available)
|
|
522
|
+
throw new Error("Messaging action is unavailable for this provider.");
|
|
523
|
+
const runId = `run:${randomUUID()}`;
|
|
524
|
+
this.ready();
|
|
525
|
+
this.db.transaction(() => {
|
|
526
|
+
stopped(signal);
|
|
527
|
+
const current = this.enrollment(this.row(plan.enrollmentId));
|
|
528
|
+
const now = this.now();
|
|
529
|
+
this.capacity("runs");
|
|
530
|
+
this.capacity("messages", 0);
|
|
531
|
+
this.capacity("events", 0);
|
|
532
|
+
if (!current.ready || current.revision !== plan.expectedRevision || current.bindingDigest !== plan.bindingDigest || Date.parse(plan.expiresAt) <= now || this.activeRun(current.id))
|
|
533
|
+
throw new Error("Messaging context expired, changed, or has an unresolved run.");
|
|
534
|
+
const grantRow = this.db.query("SELECT * FROM grants WHERE id=?").get(grantId);
|
|
535
|
+
if (grantRow === null)
|
|
536
|
+
throw new Error("Messaging grant does not exist.");
|
|
537
|
+
const grant = grantData(JSON.parse(grantRow.data));
|
|
538
|
+
automationInteger(grantRow.revoked, 0, 1);
|
|
539
|
+
automationInteger(grantRow.consumed, 0, grant.maximumActions);
|
|
540
|
+
automationInteger(grantRow.last_dispatch, 0, Number.MAX_SAFE_INTEGER);
|
|
541
|
+
if (grantRow.revoked !== 0 || grant.enrollmentId !== current.id || grant.expectedBindingDigest !== current.bindingDigest || Date.parse(grant.expiresAt) <= now || grantRow.consumed + plan.actions.length > grant.maximumActions || grantRow.last_dispatch !== 0 && now - grantRow.last_dispatch < grant.minimumIntervalMs || plan.actions.some((action) => !grant.actions.includes(action.kind)))
|
|
542
|
+
throw new Error("Messaging grant does not authorize this dispatch.");
|
|
543
|
+
this.db.query("INSERT INTO runs VALUES(?,?,?,?, 'started','[]',?,NULL)").run(runId, plan.id, plan.intentId, current.id, plan.actions.length);
|
|
544
|
+
this.db.query("UPDATE grants SET consumed=consumed+?,last_dispatch=? WHERE id=?").run(plan.actions.length, now, grantId);
|
|
545
|
+
}).immediate();
|
|
546
|
+
this.checkFiles();
|
|
547
|
+
const accepted = [];
|
|
548
|
+
let state = "accepted";
|
|
549
|
+
let reason = null;
|
|
550
|
+
for (const [index, action] of plan.actions.entries()) {
|
|
551
|
+
try {
|
|
552
|
+
this.checkFiles();
|
|
553
|
+
const observed = checkedPage(await provider.events({ coordinates: [enrollment.conversation.coordinate], cursor: this.row(enrollment.id).cursor, limit: 200 }, signal), enrollment.identity, enrollment.conversation.coordinate);
|
|
554
|
+
const ownMessages = new Set(accepted.flatMap((receipt) => receipt.messageId === null ? [] : [receipt.messageId]));
|
|
555
|
+
if (observed.gap || !observed.caughtUp || observed.messages.some((message) => message.direction !== "outgoing" || message.kind !== "message" || !ownMessages.has(message.id))) {
|
|
556
|
+
state = accepted.length ? "partial" : "failed";
|
|
557
|
+
reason = "Conversation changed before the next action.";
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
const grantRow = this.db.query("SELECT * FROM grants WHERE id=?").get(grantId);
|
|
561
|
+
const grant = grantRow === null ? null : grantData(JSON.parse(grantRow.data));
|
|
562
|
+
const current = this.enrollment(this.row(plan.enrollmentId));
|
|
563
|
+
if (signal.aborted || grantRow?.revoked !== 0 || grant === null || Date.parse(grant.expiresAt) <= this.now() || Date.parse(plan.expiresAt) <= this.now() || grant.enrollmentId !== current.id || grant.expectedBindingDigest !== current.bindingDigest || current.bindingDigest !== plan.bindingDigest || !grant.actions.includes(action.kind)) {
|
|
564
|
+
state = accepted.length ? "partial" : "failed";
|
|
565
|
+
reason = "Dispatch permission expired or changed before the next action.";
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
const result = checkedResult(await provider.send({ identity: enrollment.identity, coordinate: enrollment.conversation.coordinate, action, intentId: `${plan.intentId}:${index}` }, signal));
|
|
569
|
+
if (result.state !== "accepted") {
|
|
570
|
+
state = result.state === "indeterminate" ? "indeterminate" : accepted.length ? "partial" : "failed";
|
|
571
|
+
reason = result.reason;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
accepted.push({ messageId: result.messageId, providerReceiptId: result.providerReceiptId });
|
|
575
|
+
this.checkFiles();
|
|
576
|
+
if (this.db.query("UPDATE runs SET accepted=? WHERE id=? AND state='started'").run(canonicalJson(accepted), runId).changes !== 1)
|
|
577
|
+
throw new Error("Messaging result journal lost its claim.");
|
|
578
|
+
} catch {
|
|
579
|
+
state = "indeterminate";
|
|
580
|
+
reason = "Provider dispatch or result recording did not settle conclusively.";
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
this.checkFiles();
|
|
585
|
+
if (this.db.query("UPDATE runs SET state=?,accepted=?,reason=? WHERE id=? AND state='started'").run(state, canonicalJson(accepted), reason, runId).changes !== 1)
|
|
586
|
+
throw new Error("Messaging terminal result could not be recorded.");
|
|
587
|
+
const row = this.db.query(`SELECT ${RUN_COLUMNS} FROM runs WHERE id=?`).get(runId);
|
|
588
|
+
return this.runProjection(row);
|
|
589
|
+
}
|
|
590
|
+
async close() {
|
|
591
|
+
if (this.closed)
|
|
592
|
+
return;
|
|
593
|
+
if (this.closing)
|
|
594
|
+
throw new Error("Messaging automation shutdown is already in progress.");
|
|
595
|
+
this.closing = true;
|
|
596
|
+
for (const controller of this.controllers)
|
|
597
|
+
controller.abort();
|
|
598
|
+
const settled = await Promise.allSettled([...this.pending]);
|
|
599
|
+
const providers = await Promise.allSettled([...this.providers.values()].map((provider) => provider.close()));
|
|
600
|
+
this.db.close();
|
|
601
|
+
this.closed = true;
|
|
602
|
+
if (settled.some((result) => result.status === "rejected") || providers.some((result) => result.status === "rejected"))
|
|
603
|
+
throw new Error("Messaging automation shutdown did not establish complete cleanup.");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
export {
|
|
607
|
+
MessagingAutomationHost,
|
|
608
|
+
MESSAGING_AUTOMATION_PROTOCOL
|
|
609
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ensureImsgNativeResources,
|
|
4
|
+
installBundledMessagingRuntime,
|
|
5
|
+
readBundledMessagingAsset,
|
|
6
|
+
verifyImsgNativeResources
|
|
7
|
+
} from "./index-en5hycxp.js";
|
|
8
|
+
import"./index-yq6maz71.js";
|
|
9
|
+
import"./index-gwk7rbyj.js";
|
|
10
|
+
import"./index-z1w83f81.js";
|
|
11
|
+
export {
|
|
12
|
+
verifyImsgNativeResources,
|
|
13
|
+
readBundledMessagingAsset,
|
|
14
|
+
installBundledMessagingRuntime,
|
|
15
|
+
ensureImsgNativeResources
|
|
16
|
+
};
|
package/dist/messaging.js
CHANGED