@weaccelerateinc/agent-sdk 0.0.0-stage → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +151 -2
  2. package/approved-extension-releases/0.0.0.1.json +41 -0
  3. package/approved-extension-releases/0.0.0.2.json +34 -0
  4. package/approved-extension.d.ts +24 -0
  5. package/approved-extension.js +90 -0
  6. package/browser-connector.d.ts +109 -0
  7. package/browser-connector.js +218 -0
  8. package/browser-operations.d.ts +56 -0
  9. package/browser-operations.js +144 -0
  10. package/browser-use-host.d.ts +46 -0
  11. package/browser-use-host.js +149 -0
  12. package/checkout-diagnostics.d.ts +34 -0
  13. package/checkout-diagnostics.js +81 -0
  14. package/cli.js +105 -0
  15. package/customer.env.example +6 -0
  16. package/host-adapter.d.ts +90 -0
  17. package/host-adapter.js +357 -0
  18. package/index.d.ts +253 -0
  19. package/index.js +504 -0
  20. package/linq-messaging.d.ts +45 -0
  21. package/linq-messaging.js +123 -0
  22. package/managed-mcp.d.ts +156 -0
  23. package/managed-mcp.js +427 -0
  24. package/package.json +114 -4
  25. package/partner-starter/.env.example +13 -0
  26. package/partner-starter/README.md +61 -0
  27. package/partner-starter/gitignore.template +5 -0
  28. package/partner-starter/package.json +21 -0
  29. package/partner-starter/runtime.mjs +33 -0
  30. package/partner-starter/server.mjs +13 -0
  31. package/partner-starter/smoke.mjs +61 -0
  32. package/partner-starter/synthetic.mjs +256 -0
  33. package/partner-starter/tests/starter.test.mjs +119 -0
  34. package/partner-starter/trusted-context.example.mjs +23 -0
  35. package/partner.d.ts +27 -0
  36. package/partner.js +53 -0
  37. package/postgres-host.d.ts +14 -0
  38. package/postgres-host.js +102 -0
  39. package/private-chat.d.ts +104 -0
  40. package/private-chat.js +316 -0
  41. package/private-ingress.d.ts +46 -0
  42. package/private-ingress.js +132 -0
  43. package/starter/.env.example +19 -0
  44. package/starter/README.md +82 -0
  45. package/starter/gitignore.template +5 -0
  46. package/starter/package.json +21 -0
  47. package/starter/runtime.mjs +66 -0
  48. package/starter/server.mjs +13 -0
  49. package/starter/smoke.mjs +55 -0
  50. package/starter/synthetic.mjs +163 -0
  51. package/starter/tests/starter.test.mjs +224 -0
  52. package/starter/trusted-context.example.mjs +49 -0
@@ -0,0 +1,61 @@
1
+ # Existing-browser payment starter
2
+
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
+
5
+ ## Run without credentials
6
+
7
+ Once Accelerate supplies the matching verified 0.11.0 archive, install it, then:
8
+
9
+ ```sh
10
+ npx --no-install accelerate-agent init ./partner-demo --browser existing
11
+ cd partner-demo
12
+ npm install /secure/downloads/weaccelerateinc-agent-sdk-0.11.0.tgz
13
+ npm test
14
+ npm run smoke
15
+ ```
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. 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
+
19
+ ## Connect your platform
20
+
21
+ Select `ACCELERATE_CHECKOUT_PROFILE=managed_review_v2` explicitly. This starter selects v2 by default; the reusable `registerPartnerCheckoutTools()` API retains `managed_review_v1` for existing callers unless they opt in. Set `ACCELERATE_CHECKOUT_PROFILE=managed_review_v1` only for that legacy manual-fill flow.
22
+
23
+ Set `ACCELERATE_STARTER_MODE=live` and an operator-owned absolute `ACCELERATE_CONTEXT_MODULE` exporting `createPartnerDependencies({client,privateChat})`. Use `trusted-context.example.mjs` and the SDK TypeScript contracts. Required dependencies:
24
+
25
+ - `resolveContext`: verify the customer/workspace/conversation/phone on every invocation.
26
+ - `persistence`: durable cross-process lock and independently committed safe records. `@weaccelerateinc/agent-sdk/postgres-host` accepts your existing pg Pool and an independently managed 32-byte encryption key. Run its explicit `initialize()` migration during deployment.
27
+ - `browserConnector`: `createBrowserUseConnector` with packaged `createBrowserUseHost` provider checks or a provisioned `createExistingBrowserConnector`; use your actual browser/document records, extension readiness, recording policy, durable drain/fence and independent cleanup proof.
28
+ - `authenticate`: verify private raw webhook bytes and map to an established direct identity. Never derive authorization from tool arguments or a model transcript.
29
+ - `messaging`: `PrivateMessagingAdapter` with provider-confirmed sent receipts. `@weaccelerateinc/agent-sdk/linq-messaging` supplies Linq/iMessage transport and an authenticator using your existing signature verifier.
30
+ - `admitOrdinaryMessage`, `dispatchSafeProgress`: your durable deduplicated queues. Only fresh allowlisted safe events enter the model.
31
+
32
+ `configureStarter` returns `privateIngress`. Mount `privateIngress.handle(request,rawBody)` BEFORE framework JSON parsing, logging, tracing, history and AI admission. It wipes the mutable input buffer. Do not keep another body copy. Call `privateIngress.drain(authenticatedContext)` from your existing durable event worker to recover safe prompt/image delivery. Never retry fill or submit there.
33
+
34
+ The stdio launcher demonstrates tool registration only; it cannot host your application's webhook or authenticate arbitrary remote clients. Embed `configureStarter` in your authenticated server for live traffic.
35
+
36
+ ## Flow
37
+
38
+ Shop normally in your browser, completing required non-secret contact/shipping and cardholder-name (`cc-name`) fields → reach payment → `accelerate_checkout_start` attaches/fences that browser → `accelerate_checkout_request_payment` starts private OTP/card selection → apply address → verify real total using `accelerate_checkout_request_approval` (private CVV under this profile) → successful private CVV immediately continues one armed fill in the backend → private masked PNG + final total → five-minute fresh yes/no decision → separately explicit agent submit → cleanup.
39
+
40
+ At create, v2 declares `fill_after_verification:true` and requires the service to acknowledge capability before private OTP; this does not arm fill. Later, `accelerate_checkout_request_approval` sends `fill_after_verification:true` from the trusted SDK backend, bound to the actual quote and revision. The service must acknowledge that flag before requesting CVV. V2 exposes no `accelerate_checkout_fill` tool and never waits for a model roundtrip after private CVV. Status reads and outbox recovery cannot start another fill. Final approval and submission remain separate. Do not silently opt legacy checkouts into v2.
41
+
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
+
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
+
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.
60
+
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.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ *.log
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "accelerate-existing-browser-starter",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.mjs",
11
+ "smoke": "node smoke.mjs",
12
+ "test": "node --test tests/*.test.mjs"
13
+ },
14
+ "dependencies": {
15
+ "@weaccelerateinc/agent-sdk": "0.11.0",
16
+ "@modelcontextprotocol/server": "2.3.1"
17
+ },
18
+ "devDependencies": {
19
+ "@modelcontextprotocol/client": "2.3.1"
20
+ }
21
+ }
@@ -0,0 +1,33 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { AccelerateClient } from '@weaccelerateinc/agent-sdk';
4
+ import { AcceleratePrivateChatClient } from '@weaccelerateinc/agent-sdk/private-chat';
5
+ import { registerPartnerCheckoutTools } from '@weaccelerateinc/agent-sdk/partner';
6
+ import { createSyntheticDependencies } from './synthetic.mjs';
7
+
8
+ export async function configureStarter(server, env = process.env) {
9
+ const mode = env.ACCELERATE_STARTER_MODE ?? 'synthetic';
10
+ const profile = env.ACCELERATE_CHECKOUT_PROFILE ?? 'managed_review_v2';
11
+ if (!['managed_review_v1', 'managed_review_v2'].includes(profile)) throw Error('Unsupported partner checkout profile.');
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';
15
+ if (!['sandbox_fill_only', 'purchase'].includes(purpose) ||
16
+ env.ACCELERATE_ENABLE_PURCHASE_SUBMIT !== undefined && !['true', 'false'].includes(env.ACCELERATE_ENABLE_PURCHASE_SUBMIT) ||
17
+ enableSubmit && purpose !== 'purchase') throw Error('Unsupported server purchase policy.');
18
+ let dependencies;
19
+ if (mode === 'synthetic') dependencies = createSyntheticDependencies({ purpose, profile });
20
+ else if (mode === 'live') {
21
+ if (typeof env.ACCELERATE_CONTEXT_MODULE !== 'string' || !isAbsolute(env.ACCELERATE_CONTEXT_MODULE)) {
22
+ throw Error('A trusted absolute server context module is required.');
23
+ }
24
+ const module = await import(pathToFileURL(env.ACCELERATE_CONTEXT_MODULE).href);
25
+ if (typeof module.createPartnerDependencies !== 'function') throw Error('The trusted partner module is incomplete.');
26
+ const client = AccelerateClient.fromEnv(env), privateChat = AcceleratePrivateChatClient.fromEnv(env);
27
+ dependencies = { ...await module.createPartnerDependencies({ client, privateChat }), client, privateChat };
28
+ } else throw Error('Unsupported starter mode.');
29
+ const integration = registerPartnerCheckoutTools(server, { ...dependencies, profile, purpose, enableSubmit });
30
+ // Mount privateIngress.handle on the platform's AUTHENTICATED webhook before
31
+ // parsing/history/AI, and drain from its existing durable outbox worker.
32
+ return { mode, profile, ...integration, counters: dependencies.counters };
33
+ }
@@ -0,0 +1,13 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
3
+ import { configureStarter } from './runtime.mjs';
4
+
5
+ const server = new McpServer({ name: 'accelerate-partner-starter', version: '0.1.0' });
6
+ try {
7
+ await configureStarter(server);
8
+ server.onerror = () => {}; // Never print protocol arguments or provider errors.
9
+ await server.connect(new StdioServerTransport());
10
+ } catch {
11
+ process.stderr.write(JSON.stringify({ ok: false, code: 'starter_configuration_unavailable' }) + '\n');
12
+ process.exitCode = 1;
13
+ }
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { Client } from '@modelcontextprotocol/client';
4
+ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
5
+
6
+ const client = new Client({ name: 'accelerate-starter-smoke', version: '0.1.0' });
7
+ const transport = new StdioClientTransport({ command: process.execPath,
8
+ args: [fileURLToPath(new URL('./server.mjs', import.meta.url))],
9
+ env: { ...getDefaultEnvironment(), ACCELERATE_STARTER_MODE: 'synthetic', ACCELERATE_CHECKOUT_PROFILE: 'managed_review_v2' }, stderr: 'pipe' });
10
+ let complete = false;
11
+ const timer = setTimeout(() => { void transport.close(); }, 15000);
12
+ try {
13
+ await client.connect(transport);
14
+ const tools = await client.listTools();
15
+ assert.equal(tools.tools.length, 8);
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'), true);
18
+ assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_fill'), false);
19
+ const call = async (name, args) => {
20
+ const result = await client.callTool({ name, arguments: args });
21
+ const value = result.structuredContent ?? JSON.parse(result.content.find(part => part.type === 'text').text);
22
+ assert.equal(value.ok, true); return value;
23
+ };
24
+ const checkoutUrl = 'https://demos.weaccelerate.com/normal/payment';
25
+ const checkout = (await call('accelerate_checkout_start', { start_url: checkoutUrl })).checkout;
26
+ assert.equal(checkout.purpose, 'purchase'); assert.equal(checkout.submit_allowed, true);
27
+ assert.equal(checkout.approval_policy, 'after_fill'); assert.equal(checkout.purchase_approved, false);
28
+ assert.equal(checkout.final_click_actor, 'customer');
29
+ assert.equal(checkout.status, 'wallet_required'); assert.equal(checkout.card, undefined);
30
+ const id = checkout.id;
31
+ assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'wallet_required');
32
+ await call('accelerate_checkout_browser', { checkout_id: id, operation: 'browser_snapshot', arguments: { filter: 'all' } });
33
+ assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'wallet_required');
34
+ const requested = await call('accelerate_checkout_request_payment', { checkout_id: id });
35
+ assert.equal(requested.checkout.status, 'wallet_required');
36
+ assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'selection_ready');
37
+ await call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: checkoutUrl });
38
+ const quote = (await call('accelerate_checkout_request_approval', { checkout_id: id, checkout_url: checkoutUrl,
39
+ amount_cents: 100, currency: 'USD', checkout_total_selector: '#checkout-total', submit_selector: '#place-order' })).checkout;
40
+ assert.equal(quote.fill_after_verification, true);
41
+ assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'ready_for_submit');
42
+ const reviewed = (await call('accelerate_checkout_status', { checkout_id: id })).checkout;
43
+ assert.equal(reviewed.purchase_approved, false);
44
+ const remaining = Date.parse(reviewed.submit_expires_at) - Date.now();
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');
49
+ assert.equal((await call('accelerate_checkout_cancel', { checkout_id: id })).checkout.status, 'cancelled');
50
+ complete = true;
51
+ process.stdout.write(JSON.stringify({ ok: true, synthetic_only: true, mcp_protocol: true,
52
+ profile: 'managed_review_v2', existing_browser_connector: true, browser_mode: 'customer_backend_trusted', verification_deferred: true, private_verification_before_fill: true,
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');
56
+ } catch {
57
+ process.stderr.write(JSON.stringify({ ok: false, code: 'synthetic_smoke_failed' }) + '\n');
58
+ } finally {
59
+ clearTimeout(timer); await client.close().catch(() => {}); await transport.close().catch(() => {});
60
+ process.exitCode = complete ? 0 : 1;
61
+ }
@@ -0,0 +1,256 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createExistingBrowserConnector } from '@weaccelerateinc/agent-sdk/browser-connector';
3
+ import { AccelerateClient } from '@weaccelerateinc/agent-sdk';
4
+ import { AcceleratePrivateChatClient, deliverPrivateCheckoutReview } from '@weaccelerateinc/agent-sdk/private-chat';
5
+
6
+ const context = Object.freeze({ customer_id: 'synthetic-customer', workspace_id: 'synthetic-workspace',
7
+ conversation_id: 'synthetic-direct-conversation', phone_number: '+15555550123' });
8
+ const origin = 'https://synthetic.accelerate.invalid';
9
+ export const syntheticCheckoutUrl = 'https://demos.weaccelerate.com/normal/payment';
10
+ const invalid = () => { throw Error('Synthetic checkout request rejected.'); };
11
+ const digest = value => createHash('sha256').update(value).digest('base64url');
12
+ const png = () => Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 1, 2, 3]);
13
+
14
+ /** In-process fixtures only: no network, source, browser, raw OTP or security code.
15
+ * Private verification is simulated; safe image delivery uses the real SDK. */
16
+ function createSyntheticHost({ purpose = 'sandbox_fill_only', profile = 'managed_review_v1', deliverReview } = {}) {
17
+ if (!['sandbox_fill_only', 'purchase'].includes(purpose) || !['managed_review_v1', 'managed_review_v2'].includes(profile)) invalid();
18
+ const records = new Map(), owners = new Map(), events = new Set(), messages = new Map();
19
+ const calls = { create: 0, browser: 0, payment_requested: 0, approval_requested: 0,
20
+ private_verification: 0, fill: 0, review: 0, review_sent: 0, review_acknowledged: 0,
21
+ purchase_decision: 0, submit: 0, final_click_arm: 0, final_click_host: 0, final_click_complete: 0, cancel: 0, external: 0 };
22
+ let tail = Promise.resolve();
23
+ const locked = async work => {
24
+ const previous = tail; let release;
25
+ tail = new Promise(resolve => { release = resolve; }); await previous;
26
+ try { return await work(); } finally { release(); }
27
+ };
28
+ const view = record => ({ id: record.id, status: record.status, revision: record.revision,
29
+ expires_at: record.expires_at, purpose, submit_allowed: purpose === 'purchase',
30
+ approval_policy: 'after_fill', purchase_approved: record.purchaseApproved === true,
31
+ retry_safe: false, payment_confirmed: false,
32
+ ...(purpose === 'purchase' ? { final_click_actor: 'customer' } : {}),
33
+ ...(record.submitActor ? { submit_actor: 'customer' } : {}),
34
+ ...(record.fillAfterVerificationProfile ? { fill_after_verification: true } : {}),
35
+ ...(record.selected ? { card: { brand: 'Visa', last4: '4242' }, address: { label: 'Saved address ***23' } } : {}),
36
+ ...(record.quote ? { quote: record.quote } : {}),
37
+ ...(record.review ? { submit_expires_at: record.review.expires_at } : {}),
38
+ ...(record.status === 'cancelled' ? { diagnostics: { version: 'v1', stage: 'cancelled', next_actions: ['get_status'], support_reference: 'acs_' + 'a'.repeat(32), cleanup_status: 'confirmed' } } : {}) });
39
+ const privateView = record => ({ checkout: view(record), stage: record.stage, handled: true, duplicate: false,
40
+ cards: [], addresses: [],
41
+ ...(['cvv_required', 'review_delivery_required', 'review_delivered', 'approval_required', 'approved_for_submit'].includes(record.stage)
42
+ ? { approval: { revision: record.revision, ...record.quote } } : {}),
43
+ ...(record.review ? { review: { ...record.review } } : {}) });
44
+ const fill = record => {
45
+ if (record.status !== 'approved') invalid();
46
+ calls.fill++; record.status = 'ready_for_submit'; record.stage = 'review_delivery_required';
47
+ record.review = { id: 'rvr_' + digest(record.id + ':' + record.revision), delivered: false, approval_window_ms: 300000,
48
+ expires_at: new Date(Date.now() + 420000).toISOString(), delivery_expires_at: new Date(Date.now() + 60000).toISOString() };
49
+ };
50
+ const verify = record => {
51
+ calls.approval_requested++; calls.private_verification++; record.status = 'approved'; record.stage = 'verified';
52
+ if (record.fillAfterVerification) fill(record);
53
+ };
54
+ const fetchImpl = async (url, request) => {
55
+ const parsed = new URL(url);
56
+ if (parsed.origin !== origin) { calls.external++; invalid(); }
57
+ const body = request.body ? JSON.parse(request.body) : undefined;
58
+ if (parsed.pathname === '/v1/checkouts' && request.method === 'POST') {
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' ||
61
+ Object.keys(context).some(key => body[key] !== context[key]) || body.start_url !== syntheticCheckoutUrl ||
62
+ JSON.stringify(body.browser) !== JSON.stringify({ provider: 'connector', connector_id: 'synthetic_cdp', browser_id: 'existing:browser:1', custody_mode: 'customer_backend_trusted' })) invalid();
63
+ if ((profile === 'managed_review_v2') !== (body.fill_after_verification === true)) invalid();
64
+ const id = 'mcs_' + digest(body.idempotency_key);
65
+ if (!records.has(id)) { calls.create++; records.set(id, { id, status: 'wallet_required', stage: 'not_started', revision: 0,
66
+ expires_at: new Date(Date.now() + 900000).toISOString(), selected: false, fillAfterVerificationProfile: body.fill_after_verification === true }); }
67
+ return Response.json(view(records.get(id)), { status: 201 });
68
+ }
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);
70
+ if (!match || !records.has(match[1])) invalid();
71
+ const record = records.get(match[1]), action = match[2];
72
+ if (!action && request.method === 'GET') return Response.json(view(record));
73
+ if (request.method !== 'POST') invalid();
74
+ if (action?.startsWith('private-chat/')) {
75
+ if (['customer_id', 'workspace_id', 'conversation_id'].some(key => body[key] !== context[key])) invalid();
76
+ if (action === 'private-chat/start') {
77
+ if (record.stage === 'not_started') { calls.payment_requested++; record.selected = true; record.status = 'selection_ready'; record.stage = 'preparing_checkout'; record.revision = 1; }
78
+ return Response.json(privateView(record));
79
+ }
80
+ if (action === 'private-chat/state') {
81
+ if (record.stage === 'cvv_required') verify(record);
82
+ return Response.json(privateView(record));
83
+ }
84
+ if (action === 'private-chat/review' && record.stage === 'review_delivery_required' && Date.now() < Date.parse(record.review.delivery_expires_at)) {
85
+ calls.review++; record.imageRetrieved = true;
86
+ return new Response(png(), { headers: { 'Content-Type': 'image/png',
87
+ 'X-Accelerate-Review-Id': record.review.id, 'X-Accelerate-Review-Expires-At': record.review.expires_at,
88
+ 'X-Accelerate-Review-Amount-Cents': String(record.quote.amount_cents), 'X-Accelerate-Review-Currency': 'USD',
89
+ 'X-Accelerate-Review-Approval-Window-Ms': '300000', 'X-Accelerate-Review-Delivery-Expires-At': record.review.delivery_expires_at } });
90
+ }
91
+ if (action === 'private-chat/review-delivered') {
92
+ const receipt = messages.get(body.message_id);
93
+ if (!record.imageRetrieved || body.review_id !== record.review?.id || !receipt || receipt.checkout !== record.id ||
94
+ receipt.sent_at !== body.sent_at || Date.parse(body.sent_at) > Date.now() ||
95
+ Date.parse(body.sent_at) >= Date.parse(record.review.delivery_expires_at)) invalid();
96
+ if (!record.review.delivered) { calls.review_acknowledged++; record.review.delivered = true;
97
+ record.review.expires_at = new Date(Date.parse(body.sent_at) + 300000).toISOString();
98
+ record.stage = purpose === 'purchase' ? 'approval_required' : 'review_delivered'; }
99
+ return Response.json(privateView(record));
100
+ }
101
+ if (action === 'private-chat/reply' && purpose === 'purchase' && record.stage === 'approval_required' &&
102
+ record.review?.delivered && typeof body.sent_at === 'string' && Date.parse(body.sent_at) <= Date.now() &&
103
+ Date.parse(body.sent_at) > Date.parse(record.review.expires_at) - 300000 && Date.now() < Date.parse(record.review.expires_at)) {
104
+ if (!['approve', 'decline'].includes(body.text)) invalid();
105
+ calls.purchase_decision++;
106
+ if (body.text === 'approve') { record.purchaseApproved = true; record.stage = 'approved_for_submit'; }
107
+ else { record.status = 'cancelled'; record.stage = 'closed'; delete record.review; }
108
+ return Response.json(privateView(record));
109
+ }
110
+ invalid();
111
+ }
112
+ if (action === 'browser' && ['wallet_required', 'selection_ready', 'details_ready'].includes(record.status)) {
113
+ calls.browser++;
114
+ return Response.json({ content: [{ type: 'text',
115
+ text: 'Synthetic demo checkout\nUSD $1.00\n#checkout-total\n#place-order\n[e1_1] Button Continue' }] });
116
+ }
117
+ if (action === 'details' && record.status === 'selection_ready' && body.checkout_url === syntheticCheckoutUrl) record.status = 'details_ready';
118
+ else if (action === 'approval' && record.status === 'details_ready' && body.checkout_url === syntheticCheckoutUrl &&
119
+ body.amount_cents === 100 && body.currency === 'USD') {
120
+ if ((profile === 'managed_review_v2') !== (body.fill_after_verification === true)) invalid();
121
+ record.fillAfterVerification = body.fill_after_verification === true;
122
+ record.submitSelector = body.submit_selector;
123
+ record.status = 'approval_required'; record.stage = 'cvv_required'; record.revision++;
124
+ record.quote = { checkout_origin: 'https://demos.weaccelerate.com', amount_cents: 100, currency: 'USD' };
125
+ } else if (action === 'fill' && record.status === 'approved' && body.revision === record.revision) {
126
+ if (record.fillAfterVerification) invalid();
127
+ fill(record);
128
+ } else if (action === 'cancel') { calls.cancel++; record.status = 'cancelled'; record.stage = 'closed'; record.purchaseApproved = false; delete record.review; }
129
+ else if (action === 'submit') {
130
+ // The executor click path is closed for every purchase. Counting it proves the fixture never uses it.
131
+ calls.submit++;
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;
148
+ } else invalid();
149
+ return Response.json(view(record));
150
+ };
151
+ const options = { baseUrl: origin, apiKey: 'synthetic_tenant_not_a_real_key'.padEnd(48, 'x'),
152
+ merchantId: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee', fetchImpl };
153
+ const client = new AccelerateClient(options), privateChat = new AcceleratePrivateChatClient(options);
154
+ const host = { client, privateChat,
155
+ resolveContext: async () => ({ ...context }),
156
+ withOwnedCheckout: async (current, id, work) => locked(async () => {
157
+ const saved = owners.get(id);
158
+ if (!saved || Object.keys(context).some(key => current[key] !== context[key] || saved[key] !== current[key])) invalid();
159
+ return work({ ...saved });
160
+ }),
161
+ withCheckoutCreation: async (current, work) => locked(() => {
162
+ if (Object.keys(context).some(key => current[key] !== context[key])) invalid();
163
+ return work({ idempotency_key: 'synthetic-exact-attempt-1', persistCreated: async saved => {
164
+ if (Object.keys(context).some(key => saved[key] !== context[key])) invalid();
165
+ owners.set(saved.checkout_id, { ...saved });
166
+ } });
167
+ }),
168
+ sendPrivateReview: async (current, message) => {
169
+ if (Object.keys(context).some(key => current[key] !== context[key]) || message.contentType !== 'image/png' ||
170
+ !message.bytes.equals(png()) || message.signal.aborted || Date.now() >= Date.parse(message.delivery_expires_at)) invalid();
171
+ const record = [...records.values()].find(value => value.stage === 'review_delivery_required');
172
+ if (!record || !message.text.includes('1.00 USD') || (purpose === 'sandbox_fill_only'
173
+ ? !message.text.includes('No purchase approval is needed') || message.text.includes('Do you approve')
174
+ : !message.text.includes('5 minutes from this message'))) invalid();
175
+ const message_id = 'synthetic-review-' + digest(message.idempotency_key);
176
+ if (!messages.has(message_id)) { calls.review_sent++; messages.set(message_id, { checkout: record.id, sent_at: new Date(Date.now()).toISOString() }); }
177
+ return { message_id, sent_at: messages.get(message_id).sent_at };
178
+ },
179
+ dispatchCheckoutEvent: async (current, event) => {
180
+ if (Object.keys(context).some(key => current[key] !== context[key]) ||
181
+ event.version !== 'v1' || !['payment_requested', 'approval_requested', 'review_ready'].includes(event.kind) ||
182
+ event.idempotency_key !== 'accelerate-checkout-event:' + createHash('sha256').update(JSON.stringify([
183
+ 'v1', event.kind, event.checkout.id, ['approval_requested', 'review_ready'].includes(event.kind) ? event.checkout.revision : 0])).digest('hex') ||
184
+ !owners.has(event.checkout.id)) invalid();
185
+ if (events.has(event.idempotency_key)) return;
186
+ const record = records.get(event.checkout.id);
187
+ if (event.kind === 'payment_requested' && record.status === 'wallet_required') {
188
+ calls.payment_requested++; record.selected = true; record.status = 'selection_ready'; record.stage = 'preparing_checkout'; record.revision = 1;
189
+ } else if (event.kind === 'approval_requested' && record.stage === 'cvv_required') {
190
+ verify(record);
191
+ } else if (event.kind === 'review_ready' && record.stage === 'review_delivery_required') {
192
+ if (deliverReview) await deliverReview(current, event);
193
+ else await deliverPrivateCheckoutReview({ client: privateChat, state: privateView(record),
194
+ binding: Object.fromEntries(['customer_id', 'workspace_id', 'conversation_id'].map(key => [key, context[key]])),
195
+ messageId: event.idempotency_key, send: message => host.sendPrivateReview(current, message) });
196
+ } else invalid();
197
+ // Only receipt acknowledgement completes this event; a failure retains
198
+ // this exact event for redelivery, without replaying private filling.
199
+ events.add(event.idempotency_key);
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
+ },
208
+ counters: () => ({ ...calls }),
209
+ };
210
+ return host;
211
+ }
212
+
213
+ /** Synthetic-only host dependencies. This memory store is deliberately not
214
+ * exported as a production persistence adapter. No browser/provider is created. */
215
+ export function createSyntheticDependencies({ purpose = 'sandbox_fill_only', profile = 'managed_review_v1' } = {}) {
216
+ const fixture = createSyntheticHost({ purpose, profile }); let record, tail = Promise.resolve(), fenced = false;
217
+ const saved = new Map();
218
+ const persistence = { async withLock(_key, work) {
219
+ const previous = tail; let release; tail = new Promise(resolve => { release = resolve; }); await previous;
220
+ try { return await work({ load: async () => record && structuredClone(record), commit: async value => { record = structuredClone(value); } }); }
221
+ finally { release(); }
222
+ } };
223
+ const extensionRelease = {
224
+ schema_version: 1, release_id: 'accelerate-extension-0.0.0.2', status: 'approved', extension_version: '0.0.0.2',
225
+ artifact: { format: 'zip', sha256: 'a'.repeat(64), bytes: 1234 },
226
+ bridge: { protocol: 'accelerate_extension_bridge_v1', sha256: 'b'.repeat(64) },
227
+ capabilities: { manual_card_fill: true, document_bound_single_use: true, autosave_disabled: true, automatic_submit: false, maximum_retention_ms: 480000 },
228
+ compatibility: { sdk_versions: ['0.10.1', '0.10.2', '0.10.3', '0.10.4', '0.11.0'], executor_contract: 'accelerate_manual_checkout_v1' },
229
+ qualifications: [{ provider: 'connector', scope: 'synthetic_only', evidence: 'docs/evidence/synthetic-starter.json' }],
230
+ }; // Fake metadata for this in-process fixture only; never load it in live mode.
231
+ const browser = { owner: context, browser: { provider: 'connector', connector_id: 'synthetic_cdp', browser_id: 'existing:browser:1', custody_mode: 'customer_backend_trusted' },
232
+ document_id: 'existing:document:1', checkout_url: syntheticCheckoutUrl, expires_at: new Date(Date.now() + 1800000).toISOString() };
233
+ const browserConnector = createExistingBrowserConnector({ connectorId: 'synthetic_cdp', extensionRelease, host: {
234
+ resolveOwnedBrowser: async () => structuredClone(browser),
235
+ inspectBrowser: async () => ({ ...structuredClone(browser), recording_disabled: true,
236
+ extension: { success: true, initialized: true, wasmInitialized: true, autosaveDisabled: true, boundedRetentionSupported: true, maximumRetentionMs: 480000,
237
+ extension_version: '0.0.0.2', artifact_sha256: 'a'.repeat(64), bridge_protocol: 'accelerate_extension_bridge_v1' },
238
+ automation: { ordinary_operations_drained: fenced, agent_access_fenced: fenced, fence_id: fenced ? 'synthetic:fence:1' : null } }),
239
+ drainAndFence: async () => { fenced = true; return { ordinary_operations_drained: true, agent_access_fenced: true, fence_id: 'synthetic:fence:1' }; },
240
+ bindCheckout: async (_context, value) => { saved.set(value.checkout_id, value); },
241
+ resolveCheckoutBrowser: async (_context, { checkout_id }) => { const { checkout_id: _id, attempt_key: _key, ...value } = saved.get(checkout_id); return value; },
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
+ } } : {}),
248
+ } });
249
+ return { client: fixture.client, privateChat: fixture.privateChat, resolveContext: fixture.resolveContext,
250
+ persistence, browserConnector,
251
+ authenticate: async () => { throw Error('Synthetic mode has no public webhook.'); },
252
+ messaging: { sendText: async () => ({ message_id: 'synthetic-private-prompt', sent_at: new Date().toISOString() }), sendReview: fixture.sendPrivateReview },
253
+ admitOrdinaryMessage: async () => { throw Error('Synthetic mode has no AI ingress.'); },
254
+ dispatchSafeProgress: async () => {}, counters: fixture.counters, record: () => structuredClone(record),
255
+ };
256
+ }
@@ -0,0 +1,119 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { configureStarter } from '../runtime.mjs';
4
+ import { createSyntheticDependencies, syntheticCheckoutUrl } from '../synthetic.mjs';
5
+ import { registerPartnerCheckoutTools } from '@weaccelerateinc/agent-sdk/partner';
6
+ async function fixture(env = {}) {
7
+ const tools = new Map(); const integration = await configureStarter({ registerTool(name, _config, callback) { tools.set(name, callback); } }, env);
8
+ const call = async (name, args) => { const result = (await tools.get(name)(args, {})).structuredContent; assert.equal(result.ok, true, JSON.stringify(result)); return result; };
9
+ return { ...integration, tools, call };
10
+ }
11
+ test('explicit fill-only v2 verifies privately then fills and acknowledges review without a submit tool', async t => {
12
+ const old = globalThis.fetch; globalThis.fetch = () => assert.fail('No network in synthetic starter'); t.after(() => { globalThis.fetch = old; });
13
+ const f = await fixture({ ACCELERATE_STARTER_PURPOSE: 'sandbox_fill_only' }), checkout = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout;
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);
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);
17
+ assert.equal(f.profile, 'managed_review_v2');
18
+ await f.call('accelerate_checkout_request_payment', { checkout_id: id });
19
+ assert.equal((await f.call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'selection_ready');
20
+ await f.call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
21
+ const quote = (await f.call('accelerate_checkout_request_approval', { checkout_id: id, checkout_url: syntheticCheckoutUrl, amount_cents: 100, currency: 'USD', checkout_total_selector: '#checkout-total', submit_selector: '#place-order' })).checkout;
22
+ assert.equal(quote.fill_after_verification, true);
23
+ assert.equal((await f.call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'ready_for_submit');
24
+ assert.equal(f.counters().fill, 1); assert.equal(f.counters().review_sent, 1); assert.equal(f.counters().review_acknowledged, 1);
25
+ // Safe state reads recover delivery only; they cannot replay the armed fill.
26
+ await f.call('accelerate_checkout_status', { checkout_id: id });
27
+ assert.equal(f.counters().fill, 1); assert.equal(f.counters().private_verification, 1);
28
+ assert.equal(f.counters().purchase_decision, 0);
29
+ await f.call('accelerate_checkout_cancel', { checkout_id: id });
30
+ assert.equal(f.counters().submit, 0); assert.equal(f.counters().cancel, 1); assert.equal(f.counters().external, 0);
31
+ });
32
+ test('legacy v1 remains an explicit starter profile with one manual fill and no silent upgrade', async () => {
33
+ const f = await fixture({ ACCELERATE_CHECKOUT_PROFILE: 'managed_review_v1', ACCELERATE_STARTER_PURPOSE: 'sandbox_fill_only' });
34
+ assert.equal(f.tools.has('accelerate_checkout_fill'), true); assert.equal(f.tools.size, 8);
35
+ const id = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout.id;
36
+ await f.call('accelerate_checkout_request_payment', { checkout_id: id });
37
+ await f.call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
38
+ const quote = (await f.call('accelerate_checkout_request_approval', { checkout_id: id, checkout_url: syntheticCheckoutUrl, amount_cents: 100, currency: 'USD', checkout_total_selector: '#checkout-total', submit_selector: '#place-order' })).checkout;
39
+ assert.equal(quote.fill_after_verification, undefined);
40
+ assert.equal((await f.call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'approved');
41
+ assert.equal(f.counters().fill, 0);
42
+ await f.call('accelerate_checkout_fill', { checkout_id: id, revision: quote.revision });
43
+ const repeated = (await f.tools.get('accelerate_checkout_fill')({ checkout_id: id, revision: quote.revision }, {})).structuredContent;
44
+ assert.equal(repeated.ok, false); assert.equal(f.counters().fill, 1);
45
+ assert.equal(f.counters().review_acknowledged, 1); assert.equal(f.counters().submit, 0);
46
+ await f.call('accelerate_checkout_cancel', { checkout_id: id });
47
+ });
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();
51
+ assert.equal(f.tools.has('accelerate_checkout_fill'), false); assert.equal(f.tools.has('accelerate_checkout_submit'), true);
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;
57
+ await f.call('accelerate_checkout_request_payment', { checkout_id: id });
58
+ assert.equal(f.counters().fill, 0);
59
+ await f.call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
60
+ const quote = (await f.call('accelerate_checkout_request_approval', { checkout_id: id, checkout_url: syntheticCheckoutUrl, amount_cents: 100, currency: 'USD', checkout_total_selector: '#checkout-total', submit_selector: '#place-order' })).checkout;
61
+ const current = (await f.call('accelerate_checkout_status', { checkout_id: id })).checkout;
62
+ assert.equal(current.status, 'ready_for_submit'); assert.equal(current.purchase_approved, false);
63
+ const remaining = Date.parse(current.submit_expires_at) - Date.now();
64
+ assert.ok(remaining > 290000 && remaining <= 300000);
65
+ assert.equal(f.counters().fill, 1); assert.equal(f.counters().review_acknowledged, 1);
66
+ assert.equal(f.counters().submit, 0); assert.equal(f.counters().purchase_decision, 0);
67
+ const rejected = (await f.tools.get('accelerate_checkout_submit')({ checkout_id: id, revision: quote.revision }, {})).structuredContent;
68
+ assert.equal(rejected.ok, false); assert.equal(f.counters().submit, 0);
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);
79
+ });
80
+ test('live setup requires a trusted backend module and preserves explicit custody selection', async () => {
81
+ await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_STARTER_MODE: 'live' }), /trusted absolute/);
82
+ await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_STARTER_MODE: 'unknown' }), /Unsupported/);
83
+ await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_CHECKOUT_PROFILE: 'unknown' }), /profile/);
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 });
119
+ });
@@ -0,0 +1,23 @@
1
+ /** Connect your EXISTING authenticated backend. No executable insecure defaults,
2
+ * anonymous webhook, in-memory production locks or browser creation here.
3
+ * Imports are SDK subpaths: /browser-connector, /host-adapter, /postgres-host,
4
+ * /linq-messaging. Linq is optional; any PrivateMessagingAdapter can implement it.
5
+ */
6
+ export async function createPartnerDependencies({ client, privateChat }) {
7
+ void client; void privateChat;
8
+ // Return:
9
+ // resolveContext(extra): verified customer/workspace/conversation/phone from auth;
10
+ // persistence: createPostgresCheckoutPersistence({ pool, encryptionKey });
11
+ // browserConnector: createBrowserUseConnector({ extensionRelease,
12
+ // host: createBrowserUseHost({apiKey,merchantId,extensionId,extensionRelease,host}) }) OR
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.
17
+ // authenticate(request, rawBody): verify webhook + established direct identity;
18
+ // messaging: createLinqMessagingAdapter({ client: linq, sendingPhone, resolveDestination });
19
+ // admitOrdinaryMessage(event), dispatchSafeProgress(event): durable dedupe + AI queue.
20
+ // Initialize persistence schema explicitly during deployment, not per request.
21
+ // See installed module types for the complete connector/storage/provider contracts.
22
+ throw Error('Supply authenticated platform services and a qualified existing-browser connector.');
23
+ }