@weaccelerateinc/agent-sdk 0.10.3 → 0.12.0
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/README.md +11 -7
- package/approved-extension-releases/0.0.0.1.json +4 -1
- package/approved-extension-releases/0.0.0.2.json +4 -1
- package/approved-extension.js +1 -1
- package/browser-connector.d.ts +22 -0
- package/browser-connector.js +48 -4
- package/browser-provenance.js +17 -0
- package/browser-use-host.d.ts +14 -0
- package/browser-use-host.js +77 -18
- package/checkout-diagnostics.d.ts +13 -2
- package/checkout-diagnostics.js +18 -4
- package/cli.js +4 -1
- package/host-adapter.d.ts +5 -0
- package/host-adapter.js +21 -5
- package/index.d.ts +63 -5
- package/index.js +84 -13
- package/managed-mcp.d.ts +9 -3
- package/managed-mcp.js +56 -7
- package/package.json +21 -2
- package/partner-starter/.env.example +4 -2
- package/partner-starter/README.md +19 -6
- package/partner-starter/package.json +1 -1
- package/partner-starter/runtime.mjs +3 -2
- package/partner-starter/smoke.mjs +10 -5
- package/partner-starter/synthetic.mjs +37 -7
- package/partner-starter/tests/starter.test.mjs +58 -8
- package/partner-starter/trusted-context.example.mjs +3 -0
- package/partner.js +4 -0
- package/postgres-host.d.ts +7 -3
- package/postgres-whatsapp-journal.d.ts +8 -0
- package/postgres-whatsapp-journal.js +95 -0
- package/private-chat.js +10 -3
- package/private-ingress.d.ts +4 -0
- package/private-ingress.js +22 -9
- package/starter/README.md +1 -7
- package/starter/package.json +1 -1
- package/starter/runtime.mjs +7 -2
- package/starter/synthetic.mjs +1 -1
- package/starter/tests/starter.test.mjs +7 -39
- package/whatsapp-journal.d.ts +11 -0
- package/whatsapp-journal.js +78 -0
- package/whatsapp-messaging.d.ts +64 -0
- package/whatsapp-messaging.js +182 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { AccelerateError } from './index.js';
|
|
3
|
+
import { authenticatedCheckoutContext } from './host-adapter.js';
|
|
4
|
+
|
|
5
|
+
const id = /^[A-Za-z0-9][A-Za-z0-9_.:+/=-]{0,499}$/;
|
|
6
|
+
const safeId = value => `wa:${hash(value)}`;
|
|
7
|
+
const digits = /^[1-9]\d{5,19}$/;
|
|
8
|
+
const keyPattern = /^[A-Za-z0-9_.:-]{1,200}$/;
|
|
9
|
+
const fail = () => { throw new AccelerateError('request_failed'); };
|
|
10
|
+
const check = value => { if (!value) fail(); };
|
|
11
|
+
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
12
|
+
const validTime = value => typeof value === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value) && Number.isFinite(Date.parse(value)) && Date.parse(value) <= Date.now();
|
|
13
|
+
const fromEpoch = value => {
|
|
14
|
+
check(typeof value === 'string' && /^\d{10}$/.test(value));
|
|
15
|
+
const ms = Number(value) * 1000; check(Number.isSafeInteger(ms) && ms <= Date.now());
|
|
16
|
+
return new Date(ms).toISOString();
|
|
17
|
+
};
|
|
18
|
+
const phoneDigits = value => { check(typeof value === 'string' && /^\+[1-9]\d{6,14}$/.test(value)); return value.slice(1); };
|
|
19
|
+
function bounded(signal, deadline) {
|
|
20
|
+
const ms = deadline === undefined ? 45000 : Date.parse(deadline) - Date.now();
|
|
21
|
+
check(Number.isFinite(ms) && ms > 0);
|
|
22
|
+
return AbortSignal.any([AbortSignal.timeout(Math.min(ms, 45000)), ...(signal ? [signal] : [])]);
|
|
23
|
+
}
|
|
24
|
+
async function run(signal, task) {
|
|
25
|
+
signal.throwIfAborted();
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const abort = () => reject(new AccelerateError('request_failed'));
|
|
28
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
29
|
+
Promise.resolve().then(task).then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
30
|
+
if (signal.aborted) abort();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const wait = signal => run(signal, () => new Promise(resolve => setTimeout(resolve, 250)));
|
|
34
|
+
function validateJournal(journal) { check(['reserve', 'bind', 'recordStatus', 'get'].every(k => typeof journal?.[k] === 'function')); }
|
|
35
|
+
|
|
36
|
+
/** The injected journal MUST be durable, atomic, and linearizable across all
|
|
37
|
+
* workers. No image, caption, reply or raw provider envelope may be stored. */
|
|
38
|
+
export function createWhatsAppMessagingAdapter({ client, journal, sendingPhoneId, resolveDestination } = {}) {
|
|
39
|
+
check(typeof client?.uploadImage === 'function' && typeof client?.sendImage === 'function' && typeof client?.sendText === 'function' && digits.test(sendingPhoneId) && typeof resolveDestination === 'function');
|
|
40
|
+
validateJournal(journal);
|
|
41
|
+
async function deliver(input, message, review, options = {}) {
|
|
42
|
+
let bytes;
|
|
43
|
+
try {
|
|
44
|
+
const context = authenticatedCheckoutContext(input);
|
|
45
|
+
check(message && typeof message.text === 'string' && message.text.length > 0 && message.text.length <= (review ? 1024 : 4096) && keyPattern.test(message.idempotency_key));
|
|
46
|
+
if (review) check(Buffer.isBuffer(message.bytes) && message.bytes.length > 8 && message.bytes.length <= 2 * 1024 * 1024 && message.bytes.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10])) && message.contentType === 'image/png' && message.signal instanceof AbortSignal);
|
|
47
|
+
const signal = bounded(review ? message.signal : options.signal, review ? message.delivery_expires_at : undefined);
|
|
48
|
+
const destination = await run(signal, () => resolveDestination(context));
|
|
49
|
+
check(destination && destination.phone_number_id === sendingPhoneId && (destination.recipient_id === phoneDigits(context.phone_number) || digits.test(destination.recipient_id) && destination.verified_wa_id === destination.recipient_id));
|
|
50
|
+
const scope = { key: hash([context.customer_id, context.workspace_id, context.conversation_id, message.idempotency_key]),
|
|
51
|
+
sender: hash(sendingPhoneId), recipient: hash(destination.recipient_id) };
|
|
52
|
+
const reserved = await run(signal, () => journal.reserve(scope));
|
|
53
|
+
check(reserved && typeof reserved.created === 'boolean');
|
|
54
|
+
if (reserved.created) {
|
|
55
|
+
let accepted;
|
|
56
|
+
if (review) {
|
|
57
|
+
bytes = Buffer.from(message.bytes);
|
|
58
|
+
const uploaded = await run(signal, () => client.uploadImage({ bytes, contentType: 'image/png', signal }));
|
|
59
|
+
check(typeof uploaded?.media_id === 'string' && id.test(uploaded.media_id));
|
|
60
|
+
accepted = await run(signal, () => client.sendImage({ phone_number_id: sendingPhoneId, recipient_id: destination.recipient_id, media_id: uploaded.media_id, caption: message.text, signal }));
|
|
61
|
+
} else accepted = await run(signal, () => client.sendText({ phone_number_id: sendingPhoneId, recipient_id: destination.recipient_id, text: message.text, signal }));
|
|
62
|
+
check(typeof accepted?.message_id === 'string' && id.test(accepted.message_id));
|
|
63
|
+
await run(signal, () => journal.bind({ ...scope, message_id: safeId(accepted.message_id) }));
|
|
64
|
+
}
|
|
65
|
+
for (;;) {
|
|
66
|
+
const value = await run(signal, () => journal.get(scope));
|
|
67
|
+
check(value && value.sender === scope.sender && value.recipient === scope.recipient);
|
|
68
|
+
// A reservation without an ID has an ambiguous provider outcome. Never send again.
|
|
69
|
+
check(typeof value.message_id === 'string' && id.test(value.message_id));
|
|
70
|
+
if (value.status === 'failed') fail();
|
|
71
|
+
if (value.status === 'sent') {
|
|
72
|
+
check(validTime(value.sent_at) && (!review || Date.parse(value.sent_at) < Date.parse(message.delivery_expires_at)));
|
|
73
|
+
return Object.freeze({ message_id: value.message_id, sent_at: value.sent_at });
|
|
74
|
+
}
|
|
75
|
+
check(value.status === 'pending');
|
|
76
|
+
await wait(signal);
|
|
77
|
+
}
|
|
78
|
+
} catch { throw new AccelerateError('request_failed'); }
|
|
79
|
+
finally { bytes?.fill(0); }
|
|
80
|
+
}
|
|
81
|
+
async function findReviewReceipt(input, idempotencyKey, options = {}) {
|
|
82
|
+
try {
|
|
83
|
+
const context = authenticatedCheckoutContext(input);
|
|
84
|
+
check(typeof idempotencyKey === 'string' && /^accelerate-chat:[a-f0-9]{64}$/.test(idempotencyKey));
|
|
85
|
+
const signal = bounded(options.signal);
|
|
86
|
+
const destination = await run(signal, () => resolveDestination(context));
|
|
87
|
+
check(destination && destination.phone_number_id === sendingPhoneId && (destination.recipient_id === phoneDigits(context.phone_number) || digits.test(destination.recipient_id) && destination.verified_wa_id === destination.recipient_id));
|
|
88
|
+
const scope = { key: hash([context.customer_id, context.workspace_id, context.conversation_id, idempotencyKey]), sender: hash(sendingPhoneId), recipient: hash(destination.recipient_id) };
|
|
89
|
+
const value = await run(signal, () => journal.get(scope));
|
|
90
|
+
check(value && value.sender === scope.sender && value.recipient === scope.recipient && value.status === 'sent' && typeof value.message_id === 'string' && id.test(value.message_id) && validTime(value.sent_at));
|
|
91
|
+
return Object.freeze({ message_id: value.message_id, sent_at: value.sent_at });
|
|
92
|
+
} catch { throw new AccelerateError('request_failed'); }
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze({ sendText: (context, message, options) => deliver(context, message, false, options), sendReview: (context, message) => deliver(context, message, true), findReviewReceipt });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A batch handler verifies the original bytes once and invokes onMessage in
|
|
98
|
+
* provider array order. Callback processing must run before history or AI and
|
|
99
|
+
* must deduplicate provider message IDs across whole-webhook retries. */
|
|
100
|
+
export function createWhatsAppWebhookBatchHandler({ verifyWebhook, resolveIdentity, sendingPhoneId, journal, onMessage } = {}) {
|
|
101
|
+
check(typeof verifyWebhook === 'function' && typeof resolveIdentity === 'function' && digits.test(sendingPhoneId) && typeof onMessage === 'function'); validateJournal(journal);
|
|
102
|
+
return async (request, rawBody) => {
|
|
103
|
+
let body;
|
|
104
|
+
try {
|
|
105
|
+
check(rawBody instanceof Uint8Array && rawBody.byteLength <= 262144 && await verifyWebhook(request, rawBody) === true);
|
|
106
|
+
body = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(rawBody));
|
|
107
|
+
check(body?.object === 'whatsapp_business_account' && Array.isArray(body.entry) && body.entry.length > 0);
|
|
108
|
+
let retry = false;
|
|
109
|
+
for (const entry of body.entry) {
|
|
110
|
+
if (!Array.isArray(entry?.changes)) { retry = true; continue; }
|
|
111
|
+
for (const change of entry.changes) {
|
|
112
|
+
if (change?.field !== 'messages') continue;
|
|
113
|
+
const value = change.value;
|
|
114
|
+
if (value?.messaging_product !== 'whatsapp' || value.metadata?.phone_number_id !== sendingPhoneId) { retry = true; continue; }
|
|
115
|
+
if ((value.statuses !== undefined && !Array.isArray(value.statuses)) || (value.messages !== undefined && !Array.isArray(value.messages))) { retry = true; continue; }
|
|
116
|
+
const statuses = value.statuses ?? [], messages = value.messages ?? [];
|
|
117
|
+
// Separate arrays have no shared provider ordering. Keep each array's order.
|
|
118
|
+
for (const status of statuses) {
|
|
119
|
+
try {
|
|
120
|
+
if (!['sent', 'failed'].includes(status?.status)) continue;
|
|
121
|
+
check(id.test(status.id) && digits.test(status.recipient_id));
|
|
122
|
+
const time = fromEpoch(status.timestamp);
|
|
123
|
+
await journal.recordStatus({ sender: hash(sendingPhoneId), recipient: hash(status.recipient_id), message_id: safeId(status.id), status: status.status, timestamp: time });
|
|
124
|
+
} catch { retry = true; }
|
|
125
|
+
}
|
|
126
|
+
for (const message of messages) {
|
|
127
|
+
let result;
|
|
128
|
+
try {
|
|
129
|
+
if (message?.type !== 'text') continue;
|
|
130
|
+
// Never treat explicitly shared/group traffic as direct.
|
|
131
|
+
if (value.group_id != null || value.is_group === true || message.group_id != null || message.groupId != null || message.group != null || message.is_group === true || message.isGroup === true || message.context?.group_id != null) continue;
|
|
132
|
+
check(message && id.test(message.id) && digits.test(message.from) && typeof message.text?.body === 'string' && message.text.body.length <= 2000);
|
|
133
|
+
const sent_at = fromEpoch(message.timestamp), phone_number = `+${message.from}`;
|
|
134
|
+
const resolved = await resolveIdentity({ phone_number, wa_id: message.from, phone_number_id: sendingPhoneId });
|
|
135
|
+
if (resolved === null) continue;
|
|
136
|
+
const mapped = resolved && Object.hasOwn(resolved, 'context') ? resolved : undefined;
|
|
137
|
+
const context = authenticatedCheckoutContext(mapped ? mapped.context : resolved);
|
|
138
|
+
check(context.phone_number === phone_number || mapped?.verified_wa_id === message.from);
|
|
139
|
+
result = { authenticated: true, direct: true, context, message_id: safeId(message.id), text: message.text.body, sent_at };
|
|
140
|
+
if (message.context?.id !== undefined) { check(id.test(message.context.id)); result.reply_to_message_id = safeId(message.context.id); }
|
|
141
|
+
const handled = await onMessage(result);
|
|
142
|
+
if (handled?.retry === true) retry = true;
|
|
143
|
+
} catch { retry = true; }
|
|
144
|
+
finally { if (result) result.text = ''; if (message?.text && typeof message.text === 'object') message.text.body = ''; }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
check(!retry);
|
|
149
|
+
return Object.freeze({ handled: true });
|
|
150
|
+
} catch { throw new AccelerateError('permission_denied'); }
|
|
151
|
+
finally {
|
|
152
|
+
if (Array.isArray(body?.entry)) for (const entry of body.entry) if (Array.isArray(entry?.changes)) for (const change of entry.changes) if (Array.isArray(change?.value?.messages)) for (const message of change.value.messages) if (message?.text && typeof message.text === 'object') message.text.body = '';
|
|
153
|
+
if (rawBody instanceof Uint8Array) rawBody.fill(0);
|
|
154
|
+
body = undefined;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Compatibility authenticator accepts exactly one message or status event.
|
|
160
|
+
* Use the batch handler for provider endpoints that can deliver batches. */
|
|
161
|
+
export function createWhatsAppWebhookAuthenticator(options = {}) {
|
|
162
|
+
return async (request, rawBody) => {
|
|
163
|
+
let message, count = 0;
|
|
164
|
+
// Batch compatibility cannot safely return multiple messages to a caller
|
|
165
|
+
// that only consumes one; reject before any journal or callback side effect.
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
check(rawBody instanceof Uint8Array && rawBody.byteLength <= 262144 && await options.verifyWebhook(request, rawBody) === true);
|
|
169
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(rawBody));
|
|
170
|
+
check(Array.isArray(parsed?.entry) && parsed.entry.length === 1 && Array.isArray(parsed.entry[0]?.changes) && parsed.entry[0].changes.length === 1);
|
|
171
|
+
const value = parsed.entry[0].changes[0]?.value;
|
|
172
|
+
check((value?.messages?.length ?? 0) + (value?.statuses?.length ?? 0) === 1);
|
|
173
|
+
const handler = createWhatsAppWebhookBatchHandler({ ...options, verifyWebhook: () => true, onMessage: event => { count++; message = { ...event }; } });
|
|
174
|
+
await handler(request, rawBody);
|
|
175
|
+
return count ? message : { kind: 'ignore' };
|
|
176
|
+
} catch { throw new AccelerateError('permission_denied'); }
|
|
177
|
+
finally {
|
|
178
|
+
if (Array.isArray(parsed?.entry)) for (const entry of parsed.entry) if (Array.isArray(entry?.changes)) for (const change of entry.changes) if (Array.isArray(change?.value?.messages)) for (const item of change.value.messages) if (item?.text && typeof item.text === 'object') item.text.body = '';
|
|
179
|
+
if (rawBody instanceof Uint8Array) rawBody.fill(0); parsed = undefined;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|