@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.
Files changed (52) hide show
  1. package/README.md +149 -2
  2. package/approved-extension-releases/0.0.0.1.json +39 -0
  3. package/approved-extension-releases/0.0.0.2.json +32 -0
  4. package/approved-extension.d.ts +24 -0
  5. package/approved-extension.js +90 -0
  6. package/browser-connector.d.ts +87 -0
  7. package/browser-connector.js +179 -0
  8. package/browser-operations.d.ts +56 -0
  9. package/browser-operations.js +144 -0
  10. package/browser-use-host.d.ts +43 -0
  11. package/browser-use-host.js +147 -0
  12. package/checkout-diagnostics.d.ts +23 -0
  13. package/checkout-diagnostics.js +67 -0
  14. package/cli.js +102 -0
  15. package/customer.env.example +6 -0
  16. package/host-adapter.d.ts +90 -0
  17. package/host-adapter.js +353 -0
  18. package/index.d.ts +209 -0
  19. package/index.js +441 -0
  20. package/linq-messaging.d.ts +45 -0
  21. package/linq-messaging.js +123 -0
  22. package/managed-mcp.d.ts +150 -0
  23. package/managed-mcp.js +382 -0
  24. package/package.json +114 -4
  25. package/partner-starter/.env.example +11 -0
  26. package/partner-starter/README.md +48 -0
  27. package/partner-starter/gitignore.template +5 -0
  28. package/partner-starter/package.json +21 -0
  29. package/partner-starter/runtime.mjs +32 -0
  30. package/partner-starter/server.mjs +13 -0
  31. package/partner-starter/smoke.mjs +56 -0
  32. package/partner-starter/synthetic.mjs +226 -0
  33. package/partner-starter/tests/starter.test.mjs +69 -0
  34. package/partner-starter/trusted-context.example.mjs +20 -0
  35. package/partner.d.ts +27 -0
  36. package/partner.js +49 -0
  37. package/postgres-host.d.ts +14 -0
  38. package/postgres-host.js +102 -0
  39. package/private-chat.d.ts +104 -0
  40. package/private-chat.js +309 -0
  41. package/private-ingress.d.ts +46 -0
  42. package/private-ingress.js +132 -0
  43. package/starter/.env.example +19 -0
  44. package/starter/README.md +88 -0
  45. package/starter/gitignore.template +5 -0
  46. package/starter/package.json +21 -0
  47. package/starter/runtime.mjs +61 -0
  48. package/starter/server.mjs +13 -0
  49. package/starter/smoke.mjs +55 -0
  50. package/starter/synthetic.mjs +163 -0
  51. package/starter/tests/starter.test.mjs +256 -0
  52. package/starter/trusted-context.example.mjs +49 -0
@@ -0,0 +1,56 @@
1
+ export type BrowserStep =
2
+ | { type: 'click'; ref: string; button?: 'left'; num_clicks?: 1; modifiers?: [] }
3
+ | { type: 'hover' | 'scroll_to'; ref: string }
4
+ | { type: 'fill'; ref: string; value: string }
5
+ | { type: 'type'; text: string; repeat?: number }
6
+ | { type: 'key'; text: 'Enter' | 'Tab' | 'Escape' | 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' |
7
+ 'Backspace' | 'Delete' | 'Space'; repeat?: number }
8
+ | { type: 'wait'; ms?: number };
9
+ export type BrowserCondition =
10
+ | { type: 'text'; text: string; exists?: boolean }
11
+ | ({ type: 'url' | 'title'; exists?: boolean } &
12
+ ({ equals: string; contains?: string } | { equals?: string; contains: string }));
13
+ export type BrowserOperation =
14
+ | { operation: 'browser_snapshot'; arguments: { filter?: 'all' | 'interactive'; ref?: 'root'; depth?: number } }
15
+ | { operation: 'browser_text'; arguments: Record<string, never> }
16
+ | { operation: 'browser_find'; arguments: { query: string } }
17
+ | { operation: 'browser_wait_for'; arguments: { expect: BrowserCondition; timeout_ms?: number; poll_ms?: number } }
18
+ | { operation: 'browser_act'; arguments: { steps: BrowserStep[]; timeout_ms?: number; successor?: { filter?: 'all' | 'interactive'; depth?: number } } }
19
+ | { operation: 'navigate'; arguments: { url: string } };
20
+ export interface BrowserResult { content: Array<{ type: 'text'; text: string }> }
21
+
22
+ /** Dependency-free JSON Schema vocabulary used by the published browser grammar. */
23
+ export interface BrowserOperationJsonSchema {
24
+ [keyword: string]: unknown;
25
+ $schema?: 'https://json-schema.org/draft/2020-12/schema';
26
+ type?: 'object' | 'array' | 'string' | 'integer' | 'boolean';
27
+ description?: string;
28
+ properties?: Record<string, BrowserOperationJsonSchema>;
29
+ required?: string[];
30
+ additionalProperties?: boolean;
31
+ oneOf?: BrowserOperationJsonSchema[];
32
+ items?: BrowserOperationJsonSchema;
33
+ const?: string | number;
34
+ enum?: string[];
35
+ pattern?: string;
36
+ format?: string;
37
+ minimum?: number;
38
+ maximum?: number;
39
+ minLength?: number;
40
+ maxLength?: number;
41
+ minItems?: number;
42
+ maxItems?: number;
43
+ }
44
+
45
+ /** Frozen model-visible grammar. normalizeBrowserOperation remains required for semantic safety. */
46
+ export const browserOperationJsonSchema: Readonly<BrowserOperationJsonSchema & {
47
+ type: 'object'; oneOf: BrowserOperationJsonSchema[];
48
+ }>;
49
+
50
+ /** Strictly parses unknown JSON input through the same runtime semantic normalizer. */
51
+ export function parseBrowserOperation(value: unknown): BrowserOperation;
52
+
53
+ /** Throws a fixed error on unsupported or credential-bearing preparation input. */
54
+ export function normalizeBrowserOperation<T extends BrowserOperation['operation']>(
55
+ operation: T, args: Extract<BrowserOperation, { operation: T }>['arguments']
56
+ ): Extract<BrowserOperation, { operation: T }>['arguments'];
@@ -0,0 +1,144 @@
1
+ // One wire grammar shared by the customer SDK and Accelerate's upstream adapter.
2
+ // It contains no transport, tenant credentials or provider capabilities.
3
+ const sensitive = /(?:\d[ -]?){13,19}|\b(?:0[1-9]|1[0-2])\s*\/\s*(?:20)?\d{2}\b|\b(?:cvv|cvc|csc|security\s*code)\b["'\s:=]{0,12}\d{3,4}\b|\b(?:Bearer\s|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.|aw[12]\.|agw_[A-Za-z0-9_-]{43}|atk_(?:test|live)_|tok_(?:sandbox|live)_|vgs_)|\b(?:cdp_ws_url|browser_live_view_url|devtoolsFrontendUrl|webSocketDebuggerUrl|kernel_session_id|playwright_endpoint)\b|(?:wss?|chrome-extension|devtools):\/\/|https?:\/\/[^\s/]*(?:onkernel\.com|verygoodproxy\.com|verygoodvault\.com)(?:[/:\s]|$)/i;
4
+ const limits = { timeout: [1, 8000], poll: [10, 1000], depth: [1, 15], repeat: [1, 3], wait: [1, 1000],
5
+ steps: [1, 8], ref: 60, text: 2000, query: 1000, url: 2048 };
6
+ const filters = ['all', 'interactive'];
7
+ const keys = ['Enter', 'Tab', 'Escape', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Space'];
8
+ const refPattern = /^e\d+_\d+$/u;
9
+ const integerSchema = ([minimum, maximum]) => ({ type: 'integer', minimum, maximum });
10
+ const textSchema = (maxLength, minLength = 0) => ({ type: 'string', minLength, maxLength, pattern: '^[^\\u0000-\\u001f\\u007f]*$' });
11
+ const objectSchema = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
12
+ const snapshotSchema = (successor = false) => objectSchema({ filter: { type: 'string', enum: filters },
13
+ ...(!successor ? { ref: { type: 'string', const: 'root' } } : {}), depth: integerSchema(limits.depth) });
14
+ const conditionSchema = { oneOf: [
15
+ objectSchema({ type: { const: 'text' }, text: textSchema(limits.query, 1), exists: { type: 'boolean' } }, ['type', 'text']),
16
+ objectSchema({ type: { type: 'string', enum: ['url', 'title'] }, equals: textSchema(limits.url, 1),
17
+ contains: textSchema(limits.query, 1), exists: { type: 'boolean' } }, ['type', 'equals']),
18
+ objectSchema({ type: { type: 'string', enum: ['url', 'title'] }, contains: textSchema(limits.query, 1),
19
+ exists: { type: 'boolean' } }, ['type', 'contains']),
20
+ ] };
21
+ const refSchema = { type: 'string', pattern: refPattern.source, maxLength: limits.ref,
22
+ description: 'Copy a bare ref from the latest result. Every browser result replaces earlier refs.' };
23
+ const stepSchema = { oneOf: [
24
+ objectSchema({ type: { const: 'click' }, ref: refSchema, button: { const: 'left' }, num_clicks: { const: 1 },
25
+ modifiers: { type: 'array', maxItems: 0 } }, ['type', 'ref']),
26
+ objectSchema({ type: { type: 'string', enum: ['hover', 'scroll_to'] }, ref: refSchema }, ['type', 'ref']),
27
+ objectSchema({ type: { const: 'fill' }, ref: refSchema, value: { ...textSchema(limits.text),
28
+ description: 'Non-sensitive shopping/contact text only. Never enter card data or credentials.' } }, ['type', 'ref', 'value']),
29
+ objectSchema({ type: { const: 'type' }, text: textSchema(limits.text), repeat: integerSchema(limits.repeat) }, ['type', 'text']),
30
+ objectSchema({ type: { const: 'key' }, text: { type: 'string', enum: keys }, repeat: integerSchema(limits.repeat) }, ['type', 'text']),
31
+ objectSchema({ type: { const: 'wait' }, ms: integerSchema(limits.wait) }, ['type']),
32
+ ] };
33
+ const operationSchema = (operation, args) => objectSchema({ operation: { type: 'string', const: operation }, arguments: args }, ['operation', 'arguments']);
34
+ const freezeSchema = value => {
35
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
36
+ for (const child of Object.values(value)) freezeSchema(child);
37
+ Object.freeze(value);
38
+ }
39
+ return value;
40
+ };
41
+
42
+ /** Model-visible structural grammar. Always run normalizeBrowserOperation too;
43
+ * credential detection, URL safety and private checkout permissions remain runtime checks. */
44
+ export const browserOperationJsonSchema = freezeSchema({ $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object',
45
+ description: 'Restricted checkout preparation only. No scripts, screenshots, card entry or final submission.',
46
+ oneOf: [
47
+ operationSchema('browser_snapshot', snapshotSchema()),
48
+ operationSchema('browser_text', objectSchema({})),
49
+ operationSchema('browser_find', objectSchema({ query: textSchema(limits.query, 1) }, ['query'])),
50
+ operationSchema('browser_wait_for', objectSchema({ expect: conditionSchema,
51
+ timeout_ms: integerSchema(limits.timeout), poll_ms: integerSchema(limits.poll) }, ['expect'])),
52
+ operationSchema('browser_act', objectSchema({ steps: { type: 'array', items: stepSchema,
53
+ minItems: limits.steps[0], maxItems: limits.steps[1] }, timeout_ms: integerSchema(limits.timeout), successor: snapshotSchema(true) }, ['steps'])),
54
+ operationSchema('navigate', objectSchema({ url: { ...textSchema(limits.url, 1), format: 'uri',
55
+ description: 'Actual intended public HTTPS merchant URL; never use navigation to retry uncertain payment work.' } }, ['url'])),
56
+ ] });
57
+ const check = value => { if (!value) throw new Error('invalid_browser_operation'); };
58
+ function shape(value, required, optional = []) {
59
+ check(value !== null && typeof value === 'object' && !Array.isArray(value) &&
60
+ [Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
61
+ required.every(field => Object.hasOwn(value, field)) &&
62
+ Object.keys(value).every(field => required.includes(field) || optional.includes(field)));
63
+ }
64
+ function text(value, max, nonempty = false) {
65
+ check(typeof value === 'string' && value.length <= max && (!nonempty || value.length > 0) &&
66
+ !/[\u0000-\u001f\u007f]/u.test(value) && !sensitive.test(value));
67
+ }
68
+ const optionalInteger = (value, min, max) => check(value === undefined || Number.isInteger(value) && value >= min && value <= max);
69
+ function snapshot(args, successor = false) {
70
+ shape(args, [], successor ? ['filter', 'depth'] : ['filter', 'ref', 'depth']);
71
+ check(args.filter === undefined || filters.includes(args.filter));
72
+ check(args.ref === undefined || args.ref === 'root'); optionalInteger(args.depth, ...limits.depth);
73
+ }
74
+ function checkoutUrl(value) {
75
+ text(value, limits.url, true); check(!/\s/u.test(value));
76
+ const url = new URL(value);
77
+ // A checkout fragment selects merchant payment state; retain the exact URL.
78
+ check(url.protocol === 'https:' && !url.username && !url.password && !url.port &&
79
+ value.match(/^https:\/\/([^/?#]+)/)?.[1] === url.hostname &&
80
+ /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(url.hostname) &&
81
+ !/(?:^|\.)(?:localhost|local|internal|lan|corp|home|home\.arpa)$/u.test(url.hostname));
82
+ }
83
+
84
+ /** Validate the restricted preparation operation and return new JSON-only arguments.
85
+ * Harmless Browser Loop click defaults are stripped before reaching the gateway.
86
+ * Unsupported fields fail locally, preserving the active browser's usability.
87
+ */
88
+ export function normalizeBrowserOperation(operation, args) {
89
+ try {
90
+ if (operation === 'navigate') { shape(args, ['url']); checkoutUrl(args.url); }
91
+ else if (operation === 'browser_text') shape(args, []);
92
+ else if (operation === 'browser_find') { shape(args, ['query']); text(args.query, limits.query, true); }
93
+ else if (operation === 'browser_snapshot') snapshot(args);
94
+ else if (operation === 'browser_wait_for') {
95
+ shape(args, ['expect'], ['timeout_ms', 'poll_ms']);
96
+ if (args.expect?.type === 'text') {
97
+ shape(args.expect, ['type', 'text'], ['exists']);
98
+ text(args.expect.text, limits.query, true);
99
+ } else {
100
+ shape(args.expect, ['type'], ['equals', 'contains', 'exists']);
101
+ check(['url', 'title'].includes(args.expect.type));
102
+ check(args.expect.equals !== undefined || args.expect.contains !== undefined);
103
+ for (const name of ['equals', 'contains']) if (args.expect[name] !== undefined) text(args.expect[name], name === 'equals' ? limits.url : limits.query, true);
104
+ }
105
+ check(args.expect.exists === undefined || typeof args.expect.exists === 'boolean');
106
+ optionalInteger(args.timeout_ms, ...limits.timeout); optionalInteger(args.poll_ms, ...limits.poll);
107
+ } else if (operation === 'browser_act') {
108
+ shape(args, ['steps'], ['timeout_ms', 'successor']);
109
+ check(Array.isArray(args.steps) && args.steps.length >= limits.steps[0] && args.steps.length <= limits.steps[1]);
110
+ optionalInteger(args.timeout_ms, ...limits.timeout);
111
+ if (args.successor !== undefined) snapshot(args.successor, true);
112
+ const steps = args.steps.map(step => {
113
+ if (step?.type === 'click') {
114
+ shape(step, ['type', 'ref'], ['button', 'num_clicks', 'modifiers']);
115
+ check(step.button === undefined || step.button === 'left');
116
+ check(step.num_clicks === undefined || step.num_clicks === 1);
117
+ check(step.modifiers === undefined || Array.isArray(step.modifiers) && step.modifiers.length === 0);
118
+ } else if (['hover', 'scroll_to', 'fill'].includes(step?.type)) {
119
+ shape(step, step.type === 'fill' ? ['type', 'ref', 'value'] : ['type', 'ref']);
120
+ } else if (['type', 'key'].includes(step?.type)) {
121
+ shape(step, ['type', 'text'], ['repeat']); text(step.text, limits.text);
122
+ optionalInteger(step.repeat, ...limits.repeat);
123
+ if (step.type === 'key') check(keys.includes(step.text));
124
+ return step;
125
+ } else {
126
+ shape(step, ['type'], ['ms']); check(step.type === 'wait'); optionalInteger(step.ms, ...limits.wait); return step;
127
+ }
128
+ check(typeof step.ref === 'string' && step.ref.length <= limits.ref && refPattern.test(step.ref));
129
+ if (step.type === 'fill') text(step.value, limits.text);
130
+ return step.type === 'click' ? { type: 'click', ref: step.ref } : step;
131
+ });
132
+ args = { ...args, steps };
133
+ } else check(false);
134
+ return JSON.parse(JSON.stringify(args));
135
+ } catch { throw new Error('invalid_browser_operation'); }
136
+ }
137
+
138
+ /** Parse an unknown framework input without requiring an unsafe TypeScript assertion. */
139
+ export function parseBrowserOperation(value) {
140
+ try {
141
+ shape(value, ['operation', 'arguments']);
142
+ return { operation: value.operation, arguments: normalizeBrowserOperation(value.operation, value.arguments) };
143
+ } catch { throw new Error('invalid_browser_operation'); }
144
+ }
@@ -0,0 +1,43 @@
1
+ import type { ManagedCheckoutContext } from './managed-mcp.js';
2
+ import type { RequestOptions } from './index.js';
3
+ import type { ApprovedExtensionRelease, ApprovedExtensionRuntime } from './approved-extension.js';
4
+ import type { BrowserConnectorHost, OwnedBrowserDocument, FencedBrowserDocument, BrowserReadiness } from './browser-connector.js';
5
+
6
+ /** Existing platform automation backend observes only the bound document and
7
+ * fixed extension health, never card values. It retains private CDP credentials. */
8
+ export interface BrowserUseDocumentObservation {
9
+ document_id: string;
10
+ checkout_url: string;
11
+ extension: Omit<ApprovedExtensionRuntime, 'artifact_sha256'>;
12
+ automation: BrowserReadiness['automation'];
13
+ }
14
+ export interface BrowserUsePlatformHost {
15
+ /** Resolve an EXISTING browser from authenticated durable ownership. SDK reads actual provider expiry. */
16
+ resolveOwnedBrowser(context: Readonly<ManagedCheckoutContext>, request: Readonly<{ attempt_key: string; checkout_url: string }>,
17
+ options: RequestOptions): Promise<Omit<OwnedBrowserDocument, 'expires_at'> & { expires_at?: string }>;
18
+ drainAndFence: BrowserConnectorHost['drainAndFence'];
19
+ bindCheckout: BrowserConnectorHost['bindCheckout'];
20
+ resolveCheckoutBrowser: BrowserConnectorHost['resolveCheckoutBrowser'];
21
+ /** Use existing server browser control. Wake only the pinned MV3 registration
22
+ * if suspended. Do not create/navigate a page or accept model-authored code. */
23
+ inspectDocument(context: Readonly<ManagedCheckoutContext>, browser: Readonly<OwnedBrowserDocument>,
24
+ runtime: Readonly<{ runtime_id: string }>, options: RequestOptions): Promise<BrowserUseDocumentObservation>;
25
+ /** Independently verify extension/page cleanup and retain every payment uncertainty fence. */
26
+ confirmLocalCleanup(context: Readonly<ManagedCheckoutContext>, browser: Readonly<FencedBrowserDocument>,
27
+ request: Readonly<{ checkout_id: import('./index.js').CheckoutId }>, options: RequestOptions): Promise<{
28
+ credentials_cleared: true; fence_retained: true;
29
+ }>;
30
+ }
31
+ /** Packaged Browser Use Cloud v4 read-only provider adapter. API key stays in
32
+ * the authenticated backend. No create, extension upload, navigation or stop is
33
+ * performed; Accelerate's executor/reaper owns payment retirement. */
34
+ export function createBrowserUseHost(options: {
35
+ apiKey: string;
36
+ merchantId: string;
37
+ extensionId: string;
38
+ extensionRelease: ApprovedExtensionRelease;
39
+ host: BrowserUsePlatformHost;
40
+ fetchImpl?: typeof fetch;
41
+ /** Test clock only. */
42
+ now?: () => number;
43
+ }): Readonly<BrowserConnectorHost>;
@@ -0,0 +1,147 @@
1
+ import { AccelerateError } from './index.js';
2
+ import { validateApprovedExtensionRelease, verifyApprovedExtensionRuntime } from './approved-extension.js';
3
+ import { normalizeBrowserOperation } from './browser-operations.js';
4
+
5
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
6
+ const RUNTIME = /^[a-p]{32}$/u;
7
+ const BASE = 'https://api.browser-use.com/api/v4';
8
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
9
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
10
+ const check = (ok, code = 'integration_unavailable') => { if (!ok) throw new AccelerateError(code); };
11
+ function exact(value, required, optional = []) {
12
+ check(object(value) && required.every(key => Object.hasOwn(value, key)) &&
13
+ Object.keys(value).every(key => required.includes(key) || optional.includes(key)));
14
+ }
15
+ function bounded(work, signal) {
16
+ return new Promise((resolve, reject) => {
17
+ const abort = () => { signal.removeEventListener('abort', abort); reject(new AccelerateError('integration_unavailable')); };
18
+ signal.addEventListener('abort', abort, { once: true });
19
+ if (signal.aborted) { abort(); return; }
20
+ Promise.resolve(work).then(value => { signal.removeEventListener('abort', abort); resolve(value); }, abort);
21
+ });
22
+ }
23
+ async function json(response, signal) {
24
+ const reader = response.body.getReader(), chunks = []; let length = 0;
25
+ const cancel = () => { void reader.cancel().catch(() => {}); };
26
+ signal.addEventListener('abort', cancel, { once: true });
27
+ try {
28
+ for (;;) {
29
+ check(!signal.aborted);
30
+ const { value, done } = await bounded(reader.read(), signal);
31
+ check(!signal.aborted);
32
+ if (done) break;
33
+ length += value.byteLength; check(length <= 262144); chunks.push(value);
34
+ }
35
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
36
+ } finally {
37
+ signal.removeEventListener('abort', cancel); cancel();
38
+ try { reader.releaseLock(); } catch {}
39
+ }
40
+ }
41
+ const freeze = value => {
42
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(freeze); Object.freeze(value); }
43
+ return value;
44
+ };
45
+
46
+ export function createBrowserUseHost(configuration = {}) {
47
+ exact(configuration, ['apiKey', 'merchantId', 'extensionId', 'extensionRelease', 'host'], ['fetchImpl', 'now']);
48
+ const { apiKey, merchantId, extensionId, host, fetchImpl = fetch, now = Date.now } = configuration;
49
+ check(typeof apiKey === 'string' && apiKey.length >= 16 && apiKey.length <= 500 && !/[\u0000-\u0020\u007f]/u.test(apiKey) &&
50
+ UUID.test(merchantId) && UUID.test(extensionId) && typeof fetchImpl === 'function' && typeof now === 'function' && host &&
51
+ ['resolveOwnedBrowser', 'drainAndFence', 'bindCheckout', 'resolveCheckoutBrowser', 'inspectDocument', 'confirmLocalCleanup']
52
+ .every(key => typeof host[key] === 'function'), 'configuration_invalid');
53
+ let release;
54
+ try { release = validateApprovedExtensionRelease(configuration.extensionRelease); }
55
+ catch { throw new AccelerateError('configuration_invalid'); }
56
+ check(release.qualifications.some(value => value.provider === 'browser_use'), 'configuration_invalid');
57
+ const callHost = async (method, ...args) => {
58
+ try { return await host[method](...args); }
59
+ catch { throw new AccelerateError('integration_unavailable'); }
60
+ };
61
+ const owned = (context, document) => {
62
+ exact(document, ['owner', 'browser', 'document_id', 'checkout_url'], ['expires_at', 'fence_id']);
63
+ exact(document.owner, ['customer_id', 'workspace_id', 'conversation_id', 'phone_number']);
64
+ check(['customer_id', 'workspace_id', 'conversation_id', 'phone_number'].every(key => document.owner[key] === context[key]), 'permission_denied');
65
+ exact(document.browser, ['provider', 'browser_id', 'custody_mode']);
66
+ check(document.browser.provider === 'browser_use' && document.browser.custody_mode === 'customer_backend_trusted' && UUID.test(document.browser.browser_id) &&
67
+ typeof document.document_id === 'string' && /^[A-Za-z0-9_.:-]{1,200}$/u.test(document.document_id), 'permission_denied');
68
+ try { normalizeBrowserOperation('navigate', { url: document.checkout_url }); }
69
+ catch { throw new AccelerateError('permission_denied'); }
70
+ return { owner: { ...document.owner }, browser: { ...document.browser }, document_id: document.document_id, checkout_url: document.checkout_url };
71
+ };
72
+ const resource = async (context, document, requiredStatus, options = {}) => {
73
+ const binding = owned(context, document).browser;
74
+ const timeout = AbortSignal.timeout(10000), signal = AbortSignal.any([timeout, ...(options.signal ? [options.signal] : [])]);
75
+ const endpoint = `${BASE}/browsers/${binding.browser_id}`;
76
+ let response;
77
+ try {
78
+ check(!signal.aborted);
79
+ const pending = Promise.resolve(fetchImpl(endpoint, { method: 'GET', headers: {
80
+ 'X-Browser-Use-API-Key': apiKey, Accept: 'application/json',
81
+ }, cache: 'no-store', redirect: 'error', signal }));
82
+ void pending.then(value => { if (signal.aborted) void value.body?.cancel().catch(() => {}); }, () => {});
83
+ response = await bounded(pending, signal);
84
+ check(response.ok && !response.redirected && (!response.url || response.url === endpoint) &&
85
+ /^application\/json(?:\s*;|$)/iu.test(response.headers.get('content-type') ?? '') && response.body);
86
+ const length = response.headers.get('content-length');
87
+ check(length === null || /^\d+$/u.test(length) && Number(length) <= 262144);
88
+ const value = await json(response, signal);
89
+ const metadata = value?.metadata;
90
+ check(object(value) && value.id === binding.browser_id && value.status === requiredStatus &&
91
+ value.recordingAvailable === false && (value.recordingUrl == null || value.recordingUrl === '') &&
92
+ object(metadata) && metadata.accelerate_merchant_id === merchantId &&
93
+ metadata.accelerate_owner_user_id === context.customer_id && metadata.accelerate_workspace_id === context.workspace_id &&
94
+ metadata.accelerate_custody_mode === 'customer_backend_trusted' && metadata.accelerate_agent_extension_id === extensionId &&
95
+ metadata.accelerate_extension_sha256 === release.artifact.sha256 && metadata.accelerate_extension_version === release.extension_version &&
96
+ typeof value.timeoutAt === 'string' && Number.isFinite(Date.parse(value.timeoutAt)) &&
97
+ (requiredStatus === 'stopped' || Date.parse(value.timeoutAt) > now()) &&
98
+ Array.isArray(value.extensions) && value.extensions.length === 1 && value.extensions[0]?.id === extensionId &&
99
+ value.extensions[0]?.version === release.extension_version && RUNTIME.test(value.extensions[0]?.runtimeId ?? ''));
100
+ // Deliberately project away provider CDP/viewer URLs and unknown response fields.
101
+ const read = freeze({ expires_at: value.timeoutAt, runtime_id: value.extensions[0].runtimeId });
102
+ if (!options.withConnection) return read;
103
+ check(typeof value.cdpUrl === 'string' && value.cdpUrl.length <= 2048 && typeof value.startedAt === 'string');
104
+ return freeze({ ...read, connection: freeze({ id: value.id, status: value.status, cdpUrl: value.cdpUrl, startedAt: value.startedAt,
105
+ timeoutAt: value.timeoutAt, recordingAvailable: false, metadata, extensions: value.extensions }) });
106
+ } catch {
107
+ void response?.body?.cancel().catch(() => {});
108
+ throw new AccelerateError(options.signal?.aborted ? 'request_cancelled' : 'integration_unavailable');
109
+ }
110
+ };
111
+ return Object.freeze({
112
+ async resolveOwnedBrowser(context, request, options) {
113
+ const document = await callHost('resolveOwnedBrowser', context, request, options);
114
+ const observed = await resource(context, document, 'active', options);
115
+ return { ...owned(context, document), expires_at: observed.expires_at };
116
+ },
117
+ /** This browser's connection for Accelerate, read here so the provider key stays with the host. */
118
+ async clientBrowserResource(context, document, options) {
119
+ return (await resource(context, document, 'active', { ...options, withConnection: true })).connection;
120
+ },
121
+ async inspectBrowser(context, document, options) {
122
+ const observed = await resource(context, document, 'active', options);
123
+ const current = await callHost('inspectDocument', context, document, freeze({ runtime_id: observed.runtime_id }), options);
124
+ exact(current, ['document_id', 'checkout_url', 'extension', 'automation']);
125
+ exact(current.extension, ['extension_version', 'success', 'initialized', 'wasmInitialized', 'autosaveDisabled',
126
+ 'boundedRetentionSupported', 'maximumRetentionMs'], ['bridge_protocol']);
127
+ const extension = { ...current.extension, artifact_sha256: release.artifact.sha256 };
128
+ try { verifyApprovedExtensionRuntime(extension, release, { provider: 'browser_use' }); }
129
+ catch { throw new AccelerateError('integration_unavailable'); }
130
+ return { ...owned(context, document), document_id: current.document_id, checkout_url: current.checkout_url, expires_at: observed.expires_at,
131
+ recording_disabled: true, extension,
132
+ automation: current.automation };
133
+ },
134
+ drainAndFence: (...args) => callHost('drainAndFence', ...args),
135
+ bindCheckout: (...args) => callHost('bindCheckout', ...args),
136
+ resolveCheckoutBrowser: (...args) => callHost('resolveCheckoutBrowser', ...args),
137
+ async confirmCleanup(context, document, request, options) {
138
+ // A service closure or stop response is insufficient: GET the exact
139
+ // original provider resource before accepting the local cleanup proof.
140
+ await resource(context, document, 'stopped', options);
141
+ const receipt = await callHost('confirmLocalCleanup', context, document, request, options);
142
+ exact(receipt, ['credentials_cleared', 'fence_retained']);
143
+ check(receipt.credentials_cleared === true && receipt.fence_retained === true);
144
+ return { provider_browser_stopped: true, credentials_cleared: true, fence_retained: true };
145
+ },
146
+ });
147
+ }
@@ -0,0 +1,23 @@
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' |
3
+ 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
4
+ export type CheckoutDiagnosticAction = 'get_status' | 'continue_private_chat' | 'prepare_checkout' | 'apply_details' |
5
+ 'request_approval' | 'fill' | 'submit' | 'cancel' | 'reconcile_order';
6
+ export type CheckoutCleanupStatus = 'not_required' | 'not_started' | 'pending' | 'confirmed';
7
+ export interface CheckoutDiagnostics {
8
+ version: 'v1';
9
+ stage: CheckoutDiagnosticStage;
10
+ /** Guidance only. Server authorization, revision, private consent and one-use checks remain mandatory. */
11
+ next_actions: CheckoutDiagnosticAction[];
12
+ support_reference: `acs_${string}`;
13
+ /** not_required means no retained browser reference. confirmed means worker closure acknowledgement; neither is independent provider proof. */
14
+ cleanup_status: CheckoutCleanupStatus;
15
+ submit_deadline?: string;
16
+ }
17
+ export const checkoutDiagnosticStages: readonly CheckoutDiagnosticStage[];
18
+ export const checkoutDiagnosticActions: readonly CheckoutDiagnosticAction[];
19
+ export const checkoutCleanupStatuses: readonly CheckoutCleanupStatus[];
20
+ export const checkoutDiagnosticsJsonSchema: Readonly<Record<string, unknown>>;
21
+ export function validateCheckoutDiagnostics(value: unknown, checkout?: {
22
+ status: string; purpose?: string; submit_allowed?: boolean; submit_expires_at?: string; approval_policy?: string; purchase_approved?: boolean;
23
+ }): CheckoutDiagnostics;
@@ -0,0 +1,67 @@
1
+ // Safe lifecycle metadata only. No transport, credentials, provider text or automatic actions.
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',
4
+ 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
5
+ export const checkoutDiagnosticActions = Object.freeze(['get_status', 'continue_private_chat', 'prepare_checkout', 'apply_details',
6
+ 'request_approval', 'fill', 'submit', 'cancel', 'reconcile_order']);
7
+ export const checkoutCleanupStatuses = Object.freeze(['not_required', 'not_started', 'pending', 'confirmed']);
8
+ const freeze = (value) => { if (value && typeof value === 'object' && !Object.isFrozen(value)) {
9
+ for (const item of Object.values(value))
10
+ freeze(item);
11
+ Object.freeze(value);
12
+ } return value; };
13
+ export const checkoutDiagnosticsJsonSchema = freeze({ type: 'object', additionalProperties: false,
14
+ required: ['version', 'stage', 'next_actions', 'support_reference', 'cleanup_status'], properties: {
15
+ version: { const: 'v1' }, stage: { type: 'string', enum: checkoutDiagnosticStages },
16
+ next_actions: { type: 'array', minItems: 1, maxItems: checkoutDiagnosticActions.length, uniqueItems: true,
17
+ items: { type: 'string', enum: checkoutDiagnosticActions } },
18
+ support_reference: { type: 'string', pattern: '^acs_[a-f0-9]{32}$' },
19
+ cleanup_status: { type: 'string', enum: checkoutCleanupStatuses }, submit_deadline: { type: 'string', format: 'date-time' },
20
+ } });
21
+ const terminal = new Set(['cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
22
+ const timestamp = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(value) && Number.isFinite(Date.parse(value));
23
+ const check = (value) => { if (!value) {
24
+ const error = Object.assign(Error('Invalid Accelerate checkout diagnostics.'), { code: 'invalid_response' });
25
+ throw error;
26
+ } };
27
+ /** Validate fixed metadata; optional checkout binding prevents contradictory action guidance. Never authorizes an action. */
28
+ export function validateCheckoutDiagnostics(input, checkout) {
29
+ // Validation below checks every property before returning this value.
30
+ const value = input;
31
+ check(value !== null && typeof value === 'object' && !Array.isArray(value) &&
32
+ [Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
33
+ Object.keys(value).every(key => Object.hasOwn(checkoutDiagnosticsJsonSchema.properties, key)) &&
34
+ checkoutDiagnosticsJsonSchema.required.every(key => Object.hasOwn(value, key)) && value.version === 'v1' &&
35
+ checkoutDiagnosticStages.includes(value.stage) && Array.isArray(value.next_actions) && value.next_actions.length >= 1 &&
36
+ value.next_actions.length <= checkoutDiagnosticActions.length && new Set(value.next_actions).size === value.next_actions.length &&
37
+ value.next_actions.every(action => checkoutDiagnosticActions.includes(action)) &&
38
+ typeof value.support_reference === 'string' && /^acs_[a-f0-9]{32}$/.test(value.support_reference) &&
39
+ checkoutCleanupStatuses.includes(value.cleanup_status) && (value.submit_deadline === undefined || timestamp(value.submit_deadline)));
40
+ if (checkout !== undefined) {
41
+ const { status, purpose = 'purchase', submit_allowed, approval_policy, purchase_approved } = checkout;
42
+ check(terminal.has(status) ? value.stage === status : status === 'wallet_required'
43
+ ? ['not_started', 'identity_required', 'otp_required', 'selection_required'].includes(value.stage)
44
+ : ['selection_ready', 'details_ready'].includes(status) ? value.stage === 'preparing_checkout'
45
+ : status === 'approval_required' ? ['approval_required', 'cvv_required'].includes(value.stage) : approval_policy === 'after_fill' && status === 'ready_for_submit' ? ['review_delivery_required', 'review_delivered', 'approval_required', 'approved_for_submit'].includes(value.stage) : value.stage === status);
46
+ check(status === 'ready_for_submit' ? timestamp(value.submit_deadline) && value.submit_deadline === checkout.submit_expires_at : value.submit_deadline === undefined);
47
+ for (const action of value.next_actions) {
48
+ if (action === 'submit')
49
+ check(status === 'ready_for_submit' && purpose === 'purchase' && submit_allowed !== false && (approval_policy !== 'after_fill' || purchase_approved === true));
50
+ if (action === 'fill')
51
+ check(status === 'approved' && purpose !== 'sandbox_source_comparison');
52
+ if (['prepare_checkout', 'apply_details'].includes(action))
53
+ check(['selection_ready', 'details_ready'].includes(status));
54
+ if (action === 'request_approval')
55
+ check(status === 'details_ready');
56
+ if (action === 'continue_private_chat')
57
+ check(['wallet_required', 'approval_required'].includes(status) || approval_policy === 'after_fill' && status === 'ready_for_submit' && !purchase_approved);
58
+ if (action === 'reconcile_order')
59
+ check(['submit_attempted', 'outcome_unknown'].includes(status));
60
+ }
61
+ if (!terminal.has(status))
62
+ check(value.cleanup_status !== 'pending' && value.cleanup_status !== 'confirmed');
63
+ if (terminal.has(status))
64
+ check(value.cleanup_status !== 'not_started');
65
+ }
66
+ return value;
67
+ }
package/cli.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import { AccelerateClient, AccelerateError } from './index.js';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, mkdir, open, readdir, realpath, readFile } from 'node:fs/promises';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const help = `Usage: accelerate-agent doctor
9
+ accelerate-agent init <empty-directory> [--browser existing]
10
+ accelerate-agent --version
11
+
12
+ Set ACCELERATE_API_KEY and ACCELERATE_MERCHANT_ID on your server.
13
+ ACCELERATE_BASE_URL is optional for Accelerate's hosted service.
14
+
15
+ Doctor checks API connectivity and the credential pair. It does not create a
16
+ checkout, send a code, release credentials, fill a form or submit an order.
17
+ It does not qualify the downstream payment source or browser configuration.
18
+
19
+ Init copies the packaged synthetic MCP starter without credentials, network
20
+ requests or overwrites. --browser existing selects the new provider-independent
21
+ existing-browser starter; omission preserves the managed Kernel starter.
22
+ Existing symlink paths and nonempty targets are rejected.
23
+ `;
24
+ const starterFiles = ['package.json', '.env.example', '.gitignore', 'README.md', 'server.mjs', 'runtime.mjs',
25
+ 'synthetic.mjs', 'smoke.mjs', 'trusted-context.example.mjs', 'tests/starter.test.mjs'];
26
+ const initFailure = code => { const error = Error('Starter initialization unavailable.'); error.code = code; throw error; };
27
+ async function initializeStarter(directory, existingBrowser = false) {
28
+ if (typeof directory !== 'string' || !directory || directory.length > 2048 || /[\u0000-\u001f\u007f]/u.test(directory)) initFailure('init_target_invalid');
29
+ const target = resolve(directory), parent = dirname(target);
30
+ if (await realpath(parent) !== parent || !(await lstat(parent)).isDirectory()) initFailure('init_target_invalid');
31
+ try {
32
+ const stat = await lstat(target);
33
+ if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(target) !== target) initFailure('init_target_invalid');
34
+ if ((await readdir(target)).length !== 0) initFailure('init_target_not_empty');
35
+ } catch (error) {
36
+ if (error?.code !== 'ENOENT') throw error;
37
+ await mkdir(target, { mode: 0o700 });
38
+ }
39
+ const sourceRoot = resolve(fileURLToPath(new URL(existingBrowser ? './partner-starter/' : './starter/', import.meta.url)));
40
+ if (await realpath(sourceRoot) !== sourceRoot) initFailure('init_failed');
41
+ const original = await lstat(target); let total = 0;
42
+ for (const name of starterFiles) {
43
+ const current = await lstat(target);
44
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== original.dev || current.ino !== original.ino ||
45
+ await realpath(target) !== target) initFailure('init_target_invalid');
46
+ // npm strips .gitignore from archives; emit it from this explicit template.
47
+ const sourcePath = join(sourceRoot, name === '.gitignore' ? 'gitignore.template' : name);
48
+ if (await realpath(sourcePath) !== sourcePath) initFailure('init_failed');
49
+ const source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
50
+ let data;
51
+ try {
52
+ const stat = await source.stat();
53
+ if (!stat.isFile() || stat.size < 1 || stat.size > 65536 || (total += stat.size) > 262144) initFailure('init_failed');
54
+ data = await source.readFile();
55
+ } finally { await source.close(); }
56
+ const destination = join(target, name), folder = dirname(destination);
57
+ if (folder !== target) {
58
+ try { await mkdir(folder, { mode: 0o700 }); } catch (error) { if (error?.code !== 'EEXIST') throw error; }
59
+ if (!(await lstat(folder)).isDirectory() || await realpath(folder) !== folder) initFailure('init_target_invalid');
60
+ }
61
+ const output = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
62
+ try { await output.writeFile(data); } finally { await output.close(); }
63
+ }
64
+ }
65
+ const args = process.argv.slice(2);
66
+ if (args.length === 1 && ['--version', '-v'].includes(args[0])) {
67
+ const { version } = JSON.parse(await readFile(new URL('./package.json', import.meta.url), 'utf8'));
68
+ if (!/^\d+\.\d+\.\d+$/.test(version)) throw Error('Invalid SDK package version.');
69
+ process.stdout.write(version + '\n');
70
+ } else if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
71
+ process.stdout.write(help);
72
+ } else if (args[0] === 'init' && (args.length === 2 || args.length === 4 && args[2] === '--browser' && args[3] === 'existing')) {
73
+ try {
74
+ await initializeStarter(args[1], args.length === 4);
75
+ process.stdout.write(JSON.stringify({ ok: true, status: 'initialized', mode: 'synthetic', next_action: 'install_dependencies' }) + '\n');
76
+ } catch (error) {
77
+ const code = ['init_target_invalid', 'init_target_not_empty'].includes(error?.code) ? error.code : 'init_failed';
78
+ process.stdout.write(JSON.stringify({ ok: false, code }) + '\n'); process.exitCode = 1;
79
+ }
80
+ } else if (args.length !== 1 || args[0] !== 'doctor') {
81
+ process.stderr.write(help);
82
+ process.exitCode = 2;
83
+ } else {
84
+ try {
85
+ const result = await AccelerateClient.fromEnv().connection.check();
86
+ // Do not print environment values, merchant identifiers or upstream bodies.
87
+ process.stdout.write(JSON.stringify({ ok: true, status: result.status, api_version: result.api_version,
88
+ interaction_modes: result.interaction_modes, currencies: result.currencies,
89
+ downstream_payment_readiness: 'not_checked' }) + '\n');
90
+ } catch (error) {
91
+ const codes = new Set(['missing_api_key', 'missing_merchant_id', 'invalid_api_key', 'invalid_merchant_id',
92
+ 'invalid_base_url', 'invalid_input', 'tenant_unauthorized', 'unauthorized', 'permission_denied',
93
+ 'configuration_error', 'configuration_invalid', 'route_not_found', 'invalid_response',
94
+ 'request_timeout', 'request_cancelled', 'request_failed', 'upstream_unavailable']);
95
+ const actions = new Set(['check_configuration', 'check_request', 'check_status', 'stop']);
96
+ const code = error instanceof AccelerateError && codes.has(error.code) ? error.code : 'request_failed';
97
+ const nextAction = error instanceof AccelerateError && actions.has(error.nextAction)
98
+ ? error.nextAction : 'check_configuration';
99
+ process.stdout.write(JSON.stringify({ ok: false, code, next_action: nextAction }) + '\n');
100
+ process.exitCode = 1;
101
+ }
102
+ }
@@ -0,0 +1,6 @@
1
+ # Required: provide these through your backend's secret configuration.
2
+ ACCELERATE_API_KEY=
3
+ ACCELERATE_MERCHANT_ID=
4
+
5
+ # Optional: omit when using Accelerate's hosted managed service.
6
+ # ACCELERATE_BASE_URL=https://accelerate-agent-sdk.vercel.app