@weaccelerateinc/agent-sdk 0.10.3 → 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 +9 -7
- package/approved-extension-releases/0.0.0.1.json +3 -1
- package/approved-extension-releases/0.0.0.2.json +3 -1
- package/approved-extension.js +1 -1
- package/browser-connector.d.ts +22 -0
- package/browser-connector.js +41 -2
- package/browser-use-host.d.ts +3 -0
- package/browser-use-host.js +2 -0
- package/checkout-diagnostics.d.ts +13 -2
- package/checkout-diagnostics.js +18 -4
- package/cli.js +4 -1
- package/host-adapter.js +9 -5
- package/index.d.ts +48 -4
- package/index.js +74 -11
- package/managed-mcp.d.ts +9 -3
- package/managed-mcp.js +51 -6
- package/package.json +1 -1
- 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/private-chat.js +10 -3
- 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/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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 { validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
4
|
+
import { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
|
|
5
5
|
export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
6
6
|
|
|
7
7
|
const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
|
|
8
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
9
|
const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved',
|
|
10
|
-
'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
10
|
+
'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
11
11
|
const WAIT_STOPS = new Set(['ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
12
12
|
const HOSTED_SERVICE = 'https://accelerate-agent-sdk.vercel.app';
|
|
13
13
|
const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied', 'not_found', 'conflict',
|
|
@@ -15,12 +15,13 @@ const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied
|
|
|
15
15
|
'operation_in_progress', 'fill_continuation_required', 'review_unavailable', 'capability_disabled', 'checkout_unsupported', 'integration_unavailable', 'outcome_unknown',
|
|
16
16
|
'authentication_required', 'checkout_closed', 'checkout_frozen', 'upstream_unavailable', 'wallet_required',
|
|
17
17
|
'revision_conflict', 'address_changed', 'details_required', 'quote_changed', 'submit_unavailable',
|
|
18
|
-
'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed',
|
|
18
|
+
'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed', 'verification_pending',
|
|
19
19
|
'verification_consumed', 'handoff_consumed', 'wallet_refreshing', 'consent_required',
|
|
20
20
|
'idempotency_conflict', 'operation_conflict', 'checkout_not_found', 'tenant_unauthorized',
|
|
21
21
|
'checkout_busy', 'store_unavailable', 'store_integrity_error', 'selection_rejected', 'upstream_invalid',
|
|
22
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'
|
|
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']);
|
|
24
25
|
const MESSAGES = {
|
|
25
26
|
invalid_input: 'Invalid Accelerate client request.',
|
|
26
27
|
invalid_response: 'Accelerate returned an unrecognized private response.',
|
|
@@ -41,6 +42,8 @@ for (const [codes, message, nextAction] of [
|
|
|
41
42
|
'Check the server API key, merchant ID and enabled Accelerate capabilities.', 'check_configuration'],
|
|
42
43
|
[['invalid_input', 'invalid_json', 'json_required', 'request_too_large', 'method_not_allowed', 'route_not_found', 'upstream_request_invalid'],
|
|
43
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'],
|
|
44
47
|
[['operation_in_progress', 'operation_conflict', 'checkout_busy', 'rate_limited', 'wallet_refreshing', 'conflict'],
|
|
45
48
|
'Read checkout status before continuing. Do not repeat an uncertain payment action.', 'check_status'],
|
|
46
49
|
[['wallet_required', 'authentication_required', 'authentication_completed', 'selection_rejected'],
|
|
@@ -62,6 +65,16 @@ for (const [codes, message, nextAction] of [
|
|
|
62
65
|
'Reconcile checkout status and the merchant order. Do not repeat the payment action.', 'reconcile_order'],
|
|
63
66
|
[['checkout_event_unconfirmed'],
|
|
64
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'],
|
|
65
78
|
]) for (const code of codes) GUIDANCE[code] = Object.freeze({ message, nextAction });
|
|
66
79
|
|
|
67
80
|
/** Safe diagnostics only: no response body, URL, credentials, cause or provider stack. */
|
|
@@ -115,6 +128,16 @@ function preparationText(value, max = 2000, code = 'invalid_input') {
|
|
|
115
128
|
!/\b(?:cvv|cvc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), code);
|
|
116
129
|
return value;
|
|
117
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
|
+
}
|
|
118
141
|
function browserInput(input) {
|
|
119
142
|
keys(input, ['operation', 'arguments']);
|
|
120
143
|
try { return { operation: input.operation, arguments: normalizeBrowserOperation(input.operation, input.arguments) }; }
|
|
@@ -144,7 +167,7 @@ function safeText(value, max = 1000, multiline = false) {
|
|
|
144
167
|
}
|
|
145
168
|
function statusResponse(value, expectedId, { create, origin, loopback }) {
|
|
146
169
|
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'] : [])],
|
|
170
|
+
'fill_after_verification', 'reconciliation', 'final_click_actor', 'submit_actor', 'retry_safe', 'payment_confirmed', ...(create ? ['wallet_url'] : [])],
|
|
148
171
|
['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], 'invalid_response');
|
|
149
172
|
check(typeof value.id === 'string' && ID.test(value.id) && (!expectedId || value.id === expectedId) && STATUSES.has(value.status) &&
|
|
150
173
|
value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
|
|
@@ -152,10 +175,19 @@ function statusResponse(value, expectedId, { create, origin, loopback }) {
|
|
|
152
175
|
if (value.purpose !== undefined || value.submit_allowed !== undefined) check(
|
|
153
176
|
['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(value.purpose) &&
|
|
154
177
|
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
|
|
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');
|
|
156
179
|
if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' &&
|
|
157
180
|
['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
|
|
158
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
|
+
}
|
|
159
191
|
if (value.diagnostics !== undefined) {
|
|
160
192
|
try { validateCheckoutDiagnostics(value.diagnostics, value); } catch { fail('invalid_response'); }
|
|
161
193
|
}
|
|
@@ -246,6 +278,16 @@ function retirementInspectionResponse(value, id, input) {
|
|
|
246
278
|
value.retirement.recording_disabled === true, 'invalid_response');
|
|
247
279
|
return value;
|
|
248
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
|
+
}
|
|
249
291
|
function connectionResponse(value, merchantId) {
|
|
250
292
|
keys(value, ['status', 'api_version', 'merchant_id', 'interaction_modes', 'currencies'], undefined, 'invalid_response');
|
|
251
293
|
check(value.status === 'connected' && value.api_version === 'v1' && value.merchant_id === merchantId &&
|
|
@@ -340,8 +382,24 @@ export class AccelerateClient {
|
|
|
340
382
|
return result;
|
|
341
383
|
},
|
|
342
384
|
fill: async (id, input, options) => this.#request('POST', checkoutId(id), '/fill', revisionInput(input), options),
|
|
343
|
-
|
|
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
|
+
},
|
|
344
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
|
+
},
|
|
345
403
|
});
|
|
346
404
|
}
|
|
347
405
|
async #waitFor(id, input, options = {}) {
|
|
@@ -373,7 +431,7 @@ export class AccelerateClient {
|
|
|
373
431
|
} finally { clearTimeout(timer); }
|
|
374
432
|
}
|
|
375
433
|
#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'],
|
|
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'],
|
|
377
435
|
['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key']);
|
|
378
436
|
check(typeof input.customer_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.customer_id));
|
|
379
437
|
check(typeof input.workspace_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.workspace_id));
|
|
@@ -386,6 +444,10 @@ export class AccelerateClient {
|
|
|
386
444
|
check(input.approval_policy !== 'after_fill' || input.interaction_mode === 'chat' && input.purpose !== 'sandbox_source_comparison');
|
|
387
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'));
|
|
388
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');
|
|
389
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);
|
|
390
452
|
if (input.browser !== undefined) {
|
|
391
453
|
keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]);
|
|
@@ -401,7 +463,8 @@ export class AccelerateClient {
|
|
|
401
463
|
check(input.purpose !== 'sandbox_fill_only' || result.purpose === 'sandbox_fill_only' && result.submit_allowed === false, 'invalid_response');
|
|
402
464
|
check(input.approval_policy !== 'after_fill' || result.approval_policy === 'after_fill' && result.purchase_approved === false, 'invalid_response');
|
|
403
465
|
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');
|
|
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;
|
|
405
468
|
});
|
|
406
469
|
}
|
|
407
470
|
async #request(method, id, suffix, body, options = {}, connection = false) {
|
|
@@ -409,7 +472,7 @@ export class AccelerateClient {
|
|
|
409
472
|
check(options.signal === undefined || options.signal instanceof AbortSignal);
|
|
410
473
|
const controller = new AbortController();
|
|
411
474
|
const milliseconds = connection ? 45000 : ['/inspect-creation', '/inspect-retirement'].includes(suffix) ? 60000 : id === null ? 245000 : ({ '/fill': 235000, '/details': 125000, '/verify-quote': 125000, '/approval': 125000,
|
|
412
|
-
'/
|
|
475
|
+
'/final-click/arm': 20000, '/final-click/complete': 125000, '/browser': 85000, '/cancel': 45000, '/reconcile': 45000, '': 45000 })[suffix];
|
|
413
476
|
const timer = setTimeout(() => controller.abort(), milliseconds); timer.unref?.();
|
|
414
477
|
const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
|
|
415
478
|
const url = connection ? `${this.#origin}/v1/connection` :
|
|
@@ -429,7 +492,7 @@ export class AccelerateClient {
|
|
|
429
492
|
if (!response.ok) throw new AccelerateError(ERROR_CODES.has(value?.error?.code) ? value.error.code : 'request_failed', response.status,
|
|
430
493
|
response.headers.get('x-accelerate-support-reference'));
|
|
431
494
|
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, {
|
|
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, {
|
|
433
496
|
create: id === null, origin: this.#origin, loopback: this.#loopback });
|
|
434
497
|
} catch (error) {
|
|
435
498
|
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'. */
|
package/managed-mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AccelerateClient, AccelerateError } from './index.js';
|
|
2
2
|
import { browserOperationJsonSchema, parseBrowserOperation, normalizeBrowserOperation } from './browser-operations.js';
|
|
3
|
-
import { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
3
|
+
import { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
|
|
6
6
|
const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
|
|
@@ -8,9 +8,12 @@ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/;
|
|
|
8
8
|
const PHONE = /^\+[1-9]\d{6,14}$/;
|
|
9
9
|
const CONTEXT_KEYS = ['customer_id', 'workspace_id', 'conversation_id', 'phone_number'];
|
|
10
10
|
const STATUSES = ['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved',
|
|
11
|
-
'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown'];
|
|
11
|
+
'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown'];
|
|
12
12
|
const NEXT_ACTIONS = ['check_configuration', 'check_request', 'check_status', 'complete_wallet',
|
|
13
13
|
'complete_verification', 'refresh_quote', 'refresh_revision', 'reconcile_order', 'stop'];
|
|
14
|
+
// Errors that prove the final-click arm was not created. Everything else after an arm request is an unknown outcome.
|
|
15
|
+
const PRE_ARM_REFUSALS = new Set(['approval_required', 'revision_conflict', 'checkout_expired', 'checkout_closed', 'customer_click_required',
|
|
16
|
+
'permission_denied', 'submit_unavailable', 'capability_disabled']);
|
|
14
17
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
|
15
18
|
[Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
16
19
|
const check = (value, code = 'invalid_input') => { if (!value) throw new AccelerateError(code); };
|
|
@@ -59,6 +62,7 @@ const checkoutSchema = schemaObject({
|
|
|
59
62
|
expires_at: timestampSchema, purpose: { type: 'string', enum: ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'] },
|
|
60
63
|
approval_policy: { const: 'after_fill' }, purchase_approved: { type: 'boolean' },
|
|
61
64
|
fill_after_verification: { const: true },
|
|
65
|
+
final_click_actor: { const: 'customer' }, submit_actor: { const: 'customer' },
|
|
62
66
|
submit_allowed: { type: 'boolean' }, retry_safe: { const: false }, payment_confirmed: { const: false },
|
|
63
67
|
card: schemaObject({ last4: { anyOf: [{ type: 'string', pattern: '^\\d{4}$' }, { type: 'null' }] }, brand: nullableText(40) }),
|
|
64
68
|
address: schemaObject({ label: nullableText(400), city: nullableText(128), state: nullableText(128), postal_code_mask: nullableText(128) }, ['label']),
|
|
@@ -107,7 +111,7 @@ export const managedCheckoutToolDefinitions = freeze([
|
|
|
107
111
|
description: 'Cancel this exact owned checkout and request private browser cleanup. Cancellation does not reverse an attempted order or prove that no purchase occurred. Keep uncertain-payment fences until independently reconciled.',
|
|
108
112
|
inputSchema: idInput, outputSchema: outputSchema('checkout'), destructive: true },
|
|
109
113
|
{ name: 'accelerate_checkout_submit', title: 'Explicitly submit approved purchase',
|
|
110
|
-
description: 'Explicit final purchase decision for this exact owned revision while its returned ready claim remains valid.
|
|
114
|
+
description: 'Explicit final purchase decision for this exact owned revision while its returned ready claim remains valid. Accelerate never clicks: this arms one short-lived click, your own backend performs the single physical final click in the customer’s existing browser, then Accelerate observes the outcome once. Never automatically invoke after fill or retry after uncertainty. The default review profile requires the customer’s private post-fill approval first. submit_attempted is not order or payment confirmation. Available only when the server explicitly enables purchase submission.',
|
|
111
115
|
inputSchema: revisionInput, outputSchema: outputSchema('checkout'), destructive: true, submitOnly: true },
|
|
112
116
|
].map(value => ({ ...value, annotations: { readOnlyHint: value.readOnly === true, idempotentHint: value.readOnly === true,
|
|
113
117
|
destructiveHint: value.destructive === true, openWorldHint: true } })));
|
|
@@ -126,7 +130,7 @@ function safeText(value, max = 1000, multiline = false) {
|
|
|
126
130
|
function timestamp(value) { check(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value) && Number.isFinite(Date.parse(value)), 'invalid_response'); return value; }
|
|
127
131
|
function projectCheckout(value) {
|
|
128
132
|
shape(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'],
|
|
129
|
-
['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics'], 'invalid_response');
|
|
133
|
+
['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');
|
|
130
134
|
check(typeof value.id === 'string' && ID.test(value.id) && STATUSES.includes(value.status) && Number.isSafeInteger(value.revision) &&
|
|
131
135
|
value.revision >= 0 && value.revision <= 2147483647 && value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
|
|
132
136
|
const result = { id: value.id, status: value.status, revision: value.revision, expires_at: timestamp(value.expires_at), retry_safe: false, payment_confirmed: false };
|
|
@@ -135,7 +139,7 @@ function projectCheckout(value) {
|
|
|
135
139
|
result.purpose = value.purpose; result.submit_allowed = value.submit_allowed;
|
|
136
140
|
}
|
|
137
141
|
if (value.approval_policy !== undefined || value.purchase_approved !== undefined) {
|
|
138
|
-
check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean' && (!value.purchase_approved || value.status
|
|
142
|
+
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');
|
|
139
143
|
result.approval_policy = value.approval_policy; result.purchase_approved = value.purchase_approved;
|
|
140
144
|
}
|
|
141
145
|
if (value.fill_after_verification !== undefined) {
|
|
@@ -143,6 +147,17 @@ function projectCheckout(value) {
|
|
|
143
147
|
['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
|
|
144
148
|
result.fill_after_verification = true;
|
|
145
149
|
}
|
|
150
|
+
// The customer's own backend/browser performs any final click; Accelerate never does.
|
|
151
|
+
if (value.final_click_actor !== undefined) {
|
|
152
|
+
check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
|
|
153
|
+
result.final_click_actor = 'customer';
|
|
154
|
+
}
|
|
155
|
+
if (value.submit_actor !== undefined) {
|
|
156
|
+
check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' && ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
|
|
157
|
+
result.submit_actor = 'customer';
|
|
158
|
+
}
|
|
159
|
+
// An attested attempt names the customer; an unknown outcome cannot assert that a click happened.
|
|
160
|
+
if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
|
|
146
161
|
if (value.card !== undefined) {
|
|
147
162
|
shape(value.card, ['last4', 'brand'], [], 'invalid_response');
|
|
148
163
|
check(value.card.last4 === null || typeof value.card.last4 === 'string' && /^\d{4}$/u.test(value.card.last4), 'invalid_response');
|
|
@@ -164,6 +179,8 @@ function projectCheckout(value) {
|
|
|
164
179
|
result.quote = { checkout_origin: value.quote.checkout_origin, amount_cents: value.quote.amount_cents, currency: 'USD' };
|
|
165
180
|
}
|
|
166
181
|
if (value.submit_expires_at !== undefined) result.submit_expires_at = timestamp(value.submit_expires_at);
|
|
182
|
+
// A backend record: checked here so a reconciled checkout stays readable, and never projected to the agent.
|
|
183
|
+
if (value.reconciliation !== undefined) { try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { check(false, 'invalid_response'); } }
|
|
167
184
|
if (value.diagnostics !== undefined) result.diagnostics = JSON.parse(JSON.stringify(validateCheckoutDiagnostics(value.diagnostics, result)));
|
|
168
185
|
return result;
|
|
169
186
|
}
|
|
@@ -243,6 +260,9 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
243
260
|
const { verificationTiming, approvalPolicy, fillAfterVerification = false } = flowProfile({ profile, verificationTiming: requestedTiming, approvalPolicy: requestedApproval });
|
|
244
261
|
check(server && typeof server.registerTool === 'function' && client?.checkouts &&
|
|
245
262
|
['create', 'get', 'browser', 'applyDetails', 'requestApproval', 'fill', 'submit', 'cancel'].every(key => typeof client.checkouts[key] === 'function') &&
|
|
263
|
+
(!enableSubmit || ['armFinalClick', 'completeFinalClick'].every(key => typeof client.checkouts[key] === 'function')) &&
|
|
264
|
+
// Purchases are never clicked by Accelerate or by Accelerate's private Kernel browser: only the customer's own browser host can click.
|
|
265
|
+
(purpose !== 'purchase' || browserMode === 'customer_backend_trusted' && ['prepareFinalClick', 'performFinalClick'].every(key => typeof browserConnector?.[key] === 'function')) &&
|
|
246
266
|
[resolveContext, withOwnedCheckout, withCheckoutCreation, dispatchCheckoutEvent].every(value => typeof value === 'function') &&
|
|
247
267
|
['purchase', 'sandbox_fill_only'].includes(purpose) && typeof enableSubmit === 'boolean' && (!enableSubmit || purpose === 'purchase') &&
|
|
248
268
|
['on_start', 'on_payment'].includes(verificationTiming) && ['before_fill', 'after_fill'].includes(approvalPolicy) &&
|
|
@@ -251,6 +271,7 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
251
271
|
(browserConnector === undefined || browserMode === 'customer_backend_trusted' && browserConnector?.contract === 'accelerate_existing_browser_v1' &&
|
|
252
272
|
['prepareAttachment', 'commitAttachment', 'assertPaymentReady', 'confirmCleanup'].every(key => typeof browserConnector[key] === 'function')));
|
|
253
273
|
const requirePolicy = checkout => {
|
|
274
|
+
check(purpose !== 'purchase' || checkout.final_click_actor === 'customer', 'invalid_response');
|
|
254
275
|
check(approvalPolicy === 'after_fill' ? checkout.approval_policy === 'after_fill' && typeof checkout.purchase_approved === 'boolean'
|
|
255
276
|
: checkout.approval_policy === undefined && checkout.purchase_approved === undefined, 'invalid_response');
|
|
256
277
|
return checkout;
|
|
@@ -322,6 +343,7 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
322
343
|
creationStarted = true;
|
|
323
344
|
const created = await client.checkouts.create({ ...context, start_url: input.start_url,
|
|
324
345
|
idempotency_key: scope.idempotency_key, interaction_mode: 'chat', purpose,
|
|
346
|
+
...(purpose === 'purchase' ? { final_click_actor: 'customer' } : {}),
|
|
325
347
|
...(browserMode === 'customer_backend_trusted' ? { browser: { ...browser } } : {}),
|
|
326
348
|
...(attachment?.browser_resource ?? scope.browser_resource ? { browser_resource: attachment?.browser_resource ?? scope.browser_resource } : {}),
|
|
327
349
|
...(approvalPolicy === 'after_fill' ? { approval_policy: approvalPolicy } : {}),
|
|
@@ -350,7 +372,30 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
350
372
|
value = await client.checkouts.requestApproval(id, { ...approval, ...(fillAfterVerification ? { fill_after_verification: true } : {}) }, options);
|
|
351
373
|
check(!fillAfterVerification || value.fill_after_verification === true, 'invalid_response');
|
|
352
374
|
} else if (definition.name === 'accelerate_checkout_fill') value = await client.checkouts.fill(id, { revision: input.revision }, options);
|
|
353
|
-
else if (definition.name === 'accelerate_checkout_submit') {
|
|
375
|
+
else if (definition.name === 'accelerate_checkout_submit') {
|
|
376
|
+
check(enableSubmit && purpose === 'purchase' && typeof browserConnector?.performFinalClick === 'function' && typeof browserConnector.prepareFinalClick === 'function', 'permission_denied');
|
|
377
|
+
// prepare -> arm -> customer-owned host click -> complete, exactly once. The arm token and target stay in this scope.
|
|
378
|
+
// Slow provider/extension readiness runs BEFORE the arm so it cannot consume the short click window.
|
|
379
|
+
const preparation = await browserConnector.prepareFinalClick(context, { checkout_id: id }, options); requireAdmission();
|
|
380
|
+
let armed;
|
|
381
|
+
try { armed = await client.checkouts.armFinalClick(id, { revision: input.revision }, options); }
|
|
382
|
+
catch (error) {
|
|
383
|
+
// Only an explicit refusal proves nothing was armed. Transport, schema, timeout and upstream failures may have burned the arm.
|
|
384
|
+
if (error instanceof AccelerateError && PRE_ARM_REFUSALS.has(error.code)) throw error;
|
|
385
|
+
throw new AccelerateError('outcome_unknown');
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
await browserConnector.performFinalClick(context, { checkout_id: id, preparation,
|
|
389
|
+
checkout_url: armed.target.checkout_url, submit_selector: armed.target.submit_selector,
|
|
390
|
+
armed_expires_at: armed.armed_expires_at }, options);
|
|
391
|
+
} catch { /* Host failure is not retried; the service observation below is the only truth. */ }
|
|
392
|
+
try { value = await client.checkouts.completeFinalClick(id, { revision: input.revision, arm_id: armed.arm_id }, options); }
|
|
393
|
+
catch { throw new AccelerateError('outcome_unknown'); }
|
|
394
|
+
// After a click attempt any unusable observation is an uncertain outcome, never a retryable failure.
|
|
395
|
+
try { requirePolicy(projectCheckout(value)); } catch { throw new AccelerateError('outcome_unknown'); }
|
|
396
|
+
check(['submit_attempted', 'outcome_unknown'].includes(value?.status) && value.final_click_actor === 'customer' &&
|
|
397
|
+
(value.status === 'outcome_unknown' || value.submit_actor === 'customer'), 'outcome_unknown');
|
|
398
|
+
}
|
|
354
399
|
else value = await client.checkouts.cancel(id, options);
|
|
355
400
|
requireAdmission();
|
|
356
401
|
const projected = kind === 'browser' ? projectBrowser(value) : requirePolicy(projectCheckout(value));
|
package/package.json
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
ACCELERATE_STARTER_MODE=synthetic
|
|
3
3
|
# Explicit new profile: private CVV continues the armed fill in the backend.
|
|
4
4
|
ACCELERATE_CHECKOUT_PROFILE=managed_review_v2
|
|
5
|
-
ACCELERATE_STARTER_PURPOSE=
|
|
6
|
-
ACCELERATE_ENABLE_PURCHASE_SUBMIT=
|
|
5
|
+
ACCELERATE_STARTER_PURPOSE=purchase
|
|
6
|
+
ACCELERATE_ENABLE_PURCHASE_SUBMIT=true
|
|
7
|
+
# For a fill-only test, set purpose to sandbox_fill_only and submit to false.
|
|
8
|
+
# This file is a template; export values or load it with your server environment.
|
|
7
9
|
# Live: operator-controlled absolute file exporting createPartnerDependencies().
|
|
8
10
|
# ACCELERATE_CONTEXT_MODULE=/absolute/protected/partner-host.mjs
|
|
9
11
|
# ACCELERATE_API_KEY=<tenant server key>
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Existing-browser payment starter
|
|
2
2
|
|
|
3
|
-
SDK
|
|
3
|
+
SDK 0.11.0, explicit `managed_review_v2`, `customer_backend_trusted`. Newly generated existing-browser apps default to `purpose:'purchase'` with the submit tool enabled. The matching service must support this profile; publishing the SDK does not deploy the service or qualify a merchant layout. Your platform creates its browser and installs the approved extension. This starter attaches that same browser at payment; it never creates a replacement. The older `accelerate-agent init <dir>` starter still selects Accelerate-managed Kernel custody and its fill-only default.
|
|
4
4
|
|
|
5
5
|
## Run without credentials
|
|
6
6
|
|
|
7
|
-
Once Accelerate supplies the matching verified 0.
|
|
7
|
+
Once Accelerate supplies the matching verified 0.11.0 archive, install it, then:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npx --no-install accelerate-agent init ./partner-demo --browser existing
|
|
11
11
|
cd partner-demo
|
|
12
|
-
npm install /secure/downloads/weaccelerateinc-agent-sdk-0.
|
|
12
|
+
npm install /secure/downloads/weaccelerateinc-agent-sdk-0.11.0.tgz
|
|
13
13
|
npm test
|
|
14
14
|
npm run smoke
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The synthetic fixture uses the actual connector, durable-host interface, private ingress/outbox and MCP registration. It simulates verification/provider operations; no real browser, message, credential or order is created. Its in-memory persistence and fake extension metadata are test fixtures only.
|
|
17
|
+
The synthetic fixture uses the actual connector, durable-host interface, private ingress/outbox and MCP registration. It simulates verification/provider operations; no real browser, message, credential or order is created. The smoke exercises default purchase mode, confirms submission is rejected before private approval, then cancels without approving or submitting. Its in-memory persistence and fake extension metadata are test fixtures only.
|
|
18
18
|
|
|
19
19
|
## Connect your platform
|
|
20
20
|
|
|
@@ -41,8 +41,21 @@ At create, v2 declares `fill_after_verification:true` and requires the service t
|
|
|
41
41
|
|
|
42
42
|
Before attachment, use ordinary customer details for required non-secret fields or ask for missing details. Leave card number/PAN, expiry and CVV empty; never obtain them through ordinary browser tools.
|
|
43
43
|
|
|
44
|
-
PAN displays stars + last four, CVV stars, expiry visible. Filled-page inspection is blocked for the agent. Provider-owner backend access remains trusted.
|
|
44
|
+
PAN displays stars + last four, CVV stars, expiry visible. Filled-page inspection is blocked for the agent. Provider-owner backend access remains trusted. `universal_card_v1` is private runtime admission for the exact HTTPS checkout document and requested purpose, including both purchase and fill-only. It detects compatible payment fields and safe review support across merchants; unsupported or ambiguous forms fail before OTP/CVV without a replacement browser. It requires a matching service/executor deployment. The approved extension's original manifest records demo fill/review/cancel evidence; the [2 November sandbox report](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/qualification/REPORT.md) includes additional real merchant fill/review runs, with no real-store order submission or production qualification. The universal path reuses the approved 0.0.0.1 extension; it does not require candidate 0.0.0.2.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
## Purchase and fill-only configuration
|
|
47
|
+
|
|
48
|
+
The new starter defaults are:
|
|
49
|
+
|
|
50
|
+
```dotenv
|
|
51
|
+
ACCELERATE_STARTER_PURPOSE=purchase
|
|
52
|
+
ACCELERATE_ENABLE_PURCHASE_SUBMIT=true
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Purchase mode exposes `accelerate_checkout_submit`. Accelerate never sends the final click: that tool arms one short click, your `createPartnerDependencies` browser connector host must implement `performFinalClick` to click once in the customer's existing browser, then Accelerate observes the outcome. Registration fails without that host. The synthetic fixture simulates the arm, host click and completion; its smoke still cancels without a purchase decision, and the starter tests run the approved path. Private CVV, a delivered masked review, a fresh customer decision and a separate explicit submit for the matching revision are still required. A ready form or customer approval never submits automatically. The default `ACCELERATE_STARTER_MODE=synthetic` makes no external requests. `live` selects real backend dependencies, not the production wallet environment; the approved key, Merchant ID and service configuration select that environment.
|
|
56
|
+
|
|
57
|
+
For a no-order test, set `ACCELERATE_STARTER_PURPOSE=sandbox_fill_only` and remove the submit override or set `ACCELERATE_ENABLE_PURCHASE_SUBMIT=false`. If you copied `.env.example`, change both lines. An explicit true submit setting with fill-only purpose is rejected. After its informational review, cancel and verify cleanup. Purchase purpose with explicit submit false is also supported.
|
|
58
|
+
|
|
59
|
+
The starter does not load `.env` automatically; export the settings or load them through your server's environment configuration. This change affects future `init --browser existing` output after 0.11.0 is released. Published 0.10.3 and already generated apps retain their old files; configure their existing switches explicitly to enable purchase. Reusable SDK registration defaults remain fill-only. Existing checkout purpose is immutable, so an existing fill-only checkout cannot become a purchase. Uncertain outcomes retain fences and require reconciliation; status/cleanup never authorize a replacement by themselves.
|
|
47
60
|
|
|
48
61
|
Before live testing, confirm the session is executing the deployed v2 tool bundle. Updating a durable agent app does not necessarily replace an active session’s tools; settle its old work and use the framework’s supported idle deployment handoff or create a new authenticated session. Verify the quote response acknowledges `fill_after_verification:true`; a successful deployment alone is insufficient.
|
|
@@ -9,8 +9,9 @@ export async function configureStarter(server, env = process.env) {
|
|
|
9
9
|
const mode = env.ACCELERATE_STARTER_MODE ?? 'synthetic';
|
|
10
10
|
const profile = env.ACCELERATE_CHECKOUT_PROFILE ?? 'managed_review_v2';
|
|
11
11
|
if (!['managed_review_v1', 'managed_review_v2'].includes(profile)) throw Error('Unsupported partner checkout profile.');
|
|
12
|
-
const purpose = env.ACCELERATE_STARTER_PURPOSE ?? '
|
|
13
|
-
const enableSubmit = env.ACCELERATE_ENABLE_PURCHASE_SUBMIT ===
|
|
12
|
+
const purpose = env.ACCELERATE_STARTER_PURPOSE ?? 'purchase';
|
|
13
|
+
const enableSubmit = env.ACCELERATE_ENABLE_PURCHASE_SUBMIT === undefined
|
|
14
|
+
? purpose === 'purchase' : env.ACCELERATE_ENABLE_PURCHASE_SUBMIT === 'true';
|
|
14
15
|
if (!['sandbox_fill_only', 'purchase'].includes(purpose) ||
|
|
15
16
|
env.ACCELERATE_ENABLE_PURCHASE_SUBMIT !== undefined && !['true', 'false'].includes(env.ACCELERATE_ENABLE_PURCHASE_SUBMIT) ||
|
|
16
17
|
enableSubmit && purpose !== 'purchase') throw Error('Unsupported server purchase policy.');
|
|
@@ -12,9 +12,9 @@ const timer = setTimeout(() => { void transport.close(); }, 15000);
|
|
|
12
12
|
try {
|
|
13
13
|
await client.connect(transport);
|
|
14
14
|
const tools = await client.listTools();
|
|
15
|
-
assert.equal(tools.tools.length,
|
|
15
|
+
assert.equal(tools.tools.length, 8);
|
|
16
16
|
assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_request_payment'), true);
|
|
17
|
-
assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_submit'),
|
|
17
|
+
assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_submit'), true);
|
|
18
18
|
assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_fill'), false);
|
|
19
19
|
const call = async (name, args) => {
|
|
20
20
|
const result = await client.callTool({ name, arguments: args });
|
|
@@ -23,8 +23,9 @@ try {
|
|
|
23
23
|
};
|
|
24
24
|
const checkoutUrl = 'https://demos.weaccelerate.com/normal/payment';
|
|
25
25
|
const checkout = (await call('accelerate_checkout_start', { start_url: checkoutUrl })).checkout;
|
|
26
|
-
assert.equal(checkout.purpose, '
|
|
26
|
+
assert.equal(checkout.purpose, 'purchase'); assert.equal(checkout.submit_allowed, true);
|
|
27
27
|
assert.equal(checkout.approval_policy, 'after_fill'); assert.equal(checkout.purchase_approved, false);
|
|
28
|
+
assert.equal(checkout.final_click_actor, 'customer');
|
|
28
29
|
assert.equal(checkout.status, 'wallet_required'); assert.equal(checkout.card, undefined);
|
|
29
30
|
const id = checkout.id;
|
|
30
31
|
assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'wallet_required');
|
|
@@ -42,12 +43,16 @@ try {
|
|
|
42
43
|
assert.equal(reviewed.purchase_approved, false);
|
|
43
44
|
const remaining = Date.parse(reviewed.submit_expires_at) - Date.now();
|
|
44
45
|
assert.ok(remaining > 290000 && remaining <= 300000);
|
|
46
|
+
const rejected = await client.callTool({ name: 'accelerate_checkout_submit', arguments: { checkout_id: id, revision: reviewed.revision } });
|
|
47
|
+
const rejection = rejected.structuredContent ?? JSON.parse(rejected.content.find(part => part.type === 'text').text);
|
|
48
|
+
assert.equal(rejection.ok, false); assert.equal(rejection.error.code, 'submit_unavailable');
|
|
45
49
|
assert.equal((await call('accelerate_checkout_cancel', { checkout_id: id })).checkout.status, 'cancelled');
|
|
46
50
|
complete = true;
|
|
47
51
|
process.stdout.write(JSON.stringify({ ok: true, synthetic_only: true, mcp_protocol: true,
|
|
48
52
|
profile: 'managed_review_v2', existing_browser_connector: true, browser_mode: 'customer_backend_trusted', verification_deferred: true, private_verification_before_fill: true,
|
|
49
|
-
private_review_receipt_acknowledged: true, approval_window_ms: 300000, purchase_approval_requested:
|
|
50
|
-
|
|
53
|
+
private_review_receipt_acknowledged: true, approval_window_ms: 300000, purchase_approval_requested: true,
|
|
54
|
+
purpose: 'purchase', submit_tool_present: true, unapproved_submit_blocked: true, purchase_decision_received: false,
|
|
55
|
+
submit_attempted: false, final_click_actor: 'customer', manual_fill_tool_absent: true, fill_ready: true, cancelled: true, live_checkout_requests: 0 }) + '\n');
|
|
51
56
|
} catch {
|
|
52
57
|
process.stderr.write(JSON.stringify({ ok: false, code: 'synthetic_smoke_failed' }) + '\n');
|
|
53
58
|
} finally {
|