@envarai/envarpay 0.2.0-alpha.5 → 0.2.0-alpha.7

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.
@@ -0,0 +1,37 @@
1
+ type SignalName = 'ledger_read' | 'payment_unknown' | 'execution_unknown' | 'recorded_provider_failure' | 'budget_near_limit' | 'budget_exhausted' | 'report_backlog' | 'report_failure';
2
+ type Signal = {
3
+ name: SignalName;
4
+ status: 'clear' | 'alert' | 'unavailable' | 'not_applicable';
5
+ severity: 'info' | 'warning' | 'critical';
6
+ count: number | null;
7
+ };
8
+ type Budget = {
9
+ unit: string;
10
+ maxTotal: string;
11
+ reserved: string;
12
+ spent: string;
13
+ remaining: string;
14
+ state: 'available' | 'near_limit' | 'exhausted';
15
+ };
16
+ export interface LocalAlertOptions {
17
+ policyPath?: string;
18
+ integrationPath?: string;
19
+ integrationDisabled?: boolean;
20
+ }
21
+ /** Owner-local aggregate evidence only. Never opens a runtime store, signer, vault or network. */
22
+ export declare function inspectLocalAlerts(path: string, options?: LocalAlertOptions): {
23
+ role: "unavailable" | "buyer" | "seller";
24
+ view: "alerts";
25
+ observation: "local_persisted_state_only";
26
+ status: string;
27
+ signals: Signal[];
28
+ budgets: Budget[];
29
+ alertCount: number;
30
+ unavailableCount: number;
31
+ providerHealth: "unavailable_not_probed";
32
+ recommendedExitCode: number;
33
+ paymentPerformed: boolean;
34
+ refundPerformed: boolean;
35
+ networkRequests: number;
36
+ };
37
+ export {};
@@ -0,0 +1,127 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { lstatSync, readFileSync } from 'node:fs';
3
+ import { loadBuyerPolicy } from './config.js';
4
+ const CURRENCIES = {
5
+ usd: 'usd_cents',
6
+ 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 'base_usdc_atomic',
7
+ 'eip155:84532/erc20:0x036cbd53842c5426634e7929541ec2318f3dcf7e': 'base_sepolia_usdc_atomic',
8
+ };
9
+ function privateFile(path, limit) {
10
+ const stat = lstatSync(path);
11
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()) || (limit !== undefined && stat.size > limit))
12
+ throw new Error('private_source_unavailable');
13
+ }
14
+ function database(path) {
15
+ privateFile(path);
16
+ const db = new DatabaseSync(path, { readOnly: true });
17
+ try {
18
+ db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN');
19
+ return db;
20
+ }
21
+ catch (error) {
22
+ db.close();
23
+ throw error;
24
+ }
25
+ }
26
+ const count = (db, sql) => Number(db.prepare(sql).get().n);
27
+ const signal = (name, value, severity = 'warning') => ({ name, status: value ? 'alert' : 'clear', severity: value ? severity : 'info', count: value });
28
+ const missing = (name) => ({ name, status: 'unavailable', severity: 'warning', count: null });
29
+ const irrelevant = (name) => ({ name, status: 'not_applicable', severity: 'info', count: null });
30
+ /** Owner-local aggregate evidence only. Never opens a runtime store, signer, vault or network. */
31
+ export function inspectLocalAlerts(path, options = {}) {
32
+ const signals = [], budgets = [];
33
+ let role = 'unavailable', db;
34
+ try {
35
+ db = database(path);
36
+ const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
37
+ if (tables.has('buyer_purchases') === tables.has('commerce_orders'))
38
+ throw new Error('unknown_ledger');
39
+ role = tables.has('buyer_purchases') ? 'buyer' : 'seller';
40
+ if (db.prepare(`SELECT version FROM ${role === 'buyer' ? 'buyer_meta' : 'commerce_meta'}`).get()?.version !== (role === 'buyer' ? 1 : 2))
41
+ throw new Error('unknown_version');
42
+ const unknownPayments = count(db, role === 'buyer' ? "SELECT COUNT(*) n FROM buyer_purchases WHERE json_extract(record_json,'$.paymentState')='unknown'" : "SELECT COUNT(*) n FROM commerce_orders WHERE payment_state='unknown'");
43
+ const unknownExecution = count(db, role === 'buyer' ? "SELECT COUNT(*) n FROM buyer_purchases WHERE json_extract(record_json,'$.executionState')='unknown'" : "SELECT COUNT(*) n FROM commerce_orders WHERE execution_state='unknown'");
44
+ // Only concrete persisted provider errors count. Generic unknown is not proof of provider failure.
45
+ const providerFailures = count(db, role === 'buyer'
46
+ ? "SELECT COUNT(*) n FROM buyer_purchases WHERE json_extract(record_json,'$.paymentState')!='confirmed' AND json_extract(record_json,'$.errorCode') IN ('stripe_transport_unknown','stripe_request_failed','stripe_rate_limit','stripe_response','stripe_response_size')"
47
+ : "SELECT COUNT(*) n FROM commerce_payment_attempts WHERE state!='confirmed' AND json_extract(receipt_json,'$.success')=0 AND length(json_extract(receipt_json,'$.errorReason'))>0");
48
+ signals.push(signal('ledger_read', 0), signal('payment_unknown', unknownPayments), signal('execution_unknown', unknownExecution), signal('recorded_provider_failure', providerFailures));
49
+ if (role === 'seller')
50
+ signals.push(irrelevant('budget_near_limit'), irrelevant('budget_exhausted'));
51
+ else {
52
+ try {
53
+ if (!options.policyPath)
54
+ throw new Error('policy_required');
55
+ privateFile(options.policyPath, 1024 * 1024);
56
+ const policy = loadBuyerPolicy(JSON.parse(readFileSync(options.policyPath, 'utf8')));
57
+ const usage = new Map();
58
+ for (const b of policy.budgets)
59
+ usage.set(b.currency, { reserved: 0n, spent: 0n });
60
+ for (const row of db.prepare("SELECT currency,amount,budget_state FROM buyer_purchases WHERE budget_state!='none'").iterate()) {
61
+ const currency = String(row.currency), amount = String(row.amount), state = row.budget_state;
62
+ const target = usage.get(currency);
63
+ if (!target || !/^[1-9][0-9]{0,77}$/.test(amount) || !['spent', 'reserved'].includes(String(state)))
64
+ throw new Error('budget_binding');
65
+ target[state === 'spent' ? 'spent' : 'reserved'] += BigInt(amount);
66
+ }
67
+ const checked = policy.budgets.map(b => {
68
+ const u = usage.get(b.currency), cap = BigInt(b.maxTotal), used = u.reserved + u.spent;
69
+ const state = used >= cap ? 'exhausted' : used * 10n >= cap * 9n ? 'near_limit' : 'available';
70
+ return { unit: CURRENCIES[b.currency], maxTotal: cap.toString(), reserved: u.reserved.toString(), spent: u.spent.toString(), remaining: (used < cap ? cap - used : 0n).toString(), state };
71
+ });
72
+ budgets.push(...checked);
73
+ signals.push(signal('budget_near_limit', checked.filter(b => b.state === 'near_limit').length), signal('budget_exhausted', checked.filter(b => b.state === 'exhausted').length, 'critical'));
74
+ }
75
+ catch {
76
+ signals.push(missing('budget_near_limit'), missing('budget_exhausted'));
77
+ }
78
+ }
79
+ }
80
+ catch {
81
+ role = 'unavailable';
82
+ signals.length = 0;
83
+ budgets.length = 0;
84
+ signals.push(...['ledger_read', 'payment_unknown', 'execution_unknown', 'recorded_provider_failure', 'budget_near_limit', 'budget_exhausted'].map(missing));
85
+ }
86
+ finally {
87
+ db?.close();
88
+ }
89
+ let integration;
90
+ try {
91
+ if (options.integrationDisabled && !options.integrationPath) {
92
+ signals.push(irrelevant('report_backlog'), irrelevant('report_failure'));
93
+ }
94
+ else {
95
+ if (options.integrationDisabled)
96
+ throw new Error('ambiguous_integration');
97
+ if (!options.integrationPath)
98
+ throw new Error('integration_not_supplied');
99
+ integration = database(options.integrationPath);
100
+ const binding = integration.prepare("SELECT value FROM meta WHERE key='binding'").get();
101
+ const source = integration.prepare("SELECT value FROM meta WHERE key='source_instance'").get();
102
+ if (!binding || !source || typeof binding.value !== 'string' || typeof source.value !== 'string')
103
+ throw new Error('integration_schema');
104
+ const parsed = JSON.parse(binding.value);
105
+ if (!parsed || typeof parsed.platformOrigin !== 'string' || typeof parsed.agentId !== 'string' || typeof parsed.runtimeAgentId !== 'string')
106
+ throw new Error('integration_schema');
107
+ const pending = count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending'");
108
+ const failed = count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending' AND attempts>0 AND last_error<>''");
109
+ signals.push(signal('report_backlog', pending), signal('report_failure', failed));
110
+ }
111
+ }
112
+ catch {
113
+ signals.push(missing('report_backlog'), missing('report_failure'));
114
+ }
115
+ finally {
116
+ integration?.close();
117
+ }
118
+ const alertCount = signals.filter(s => s.status === 'alert').length, unavailableCount = signals.filter(s => s.status === 'unavailable').length;
119
+ return {
120
+ role, view: 'alerts', observation: 'local_persisted_state_only',
121
+ status: role === 'unavailable' ? 'unavailable' : unavailableCount ? 'partial' : 'available',
122
+ signals, budgets, alertCount, unavailableCount,
123
+ providerHealth: 'unavailable_not_probed',
124
+ recommendedExitCode: unavailableCount ? 3 : alertCount ? 2 : 0,
125
+ paymentPerformed: false, refundPerformed: false, networkRequests: 0,
126
+ };
127
+ }
@@ -19,14 +19,14 @@ import { CredentialVault } from './vault.js';
19
19
  import { X402Gate } from './x402.js';
20
20
  import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
21
21
  import { CommerceError } from './types.js';
22
- import { inspectLedger } from './operations.js';
22
+ import { inspectLedger, inspectLocalAlerts } from './operations.js';
23
23
  const { values, positionals } = parseArgs({ allowPositionals: true, options: {
24
24
  config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
25
25
  version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
26
26
  port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { type: 'string' },
27
- after: { type: 'string' }, limit: { type: 'string' },
27
+ after: { type: 'string' }, limit: { type: 'string' }, 'refund-review': { type: 'boolean' }, alerts: { type: 'boolean' }, 'fail-on-alert': { type: 'boolean' }, 'integration-state': { type: 'string' }, 'no-integration': { type: 'boolean' },
28
28
  } });
29
- const usage = 'envarpay init --directory DIR | inspect --state DB [--after ROW --limit 100] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
29
+ const usage = 'envarpay init --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
30
30
  const closers = [];
31
31
  function privateFile(path) {
32
32
  const stat = lstatSync(path);
@@ -86,10 +86,21 @@ async function run() {
86
86
  return;
87
87
  }
88
88
  const command = positionals[0];
89
+ if (values.alerts || values['fail-on-alert'] || values['integration-state'] || values['no-integration']) {
90
+ if (command !== 'inspect' || !values.state || !values.alerts || values['refund-review'] || values.after || values.limit || (values['no-integration'] && values['integration-state']))
91
+ throw new CommerceError('inspection_view', 'Alert inspection needs inspect --state and --alerts, without pagination or refund review; choose at most one integration source');
92
+ const report = inspectLocalAlerts(values.state, { policyPath: values.config, integrationPath: values['integration-state'], integrationDisabled: values['no-integration'] });
93
+ console.log(JSON.stringify(report, null, 2));
94
+ if (values['fail-on-alert'])
95
+ process.exitCode = report.recommendedExitCode;
96
+ return;
97
+ }
89
98
  if (command === 'inspect' && values.state) {
90
- console.log(JSON.stringify(inspectLedger(values.state, Number(values.after ?? 0), Number(values.limit ?? 100)), null, 2));
99
+ console.log(JSON.stringify(inspectLedger(values.state, Number(values.after ?? 0), Number(values.limit ?? 100), values['refund-review'] ? 'refund-review' : 'attention'), null, 2));
91
100
  return;
92
101
  }
102
+ if (values['refund-review'])
103
+ throw new CommerceError('inspection_view', 'Refund review is available only for inspect');
93
104
  if ((values['envar-config'] || values['envar-credentials']) && command !== 'serve')
94
105
  throw new CommerceError('envar_role', 'Configuration synchronization is available on seller serve only');
95
106
  if (Boolean(values['envar-config']) !== Boolean(values['envar-credentials']))
@@ -1,9 +1,14 @@
1
+ export { inspectLocalAlerts, type LocalAlertOptions } from './alerts.js';
2
+ export type InspectionView = 'attention' | 'refund-review';
1
3
  /** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
2
- export declare function inspectLedger(path: string, after?: number, limit?: number): {
4
+ export declare function inspectLedger(path: string, after?: number, limit?: number, view?: InspectionView): {
3
5
  role: string;
6
+ view: InspectionView;
4
7
  total: number;
5
8
  attentionCount: number;
9
+ refundReviewCount: number;
6
10
  items: {
11
+ reason?: "confirmed_payment_failed_execution" | undefined;
7
12
  cursor: number;
8
13
  id: string;
9
14
  paymentState: string;
@@ -13,4 +18,5 @@ export declare function inspectLedger(path: string, after?: number, limit?: numb
13
18
  nextCursor: number;
14
19
  hasMore: boolean;
15
20
  paymentPerformed: boolean;
21
+ refundPerformed: boolean;
16
22
  };
@@ -1,13 +1,16 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import { lstatSync } from 'node:fs';
3
3
  import { CommerceError } from './types.js';
4
+ export { inspectLocalAlerts } from './alerts.js';
4
5
  /** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
5
- export function inspectLedger(path, after = 0, limit = 100) {
6
+ export function inspectLedger(path, after = 0, limit = 100, view = 'attention') {
6
7
  const stat = lstatSync(path);
7
8
  if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
8
9
  throw new CommerceError('private_file', 'Inspect an existing ledger owned only by the current user');
9
10
  if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
10
11
  throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit from1 to100');
12
+ if (!['attention', 'refund-review'].includes(view))
13
+ throw new CommerceError('inspection_view', 'Use attention or refund-review inspection');
11
14
  const db = new DatabaseSync(path, { readOnly: true });
12
15
  try {
13
16
  db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000');
@@ -20,12 +23,15 @@ export function inspectLedger(path, after = 0, limit = 100) {
20
23
  throw new CommerceError('store_version', 'Unsupported commerce ledger version');
21
24
  const table = role === 'buyer' ? 'buyer_purchases' : 'commerce_orders';
22
25
  const total = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
23
- const filter = role === 'buyer' ? "json_extract(record_json,'$.state') IN ('signing','submitted','unknown') OR json_extract(record_json,'$.paymentState') IN ('reserved','unknown') OR json_extract(record_json,'$.executionState')='unknown'" : "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
26
+ const attention = role === 'buyer' ? "json_extract(record_json,'$.state') IN ('signing','submitted','unknown') OR json_extract(record_json,'$.paymentState') IN ('reserved','unknown') OR json_extract(record_json,'$.executionState')='unknown'" : "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
27
+ const refundReview = role === 'buyer' ? "json_extract(record_json,'$.paymentState')='confirmed' AND json_extract(record_json,'$.executionState')='failed'" : "payment_state='confirmed' AND execution_state='failed'";
28
+ const filter = view === 'refund-review' ? refundReview : attention;
24
29
  const selected = role === 'buyer' ? "rowid AS cursor,id,json_extract(record_json,'$.paymentState') AS payment_state,json_extract(record_json,'$.executionState') AS execution_state,json_extract(record_json,'$.task.id') AS task_id" : "rowid AS cursor,id,payment_state,execution_state,task_id";
25
30
  const rows = db.prepare(`SELECT ${selected} FROM ${table} WHERE rowid>? AND (${filter}) ORDER BY rowid LIMIT ?`).all(after, limit + 1);
26
- const page = rows.slice(0, limit).map(row => ({ cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state), executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null }));
27
- const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${filter}`).get().n);
28
- return { role, total, attentionCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false };
31
+ const page = rows.slice(0, limit).map(row => ({ cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state), executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null, ...(view === 'refund-review' ? { reason: 'confirmed_payment_failed_execution' } : {}) }));
32
+ const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${attention}`).get().n);
33
+ const refundReviewCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${refundReview}`).get().n);
34
+ return { role, view, total, attentionCount, refundReviewCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false, refundPerformed: false };
29
35
  }
30
36
  finally {
31
37
  db.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envarai/envarpay",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.2.0-alpha.7",
4
4
  "description": "Independent A2A service commerce, budgeted buyers and x402/MPP payments",
5
5
  "type": "module",
6
6
  "license": "MIT",