@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,357 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { AccelerateError } from './index.js';
4
+ import { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
5
+ import { renderPrivateChatState, privateChatProgress } from './private-chat.js';
6
+
7
+ const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
8
+ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/;
9
+ const MESSAGE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/;
10
+ const RECOVER = new Set(['accelerate_checkout_status', 'accelerate_checkout_cancel', 'private_state', 'private_cancel']);
11
+ const TERMINAL = new Set(['cancelled', 'expired', 'failed']);
12
+ const STATUS_ORDER = new Map(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed'].map((status, index) => [status, index]));
13
+ const STAGES = new Set(['not_started', 'identity_required', 'otp_required', 'selection_required', 'preparing_checkout', 'approval_required', 'cvv_required', 'verified', 'review_delivery_required', 'review_delivered', 'approved_for_submit', 'closed']);
14
+ const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
15
+ const object = value => value && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
16
+ const check = (condition, code = 'invalid_input') => { if (!condition) throw new AccelerateError(code); };
17
+ const clone = value => structuredClone(value);
18
+ const validDate = value => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value) && Number.isFinite(Date.parse(value));
19
+ function canonical(value) {
20
+ if (Array.isArray(value)) return value.map(canonical);
21
+ if (object(value)) return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
22
+ return value;
23
+ }
24
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
25
+ function exact(value, required, optional = [], code = 'invalid_input') {
26
+ check(object(value) && required.every(key => Object.hasOwn(value, key)) && Object.keys(value).every(key => required.includes(key) || optional.includes(key)), code);
27
+ }
28
+ export function authenticatedCheckoutContext(value) {
29
+ exact(value, ['customer_id', 'workspace_id', 'conversation_id', 'phone_number'], [], 'permission_denied');
30
+ check(['customer_id', 'workspace_id', 'conversation_id'].every(key => typeof value[key] === 'string' && OWNER.test(value[key])) && /^\+[1-9]\d{6,14}$/.test(value.phone_number), 'permission_denied');
31
+ return Object.freeze({ ...value });
32
+ }
33
+ function owner(value) {
34
+ return Object.fromEntries(['customer_id', 'workspace_id', 'conversation_id'].map(key => [key, value[key]]));
35
+ }
36
+ export function checkoutHostOwnerKey(context) {
37
+ const resolved = authenticatedCheckoutContext(context);
38
+ return `accelerate-partner-host:${digest(['v1', resolved.customer_id, resolved.workspace_id])}`;
39
+ }
40
+ function sameOwner(left, right) { return ['customer_id', 'workspace_id', 'conversation_id'].every(key => left[key] === right[key]); }
41
+ function sameAccount(left, right) { return ['customer_id', 'workspace_id'].every(key => left[key] === right[key]); }
42
+ function samePhone(record, context) { return record.phone_binding === undefined || record.phone_binding === digest(['v1', context.customer_id, context.workspace_id, context.phone_number]); }
43
+ function requireCleanClosedAttempt(record) {
44
+ const old = record.attempt;
45
+ check(old?.phase === 'closed' && !old.uncertain && !old.pending && TERMINAL.has(old.checkout?.status) && old.checkout?.diagnostics?.cleanup_status === 'confirmed' && old.cleanup_confirmed === true, 'outcome_unknown');
46
+ check(record.outbox.every(item => item.delivered) && (record.deliveries ?? []).every(item => item.receipt !== undefined), 'operation_conflict');
47
+ }
48
+ function safeText(value, max = 10000) {
49
+ check(typeof value === 'string' && value.length > 0 && value.length <= max && !/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]|(?:\d[ -]?){13,19}|(?:wss?|devtools|chrome-extension):\/\/|Bearer\s|\b(?:session_token|handoff_token|submit_token|api_key)\b/i.test(value), 'invalid_response');
50
+ return value;
51
+ }
52
+ function safeCheckout(value) {
53
+ exact(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics', 'reconciliation', 'final_click_actor', 'submit_actor'], 'invalid_response');
54
+ check(ID.test(value.id) && STATUSES.has(value.status) && Number.isSafeInteger(value.revision) && value.revision >= 0 && value.retry_safe === false && value.payment_confirmed === false && validDate(value.expires_at), 'invalid_response');
55
+ if (value.card !== undefined) {
56
+ exact(value.card, ['brand', 'last4'], [], 'invalid_response');
57
+ check(value.card.last4 === null || typeof value.card.last4 === 'string' && /^\d{4}$/.test(value.card.last4), 'invalid_response');
58
+ if (value.card.brand !== null) safeText(value.card.brand, 40);
59
+ }
60
+ if (value.address !== undefined) {
61
+ exact(value.address, ['label'], ['city', 'state', 'postal_code_mask'], 'invalid_response');
62
+ for (const key of ['label', 'city', 'state']) if (value.address[key] != null) safeText(value.address[key], 400);
63
+ check(value.address.postal_code_mask == null || /^\*{3}[A-Za-z0-9 -]{1,2}$/.test(value.address.postal_code_mask), 'invalid_response');
64
+ }
65
+ if (value.quote !== undefined) {
66
+ exact(value.quote, ['checkout_origin', 'amount_cents', 'currency'], [], 'invalid_response');
67
+ let url; try { url = new URL(value.quote.checkout_origin); } catch { check(false, 'invalid_response'); }
68
+ check(url.protocol === 'https:' && url.origin === value.quote.checkout_origin && !url.username && !url.password && !url.port && Number.isSafeInteger(value.quote.amount_cents) && value.quote.amount_cents > 0 && value.quote.currency === 'USD', 'invalid_response');
69
+ }
70
+ if (value.purpose !== undefined) check(['purchase', 'sandbox_fill_only', 'sandbox_source_comparison'].includes(value.purpose) && value.submit_allowed === (value.purpose === 'purchase'), 'invalid_response');
71
+ if (value.submit_expires_at !== undefined) check(validDate(value.submit_expires_at), 'invalid_response');
72
+ if (value.submit_allowed !== undefined) check(typeof value.submit_allowed === 'boolean', 'invalid_response');
73
+ if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' && ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
74
+ if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
75
+ if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' && ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
76
+ if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
77
+ if (value.purchase_approved !== undefined) check(typeof value.purchase_approved === 'boolean', 'invalid_response');
78
+ if (value.approval_policy !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean', 'invalid_response');
79
+ if (value.reconciliation !== undefined) { try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { check(false, 'invalid_response'); } }
80
+ // The SDK owns detailed diagnostics validation. This projection keeps no URLs,
81
+ // provider capabilities or data that could accidentally become an effect input.
82
+ const result = clone(value);
83
+ if (result.diagnostics !== undefined) result.diagnostics = validateCheckoutDiagnostics(result.diagnostics, result);
84
+ return result;
85
+ }
86
+ function validateRecord(value, context) {
87
+ if (value === undefined || value === null) return { version: 'v1', owner: owner(context), phone_binding: digest(['v1', context.customer_id, context.workspace_id, context.phone_number]), outbox: [], history: [], deliveries: [] };
88
+ exact(value, ['version', 'owner', 'outbox', 'history'], ['attempt', 'deliveries', 'phone_binding'], 'operation_conflict');
89
+ check(value.version === 'v1' && sameOwner(value.owner, context), 'permission_denied');
90
+ if (value.phone_binding !== undefined) {
91
+ check(typeof value.phone_binding === 'string' && /^[a-f0-9]{64}$/.test(value.phone_binding), 'operation_conflict');
92
+ if (context.phone_number !== undefined) check(value.phone_binding === digest(['v1', context.customer_id, context.workspace_id, context.phone_number]), 'permission_denied');
93
+ }
94
+ exact(value.owner, ['customer_id', 'workspace_id', 'conversation_id'], [], 'operation_conflict');
95
+ check(['customer_id', 'workspace_id', 'conversation_id'].every(key => typeof value.owner[key] === 'string' && OWNER.test(value.owner[key])), 'operation_conflict');
96
+ check(Array.isArray(value.outbox) && value.outbox.length <= 500 && Array.isArray(value.history) && value.history.length <= 1000, 'operation_conflict');
97
+ if (value.attempt !== undefined) {
98
+ exact(value.attempt, ['idempotency_key', 'phase', 'uncertain'], ['checkout_id', 'pending', 'private_stage', 'checkout', 'effects', 'cleanup_confirmed'], 'operation_conflict');
99
+ check(/^accelerate-host:[a-f0-9-]{36}$/.test(value.attempt.idempotency_key) && ['creating', 'active', 'uncertain', 'closed'].includes(value.attempt.phase) && typeof value.attempt.uncertain === 'boolean', 'operation_conflict');
100
+ if (value.attempt.cleanup_confirmed !== undefined) check(typeof value.attempt.cleanup_confirmed === 'boolean', 'operation_conflict');
101
+ if (value.attempt.effects !== undefined) check(Array.isArray(value.attempt.effects) && value.attempt.effects.every(item => ['accelerate_checkout_fill', 'accelerate_checkout_submit'].includes(item)) && new Set(value.attempt.effects).size === value.attempt.effects.length, 'operation_conflict');
102
+ if (value.attempt.checkout_id !== undefined) check(ID.test(value.attempt.checkout_id), 'operation_conflict');
103
+ if (value.attempt.pending !== undefined) {
104
+ exact(value.attempt.pending, ['operation'], ['message_id', 'revision'], 'operation_conflict');
105
+ check(/^[a-z_]{1,100}$/.test(value.attempt.pending.operation) && (value.attempt.pending.message_id === undefined || MESSAGE.test(value.attempt.pending.message_id)), 'operation_conflict');
106
+ }
107
+ if (value.attempt.pending?.revision !== undefined) check(Number.isSafeInteger(value.attempt.pending.revision) && value.attempt.pending.revision > 0, 'operation_conflict');
108
+ if (value.attempt.private_stage !== undefined) check(STAGES.has(value.attempt.private_stage), 'operation_conflict');
109
+ if (value.attempt.checkout) check(safeCheckout(value.attempt.checkout).id === value.attempt.checkout_id, 'operation_conflict');
110
+ }
111
+ for (const item of value.history) {
112
+ exact(item, ['idempotency_key', 'checkout_id', 'status', 'uncertain'], [], 'operation_conflict');
113
+ check(ID.test(item.checkout_id) && TERMINAL.has(item.status) && item.uncertain === false, 'operation_conflict');
114
+ }
115
+ if (value.deliveries !== undefined) {
116
+ check(Array.isArray(value.deliveries) && value.deliveries.length <= 500, 'operation_conflict');
117
+ for (const item of value.deliveries) {
118
+ exact(item, ['idempotency_key'], ['receipt'], 'operation_conflict');
119
+ check(/^accelerate-chat:[a-f0-9]{64}$/.test(item.idempotency_key), 'operation_conflict');
120
+ if (item.receipt !== undefined) { exact(item.receipt, ['message_id', 'sent_at'], [], 'operation_conflict'); check(MESSAGE.test(item.receipt.message_id) && validDate(item.receipt.sent_at), 'operation_conflict'); }
121
+ }
122
+ }
123
+ for (const item of value.outbox) validateOutbox(item);
124
+ return clone(value);
125
+ }
126
+ function validateOutbox(item) {
127
+ exact(item, ['id', 'kind', 'payload', 'delivered'], [], 'invalid_response');
128
+ check(/^accelerate-(?:checkout-event|chat|progress):[a-f0-9]{64}$/.test(item.id) && ['checkout_event', 'private_prompt', 'safe_progress'].includes(item.kind) && typeof item.delivered === 'boolean', 'invalid_response');
129
+ if (item.kind === 'checkout_event') {
130
+ exact(item.payload, ['version', 'kind', 'idempotency_key', 'checkout'], [], 'invalid_response');
131
+ check(item.payload.version === 'v1' && ['created', 'payment_requested', 'approval_requested', 'review_ready'].includes(item.payload.kind) && item.payload.idempotency_key === item.id, 'invalid_response');
132
+ safeCheckout(item.payload.checkout);
133
+ } else {
134
+ exact(item.payload, ['checkout_id', 'text', 'idempotency_key'], item.kind === 'safe_progress' ? ['stage'] : [], 'invalid_response');
135
+ check(ID.test(item.payload.checkout_id) && item.payload.idempotency_key === item.id, 'invalid_response'); safeText(item.payload.text);
136
+ if (item.kind === 'safe_progress') check(['preparing_checkout', 'verified', 'approved_for_submit', 'closed'].includes(item.payload.stage), 'invalid_response');
137
+ }
138
+ }
139
+ function observe(record, checkout) {
140
+ const projection = safeCheckout(checkout);
141
+ check(projection.id === record.attempt.checkout_id, 'permission_denied');
142
+ const saved = record.attempt.checkout;
143
+ if (saved && !['outcome_unknown', 'submit_attempted'].includes(projection.status)) {
144
+ if (projection.revision < saved.revision) return;
145
+ if (projection.revision === saved.revision && STATUS_ORDER.has(saved.status) && STATUS_ORDER.has(projection.status) && STATUS_ORDER.get(saved.status) > STATUS_ORDER.get(projection.status)) return;
146
+ }
147
+ // Inline review delivery may have already closed this browser while the outer
148
+ // fill callback still holds its earlier ready_for_submit projection.
149
+ if (saved && TERMINAL.has(saved.status) && !TERMINAL.has(projection.status) && !['outcome_unknown', 'submit_attempted'].includes(projection.status) && projection.revision <= saved.revision) return;
150
+ record.attempt.checkout = projection;
151
+ if (['outcome_unknown', 'submit_attempted'].includes(projection.status)) record.attempt.uncertain = true;
152
+ record.attempt.phase = record.attempt.uncertain ? 'uncertain' : TERMINAL.has(projection.status) ? 'closed' : 'active';
153
+ }
154
+ function recoverable(record, metadata) {
155
+ const attempt = record.attempt;
156
+ if (!attempt?.pending && !attempt?.uncertain) return;
157
+ const duplicatePrivateReply = metadata.operation === 'private_reply' && metadata.message_id && attempt.pending?.operation === 'private_reply' && metadata.message_id === attempt.pending.message_id && !attempt.uncertain;
158
+ check(RECOVER.has(metadata.operation) || duplicatePrivateReply, 'outcome_unknown');
159
+ }
160
+
161
+ /** The host owns persistence. No memory store or timer can satisfy this contract. */
162
+ export function createDurableCheckoutHost({ persistence, resolveContext } = {}) {
163
+ check(typeof persistence?.withLock === 'function' && typeof resolveContext === 'function');
164
+ const scope = new AsyncLocalStorage();
165
+ const locked = async (input, work, { creationRebind = false } = {}) => {
166
+ const context = authenticatedCheckoutContext(input), key = checkoutHostOwnerKey(context), current = scope.getStore();
167
+ if (current?.active) {
168
+ // A live nested operation may not rebind even a closed record. Existing
169
+ // callbacks retain the original conversation/phone until they unwind.
170
+ check(current.key === key && sameOwner(current.record.owner, context) && samePhone(current.record, context), 'permission_denied');
171
+ return work(current, context);
172
+ }
173
+ let calls = 0, completed = false, expected;
174
+ const result = await persistence.withLock(key, async access => {
175
+ check(++calls === 1 && typeof access?.load === 'function' && typeof access?.commit === 'function', 'operation_conflict');
176
+ const saved = await access.load();
177
+ let record;
178
+ if (creationRebind && saved !== undefined && saved !== null) {
179
+ // Validate the persisted schema before considering a different verified
180
+ // conversation. Account identity still scopes this same owner lock.
181
+ record = validateRecord(saved, saved.owner);
182
+ check(sameAccount(record.owner, context), 'permission_denied');
183
+ if (!sameOwner(record.owner, context) || !samePhone(record, context)) requireCleanClosedAttempt(record);
184
+ } else record = validateRecord(saved, context);
185
+ const active = { key, access, record, active: true, effectDepth: 0 };
186
+ try { return await scope.run(active, async () => { expected = await work(active, context); completed = true; return expected; }); }
187
+ finally { active.active = false; }
188
+ });
189
+ check(calls === 1 && completed && result === expected, 'operation_conflict'); return result;
190
+ };
191
+ const commit = async active => {
192
+ check(active.active, 'operation_conflict');
193
+ // Never pass live mutable state to a persistence implementation.
194
+ validateRecord(active.record, active.record.owner);
195
+ await active.access.commit(clone(active.record));
196
+ check(active.active, 'operation_conflict');
197
+ };
198
+ const enqueue = async (active, item) => {
199
+ validateOutbox(item);
200
+ const existing = active.record.outbox.find(entry => entry.id === item.id);
201
+ if (existing) { check(JSON.stringify(canonical({ ...existing, delivered: false })) === JSON.stringify(canonical(item)), 'idempotency_conflict'); return; }
202
+ check(active.record.outbox.length < 500, 'operation_conflict');
203
+ active.record.outbox.push(clone(item)); await commit(active);
204
+ };
205
+ const withOwned = (context, checkoutId, work, metadata = { operation: 'unclassified' }) => locked(context, async (active, resolved) => {
206
+ check(typeof work === 'function' && ID.test(checkoutId) && active.record.attempt?.checkout_id === checkoutId, 'permission_denied');
207
+ exact(metadata, ['operation'], ['message_id', 'revision']);
208
+ check(metadata.revision === undefined || Number.isSafeInteger(metadata.revision) && metadata.revision > 0); check(/^[a-z_]{1,100}$/.test(metadata.operation));
209
+ if (!active.effectDepth || active.record.attempt.uncertain) recoverable(active.record, metadata);
210
+ // A lost previous effect acknowledgement remains sticky even if a safe read
211
+ // succeeds. Read-only failures do not create a new payment ambiguity.
212
+ if (active.record.attempt.effects?.includes(metadata.operation)) check(false, 'checkout_frozen');
213
+ if (metadata.operation === 'accelerate_checkout_fill') {
214
+ const checkout = active.record.attempt.checkout;
215
+ check(checkout?.status === 'approved' && active.record.attempt.private_stage === 'verified' && metadata.revision === checkout.revision, 'verification_required');
216
+ }
217
+ if (metadata.operation === 'accelerate_checkout_submit') {
218
+ const checkout = active.record.attempt.checkout;
219
+ check(checkout?.purpose === 'purchase' && checkout.submit_allowed === true && checkout.final_click_actor === 'customer' && checkout.status === 'ready_for_submit' && checkout.purchase_approved === true && active.record.attempt.private_stage === 'approved_for_submit' && validDate(checkout.submit_expires_at) && Date.parse(checkout.submit_expires_at) > Date.now() && metadata.revision === checkout.revision, 'submit_unavailable');
220
+ }
221
+ const oneUse = ['accelerate_checkout_fill', 'accelerate_checkout_submit'].includes(metadata.operation);
222
+ if (oneUse) {
223
+ active.record.attempt.effects ??= [];
224
+ check(!active.record.attempt.effects.includes(metadata.operation), 'checkout_frozen');
225
+ active.record.attempt.effects.push(metadata.operation);
226
+ }
227
+ const recovery = RECOVER.has(metadata.operation), previousPending = active.record.attempt.pending;
228
+ if (!recovery) { active.record.attempt.pending = { ...metadata }; await commit(active); }
229
+ const binding = Object.freeze({ ...resolved, checkout_id: checkoutId });
230
+ try {
231
+ active.effectDepth++;
232
+ let result;
233
+ try { result = await work(binding); } finally { active.effectDepth--; }
234
+ if (result?.checkout) observe(active.record, result.checkout);
235
+ if (!recovery) delete active.record.attempt.pending;
236
+ else if (previousPending) active.record.attempt.pending = previousPending;
237
+ await commit(active); return result;
238
+ } catch (error) {
239
+ if (!recovery) {
240
+ // Preserve the already-committed pending marker if this acknowledgement
241
+ // also fails; a later process must still refuse another effect.
242
+ active.record.attempt.uncertain = true; active.record.attempt.phase = 'uncertain';
243
+ try { await commit(active); } catch {}
244
+ }
245
+ throw error instanceof AccelerateError ? error : new AccelerateError(recovery ? 'request_failed' : 'outcome_unknown');
246
+ }
247
+ });
248
+ return Object.freeze({
249
+ resolveContext: async extra => authenticatedCheckoutContext(await resolveContext(extra)),
250
+ withCheckoutCreation: (context, work) => locked(context, async (active, resolved) => {
251
+ check(typeof work === 'function');
252
+ if (active.record.attempt) {
253
+ const old = active.record.attempt;
254
+ requireCleanClosedAttempt(active.record);
255
+ // Ordinary closed history is bounded. Uncertain attempts are never
256
+ // admitted here, aged out or removed by this compaction.
257
+ active.record.history = active.record.history.slice(-999);
258
+ active.record.history.push({ idempotency_key: old.idempotency_key, checkout_id: old.checkout_id, status: old.checkout.status, uncertain: false });
259
+ }
260
+ if (active.record.attempt) { active.record.outbox = []; active.record.deliveries = []; }
261
+ // Rebind only in the same durable write as the new creation fence. A lost
262
+ // commit acknowledgement cannot leave an unfenced owner-only row.
263
+ active.record.owner = owner(resolved);
264
+ active.record.phone_binding = digest(['v1', resolved.customer_id, resolved.workspace_id, resolved.phone_number]);
265
+ active.record.attempt = { idempotency_key: `accelerate-host:${randomUUID()}`, phase: 'creating', uncertain: false, cleanup_confirmed: false, effects: [], pending: { operation: 'creation' } };
266
+ await commit(active);
267
+ let persisted = false;
268
+ try {
269
+ const result = await work({ idempotency_key: active.record.attempt.idempotency_key, persistCreated: async binding => {
270
+ check(!persisted && binding?.checkout_id && ID.test(binding.checkout_id) && sameOwner(binding, resolved) && binding.phone_number === resolved.phone_number, 'permission_denied');
271
+ check(!active.record.attempt.checkout_id || active.record.attempt.checkout_id === binding.checkout_id, 'idempotency_conflict');
272
+ active.record.attempt.checkout_id = binding.checkout_id; await commit(active); persisted = true;
273
+ } });
274
+ check(persisted, 'outcome_unknown');
275
+ if (result?.checkout) observe(active.record, result.checkout);
276
+ else active.record.attempt.phase = 'active';
277
+ delete active.record.attempt.pending; await commit(active); return result;
278
+ } catch (error) {
279
+ active.record.attempt.uncertain = true; active.record.attempt.phase = 'uncertain'; try { await commit(active); } catch {}
280
+ throw error instanceof AccelerateError ? error : new AccelerateError('outcome_unknown');
281
+ }
282
+ }, { creationRebind: true }),
283
+ withOwnedCheckout: withOwned,
284
+ dispatchCheckoutEvent: (context, event) => locked(context, async active => {
285
+ check(active.record.attempt?.checkout_id === event?.checkout?.id, 'permission_denied');
286
+ await enqueue(active, { id: event.idempotency_key, kind: 'checkout_event', payload: event, delivered: false });
287
+ }),
288
+ withPrivateDelivery: (context, idempotencyKey, deliver) => locked(context, async active => {
289
+ check(/^accelerate-chat:[a-f0-9]{64}$/.test(idempotencyKey) && typeof deliver === 'function');
290
+ active.record.deliveries ??= [];
291
+ const saved = active.record.deliveries.find(item => item.idempotency_key === idempotencyKey);
292
+ if (saved?.receipt) return Object.freeze(clone(saved.receipt));
293
+ // An unconfirmed native send requires provider reconciliation; do not
294
+ // create a different attachment under an already-used message key.
295
+ check(!saved, 'outcome_unknown');
296
+ check(active.record.deliveries.length < 500, 'operation_conflict');
297
+ const entry = { idempotency_key: idempotencyKey }; active.record.deliveries.push(entry); await commit(active);
298
+ const receipt = await deliver();
299
+ exact(receipt, ['message_id', 'sent_at'], [], 'invalid_response');
300
+ check(MESSAGE.test(receipt.message_id) && validDate(receipt.sent_at) && Date.parse(receipt.sent_at) <= Date.now(), 'invalid_response');
301
+ entry.receipt = clone(receipt); await commit(active); return Object.freeze(clone(receipt));
302
+ }),
303
+ confirmBrowserCleanup: (context, checkoutId) => locked(context, async active => {
304
+ check(active.record.attempt?.checkout_id === checkoutId && active.record.attempt.checkout?.diagnostics?.cleanup_status === 'confirmed', 'operation_conflict');
305
+ active.record.attempt.cleanup_confirmed = true; await commit(active);
306
+ }),
307
+ /** For private callbacks only. A provider message ID may recover its own
308
+ * idempotent reply; no reply text enters persistence or the outbox. */
309
+ withPrivateCheckout: (context, metadata, work) => locked(context, async (active, resolved) => {
310
+ exact(metadata, ['operation'], ['message_id']);
311
+ check(metadata.message_id === undefined || typeof metadata.message_id === 'string' && MESSAGE.test(metadata.message_id));
312
+ const attempt = active.record.attempt;
313
+ if (!attempt?.checkout_id) return undefined;
314
+ if (!active.effectDepth || attempt.uncertain) recoverable(active.record, metadata);
315
+ check(['private_start', 'private_reply', 'private_state', 'private_cancel', 'review_delivery'].includes(metadata.operation) && typeof work === 'function');
316
+ const recovery = RECOVER.has(metadata.operation), previousPending = attempt.pending, nested = active.effectDepth > 0;
317
+ if (!recovery) { attempt.pending = { ...metadata }; await commit(active); }
318
+ try {
319
+ active.effectDepth++;
320
+ let result;
321
+ try { result = await work(Object.freeze({ ...resolved, checkout_id: attempt.checkout_id,
322
+ previous_stage: attempt.private_stage, checkout: attempt.checkout ? Object.freeze(clone(attempt.checkout)) : undefined,
323
+ savePrivateState: async (state, { messageId, suppressPrompt = false } = {}) => {
324
+ check(state?.checkout?.id === attempt.checkout_id && MESSAGE.test(messageId), 'invalid_response');
325
+ // These renderers revalidate the complete private projection. Only
326
+ // safe outgoing wording is eligible for durable storage.
327
+ const prompt = renderPrivateChatState(state, { messageId }), progress = privateChatProgress(state, { messageId });
328
+ const previousStage = attempt.private_stage;
329
+ observe(active.record, state.checkout); attempt.private_stage = state.stage;
330
+ await commit(active);
331
+ if (prompt && !suppressPrompt && state.stage !== 'review_delivery_required') await enqueue(active, { id: prompt.idempotency_key, kind: 'private_prompt', payload: { checkout_id: attempt.checkout_id, ...prompt }, delivered: false });
332
+ if (progress && (previousStage !== state.stage || state.duplicate)) await enqueue(active, { id: progress.idempotency_key, kind: 'safe_progress', payload: { checkout_id: attempt.checkout_id, ...progress }, delivered: false });
333
+ },
334
+ })); } finally { active.effectDepth--; }
335
+ if (previousPending && (recovery || nested)) attempt.pending = previousPending; else if (!recovery) delete attempt.pending;
336
+ await commit(active); return result;
337
+ } catch (error) {
338
+ // A reply may be replayed ONLY under its original authenticated provider
339
+ // ID, relying on the private service's durable deduplication. Other
340
+ // uncertain effects block future effects while retaining safe recovery.
341
+ if (!recovery && metadata.operation !== 'private_reply') { attempt.uncertain = true; attempt.phase = 'uncertain'; try { await commit(active); } catch {} }
342
+ throw new AccelerateError('request_failed');
343
+ }
344
+ }),
345
+ /** At-least-once SAFE delivery only. The downstream provider/queue must
346
+ * deduplicate item.id. Never put a payment action in this handler. */
347
+ drainOutbox: (context, deliver) => locked(context, async (active, resolved) => {
348
+ check(typeof deliver === 'function'); let delivered = 0;
349
+ for (const item of active.record.outbox) {
350
+ if (item.delivered) continue;
351
+ await deliver(Object.freeze(clone(item)), resolved);
352
+ item.delivered = true; await commit(active); delivered++;
353
+ }
354
+ return delivered;
355
+ }),
356
+ });
357
+ }
package/index.d.ts ADDED
@@ -0,0 +1,253 @@
1
+ import type { BrowserOperation, BrowserResult } from './browser-operations.js';
2
+ import type { CheckoutDiagnostics, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
3
+ export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
4
+ export type { CheckoutDiagnostics, CheckoutDiagnosticStage, CheckoutDiagnosticAction, CheckoutCleanupStatus, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
5
+ export type { BrowserOperation, BrowserResult, BrowserStep, BrowserCondition } from './browser-operations.js';
6
+
7
+ export type CheckoutId = `mcs_${string}`;
8
+ export type CheckoutPurpose = 'purchase' | 'sandbox_source_comparison' | 'sandbox_fill_only';
9
+ export type CheckoutStatus = 'wallet_required' | 'selection_ready' | 'details_ready' | 'approval_required' |
10
+ 'approved' | 'ready_for_submit' | 'final_click_armed' | 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
11
+ export interface RequestOptions { signal?: AbortSignal }
12
+ export interface WaitForCheckout {
13
+ /** Return on these states, or immediately on ready_for_submit or any terminal state. GET requests only. */
14
+ statuses: readonly CheckoutStatus[];
15
+ /** Overall wait, including requests. Default 60 seconds; range 1–300000 milliseconds. */
16
+ timeoutMs?: number;
17
+ /** Delay between successful status reads. Default 1000; range 250–5000 milliseconds. Errors are never retried. */
18
+ intervalMs?: number;
19
+ }
20
+ export type AccelerateNextAction = 'check_configuration' | 'check_request' | 'check_status' | 'complete_wallet' |
21
+ 'complete_verification' | 'refresh_quote' | 'refresh_revision' | 'reconcile_order' | 'stop';
22
+ export interface AccelerateEnvironment {
23
+ /** Optional HTTPS origin; defaults to Accelerate's hosted managed service. */
24
+ ACCELERATE_BASE_URL?: string;
25
+ ACCELERATE_API_KEY?: string;
26
+ ACCELERATE_MERCHANT_ID?: string;
27
+ }
28
+ export interface AccelerateConnection {
29
+ status: 'connected';
30
+ api_version: 'v1';
31
+ merchant_id: string;
32
+ interaction_modes: ['chat', 'hosted'];
33
+ currencies: ['USD'];
34
+ }
35
+ export interface Checkout {
36
+ id: CheckoutId;
37
+ status: CheckoutStatus;
38
+ revision: number;
39
+ expires_at: string;
40
+ /** Immutable policy metadata. Omitted only by service versions predating this field. */
41
+ purpose?: CheckoutPurpose;
42
+ /** Submission is enabled only for purchase purpose; never for sandbox_fill_only. */
43
+ submit_allowed?: boolean;
44
+ /** after_fill: final customer approval follows a privately delivered masked review. */
45
+ approval_policy?: 'after_fill';
46
+ purchase_approved?: boolean;
47
+ /** Explicit v2 policy acknowledged at creation; a later explicit quote arm lets private CVV continue
48
+ * one fill under the backend lock. Does not authorize final submission. */
49
+ fill_after_verification?: true;
50
+ /** Purchases only: the customer's own trusted backend/browser performs the physical final click. Accelerate never does. */
51
+ final_click_actor?: 'customer';
52
+ /** Present on submit_attempted (required for a purchase) and optionally on outcome_unknown, which cannot assert that a click happened. Never payment confirmation. */
53
+ submit_actor?: 'customer';
54
+ card?: { last4: string | null; brand: string | null };
55
+ address?: { label: string | null; city?: string | null; state?: string | null; postal_code_mask?: string | null };
56
+ quote?: { checkout_origin: string; amount_cents: number; currency: 'USD' };
57
+ submit_expires_at?: string;
58
+ /** Present once the client backend recorded the merchant's result. Absent on older services.
59
+ * This is the client's statement, not Accelerate's proof, so payment_confirmed stays false. */
60
+ reconciliation?: CheckoutReconciliation;
61
+ retry_safe: false;
62
+ payment_confirmed: false;
63
+ /** Negotiated safe metadata; absent when connecting to an older service. */
64
+ diagnostics?: CheckoutDiagnostics;
65
+ }
66
+ export interface CreatedCheckout extends Checkout { wallet_url?: string }
67
+ export interface ReconciledCheckout extends Checkout { reconciliation: CheckoutReconciliation }
68
+ /** Server only, after your backend checked the merchant's order system. Never model input.
69
+ * order_reference is stored only as a hash and is never returned. */
70
+ export type CheckoutReconciliationRequest = {
71
+ state: 'confirmed';
72
+ order_reference: string;
73
+ /** Must equal the approved quote exactly. */
74
+ amount_cents: number;
75
+ currency: 'USD';
76
+ source: string;
77
+ } | {
78
+ state: Exclude<CheckoutReconciliationState, 'confirmed'>;
79
+ order_reference?: string;
80
+ amount_cents?: number;
81
+ currency?: 'USD';
82
+ source: string;
83
+ };
84
+ /** Server-owned existing browser, resolved from the authenticated application's
85
+ * durable browser record. Never accept this binding from model arguments.
86
+ * The customer backend retains provider access; this is not private-from-creation custody. */
87
+ export interface BrowserUseBrowserBinding {
88
+ provider: 'browser_use';
89
+ browser_id: string;
90
+ custody_mode: 'customer_backend_trusted';
91
+ }
92
+ /** Registered server-side executor connector; IDs are resolved from owned backend records. */
93
+ export interface RegisteredBrowserBinding {
94
+ provider: 'connector';
95
+ connector_id: string;
96
+ browser_id: string;
97
+ custody_mode: 'customer_backend_trusted';
98
+ }
99
+ export type CustomerBrowserBinding = BrowserUseBrowserBinding | RegisteredBrowserBinding;
100
+ /** Derive every field from authenticated server ownership and its immutable browser reservation. Never expose as model input. */
101
+ export interface CreationInspectionRequest {
102
+ customer_id: string;
103
+ workspace_id: string;
104
+ conversation_id: string;
105
+ idempotency_key: string;
106
+ browser: BrowserUseBrowserBinding;
107
+ }
108
+ export type CreationInspection = { confirmed: false } | {
109
+ confirmed: true;
110
+ checkout_id: CheckoutId;
111
+ revision: 0;
112
+ purpose: 'purchase' | 'sandbox_fill_only';
113
+ creation_key_sha256: string;
114
+ retirement: { browser_provider: 'browser_use'; browser_id: string; retired: true; provider_stopped: true; private_lease_closed: true; recording_disabled: true };
115
+ };
116
+ /** Read-only proof for an already terminal, retired fill-only attempt. Never permits replay or purchase. */
117
+ export interface RetirementInspectionRequest extends CreationInspectionRequest { revision: number; }
118
+ export type RetirementInspection = { confirmed: false } | {
119
+ confirmed: true;
120
+ checkout_id: CheckoutId;
121
+ revision: number;
122
+ status: 'outcome_unknown';
123
+ purpose: 'sandbox_fill_only';
124
+ approval_policy: 'after_fill';
125
+ submit_allowed: false;
126
+ purchase_approved: false;
127
+ creation_key_sha256: string;
128
+ retirement: Extract<CreationInspection, { confirmed: true }>['retirement'];
129
+ };
130
+ export interface CreateCheckout {
131
+ /** Derive from your server's authenticated user, never model input. */
132
+ customer_id: string;
133
+ /** Derive from the authenticated workspace. */
134
+ workspace_id: string;
135
+ phone_number: string;
136
+ start_url: string;
137
+ /** Reuse only for this exact creation attempt. */
138
+ idempotency_key: string;
139
+ /** Default hosted. Chat creates no hosted wallet link or browser session capability. */
140
+ interaction_mode?: 'hosted' | 'chat';
141
+ /** Required in chat mode; derive from the authenticated messaging webhook. */
142
+ conversation_id?: string;
143
+ /** Immutable. Source comparison cannot approve/fill/submit. Fill-only uses chat and can never submit. */
144
+ purpose?: CheckoutPurpose;
145
+ /** Defaults before_fill for compatibility. after_fill requires chat and private review delivery acknowledgement. */
146
+ approval_policy?: 'before_fill' | 'after_fill';
147
+ /** Declare v2 before OTP and require its true acknowledgement. Does not arm filling until requestApproval explicitly repeats true. */
148
+ fill_after_verification?: true;
149
+ /** Explicit chat/after_fill attachment. Omit to retain Accelerate-created
150
+ * private Kernel custody. Attachment never navigates or replaces the document. */
151
+ browser?: CustomerBrowserBinding;
152
+ /** Required for purchase (the default purpose): the customer's backend, not Accelerate, performs the final click.
153
+ * Purchase also requires chat, after_fill and a customer_backend_trusted browser. */
154
+ final_click_actor?: 'customer';
155
+ /** Your own provider read of `browser`, so Accelerate never needs your provider API key.
156
+ * Send it on every create. It is held in memory for this checkout and never stored. */
157
+ browser_resource?: ClientBrowserResource;
158
+ }
159
+ /** The fields of Browser Use `GET /api/v4/browsers/{id}` that Accelerate needs. */
160
+ export interface ClientBrowserResource {
161
+ id: string;
162
+ status: 'active';
163
+ cdpUrl: string;
164
+ startedAt: string;
165
+ timeoutAt: string;
166
+ recordingAvailable: false;
167
+ metadata: Record<string, string>;
168
+ extensions: [{ id: string; version: string; runtimeId: string }];
169
+ }
170
+ /** Hypothesis to inspect against the actual checkout; never a caller assertion of currency evidence. */
171
+ export interface QuoteVerificationRequest {
172
+ revision: number;
173
+ checkout_url: string;
174
+ expected_amount_cents: number;
175
+ expected_currency: 'USD';
176
+ checkout_total_selector: string;
177
+ }
178
+ /** Read-only verification of this revision. Does not authorize approval, CVV, filling or submission. */
179
+ export interface VerifiedCheckoutQuote {
180
+ checkout_id: CheckoutId;
181
+ revision: number;
182
+ verified: true;
183
+ quote: { checkout_origin: string; amount_cents: number; currency: 'USD' };
184
+ }
185
+ export interface ApprovalRequest {
186
+ checkout_url: string;
187
+ amount_cents: number;
188
+ currency: 'USD';
189
+ checkout_total_selector: string;
190
+ submit_selector: string;
191
+ /** Opt in explicitly; only chat/after_fill purchase or sandbox_fill_only.
192
+ * Successful private verification fills this exact revision without AI. */
193
+ fill_after_verification?: true;
194
+ }
195
+ /** BACKEND ONLY. The target and arm_id must never appear in MCP arguments, results, history or outbox. */
196
+ export interface ArmedFinalClick {
197
+ /** Opaque bounded string (20..16000 printable ASCII). Never display or log. */
198
+ arm_id: string;
199
+ /** Short window (service limit 15 seconds) in which the customer host may click once. */
200
+ armed_expires_at: string;
201
+ target: { checkout_url: string; submit_selector: string };
202
+ retry_safe: false;
203
+ payment_confirmed: false;
204
+ }
205
+ export interface AccelerateClientOptions {
206
+ baseUrl: string;
207
+ apiKey: string;
208
+ merchantId: string;
209
+ fetchImpl?: typeof fetch;
210
+ /** Test servers only. Production origins must use HTTPS. */
211
+ allowLoopbackForTests?: boolean;
212
+ }
213
+ export class AccelerateError extends Error {
214
+ readonly code: string;
215
+ readonly status: number;
216
+ readonly retrySafe: false;
217
+ readonly paymentConfirmed: false;
218
+ /** Fixed local guidance only; it never authorizes or performs a payment action. */
219
+ readonly nextAction: AccelerateNextAction;
220
+ /** Opaque support correlation only, never an authorization capability. */
221
+ readonly supportReference?: string;
222
+ constructor(code?: string, status?: number, supportReference?: string);
223
+ }
224
+ export class AccelerateClient {
225
+ /** Reads process.env by default. Requires API key and merchant ID; server only. */
226
+ static fromEnv(env?: AccelerateEnvironment): AccelerateClient;
227
+ constructor(options: AccelerateClientOptions);
228
+ /** Authenticated API/configuration check only; does not qualify downstream payment readiness. */
229
+ readonly connection: { check(options?: RequestOptions): Promise<AccelerateConnection> };
230
+ readonly checkouts: {
231
+ /** Pure inspection of an exact failed creation and authoritative provider/lease retirement; never creates, cancels, verifies or fills. */
232
+ inspectCreation(input: CreationInspectionRequest, options?: RequestOptions): Promise<CreationInspection>;
233
+ inspectRetirement(id: CheckoutId, input: RetirementInspectionRequest, options?: RequestOptions): Promise<RetirementInspection>;
234
+ create(input: CreateCheckout, options?: RequestOptions): Promise<CreatedCheckout>;
235
+ get(id: CheckoutId, options?: RequestOptions): Promise<Checkout>;
236
+ /** Stops on readiness/terminal states even if not requested; inspect returned status before continuing. */
237
+ waitFor(id: CheckoutId, input: WaitForCheckout, options?: RequestOptions): Promise<Checkout>;
238
+ browser(id: CheckoutId, input: BrowserOperation, options?: RequestOptions): Promise<BrowserResult>;
239
+ applyDetails(id: CheckoutId, input: { checkout_url: string }, options?: RequestOptions): Promise<Checkout>;
240
+ verifyQuote(id: CheckoutId, input: QuoteVerificationRequest, options?: RequestOptions): Promise<VerifiedCheckoutQuote>;
241
+ requestApproval(id: CheckoutId, input: ApprovalRequest, options?: RequestOptions): Promise<Checkout>;
242
+ fill(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<Checkout>;
243
+ /** Removed behavior: Accelerate never clicks. Always rejects with customer_click_required and sends no request. */
244
+ submit(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<never>;
245
+ /** Backend only: prepare one short-lived customer-owned final click. Accelerate sends no physical input. */
246
+ armFinalClick(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<ArmedFinalClick>;
247
+ /** Backend only: observe the customer's click once. Dispatch claims are not sent; never retry. */
248
+ completeFinalClick(id: CheckoutId, input: { revision: number; arm_id: string }, options?: RequestOptions): Promise<Checkout>;
249
+ cancel(id: CheckoutId, options?: RequestOptions): Promise<Checkout>;
250
+ /** Records one merchant result after submit. Repeating the same input returns the same result; different input is refused. */
251
+ reconcile(id: CheckoutId, input: CheckoutReconciliationRequest, options?: RequestOptions): Promise<ReconciledCheckout>;
252
+ };
253
+ }