@weaccelerateinc/agent-sdk 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/approved-extension-releases/0.0.0.1.json +3 -1
- package/approved-extension-releases/0.0.0.2.json +3 -1
- package/approved-extension.js +1 -1
- package/browser-connector.d.ts +22 -0
- package/browser-connector.js +41 -2
- package/browser-use-host.d.ts +3 -0
- package/browser-use-host.js +2 -0
- package/checkout-diagnostics.d.ts +13 -2
- package/checkout-diagnostics.js +18 -4
- package/cli.js +4 -1
- package/host-adapter.js +9 -5
- package/index.d.ts +48 -4
- package/index.js +74 -11
- package/managed-mcp.d.ts +9 -3
- package/managed-mcp.js +51 -6
- package/package.json +1 -1
- package/partner-starter/.env.example +4 -2
- package/partner-starter/README.md +19 -6
- package/partner-starter/package.json +1 -1
- package/partner-starter/runtime.mjs +3 -2
- package/partner-starter/smoke.mjs +10 -5
- package/partner-starter/synthetic.mjs +37 -7
- package/partner-starter/tests/starter.test.mjs +58 -8
- package/partner-starter/trusted-context.example.mjs +3 -0
- package/partner.js +4 -0
- package/private-chat.js +10 -3
- package/starter/README.md +1 -7
- package/starter/package.json +1 -1
- package/starter/runtime.mjs +7 -2
- package/starter/synthetic.mjs +1 -1
- package/starter/tests/starter.test.mjs +7 -39
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
# @weaccelerateinc/agent-sdk 0.
|
|
1
|
+
# @weaccelerateinc/agent-sdk 0.11.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Version 0.11.0 is a breaking release: **Accelerate never sends the final payment click.** A purchase arms one short click, the customer's agent performs it once through its own browser connection (`BrowserConnectorHost.performFinalClick`) in the customer's existing browser, then Accelerate observes the outcome. Purchase requires `final_click_actor:'customer'` and that host; the managed Kernel browser is fill-only. `client.checkouts.submit` rejects locally with `customer_click_required`. See [customer-owned final click](../../docs/customer-final-click.md). Release 0.10.3 keeps its existing behavior. Merchant layouts remain limited to the qualified sandbox page; this SDK release does not by itself qualify any merchant layout, a service deployment or production payments.
|
|
4
|
+
|
|
5
|
+
Server-only ESM SDK with TypeScript declarations, Node 22+, zero runtime dependencies. New partners use their existing browser and explicitly select `managed_review_v2`, which continues an armed fill immediately after private CVV verification. Existing callers keep `managed_review_v1` unless they opt in. Public npm releases use `@weaccelerateinc/agent-sdk`; install an exact available version with `npm install @weaccelerateinc/agent-sdk@0.11.0`. Check `npm view @weaccelerateinc/agent-sdk@0.11.0 version` to confirm registry availability. The package remains `UNLICENSED`.
|
|
4
6
|
|
|
5
7
|
```sh
|
|
6
|
-
npm install --ignore-scripts @weaccelerateinc/agent-sdk@0.
|
|
8
|
+
npm install --ignore-scripts @weaccelerateinc/agent-sdk@0.11.0
|
|
7
9
|
npx --no-install accelerate-agent init ./partner-demo --browser existing
|
|
8
10
|
```
|
|
9
11
|
|
|
@@ -25,15 +27,15 @@ The platform creates the browser and installs the approved extension using its e
|
|
|
25
27
|
| `/managed-mcp` | Lower-level canonical tool registration and explicit legacy/managed browser compatibility. |
|
|
26
28
|
| `/browser-operations` | Strict bounded operation schemas; attached-customer mode permits pre-fill reads only. |
|
|
27
29
|
|
|
28
|
-
The matching [partner guide](https://github.com/weaccelerateinc/accelerate
|
|
30
|
+
The matching [partner guide](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/docs/new-customer-integration.md) and packaged `partner-starter/` show composition. Connect actual backend authentication, document controls, durable queues and private webhook before live use; examples intentionally fail without them.
|
|
29
31
|
|
|
30
32
|
Store `ACCELERATE_API_KEY` and `ACCELERATE_MERCHANT_ID` on the server. `ACCELERATE_BASE_URL` defaults to `https://accelerate-agent-sdk.vercel.app`. `accelerate-agent doctor` checks only that credential/API pair, never live downstream readiness. `new AccelerateClient({baseUrl,apiKey,merchantId,fetchImpl?})` supports custom transport; `allowLoopbackForTests:true` is local-test-only.
|
|
31
33
|
|
|
32
|
-
For v2, create declares `fill_after_verification:true` and requires the service acknowledgement before OTP. That declaration alone cannot fill. The later verified quote repeats the flag to arm one exact revision; the successful private CVV reply fills immediately under the same backend lease and returns `review_delivery_required`. Private ingress delivers and acknowledges the masked image without asking the model to call fill. The v2 MCP registry omits the fill tool; an armed checkout rejects direct manual fill with `fill_continuation_required`. Read only its safe status. Final submit remains explicit. See [managed review v2](https://github.com/weaccelerateinc/accelerate
|
|
34
|
+
For v2, create declares `fill_after_verification:true` and requires the service acknowledgement before OTP. That declaration alone cannot fill. The later verified quote repeats the flag to arm one exact revision; the successful private CVV reply fills immediately under the same backend lease and returns `review_delivery_required`. Private ingress delivers and acknowledges the masked image without asking the model to call fill. The v2 MCP registry omits the fill tool; an armed checkout rejects direct manual fill with `fill_continuation_required`. Read only its safe status. Final submit remains explicit. See [managed review v2](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/docs/managed-review-v2.md).
|
|
33
35
|
|
|
34
36
|
Required sequence: shop → payment → same-browser attachment → private OTP/card → address/final amount → private CVV → fill once → private masked image/amount → five-minute decision → explicit submit → cleanup. PAN stars+last four, CVV stars, expiry visible. Compatible layouts require the matching qualified service/executor; unsupported fields or safe review stop before private collection. Provider-owner access remains trusted.
|
|
35
37
|
|
|
36
|
-
`registerPartnerCheckoutTools({profile:'managed_review_v2', ...})` selects automatic private continuation and defaults
|
|
38
|
+
`registerPartnerCheckoutTools({profile:'managed_review_v2', ...})` selects automatic private continuation and retains its API defaults `purpose:'sandbox_fill_only',enableSubmit:false`. Pass `purpose:'purchase',enableSubmit:true` for purchase registration; it also requires a connector whose host implements `performFinalClick`. New `init --browser existing` projects from 0.11.0 select purchase mode by default; already generated apps keep their copied runtime. Its returned private ingress must run before framework logging/parsing/history/AI. A fresh private receipt/approval enables purchase; it never triggers submission. Fill-only tests cancel after delivered informational review. Private replies/browser credentials are never tool inputs/results. Recover only safe deliveries; never replay fill/submit.
|
|
37
39
|
|
|
38
40
|
Compatibility: `accelerate-agent init <dir>` without `--browser existing` keeps the original managed Kernel starter. `registerManagedCheckoutTools` still defaults to `browserMode:'accelerate_managed'`. Existing before-fill integrations explicitly use `legacy_before_fill_v1`. Low-level hosted/raw APIs keep their compatibility defaults; a package upgrade does not change custody.
|
|
39
41
|
|
|
@@ -117,7 +119,7 @@ import {
|
|
|
117
119
|
|
|
118
120
|
Use this entry point only in authenticated ingress **before** history, queues, analytics or model calls. `chat.start/get/reply/cancel` require the exact customer/workspace/conversation binding. `chat.reply` also requires a stable provider message ID and transient `text`; the mutable input text is cleared after the request, including failures.
|
|
119
121
|
|
|
120
|
-
`renderPrivateChatState(state,{messageId})` returns a safe prompt or `undefined`. `privateChatProgress` returns a fixed safe preparation/verification/closure event. Both revalidate safe metadata and provide a hashed delivery idempotency key. They do not verify webhooks, authorize owners or replace durable ingress/delivery deduplication. Reuse your existing authenticated webhook, ownership lock and message sender. See the [private ingress example](https://github.com/weaccelerateinc/accelerate
|
|
122
|
+
`renderPrivateChatState(state,{messageId})` returns a safe prompt or `undefined`. `privateChatProgress` returns a fixed safe preparation/verification/closure event. Both revalidate safe metadata and provide a hashed delivery idempotency key. They do not verify webhooks, authorize owners or replace durable ingress/delivery deduplication. Reuse your existing authenticated webhook, ownership lock and message sender. See the [private ingress example](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/examples/private-chat-ingress.js).
|
|
121
123
|
|
|
122
124
|
## Safe results and recovery
|
|
123
125
|
|
package/approved-extension.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
const SDK_VERSION = '0.
|
|
3
|
+
const SDK_VERSION = '0.11.0';
|
|
4
4
|
const EXECUTOR_CONTRACT = 'accelerate_manual_checkout_v1';
|
|
5
5
|
const BRIDGE_PROTOCOL = 'accelerate_extension_bridge_v1';
|
|
6
6
|
const LEGACY_SHA256 = '65574cbb27ab344687963f72a136f2653551b57458b284e486946b27699099c8';
|
package/browser-connector.d.ts
CHANGED
|
@@ -46,8 +46,22 @@ export interface BrowserConnectorHost {
|
|
|
46
46
|
}>, options: RequestOptions): Promise<{
|
|
47
47
|
provider_browser_stopped: true; credentials_cleared: true; fence_retained: true;
|
|
48
48
|
}>;
|
|
49
|
+
/** Optional, but REQUIRED for purchase. Perform exactly one trusted pointer click on the unique selector in the exact bound
|
|
50
|
+
* browser, document and URL under the existing owner fence. No retry, no general DOM reads, never lift the fence.
|
|
51
|
+
* Accelerate sends no physical input. A thrown error or dispatched:false is observation only; the caller then completes once. */
|
|
52
|
+
performFinalClick?(context: Readonly<ManagedCheckoutContext>, request: Readonly<FinalClickRequest>, options: RequestOptions): Promise<{ dispatched: boolean }>;
|
|
53
|
+
}
|
|
54
|
+
/** Private request to the customer-owned browser host for the ONE physical final click. Never model/MCP data. */
|
|
55
|
+
export interface FinalClickRequest extends FencedBrowserDocument {
|
|
56
|
+
checkout_id: CheckoutId;
|
|
57
|
+
/** Unique selector bound at approval, inside the exact fenced document. */
|
|
58
|
+
submit_selector: string;
|
|
59
|
+
armed_expires_at: string;
|
|
49
60
|
}
|
|
50
61
|
declare const attachmentBrand: unique symbol;
|
|
62
|
+
declare const finalClickPreparationBrand: unique symbol;
|
|
63
|
+
/** One-use private readiness proof from prepareFinalClick; valid for one performFinalClick on this connector instance. */
|
|
64
|
+
export interface PreparedFinalClick { readonly [finalClickPreparationBrand]: true }
|
|
51
65
|
/** Private one-connector capability returned only to the authenticated backend. */
|
|
52
66
|
export interface PreparedBrowserAttachment {
|
|
53
67
|
readonly browser: Readonly<CustomerBrowserBinding>;
|
|
@@ -70,6 +84,14 @@ export interface BrowserConnector {
|
|
|
70
84
|
confirmCleanup(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
|
|
71
85
|
checkout_id: CheckoutId;
|
|
72
86
|
}>, options?: RequestOptions): Promise<void>;
|
|
87
|
+
/** Present only when the host implements performFinalClick. Call BEFORE arming: resolves the fenced owned document, checks
|
|
88
|
+
* provider/extension readiness and the drained fence, without lifting it, so none of that work consumes the short click window. */
|
|
89
|
+
prepareFinalClick?(context: Readonly<ManagedCheckoutContext>, request: Readonly<{ checkout_id: CheckoutId }>, options?: RequestOptions): Promise<PreparedFinalClick>;
|
|
90
|
+
/** Cheap checks only (one-use preparation, exact URL, arm window <= 15s, browser outlives the arm), then asks the customer's
|
|
91
|
+
* host for one physical click. The arm token never reaches the host. No retry. */
|
|
92
|
+
performFinalClick?(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
|
|
93
|
+
checkout_id: CheckoutId; preparation: PreparedFinalClick; checkout_url: string; submit_selector: string; armed_expires_at: string;
|
|
94
|
+
}>, options?: RequestOptions): Promise<{ dispatched: boolean }>;
|
|
73
95
|
}
|
|
74
96
|
export interface ExistingBrowserConnectorOptions {
|
|
75
97
|
/** Provisioned server connector ID; the Accelerate executor must enable this ID separately. */
|
package/browser-connector.js
CHANGED
|
@@ -91,14 +91,14 @@ function makeConnector(configuration, provider) {
|
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
93
|
const checkpoint = opts => check(!opts.signal?.aborted, 'request_cancelled');
|
|
94
|
-
const inspect = async (context, expected, opts, fenceId) => {
|
|
94
|
+
const inspect = async (context, expected, opts, fenceId, minimum = minimumRemainingMs) => {
|
|
95
95
|
checkpoint(opts);
|
|
96
96
|
const value = await hostCall('inspectBrowser', context, expected, opts);
|
|
97
97
|
checkpoint(opts);
|
|
98
98
|
const actual = record(value, context, config, { readiness: true });
|
|
99
99
|
sameDocument(actual, expected);
|
|
100
100
|
const current = now();
|
|
101
|
-
check(Number.isFinite(current) && Date.parse(actual.expires_at) - current >=
|
|
101
|
+
check(Number.isFinite(current) && Date.parse(actual.expires_at) - current >= minimum &&
|
|
102
102
|
value.recording_disabled === true, 'integration_unavailable');
|
|
103
103
|
check(object(value.extension) && value.extension.artifact_sha256 === release.artifact.sha256, 'integration_unavailable');
|
|
104
104
|
try { verifyApprovedExtensionRuntime(value.extension, release, { executorContract: 'accelerate_manual_checkout_v1', provider }); }
|
|
@@ -115,6 +115,44 @@ function makeConnector(configuration, provider) {
|
|
|
115
115
|
checkpoint(opts);
|
|
116
116
|
return record(resolved, context, config, { fenced: true });
|
|
117
117
|
};
|
|
118
|
+
const stamp = value => {
|
|
119
|
+
check(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value) && Number.isFinite(Date.parse(value)), 'invalid_input');
|
|
120
|
+
return Date.parse(value);
|
|
121
|
+
};
|
|
122
|
+
const preparations = new WeakMap();
|
|
123
|
+
const canClick = typeof host.performFinalClick === 'function';
|
|
124
|
+
// Slow work (ownership lookup, provider read, runtime inspection) happens here, BEFORE the short arm exists,
|
|
125
|
+
// so none of it consumes the click window. The fence is never lifted. One preparation serves one click.
|
|
126
|
+
const prepareClick = canClick ? async (contextValue, input, requestOptions) => {
|
|
127
|
+
const context = owner(contextValue), opts = options(requestOptions);
|
|
128
|
+
shape(input, ['checkout_id'], [], 'invalid_input');
|
|
129
|
+
const expected = await resolve(context, input.checkout_id, opts);
|
|
130
|
+
// Fenced, drained runtime check; the ten-minute attachment headroom does not apply after the review window.
|
|
131
|
+
await inspect(context, expected, opts, expected.fence_id, 1);
|
|
132
|
+
const preparation = Object.freeze({});
|
|
133
|
+
preparations.set(preparation, { context, checkout_id: input.checkout_id, record: expected, at: now(), used: false });
|
|
134
|
+
return preparation;
|
|
135
|
+
} : undefined;
|
|
136
|
+
const finalClick = canClick ? async (contextValue, input, requestOptions) => {
|
|
137
|
+
const context = owner(contextValue), opts = options(requestOptions);
|
|
138
|
+
shape(input, ['checkout_id', 'preparation', 'checkout_url', 'submit_selector', 'armed_expires_at'], [], 'invalid_input');
|
|
139
|
+
const saved = preparations.get(input.preparation);
|
|
140
|
+
check(saved && !saved.used && sameOwner(saved.context, context) && saved.checkout_id === input.checkout_id, 'permission_denied');
|
|
141
|
+
saved.used = true;
|
|
142
|
+
check(typeof input.submit_selector === 'string' && input.submit_selector.length > 0 && input.submit_selector.length <= 200 &&
|
|
143
|
+
!/[\u0000-\u001f\u007f]/u.test(input.submit_selector), 'invalid_input');
|
|
144
|
+
const armedUntil = stamp(input.armed_expires_at), started = now();
|
|
145
|
+
// The service arm lives at most 15 seconds (never accepted longer); refuse an expired or implausibly long window.
|
|
146
|
+
check(Number.isFinite(started) && armedUntil > started && armedUntil - started <= 15000 && started - saved.at <= 60000, 'submit_unavailable');
|
|
147
|
+
// Cheap checks only inside the arm window: exact URL, browser outlives the arm, then the customer's physical click.
|
|
148
|
+
check(url(input.checkout_url) === saved.record.checkout_url && Date.parse(saved.record.expires_at) > armedUntil, 'permission_denied');
|
|
149
|
+
checkpoint(opts);
|
|
150
|
+
const result = await hostCall('performFinalClick', context, freeze({ checkout_id: input.checkout_id,
|
|
151
|
+
submit_selector: input.submit_selector, armed_expires_at: input.armed_expires_at, ...saved.record }), opts);
|
|
152
|
+
shape(result, ['dispatched'], [], 'integration_unavailable');
|
|
153
|
+
check(typeof result.dispatched === 'boolean', 'integration_unavailable');
|
|
154
|
+
return Object.freeze({ dispatched: result.dispatched });
|
|
155
|
+
} : undefined;
|
|
118
156
|
// Every method belongs to the server integration. A connector never creates a
|
|
119
157
|
// replacement browser, releases its durable fence or retries a payment call.
|
|
120
158
|
return Object.freeze({ contract: 'accelerate_existing_browser_v1', provider,
|
|
@@ -163,6 +201,7 @@ function makeConnector(configuration, provider) {
|
|
|
163
201
|
if (input.checkout_url !== undefined) check(url(input.checkout_url) === expected.checkout_url, 'permission_denied');
|
|
164
202
|
await inspect(context, expected, opts, expected.fence_id);
|
|
165
203
|
},
|
|
204
|
+
...(finalClick ? { prepareFinalClick: prepareClick, performFinalClick: finalClick } : {}),
|
|
166
205
|
async confirmCleanup(contextValue, input, requestOptions) {
|
|
167
206
|
const context = owner(contextValue), opts = options(requestOptions);
|
|
168
207
|
shape(input, ['checkout_id'], [], 'invalid_input');
|
package/browser-use-host.d.ts
CHANGED
|
@@ -22,6 +22,9 @@ export interface BrowserUsePlatformHost {
|
|
|
22
22
|
* if suspended. Do not create/navigate a page or accept model-authored code. */
|
|
23
23
|
inspectDocument(context: Readonly<ManagedCheckoutContext>, browser: Readonly<OwnedBrowserDocument>,
|
|
24
24
|
runtime: Readonly<{ runtime_id: string }>, options: RequestOptions): Promise<BrowserUseDocumentObservation>;
|
|
25
|
+
/** Optional; required for purchase. One trusted pointer click on the unique bound selector in the exact fenced document,
|
|
26
|
+
* through your existing server browser control. No retry, no general DOM reads, never lift the fence. */
|
|
27
|
+
performFinalClick?: BrowserConnectorHost['performFinalClick'];
|
|
25
28
|
/** Independently verify extension/page cleanup and retain every payment uncertainty fence. */
|
|
26
29
|
confirmLocalCleanup(context: Readonly<ManagedCheckoutContext>, browser: Readonly<FencedBrowserDocument>,
|
|
27
30
|
request: Readonly<{ checkout_id: import('./index.js').CheckoutId }>, options: RequestOptions): Promise<{
|
package/browser-use-host.js
CHANGED
|
@@ -131,6 +131,8 @@ export function createBrowserUseHost(configuration = {}) {
|
|
|
131
131
|
recording_disabled: true, extension,
|
|
132
132
|
automation: current.automation };
|
|
133
133
|
},
|
|
134
|
+
// Optional: only a platform host that can click may enable purchases. Readiness was read by prepareFinalClick before the arm.
|
|
135
|
+
...(typeof host.performFinalClick === 'function' ? { performFinalClick: (...args) => callHost('performFinalClick', ...args) } : {}),
|
|
134
136
|
drainAndFence: (...args) => callHost('drainAndFence', ...args),
|
|
135
137
|
bindCheckout: (...args) => callHost('bindCheckout', ...args),
|
|
136
138
|
resolveCheckoutBrowser: (...args) => callHost('resolveCheckoutBrowser', ...args),
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
export type CheckoutDiagnosticStage = 'not_started' | 'identity_required' | 'otp_required' | 'selection_required' |
|
|
2
|
-
'preparing_checkout' | 'approval_required' | 'cvv_required' | 'approved' | 'ready_for_submit' | 'review_delivery_required' | 'review_delivered' | 'approved_for_submit' |
|
|
2
|
+
'preparing_checkout' | 'approval_required' | 'cvv_required' | 'approved' | 'ready_for_submit' | 'review_delivery_required' | 'review_delivered' | 'approved_for_submit' | 'final_click_armed' |
|
|
3
3
|
'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
|
|
4
4
|
export type CheckoutDiagnosticAction = 'get_status' | 'continue_private_chat' | 'prepare_checkout' | 'apply_details' |
|
|
5
5
|
'request_approval' | 'fill' | 'submit' | 'cancel' | 'reconcile_order';
|
|
6
6
|
export type CheckoutCleanupStatus = 'not_required' | 'not_started' | 'pending' | 'confirmed';
|
|
7
|
+
export type CheckoutReconciliationState = 'confirmed' | 'declined' | 'not_found';
|
|
8
|
+
/** The client backend's statement of the merchant's result. Not Accelerate's proof: payment_confirmed stays false. */
|
|
9
|
+
export interface CheckoutReconciliation {
|
|
10
|
+
state: CheckoutReconciliationState;
|
|
11
|
+
/** Short label for where the client read the result, such as merchant_order_api. */
|
|
12
|
+
source: string;
|
|
13
|
+
amount_cents: number | null;
|
|
14
|
+
currency: 'USD' | null;
|
|
15
|
+
reconciled_at: string;
|
|
16
|
+
}
|
|
7
17
|
export interface CheckoutDiagnostics {
|
|
8
18
|
version: 'v1';
|
|
9
19
|
stage: CheckoutDiagnosticStage;
|
|
@@ -19,5 +29,6 @@ export const checkoutDiagnosticActions: readonly CheckoutDiagnosticAction[];
|
|
|
19
29
|
export const checkoutCleanupStatuses: readonly CheckoutCleanupStatus[];
|
|
20
30
|
export const checkoutDiagnosticsJsonSchema: Readonly<Record<string, unknown>>;
|
|
21
31
|
export function validateCheckoutDiagnostics(value: unknown, checkout?: {
|
|
22
|
-
status: string; purpose?: string; submit_allowed?: boolean; submit_expires_at?: string; approval_policy?: string; purchase_approved?: boolean;
|
|
32
|
+
status: string; purpose?: string; submit_allowed?: boolean; submit_expires_at?: string; approval_policy?: string; purchase_approved?: boolean; reconciliation?: unknown; final_click_actor?: string;
|
|
23
33
|
}): CheckoutDiagnostics;
|
|
34
|
+
export function validateCheckoutReconciliation(value: unknown, status?: string): CheckoutReconciliation;
|
package/checkout-diagnostics.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Safe lifecycle metadata only. No transport, credentials, provider text or automatic actions.
|
|
2
2
|
export const checkoutDiagnosticStages = Object.freeze(['not_started', 'identity_required', 'otp_required', 'selection_required',
|
|
3
|
-
'preparing_checkout', 'approval_required', 'cvv_required', 'approved', 'ready_for_submit', 'review_delivery_required', 'review_delivered', 'approved_for_submit',
|
|
3
|
+
'preparing_checkout', 'approval_required', 'cvv_required', 'approved', 'ready_for_submit', 'review_delivery_required', 'review_delivered', 'approved_for_submit', 'final_click_armed',
|
|
4
4
|
'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
5
5
|
export const checkoutDiagnosticActions = Object.freeze(['get_status', 'continue_private_chat', 'prepare_checkout', 'apply_details',
|
|
6
6
|
'request_approval', 'fill', 'submit', 'cancel', 'reconcile_order']);
|
|
@@ -38,7 +38,7 @@ export function validateCheckoutDiagnostics(input, checkout) {
|
|
|
38
38
|
typeof value.support_reference === 'string' && /^acs_[a-f0-9]{32}$/.test(value.support_reference) &&
|
|
39
39
|
checkoutCleanupStatuses.includes(value.cleanup_status) && (value.submit_deadline === undefined || timestamp(value.submit_deadline)));
|
|
40
40
|
if (checkout !== undefined) {
|
|
41
|
-
const { status, purpose = 'purchase', submit_allowed, approval_policy, purchase_approved } = checkout;
|
|
41
|
+
const { status, purpose = 'purchase', submit_allowed, approval_policy, purchase_approved, reconciliation, final_click_actor } = checkout;
|
|
42
42
|
check(terminal.has(status) ? value.stage === status : status === 'wallet_required'
|
|
43
43
|
? ['not_started', 'identity_required', 'otp_required', 'selection_required'].includes(value.stage)
|
|
44
44
|
: ['selection_ready', 'details_ready'].includes(status) ? value.stage === 'preparing_checkout'
|
|
@@ -46,7 +46,7 @@ export function validateCheckoutDiagnostics(input, checkout) {
|
|
|
46
46
|
check(status === 'ready_for_submit' ? timestamp(value.submit_deadline) && value.submit_deadline === checkout.submit_expires_at : value.submit_deadline === undefined);
|
|
47
47
|
for (const action of value.next_actions) {
|
|
48
48
|
if (action === 'submit')
|
|
49
|
-
check(status === 'ready_for_submit' && purpose === 'purchase' && submit_allowed !== false && (approval_policy !== 'after_fill' || purchase_approved === true));
|
|
49
|
+
check(status === 'ready_for_submit' && purpose === 'purchase' && submit_allowed !== false && final_click_actor === 'customer' && (approval_policy !== 'after_fill' || purchase_approved === true));
|
|
50
50
|
if (action === 'fill')
|
|
51
51
|
check(status === 'approved' && purpose !== 'sandbox_source_comparison');
|
|
52
52
|
if (['prepare_checkout', 'apply_details'].includes(action))
|
|
@@ -56,8 +56,11 @@ export function validateCheckoutDiagnostics(input, checkout) {
|
|
|
56
56
|
if (action === 'continue_private_chat')
|
|
57
57
|
check(['wallet_required', 'approval_required'].includes(status) || approval_policy === 'after_fill' && status === 'ready_for_submit' && !purchase_approved);
|
|
58
58
|
if (action === 'reconcile_order')
|
|
59
|
-
check(['submit_attempted', 'outcome_unknown'].includes(status));
|
|
59
|
+
check(['submit_attempted', 'outcome_unknown'].includes(status) && reconciliation === undefined);
|
|
60
60
|
}
|
|
61
|
+
// While a customer click is armed only a safe read or cancel is advertised: no browser work, no re-arm.
|
|
62
|
+
if (status === 'final_click_armed')
|
|
63
|
+
check(value.next_actions.every(action => ['get_status', 'cancel'].includes(action)));
|
|
61
64
|
if (!terminal.has(status))
|
|
62
65
|
check(value.cleanup_status !== 'pending' && value.cleanup_status !== 'confirmed');
|
|
63
66
|
if (terminal.has(status))
|
|
@@ -65,3 +68,14 @@ export function validateCheckoutDiagnostics(input, checkout) {
|
|
|
65
68
|
}
|
|
66
69
|
return value;
|
|
67
70
|
}
|
|
71
|
+
/** Validate the client backend's stored statement of the merchant's result. Never proof of payment. */
|
|
72
|
+
export function validateCheckoutReconciliation(input, status) {
|
|
73
|
+
const value = input;
|
|
74
|
+
check(value !== null && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
|
|
75
|
+
Object.keys(value).length === 5 && ['confirmed', 'declined', 'not_found'].includes(value.state) &&
|
|
76
|
+
typeof value.source === 'string' && /^[a-z][a-z0-9_.-]{0,39}$/.test(value.source) && !/(?:\d[ -]?){13,19}/.test(value.source) &&
|
|
77
|
+
(value.amount_cents === null || Number.isSafeInteger(value.amount_cents) && value.amount_cents > 0 && value.amount_cents <= 2147483647) &&
|
|
78
|
+
(value.currency === null || value.currency === 'USD') && (value.state !== 'confirmed' || value.amount_cents !== null && value.currency === 'USD') &&
|
|
79
|
+
timestamp(value.reconciled_at) && (status === undefined || ['submit_attempted', 'outcome_unknown'].includes(status)));
|
|
80
|
+
return value;
|
|
81
|
+
}
|
package/cli.js
CHANGED
|
@@ -18,7 +18,10 @@ It does not qualify the downstream payment source or browser configuration.
|
|
|
18
18
|
|
|
19
19
|
Init copies the packaged synthetic MCP starter without credentials, network
|
|
20
20
|
requests or overwrites. --browser existing selects the new provider-independent
|
|
21
|
-
existing-browser starter
|
|
21
|
+
existing-browser starter with purchase purpose and a submit tool; omission
|
|
22
|
+
preserves the managed Kernel starter, which is fill-only. Both start in
|
|
23
|
+
synthetic mode. Purchase requires private approval, an explicit submit and your
|
|
24
|
+
own browser host performing the one final click: Accelerate never clicks.
|
|
22
25
|
Existing symlink paths and nonempty targets are rejected.
|
|
23
26
|
`;
|
|
24
27
|
const starterFiles = ['package.json', '.env.example', '.gitignore', 'README.md', 'server.mjs', 'runtime.mjs',
|
package/host-adapter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { AccelerateError } from './index.js';
|
|
4
|
-
import { validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
4
|
+
import { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
|
|
5
5
|
import { renderPrivateChatState, privateChatProgress } from './private-chat.js';
|
|
6
6
|
|
|
7
7
|
const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
|
|
@@ -9,9 +9,9 @@ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/;
|
|
|
9
9
|
const MESSAGE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/;
|
|
10
10
|
const RECOVER = new Set(['accelerate_checkout_status', 'accelerate_checkout_cancel', 'private_state', 'private_cancel']);
|
|
11
11
|
const TERMINAL = new Set(['cancelled', 'expired', 'failed']);
|
|
12
|
-
const STATUS_ORDER = new Map(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit'].map((status, index) => [status, index]));
|
|
12
|
+
const STATUS_ORDER = new Map(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed'].map((status, index) => [status, index]));
|
|
13
13
|
const STAGES = new Set(['not_started', 'identity_required', 'otp_required', 'selection_required', 'preparing_checkout', 'approval_required', 'cvv_required', 'verified', 'review_delivery_required', 'review_delivered', 'approved_for_submit', 'closed']);
|
|
14
|
-
const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
14
|
+
const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
15
15
|
const object = value => value && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
16
16
|
const check = (condition, code = 'invalid_input') => { if (!condition) throw new AccelerateError(code); };
|
|
17
17
|
const clone = value => structuredClone(value);
|
|
@@ -50,7 +50,7 @@ function safeText(value, max = 10000) {
|
|
|
50
50
|
return value;
|
|
51
51
|
}
|
|
52
52
|
function safeCheckout(value) {
|
|
53
|
-
exact(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics'], 'invalid_response');
|
|
53
|
+
exact(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics', 'reconciliation', 'final_click_actor', 'submit_actor'], 'invalid_response');
|
|
54
54
|
check(ID.test(value.id) && STATUSES.has(value.status) && Number.isSafeInteger(value.revision) && value.revision >= 0 && value.retry_safe === false && value.payment_confirmed === false && validDate(value.expires_at), 'invalid_response');
|
|
55
55
|
if (value.card !== undefined) {
|
|
56
56
|
exact(value.card, ['brand', 'last4'], [], 'invalid_response');
|
|
@@ -71,8 +71,12 @@ function safeCheckout(value) {
|
|
|
71
71
|
if (value.submit_expires_at !== undefined) check(validDate(value.submit_expires_at), 'invalid_response');
|
|
72
72
|
if (value.submit_allowed !== undefined) check(typeof value.submit_allowed === 'boolean', 'invalid_response');
|
|
73
73
|
if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' && ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
|
|
74
|
+
if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
|
|
75
|
+
if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' && ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
|
|
76
|
+
if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
|
|
74
77
|
if (value.purchase_approved !== undefined) check(typeof value.purchase_approved === 'boolean', 'invalid_response');
|
|
75
78
|
if (value.approval_policy !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean', 'invalid_response');
|
|
79
|
+
if (value.reconciliation !== undefined) { try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { check(false, 'invalid_response'); } }
|
|
76
80
|
// The SDK owns detailed diagnostics validation. This projection keeps no URLs,
|
|
77
81
|
// provider capabilities or data that could accidentally become an effect input.
|
|
78
82
|
const result = clone(value);
|
|
@@ -212,7 +216,7 @@ export function createDurableCheckoutHost({ persistence, resolveContext } = {})
|
|
|
212
216
|
}
|
|
213
217
|
if (metadata.operation === 'accelerate_checkout_submit') {
|
|
214
218
|
const checkout = active.record.attempt.checkout;
|
|
215
|
-
check(checkout?.purpose === 'purchase' && checkout.submit_allowed === true && checkout.status === 'ready_for_submit' && checkout.purchase_approved === true && active.record.attempt.private_stage === 'approved_for_submit' && validDate(checkout.submit_expires_at) && Date.parse(checkout.submit_expires_at) > Date.now() && metadata.revision === checkout.revision, 'submit_unavailable');
|
|
219
|
+
check(checkout?.purpose === 'purchase' && checkout.submit_allowed === true && checkout.final_click_actor === 'customer' && checkout.status === 'ready_for_submit' && checkout.purchase_approved === true && active.record.attempt.private_stage === 'approved_for_submit' && validDate(checkout.submit_expires_at) && Date.parse(checkout.submit_expires_at) > Date.now() && metadata.revision === checkout.revision, 'submit_unavailable');
|
|
216
220
|
}
|
|
217
221
|
const oneUse = ['accelerate_checkout_fill', 'accelerate_checkout_submit'].includes(metadata.operation);
|
|
218
222
|
if (oneUse) {
|
package/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { BrowserOperation, BrowserResult } from './browser-operations.js';
|
|
2
|
-
import type { CheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
2
|
+
import type { CheckoutDiagnostics, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
|
|
3
3
|
export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
|
|
4
|
-
export type { CheckoutDiagnostics, CheckoutDiagnosticStage, CheckoutDiagnosticAction, CheckoutCleanupStatus } from './checkout-diagnostics.js';
|
|
4
|
+
export type { CheckoutDiagnostics, CheckoutDiagnosticStage, CheckoutDiagnosticAction, CheckoutCleanupStatus, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
|
|
5
5
|
export type { BrowserOperation, BrowserResult, BrowserStep, BrowserCondition } from './browser-operations.js';
|
|
6
6
|
|
|
7
7
|
export type CheckoutId = `mcs_${string}`;
|
|
8
8
|
export type CheckoutPurpose = 'purchase' | 'sandbox_source_comparison' | 'sandbox_fill_only';
|
|
9
9
|
export type CheckoutStatus = 'wallet_required' | 'selection_ready' | 'details_ready' | 'approval_required' |
|
|
10
|
-
'approved' | 'ready_for_submit' | 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
|
|
10
|
+
'approved' | 'ready_for_submit' | 'final_click_armed' | 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
|
|
11
11
|
export interface RequestOptions { signal?: AbortSignal }
|
|
12
12
|
export interface WaitForCheckout {
|
|
13
13
|
/** Return on these states, or immediately on ready_for_submit or any terminal state. GET requests only. */
|
|
@@ -47,16 +47,40 @@ export interface Checkout {
|
|
|
47
47
|
/** Explicit v2 policy acknowledged at creation; a later explicit quote arm lets private CVV continue
|
|
48
48
|
* one fill under the backend lock. Does not authorize final submission. */
|
|
49
49
|
fill_after_verification?: true;
|
|
50
|
+
/** Purchases only: the customer's own trusted backend/browser performs the physical final click. Accelerate never does. */
|
|
51
|
+
final_click_actor?: 'customer';
|
|
52
|
+
/** Present on submit_attempted (required for a purchase) and optionally on outcome_unknown, which cannot assert that a click happened. Never payment confirmation. */
|
|
53
|
+
submit_actor?: 'customer';
|
|
50
54
|
card?: { last4: string | null; brand: string | null };
|
|
51
55
|
address?: { label: string | null; city?: string | null; state?: string | null; postal_code_mask?: string | null };
|
|
52
56
|
quote?: { checkout_origin: string; amount_cents: number; currency: 'USD' };
|
|
53
57
|
submit_expires_at?: string;
|
|
58
|
+
/** Present once the client backend recorded the merchant's result. Absent on older services.
|
|
59
|
+
* This is the client's statement, not Accelerate's proof, so payment_confirmed stays false. */
|
|
60
|
+
reconciliation?: CheckoutReconciliation;
|
|
54
61
|
retry_safe: false;
|
|
55
62
|
payment_confirmed: false;
|
|
56
63
|
/** Negotiated safe metadata; absent when connecting to an older service. */
|
|
57
64
|
diagnostics?: CheckoutDiagnostics;
|
|
58
65
|
}
|
|
59
66
|
export interface CreatedCheckout extends Checkout { wallet_url?: string }
|
|
67
|
+
export interface ReconciledCheckout extends Checkout { reconciliation: CheckoutReconciliation }
|
|
68
|
+
/** Server only, after your backend checked the merchant's order system. Never model input.
|
|
69
|
+
* order_reference is stored only as a hash and is never returned. */
|
|
70
|
+
export type CheckoutReconciliationRequest = {
|
|
71
|
+
state: 'confirmed';
|
|
72
|
+
order_reference: string;
|
|
73
|
+
/** Must equal the approved quote exactly. */
|
|
74
|
+
amount_cents: number;
|
|
75
|
+
currency: 'USD';
|
|
76
|
+
source: string;
|
|
77
|
+
} | {
|
|
78
|
+
state: Exclude<CheckoutReconciliationState, 'confirmed'>;
|
|
79
|
+
order_reference?: string;
|
|
80
|
+
amount_cents?: number;
|
|
81
|
+
currency?: 'USD';
|
|
82
|
+
source: string;
|
|
83
|
+
};
|
|
60
84
|
/** Server-owned existing browser, resolved from the authenticated application's
|
|
61
85
|
* durable browser record. Never accept this binding from model arguments.
|
|
62
86
|
* The customer backend retains provider access; this is not private-from-creation custody. */
|
|
@@ -125,6 +149,9 @@ export interface CreateCheckout {
|
|
|
125
149
|
/** Explicit chat/after_fill attachment. Omit to retain Accelerate-created
|
|
126
150
|
* private Kernel custody. Attachment never navigates or replaces the document. */
|
|
127
151
|
browser?: CustomerBrowserBinding;
|
|
152
|
+
/** Required for purchase (the default purpose): the customer's backend, not Accelerate, performs the final click.
|
|
153
|
+
* Purchase also requires chat, after_fill and a customer_backend_trusted browser. */
|
|
154
|
+
final_click_actor?: 'customer';
|
|
128
155
|
/** Your own provider read of `browser`, so Accelerate never needs your provider API key.
|
|
129
156
|
* Send it on every create. It is held in memory for this checkout and never stored. */
|
|
130
157
|
browser_resource?: ClientBrowserResource;
|
|
@@ -165,6 +192,16 @@ export interface ApprovalRequest {
|
|
|
165
192
|
* Successful private verification fills this exact revision without AI. */
|
|
166
193
|
fill_after_verification?: true;
|
|
167
194
|
}
|
|
195
|
+
/** BACKEND ONLY. The target and arm_id must never appear in MCP arguments, results, history or outbox. */
|
|
196
|
+
export interface ArmedFinalClick {
|
|
197
|
+
/** Opaque bounded string (20..16000 printable ASCII). Never display or log. */
|
|
198
|
+
arm_id: string;
|
|
199
|
+
/** Short window (service limit 15 seconds) in which the customer host may click once. */
|
|
200
|
+
armed_expires_at: string;
|
|
201
|
+
target: { checkout_url: string; submit_selector: string };
|
|
202
|
+
retry_safe: false;
|
|
203
|
+
payment_confirmed: false;
|
|
204
|
+
}
|
|
168
205
|
export interface AccelerateClientOptions {
|
|
169
206
|
baseUrl: string;
|
|
170
207
|
apiKey: string;
|
|
@@ -203,7 +240,14 @@ export class AccelerateClient {
|
|
|
203
240
|
verifyQuote(id: CheckoutId, input: QuoteVerificationRequest, options?: RequestOptions): Promise<VerifiedCheckoutQuote>;
|
|
204
241
|
requestApproval(id: CheckoutId, input: ApprovalRequest, options?: RequestOptions): Promise<Checkout>;
|
|
205
242
|
fill(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<Checkout>;
|
|
206
|
-
|
|
243
|
+
/** Removed behavior: Accelerate never clicks. Always rejects with customer_click_required and sends no request. */
|
|
244
|
+
submit(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<never>;
|
|
245
|
+
/** Backend only: prepare one short-lived customer-owned final click. Accelerate sends no physical input. */
|
|
246
|
+
armFinalClick(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<ArmedFinalClick>;
|
|
247
|
+
/** Backend only: observe the customer's click once. Dispatch claims are not sent; never retry. */
|
|
248
|
+
completeFinalClick(id: CheckoutId, input: { revision: number; arm_id: string }, options?: RequestOptions): Promise<Checkout>;
|
|
207
249
|
cancel(id: CheckoutId, options?: RequestOptions): Promise<Checkout>;
|
|
250
|
+
/** Records one merchant result after submit. Repeating the same input returns the same result; different input is refused. */
|
|
251
|
+
reconcile(id: CheckoutId, input: CheckoutReconciliationRequest, options?: RequestOptions): Promise<ReconciledCheckout>;
|
|
208
252
|
};
|
|
209
253
|
}
|