@weaccelerateinc/agent-sdk 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/approved-extension-releases/0.0.0.1.json +4 -1
- package/approved-extension-releases/0.0.0.2.json +4 -1
- package/approved-extension.js +1 -1
- package/browser-connector.d.ts +22 -0
- package/browser-connector.js +48 -4
- package/browser-provenance.js +17 -0
- package/browser-use-host.d.ts +14 -0
- package/browser-use-host.js +77 -18
- package/checkout-diagnostics.d.ts +13 -2
- package/checkout-diagnostics.js +18 -4
- package/cli.js +4 -1
- package/host-adapter.d.ts +5 -0
- package/host-adapter.js +21 -5
- package/index.d.ts +63 -5
- package/index.js +84 -13
- package/managed-mcp.d.ts +9 -3
- package/managed-mcp.js +56 -7
- package/package.json +21 -2
- package/partner-starter/.env.example +4 -2
- package/partner-starter/README.md +19 -6
- package/partner-starter/package.json +1 -1
- package/partner-starter/runtime.mjs +3 -2
- package/partner-starter/smoke.mjs +10 -5
- package/partner-starter/synthetic.mjs +37 -7
- package/partner-starter/tests/starter.test.mjs +58 -8
- package/partner-starter/trusted-context.example.mjs +3 -0
- package/partner.js +4 -0
- package/postgres-host.d.ts +7 -3
- package/postgres-whatsapp-journal.d.ts +8 -0
- package/postgres-whatsapp-journal.js +95 -0
- package/private-chat.js +10 -3
- package/private-ingress.d.ts +4 -0
- package/private-ingress.js +22 -9
- package/starter/README.md +1 -7
- package/starter/package.json +1 -1
- package/starter/runtime.mjs +7 -2
- package/starter/synthetic.mjs +1 -1
- package/starter/tests/starter.test.mjs +7 -39
- package/whatsapp-journal.d.ts +11 -0
- package/whatsapp-journal.js +78 -0
- package/whatsapp-messaging.d.ts +64 -0
- package/whatsapp-messaging.js +182 -0
package/host-adapter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { AccelerateError } from './index.js';
|
|
4
|
-
import { validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
4
|
+
import { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
|
|
5
5
|
import { renderPrivateChatState, privateChatProgress } from './private-chat.js';
|
|
6
6
|
|
|
7
7
|
const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
|
|
@@ -9,9 +9,9 @@ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/;
|
|
|
9
9
|
const MESSAGE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/;
|
|
10
10
|
const RECOVER = new Set(['accelerate_checkout_status', 'accelerate_checkout_cancel', 'private_state', 'private_cancel']);
|
|
11
11
|
const TERMINAL = new Set(['cancelled', 'expired', 'failed']);
|
|
12
|
-
const STATUS_ORDER = new Map(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit'].map((status, index) => [status, index]));
|
|
12
|
+
const STATUS_ORDER = new Map(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed'].map((status, index) => [status, index]));
|
|
13
13
|
const STAGES = new Set(['not_started', 'identity_required', 'otp_required', 'selection_required', 'preparing_checkout', 'approval_required', 'cvv_required', 'verified', 'review_delivery_required', 'review_delivered', 'approved_for_submit', 'closed']);
|
|
14
|
-
const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
14
|
+
const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
15
15
|
const object = value => value && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
16
16
|
const check = (condition, code = 'invalid_input') => { if (!condition) throw new AccelerateError(code); };
|
|
17
17
|
const clone = value => structuredClone(value);
|
|
@@ -50,7 +50,7 @@ function safeText(value, max = 10000) {
|
|
|
50
50
|
return value;
|
|
51
51
|
}
|
|
52
52
|
function safeCheckout(value) {
|
|
53
|
-
exact(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics'], 'invalid_response');
|
|
53
|
+
exact(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics', 'reconciliation', 'final_click_actor', 'submit_actor'], 'invalid_response');
|
|
54
54
|
check(ID.test(value.id) && STATUSES.has(value.status) && Number.isSafeInteger(value.revision) && value.revision >= 0 && value.retry_safe === false && value.payment_confirmed === false && validDate(value.expires_at), 'invalid_response');
|
|
55
55
|
if (value.card !== undefined) {
|
|
56
56
|
exact(value.card, ['brand', 'last4'], [], 'invalid_response');
|
|
@@ -71,8 +71,12 @@ function safeCheckout(value) {
|
|
|
71
71
|
if (value.submit_expires_at !== undefined) check(validDate(value.submit_expires_at), 'invalid_response');
|
|
72
72
|
if (value.submit_allowed !== undefined) check(typeof value.submit_allowed === 'boolean', 'invalid_response');
|
|
73
73
|
if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' && ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
|
|
74
|
+
if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
|
|
75
|
+
if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' && ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
|
|
76
|
+
if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
|
|
74
77
|
if (value.purchase_approved !== undefined) check(typeof value.purchase_approved === 'boolean', 'invalid_response');
|
|
75
78
|
if (value.approval_policy !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean', 'invalid_response');
|
|
79
|
+
if (value.reconciliation !== undefined) { try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { check(false, 'invalid_response'); } }
|
|
76
80
|
// The SDK owns detailed diagnostics validation. This projection keeps no URLs,
|
|
77
81
|
// provider capabilities or data that could accidentally become an effect input.
|
|
78
82
|
const result = clone(value);
|
|
@@ -212,7 +216,7 @@ export function createDurableCheckoutHost({ persistence, resolveContext } = {})
|
|
|
212
216
|
}
|
|
213
217
|
if (metadata.operation === 'accelerate_checkout_submit') {
|
|
214
218
|
const checkout = active.record.attempt.checkout;
|
|
215
|
-
check(checkout?.purpose === 'purchase' && checkout.submit_allowed === true && checkout.status === 'ready_for_submit' && checkout.purchase_approved === true && active.record.attempt.private_stage === 'approved_for_submit' && validDate(checkout.submit_expires_at) && Date.parse(checkout.submit_expires_at) > Date.now() && metadata.revision === checkout.revision, 'submit_unavailable');
|
|
219
|
+
check(checkout?.purpose === 'purchase' && checkout.submit_allowed === true && checkout.final_click_actor === 'customer' && checkout.status === 'ready_for_submit' && checkout.purchase_approved === true && active.record.attempt.private_stage === 'approved_for_submit' && validDate(checkout.submit_expires_at) && Date.parse(checkout.submit_expires_at) > Date.now() && metadata.revision === checkout.revision, 'submit_unavailable');
|
|
216
220
|
}
|
|
217
221
|
const oneUse = ['accelerate_checkout_fill', 'accelerate_checkout_submit'].includes(metadata.operation);
|
|
218
222
|
if (oneUse) {
|
|
@@ -296,6 +300,18 @@ export function createDurableCheckoutHost({ persistence, resolveContext } = {})
|
|
|
296
300
|
check(MESSAGE.test(receipt.message_id) && validDate(receipt.sent_at) && Date.parse(receipt.sent_at) <= Date.now(), 'invalid_response');
|
|
297
301
|
entry.receipt = clone(receipt); await commit(active); return Object.freeze(clone(receipt));
|
|
298
302
|
}),
|
|
303
|
+
reconcilePrivateDelivery: (context, idempotencyKey, lookup) => locked(context, async (active, resolved) => {
|
|
304
|
+
check(/^accelerate-chat:[a-f0-9]{64}$/.test(idempotencyKey) && typeof lookup === 'function');
|
|
305
|
+
const entry = active.record.deliveries?.find(item => item.idempotency_key === idempotencyKey);
|
|
306
|
+
check(entry && active.record.attempt?.checkout_id, 'permission_denied');
|
|
307
|
+
if (entry.receipt) return Object.freeze(clone(entry.receipt));
|
|
308
|
+
// Only a trusted read-only journal lookup may be passed here. It must bind
|
|
309
|
+
// the exact owner, destination and prompt key; it must never send.
|
|
310
|
+
const receipt = await lookup(Object.freeze({ ...resolved }), idempotencyKey);
|
|
311
|
+
exact(receipt, ['message_id', 'sent_at'], [], 'invalid_response');
|
|
312
|
+
check(MESSAGE.test(receipt.message_id) && validDate(receipt.sent_at) && Date.parse(receipt.sent_at) <= Date.now(), 'invalid_response');
|
|
313
|
+
entry.receipt = clone(receipt); await commit(active); return Object.freeze(clone(receipt));
|
|
314
|
+
}),
|
|
299
315
|
confirmBrowserCleanup: (context, checkoutId) => locked(context, async active => {
|
|
300
316
|
check(active.record.attempt?.checkout_id === checkoutId && active.record.attempt.checkout?.diagnostics?.cleanup_status === 'confirmed', 'operation_conflict');
|
|
301
317
|
active.record.attempt.cleanup_confirmed = true; await commit(active);
|
package/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { BrowserOperation, BrowserResult } from './browser-operations.js';
|
|
2
|
-
import type { CheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
2
|
+
import type { CheckoutDiagnostics, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
|
|
3
3
|
export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
4
|
-
export type { CheckoutDiagnostics, CheckoutDiagnosticStage, CheckoutDiagnosticAction, CheckoutCleanupStatus } from './checkout-diagnostics.js';
|
|
4
|
+
export type { CheckoutDiagnostics, CheckoutDiagnosticStage, CheckoutDiagnosticAction, CheckoutCleanupStatus, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
|
|
5
5
|
export type { BrowserOperation, BrowserResult, BrowserStep, BrowserCondition } from './browser-operations.js';
|
|
6
6
|
|
|
7
7
|
export type CheckoutId = `mcs_${string}`;
|
|
8
8
|
export type CheckoutPurpose = 'purchase' | 'sandbox_source_comparison' | 'sandbox_fill_only';
|
|
9
9
|
export type CheckoutStatus = 'wallet_required' | 'selection_ready' | 'details_ready' | 'approval_required' |
|
|
10
|
-
'approved' | 'ready_for_submit' | 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
|
|
10
|
+
'approved' | 'ready_for_submit' | 'final_click_armed' | 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
|
|
11
11
|
export interface RequestOptions { signal?: AbortSignal }
|
|
12
12
|
export interface WaitForCheckout {
|
|
13
13
|
/** Return on these states, or immediately on ready_for_submit or any terminal state. GET requests only. */
|
|
@@ -47,16 +47,40 @@ export interface Checkout {
|
|
|
47
47
|
/** Explicit v2 policy acknowledged at creation; a later explicit quote arm lets private CVV continue
|
|
48
48
|
* one fill under the backend lock. Does not authorize final submission. */
|
|
49
49
|
fill_after_verification?: true;
|
|
50
|
+
/** Purchases only: the customer's own trusted backend/browser performs the physical final click. Accelerate never does. */
|
|
51
|
+
final_click_actor?: 'customer';
|
|
52
|
+
/** Present on submit_attempted (required for a purchase) and optionally on outcome_unknown, which cannot assert that a click happened. Never payment confirmation. */
|
|
53
|
+
submit_actor?: 'customer';
|
|
50
54
|
card?: { last4: string | null; brand: string | null };
|
|
51
55
|
address?: { label: string | null; city?: string | null; state?: string | null; postal_code_mask?: string | null };
|
|
52
56
|
quote?: { checkout_origin: string; amount_cents: number; currency: 'USD' };
|
|
53
57
|
submit_expires_at?: string;
|
|
58
|
+
/** Present once the client backend recorded the merchant's result. Absent on older services.
|
|
59
|
+
* This is the client's statement, not Accelerate's proof, so payment_confirmed stays false. */
|
|
60
|
+
reconciliation?: CheckoutReconciliation;
|
|
54
61
|
retry_safe: false;
|
|
55
62
|
payment_confirmed: false;
|
|
56
63
|
/** Negotiated safe metadata; absent when connecting to an older service. */
|
|
57
64
|
diagnostics?: CheckoutDiagnostics;
|
|
58
65
|
}
|
|
59
66
|
export interface CreatedCheckout extends Checkout { wallet_url?: string }
|
|
67
|
+
export interface ReconciledCheckout extends Checkout { reconciliation: CheckoutReconciliation }
|
|
68
|
+
/** Server only, after your backend checked the merchant's order system. Never model input.
|
|
69
|
+
* order_reference is stored only as a hash and is never returned. */
|
|
70
|
+
export type CheckoutReconciliationRequest = {
|
|
71
|
+
state: 'confirmed';
|
|
72
|
+
order_reference: string;
|
|
73
|
+
/** Must equal the approved quote exactly. */
|
|
74
|
+
amount_cents: number;
|
|
75
|
+
currency: 'USD';
|
|
76
|
+
source: string;
|
|
77
|
+
} | {
|
|
78
|
+
state: Exclude<CheckoutReconciliationState, 'confirmed'>;
|
|
79
|
+
order_reference?: string;
|
|
80
|
+
amount_cents?: number;
|
|
81
|
+
currency?: 'USD';
|
|
82
|
+
source: string;
|
|
83
|
+
};
|
|
60
84
|
/** Server-owned existing browser, resolved from the authenticated application's
|
|
61
85
|
* durable browser record. Never accept this binding from model arguments.
|
|
62
86
|
* The customer backend retains provider access; this is not private-from-creation custody. */
|
|
@@ -64,6 +88,16 @@ export interface BrowserUseBrowserBinding {
|
|
|
64
88
|
provider: 'browser_use';
|
|
65
89
|
browser_id: string;
|
|
66
90
|
custody_mode: 'customer_backend_trusted';
|
|
91
|
+
/** Opt-in provider run binding from a durable authenticated owner mapping. Never model input. */
|
|
92
|
+
provenance?: BrowserUseRunProvenance;
|
|
93
|
+
}
|
|
94
|
+
export interface BrowserUseRunProvenance {
|
|
95
|
+
type: 'browser_use_run_v1';
|
|
96
|
+
run_id: string;
|
|
97
|
+
session_id: string;
|
|
98
|
+
provider_workspace_id: string | null;
|
|
99
|
+
/** SHA-256 of the host's immutable owner/run reservation, not raw shopper data. */
|
|
100
|
+
binding_sha256: string;
|
|
67
101
|
}
|
|
68
102
|
/** Registered server-side executor connector; IDs are resolved from owned backend records. */
|
|
69
103
|
export interface RegisteredBrowserBinding {
|
|
@@ -125,6 +159,9 @@ export interface CreateCheckout {
|
|
|
125
159
|
/** Explicit chat/after_fill attachment. Omit to retain Accelerate-created
|
|
126
160
|
* private Kernel custody. Attachment never navigates or replaces the document. */
|
|
127
161
|
browser?: CustomerBrowserBinding;
|
|
162
|
+
/** Required for purchase (the default purpose): the customer's backend, not Accelerate, performs the final click.
|
|
163
|
+
* Purchase also requires chat, after_fill and a customer_backend_trusted browser. */
|
|
164
|
+
final_click_actor?: 'customer';
|
|
128
165
|
/** Your own provider read of `browser`, so Accelerate never needs your provider API key.
|
|
129
166
|
* Send it on every create. It is held in memory for this checkout and never stored. */
|
|
130
167
|
browser_resource?: ClientBrowserResource;
|
|
@@ -137,8 +174,12 @@ export interface ClientBrowserResource {
|
|
|
137
174
|
startedAt: string;
|
|
138
175
|
timeoutAt: string;
|
|
139
176
|
recordingAvailable: false;
|
|
177
|
+
agentSessionId?: string;
|
|
178
|
+
agentRun?: { id: string; sessionId: string; workspaceId: string | null; status: 'completed' | 'cancelled' };
|
|
179
|
+
agentSession?: { sessionId: string; workspaceId: string | null; latestRunId: string; status: 'completed' | 'cancelled' };
|
|
180
|
+
agentQueue?: { queue: never[] };
|
|
140
181
|
metadata: Record<string, string>;
|
|
141
|
-
extensions: [{ id: string; version: string; runtimeId: string }];
|
|
182
|
+
extensions: [{ id: string; version: string; runtimeId: string }, ...{ id: string; version: string; runtimeId: string }[]];
|
|
142
183
|
}
|
|
143
184
|
/** Hypothesis to inspect against the actual checkout; never a caller assertion of currency evidence. */
|
|
144
185
|
export interface QuoteVerificationRequest {
|
|
@@ -165,6 +206,16 @@ export interface ApprovalRequest {
|
|
|
165
206
|
* Successful private verification fills this exact revision without AI. */
|
|
166
207
|
fill_after_verification?: true;
|
|
167
208
|
}
|
|
209
|
+
/** BACKEND ONLY. The target and arm_id must never appear in MCP arguments, results, history or outbox. */
|
|
210
|
+
export interface ArmedFinalClick {
|
|
211
|
+
/** Opaque bounded string (20..16000 printable ASCII). Never display or log. */
|
|
212
|
+
arm_id: string;
|
|
213
|
+
/** Short window (service limit 15 seconds) in which the customer host may click once. */
|
|
214
|
+
armed_expires_at: string;
|
|
215
|
+
target: { checkout_url: string; submit_selector: string };
|
|
216
|
+
retry_safe: false;
|
|
217
|
+
payment_confirmed: false;
|
|
218
|
+
}
|
|
168
219
|
export interface AccelerateClientOptions {
|
|
169
220
|
baseUrl: string;
|
|
170
221
|
apiKey: string;
|
|
@@ -203,7 +254,14 @@ export class AccelerateClient {
|
|
|
203
254
|
verifyQuote(id: CheckoutId, input: QuoteVerificationRequest, options?: RequestOptions): Promise<VerifiedCheckoutQuote>;
|
|
204
255
|
requestApproval(id: CheckoutId, input: ApprovalRequest, options?: RequestOptions): Promise<Checkout>;
|
|
205
256
|
fill(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<Checkout>;
|
|
206
|
-
|
|
257
|
+
/** Removed behavior: Accelerate never clicks. Always rejects with customer_click_required and sends no request. */
|
|
258
|
+
submit(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<never>;
|
|
259
|
+
/** Backend only: prepare one short-lived customer-owned final click. Accelerate sends no physical input. */
|
|
260
|
+
armFinalClick(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<ArmedFinalClick>;
|
|
261
|
+
/** Backend only: observe the customer's click once. Dispatch claims are not sent; never retry. */
|
|
262
|
+
completeFinalClick(id: CheckoutId, input: { revision: number; arm_id: string }, options?: RequestOptions): Promise<Checkout>;
|
|
207
263
|
cancel(id: CheckoutId, options?: RequestOptions): Promise<Checkout>;
|
|
264
|
+
/** Records one merchant result after submit. Repeating the same input returns the same result; different input is refused. */
|
|
265
|
+
reconcile(id: CheckoutId, input: CheckoutReconciliationRequest, options?: RequestOptions): Promise<ReconciledCheckout>;
|
|
208
266
|
};
|
|
209
267
|
}
|
package/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { env as processEnv, versions } from 'node:process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { normalizeBrowserOperation } from './browser-operations.js';
|
|
4
|
-
import {
|
|
4
|
+
import { browserUseRunProvenance } from './browser-provenance.js';
|
|
5
|
+
import { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
|
|
5
6
|
export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
6
7
|
|
|
7
8
|
const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
|
|
8
9
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
10
|
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
|
+
'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
11
12
|
const WAIT_STOPS = new Set(['ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
12
13
|
const HOSTED_SERVICE = 'https://accelerate-agent-sdk.vercel.app';
|
|
13
14
|
const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied', 'not_found', 'conflict',
|
|
@@ -15,12 +16,13 @@ const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied
|
|
|
15
16
|
'operation_in_progress', 'fill_continuation_required', 'review_unavailable', 'capability_disabled', 'checkout_unsupported', 'integration_unavailable', 'outcome_unknown',
|
|
16
17
|
'authentication_required', 'checkout_closed', 'checkout_frozen', 'upstream_unavailable', 'wallet_required',
|
|
17
18
|
'revision_conflict', 'address_changed', 'details_required', 'quote_changed', 'submit_unavailable',
|
|
18
|
-
'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed',
|
|
19
|
+
'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed', 'verification_pending',
|
|
19
20
|
'verification_consumed', 'handoff_consumed', 'wallet_refreshing', 'consent_required',
|
|
20
21
|
'idempotency_conflict', 'operation_conflict', 'checkout_not_found', 'tenant_unauthorized',
|
|
21
22
|
'checkout_busy', 'store_unavailable', 'store_integrity_error', 'selection_rejected', 'upstream_invalid',
|
|
22
23
|
'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
|
+
'route_not_found', 'method_not_allowed', 'json_required', 'invalid_json', 'request_too_large', 'checkout_event_unconfirmed',
|
|
25
|
+
'reconciliation_unavailable', 'reconciliation_mismatch', 'reconciliation_conflict', 'customer_click_required', 'final_click_in_progress']);
|
|
24
26
|
const MESSAGES = {
|
|
25
27
|
invalid_input: 'Invalid Accelerate client request.',
|
|
26
28
|
invalid_response: 'Accelerate returned an unrecognized private response.',
|
|
@@ -41,6 +43,8 @@ for (const [codes, message, nextAction] of [
|
|
|
41
43
|
'Check the server API key, merchant ID and enabled Accelerate capabilities.', 'check_configuration'],
|
|
42
44
|
[['invalid_input', 'invalid_json', 'json_required', 'request_too_large', 'method_not_allowed', 'route_not_found', 'upstream_request_invalid'],
|
|
43
45
|
'Check the checkout request against the SDK contract.', 'check_request'],
|
|
46
|
+
[['verification_pending'],
|
|
47
|
+
'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'],
|
|
44
48
|
[['operation_in_progress', 'operation_conflict', 'checkout_busy', 'rate_limited', 'wallet_refreshing', 'conflict'],
|
|
45
49
|
'Read checkout status before continuing. Do not repeat an uncertain payment action.', 'check_status'],
|
|
46
50
|
[['wallet_required', 'authentication_required', 'authentication_completed', 'selection_rejected'],
|
|
@@ -62,6 +66,16 @@ for (const [codes, message, nextAction] of [
|
|
|
62
66
|
'Reconcile checkout status and the merchant order. Do not repeat the payment action.', 'reconcile_order'],
|
|
63
67
|
[['checkout_event_unconfirmed'],
|
|
64
68
|
'Private chat progress delivery could not be confirmed. Check this existing checkout; do not create a replacement.', 'check_status'],
|
|
69
|
+
[['final_click_in_progress'],
|
|
70
|
+
'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'],
|
|
71
|
+
[['customer_click_required'],
|
|
72
|
+
'Accelerate never performs the final payment click. Use the customer-owned browser host: arm, click once in the customer browser, then complete.', 'check_request'],
|
|
73
|
+
[['reconciliation_unavailable'],
|
|
74
|
+
'This checkout has no submit outcome to reconcile. Nothing was recorded. Read its status.', 'check_status'],
|
|
75
|
+
[['reconciliation_mismatch'],
|
|
76
|
+
'The order does not match the approved quote. Nothing was recorded. Check the merchant order.', 'reconcile_order'],
|
|
77
|
+
[['reconciliation_conflict'],
|
|
78
|
+
'A different result is already recorded for this checkout. It cannot be replaced.', 'check_status'],
|
|
65
79
|
]) for (const code of codes) GUIDANCE[code] = Object.freeze({ message, nextAction });
|
|
66
80
|
|
|
67
81
|
/** Safe diagnostics only: no response body, URL, credentials, cause or provider stack. */
|
|
@@ -115,6 +129,16 @@ function preparationText(value, max = 2000, code = 'invalid_input') {
|
|
|
115
129
|
!/\b(?:cvv|cvc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), code);
|
|
116
130
|
return value;
|
|
117
131
|
}
|
|
132
|
+
function reconciliationInput(input) {
|
|
133
|
+
keys(input, ['state', 'order_reference', 'amount_cents', 'currency', 'source'], ['state', 'source']);
|
|
134
|
+
check(['confirmed', 'declined', 'not_found'].includes(input.state) && typeof input.source === 'string' &&
|
|
135
|
+
/^[a-z][a-z0-9_.-]{0,39}$/u.test(input.source) && !/(?:\d[ -]?){13,19}/u.test(input.source));
|
|
136
|
+
if (input.order_reference !== undefined) string(input.order_reference, 200);
|
|
137
|
+
if (input.amount_cents !== undefined) integer(input.amount_cents, 1, 2147483647);
|
|
138
|
+
check(input.currency === undefined || input.currency === 'USD');
|
|
139
|
+
check(input.state !== 'confirmed' || [input.order_reference, input.amount_cents, input.currency].every(value => value !== undefined));
|
|
140
|
+
return { ...input };
|
|
141
|
+
}
|
|
118
142
|
function browserInput(input) {
|
|
119
143
|
keys(input, ['operation', 'arguments']);
|
|
120
144
|
try { return { operation: input.operation, arguments: normalizeBrowserOperation(input.operation, input.arguments) }; }
|
|
@@ -144,7 +168,7 @@ function safeText(value, max = 1000, multiline = false) {
|
|
|
144
168
|
}
|
|
145
169
|
function statusResponse(value, expectedId, { create, origin, loopback }) {
|
|
146
170
|
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'] : [])],
|
|
171
|
+
'fill_after_verification', 'reconciliation', 'final_click_actor', 'submit_actor', 'retry_safe', 'payment_confirmed', ...(create ? ['wallet_url'] : [])],
|
|
148
172
|
['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], 'invalid_response');
|
|
149
173
|
check(typeof value.id === 'string' && ID.test(value.id) && (!expectedId || value.id === expectedId) && STATUSES.has(value.status) &&
|
|
150
174
|
value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
|
|
@@ -152,10 +176,19 @@ function statusResponse(value, expectedId, { create, origin, loopback }) {
|
|
|
152
176
|
if (value.purpose !== undefined || value.submit_allowed !== undefined) check(
|
|
153
177
|
['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(value.purpose) &&
|
|
154
178
|
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
|
|
179
|
+
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');
|
|
156
180
|
if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' &&
|
|
157
181
|
['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
|
|
158
182
|
if (value.submit_expires_at !== undefined) timestamp(value.submit_expires_at);
|
|
183
|
+
// Only the customer's own backend/browser may ever perform the final click.
|
|
184
|
+
if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
|
|
185
|
+
if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' &&
|
|
186
|
+
['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
|
|
187
|
+
// An attested attempt names the customer; an unknown outcome cannot assert that a click happened.
|
|
188
|
+
if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
|
|
189
|
+
if (value.reconciliation !== undefined) {
|
|
190
|
+
try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { fail('invalid_response'); }
|
|
191
|
+
}
|
|
159
192
|
if (value.diagnostics !== undefined) {
|
|
160
193
|
try { validateCheckoutDiagnostics(value.diagnostics, value); } catch { fail('invalid_response'); }
|
|
161
194
|
}
|
|
@@ -210,7 +243,10 @@ function creationInspectionInput(input) {
|
|
|
210
243
|
for (const name of ['customer_id', 'workspace_id', 'conversation_id'])
|
|
211
244
|
check(typeof input[name] === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input[name]));
|
|
212
245
|
string(input.idempotency_key, 200);
|
|
213
|
-
keys(input.browser, ['provider', 'browser_id', 'custody_mode']);
|
|
246
|
+
keys(input.browser, ['provider', 'browser_id', 'custody_mode', 'provenance'], ['provider', 'browser_id', 'custody_mode']);
|
|
247
|
+
if (Object.hasOwn(input.browser, 'provenance')) {
|
|
248
|
+
try { browserUseRunProvenance(input.browser.provenance); } catch { check(false); }
|
|
249
|
+
}
|
|
214
250
|
check(input.browser.provider === 'browser_use' && typeof input.browser.browser_id === 'string' &&
|
|
215
251
|
UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase() &&
|
|
216
252
|
input.browser.custody_mode === 'customer_backend_trusted');
|
|
@@ -246,6 +282,16 @@ function retirementInspectionResponse(value, id, input) {
|
|
|
246
282
|
value.retirement.recording_disabled === true, 'invalid_response');
|
|
247
283
|
return value;
|
|
248
284
|
}
|
|
285
|
+
const ARM_ID = /^[\x21-\x7e]{20,16000}$/u;
|
|
286
|
+
function armId(value, code = 'invalid_input') { check(typeof value === 'string' && ARM_ID.test(value), code); return value; }
|
|
287
|
+
function finalClickArmResponse(value, input) {
|
|
288
|
+
keys(value, ['arm_id', 'armed_expires_at', 'target', 'retry_safe', 'payment_confirmed'], undefined, 'invalid_response');
|
|
289
|
+
armId(value.arm_id, 'invalid_response'); timestamp(value.armed_expires_at);
|
|
290
|
+
check(value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
|
|
291
|
+
keys(value.target, ['checkout_url', 'submit_selector'], undefined, 'invalid_response');
|
|
292
|
+
try { merchantUrl(value.target.checkout_url); string(value.target.submit_selector, 200); } catch { fail('invalid_response'); }
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
249
295
|
function connectionResponse(value, merchantId) {
|
|
250
296
|
keys(value, ['status', 'api_version', 'merchant_id', 'interaction_modes', 'currencies'], undefined, 'invalid_response');
|
|
251
297
|
check(value.status === 'connected' && value.api_version === 'v1' && value.merchant_id === merchantId &&
|
|
@@ -340,8 +386,24 @@ export class AccelerateClient {
|
|
|
340
386
|
return result;
|
|
341
387
|
},
|
|
342
388
|
fill: async (id, input, options) => this.#request('POST', checkoutId(id), '/fill', revisionInput(input), options),
|
|
343
|
-
|
|
389
|
+
// The old executor-click endpoint is permanently closed: Accelerate never sends the final click.
|
|
390
|
+
submit: async () => fail('customer_click_required'),
|
|
391
|
+
/** Backend only. The returned target is never model/MCP output. */
|
|
392
|
+
armFinalClick: async (id, input, options) => this.#request('POST', checkoutId(id), '/final-click/arm', revisionInput(input), options),
|
|
393
|
+
completeFinalClick: async (id, input, options) => {
|
|
394
|
+
keys(input, ['revision', 'arm_id']); integer(input.revision, 1, 2147483647); armId(input.arm_id);
|
|
395
|
+
const result = await this.#request('POST', checkoutId(id), '/final-click/complete', { revision: input.revision, arm_id: input.arm_id }, options);
|
|
396
|
+
check(['submit_attempted', 'outcome_unknown'].includes(result.status) && result.purpose === 'purchase' &&
|
|
397
|
+
result.final_click_actor === 'customer' && (result.status === 'outcome_unknown' || result.submit_actor === 'customer'), 'invalid_response');
|
|
398
|
+
return result;
|
|
399
|
+
},
|
|
344
400
|
cancel: async (id, options) => this.#request('POST', checkoutId(id), '/cancel', {}, options),
|
|
401
|
+
reconcile: async (id, input, options) => {
|
|
402
|
+
const body = reconciliationInput(input);
|
|
403
|
+
const result = await this.#request('POST', checkoutId(id), '/reconcile', body, options);
|
|
404
|
+
check(result.reconciliation?.state === body.state && result.reconciliation.source === body.source, 'invalid_response');
|
|
405
|
+
return result;
|
|
406
|
+
},
|
|
345
407
|
});
|
|
346
408
|
}
|
|
347
409
|
async #waitFor(id, input, options = {}) {
|
|
@@ -373,7 +435,7 @@ export class AccelerateClient {
|
|
|
373
435
|
} finally { clearTimeout(timer); }
|
|
374
436
|
}
|
|
375
437
|
#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'],
|
|
438
|
+
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'],
|
|
377
439
|
['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key']);
|
|
378
440
|
check(typeof input.customer_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.customer_id));
|
|
379
441
|
check(typeof input.workspace_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.workspace_id));
|
|
@@ -386,9 +448,17 @@ export class AccelerateClient {
|
|
|
386
448
|
check(input.approval_policy !== 'after_fill' || input.interaction_mode === 'chat' && input.purpose !== 'sandbox_source_comparison');
|
|
387
449
|
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
450
|
check(input.purpose !== 'sandbox_fill_only' || input.interaction_mode === 'chat');
|
|
451
|
+
// Purchases are never clicked by Accelerate. They require the customer's own trusted browser and final-click actor.
|
|
452
|
+
check(input.final_click_actor === undefined || input.final_click_actor === 'customer' && (input.purpose ?? 'purchase') === 'purchase');
|
|
453
|
+
if ((input.purpose ?? 'purchase') === 'purchase') check(input.final_click_actor === 'customer' && input.interaction_mode === 'chat' &&
|
|
454
|
+
input.approval_policy === 'after_fill' && input.browser?.custody_mode === 'customer_backend_trusted');
|
|
389
455
|
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
456
|
if (input.browser !== undefined) {
|
|
391
|
-
keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]
|
|
457
|
+
keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : ['provenance'])],
|
|
458
|
+
['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]);
|
|
459
|
+
if (Object.hasOwn(input.browser, 'provenance')) {
|
|
460
|
+
try { browserUseRunProvenance(input.browser.provenance); } catch { check(false); }
|
|
461
|
+
}
|
|
392
462
|
const supportedBinding = input.browser.provider === 'browser_use'
|
|
393
463
|
? typeof input.browser.browser_id === 'string' && UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase()
|
|
394
464
|
: input.browser.provider === 'connector' && typeof input.browser.connector_id === 'string' && /^[a-z][a-z0-9_-]{0,63}$/u.test(input.browser.connector_id) &&
|
|
@@ -401,7 +471,8 @@ export class AccelerateClient {
|
|
|
401
471
|
check(input.purpose !== 'sandbox_fill_only' || result.purpose === 'sandbox_fill_only' && result.submit_allowed === false, 'invalid_response');
|
|
402
472
|
check(input.approval_policy !== 'after_fill' || result.approval_policy === 'after_fill' && result.purchase_approved === false, 'invalid_response');
|
|
403
473
|
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');
|
|
474
|
+
check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response');
|
|
475
|
+
check(input.final_click_actor !== 'customer' || result.final_click_actor === 'customer' && result.purpose === 'purchase', 'invalid_response'); return result;
|
|
405
476
|
});
|
|
406
477
|
}
|
|
407
478
|
async #request(method, id, suffix, body, options = {}, connection = false) {
|
|
@@ -409,7 +480,7 @@ export class AccelerateClient {
|
|
|
409
480
|
check(options.signal === undefined || options.signal instanceof AbortSignal);
|
|
410
481
|
const controller = new AbortController();
|
|
411
482
|
const milliseconds = connection ? 45000 : ['/inspect-creation', '/inspect-retirement'].includes(suffix) ? 60000 : id === null ? 245000 : ({ '/fill': 235000, '/details': 125000, '/verify-quote': 125000, '/approval': 125000,
|
|
412
|
-
'/
|
|
483
|
+
'/final-click/arm': 20000, '/final-click/complete': 125000, '/browser': 85000, '/cancel': 45000, '/reconcile': 45000, '': 45000 })[suffix];
|
|
413
484
|
const timer = setTimeout(() => controller.abort(), milliseconds); timer.unref?.();
|
|
414
485
|
const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
|
|
415
486
|
const url = connection ? `${this.#origin}/v1/connection` :
|
|
@@ -429,7 +500,7 @@ export class AccelerateClient {
|
|
|
429
500
|
if (!response.ok) throw new AccelerateError(ERROR_CODES.has(value?.error?.code) ? value.error.code : 'request_failed', response.status,
|
|
430
501
|
response.headers.get('x-accelerate-support-reference'));
|
|
431
502
|
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, {
|
|
503
|
+
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, {
|
|
433
504
|
create: id === null, origin: this.#origin, loopback: this.#loopback });
|
|
434
505
|
} catch (error) {
|
|
435
506
|
Promise.resolve(response?.body?.cancel()).catch(() => {});
|
package/managed-mcp.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccelerateClient, AccelerateNextAction, Checkout, CheckoutId, CustomerBrowserBinding } from './index.js';
|
|
1
|
+
import type { AccelerateClient, AccelerateNextAction, Checkout, CheckoutId, ClientBrowserResource, CustomerBrowserBinding } from './index.js';
|
|
2
2
|
import type { BrowserResult } from './browser-operations.js';
|
|
3
3
|
import type { BrowserConnector } from './browser-connector.js';
|
|
4
4
|
|
|
@@ -17,6 +17,8 @@ export interface ManagedCheckoutCreation {
|
|
|
17
17
|
/** Optional existing browser from the host's owned workflow, never MCP input.
|
|
18
18
|
* Only permitted with browserMode:'customer_backend_trusted'. */
|
|
19
19
|
browser?: CustomerBrowserBinding;
|
|
20
|
+
/** Your own provider read of `browser`, so Accelerate never needs your provider API key. */
|
|
21
|
+
browser_resource?: ClientBrowserResource;
|
|
20
22
|
/** Resolve only after durable storage. Retain an uncertainty fence if acknowledgement is lost. */
|
|
21
23
|
persistCreated(binding: Readonly<ManagedCheckoutBinding>): Promise<void>;
|
|
22
24
|
}
|
|
@@ -108,9 +110,13 @@ export interface ManagedCheckoutToolsOptions<Extra = unknown> {
|
|
|
108
110
|
* checkout ID/revision; the existing owner record and uncertainty fence remain.
|
|
109
111
|
*/
|
|
110
112
|
dispatchCheckoutEvent(context: Readonly<ManagedCheckoutContext>, event: Readonly<ManagedCheckoutEvent>): Promise<void>;
|
|
111
|
-
/** Immutable for newly created checkouts. Default sandbox_fill_only.
|
|
113
|
+
/** Immutable for newly created checkouts. Default sandbox_fill_only. Purchase requires browserMode
|
|
114
|
+
* customer_backend_trusted and a browserConnector whose host implements performFinalClick: Accelerate
|
|
115
|
+
* never sends the final click, and Accelerate-managed private browsers cannot purchase. */
|
|
112
116
|
purpose?: 'sandbox_fill_only' | 'purchase';
|
|
113
|
-
/** Default false. Registers final-submit tool only when true and purpose is purchase.
|
|
117
|
+
/** Default false. Registers the final-submit tool only when true and purpose is purchase. That tool
|
|
118
|
+
* arms one short click, calls browserConnector.performFinalClick once in the customer's browser, then
|
|
119
|
+
* completes once to observe. Its result is a safe Checkout; the arm token and target never leave the backend. */
|
|
114
120
|
enableSubmit?: boolean;
|
|
115
121
|
/** Default managed_review_v1: verification at payment and approval after fill.
|
|
116
122
|
* Legacy before-fill behavior requires this explicit profile or approvalPolicy:'before_fill'. */
|