@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { createSyntheticHost, syntheticCheckoutUrl } from '../synthetic.mjs';
|
|
9
|
+
import { configureStarter } from '../runtime.mjs';
|
|
10
|
+
|
|
11
|
+
const event = (kind, checkout) => ({ version: 'v1', kind, checkout,
|
|
12
|
+
idempotency_key: 'accelerate-checkout-event:' + createHash('sha256')
|
|
13
|
+
.update(JSON.stringify(['v1', kind, checkout.id, ['approval_requested', 'review_ready'].includes(kind) ? checkout.revision : 0])).digest('hex') });
|
|
14
|
+
async function prepared(purpose = 'sandbox_fill_only') {
|
|
15
|
+
const host = createSyntheticHost({ purpose }), context = await host.resolveContext();
|
|
16
|
+
const checkout = await host.withCheckoutCreation(context, async creation => {
|
|
17
|
+
const value = await host.client.checkouts.create({ ...context, start_url: syntheticCheckoutUrl,
|
|
18
|
+
idempotency_key: creation.idempotency_key, interaction_mode: 'chat', purpose, approval_policy: 'after_fill' });
|
|
19
|
+
await creation.persistCreated({ checkout_id: value.id, ...context }); return value;
|
|
20
|
+
});
|
|
21
|
+
const dispatch = (kind, value) => host.dispatchCheckoutEvent(context, event(kind, value));
|
|
22
|
+
const binding = Object.fromEntries(['customer_id', 'workspace_id', 'conversation_id'].map(key => [key, context[key]]));
|
|
23
|
+
const reachFill = async () => {
|
|
24
|
+
await dispatch('payment_requested', checkout);
|
|
25
|
+
await host.client.checkouts.applyDetails(checkout.id, { checkout_url: syntheticCheckoutUrl });
|
|
26
|
+
const quote = await host.client.checkouts.requestApproval(checkout.id, { checkout_url: syntheticCheckoutUrl,
|
|
27
|
+
amount_cents: 100, currency: 'USD', checkout_total_selector: '#checkout-total', submit_selector: '#place-order' });
|
|
28
|
+
await dispatch('approval_requested', quote);
|
|
29
|
+
return host.client.checkouts.fill(checkout.id, { revision: quote.revision });
|
|
30
|
+
};
|
|
31
|
+
return { host, context, checkout, dispatch, binding, reachFill };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('default demo defers verification, verifies privately, fills, acknowledges safe review and cancels without approval or network', async t => {
|
|
35
|
+
const previous = globalThis.fetch; globalThis.fetch = () => assert.fail('Synthetic mode cannot use external fetch.');
|
|
36
|
+
t.after(() => { globalThis.fetch = previous; });
|
|
37
|
+
const { host, context, checkout, dispatch, binding, reachFill } = await prepared();
|
|
38
|
+
assert.equal(checkout.approval_policy, 'after_fill'); assert.equal(checkout.purchase_approved, false);
|
|
39
|
+
assert.equal(checkout.submit_allowed, false); assert.equal(checkout.status, 'wallet_required'); assert.equal(checkout.card, undefined);
|
|
40
|
+
await host.client.checkouts.browser(checkout.id, { operation: 'browser_snapshot', arguments: { filter: 'all' } });
|
|
41
|
+
assert.equal((await host.client.checkouts.get(checkout.id)).status, 'wallet_required');
|
|
42
|
+
assert.equal(host.counters().payment_requested, 0); assert.equal(host.counters().private_verification, 0);
|
|
43
|
+
await assert.rejects(dispatch('created', checkout));
|
|
44
|
+
await assert.rejects(host.dispatchCheckoutEvent(context, { ...event('payment_requested', checkout), idempotency_key: 'invalid' }));
|
|
45
|
+
const filled = await reachFill();
|
|
46
|
+
assert.equal(filled.status, 'ready_for_submit'); assert.equal(filled.purchase_approved, false);
|
|
47
|
+
assert.equal((await host.privateChat.chat.get(checkout.id, binding)).stage, 'review_delivery_required');
|
|
48
|
+
assert.equal(host.counters().review_acknowledged, 0);
|
|
49
|
+
await dispatch('review_ready', filled); await dispatch('review_ready', filled);
|
|
50
|
+
const reviewed = await host.privateChat.chat.get(checkout.id, binding);
|
|
51
|
+
assert.equal(reviewed.stage, 'review_delivered'); assert.equal(reviewed.review.approval_window_ms, 300000);
|
|
52
|
+
assert.ok(Date.parse(reviewed.review.expires_at) - Date.now() > 299000);
|
|
53
|
+
await assert.rejects(host.privateChat.chat.reply(checkout.id, { ...binding, message_id: 'no-approval-for-fill-only',
|
|
54
|
+
text: 'approve', sent_at: new Date().toISOString() }));
|
|
55
|
+
assert.equal(host.counters().purchase_decision, 0); assert.equal(host.counters().submit, 0);
|
|
56
|
+
assert.equal((await host.client.checkouts.cancel(checkout.id)).status, 'cancelled');
|
|
57
|
+
assert.deepEqual(host.counters(), { create: 1, browser: 1, payment_requested: 1, approval_requested: 1,
|
|
58
|
+
private_verification: 1, fill: 1, review: 1, review_sent: 1, review_acknowledged: 1,
|
|
59
|
+
purchase_decision: 0, submit: 0, cancel: 1, external: 0 });
|
|
60
|
+
await assert.rejects(host.withOwnedCheckout({ ...context, customer_id: 'another-customer' }, checkout.id, () => assert.fail('Wrong owner admitted.')));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('review delivery failure retains the same event and retries image delivery without replaying fill', async () => {
|
|
64
|
+
const { host, checkout, dispatch, binding, reachFill } = await prepared();
|
|
65
|
+
const filled = await reachFill(), send = host.sendPrivateReview;
|
|
66
|
+
let borrowed;
|
|
67
|
+
host.sendPrivateReview = async (_context, message) => { borrowed = message.bytes; throw Error('SYNTHETIC_QUEUED_ONLY'); };
|
|
68
|
+
await assert.rejects(dispatch('review_ready', filled));
|
|
69
|
+
assert.ok(borrowed.every(byte => byte === 0));
|
|
70
|
+
assert.equal((await host.privateChat.chat.get(checkout.id, binding)).stage, 'review_delivery_required');
|
|
71
|
+
assert.equal(host.counters().review_acknowledged, 0); assert.equal(host.counters().fill, 1);
|
|
72
|
+
host.sendPrivateReview = send; await dispatch('review_ready', filled);
|
|
73
|
+
assert.equal(host.counters().review_acknowledged, 1); assert.equal(host.counters().fill, 1);
|
|
74
|
+
assert.equal(host.counters().submit, 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('purchase profile waits the full five-minute receipt-bound window and requires a distinct final submit', async t => {
|
|
78
|
+
const originalNow = Date.now; let now = originalNow(); Date.now = () => now;
|
|
79
|
+
t.after(() => { Date.now = originalNow; });
|
|
80
|
+
const { host, checkout, dispatch, binding, reachFill } = await prepared('purchase');
|
|
81
|
+
const filled = await reachFill();
|
|
82
|
+
await assert.rejects(host.privateChat.chat.reply(checkout.id, { ...binding, message_id: 'too-early', text: 'approve', sent_at: new Date(now).toISOString() }));
|
|
83
|
+
await assert.rejects(host.client.checkouts.submit(checkout.id, { revision: filled.revision }), { code: 'submit_unavailable' });
|
|
84
|
+
now += 45000; await dispatch('review_ready', filled);
|
|
85
|
+
const reviewed = await host.privateChat.chat.get(checkout.id, binding);
|
|
86
|
+
assert.equal(reviewed.stage, 'approval_required'); assert.equal(Date.parse(reviewed.review.expires_at), now + 300000);
|
|
87
|
+
now += 299999;
|
|
88
|
+
const approved = await host.privateChat.chat.reply(checkout.id, { ...binding, message_id: 'private-decision', text: 'approve', sent_at: new Date(now).toISOString() });
|
|
89
|
+
assert.equal(approved.stage, 'approved_for_submit'); assert.equal(approved.checkout.purchase_approved, true);
|
|
90
|
+
assert.equal(host.counters().submit, 1); // Only the deliberately denied early request.
|
|
91
|
+
assert.equal((await host.client.checkouts.get(checkout.id)).status, 'ready_for_submit');
|
|
92
|
+
const submitted = await host.client.checkouts.submit(checkout.id, { revision: filled.revision });
|
|
93
|
+
assert.equal(submitted.status, 'submit_attempted'); assert.equal(submitted.payment_confirmed, false);
|
|
94
|
+
await assert.rejects(host.client.checkouts.submit(checkout.id, { revision: filled.revision }), { code: 'submit_unavailable' });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('purchase decision at five minutes is expired, and a private decline cancels without submission', async t => {
|
|
98
|
+
const originalNow = Date.now; let now = originalNow(); Date.now = () => now;
|
|
99
|
+
t.after(() => { Date.now = originalNow; });
|
|
100
|
+
const expired = await prepared('purchase'), filled = await expired.reachFill();
|
|
101
|
+
await expired.dispatch('review_ready', filled); now += 300000;
|
|
102
|
+
await assert.rejects(expired.host.privateChat.chat.reply(expired.checkout.id, { ...expired.binding,
|
|
103
|
+
message_id: 'late-decision', text: 'approve', sent_at: new Date(now).toISOString() }));
|
|
104
|
+
assert.equal(expired.host.counters().purchase_decision, 0); assert.equal(expired.host.counters().submit, 0);
|
|
105
|
+
const declined = await prepared('purchase'), next = await declined.reachFill();
|
|
106
|
+
await declined.dispatch('review_ready', next); now++;
|
|
107
|
+
const closed = await declined.host.privateChat.chat.reply(declined.checkout.id, { ...declined.binding,
|
|
108
|
+
message_id: 'private-decline', text: 'decline', sent_at: new Date(now).toISOString() });
|
|
109
|
+
assert.equal(closed.checkout.status, 'cancelled'); assert.equal(declined.host.counters().submit, 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('starter registers the supported managed review profile and never accepts model purchase policy', async () => {
|
|
113
|
+
const tools = new Map();
|
|
114
|
+
const configured = await configureStarter({ registerTool(name, config, callback) { tools.set(name, { config, callback }); } }, {});
|
|
115
|
+
assert.equal(configured.mode, 'synthetic'); assert.equal(tools.size, 8); assert.equal(tools.has('accelerate_checkout_submit'), false);
|
|
116
|
+
const call = async (name, args) => (await tools.get(name).callback(args, {})).structuredContent;
|
|
117
|
+
const created = await call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl });
|
|
118
|
+
assert.equal(created.ok, true); assert.equal(created.checkout.approval_policy, 'after_fill');
|
|
119
|
+
const rejected = await call('accelerate_checkout_start', { start_url: syntheticCheckoutUrl, purpose: 'purchase' });
|
|
120
|
+
assert.equal(rejected.ok, false); assert.equal(rejected.error.code, 'invalid_input');
|
|
121
|
+
const purchaseTools = new Map();
|
|
122
|
+
await configureStarter({ registerTool(name, _config, callback) { purchaseTools.set(name, callback); } },
|
|
123
|
+
{ ACCELERATE_STARTER_PURPOSE: 'purchase', ACCELERATE_ENABLE_PURCHASE_SUBMIT: 'true' });
|
|
124
|
+
assert.equal(purchaseTools.has('accelerate_checkout_submit'), true);
|
|
125
|
+
for (const env of [{ ACCELERATE_STARTER_PURPOSE: 'unknown' }, { ACCELERATE_ENABLE_PURCHASE_SUBMIT: 'true' },
|
|
126
|
+
{ ACCELERATE_ENABLE_PURCHASE_SUBMIT: '1' }]) await assert.rejects(configureStarter({ registerTool() { assert.fail('Invalid policy exposed tools.'); } }, env));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('real mode fails closed without authenticated hooks and an actual private review sender', async t => {
|
|
130
|
+
const directory = await mkdtemp(join(tmpdir(), 'accelerate-starter-host-'));
|
|
131
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
132
|
+
const modulePath = join(directory, 'missing-private-sender.mjs');
|
|
133
|
+
await writeFile(modulePath, "export async function createTrustedHost(){return Object.fromEntries(['resolveContext','withOwnedCheckout','withCheckoutCreation','dispatchCheckoutEvent'].map(key=>[key,async()=>{}]));}\n");
|
|
134
|
+
const live = { ACCELERATE_STARTER_MODE: 'live', ACCELERATE_CONTEXT_MODULE: modulePath,
|
|
135
|
+
ACCELERATE_API_KEY: 'synthetic_not_real_key'.padEnd(48, 'x'), ACCELERATE_MERCHANT_ID: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
|
|
136
|
+
ACCELERATE_BASE_URL: 'https://managed.example' };
|
|
137
|
+
for (const env of [{ ACCELERATE_STARTER_MODE: 'live' }, { ACCELERATE_STARTER_MODE: 'live', ACCELERATE_CONTEXT_MODULE: './relative.mjs' },
|
|
138
|
+
{ ...live, ACCELERATE_CONTEXT_MODULE: new URL('../trusted-context.example.mjs', import.meta.url).pathname }, live,
|
|
139
|
+
{ ACCELERATE_STARTER_MODE: 'unknown' }]) {
|
|
140
|
+
await assert.rejects(configureStarter({ registerTool() { assert.fail('Unverified live host cannot expose tools.'); } }, env));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a live host cannot acknowledge queued, downgraded or mismatched private review delivery', async t => {
|
|
145
|
+
const directory = await mkdtemp(join(tmpdir(), 'accelerate-starter-unconfirmed-'));
|
|
146
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
147
|
+
const modulePath = join(directory, 'queued-host.mjs');
|
|
148
|
+
const context = { customer_id: 'fixture-customer', workspace_id: 'fixture-workspace',
|
|
149
|
+
conversation_id: 'fixture-conversation', phone_number: '+15555550123' };
|
|
150
|
+
await writeFile(modulePath, `const context=${JSON.stringify(context)};
|
|
151
|
+
export async function createTrustedHost(){return {
|
|
152
|
+
resolveContext:async()=>({...context}),
|
|
153
|
+
withOwnedCheckout:async(_context,id,work)=>work({checkout_id:id,...context}),
|
|
154
|
+
withCheckoutCreation:async()=>{throw Error('Unexpected creation.');},
|
|
155
|
+
dispatchCheckoutEvent:async()=>{},
|
|
156
|
+
sendPrivateReview:async()=>{throw Error('Queued delivery is not a receipt.');}
|
|
157
|
+
};}\n`);
|
|
158
|
+
const id = 'mcs_' + 'q'.repeat(43), now = Date.now();
|
|
159
|
+
const checkout = { id, status: 'ready_for_submit', revision: 2, expires_at: new Date(now + 900000).toISOString(),
|
|
160
|
+
submit_expires_at: new Date(now + 420000).toISOString(), purpose: 'sandbox_fill_only', approval_policy: 'after_fill',
|
|
161
|
+
purchase_approved: false, submit_allowed: false, retry_safe: false, payment_confirmed: false,
|
|
162
|
+
quote: { checkout_origin: 'https://demos.weaccelerate.com', amount_cents: 100, currency: 'USD' } };
|
|
163
|
+
const previous = globalThis.fetch, paths = [];
|
|
164
|
+
let scenario = 'queued';
|
|
165
|
+
globalThis.fetch = async url => {
|
|
166
|
+
const path = new URL(url).pathname; paths.push(path);
|
|
167
|
+
if (path === `/v1/checkouts/${id}/fill`) return Response.json(checkout);
|
|
168
|
+
assert.equal(path, `/v1/checkouts/${id}/private-chat/state`);
|
|
169
|
+
const value = { checkout: { ...checkout }, stage: 'review_delivery_required', handled: true, duplicate: false, cards: [], addresses: [],
|
|
170
|
+
approval: { revision: 2, ...checkout.quote }, review: { id: 'rvr_' + 'r'.repeat(43), delivered: false,
|
|
171
|
+
expires_at: checkout.submit_expires_at, approval_window_ms: 300000, delivery_expires_at: new Date(now + 60000).toISOString() } };
|
|
172
|
+
if (scenario !== 'queued') { value.stage = 'review_delivered'; value.review.delivered = true; }
|
|
173
|
+
if (scenario === 'legacy-window') { delete value.review.approval_window_ms; delete value.review.delivery_expires_at; }
|
|
174
|
+
if (scenario === 'wrong-purpose') { value.checkout.purpose = 'purchase'; value.checkout.submit_allowed = true; value.stage = 'approval_required'; }
|
|
175
|
+
if (scenario === 'wrong-quote') { value.checkout.quote = { ...checkout.quote, amount_cents: 200 }; value.approval.amount_cents = 200; }
|
|
176
|
+
return Response.json(value);
|
|
177
|
+
};
|
|
178
|
+
t.after(() => { globalThis.fetch = previous; });
|
|
179
|
+
const tools = new Map();
|
|
180
|
+
await configureStarter({ registerTool(name, _config, callback) { tools.set(name, callback); } }, {
|
|
181
|
+
ACCELERATE_STARTER_MODE: 'live', ACCELERATE_CONTEXT_MODULE: modulePath,
|
|
182
|
+
ACCELERATE_API_KEY: 'synthetic_not_real_key'.padEnd(48, 'x'), ACCELERATE_MERCHANT_ID: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
|
|
183
|
+
ACCELERATE_BASE_URL: 'https://managed.example',
|
|
184
|
+
});
|
|
185
|
+
for (scenario of ['queued', 'legacy-window', 'wrong-purpose', 'wrong-quote']) {
|
|
186
|
+
paths.length = 0;
|
|
187
|
+
const result = (await tools.get('accelerate_checkout_fill')({ checkout_id: id, revision: 2 }, {})).structuredContent;
|
|
188
|
+
assert.equal(result.ok, false, scenario); assert.equal(result.error.code, 'checkout_event_unconfirmed', scenario); assert.equal(result.checkout.id, id);
|
|
189
|
+
assert.deepEqual(paths, [`/v1/checkouts/${id}/fill`, `/v1/checkouts/${id}/private-chat/state`]);
|
|
190
|
+
}
|
|
191
|
+
assert.equal(paths.some(path => /\/(?:submit|cancel)$/.test(path)), false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('durable review events accept reordered quote keys and reject changed quote or checkout before image delivery', async t => {
|
|
195
|
+
const directory = await mkdtemp(join(tmpdir(), 'accelerate-starter-durable-'));
|
|
196
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
197
|
+
const modulePath = join(directory, 'durable-host.mjs');
|
|
198
|
+
const context = { customer_id: 'fixture-customer', workspace_id: 'fixture-workspace',
|
|
199
|
+
conversation_id: 'fixture-conversation', phone_number: '+15555550123' };
|
|
200
|
+
await writeFile(modulePath, `const context=${JSON.stringify(context)}; let scenario;
|
|
201
|
+
export function setScenario(value){scenario=value;}
|
|
202
|
+
export async function createTrustedHost({deliverReview}){return {
|
|
203
|
+
resolveContext:async()=>({...context}),
|
|
204
|
+
withOwnedCheckout:async(_context,id,work)=>work({checkout_id:id,...context}),
|
|
205
|
+
withCheckoutCreation:async()=>{throw Error('Unexpected creation.');},
|
|
206
|
+
dispatchCheckoutEvent:async(context,event)=>{
|
|
207
|
+
const durable=JSON.parse(JSON.stringify(event)),quote=durable.checkout.quote;
|
|
208
|
+
durable.checkout.quote={currency:quote.currency,amount_cents:quote.amount_cents,checkout_origin:quote.checkout_origin};
|
|
209
|
+
if(scenario==='changed-quote')durable.checkout.quote.amount_cents++;
|
|
210
|
+
if(scenario==='changed-id')durable.checkout.id='mcs_'+'z'.repeat(43);
|
|
211
|
+
await deliverReview(context,durable);
|
|
212
|
+
},
|
|
213
|
+
sendPrivateReview:async()=>({message_id:'durable-provider-receipt',sent_at:new Date().toISOString()})
|
|
214
|
+
};}\n`);
|
|
215
|
+
const module = await import(pathToFileURL(modulePath).href), id = 'mcs_' + 'd'.repeat(43), now = Date.now();
|
|
216
|
+
const checkout = { id, status: 'ready_for_submit', revision: 2, expires_at: new Date(now + 900000).toISOString(),
|
|
217
|
+
submit_expires_at: new Date(now + 420000).toISOString(), purpose: 'sandbox_fill_only', approval_policy: 'after_fill',
|
|
218
|
+
purchase_approved: false, submit_allowed: false, retry_safe: false, payment_confirmed: false,
|
|
219
|
+
quote: { checkout_origin: 'https://demos.weaccelerate.com', amount_cents: 100, currency: 'USD' } };
|
|
220
|
+
const review = { id: 'rvr_' + 'r'.repeat(43), delivered: false, expires_at: checkout.submit_expires_at,
|
|
221
|
+
approval_window_ms: 300000, delivery_expires_at: new Date(now + 60000).toISOString() };
|
|
222
|
+
let acknowledged = false, expiresAt = checkout.submit_expires_at;
|
|
223
|
+
const previous = globalThis.fetch, paths = [];
|
|
224
|
+
globalThis.fetch = async (url, request) => {
|
|
225
|
+
const path = new URL(url).pathname; paths.push(path);
|
|
226
|
+
if (path.endsWith('/fill')) return Response.json(checkout);
|
|
227
|
+
if (path.endsWith('/private-chat/review')) return new Response(Buffer.from([137,80,78,71,13,10,26,10,1]), { headers: {
|
|
228
|
+
'Content-Type': 'image/png', 'X-Accelerate-Review-Id': review.id, 'X-Accelerate-Review-Expires-At': review.expires_at,
|
|
229
|
+
'X-Accelerate-Review-Amount-Cents': '100', 'X-Accelerate-Review-Currency': 'USD',
|
|
230
|
+
'X-Accelerate-Review-Approval-Window-Ms': '300000', 'X-Accelerate-Review-Delivery-Expires-At': review.delivery_expires_at } });
|
|
231
|
+
if (path.endsWith('/private-chat/review-delivered')) {
|
|
232
|
+
const receipt = JSON.parse(request.body); acknowledged = true;
|
|
233
|
+
expiresAt = new Date(Date.parse(receipt.sent_at) + 300000).toISOString();
|
|
234
|
+
} else assert.ok(path.endsWith('/private-chat/state'));
|
|
235
|
+
return Response.json({ checkout: { ...checkout, submit_expires_at: expiresAt },
|
|
236
|
+
stage: acknowledged ? 'review_delivered' : 'review_delivery_required', handled: true, duplicate: false, cards: [], addresses: [],
|
|
237
|
+
approval: { revision: 2, ...checkout.quote }, review: { ...review, delivered: acknowledged, expires_at: expiresAt } });
|
|
238
|
+
};
|
|
239
|
+
t.after(() => { globalThis.fetch = previous; });
|
|
240
|
+
const tools = new Map();
|
|
241
|
+
await configureStarter({ registerTool(name, _config, callback) { tools.set(name, callback); } }, {
|
|
242
|
+
ACCELERATE_STARTER_MODE: 'live', ACCELERATE_CONTEXT_MODULE: modulePath,
|
|
243
|
+
ACCELERATE_API_KEY: 'synthetic_not_real_key'.padEnd(48, 'x'), ACCELERATE_MERCHANT_ID: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
|
|
244
|
+
ACCELERATE_BASE_URL: 'https://managed.example',
|
|
245
|
+
});
|
|
246
|
+
for (const scenario of ['reordered', 'changed-quote', 'changed-id']) {
|
|
247
|
+
module.setScenario(scenario); paths.length = 0; acknowledged = false; expiresAt = checkout.submit_expires_at;
|
|
248
|
+
const result = (await tools.get('accelerate_checkout_fill')({ checkout_id: id, revision: 2 }, {})).structuredContent;
|
|
249
|
+
assert.equal(result.ok, scenario === 'reordered', scenario);
|
|
250
|
+
assert.equal(acknowledged, scenario === 'reordered', scenario);
|
|
251
|
+
assert.equal(paths.filter(path => path.endsWith('/private-chat/review')).length, scenario === 'reordered' ? 1 : 0);
|
|
252
|
+
if (scenario !== 'reordered') {
|
|
253
|
+
assert.equal(result.error.code, 'checkout_event_unconfirmed'); assert.equal(paths.length, 2);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-mode contract ONLY. This deliberately cannot authenticate anyone.
|
|
3
|
+
* Replace with your existing authenticated backend and durable ownership store.
|
|
4
|
+
* Never turn MCP arguments/_meta into identity or use synthetic identities here.
|
|
5
|
+
*/
|
|
6
|
+
export async function createTrustedHost({ client, privateChat, deliverReview }) {
|
|
7
|
+
// Return these existing application hooks:
|
|
8
|
+
// resolveContext(extra) -> verified {customer_id,workspace_id,conversation_id,phone_number}
|
|
9
|
+
// withOwnedCheckout(context,id,work) -> work(savedBinding) under the owner lock
|
|
10
|
+
// savedBinding is EXACT {checkout_id,customer_id,workspace_id,conversation_id,phone_number}.
|
|
11
|
+
// withCheckoutCreation(context,work) -> work({idempotency_key,persistCreated})
|
|
12
|
+
// persistCreated(savedBinding) resolves only after durable ownership acceptance.
|
|
13
|
+
// Retain the unresolved-attempt fence if create/persist acknowledgement is lost.
|
|
14
|
+
// This starter sets verificationTiming:'on_payment'. Creating its browser
|
|
15
|
+
// does not dispatch an event or start private verification. The agent calls
|
|
16
|
+
// accelerate_checkout_request_payment only when the checkout needs payment.
|
|
17
|
+
// dispatchCheckoutEvent(context,event) durably accepts/deduplicates the safe
|
|
18
|
+
// payment_requested/approval_requested event. payment_requested invokes
|
|
19
|
+
// privateChat.chat.start; approval_requested invokes privateChat.chat.get.
|
|
20
|
+
// Render and privately deliver the prompt; recover via this durable outbox
|
|
21
|
+
// if acknowledgement is lost. Status tools never trigger prompt delivery.
|
|
22
|
+
// The bundled runtime selects profile:'managed_review_v1': on_payment,
|
|
23
|
+
// after_fill, and a full five minutes after confirmed review delivery.
|
|
24
|
+
// approval_requested prepares fresh CVV privately, without purchase approval.
|
|
25
|
+
// dispatchCheckoutEvent must durably deduplicate review_ready and invoke
|
|
26
|
+
// await deliverReview(context,event) from that same event worker. Resolve the
|
|
27
|
+
// event only AFTER the helper confirms the provider receipt and service ACK;
|
|
28
|
+
// preserve unresolved delivery work on failure. Retry this exact review event,
|
|
29
|
+
// never fill again. Already-acknowledged reviews do not send another image.
|
|
30
|
+
// sendPrivateReview(context,message) -> {message_id,sent_at} is REQUIRED.
|
|
31
|
+
// Authenticate the private destination from context. Upload message.bytes and
|
|
32
|
+
// send message.text TOGETHER, using message.idempotency_key. Honor its signal
|
|
33
|
+
// and delivery_expires_at. Return the actual provider-confirmed sent receipt;
|
|
34
|
+
// never synthesize sent_at or acknowledge a queued-only send. The SDK fetches
|
|
35
|
+
// and verifies the safe PNG, sends it through this hook, then wipes its bytes.
|
|
36
|
+
// Private replies must be routed through privateChat.chat.reply before any
|
|
37
|
+
// message/history/model persistence; OTP/CVV never enter model tool arguments.
|
|
38
|
+
// sandbox_fill_only sends an informational review and needs no purchase
|
|
39
|
+
// approval. The agent cancels that test after delivery; submit stays disabled.
|
|
40
|
+
// A qualified purchase waits for the private simple approve/decline decision;
|
|
41
|
+
// approval does not submit. Explicit server configuration exposes the separate
|
|
42
|
+
// agent submit tool. The filled page remains private throughout this interval.
|
|
43
|
+
// Fresh approval replies must carry authenticated provider sent_at and the
|
|
44
|
+
// reply target when present. Do not synthesize sent_at from receipt time.
|
|
45
|
+
// The separate on_start compatibility profile uses a created event instead.
|
|
46
|
+
// Do not log either client, raw messages, callbacks or private webhook bodies.
|
|
47
|
+
void client; void privateChat; void deliverReview;
|
|
48
|
+
throw Error('Implement the authenticated host contract before enabling real mode.');
|
|
49
|
+
}
|