paybridge 0.9.0 → 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 CHANGED
@@ -61,6 +61,86 @@ Runnable integrations for common Node.js frameworks:
61
61
 
62
62
  Each example uses `PayBridgeRouter` with Stripe + PayStack and demonstrates webhook signature verification, idempotency, and provider-specific routing.
63
63
 
64
+ ## Drift Detection
65
+
66
+ Payment providers change their APIs without notice. A field gets renamed, an endpoint moves, a type changes from `string` to `number`. Your integration silently breaks.
67
+
68
+ PayBridge includes **drift detection** — capture the shape of every provider's sandbox response, store it as a baseline, and get alerted the moment something changes.
69
+
70
+ ### Quick Start
71
+
72
+ ```bash
73
+ # Capture baselines (one-time setup)
74
+ npx paybridge drift-check --capture
75
+
76
+ # Check for drift
77
+ npx paybridge drift-check
78
+
79
+ # Watch continuously (6-hour interval)
80
+ npx paybridge drift-watch --interval 6h --webhook-url https://hooks.slack.com/...
81
+ ```
82
+
83
+ ### Example Output
84
+
85
+ ```
86
+ === Drift Detection ===
87
+
88
+ [✓] stripe — no drift
89
+ [⚠] mollie — drift detected:
90
+ + new keys: data.expiresAt, _links.dashboard.href
91
+ - removed keys: data.metadata.legacy
92
+ ! type changed: data.amount.value (string → number)
93
+ [⚠] square — drift detected:
94
+ + new keys: payment_link.created_at_iso
95
+ [ ] paystack — Missing: PAYSTACK_API_KEY
96
+ ```
97
+
98
+ Exit code 1 if drift detected, 0 if clean. Perfect for CI/CD pipelines or cron jobs.
99
+
100
+ ### Why It Matters
101
+
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
+
104
+ ## Reconciliation
105
+
106
+ 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.
107
+
108
+ PayBridge's **reconcile** command diffs your database against each provider's current state, catching payments where your local status doesn't match reality.
109
+
110
+ ### Quick Start
111
+
112
+ ```bash
113
+ # From JSONL file
114
+ echo '{"provider":"stripe","reference":"pay_001","expectedStatus":"pending"}' > expected.jsonl
115
+ npx paybridge reconcile --input expected.jsonl
116
+
117
+ # From SQL query (Postgres example)
118
+ psql -t -c "SELECT provider, reference, status AS \"expectedStatus\" FROM payments WHERE status='pending' AND created_at > now() - interval '24 hours'" \
119
+ | npx paybridge reconcile
120
+
121
+ # CSV format
122
+ cat payments.csv | npx paybridge reconcile
123
+ ```
124
+
125
+ ### Example Output
126
+
127
+ ```
128
+ [✓] stripe:pay_001 — completed (match)
129
+ [!] stripe:pay_002 — expected pending, actual completed (MISSED WEBHOOK)
130
+ [?] paystack:pay_003 — not-found (no provider record)
131
+ [✗] stripe:pay_004 — error (HTTP 503)
132
+ [ ] adyen:pay_005 — skipped (missing ADYEN_API_KEY)
133
+
134
+ Reconciled: 5
135
+ Match: 1
136
+ Mismatch (missed webhook): 1
137
+ Not found: 1
138
+ Error: 1
139
+ Skipped: 1
140
+ ```
141
+
142
+ Exit code 1 if any mismatch, 0 if clean. Add `--webhook-url` to POST mismatch reports to Slack/Discord/your ops channel. Use `--json` for pipeline integration.
143
+
64
144
  ## Quick Start
65
145
 
66
146
  > **Upgrading from 0.1 or 0.2?** See [docs/migration.md](docs/migration.md).
@@ -0,0 +1 @@
1
+ export declare function runDriftWatch(args: string[]): Promise<void>;
@@ -0,0 +1,118 @@
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.runDriftWatch = runDriftWatch;
37
+ const path = __importStar(require("node:path"));
38
+ const runners_1 = require("../runners");
39
+ const drift_store_1 = require("../drift-store");
40
+ const drift_1 = require("./drift");
41
+ const utils_1 = require("../utils");
42
+ const INTERVALS = {
43
+ '30m': 30 * 60 * 1000,
44
+ '1h': 60 * 60 * 1000,
45
+ '6h': 6 * 60 * 60 * 1000,
46
+ '12h': 12 * 60 * 60 * 1000,
47
+ '24h': 24 * 60 * 60 * 1000,
48
+ };
49
+ function parseInterval(str) {
50
+ const interval = INTERVALS[str];
51
+ if (!interval) {
52
+ throw new Error(`Invalid interval: ${str}. Supported: ${Object.keys(INTERVALS).join(', ')}`);
53
+ }
54
+ return interval;
55
+ }
56
+ async function runDriftWatch(args) {
57
+ const intervalArg = args.find((a, i) => args[i - 1] === '--interval') || '6h';
58
+ const interval = parseInterval(intervalArg);
59
+ const once = args.includes('--once');
60
+ const webhookUrl = args.find((a, i) => args[i - 1] === '--webhook-url');
61
+ const baselineDir = args.find((a, i) => args[i - 1] === '--baseline-dir') ||
62
+ path.join(process.cwd(), '.paybridge', 'drift-baseline');
63
+ const store = new drift_store_1.FileDriftStore(baselineDir);
64
+ let running = true;
65
+ const cleanup = () => {
66
+ running = false;
67
+ console.log('\nStopping drift watch...');
68
+ process.exit(0);
69
+ };
70
+ process.on('SIGTERM', cleanup);
71
+ process.on('SIGINT', cleanup);
72
+ const runCheck = async () => {
73
+ const timestamp = new Date().toISOString();
74
+ console.log(`\n${(0, utils_1.colorize)('[drift-watch]', 'cyan')} Running check at ${timestamp}\n`);
75
+ try {
76
+ const results = await (0, drift_1.runDriftCheck)(runners_1.runners, store, { webhookUrl });
77
+ const driftCount = results.filter((r) => r.status === 'drift').length;
78
+ const noDriftCount = results.filter((r) => r.status === 'no-drift').length;
79
+ const skippedCount = results.filter((r) => r.status === 'skipped').length;
80
+ const errorCount = results.filter((r) => r.status === 'error').length;
81
+ for (const res of results) {
82
+ if (res.status === 'drift' && res.report) {
83
+ console.log(`${(0, utils_1.colorize)('[⚠]', 'yellow')} ${res.provider} — drift detected`);
84
+ if (res.report.addedKeys.length > 0) {
85
+ console.log(` + new keys: ${res.report.addedKeys.join(', ')}`);
86
+ }
87
+ if (res.report.removedKeys.length > 0) {
88
+ console.log(` - removed keys: ${res.report.removedKeys.join(', ')}`);
89
+ }
90
+ if (res.report.typeChanges.length > 0) {
91
+ for (const change of res.report.typeChanges) {
92
+ console.log(` ! type changed: ${change.key} (${change.oldType} → ${change.newType})`);
93
+ }
94
+ }
95
+ }
96
+ else if (res.status === 'error') {
97
+ console.log(`${(0, utils_1.colorize)('[✗]', 'red')} ${res.provider} ERROR: ${res.message}`);
98
+ }
99
+ }
100
+ console.log(`\n${(0, utils_1.colorize)('[summary]', 'cyan')} drift: ${driftCount}, clean: ${noDriftCount}, skipped: ${skippedCount}, errors: ${errorCount}`);
101
+ }
102
+ catch (err) {
103
+ console.error(`${(0, utils_1.colorize)('[!]', 'red')} Check failed: ${err.message}`);
104
+ }
105
+ };
106
+ await runCheck();
107
+ if (once) {
108
+ process.exit(0);
109
+ }
110
+ console.log(`\n${(0, utils_1.colorize)('[drift-watch]', 'cyan')} Watching every ${intervalArg}. Press Ctrl+C to stop.\n`);
111
+ const timer = setInterval(() => {
112
+ if (running) {
113
+ runCheck();
114
+ }
115
+ }, interval);
116
+ timer.unref();
117
+ await new Promise(() => { });
118
+ }
@@ -0,0 +1,20 @@
1
+ import { ProviderRunner } from '../runners';
2
+ import { DriftStore } from '../drift-store';
3
+ import { DriftReport } from '../../drift-detector';
4
+ interface DriftCheckOptions {
5
+ capture?: boolean;
6
+ baselineDir?: string;
7
+ json?: boolean;
8
+ webhookUrl?: string;
9
+ providers?: string[];
10
+ }
11
+ interface DriftCheckResult {
12
+ provider: string;
13
+ status: 'captured' | 'no-baseline' | 'no-drift' | 'drift' | 'skipped' | 'error';
14
+ message?: string;
15
+ report?: DriftReport;
16
+ keyCount?: number;
17
+ }
18
+ export declare function runDriftCheck(providedRunners: ProviderRunner[], store: DriftStore, opts: DriftCheckOptions): Promise<DriftCheckResult[]>;
19
+ export declare function runDrift(args: string[]): Promise<void>;
20
+ export {};
@@ -0,0 +1,212 @@
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.runDriftCheck = runDriftCheck;
37
+ exports.runDrift = runDrift;
38
+ const path = __importStar(require("node:path"));
39
+ const runners_1 = require("../runners");
40
+ const drift_store_1 = require("../drift-store");
41
+ const drift_detector_1 = require("../../drift-detector");
42
+ const utils_1 = require("../utils");
43
+ async function postWebhook(url, payload) {
44
+ const res = await fetch(url, {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify(payload),
48
+ });
49
+ if (!res.ok) {
50
+ throw new Error(`Webhook POST failed: ${res.status} ${res.statusText}`);
51
+ }
52
+ }
53
+ async function runDriftCheck(providedRunners, store, opts) {
54
+ const libVersion = '0.10.0';
55
+ const results = [];
56
+ for (const runner of providedRunners) {
57
+ const missing = runner.envRequired.filter((key) => !process.env[key]);
58
+ if (missing.length > 0) {
59
+ results.push({
60
+ provider: runner.name,
61
+ status: 'skipped',
62
+ message: `Missing: ${missing.join(', ')}`,
63
+ });
64
+ continue;
65
+ }
66
+ try {
67
+ const response = await runner.run();
68
+ const shape = (0, drift_detector_1.captureShape)(response);
69
+ shape.status = response.status;
70
+ if (opts.capture) {
71
+ const baseline = {
72
+ providerName: runner.name,
73
+ operation: 'createPayment',
74
+ shape,
75
+ libVersion,
76
+ };
77
+ await store.save(baseline);
78
+ results.push({
79
+ provider: runner.name,
80
+ status: 'captured',
81
+ keyCount: shape.keys.length,
82
+ });
83
+ }
84
+ else {
85
+ const baseline = await store.load(runner.name);
86
+ if (!baseline) {
87
+ results.push({
88
+ provider: runner.name,
89
+ status: 'no-baseline',
90
+ message: 'No baseline found (run with --capture to create)',
91
+ });
92
+ }
93
+ else {
94
+ const report = (0, drift_detector_1.diffBaseline)(baseline, shape, runner.name);
95
+ if (report.driftDetected) {
96
+ results.push({
97
+ provider: runner.name,
98
+ status: 'drift',
99
+ report,
100
+ });
101
+ if (opts.webhookUrl) {
102
+ try {
103
+ await postWebhook(opts.webhookUrl, { provider: runner.name, drift: report, libVersion });
104
+ }
105
+ catch (err) {
106
+ console.error(`${(0, utils_1.colorize)('[!]', 'yellow')} ${runner.name} webhook failed: ${err.message}`);
107
+ }
108
+ }
109
+ }
110
+ else {
111
+ results.push({
112
+ provider: runner.name,
113
+ status: 'no-drift',
114
+ });
115
+ }
116
+ }
117
+ }
118
+ }
119
+ catch (error) {
120
+ results.push({
121
+ provider: runner.name,
122
+ status: 'error',
123
+ message: error.message || String(error),
124
+ });
125
+ }
126
+ }
127
+ return results;
128
+ }
129
+ async function runDrift(args) {
130
+ const capture = args.includes('--capture');
131
+ const jsonOutput = args.includes('--json');
132
+ const baselineDir = args.find((a, i) => args[i - 1] === '--baseline-dir') ||
133
+ path.join(process.cwd(), '.paybridge', 'drift-baseline');
134
+ const webhookUrl = args.find((a, i) => args[i - 1] === '--webhook-url');
135
+ const providerNames = args.filter((a) => !a.startsWith('--') && a !== 'drift-check');
136
+ let selectedRunners = runners_1.runners;
137
+ if (providerNames.length > 0) {
138
+ selectedRunners = runners_1.runners.filter((r) => providerNames.includes(r.name));
139
+ const unknownProviders = providerNames.filter((p) => !runners_1.runners.find((r) => r.name === p));
140
+ if (unknownProviders.length > 0) {
141
+ console.error(`Unknown providers: ${unknownProviders.join(', ')}`);
142
+ console.error(`Available: ${runners_1.runners.map((r) => r.name).join(', ')}`);
143
+ process.exit(1);
144
+ }
145
+ }
146
+ const store = new drift_store_1.FileDriftStore(baselineDir);
147
+ const results = await runDriftCheck(selectedRunners, store, { capture, baselineDir, json: jsonOutput, webhookUrl });
148
+ if (jsonOutput) {
149
+ console.log(JSON.stringify(results, null, 2));
150
+ }
151
+ else {
152
+ if (capture) {
153
+ console.log('\n=== Drift Baseline Capture ===\n');
154
+ for (const res of results) {
155
+ if (res.status === 'captured') {
156
+ console.log(`${(0, utils_1.colorize)('[saved]', 'green')} ${res.provider} baseline (${res.keyCount} keys)`);
157
+ }
158
+ else if (res.status === 'skipped') {
159
+ console.log(`${(0, utils_1.colorize)('[ ]', 'dim')} ${res.provider} — ${res.message}`);
160
+ }
161
+ else if (res.status === 'error') {
162
+ console.log(`${(0, utils_1.colorize)('[✗]', 'red')} ${res.provider} ERROR: ${res.message}`);
163
+ }
164
+ }
165
+ }
166
+ else {
167
+ console.log('\n=== Drift Detection ===\n');
168
+ for (const res of results) {
169
+ if (res.status === 'no-drift') {
170
+ console.log(`${(0, utils_1.colorize)('[✓]', 'green')} ${res.provider} — no drift`);
171
+ }
172
+ else if (res.status === 'no-baseline') {
173
+ console.log(`${(0, utils_1.colorize)('[ ]', 'dim')} ${res.provider} — ${res.message}`);
174
+ }
175
+ else if (res.status === 'skipped') {
176
+ console.log(`${(0, utils_1.colorize)('[ ]', 'dim')} ${res.provider} — ${res.message}`);
177
+ }
178
+ else if (res.status === 'drift' && res.report) {
179
+ console.log(`${(0, utils_1.colorize)('[⚠]', 'yellow')} ${res.provider} — drift detected:`);
180
+ if (res.report.addedKeys.length > 0) {
181
+ console.log(` ${(0, utils_1.colorize)('+', 'green')} new keys: ${res.report.addedKeys.join(', ')}`);
182
+ }
183
+ if (res.report.removedKeys.length > 0) {
184
+ console.log(` ${(0, utils_1.colorize)('-', 'red')} removed keys: ${res.report.removedKeys.join(', ')}`);
185
+ }
186
+ if (res.report.typeChanges.length > 0) {
187
+ for (const change of res.report.typeChanges) {
188
+ console.log(` ${(0, utils_1.colorize)('!', 'yellow')} type changed: ${change.key} (${change.oldType} → ${change.newType})`);
189
+ }
190
+ }
191
+ if (res.report.statusChanged) {
192
+ console.log(` ${(0, utils_1.colorize)('!', 'yellow')} status changed: ${res.report.statusChanged.old} → ${res.report.statusChanged.new}`);
193
+ }
194
+ }
195
+ else if (res.status === 'error') {
196
+ console.log(`${(0, utils_1.colorize)('[✗]', 'red')} ${res.provider} ERROR: ${res.message}`);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ const driftCount = results.filter((r) => r.status === 'drift').length;
202
+ const errorCount = results.filter((r) => r.status === 'error').length;
203
+ if (errorCount > 0) {
204
+ process.exit(2);
205
+ }
206
+ else if (driftCount > 0) {
207
+ process.exit(1);
208
+ }
209
+ else {
210
+ process.exit(0);
211
+ }
212
+ }
@@ -0,0 +1 @@
1
+ export declare function runReconcileCommand(args: string[]): Promise<void>;