@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
package/README.md CHANGED
@@ -1,3 +1,150 @@
1
- # Temporary Holding Version
1
+ # @weaccelerateinc/agent-sdk 0.10.3
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 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.10.3`. Check `npm view @weaccelerateinc/agent-sdk@0.10.3 version` to confirm registry availability. The package remains `UNLICENSED`.
4
+
5
+ ```sh
6
+ npm install --ignore-scripts @weaccelerateinc/agent-sdk@0.10.3
7
+ npx --no-install accelerate-agent init ./partner-demo --browser existing
8
+ ```
9
+
10
+ The platform creates the browser and installs the approved extension using its existing setup. Accelerate attaches it only at payment. SDK checks fail before private collection if ownership, recording, approved runtime, lifetime, drain/fencing or qualification is unsupported. No provider replacement fallback is created.
11
+
12
+ | Export | Purpose |
13
+ | --- | --- |
14
+ | `@weaccelerateinc/agent-sdk` | `AccelerateClient`, safe errors/status/REST checkout methods. |
15
+ | `/partner` | `registerPartnerCheckoutTools`: composes common connector, durable host, private ingress and canonical MCP tools with existing-browser custody. |
16
+ | `/browser-connector` | Provider-independent attachment/readiness/fence/cleanup contract, generic connector and Browser Use recipe. |
17
+ | `/browser-use-host` | Fixed-origin bounded Browser Use v4 metadata/extension/recording/lifetime reads and stopped-resource proof, using the platform's existing document automation. No browser creation/upload. |
18
+ | `/approved-extension` | Validate trusted release metadata, verify ZIP checksum and live runtime compatibility. |
19
+ | `/approved-extension-releases/*` | Versioned capability/compatibility/qualification manifests. Candidate builds rejected for live use. |
20
+ | `/host-adapter` | Durable ownership, independent effect admissions, safe outbox, native review receipts and uncertainty fences. |
21
+ | `/postgres-host` | AES-256-GCM records with cross-process session locks; accepts your pg Pool. Explicit `initialize()` migration; no implicit database provisioning. |
22
+ | `/private-ingress` | Authenticated sensitive interception before AI/history and safe outbox recovery. |
23
+ | `/linq-messaging` | Linq/iMessage native private image+text/receipt transport and webhook authenticator using your actual signature verifier. |
24
+ | `/private-chat` | Private state/reply/review APIs and bounded `deliverPrivateCheckoutReview`; never register as model tools. |
25
+ | `/managed-mcp` | Lower-level canonical tool registration and explicit legacy/managed browser compatibility. |
26
+ | `/browser-operations` | Strict bounded operation schemas; attached-customer mode permits pre-fill reads only. |
27
+
28
+ The matching [partner guide](https://github.com/weaccelerateinc/accelerate-agent-sdk/blob/main/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
+
30
+ 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
+
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-agent-sdk/blob/main/docs/managed-review-v2.md).
33
+
34
+ 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
+
36
+ `registerPartnerCheckoutTools({profile:'managed_review_v2', ...})` selects automatic private continuation and defaults to `purpose:'sandbox_fill_only',enableSubmit:false`. 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
+
38
+ 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
+
40
+ ## Bounded status waiting
41
+
42
+ ```js
43
+ const selected = await accelerate.checkouts.waitFor(id, {
44
+ statuses: ['selection_ready'],
45
+ timeoutMs: 120_000,
46
+ }, { signal });
47
+ if (selected.status !== 'selection_ready') {
48
+ // Handle the actual state; do not advance a payment automatically.
49
+ return;
50
+ }
51
+ ```
52
+
53
+ `waitFor` uses sequential GET requests only. Defaults: 60-second overall timeout and one-second interval. Maximum overall timeout: five minutes; interval range: 250–5000ms. It returns immediately for any requested state, `ready_for_submit` or any terminal state, even if not requested. It aborts hung requests within the overall deadline, rejects as `wait_timeout`, and never retries errors. Readiness must not be hidden behind a long polling loop.
54
+
55
+ ## Browser preparation and tool schemas
56
+
57
+ Supported operations: `browser_snapshot`, `browser_text`, `browser_find`, `browser_wait_for`, `browser_act`, `navigate`.
58
+
59
+ ```js
60
+ import { browserOperationJsonSchema } from '@weaccelerateinc/agent-sdk/browser-operations';
61
+ // Use this framework-neutral schema as the browser-operation tool input schema.
62
+
63
+ const snapshot = await accelerate.checkouts.browser(id, {
64
+ operation: 'browser_snapshot',
65
+ arguments: { filter: 'interactive', depth: 5 },
66
+ });
67
+ await accelerate.checkouts.browser(id, {
68
+ operation: 'browser_act',
69
+ arguments: {
70
+ steps: [{ type: 'click', ref: 'e1_1' }], // current snapshot reference
71
+ successor: { filter: 'all', depth: 5 },
72
+ },
73
+ });
74
+ await accelerate.checkouts.browser(id, {
75
+ operation: 'browser_wait_for',
76
+ arguments: { expect: { type: 'url', contains: '/payment' }, timeout_ms: 8000 },
77
+ });
78
+ ```
79
+
80
+ Wait conditions follow their `type`: text waits require nonempty `text`; URL/title waits require nonempty `equals` or `contains`. Do not use `text` for a URL/title condition, or URL comparators for text. `exists: false` negates the match. If both URL/title comparators are provided, `equals` takes precedence. Malformed conditions are rejected locally before an HTTP request; a valid condition that fails to appear still stops the checkout under the existing browser failure policy.
81
+
82
+ The JSON schema and input normalizer share operation bounds and discriminated shapes. For framework values typed as unknown, `parseBrowserOperation(value)` returns a validated, normalized `BrowserOperation` without a TypeScript assertion. The normalizer and server remain the security authority. References have shape `eN_N`; newer snapshots replace them. Actions: click, hover, scroll_to, fill, type, key and bounded wait. Batches allow at most eight steps and action waits at most eight seconds. Arbitrary scripts, selectors in actions, screenshots, tabs, payment controls and final purchase clicks are excluded. Responses contain bounded text without browser control URLs or raw input values.
83
+
84
+ ## Verify a quote before private approval
85
+
86
+ After `applyDetails`, inspect the current amount using the original total selector and ask the backend to verify its currency. `expected_currency` is an expectation to check, not evidence supplied by the agent; a dollar symbol alone does not establish USD.
87
+
88
+ ```js
89
+ const verified = await accelerate.checkouts.verifyQuote(id, {
90
+ revision: details.revision,
91
+ checkout_url: exactCheckoutUrl,
92
+ expected_amount_cents: 1299,
93
+ expected_currency: 'USD',
94
+ checkout_total_selector: originalTotalSelector,
95
+ });
96
+ ```
97
+
98
+ The bounded receipt contains only `checkout_id`, the unchanged `revision`, `verified:true` and `{checkout_origin,amount_cents,currency}` in `quote`. It requires the current selected-address details, exact checkout URL (including query and fragment) and authoritative backend currency proof. It does not arm filling, request CVV or approve a purchase. Use its verified amount and currency in the existing `requestApproval` flow, which independently inspects the original quote again. A rejected inspection is a blocker, not permission to infer currency or repeat uncertain payment work.
99
+
100
+ For `sandbox_fill_only`, retain the actual visible original payment continuation as `submit_selector`, such as Review Your Order, even when disabled. Filling never clicks it. Purchase purpose retains its final-submit readiness and separate private approval requirements.
101
+
102
+ ## Inspect a failed creation without replay
103
+
104
+ `checkouts.inspectCreation({customer_id,workspace_id,conversation_id,idempotency_key,browser})` is a server-only read. Derive scope, the original key and exact Browser Use binding from authenticated ownership and the application's permanent reservation; never expose these fields as model input.
105
+
106
+ The service looks up only that tenant/key and requires the exact owner, private conversation and browser, a failed revision-zero creation before authentication, no operation lock or private effects, and independent stopped-provider/closed-lease proof. A positive receipt contains `confirmed:true`, the opaque checkout ID, revision zero, immutable purpose, the key's SHA-256 and strict retirement booleans. Missing or incomplete proof returns only `confirmed:false`. Inspection never creates, replays, verifies, fills, cancels, recovers a lock or writes checkout state. Keep the old browser permanently reserved. A separately authorized fresh request still needs a new ordinary browser and normal fresh-start checks.
107
+
108
+ ## Private chat helpers
109
+
110
+ ```js
111
+ import {
112
+ AcceleratePrivateChatClient,
113
+ renderPrivateChatState,
114
+ privateChatProgress,
115
+ } from '@weaccelerateinc/agent-sdk/private-chat';
116
+ ```
117
+
118
+ 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
+
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-agent-sdk/blob/main/examples/private-chat-ingress.js).
121
+
122
+ ## Safe results and recovery
123
+
124
+ Ordinary status includes only opaque checkout ID, state, revision, expiry, purpose/policy, optional masked wallet and quote metadata. It contains no PAN, expiry, CVV, wallet auth capability, source ID or unrestricted browser access.
125
+
126
+ Current and compatible earlier clients opt ordinary requests into `X-Accelerate-Diagnostics: v1`. When the service supports it, safe checkout `diagnostics` includes fixed `stage`, `next_actions`, opaque `support_reference`, `cleanup_status` and the exact `submit_deadline` at readiness. Cleanup `confirmed` means worker acknowledgement, not independently verified provider destruction. `AccelerateError.supportReference` correlates negotiated errors without exposing upstream text. Older ordinary response bodies are unchanged without opt-in; private-chat and hosted bodies retain their existing contract. Current clients accept older service responses without diagnostics, but the supported review profile still requires its policy echo and actual full-window proof. A compatible status response is not a profile-readiness preflight.
127
+
128
+ Every request accepts `{signal}` last. Bounds including server/cleanup headroom: create 245s; fill 235s; details/verifyQuote/approval/submit 125s; browser 85s; inspectCreation 60s; status/cancel/connection 45s. These are transport bounds, not provider-destruction guarantees.
129
+
130
+ No request automatically retries. `AccelerateError` provides fixed `code`, `status`, `message`, `nextAction`, `retrySafe:false`, `paymentConfirmed:false`; upstream bodies and credential-bearing diagnostics are excluded. `nextAction` is guidance only:
131
+
132
+ | Value | Backend action |
133
+ | --- | --- |
134
+ | `check_configuration` | Check provisioned key, merchant and service configuration |
135
+ | `check_request` | Correct local request shape |
136
+ | `complete_wallet` / `complete_verification` | Continue the existing private shopper flow |
137
+ | `refresh_quote` / `refresh_revision` | Inspect safe state and prepare a new valid quote/revision if allowed |
138
+ | `check_status` | Read the existing checkout; do not repeat payment effects |
139
+ | `reconcile_order` | Check the merchant independently before any replacement purchase |
140
+ | `stop` | Stop and investigate |
141
+
142
+ An uncertain fill/submit outcome must be reconciled; do not automatically replay or create a replacement purchase. `submit_attempted` and `outcome_unknown` never confirm an order. The SDK does not certify PCI scope or source correctness.
143
+
144
+ ## Development
145
+
146
+ ```sh
147
+ npm test --workspace @weaccelerateinc/agent-sdk
148
+ ```
149
+
150
+ Tests use synthetic data and local HTTP; they do not collect live credentials or make purchases.
@@ -0,0 +1,39 @@
1
+ {
2
+ "schema_version": 1,
3
+ "release_id": "accelerate-extension-0.0.0.1",
4
+ "status": "approved",
5
+ "extension_version": "0.0.0.1",
6
+ "artifact": {
7
+ "format": "zip",
8
+ "sha256": "65574cbb27ab344687963f72a136f2653551b57458b284e486946b27699099c8",
9
+ "bytes": 29397270
10
+ },
11
+ "bridge": {
12
+ "protocol": "accelerate_extension_bridge_v1",
13
+ "sha256": "1ba039eb78a5b18bfab4486c7835f1ac188b96682ec67c92bfef889c763811a6",
14
+ "runtime_reporting": "legacy_pinned_artifact"
15
+ },
16
+ "capabilities": {
17
+ "manual_card_fill": true,
18
+ "document_bound_single_use": true,
19
+ "autosave_disabled": true,
20
+ "automatic_submit": false,
21
+ "maximum_retention_ms": 480000
22
+ },
23
+ "compatibility": {
24
+ "sdk_versions": [
25
+ "0.10.0",
26
+ "0.10.1",
27
+ "0.10.2",
28
+ "0.10.3"
29
+ ],
30
+ "executor_contract": "accelerate_manual_checkout_v1"
31
+ },
32
+ "qualifications": [
33
+ {
34
+ "provider": "browser_use",
35
+ "scope": "sandbox_demo_normal_fill_review_cancel",
36
+ "evidence": "docs/evidence/browser-use-wallet-fill-review-2026-10-27.json"
37
+ }
38
+ ]
39
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schema_version": 1,
3
+ "release_id": "accelerate-extension-0.0.0.2",
4
+ "status": "candidate_pending_browser_qualification",
5
+ "extension_version": "0.0.0.2",
6
+ "artifact": {
7
+ "format": "zip",
8
+ "sha256": "7a7a4ab8a874f7b8e2387433534ca0c6e78df3c4465f2e4d7f7d56a128951839",
9
+ "bytes": 29397375
10
+ },
11
+ "bridge": {
12
+ "protocol": "accelerate_extension_bridge_v1",
13
+ "sha256": "e86412da654acaca1ed108b624a112c6c5439d9d0a0f1beb90a59c86b68ed40b"
14
+ },
15
+ "capabilities": {
16
+ "manual_card_fill": true,
17
+ "document_bound_single_use": true,
18
+ "autosave_disabled": true,
19
+ "automatic_submit": false,
20
+ "maximum_retention_ms": 480000
21
+ },
22
+ "compatibility": {
23
+ "sdk_versions": [
24
+ "0.10.0",
25
+ "0.10.1",
26
+ "0.10.2",
27
+ "0.10.3"
28
+ ],
29
+ "executor_contract": "accelerate_manual_checkout_v1"
30
+ },
31
+ "qualifications": []
32
+ }
@@ -0,0 +1,24 @@
1
+ export interface ApprovedExtensionRelease {
2
+ schema_version: 1;
3
+ release_id: `accelerate-extension-${string}`;
4
+ status: 'approved' | 'candidate_pending_browser_qualification';
5
+ extension_version: string;
6
+ artifact: { format: 'zip'; sha256: string; bytes: number };
7
+ bridge: { protocol: 'accelerate_extension_bridge_v1'; sha256: string; runtime_reporting?: 'protocol_and_version' | 'legacy_pinned_artifact' };
8
+ capabilities: { manual_card_fill: true; document_bound_single_use: true; autosave_disabled: true; automatic_submit: false; maximum_retention_ms: 480000 };
9
+ compatibility: { sdk_versions: string[]; executor_contract: 'accelerate_manual_checkout_v1' };
10
+ qualifications: Array<{ provider: string; scope: string; evidence: string }>;
11
+ }
12
+ export interface ApprovedExtensionRuntime {
13
+ extension_version: string;
14
+ artifact_sha256: string;
15
+ success: true; initialized: true; wasmInitialized: true; autosaveDisabled: true;
16
+ boundedRetentionSupported: true; maximumRetentionMs: 480000;
17
+ bridge_protocol?: 'accelerate_extension_bridge_v1';
18
+ }
19
+ export class ApprovedExtensionError extends Error { readonly code: string; }
20
+ export function validateApprovedExtensionRelease(input: unknown, options?: { allowCandidate?: boolean }): Readonly<ApprovedExtensionRelease>;
21
+ export function verifyApprovedExtensionArtifact(bytes: Uint8Array, release: ApprovedExtensionRelease): Readonly<{ verified: true; extension_version: string; artifact_sha256: string }>;
22
+ export function verifyApprovedExtensionRuntime(runtime: ApprovedExtensionRuntime, release: ApprovedExtensionRelease, options: {
23
+ sdkVersion?: string; executorContract?: 'accelerate_manual_checkout_v1'; provider: string;
24
+ }): Readonly<{ verified: true; extension_version: string; artifact_sha256: string; bridge_protocol: 'accelerate_extension_bridge_v1'; provider: string }>;
@@ -0,0 +1,90 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const SDK_VERSION = '0.10.3';
4
+ const EXECUTOR_CONTRACT = 'accelerate_manual_checkout_v1';
5
+ const BRIDGE_PROTOCOL = 'accelerate_extension_bridge_v1';
6
+ const LEGACY_SHA256 = '65574cbb27ab344687963f72a136f2653551b57458b284e486946b27699099c8';
7
+ const hashPattern = /^[a-f0-9]{64}(?![\s\S])/;
8
+ const versionPattern = /^(?:0|[1-9]\d{0,4})(?:\.(?:0|[1-9]\d{0,4})){0,3}(?![\s\S])/;
9
+ const identifier = /^[a-z][a-z0-9_-]{0,79}(?![\s\S])/;
10
+ const reject = code => { throw new ApprovedExtensionError(code); };
11
+ const check = (condition, code = 'invalid_extension_release') => { if (!condition) reject(code); };
12
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
13
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
14
+ function keys(value, required, optional = []) {
15
+ check(object(value));
16
+ check(required.every(key => Object.hasOwn(value, key)) &&
17
+ Object.keys(value).every(key => required.includes(key) || optional.includes(key)));
18
+ }
19
+ const version = value => typeof value === 'string' && versionPattern.test(value) &&
20
+ value.split('.').every(part => Number(part) <= 65535) && value.split('.').some(part => Number(part) > 0);
21
+ const freeze = value => {
22
+ if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); }
23
+ return value;
24
+ };
25
+
26
+ export class ApprovedExtensionError extends Error {
27
+ constructor(code) { super('The Accelerate extension could not be verified.'); this.name = 'ApprovedExtensionError'; this.code = code; }
28
+ }
29
+
30
+ /** Validate operator-supplied metadata. Metadata must come from trusted backend
31
+ * configuration; this validates a contract and is not a publisher signature. */
32
+ export function validateApprovedExtensionRelease(input, { allowCandidate = false } = {}) {
33
+ check(typeof allowCandidate === 'boolean');
34
+ keys(input, ['schema_version', 'release_id', 'status', 'extension_version', 'artifact', 'bridge', 'capabilities', 'compatibility', 'qualifications']);
35
+ check(input.schema_version === 1 && version(input.extension_version) && input.release_id === `accelerate-extension-${input.extension_version}`);
36
+ check(['approved', 'candidate_pending_browser_qualification'].includes(input.status));
37
+ check(input.status === 'approved' || allowCandidate, 'extension_release_not_approved');
38
+ keys(input.artifact, ['format', 'sha256', 'bytes']);
39
+ check(input.artifact.format === 'zip' && typeof input.artifact.sha256 === 'string' && hashPattern.test(input.artifact.sha256) &&
40
+ Number.isSafeInteger(input.artifact.bytes) && input.artifact.bytes > 0 && input.artifact.bytes <= 100 * 1024 * 1024);
41
+ keys(input.bridge, ['protocol', 'sha256'], ['runtime_reporting']);
42
+ check(input.bridge.protocol === BRIDGE_PROTOCOL && typeof input.bridge.sha256 === 'string' && hashPattern.test(input.bridge.sha256));
43
+ const reporting = input.bridge.runtime_reporting ?? 'protocol_and_version';
44
+ check(['protocol_and_version', 'legacy_pinned_artifact'].includes(reporting));
45
+ if (reporting === 'legacy_pinned_artifact') check(input.extension_version === '0.0.0.1' && input.artifact.sha256 === LEGACY_SHA256);
46
+ keys(input.capabilities, ['manual_card_fill', 'document_bound_single_use', 'autosave_disabled', 'automatic_submit', 'maximum_retention_ms']);
47
+ check(input.capabilities.manual_card_fill === true && input.capabilities.document_bound_single_use === true &&
48
+ input.capabilities.autosave_disabled === true && input.capabilities.automatic_submit === false && input.capabilities.maximum_retention_ms === 480000);
49
+ keys(input.compatibility, ['sdk_versions', 'executor_contract']);
50
+ check(Array.isArray(input.compatibility.sdk_versions) && input.compatibility.sdk_versions.length > 0 && input.compatibility.sdk_versions.length <= 20 &&
51
+ input.compatibility.sdk_versions.every(item => typeof item === 'string' && /^\d+\.\d+\.\d+(?![\s\S])/.test(item)) &&
52
+ new Set(input.compatibility.sdk_versions).size === input.compatibility.sdk_versions.length && input.compatibility.executor_contract === EXECUTOR_CONTRACT);
53
+ check(Array.isArray(input.qualifications) && input.qualifications.length <= 32);
54
+ for (const qualification of input.qualifications) {
55
+ keys(qualification, ['provider', 'scope', 'evidence']);
56
+ check(typeof qualification.provider === 'string' && identifier.test(qualification.provider) &&
57
+ typeof qualification.scope === 'string' && identifier.test(qualification.scope) &&
58
+ typeof qualification.evidence === 'string' && /^docs\/evidence\/[a-z0-9][a-z0-9._-]{0,150}\.json(?![\s\S])/.test(qualification.evidence));
59
+ }
60
+ check(input.status !== 'approved' || input.qualifications.length > 0, 'extension_qualification_missing');
61
+ return freeze(structuredClone(input));
62
+ }
63
+
64
+ /** Verify local ZIP bytes before a platform installs them. No upload occurs. */
65
+ export function verifyApprovedExtensionArtifact(bytes, release) {
66
+ const approved = validateApprovedExtensionRelease(release);
67
+ check(bytes instanceof Uint8Array && bytes.byteLength === approved.artifact.bytes, 'extension_artifact_mismatch');
68
+ const actual = createHash('sha256').update(bytes).digest('hex');
69
+ check(actual === approved.artifact.sha256, 'extension_artifact_mismatch');
70
+ return freeze({ verified: true, extension_version: approved.extension_version, artifact_sha256: actual });
71
+ }
72
+
73
+ /** Verify private connector observations. The artifact pin must be derived from
74
+ * trusted installation records, not a field supplied by an agent/model. */
75
+ export function verifyApprovedExtensionRuntime(runtime, release, {
76
+ sdkVersion = SDK_VERSION, executorContract = EXECUTOR_CONTRACT, provider,
77
+ } = {}) {
78
+ const approved = validateApprovedExtensionRelease(release);
79
+ check(approved.compatibility.sdk_versions.includes(sdkVersion) && executorContract === approved.compatibility.executor_contract, 'extension_compatibility_mismatch');
80
+ check(typeof provider === 'string' && identifier.test(provider) && approved.qualifications.some(item => item.provider === provider), 'extension_provider_unqualified');
81
+ keys(runtime, ['extension_version', 'artifact_sha256', 'success', 'initialized', 'wasmInitialized', 'autosaveDisabled', 'boundedRetentionSupported', 'maximumRetentionMs'], ['bridge_protocol']);
82
+ check(runtime.extension_version === approved.extension_version && runtime.artifact_sha256 === approved.artifact.sha256, 'extension_runtime_mismatch');
83
+ check(runtime.success === true && runtime.initialized === true && runtime.wasmInitialized === true && runtime.autosaveDisabled === true &&
84
+ runtime.boundedRetentionSupported === true && runtime.maximumRetentionMs === approved.capabilities.maximum_retention_ms, 'extension_runtime_not_ready');
85
+ if (approved.bridge.runtime_reporting === 'legacy_pinned_artifact') {
86
+ check(runtime.bridge_protocol === undefined || runtime.bridge_protocol === BRIDGE_PROTOCOL, 'extension_runtime_mismatch');
87
+ } else check(runtime.bridge_protocol === BRIDGE_PROTOCOL, 'extension_runtime_mismatch');
88
+ return freeze({ verified: true, extension_version: approved.extension_version, artifact_sha256: approved.artifact.sha256,
89
+ bridge_protocol: BRIDGE_PROTOCOL, provider });
90
+ }
@@ -0,0 +1,87 @@
1
+ import type { CheckoutId, CustomerBrowserBinding, RequestOptions } from './index.js';
2
+ import type { ManagedCheckoutContext } from './managed-mcp.js';
3
+ import type { ApprovedExtensionRelease, ApprovedExtensionRuntime } from './approved-extension.js';
4
+
5
+ /** Private, authenticated host projection. Never put this record in MCP arguments/results. */
6
+ export interface OwnedBrowserDocument {
7
+ owner: ManagedCheckoutContext;
8
+ browser: CustomerBrowserBinding;
9
+ document_id: string;
10
+ checkout_url: string;
11
+ expires_at: string;
12
+ }
13
+ export interface FencedBrowserDocument extends OwnedBrowserDocument {
14
+ fence_id: string;
15
+ }
16
+ export interface BrowserReadiness extends OwnedBrowserDocument {
17
+ recording_disabled: true;
18
+ /** Actual runtime receipt plus the independently pinned installed ZIP checksum. */
19
+ extension: ApprovedExtensionRuntime;
20
+ automation: {
21
+ ordinary_operations_drained: boolean;
22
+ agent_access_fenced: boolean;
23
+ fence_id: string | null;
24
+ };
25
+ }
26
+ /** Durable fencing is a host responsibility; acquiring a JavaScript mutex is insufficient. */
27
+ export interface BrowserConnectorHost {
28
+ resolveOwnedBrowser(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
29
+ attempt_key: string; checkout_url: string;
30
+ }>, options: RequestOptions): Promise<OwnedBrowserDocument>;
31
+ inspectBrowser(context: Readonly<ManagedCheckoutContext>, browser: Readonly<OwnedBrowserDocument>, options: RequestOptions): Promise<BrowserReadiness>;
32
+ /** Persist the fence, reject new ordinary work and await all in-flight automation before resolving. */
33
+ drainAndFence(context: Readonly<ManagedCheckoutContext>, browser: Readonly<OwnedBrowserDocument>, request: Readonly<{
34
+ attempt_key: string;
35
+ }>, options: RequestOptions): Promise<{
36
+ fence_id: string; ordinary_operations_drained: true; agent_access_fenced: true;
37
+ }>;
38
+ /** Persist exact attempt/browser/document/fence binding. A lost ACK must retain its uncertainty fence. */
39
+ bindCheckout(context: Readonly<ManagedCheckoutContext>, request: Readonly<FencedBrowserDocument & {
40
+ checkout_id: CheckoutId; attempt_key: string;
41
+ }>, options: RequestOptions): Promise<void>;
42
+ resolveCheckoutBrowser(context: Readonly<ManagedCheckoutContext>, request: Readonly<{ checkout_id: CheckoutId }>, options: RequestOptions): Promise<FencedBrowserDocument>;
43
+ /** Verify cleanup independently after Accelerate reports closure. Never release an uncertain-payment fence. */
44
+ confirmCleanup(context: Readonly<ManagedCheckoutContext>, browser: Readonly<FencedBrowserDocument>, request: Readonly<{
45
+ checkout_id: CheckoutId;
46
+ }>, options: RequestOptions): Promise<{
47
+ provider_browser_stopped: true; credentials_cleared: true; fence_retained: true;
48
+ }>;
49
+ }
50
+ declare const attachmentBrand: unique symbol;
51
+ /** Private one-connector capability returned only to the authenticated backend. */
52
+ export interface PreparedBrowserAttachment {
53
+ readonly browser: Readonly<CustomerBrowserBinding>;
54
+ readonly [attachmentBrand]: true;
55
+ }
56
+ export interface BrowserConnector {
57
+ readonly contract: 'accelerate_existing_browser_v1';
58
+ readonly provider: 'browser_use' | 'connector';
59
+ readonly connector_id?: string;
60
+ prepareAttachment(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
61
+ attempt_key: string; checkout_url: string;
62
+ }>, options?: RequestOptions): Promise<PreparedBrowserAttachment>;
63
+ commitAttachment(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
64
+ checkout_id: CheckoutId; attachment: PreparedBrowserAttachment;
65
+ }>, options?: RequestOptions): Promise<void>;
66
+ /** Fresh check before private OTP dispatch and before quote/CVV dispatch. */
67
+ assertPaymentReady(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
68
+ checkout_id: CheckoutId; checkout_url?: string;
69
+ }>, options?: RequestOptions): Promise<void>;
70
+ confirmCleanup(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
71
+ checkout_id: CheckoutId;
72
+ }>, options?: RequestOptions): Promise<void>;
73
+ }
74
+ export interface ExistingBrowserConnectorOptions {
75
+ /** Provisioned server connector ID; the Accelerate executor must enable this ID separately. */
76
+ connectorId: string;
77
+ extensionRelease: ApprovedExtensionRelease;
78
+ host: BrowserConnectorHost;
79
+ /** Can only increase the required ten-minute headroom, never shorten approval. */
80
+ minimumRemainingMs?: number;
81
+ /** Test clock only. */
82
+ now?: () => number;
83
+ }
84
+ /** Generic registered-executor connector. Does not create/upload/replace a browser or provision a provider. */
85
+ export function createExistingBrowserConnector(options: ExistingBrowserConnectorOptions): BrowserConnector;
86
+ /** First provider recipe. Uses Browser Use IDs through the same host contract; no API key is accepted. */
87
+ export function createBrowserUseConnector(options: Omit<ExistingBrowserConnectorOptions, 'connectorId'>): BrowserConnector;
@@ -0,0 +1,179 @@
1
+ import { AccelerateError } from './index.js';
2
+ import { normalizeBrowserOperation } from './browser-operations.js';
3
+ import { validateApprovedExtensionRelease, verifyApprovedExtensionRuntime } from './approved-extension.js';
4
+
5
+ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/u;
6
+ const ID = /^mcs_[A-Za-z0-9_-]{43}$/u;
7
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
8
+ const CONNECTOR = /^[a-z][a-z0-9_-]{0,63}$/u;
9
+ const OPAQUE = /^[A-Za-z0-9_.:-]{1,200}$/u;
10
+ const CONTEXT = ['customer_id', 'workspace_id', 'conversation_id', 'phone_number'];
11
+ const DOC = ['owner', 'browser', 'document_id', 'checkout_url', 'expires_at'];
12
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
13
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
14
+ const check = (ok, code = 'configuration_invalid') => { if (!ok) throw new AccelerateError(code); };
15
+ function shape(value, required, optional = [], code) {
16
+ check(object(value) && required.every(key => Object.hasOwn(value, key)) &&
17
+ Object.keys(value).every(key => required.includes(key) || optional.includes(key)), code);
18
+ }
19
+ function freeze(value) {
20
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
21
+ Object.values(value).forEach(freeze); Object.freeze(value);
22
+ }
23
+ return value;
24
+ }
25
+ function owner(value) {
26
+ shape(value, CONTEXT, [], 'permission_denied');
27
+ check(CONTEXT.slice(0, 3).every(key => typeof value[key] === 'string' && OWNER.test(value[key])) &&
28
+ typeof value.phone_number === 'string' && /^\+[1-9]\d{6,14}$/u.test(value.phone_number), 'permission_denied');
29
+ return freeze(Object.fromEntries(CONTEXT.map(key => [key, value[key]])));
30
+ }
31
+ const sameOwner = (a, b) => CONTEXT.every(key => a[key] === b[key]);
32
+ function url(value) {
33
+ try { normalizeBrowserOperation('navigate', { url: value }); }
34
+ catch { throw new AccelerateError('invalid_input'); }
35
+ return value;
36
+ }
37
+ function options(value = {}) {
38
+ shape(value, [], ['signal']); check(value.signal === undefined || value.signal instanceof AbortSignal);
39
+ check(!value.signal?.aborted, 'request_cancelled');
40
+ return Object.freeze(value.signal ? { signal: value.signal } : {});
41
+ }
42
+ function browser(value, provider, connectorId) {
43
+ shape(value, ['provider', 'browser_id', 'custody_mode'], provider === 'connector' ? ['connector_id'] : [], 'permission_denied');
44
+ check(value.provider === provider && value.custody_mode === 'customer_backend_trusted' &&
45
+ typeof value.browser_id === 'string' && (provider === 'browser_use' ? UUID : OPAQUE).test(value.browser_id) &&
46
+ (provider !== 'connector' || value.connector_id === connectorId), 'permission_denied');
47
+ return { ...value };
48
+ }
49
+ function record(value, context, config, { fenced = false, readiness = false, checkoutUrl } = {}) {
50
+ shape(value, DOC, [...(fenced ? ['fence_id'] : []), ...(readiness ? ['recording_disabled', 'extension', 'automation'] : [])], 'permission_denied');
51
+ const identity = owner(value.owner);
52
+ check(sameOwner(identity, context) && typeof value.document_id === 'string' && OPAQUE.test(value.document_id), 'permission_denied');
53
+ const binding = browser(value.browser, config.provider, config.connectorId);
54
+ const checkout = url(value.checkout_url);
55
+ check(!checkoutUrl || checkout === checkoutUrl, 'permission_denied');
56
+ check(typeof value.expires_at === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value.expires_at) &&
57
+ Number.isFinite(Date.parse(value.expires_at)), 'integration_unavailable');
58
+ const result = { owner: identity, browser: binding, document_id: value.document_id,
59
+ checkout_url: checkout, expires_at: value.expires_at };
60
+ if (fenced) {
61
+ check(typeof value.fence_id === 'string' && OPAQUE.test(value.fence_id), 'permission_denied');
62
+ result.fence_id = value.fence_id;
63
+ }
64
+ return freeze(result);
65
+ }
66
+ function sameDocument(actual, expected) {
67
+ check(actual.document_id === expected.document_id && actual.checkout_url === expected.checkout_url &&
68
+ ['provider', 'browser_id', 'connector_id', 'custody_mode'].every(key => actual.browser[key] === expected.browser[key]) &&
69
+ actual.expires_at === expected.expires_at, 'permission_denied');
70
+ }
71
+ function makeConnector(configuration, provider) {
72
+ shape(configuration, ['extensionRelease', 'host'], ['connectorId', 'minimumRemainingMs', 'now']);
73
+ const { connectorId, extensionRelease, host, minimumRemainingMs = 600000, now = Date.now } = configuration;
74
+ check(provider === 'browser_use' ? connectorId === undefined : typeof connectorId === 'string' && CONNECTOR.test(connectorId));
75
+ check(host && ['resolveOwnedBrowser', 'inspectBrowser', 'drainAndFence', 'bindCheckout', 'resolveCheckoutBrowser', 'confirmCleanup']
76
+ .every(key => typeof host[key] === 'function') && Number.isSafeInteger(minimumRemainingMs) &&
77
+ minimumRemainingMs >= 600000 && minimumRemainingMs <= 3600000 && typeof now === 'function');
78
+ let release;
79
+ try { release = validateApprovedExtensionRelease(extensionRelease); }
80
+ catch { throw new AccelerateError('configuration_invalid'); }
81
+ check(release.status === 'approved');
82
+ const config = { provider, connectorId };
83
+ const attachments = new WeakMap();
84
+ const hostCall = async (method, ...args) => {
85
+ try { return await host[method](...args); }
86
+ catch (error) {
87
+ // A provider transport error can contain connection URLs or credentials.
88
+ // Preserve only the SDK's fixed, allowlisted error projection.
89
+ throw error instanceof AccelerateError ? new AccelerateError(error.code, error.status, error.supportReference)
90
+ : new AccelerateError('integration_unavailable');
91
+ }
92
+ };
93
+ const checkpoint = opts => check(!opts.signal?.aborted, 'request_cancelled');
94
+ const inspect = async (context, expected, opts, fenceId) => {
95
+ checkpoint(opts);
96
+ const value = await hostCall('inspectBrowser', context, expected, opts);
97
+ checkpoint(opts);
98
+ const actual = record(value, context, config, { readiness: true });
99
+ sameDocument(actual, expected);
100
+ const current = now();
101
+ check(Number.isFinite(current) && Date.parse(actual.expires_at) - current >= minimumRemainingMs &&
102
+ value.recording_disabled === true, 'integration_unavailable');
103
+ check(object(value.extension) && value.extension.artifact_sha256 === release.artifact.sha256, 'integration_unavailable');
104
+ try { verifyApprovedExtensionRuntime(value.extension, release, { executorContract: 'accelerate_manual_checkout_v1', provider }); }
105
+ catch { throw new AccelerateError('integration_unavailable'); }
106
+ shape(value.automation, ['ordinary_operations_drained', 'agent_access_fenced', 'fence_id'], [], 'integration_unavailable');
107
+ check(typeof value.automation.ordinary_operations_drained === 'boolean' && typeof value.automation.agent_access_fenced === 'boolean' &&
108
+ (value.automation.fence_id === null || typeof value.automation.fence_id === 'string' && OPAQUE.test(value.automation.fence_id)), 'integration_unavailable');
109
+ if (fenceId) check(value.automation.ordinary_operations_drained === true && value.automation.agent_access_fenced === true &&
110
+ value.automation.fence_id === fenceId, 'operation_conflict');
111
+ };
112
+ const resolve = async (context, checkoutId, opts) => {
113
+ check(typeof checkoutId === 'string' && ID.test(checkoutId), 'invalid_input');
114
+ const resolved = await hostCall('resolveCheckoutBrowser', context, freeze({ checkout_id: checkoutId }), opts);
115
+ checkpoint(opts);
116
+ return record(resolved, context, config, { fenced: true });
117
+ };
118
+ // Every method belongs to the server integration. A connector never creates a
119
+ // replacement browser, releases its durable fence or retries a payment call.
120
+ return Object.freeze({ contract: 'accelerate_existing_browser_v1', provider,
121
+ ...(connectorId ? { connector_id: connectorId } : {}),
122
+ async prepareAttachment(contextValue, input, requestOptions) {
123
+ const context = owner(contextValue), opts = options(requestOptions);
124
+ shape(input, ['attempt_key', 'checkout_url'], [], 'invalid_input');
125
+ check(typeof input.attempt_key === 'string' && input.attempt_key.length > 0 && input.attempt_key.length <= 200 &&
126
+ !/[\u0000-\u001f\u007f]/u.test(input.attempt_key), 'invalid_input');
127
+ const request = freeze({ attempt_key: input.attempt_key, checkout_url: url(input.checkout_url) });
128
+ const resolved = await hostCall('resolveOwnedBrowser', context, request, opts);
129
+ checkpoint(opts);
130
+ const expected = record(resolved, context, config, { checkoutUrl: request.checkout_url });
131
+ // Capabilities are checked BEFORE taking the fence or collecting OTP/CVV.
132
+ await inspect(context, expected, opts);
133
+ const ack = await hostCall('drainAndFence', context, expected, freeze({ attempt_key: request.attempt_key }), opts);
134
+ checkpoint(opts);
135
+ shape(ack, ['fence_id', 'ordinary_operations_drained', 'agent_access_fenced'], [], 'integration_unavailable');
136
+ check(typeof ack.fence_id === 'string' && OPAQUE.test(ack.fence_id) && ack.ordinary_operations_drained === true &&
137
+ ack.agent_access_fenced === true, 'operation_conflict');
138
+ await inspect(context, expected, opts, ack.fence_id);
139
+ const browser_resource = typeof host.clientBrowserResource === 'function'
140
+ ? await hostCall('clientBrowserResource', context, expected, opts) : undefined;
141
+ checkpoint(opts);
142
+ const attachment = Object.freeze({ browser: expected.browser, ...(browser_resource ? { browser_resource } : {}) });
143
+ attachments.set(attachment, { context, attempt_key: request.attempt_key, record: freeze({ ...expected, fence_id: ack.fence_id }) });
144
+ return attachment;
145
+ },
146
+ async commitAttachment(contextValue, input, requestOptions) {
147
+ const context = owner(contextValue), opts = options(requestOptions);
148
+ shape(input, ['checkout_id', 'attachment'], [], 'invalid_input');
149
+ const saved = attachments.get(input.attachment);
150
+ check(saved && sameOwner(saved.context, context) && typeof input.checkout_id === 'string' && ID.test(input.checkout_id), 'permission_denied');
151
+ check(!saved.checkout_id || saved.checkout_id === input.checkout_id, 'operation_conflict');
152
+ // Bind before awaiting transport so concurrent/lost-ACK recovery cannot
153
+ // assign this attachment to a different checkout.
154
+ saved.checkout_id = input.checkout_id;
155
+ await hostCall('bindCheckout', context, freeze({ ...saved.record, checkout_id: input.checkout_id, attempt_key: saved.attempt_key }), opts);
156
+ checkpoint(opts);
157
+ // Keep the capability for an idempotent same-checkout ACK recovery only.
158
+ },
159
+ async assertPaymentReady(contextValue, input, requestOptions) {
160
+ const context = owner(contextValue), opts = options(requestOptions);
161
+ shape(input, ['checkout_id'], ['checkout_url'], 'invalid_input');
162
+ const expected = await resolve(context, input.checkout_id, opts);
163
+ if (input.checkout_url !== undefined) check(url(input.checkout_url) === expected.checkout_url, 'permission_denied');
164
+ await inspect(context, expected, opts, expected.fence_id);
165
+ },
166
+ async confirmCleanup(contextValue, input, requestOptions) {
167
+ const context = owner(contextValue), opts = options(requestOptions);
168
+ shape(input, ['checkout_id'], [], 'invalid_input');
169
+ const expected = await resolve(context, input.checkout_id, opts);
170
+ const ack = await hostCall('confirmCleanup', context, expected, freeze({ checkout_id: input.checkout_id }), opts);
171
+ checkpoint(opts);
172
+ shape(ack, ['provider_browser_stopped', 'credentials_cleared', 'fence_retained'], [], 'integration_unavailable');
173
+ check(Object.values(ack).every(value => value === true), 'integration_unavailable');
174
+ },
175
+ });
176
+ }
177
+
178
+ export function createExistingBrowserConnector(options) { return makeConnector(options ?? {}, 'connector'); }
179
+ export function createBrowserUseConnector(options) { return makeConnector(options ?? {}, 'browser_use'); }