paybridge 0.9.0 → 0.10.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,46 @@ 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
+
64
104
  ## Quick Start
65
105
 
66
106
  > **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,14 @@
1
+ import { ProviderBaseline } from '../drift-detector';
2
+ export interface DriftStore {
3
+ load(providerName: string): Promise<ProviderBaseline | null>;
4
+ save(baseline: ProviderBaseline): Promise<void>;
5
+ listProviders(): Promise<string[]>;
6
+ }
7
+ export declare class FileDriftStore implements DriftStore {
8
+ private dir;
9
+ constructor(dir: string);
10
+ private getFilePath;
11
+ load(providerName: string): Promise<ProviderBaseline | null>;
12
+ save(baseline: ProviderBaseline): Promise<void>;
13
+ listProviders(): Promise<string[]>;
14
+ }
@@ -0,0 +1,80 @@
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.FileDriftStore = void 0;
37
+ const node_fs_1 = require("node:fs");
38
+ const path = __importStar(require("node:path"));
39
+ class FileDriftStore {
40
+ constructor(dir) {
41
+ this.dir = dir;
42
+ }
43
+ getFilePath(providerName) {
44
+ return path.join(this.dir, `${providerName}.json`);
45
+ }
46
+ async load(providerName) {
47
+ const filePath = this.getFilePath(providerName);
48
+ try {
49
+ const content = await node_fs_1.promises.readFile(filePath, 'utf-8');
50
+ return JSON.parse(content);
51
+ }
52
+ catch (err) {
53
+ if (err.code === 'ENOENT') {
54
+ return null;
55
+ }
56
+ throw err;
57
+ }
58
+ }
59
+ async save(baseline) {
60
+ await node_fs_1.promises.mkdir(this.dir, { recursive: true });
61
+ const filePath = this.getFilePath(baseline.providerName);
62
+ const content = JSON.stringify(baseline, null, 2);
63
+ await node_fs_1.promises.writeFile(filePath, content, 'utf-8');
64
+ }
65
+ async listProviders() {
66
+ try {
67
+ const files = await node_fs_1.promises.readdir(this.dir);
68
+ return files
69
+ .filter((f) => f.endsWith('.json'))
70
+ .map((f) => f.slice(0, -5));
71
+ }
72
+ catch (err) {
73
+ if (err.code === 'ENOENT') {
74
+ return [];
75
+ }
76
+ throw err;
77
+ }
78
+ }
79
+ }
80
+ exports.FileDriftStore = FileDriftStore;
package/dist/cli/index.js CHANGED
@@ -5,6 +5,8 @@ const test_1 = require("./commands/test");
5
5
  const providers_1 = require("./commands/providers");
6
6
  const webhook_1 = require("./commands/webhook");
7
7
  const quote_1 = require("./commands/quote");
8
+ const drift_1 = require("./commands/drift");
9
+ const drift_watch_1 = require("./commands/drift-watch");
8
10
  const utils_1 = require("./utils");
9
11
  async function main() {
10
12
  const [, , command, ...args] = process.argv;
@@ -21,6 +23,12 @@ async function main() {
21
23
  case 'quote':
22
24
  await (0, quote_1.runQuote)(args);
23
25
  break;
26
+ case 'drift-check':
27
+ await (0, drift_1.runDrift)(args);
28
+ break;
29
+ case 'drift-watch':
30
+ await (0, drift_watch_1.runDriftWatch)(args);
31
+ break;
24
32
  case '-h':
25
33
  case '--help':
26
34
  case 'help':
@@ -6,7 +6,7 @@ const crypto_1 = require("../crypto");
6
6
  const timestamp = Date.now();
7
7
  const testCustomer = {
8
8
  name: 'Test User',
9
- email: 'test@example.com',
9
+ email: 'paybridge-sandbox@gmail.com',
10
10
  phone: '0825551234',
11
11
  };
12
12
  const testUrls = {
@@ -111,14 +111,26 @@ exports.runners = [
111
111
  credentials: { apiKey: process.env.PAYSTACK_API_KEY },
112
112
  sandbox: true,
113
113
  });
114
- const payment = await pay.createPayment({
115
- amount: 1.0,
116
- currency: 'NGN',
117
- reference: `cli-test-${timestamp}`,
118
- customer: testCustomer,
119
- urls: testUrls,
120
- });
121
- return { id: payment.id, checkoutUrl: payment.checkoutUrl, status: payment.status };
114
+ const currencies = (process.env.PAYSTACK_TEST_CURRENCY || 'NGN,ZAR,GHS,KES').split(',');
115
+ let lastErr;
116
+ for (const currency of currencies) {
117
+ try {
118
+ const payment = await pay.createPayment({
119
+ amount: 100.0,
120
+ currency: currency.trim(),
121
+ reference: `cli-test-${timestamp}-${currency.trim()}`,
122
+ customer: testCustomer,
123
+ urls: testUrls,
124
+ });
125
+ return { id: payment.id, checkoutUrl: payment.checkoutUrl, status: payment.status };
126
+ }
127
+ catch (e) {
128
+ lastErr = e;
129
+ if (!/currency not supported/i.test(e.message))
130
+ throw e;
131
+ }
132
+ }
133
+ throw lastErr ?? new Error('No currency accepted by PayStack merchant');
122
134
  },
123
135
  },
124
136
  {
@@ -289,11 +301,37 @@ exports.runners = [
289
301
  name: 'pesapal',
290
302
  envRequired: ['PESAPAL_CONSUMER_KEY', 'PESAPAL_CONSUMER_SECRET'],
291
303
  run: async () => {
304
+ const consumerKey = process.env.PESAPAL_CONSUMER_KEY;
305
+ const consumerSecret = process.env.PESAPAL_CONSUMER_SECRET;
306
+ let notificationId = process.env.PESAPAL_NOTIFICATION_ID;
307
+ if (!notificationId) {
308
+ const tokenRes = await fetch('https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken', {
309
+ method: 'POST',
310
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
311
+ body: JSON.stringify({ consumer_key: consumerKey, consumer_secret: consumerSecret }),
312
+ });
313
+ const tokenJson = await tokenRes.json();
314
+ const ipnRes = await fetch('https://cybqa.pesapal.com/pesapalv3/api/URLSetup/RegisterIPN', {
315
+ method: 'POST',
316
+ headers: {
317
+ 'Content-Type': 'application/json',
318
+ Accept: 'application/json',
319
+ Authorization: `Bearer ${tokenJson.token}`,
320
+ },
321
+ body: JSON.stringify({
322
+ url: 'https://example.com/pesapal-ipn',
323
+ ipn_notification_type: 'GET',
324
+ }),
325
+ });
326
+ const ipnJson = await ipnRes.json();
327
+ notificationId = ipnJson.ipn_id;
328
+ }
292
329
  const pay = new index_1.PayBridge({
293
330
  provider: 'pesapal',
294
331
  credentials: {
295
- apiKey: process.env.PESAPAL_CONSUMER_KEY,
296
- secretKey: process.env.PESAPAL_CONSUMER_SECRET,
332
+ apiKey: consumerKey,
333
+ secretKey: consumerSecret,
334
+ notificationId,
297
335
  },
298
336
  sandbox: true,
299
337
  });
package/dist/cli/utils.js CHANGED
@@ -70,9 +70,20 @@ COMMANDS
70
70
  webhook verify <p> Verify webhook signature (raw body from stdin)
71
71
  webhook parse <p> Parse webhook event (raw body from stdin)
72
72
  quote <p> [opts] Get a crypto on/off-ramp quote
73
+ drift-check [opts] Capture/compare provider response shapes (drift detection)
74
+ drift-watch [opts] Run drift-check on a loop (long-running monitor)
73
75
  help, -h, --help Print this help
74
76
  version, -v Print version
75
77
 
78
+ DRIFT DETECTION
79
+ drift-check [provider] Check all/single provider for API drift
80
+ drift-check --capture Capture baseline snapshots (init mode)
81
+ drift-check --json Output JSON instead of human-readable
82
+ drift-check --baseline-dir <p> Custom baseline location (default: .paybridge/drift-baseline)
83
+ drift-check --webhook-url <url> POST drift report to URL on detection
84
+ drift-watch --interval <6h|1h> Run every interval (default: 6h)
85
+ drift-watch --once Alias for drift-check (don't loop)
86
+
76
87
  PROVIDER ENV VARS
77
88
  See SETUP.md or run 'paybridge test --all' for the full list.
78
89
 
@@ -82,6 +93,9 @@ EXAMPLES
82
93
  STRIPE_API_KEY=sk_test_... paybridge test stripe
83
94
  echo '{"id":"evt_x"}' | paybridge webhook parse paystack \\
84
95
  --header x-paystack-signature=abc
96
+ paybridge drift-check --capture
97
+ paybridge drift-check stripe
98
+ paybridge drift-watch --interval 1h --webhook-url https://hooks.slack.com/...
85
99
 
86
100
  Docs: https://github.com/kobie3717/paybridge
87
101
  `.trim();
@@ -0,0 +1,40 @@
1
+ export interface ResponseShape {
2
+ keys: string[];
3
+ types: Record<string, 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'>;
4
+ status?: string;
5
+ capturedAt: string;
6
+ }
7
+ export interface ProviderBaseline {
8
+ providerName: string;
9
+ operation: string;
10
+ shape: ResponseShape;
11
+ libVersion: string;
12
+ }
13
+ export interface DriftReport {
14
+ providerName: string;
15
+ driftDetected: boolean;
16
+ addedKeys: string[];
17
+ removedKeys: string[];
18
+ typeChanges: Array<{
19
+ key: string;
20
+ oldType: string;
21
+ newType: string;
22
+ }>;
23
+ statusChanged?: {
24
+ old: string;
25
+ new: string;
26
+ };
27
+ baselineCapturedAt: string;
28
+ newCapturedAt: string;
29
+ }
30
+ export declare function captureShape(response: unknown): ResponseShape;
31
+ export declare function compareShapes(baseline: ResponseShape, current: ResponseShape): {
32
+ addedKeys: string[];
33
+ removedKeys: string[];
34
+ typeChanges: Array<{
35
+ key: string;
36
+ oldType: string;
37
+ newType: string;
38
+ }>;
39
+ };
40
+ export declare function diffBaseline(baseline: ProviderBaseline, currentShape: ResponseShape, providerName: string): DriftReport;
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.captureShape = captureShape;
4
+ exports.compareShapes = compareShapes;
5
+ exports.diffBaseline = diffBaseline;
6
+ function getType(value) {
7
+ if (value === null)
8
+ return 'null';
9
+ if (Array.isArray(value))
10
+ return 'array';
11
+ return typeof value;
12
+ }
13
+ function flattenKeys(obj, prefix = '') {
14
+ const result = [];
15
+ if (obj === null || obj === undefined) {
16
+ return result;
17
+ }
18
+ if (Array.isArray(obj)) {
19
+ if (obj.length === 0) {
20
+ return result;
21
+ }
22
+ for (const item of obj) {
23
+ const nested = flattenKeys(item, `${prefix}[*]`);
24
+ result.push(...nested);
25
+ }
26
+ }
27
+ else if (typeof obj === 'object') {
28
+ for (const [key, value] of Object.entries(obj)) {
29
+ const path = prefix ? `${prefix}.${key}` : key;
30
+ const valueType = getType(value);
31
+ if (valueType === 'object' || valueType === 'array') {
32
+ const nested = flattenKeys(value, path);
33
+ result.push(...nested);
34
+ }
35
+ else {
36
+ result.push({ path, type: valueType });
37
+ }
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ function deduplicateKeys(entries) {
43
+ const seen = new Map();
44
+ for (const entry of entries) {
45
+ if (!seen.has(entry.path)) {
46
+ seen.set(entry.path, entry.type);
47
+ }
48
+ }
49
+ return Array.from(seen.entries()).map(([path, type]) => ({ path, type }));
50
+ }
51
+ function captureShape(response) {
52
+ const entries = flattenKeys(response);
53
+ const deduplicated = deduplicateKeys(entries);
54
+ const sorted = deduplicated.sort((a, b) => a.path.localeCompare(b.path));
55
+ const keys = sorted.map((e) => e.path);
56
+ const types = {};
57
+ for (const entry of sorted) {
58
+ types[entry.path] = entry.type;
59
+ }
60
+ return {
61
+ keys,
62
+ types,
63
+ capturedAt: new Date().toISOString(),
64
+ };
65
+ }
66
+ function compareShapes(baseline, current) {
67
+ const baselineSet = new Set(baseline.keys);
68
+ const currentSet = new Set(current.keys);
69
+ const addedKeys = current.keys.filter((k) => !baselineSet.has(k));
70
+ const removedKeys = baseline.keys.filter((k) => !currentSet.has(k));
71
+ const typeChanges = [];
72
+ for (const key of current.keys) {
73
+ if (baselineSet.has(key)) {
74
+ const oldType = baseline.types[key];
75
+ const newType = current.types[key];
76
+ if (oldType !== newType) {
77
+ typeChanges.push({ key, oldType, newType });
78
+ }
79
+ }
80
+ }
81
+ return { addedKeys, removedKeys, typeChanges };
82
+ }
83
+ function diffBaseline(baseline, currentShape, providerName) {
84
+ const diff = compareShapes(baseline.shape, currentShape);
85
+ let statusChanged;
86
+ if (baseline.shape.status && currentShape.status && baseline.shape.status !== currentShape.status) {
87
+ statusChanged = { old: baseline.shape.status, new: currentShape.status };
88
+ }
89
+ const driftDetected = diff.addedKeys.length > 0 ||
90
+ diff.removedKeys.length > 0 ||
91
+ diff.typeChanges.length > 0 ||
92
+ !!statusChanged;
93
+ return {
94
+ providerName,
95
+ driftDetected,
96
+ addedKeys: diff.addedKeys,
97
+ removedKeys: diff.removedKeys,
98
+ typeChanges: diff.typeChanges,
99
+ statusChanged,
100
+ baselineCapturedAt: baseline.shape.capturedAt,
101
+ newCapturedAt: currentShape.capturedAt,
102
+ };
103
+ }
@@ -91,7 +91,7 @@ class SquareProvider extends base_1.PaymentProvider {
91
91
  buyer_email: params.customer.email,
92
92
  },
93
93
  };
94
- const response = await this.apiRequest('POST', '/checkout/payment-links', requestBody);
94
+ const response = await this.apiRequest('POST', '/online-checkout/payment-links', requestBody);
95
95
  const link = response.payment_link;
96
96
  return {
97
97
  id: link.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paybridge",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
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",
@@ -14,7 +14,7 @@
14
14
  "cli": "node dist/cli/index.js",
15
15
  "prepack": "npm run clean && npm run build:cli",
16
16
  "prepublishOnly": "npm run clean && npm run build:cli",
17
- "test": "tsc && tsc --project tsconfig.test.json && node --test 'dist-test/**/*.test.js'",
17
+ "test": "tsc && tsc --project tsconfig.test.json && find dist-test -name '*.test.js' -exec node --test {} +",
18
18
  "test:e2e:moonpay": "tsx tests/e2e/moonpay-sandbox.ts",
19
19
  "test:e2e:yellowcard": "tsx tests/e2e/yellowcard-sandbox.ts",
20
20
  "test:e2e:sandbox": "tsx tests/e2e/sandbox-validate.ts"
@@ -46,7 +46,10 @@
46
46
  "ramp",
47
47
  "east-africa",
48
48
  "cli",
49
- "command-line"
49
+ "command-line",
50
+ "drift-detection",
51
+ "monitoring",
52
+ "api-validation"
50
53
  ],
51
54
  "author": "Kobie Wentzel",
52
55
  "license": "MIT",