@weaccelerateinc/agent-sdk 0.0.0-stage → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +149 -2
  2. package/approved-extension-releases/0.0.0.1.json +39 -0
  3. package/approved-extension-releases/0.0.0.2.json +32 -0
  4. package/approved-extension.d.ts +24 -0
  5. package/approved-extension.js +90 -0
  6. package/browser-connector.d.ts +87 -0
  7. package/browser-connector.js +179 -0
  8. package/browser-operations.d.ts +56 -0
  9. package/browser-operations.js +144 -0
  10. package/browser-use-host.d.ts +43 -0
  11. package/browser-use-host.js +147 -0
  12. package/checkout-diagnostics.d.ts +23 -0
  13. package/checkout-diagnostics.js +67 -0
  14. package/cli.js +102 -0
  15. package/customer.env.example +6 -0
  16. package/host-adapter.d.ts +90 -0
  17. package/host-adapter.js +353 -0
  18. package/index.d.ts +209 -0
  19. package/index.js +441 -0
  20. package/linq-messaging.d.ts +45 -0
  21. package/linq-messaging.js +123 -0
  22. package/managed-mcp.d.ts +150 -0
  23. package/managed-mcp.js +382 -0
  24. package/package.json +114 -4
  25. package/partner-starter/.env.example +11 -0
  26. package/partner-starter/README.md +48 -0
  27. package/partner-starter/gitignore.template +5 -0
  28. package/partner-starter/package.json +21 -0
  29. package/partner-starter/runtime.mjs +32 -0
  30. package/partner-starter/server.mjs +13 -0
  31. package/partner-starter/smoke.mjs +56 -0
  32. package/partner-starter/synthetic.mjs +226 -0
  33. package/partner-starter/tests/starter.test.mjs +69 -0
  34. package/partner-starter/trusted-context.example.mjs +20 -0
  35. package/partner.d.ts +27 -0
  36. package/partner.js +49 -0
  37. package/postgres-host.d.ts +14 -0
  38. package/postgres-host.js +102 -0
  39. package/private-chat.d.ts +104 -0
  40. package/private-chat.js +309 -0
  41. package/private-ingress.d.ts +46 -0
  42. package/private-ingress.js +132 -0
  43. package/starter/.env.example +19 -0
  44. package/starter/README.md +88 -0
  45. package/starter/gitignore.template +5 -0
  46. package/starter/package.json +21 -0
  47. package/starter/runtime.mjs +61 -0
  48. package/starter/server.mjs +13 -0
  49. package/starter/smoke.mjs +55 -0
  50. package/starter/synthetic.mjs +163 -0
  51. package/starter/tests/starter.test.mjs +256 -0
  52. package/starter/trusted-context.example.mjs +49 -0
package/index.js ADDED
@@ -0,0 +1,441 @@
1
+ import { env as processEnv, versions } from 'node:process';
2
+ import { createHash } from 'node:crypto';
3
+ import { normalizeBrowserOperation } from './browser-operations.js';
4
+ import { validateCheckoutDiagnostics } from './checkout-diagnostics.js';
5
+ export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
6
+
7
+ const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
8
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved',
10
+ 'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
11
+ const WAIT_STOPS = new Set(['ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
12
+ const HOSTED_SERVICE = 'https://accelerate-agent-sdk.vercel.app';
13
+ const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied', 'not_found', 'conflict',
14
+ 'rate_limited', 'checkout_expired', 'checkout_unavailable', 'approval_required', 'invalid_state',
15
+ 'operation_in_progress', 'fill_continuation_required', 'review_unavailable', 'capability_disabled', 'checkout_unsupported', 'integration_unavailable', 'outcome_unknown',
16
+ 'authentication_required', 'checkout_closed', 'checkout_frozen', 'upstream_unavailable', 'wallet_required',
17
+ 'revision_conflict', 'address_changed', 'details_required', 'quote_changed', 'submit_unavailable',
18
+ 'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed',
19
+ 'verification_consumed', 'handoff_consumed', 'wallet_refreshing', 'consent_required',
20
+ 'idempotency_conflict', 'operation_conflict', 'checkout_not_found', 'tenant_unauthorized',
21
+ 'checkout_busy', 'store_unavailable', 'store_integrity_error', 'selection_rejected', 'upstream_invalid',
22
+ 'upstream_request_invalid', 'upstream_response_invalid', 'configuration_error', 'configuration_invalid',
23
+ 'route_not_found', 'method_not_allowed', 'json_required', 'invalid_json', 'request_too_large', 'checkout_event_unconfirmed']);
24
+ const MESSAGES = {
25
+ invalid_input: 'Invalid Accelerate client request.',
26
+ invalid_response: 'Accelerate returned an unrecognized private response.',
27
+ request_timeout: 'Accelerate request timed out. Check checkout status; do not repeat uncertain payment work.',
28
+ request_cancelled: 'Accelerate request was cancelled. Check checkout status; do not repeat uncertain payment work.',
29
+ wait_timeout: 'Checkout status wait timed out. Check status before continuing; no payment action was taken by this wait.',
30
+ missing_api_key: 'Set ACCELERATE_API_KEY on your application server.',
31
+ missing_merchant_id: 'Set ACCELERATE_MERCHANT_ID on your application server.',
32
+ invalid_api_key: 'ACCELERATE_API_KEY is invalid. Check the server key supplied by Accelerate.',
33
+ invalid_merchant_id: 'ACCELERATE_MERCHANT_ID must be the merchant UUID supplied by Accelerate.',
34
+ invalid_base_url: 'ACCELERATE_BASE_URL must be an HTTPS origin without a path, query, fragment or credentials.',
35
+ };
36
+ const GUIDANCE = Object.create(null);
37
+ for (const [codes, message, nextAction] of [
38
+ [['checkout_unsupported'],
39
+ 'This checkout’s payment fields or safe review are not supported. No private verification was started.', 'stop'],
40
+ [['unauthorized', 'tenant_unauthorized', 'permission_denied', 'configuration_error', 'configuration_invalid', 'capability_disabled', 'integration_unavailable'],
41
+ 'Check the server API key, merchant ID and enabled Accelerate capabilities.', 'check_configuration'],
42
+ [['invalid_input', 'invalid_json', 'json_required', 'request_too_large', 'method_not_allowed', 'route_not_found', 'upstream_request_invalid'],
43
+ 'Check the checkout request against the SDK contract.', 'check_request'],
44
+ [['operation_in_progress', 'operation_conflict', 'checkout_busy', 'rate_limited', 'wallet_refreshing', 'conflict'],
45
+ 'Read checkout status before continuing. Do not repeat an uncertain payment action.', 'check_status'],
46
+ [['wallet_required', 'authentication_required', 'authentication_completed', 'selection_rejected'],
47
+ 'Continue the private wallet authentication and selection flow.', 'complete_wallet'],
48
+ [['verification_required', 'verification_failed', 'consent_required', 'approval_required', 'approval_unavailable'],
49
+ 'Complete the private purchase approval and verification flow before continuing.', 'complete_verification'],
50
+ [['quote_changed', 'address_changed', 'details_required'],
51
+ 'Prepare the current checkout details and request approval for the updated quote.', 'refresh_quote'],
52
+ [['revision_conflict'], 'Read the current checkout revision before continuing.', 'refresh_revision'],
53
+ [['fill_continuation_required'],
54
+ 'The private backend continues this staged fill after security-code verification. Read the same checkout status; do not request another fill.', 'check_status'],
55
+ [['review_unavailable'],
56
+ 'This checkout has insufficient time for safe filled review and cleanup. Read its safe status; do not repeat filling.', 'stop'],
57
+ [['checkout_frozen', 'submit_unavailable', 'invalid_state'],
58
+ 'Read checkout status and use only the actions permitted for its current state.', 'check_status'],
59
+ [['checkout_closed', 'checkout_expired', 'checkout_not_found', 'not_found', 'verification_consumed', 'handoff_consumed'],
60
+ 'Stop this checkout action. Read its status and reconcile any previous payment attempt.', 'stop'],
61
+ [['outcome_unknown'],
62
+ 'Reconcile checkout status and the merchant order. Do not repeat the payment action.', 'reconcile_order'],
63
+ [['checkout_event_unconfirmed'],
64
+ 'Private chat progress delivery could not be confirmed. Check this existing checkout; do not create a replacement.', 'check_status'],
65
+ ]) for (const code of codes) GUIDANCE[code] = Object.freeze({ message, nextAction });
66
+
67
+ /** Safe diagnostics only: no response body, URL, credentials, cause or provider stack. */
68
+ export class AccelerateError extends Error {
69
+ constructor(code = 'request_failed', status = 0, supportReference) {
70
+ const known = Object.hasOwn(MESSAGES, code) || ERROR_CODES.has(code);
71
+ super(known && Object.hasOwn(MESSAGES, code) ? MESSAGES[code] : GUIDANCE[code]?.message ??
72
+ 'Accelerate could not complete the request. Check checkout status before continuing.');
73
+ this.name = 'AccelerateError';
74
+ this.code = known ? code : 'request_failed';
75
+ this.status = Number.isInteger(status) && status >= 100 && status <= 599 ? status : 0;
76
+ this.retrySafe = false;
77
+ this.paymentConfirmed = false;
78
+ this.nextAction = GUIDANCE[this.code]?.nextAction ??
79
+ (['missing_api_key', 'missing_merchant_id', 'invalid_api_key', 'invalid_merchant_id', 'invalid_base_url'].includes(this.code) ?
80
+ 'check_configuration' : 'check_status');
81
+ if (typeof supportReference === 'string' && /^(?:acs_|arq_)[a-f0-9]{32}$/.test(supportReference)) this.supportReference = supportReference;
82
+ }
83
+ }
84
+ const fail = (code = 'invalid_input') => { throw new AccelerateError(code); };
85
+ const check = (value, code) => { if (!value) fail(code); };
86
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
87
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
88
+ function keys(value, allowed, required = allowed, code = 'invalid_input') {
89
+ check(object(value) && Object.keys(value).every(key => allowed.includes(key)) &&
90
+ required.every(key => Object.hasOwn(value, key)), code);
91
+ return value;
92
+ }
93
+ function string(value, max = 200, code = 'invalid_input') {
94
+ check(typeof value === 'string' && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/u.test(value), code);
95
+ return value;
96
+ }
97
+ function integer(value, min, max, code = 'invalid_input') {
98
+ check(Number.isSafeInteger(value) && value >= min && value <= max, code); return value;
99
+ }
100
+ function checkoutId(id) { check(typeof id === 'string' && ID.test(id)); return id; }
101
+ function httpsUrl(value, { originOnly = false, loopback = false, code = 'invalid_input' } = {}) {
102
+ string(value, 2048, code);
103
+ let url; try { url = new URL(value); } catch { fail(code); }
104
+ const local = loopback && url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname);
105
+ check((url.protocol === 'https:' || local) && !url.username && !url.password && !url.hash &&
106
+ (!originOnly || (url.pathname === '/' && !url.search)), code);
107
+ return url;
108
+ }
109
+ function revisionInput(input) {
110
+ keys(input, ['revision']); return { revision: integer(input.revision, 1, 2147483647) };
111
+ }
112
+ function preparationText(value, max = 2000, code = 'invalid_input') {
113
+ string(value, max, code);
114
+ check(!/(?:\d[ -]?){13,19}/u.test(value) &&
115
+ !/\b(?:cvv|cvc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), code);
116
+ return value;
117
+ }
118
+ function browserInput(input) {
119
+ keys(input, ['operation', 'arguments']);
120
+ try { return { operation: input.operation, arguments: normalizeBrowserOperation(input.operation, input.arguments) }; }
121
+ catch { fail(); }
122
+ }
123
+ function merchantUrl(value) {
124
+ try { normalizeBrowserOperation('navigate', { url: value }); }
125
+ catch { fail(); }
126
+ return value;
127
+ }
128
+ function timestamp(value) {
129
+ check(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value) && Number.isFinite(Date.parse(value)), 'invalid_response');
130
+ return value;
131
+ }
132
+ function safeText(value, max = 1000, multiline = false) {
133
+ if (multiline) {
134
+ check(typeof value === 'string' && value.length <= max && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value) &&
135
+ !/(?:\d[ -]?){13,19}/u.test(value) &&
136
+ !/\b(?:cvv|cvc|csc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), 'invalid_response');
137
+ } else preparationText(value, max, 'invalid_response');
138
+ check(!/\b(?:cdp|browser_live_view|session_token|handoff_token|submit_token|id_token|authorizationToken)\b/iu.test(value) &&
139
+ !/\b(?:cvv|cvc|csc|security\s*code)\b["'\s:=]{0,12}\d{3,4}\b/iu.test(value) &&
140
+ !/(?:wss?:\/\/|chrome-extension:\/\/|devtools:\/\/|devtools\/browser|aw[12]\.[\w.-]+|\b(?:agw_|atk_)[A-Za-z0-9_-]+|\b(?:Bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.|tok_(?:sandbox|live)_|vgs_)|\b(?:api_key|hmac_secret)\b)/iu.test(value) &&
141
+ !/\b(?:0[1-9]|1[0-2])\s*\/\s*(?:20)?\d{2}\b/u.test(value) &&
142
+ !/https?:\/\/[^\s/]*(?:onkernel\.com|verygoodproxy\.com|verygoodvault\.com)(?:[/:\s]|$)/iu.test(value), 'invalid_response');
143
+ return value;
144
+ }
145
+ function statusResponse(value, expectedId, { create, origin, loopback }) {
146
+ keys(value, ['id', 'status', 'revision', 'expires_at', 'card', 'address', 'quote', 'submit_expires_at', 'purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'diagnostics',
147
+ 'fill_after_verification', 'retry_safe', 'payment_confirmed', ...(create ? ['wallet_url'] : [])],
148
+ ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], 'invalid_response');
149
+ check(typeof value.id === 'string' && ID.test(value.id) && (!expectedId || value.id === expectedId) && STATUSES.has(value.status) &&
150
+ value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
151
+ integer(value.revision, 0, 2147483647, 'invalid_response'); timestamp(value.expires_at);
152
+ if (value.purpose !== undefined || value.submit_allowed !== undefined) check(
153
+ ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(value.purpose) &&
154
+ value.submit_allowed === (value.purpose === 'purchase'), 'invalid_response');
155
+ if (value.approval_policy !== undefined || value.purchase_approved !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean' && (!value.purchase_approved || value.status === 'ready_for_submit' && value.purpose === 'purchase'), 'invalid_response');
156
+ if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' &&
157
+ ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
158
+ if (value.submit_expires_at !== undefined) timestamp(value.submit_expires_at);
159
+ if (value.diagnostics !== undefined) {
160
+ try { validateCheckoutDiagnostics(value.diagnostics, value); } catch { fail('invalid_response'); }
161
+ }
162
+ if (value.card !== undefined) {
163
+ keys(value.card, ['last4', 'brand'], ['last4', 'brand'], 'invalid_response');
164
+ check(value.card.last4 === null || typeof value.card.last4 === 'string' && /^\d{4}$/u.test(value.card.last4), 'invalid_response');
165
+ if (value.card.brand !== null) safeText(value.card.brand, 40);
166
+ }
167
+ if (value.address !== undefined) {
168
+ keys(value.address, ['label', 'city', 'state', 'postal_code_mask'], ['label'], 'invalid_response');
169
+ if (value.address.label !== null) safeText(value.address.label, 400);
170
+ for (const key of ['city', 'state', 'postal_code_mask']) if (value.address[key] != null) safeText(value.address[key], 128);
171
+ check(value.address.postal_code_mask == null || /[*•]/u.test(value.address.postal_code_mask), 'invalid_response');
172
+ }
173
+ if (value.quote !== undefined) {
174
+ keys(value.quote, ['checkout_origin', 'amount_cents', 'currency'], undefined, 'invalid_response');
175
+ httpsUrl(value.quote.checkout_origin, { originOnly: true, code: 'invalid_response' });
176
+ integer(value.quote.amount_cents, 1, 2147483647, 'invalid_response');
177
+ check(value.quote.currency === 'USD', 'invalid_response');
178
+ }
179
+ if (value.wallet_url !== undefined) {
180
+ string(value.wallet_url, 2048, 'invalid_response');
181
+ let url; try { url = new URL(value.wallet_url); } catch { fail('invalid_response'); }
182
+ check(url.origin === origin && url.pathname === `/checkout/${value.id}` && !url.search && !url.username && !url.password &&
183
+ /^#ticket=[A-Za-z0-9_-]{43}$/u.test(url.hash) && (url.protocol === 'https:' || loopback), 'invalid_response');
184
+ }
185
+ return value;
186
+ }
187
+ function browserResponse(value) {
188
+ keys(value, ['content', 'isError'], ['content'], 'invalid_response');
189
+ check(value.isError === undefined || value.isError === false, 'invalid_response');
190
+ check(Array.isArray(value.content) && value.content.length > 0 && value.content.length <= 20, 'invalid_response');
191
+ let total = 0;
192
+ for (const part of value.content) {
193
+ keys(part, ['type', 'text'], undefined, 'invalid_response');
194
+ check(part.type === 'text', 'invalid_response'); safeText(part.text, 24000, true);
195
+ total += Buffer.byteLength(part.text, 'utf8');
196
+ }
197
+ check(total <= 24000, 'invalid_response');
198
+ return { content: value.content };
199
+ }
200
+ function verifiedQuoteResponse(value, id, input) {
201
+ keys(value, ['checkout_id', 'revision', 'verified', 'quote'], undefined, 'invalid_response');
202
+ check(value.checkout_id === id && value.revision === input.revision && value.verified === true, 'invalid_response');
203
+ keys(value.quote, ['checkout_origin', 'amount_cents', 'currency'], undefined, 'invalid_response');
204
+ check(value.quote.checkout_origin === new URL(input.checkout_url).origin &&
205
+ value.quote.amount_cents === input.expected_amount_cents && value.quote.currency === input.expected_currency, 'invalid_response');
206
+ return value;
207
+ }
208
+ function creationInspectionInput(input) {
209
+ keys(input, ['customer_id', 'workspace_id', 'conversation_id', 'idempotency_key', 'browser']);
210
+ for (const name of ['customer_id', 'workspace_id', 'conversation_id'])
211
+ check(typeof input[name] === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input[name]));
212
+ string(input.idempotency_key, 200);
213
+ keys(input.browser, ['provider', 'browser_id', 'custody_mode']);
214
+ check(input.browser.provider === 'browser_use' && typeof input.browser.browser_id === 'string' &&
215
+ UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase() &&
216
+ input.browser.custody_mode === 'customer_backend_trusted');
217
+ return { ...input, browser: { ...input.browser } };
218
+ }
219
+ function creationInspectionResponse(value, input) {
220
+ if (value?.confirmed === false) { keys(value, ['confirmed'], undefined, 'invalid_response'); return value; }
221
+ keys(value, ['confirmed', 'checkout_id', 'revision', 'purpose', 'creation_key_sha256', 'retirement'], undefined, 'invalid_response');
222
+ check(value.confirmed === true && typeof value.checkout_id === 'string' && ID.test(value.checkout_id) && value.revision === 0 &&
223
+ ['purchase', 'sandbox_fill_only'].includes(value.purpose) &&
224
+ value.creation_key_sha256 === createHash('sha256').update(input.idempotency_key).digest('hex'), 'invalid_response');
225
+ keys(value.retirement, ['browser_provider', 'browser_id', 'retired', 'provider_stopped', 'private_lease_closed', 'recording_disabled'], undefined, 'invalid_response');
226
+ check(value.retirement.browser_provider === input.browser.provider && value.retirement.browser_id === input.browser.browser_id &&
227
+ value.retirement.retired === true && value.retirement.provider_stopped === true && value.retirement.private_lease_closed === true &&
228
+ value.retirement.recording_disabled === true, 'invalid_response');
229
+ return value;
230
+ }
231
+ function retirementInspectionInput(input) {
232
+ keys(input, ['customer_id', 'workspace_id', 'conversation_id', 'idempotency_key', 'revision', 'browser']);
233
+ const { revision, ...scope } = input;
234
+ return { ...creationInspectionInput(scope), revision: integer(revision, 1, 2147483647) };
235
+ }
236
+ function retirementInspectionResponse(value, id, input) {
237
+ if (value?.confirmed === false) { keys(value, ['confirmed'], undefined, 'invalid_response'); return value; }
238
+ keys(value, ['confirmed', 'checkout_id', 'revision', 'status', 'purpose', 'approval_policy', 'submit_allowed',
239
+ 'purchase_approved', 'creation_key_sha256', 'retirement'], undefined, 'invalid_response');
240
+ check(value.confirmed === true && value.checkout_id === id && value.revision === input.revision && value.status === 'outcome_unknown' &&
241
+ value.purpose === 'sandbox_fill_only' && value.approval_policy === 'after_fill' && value.submit_allowed === false &&
242
+ value.purchase_approved === false && value.creation_key_sha256 === createHash('sha256').update(input.idempotency_key).digest('hex'), 'invalid_response');
243
+ keys(value.retirement, ['browser_provider', 'browser_id', 'retired', 'provider_stopped', 'private_lease_closed', 'recording_disabled'], undefined, 'invalid_response');
244
+ check(value.retirement.browser_provider === input.browser.provider && value.retirement.browser_id === input.browser.browser_id &&
245
+ value.retirement.retired === true && value.retirement.provider_stopped === true && value.retirement.private_lease_closed === true &&
246
+ value.retirement.recording_disabled === true, 'invalid_response');
247
+ return value;
248
+ }
249
+ function connectionResponse(value, merchantId) {
250
+ keys(value, ['status', 'api_version', 'merchant_id', 'interaction_modes', 'currencies'], undefined, 'invalid_response');
251
+ check(value.status === 'connected' && value.api_version === 'v1' && value.merchant_id === merchantId &&
252
+ Array.isArray(value.interaction_modes) && value.interaction_modes.length === 2 &&
253
+ value.interaction_modes[0] === 'chat' && value.interaction_modes[1] === 'hosted' &&
254
+ Array.isArray(value.currencies) && value.currencies.length === 1 && value.currencies[0] === 'USD', 'invalid_response');
255
+ return value;
256
+ }
257
+ async function readJson(response, signal) {
258
+ check(response.body && /^application\/json(?:\s*;|$)/iu.test(response.headers.get('content-type') ?? ''), 'invalid_response');
259
+ const reader = response.body.getReader();
260
+ const abort = () => { void reader.cancel().catch(() => {}); };
261
+ signal.addEventListener('abort', abort, { once: true });
262
+ let size = 0; const chunks = [];
263
+ try {
264
+ while (true) {
265
+ const next = await reader.read(); if (next.done) break;
266
+ signal.throwIfAborted();
267
+ size += next.value.byteLength; check(size <= 65536, 'invalid_response'); chunks.push(next.value);
268
+ }
269
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
270
+ } finally { signal.removeEventListener('abort', abort); await reader.cancel().catch(() => {}); reader.releaseLock(); }
271
+ }
272
+ function bounded(operation, signal) {
273
+ return new Promise((resolve, reject) => {
274
+ const aborted = () => reject(new AccelerateError('request_cancelled'));
275
+ signal.addEventListener('abort', aborted, { once: true });
276
+ if (signal.aborted) aborted();
277
+ Promise.resolve(operation).then(value => {
278
+ signal.removeEventListener('abort', aborted);
279
+ if (signal.aborted) { Promise.resolve(value?.body?.cancel()).catch(() => {}); reject(new AccelerateError('request_cancelled')); }
280
+ else resolve(value);
281
+ }, error => { signal.removeEventListener('abort', aborted);
282
+ reject(error instanceof AccelerateError ? error : new AccelerateError('request_failed')); });
283
+ });
284
+ }
285
+
286
+ /** Instantiate only on your authenticated application server. Never bundle an API key in a browser. */
287
+ export class AccelerateClient {
288
+ #origin; #apiKey; #merchantId; #fetch; #loopback;
289
+ /** Server environment only. Never pass model input or expose the returned client to a browser. */
290
+ static fromEnv(env = processEnv) {
291
+ check(Number(versions.node?.split('.')[0]) >= 22 && typeof globalThis.window === 'undefined' &&
292
+ typeof globalThis.document === 'undefined' && typeof globalThis.self === 'undefined');
293
+ check(env === processEnv || object(env));
294
+ const apiKey = env.ACCELERATE_API_KEY, merchantId = env.ACCELERATE_MERCHANT_ID;
295
+ check(apiKey !== undefined && !(typeof apiKey === 'string' && apiKey.trim().length === 0), 'missing_api_key');
296
+ check(typeof apiKey === 'string' && /^[A-Za-z0-9_-]{32,256}$/u.test(apiKey), 'invalid_api_key');
297
+ check(merchantId !== undefined && !(typeof merchantId === 'string' && merchantId.trim().length === 0), 'missing_merchant_id');
298
+ check(typeof merchantId === 'string' && UUID.test(merchantId), 'invalid_merchant_id');
299
+ const baseUrl = env.ACCELERATE_BASE_URL === undefined ? HOSTED_SERVICE : env.ACCELERATE_BASE_URL;
300
+ try { httpsUrl(baseUrl, { originOnly: true }); } catch { fail('invalid_base_url'); }
301
+ return new this({ baseUrl, apiKey, merchantId });
302
+ }
303
+ constructor({ baseUrl, apiKey, merchantId, fetchImpl = globalThis.fetch, allowLoopbackForTests = false } = {}) {
304
+ check(Number(versions.node?.split('.')[0]) >= 22 && typeof globalThis.window === 'undefined' &&
305
+ typeof globalThis.document === 'undefined' && typeof globalThis.self === 'undefined');
306
+ const url = httpsUrl(baseUrl, { originOnly: true, loopback: allowLoopbackForTests === true });
307
+ check(typeof apiKey === 'string' && /^[A-Za-z0-9_-]{32,256}$/u.test(apiKey));
308
+ check(typeof merchantId === 'string' && UUID.test(merchantId) && typeof fetchImpl === 'function');
309
+ this.#origin = url.origin; this.#apiKey = apiKey; this.#merchantId = merchantId.toLowerCase();
310
+ this.#fetch = fetchImpl; this.#loopback = allowLoopbackForTests === true;
311
+ this.connection = Object.freeze({
312
+ check: async options => this.#request('GET', undefined, '', undefined, options, true),
313
+ });
314
+ this.checkouts = Object.freeze({
315
+ inspectCreation: async (input, options) => this.#request('POST', null, '/inspect-creation', creationInspectionInput(input), options),
316
+ inspectRetirement: async (id, input, options) => this.#request('POST', checkoutId(id), '/inspect-retirement', retirementInspectionInput(input), options),
317
+ create: async (input, options) => this.#create(input, options),
318
+ get: async (id, options) => this.#request('GET', checkoutId(id), '', undefined, options),
319
+ waitFor: async (id, input, options) => this.#waitFor(checkoutId(id), input, options),
320
+ browser: async (id, input, options) => this.#request('POST', checkoutId(id), '/browser', browserInput(input), options),
321
+ applyDetails: async (id, input, options) => {
322
+ keys(input, ['checkout_url']); merchantUrl(input.checkout_url);
323
+ return this.#request('POST', checkoutId(id), '/details', { checkout_url: input.checkout_url }, options);
324
+ },
325
+ verifyQuote: async (id, input, options) => {
326
+ keys(input, ['revision', 'checkout_url', 'expected_amount_cents', 'expected_currency', 'checkout_total_selector']);
327
+ integer(input.revision, 1, 2147483647); merchantUrl(input.checkout_url);
328
+ integer(input.expected_amount_cents, 1, 2147483647); check(input.expected_currency === 'USD');
329
+ string(input.checkout_total_selector, 500);
330
+ return this.#request('POST', checkoutId(id), '/verify-quote', { ...input }, options);
331
+ },
332
+ requestApproval: async (id, input, options) => {
333
+ keys(input, ['checkout_url', 'amount_cents', 'currency', 'checkout_total_selector', 'submit_selector', 'fill_after_verification'],
334
+ ['checkout_url', 'amount_cents', 'currency', 'checkout_total_selector', 'submit_selector']);
335
+ check(input.fill_after_verification === undefined || input.fill_after_verification === true);
336
+ merchantUrl(input.checkout_url); integer(input.amount_cents, 1, 2147483647); check(input.currency === 'USD');
337
+ string(input.checkout_total_selector, 500); string(input.submit_selector, 200);
338
+ const result = await this.#request('POST', checkoutId(id), '/approval', { ...input }, options);
339
+ check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response');
340
+ return result;
341
+ },
342
+ fill: async (id, input, options) => this.#request('POST', checkoutId(id), '/fill', revisionInput(input), options),
343
+ submit: async (id, input, options) => this.#request('POST', checkoutId(id), '/submit', revisionInput(input), options),
344
+ cancel: async (id, options) => this.#request('POST', checkoutId(id), '/cancel', {}, options),
345
+ });
346
+ }
347
+ async #waitFor(id, input, options = {}) {
348
+ keys(input, ['statuses', 'timeoutMs', 'intervalMs'], ['statuses']);
349
+ check(Array.isArray(input.statuses) && input.statuses.length > 0 && input.statuses.length <= STATUSES.size &&
350
+ [...input.statuses].every(status => STATUSES.has(status)));
351
+ const statuses = new Set(input.statuses);
352
+ const timeoutMs = integer(input.timeoutMs ?? 60000, 1, 300000);
353
+ const intervalMs = integer(input.intervalMs ?? 1000, 250, 5000);
354
+ keys(options, ['signal'], []);
355
+ check(options.signal === undefined || options.signal instanceof AbortSignal);
356
+ const controller = new AbortController();
357
+ const timer = setTimeout(() => controller.abort(), timeoutMs); timer.unref?.();
358
+ const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
359
+ try {
360
+ while (true) {
361
+ signal.throwIfAborted();
362
+ const checkout = await this.#request('GET', id, '', undefined, { signal });
363
+ signal.throwIfAborted();
364
+ // Readiness is an urgent decision boundary: never poll away its ten-second submit claim.
365
+ if (statuses.has(checkout.status) || WAIT_STOPS.has(checkout.status)) return checkout;
366
+ let pause;
367
+ try { await bounded(new Promise(resolve => { pause = setTimeout(resolve, intervalMs); }), signal); }
368
+ finally { clearTimeout(pause); }
369
+ }
370
+ } catch (error) {
371
+ if (signal.aborted) throw new AccelerateError(options.signal?.aborted ? 'request_cancelled' : 'wait_timeout');
372
+ throw error;
373
+ } finally { clearTimeout(timer); }
374
+ }
375
+ #create(input, options) {
376
+ keys(input, ['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key', 'interaction_mode', 'conversation_id', 'purpose', 'approval_policy', 'browser', 'browser_resource', 'fill_after_verification'],
377
+ ['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key']);
378
+ check(typeof input.customer_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.customer_id));
379
+ check(typeof input.workspace_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.workspace_id));
380
+ string(input.idempotency_key, 200);
381
+ check(typeof input.phone_number === 'string' && /^\+[1-9]\d{6,14}$/u.test(input.phone_number));
382
+ merchantUrl(input.start_url);
383
+ check(input.interaction_mode === undefined || ['hosted', 'chat'].includes(input.interaction_mode));
384
+ check(input.purpose === undefined || ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(input.purpose));
385
+ check(input.approval_policy === undefined || ['before_fill', 'after_fill'].includes(input.approval_policy));
386
+ check(input.approval_policy !== 'after_fill' || input.interaction_mode === 'chat' && input.purpose !== 'sandbox_source_comparison');
387
+ check(input.fill_after_verification === undefined || input.fill_after_verification === true && input.interaction_mode === 'chat' && input.approval_policy === 'after_fill' && ['purchase', 'sandbox_fill_only'].includes(input.purpose ?? 'purchase'));
388
+ check(input.purpose !== 'sandbox_fill_only' || input.interaction_mode === 'chat');
389
+ check(input.interaction_mode === 'chat' ? typeof input.conversation_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.conversation_id) : input.conversation_id === undefined);
390
+ if (input.browser !== undefined) {
391
+ keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]);
392
+ const supportedBinding = input.browser.provider === 'browser_use'
393
+ ? typeof input.browser.browser_id === 'string' && UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase()
394
+ : input.browser.provider === 'connector' && typeof input.browser.connector_id === 'string' && /^[a-z][a-z0-9_-]{0,63}$/u.test(input.browser.connector_id) &&
395
+ typeof input.browser.browser_id === 'string' && /^[A-Za-z0-9_.:-]{1,200}$/u.test(input.browser.browser_id);
396
+ check(supportedBinding && input.browser.custody_mode === 'customer_backend_trusted' &&
397
+ input.interaction_mode === 'chat' && input.approval_policy === 'after_fill' && input.purpose !== 'sandbox_source_comparison');
398
+ }
399
+ check(input.browser_resource === undefined || input.browser?.provider === 'browser_use' && input.browser_resource?.id === input.browser.browser_id);
400
+ return this.#request('POST', null, '', { ...input }, options).then(result => {
401
+ check(input.purpose !== 'sandbox_fill_only' || result.purpose === 'sandbox_fill_only' && result.submit_allowed === false, 'invalid_response');
402
+ check(input.approval_policy !== 'after_fill' || result.approval_policy === 'after_fill' && result.purchase_approved === false, 'invalid_response');
403
+ check(input.interaction_mode !== 'chat' || result.wallet_url === undefined, 'invalid_response');
404
+ check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response'); return result;
405
+ });
406
+ }
407
+ async #request(method, id, suffix, body, options = {}, connection = false) {
408
+ keys(options, ['signal'], []);
409
+ check(options.signal === undefined || options.signal instanceof AbortSignal);
410
+ const controller = new AbortController();
411
+ const milliseconds = connection ? 45000 : ['/inspect-creation', '/inspect-retirement'].includes(suffix) ? 60000 : id === null ? 245000 : ({ '/fill': 235000, '/details': 125000, '/verify-quote': 125000, '/approval': 125000,
412
+ '/submit': 125000, '/browser': 85000, '/cancel': 45000, '': 45000 })[suffix];
413
+ const timer = setTimeout(() => controller.abort(), milliseconds); timer.unref?.();
414
+ const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
415
+ const url = connection ? `${this.#origin}/v1/connection` :
416
+ `${this.#origin}/v1/checkouts${id === null ? '' : `/${id}`}${suffix}`;
417
+ let response;
418
+ try {
419
+ signal.throwIfAborted();
420
+ response = await bounded(this.#fetch(url, { method, redirect: 'error', cache: 'no-store', signal,
421
+ headers: { Authorization: `Bearer ${this.#apiKey}`, 'X-Accelerate-Merchant-Id': this.#merchantId,
422
+ ...(!connection ? { 'X-Accelerate-Diagnostics': 'v1' } : {}),
423
+ Accept: 'application/json', ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}) },
424
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }) }), signal);
425
+ check(!response.redirected && response.status < 300 && (!response.url || response.url === url) ||
426
+ (!response.redirected && response.status >= 400 && (!response.url || response.url === url)), 'invalid_response');
427
+ const value = await bounded(readJson(response, signal), signal);
428
+ signal.throwIfAborted();
429
+ if (!response.ok) throw new AccelerateError(ERROR_CODES.has(value?.error?.code) ? value.error.code : 'request_failed', response.status,
430
+ response.headers.get('x-accelerate-support-reference'));
431
+ return connection ? connectionResponse(value, this.#merchantId) : suffix === '/browser' ? browserResponse(value) :
432
+ suffix === '/verify-quote' ? verifiedQuoteResponse(value, id, body) : suffix === '/inspect-creation' ? creationInspectionResponse(value, body) : suffix === '/inspect-retirement' ? retirementInspectionResponse(value, id, body) : statusResponse(value, id, {
433
+ create: id === null, origin: this.#origin, loopback: this.#loopback });
434
+ } catch (error) {
435
+ Promise.resolve(response?.body?.cancel()).catch(() => {});
436
+ if (signal.aborted) throw new AccelerateError(options.signal?.aborted ? 'request_cancelled' : 'request_timeout');
437
+ if (error instanceof AccelerateError) throw error;
438
+ throw new AccelerateError('request_failed');
439
+ } finally { clearTimeout(timer); }
440
+ }
441
+ }
@@ -0,0 +1,45 @@
1
+ import type { ManagedCheckoutContext } from './managed-mcp.js';
2
+ import type { AuthenticatedPrivateMessage, PrivateMessagingAdapter } from './private-ingress.js';
3
+
4
+ interface LinqRequestOptions { signal?: AbortSignal; maxRetries?: number; timeout?: number }
5
+ export interface LinqMessage {
6
+ id: string;
7
+ chat_id?: string;
8
+ is_from_me?: boolean;
9
+ delivery_status: string;
10
+ sent_at?: string | null;
11
+ }
12
+ export interface LinqPrivateChat {
13
+ id: string;
14
+ is_group: boolean;
15
+ handles: Array<{ handle: string; is_me: boolean; left_at?: string | null; status?: string | null }>;
16
+ }
17
+ /** Structural surface compatible with the installed @linqapp/sdk. No provider
18
+ * credential is returned by this adapter or required in model-visible config. */
19
+ export interface LinqMessagingClient {
20
+ chats: {
21
+ retrieve(id: string, options?: LinqRequestOptions): Promise<LinqPrivateChat>;
22
+ messages: { send(id: string, body: { message: { idempotency_key: string; parts: Array<
23
+ { type: 'text'; value: string } | { type: 'media'; attachment_id: string }> } },
24
+ options?: LinqRequestOptions): Promise<{ chat_id: string; message: LinqMessage }> };
25
+ };
26
+ messages: { retrieve(id: string, options?: LinqRequestOptions): Promise<LinqMessage> };
27
+ attachments: { create(input: { filename: string; content_type: 'image/png'; size_bytes: number },
28
+ options?: LinqRequestOptions): Promise<{ attachment_id: string; upload_url: string; http_method: string; required_headers: Record<string, string> }> };
29
+ }
30
+ export function createLinqMessagingAdapter(options: {
31
+ client: LinqMessagingClient;
32
+ sendingPhone: string;
33
+ /** Resolve an EXISTING established private chat from backend records. Do not
34
+ * accept a recipient chosen by a model; never create a new chat here. */
35
+ resolveDestination(context: Readonly<ManagedCheckoutContext>): Promise<string> | string;
36
+ fetchImpl?: typeof fetch;
37
+ }): Readonly<PrivateMessagingAdapter>;
38
+ export function createLinqWebhookAuthenticator<Request = unknown>(options: {
39
+ /** Strict true only after real signature/OIDC verification over raw bytes. */
40
+ verifyWebhook(request: Request, rawBody: Uint8Array): Promise<boolean> | boolean;
41
+ /** Map verified provider sender to an authenticated, phone-verified account,
42
+ * workspace and exact established conversation in backend records. */
43
+ resolveIdentity(identity: { phone_number: string; chat_id: string }): Promise<ManagedCheckoutContext> | ManagedCheckoutContext;
44
+ retrieveChat(chatId: string): Promise<Pick<LinqPrivateChat, 'id' | 'is_group'>>;
45
+ }): (request: Request, rawBody: Uint8Array) => Promise<AuthenticatedPrivateMessage | { kind: 'ignore' }>;
@@ -0,0 +1,123 @@
1
+ import { AccelerateError } from './index.js';
2
+ import { authenticatedCheckoutContext } from './host-adapter.js';
3
+
4
+ const HANDLE = /^[A-Za-z0-9_-]{1,500}$/;
5
+ const MESSAGE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/;
6
+ const maximumReviewImageBytes = 8 * 1024 * 1024;
7
+ const check = condition => { if (!condition) throw new AccelerateError('request_failed'); };
8
+ const phone = value => typeof value === 'string' && /^\+[1-9][\d ()-]+$/.test(value) ? value.replace(/[ ()-]/g, '') : undefined;
9
+ function receipt(value, chatId, expectedId) {
10
+ check(value && value.chat_id === chatId && value.is_from_me === true && MESSAGE.test(value.id) && (!expectedId || value.id === expectedId));
11
+ if (!['sent', 'delivered', 'read'].includes(value.delivery_status)) return undefined;
12
+ check(typeof value.sent_at === 'string' && Number.isFinite(Date.parse(value.sent_at)) && Date.parse(value.sent_at) <= Date.now());
13
+ return { message_id: value.id, sent_at: new Date(value.sent_at).toISOString() };
14
+ }
15
+ async function wait(ms, signal) {
16
+ await new Promise((resolve, reject) => {
17
+ const timer = setTimeout(done, ms);
18
+ function done() { signal?.removeEventListener('abort', abort); resolve(); }
19
+ function abort() { clearTimeout(timer); signal?.removeEventListener('abort', abort); reject(new AccelerateError('request_cancelled')); }
20
+ signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted) abort();
21
+ });
22
+ }
23
+ function boundedSignal(signal, expiresAt) {
24
+ const remaining = expiresAt === undefined ? 45000 : Date.parse(expiresAt) - Date.now(); check(Number.isFinite(remaining) && remaining > 0);
25
+ const timeout = AbortSignal.timeout(Math.min(45000, remaining));
26
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
27
+ }
28
+ function prompt(message) {
29
+ check(message && typeof message.text === 'string' && message.text.length > 0 && message.text.length <= 10000 && typeof message.idempotency_key === 'string' && /^[A-Za-z0-9_.:-]{1,200}$/.test(message.idempotency_key));
30
+ }
31
+
32
+ /** Native Linq transport. Inject the installed @linqapp/sdk client configured
33
+ * with its server-only credential; the SDK does not install or own that key. */
34
+ export function createLinqMessagingAdapter({ client, sendingPhone, resolveDestination, fetchImpl = globalThis.fetch } = {}) {
35
+ check(typeof client?.chats?.retrieve === 'function' && typeof client?.chats?.messages?.send === 'function' && typeof client?.messages?.retrieve === 'function' && typeof client?.attachments?.create === 'function' && /^\+[1-9]\d{6,14}$/.test(sendingPhone) && typeof resolveDestination === 'function' && typeof fetchImpl === 'function');
36
+ const destination = async (input, signal) => {
37
+ const context = authenticatedCheckoutContext(input), chatId = await resolveDestination(context);
38
+ check(typeof chatId === 'string' && HANDLE.test(chatId) && chatId !== 'pending' && context.phone_number !== sendingPhone);
39
+ signal.throwIfAborted();
40
+ const chat = await client.chats.retrieve(chatId, { signal, maxRetries: 0, timeout: 5000 });
41
+ check(chat?.id === chatId && chat.is_group === false && Array.isArray(chat.handles) && chat.handles.length === 2);
42
+ const own = chat.handles.filter(handle => handle.is_me === true), recipient = chat.handles.filter(handle => handle.is_me === false);
43
+ check(own.length === 1 && recipient.length === 1 && phone(own[0].handle) === sendingPhone && phone(recipient[0].handle) === context.phone_number && chat.handles.every(handle => handle.left_at == null && !['left', 'removed'].includes(handle.status)));
44
+ return chatId;
45
+ };
46
+ const send = async (chatId, message, signal, expiresAt) => {
47
+ signal.throwIfAborted();
48
+ const sent = await client.chats.messages.send(chatId, { message }, { signal, maxRetries: 0, timeout: 15000 });
49
+ check(sent?.chat_id === chatId && MESSAGE.test(sent?.message?.id));
50
+ const expectedId = sent.message.id;
51
+ // The send envelope proves this is our send; retrieval independently checks
52
+ // direction. Neither API's queue timestamp can arm purchase approval.
53
+ let current = { ...sent.message, chat_id: chatId, is_from_me: true };
54
+ for (let attempt = 0; attempt < 12; attempt++) {
55
+ signal.throwIfAborted(); check(expiresAt === undefined || Date.parse(expiresAt) > Date.now());
56
+ const confirmed = receipt(current, chatId, expectedId);
57
+ if (confirmed) return confirmed;
58
+ check(current.delivery_status !== 'failed');
59
+ await wait(1000, signal);
60
+ current = await client.messages.retrieve(expectedId, { signal, maxRetries: 0, timeout: 5000 });
61
+ }
62
+ throw new AccelerateError('request_failed');
63
+ };
64
+ return Object.freeze({
65
+ async sendText(context, message, options = {}) {
66
+ try {
67
+ prompt(message); const signal = boundedSignal(options.signal), chatId = await destination(context, signal);
68
+ return await send(chatId, { parts: [{ type: 'text', value: message.text }], idempotency_key: message.idempotency_key }, signal);
69
+ } catch { throw new AccelerateError('request_failed'); }
70
+ },
71
+ async sendReview(context, message) {
72
+ let uploadCopy;
73
+ try {
74
+ prompt(message);
75
+ check(Buffer.isBuffer(message.bytes) && message.bytes.length > 8 && message.bytes.length <= maximumReviewImageBytes && message.bytes.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10])) && message.contentType === 'image/png' && message.signal instanceof AbortSignal);
76
+ const signal = boundedSignal(message.signal, message.delivery_expires_at), chatId = await destination(context, signal);
77
+ const attachment = await client.attachments.create({ filename: 'accelerate-payment-review.png', content_type: 'image/png', size_bytes: message.bytes.length }, { signal, maxRetries: 0, timeout: 10000 });
78
+ let url; try { url = new URL(attachment.upload_url); } catch { check(false); }
79
+ check(url.protocol === 'https:' && !url.username && !url.password && attachment.http_method === 'PUT' && HANDLE.test(attachment.attachment_id) && attachment.required_headers && typeof attachment.required_headers === 'object' && Object.entries(attachment.required_headers).every(([key, value]) => typeof key === 'string' && typeof value === 'string'));
80
+ signal.throwIfAborted(); uploadCopy = Buffer.from(message.bytes);
81
+ const uploaded = await fetchImpl(url, { method: 'PUT', redirect: 'error', cache: 'no-store', credentials: 'omit', referrerPolicy: 'no-referrer', signal, headers: attachment.required_headers, body: uploadCopy });
82
+ try { await uploaded.body?.cancel(); } catch {}
83
+ check(uploaded.ok && !uploaded.redirected); signal.throwIfAborted();
84
+ return await send(chatId, { parts: [{ type: 'media', attachment_id: attachment.attachment_id }, { type: 'text', value: message.text }], idempotency_key: message.idempotency_key }, signal, message.delivery_expires_at);
85
+ } catch { throw new AccelerateError('request_failed'); }
86
+ finally { uploadCopy?.fill(0); }
87
+ },
88
+ });
89
+ }
90
+
91
+ /** Signature/OIDC verification and identity resolution are host dependencies.
92
+ * Never treat provider sender text, chat ID or a successful JSON parse as auth. */
93
+ export function createLinqWebhookAuthenticator({ verifyWebhook, resolveIdentity, retrieveChat } = {}) {
94
+ check(typeof verifyWebhook === 'function' && typeof resolveIdentity === 'function' && typeof retrieveChat === 'function');
95
+ return async (request, rawBody) => {
96
+ let decoded;
97
+ try {
98
+ check(rawBody instanceof Uint8Array && rawBody.byteLength <= 262144 && await verifyWebhook(request, rawBody) === true);
99
+ decoded = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(rawBody));
100
+ if (decoded?.event_type !== 'message.received') return { kind: 'ignore' };
101
+ const data = decoded.data;
102
+ check(data && MESSAGE.test(data.id) && ['inbound', 'outbound'].includes(data.direction) && HANDLE.test(data.chat?.id) && data.sender_handle && typeof data.sender_handle.handle === 'string' && Array.isArray(data.parts) && data.parts.length <= 100);
103
+ if (data.direction !== 'inbound' || data.sender_handle.is_me === true) return { kind: 'ignore' };
104
+ const senderPhone = phone(data.sender_handle.handle); check(senderPhone && /^\+[1-9]\d{6,14}$/.test(senderPhone));
105
+ let isGroup = data.chat.is_group;
106
+ if (typeof isGroup !== 'boolean') { const chat = await retrieveChat(data.chat.id); check(chat?.id === data.chat.id && typeof chat.is_group === 'boolean'); isGroup = chat.is_group; }
107
+ check(isGroup === false);
108
+ const context = authenticatedCheckoutContext(await resolveIdentity({ phone_number: senderPhone, chat_id: data.chat.id }));
109
+ check(context.phone_number === senderPhone);
110
+ const parts = data.parts.map(part => { check(part && typeof part.type === 'string' && (part.value === undefined || typeof part.value === 'string')); return part; });
111
+ const text = parts.filter(part => ['text', 'link'].includes(part.type)).map(part => part.value ?? '').join('\n').trim(); check(text.length <= 2000);
112
+ const result = { authenticated: true, direct: true, context, message_id: data.id, text };
113
+ // Historical/reconciled deliveries cannot approve a current payment.
114
+ if (!data.reconciled_at && data.sent_at != null) { check(typeof data.sent_at === 'string' && Number.isFinite(Date.parse(data.sent_at))); result.sent_at = new Date(data.sent_at).toISOString(); }
115
+ if (data.reply_to?.message_id !== undefined) { check(MESSAGE.test(data.reply_to.message_id)); result.reply_to_message_id = data.reply_to.message_id; }
116
+ return result;
117
+ } catch { throw new AccelerateError('permission_denied'); }
118
+ finally {
119
+ if (Array.isArray(decoded?.data?.parts)) for (const part of decoded.data.parts) { if (part && typeof part === 'object' && Object.hasOwn(part, 'value')) part.value = ''; }
120
+ decoded = undefined;
121
+ }
122
+ };
123
+ }