@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/managed-mcp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AccelerateClient, AccelerateError } from './index.js';
|
|
2
|
+
import { browserUseRunProvenance } from './browser-provenance.js';
|
|
2
3
|
import { browserOperationJsonSchema, parseBrowserOperation, normalizeBrowserOperation } from './browser-operations.js';
|
|
3
|
-
import { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
4
|
+
import { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
5
6
|
|
|
6
7
|
const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
|
|
@@ -8,9 +9,12 @@ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/;
|
|
|
8
9
|
const PHONE = /^\+[1-9]\d{6,14}$/;
|
|
9
10
|
const CONTEXT_KEYS = ['customer_id', 'workspace_id', 'conversation_id', 'phone_number'];
|
|
10
11
|
const STATUSES = ['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved',
|
|
11
|
-
'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown'];
|
|
12
|
+
'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown'];
|
|
12
13
|
const NEXT_ACTIONS = ['check_configuration', 'check_request', 'check_status', 'complete_wallet',
|
|
13
14
|
'complete_verification', 'refresh_quote', 'refresh_revision', 'reconcile_order', 'stop'];
|
|
15
|
+
// Errors that prove the final-click arm was not created. Everything else after an arm request is an unknown outcome.
|
|
16
|
+
const PRE_ARM_REFUSALS = new Set(['approval_required', 'revision_conflict', 'checkout_expired', 'checkout_closed', 'customer_click_required',
|
|
17
|
+
'permission_denied', 'submit_unavailable', 'capability_disabled']);
|
|
14
18
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
|
15
19
|
[Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
16
20
|
const check = (value, code = 'invalid_input') => { if (!value) throw new AccelerateError(code); };
|
|
@@ -59,6 +63,7 @@ const checkoutSchema = schemaObject({
|
|
|
59
63
|
expires_at: timestampSchema, purpose: { type: 'string', enum: ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'] },
|
|
60
64
|
approval_policy: { const: 'after_fill' }, purchase_approved: { type: 'boolean' },
|
|
61
65
|
fill_after_verification: { const: true },
|
|
66
|
+
final_click_actor: { const: 'customer' }, submit_actor: { const: 'customer' },
|
|
62
67
|
submit_allowed: { type: 'boolean' }, retry_safe: { const: false }, payment_confirmed: { const: false },
|
|
63
68
|
card: schemaObject({ last4: { anyOf: [{ type: 'string', pattern: '^\\d{4}$' }, { type: 'null' }] }, brand: nullableText(40) }),
|
|
64
69
|
address: schemaObject({ label: nullableText(400), city: nullableText(128), state: nullableText(128), postal_code_mask: nullableText(128) }, ['label']),
|
|
@@ -107,7 +112,7 @@ export const managedCheckoutToolDefinitions = freeze([
|
|
|
107
112
|
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
113
|
inputSchema: idInput, outputSchema: outputSchema('checkout'), destructive: true },
|
|
109
114
|
{ 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.
|
|
115
|
+
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
116
|
inputSchema: revisionInput, outputSchema: outputSchema('checkout'), destructive: true, submitOnly: true },
|
|
112
117
|
].map(value => ({ ...value, annotations: { readOnlyHint: value.readOnly === true, idempotentHint: value.readOnly === true,
|
|
113
118
|
destructiveHint: value.destructive === true, openWorldHint: true } })));
|
|
@@ -126,7 +131,7 @@ function safeText(value, max = 1000, multiline = false) {
|
|
|
126
131
|
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
132
|
function projectCheckout(value) {
|
|
128
133
|
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');
|
|
134
|
+
['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
135
|
check(typeof value.id === 'string' && ID.test(value.id) && STATUSES.includes(value.status) && Number.isSafeInteger(value.revision) &&
|
|
131
136
|
value.revision >= 0 && value.revision <= 2147483647 && value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
|
|
132
137
|
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 +140,7 @@ function projectCheckout(value) {
|
|
|
135
140
|
result.purpose = value.purpose; result.submit_allowed = value.submit_allowed;
|
|
136
141
|
}
|
|
137
142
|
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
|
|
143
|
+
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
144
|
result.approval_policy = value.approval_policy; result.purchase_approved = value.purchase_approved;
|
|
140
145
|
}
|
|
141
146
|
if (value.fill_after_verification !== undefined) {
|
|
@@ -143,6 +148,17 @@ function projectCheckout(value) {
|
|
|
143
148
|
['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
|
|
144
149
|
result.fill_after_verification = true;
|
|
145
150
|
}
|
|
151
|
+
// The customer's own backend/browser performs any final click; Accelerate never does.
|
|
152
|
+
if (value.final_click_actor !== undefined) {
|
|
153
|
+
check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
|
|
154
|
+
result.final_click_actor = 'customer';
|
|
155
|
+
}
|
|
156
|
+
if (value.submit_actor !== undefined) {
|
|
157
|
+
check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' && ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
|
|
158
|
+
result.submit_actor = 'customer';
|
|
159
|
+
}
|
|
160
|
+
// An attested attempt names the customer; an unknown outcome cannot assert that a click happened.
|
|
161
|
+
if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
|
|
146
162
|
if (value.card !== undefined) {
|
|
147
163
|
shape(value.card, ['last4', 'brand'], [], 'invalid_response');
|
|
148
164
|
check(value.card.last4 === null || typeof value.card.last4 === 'string' && /^\d{4}$/u.test(value.card.last4), 'invalid_response');
|
|
@@ -164,6 +180,8 @@ function projectCheckout(value) {
|
|
|
164
180
|
result.quote = { checkout_origin: value.quote.checkout_origin, amount_cents: value.quote.amount_cents, currency: 'USD' };
|
|
165
181
|
}
|
|
166
182
|
if (value.submit_expires_at !== undefined) result.submit_expires_at = timestamp(value.submit_expires_at);
|
|
183
|
+
// A backend record: checked here so a reconciled checkout stays readable, and never projected to the agent.
|
|
184
|
+
if (value.reconciliation !== undefined) { try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { check(false, 'invalid_response'); } }
|
|
167
185
|
if (value.diagnostics !== undefined) result.diagnostics = JSON.parse(JSON.stringify(validateCheckoutDiagnostics(value.diagnostics, result)));
|
|
168
186
|
return result;
|
|
169
187
|
}
|
|
@@ -243,6 +261,9 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
243
261
|
const { verificationTiming, approvalPolicy, fillAfterVerification = false } = flowProfile({ profile, verificationTiming: requestedTiming, approvalPolicy: requestedApproval });
|
|
244
262
|
check(server && typeof server.registerTool === 'function' && client?.checkouts &&
|
|
245
263
|
['create', 'get', 'browser', 'applyDetails', 'requestApproval', 'fill', 'submit', 'cancel'].every(key => typeof client.checkouts[key] === 'function') &&
|
|
264
|
+
(!enableSubmit || ['armFinalClick', 'completeFinalClick'].every(key => typeof client.checkouts[key] === 'function')) &&
|
|
265
|
+
// Purchases are never clicked by Accelerate or by Accelerate's private Kernel browser: only the customer's own browser host can click.
|
|
266
|
+
(purpose !== 'purchase' || browserMode === 'customer_backend_trusted' && ['prepareFinalClick', 'performFinalClick'].every(key => typeof browserConnector?.[key] === 'function')) &&
|
|
246
267
|
[resolveContext, withOwnedCheckout, withCheckoutCreation, dispatchCheckoutEvent].every(value => typeof value === 'function') &&
|
|
247
268
|
['purchase', 'sandbox_fill_only'].includes(purpose) && typeof enableSubmit === 'boolean' && (!enableSubmit || purpose === 'purchase') &&
|
|
248
269
|
['on_start', 'on_payment'].includes(verificationTiming) && ['before_fill', 'after_fill'].includes(approvalPolicy) &&
|
|
@@ -251,6 +272,7 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
251
272
|
(browserConnector === undefined || browserMode === 'customer_backend_trusted' && browserConnector?.contract === 'accelerate_existing_browser_v1' &&
|
|
252
273
|
['prepareAttachment', 'commitAttachment', 'assertPaymentReady', 'confirmCleanup'].every(key => typeof browserConnector[key] === 'function')));
|
|
253
274
|
const requirePolicy = checkout => {
|
|
275
|
+
check(purpose !== 'purchase' || checkout.final_click_actor === 'customer', 'invalid_response');
|
|
254
276
|
check(approvalPolicy === 'after_fill' ? checkout.approval_policy === 'after_fill' && typeof checkout.purchase_approved === 'boolean'
|
|
255
277
|
: checkout.approval_policy === undefined && checkout.purchase_approved === undefined, 'invalid_response');
|
|
256
278
|
return checkout;
|
|
@@ -312,7 +334,10 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
312
334
|
requireAdmission();
|
|
313
335
|
const browser = attachment ? attachment.browser : scope.browser;
|
|
314
336
|
if (browserMode === 'customer_backend_trusted') {
|
|
315
|
-
shape(browser, ['provider', 'browser_id', 'custody_mode'], browser?.provider === 'connector' ? ['connector_id'] : [], 'permission_denied');
|
|
337
|
+
shape(browser, ['provider', 'browser_id', 'custody_mode'], browser?.provider === 'connector' ? ['connector_id'] : ['provenance'], 'permission_denied');
|
|
338
|
+
if (browser.provenance !== undefined) {
|
|
339
|
+
try { browserUseRunProvenance(browser.provenance); } catch { check(false, 'permission_denied'); }
|
|
340
|
+
}
|
|
316
341
|
const valid = browser.provider === 'browser_use'
|
|
317
342
|
? typeof browser.browser_id === 'string' && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u.test(browser.browser_id)
|
|
318
343
|
: browser.provider === 'connector' && typeof browser.connector_id === 'string' && /^[a-z][a-z0-9_-]{0,63}$/u.test(browser.connector_id) &&
|
|
@@ -322,6 +347,7 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
322
347
|
creationStarted = true;
|
|
323
348
|
const created = await client.checkouts.create({ ...context, start_url: input.start_url,
|
|
324
349
|
idempotency_key: scope.idempotency_key, interaction_mode: 'chat', purpose,
|
|
350
|
+
...(purpose === 'purchase' ? { final_click_actor: 'customer' } : {}),
|
|
325
351
|
...(browserMode === 'customer_backend_trusted' ? { browser: { ...browser } } : {}),
|
|
326
352
|
...(attachment?.browser_resource ?? scope.browser_resource ? { browser_resource: attachment?.browser_resource ?? scope.browser_resource } : {}),
|
|
327
353
|
...(approvalPolicy === 'after_fill' ? { approval_policy: approvalPolicy } : {}),
|
|
@@ -350,7 +376,30 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
|
|
|
350
376
|
value = await client.checkouts.requestApproval(id, { ...approval, ...(fillAfterVerification ? { fill_after_verification: true } : {}) }, options);
|
|
351
377
|
check(!fillAfterVerification || value.fill_after_verification === true, 'invalid_response');
|
|
352
378
|
} 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') {
|
|
379
|
+
else if (definition.name === 'accelerate_checkout_submit') {
|
|
380
|
+
check(enableSubmit && purpose === 'purchase' && typeof browserConnector?.performFinalClick === 'function' && typeof browserConnector.prepareFinalClick === 'function', 'permission_denied');
|
|
381
|
+
// prepare -> arm -> customer-owned host click -> complete, exactly once. The arm token and target stay in this scope.
|
|
382
|
+
// Slow provider/extension readiness runs BEFORE the arm so it cannot consume the short click window.
|
|
383
|
+
const preparation = await browserConnector.prepareFinalClick(context, { checkout_id: id }, options); requireAdmission();
|
|
384
|
+
let armed;
|
|
385
|
+
try { armed = await client.checkouts.armFinalClick(id, { revision: input.revision }, options); }
|
|
386
|
+
catch (error) {
|
|
387
|
+
// Only an explicit refusal proves nothing was armed. Transport, schema, timeout and upstream failures may have burned the arm.
|
|
388
|
+
if (error instanceof AccelerateError && PRE_ARM_REFUSALS.has(error.code)) throw error;
|
|
389
|
+
throw new AccelerateError('outcome_unknown');
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
await browserConnector.performFinalClick(context, { checkout_id: id, preparation,
|
|
393
|
+
checkout_url: armed.target.checkout_url, submit_selector: armed.target.submit_selector,
|
|
394
|
+
armed_expires_at: armed.armed_expires_at }, options);
|
|
395
|
+
} catch { /* Host failure is not retried; the service observation below is the only truth. */ }
|
|
396
|
+
try { value = await client.checkouts.completeFinalClick(id, { revision: input.revision, arm_id: armed.arm_id }, options); }
|
|
397
|
+
catch { throw new AccelerateError('outcome_unknown'); }
|
|
398
|
+
// After a click attempt any unusable observation is an uncertain outcome, never a retryable failure.
|
|
399
|
+
try { requirePolicy(projectCheckout(value)); } catch { throw new AccelerateError('outcome_unknown'); }
|
|
400
|
+
check(['submit_attempted', 'outcome_unknown'].includes(value?.status) && value.final_click_actor === 'customer' &&
|
|
401
|
+
(value.status === 'outcome_unknown' || value.submit_actor === 'customer'), 'outcome_unknown');
|
|
402
|
+
}
|
|
354
403
|
else value = await client.checkouts.cancel(id, options);
|
|
355
404
|
requireAdmission();
|
|
356
405
|
const projected = kind === 'browser' ? projectBrowser(value) : requirePolicy(projectCheckout(value));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weaccelerateinc/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Server-only payment SDK, existing-browser connectors and private host adapters for agent platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -57,6 +57,18 @@
|
|
|
57
57
|
"./browser-use-host": {
|
|
58
58
|
"types": "./browser-use-host.d.ts",
|
|
59
59
|
"import": "./browser-use-host.js"
|
|
60
|
+
},
|
|
61
|
+
"./whatsapp-messaging": {
|
|
62
|
+
"types": "./whatsapp-messaging.d.ts",
|
|
63
|
+
"import": "./whatsapp-messaging.js"
|
|
64
|
+
},
|
|
65
|
+
"./whatsapp-journal": {
|
|
66
|
+
"types": "./whatsapp-journal.d.ts",
|
|
67
|
+
"import": "./whatsapp-journal.js"
|
|
68
|
+
},
|
|
69
|
+
"./postgres-whatsapp-journal": {
|
|
70
|
+
"types": "./postgres-whatsapp-journal.d.ts",
|
|
71
|
+
"import": "./postgres-whatsapp-journal.js"
|
|
60
72
|
}
|
|
61
73
|
},
|
|
62
74
|
"files": [
|
|
@@ -91,7 +103,14 @@
|
|
|
91
103
|
"approved-extension-releases",
|
|
92
104
|
"partner-starter",
|
|
93
105
|
"browser-use-host.js",
|
|
94
|
-
"browser-use-host.d.ts"
|
|
106
|
+
"browser-use-host.d.ts",
|
|
107
|
+
"whatsapp-messaging.js",
|
|
108
|
+
"whatsapp-messaging.d.ts",
|
|
109
|
+
"whatsapp-journal.js",
|
|
110
|
+
"whatsapp-journal.d.ts",
|
|
111
|
+
"browser-provenance.js",
|
|
112
|
+
"postgres-whatsapp-journal.js",
|
|
113
|
+
"postgres-whatsapp-journal.d.ts"
|
|
95
114
|
],
|
|
96
115
|
"engines": {
|
|
97
116
|
"node": ">=22"
|
|
@@ -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.12.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.12.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.12.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. The purchase default introduced in 0.11.0 applies to newly generated `init --browser existing` projects. 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 {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { createExistingBrowserConnector } from '@weaccelerateinc/agent-sdk/browser-connector';
|
|
3
3
|
import { AccelerateClient } from '@weaccelerateinc/agent-sdk';
|
|
4
4
|
import { AcceleratePrivateChatClient, deliverPrivateCheckoutReview } from '@weaccelerateinc/agent-sdk/private-chat';
|
|
@@ -18,7 +18,7 @@ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed
|
|
|
18
18
|
const records = new Map(), owners = new Map(), events = new Set(), messages = new Map();
|
|
19
19
|
const calls = { create: 0, browser: 0, payment_requested: 0, approval_requested: 0,
|
|
20
20
|
private_verification: 0, fill: 0, review: 0, review_sent: 0, review_acknowledged: 0,
|
|
21
|
-
purchase_decision: 0, submit: 0, cancel: 0, external: 0 };
|
|
21
|
+
purchase_decision: 0, submit: 0, final_click_arm: 0, final_click_host: 0, final_click_complete: 0, cancel: 0, external: 0 };
|
|
22
22
|
let tail = Promise.resolve();
|
|
23
23
|
const locked = async work => {
|
|
24
24
|
const previous = tail; let release;
|
|
@@ -29,6 +29,8 @@ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed
|
|
|
29
29
|
expires_at: record.expires_at, purpose, submit_allowed: purpose === 'purchase',
|
|
30
30
|
approval_policy: 'after_fill', purchase_approved: record.purchaseApproved === true,
|
|
31
31
|
retry_safe: false, payment_confirmed: false,
|
|
32
|
+
...(purpose === 'purchase' ? { final_click_actor: 'customer' } : {}),
|
|
33
|
+
...(record.submitActor ? { submit_actor: 'customer' } : {}),
|
|
32
34
|
...(record.fillAfterVerificationProfile ? { fill_after_verification: true } : {}),
|
|
33
35
|
...(record.selected ? { card: { brand: 'Visa', last4: '4242' }, address: { label: 'Saved address ***23' } } : {}),
|
|
34
36
|
...(record.quote ? { quote: record.quote } : {}),
|
|
@@ -55,6 +57,7 @@ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed
|
|
|
55
57
|
const body = request.body ? JSON.parse(request.body) : undefined;
|
|
56
58
|
if (parsed.pathname === '/v1/checkouts' && request.method === 'POST') {
|
|
57
59
|
if (body.interaction_mode !== 'chat' || body.purpose !== purpose || body.approval_policy !== 'after_fill' ||
|
|
60
|
+
(purpose === 'purchase') !== (body.final_click_actor === 'customer') || body.final_click_actor !== undefined && purpose !== 'purchase' ||
|
|
58
61
|
Object.keys(context).some(key => body[key] !== context[key]) || body.start_url !== syntheticCheckoutUrl ||
|
|
59
62
|
JSON.stringify(body.browser) !== JSON.stringify({ provider: 'connector', connector_id: 'synthetic_cdp', browser_id: 'existing:browser:1', custody_mode: 'customer_backend_trusted' })) invalid();
|
|
60
63
|
if ((profile === 'managed_review_v2') !== (body.fill_after_verification === true)) invalid();
|
|
@@ -63,7 +66,7 @@ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed
|
|
|
63
66
|
expires_at: new Date(Date.now() + 900000).toISOString(), selected: false, fillAfterVerificationProfile: body.fill_after_verification === true }); }
|
|
64
67
|
return Response.json(view(records.get(id)), { status: 201 });
|
|
65
68
|
}
|
|
66
|
-
const match = /^\/v1\/checkouts\/(mcs_[A-Za-z0-9_-]{43})(?:\/(browser|details|approval|fill|submit|cancel|private-chat\/(?:start|state|review|review-delivered|reply)))?$/.exec(parsed.pathname);
|
|
69
|
+
const match = /^\/v1\/checkouts\/(mcs_[A-Za-z0-9_-]{43})(?:\/(browser|details|approval|fill|submit|cancel|final-click\/(?:arm|complete)|private-chat\/(?:start|state|review|review-delivered|reply)))?$/.exec(parsed.pathname);
|
|
67
70
|
if (!match || !records.has(match[1])) invalid();
|
|
68
71
|
const record = records.get(match[1]), action = match[2];
|
|
69
72
|
if (!action && request.method === 'GET') return Response.json(view(record));
|
|
@@ -116,6 +119,7 @@ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed
|
|
|
116
119
|
body.amount_cents === 100 && body.currency === 'USD') {
|
|
117
120
|
if ((profile === 'managed_review_v2') !== (body.fill_after_verification === true)) invalid();
|
|
118
121
|
record.fillAfterVerification = body.fill_after_verification === true;
|
|
122
|
+
record.submitSelector = body.submit_selector;
|
|
119
123
|
record.status = 'approval_required'; record.stage = 'cvv_required'; record.revision++;
|
|
120
124
|
record.quote = { checkout_origin: 'https://demos.weaccelerate.com', amount_cents: 100, currency: 'USD' };
|
|
121
125
|
} else if (action === 'fill' && record.status === 'approved' && body.revision === record.revision) {
|
|
@@ -123,10 +127,24 @@ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed
|
|
|
123
127
|
fill(record);
|
|
124
128
|
} else if (action === 'cancel') { calls.cancel++; record.status = 'cancelled'; record.stage = 'closed'; record.purchaseApproved = false; delete record.review; }
|
|
125
129
|
else if (action === 'submit') {
|
|
130
|
+
// The executor click path is closed for every purchase. Counting it proves the fixture never uses it.
|
|
126
131
|
calls.submit++;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
return Response.json({ error: { code: 'customer_click_required' } }, { status: 403 });
|
|
133
|
+
} else if (action === 'final-click/arm') {
|
|
134
|
+
if (purpose !== 'purchase' || !record.purchaseApproved || record.stage !== 'approved_for_submit' || record.status !== 'ready_for_submit' ||
|
|
135
|
+
body.revision !== record.revision || Date.now() >= Date.parse(record.review.expires_at) || record.arm) return Response.json({ error: { code: 'submit_unavailable' } }, { status: 409 });
|
|
136
|
+
calls.final_click_arm++;
|
|
137
|
+
record.arm = { id: 'sealed.' + randomBytes(48).toString('base64url'), expires_at: new Date(Date.now() + 10000).toISOString() };
|
|
138
|
+
record.status = 'final_click_armed';
|
|
139
|
+
return Response.json({ arm_id: record.arm.id, armed_expires_at: record.arm.expires_at,
|
|
140
|
+
target: { checkout_url: syntheticCheckoutUrl, submit_selector: record.submitSelector }, retry_safe: false, payment_confirmed: false });
|
|
141
|
+
} else if (action === 'final-click/complete') {
|
|
142
|
+
if (record.status !== 'final_click_armed' || body.revision !== record.revision || body.arm_id !== record.arm?.id || Object.keys(body).length !== 2)
|
|
143
|
+
return Response.json({ error: { code: 'submit_unavailable' } }, { status: 409 });
|
|
144
|
+
calls.final_click_complete++;
|
|
145
|
+
// Observation only: the fixture saw a click only when the customer host performed one.
|
|
146
|
+
record.status = record.clicked ? 'submit_attempted' : 'outcome_unknown'; record.submitActor = record.clicked === true;
|
|
147
|
+
record.stage = 'closed'; record.purchaseApproved = false; delete record.review; delete record.arm;
|
|
130
148
|
} else invalid();
|
|
131
149
|
return Response.json(view(record));
|
|
132
150
|
};
|
|
@@ -180,6 +198,13 @@ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed
|
|
|
180
198
|
// this exact event for redelivery, without replaying private filling.
|
|
181
199
|
events.add(event.idempotency_key);
|
|
182
200
|
},
|
|
201
|
+
/** The customer's browser host: the only place a physical click is ever simulated. */
|
|
202
|
+
hostClick: request => {
|
|
203
|
+
const record = records.get(request.checkout_id);
|
|
204
|
+
if (!record?.arm || Date.now() >= Date.parse(record.arm.expires_at) || Date.parse(request.armed_expires_at) !== Date.parse(record.arm.expires_at) || record.clicked ||
|
|
205
|
+
request.submit_selector !== record.submitSelector || request.checkout_url !== syntheticCheckoutUrl) invalid();
|
|
206
|
+
calls.final_click_host++; record.clicked = true; return { dispatched: true };
|
|
207
|
+
},
|
|
183
208
|
counters: () => ({ ...calls }),
|
|
184
209
|
};
|
|
185
210
|
return host;
|
|
@@ -200,7 +225,7 @@ export function createSyntheticDependencies({ purpose = 'sandbox_fill_only', pro
|
|
|
200
225
|
artifact: { format: 'zip', sha256: 'a'.repeat(64), bytes: 1234 },
|
|
201
226
|
bridge: { protocol: 'accelerate_extension_bridge_v1', sha256: 'b'.repeat(64) },
|
|
202
227
|
capabilities: { manual_card_fill: true, document_bound_single_use: true, autosave_disabled: true, automatic_submit: false, maximum_retention_ms: 480000 },
|
|
203
|
-
compatibility: { sdk_versions: ['0.10.1', '0.10.2', '0.10.3'], executor_contract: 'accelerate_manual_checkout_v1' },
|
|
228
|
+
compatibility: { sdk_versions: ['0.10.1', '0.10.2', '0.10.3', '0.10.4', '0.11.0', '0.12.0'], executor_contract: 'accelerate_manual_checkout_v1' },
|
|
204
229
|
qualifications: [{ provider: 'connector', scope: 'synthetic_only', evidence: 'docs/evidence/synthetic-starter.json' }],
|
|
205
230
|
}; // Fake metadata for this in-process fixture only; never load it in live mode.
|
|
206
231
|
const browser = { owner: context, browser: { provider: 'connector', connector_id: 'synthetic_cdp', browser_id: 'existing:browser:1', custody_mode: 'customer_backend_trusted' },
|
|
@@ -215,6 +240,11 @@ export function createSyntheticDependencies({ purpose = 'sandbox_fill_only', pro
|
|
|
215
240
|
bindCheckout: async (_context, value) => { saved.set(value.checkout_id, value); },
|
|
216
241
|
resolveCheckoutBrowser: async (_context, { checkout_id }) => { const { checkout_id: _id, attempt_key: _key, ...value } = saved.get(checkout_id); return value; },
|
|
217
242
|
confirmCleanup: async () => ({ provider_browser_stopped: true, credentials_cleared: true, fence_retained: true }),
|
|
243
|
+
// Purchase only. The synthetic page is clicked by this customer-side host, never by Accelerate.
|
|
244
|
+
...(purpose === 'purchase' ? { performFinalClick: async (_context, request) => {
|
|
245
|
+
if (request.fence_id !== 'synthetic:fence:1' || request.browser.browser_id !== 'existing:browser:1') throw Error('Synthetic click rejected.');
|
|
246
|
+
return fixture.hostClick(request);
|
|
247
|
+
} } : {}),
|
|
218
248
|
} });
|
|
219
249
|
return { client: fixture.client, privateChat: fixture.privateChat, resolveContext: fixture.resolveContext,
|
|
220
250
|
persistence, browserConnector,
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { configureStarter } from '../runtime.mjs';
|
|
4
|
-
import { syntheticCheckoutUrl } from '../synthetic.mjs';
|
|
4
|
+
import { createSyntheticDependencies, syntheticCheckoutUrl } from '../synthetic.mjs';
|
|
5
|
+
import { registerPartnerCheckoutTools } from '@weaccelerateinc/agent-sdk/partner';
|
|
5
6
|
async function fixture(env = {}) {
|
|
6
7
|
const tools = new Map(); const integration = await configureStarter({ registerTool(name, _config, callback) { tools.set(name, callback); } }, env);
|
|
7
8
|
const call = async (name, args) => { const result = (await tools.get(name)(args, {})).structuredContent; assert.equal(result.ok, true, JSON.stringify(result)); return result; };
|
|
8
9
|
return { ...integration, tools, call };
|
|
9
10
|
}
|
|
10
|
-
test('explicit v2
|
|
11
|
+
test('explicit fill-only v2 verifies privately then fills and acknowledges review without a submit tool', async t => {
|
|
11
12
|
const old = globalThis.fetch; globalThis.fetch = () => assert.fail('No network in synthetic starter'); t.after(() => { globalThis.fetch = old; });
|
|
12
|
-
const f = await fixture(), checkout = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout;
|
|
13
|
+
const f = await fixture({ ACCELERATE_STARTER_PURPOSE: 'sandbox_fill_only' }), checkout = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout;
|
|
13
14
|
const id = checkout.id; assert.equal(checkout.fill_after_verification, true); assert.equal(f.counters().fill, 0); assert.equal(checkout.status, 'wallet_required'); assert.equal(f.counters().payment_requested, 0);
|
|
15
|
+
assert.equal(checkout.purpose, 'sandbox_fill_only'); assert.equal(checkout.submit_allowed, false);
|
|
14
16
|
assert.equal(f.tools.has('accelerate_checkout_submit'), false); assert.equal(f.tools.size, 7); assert.equal(f.tools.has('accelerate_checkout_fill'), false);
|
|
15
17
|
assert.equal(f.profile, 'managed_review_v2');
|
|
16
18
|
await f.call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
@@ -28,7 +30,7 @@ test('explicit v2 starter verifies privately then fills and acknowledges review
|
|
|
28
30
|
assert.equal(f.counters().submit, 0); assert.equal(f.counters().cancel, 1); assert.equal(f.counters().external, 0);
|
|
29
31
|
});
|
|
30
32
|
test('legacy v1 remains an explicit starter profile with one manual fill and no silent upgrade', async () => {
|
|
31
|
-
const f = await fixture({ ACCELERATE_CHECKOUT_PROFILE: 'managed_review_v1' });
|
|
33
|
+
const f = await fixture({ ACCELERATE_CHECKOUT_PROFILE: 'managed_review_v1', ACCELERATE_STARTER_PURPOSE: 'sandbox_fill_only' });
|
|
32
34
|
assert.equal(f.tools.has('accelerate_checkout_fill'), true); assert.equal(f.tools.size, 8);
|
|
33
35
|
const id = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout.id;
|
|
34
36
|
await f.call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
@@ -43,10 +45,15 @@ test('legacy v1 remains an explicit starter profile with one manual fill and no
|
|
|
43
45
|
assert.equal(f.counters().review_acknowledged, 1); assert.equal(f.counters().submit, 0);
|
|
44
46
|
await f.call('accelerate_checkout_cancel', { checkout_id: id });
|
|
45
47
|
});
|
|
46
|
-
test('v2 purchase still requires a fresh private decision and explicit submit after automatic fill', async
|
|
47
|
-
const
|
|
48
|
+
test('default v2 purchase still requires a fresh private decision and explicit submit after automatic fill', async t => {
|
|
49
|
+
const old = globalThis.fetch; globalThis.fetch = () => assert.fail('No network in synthetic starter'); t.after(() => { globalThis.fetch = old; });
|
|
50
|
+
const f = await fixture();
|
|
48
51
|
assert.equal(f.tools.has('accelerate_checkout_fill'), false); assert.equal(f.tools.has('accelerate_checkout_submit'), true);
|
|
49
|
-
|
|
52
|
+
assert.equal(f.tools.size, 8);
|
|
53
|
+
const created = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout;
|
|
54
|
+
assert.equal(created.purpose, 'purchase'); assert.equal(created.submit_allowed, true);
|
|
55
|
+
assert.equal(f.counters().payment_requested, 0);
|
|
56
|
+
const id = created.id;
|
|
50
57
|
await f.call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
51
58
|
assert.equal(f.counters().fill, 0);
|
|
52
59
|
await f.call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
|
|
@@ -60,10 +67,53 @@ test('v2 purchase still requires a fresh private decision and explicit submit af
|
|
|
60
67
|
const rejected = (await f.tools.get('accelerate_checkout_submit')({ checkout_id: id, revision: quote.revision }, {})).structuredContent;
|
|
61
68
|
assert.equal(rejected.ok, false); assert.equal(f.counters().submit, 0);
|
|
62
69
|
await f.call('accelerate_checkout_cancel', { checkout_id: id });
|
|
70
|
+
assert.equal(f.counters().external, 0);
|
|
71
|
+
});
|
|
72
|
+
test('purchase submission can be explicitly disabled without changing purchase purpose', async () => {
|
|
73
|
+
const f = await fixture({ ACCELERATE_ENABLE_PURCHASE_SUBMIT: 'false' });
|
|
74
|
+
const created = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout;
|
|
75
|
+
assert.equal(created.purpose, 'purchase'); assert.equal(created.submit_allowed, true);
|
|
76
|
+
assert.equal(f.tools.has('accelerate_checkout_submit'), false);
|
|
77
|
+
await f.call('accelerate_checkout_cancel', { checkout_id: created.id });
|
|
78
|
+
assert.equal(f.counters().submit, 0);
|
|
63
79
|
});
|
|
64
80
|
test('live setup requires a trusted backend module and preserves explicit custody selection', async () => {
|
|
65
81
|
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_STARTER_MODE: 'live' }), /trusted absolute/);
|
|
66
82
|
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_STARTER_MODE: 'unknown' }), /Unsupported/);
|
|
67
83
|
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_CHECKOUT_PROFILE: 'unknown' }), /profile/);
|
|
68
|
-
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_ENABLE_PURCHASE_SUBMIT: 'true' }), /purchase policy/);
|
|
84
|
+
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_STARTER_PURPOSE: 'sandbox_fill_only', ACCELERATE_ENABLE_PURCHASE_SUBMIT: 'true' }), /purchase policy/);
|
|
85
|
+
for (const value of ['', 'TRUE', '1'])
|
|
86
|
+
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_ENABLE_PURCHASE_SUBMIT: value }), /purchase policy/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('approved synthetic purchase: arm, one customer-host click, complete; the old executor submit is never used', async t => {
|
|
90
|
+
const clock = Date.now; let now = clock(); Date.now = () => now; t.after(() => { Date.now = clock; });
|
|
91
|
+
const tools = new Map(), deps = createSyntheticDependencies({ purpose: 'purchase', profile: 'managed_review_v2' });
|
|
92
|
+
const context = await deps.resolveContext(); let reply;
|
|
93
|
+
deps.authenticate = async () => ({ authenticated: true, direct: true, context, ...reply });
|
|
94
|
+
const integration = registerPartnerCheckoutTools({ registerTool(name, _config, callback) { tools.set(name, callback); } },
|
|
95
|
+
{ ...deps, purpose: 'purchase', enableSubmit: true, profile: 'managed_review_v2' });
|
|
96
|
+
const invoke = async (name, args) => (await tools.get(name)(args, {})).structuredContent;
|
|
97
|
+
const call = async (name, args) => { const result = await invoke(name, args); assert.equal(result.ok, true, JSON.stringify(result)); return result; };
|
|
98
|
+
const created = (await call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout, id = created.id;
|
|
99
|
+
assert.equal(created.final_click_actor, 'customer');
|
|
100
|
+
await call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
101
|
+
await call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
|
|
102
|
+
const quote = (await call('accelerate_checkout_request_approval', { checkout_id: id, checkout_url: syntheticCheckoutUrl, amount_cents: 100, currency: 'USD',
|
|
103
|
+
checkout_total_selector: '#checkout-total', submit_selector: '#place-order' })).checkout;
|
|
104
|
+
await call('accelerate_checkout_status', { checkout_id: id });
|
|
105
|
+
assert.equal((await invoke('accelerate_checkout_submit', { checkout_id: id, revision: quote.revision })).error.code, 'submit_unavailable');
|
|
106
|
+
assert.equal(deps.counters().final_click_arm, 0);
|
|
107
|
+
now++; // The decision must follow the delivered review receipt.
|
|
108
|
+
reply = { message_id: 'synthetic-private-decision', text: 'approve', sent_at: new Date(now).toISOString() };
|
|
109
|
+
assert.deepEqual(await integration.privateIngress.handle({}, Buffer.from('synthetic provider envelope')), { handled: true });
|
|
110
|
+
const done = (await call('accelerate_checkout_submit', { checkout_id: id, revision: quote.revision })).checkout;
|
|
111
|
+
assert.equal(done.status, 'submit_attempted'); assert.equal(done.submit_actor, 'customer'); assert.equal(done.payment_confirmed, false);
|
|
112
|
+
const counters = deps.counters();
|
|
113
|
+
assert.deepEqual([counters.final_click_arm, counters.final_click_host, counters.final_click_complete, counters.submit, counters.external], [1, 1, 1, 0, 0]);
|
|
114
|
+
});
|
|
115
|
+
test('purchase registration needs a host that performs the customer click; fill-only does not', () => {
|
|
116
|
+
const fillOnly = createSyntheticDependencies({ purpose: 'sandbox_fill_only' });
|
|
117
|
+
assert.throws(() => registerPartnerCheckoutTools({ registerTool() {} }, { ...fillOnly, purpose: 'purchase', enableSubmit: true }), /performFinalClick/);
|
|
118
|
+
registerPartnerCheckoutTools({ registerTool() {} }, { ...fillOnly, purpose: 'sandbox_fill_only', enableSubmit: false });
|
|
69
119
|
});
|
|
@@ -11,6 +11,9 @@ export async function createPartnerDependencies({ client, privateChat }) {
|
|
|
11
11
|
// browserConnector: createBrowserUseConnector({ extensionRelease,
|
|
12
12
|
// host: createBrowserUseHost({apiKey,merchantId,extensionId,extensionRelease,host}) }) OR
|
|
13
13
|
// createExistingBrowserConnector({ connectorId, extensionRelease, host });
|
|
14
|
+
// For purchase, host.performFinalClick(context, request, options) must perform exactly ONE trusted pointer
|
|
15
|
+
// click on request.submit_selector in the exact fenced browser/document/URL (no retry, no lifting the fence)
|
|
16
|
+
// and return { dispatched }. Accelerate never sends the final click.
|
|
14
17
|
// authenticate(request, rawBody): verify webhook + established direct identity;
|
|
15
18
|
// messaging: createLinqMessagingAdapter({ client: linq, sendingPhone, resolveDestination });
|
|
16
19
|
// admitOrdinaryMessage(event), dispatchSafeProgress(event): durable dedupe + AI queue.
|
package/partner.js
CHANGED
|
@@ -12,6 +12,10 @@ export function registerPartnerCheckoutTools(server, options = {}) {
|
|
|
12
12
|
if (browserConnector?.contract !== 'accelerate_existing_browser_v1' || !privateChat?.chat) {
|
|
13
13
|
throw Error('An existing-browser connector and private chat client are required.');
|
|
14
14
|
}
|
|
15
|
+
// Accelerate never sends the final click. A purchase needs the customer's own browser host to do it once.
|
|
16
|
+
if (purpose === 'purchase' && (typeof browserConnector.performFinalClick !== 'function' || typeof browserConnector.prepareFinalClick !== 'function')) {
|
|
17
|
+
throw Error('Purchase requires a connector whose host implements performFinalClick: the customer\u2019s own browser performs the final click.');
|
|
18
|
+
}
|
|
15
19
|
const host = createDurableCheckoutHost({ persistence, resolveContext });
|
|
16
20
|
// Keep the filled browser until explicit cancel or separately authorized
|
|
17
21
|
// submit. Review delivery can be recovered without repeating the fill.
|
package/postgres-host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostCheckoutRecord } from './host-adapter.js';
|
|
2
2
|
export const checkoutHostMigrationSql: string;
|
|
3
3
|
export interface CheckoutHostPostgresClient {
|
|
4
4
|
query(sql: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[]; rowCount?: number | null }>;
|
|
@@ -6,9 +6,13 @@ export interface CheckoutHostPostgresClient {
|
|
|
6
6
|
on?(event: 'error', listener: () => void): unknown;
|
|
7
7
|
removeListener?(event: 'error', listener: () => void): unknown;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/** Use a separate namespace for each private record type, including WhatsApp journals. */
|
|
10
|
+
export function createPostgresCheckoutPersistence<Record = HostCheckoutRecord>(options: {
|
|
10
11
|
pool: { connect(): Promise<CheckoutHostPostgresClient> };
|
|
11
12
|
encryptionKey: Buffer | string;
|
|
12
13
|
namespace?: string;
|
|
13
14
|
lockTimeoutMs?: number;
|
|
14
|
-
}): Readonly<
|
|
15
|
+
}): Readonly<{
|
|
16
|
+
withLock<T>(ownerKey: string, work: (access: { load(): Promise<Record | undefined>; commit(record: Record): Promise<void> }) => Promise<T>): Promise<T>;
|
|
17
|
+
initialize(): Promise<void>;
|
|
18
|
+
}>;
|