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/README.md +81 -0
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/expiry-scanner.d.ts +99 -0
- package/dist/expiry-scanner.d.ts.map +1 -0
- package/dist/expiry-scanner.js +216 -0
- package/dist/expiry-scanner.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +171 -46
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +30 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +91 -0
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.d.ts.map +1 -1
- package/package.json +1 -1
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(
|
|
1158
|
+
success = await this.redisSync.atomicTopup(actualKey, credits);
|
|
1127
1159
|
}
|
|
1128
1160
|
else {
|
|
1129
|
-
success = this.gate.store.addCredits(
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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 =
|
|
1222
|
-
const toBalance =
|
|
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)(
|
|
1226
|
-
toKeyMasked: (0, audit_1.maskKeyForAudit)(
|
|
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)(
|
|
1234
|
-
toKeyMasked: (0, audit_1.maskKeyForAudit)(
|
|
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)(
|
|
1244
|
-
to: { keyMasked: (0, audit_1.maskKeyForAudit)(
|
|
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(
|
|
1556
|
+
success = await this.redisSync.revokeKey(actualKey);
|
|
1522
1557
|
}
|
|
1523
1558
|
else {
|
|
1524
|
-
success = this.gate.store.revokeKey(
|
|
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)(
|
|
1567
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(actualKey),
|
|
1533
1568
|
});
|
|
1534
1569
|
this.emitWebhookAdmin('key.revoked', 'admin', {
|
|
1535
|
-
keyMasked: (0, audit_1.maskKeyForAudit)(
|
|
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.
|
|
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(
|
|
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(
|
|
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)(
|
|
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)(
|
|
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.
|
|
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(
|
|
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(
|
|
1679
|
+
this.syncKeyMutation(record.key);
|
|
1645
1680
|
this.audit.log('key.resumed', 'admin', 'Key resumed', {
|
|
1646
|
-
keyMasked: (0, audit_1.maskKeyForAudit)(
|
|
1681
|
+
keyMasked: (0, audit_1.maskKeyForAudit)(record.key),
|
|
1647
1682
|
});
|
|
1648
1683
|
this.emitWebhookAdmin('key.resumed', 'admin', {
|
|
1649
|
-
keyMasked: (0, audit_1.maskKeyForAudit)(
|
|
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.
|
|
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(
|
|
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)(
|
|
1704
|
-
sourceKeyMasked: (0, audit_1.maskKeyForAudit)(
|
|
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)(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
2076
|
-
const record = this.gate.store.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|