paygate-mcp 4.8.0 → 5.0.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/dist/server.js CHANGED
@@ -364,6 +364,8 @@ class PayGateServer {
364
364
  return this.handleResumeKey(req, res);
365
365
  case '/keys/clone':
366
366
  return this.handleCloneKey(req, res);
367
+ case '/keys/alias':
368
+ return this.handleSetAlias(req, res);
367
369
  case '/keys/rotate':
368
370
  return this.handleRotateKey(req, res);
369
371
  case '/keys/acl':
@@ -436,6 +438,10 @@ class PayGateServer {
436
438
  return this.handleWebhookStats(req, res);
437
439
  case '/webhooks/log':
438
440
  return this.handleWebhookLog(req, res);
441
+ case '/webhooks/pause':
442
+ return this.handleWebhookPause(req, res);
443
+ case '/webhooks/resume':
444
+ return this.handleWebhookResume(req, res);
439
445
  case '/webhooks/test':
440
446
  return this.handleWebhookTest(req, res);
441
447
  case '/webhooks/filters':
@@ -885,6 +891,7 @@ class PayGateServer {
885
891
  suspendKey: 'POST /keys/suspend — Temporarily suspend a key (requires X-Admin-Key)',
886
892
  resumeKey: 'POST /keys/resume — Resume a suspended key (requires X-Admin-Key)',
887
893
  cloneKey: 'POST /keys/clone — Clone a key with same config (requires X-Admin-Key)',
894
+ keyAlias: 'POST /keys/alias — Set or clear a human-readable alias for a key (requires X-Admin-Key)',
888
895
  rotateKey: 'POST /keys/rotate — Rotate a key (requires X-Admin-Key)',
889
896
  setAcl: 'POST /keys/acl — Set tool ACL (requires X-Admin-Key)',
890
897
  setExpiry: 'POST /keys/expiry — Set key expiry (requires X-Admin-Key)',
@@ -914,6 +921,8 @@ class PayGateServer {
914
921
  webhookTest: 'POST /webhooks/test — Send test event to webhook URL and return result (requires X-Admin-Key)',
915
922
  webhookStats: 'GET /webhooks/stats — Webhook delivery statistics (requires X-Admin-Key)',
916
923
  webhookLog: 'GET /webhooks/log — Webhook delivery log with status, timing, and filters (requires X-Admin-Key)',
924
+ webhookPause: 'POST /webhooks/pause — Pause webhook delivery (events buffered) (requires X-Admin-Key)',
925
+ webhookResume: 'POST /webhooks/resume — Resume webhook delivery and flush buffered events (requires X-Admin-Key)',
917
926
  webhookFilters: 'GET|POST /webhooks/filters — List or create webhook filter rules (requires X-Admin-Key)',
918
927
  updateWebhookFilter: 'POST /webhooks/filters/update — Update a webhook filter rule (requires X-Admin-Key)',
919
928
  deleteWebhookFilter: 'POST /webhooks/filters/delete — Delete a webhook filter rule (requires X-Admin-Key)',
@@ -1114,20 +1123,23 @@ class PayGateServer {
1114
1123
  res.end(JSON.stringify({ error: 'Credits must be a positive integer' }));
1115
1124
  return;
1116
1125
  }
1126
+ // Resolve alias to actual key
1127
+ const resolved = this.gate.store.resolveKey(params.key);
1128
+ const actualKey = resolved ? resolved.key : params.key;
1117
1129
  // Use Redis atomic topup when available, fall back to local store
1118
1130
  let success;
1119
1131
  if (this.redisSync) {
1120
- success = await this.redisSync.atomicTopup(params.key, credits);
1132
+ success = await this.redisSync.atomicTopup(actualKey, credits);
1121
1133
  }
1122
1134
  else {
1123
- success = this.gate.store.addCredits(params.key, credits);
1135
+ success = this.gate.store.addCredits(actualKey, credits);
1124
1136
  }
1125
1137
  if (!success) {
1126
1138
  res.writeHead(404, { 'Content-Type': 'application/json' });
1127
1139
  res.end(JSON.stringify({ error: 'Key not found or inactive' }));
1128
1140
  return;
1129
1141
  }
1130
- const record = this.gate.store.getKey(params.key);
1142
+ const record = this.gate.store.getKey(actualKey);
1131
1143
  this.audit.log('key.topup', 'admin', `Added ${credits} credits`, {
1132
1144
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1133
1145
  creditsAdded: credits,
@@ -1175,7 +1187,7 @@ class PayGateServer {
1175
1187
  return;
1176
1188
  }
1177
1189
  // Validate source key exists and has enough credits
1178
- const sourceRecord = this.gate.store.getKey(params.from);
1190
+ const sourceRecord = this.gate.store.resolveKey(params.from);
1179
1191
  if (!sourceRecord) {
1180
1192
  res.writeHead(404, { 'Content-Type': 'application/json' });
1181
1193
  res.end(JSON.stringify({ error: 'Source key not found' }));
@@ -1189,7 +1201,7 @@ class PayGateServer {
1189
1201
  return;
1190
1202
  }
1191
1203
  // Validate destination key exists (getKey returns null for revoked/expired keys)
1192
- const destRecord = this.gate.store.getKey(params.to);
1204
+ const destRecord = this.gate.store.resolveKey(params.to);
1193
1205
  if (!destRecord) {
1194
1206
  res.writeHead(404, { 'Content-Type': 'application/json' });
1195
1207
  res.end(JSON.stringify({ error: 'Destination key not found' }));
@@ -1198,13 +1210,13 @@ class PayGateServer {
1198
1210
  // Perform transfer atomically (deduct from source, add to destination)
1199
1211
  if (this.redisSync) {
1200
1212
  // Redis atomic transfer: deduct first, then add
1201
- const deducted = await this.redisSync.atomicDeduct(params.from, credits);
1213
+ const deducted = await this.redisSync.atomicDeduct(sourceRecord.key, credits);
1202
1214
  if (!deducted) {
1203
1215
  res.writeHead(400, { 'Content-Type': 'application/json' });
1204
1216
  res.end(JSON.stringify({ error: 'Redis deduction failed (insufficient credits or key not found)' }));
1205
1217
  return;
1206
1218
  }
1207
- await this.redisSync.atomicTopup(params.to, credits);
1219
+ await this.redisSync.atomicTopup(destRecord.key, credits);
1208
1220
  }
1209
1221
  else {
1210
1222
  // Local store: deduct and add
@@ -1212,20 +1224,20 @@ class PayGateServer {
1212
1224
  destRecord.credits += credits;
1213
1225
  this.gate.store.save();
1214
1226
  }
1215
- const fromBalance = this.gate.store.getKey(params.from)?.credits ?? 0;
1216
- const toBalance = this.gate.store.getKey(params.to)?.credits ?? 0;
1227
+ const fromBalance = sourceRecord.credits;
1228
+ const toBalance = destRecord.credits;
1217
1229
  const memo = params.memo || '';
1218
1230
  this.audit.log('key.credits_transferred', 'admin', `Transferred ${credits} credits`, {
1219
- fromKeyMasked: (0, audit_1.maskKeyForAudit)(params.from),
1220
- toKeyMasked: (0, audit_1.maskKeyForAudit)(params.to),
1231
+ fromKeyMasked: (0, audit_1.maskKeyForAudit)(sourceRecord.key),
1232
+ toKeyMasked: (0, audit_1.maskKeyForAudit)(destRecord.key),
1221
1233
  credits,
1222
1234
  fromBalance,
1223
1235
  toBalance,
1224
1236
  memo,
1225
1237
  });
1226
1238
  this.emitWebhookAdmin('key.credits_transferred', 'admin', {
1227
- fromKeyMasked: (0, audit_1.maskKeyForAudit)(params.from),
1228
- toKeyMasked: (0, audit_1.maskKeyForAudit)(params.to),
1239
+ fromKeyMasked: (0, audit_1.maskKeyForAudit)(sourceRecord.key),
1240
+ toKeyMasked: (0, audit_1.maskKeyForAudit)(destRecord.key),
1229
1241
  credits,
1230
1242
  fromBalance,
1231
1243
  toBalance,
@@ -1234,8 +1246,8 @@ class PayGateServer {
1234
1246
  res.writeHead(200, { 'Content-Type': 'application/json' });
1235
1247
  res.end(JSON.stringify({
1236
1248
  transferred: credits,
1237
- from: { keyMasked: (0, audit_1.maskKeyForAudit)(params.from), balance: fromBalance },
1238
- to: { keyMasked: (0, audit_1.maskKeyForAudit)(params.to), balance: toBalance },
1249
+ from: { keyMasked: (0, audit_1.maskKeyForAudit)(sourceRecord.key), balance: fromBalance, credits: fromBalance },
1250
+ to: { keyMasked: (0, audit_1.maskKeyForAudit)(destRecord.key), balance: toBalance, credits: toBalance },
1239
1251
  memo: memo || undefined,
1240
1252
  message: `Transferred ${credits} credits`,
1241
1253
  }));
@@ -1509,13 +1521,16 @@ class PayGateServer {
1509
1521
  res.end(JSON.stringify({ error: 'Missing key' }));
1510
1522
  return;
1511
1523
  }
1524
+ // Resolve alias to actual key
1525
+ const resolved = this.gate.store.resolveKeyRaw(params.key);
1526
+ const actualKey = resolved ? resolved.key : params.key;
1512
1527
  // Use Redis-backed revoke when available (broadcasts to other instances)
1513
1528
  let success;
1514
1529
  if (this.redisSync) {
1515
- success = await this.redisSync.revokeKey(params.key);
1530
+ success = await this.redisSync.revokeKey(actualKey);
1516
1531
  }
1517
1532
  else {
1518
- success = this.gate.store.revokeKey(params.key);
1533
+ success = this.gate.store.revokeKey(actualKey);
1519
1534
  }
1520
1535
  if (!success) {
1521
1536
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1523,13 +1538,13 @@ class PayGateServer {
1523
1538
  return;
1524
1539
  }
1525
1540
  this.audit.log('key.revoked', 'admin', `Key revoked`, {
1526
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1541
+ keyMasked: (0, audit_1.maskKeyForAudit)(actualKey),
1527
1542
  });
1528
1543
  this.emitWebhookAdmin('key.revoked', 'admin', {
1529
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1544
+ keyMasked: (0, audit_1.maskKeyForAudit)(actualKey),
1530
1545
  });
1531
1546
  res.writeHead(200, { 'Content-Type': 'application/json' });
1532
- res.end(JSON.stringify({ message: 'Key revoked' }));
1547
+ res.end(JSON.stringify({ message: 'Key revoked', revoked: true }));
1533
1548
  }
1534
1549
  // ─── /keys/suspend — Temporarily suspend a key ─────────────────────────────
1535
1550
  async handleSuspendKey(req, res) {
@@ -1555,7 +1570,7 @@ class PayGateServer {
1555
1570
  res.end(JSON.stringify({ error: 'Missing key' }));
1556
1571
  return;
1557
1572
  }
1558
- const record = this.gate.store.getKeyRaw(params.key);
1573
+ const record = this.gate.store.resolveKeyRaw(params.key);
1559
1574
  if (!record) {
1560
1575
  res.writeHead(404, { 'Content-Type': 'application/json' });
1561
1576
  res.end(JSON.stringify({ error: 'Key not found' }));
@@ -1571,19 +1586,19 @@ class PayGateServer {
1571
1586
  res.end(JSON.stringify({ error: 'Key is already suspended' }));
1572
1587
  return;
1573
1588
  }
1574
- const success = this.gate.store.suspendKey(params.key);
1589
+ const success = this.gate.store.suspendKey(record.key);
1575
1590
  if (!success) {
1576
1591
  res.writeHead(500, { 'Content-Type': 'application/json' });
1577
1592
  res.end(JSON.stringify({ error: 'Failed to suspend key' }));
1578
1593
  return;
1579
1594
  }
1580
- this.syncKeyMutation(params.key);
1595
+ this.syncKeyMutation(record.key);
1581
1596
  this.audit.log('key.suspended', 'admin', `Key suspended${params.reason ? ': ' + params.reason : ''}`, {
1582
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1597
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1583
1598
  reason: params.reason || null,
1584
1599
  });
1585
1600
  this.emitWebhookAdmin('key.suspended', 'admin', {
1586
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1601
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1587
1602
  reason: params.reason || null,
1588
1603
  });
1589
1604
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1613,7 +1628,7 @@ class PayGateServer {
1613
1628
  res.end(JSON.stringify({ error: 'Missing key' }));
1614
1629
  return;
1615
1630
  }
1616
- const record = this.gate.store.getKeyRaw(params.key);
1631
+ const record = this.gate.store.resolveKeyRaw(params.key);
1617
1632
  if (!record) {
1618
1633
  res.writeHead(404, { 'Content-Type': 'application/json' });
1619
1634
  res.end(JSON.stringify({ error: 'Key not found' }));
@@ -1629,18 +1644,18 @@ class PayGateServer {
1629
1644
  res.end(JSON.stringify({ error: 'Key is not suspended' }));
1630
1645
  return;
1631
1646
  }
1632
- const success = this.gate.store.resumeKey(params.key);
1647
+ const success = this.gate.store.resumeKey(record.key);
1633
1648
  if (!success) {
1634
1649
  res.writeHead(500, { 'Content-Type': 'application/json' });
1635
1650
  res.end(JSON.stringify({ error: 'Failed to resume key' }));
1636
1651
  return;
1637
1652
  }
1638
- this.syncKeyMutation(params.key);
1653
+ this.syncKeyMutation(record.key);
1639
1654
  this.audit.log('key.resumed', 'admin', 'Key resumed', {
1640
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1655
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1641
1656
  });
1642
1657
  this.emitWebhookAdmin('key.resumed', 'admin', {
1643
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1658
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1644
1659
  });
1645
1660
  res.writeHead(200, { 'Content-Type': 'application/json' });
1646
1661
  res.end(JSON.stringify({ message: 'Key resumed', suspended: false }));
@@ -1670,7 +1685,7 @@ class PayGateServer {
1670
1685
  return;
1671
1686
  }
1672
1687
  // Use getKeyRaw to allow cloning suspended/expired keys (but not revoked)
1673
- const source = this.gate.store.getKeyRaw(params.key);
1688
+ const source = this.gate.store.resolveKeyRaw(params.key);
1674
1689
  if (!source) {
1675
1690
  res.writeHead(404, { 'Content-Type': 'application/json' });
1676
1691
  res.end(JSON.stringify({ error: 'Source key not found' }));
@@ -1681,7 +1696,7 @@ class PayGateServer {
1681
1696
  res.end(JSON.stringify({ error: 'Cannot clone a revoked key' }));
1682
1697
  return;
1683
1698
  }
1684
- const cloned = this.gate.store.cloneKey(params.key, {
1699
+ const cloned = this.gate.store.cloneKey(source.key, {
1685
1700
  name: params.name,
1686
1701
  credits: params.credits,
1687
1702
  tags: params.tags,
@@ -1694,14 +1709,14 @@ class PayGateServer {
1694
1709
  }
1695
1710
  // Sync new key to Redis
1696
1711
  this.syncKeyMutation(cloned.key);
1697
- this.audit.log('key.cloned', 'admin', `Key cloned from ${(0, audit_1.maskKeyForAudit)(params.key)}`, {
1698
- sourceKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1712
+ this.audit.log('key.cloned', 'admin', `Key cloned from ${(0, audit_1.maskKeyForAudit)(source.key)}`, {
1713
+ sourceKeyMasked: (0, audit_1.maskKeyForAudit)(source.key),
1699
1714
  newKeyMasked: (0, audit_1.maskKeyForAudit)(cloned.key),
1700
1715
  name: cloned.name,
1701
1716
  credits: cloned.credits,
1702
1717
  });
1703
1718
  this.emitWebhookAdmin('key.cloned', 'admin', {
1704
- sourceKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1719
+ sourceKeyMasked: (0, audit_1.maskKeyForAudit)(source.key),
1705
1720
  newKeyMasked: (0, audit_1.maskKeyForAudit)(cloned.key),
1706
1721
  name: cloned.name,
1707
1722
  credits: cloned.credits,
@@ -1712,6 +1727,7 @@ class PayGateServer {
1712
1727
  key: cloned.key,
1713
1728
  name: cloned.name,
1714
1729
  credits: cloned.credits,
1730
+ clonedFrom: source.key.slice(0, 10) + '...',
1715
1731
  sourceName: source.name,
1716
1732
  allowedTools: cloned.allowedTools,
1717
1733
  deniedTools: cloned.deniedTools,
@@ -1724,6 +1740,65 @@ class PayGateServer {
1724
1740
  spendingLimit: cloned.spendingLimit,
1725
1741
  }));
1726
1742
  }
1743
+ // ─── /keys/alias — Set or clear key alias ──────────────────────────────────
1744
+ async handleSetAlias(req, res) {
1745
+ if (req.method !== 'POST') {
1746
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1747
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
1748
+ return;
1749
+ }
1750
+ if (!this.checkAdmin(req, res))
1751
+ return;
1752
+ const raw = await this.readBody(req);
1753
+ const params = JSON.parse(raw);
1754
+ if (!params.key) {
1755
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1756
+ res.end(JSON.stringify({ error: 'Missing "key" parameter' }));
1757
+ return;
1758
+ }
1759
+ // Resolve the key (support existing aliases for the source key)
1760
+ const record = this.gate.store.resolveKeyRaw(params.key);
1761
+ if (!record) {
1762
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1763
+ res.end(JSON.stringify({ error: 'Key not found' }));
1764
+ return;
1765
+ }
1766
+ const alias = params.alias !== undefined ? (params.alias === null || params.alias === '' ? null : String(params.alias)) : undefined;
1767
+ if (alias === undefined) {
1768
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1769
+ res.end(JSON.stringify({ error: 'Missing "alias" parameter (string to set, null to clear)' }));
1770
+ return;
1771
+ }
1772
+ const result = this.gate.store.setAlias(record.key, alias);
1773
+ if (!result.success) {
1774
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1775
+ res.end(JSON.stringify({ error: result.error }));
1776
+ return;
1777
+ }
1778
+ const action = alias ? `set to "${alias}"` : 'cleared';
1779
+ this.audit.log('key.alias_set', 'admin', `Key alias ${action} for ${record.key.slice(0, 10)}...`, {
1780
+ key: record.key.slice(0, 10),
1781
+ alias: alias || null,
1782
+ });
1783
+ // Sync to Redis if configured
1784
+ if (typeof this.syncKeyMutation === 'function') {
1785
+ this.syncKeyMutation(record.key);
1786
+ }
1787
+ // Webhook event
1788
+ if (this.gate.webhook) {
1789
+ this.gate.webhook.emitAdmin('key.created', 'admin', {
1790
+ key: record.key.slice(0, 10),
1791
+ alias: alias || null,
1792
+ event: 'alias_set',
1793
+ });
1794
+ }
1795
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1796
+ res.end(JSON.stringify({
1797
+ key: record.key.slice(0, 10) + '...',
1798
+ alias: record.alias || null,
1799
+ message: `Alias ${action}`,
1800
+ }));
1801
+ }
1727
1802
  // ─── /keys/rotate — Rotate API key ─────────────────────────────────────────
1728
1803
  async handleRotateKey(req, res) {
1729
1804
  if (req.method !== 'POST') {
@@ -1807,7 +1882,7 @@ class PayGateServer {
1807
1882
  return;
1808
1883
  }
1809
1884
  this.syncKeyMutation(params.key);
1810
- const record = this.gate.store.getKey(params.key);
1885
+ const record = this.gate.store.resolveKey(params.key);
1811
1886
  this.audit.log('key.acl_updated', 'admin', `ACL updated`, {
1812
1887
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1813
1888
  allowedTools: record?.allowedTools || [],
@@ -1859,7 +1934,7 @@ class PayGateServer {
1859
1934
  return;
1860
1935
  }
1861
1936
  this.syncKeyMutation(params.key);
1862
- const record = this.gate.store.getKeyRaw(params.key);
1937
+ const record = this.gate.store.resolveKeyRaw(params.key);
1863
1938
  this.audit.log('key.expiry_updated', 'admin', expiresAt ? `Key expiry set to ${expiresAt}` : 'Key expiry removed', {
1864
1939
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1865
1940
  expiresAt: record?.expiresAt || null,
@@ -1962,7 +2037,7 @@ class PayGateServer {
1962
2037
  return;
1963
2038
  }
1964
2039
  this.syncKeyMutation(params.key);
1965
- const record = this.gate.store.getKey(params.key);
2040
+ const record = this.gate.store.resolveKey(params.key);
1966
2041
  this.audit.log('key.tags_updated', 'admin', `Tags updated`, {
1967
2042
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1968
2043
  tags: record?.tags || {},
@@ -2009,7 +2084,7 @@ class PayGateServer {
2009
2084
  return;
2010
2085
  }
2011
2086
  this.syncKeyMutation(params.key);
2012
- const record = this.gate.store.getKey(params.key);
2087
+ const record = this.gate.store.resolveKey(params.key);
2013
2088
  this.audit.log('key.ip_updated', 'admin', `IP allowlist updated`, {
2014
2089
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
2015
2090
  ipAllowlist: record?.ipAllowlist || [],
@@ -2066,14 +2141,14 @@ class PayGateServer {
2066
2141
  res.end(JSON.stringify({ error: 'Missing key query parameter' }));
2067
2142
  return;
2068
2143
  }
2069
- // Verify key exists (use getKeyRaw to allow querying expired/suspended keys)
2070
- const record = this.gate.store.getKeyRaw(key);
2144
+ // Verify key exists (use resolveKeyRaw to allow querying by alias and expired/suspended keys)
2145
+ const record = this.gate.store.resolveKeyRaw(key);
2071
2146
  if (!record) {
2072
2147
  res.writeHead(404, { 'Content-Type': 'application/json' });
2073
2148
  res.end(JSON.stringify({ error: 'Key not found' }));
2074
2149
  return;
2075
2150
  }
2076
- const usage = this.gate.meter.getKeyUsage(key, since);
2151
+ const usage = this.gate.meter.getKeyUsage(record.key, since);
2077
2152
  res.writeHead(200, { 'Content-Type': 'application/json' });
2078
2153
  res.end(JSON.stringify({
2079
2154
  key: key.slice(0, 10) + '...',
@@ -2109,7 +2184,7 @@ class PayGateServer {
2109
2184
  res.end(JSON.stringify({ error: 'Missing key' }));
2110
2185
  return;
2111
2186
  }
2112
- const record = this.gate.store.getKey(params.key);
2187
+ const record = this.gate.store.resolveKey(params.key);
2113
2188
  if (!record) {
2114
2189
  res.writeHead(404, { 'Content-Type': 'application/json' });
2115
2190
  res.end(JSON.stringify({ error: 'Key not found or inactive' }));
@@ -2230,7 +2305,7 @@ class PayGateServer {
2230
2305
  res.end(JSON.stringify({ error: 'Missing key' }));
2231
2306
  return;
2232
2307
  }
2233
- const record = this.gate.store.getKey(params.key);
2308
+ const record = this.gate.store.resolveKey(params.key);
2234
2309
  if (!record) {
2235
2310
  res.writeHead(404, { 'Content-Type': 'application/json' });
2236
2311
  res.end(JSON.stringify({ error: 'Key not found or inactive' }));
@@ -2983,6 +3058,61 @@ class PayGateServer {
2983
3058
  entries,
2984
3059
  }));
2985
3060
  }
3061
+ // ─── /webhooks/pause — Pause webhook delivery ─────────────────────────────
3062
+ handleWebhookPause(req, res) {
3063
+ if (req.method !== 'POST') {
3064
+ res.writeHead(405, { 'Content-Type': 'application/json' });
3065
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
3066
+ return;
3067
+ }
3068
+ if (!this.checkAdmin(req, res))
3069
+ return;
3070
+ if (!this.gate.webhook) {
3071
+ res.writeHead(400, { 'Content-Type': 'application/json' });
3072
+ res.end(JSON.stringify({ error: 'No webhook configured' }));
3073
+ return;
3074
+ }
3075
+ const paused = this.gate.webhook.pause();
3076
+ if (!paused) {
3077
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3078
+ res.end(JSON.stringify({ paused: true, message: 'Already paused' }));
3079
+ return;
3080
+ }
3081
+ this.audit.log('webhook.pause', 'admin', 'Webhook delivery paused');
3082
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3083
+ res.end(JSON.stringify({
3084
+ paused: true,
3085
+ message: 'Webhook delivery paused. Events will be buffered until resumed.',
3086
+ }));
3087
+ }
3088
+ // ─── /webhooks/resume — Resume webhook delivery ───────────────────────────
3089
+ handleWebhookResume(req, res) {
3090
+ if (req.method !== 'POST') {
3091
+ res.writeHead(405, { 'Content-Type': 'application/json' });
3092
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
3093
+ return;
3094
+ }
3095
+ if (!this.checkAdmin(req, res))
3096
+ return;
3097
+ if (!this.gate.webhook) {
3098
+ res.writeHead(400, { 'Content-Type': 'application/json' });
3099
+ res.end(JSON.stringify({ error: 'No webhook configured' }));
3100
+ return;
3101
+ }
3102
+ const result = this.gate.webhook.resume();
3103
+ if (!result.resumed) {
3104
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3105
+ res.end(JSON.stringify({ paused: false, message: 'Not paused' }));
3106
+ return;
3107
+ }
3108
+ this.audit.log('webhook.resume', 'admin', `Webhook delivery resumed, ${result.flushedEvents} buffered events flushed`);
3109
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3110
+ res.end(JSON.stringify({
3111
+ paused: false,
3112
+ message: 'Webhook delivery resumed.',
3113
+ flushedEvents: result.flushedEvents,
3114
+ }));
3115
+ }
2986
3116
  // ─── /webhooks/test — Send test event ────────────────────────────────────
2987
3117
  async handleWebhookTest(req, res) {
2988
3118
  if (req.method !== 'POST') {
@@ -3478,7 +3608,7 @@ class PayGateServer {
3478
3608
  return;
3479
3609
  }
3480
3610
  // Verify the key exists
3481
- const keyRecord = this.gate.store.getKey(params.key);
3611
+ const keyRecord = this.gate.store.resolveKey(params.key);
3482
3612
  if (!keyRecord) {
3483
3613
  res.writeHead(404, { 'Content-Type': 'application/json' });
3484
3614
  res.end(JSON.stringify({ error: 'API key not found' }));
@@ -3597,7 +3727,7 @@ class PayGateServer {
3597
3727
  return;
3598
3728
  }
3599
3729
  // Verify the parent key exists and is active
3600
- const keyRecord = this.gate.store.getKey(params.key);
3730
+ const keyRecord = this.gate.store.resolveKey(params.key);
3601
3731
  if (!keyRecord || !keyRecord.active) {
3602
3732
  res.writeHead(404, { 'Content-Type': 'application/json' });
3603
3733
  res.end(JSON.stringify({ error: 'API key not found or inactive' }));