paygate-mcp 5.0.0 → 5.2.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.
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ /**
3
+ * KeyTemplateManager — Named templates for API key creation.
4
+ *
5
+ * Templates define reusable presets for key creation (credits, ACL, quotas, etc.).
6
+ * Instead of passing all options every time, use `template: "free-tier"` in POST /keys.
7
+ *
8
+ * Features:
9
+ * - CRUD: create, update, list, get, delete templates
10
+ * - File persistence (-templates.json alongside state file)
11
+ * - Templates define: credits, allowedTools, deniedTools, quota, ipAllowlist,
12
+ * spendingLimit, tags, namespace, expiryTtlSeconds, autoTopup
13
+ * - Max 100 templates
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.KeyTemplateManager = void 0;
17
+ const fs_1 = require("fs");
18
+ const path_1 = require("path");
19
+ // ─── Manager Class ────────────────────────────────────────────────────────────
20
+ const MAX_TEMPLATES = 100;
21
+ class KeyTemplateManager {
22
+ templates = new Map();
23
+ filePath;
24
+ constructor(filePath) {
25
+ this.filePath = filePath || null;
26
+ if (this.filePath) {
27
+ this.loadFromFile();
28
+ }
29
+ }
30
+ /**
31
+ * Create or update a template.
32
+ */
33
+ set(name, data) {
34
+ // Validate name
35
+ const sanitized = name.trim().slice(0, 50);
36
+ if (!/^[a-zA-Z0-9_-]+$/.test(sanitized)) {
37
+ return { success: false, error: 'Template name must contain only letters, numbers, hyphens, and underscores' };
38
+ }
39
+ const existing = this.templates.get(sanitized);
40
+ const now = new Date().toISOString();
41
+ const template = {
42
+ name: sanitized,
43
+ description: String(data.description || existing?.description || '').slice(0, 500),
44
+ credits: Math.max(0, Math.floor(Number(data.credits ?? existing?.credits ?? 100))),
45
+ allowedTools: Array.isArray(data.allowedTools) ? data.allowedTools.filter(t => typeof t === 'string').slice(0, 100) : (existing?.allowedTools || []),
46
+ deniedTools: Array.isArray(data.deniedTools) ? data.deniedTools.filter(t => typeof t === 'string').slice(0, 100) : (existing?.deniedTools || []),
47
+ quota: data.quota !== undefined ? data.quota : existing?.quota,
48
+ ipAllowlist: Array.isArray(data.ipAllowlist) ? data.ipAllowlist.filter(t => typeof t === 'string').slice(0, 100) : (existing?.ipAllowlist || []),
49
+ spendingLimit: Math.max(0, Number(data.spendingLimit ?? existing?.spendingLimit ?? 0)),
50
+ tags: typeof data.tags === 'object' && data.tags !== null ? data.tags : (existing?.tags || {}),
51
+ namespace: String(data.namespace || existing?.namespace || 'default').trim().toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 50) || 'default',
52
+ expiryTtlSeconds: Math.max(0, Math.floor(Number(data.expiryTtlSeconds ?? existing?.expiryTtlSeconds ?? 0))),
53
+ autoTopup: data.autoTopup !== undefined ? data.autoTopup : existing?.autoTopup,
54
+ createdAt: existing?.createdAt || now,
55
+ updatedAt: now,
56
+ };
57
+ // Check limit (only for new templates)
58
+ if (!existing && this.templates.size >= MAX_TEMPLATES) {
59
+ return { success: false, error: `Maximum ${MAX_TEMPLATES} templates reached` };
60
+ }
61
+ this.templates.set(sanitized, template);
62
+ this.saveToFile();
63
+ return { success: true, template };
64
+ }
65
+ /**
66
+ * Get a template by name.
67
+ */
68
+ get(name) {
69
+ return this.templates.get(name) || null;
70
+ }
71
+ /**
72
+ * Delete a template.
73
+ */
74
+ delete(name) {
75
+ const existed = this.templates.delete(name);
76
+ if (existed)
77
+ this.saveToFile();
78
+ return existed;
79
+ }
80
+ /**
81
+ * List all templates.
82
+ */
83
+ list() {
84
+ return Array.from(this.templates.values()).sort((a, b) => a.name.localeCompare(b.name));
85
+ }
86
+ /**
87
+ * Get template count.
88
+ */
89
+ get count() {
90
+ return this.templates.size;
91
+ }
92
+ // ─── File Persistence ──────────────────────────────────────────────────────
93
+ saveToFile() {
94
+ if (!this.filePath)
95
+ return;
96
+ const data = Array.from(this.templates.entries());
97
+ const json = JSON.stringify(data, null, 2);
98
+ const tmpPath = this.filePath + '.tmp';
99
+ try {
100
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(this.filePath), { recursive: true });
101
+ (0, fs_1.writeFileSync)(tmpPath, json, 'utf-8');
102
+ (0, fs_1.renameSync)(tmpPath, this.filePath);
103
+ }
104
+ catch (err) {
105
+ console.error(`[paygate] Failed to save templates: ${err.message}`);
106
+ }
107
+ }
108
+ loadFromFile() {
109
+ if (!this.filePath || !(0, fs_1.existsSync)(this.filePath))
110
+ return;
111
+ try {
112
+ const json = (0, fs_1.readFileSync)(this.filePath, 'utf-8');
113
+ const data = JSON.parse(json);
114
+ if (!Array.isArray(data))
115
+ return;
116
+ for (const [name, template] of data) {
117
+ if (name && template && typeof template.name === 'string') {
118
+ this.templates.set(name, template);
119
+ }
120
+ }
121
+ console.log(`[paygate] Loaded ${this.templates.size} template(s) from ${this.filePath}`);
122
+ }
123
+ catch (err) {
124
+ console.error(`[paygate] Failed to load templates: ${err.message}`);
125
+ }
126
+ }
127
+ }
128
+ exports.KeyTemplateManager = KeyTemplateManager;
129
+ //# sourceMappingURL=key-templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-templates.js","sourceRoot":"","sources":["../src/key-templates.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -30,6 +30,8 @@ import { ScopedTokenManager } from './tokens';
30
30
  import { AdminKeyManager } from './admin-keys';
31
31
  import { PluginManager, PayGatePlugin } from './plugin';
32
32
  import { KeyGroupManager } from './groups';
33
+ import { ExpiryScanner } from './expiry-scanner';
34
+ import { KeyTemplateManager } from './key-templates';
33
35
  /** Union type for both proxy backends */
34
36
  type ProxyBackend = McpProxy | HttpMcpProxy;
35
37
  export declare class PayGateServer {
@@ -68,6 +70,10 @@ export declare class PayGateServer {
68
70
  /** Plugin manager for extensible middleware hooks */
69
71
  readonly plugins: PluginManager;
70
72
  readonly groups: KeyGroupManager;
73
+ /** Background key expiry scanner */
74
+ readonly expiryScanner: ExpiryScanner;
75
+ /** Key template manager for reusable key presets */
76
+ readonly templates: KeyTemplateManager;
71
77
  /** Server start time (ms since epoch) */
72
78
  private readonly startedAt;
73
79
  /** Whether the server is draining (shutting down gracefully) */
@@ -148,6 +154,7 @@ export declare class PayGateServer {
148
154
  private handleSetIpAllowlist;
149
155
  private handleSearchKeysByTag;
150
156
  private handleKeyUsage;
157
+ private handleKeysExpiring;
151
158
  private handleSetAutoTopup;
152
159
  private handleBalance;
153
160
  private handleLimits;
@@ -210,11 +217,9 @@ export declare class PayGateServer {
210
217
  private handleListAdminKeys;
211
218
  private handleCreateAdminKey;
212
219
  private handleRevokeAdminKey;
213
- /**
214
- * Sync a key mutation to Redis. Call after any local KeyStore mutation
215
- * (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
216
- * Fire-and-forget: errors logged, never thrown.
217
- */
220
+ private handleListTemplates;
221
+ private handleCreateTemplate;
222
+ private handleDeleteTemplate;
218
223
  /**
219
224
  * Route admin webhook events through the WebhookRouter (for filter rules) or direct emitter.
220
225
  */
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -83,6 +83,8 @@ 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");
87
+ const key_templates_1 = require("./key-templates");
86
88
  /** Max request body size: 1MB */
87
89
  const MAX_BODY_SIZE = 1_048_576;
88
90
  class PayGateServer {
@@ -121,6 +123,10 @@ class PayGateServer {
121
123
  /** Plugin manager for extensible middleware hooks */
122
124
  plugins;
123
125
  groups;
126
+ /** Background key expiry scanner */
127
+ expiryScanner;
128
+ /** Key template manager for reusable key presets */
129
+ templates;
124
130
  /** Server start time (ms since epoch) */
125
131
  startedAt = Date.now();
126
132
  /** Whether the server is draining (shutting down gracefully) */
@@ -239,6 +245,30 @@ class PayGateServer {
239
245
  this.metrics.registerGauge('paygate_groups_total', 'Number of active key groups', () => {
240
246
  return this.groups.count;
241
247
  });
248
+ // Key expiry scanner — proactive background scanning for expiring keys
249
+ const scannerConfig = this.config.expiryScanner;
250
+ this.expiryScanner = new expiry_scanner_1.ExpiryScanner(scannerConfig ? {
251
+ enabled: scannerConfig.enabled !== false,
252
+ intervalSeconds: scannerConfig.intervalSeconds || 3600,
253
+ thresholds: scannerConfig.thresholds || [604800, 86400, 3600],
254
+ } : undefined);
255
+ // Wire scanner callbacks: audit + webhook
256
+ this.expiryScanner.onWarning = (warning) => {
257
+ const keyMasked = (0, audit_1.maskKeyForAudit)(warning.key);
258
+ 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 });
259
+ this.emitWebhookAdmin('key.expiry_warning', 'system', {
260
+ keyMasked, keyName: warning.name, alias: warning.alias || null,
261
+ namespace: warning.namespace, expiresAt: warning.expiresAt,
262
+ remainingSeconds: warning.remainingSeconds, remainingHuman: warning.remainingHuman,
263
+ thresholdSeconds: warning.thresholdSeconds,
264
+ });
265
+ };
266
+ // Key template manager for reusable key creation presets
267
+ const templatesStatePath = statePath ? statePath.replace(/\.json$/, '-templates.json') : undefined;
268
+ this.templates = new key_templates_1.KeyTemplateManager(templatesStatePath);
269
+ this.metrics.registerGauge('paygate_templates_total', 'Number of key templates', () => {
270
+ return this.templates.count;
271
+ });
242
272
  // Scoped token manager (uses bootstrap admin key as signing secret, padded to min length)
243
273
  const tokenSecret = this.bootstrapAdminKey.length >= 8
244
274
  ? this.bootstrapAdminKey
@@ -298,6 +328,8 @@ class PayGateServer {
298
328
  console.log('[paygate] Redis distributed state enabled');
299
329
  }
300
330
  await this.handler.start();
331
+ // Start the key expiry scanner (proactive background scanning)
332
+ this.expiryScanner.start(() => this.gate.store.getAllRecords());
301
333
  // Plugin lifecycle: onStart
302
334
  if (this.plugins.count > 0) {
303
335
  await this.plugins.executeStart();
@@ -384,6 +416,16 @@ class PayGateServer {
384
416
  return this.handleSetAutoTopup(req, res);
385
417
  case '/keys/usage':
386
418
  return this.handleKeyUsage(req, res);
419
+ case '/keys/expiring':
420
+ return this.handleKeysExpiring(req, res);
421
+ case '/keys/templates':
422
+ if (req.method === 'GET')
423
+ return this.handleListTemplates(req, res);
424
+ if (req.method === 'POST')
425
+ return this.handleCreateTemplate(req, res);
426
+ break;
427
+ case '/keys/templates/delete':
428
+ return this.handleDeleteTemplate(req, res);
387
429
  case '/topup':
388
430
  return this.handleTopUp(req, res);
389
431
  case '/keys/transfer':
@@ -908,6 +950,9 @@ class PayGateServer {
908
950
  searchKeys: 'POST /keys/search — Search keys by tags (requires X-Admin-Key)',
909
951
  autoTopup: 'POST /keys/auto-topup — Configure auto-topup for a key (requires X-Admin-Key)',
910
952
  keyUsage: 'GET /keys/usage?key=... — Per-key usage breakdown (requires X-Admin-Key)',
953
+ keysExpiring: 'GET /keys/expiring?within=86400 — List keys expiring within N seconds (requires X-Admin-Key)',
954
+ keyTemplates: 'GET /keys/templates — List key templates + POST to create/update (requires X-Admin-Key)',
955
+ deleteTemplate: 'POST /keys/templates/delete — Delete a key template (requires X-Admin-Key)',
911
956
  pricing: 'GET /pricing — Tool pricing breakdown (public)',
912
957
  mcpPayment: 'GET /.well-known/mcp-payment — Payment metadata (SEP-2007)',
913
958
  audit: 'GET /audit — Query audit log (requires X-Admin-Key)',
@@ -1019,14 +1064,24 @@ class PayGateServer {
1019
1064
  res.end(JSON.stringify({ error: 'Invalid JSON' }));
1020
1065
  return;
1021
1066
  }
1067
+ // Resolve template defaults (explicit params override template values)
1068
+ let tpl = null;
1069
+ if (params.template) {
1070
+ tpl = this.templates.get(params.template);
1071
+ if (!tpl) {
1072
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1073
+ res.end(JSON.stringify({ error: `Template "${params.template}" not found` }));
1074
+ return;
1075
+ }
1076
+ }
1022
1077
  const name = String(params.name || 'unnamed').slice(0, 200);
1023
- const credits = Math.max(0, Math.floor(Number(params.credits) || 100));
1078
+ const credits = Math.max(0, Math.floor(Number(params.credits ?? tpl?.credits ?? 100)));
1024
1079
  if (credits <= 0) {
1025
1080
  res.writeHead(400, { 'Content-Type': 'application/json' });
1026
1081
  res.end(JSON.stringify({ error: 'Credits must be a positive integer' }));
1027
1082
  return;
1028
1083
  }
1029
- // Calculate expiry: expiresIn (seconds) takes priority over expiresAt (ISO date)
1084
+ // Calculate expiry: expiresIn (seconds) takes priority over expiresAt (ISO date), template TTL is fallback
1030
1085
  let expiresAt = null;
1031
1086
  if (params.expiresIn && Number(params.expiresIn) > 0) {
1032
1087
  expiresAt = new Date(Date.now() + Number(params.expiresIn) * 1000).toISOString();
@@ -1034,7 +1089,10 @@ class PayGateServer {
1034
1089
  else if (params.expiresAt) {
1035
1090
  expiresAt = String(params.expiresAt);
1036
1091
  }
1037
- // Parse quota if provided
1092
+ else if (tpl && tpl.expiryTtlSeconds > 0) {
1093
+ expiresAt = new Date(Date.now() + tpl.expiryTtlSeconds * 1000).toISOString();
1094
+ }
1095
+ // Parse quota: explicit params > template > undefined
1038
1096
  let quota = undefined;
1039
1097
  if (params.quota) {
1040
1098
  quota = {
@@ -1044,15 +1102,28 @@ class PayGateServer {
1044
1102
  monthlyCreditLimit: Math.max(0, Math.floor(Number(params.quota.monthlyCreditLimit) || 0)),
1045
1103
  };
1046
1104
  }
1105
+ else if (tpl?.quota) {
1106
+ quota = { ...tpl.quota };
1107
+ }
1047
1108
  const record = this.gate.store.createKey(name, credits, {
1048
- allowedTools: params.allowedTools,
1049
- deniedTools: params.deniedTools,
1109
+ allowedTools: params.allowedTools || (tpl ? [...tpl.allowedTools] : undefined),
1110
+ deniedTools: params.deniedTools || (tpl ? [...tpl.deniedTools] : undefined),
1050
1111
  expiresAt,
1051
1112
  quota,
1052
- tags: params.tags,
1053
- ipAllowlist: params.ipAllowlist,
1054
- namespace: params.namespace,
1113
+ tags: params.tags || (tpl ? { ...tpl.tags } : undefined),
1114
+ ipAllowlist: params.ipAllowlist || (tpl ? [...tpl.ipAllowlist] : undefined),
1115
+ namespace: params.namespace || tpl?.namespace,
1055
1116
  });
1117
+ // Apply template spending limit if not explicitly set
1118
+ if (tpl && tpl.spendingLimit > 0 && record.spendingLimit === 0) {
1119
+ record.spendingLimit = tpl.spendingLimit;
1120
+ this.gate.store.save();
1121
+ }
1122
+ // Apply template auto-topup if not explicitly configured
1123
+ if (tpl?.autoTopup && !record.autoTopup) {
1124
+ record.autoTopup = { ...tpl.autoTopup };
1125
+ this.gate.store.save();
1126
+ }
1056
1127
  // Sync new key to Redis (if configured)
1057
1128
  if (this.redisSync) {
1058
1129
  this.redisSync.saveKey(record).catch(() => { });
@@ -2160,6 +2231,34 @@ class PayGateServer {
2160
2231
  ...usage,
2161
2232
  }, null, 2));
2162
2233
  }
2234
+ // ─── /keys/expiring — List keys expiring within a time window ───────────────
2235
+ handleKeysExpiring(req, res) {
2236
+ if (req.method !== 'GET') {
2237
+ res.writeHead(405, { 'Content-Type': 'application/json' });
2238
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
2239
+ return;
2240
+ }
2241
+ if (!this.checkAdmin(req, res))
2242
+ return;
2243
+ const urlParts = req.url?.split('?') || [];
2244
+ const params = new URLSearchParams(urlParts[1] || '');
2245
+ const withinStr = params.get('within');
2246
+ const within = withinStr ? parseInt(withinStr, 10) : 86400; // Default: 24 hours
2247
+ if (isNaN(within) || within <= 0) {
2248
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2249
+ res.end(JSON.stringify({ error: 'Invalid within parameter — must be a positive number of seconds' }));
2250
+ return;
2251
+ }
2252
+ const allKeys = this.gate.store.getAllRecords();
2253
+ const expiring = expiry_scanner_1.ExpiryScanner.queryExpiring(allKeys, within);
2254
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2255
+ res.end(JSON.stringify({
2256
+ within,
2257
+ count: expiring.length,
2258
+ scanner: this.expiryScanner.status,
2259
+ keys: expiring,
2260
+ }, null, 2));
2261
+ }
2163
2262
  // ─── /keys/auto-topup — Configure auto-topup ────────────────────────────────
2164
2263
  async handleSetAutoTopup(req, res) {
2165
2264
  if (req.method !== 'POST') {
@@ -4210,11 +4309,82 @@ class PayGateServer {
4210
4309
  res.writeHead(200, { 'Content-Type': 'application/json' });
4211
4310
  res.end(JSON.stringify({ revoked: true }));
4212
4311
  }
4213
- /**
4214
- * Sync a key mutation to Redis. Call after any local KeyStore mutation
4215
- * (setAcl, setExpiry, setQuota, setTags, setIpAllowlist, setSpendingLimit).
4216
- * Fire-and-forget: errors logged, never thrown.
4217
- */
4312
+ // ─── /keys/templates — CRUD ────────────────────────────────────────────────
4313
+ handleListTemplates(req, res) {
4314
+ if (!this.checkAdmin(req, res))
4315
+ return;
4316
+ const templates = this.templates.list();
4317
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4318
+ res.end(JSON.stringify({ total: templates.length, templates }));
4319
+ }
4320
+ async handleCreateTemplate(req, res) {
4321
+ if (!this.checkAdmin(req, res, 'admin'))
4322
+ return;
4323
+ const body = await this.readBody(req);
4324
+ let params;
4325
+ try {
4326
+ params = JSON.parse(body);
4327
+ }
4328
+ catch {
4329
+ res.writeHead(400, { 'Content-Type': 'application/json' });
4330
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
4331
+ return;
4332
+ }
4333
+ const name = params.name;
4334
+ if (!name || typeof name !== 'string') {
4335
+ res.writeHead(400, { 'Content-Type': 'application/json' });
4336
+ res.end(JSON.stringify({ error: 'Missing required field: name' }));
4337
+ return;
4338
+ }
4339
+ const existing = this.templates.get(name);
4340
+ const result = this.templates.set(name, params);
4341
+ if (!result.success) {
4342
+ res.writeHead(400, { 'Content-Type': 'application/json' });
4343
+ res.end(JSON.stringify({ error: result.error }));
4344
+ return;
4345
+ }
4346
+ const eventType = existing ? 'template.updated' : 'template.created';
4347
+ this.audit.log(eventType, 'admin', `${existing ? 'Updated' : 'Created'} template: ${name}`, {
4348
+ templateName: name,
4349
+ });
4350
+ res.writeHead(existing ? 200 : 201, { 'Content-Type': 'application/json' });
4351
+ res.end(JSON.stringify({ template: result.template }));
4352
+ }
4353
+ async handleDeleteTemplate(req, res) {
4354
+ if (req.method !== 'POST') {
4355
+ res.writeHead(405, { 'Content-Type': 'application/json' });
4356
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
4357
+ return;
4358
+ }
4359
+ if (!this.checkAdmin(req, res, 'admin'))
4360
+ return;
4361
+ const body = await this.readBody(req);
4362
+ let params;
4363
+ try {
4364
+ params = JSON.parse(body);
4365
+ }
4366
+ catch {
4367
+ res.writeHead(400, { 'Content-Type': 'application/json' });
4368
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
4369
+ return;
4370
+ }
4371
+ if (!params.name || typeof params.name !== 'string') {
4372
+ res.writeHead(400, { 'Content-Type': 'application/json' });
4373
+ res.end(JSON.stringify({ error: 'Missing required field: name' }));
4374
+ return;
4375
+ }
4376
+ const deleted = this.templates.delete(params.name);
4377
+ if (!deleted) {
4378
+ res.writeHead(404, { 'Content-Type': 'application/json' });
4379
+ res.end(JSON.stringify({ error: `Template "${params.name}" not found` }));
4380
+ return;
4381
+ }
4382
+ this.audit.log('template.deleted', 'admin', `Deleted template: ${params.name}`, {
4383
+ templateName: params.name,
4384
+ });
4385
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4386
+ res.end(JSON.stringify({ deleted: true, name: params.name }));
4387
+ }
4218
4388
  /**
4219
4389
  * Route admin webhook events through the WebhookRouter (for filter rules) or direct emitter.
4220
4390
  */
@@ -4264,6 +4434,7 @@ class PayGateServer {
4264
4434
  this.sessions.destroy();
4265
4435
  this.audit.destroy();
4266
4436
  this.tokens.destroy();
4437
+ this.expiryScanner.destroy();
4267
4438
  if (this.redisSync) {
4268
4439
  await this.redisSync.destroy();
4269
4440
  }
@@ -4319,6 +4490,7 @@ class PayGateServer {
4319
4490
  this.sessions.destroy();
4320
4491
  this.audit.destroy();
4321
4492
  this.tokens.destroy();
4493
+ this.expiryScanner.destroy();
4322
4494
  if (this.redisSync) {
4323
4495
  await this.redisSync.destroy();
4324
4496
  }