@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.
- package/README.md +151 -2
- package/approved-extension-releases/0.0.0.1.json +41 -0
- package/approved-extension-releases/0.0.0.2.json +34 -0
- package/approved-extension.d.ts +24 -0
- package/approved-extension.js +90 -0
- package/browser-connector.d.ts +109 -0
- package/browser-connector.js +218 -0
- package/browser-operations.d.ts +56 -0
- package/browser-operations.js +144 -0
- package/browser-use-host.d.ts +46 -0
- package/browser-use-host.js +149 -0
- package/checkout-diagnostics.d.ts +34 -0
- package/checkout-diagnostics.js +81 -0
- package/cli.js +105 -0
- package/customer.env.example +6 -0
- package/host-adapter.d.ts +90 -0
- package/host-adapter.js +357 -0
- package/index.d.ts +253 -0
- package/index.js +504 -0
- package/linq-messaging.d.ts +45 -0
- package/linq-messaging.js +123 -0
- package/managed-mcp.d.ts +156 -0
- package/managed-mcp.js +427 -0
- package/package.json +114 -4
- package/partner-starter/.env.example +13 -0
- package/partner-starter/README.md +61 -0
- package/partner-starter/gitignore.template +5 -0
- package/partner-starter/package.json +21 -0
- package/partner-starter/runtime.mjs +33 -0
- package/partner-starter/server.mjs +13 -0
- package/partner-starter/smoke.mjs +61 -0
- package/partner-starter/synthetic.mjs +256 -0
- package/partner-starter/tests/starter.test.mjs +119 -0
- package/partner-starter/trusted-context.example.mjs +23 -0
- package/partner.d.ts +27 -0
- package/partner.js +53 -0
- package/postgres-host.d.ts +14 -0
- package/postgres-host.js +102 -0
- package/private-chat.d.ts +104 -0
- package/private-chat.js +316 -0
- package/private-ingress.d.ts +46 -0
- package/private-ingress.js +132 -0
- package/starter/.env.example +19 -0
- package/starter/README.md +82 -0
- package/starter/gitignore.template +5 -0
- package/starter/package.json +21 -0
- package/starter/runtime.mjs +66 -0
- package/starter/server.mjs +13 -0
- package/starter/smoke.mjs +55 -0
- package/starter/synthetic.mjs +163 -0
- package/starter/tests/starter.test.mjs +224 -0
- package/starter/trusted-context.example.mjs +49 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type CheckoutDiagnosticStage = 'not_started' | 'identity_required' | 'otp_required' | 'selection_required' |
|
|
2
|
+
'preparing_checkout' | 'approval_required' | 'cvv_required' | 'approved' | 'ready_for_submit' | 'review_delivery_required' | 'review_delivered' | 'approved_for_submit' | 'final_click_armed' |
|
|
3
|
+
'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
|
|
4
|
+
export type CheckoutDiagnosticAction = 'get_status' | 'continue_private_chat' | 'prepare_checkout' | 'apply_details' |
|
|
5
|
+
'request_approval' | 'fill' | 'submit' | 'cancel' | 'reconcile_order';
|
|
6
|
+
export type CheckoutCleanupStatus = 'not_required' | 'not_started' | 'pending' | 'confirmed';
|
|
7
|
+
export type CheckoutReconciliationState = 'confirmed' | 'declined' | 'not_found';
|
|
8
|
+
/** The client backend's statement of the merchant's result. Not Accelerate's proof: payment_confirmed stays false. */
|
|
9
|
+
export interface CheckoutReconciliation {
|
|
10
|
+
state: CheckoutReconciliationState;
|
|
11
|
+
/** Short label for where the client read the result, such as merchant_order_api. */
|
|
12
|
+
source: string;
|
|
13
|
+
amount_cents: number | null;
|
|
14
|
+
currency: 'USD' | null;
|
|
15
|
+
reconciled_at: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CheckoutDiagnostics {
|
|
18
|
+
version: 'v1';
|
|
19
|
+
stage: CheckoutDiagnosticStage;
|
|
20
|
+
/** Guidance only. Server authorization, revision, private consent and one-use checks remain mandatory. */
|
|
21
|
+
next_actions: CheckoutDiagnosticAction[];
|
|
22
|
+
support_reference: `acs_${string}`;
|
|
23
|
+
/** not_required means no retained browser reference. confirmed means worker closure acknowledgement; neither is independent provider proof. */
|
|
24
|
+
cleanup_status: CheckoutCleanupStatus;
|
|
25
|
+
submit_deadline?: string;
|
|
26
|
+
}
|
|
27
|
+
export const checkoutDiagnosticStages: readonly CheckoutDiagnosticStage[];
|
|
28
|
+
export const checkoutDiagnosticActions: readonly CheckoutDiagnosticAction[];
|
|
29
|
+
export const checkoutCleanupStatuses: readonly CheckoutCleanupStatus[];
|
|
30
|
+
export const checkoutDiagnosticsJsonSchema: Readonly<Record<string, unknown>>;
|
|
31
|
+
export function validateCheckoutDiagnostics(value: unknown, checkout?: {
|
|
32
|
+
status: string; purpose?: string; submit_allowed?: boolean; submit_expires_at?: string; approval_policy?: string; purchase_approved?: boolean; reconciliation?: unknown; final_click_actor?: string;
|
|
33
|
+
}): CheckoutDiagnostics;
|
|
34
|
+
export function validateCheckoutReconciliation(value: unknown, status?: string): CheckoutReconciliation;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Safe lifecycle metadata only. No transport, credentials, provider text or automatic actions.
|
|
2
|
+
export const checkoutDiagnosticStages = Object.freeze(['not_started', 'identity_required', 'otp_required', 'selection_required',
|
|
3
|
+
'preparing_checkout', 'approval_required', 'cvv_required', 'approved', 'ready_for_submit', 'review_delivery_required', 'review_delivered', 'approved_for_submit', 'final_click_armed',
|
|
4
|
+
'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
5
|
+
export const checkoutDiagnosticActions = Object.freeze(['get_status', 'continue_private_chat', 'prepare_checkout', 'apply_details',
|
|
6
|
+
'request_approval', 'fill', 'submit', 'cancel', 'reconcile_order']);
|
|
7
|
+
export const checkoutCleanupStatuses = Object.freeze(['not_required', 'not_started', 'pending', 'confirmed']);
|
|
8
|
+
const freeze = (value) => { if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
9
|
+
for (const item of Object.values(value))
|
|
10
|
+
freeze(item);
|
|
11
|
+
Object.freeze(value);
|
|
12
|
+
} return value; };
|
|
13
|
+
export const checkoutDiagnosticsJsonSchema = freeze({ type: 'object', additionalProperties: false,
|
|
14
|
+
required: ['version', 'stage', 'next_actions', 'support_reference', 'cleanup_status'], properties: {
|
|
15
|
+
version: { const: 'v1' }, stage: { type: 'string', enum: checkoutDiagnosticStages },
|
|
16
|
+
next_actions: { type: 'array', minItems: 1, maxItems: checkoutDiagnosticActions.length, uniqueItems: true,
|
|
17
|
+
items: { type: 'string', enum: checkoutDiagnosticActions } },
|
|
18
|
+
support_reference: { type: 'string', pattern: '^acs_[a-f0-9]{32}$' },
|
|
19
|
+
cleanup_status: { type: 'string', enum: checkoutCleanupStatuses }, submit_deadline: { type: 'string', format: 'date-time' },
|
|
20
|
+
} });
|
|
21
|
+
const terminal = new Set(['cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
|
|
22
|
+
const timestamp = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(value) && Number.isFinite(Date.parse(value));
|
|
23
|
+
const check = (value) => { if (!value) {
|
|
24
|
+
const error = Object.assign(Error('Invalid Accelerate checkout diagnostics.'), { code: 'invalid_response' });
|
|
25
|
+
throw error;
|
|
26
|
+
} };
|
|
27
|
+
/** Validate fixed metadata; optional checkout binding prevents contradictory action guidance. Never authorizes an action. */
|
|
28
|
+
export function validateCheckoutDiagnostics(input, checkout) {
|
|
29
|
+
// Validation below checks every property before returning this value.
|
|
30
|
+
const value = input;
|
|
31
|
+
check(value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
|
32
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
|
|
33
|
+
Object.keys(value).every(key => Object.hasOwn(checkoutDiagnosticsJsonSchema.properties, key)) &&
|
|
34
|
+
checkoutDiagnosticsJsonSchema.required.every(key => Object.hasOwn(value, key)) && value.version === 'v1' &&
|
|
35
|
+
checkoutDiagnosticStages.includes(value.stage) && Array.isArray(value.next_actions) && value.next_actions.length >= 1 &&
|
|
36
|
+
value.next_actions.length <= checkoutDiagnosticActions.length && new Set(value.next_actions).size === value.next_actions.length &&
|
|
37
|
+
value.next_actions.every(action => checkoutDiagnosticActions.includes(action)) &&
|
|
38
|
+
typeof value.support_reference === 'string' && /^acs_[a-f0-9]{32}$/.test(value.support_reference) &&
|
|
39
|
+
checkoutCleanupStatuses.includes(value.cleanup_status) && (value.submit_deadline === undefined || timestamp(value.submit_deadline)));
|
|
40
|
+
if (checkout !== undefined) {
|
|
41
|
+
const { status, purpose = 'purchase', submit_allowed, approval_policy, purchase_approved, reconciliation, final_click_actor } = checkout;
|
|
42
|
+
check(terminal.has(status) ? value.stage === status : status === 'wallet_required'
|
|
43
|
+
? ['not_started', 'identity_required', 'otp_required', 'selection_required'].includes(value.stage)
|
|
44
|
+
: ['selection_ready', 'details_ready'].includes(status) ? value.stage === 'preparing_checkout'
|
|
45
|
+
: status === 'approval_required' ? ['approval_required', 'cvv_required'].includes(value.stage) : approval_policy === 'after_fill' && status === 'ready_for_submit' ? ['review_delivery_required', 'review_delivered', 'approval_required', 'approved_for_submit'].includes(value.stage) : value.stage === status);
|
|
46
|
+
check(status === 'ready_for_submit' ? timestamp(value.submit_deadline) && value.submit_deadline === checkout.submit_expires_at : value.submit_deadline === undefined);
|
|
47
|
+
for (const action of value.next_actions) {
|
|
48
|
+
if (action === 'submit')
|
|
49
|
+
check(status === 'ready_for_submit' && purpose === 'purchase' && submit_allowed !== false && final_click_actor === 'customer' && (approval_policy !== 'after_fill' || purchase_approved === true));
|
|
50
|
+
if (action === 'fill')
|
|
51
|
+
check(status === 'approved' && purpose !== 'sandbox_source_comparison');
|
|
52
|
+
if (['prepare_checkout', 'apply_details'].includes(action))
|
|
53
|
+
check(['selection_ready', 'details_ready'].includes(status));
|
|
54
|
+
if (action === 'request_approval')
|
|
55
|
+
check(status === 'details_ready');
|
|
56
|
+
if (action === 'continue_private_chat')
|
|
57
|
+
check(['wallet_required', 'approval_required'].includes(status) || approval_policy === 'after_fill' && status === 'ready_for_submit' && !purchase_approved);
|
|
58
|
+
if (action === 'reconcile_order')
|
|
59
|
+
check(['submit_attempted', 'outcome_unknown'].includes(status) && reconciliation === undefined);
|
|
60
|
+
}
|
|
61
|
+
// While a customer click is armed only a safe read or cancel is advertised: no browser work, no re-arm.
|
|
62
|
+
if (status === 'final_click_armed')
|
|
63
|
+
check(value.next_actions.every(action => ['get_status', 'cancel'].includes(action)));
|
|
64
|
+
if (!terminal.has(status))
|
|
65
|
+
check(value.cleanup_status !== 'pending' && value.cleanup_status !== 'confirmed');
|
|
66
|
+
if (terminal.has(status))
|
|
67
|
+
check(value.cleanup_status !== 'not_started');
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
/** Validate the client backend's stored statement of the merchant's result. Never proof of payment. */
|
|
72
|
+
export function validateCheckoutReconciliation(input, status) {
|
|
73
|
+
const value = input;
|
|
74
|
+
check(value !== null && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
|
|
75
|
+
Object.keys(value).length === 5 && ['confirmed', 'declined', 'not_found'].includes(value.state) &&
|
|
76
|
+
typeof value.source === 'string' && /^[a-z][a-z0-9_.-]{0,39}$/.test(value.source) && !/(?:\d[ -]?){13,19}/.test(value.source) &&
|
|
77
|
+
(value.amount_cents === null || Number.isSafeInteger(value.amount_cents) && value.amount_cents > 0 && value.amount_cents <= 2147483647) &&
|
|
78
|
+
(value.currency === null || value.currency === 'USD') && (value.state !== 'confirmed' || value.amount_cents !== null && value.currency === 'USD') &&
|
|
79
|
+
timestamp(value.reconciled_at) && (status === undefined || ['submit_attempted', 'outcome_unknown'].includes(status)));
|
|
80
|
+
return value;
|
|
81
|
+
}
|
package/cli.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { AccelerateClient, AccelerateError } from './index.js';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, open, readdir, realpath, readFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const help = `Usage: accelerate-agent doctor
|
|
9
|
+
accelerate-agent init <empty-directory> [--browser existing]
|
|
10
|
+
accelerate-agent --version
|
|
11
|
+
|
|
12
|
+
Set ACCELERATE_API_KEY and ACCELERATE_MERCHANT_ID on your server.
|
|
13
|
+
ACCELERATE_BASE_URL is optional for Accelerate's hosted service.
|
|
14
|
+
|
|
15
|
+
Doctor checks API connectivity and the credential pair. It does not create a
|
|
16
|
+
checkout, send a code, release credentials, fill a form or submit an order.
|
|
17
|
+
It does not qualify the downstream payment source or browser configuration.
|
|
18
|
+
|
|
19
|
+
Init copies the packaged synthetic MCP starter without credentials, network
|
|
20
|
+
requests or overwrites. --browser existing selects the new provider-independent
|
|
21
|
+
existing-browser starter with purchase purpose and a submit tool; omission
|
|
22
|
+
preserves the managed Kernel starter, which is fill-only. Both start in
|
|
23
|
+
synthetic mode. Purchase requires private approval, an explicit submit and your
|
|
24
|
+
own browser host performing the one final click: Accelerate never clicks.
|
|
25
|
+
Existing symlink paths and nonempty targets are rejected.
|
|
26
|
+
`;
|
|
27
|
+
const starterFiles = ['package.json', '.env.example', '.gitignore', 'README.md', 'server.mjs', 'runtime.mjs',
|
|
28
|
+
'synthetic.mjs', 'smoke.mjs', 'trusted-context.example.mjs', 'tests/starter.test.mjs'];
|
|
29
|
+
const initFailure = code => { const error = Error('Starter initialization unavailable.'); error.code = code; throw error; };
|
|
30
|
+
async function initializeStarter(directory, existingBrowser = false) {
|
|
31
|
+
if (typeof directory !== 'string' || !directory || directory.length > 2048 || /[\u0000-\u001f\u007f]/u.test(directory)) initFailure('init_target_invalid');
|
|
32
|
+
const target = resolve(directory), parent = dirname(target);
|
|
33
|
+
if (await realpath(parent) !== parent || !(await lstat(parent)).isDirectory()) initFailure('init_target_invalid');
|
|
34
|
+
try {
|
|
35
|
+
const stat = await lstat(target);
|
|
36
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(target) !== target) initFailure('init_target_invalid');
|
|
37
|
+
if ((await readdir(target)).length !== 0) initFailure('init_target_not_empty');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
40
|
+
await mkdir(target, { mode: 0o700 });
|
|
41
|
+
}
|
|
42
|
+
const sourceRoot = resolve(fileURLToPath(new URL(existingBrowser ? './partner-starter/' : './starter/', import.meta.url)));
|
|
43
|
+
if (await realpath(sourceRoot) !== sourceRoot) initFailure('init_failed');
|
|
44
|
+
const original = await lstat(target); let total = 0;
|
|
45
|
+
for (const name of starterFiles) {
|
|
46
|
+
const current = await lstat(target);
|
|
47
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== original.dev || current.ino !== original.ino ||
|
|
48
|
+
await realpath(target) !== target) initFailure('init_target_invalid');
|
|
49
|
+
// npm strips .gitignore from archives; emit it from this explicit template.
|
|
50
|
+
const sourcePath = join(sourceRoot, name === '.gitignore' ? 'gitignore.template' : name);
|
|
51
|
+
if (await realpath(sourcePath) !== sourcePath) initFailure('init_failed');
|
|
52
|
+
const source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
const stat = await source.stat();
|
|
56
|
+
if (!stat.isFile() || stat.size < 1 || stat.size > 65536 || (total += stat.size) > 262144) initFailure('init_failed');
|
|
57
|
+
data = await source.readFile();
|
|
58
|
+
} finally { await source.close(); }
|
|
59
|
+
const destination = join(target, name), folder = dirname(destination);
|
|
60
|
+
if (folder !== target) {
|
|
61
|
+
try { await mkdir(folder, { mode: 0o700 }); } catch (error) { if (error?.code !== 'EEXIST') throw error; }
|
|
62
|
+
if (!(await lstat(folder)).isDirectory() || await realpath(folder) !== folder) initFailure('init_target_invalid');
|
|
63
|
+
}
|
|
64
|
+
const output = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
65
|
+
try { await output.writeFile(data); } finally { await output.close(); }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const args = process.argv.slice(2);
|
|
69
|
+
if (args.length === 1 && ['--version', '-v'].includes(args[0])) {
|
|
70
|
+
const { version } = JSON.parse(await readFile(new URL('./package.json', import.meta.url), 'utf8'));
|
|
71
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) throw Error('Invalid SDK package version.');
|
|
72
|
+
process.stdout.write(version + '\n');
|
|
73
|
+
} else if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
74
|
+
process.stdout.write(help);
|
|
75
|
+
} else if (args[0] === 'init' && (args.length === 2 || args.length === 4 && args[2] === '--browser' && args[3] === 'existing')) {
|
|
76
|
+
try {
|
|
77
|
+
await initializeStarter(args[1], args.length === 4);
|
|
78
|
+
process.stdout.write(JSON.stringify({ ok: true, status: 'initialized', mode: 'synthetic', next_action: 'install_dependencies' }) + '\n');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const code = ['init_target_invalid', 'init_target_not_empty'].includes(error?.code) ? error.code : 'init_failed';
|
|
81
|
+
process.stdout.write(JSON.stringify({ ok: false, code }) + '\n'); process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
} else if (args.length !== 1 || args[0] !== 'doctor') {
|
|
84
|
+
process.stderr.write(help);
|
|
85
|
+
process.exitCode = 2;
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
const result = await AccelerateClient.fromEnv().connection.check();
|
|
89
|
+
// Do not print environment values, merchant identifiers or upstream bodies.
|
|
90
|
+
process.stdout.write(JSON.stringify({ ok: true, status: result.status, api_version: result.api_version,
|
|
91
|
+
interaction_modes: result.interaction_modes, currencies: result.currencies,
|
|
92
|
+
downstream_payment_readiness: 'not_checked' }) + '\n');
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const codes = new Set(['missing_api_key', 'missing_merchant_id', 'invalid_api_key', 'invalid_merchant_id',
|
|
95
|
+
'invalid_base_url', 'invalid_input', 'tenant_unauthorized', 'unauthorized', 'permission_denied',
|
|
96
|
+
'configuration_error', 'configuration_invalid', 'route_not_found', 'invalid_response',
|
|
97
|
+
'request_timeout', 'request_cancelled', 'request_failed', 'upstream_unavailable']);
|
|
98
|
+
const actions = new Set(['check_configuration', 'check_request', 'check_status', 'stop']);
|
|
99
|
+
const code = error instanceof AccelerateError && codes.has(error.code) ? error.code : 'request_failed';
|
|
100
|
+
const nextAction = error instanceof AccelerateError && actions.has(error.nextAction)
|
|
101
|
+
? error.nextAction : 'check_configuration';
|
|
102
|
+
process.stdout.write(JSON.stringify({ ok: false, code, next_action: nextAction }) + '\n');
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Checkout, CheckoutId, AccelerateClient } from './index.js';
|
|
2
|
+
import type { ManagedCheckoutContext, ManagedCheckoutEvent, ManagedCheckoutToolsOptions } from './managed-mcp.js';
|
|
3
|
+
import type { PrivateChatState, PrivateChatPrompt, PrivateChatProgress } from './private-chat.js';
|
|
4
|
+
|
|
5
|
+
export type HostOwner = Pick<ManagedCheckoutContext, 'customer_id' | 'workspace_id' | 'conversation_id'>;
|
|
6
|
+
export interface HostCheckoutAttempt {
|
|
7
|
+
idempotency_key: string;
|
|
8
|
+
checkout_id?: CheckoutId;
|
|
9
|
+
phase: 'creating' | 'active' | 'uncertain' | 'closed';
|
|
10
|
+
/** Sticky across successful status/cancel; requires separate operator reconciliation. */
|
|
11
|
+
uncertain: boolean;
|
|
12
|
+
/** Durable one-use fill/submit admissions survive a lost final commit ACK. */
|
|
13
|
+
/** Independent provider cleanup proof, separate from service ACK. */
|
|
14
|
+
cleanup_confirmed?: boolean;
|
|
15
|
+
effects?: Array<'accelerate_checkout_fill' | 'accelerate_checkout_submit'>;
|
|
16
|
+
pending?: { operation: string; message_id?: string; revision?: number };
|
|
17
|
+
private_stage?: PrivateChatState['stage'];
|
|
18
|
+
checkout?: Checkout;
|
|
19
|
+
}
|
|
20
|
+
export type HostOutboxItem = {
|
|
21
|
+
id: string;
|
|
22
|
+
delivered: boolean;
|
|
23
|
+
} & ({ kind: 'checkout_event'; payload: ManagedCheckoutEvent } |
|
|
24
|
+
{ kind: 'private_prompt'; payload: PrivateChatPrompt & { checkout_id: CheckoutId } } |
|
|
25
|
+
{ kind: 'safe_progress'; payload: PrivateChatProgress & { checkout_id: CheckoutId } });
|
|
26
|
+
/** No OTP, CVV, PAN, image bytes, browser connection credentials or private reply
|
|
27
|
+
* text may enter this record. Encrypt at rest according to host policy. */
|
|
28
|
+
export interface HostCheckoutRecord {
|
|
29
|
+
version: 'v1';
|
|
30
|
+
owner: HostOwner;
|
|
31
|
+
/** Authenticated original phone binding hash; never the raw phone number. */
|
|
32
|
+
phone_binding?: string;
|
|
33
|
+
attempt?: HostCheckoutAttempt;
|
|
34
|
+
history: Array<{ idempotency_key: string; checkout_id: CheckoutId; status: 'cancelled' | 'expired' | 'failed'; uncertain: false }>;
|
|
35
|
+
outbox: HostOutboxItem[];
|
|
36
|
+
/** Native-send receipts only, never image bytes/URLs or reply text. A pending
|
|
37
|
+
* send without receipt requires provider reconciliation, not another upload. */
|
|
38
|
+
deliveries?: Array<{ idempotency_key: string; receipt?: { message_id: string; sent_at: string } }>;
|
|
39
|
+
}
|
|
40
|
+
export interface HostPersistenceAccess {
|
|
41
|
+
load(): Promise<HostCheckoutRecord | undefined>;
|
|
42
|
+
/** Resolve only after a DURABLE independent commit. This write MUST survive a
|
|
43
|
+
* later callback failure; an enclosing rollback transaction is incompatible. */
|
|
44
|
+
commit(record: HostCheckoutRecord): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export interface HostPersistence {
|
|
47
|
+
/** Hold a cross-process owner lock and call work exactly once. No automatic
|
|
48
|
+
* transaction/callback retries. Lost locks/commit acknowledgements must reject.
|
|
49
|
+
* A local Map/mutex does not implement this production durability contract. */
|
|
50
|
+
withLock<T>(ownerKey: string, work: (access: HostPersistenceAccess) => Promise<T>): Promise<T>;
|
|
51
|
+
}
|
|
52
|
+
export interface PrivateHostOperation {
|
|
53
|
+
operation: 'private_start' | 'private_reply' | 'private_state' | 'private_cancel' | 'review_delivery';
|
|
54
|
+
/** Original authenticated provider ID; required for recoverable reply replay. */
|
|
55
|
+
message_id?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface OwnedPrivateCheckout extends ManagedCheckoutContext {
|
|
58
|
+
checkout_id: CheckoutId;
|
|
59
|
+
previous_stage?: PrivateChatState['stage'];
|
|
60
|
+
checkout?: Readonly<Checkout>;
|
|
61
|
+
/** Validates and commits only the safe state/progress/outgoing prompt. */
|
|
62
|
+
savePrivateState(state: PrivateChatState, options: { messageId: string; suppressPrompt?: boolean }): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export interface DurableCheckoutHost<Extra = unknown> {
|
|
65
|
+
resolveContext: ManagedCheckoutToolsOptions<Extra>['resolveContext'];
|
|
66
|
+
withCheckoutCreation: ManagedCheckoutToolsOptions<Extra>['withCheckoutCreation'];
|
|
67
|
+
withOwnedCheckout: ManagedCheckoutToolsOptions<Extra>['withOwnedCheckout'];
|
|
68
|
+
dispatchCheckoutEvent: ManagedCheckoutToolsOptions<Extra>['dispatchCheckoutEvent'];
|
|
69
|
+
/** Durable actual native-send receipt cache; lost SDK ACK can reuse receipt
|
|
70
|
+
* without uploading/sending the same review again. */
|
|
71
|
+
withPrivateDelivery(context: ManagedCheckoutContext, idempotencyKey: string,
|
|
72
|
+
deliver: () => Promise<{ message_id: string; sent_at: string }>): Promise<Readonly<{ message_id: string; sent_at: string }>>;
|
|
73
|
+
/** Call ONLY after connector independently verifies stopped/cleared and retained fence. */
|
|
74
|
+
confirmBrowserCleanup(context: ManagedCheckoutContext, checkoutId: CheckoutId): Promise<void>;
|
|
75
|
+
withPrivateCheckout<T>(context: ManagedCheckoutContext, metadata: PrivateHostOperation,
|
|
76
|
+
work: (checkout: Readonly<OwnedPrivateCheckout>) => Promise<T>): Promise<T | undefined>;
|
|
77
|
+
/** Safe deliveries only. Recipient/queue must durably deduplicate item.id.
|
|
78
|
+
* Failure leaves the same pending delivery; never repeat fill or submit here. */
|
|
79
|
+
drainOutbox(context: ManagedCheckoutContext,
|
|
80
|
+
deliver: (item: Readonly<HostOutboxItem>, context: Readonly<ManagedCheckoutContext>) => Promise<void>): Promise<number>;
|
|
81
|
+
}
|
|
82
|
+
/** Strict runtime validation only; caller must actually authenticate upstream. */
|
|
83
|
+
export function authenticatedCheckoutContext(value: unknown): Readonly<ManagedCheckoutContext>;
|
|
84
|
+
/** Hash-only key scopes one active payment fence to customer+workspace. Exact
|
|
85
|
+
* authenticated conversation is additionally checked under the owner lock. */
|
|
86
|
+
export function checkoutHostOwnerKey(context: ManagedCheckoutContext): string;
|
|
87
|
+
export function createDurableCheckoutHost<Extra = unknown>(options: {
|
|
88
|
+
persistence: HostPersistence;
|
|
89
|
+
resolveContext: ManagedCheckoutToolsOptions<Extra>['resolveContext'];
|
|
90
|
+
}): Readonly<DurableCheckoutHost<Extra>>;
|