paybridge 0.12.0 → 1.0.0-rc.2
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 +70 -0
- package/dist/audit-report.d.ts +102 -0
- package/dist/audit-report.js +663 -0
- package/dist/cli/commands/audit.d.ts +1 -0
- package/dist/cli/commands/audit.js +308 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/utils.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,6 +101,76 @@ Exit code 1 if drift detected, 0 if clean. Perfect for CI/CD pipelines or cron j
|
|
|
101
101
|
|
|
102
102
|
The Square `/checkout/payment-links → /online-checkout/payment-links` endpoint change would have shipped silently to production. With `drift-check` running daily, you get a Slack alert the moment it happens.
|
|
103
103
|
|
|
104
|
+
## Audit Reports
|
|
105
|
+
|
|
106
|
+
Your payment stack generates signals across silos: drift detection in one CLI command, reconciliation in another, success rates in third-party dashboards, latency buried in logs. **Audit reports** unify every observability feature into one comprehensive artifact.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Generate HTML report (default)
|
|
110
|
+
npx paybridge audit --window 30d --ledger-pg postgresql://localhost/db
|
|
111
|
+
|
|
112
|
+
# JSON for CI/CD pipelines
|
|
113
|
+
npx paybridge audit --format json --output - | jq '.summary.anomalyCounts.high' | \
|
|
114
|
+
xargs -I {} test {} -eq 0 || exit 1
|
|
115
|
+
|
|
116
|
+
# Markdown for email
|
|
117
|
+
npx paybridge audit --format md --output - | mail -s "Monthly Audit" finance@example.com
|
|
118
|
+
|
|
119
|
+
# Include reconciliation data
|
|
120
|
+
npx paybridge audit --reconcile-input expected.jsonl --window 7d
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### What's Included
|
|
124
|
+
|
|
125
|
+
- **Success rate analytics** — per-provider success/failure/timeout counts with half-window comparison (detects degradation)
|
|
126
|
+
- **Latency metrics** — avg + p95 per provider, with anomaly detection (>2s/5s/10s thresholds)
|
|
127
|
+
- **Fee estimation** — calculated from actual transaction volume × provider fee structure
|
|
128
|
+
- **Drift events** — timeline of baseline captures per provider
|
|
129
|
+
- **Reconciliation** — missed webhook count + mismatch table (if `--reconcile-input` provided)
|
|
130
|
+
- **Anomaly detection** — success rate drops (>10% decline), consecutive failures (3+), high latency, PII in raw responses
|
|
131
|
+
- **Compliance flags** — scans metadata for PII leakage (email, card_number, iban, etc.)
|
|
132
|
+
|
|
133
|
+
### Output Formats
|
|
134
|
+
|
|
135
|
+
- **HTML** — dark mode, print-to-PDF friendly (Cmd/Ctrl+P), collapsible per-provider deep-dive. The artifact a CFO opens in a browser.
|
|
136
|
+
- **Markdown** — copy-paste into Notion/Confluence/Slack. Clean GFM tables.
|
|
137
|
+
- **JSON** — machine-readable. Pipe to CI/CD gates, monitoring dashboards, or alerting systems.
|
|
138
|
+
|
|
139
|
+
### Exit Codes
|
|
140
|
+
|
|
141
|
+
- `0` — no high-severity anomalies
|
|
142
|
+
- `1` — high-severity anomalies detected (fails CI builds)
|
|
143
|
+
|
|
144
|
+
### Cron Integration
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Daily 6 AM audit
|
|
148
|
+
0 6 * * * cd /app && npx paybridge audit --window 7d --ledger-pg $DB_URL --output /var/reports/audit-$(date +\%Y-\%m-\%d).html
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Programmatic API
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { generateAuditReport, renderAuditAsHtml } from 'paybridge';
|
|
155
|
+
import { createPostgresLedgerStore } from 'paybridge';
|
|
156
|
+
import { Pool } from 'pg';
|
|
157
|
+
|
|
158
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
159
|
+
const ledger = createPostgresLedgerStore({ pool });
|
|
160
|
+
|
|
161
|
+
const report = await generateAuditReport({
|
|
162
|
+
providers: [
|
|
163
|
+
{ name: 'stripe', capabilities: stripeProvider.getCapabilities() },
|
|
164
|
+
{ name: 'paystack', capabilities: paystackProvider.getCapabilities() },
|
|
165
|
+
],
|
|
166
|
+
ledger,
|
|
167
|
+
windowMs: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const html = renderAuditAsHtml(report);
|
|
171
|
+
await sendEmail({ to: 'finance@company.com', html });
|
|
172
|
+
```
|
|
173
|
+
|
|
104
174
|
## Reconciliation
|
|
105
175
|
|
|
106
176
|
Webhooks can fail. Networks blip. Your server hiccups. Provider retries don't reach you. Without reconciliation, you discover missed webhooks when a customer complains their account wasn't credited.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { LedgerStore } from './ledger';
|
|
2
|
+
import type { DriftStore } from './cli/drift-store';
|
|
3
|
+
import type { ProviderCapabilities } from './routing-types';
|
|
4
|
+
import type { ReconcileResult } from './cli/reconcile-types';
|
|
5
|
+
export interface AuditInput {
|
|
6
|
+
providers: Array<{
|
|
7
|
+
name: string;
|
|
8
|
+
capabilities: ProviderCapabilities;
|
|
9
|
+
}>;
|
|
10
|
+
windowMs?: number;
|
|
11
|
+
ledger?: LedgerStore;
|
|
12
|
+
driftStore?: DriftStore;
|
|
13
|
+
reconcileResults?: ReconcileResult[];
|
|
14
|
+
generatedAt?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ProviderAuditSection {
|
|
17
|
+
name: string;
|
|
18
|
+
region?: string;
|
|
19
|
+
totalAttempts: number;
|
|
20
|
+
successRate: number | null;
|
|
21
|
+
avgLatencyMs: number | null;
|
|
22
|
+
p95LatencyMs: number | null;
|
|
23
|
+
failureCount: number;
|
|
24
|
+
rateLimitedCount: number;
|
|
25
|
+
timeoutCount: number;
|
|
26
|
+
estimatedFeesPaid: number;
|
|
27
|
+
estimatedFeeCurrency: string;
|
|
28
|
+
driftEvents: Array<{
|
|
29
|
+
at: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
}>;
|
|
32
|
+
anomalies: AuditAnomaly[];
|
|
33
|
+
}
|
|
34
|
+
export type AuditAnomaly = {
|
|
35
|
+
type: 'success_rate_drop';
|
|
36
|
+
severity: 'low' | 'medium' | 'high';
|
|
37
|
+
description: string;
|
|
38
|
+
previousRate: number;
|
|
39
|
+
currentRate: number;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'drift_detected';
|
|
42
|
+
severity: 'medium';
|
|
43
|
+
description: string;
|
|
44
|
+
detectedAt: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'consecutive_failures';
|
|
47
|
+
severity: 'high';
|
|
48
|
+
description: string;
|
|
49
|
+
count: number;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'high_latency';
|
|
52
|
+
severity: 'low' | 'medium' | 'high';
|
|
53
|
+
description: string;
|
|
54
|
+
p95Ms: number;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'pii_in_raw';
|
|
57
|
+
severity: 'high';
|
|
58
|
+
description: string;
|
|
59
|
+
field: string;
|
|
60
|
+
};
|
|
61
|
+
export interface AuditReport {
|
|
62
|
+
generatedAt: string;
|
|
63
|
+
windowMs: number;
|
|
64
|
+
windowStart: string;
|
|
65
|
+
windowEnd: string;
|
|
66
|
+
summary: {
|
|
67
|
+
totalProviders: number;
|
|
68
|
+
totalAttempts: number;
|
|
69
|
+
overallSuccessRate: number | null;
|
|
70
|
+
totalEstimatedFees: number;
|
|
71
|
+
totalEstimatedFeesCurrency: string;
|
|
72
|
+
anomalyCounts: {
|
|
73
|
+
high: number;
|
|
74
|
+
medium: number;
|
|
75
|
+
low: number;
|
|
76
|
+
};
|
|
77
|
+
missedWebhookCount: number;
|
|
78
|
+
};
|
|
79
|
+
providers: ProviderAuditSection[];
|
|
80
|
+
reconciliation?: {
|
|
81
|
+
total: number;
|
|
82
|
+
match: number;
|
|
83
|
+
mismatch: number;
|
|
84
|
+
notFound: number;
|
|
85
|
+
error: number;
|
|
86
|
+
mismatches: Array<{
|
|
87
|
+
provider: string;
|
|
88
|
+
reference: string;
|
|
89
|
+
expected: string;
|
|
90
|
+
actual: string;
|
|
91
|
+
}>;
|
|
92
|
+
};
|
|
93
|
+
complianceFlags: Array<{
|
|
94
|
+
provider: string;
|
|
95
|
+
finding: string;
|
|
96
|
+
severity: 'high' | 'medium' | 'low';
|
|
97
|
+
}>;
|
|
98
|
+
}
|
|
99
|
+
export declare function generateAuditReport(input: AuditInput): Promise<AuditReport>;
|
|
100
|
+
export declare function renderAuditAsHtml(report: AuditReport): string;
|
|
101
|
+
export declare function renderAuditAsMarkdown(report: AuditReport): string;
|
|
102
|
+
export declare function renderAuditAsJson(report: AuditReport, pretty?: boolean): string;
|
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateAuditReport = generateAuditReport;
|
|
4
|
+
exports.renderAuditAsHtml = renderAuditAsHtml;
|
|
5
|
+
exports.renderAuditAsMarkdown = renderAuditAsMarkdown;
|
|
6
|
+
exports.renderAuditAsJson = renderAuditAsJson;
|
|
7
|
+
const PII_KEYS = ['email', 'card_number', 'iban', 'phone', 'id_number', 'ssn', 'passport', 'cvv', 'tax_id'];
|
|
8
|
+
async function generateAuditReport(input) {
|
|
9
|
+
const windowMs = input.windowMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
11
|
+
const windowEnd = generatedAt;
|
|
12
|
+
const windowStart = new Date(new Date(generatedAt).getTime() - windowMs).toISOString();
|
|
13
|
+
const providerSections = [];
|
|
14
|
+
const complianceFlags = [];
|
|
15
|
+
let totalAttempts = 0;
|
|
16
|
+
let totalSuccess = 0;
|
|
17
|
+
let totalFees = 0;
|
|
18
|
+
let feeCurrencies = new Set();
|
|
19
|
+
for (const providerMeta of input.providers) {
|
|
20
|
+
const { name, capabilities } = providerMeta;
|
|
21
|
+
let entries = [];
|
|
22
|
+
if (input.ledger) {
|
|
23
|
+
entries = await input.ledger.query({
|
|
24
|
+
provider: name,
|
|
25
|
+
fromTimestamp: windowStart,
|
|
26
|
+
toTimestamp: windowEnd,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const totalProviderAttempts = entries.length;
|
|
30
|
+
const successCount = entries.filter((e) => e.status === 'success').length;
|
|
31
|
+
const failureCount = entries.filter((e) => e.status === 'failed').length;
|
|
32
|
+
const rateLimitedCount = entries.filter((e) => e.status === 'rate_limited').length;
|
|
33
|
+
const timeoutCount = entries.filter((e) => e.status === 'timeout').length;
|
|
34
|
+
const successRate = totalProviderAttempts > 0 ? successCount / totalProviderAttempts : null;
|
|
35
|
+
const durations = entries
|
|
36
|
+
.filter((e) => e.durationMs !== undefined && e.durationMs !== null)
|
|
37
|
+
.map((e) => e.durationMs);
|
|
38
|
+
const avgLatencyMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : null;
|
|
39
|
+
const p95LatencyMs = calculateP95(durations);
|
|
40
|
+
const successfulEntries = entries.filter((e) => e.status === 'success' && e.amount !== undefined);
|
|
41
|
+
let estimatedFeesPaid = 0;
|
|
42
|
+
for (const entry of successfulEntries) {
|
|
43
|
+
const amount = entry.amount;
|
|
44
|
+
const fee = capabilities.fees.fixed + (amount * capabilities.fees.percent) / 100;
|
|
45
|
+
estimatedFeesPaid += fee;
|
|
46
|
+
}
|
|
47
|
+
feeCurrencies.add(capabilities.fees.currency);
|
|
48
|
+
totalFees += estimatedFeesPaid;
|
|
49
|
+
const driftEvents = [];
|
|
50
|
+
if (input.driftStore) {
|
|
51
|
+
const baseline = await input.driftStore.load(name);
|
|
52
|
+
if (baseline && baseline.shape.capturedAt) {
|
|
53
|
+
const capturedTime = new Date(baseline.shape.capturedAt).getTime();
|
|
54
|
+
const windowStartTime = new Date(windowStart).getTime();
|
|
55
|
+
if (capturedTime >= windowStartTime) {
|
|
56
|
+
driftEvents.push({
|
|
57
|
+
at: baseline.shape.capturedAt,
|
|
58
|
+
summary: `Baseline snapshot captured for ${baseline.operation}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const anomalies = [];
|
|
64
|
+
if (totalProviderAttempts > 0) {
|
|
65
|
+
const halfWindow = windowMs / 2;
|
|
66
|
+
const midpoint = new Date(new Date(windowStart).getTime() + halfWindow).toISOString();
|
|
67
|
+
const previousEntries = entries.filter((e) => e.timestamp < midpoint);
|
|
68
|
+
const currentEntries = entries.filter((e) => e.timestamp >= midpoint);
|
|
69
|
+
if (previousEntries.length >= 20 && currentEntries.length >= 20) {
|
|
70
|
+
const previousSuccessRate = previousEntries.filter((e) => e.status === 'success').length / previousEntries.length;
|
|
71
|
+
const currentSuccessRate = currentEntries.filter((e) => e.status === 'success').length / currentEntries.length;
|
|
72
|
+
const drop = previousSuccessRate - currentSuccessRate;
|
|
73
|
+
if (drop >= 0.30) {
|
|
74
|
+
anomalies.push({
|
|
75
|
+
type: 'success_rate_drop',
|
|
76
|
+
severity: 'high',
|
|
77
|
+
description: `Success rate dropped by ${(drop * 100).toFixed(1)}% in recent window`,
|
|
78
|
+
previousRate: previousSuccessRate,
|
|
79
|
+
currentRate: currentSuccessRate,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (drop >= 0.20) {
|
|
83
|
+
anomalies.push({
|
|
84
|
+
type: 'success_rate_drop',
|
|
85
|
+
severity: 'medium',
|
|
86
|
+
description: `Success rate dropped by ${(drop * 100).toFixed(1)}% in recent window`,
|
|
87
|
+
previousRate: previousSuccessRate,
|
|
88
|
+
currentRate: currentSuccessRate,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (drop >= 0.10) {
|
|
92
|
+
anomalies.push({
|
|
93
|
+
type: 'success_rate_drop',
|
|
94
|
+
severity: 'low',
|
|
95
|
+
description: `Success rate dropped by ${(drop * 100).toFixed(1)}% in recent window`,
|
|
96
|
+
previousRate: previousSuccessRate,
|
|
97
|
+
currentRate: currentSuccessRate,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const consecutiveFailures = detectConsecutiveFailures(entries);
|
|
102
|
+
if (consecutiveFailures >= 3) {
|
|
103
|
+
anomalies.push({
|
|
104
|
+
type: 'consecutive_failures',
|
|
105
|
+
severity: 'high',
|
|
106
|
+
description: `${consecutiveFailures} consecutive failures detected`,
|
|
107
|
+
count: consecutiveFailures,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (p95LatencyMs !== null) {
|
|
111
|
+
if (p95LatencyMs > 10000) {
|
|
112
|
+
anomalies.push({
|
|
113
|
+
type: 'high_latency',
|
|
114
|
+
severity: 'high',
|
|
115
|
+
description: `p95 latency ${p95LatencyMs.toFixed(0)}ms exceeds 10s threshold`,
|
|
116
|
+
p95Ms: p95LatencyMs,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (p95LatencyMs > 5000) {
|
|
120
|
+
anomalies.push({
|
|
121
|
+
type: 'high_latency',
|
|
122
|
+
severity: 'medium',
|
|
123
|
+
description: `p95 latency ${p95LatencyMs.toFixed(0)}ms exceeds 5s threshold`,
|
|
124
|
+
p95Ms: p95LatencyMs,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else if (p95LatencyMs > 2000) {
|
|
128
|
+
anomalies.push({
|
|
129
|
+
type: 'high_latency',
|
|
130
|
+
severity: 'low',
|
|
131
|
+
description: `p95 latency ${p95LatencyMs.toFixed(0)}ms exceeds 2s threshold`,
|
|
132
|
+
p95Ms: p95LatencyMs,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const sampleEntries = entries.slice(0, Math.min(10, entries.length));
|
|
137
|
+
for (const entry of sampleEntries) {
|
|
138
|
+
if (entry.metadata && typeof entry.metadata === 'object') {
|
|
139
|
+
const raw = entry.metadata.raw;
|
|
140
|
+
if (raw && typeof raw === 'object') {
|
|
141
|
+
for (const key of PII_KEYS) {
|
|
142
|
+
if (hasKey(raw, key)) {
|
|
143
|
+
anomalies.push({
|
|
144
|
+
type: 'pii_in_raw',
|
|
145
|
+
severity: 'high',
|
|
146
|
+
description: `PII field '${key}' found in raw response metadata`,
|
|
147
|
+
field: key,
|
|
148
|
+
});
|
|
149
|
+
complianceFlags.push({
|
|
150
|
+
provider: name,
|
|
151
|
+
finding: `PII field '${key}' found in raw response metadata`,
|
|
152
|
+
severity: 'high',
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
totalAttempts += totalProviderAttempts;
|
|
162
|
+
totalSuccess += successCount;
|
|
163
|
+
providerSections.push({
|
|
164
|
+
name,
|
|
165
|
+
region: capabilities.country,
|
|
166
|
+
totalAttempts: totalProviderAttempts,
|
|
167
|
+
successRate,
|
|
168
|
+
avgLatencyMs,
|
|
169
|
+
p95LatencyMs,
|
|
170
|
+
failureCount,
|
|
171
|
+
rateLimitedCount,
|
|
172
|
+
timeoutCount,
|
|
173
|
+
estimatedFeesPaid,
|
|
174
|
+
estimatedFeeCurrency: capabilities.fees.currency,
|
|
175
|
+
driftEvents,
|
|
176
|
+
anomalies,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const overallSuccessRate = totalAttempts > 0 ? totalSuccess / totalAttempts : null;
|
|
180
|
+
const anomalyCounts = { high: 0, medium: 0, low: 0 };
|
|
181
|
+
for (const section of providerSections) {
|
|
182
|
+
for (const anomaly of section.anomalies) {
|
|
183
|
+
anomalyCounts[anomaly.severity]++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let reconciliation;
|
|
187
|
+
let missedWebhookCount = 0;
|
|
188
|
+
if (input.reconcileResults) {
|
|
189
|
+
const mismatches = input.reconcileResults
|
|
190
|
+
.filter((r) => r.classification === 'mismatch')
|
|
191
|
+
.map((r) => ({
|
|
192
|
+
provider: r.provider,
|
|
193
|
+
reference: r.reference,
|
|
194
|
+
expected: r.expectedStatus,
|
|
195
|
+
actual: r.actualStatus ?? 'unknown',
|
|
196
|
+
}));
|
|
197
|
+
missedWebhookCount = mismatches.length;
|
|
198
|
+
reconciliation = {
|
|
199
|
+
total: input.reconcileResults.length,
|
|
200
|
+
match: input.reconcileResults.filter((r) => r.classification === 'match').length,
|
|
201
|
+
mismatch: input.reconcileResults.filter((r) => r.classification === 'mismatch').length,
|
|
202
|
+
notFound: input.reconcileResults.filter((r) => r.classification === 'not-found').length,
|
|
203
|
+
error: input.reconcileResults.filter((r) => r.classification === 'error').length,
|
|
204
|
+
mismatches,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
generatedAt,
|
|
209
|
+
windowMs,
|
|
210
|
+
windowStart,
|
|
211
|
+
windowEnd,
|
|
212
|
+
summary: {
|
|
213
|
+
totalProviders: input.providers.length,
|
|
214
|
+
totalAttempts,
|
|
215
|
+
overallSuccessRate,
|
|
216
|
+
totalEstimatedFees: totalFees,
|
|
217
|
+
totalEstimatedFeesCurrency: feeCurrencies.size === 1 ? Array.from(feeCurrencies)[0] : 'mixed',
|
|
218
|
+
anomalyCounts,
|
|
219
|
+
missedWebhookCount,
|
|
220
|
+
},
|
|
221
|
+
providers: providerSections,
|
|
222
|
+
reconciliation,
|
|
223
|
+
complianceFlags,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function calculateP95(values) {
|
|
227
|
+
if (values.length === 0)
|
|
228
|
+
return null;
|
|
229
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
230
|
+
const index = Math.ceil(sorted.length * 0.95) - 1;
|
|
231
|
+
return sorted[Math.max(0, index)];
|
|
232
|
+
}
|
|
233
|
+
function detectConsecutiveFailures(entries) {
|
|
234
|
+
let maxConsecutive = 0;
|
|
235
|
+
let currentConsecutive = 0;
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
if (entry.status === 'failed' || entry.status === 'timeout' || entry.status === 'rate_limited') {
|
|
238
|
+
currentConsecutive++;
|
|
239
|
+
maxConsecutive = Math.max(maxConsecutive, currentConsecutive);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
currentConsecutive = 0;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return maxConsecutive;
|
|
246
|
+
}
|
|
247
|
+
function hasKey(obj, key) {
|
|
248
|
+
if (!obj || typeof obj !== 'object')
|
|
249
|
+
return false;
|
|
250
|
+
if (key in obj)
|
|
251
|
+
return true;
|
|
252
|
+
for (const k of Object.keys(obj)) {
|
|
253
|
+
if (typeof obj[k] === 'object' && hasKey(obj[k], key)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
function renderAuditAsHtml(report) {
|
|
260
|
+
const { summary, providers, reconciliation, complianceFlags } = report;
|
|
261
|
+
const severityColor = (severity) => {
|
|
262
|
+
return severity === 'high' ? '#ff4444' : severity === 'medium' ? '#ffaa00' : '#ffdd00';
|
|
263
|
+
};
|
|
264
|
+
const anomalyRows = providers.flatMap((p) => p.anomalies.map((a) => ({ provider: p.name, anomaly: a }))).sort((a, b) => {
|
|
265
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
266
|
+
return order[a.anomaly.severity] - order[b.anomaly.severity];
|
|
267
|
+
});
|
|
268
|
+
return `<!DOCTYPE html>
|
|
269
|
+
<html lang="en">
|
|
270
|
+
<head>
|
|
271
|
+
<meta charset="UTF-8">
|
|
272
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
273
|
+
<title>PayBridge Audit Report</title>
|
|
274
|
+
<style>
|
|
275
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
276
|
+
body {
|
|
277
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
278
|
+
background: #0d1117;
|
|
279
|
+
color: #c9d1d9;
|
|
280
|
+
padding: 2rem;
|
|
281
|
+
line-height: 1.6;
|
|
282
|
+
}
|
|
283
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
284
|
+
header {
|
|
285
|
+
background: linear-gradient(135deg, #161b22 0%, #1c2128 100%);
|
|
286
|
+
padding: 2rem;
|
|
287
|
+
border-radius: 12px;
|
|
288
|
+
margin-bottom: 2rem;
|
|
289
|
+
border: 1px solid #30363d;
|
|
290
|
+
position: relative;
|
|
291
|
+
}
|
|
292
|
+
header::before {
|
|
293
|
+
content: 'AUDIT REPORT — INTERNAL USE ONLY';
|
|
294
|
+
position: absolute;
|
|
295
|
+
top: 1rem;
|
|
296
|
+
right: 2rem;
|
|
297
|
+
font-size: 0.7rem;
|
|
298
|
+
color: #484f58;
|
|
299
|
+
letter-spacing: 1px;
|
|
300
|
+
}
|
|
301
|
+
h1 { color: #58a6ff; font-size: 2rem; margin-bottom: 0.5rem; }
|
|
302
|
+
.meta { color: #8b949e; font-size: 0.9rem; }
|
|
303
|
+
.summary {
|
|
304
|
+
display: grid;
|
|
305
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
306
|
+
gap: 1rem;
|
|
307
|
+
margin-bottom: 2rem;
|
|
308
|
+
}
|
|
309
|
+
.card {
|
|
310
|
+
background: #161b22;
|
|
311
|
+
border: 1px solid #30363d;
|
|
312
|
+
border-radius: 8px;
|
|
313
|
+
padding: 1.5rem;
|
|
314
|
+
transition: transform 0.2s;
|
|
315
|
+
}
|
|
316
|
+
.card:hover { transform: translateY(-2px); border-color: #58a6ff; }
|
|
317
|
+
.card-label { color: #8b949e; font-size: 0.85rem; margin-bottom: 0.5rem; }
|
|
318
|
+
.card-value { color: #58a6ff; font-size: 2rem; font-weight: bold; }
|
|
319
|
+
.card-unit { color: #8b949e; font-size: 1rem; }
|
|
320
|
+
table {
|
|
321
|
+
width: 100%;
|
|
322
|
+
border-collapse: collapse;
|
|
323
|
+
background: #161b22;
|
|
324
|
+
border-radius: 8px;
|
|
325
|
+
overflow: hidden;
|
|
326
|
+
margin-bottom: 2rem;
|
|
327
|
+
}
|
|
328
|
+
th {
|
|
329
|
+
background: #21262d;
|
|
330
|
+
color: #c9d1d9;
|
|
331
|
+
text-align: left;
|
|
332
|
+
padding: 1rem;
|
|
333
|
+
font-weight: 600;
|
|
334
|
+
border-bottom: 2px solid #30363d;
|
|
335
|
+
}
|
|
336
|
+
td {
|
|
337
|
+
padding: 0.75rem 1rem;
|
|
338
|
+
border-bottom: 1px solid #21262d;
|
|
339
|
+
}
|
|
340
|
+
tr:last-child td { border-bottom: none; }
|
|
341
|
+
.section { margin-bottom: 3rem; }
|
|
342
|
+
.section-title {
|
|
343
|
+
color: #58a6ff;
|
|
344
|
+
font-size: 1.5rem;
|
|
345
|
+
margin-bottom: 1rem;
|
|
346
|
+
padding-bottom: 0.5rem;
|
|
347
|
+
border-bottom: 2px solid #30363d;
|
|
348
|
+
}
|
|
349
|
+
.badge {
|
|
350
|
+
display: inline-block;
|
|
351
|
+
padding: 0.25rem 0.75rem;
|
|
352
|
+
border-radius: 12px;
|
|
353
|
+
font-size: 0.8rem;
|
|
354
|
+
font-weight: 600;
|
|
355
|
+
}
|
|
356
|
+
.badge-high { background: #ff4444; color: white; }
|
|
357
|
+
.badge-medium { background: #ffaa00; color: white; }
|
|
358
|
+
.badge-low { background: #ffdd00; color: #0d1117; }
|
|
359
|
+
details {
|
|
360
|
+
background: #161b22;
|
|
361
|
+
border: 1px solid #30363d;
|
|
362
|
+
border-radius: 8px;
|
|
363
|
+
padding: 1rem;
|
|
364
|
+
margin-bottom: 1rem;
|
|
365
|
+
}
|
|
366
|
+
summary {
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
color: #58a6ff;
|
|
370
|
+
user-select: none;
|
|
371
|
+
}
|
|
372
|
+
summary:hover { text-decoration: underline; }
|
|
373
|
+
.no-data { color: #8b949e; font-style: italic; }
|
|
374
|
+
footer {
|
|
375
|
+
text-align: center;
|
|
376
|
+
color: #484f58;
|
|
377
|
+
margin-top: 3rem;
|
|
378
|
+
padding-top: 2rem;
|
|
379
|
+
border-top: 1px solid #21262d;
|
|
380
|
+
font-size: 0.9rem;
|
|
381
|
+
}
|
|
382
|
+
@media print {
|
|
383
|
+
body { background: white; color: black; }
|
|
384
|
+
.card, table, details { background: white; border-color: #ddd; }
|
|
385
|
+
th { background: #f5f5f5; }
|
|
386
|
+
.badge-high { background: #ffcccc; color: #cc0000; }
|
|
387
|
+
.badge-medium { background: #ffe6cc; color: #cc6600; }
|
|
388
|
+
.badge-low { background: #ffffcc; color: #666600; }
|
|
389
|
+
header::before { color: #ccc; }
|
|
390
|
+
}
|
|
391
|
+
</style>
|
|
392
|
+
</head>
|
|
393
|
+
<body>
|
|
394
|
+
<div class="container">
|
|
395
|
+
<header>
|
|
396
|
+
<h1>PayBridge Audit Report</h1>
|
|
397
|
+
<div class="meta">
|
|
398
|
+
<div>Generated: ${new Date(report.generatedAt).toLocaleString()}</div>
|
|
399
|
+
<div>Window: ${formatDuration(report.windowMs)} (${report.windowStart.split('T')[0]} to ${report.windowEnd.split('T')[0]})</div>
|
|
400
|
+
</div>
|
|
401
|
+
</header>
|
|
402
|
+
|
|
403
|
+
<div class="summary">
|
|
404
|
+
<div class="card">
|
|
405
|
+
<div class="card-label">Total Providers</div>
|
|
406
|
+
<div class="card-value">${summary.totalProviders}</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="card">
|
|
409
|
+
<div class="card-label">Total Attempts</div>
|
|
410
|
+
<div class="card-value">${summary.totalAttempts.toLocaleString()}</div>
|
|
411
|
+
</div>
|
|
412
|
+
<div class="card">
|
|
413
|
+
<div class="card-label">Success Rate</div>
|
|
414
|
+
<div class="card-value">${summary.overallSuccessRate !== null ? (summary.overallSuccessRate * 100).toFixed(1) : '—'}<span class="card-unit">%</span></div>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="card">
|
|
417
|
+
<div class="card-label">Estimated Fees</div>
|
|
418
|
+
<div class="card-value">${summary.totalEstimatedFees.toFixed(2)} <span class="card-unit">${summary.totalEstimatedFeesCurrency}</span></div>
|
|
419
|
+
</div>
|
|
420
|
+
<div class="card">
|
|
421
|
+
<div class="card-label">Anomalies</div>
|
|
422
|
+
<div class="card-value">
|
|
423
|
+
<span style="color: #ff4444">${summary.anomalyCounts.high}</span> /
|
|
424
|
+
<span style="color: #ffaa00">${summary.anomalyCounts.medium}</span> /
|
|
425
|
+
<span style="color: #ffdd00">${summary.anomalyCounts.low}</span>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
<div class="card">
|
|
429
|
+
<div class="card-label">Missed Webhooks</div>
|
|
430
|
+
<div class="card-value" style="color: ${summary.missedWebhookCount > 0 ? '#ff4444' : '#3fb950'}">
|
|
431
|
+
${summary.missedWebhookCount}
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<div class="section">
|
|
437
|
+
<h2 class="section-title">Provider Overview</h2>
|
|
438
|
+
<table>
|
|
439
|
+
<thead>
|
|
440
|
+
<tr>
|
|
441
|
+
<th>Provider</th>
|
|
442
|
+
<th>Region</th>
|
|
443
|
+
<th>Attempts</th>
|
|
444
|
+
<th>Success Rate</th>
|
|
445
|
+
<th>Avg Latency</th>
|
|
446
|
+
<th>p95 Latency</th>
|
|
447
|
+
<th>Fees Paid</th>
|
|
448
|
+
<th>Anomalies</th>
|
|
449
|
+
</tr>
|
|
450
|
+
</thead>
|
|
451
|
+
<tbody>
|
|
452
|
+
${providers.map((p) => `
|
|
453
|
+
<tr>
|
|
454
|
+
<td><strong>${p.name}</strong></td>
|
|
455
|
+
<td>${p.region ?? '—'}</td>
|
|
456
|
+
<td>${p.totalAttempts.toLocaleString()}</td>
|
|
457
|
+
<td>${p.successRate !== null ? (p.successRate * 100).toFixed(1) + '%' : '—'}</td>
|
|
458
|
+
<td>${p.avgLatencyMs !== null ? p.avgLatencyMs.toFixed(0) + 'ms' : '—'}</td>
|
|
459
|
+
<td>${p.p95LatencyMs !== null ? p.p95LatencyMs.toFixed(0) + 'ms' : '—'}</td>
|
|
460
|
+
<td>${p.estimatedFeesPaid.toFixed(2)} ${p.estimatedFeeCurrency}</td>
|
|
461
|
+
<td>${p.anomalies.length > 0 ? p.anomalies.length : '—'}</td>
|
|
462
|
+
</tr>
|
|
463
|
+
`).join('')}
|
|
464
|
+
</tbody>
|
|
465
|
+
</table>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
${anomalyRows.length > 0 ? `
|
|
469
|
+
<div class="section">
|
|
470
|
+
<h2 class="section-title">Anomalies</h2>
|
|
471
|
+
<table>
|
|
472
|
+
<thead>
|
|
473
|
+
<tr>
|
|
474
|
+
<th>Severity</th>
|
|
475
|
+
<th>Provider</th>
|
|
476
|
+
<th>Type</th>
|
|
477
|
+
<th>Description</th>
|
|
478
|
+
</tr>
|
|
479
|
+
</thead>
|
|
480
|
+
<tbody>
|
|
481
|
+
${anomalyRows.map(({ provider, anomaly }) => `
|
|
482
|
+
<tr>
|
|
483
|
+
<td><span class="badge badge-${anomaly.severity}">${anomaly.severity.toUpperCase()}</span></td>
|
|
484
|
+
<td>${provider}</td>
|
|
485
|
+
<td>${anomaly.type}</td>
|
|
486
|
+
<td>${anomaly.description}</td>
|
|
487
|
+
</tr>
|
|
488
|
+
`).join('')}
|
|
489
|
+
</tbody>
|
|
490
|
+
</table>
|
|
491
|
+
</div>
|
|
492
|
+
` : ''}
|
|
493
|
+
|
|
494
|
+
${reconciliation ? `
|
|
495
|
+
<div class="section">
|
|
496
|
+
<h2 class="section-title">Reconciliation</h2>
|
|
497
|
+
<div class="summary">
|
|
498
|
+
<div class="card">
|
|
499
|
+
<div class="card-label">Match</div>
|
|
500
|
+
<div class="card-value" style="color: #3fb950">${reconciliation.match}</div>
|
|
501
|
+
</div>
|
|
502
|
+
<div class="card">
|
|
503
|
+
<div class="card-label">Mismatch</div>
|
|
504
|
+
<div class="card-value" style="color: #ff4444">${reconciliation.mismatch}</div>
|
|
505
|
+
</div>
|
|
506
|
+
<div class="card">
|
|
507
|
+
<div class="card-label">Not Found</div>
|
|
508
|
+
<div class="card-value" style="color: #ffaa00">${reconciliation.notFound}</div>
|
|
509
|
+
</div>
|
|
510
|
+
<div class="card">
|
|
511
|
+
<div class="card-label">Error</div>
|
|
512
|
+
<div class="card-value" style="color: #ff4444">${reconciliation.error}</div>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
${reconciliation.mismatches.length > 0 ? `
|
|
516
|
+
<table>
|
|
517
|
+
<thead>
|
|
518
|
+
<tr>
|
|
519
|
+
<th>Provider</th>
|
|
520
|
+
<th>Reference</th>
|
|
521
|
+
<th>Expected</th>
|
|
522
|
+
<th>Actual</th>
|
|
523
|
+
</tr>
|
|
524
|
+
</thead>
|
|
525
|
+
<tbody>
|
|
526
|
+
${reconciliation.mismatches.map((m) => `
|
|
527
|
+
<tr>
|
|
528
|
+
<td>${m.provider}</td>
|
|
529
|
+
<td><code>${m.reference}</code></td>
|
|
530
|
+
<td>${m.expected}</td>
|
|
531
|
+
<td>${m.actual}</td>
|
|
532
|
+
</tr>
|
|
533
|
+
`).join('')}
|
|
534
|
+
</tbody>
|
|
535
|
+
</table>
|
|
536
|
+
` : '<p class="no-data">No mismatches detected.</p>'}
|
|
537
|
+
</div>
|
|
538
|
+
` : ''}
|
|
539
|
+
|
|
540
|
+
${complianceFlags.length > 0 ? `
|
|
541
|
+
<div class="section">
|
|
542
|
+
<h2 class="section-title">Compliance Flags</h2>
|
|
543
|
+
<table>
|
|
544
|
+
<thead>
|
|
545
|
+
<tr>
|
|
546
|
+
<th>Severity</th>
|
|
547
|
+
<th>Provider</th>
|
|
548
|
+
<th>Finding</th>
|
|
549
|
+
</tr>
|
|
550
|
+
</thead>
|
|
551
|
+
<tbody>
|
|
552
|
+
${complianceFlags.map((f) => `
|
|
553
|
+
<tr>
|
|
554
|
+
<td><span class="badge badge-${f.severity}">${f.severity.toUpperCase()}</span></td>
|
|
555
|
+
<td>${f.provider}</td>
|
|
556
|
+
<td>${f.finding}</td>
|
|
557
|
+
</tr>
|
|
558
|
+
`).join('')}
|
|
559
|
+
</tbody>
|
|
560
|
+
</table>
|
|
561
|
+
</div>
|
|
562
|
+
` : ''}
|
|
563
|
+
|
|
564
|
+
<div class="section">
|
|
565
|
+
<h2 class="section-title">Per-Provider Details</h2>
|
|
566
|
+
${providers.map((p) => `
|
|
567
|
+
<details>
|
|
568
|
+
<summary>${p.name} — ${p.totalAttempts} attempts, ${p.successRate !== null ? (p.successRate * 100).toFixed(1) + '%' : 'N/A'} success</summary>
|
|
569
|
+
<div style="margin-top: 1rem;">
|
|
570
|
+
<p><strong>Failures:</strong> ${p.failureCount} (Rate Limited: ${p.rateLimitedCount}, Timeout: ${p.timeoutCount})</p>
|
|
571
|
+
<p><strong>Latency:</strong> Avg ${p.avgLatencyMs !== null ? p.avgLatencyMs.toFixed(0) : 'N/A'}ms, p95 ${p.p95LatencyMs !== null ? p.p95LatencyMs.toFixed(0) : 'N/A'}ms</p>
|
|
572
|
+
<p><strong>Drift Events:</strong> ${p.driftEvents.length > 0 ? p.driftEvents.map((d) => `${d.at} - ${d.summary}`).join(', ') : 'None'}</p>
|
|
573
|
+
<p><strong>Anomalies:</strong> ${p.anomalies.length > 0 ? p.anomalies.map((a) => a.description).join('; ') : 'None'}</p>
|
|
574
|
+
</div>
|
|
575
|
+
</details>
|
|
576
|
+
`).join('')}
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<footer>
|
|
580
|
+
Generated by <strong>paybridge audit</strong><br>
|
|
581
|
+
Source: <a href="https://github.com/kobie3717/paybridge" style="color: #58a6ff;">github.com/kobie3717/paybridge</a>
|
|
582
|
+
</footer>
|
|
583
|
+
</div>
|
|
584
|
+
</body>
|
|
585
|
+
</html>`;
|
|
586
|
+
}
|
|
587
|
+
function renderAuditAsMarkdown(report) {
|
|
588
|
+
const { summary, providers, reconciliation, complianceFlags } = report;
|
|
589
|
+
let md = `# PayBridge Audit Report\n\n`;
|
|
590
|
+
md += `**Generated:** ${new Date(report.generatedAt).toLocaleString()}\n`;
|
|
591
|
+
md += `**Window:** ${formatDuration(report.windowMs)} (${report.windowStart.split('T')[0]} to ${report.windowEnd.split('T')[0]})\n\n`;
|
|
592
|
+
md += `## Executive Summary\n\n`;
|
|
593
|
+
md += `| Metric | Value |\n`;
|
|
594
|
+
md += `|--------|-------|\n`;
|
|
595
|
+
md += `| Total Providers | ${summary.totalProviders} |\n`;
|
|
596
|
+
md += `| Total Attempts | ${summary.totalAttempts.toLocaleString()} |\n`;
|
|
597
|
+
md += `| Success Rate | ${summary.overallSuccessRate !== null ? (summary.overallSuccessRate * 100).toFixed(1) + '%' : 'N/A'} |\n`;
|
|
598
|
+
md += `| Estimated Fees | ${summary.totalEstimatedFees.toFixed(2)} ${summary.totalEstimatedFeesCurrency} |\n`;
|
|
599
|
+
md += `| Anomalies (H/M/L) | ${summary.anomalyCounts.high} / ${summary.anomalyCounts.medium} / ${summary.anomalyCounts.low} |\n`;
|
|
600
|
+
md += `| Missed Webhooks | ${summary.missedWebhookCount} |\n\n`;
|
|
601
|
+
md += `## Provider Overview\n\n`;
|
|
602
|
+
md += `| Provider | Region | Attempts | Success Rate | Avg Latency | p95 Latency | Fees Paid | Anomalies |\n`;
|
|
603
|
+
md += `|----------|--------|----------|--------------|-------------|-------------|-----------|----------|\n`;
|
|
604
|
+
for (const p of providers) {
|
|
605
|
+
md += `| ${p.name} | ${p.region ?? 'N/A'} | ${p.totalAttempts} | ${p.successRate !== null ? (p.successRate * 100).toFixed(1) + '%' : 'N/A'} | `;
|
|
606
|
+
md += `${p.avgLatencyMs !== null ? p.avgLatencyMs.toFixed(0) + 'ms' : 'N/A'} | ${p.p95LatencyMs !== null ? p.p95LatencyMs.toFixed(0) + 'ms' : 'N/A'} | `;
|
|
607
|
+
md += `${p.estimatedFeesPaid.toFixed(2)} ${p.estimatedFeeCurrency} | ${p.anomalies.length || 'None'} |\n`;
|
|
608
|
+
}
|
|
609
|
+
md += `\n`;
|
|
610
|
+
const anomalyRows = providers.flatMap((p) => p.anomalies.map((a) => ({ provider: p.name, anomaly: a }))).sort((a, b) => {
|
|
611
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
612
|
+
return order[a.anomaly.severity] - order[b.anomaly.severity];
|
|
613
|
+
});
|
|
614
|
+
if (anomalyRows.length > 0) {
|
|
615
|
+
md += `## Anomalies\n\n`;
|
|
616
|
+
md += `| Severity | Provider | Type | Description |\n`;
|
|
617
|
+
md += `|----------|----------|------|-------------|\n`;
|
|
618
|
+
for (const { provider, anomaly } of anomalyRows) {
|
|
619
|
+
md += `| ${anomaly.severity.toUpperCase()} | ${provider} | ${anomaly.type} | ${anomaly.description} |\n`;
|
|
620
|
+
}
|
|
621
|
+
md += `\n`;
|
|
622
|
+
}
|
|
623
|
+
if (reconciliation) {
|
|
624
|
+
md += `## Reconciliation\n\n`;
|
|
625
|
+
md += `- **Match:** ${reconciliation.match}\n`;
|
|
626
|
+
md += `- **Mismatch:** ${reconciliation.mismatch}\n`;
|
|
627
|
+
md += `- **Not Found:** ${reconciliation.notFound}\n`;
|
|
628
|
+
md += `- **Error:** ${reconciliation.error}\n\n`;
|
|
629
|
+
if (reconciliation.mismatches.length > 0) {
|
|
630
|
+
md += `### Mismatches\n\n`;
|
|
631
|
+
md += `| Provider | Reference | Expected | Actual |\n`;
|
|
632
|
+
md += `|----------|-----------|----------|--------|\n`;
|
|
633
|
+
for (const m of reconciliation.mismatches) {
|
|
634
|
+
md += `| ${m.provider} | \`${m.reference}\` | ${m.expected} | ${m.actual} |\n`;
|
|
635
|
+
}
|
|
636
|
+
md += `\n`;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (complianceFlags.length > 0) {
|
|
640
|
+
md += `## Compliance Flags\n\n`;
|
|
641
|
+
md += `| Severity | Provider | Finding |\n`;
|
|
642
|
+
md += `|----------|----------|----------|\n`;
|
|
643
|
+
for (const f of complianceFlags) {
|
|
644
|
+
md += `| ${f.severity.toUpperCase()} | ${f.provider} | ${f.finding} |\n`;
|
|
645
|
+
}
|
|
646
|
+
md += `\n`;
|
|
647
|
+
}
|
|
648
|
+
md += `---\n\n`;
|
|
649
|
+
md += `*Generated by **paybridge audit** — [github.com/kobie3717/paybridge](https://github.com/kobie3717/paybridge)*\n`;
|
|
650
|
+
return md;
|
|
651
|
+
}
|
|
652
|
+
function renderAuditAsJson(report, pretty = false) {
|
|
653
|
+
return JSON.stringify(report, null, pretty ? 2 : 0);
|
|
654
|
+
}
|
|
655
|
+
function formatDuration(ms) {
|
|
656
|
+
const days = Math.floor(ms / (24 * 60 * 60 * 1000));
|
|
657
|
+
if (days >= 1)
|
|
658
|
+
return `${days}d`;
|
|
659
|
+
const hours = Math.floor(ms / (60 * 60 * 1000));
|
|
660
|
+
if (hours >= 1)
|
|
661
|
+
return `${hours}h`;
|
|
662
|
+
return `${Math.floor(ms / (60 * 1000))}m`;
|
|
663
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runAudit(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runAudit = runAudit;
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const audit_report_1 = require("../../audit-report");
|
|
39
|
+
const drift_store_1 = require("../drift-store");
|
|
40
|
+
const postgres_ledger_1 = require("../../stores/postgres-ledger");
|
|
41
|
+
const runners_1 = require("../runners");
|
|
42
|
+
const index_1 = require("../../index");
|
|
43
|
+
const reconcile_1 = require("../reconcile");
|
|
44
|
+
const utils_1 = require("../utils");
|
|
45
|
+
function parseWindow(windowStr) {
|
|
46
|
+
const match = windowStr.match(/^(\d+)(d|h|m)$/);
|
|
47
|
+
if (!match) {
|
|
48
|
+
throw new Error(`Invalid window format: ${windowStr}. Use format like 7d, 24h, 60m`);
|
|
49
|
+
}
|
|
50
|
+
const value = parseInt(match[1], 10);
|
|
51
|
+
const unit = match[2];
|
|
52
|
+
switch (unit) {
|
|
53
|
+
case 'd':
|
|
54
|
+
return value * 24 * 60 * 60 * 1000;
|
|
55
|
+
case 'h':
|
|
56
|
+
return value * 60 * 60 * 1000;
|
|
57
|
+
case 'm':
|
|
58
|
+
return value * 60 * 1000;
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Unknown unit: ${unit}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseArgs(args) {
|
|
64
|
+
const opts = {
|
|
65
|
+
format: 'html',
|
|
66
|
+
window: 7 * 24 * 60 * 60 * 1000,
|
|
67
|
+
driftDir: '.paybridge/drift-baseline',
|
|
68
|
+
};
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
switch (arg) {
|
|
72
|
+
case '--output':
|
|
73
|
+
opts.output = args[++i];
|
|
74
|
+
break;
|
|
75
|
+
case '--format':
|
|
76
|
+
const format = args[++i];
|
|
77
|
+
if (format !== 'html' && format !== 'md' && format !== 'json') {
|
|
78
|
+
throw new Error(`Invalid format: ${format}. Use html, md, or json`);
|
|
79
|
+
}
|
|
80
|
+
opts.format = format;
|
|
81
|
+
break;
|
|
82
|
+
case '--window':
|
|
83
|
+
opts.window = parseWindow(args[++i]);
|
|
84
|
+
break;
|
|
85
|
+
case '--providers':
|
|
86
|
+
opts.providers = args[++i].split(',').map((s) => s.trim());
|
|
87
|
+
break;
|
|
88
|
+
case '--ledger-pg':
|
|
89
|
+
opts.ledgerPg = args[++i];
|
|
90
|
+
break;
|
|
91
|
+
case '--drift-dir':
|
|
92
|
+
opts.driftDir = args[++i];
|
|
93
|
+
break;
|
|
94
|
+
case '--reconcile-input':
|
|
95
|
+
opts.reconcileInput = args[++i];
|
|
96
|
+
break;
|
|
97
|
+
case '-h':
|
|
98
|
+
case '--help':
|
|
99
|
+
printHelp();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!opts.output) {
|
|
107
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
108
|
+
const ext = opts.format === 'html' ? 'html' : opts.format === 'md' ? 'md' : 'json';
|
|
109
|
+
opts.output = `paybridge-audit-${timestamp}.${ext}`;
|
|
110
|
+
}
|
|
111
|
+
return opts;
|
|
112
|
+
}
|
|
113
|
+
function printHelp() {
|
|
114
|
+
console.log(`
|
|
115
|
+
paybridge audit — Generate comprehensive payment stack audit report
|
|
116
|
+
|
|
117
|
+
USAGE
|
|
118
|
+
paybridge audit [options]
|
|
119
|
+
|
|
120
|
+
OPTIONS
|
|
121
|
+
--output <file> Output file path (default: paybridge-audit-<date>.<ext>)
|
|
122
|
+
Use '-' for stdout
|
|
123
|
+
--format <type> Output format: html, md, json (default: html)
|
|
124
|
+
--window <duration> Analysis window: 7d, 30d, 90d, 24h, 1d (default: 7d)
|
|
125
|
+
--providers <list> Comma-separated provider names (default: all configured)
|
|
126
|
+
--ledger-pg <conn> PostgreSQL connection string for ledger data
|
|
127
|
+
--drift-dir <path> Drift baseline directory (default: .paybridge/drift-baseline)
|
|
128
|
+
--reconcile-input <file> Include reconciliation data from file (JSONL or CSV)
|
|
129
|
+
-h, --help Print this help
|
|
130
|
+
|
|
131
|
+
EXAMPLES
|
|
132
|
+
paybridge audit
|
|
133
|
+
paybridge audit --window 30d --format json --output report.json
|
|
134
|
+
paybridge audit --ledger-pg postgresql://user:pass@localhost/db
|
|
135
|
+
paybridge audit --output - --format md | mail -s "Audit" finance@example.com
|
|
136
|
+
|
|
137
|
+
EXIT CODES
|
|
138
|
+
0 No high-severity anomalies
|
|
139
|
+
1 High-severity anomalies detected
|
|
140
|
+
|
|
141
|
+
OUTPUT
|
|
142
|
+
HTML reports are print-to-PDF friendly (Cmd/Ctrl+P in browser).
|
|
143
|
+
JSON reports can be piped to CI/CD pipelines.
|
|
144
|
+
|
|
145
|
+
Docs: https://github.com/kobie3717/paybridge
|
|
146
|
+
`.trim());
|
|
147
|
+
}
|
|
148
|
+
async function runAudit(args) {
|
|
149
|
+
const opts = parseArgs(args);
|
|
150
|
+
const providerConfigs = opts.providers
|
|
151
|
+
? runners_1.runners.filter((r) => opts.providers.includes(r.name))
|
|
152
|
+
: runners_1.runners.filter((r) => r.envRequired.every((e) => process.env[e]));
|
|
153
|
+
const providers = providerConfigs.map((r) => {
|
|
154
|
+
let credentials = {};
|
|
155
|
+
for (const envVar of r.envRequired) {
|
|
156
|
+
if (envVar === 'STRIPE_API_KEY')
|
|
157
|
+
credentials.apiKey = process.env.STRIPE_API_KEY;
|
|
158
|
+
else if (envVar === 'YOCO_API_KEY')
|
|
159
|
+
credentials.apiKey = process.env.YOCO_API_KEY;
|
|
160
|
+
else if (envVar === 'SOFTYCOMP_API_KEY') {
|
|
161
|
+
credentials.apiKey = process.env.SOFTYCOMP_API_KEY;
|
|
162
|
+
credentials.secretKey = process.env.SOFTYCOMP_SECRET_KEY;
|
|
163
|
+
}
|
|
164
|
+
else if (envVar === 'OZOW_API_KEY') {
|
|
165
|
+
credentials.apiKey = process.env.OZOW_API_KEY;
|
|
166
|
+
credentials.siteCode = process.env.OZOW_SITE_CODE;
|
|
167
|
+
credentials.privateKey = process.env.OZOW_PRIVATE_KEY;
|
|
168
|
+
}
|
|
169
|
+
else if (envVar === 'PAYFAST_MERCHANT_ID') {
|
|
170
|
+
credentials.merchantId = process.env.PAYFAST_MERCHANT_ID;
|
|
171
|
+
credentials.merchantKey = process.env.PAYFAST_MERCHANT_KEY;
|
|
172
|
+
credentials.passphrase = process.env.PAYFAST_PASSPHRASE;
|
|
173
|
+
}
|
|
174
|
+
else if (envVar === 'PAYSTACK_API_KEY')
|
|
175
|
+
credentials.apiKey = process.env.PAYSTACK_API_KEY;
|
|
176
|
+
else if (envVar === 'PEACH_ACCESS_TOKEN') {
|
|
177
|
+
credentials.apiKey = process.env.PEACH_ACCESS_TOKEN;
|
|
178
|
+
credentials.secretKey = process.env.PEACH_ENTITY_ID;
|
|
179
|
+
}
|
|
180
|
+
else if (envVar === 'FLUTTERWAVE_API_KEY')
|
|
181
|
+
credentials.apiKey = process.env.FLUTTERWAVE_API_KEY;
|
|
182
|
+
else if (envVar === 'ADYEN_API_KEY') {
|
|
183
|
+
credentials.apiKey = process.env.ADYEN_API_KEY;
|
|
184
|
+
credentials.merchantAccount = process.env.ADYEN_MERCHANT_ACCOUNT;
|
|
185
|
+
}
|
|
186
|
+
else if (envVar === 'MERCADOPAGO_ACCESS_TOKEN')
|
|
187
|
+
credentials.apiKey = process.env.MERCADOPAGO_ACCESS_TOKEN;
|
|
188
|
+
else if (envVar === 'RAZORPAY_KEY_ID') {
|
|
189
|
+
credentials.apiKey = process.env.RAZORPAY_KEY_ID;
|
|
190
|
+
credentials.secretKey = process.env.RAZORPAY_KEY_SECRET;
|
|
191
|
+
}
|
|
192
|
+
else if (envVar === 'MOLLIE_API_KEY')
|
|
193
|
+
credentials.apiKey = process.env.MOLLIE_API_KEY;
|
|
194
|
+
else if (envVar === 'SQUARE_ACCESS_TOKEN') {
|
|
195
|
+
credentials.apiKey = process.env.SQUARE_ACCESS_TOKEN;
|
|
196
|
+
credentials.locationId = process.env.SQUARE_LOCATION_ID;
|
|
197
|
+
}
|
|
198
|
+
else if (envVar === 'PESAPAL_CONSUMER_KEY') {
|
|
199
|
+
credentials.apiKey = process.env.PESAPAL_CONSUMER_KEY;
|
|
200
|
+
credentials.secretKey = process.env.PESAPAL_CONSUMER_SECRET;
|
|
201
|
+
credentials.notificationId = process.env.PESAPAL_NOTIFICATION_ID || 'dummy';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const pay = new index_1.PayBridge({
|
|
205
|
+
provider: r.name,
|
|
206
|
+
credentials,
|
|
207
|
+
sandbox: true,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
name: r.name,
|
|
211
|
+
capabilities: pay.provider.getCapabilities(),
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
if (providers.length === 0) {
|
|
215
|
+
console.error((0, utils_1.colorize)('Error: No providers configured. Set environment variables for at least one provider.', 'red'));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const input = {
|
|
219
|
+
providers,
|
|
220
|
+
windowMs: opts.window,
|
|
221
|
+
};
|
|
222
|
+
if (opts.ledgerPg) {
|
|
223
|
+
try {
|
|
224
|
+
// @ts-ignore - pg is an optional peer dependency
|
|
225
|
+
const pgModule = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
226
|
+
const Pool = pgModule.default?.Pool || pgModule.Pool;
|
|
227
|
+
const pool = new Pool({ connectionString: opts.ledgerPg });
|
|
228
|
+
input.ledger = (0, postgres_ledger_1.createPostgresLedgerStore)({ pool });
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
console.error((0, utils_1.colorize)(`Error: pg module not installed. Run: npm install pg`, 'red'));
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (opts.driftDir) {
|
|
236
|
+
input.driftStore = new drift_store_1.FileDriftStore(opts.driftDir);
|
|
237
|
+
}
|
|
238
|
+
if (opts.reconcileInput) {
|
|
239
|
+
const content = await node_fs_1.promises.readFile(opts.reconcileInput, 'utf-8');
|
|
240
|
+
const records = [];
|
|
241
|
+
const lines = content.trim().split('\n');
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
if (line.trim().startsWith('#') || !line.trim())
|
|
244
|
+
continue;
|
|
245
|
+
if (line.trim().startsWith('{')) {
|
|
246
|
+
const record = JSON.parse(line);
|
|
247
|
+
records.push(record);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const parts = line.split(/\t|,/).map((s) => s.trim());
|
|
251
|
+
if (parts.length >= 3) {
|
|
252
|
+
records.push({
|
|
253
|
+
provider: parts[0],
|
|
254
|
+
reference: parts[1],
|
|
255
|
+
expectedStatus: parts[2],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const buildProvider = (providerName) => {
|
|
261
|
+
const runner = runners_1.runners.find((r) => r.name === providerName);
|
|
262
|
+
if (!runner)
|
|
263
|
+
throw new Error(`Unknown provider: ${providerName}`);
|
|
264
|
+
return new index_1.PayBridge({
|
|
265
|
+
provider: providerName,
|
|
266
|
+
credentials: {},
|
|
267
|
+
sandbox: true,
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
const hasCredsFor = (providerName) => {
|
|
271
|
+
const runner = runners_1.runners.find((r) => r.name === providerName);
|
|
272
|
+
return runner ? runner.envRequired.every((e) => process.env[e]) : false;
|
|
273
|
+
};
|
|
274
|
+
const { results } = await (0, reconcile_1.runReconcile)(records, { buildProvider, hasCredsFor });
|
|
275
|
+
input.reconcileResults = results;
|
|
276
|
+
}
|
|
277
|
+
const report = await (0, audit_report_1.generateAuditReport)(input);
|
|
278
|
+
let output;
|
|
279
|
+
switch (opts.format) {
|
|
280
|
+
case 'html':
|
|
281
|
+
output = (0, audit_report_1.renderAuditAsHtml)(report);
|
|
282
|
+
break;
|
|
283
|
+
case 'md':
|
|
284
|
+
output = (0, audit_report_1.renderAuditAsMarkdown)(report);
|
|
285
|
+
break;
|
|
286
|
+
case 'json':
|
|
287
|
+
output = (0, audit_report_1.renderAuditAsJson)(report, true);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (opts.output === '-') {
|
|
291
|
+
console.log(output);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
if (!opts.output) {
|
|
295
|
+
throw new Error('Output path is required');
|
|
296
|
+
}
|
|
297
|
+
await node_fs_1.promises.writeFile(opts.output, output, 'utf-8');
|
|
298
|
+
console.log((0, utils_1.colorize)(`Audit written to ${opts.output}`, 'green'));
|
|
299
|
+
console.log(`Total providers: ${report.summary.totalProviders}`);
|
|
300
|
+
console.log(`Anomalies: ${(0, utils_1.colorize)(`${report.summary.anomalyCounts.high}`, report.summary.anomalyCounts.high > 0 ? 'red' : 'green')} high, ${report.summary.anomalyCounts.medium} medium, ${report.summary.anomalyCounts.low} low`);
|
|
301
|
+
if (opts.format === 'html') {
|
|
302
|
+
console.log((0, utils_1.colorize)('Open in browser to review.', 'cyan'));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (report.summary.anomalyCounts.high > 0) {
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const quote_1 = require("./commands/quote");
|
|
|
8
8
|
const drift_1 = require("./commands/drift");
|
|
9
9
|
const drift_watch_1 = require("./commands/drift-watch");
|
|
10
10
|
const reconcile_1 = require("./commands/reconcile");
|
|
11
|
+
const audit_1 = require("./commands/audit");
|
|
11
12
|
const utils_1 = require("./utils");
|
|
12
13
|
async function main() {
|
|
13
14
|
const [, , command, ...args] = process.argv;
|
|
@@ -33,6 +34,9 @@ async function main() {
|
|
|
33
34
|
case 'reconcile':
|
|
34
35
|
await (0, reconcile_1.runReconcileCommand)(args);
|
|
35
36
|
break;
|
|
37
|
+
case 'audit':
|
|
38
|
+
await (0, audit_1.runAudit)(args);
|
|
39
|
+
break;
|
|
36
40
|
case '-h':
|
|
37
41
|
case '--help':
|
|
38
42
|
case 'help':
|
package/dist/cli/utils.js
CHANGED
|
@@ -73,6 +73,8 @@ COMMANDS
|
|
|
73
73
|
drift-check [opts] Capture/compare provider response shapes (drift detection)
|
|
74
74
|
drift-watch [opts] Run drift-check on a loop (long-running monitor)
|
|
75
75
|
reconcile [opts] Reconcile your DB against provider state (detects missed webhooks)
|
|
76
|
+
audit [opts] Generate comprehensive audit report (drift + reconcile +
|
|
77
|
+
success rates + fees + anomalies)
|
|
76
78
|
help, -h, --help Print this help
|
|
77
79
|
version, -v Print version
|
|
78
80
|
|
|
@@ -99,6 +101,8 @@ EXAMPLES
|
|
|
99
101
|
paybridge drift-watch --interval 1h --webhook-url https://hooks.slack.com/...
|
|
100
102
|
paybridge reconcile --input expected.jsonl
|
|
101
103
|
psql -t -c "SELECT provider, reference, status FROM payments" | paybridge reconcile
|
|
104
|
+
paybridge audit --window 30d --ledger-pg postgresql://localhost/db
|
|
105
|
+
paybridge audit --output - --format md | mail -s "Audit" finance@example.com
|
|
102
106
|
|
|
103
107
|
Docs: https://github.com/kobie3717/paybridge
|
|
104
108
|
`.trim();
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export { createPostgresLedgerStore, type PostgresLedgerStoreOptions, getCreateTa
|
|
|
26
26
|
export type { PgPoolLike, PgQueryResult } from './stores/postgres';
|
|
27
27
|
export { runReconcile, type ReconcileDeps } from './cli/reconcile';
|
|
28
28
|
export * from './cli/reconcile-types';
|
|
29
|
+
export * from './drift-detector';
|
|
30
|
+
export { FileDriftStore, type DriftStore } from './cli/drift-store';
|
|
31
|
+
export { generateAuditReport, renderAuditAsHtml, renderAuditAsMarkdown, renderAuditAsJson, type AuditInput, type AuditReport, type ProviderAuditSection, type AuditAnomaly, } from './audit-report';
|
|
29
32
|
export declare class PayBridge {
|
|
30
33
|
readonly provider: PaymentProvider;
|
|
31
34
|
constructor(config: PayBridgeConfig);
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
20
20
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.PayBridge = exports.runReconcile = exports.getPostgresLedgerTableSql = exports.createPostgresLedgerStore = exports.createRedisLedgerStore = exports.createRedisIdempotencyStore = exports.createRedisCircuitBreakerStore = void 0;
|
|
23
|
+
exports.PayBridge = exports.renderAuditAsJson = exports.renderAuditAsMarkdown = exports.renderAuditAsHtml = exports.generateAuditReport = exports.FileDriftStore = exports.runReconcile = exports.getPostgresLedgerTableSql = exports.createPostgresLedgerStore = exports.createRedisLedgerStore = exports.createRedisIdempotencyStore = exports.createRedisCircuitBreakerStore = void 0;
|
|
24
24
|
const softycomp_1 = require("./providers/softycomp");
|
|
25
25
|
const yoco_1 = require("./providers/yoco");
|
|
26
26
|
const ozow_1 = require("./providers/ozow");
|
|
@@ -60,6 +60,14 @@ Object.defineProperty(exports, "getPostgresLedgerTableSql", { enumerable: true,
|
|
|
60
60
|
var reconcile_1 = require("./cli/reconcile");
|
|
61
61
|
Object.defineProperty(exports, "runReconcile", { enumerable: true, get: function () { return reconcile_1.runReconcile; } });
|
|
62
62
|
__exportStar(require("./cli/reconcile-types"), exports);
|
|
63
|
+
__exportStar(require("./drift-detector"), exports);
|
|
64
|
+
var drift_store_1 = require("./cli/drift-store");
|
|
65
|
+
Object.defineProperty(exports, "FileDriftStore", { enumerable: true, get: function () { return drift_store_1.FileDriftStore; } });
|
|
66
|
+
var audit_report_1 = require("./audit-report");
|
|
67
|
+
Object.defineProperty(exports, "generateAuditReport", { enumerable: true, get: function () { return audit_report_1.generateAuditReport; } });
|
|
68
|
+
Object.defineProperty(exports, "renderAuditAsHtml", { enumerable: true, get: function () { return audit_report_1.renderAuditAsHtml; } });
|
|
69
|
+
Object.defineProperty(exports, "renderAuditAsMarkdown", { enumerable: true, get: function () { return audit_report_1.renderAuditAsMarkdown; } });
|
|
70
|
+
Object.defineProperty(exports, "renderAuditAsJson", { enumerable: true, get: function () { return audit_report_1.renderAuditAsJson; } });
|
|
63
71
|
class PayBridge {
|
|
64
72
|
constructor(config) {
|
|
65
73
|
this.provider = this.createProvider(config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paybridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
4
|
"description": "One API for fiat + crypto payments. Multi-provider routing, automatic failover, MoonPay on/off-ramp. SA-first, global-ready.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|