@weaccelerateinc/agent-sdk 0.0.0-stage → 0.11.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 +151 -2
- package/approved-extension-releases/0.0.0.1.json +41 -0
- package/approved-extension-releases/0.0.0.2.json +34 -0
- package/approved-extension.d.ts +24 -0
- package/approved-extension.js +90 -0
- package/browser-connector.d.ts +109 -0
- package/browser-connector.js +218 -0
- package/browser-operations.d.ts +56 -0
- package/browser-operations.js +144 -0
- package/browser-use-host.d.ts +46 -0
- package/browser-use-host.js +149 -0
- package/checkout-diagnostics.d.ts +34 -0
- package/checkout-diagnostics.js +81 -0
- package/cli.js +105 -0
- package/customer.env.example +6 -0
- package/host-adapter.d.ts +90 -0
- package/host-adapter.js +357 -0
- package/index.d.ts +253 -0
- package/index.js +504 -0
- package/linq-messaging.d.ts +45 -0
- package/linq-messaging.js +123 -0
- package/managed-mcp.d.ts +156 -0
- package/managed-mcp.js +427 -0
- package/package.json +114 -4
- package/partner-starter/.env.example +13 -0
- package/partner-starter/README.md +61 -0
- package/partner-starter/gitignore.template +5 -0
- package/partner-starter/package.json +21 -0
- package/partner-starter/runtime.mjs +33 -0
- package/partner-starter/server.mjs +13 -0
- package/partner-starter/smoke.mjs +61 -0
- package/partner-starter/synthetic.mjs +256 -0
- package/partner-starter/tests/starter.test.mjs +119 -0
- package/partner-starter/trusted-context.example.mjs +23 -0
- package/partner.d.ts +27 -0
- package/partner.js +53 -0
- package/postgres-host.d.ts +14 -0
- package/postgres-host.js +102 -0
- package/private-chat.d.ts +104 -0
- package/private-chat.js +316 -0
- package/private-ingress.d.ts +46 -0
- package/private-ingress.js +132 -0
- package/starter/.env.example +19 -0
- package/starter/README.md +82 -0
- package/starter/gitignore.template +5 -0
- package/starter/package.json +21 -0
- package/starter/runtime.mjs +66 -0
- package/starter/server.mjs +13 -0
- package/starter/smoke.mjs +55 -0
- package/starter/synthetic.mjs +163 -0
- package/starter/tests/starter.test.mjs +224 -0
- package/starter/trusted-context.example.mjs +49 -0
package/index.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
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, validateCheckoutReconciliation } 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', 'final_click_armed', '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', 'verification_pending',
|
|
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
|
+
'reconciliation_unavailable', 'reconciliation_mismatch', 'reconciliation_conflict', 'customer_click_required', 'final_click_in_progress']);
|
|
25
|
+
const MESSAGES = {
|
|
26
|
+
invalid_input: 'Invalid Accelerate client request.',
|
|
27
|
+
invalid_response: 'Accelerate returned an unrecognized private response.',
|
|
28
|
+
request_timeout: 'Accelerate request timed out. Check checkout status; do not repeat uncertain payment work.',
|
|
29
|
+
request_cancelled: 'Accelerate request was cancelled. Check checkout status; do not repeat uncertain payment work.',
|
|
30
|
+
wait_timeout: 'Checkout status wait timed out. Check status before continuing; no payment action was taken by this wait.',
|
|
31
|
+
missing_api_key: 'Set ACCELERATE_API_KEY on your application server.',
|
|
32
|
+
missing_merchant_id: 'Set ACCELERATE_MERCHANT_ID on your application server.',
|
|
33
|
+
invalid_api_key: 'ACCELERATE_API_KEY is invalid. Check the server key supplied by Accelerate.',
|
|
34
|
+
invalid_merchant_id: 'ACCELERATE_MERCHANT_ID must be the merchant UUID supplied by Accelerate.',
|
|
35
|
+
invalid_base_url: 'ACCELERATE_BASE_URL must be an HTTPS origin without a path, query, fragment or credentials.',
|
|
36
|
+
};
|
|
37
|
+
const GUIDANCE = Object.create(null);
|
|
38
|
+
for (const [codes, message, nextAction] of [
|
|
39
|
+
[['checkout_unsupported'],
|
|
40
|
+
'This checkout’s payment fields or safe review are not supported. No private verification was started.', 'stop'],
|
|
41
|
+
[['unauthorized', 'tenant_unauthorized', 'permission_denied', 'configuration_error', 'configuration_invalid', 'capability_disabled', 'integration_unavailable'],
|
|
42
|
+
'Check the server API key, merchant ID and enabled Accelerate capabilities.', 'check_configuration'],
|
|
43
|
+
[['invalid_input', 'invalid_json', 'json_required', 'request_too_large', 'method_not_allowed', 'route_not_found', 'upstream_request_invalid'],
|
|
44
|
+
'Check the checkout request against the SDK contract.', 'check_request'],
|
|
45
|
+
[['verification_pending'],
|
|
46
|
+
'A private verification for this shopper, card, total and site is still open. Wait for it to expire, up to five minutes, then start a new checkout.', 'stop'],
|
|
47
|
+
[['operation_in_progress', 'operation_conflict', 'checkout_busy', 'rate_limited', 'wallet_refreshing', 'conflict'],
|
|
48
|
+
'Read checkout status before continuing. Do not repeat an uncertain payment action.', 'check_status'],
|
|
49
|
+
[['wallet_required', 'authentication_required', 'authentication_completed', 'selection_rejected'],
|
|
50
|
+
'Continue the private wallet authentication and selection flow.', 'complete_wallet'],
|
|
51
|
+
[['verification_required', 'verification_failed', 'consent_required', 'approval_required', 'approval_unavailable'],
|
|
52
|
+
'Complete the private purchase approval and verification flow before continuing.', 'complete_verification'],
|
|
53
|
+
[['quote_changed', 'address_changed', 'details_required'],
|
|
54
|
+
'Prepare the current checkout details and request approval for the updated quote.', 'refresh_quote'],
|
|
55
|
+
[['revision_conflict'], 'Read the current checkout revision before continuing.', 'refresh_revision'],
|
|
56
|
+
[['fill_continuation_required'],
|
|
57
|
+
'The private backend continues this staged fill after security-code verification. Read the same checkout status; do not request another fill.', 'check_status'],
|
|
58
|
+
[['review_unavailable'],
|
|
59
|
+
'This checkout has insufficient time for safe filled review and cleanup. Read its safe status; do not repeat filling.', 'stop'],
|
|
60
|
+
[['checkout_frozen', 'submit_unavailable', 'invalid_state'],
|
|
61
|
+
'Read checkout status and use only the actions permitted for its current state.', 'check_status'],
|
|
62
|
+
[['checkout_closed', 'checkout_expired', 'checkout_not_found', 'not_found', 'verification_consumed', 'handoff_consumed'],
|
|
63
|
+
'Stop this checkout action. Read its status and reconcile any previous payment attempt.', 'stop'],
|
|
64
|
+
[['outcome_unknown'],
|
|
65
|
+
'Reconcile checkout status and the merchant order. Do not repeat the payment action.', 'reconcile_order'],
|
|
66
|
+
[['checkout_event_unconfirmed'],
|
|
67
|
+
'Private chat progress delivery could not be confirmed. Check this existing checkout; do not create a replacement.', 'check_status'],
|
|
68
|
+
[['final_click_in_progress'],
|
|
69
|
+
'A customer-owned final click is armed and not yet observed. Read the checkout status; do not cancel, re-arm or click again.', 'check_status'],
|
|
70
|
+
[['customer_click_required'],
|
|
71
|
+
'Accelerate never performs the final payment click. Use the customer-owned browser host: arm, click once in the customer browser, then complete.', 'check_request'],
|
|
72
|
+
[['reconciliation_unavailable'],
|
|
73
|
+
'This checkout has no submit outcome to reconcile. Nothing was recorded. Read its status.', 'check_status'],
|
|
74
|
+
[['reconciliation_mismatch'],
|
|
75
|
+
'The order does not match the approved quote. Nothing was recorded. Check the merchant order.', 'reconcile_order'],
|
|
76
|
+
[['reconciliation_conflict'],
|
|
77
|
+
'A different result is already recorded for this checkout. It cannot be replaced.', 'check_status'],
|
|
78
|
+
]) for (const code of codes) GUIDANCE[code] = Object.freeze({ message, nextAction });
|
|
79
|
+
|
|
80
|
+
/** Safe diagnostics only: no response body, URL, credentials, cause or provider stack. */
|
|
81
|
+
export class AccelerateError extends Error {
|
|
82
|
+
constructor(code = 'request_failed', status = 0, supportReference) {
|
|
83
|
+
const known = Object.hasOwn(MESSAGES, code) || ERROR_CODES.has(code);
|
|
84
|
+
super(known && Object.hasOwn(MESSAGES, code) ? MESSAGES[code] : GUIDANCE[code]?.message ??
|
|
85
|
+
'Accelerate could not complete the request. Check checkout status before continuing.');
|
|
86
|
+
this.name = 'AccelerateError';
|
|
87
|
+
this.code = known ? code : 'request_failed';
|
|
88
|
+
this.status = Number.isInteger(status) && status >= 100 && status <= 599 ? status : 0;
|
|
89
|
+
this.retrySafe = false;
|
|
90
|
+
this.paymentConfirmed = false;
|
|
91
|
+
this.nextAction = GUIDANCE[this.code]?.nextAction ??
|
|
92
|
+
(['missing_api_key', 'missing_merchant_id', 'invalid_api_key', 'invalid_merchant_id', 'invalid_base_url'].includes(this.code) ?
|
|
93
|
+
'check_configuration' : 'check_status');
|
|
94
|
+
if (typeof supportReference === 'string' && /^(?:acs_|arq_)[a-f0-9]{32}$/.test(supportReference)) this.supportReference = supportReference;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const fail = (code = 'invalid_input') => { throw new AccelerateError(code); };
|
|
98
|
+
const check = (value, code) => { if (!value) fail(code); };
|
|
99
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
|
100
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
101
|
+
function keys(value, allowed, required = allowed, code = 'invalid_input') {
|
|
102
|
+
check(object(value) && Object.keys(value).every(key => allowed.includes(key)) &&
|
|
103
|
+
required.every(key => Object.hasOwn(value, key)), code);
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function string(value, max = 200, code = 'invalid_input') {
|
|
107
|
+
check(typeof value === 'string' && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/u.test(value), code);
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function integer(value, min, max, code = 'invalid_input') {
|
|
111
|
+
check(Number.isSafeInteger(value) && value >= min && value <= max, code); return value;
|
|
112
|
+
}
|
|
113
|
+
function checkoutId(id) { check(typeof id === 'string' && ID.test(id)); return id; }
|
|
114
|
+
function httpsUrl(value, { originOnly = false, loopback = false, code = 'invalid_input' } = {}) {
|
|
115
|
+
string(value, 2048, code);
|
|
116
|
+
let url; try { url = new URL(value); } catch { fail(code); }
|
|
117
|
+
const local = loopback && url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname);
|
|
118
|
+
check((url.protocol === 'https:' || local) && !url.username && !url.password && !url.hash &&
|
|
119
|
+
(!originOnly || (url.pathname === '/' && !url.search)), code);
|
|
120
|
+
return url;
|
|
121
|
+
}
|
|
122
|
+
function revisionInput(input) {
|
|
123
|
+
keys(input, ['revision']); return { revision: integer(input.revision, 1, 2147483647) };
|
|
124
|
+
}
|
|
125
|
+
function preparationText(value, max = 2000, code = 'invalid_input') {
|
|
126
|
+
string(value, max, code);
|
|
127
|
+
check(!/(?:\d[ -]?){13,19}/u.test(value) &&
|
|
128
|
+
!/\b(?:cvv|cvc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), code);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function reconciliationInput(input) {
|
|
132
|
+
keys(input, ['state', 'order_reference', 'amount_cents', 'currency', 'source'], ['state', 'source']);
|
|
133
|
+
check(['confirmed', 'declined', 'not_found'].includes(input.state) && typeof input.source === 'string' &&
|
|
134
|
+
/^[a-z][a-z0-9_.-]{0,39}$/u.test(input.source) && !/(?:\d[ -]?){13,19}/u.test(input.source));
|
|
135
|
+
if (input.order_reference !== undefined) string(input.order_reference, 200);
|
|
136
|
+
if (input.amount_cents !== undefined) integer(input.amount_cents, 1, 2147483647);
|
|
137
|
+
check(input.currency === undefined || input.currency === 'USD');
|
|
138
|
+
check(input.state !== 'confirmed' || [input.order_reference, input.amount_cents, input.currency].every(value => value !== undefined));
|
|
139
|
+
return { ...input };
|
|
140
|
+
}
|
|
141
|
+
function browserInput(input) {
|
|
142
|
+
keys(input, ['operation', 'arguments']);
|
|
143
|
+
try { return { operation: input.operation, arguments: normalizeBrowserOperation(input.operation, input.arguments) }; }
|
|
144
|
+
catch { fail(); }
|
|
145
|
+
}
|
|
146
|
+
function merchantUrl(value) {
|
|
147
|
+
try { normalizeBrowserOperation('navigate', { url: value }); }
|
|
148
|
+
catch { fail(); }
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
function timestamp(value) {
|
|
152
|
+
check(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value) && Number.isFinite(Date.parse(value)), 'invalid_response');
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function safeText(value, max = 1000, multiline = false) {
|
|
156
|
+
if (multiline) {
|
|
157
|
+
check(typeof value === 'string' && value.length <= max && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value) &&
|
|
158
|
+
!/(?:\d[ -]?){13,19}/u.test(value) &&
|
|
159
|
+
!/\b(?:cvv|cvc|csc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), 'invalid_response');
|
|
160
|
+
} else preparationText(value, max, 'invalid_response');
|
|
161
|
+
check(!/\b(?:cdp|browser_live_view|session_token|handoff_token|submit_token|id_token|authorizationToken)\b/iu.test(value) &&
|
|
162
|
+
!/\b(?:cvv|cvc|csc|security\s*code)\b["'\s:=]{0,12}\d{3,4}\b/iu.test(value) &&
|
|
163
|
+
!/(?: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) &&
|
|
164
|
+
!/\b(?:0[1-9]|1[0-2])\s*\/\s*(?:20)?\d{2}\b/u.test(value) &&
|
|
165
|
+
!/https?:\/\/[^\s/]*(?:onkernel\.com|verygoodproxy\.com|verygoodvault\.com)(?:[/:\s]|$)/iu.test(value), 'invalid_response');
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function statusResponse(value, expectedId, { create, origin, loopback }) {
|
|
169
|
+
keys(value, ['id', 'status', 'revision', 'expires_at', 'card', 'address', 'quote', 'submit_expires_at', 'purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'diagnostics',
|
|
170
|
+
'fill_after_verification', 'reconciliation', 'final_click_actor', 'submit_actor', 'retry_safe', 'payment_confirmed', ...(create ? ['wallet_url'] : [])],
|
|
171
|
+
['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], 'invalid_response');
|
|
172
|
+
check(typeof value.id === 'string' && ID.test(value.id) && (!expectedId || value.id === expectedId) && STATUSES.has(value.status) &&
|
|
173
|
+
value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
|
|
174
|
+
integer(value.revision, 0, 2147483647, 'invalid_response'); timestamp(value.expires_at);
|
|
175
|
+
if (value.purpose !== undefined || value.submit_allowed !== undefined) check(
|
|
176
|
+
['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(value.purpose) &&
|
|
177
|
+
value.submit_allowed === (value.purpose === 'purchase'), 'invalid_response');
|
|
178
|
+
if (value.approval_policy !== undefined || value.purchase_approved !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean' && (!value.purchase_approved || ['ready_for_submit', 'final_click_armed'].includes(value.status) && value.purpose === 'purchase'), 'invalid_response');
|
|
179
|
+
if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' &&
|
|
180
|
+
['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
|
|
181
|
+
if (value.submit_expires_at !== undefined) timestamp(value.submit_expires_at);
|
|
182
|
+
// Only the customer's own backend/browser may ever perform the final click.
|
|
183
|
+
if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
|
|
184
|
+
if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' &&
|
|
185
|
+
['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
|
|
186
|
+
// An attested attempt names the customer; an unknown outcome cannot assert that a click happened.
|
|
187
|
+
if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
|
|
188
|
+
if (value.reconciliation !== undefined) {
|
|
189
|
+
try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { fail('invalid_response'); }
|
|
190
|
+
}
|
|
191
|
+
if (value.diagnostics !== undefined) {
|
|
192
|
+
try { validateCheckoutDiagnostics(value.diagnostics, value); } catch { fail('invalid_response'); }
|
|
193
|
+
}
|
|
194
|
+
if (value.card !== undefined) {
|
|
195
|
+
keys(value.card, ['last4', 'brand'], ['last4', 'brand'], 'invalid_response');
|
|
196
|
+
check(value.card.last4 === null || typeof value.card.last4 === 'string' && /^\d{4}$/u.test(value.card.last4), 'invalid_response');
|
|
197
|
+
if (value.card.brand !== null) safeText(value.card.brand, 40);
|
|
198
|
+
}
|
|
199
|
+
if (value.address !== undefined) {
|
|
200
|
+
keys(value.address, ['label', 'city', 'state', 'postal_code_mask'], ['label'], 'invalid_response');
|
|
201
|
+
if (value.address.label !== null) safeText(value.address.label, 400);
|
|
202
|
+
for (const key of ['city', 'state', 'postal_code_mask']) if (value.address[key] != null) safeText(value.address[key], 128);
|
|
203
|
+
check(value.address.postal_code_mask == null || /[*•]/u.test(value.address.postal_code_mask), 'invalid_response');
|
|
204
|
+
}
|
|
205
|
+
if (value.quote !== undefined) {
|
|
206
|
+
keys(value.quote, ['checkout_origin', 'amount_cents', 'currency'], undefined, 'invalid_response');
|
|
207
|
+
httpsUrl(value.quote.checkout_origin, { originOnly: true, code: 'invalid_response' });
|
|
208
|
+
integer(value.quote.amount_cents, 1, 2147483647, 'invalid_response');
|
|
209
|
+
check(value.quote.currency === 'USD', 'invalid_response');
|
|
210
|
+
}
|
|
211
|
+
if (value.wallet_url !== undefined) {
|
|
212
|
+
string(value.wallet_url, 2048, 'invalid_response');
|
|
213
|
+
let url; try { url = new URL(value.wallet_url); } catch { fail('invalid_response'); }
|
|
214
|
+
check(url.origin === origin && url.pathname === `/checkout/${value.id}` && !url.search && !url.username && !url.password &&
|
|
215
|
+
/^#ticket=[A-Za-z0-9_-]{43}$/u.test(url.hash) && (url.protocol === 'https:' || loopback), 'invalid_response');
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
function browserResponse(value) {
|
|
220
|
+
keys(value, ['content', 'isError'], ['content'], 'invalid_response');
|
|
221
|
+
check(value.isError === undefined || value.isError === false, 'invalid_response');
|
|
222
|
+
check(Array.isArray(value.content) && value.content.length > 0 && value.content.length <= 20, 'invalid_response');
|
|
223
|
+
let total = 0;
|
|
224
|
+
for (const part of value.content) {
|
|
225
|
+
keys(part, ['type', 'text'], undefined, 'invalid_response');
|
|
226
|
+
check(part.type === 'text', 'invalid_response'); safeText(part.text, 24000, true);
|
|
227
|
+
total += Buffer.byteLength(part.text, 'utf8');
|
|
228
|
+
}
|
|
229
|
+
check(total <= 24000, 'invalid_response');
|
|
230
|
+
return { content: value.content };
|
|
231
|
+
}
|
|
232
|
+
function verifiedQuoteResponse(value, id, input) {
|
|
233
|
+
keys(value, ['checkout_id', 'revision', 'verified', 'quote'], undefined, 'invalid_response');
|
|
234
|
+
check(value.checkout_id === id && value.revision === input.revision && value.verified === true, 'invalid_response');
|
|
235
|
+
keys(value.quote, ['checkout_origin', 'amount_cents', 'currency'], undefined, 'invalid_response');
|
|
236
|
+
check(value.quote.checkout_origin === new URL(input.checkout_url).origin &&
|
|
237
|
+
value.quote.amount_cents === input.expected_amount_cents && value.quote.currency === input.expected_currency, 'invalid_response');
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
function creationInspectionInput(input) {
|
|
241
|
+
keys(input, ['customer_id', 'workspace_id', 'conversation_id', 'idempotency_key', 'browser']);
|
|
242
|
+
for (const name of ['customer_id', 'workspace_id', 'conversation_id'])
|
|
243
|
+
check(typeof input[name] === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input[name]));
|
|
244
|
+
string(input.idempotency_key, 200);
|
|
245
|
+
keys(input.browser, ['provider', 'browser_id', 'custody_mode']);
|
|
246
|
+
check(input.browser.provider === 'browser_use' && typeof input.browser.browser_id === 'string' &&
|
|
247
|
+
UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase() &&
|
|
248
|
+
input.browser.custody_mode === 'customer_backend_trusted');
|
|
249
|
+
return { ...input, browser: { ...input.browser } };
|
|
250
|
+
}
|
|
251
|
+
function creationInspectionResponse(value, input) {
|
|
252
|
+
if (value?.confirmed === false) { keys(value, ['confirmed'], undefined, 'invalid_response'); return value; }
|
|
253
|
+
keys(value, ['confirmed', 'checkout_id', 'revision', 'purpose', 'creation_key_sha256', 'retirement'], undefined, 'invalid_response');
|
|
254
|
+
check(value.confirmed === true && typeof value.checkout_id === 'string' && ID.test(value.checkout_id) && value.revision === 0 &&
|
|
255
|
+
['purchase', 'sandbox_fill_only'].includes(value.purpose) &&
|
|
256
|
+
value.creation_key_sha256 === createHash('sha256').update(input.idempotency_key).digest('hex'), 'invalid_response');
|
|
257
|
+
keys(value.retirement, ['browser_provider', 'browser_id', 'retired', 'provider_stopped', 'private_lease_closed', 'recording_disabled'], undefined, 'invalid_response');
|
|
258
|
+
check(value.retirement.browser_provider === input.browser.provider && value.retirement.browser_id === input.browser.browser_id &&
|
|
259
|
+
value.retirement.retired === true && value.retirement.provider_stopped === true && value.retirement.private_lease_closed === true &&
|
|
260
|
+
value.retirement.recording_disabled === true, 'invalid_response');
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
function retirementInspectionInput(input) {
|
|
264
|
+
keys(input, ['customer_id', 'workspace_id', 'conversation_id', 'idempotency_key', 'revision', 'browser']);
|
|
265
|
+
const { revision, ...scope } = input;
|
|
266
|
+
return { ...creationInspectionInput(scope), revision: integer(revision, 1, 2147483647) };
|
|
267
|
+
}
|
|
268
|
+
function retirementInspectionResponse(value, id, input) {
|
|
269
|
+
if (value?.confirmed === false) { keys(value, ['confirmed'], undefined, 'invalid_response'); return value; }
|
|
270
|
+
keys(value, ['confirmed', 'checkout_id', 'revision', 'status', 'purpose', 'approval_policy', 'submit_allowed',
|
|
271
|
+
'purchase_approved', 'creation_key_sha256', 'retirement'], undefined, 'invalid_response');
|
|
272
|
+
check(value.confirmed === true && value.checkout_id === id && value.revision === input.revision && value.status === 'outcome_unknown' &&
|
|
273
|
+
value.purpose === 'sandbox_fill_only' && value.approval_policy === 'after_fill' && value.submit_allowed === false &&
|
|
274
|
+
value.purchase_approved === false && value.creation_key_sha256 === createHash('sha256').update(input.idempotency_key).digest('hex'), 'invalid_response');
|
|
275
|
+
keys(value.retirement, ['browser_provider', 'browser_id', 'retired', 'provider_stopped', 'private_lease_closed', 'recording_disabled'], undefined, 'invalid_response');
|
|
276
|
+
check(value.retirement.browser_provider === input.browser.provider && value.retirement.browser_id === input.browser.browser_id &&
|
|
277
|
+
value.retirement.retired === true && value.retirement.provider_stopped === true && value.retirement.private_lease_closed === true &&
|
|
278
|
+
value.retirement.recording_disabled === true, 'invalid_response');
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
const ARM_ID = /^[\x21-\x7e]{20,16000}$/u;
|
|
282
|
+
function armId(value, code = 'invalid_input') { check(typeof value === 'string' && ARM_ID.test(value), code); return value; }
|
|
283
|
+
function finalClickArmResponse(value, input) {
|
|
284
|
+
keys(value, ['arm_id', 'armed_expires_at', 'target', 'retry_safe', 'payment_confirmed'], undefined, 'invalid_response');
|
|
285
|
+
armId(value.arm_id, 'invalid_response'); timestamp(value.armed_expires_at);
|
|
286
|
+
check(value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
|
|
287
|
+
keys(value.target, ['checkout_url', 'submit_selector'], undefined, 'invalid_response');
|
|
288
|
+
try { merchantUrl(value.target.checkout_url); string(value.target.submit_selector, 200); } catch { fail('invalid_response'); }
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
function connectionResponse(value, merchantId) {
|
|
292
|
+
keys(value, ['status', 'api_version', 'merchant_id', 'interaction_modes', 'currencies'], undefined, 'invalid_response');
|
|
293
|
+
check(value.status === 'connected' && value.api_version === 'v1' && value.merchant_id === merchantId &&
|
|
294
|
+
Array.isArray(value.interaction_modes) && value.interaction_modes.length === 2 &&
|
|
295
|
+
value.interaction_modes[0] === 'chat' && value.interaction_modes[1] === 'hosted' &&
|
|
296
|
+
Array.isArray(value.currencies) && value.currencies.length === 1 && value.currencies[0] === 'USD', 'invalid_response');
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
async function readJson(response, signal) {
|
|
300
|
+
check(response.body && /^application\/json(?:\s*;|$)/iu.test(response.headers.get('content-type') ?? ''), 'invalid_response');
|
|
301
|
+
const reader = response.body.getReader();
|
|
302
|
+
const abort = () => { void reader.cancel().catch(() => {}); };
|
|
303
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
304
|
+
let size = 0; const chunks = [];
|
|
305
|
+
try {
|
|
306
|
+
while (true) {
|
|
307
|
+
const next = await reader.read(); if (next.done) break;
|
|
308
|
+
signal.throwIfAborted();
|
|
309
|
+
size += next.value.byteLength; check(size <= 65536, 'invalid_response'); chunks.push(next.value);
|
|
310
|
+
}
|
|
311
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
312
|
+
} finally { signal.removeEventListener('abort', abort); await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
313
|
+
}
|
|
314
|
+
function bounded(operation, signal) {
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const aborted = () => reject(new AccelerateError('request_cancelled'));
|
|
317
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
318
|
+
if (signal.aborted) aborted();
|
|
319
|
+
Promise.resolve(operation).then(value => {
|
|
320
|
+
signal.removeEventListener('abort', aborted);
|
|
321
|
+
if (signal.aborted) { Promise.resolve(value?.body?.cancel()).catch(() => {}); reject(new AccelerateError('request_cancelled')); }
|
|
322
|
+
else resolve(value);
|
|
323
|
+
}, error => { signal.removeEventListener('abort', aborted);
|
|
324
|
+
reject(error instanceof AccelerateError ? error : new AccelerateError('request_failed')); });
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Instantiate only on your authenticated application server. Never bundle an API key in a browser. */
|
|
329
|
+
export class AccelerateClient {
|
|
330
|
+
#origin; #apiKey; #merchantId; #fetch; #loopback;
|
|
331
|
+
/** Server environment only. Never pass model input or expose the returned client to a browser. */
|
|
332
|
+
static fromEnv(env = processEnv) {
|
|
333
|
+
check(Number(versions.node?.split('.')[0]) >= 22 && typeof globalThis.window === 'undefined' &&
|
|
334
|
+
typeof globalThis.document === 'undefined' && typeof globalThis.self === 'undefined');
|
|
335
|
+
check(env === processEnv || object(env));
|
|
336
|
+
const apiKey = env.ACCELERATE_API_KEY, merchantId = env.ACCELERATE_MERCHANT_ID;
|
|
337
|
+
check(apiKey !== undefined && !(typeof apiKey === 'string' && apiKey.trim().length === 0), 'missing_api_key');
|
|
338
|
+
check(typeof apiKey === 'string' && /^[A-Za-z0-9_-]{32,256}$/u.test(apiKey), 'invalid_api_key');
|
|
339
|
+
check(merchantId !== undefined && !(typeof merchantId === 'string' && merchantId.trim().length === 0), 'missing_merchant_id');
|
|
340
|
+
check(typeof merchantId === 'string' && UUID.test(merchantId), 'invalid_merchant_id');
|
|
341
|
+
const baseUrl = env.ACCELERATE_BASE_URL === undefined ? HOSTED_SERVICE : env.ACCELERATE_BASE_URL;
|
|
342
|
+
try { httpsUrl(baseUrl, { originOnly: true }); } catch { fail('invalid_base_url'); }
|
|
343
|
+
return new this({ baseUrl, apiKey, merchantId });
|
|
344
|
+
}
|
|
345
|
+
constructor({ baseUrl, apiKey, merchantId, fetchImpl = globalThis.fetch, allowLoopbackForTests = false } = {}) {
|
|
346
|
+
check(Number(versions.node?.split('.')[0]) >= 22 && typeof globalThis.window === 'undefined' &&
|
|
347
|
+
typeof globalThis.document === 'undefined' && typeof globalThis.self === 'undefined');
|
|
348
|
+
const url = httpsUrl(baseUrl, { originOnly: true, loopback: allowLoopbackForTests === true });
|
|
349
|
+
check(typeof apiKey === 'string' && /^[A-Za-z0-9_-]{32,256}$/u.test(apiKey));
|
|
350
|
+
check(typeof merchantId === 'string' && UUID.test(merchantId) && typeof fetchImpl === 'function');
|
|
351
|
+
this.#origin = url.origin; this.#apiKey = apiKey; this.#merchantId = merchantId.toLowerCase();
|
|
352
|
+
this.#fetch = fetchImpl; this.#loopback = allowLoopbackForTests === true;
|
|
353
|
+
this.connection = Object.freeze({
|
|
354
|
+
check: async options => this.#request('GET', undefined, '', undefined, options, true),
|
|
355
|
+
});
|
|
356
|
+
this.checkouts = Object.freeze({
|
|
357
|
+
inspectCreation: async (input, options) => this.#request('POST', null, '/inspect-creation', creationInspectionInput(input), options),
|
|
358
|
+
inspectRetirement: async (id, input, options) => this.#request('POST', checkoutId(id), '/inspect-retirement', retirementInspectionInput(input), options),
|
|
359
|
+
create: async (input, options) => this.#create(input, options),
|
|
360
|
+
get: async (id, options) => this.#request('GET', checkoutId(id), '', undefined, options),
|
|
361
|
+
waitFor: async (id, input, options) => this.#waitFor(checkoutId(id), input, options),
|
|
362
|
+
browser: async (id, input, options) => this.#request('POST', checkoutId(id), '/browser', browserInput(input), options),
|
|
363
|
+
applyDetails: async (id, input, options) => {
|
|
364
|
+
keys(input, ['checkout_url']); merchantUrl(input.checkout_url);
|
|
365
|
+
return this.#request('POST', checkoutId(id), '/details', { checkout_url: input.checkout_url }, options);
|
|
366
|
+
},
|
|
367
|
+
verifyQuote: async (id, input, options) => {
|
|
368
|
+
keys(input, ['revision', 'checkout_url', 'expected_amount_cents', 'expected_currency', 'checkout_total_selector']);
|
|
369
|
+
integer(input.revision, 1, 2147483647); merchantUrl(input.checkout_url);
|
|
370
|
+
integer(input.expected_amount_cents, 1, 2147483647); check(input.expected_currency === 'USD');
|
|
371
|
+
string(input.checkout_total_selector, 500);
|
|
372
|
+
return this.#request('POST', checkoutId(id), '/verify-quote', { ...input }, options);
|
|
373
|
+
},
|
|
374
|
+
requestApproval: async (id, input, options) => {
|
|
375
|
+
keys(input, ['checkout_url', 'amount_cents', 'currency', 'checkout_total_selector', 'submit_selector', 'fill_after_verification'],
|
|
376
|
+
['checkout_url', 'amount_cents', 'currency', 'checkout_total_selector', 'submit_selector']);
|
|
377
|
+
check(input.fill_after_verification === undefined || input.fill_after_verification === true);
|
|
378
|
+
merchantUrl(input.checkout_url); integer(input.amount_cents, 1, 2147483647); check(input.currency === 'USD');
|
|
379
|
+
string(input.checkout_total_selector, 500); string(input.submit_selector, 200);
|
|
380
|
+
const result = await this.#request('POST', checkoutId(id), '/approval', { ...input }, options);
|
|
381
|
+
check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response');
|
|
382
|
+
return result;
|
|
383
|
+
},
|
|
384
|
+
fill: async (id, input, options) => this.#request('POST', checkoutId(id), '/fill', revisionInput(input), options),
|
|
385
|
+
// The old executor-click endpoint is permanently closed: Accelerate never sends the final click.
|
|
386
|
+
submit: async () => fail('customer_click_required'),
|
|
387
|
+
/** Backend only. The returned target is never model/MCP output. */
|
|
388
|
+
armFinalClick: async (id, input, options) => this.#request('POST', checkoutId(id), '/final-click/arm', revisionInput(input), options),
|
|
389
|
+
completeFinalClick: async (id, input, options) => {
|
|
390
|
+
keys(input, ['revision', 'arm_id']); integer(input.revision, 1, 2147483647); armId(input.arm_id);
|
|
391
|
+
const result = await this.#request('POST', checkoutId(id), '/final-click/complete', { revision: input.revision, arm_id: input.arm_id }, options);
|
|
392
|
+
check(['submit_attempted', 'outcome_unknown'].includes(result.status) && result.purpose === 'purchase' &&
|
|
393
|
+
result.final_click_actor === 'customer' && (result.status === 'outcome_unknown' || result.submit_actor === 'customer'), 'invalid_response');
|
|
394
|
+
return result;
|
|
395
|
+
},
|
|
396
|
+
cancel: async (id, options) => this.#request('POST', checkoutId(id), '/cancel', {}, options),
|
|
397
|
+
reconcile: async (id, input, options) => {
|
|
398
|
+
const body = reconciliationInput(input);
|
|
399
|
+
const result = await this.#request('POST', checkoutId(id), '/reconcile', body, options);
|
|
400
|
+
check(result.reconciliation?.state === body.state && result.reconciliation.source === body.source, 'invalid_response');
|
|
401
|
+
return result;
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async #waitFor(id, input, options = {}) {
|
|
406
|
+
keys(input, ['statuses', 'timeoutMs', 'intervalMs'], ['statuses']);
|
|
407
|
+
check(Array.isArray(input.statuses) && input.statuses.length > 0 && input.statuses.length <= STATUSES.size &&
|
|
408
|
+
[...input.statuses].every(status => STATUSES.has(status)));
|
|
409
|
+
const statuses = new Set(input.statuses);
|
|
410
|
+
const timeoutMs = integer(input.timeoutMs ?? 60000, 1, 300000);
|
|
411
|
+
const intervalMs = integer(input.intervalMs ?? 1000, 250, 5000);
|
|
412
|
+
keys(options, ['signal'], []);
|
|
413
|
+
check(options.signal === undefined || options.signal instanceof AbortSignal);
|
|
414
|
+
const controller = new AbortController();
|
|
415
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs); timer.unref?.();
|
|
416
|
+
const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
|
|
417
|
+
try {
|
|
418
|
+
while (true) {
|
|
419
|
+
signal.throwIfAborted();
|
|
420
|
+
const checkout = await this.#request('GET', id, '', undefined, { signal });
|
|
421
|
+
signal.throwIfAborted();
|
|
422
|
+
// Readiness is an urgent decision boundary: never poll away its ten-second submit claim.
|
|
423
|
+
if (statuses.has(checkout.status) || WAIT_STOPS.has(checkout.status)) return checkout;
|
|
424
|
+
let pause;
|
|
425
|
+
try { await bounded(new Promise(resolve => { pause = setTimeout(resolve, intervalMs); }), signal); }
|
|
426
|
+
finally { clearTimeout(pause); }
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (signal.aborted) throw new AccelerateError(options.signal?.aborted ? 'request_cancelled' : 'wait_timeout');
|
|
430
|
+
throw error;
|
|
431
|
+
} finally { clearTimeout(timer); }
|
|
432
|
+
}
|
|
433
|
+
#create(input, options) {
|
|
434
|
+
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', 'final_click_actor'],
|
|
435
|
+
['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key']);
|
|
436
|
+
check(typeof input.customer_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.customer_id));
|
|
437
|
+
check(typeof input.workspace_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.workspace_id));
|
|
438
|
+
string(input.idempotency_key, 200);
|
|
439
|
+
check(typeof input.phone_number === 'string' && /^\+[1-9]\d{6,14}$/u.test(input.phone_number));
|
|
440
|
+
merchantUrl(input.start_url);
|
|
441
|
+
check(input.interaction_mode === undefined || ['hosted', 'chat'].includes(input.interaction_mode));
|
|
442
|
+
check(input.purpose === undefined || ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(input.purpose));
|
|
443
|
+
check(input.approval_policy === undefined || ['before_fill', 'after_fill'].includes(input.approval_policy));
|
|
444
|
+
check(input.approval_policy !== 'after_fill' || input.interaction_mode === 'chat' && input.purpose !== 'sandbox_source_comparison');
|
|
445
|
+
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'));
|
|
446
|
+
check(input.purpose !== 'sandbox_fill_only' || input.interaction_mode === 'chat');
|
|
447
|
+
// Purchases are never clicked by Accelerate. They require the customer's own trusted browser and final-click actor.
|
|
448
|
+
check(input.final_click_actor === undefined || input.final_click_actor === 'customer' && (input.purpose ?? 'purchase') === 'purchase');
|
|
449
|
+
if ((input.purpose ?? 'purchase') === 'purchase') check(input.final_click_actor === 'customer' && input.interaction_mode === 'chat' &&
|
|
450
|
+
input.approval_policy === 'after_fill' && input.browser?.custody_mode === 'customer_backend_trusted');
|
|
451
|
+
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);
|
|
452
|
+
if (input.browser !== undefined) {
|
|
453
|
+
keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]);
|
|
454
|
+
const supportedBinding = input.browser.provider === 'browser_use'
|
|
455
|
+
? typeof input.browser.browser_id === 'string' && UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase()
|
|
456
|
+
: input.browser.provider === 'connector' && typeof input.browser.connector_id === 'string' && /^[a-z][a-z0-9_-]{0,63}$/u.test(input.browser.connector_id) &&
|
|
457
|
+
typeof input.browser.browser_id === 'string' && /^[A-Za-z0-9_.:-]{1,200}$/u.test(input.browser.browser_id);
|
|
458
|
+
check(supportedBinding && input.browser.custody_mode === 'customer_backend_trusted' &&
|
|
459
|
+
input.interaction_mode === 'chat' && input.approval_policy === 'after_fill' && input.purpose !== 'sandbox_source_comparison');
|
|
460
|
+
}
|
|
461
|
+
check(input.browser_resource === undefined || input.browser?.provider === 'browser_use' && input.browser_resource?.id === input.browser.browser_id);
|
|
462
|
+
return this.#request('POST', null, '', { ...input }, options).then(result => {
|
|
463
|
+
check(input.purpose !== 'sandbox_fill_only' || result.purpose === 'sandbox_fill_only' && result.submit_allowed === false, 'invalid_response');
|
|
464
|
+
check(input.approval_policy !== 'after_fill' || result.approval_policy === 'after_fill' && result.purchase_approved === false, 'invalid_response');
|
|
465
|
+
check(input.interaction_mode !== 'chat' || result.wallet_url === undefined, 'invalid_response');
|
|
466
|
+
check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response');
|
|
467
|
+
check(input.final_click_actor !== 'customer' || result.final_click_actor === 'customer' && result.purpose === 'purchase', 'invalid_response'); return result;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
async #request(method, id, suffix, body, options = {}, connection = false) {
|
|
471
|
+
keys(options, ['signal'], []);
|
|
472
|
+
check(options.signal === undefined || options.signal instanceof AbortSignal);
|
|
473
|
+
const controller = new AbortController();
|
|
474
|
+
const milliseconds = connection ? 45000 : ['/inspect-creation', '/inspect-retirement'].includes(suffix) ? 60000 : id === null ? 245000 : ({ '/fill': 235000, '/details': 125000, '/verify-quote': 125000, '/approval': 125000,
|
|
475
|
+
'/final-click/arm': 20000, '/final-click/complete': 125000, '/browser': 85000, '/cancel': 45000, '/reconcile': 45000, '': 45000 })[suffix];
|
|
476
|
+
const timer = setTimeout(() => controller.abort(), milliseconds); timer.unref?.();
|
|
477
|
+
const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
|
|
478
|
+
const url = connection ? `${this.#origin}/v1/connection` :
|
|
479
|
+
`${this.#origin}/v1/checkouts${id === null ? '' : `/${id}`}${suffix}`;
|
|
480
|
+
let response;
|
|
481
|
+
try {
|
|
482
|
+
signal.throwIfAborted();
|
|
483
|
+
response = await bounded(this.#fetch(url, { method, redirect: 'error', cache: 'no-store', signal,
|
|
484
|
+
headers: { Authorization: `Bearer ${this.#apiKey}`, 'X-Accelerate-Merchant-Id': this.#merchantId,
|
|
485
|
+
...(!connection ? { 'X-Accelerate-Diagnostics': 'v1' } : {}),
|
|
486
|
+
Accept: 'application/json', ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}) },
|
|
487
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }) }), signal);
|
|
488
|
+
check(!response.redirected && response.status < 300 && (!response.url || response.url === url) ||
|
|
489
|
+
(!response.redirected && response.status >= 400 && (!response.url || response.url === url)), 'invalid_response');
|
|
490
|
+
const value = await bounded(readJson(response, signal), signal);
|
|
491
|
+
signal.throwIfAborted();
|
|
492
|
+
if (!response.ok) throw new AccelerateError(ERROR_CODES.has(value?.error?.code) ? value.error.code : 'request_failed', response.status,
|
|
493
|
+
response.headers.get('x-accelerate-support-reference'));
|
|
494
|
+
return connection ? connectionResponse(value, this.#merchantId) : suffix === '/browser' ? browserResponse(value) :
|
|
495
|
+
suffix === '/verify-quote' ? verifiedQuoteResponse(value, id, body) : suffix === '/final-click/arm' ? finalClickArmResponse(value, body) : suffix === '/inspect-creation' ? creationInspectionResponse(value, body) : suffix === '/inspect-retirement' ? retirementInspectionResponse(value, id, body) : statusResponse(value, id, {
|
|
496
|
+
create: id === null, origin: this.#origin, loopback: this.#loopback });
|
|
497
|
+
} catch (error) {
|
|
498
|
+
Promise.resolve(response?.body?.cancel()).catch(() => {});
|
|
499
|
+
if (signal.aborted) throw new AccelerateError(options.signal?.aborted ? 'request_cancelled' : 'request_timeout');
|
|
500
|
+
if (error instanceof AccelerateError) throw error;
|
|
501
|
+
throw new AccelerateError('request_failed');
|
|
502
|
+
} finally { clearTimeout(timer); }
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -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' }>;
|