@weaccelerateinc/agent-sdk 0.0.0-stage → 0.10.3
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 +149 -2
- package/approved-extension-releases/0.0.0.1.json +39 -0
- package/approved-extension-releases/0.0.0.2.json +32 -0
- package/approved-extension.d.ts +24 -0
- package/approved-extension.js +90 -0
- package/browser-connector.d.ts +87 -0
- package/browser-connector.js +179 -0
- package/browser-operations.d.ts +56 -0
- package/browser-operations.js +144 -0
- package/browser-use-host.d.ts +43 -0
- package/browser-use-host.js +147 -0
- package/checkout-diagnostics.d.ts +23 -0
- package/checkout-diagnostics.js +67 -0
- package/cli.js +102 -0
- package/customer.env.example +6 -0
- package/host-adapter.d.ts +90 -0
- package/host-adapter.js +353 -0
- package/index.d.ts +209 -0
- package/index.js +441 -0
- package/linq-messaging.d.ts +45 -0
- package/linq-messaging.js +123 -0
- package/managed-mcp.d.ts +150 -0
- package/managed-mcp.js +382 -0
- package/package.json +114 -4
- package/partner-starter/.env.example +11 -0
- package/partner-starter/README.md +48 -0
- package/partner-starter/gitignore.template +5 -0
- package/partner-starter/package.json +21 -0
- package/partner-starter/runtime.mjs +32 -0
- package/partner-starter/server.mjs +13 -0
- package/partner-starter/smoke.mjs +56 -0
- package/partner-starter/synthetic.mjs +226 -0
- package/partner-starter/tests/starter.test.mjs +69 -0
- package/partner-starter/trusted-context.example.mjs +20 -0
- package/partner.d.ts +27 -0
- package/partner.js +49 -0
- package/postgres-host.d.ts +14 -0
- package/postgres-host.js +102 -0
- package/private-chat.d.ts +104 -0
- package/private-chat.js +309 -0
- package/private-ingress.d.ts +46 -0
- package/private-ingress.js +132 -0
- package/starter/.env.example +19 -0
- package/starter/README.md +88 -0
- package/starter/gitignore.template +5 -0
- package/starter/package.json +21 -0
- package/starter/runtime.mjs +61 -0
- package/starter/server.mjs +13 -0
- package/starter/smoke.mjs +55 -0
- package/starter/synthetic.mjs +163 -0
- package/starter/tests/starter.test.mjs +256 -0
- package/starter/trusted-context.example.mjs +49 -0
package/package.json
CHANGED
|
@@ -1,6 +1,116 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weaccelerateinc/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.10.3",
|
|
4
|
+
"description": "Server-only payment SDK, existing-browser connectors and private host adapters for agent platforms",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"accelerate-agent": "./cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"import": "./index.js"
|
|
15
|
+
},
|
|
16
|
+
"./browser-operations": {
|
|
17
|
+
"types": "./browser-operations.d.ts",
|
|
18
|
+
"import": "./browser-operations.js"
|
|
19
|
+
},
|
|
20
|
+
"./private-chat": {
|
|
21
|
+
"types": "./private-chat.d.ts",
|
|
22
|
+
"import": "./private-chat.js"
|
|
23
|
+
},
|
|
24
|
+
"./managed-mcp": {
|
|
25
|
+
"types": "./managed-mcp.d.ts",
|
|
26
|
+
"import": "./managed-mcp.js"
|
|
27
|
+
},
|
|
28
|
+
"./approved-extension": {
|
|
29
|
+
"types": "./approved-extension.d.ts",
|
|
30
|
+
"import": "./approved-extension.js"
|
|
31
|
+
},
|
|
32
|
+
"./browser-connector": {
|
|
33
|
+
"types": "./browser-connector.d.ts",
|
|
34
|
+
"import": "./browser-connector.js"
|
|
35
|
+
},
|
|
36
|
+
"./host-adapter": {
|
|
37
|
+
"types": "./host-adapter.d.ts",
|
|
38
|
+
"import": "./host-adapter.js"
|
|
39
|
+
},
|
|
40
|
+
"./postgres-host": {
|
|
41
|
+
"types": "./postgres-host.d.ts",
|
|
42
|
+
"import": "./postgres-host.js"
|
|
43
|
+
},
|
|
44
|
+
"./private-ingress": {
|
|
45
|
+
"types": "./private-ingress.d.ts",
|
|
46
|
+
"import": "./private-ingress.js"
|
|
47
|
+
},
|
|
48
|
+
"./linq-messaging": {
|
|
49
|
+
"types": "./linq-messaging.d.ts",
|
|
50
|
+
"import": "./linq-messaging.js"
|
|
51
|
+
},
|
|
52
|
+
"./partner": {
|
|
53
|
+
"types": "./partner.d.ts",
|
|
54
|
+
"import": "./partner.js"
|
|
55
|
+
},
|
|
56
|
+
"./approved-extension-releases/*": "./approved-extension-releases/*",
|
|
57
|
+
"./browser-use-host": {
|
|
58
|
+
"types": "./browser-use-host.d.ts",
|
|
59
|
+
"import": "./browser-use-host.js"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"index.js",
|
|
64
|
+
"index.d.ts",
|
|
65
|
+
"browser-operations.js",
|
|
66
|
+
"browser-operations.d.ts",
|
|
67
|
+
"private-chat.js",
|
|
68
|
+
"private-chat.d.ts",
|
|
69
|
+
"cli.js",
|
|
70
|
+
"customer.env.example",
|
|
71
|
+
"README.md",
|
|
72
|
+
"managed-mcp.js",
|
|
73
|
+
"managed-mcp.d.ts",
|
|
74
|
+
"checkout-diagnostics.js",
|
|
75
|
+
"checkout-diagnostics.d.ts",
|
|
76
|
+
"starter",
|
|
77
|
+
"approved-extension.js",
|
|
78
|
+
"approved-extension.d.ts",
|
|
79
|
+
"browser-connector.js",
|
|
80
|
+
"browser-connector.d.ts",
|
|
81
|
+
"host-adapter.js",
|
|
82
|
+
"host-adapter.d.ts",
|
|
83
|
+
"postgres-host.js",
|
|
84
|
+
"postgres-host.d.ts",
|
|
85
|
+
"private-ingress.js",
|
|
86
|
+
"private-ingress.d.ts",
|
|
87
|
+
"linq-messaging.js",
|
|
88
|
+
"linq-messaging.d.ts",
|
|
89
|
+
"partner.js",
|
|
90
|
+
"partner.d.ts",
|
|
91
|
+
"approved-extension-releases",
|
|
92
|
+
"partner-starter",
|
|
93
|
+
"browser-use-host.js",
|
|
94
|
+
"browser-use-host.d.ts"
|
|
95
|
+
],
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": ">=22"
|
|
98
|
+
},
|
|
99
|
+
"scripts": {
|
|
100
|
+
"test": "node --test test/*.test.js"
|
|
101
|
+
},
|
|
102
|
+
"license": "UNLICENSED",
|
|
103
|
+
"repository": {
|
|
104
|
+
"type": "git",
|
|
105
|
+
"url": "git+https://github.com/weaccelerateinc/accelerate.git",
|
|
106
|
+
"directory": "products/agent-sdk/packages/sdk"
|
|
107
|
+
},
|
|
108
|
+
"homepage": "https://github.com/weaccelerateinc/accelerate/tree/main/products/agent-sdk#readme",
|
|
109
|
+
"bugs": {
|
|
110
|
+
"url": "https://github.com/weaccelerateinc/accelerate/issues"
|
|
111
|
+
},
|
|
112
|
+
"publishConfig": {
|
|
113
|
+
"access": "public",
|
|
114
|
+
"registry": "https://registry.npmjs.org/"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Synthetic mode makes no external requests and needs no credentials.
|
|
2
|
+
ACCELERATE_STARTER_MODE=synthetic
|
|
3
|
+
# Explicit new profile: private CVV continues the armed fill in the backend.
|
|
4
|
+
ACCELERATE_CHECKOUT_PROFILE=managed_review_v2
|
|
5
|
+
ACCELERATE_STARTER_PURPOSE=sandbox_fill_only
|
|
6
|
+
ACCELERATE_ENABLE_PURCHASE_SUBMIT=false
|
|
7
|
+
# Live: operator-controlled absolute file exporting createPartnerDependencies().
|
|
8
|
+
# ACCELERATE_CONTEXT_MODULE=/absolute/protected/partner-host.mjs
|
|
9
|
+
# ACCELERATE_API_KEY=<tenant server key>
|
|
10
|
+
# ACCELERATE_MERCHANT_ID=<provisioned merchant UUID>
|
|
11
|
+
# ACCELERATE_BASE_URL=https://accelerate-agent-sdk.vercel.app
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Existing-browser payment starter
|
|
2
|
+
|
|
3
|
+
SDK source 0.10.3, explicit `managed_review_v2`, `customer_backend_trusted`. The matching service must support this profile; source changes do not establish an archive publication or live deployment. 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.
|
|
4
|
+
|
|
5
|
+
## Run without credentials
|
|
6
|
+
|
|
7
|
+
Once Accelerate supplies the matching verified 0.10.3 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.10.3.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. 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. The published release baseline qualifies `https://demos.weaccelerate.com/normal/payment`. The new universal-card source path probes compatible public HTTPS card forms instead of keeping a merchant hostname allowlist. It requires private `universal_card_v1` admission bound to the exact existing document and immutable purpose before OTP/CVV; ambiguous/unsupported forms fail without a replacement browser. That capability requires coordinated service/executor deployment and does not promise every checkout works. A candidate artifact or generic interface alone does not qualify a browser.
|
|
45
|
+
|
|
46
|
+
The default is `sandbox_fill_only`, with no submit tool. After its informational image, call cancel and verify cleanup. Enable purchase only through explicit backend `purpose:'purchase'` and `enableSubmit:true` after qualification. Customer approval never automatically submits. Uncertain outcomes retain fences and require reconciliation; status/cleanup never authorize a replacement by themselves.
|
|
47
|
+
|
|
48
|
+
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,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.10.3",
|
|
16
|
+
"@modelcontextprotocol/server": "2.3.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@modelcontextprotocol/client": "2.3.1"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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 ?? 'sandbox_fill_only';
|
|
13
|
+
const enableSubmit = env.ACCELERATE_ENABLE_PURCHASE_SUBMIT === 'true';
|
|
14
|
+
if (!['sandbox_fill_only', 'purchase'].includes(purpose) ||
|
|
15
|
+
env.ACCELERATE_ENABLE_PURCHASE_SUBMIT !== undefined && !['true', 'false'].includes(env.ACCELERATE_ENABLE_PURCHASE_SUBMIT) ||
|
|
16
|
+
enableSubmit && purpose !== 'purchase') throw Error('Unsupported server purchase policy.');
|
|
17
|
+
let dependencies;
|
|
18
|
+
if (mode === 'synthetic') dependencies = createSyntheticDependencies({ purpose, profile });
|
|
19
|
+
else if (mode === 'live') {
|
|
20
|
+
if (typeof env.ACCELERATE_CONTEXT_MODULE !== 'string' || !isAbsolute(env.ACCELERATE_CONTEXT_MODULE)) {
|
|
21
|
+
throw Error('A trusted absolute server context module is required.');
|
|
22
|
+
}
|
|
23
|
+
const module = await import(pathToFileURL(env.ACCELERATE_CONTEXT_MODULE).href);
|
|
24
|
+
if (typeof module.createPartnerDependencies !== 'function') throw Error('The trusted partner module is incomplete.');
|
|
25
|
+
const client = AccelerateClient.fromEnv(env), privateChat = AcceleratePrivateChatClient.fromEnv(env);
|
|
26
|
+
dependencies = { ...await module.createPartnerDependencies({ client, privateChat }), client, privateChat };
|
|
27
|
+
} else throw Error('Unsupported starter mode.');
|
|
28
|
+
const integration = registerPartnerCheckoutTools(server, { ...dependencies, profile, purpose, enableSubmit });
|
|
29
|
+
// Mount privateIngress.handle on the platform's AUTHENTICATED webhook before
|
|
30
|
+
// parsing/history/AI, and drain from its existing durable outbox worker.
|
|
31
|
+
return { mode, profile, ...integration, counters: dependencies.counters };
|
|
32
|
+
}
|
|
@@ -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,56 @@
|
|
|
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, 7);
|
|
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'), false);
|
|
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, 'sandbox_fill_only'); assert.equal(checkout.submit_allowed, false);
|
|
27
|
+
assert.equal(checkout.approval_policy, 'after_fill'); assert.equal(checkout.purchase_approved, false);
|
|
28
|
+
assert.equal(checkout.status, 'wallet_required'); assert.equal(checkout.card, undefined);
|
|
29
|
+
const id = checkout.id;
|
|
30
|
+
assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'wallet_required');
|
|
31
|
+
await call('accelerate_checkout_browser', { checkout_id: id, operation: 'browser_snapshot', arguments: { filter: 'all' } });
|
|
32
|
+
assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'wallet_required');
|
|
33
|
+
const requested = await call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
34
|
+
assert.equal(requested.checkout.status, 'wallet_required');
|
|
35
|
+
assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'selection_ready');
|
|
36
|
+
await call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: checkoutUrl });
|
|
37
|
+
const quote = (await call('accelerate_checkout_request_approval', { checkout_id: id, checkout_url: checkoutUrl,
|
|
38
|
+
amount_cents: 100, currency: 'USD', checkout_total_selector: '#checkout-total', submit_selector: '#place-order' })).checkout;
|
|
39
|
+
assert.equal(quote.fill_after_verification, true);
|
|
40
|
+
assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'ready_for_submit');
|
|
41
|
+
const reviewed = (await call('accelerate_checkout_status', { checkout_id: id })).checkout;
|
|
42
|
+
assert.equal(reviewed.purchase_approved, false);
|
|
43
|
+
const remaining = Date.parse(reviewed.submit_expires_at) - Date.now();
|
|
44
|
+
assert.ok(remaining > 290000 && remaining <= 300000);
|
|
45
|
+
assert.equal((await call('accelerate_checkout_cancel', { checkout_id: id })).checkout.status, 'cancelled');
|
|
46
|
+
complete = true;
|
|
47
|
+
process.stdout.write(JSON.stringify({ ok: true, synthetic_only: true, mcp_protocol: true,
|
|
48
|
+
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: false,
|
|
50
|
+
submit_tool_absent: true, manual_fill_tool_absent: true, fill_ready: true, cancelled: true, live_checkout_requests: 0 }) + '\n');
|
|
51
|
+
} catch {
|
|
52
|
+
process.stderr.write(JSON.stringify({ ok: false, code: 'synthetic_smoke_failed' }) + '\n');
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer); await client.close().catch(() => {}); await transport.close().catch(() => {});
|
|
55
|
+
process.exitCode = complete ? 0 : 1;
|
|
56
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { createHash } 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, 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
|
+
...(record.fillAfterVerificationProfile ? { fill_after_verification: true } : {}),
|
|
33
|
+
...(record.selected ? { card: { brand: 'Visa', last4: '4242' }, address: { label: 'Saved address ***23' } } : {}),
|
|
34
|
+
...(record.quote ? { quote: record.quote } : {}),
|
|
35
|
+
...(record.review ? { submit_expires_at: record.review.expires_at } : {}),
|
|
36
|
+
...(record.status === 'cancelled' ? { diagnostics: { version: 'v1', stage: 'cancelled', next_actions: ['get_status'], support_reference: 'acs_' + 'a'.repeat(32), cleanup_status: 'confirmed' } } : {}) });
|
|
37
|
+
const privateView = record => ({ checkout: view(record), stage: record.stage, handled: true, duplicate: false,
|
|
38
|
+
cards: [], addresses: [],
|
|
39
|
+
...(['cvv_required', 'review_delivery_required', 'review_delivered', 'approval_required', 'approved_for_submit'].includes(record.stage)
|
|
40
|
+
? { approval: { revision: record.revision, ...record.quote } } : {}),
|
|
41
|
+
...(record.review ? { review: { ...record.review } } : {}) });
|
|
42
|
+
const fill = record => {
|
|
43
|
+
if (record.status !== 'approved') invalid();
|
|
44
|
+
calls.fill++; record.status = 'ready_for_submit'; record.stage = 'review_delivery_required';
|
|
45
|
+
record.review = { id: 'rvr_' + digest(record.id + ':' + record.revision), delivered: false, approval_window_ms: 300000,
|
|
46
|
+
expires_at: new Date(Date.now() + 420000).toISOString(), delivery_expires_at: new Date(Date.now() + 60000).toISOString() };
|
|
47
|
+
};
|
|
48
|
+
const verify = record => {
|
|
49
|
+
calls.approval_requested++; calls.private_verification++; record.status = 'approved'; record.stage = 'verified';
|
|
50
|
+
if (record.fillAfterVerification) fill(record);
|
|
51
|
+
};
|
|
52
|
+
const fetchImpl = async (url, request) => {
|
|
53
|
+
const parsed = new URL(url);
|
|
54
|
+
if (parsed.origin !== origin) { calls.external++; invalid(); }
|
|
55
|
+
const body = request.body ? JSON.parse(request.body) : undefined;
|
|
56
|
+
if (parsed.pathname === '/v1/checkouts' && request.method === 'POST') {
|
|
57
|
+
if (body.interaction_mode !== 'chat' || body.purpose !== purpose || body.approval_policy !== 'after_fill' ||
|
|
58
|
+
Object.keys(context).some(key => body[key] !== context[key]) || body.start_url !== syntheticCheckoutUrl ||
|
|
59
|
+
JSON.stringify(body.browser) !== JSON.stringify({ provider: 'connector', connector_id: 'synthetic_cdp', browser_id: 'existing:browser:1', custody_mode: 'customer_backend_trusted' })) invalid();
|
|
60
|
+
if ((profile === 'managed_review_v2') !== (body.fill_after_verification === true)) invalid();
|
|
61
|
+
const id = 'mcs_' + digest(body.idempotency_key);
|
|
62
|
+
if (!records.has(id)) { calls.create++; records.set(id, { id, status: 'wallet_required', stage: 'not_started', revision: 0,
|
|
63
|
+
expires_at: new Date(Date.now() + 900000).toISOString(), selected: false, fillAfterVerificationProfile: body.fill_after_verification === true }); }
|
|
64
|
+
return Response.json(view(records.get(id)), { status: 201 });
|
|
65
|
+
}
|
|
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);
|
|
67
|
+
if (!match || !records.has(match[1])) invalid();
|
|
68
|
+
const record = records.get(match[1]), action = match[2];
|
|
69
|
+
if (!action && request.method === 'GET') return Response.json(view(record));
|
|
70
|
+
if (request.method !== 'POST') invalid();
|
|
71
|
+
if (action?.startsWith('private-chat/')) {
|
|
72
|
+
if (['customer_id', 'workspace_id', 'conversation_id'].some(key => body[key] !== context[key])) invalid();
|
|
73
|
+
if (action === 'private-chat/start') {
|
|
74
|
+
if (record.stage === 'not_started') { calls.payment_requested++; record.selected = true; record.status = 'selection_ready'; record.stage = 'preparing_checkout'; record.revision = 1; }
|
|
75
|
+
return Response.json(privateView(record));
|
|
76
|
+
}
|
|
77
|
+
if (action === 'private-chat/state') {
|
|
78
|
+
if (record.stage === 'cvv_required') verify(record);
|
|
79
|
+
return Response.json(privateView(record));
|
|
80
|
+
}
|
|
81
|
+
if (action === 'private-chat/review' && record.stage === 'review_delivery_required' && Date.now() < Date.parse(record.review.delivery_expires_at)) {
|
|
82
|
+
calls.review++; record.imageRetrieved = true;
|
|
83
|
+
return new Response(png(), { headers: { 'Content-Type': 'image/png',
|
|
84
|
+
'X-Accelerate-Review-Id': record.review.id, 'X-Accelerate-Review-Expires-At': record.review.expires_at,
|
|
85
|
+
'X-Accelerate-Review-Amount-Cents': String(record.quote.amount_cents), 'X-Accelerate-Review-Currency': 'USD',
|
|
86
|
+
'X-Accelerate-Review-Approval-Window-Ms': '300000', 'X-Accelerate-Review-Delivery-Expires-At': record.review.delivery_expires_at } });
|
|
87
|
+
}
|
|
88
|
+
if (action === 'private-chat/review-delivered') {
|
|
89
|
+
const receipt = messages.get(body.message_id);
|
|
90
|
+
if (!record.imageRetrieved || body.review_id !== record.review?.id || !receipt || receipt.checkout !== record.id ||
|
|
91
|
+
receipt.sent_at !== body.sent_at || Date.parse(body.sent_at) > Date.now() ||
|
|
92
|
+
Date.parse(body.sent_at) >= Date.parse(record.review.delivery_expires_at)) invalid();
|
|
93
|
+
if (!record.review.delivered) { calls.review_acknowledged++; record.review.delivered = true;
|
|
94
|
+
record.review.expires_at = new Date(Date.parse(body.sent_at) + 300000).toISOString();
|
|
95
|
+
record.stage = purpose === 'purchase' ? 'approval_required' : 'review_delivered'; }
|
|
96
|
+
return Response.json(privateView(record));
|
|
97
|
+
}
|
|
98
|
+
if (action === 'private-chat/reply' && purpose === 'purchase' && record.stage === 'approval_required' &&
|
|
99
|
+
record.review?.delivered && typeof body.sent_at === 'string' && Date.parse(body.sent_at) <= Date.now() &&
|
|
100
|
+
Date.parse(body.sent_at) > Date.parse(record.review.expires_at) - 300000 && Date.now() < Date.parse(record.review.expires_at)) {
|
|
101
|
+
if (!['approve', 'decline'].includes(body.text)) invalid();
|
|
102
|
+
calls.purchase_decision++;
|
|
103
|
+
if (body.text === 'approve') { record.purchaseApproved = true; record.stage = 'approved_for_submit'; }
|
|
104
|
+
else { record.status = 'cancelled'; record.stage = 'closed'; delete record.review; }
|
|
105
|
+
return Response.json(privateView(record));
|
|
106
|
+
}
|
|
107
|
+
invalid();
|
|
108
|
+
}
|
|
109
|
+
if (action === 'browser' && ['wallet_required', 'selection_ready', 'details_ready'].includes(record.status)) {
|
|
110
|
+
calls.browser++;
|
|
111
|
+
return Response.json({ content: [{ type: 'text',
|
|
112
|
+
text: 'Synthetic demo checkout\nUSD $1.00\n#checkout-total\n#place-order\n[e1_1] Button Continue' }] });
|
|
113
|
+
}
|
|
114
|
+
if (action === 'details' && record.status === 'selection_ready' && body.checkout_url === syntheticCheckoutUrl) record.status = 'details_ready';
|
|
115
|
+
else if (action === 'approval' && record.status === 'details_ready' && body.checkout_url === syntheticCheckoutUrl &&
|
|
116
|
+
body.amount_cents === 100 && body.currency === 'USD') {
|
|
117
|
+
if ((profile === 'managed_review_v2') !== (body.fill_after_verification === true)) invalid();
|
|
118
|
+
record.fillAfterVerification = body.fill_after_verification === true;
|
|
119
|
+
record.status = 'approval_required'; record.stage = 'cvv_required'; record.revision++;
|
|
120
|
+
record.quote = { checkout_origin: 'https://demos.weaccelerate.com', amount_cents: 100, currency: 'USD' };
|
|
121
|
+
} else if (action === 'fill' && record.status === 'approved' && body.revision === record.revision) {
|
|
122
|
+
if (record.fillAfterVerification) invalid();
|
|
123
|
+
fill(record);
|
|
124
|
+
} else if (action === 'cancel') { calls.cancel++; record.status = 'cancelled'; record.stage = 'closed'; record.purchaseApproved = false; delete record.review; }
|
|
125
|
+
else if (action === 'submit') {
|
|
126
|
+
calls.submit++;
|
|
127
|
+
if (purpose !== 'purchase' || !record.purchaseApproved || record.stage !== 'approved_for_submit' ||
|
|
128
|
+
body.revision !== record.revision || Date.now() >= Date.parse(record.review.expires_at)) return Response.json({ error: { code: 'submit_unavailable' } }, { status: 409 });
|
|
129
|
+
record.status = 'submit_attempted'; record.stage = 'closed'; record.purchaseApproved = false; delete record.review;
|
|
130
|
+
} else invalid();
|
|
131
|
+
return Response.json(view(record));
|
|
132
|
+
};
|
|
133
|
+
const options = { baseUrl: origin, apiKey: 'synthetic_tenant_not_a_real_key'.padEnd(48, 'x'),
|
|
134
|
+
merchantId: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee', fetchImpl };
|
|
135
|
+
const client = new AccelerateClient(options), privateChat = new AcceleratePrivateChatClient(options);
|
|
136
|
+
const host = { client, privateChat,
|
|
137
|
+
resolveContext: async () => ({ ...context }),
|
|
138
|
+
withOwnedCheckout: async (current, id, work) => locked(async () => {
|
|
139
|
+
const saved = owners.get(id);
|
|
140
|
+
if (!saved || Object.keys(context).some(key => current[key] !== context[key] || saved[key] !== current[key])) invalid();
|
|
141
|
+
return work({ ...saved });
|
|
142
|
+
}),
|
|
143
|
+
withCheckoutCreation: async (current, work) => locked(() => {
|
|
144
|
+
if (Object.keys(context).some(key => current[key] !== context[key])) invalid();
|
|
145
|
+
return work({ idempotency_key: 'synthetic-exact-attempt-1', persistCreated: async saved => {
|
|
146
|
+
if (Object.keys(context).some(key => saved[key] !== context[key])) invalid();
|
|
147
|
+
owners.set(saved.checkout_id, { ...saved });
|
|
148
|
+
} });
|
|
149
|
+
}),
|
|
150
|
+
sendPrivateReview: async (current, message) => {
|
|
151
|
+
if (Object.keys(context).some(key => current[key] !== context[key]) || message.contentType !== 'image/png' ||
|
|
152
|
+
!message.bytes.equals(png()) || message.signal.aborted || Date.now() >= Date.parse(message.delivery_expires_at)) invalid();
|
|
153
|
+
const record = [...records.values()].find(value => value.stage === 'review_delivery_required');
|
|
154
|
+
if (!record || !message.text.includes('1.00 USD') || (purpose === 'sandbox_fill_only'
|
|
155
|
+
? !message.text.includes('No purchase approval is needed') || message.text.includes('Do you approve')
|
|
156
|
+
: !message.text.includes('5 minutes from this message'))) invalid();
|
|
157
|
+
const message_id = 'synthetic-review-' + digest(message.idempotency_key);
|
|
158
|
+
if (!messages.has(message_id)) { calls.review_sent++; messages.set(message_id, { checkout: record.id, sent_at: new Date(Date.now()).toISOString() }); }
|
|
159
|
+
return { message_id, sent_at: messages.get(message_id).sent_at };
|
|
160
|
+
},
|
|
161
|
+
dispatchCheckoutEvent: async (current, event) => {
|
|
162
|
+
if (Object.keys(context).some(key => current[key] !== context[key]) ||
|
|
163
|
+
event.version !== 'v1' || !['payment_requested', 'approval_requested', 'review_ready'].includes(event.kind) ||
|
|
164
|
+
event.idempotency_key !== 'accelerate-checkout-event:' + createHash('sha256').update(JSON.stringify([
|
|
165
|
+
'v1', event.kind, event.checkout.id, ['approval_requested', 'review_ready'].includes(event.kind) ? event.checkout.revision : 0])).digest('hex') ||
|
|
166
|
+
!owners.has(event.checkout.id)) invalid();
|
|
167
|
+
if (events.has(event.idempotency_key)) return;
|
|
168
|
+
const record = records.get(event.checkout.id);
|
|
169
|
+
if (event.kind === 'payment_requested' && record.status === 'wallet_required') {
|
|
170
|
+
calls.payment_requested++; record.selected = true; record.status = 'selection_ready'; record.stage = 'preparing_checkout'; record.revision = 1;
|
|
171
|
+
} else if (event.kind === 'approval_requested' && record.stage === 'cvv_required') {
|
|
172
|
+
verify(record);
|
|
173
|
+
} else if (event.kind === 'review_ready' && record.stage === 'review_delivery_required') {
|
|
174
|
+
if (deliverReview) await deliverReview(current, event);
|
|
175
|
+
else await deliverPrivateCheckoutReview({ client: privateChat, state: privateView(record),
|
|
176
|
+
binding: Object.fromEntries(['customer_id', 'workspace_id', 'conversation_id'].map(key => [key, context[key]])),
|
|
177
|
+
messageId: event.idempotency_key, send: message => host.sendPrivateReview(current, message) });
|
|
178
|
+
} else invalid();
|
|
179
|
+
// Only receipt acknowledgement completes this event; a failure retains
|
|
180
|
+
// this exact event for redelivery, without replaying private filling.
|
|
181
|
+
events.add(event.idempotency_key);
|
|
182
|
+
},
|
|
183
|
+
counters: () => ({ ...calls }),
|
|
184
|
+
};
|
|
185
|
+
return host;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Synthetic-only host dependencies. This memory store is deliberately not
|
|
189
|
+
* exported as a production persistence adapter. No browser/provider is created. */
|
|
190
|
+
export function createSyntheticDependencies({ purpose = 'sandbox_fill_only', profile = 'managed_review_v1' } = {}) {
|
|
191
|
+
const fixture = createSyntheticHost({ purpose, profile }); let record, tail = Promise.resolve(), fenced = false;
|
|
192
|
+
const saved = new Map();
|
|
193
|
+
const persistence = { async withLock(_key, work) {
|
|
194
|
+
const previous = tail; let release; tail = new Promise(resolve => { release = resolve; }); await previous;
|
|
195
|
+
try { return await work({ load: async () => record && structuredClone(record), commit: async value => { record = structuredClone(value); } }); }
|
|
196
|
+
finally { release(); }
|
|
197
|
+
} };
|
|
198
|
+
const extensionRelease = {
|
|
199
|
+
schema_version: 1, release_id: 'accelerate-extension-0.0.0.2', status: 'approved', extension_version: '0.0.0.2',
|
|
200
|
+
artifact: { format: 'zip', sha256: 'a'.repeat(64), bytes: 1234 },
|
|
201
|
+
bridge: { protocol: 'accelerate_extension_bridge_v1', sha256: 'b'.repeat(64) },
|
|
202
|
+
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' },
|
|
204
|
+
qualifications: [{ provider: 'connector', scope: 'synthetic_only', evidence: 'docs/evidence/synthetic-starter.json' }],
|
|
205
|
+
}; // Fake metadata for this in-process fixture only; never load it in live mode.
|
|
206
|
+
const browser = { owner: context, browser: { provider: 'connector', connector_id: 'synthetic_cdp', browser_id: 'existing:browser:1', custody_mode: 'customer_backend_trusted' },
|
|
207
|
+
document_id: 'existing:document:1', checkout_url: syntheticCheckoutUrl, expires_at: new Date(Date.now() + 1800000).toISOString() };
|
|
208
|
+
const browserConnector = createExistingBrowserConnector({ connectorId: 'synthetic_cdp', extensionRelease, host: {
|
|
209
|
+
resolveOwnedBrowser: async () => structuredClone(browser),
|
|
210
|
+
inspectBrowser: async () => ({ ...structuredClone(browser), recording_disabled: true,
|
|
211
|
+
extension: { success: true, initialized: true, wasmInitialized: true, autosaveDisabled: true, boundedRetentionSupported: true, maximumRetentionMs: 480000,
|
|
212
|
+
extension_version: '0.0.0.2', artifact_sha256: 'a'.repeat(64), bridge_protocol: 'accelerate_extension_bridge_v1' },
|
|
213
|
+
automation: { ordinary_operations_drained: fenced, agent_access_fenced: fenced, fence_id: fenced ? 'synthetic:fence:1' : null } }),
|
|
214
|
+
drainAndFence: async () => { fenced = true; return { ordinary_operations_drained: true, agent_access_fenced: true, fence_id: 'synthetic:fence:1' }; },
|
|
215
|
+
bindCheckout: async (_context, value) => { saved.set(value.checkout_id, value); },
|
|
216
|
+
resolveCheckoutBrowser: async (_context, { checkout_id }) => { const { checkout_id: _id, attempt_key: _key, ...value } = saved.get(checkout_id); return value; },
|
|
217
|
+
confirmCleanup: async () => ({ provider_browser_stopped: true, credentials_cleared: true, fence_retained: true }),
|
|
218
|
+
} });
|
|
219
|
+
return { client: fixture.client, privateChat: fixture.privateChat, resolveContext: fixture.resolveContext,
|
|
220
|
+
persistence, browserConnector,
|
|
221
|
+
authenticate: async () => { throw Error('Synthetic mode has no public webhook.'); },
|
|
222
|
+
messaging: { sendText: async () => ({ message_id: 'synthetic-private-prompt', sent_at: new Date().toISOString() }), sendReview: fixture.sendPrivateReview },
|
|
223
|
+
admitOrdinaryMessage: async () => { throw Error('Synthetic mode has no AI ingress.'); },
|
|
224
|
+
dispatchSafeProgress: async () => {}, counters: fixture.counters, record: () => structuredClone(record),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { configureStarter } from '../runtime.mjs';
|
|
4
|
+
import { syntheticCheckoutUrl } from '../synthetic.mjs';
|
|
5
|
+
async function fixture(env = {}) {
|
|
6
|
+
const tools = new Map(); const integration = await configureStarter({ registerTool(name, _config, callback) { tools.set(name, callback); } }, env);
|
|
7
|
+
const call = async (name, args) => { const result = (await tools.get(name)(args, {})).structuredContent; assert.equal(result.ok, true, JSON.stringify(result)); return result; };
|
|
8
|
+
return { ...integration, tools, call };
|
|
9
|
+
}
|
|
10
|
+
test('explicit v2 starter verifies privately then fills and acknowledges review without a model fill tool', async t => {
|
|
11
|
+
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 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);
|
|
14
|
+
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
|
+
assert.equal(f.profile, 'managed_review_v2');
|
|
16
|
+
await f.call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
17
|
+
assert.equal((await f.call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'selection_ready');
|
|
18
|
+
await f.call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
|
|
19
|
+
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;
|
|
20
|
+
assert.equal(quote.fill_after_verification, true);
|
|
21
|
+
assert.equal((await f.call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'ready_for_submit');
|
|
22
|
+
assert.equal(f.counters().fill, 1); assert.equal(f.counters().review_sent, 1); assert.equal(f.counters().review_acknowledged, 1);
|
|
23
|
+
// Safe state reads recover delivery only; they cannot replay the armed fill.
|
|
24
|
+
await f.call('accelerate_checkout_status', { checkout_id: id });
|
|
25
|
+
assert.equal(f.counters().fill, 1); assert.equal(f.counters().private_verification, 1);
|
|
26
|
+
assert.equal(f.counters().purchase_decision, 0);
|
|
27
|
+
await f.call('accelerate_checkout_cancel', { checkout_id: id });
|
|
28
|
+
assert.equal(f.counters().submit, 0); assert.equal(f.counters().cancel, 1); assert.equal(f.counters().external, 0);
|
|
29
|
+
});
|
|
30
|
+
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' });
|
|
32
|
+
assert.equal(f.tools.has('accelerate_checkout_fill'), true); assert.equal(f.tools.size, 8);
|
|
33
|
+
const id = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout.id;
|
|
34
|
+
await f.call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
35
|
+
await f.call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
|
|
36
|
+
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;
|
|
37
|
+
assert.equal(quote.fill_after_verification, undefined);
|
|
38
|
+
assert.equal((await f.call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'approved');
|
|
39
|
+
assert.equal(f.counters().fill, 0);
|
|
40
|
+
await f.call('accelerate_checkout_fill', { checkout_id: id, revision: quote.revision });
|
|
41
|
+
const repeated = (await f.tools.get('accelerate_checkout_fill')({ checkout_id: id, revision: quote.revision }, {})).structuredContent;
|
|
42
|
+
assert.equal(repeated.ok, false); assert.equal(f.counters().fill, 1);
|
|
43
|
+
assert.equal(f.counters().review_acknowledged, 1); assert.equal(f.counters().submit, 0);
|
|
44
|
+
await f.call('accelerate_checkout_cancel', { checkout_id: id });
|
|
45
|
+
});
|
|
46
|
+
test('v2 purchase still requires a fresh private decision and explicit submit after automatic fill', async () => {
|
|
47
|
+
const f = await fixture({ ACCELERATE_STARTER_PURPOSE: 'purchase', ACCELERATE_ENABLE_PURCHASE_SUBMIT: 'true' });
|
|
48
|
+
assert.equal(f.tools.has('accelerate_checkout_fill'), false); assert.equal(f.tools.has('accelerate_checkout_submit'), true);
|
|
49
|
+
const id = (await f.call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl })).checkout.id;
|
|
50
|
+
await f.call('accelerate_checkout_request_payment', { checkout_id: id });
|
|
51
|
+
assert.equal(f.counters().fill, 0);
|
|
52
|
+
await f.call('accelerate_checkout_apply_details', { checkout_id: id, checkout_url: syntheticCheckoutUrl });
|
|
53
|
+
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;
|
|
54
|
+
const current = (await f.call('accelerate_checkout_status', { checkout_id: id })).checkout;
|
|
55
|
+
assert.equal(current.status, 'ready_for_submit'); assert.equal(current.purchase_approved, false);
|
|
56
|
+
const remaining = Date.parse(current.submit_expires_at) - Date.now();
|
|
57
|
+
assert.ok(remaining > 290000 && remaining <= 300000);
|
|
58
|
+
assert.equal(f.counters().fill, 1); assert.equal(f.counters().review_acknowledged, 1);
|
|
59
|
+
assert.equal(f.counters().submit, 0); assert.equal(f.counters().purchase_decision, 0);
|
|
60
|
+
const rejected = (await f.tools.get('accelerate_checkout_submit')({ checkout_id: id, revision: quote.revision }, {})).structuredContent;
|
|
61
|
+
assert.equal(rejected.ok, false); assert.equal(f.counters().submit, 0);
|
|
62
|
+
await f.call('accelerate_checkout_cancel', { checkout_id: id });
|
|
63
|
+
});
|
|
64
|
+
test('live setup requires a trusted backend module and preserves explicit custody selection', async () => {
|
|
65
|
+
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_STARTER_MODE: 'live' }), /trusted absolute/);
|
|
66
|
+
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_STARTER_MODE: 'unknown' }), /Unsupported/);
|
|
67
|
+
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_CHECKOUT_PROFILE: 'unknown' }), /profile/);
|
|
68
|
+
await assert.rejects(configureStarter({ registerTool() {} }, { ACCELERATE_ENABLE_PURCHASE_SUBMIT: 'true' }), /purchase policy/);
|
|
69
|
+
});
|