ebay-mcp-remote-edition 2.0.10 → 2.0.12
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/build/auth/kv-store.js +87 -2
- package/build/auth/multi-user-store.js +19 -3
- package/build/server-http.js +9 -3
- package/package.json +1 -1
package/build/auth/kv-store.js
CHANGED
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
/**
|
|
3
|
+
* Purely in-memory KV store with optional per-entry TTL.
|
|
4
|
+
* Useful for local development, single-user setups, or when
|
|
5
|
+
* EBAY_TOKEN_STORE_BACKEND=memory.
|
|
6
|
+
* All data is lost on process restart.
|
|
7
|
+
*/
|
|
8
|
+
export class InMemoryKVStore {
|
|
9
|
+
store = new Map();
|
|
10
|
+
async get(key) {
|
|
11
|
+
const entry = this.store.get(key);
|
|
12
|
+
if (!entry)
|
|
13
|
+
return null;
|
|
14
|
+
if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) {
|
|
15
|
+
this.store.delete(key);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return entry.value;
|
|
19
|
+
}
|
|
20
|
+
async put(key, value, expirationTtl) {
|
|
21
|
+
this.store.set(key, {
|
|
22
|
+
value,
|
|
23
|
+
expiresAt: expirationTtl !== undefined ? Date.now() + expirationTtl * 1_000 : undefined,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async delete(key) {
|
|
27
|
+
this.store.delete(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cloudflare KV REST API backend with an in-memory read-through cache.
|
|
32
|
+
* Used when EBAY_TOKEN_STORE_BACKEND=cloudflare-kv (the default for hosted deployments).
|
|
33
|
+
*/
|
|
2
34
|
export class CloudflareKVStore {
|
|
3
35
|
client;
|
|
4
36
|
accountId;
|
|
5
37
|
namespaceId;
|
|
6
|
-
|
|
38
|
+
/** In-memory read-through cache to avoid redundant Cloudflare KV API calls */
|
|
39
|
+
cache = new Map();
|
|
40
|
+
/** How long (ms) to hold a cached value before re-fetching from KV. Default: 5 minutes. */
|
|
41
|
+
cacheTtlMs;
|
|
42
|
+
constructor(cacheTtlMs = 5 * 60 * 1_000) {
|
|
43
|
+
this.cacheTtlMs = cacheTtlMs;
|
|
7
44
|
this.accountId = process.env.CLOUDFLARE_ACCOUNT_ID || '';
|
|
8
45
|
this.namespaceId = process.env.CLOUDFLARE_KV_NAMESPACE_ID || '';
|
|
9
46
|
const apiToken = process.env.CLOUDFLARE_API_TOKEN || '';
|
|
@@ -16,15 +53,27 @@ export class CloudflareKVStore {
|
|
|
16
53
|
timeout: 30000,
|
|
17
54
|
});
|
|
18
55
|
}
|
|
56
|
+
isCacheValid(entry) {
|
|
57
|
+
return Date.now() < entry.expiresAt;
|
|
58
|
+
}
|
|
19
59
|
async get(key) {
|
|
60
|
+
// Serve from in-memory cache when still fresh
|
|
61
|
+
const cached = this.cache.get(key);
|
|
62
|
+
if (cached && this.isCacheValid(cached)) {
|
|
63
|
+
return cached.value;
|
|
64
|
+
}
|
|
20
65
|
try {
|
|
21
66
|
const response = await this.client.get(`/values/${encodeURIComponent(key)}`, {
|
|
22
67
|
responseType: 'text',
|
|
23
68
|
});
|
|
24
|
-
|
|
69
|
+
const value = JSON.parse(response.data);
|
|
70
|
+
this.cache.set(key, { value, expiresAt: Date.now() + this.cacheTtlMs });
|
|
71
|
+
return value;
|
|
25
72
|
}
|
|
26
73
|
catch (error) {
|
|
27
74
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
75
|
+
// Cache the miss so we don't hammer KV for non-existent keys either
|
|
76
|
+
this.cache.set(key, { value: null, expiresAt: Date.now() + this.cacheTtlMs });
|
|
28
77
|
return null;
|
|
29
78
|
}
|
|
30
79
|
throw error;
|
|
@@ -33,8 +82,44 @@ export class CloudflareKVStore {
|
|
|
33
82
|
async put(key, value, expirationTtl) {
|
|
34
83
|
const params = expirationTtl ? { expiration_ttl: expirationTtl } : undefined;
|
|
35
84
|
await this.client.put(`/values/${encodeURIComponent(key)}`, JSON.stringify(value), { params });
|
|
85
|
+
// Keep the in-memory cache consistent with what we wrote.
|
|
86
|
+
// If the KV entry has its own TTL, honour it for the cache as well.
|
|
87
|
+
const cacheTtl = expirationTtl ? Math.min(expirationTtl * 1_000, this.cacheTtlMs) : this.cacheTtlMs;
|
|
88
|
+
this.cache.set(key, { value, expiresAt: Date.now() + cacheTtl });
|
|
36
89
|
}
|
|
37
90
|
async delete(key) {
|
|
38
91
|
await this.client.delete(`/values/${encodeURIComponent(key)}`);
|
|
92
|
+
this.cache.delete(key);
|
|
93
|
+
}
|
|
94
|
+
/** Manually evict a single key from the local cache (e.g. after an external update). */
|
|
95
|
+
invalidate(key) {
|
|
96
|
+
this.cache.delete(key);
|
|
97
|
+
}
|
|
98
|
+
/** Flush the entire in-memory cache. */
|
|
99
|
+
flushCache() {
|
|
100
|
+
this.cache.clear();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Returns the appropriate KV store backend based on the EBAY_TOKEN_STORE_BACKEND
|
|
106
|
+
* environment variable:
|
|
107
|
+
*
|
|
108
|
+
* memory → InMemoryKVStore (no external dependencies, data lost on restart)
|
|
109
|
+
* cloudflare-kv → CloudflareKVStore (default; requires CLOUDFLARE_* env vars)
|
|
110
|
+
*
|
|
111
|
+
* If the variable is unset or unrecognised, defaults to cloudflare-kv so that
|
|
112
|
+
* existing hosted deployments continue to work without any config change.
|
|
113
|
+
*/
|
|
114
|
+
export function createKVStore() {
|
|
115
|
+
const backend = (process.env.EBAY_TOKEN_STORE_BACKEND ?? 'cloudflare-kv').toLowerCase().trim();
|
|
116
|
+
switch (backend) {
|
|
117
|
+
case 'memory':
|
|
118
|
+
case 'in-memory':
|
|
119
|
+
return new InMemoryKVStore();
|
|
120
|
+
case 'cloudflare-kv':
|
|
121
|
+
case 'cloudflare':
|
|
122
|
+
default:
|
|
123
|
+
return new CloudflareKVStore();
|
|
39
124
|
}
|
|
40
125
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { createKVStore } from '../auth/kv-store.js';
|
|
3
3
|
export class MultiUserAuthStore {
|
|
4
|
-
kv =
|
|
4
|
+
kv = createKVStore();
|
|
5
|
+
/**
|
|
6
|
+
* In-memory map of sessionToken → timestamp of last KV write for touchSession.
|
|
7
|
+
* Prevents a KV PUT on every single authenticated request — we only persist
|
|
8
|
+
* `lastUsedAt` once per TOUCH_THROTTLE_MS (default: 1 hour).
|
|
9
|
+
*/
|
|
10
|
+
sessionTouchCache = new Map();
|
|
11
|
+
static TOUCH_THROTTLE_MS = 60 * 60 * 1_000; // 1 hour
|
|
5
12
|
stateKey(state) {
|
|
6
13
|
return `oauth_state:${state}`;
|
|
7
14
|
}
|
|
@@ -60,12 +67,21 @@ export class MultiUserAuthStore {
|
|
|
60
67
|
return await this.kv.get(this.sessionKey(sessionToken));
|
|
61
68
|
}
|
|
62
69
|
async touchSession(sessionToken) {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const lastTouched = this.sessionTouchCache.get(sessionToken);
|
|
72
|
+
// Skip the KV write entirely if we touched this session recently.
|
|
73
|
+
// The in-memory cache in CloudflareKVStore already keeps reads free,
|
|
74
|
+
// so the only cost we're avoiding here is the unnecessary KV PUT.
|
|
75
|
+
if (lastTouched !== undefined && now - lastTouched < MultiUserAuthStore.TOUCH_THROTTLE_MS) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
63
78
|
const record = await this.getSession(sessionToken);
|
|
64
79
|
if (!record || record.revokedAt) {
|
|
65
80
|
return;
|
|
66
81
|
}
|
|
67
|
-
record.lastUsedAt = new Date().toISOString();
|
|
82
|
+
record.lastUsedAt = new Date(now).toISOString();
|
|
68
83
|
await this.kv.put(this.sessionKey(sessionToken), record);
|
|
84
|
+
this.sessionTouchCache.set(sessionToken, now);
|
|
69
85
|
}
|
|
70
86
|
async revokeSession(sessionToken) {
|
|
71
87
|
const record = await this.getSession(sessionToken);
|
package/build/server-http.js
CHANGED
|
@@ -405,7 +405,9 @@ function createApp() {
|
|
|
405
405
|
}
|
|
406
406
|
});
|
|
407
407
|
app.get('/admin/session/:sessionToken', requireAdmin, async (req, res) => {
|
|
408
|
-
const
|
|
408
|
+
const tokenParam = req.params.sessionToken;
|
|
409
|
+
const sessionToken = typeof tokenParam === 'string' ? tokenParam : tokenParam[0];
|
|
410
|
+
const session = await authStore.getSession(sessionToken);
|
|
409
411
|
if (!session) {
|
|
410
412
|
res.status(404).json({ error: 'not_found' });
|
|
411
413
|
return;
|
|
@@ -433,11 +435,15 @@ function createApp() {
|
|
|
433
435
|
});
|
|
434
436
|
});
|
|
435
437
|
app.post('/admin/session/:sessionToken/revoke', requireAdmin, async (req, res) => {
|
|
436
|
-
|
|
438
|
+
const tokenParam = req.params.sessionToken;
|
|
439
|
+
const sessionToken = typeof tokenParam === 'string' ? tokenParam : tokenParam[0];
|
|
440
|
+
await authStore.revokeSession(sessionToken);
|
|
437
441
|
res.json({ ok: true, revoked: true });
|
|
438
442
|
});
|
|
439
443
|
app.delete('/admin/session/:sessionToken', requireAdmin, async (req, res) => {
|
|
440
|
-
|
|
444
|
+
const tokenParam = req.params.sessionToken;
|
|
445
|
+
const sessionToken = typeof tokenParam === 'string' ? tokenParam : tokenParam[0];
|
|
446
|
+
await authStore.deleteSession(sessionToken);
|
|
441
447
|
res.json({ ok: true, deleted: true });
|
|
442
448
|
});
|
|
443
449
|
const transports = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ebay-mcp-remote-edition",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.12",
|
|
4
4
|
"description": "Remote + Local MCP server for eBay APIs - provides access to eBay developer functionality through MCP (Model Context Protocol)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|