@weaccelerateinc/agent-sdk 0.0.0-stage → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +151 -2
  2. package/approved-extension-releases/0.0.0.1.json +41 -0
  3. package/approved-extension-releases/0.0.0.2.json +34 -0
  4. package/approved-extension.d.ts +24 -0
  5. package/approved-extension.js +90 -0
  6. package/browser-connector.d.ts +109 -0
  7. package/browser-connector.js +218 -0
  8. package/browser-operations.d.ts +56 -0
  9. package/browser-operations.js +144 -0
  10. package/browser-use-host.d.ts +46 -0
  11. package/browser-use-host.js +149 -0
  12. package/checkout-diagnostics.d.ts +34 -0
  13. package/checkout-diagnostics.js +81 -0
  14. package/cli.js +105 -0
  15. package/customer.env.example +6 -0
  16. package/host-adapter.d.ts +90 -0
  17. package/host-adapter.js +357 -0
  18. package/index.d.ts +253 -0
  19. package/index.js +504 -0
  20. package/linq-messaging.d.ts +45 -0
  21. package/linq-messaging.js +123 -0
  22. package/managed-mcp.d.ts +156 -0
  23. package/managed-mcp.js +427 -0
  24. package/package.json +114 -4
  25. package/partner-starter/.env.example +13 -0
  26. package/partner-starter/README.md +61 -0
  27. package/partner-starter/gitignore.template +5 -0
  28. package/partner-starter/package.json +21 -0
  29. package/partner-starter/runtime.mjs +33 -0
  30. package/partner-starter/server.mjs +13 -0
  31. package/partner-starter/smoke.mjs +61 -0
  32. package/partner-starter/synthetic.mjs +256 -0
  33. package/partner-starter/tests/starter.test.mjs +119 -0
  34. package/partner-starter/trusted-context.example.mjs +23 -0
  35. package/partner.d.ts +27 -0
  36. package/partner.js +53 -0
  37. package/postgres-host.d.ts +14 -0
  38. package/postgres-host.js +102 -0
  39. package/private-chat.d.ts +104 -0
  40. package/private-chat.js +316 -0
  41. package/private-ingress.d.ts +46 -0
  42. package/private-ingress.js +132 -0
  43. package/starter/.env.example +19 -0
  44. package/starter/README.md +82 -0
  45. package/starter/gitignore.template +5 -0
  46. package/starter/package.json +21 -0
  47. package/starter/runtime.mjs +66 -0
  48. package/starter/server.mjs +13 -0
  49. package/starter/smoke.mjs +55 -0
  50. package/starter/synthetic.mjs +163 -0
  51. package/starter/tests/starter.test.mjs +224 -0
  52. package/starter/trusted-context.example.mjs +49 -0
@@ -0,0 +1,123 @@
1
+ import { AccelerateError } from './index.js';
2
+ import { authenticatedCheckoutContext } from './host-adapter.js';
3
+
4
+ const HANDLE = /^[A-Za-z0-9_-]{1,500}$/;
5
+ const MESSAGE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/;
6
+ const maximumReviewImageBytes = 8 * 1024 * 1024;
7
+ const check = condition => { if (!condition) throw new AccelerateError('request_failed'); };
8
+ const phone = value => typeof value === 'string' && /^\+[1-9][\d ()-]+$/.test(value) ? value.replace(/[ ()-]/g, '') : undefined;
9
+ function receipt(value, chatId, expectedId) {
10
+ check(value && value.chat_id === chatId && value.is_from_me === true && MESSAGE.test(value.id) && (!expectedId || value.id === expectedId));
11
+ if (!['sent', 'delivered', 'read'].includes(value.delivery_status)) return undefined;
12
+ check(typeof value.sent_at === 'string' && Number.isFinite(Date.parse(value.sent_at)) && Date.parse(value.sent_at) <= Date.now());
13
+ return { message_id: value.id, sent_at: new Date(value.sent_at).toISOString() };
14
+ }
15
+ async function wait(ms, signal) {
16
+ await new Promise((resolve, reject) => {
17
+ const timer = setTimeout(done, ms);
18
+ function done() { signal?.removeEventListener('abort', abort); resolve(); }
19
+ function abort() { clearTimeout(timer); signal?.removeEventListener('abort', abort); reject(new AccelerateError('request_cancelled')); }
20
+ signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted) abort();
21
+ });
22
+ }
23
+ function boundedSignal(signal, expiresAt) {
24
+ const remaining = expiresAt === undefined ? 45000 : Date.parse(expiresAt) - Date.now(); check(Number.isFinite(remaining) && remaining > 0);
25
+ const timeout = AbortSignal.timeout(Math.min(45000, remaining));
26
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
27
+ }
28
+ function prompt(message) {
29
+ check(message && typeof message.text === 'string' && message.text.length > 0 && message.text.length <= 10000 && typeof message.idempotency_key === 'string' && /^[A-Za-z0-9_.:-]{1,200}$/.test(message.idempotency_key));
30
+ }
31
+
32
+ /** Native Linq transport. Inject the installed @linqapp/sdk client configured
33
+ * with its server-only credential; the SDK does not install or own that key. */
34
+ export function createLinqMessagingAdapter({ client, sendingPhone, resolveDestination, fetchImpl = globalThis.fetch } = {}) {
35
+ check(typeof client?.chats?.retrieve === 'function' && typeof client?.chats?.messages?.send === 'function' && typeof client?.messages?.retrieve === 'function' && typeof client?.attachments?.create === 'function' && /^\+[1-9]\d{6,14}$/.test(sendingPhone) && typeof resolveDestination === 'function' && typeof fetchImpl === 'function');
36
+ const destination = async (input, signal) => {
37
+ const context = authenticatedCheckoutContext(input), chatId = await resolveDestination(context);
38
+ check(typeof chatId === 'string' && HANDLE.test(chatId) && chatId !== 'pending' && context.phone_number !== sendingPhone);
39
+ signal.throwIfAborted();
40
+ const chat = await client.chats.retrieve(chatId, { signal, maxRetries: 0, timeout: 5000 });
41
+ check(chat?.id === chatId && chat.is_group === false && Array.isArray(chat.handles) && chat.handles.length === 2);
42
+ const own = chat.handles.filter(handle => handle.is_me === true), recipient = chat.handles.filter(handle => handle.is_me === false);
43
+ check(own.length === 1 && recipient.length === 1 && phone(own[0].handle) === sendingPhone && phone(recipient[0].handle) === context.phone_number && chat.handles.every(handle => handle.left_at == null && !['left', 'removed'].includes(handle.status)));
44
+ return chatId;
45
+ };
46
+ const send = async (chatId, message, signal, expiresAt) => {
47
+ signal.throwIfAborted();
48
+ const sent = await client.chats.messages.send(chatId, { message }, { signal, maxRetries: 0, timeout: 15000 });
49
+ check(sent?.chat_id === chatId && MESSAGE.test(sent?.message?.id));
50
+ const expectedId = sent.message.id;
51
+ // The send envelope proves this is our send; retrieval independently checks
52
+ // direction. Neither API's queue timestamp can arm purchase approval.
53
+ let current = { ...sent.message, chat_id: chatId, is_from_me: true };
54
+ for (let attempt = 0; attempt < 12; attempt++) {
55
+ signal.throwIfAborted(); check(expiresAt === undefined || Date.parse(expiresAt) > Date.now());
56
+ const confirmed = receipt(current, chatId, expectedId);
57
+ if (confirmed) return confirmed;
58
+ check(current.delivery_status !== 'failed');
59
+ await wait(1000, signal);
60
+ current = await client.messages.retrieve(expectedId, { signal, maxRetries: 0, timeout: 5000 });
61
+ }
62
+ throw new AccelerateError('request_failed');
63
+ };
64
+ return Object.freeze({
65
+ async sendText(context, message, options = {}) {
66
+ try {
67
+ prompt(message); const signal = boundedSignal(options.signal), chatId = await destination(context, signal);
68
+ return await send(chatId, { parts: [{ type: 'text', value: message.text }], idempotency_key: message.idempotency_key }, signal);
69
+ } catch { throw new AccelerateError('request_failed'); }
70
+ },
71
+ async sendReview(context, message) {
72
+ let uploadCopy;
73
+ try {
74
+ prompt(message);
75
+ check(Buffer.isBuffer(message.bytes) && message.bytes.length > 8 && message.bytes.length <= maximumReviewImageBytes && message.bytes.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10])) && message.contentType === 'image/png' && message.signal instanceof AbortSignal);
76
+ const signal = boundedSignal(message.signal, message.delivery_expires_at), chatId = await destination(context, signal);
77
+ const attachment = await client.attachments.create({ filename: 'accelerate-payment-review.png', content_type: 'image/png', size_bytes: message.bytes.length }, { signal, maxRetries: 0, timeout: 10000 });
78
+ let url; try { url = new URL(attachment.upload_url); } catch { check(false); }
79
+ check(url.protocol === 'https:' && !url.username && !url.password && attachment.http_method === 'PUT' && HANDLE.test(attachment.attachment_id) && attachment.required_headers && typeof attachment.required_headers === 'object' && Object.entries(attachment.required_headers).every(([key, value]) => typeof key === 'string' && typeof value === 'string'));
80
+ signal.throwIfAborted(); uploadCopy = Buffer.from(message.bytes);
81
+ const uploaded = await fetchImpl(url, { method: 'PUT', redirect: 'error', cache: 'no-store', credentials: 'omit', referrerPolicy: 'no-referrer', signal, headers: attachment.required_headers, body: uploadCopy });
82
+ try { await uploaded.body?.cancel(); } catch {}
83
+ check(uploaded.ok && !uploaded.redirected); signal.throwIfAborted();
84
+ return await send(chatId, { parts: [{ type: 'media', attachment_id: attachment.attachment_id }, { type: 'text', value: message.text }], idempotency_key: message.idempotency_key }, signal, message.delivery_expires_at);
85
+ } catch { throw new AccelerateError('request_failed'); }
86
+ finally { uploadCopy?.fill(0); }
87
+ },
88
+ });
89
+ }
90
+
91
+ /** Signature/OIDC verification and identity resolution are host dependencies.
92
+ * Never treat provider sender text, chat ID or a successful JSON parse as auth. */
93
+ export function createLinqWebhookAuthenticator({ verifyWebhook, resolveIdentity, retrieveChat } = {}) {
94
+ check(typeof verifyWebhook === 'function' && typeof resolveIdentity === 'function' && typeof retrieveChat === 'function');
95
+ return async (request, rawBody) => {
96
+ let decoded;
97
+ try {
98
+ check(rawBody instanceof Uint8Array && rawBody.byteLength <= 262144 && await verifyWebhook(request, rawBody) === true);
99
+ decoded = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(rawBody));
100
+ if (decoded?.event_type !== 'message.received') return { kind: 'ignore' };
101
+ const data = decoded.data;
102
+ check(data && MESSAGE.test(data.id) && ['inbound', 'outbound'].includes(data.direction) && HANDLE.test(data.chat?.id) && data.sender_handle && typeof data.sender_handle.handle === 'string' && Array.isArray(data.parts) && data.parts.length <= 100);
103
+ if (data.direction !== 'inbound' || data.sender_handle.is_me === true) return { kind: 'ignore' };
104
+ const senderPhone = phone(data.sender_handle.handle); check(senderPhone && /^\+[1-9]\d{6,14}$/.test(senderPhone));
105
+ let isGroup = data.chat.is_group;
106
+ if (typeof isGroup !== 'boolean') { const chat = await retrieveChat(data.chat.id); check(chat?.id === data.chat.id && typeof chat.is_group === 'boolean'); isGroup = chat.is_group; }
107
+ check(isGroup === false);
108
+ const context = authenticatedCheckoutContext(await resolveIdentity({ phone_number: senderPhone, chat_id: data.chat.id }));
109
+ check(context.phone_number === senderPhone);
110
+ const parts = data.parts.map(part => { check(part && typeof part.type === 'string' && (part.value === undefined || typeof part.value === 'string')); return part; });
111
+ const text = parts.filter(part => ['text', 'link'].includes(part.type)).map(part => part.value ?? '').join('\n').trim(); check(text.length <= 2000);
112
+ const result = { authenticated: true, direct: true, context, message_id: data.id, text };
113
+ // Historical/reconciled deliveries cannot approve a current payment.
114
+ if (!data.reconciled_at && data.sent_at != null) { check(typeof data.sent_at === 'string' && Number.isFinite(Date.parse(data.sent_at))); result.sent_at = new Date(data.sent_at).toISOString(); }
115
+ if (data.reply_to?.message_id !== undefined) { check(MESSAGE.test(data.reply_to.message_id)); result.reply_to_message_id = data.reply_to.message_id; }
116
+ return result;
117
+ } catch { throw new AccelerateError('permission_denied'); }
118
+ finally {
119
+ if (Array.isArray(decoded?.data?.parts)) for (const part of decoded.data.parts) { if (part && typeof part === 'object' && Object.hasOwn(part, 'value')) part.value = ''; }
120
+ decoded = undefined;
121
+ }
122
+ };
123
+ }
@@ -0,0 +1,156 @@
1
+ import type { AccelerateClient, AccelerateNextAction, Checkout, CheckoutId, ClientBrowserResource, CustomerBrowserBinding } from './index.js';
2
+ import type { BrowserResult } from './browser-operations.js';
3
+ import type { BrowserConnector } from './browser-connector.js';
4
+
5
+ /** Resolve from authenticated host state. MCP arguments and _meta alone are not identity proof. */
6
+ export interface ManagedCheckoutContext {
7
+ customer_id: string;
8
+ workspace_id: string;
9
+ conversation_id: string;
10
+ phone_number: string;
11
+ }
12
+ /** Minimal private storage projection; never returned through a model tool. */
13
+ export interface ManagedCheckoutBinding extends ManagedCheckoutContext { checkout_id: CheckoutId }
14
+ export interface ManagedCheckoutCreation {
15
+ /** Server-derived key for this exact creation attempt, protected by the host's durable fence. */
16
+ idempotency_key: string;
17
+ /** Optional existing browser from the host's owned workflow, never MCP input.
18
+ * Only permitted with browserMode:'customer_backend_trusted'. */
19
+ browser?: CustomerBrowserBinding;
20
+ /** Your own provider read of `browser`, so Accelerate never needs your provider API key. */
21
+ browser_resource?: ClientBrowserResource;
22
+ /** Resolve only after durable storage. Retain an uncertainty fence if acknowledgement is lost. */
23
+ persistCreated(binding: Readonly<ManagedCheckoutBinding>): Promise<void>;
24
+ }
25
+ export interface ManagedMcpError {
26
+ code: string;
27
+ message: string;
28
+ status: number;
29
+ next_action: AccelerateNextAction;
30
+ retry_safe: false;
31
+ payment_confirmed: false;
32
+ support_reference?: `acs_${string}` | `arq_${string}`;
33
+ }
34
+ export type ManagedMcpOutput = { ok: true; checkout: Checkout } | { ok: true; browser: BrowserResult } |
35
+ { ok: false; error: ManagedMcpError; checkout?: Checkout };
36
+ export interface ManagedCheckoutEvent {
37
+ version: 'v1';
38
+ kind: 'created' | 'payment_requested' | 'approval_requested' | 'review_ready';
39
+ /** Stable hash-only key; deduplicate delivery durably. Never retry the underlying SDK operation. */
40
+ idempotency_key: `accelerate-checkout-event:${string}`;
41
+ checkout: Checkout;
42
+ }
43
+ export interface ManagedMcpToolResult {
44
+ [key: string]: unknown;
45
+ content: Array<{ type: 'text'; text: string }>;
46
+ structuredContent: ManagedMcpOutput;
47
+ isError?: true;
48
+ }
49
+ export interface ManagedMcpStandardSchema {
50
+ readonly '~standard': {
51
+ readonly version: 1;
52
+ readonly vendor: string;
53
+ readonly validate: (value: unknown) => { value: unknown } | { issues: Array<{ message: string }> };
54
+ readonly jsonSchema: {
55
+ readonly input: (options?: unknown) => Record<string, unknown>;
56
+ readonly output: (options?: unknown) => Record<string, unknown>;
57
+ };
58
+ };
59
+ }
60
+ export interface ManagedMcpRegisteredDefinition {
61
+ title: string;
62
+ description: string;
63
+ inputSchema: ManagedMcpStandardSchema;
64
+ outputSchema: ManagedMcpStandardSchema;
65
+ annotations: { readOnlyHint: boolean; idempotentHint: boolean; destructiveHint: boolean; openWorldHint: true };
66
+ }
67
+ /** Structural MCP2 registration surface; no protocol or schema package is imported at runtime. */
68
+ export interface ManagedMcpServer<Extra = unknown> {
69
+ registerTool(name: string, definition: ManagedMcpRegisteredDefinition,
70
+ handler: (args: unknown, extra: Extra) => Promise<ManagedMcpToolResult>): unknown;
71
+ }
72
+ export type ManagedCheckoutProfileId = 'managed_review_v2' | 'managed_review_v1' | 'legacy_before_fill_v1';
73
+ /** Immutable flow metadata. Does not qualify a merchant layout or grant submit
74
+ * authority. The private sender must verify the actual five-minute window. */
75
+ export const managedCheckoutProfiles: Readonly<{
76
+ managed_review_v2: Readonly<{ id: 'managed_review_v2'; verificationTiming: 'on_payment';
77
+ approvalPolicy: 'after_fill'; approvalWindowMs: 300000; fillAfterVerification: true }>;
78
+ managed_review_v1: Readonly<{ id: 'managed_review_v1'; verificationTiming: 'on_payment';
79
+ approvalPolicy: 'after_fill'; approvalWindowMs: 300000 }>;
80
+ legacy_before_fill_v1: Readonly<{ id: 'legacy_before_fill_v1'; verificationTiming: 'on_start';
81
+ approvalPolicy: 'before_fill' }>;
82
+ }>;
83
+ export interface ManagedCheckoutToolsOptions<Extra = unknown> {
84
+ /** Defaults to AccelerateClient.fromEnv(). Instantiate only on the authenticated application server. */
85
+ client?: AccelerateClient;
86
+ /** Called on EVERY tool invocation; resolve verified host context, not untrusted model metadata. */
87
+ resolveContext(extra: Extra): Promise<ManagedCheckoutContext> | ManagedCheckoutContext;
88
+ /**
89
+ * Hold your existing durable owner lock and uncertainty fence, authorize ALL
90
+ * binding fields, invoke work exactly once with a minimal saved binding, and
91
+ * return its result. Never retry an operation or clear an uncertain purchase.
92
+ */
93
+ withOwnedCheckout<T>(context: Readonly<ManagedCheckoutContext>, checkoutId: CheckoutId,
94
+ work: (binding: Readonly<ManagedCheckoutBinding>) => Promise<T>,
95
+ metadata?: Readonly<{ operation: ManagedCheckoutToolName; revision?: number }>): Promise<T>;
96
+ /**
97
+ * Hold the same durable owner lock/fence until work resolves. Supply a stable
98
+ * server-derived creation key and persistCreated acknowledgement. If creation
99
+ * or persistence is uncertain, preserve that attempt and reconcile it; never
100
+ * create a replacement or automatically compensate with another payment call.
101
+ */
102
+ withCheckoutCreation<T>(context: Readonly<ManagedCheckoutContext>,
103
+ work: (creation: ManagedCheckoutCreation) => Promise<T>, metadata?: Readonly<{ start_url: string }>): Promise<T>;
104
+ /**
105
+ * Durably accept/deduplicate this SAFE event under its key before privately
106
+ * starting/reading chat state and rendering/delivering prompts. Use an outbox
107
+ * to recover lost acknowledgements with the SAME event key, never a new
108
+ * checkout/payment action. The private client/replies remain outside MCP.
109
+ * On unconfirmed acceptance the tool returns a fixed error and known safe
110
+ * checkout ID/revision; the existing owner record and uncertainty fence remain.
111
+ */
112
+ dispatchCheckoutEvent(context: Readonly<ManagedCheckoutContext>, event: Readonly<ManagedCheckoutEvent>): Promise<void>;
113
+ /** Immutable for newly created checkouts. Default sandbox_fill_only. Purchase requires browserMode
114
+ * customer_backend_trusted and a browserConnector whose host implements performFinalClick: Accelerate
115
+ * never sends the final click, and Accelerate-managed private browsers cannot purchase. */
116
+ purpose?: 'sandbox_fill_only' | 'purchase';
117
+ /** Default false. Registers the final-submit tool only when true and purpose is purchase. That tool
118
+ * arms one short click, calls browserConnector.performFinalClick once in the customer's browser, then
119
+ * completes once to observe. Its result is a safe Checkout; the arm token and target never leave the backend. */
120
+ enableSubmit?: boolean;
121
+ /** Default managed_review_v1: verification at payment and approval after fill.
122
+ * Legacy before-fill behavior requires this explicit profile or approvalPolicy:'before_fill'. */
123
+ profile?: ManagedCheckoutProfileId;
124
+ /** Must be on_payment for managed_review_v1/v2. Legacy allows either timing and defaults to on_start. */
125
+ verificationTiming?: 'on_start' | 'on_payment';
126
+ /** Must match the selected profile. An explicit before_fill without profile
127
+ * deliberately selects legacy behavior; requested after-fill never downgrades. */
128
+ approvalPolicy?: 'before_fill' | 'after_fill';
129
+ /** Explicitly selects trusted customer-backend existing-browser attachment. The
130
+ * default remains Accelerate-created private browser custody. */
131
+ browserMode?: 'accelerate_managed' | 'customer_backend_trusted';
132
+ /** Optional strict existing-browser lifecycle. Requires customer_backend_trusted
133
+ * and managed_review_v1. The legacy server-provided scope.browser path remains explicit. */
134
+ browserConnector?: BrowserConnector;
135
+ }
136
+ export type ManagedCheckoutToolName = 'accelerate_checkout_start' | 'accelerate_checkout_status' | 'accelerate_checkout_browser' |
137
+ 'accelerate_checkout_request_payment' | 'accelerate_checkout_apply_details' | 'accelerate_checkout_request_approval' |
138
+ 'accelerate_checkout_fill' | 'accelerate_checkout_cancel' | 'accelerate_checkout_submit';
139
+ export interface ManagedCheckoutToolDefinition {
140
+ name: string;
141
+ title: string;
142
+ description: string;
143
+ inputSchema: Readonly<Record<string, unknown>>;
144
+ outputSchema: Readonly<Record<string, unknown>>;
145
+ annotations: Readonly<ManagedMcpRegisteredDefinition['annotations']>;
146
+ readOnly?: true;
147
+ destructive?: true;
148
+ submitOnly?: true;
149
+ /** Registered only when verificationTiming is on_payment. */
150
+ paymentOnly?: true;
151
+ }
152
+ /** Frozen canonical JSON Schemas and descriptions for discovery/documentation. */
153
+ export const managedCheckoutToolDefinitions: readonly Readonly<ManagedCheckoutToolDefinition>[];
154
+ /** Register on the customer's authenticated MCP server, with private ingress configured separately. */
155
+ export function registerManagedCheckoutTools<Extra = unknown>(server: ManagedMcpServer<Extra>,
156
+ options: ManagedCheckoutToolsOptions<Extra>): readonly unknown[];