paygate-mcp 4.9.0 → 5.1.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
@@ -83,6 +83,7 @@ const tokens_1 = require("./tokens");
83
83
  const admin_keys_1 = require("./admin-keys");
84
84
  const plugin_1 = require("./plugin");
85
85
  const groups_1 = require("./groups");
86
+ const expiry_scanner_1 = require("./expiry-scanner");
86
87
  /** Max request body size: 1MB */
87
88
  const MAX_BODY_SIZE = 1_048_576;
88
89
  class PayGateServer {
@@ -121,6 +122,8 @@ class PayGateServer {
121
122
  /** Plugin manager for extensible middleware hooks */
122
123
  plugins;
123
124
  groups;
125
+ /** Background key expiry scanner */
126
+ expiryScanner;
124
127
  /** Server start time (ms since epoch) */
125
128
  startedAt = Date.now();
126
129
  /** Whether the server is draining (shutting down gracefully) */
@@ -239,6 +242,24 @@ class PayGateServer {
239
242
  this.metrics.registerGauge('paygate_groups_total', 'Number of active key groups', () => {
240
243
  return this.groups.count;
241
244
  });
245
+ // Key expiry scanner — proactive background scanning for expiring keys
246
+ const scannerConfig = this.config.expiryScanner;
247
+ this.expiryScanner = new expiry_scanner_1.ExpiryScanner(scannerConfig ? {
248
+ enabled: scannerConfig.enabled !== false,
249
+ intervalSeconds: scannerConfig.intervalSeconds || 3600,
250
+ thresholds: scannerConfig.thresholds || [604800, 86400, 3600],
251
+ } : undefined);
252
+ // Wire scanner callbacks: audit + webhook
253
+ this.expiryScanner.onWarning = (warning) => {
254
+ const keyMasked = (0, audit_1.maskKeyForAudit)(warning.key);
255
+ this.audit.log('key.expiry_warning', 'system', `Key "${warning.name}" expires in ${warning.remainingHuman} (threshold: ${warning.thresholdSeconds}s)`, { keyMasked, expiresAt: warning.expiresAt, remainingSeconds: warning.remainingSeconds, thresholdSeconds: warning.thresholdSeconds, alias: warning.alias || null });
256
+ this.emitWebhookAdmin('key.expiry_warning', 'system', {
257
+ keyMasked, keyName: warning.name, alias: warning.alias || null,
258
+ namespace: warning.namespace, expiresAt: warning.expiresAt,
259
+ remainingSeconds: warning.remainingSeconds, remainingHuman: warning.remainingHuman,
260
+ thresholdSeconds: warning.thresholdSeconds,
261
+ });
262
+ };
242
263
  // Scoped token manager (uses bootstrap admin key as signing secret, padded to min length)
243
264
  const tokenSecret = this.bootstrapAdminKey.length >= 8
244
265
  ? this.bootstrapAdminKey
@@ -298,6 +319,8 @@ class PayGateServer {
298
319
  console.log('[paygate] Redis distributed state enabled');
299
320
  }
300
321
  await this.handler.start();
322
+ // Start the key expiry scanner (proactive background scanning)
323
+ this.expiryScanner.start(() => this.gate.store.getAllRecords());
301
324
  // Plugin lifecycle: onStart
302
325
  if (this.plugins.count > 0) {
303
326
  await this.plugins.executeStart();
@@ -364,6 +387,8 @@ class PayGateServer {
364
387
  return this.handleResumeKey(req, res);
365
388
  case '/keys/clone':
366
389
  return this.handleCloneKey(req, res);
390
+ case '/keys/alias':
391
+ return this.handleSetAlias(req, res);
367
392
  case '/keys/rotate':
368
393
  return this.handleRotateKey(req, res);
369
394
  case '/keys/acl':
@@ -382,6 +407,8 @@ class PayGateServer {
382
407
  return this.handleSetAutoTopup(req, res);
383
408
  case '/keys/usage':
384
409
  return this.handleKeyUsage(req, res);
410
+ case '/keys/expiring':
411
+ return this.handleKeysExpiring(req, res);
385
412
  case '/topup':
386
413
  return this.handleTopUp(req, res);
387
414
  case '/keys/transfer':
@@ -889,6 +916,7 @@ class PayGateServer {
889
916
  suspendKey: 'POST /keys/suspend — Temporarily suspend a key (requires X-Admin-Key)',
890
917
  resumeKey: 'POST /keys/resume — Resume a suspended key (requires X-Admin-Key)',
891
918
  cloneKey: 'POST /keys/clone — Clone a key with same config (requires X-Admin-Key)',
919
+ keyAlias: 'POST /keys/alias — Set or clear a human-readable alias for a key (requires X-Admin-Key)',
892
920
  rotateKey: 'POST /keys/rotate — Rotate a key (requires X-Admin-Key)',
893
921
  setAcl: 'POST /keys/acl — Set tool ACL (requires X-Admin-Key)',
894
922
  setExpiry: 'POST /keys/expiry — Set key expiry (requires X-Admin-Key)',
@@ -905,6 +933,7 @@ class PayGateServer {
905
933
  searchKeys: 'POST /keys/search — Search keys by tags (requires X-Admin-Key)',
906
934
  autoTopup: 'POST /keys/auto-topup — Configure auto-topup for a key (requires X-Admin-Key)',
907
935
  keyUsage: 'GET /keys/usage?key=... — Per-key usage breakdown (requires X-Admin-Key)',
936
+ keysExpiring: 'GET /keys/expiring?within=86400 — List keys expiring within N seconds (requires X-Admin-Key)',
908
937
  pricing: 'GET /pricing — Tool pricing breakdown (public)',
909
938
  mcpPayment: 'GET /.well-known/mcp-payment — Payment metadata (SEP-2007)',
910
939
  audit: 'GET /audit — Query audit log (requires X-Admin-Key)',
@@ -1120,20 +1149,23 @@ class PayGateServer {
1120
1149
  res.end(JSON.stringify({ error: 'Credits must be a positive integer' }));
1121
1150
  return;
1122
1151
  }
1152
+ // Resolve alias to actual key
1153
+ const resolved = this.gate.store.resolveKey(params.key);
1154
+ const actualKey = resolved ? resolved.key : params.key;
1123
1155
  // Use Redis atomic topup when available, fall back to local store
1124
1156
  let success;
1125
1157
  if (this.redisSync) {
1126
- success = await this.redisSync.atomicTopup(params.key, credits);
1158
+ success = await this.redisSync.atomicTopup(actualKey, credits);
1127
1159
  }
1128
1160
  else {
1129
- success = this.gate.store.addCredits(params.key, credits);
1161
+ success = this.gate.store.addCredits(actualKey, credits);
1130
1162
  }
1131
1163
  if (!success) {
1132
1164
  res.writeHead(404, { 'Content-Type': 'application/json' });
1133
1165
  res.end(JSON.stringify({ error: 'Key not found or inactive' }));
1134
1166
  return;
1135
1167
  }
1136
- const record = this.gate.store.getKey(params.key);
1168
+ const record = this.gate.store.getKey(actualKey);
1137
1169
  this.audit.log('key.topup', 'admin', `Added ${credits} credits`, {
1138
1170
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1139
1171
  creditsAdded: credits,
@@ -1181,7 +1213,7 @@ class PayGateServer {
1181
1213
  return;
1182
1214
  }
1183
1215
  // Validate source key exists and has enough credits
1184
- const sourceRecord = this.gate.store.getKey(params.from);
1216
+ const sourceRecord = this.gate.store.resolveKey(params.from);
1185
1217
  if (!sourceRecord) {
1186
1218
  res.writeHead(404, { 'Content-Type': 'application/json' });
1187
1219
  res.end(JSON.stringify({ error: 'Source key not found' }));
@@ -1195,7 +1227,7 @@ class PayGateServer {
1195
1227
  return;
1196
1228
  }
1197
1229
  // Validate destination key exists (getKey returns null for revoked/expired keys)
1198
- const destRecord = this.gate.store.getKey(params.to);
1230
+ const destRecord = this.gate.store.resolveKey(params.to);
1199
1231
  if (!destRecord) {
1200
1232
  res.writeHead(404, { 'Content-Type': 'application/json' });
1201
1233
  res.end(JSON.stringify({ error: 'Destination key not found' }));
@@ -1204,13 +1236,13 @@ class PayGateServer {
1204
1236
  // Perform transfer atomically (deduct from source, add to destination)
1205
1237
  if (this.redisSync) {
1206
1238
  // Redis atomic transfer: deduct first, then add
1207
- const deducted = await this.redisSync.atomicDeduct(params.from, credits);
1239
+ const deducted = await this.redisSync.atomicDeduct(sourceRecord.key, credits);
1208
1240
  if (!deducted) {
1209
1241
  res.writeHead(400, { 'Content-Type': 'application/json' });
1210
1242
  res.end(JSON.stringify({ error: 'Redis deduction failed (insufficient credits or key not found)' }));
1211
1243
  return;
1212
1244
  }
1213
- await this.redisSync.atomicTopup(params.to, credits);
1245
+ await this.redisSync.atomicTopup(destRecord.key, credits);
1214
1246
  }
1215
1247
  else {
1216
1248
  // Local store: deduct and add
@@ -1218,20 +1250,20 @@ class PayGateServer {
1218
1250
  destRecord.credits += credits;
1219
1251
  this.gate.store.save();
1220
1252
  }
1221
- const fromBalance = this.gate.store.getKey(params.from)?.credits ?? 0;
1222
- const toBalance = this.gate.store.getKey(params.to)?.credits ?? 0;
1253
+ const fromBalance = sourceRecord.credits;
1254
+ const toBalance = destRecord.credits;
1223
1255
  const memo = params.memo || '';
1224
1256
  this.audit.log('key.credits_transferred', 'admin', `Transferred ${credits} credits`, {
1225
- fromKeyMasked: (0, audit_1.maskKeyForAudit)(params.from),
1226
- toKeyMasked: (0, audit_1.maskKeyForAudit)(params.to),
1257
+ fromKeyMasked: (0, audit_1.maskKeyForAudit)(sourceRecord.key),
1258
+ toKeyMasked: (0, audit_1.maskKeyForAudit)(destRecord.key),
1227
1259
  credits,
1228
1260
  fromBalance,
1229
1261
  toBalance,
1230
1262
  memo,
1231
1263
  });
1232
1264
  this.emitWebhookAdmin('key.credits_transferred', 'admin', {
1233
- fromKeyMasked: (0, audit_1.maskKeyForAudit)(params.from),
1234
- toKeyMasked: (0, audit_1.maskKeyForAudit)(params.to),
1265
+ fromKeyMasked: (0, audit_1.maskKeyForAudit)(sourceRecord.key),
1266
+ toKeyMasked: (0, audit_1.maskKeyForAudit)(destRecord.key),
1235
1267
  credits,
1236
1268
  fromBalance,
1237
1269
  toBalance,
@@ -1240,8 +1272,8 @@ class PayGateServer {
1240
1272
  res.writeHead(200, { 'Content-Type': 'application/json' });
1241
1273
  res.end(JSON.stringify({
1242
1274
  transferred: credits,
1243
- from: { keyMasked: (0, audit_1.maskKeyForAudit)(params.from), balance: fromBalance },
1244
- to: { keyMasked: (0, audit_1.maskKeyForAudit)(params.to), balance: toBalance },
1275
+ from: { keyMasked: (0, audit_1.maskKeyForAudit)(sourceRecord.key), balance: fromBalance, credits: fromBalance },
1276
+ to: { keyMasked: (0, audit_1.maskKeyForAudit)(destRecord.key), balance: toBalance, credits: toBalance },
1245
1277
  memo: memo || undefined,
1246
1278
  message: `Transferred ${credits} credits`,
1247
1279
  }));
@@ -1515,13 +1547,16 @@ class PayGateServer {
1515
1547
  res.end(JSON.stringify({ error: 'Missing key' }));
1516
1548
  return;
1517
1549
  }
1550
+ // Resolve alias to actual key
1551
+ const resolved = this.gate.store.resolveKeyRaw(params.key);
1552
+ const actualKey = resolved ? resolved.key : params.key;
1518
1553
  // Use Redis-backed revoke when available (broadcasts to other instances)
1519
1554
  let success;
1520
1555
  if (this.redisSync) {
1521
- success = await this.redisSync.revokeKey(params.key);
1556
+ success = await this.redisSync.revokeKey(actualKey);
1522
1557
  }
1523
1558
  else {
1524
- success = this.gate.store.revokeKey(params.key);
1559
+ success = this.gate.store.revokeKey(actualKey);
1525
1560
  }
1526
1561
  if (!success) {
1527
1562
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1529,13 +1564,13 @@ class PayGateServer {
1529
1564
  return;
1530
1565
  }
1531
1566
  this.audit.log('key.revoked', 'admin', `Key revoked`, {
1532
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1567
+ keyMasked: (0, audit_1.maskKeyForAudit)(actualKey),
1533
1568
  });
1534
1569
  this.emitWebhookAdmin('key.revoked', 'admin', {
1535
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1570
+ keyMasked: (0, audit_1.maskKeyForAudit)(actualKey),
1536
1571
  });
1537
1572
  res.writeHead(200, { 'Content-Type': 'application/json' });
1538
- res.end(JSON.stringify({ message: 'Key revoked' }));
1573
+ res.end(JSON.stringify({ message: 'Key revoked', revoked: true }));
1539
1574
  }
1540
1575
  // ─── /keys/suspend — Temporarily suspend a key ─────────────────────────────
1541
1576
  async handleSuspendKey(req, res) {
@@ -1561,7 +1596,7 @@ class PayGateServer {
1561
1596
  res.end(JSON.stringify({ error: 'Missing key' }));
1562
1597
  return;
1563
1598
  }
1564
- const record = this.gate.store.getKeyRaw(params.key);
1599
+ const record = this.gate.store.resolveKeyRaw(params.key);
1565
1600
  if (!record) {
1566
1601
  res.writeHead(404, { 'Content-Type': 'application/json' });
1567
1602
  res.end(JSON.stringify({ error: 'Key not found' }));
@@ -1577,19 +1612,19 @@ class PayGateServer {
1577
1612
  res.end(JSON.stringify({ error: 'Key is already suspended' }));
1578
1613
  return;
1579
1614
  }
1580
- const success = this.gate.store.suspendKey(params.key);
1615
+ const success = this.gate.store.suspendKey(record.key);
1581
1616
  if (!success) {
1582
1617
  res.writeHead(500, { 'Content-Type': 'application/json' });
1583
1618
  res.end(JSON.stringify({ error: 'Failed to suspend key' }));
1584
1619
  return;
1585
1620
  }
1586
- this.syncKeyMutation(params.key);
1621
+ this.syncKeyMutation(record.key);
1587
1622
  this.audit.log('key.suspended', 'admin', `Key suspended${params.reason ? ': ' + params.reason : ''}`, {
1588
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1623
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1589
1624
  reason: params.reason || null,
1590
1625
  });
1591
1626
  this.emitWebhookAdmin('key.suspended', 'admin', {
1592
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1627
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1593
1628
  reason: params.reason || null,
1594
1629
  });
1595
1630
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1619,7 +1654,7 @@ class PayGateServer {
1619
1654
  res.end(JSON.stringify({ error: 'Missing key' }));
1620
1655
  return;
1621
1656
  }
1622
- const record = this.gate.store.getKeyRaw(params.key);
1657
+ const record = this.gate.store.resolveKeyRaw(params.key);
1623
1658
  if (!record) {
1624
1659
  res.writeHead(404, { 'Content-Type': 'application/json' });
1625
1660
  res.end(JSON.stringify({ error: 'Key not found' }));
@@ -1635,18 +1670,18 @@ class PayGateServer {
1635
1670
  res.end(JSON.stringify({ error: 'Key is not suspended' }));
1636
1671
  return;
1637
1672
  }
1638
- const success = this.gate.store.resumeKey(params.key);
1673
+ const success = this.gate.store.resumeKey(record.key);
1639
1674
  if (!success) {
1640
1675
  res.writeHead(500, { 'Content-Type': 'application/json' });
1641
1676
  res.end(JSON.stringify({ error: 'Failed to resume key' }));
1642
1677
  return;
1643
1678
  }
1644
- this.syncKeyMutation(params.key);
1679
+ this.syncKeyMutation(record.key);
1645
1680
  this.audit.log('key.resumed', 'admin', 'Key resumed', {
1646
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1681
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1647
1682
  });
1648
1683
  this.emitWebhookAdmin('key.resumed', 'admin', {
1649
- keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1684
+ keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
1650
1685
  });
1651
1686
  res.writeHead(200, { 'Content-Type': 'application/json' });
1652
1687
  res.end(JSON.stringify({ message: 'Key resumed', suspended: false }));
@@ -1676,7 +1711,7 @@ class PayGateServer {
1676
1711
  return;
1677
1712
  }
1678
1713
  // Use getKeyRaw to allow cloning suspended/expired keys (but not revoked)
1679
- const source = this.gate.store.getKeyRaw(params.key);
1714
+ const source = this.gate.store.resolveKeyRaw(params.key);
1680
1715
  if (!source) {
1681
1716
  res.writeHead(404, { 'Content-Type': 'application/json' });
1682
1717
  res.end(JSON.stringify({ error: 'Source key not found' }));
@@ -1687,7 +1722,7 @@ class PayGateServer {
1687
1722
  res.end(JSON.stringify({ error: 'Cannot clone a revoked key' }));
1688
1723
  return;
1689
1724
  }
1690
- const cloned = this.gate.store.cloneKey(params.key, {
1725
+ const cloned = this.gate.store.cloneKey(source.key, {
1691
1726
  name: params.name,
1692
1727
  credits: params.credits,
1693
1728
  tags: params.tags,
@@ -1700,14 +1735,14 @@ class PayGateServer {
1700
1735
  }
1701
1736
  // Sync new key to Redis
1702
1737
  this.syncKeyMutation(cloned.key);
1703
- this.audit.log('key.cloned', 'admin', `Key cloned from ${(0, audit_1.maskKeyForAudit)(params.key)}`, {
1704
- sourceKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1738
+ this.audit.log('key.cloned', 'admin', `Key cloned from ${(0, audit_1.maskKeyForAudit)(source.key)}`, {
1739
+ sourceKeyMasked: (0, audit_1.maskKeyForAudit)(source.key),
1705
1740
  newKeyMasked: (0, audit_1.maskKeyForAudit)(cloned.key),
1706
1741
  name: cloned.name,
1707
1742
  credits: cloned.credits,
1708
1743
  });
1709
1744
  this.emitWebhookAdmin('key.cloned', 'admin', {
1710
- sourceKeyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1745
+ sourceKeyMasked: (0, audit_1.maskKeyForAudit)(source.key),
1711
1746
  newKeyMasked: (0, audit_1.maskKeyForAudit)(cloned.key),
1712
1747
  name: cloned.name,
1713
1748
  credits: cloned.credits,
@@ -1718,6 +1753,7 @@ class PayGateServer {
1718
1753
  key: cloned.key,
1719
1754
  name: cloned.name,
1720
1755
  credits: cloned.credits,
1756
+ clonedFrom: source.key.slice(0, 10) + '...',
1721
1757
  sourceName: source.name,
1722
1758
  allowedTools: cloned.allowedTools,
1723
1759
  deniedTools: cloned.deniedTools,
@@ -1730,6 +1766,65 @@ class PayGateServer {
1730
1766
  spendingLimit: cloned.spendingLimit,
1731
1767
  }));
1732
1768
  }
1769
+ // ─── /keys/alias — Set or clear key alias ──────────────────────────────────
1770
+ async handleSetAlias(req, res) {
1771
+ if (req.method !== 'POST') {
1772
+ res.writeHead(405, { 'Content-Type': 'application/json' });
1773
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
1774
+ return;
1775
+ }
1776
+ if (!this.checkAdmin(req, res))
1777
+ return;
1778
+ const raw = await this.readBody(req);
1779
+ const params = JSON.parse(raw);
1780
+ if (!params.key) {
1781
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1782
+ res.end(JSON.stringify({ error: 'Missing "key" parameter' }));
1783
+ return;
1784
+ }
1785
+ // Resolve the key (support existing aliases for the source key)
1786
+ const record = this.gate.store.resolveKeyRaw(params.key);
1787
+ if (!record) {
1788
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1789
+ res.end(JSON.stringify({ error: 'Key not found' }));
1790
+ return;
1791
+ }
1792
+ const alias = params.alias !== undefined ? (params.alias === null || params.alias === '' ? null : String(params.alias)) : undefined;
1793
+ if (alias === undefined) {
1794
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1795
+ res.end(JSON.stringify({ error: 'Missing "alias" parameter (string to set, null to clear)' }));
1796
+ return;
1797
+ }
1798
+ const result = this.gate.store.setAlias(record.key, alias);
1799
+ if (!result.success) {
1800
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1801
+ res.end(JSON.stringify({ error: result.error }));
1802
+ return;
1803
+ }
1804
+ const action = alias ? `set to "${alias}"` : 'cleared';
1805
+ this.audit.log('key.alias_set', 'admin', `Key alias ${action} for ${record.key.slice(0, 10)}...`, {
1806
+ key: record.key.slice(0, 10),
1807
+ alias: alias || null,
1808
+ });
1809
+ // Sync to Redis if configured
1810
+ if (typeof this.syncKeyMutation === 'function') {
1811
+ this.syncKeyMutation(record.key);
1812
+ }
1813
+ // Webhook event
1814
+ if (this.gate.webhook) {
1815
+ this.gate.webhook.emitAdmin('key.created', 'admin', {
1816
+ key: record.key.slice(0, 10),
1817
+ alias: alias || null,
1818
+ event: 'alias_set',
1819
+ });
1820
+ }
1821
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1822
+ res.end(JSON.stringify({
1823
+ key: record.key.slice(0, 10) + '...',
1824
+ alias: record.alias || null,
1825
+ message: `Alias ${action}`,
1826
+ }));
1827
+ }
1733
1828
  // ─── /keys/rotate — Rotate API key ─────────────────────────────────────────
1734
1829
  async handleRotateKey(req, res) {
1735
1830
  if (req.method !== 'POST') {
@@ -1813,7 +1908,7 @@ class PayGateServer {
1813
1908
  return;
1814
1909
  }
1815
1910
  this.syncKeyMutation(params.key);
1816
- const record = this.gate.store.getKey(params.key);
1911
+ const record = this.gate.store.resolveKey(params.key);
1817
1912
  this.audit.log('key.acl_updated', 'admin', `ACL updated`, {
1818
1913
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1819
1914
  allowedTools: record?.allowedTools || [],
@@ -1865,7 +1960,7 @@ class PayGateServer {
1865
1960
  return;
1866
1961
  }
1867
1962
  this.syncKeyMutation(params.key);
1868
- const record = this.gate.store.getKeyRaw(params.key);
1963
+ const record = this.gate.store.resolveKeyRaw(params.key);
1869
1964
  this.audit.log('key.expiry_updated', 'admin', expiresAt ? `Key expiry set to ${expiresAt}` : 'Key expiry removed', {
1870
1965
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1871
1966
  expiresAt: record?.expiresAt || null,
@@ -1968,7 +2063,7 @@ class PayGateServer {
1968
2063
  return;
1969
2064
  }
1970
2065
  this.syncKeyMutation(params.key);
1971
- const record = this.gate.store.getKey(params.key);
2066
+ const record = this.gate.store.resolveKey(params.key);
1972
2067
  this.audit.log('key.tags_updated', 'admin', `Tags updated`, {
1973
2068
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
1974
2069
  tags: record?.tags || {},
@@ -2015,7 +2110,7 @@ class PayGateServer {
2015
2110
  return;
2016
2111
  }
2017
2112
  this.syncKeyMutation(params.key);
2018
- const record = this.gate.store.getKey(params.key);
2113
+ const record = this.gate.store.resolveKey(params.key);
2019
2114
  this.audit.log('key.ip_updated', 'admin', `IP allowlist updated`, {
2020
2115
  keyMasked: (0, audit_1.maskKeyForAudit)(params.key),
2021
2116
  ipAllowlist: record?.ipAllowlist || [],
@@ -2072,14 +2167,14 @@ class PayGateServer {
2072
2167
  res.end(JSON.stringify({ error: 'Missing key query parameter' }));
2073
2168
  return;
2074
2169
  }
2075
- // Verify key exists (use getKeyRaw to allow querying expired/suspended keys)
2076
- const record = this.gate.store.getKeyRaw(key);
2170
+ // Verify key exists (use resolveKeyRaw to allow querying by alias and expired/suspended keys)
2171
+ const record = this.gate.store.resolveKeyRaw(key);
2077
2172
  if (!record) {
2078
2173
  res.writeHead(404, { 'Content-Type': 'application/json' });
2079
2174
  res.end(JSON.stringify({ error: 'Key not found' }));
2080
2175
  return;
2081
2176
  }
2082
- const usage = this.gate.meter.getKeyUsage(key, since);
2177
+ const usage = this.gate.meter.getKeyUsage(record.key, since);
2083
2178
  res.writeHead(200, { 'Content-Type': 'application/json' });
2084
2179
  res.end(JSON.stringify({
2085
2180
  key: key.slice(0, 10) + '...',
@@ -2091,6 +2186,34 @@ class PayGateServer {
2091
2186
  ...usage,
2092
2187
  }, null, 2));
2093
2188
  }
2189
+ // ─── /keys/expiring — List keys expiring within a time window ───────────────
2190
+ handleKeysExpiring(req, res) {
2191
+ if (req.method !== 'GET') {
2192
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2193
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
2194
+ return;
2195
+ }
2196
+ if (!this.checkAdmin(req, res))
2197
+ return;
2198
+ const urlParts = req.url?.split('?') || [];
2199
+ const params = new URLSearchParams(urlParts[1] || '');
2200
+ const withinStr = params.get('within');
2201
+ const within = withinStr ? parseInt(withinStr, 10) : 86400; // Default: 24 hours
2202
+ if (isNaN(within) || within <= 0) {
2203
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2204
+ res.end(JSON.stringify({ error: 'Invalid within parameter — must be a positive number of seconds' }));
2205
+ return;
2206
+ }
2207
+ const allKeys = this.gate.store.getAllRecords();
2208
+ const expiring = expiry_scanner_1.ExpiryScanner.queryExpiring(allKeys, within);
2209
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2210
+ res.end(JSON.stringify({
2211
+ within,
2212
+ count: expiring.length,
2213
+ scanner: this.expiryScanner.status,
2214
+ keys: expiring,
2215
+ }, null, 2));
2216
+ }
2094
2217
  // ─── /keys/auto-topup — Configure auto-topup ────────────────────────────────
2095
2218
  async handleSetAutoTopup(req, res) {
2096
2219
  if (req.method !== 'POST') {
@@ -2115,7 +2238,7 @@ class PayGateServer {
2115
2238
  res.end(JSON.stringify({ error: 'Missing key' }));
2116
2239
  return;
2117
2240
  }
2118
- const record = this.gate.store.getKey(params.key);
2241
+ const record = this.gate.store.resolveKey(params.key);
2119
2242
  if (!record) {
2120
2243
  res.writeHead(404, { 'Content-Type': 'application/json' });
2121
2244
  res.end(JSON.stringify({ error: 'Key not found or inactive' }));
@@ -2236,7 +2359,7 @@ class PayGateServer {
2236
2359
  res.end(JSON.stringify({ error: 'Missing key' }));
2237
2360
  return;
2238
2361
  }
2239
- const record = this.gate.store.getKey(params.key);
2362
+ const record = this.gate.store.resolveKey(params.key);
2240
2363
  if (!record) {
2241
2364
  res.writeHead(404, { 'Content-Type': 'application/json' });
2242
2365
  res.end(JSON.stringify({ error: 'Key not found or inactive' }));
@@ -3539,7 +3662,7 @@ class PayGateServer {
3539
3662
  return;
3540
3663
  }
3541
3664
  // Verify the key exists
3542
- const keyRecord = this.gate.store.getKey(params.key);
3665
+ const keyRecord = this.gate.store.resolveKey(params.key);
3543
3666
  if (!keyRecord) {
3544
3667
  res.writeHead(404, { 'Content-Type': 'application/json' });
3545
3668
  res.end(JSON.stringify({ error: 'API key not found' }));
@@ -3658,7 +3781,7 @@ class PayGateServer {
3658
3781
  return;
3659
3782
  }
3660
3783
  // Verify the parent key exists and is active
3661
- const keyRecord = this.gate.store.getKey(params.key);
3784
+ const keyRecord = this.gate.store.resolveKey(params.key);
3662
3785
  if (!keyRecord || !keyRecord.active) {
3663
3786
  res.writeHead(404, { 'Content-Type': 'application/json' });
3664
3787
  res.end(JSON.stringify({ error: 'API key not found or inactive' }));
@@ -4195,6 +4318,7 @@ class PayGateServer {
4195
4318
  this.sessions.destroy();
4196
4319
  this.audit.destroy();
4197
4320
  this.tokens.destroy();
4321
+ this.expiryScanner.destroy();
4198
4322
  if (this.redisSync) {
4199
4323
  await this.redisSync.destroy();
4200
4324
  }
@@ -4250,6 +4374,7 @@ class PayGateServer {
4250
4374
  this.sessions.destroy();
4251
4375
  this.audit.destroy();
4252
4376
  this.tokens.destroy();
4377
+ this.expiryScanner.destroy();
4253
4378
  if (this.redisSync) {
4254
4379
  await this.redisSync.destroy();
4255
4380
  }