webcake-storefront-mcp 1.1.0 → 1.1.2
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 +1 -1
- package/README.vi.md +1 -1
- package/dist/auth/oauth-server.js +235 -39
- package/dist/config.js +1 -1
- package/dist/db.js +48 -61
- package/dist/http.js +14 -7
- package/dist/legal.js +1 -1
- package/dist/persistence/postgres.js +132 -0
- package/dist/persistence/redis.js +66 -0
- package/dist/tools/context.js +2 -2
- package/dist/tools/images.js +3 -3
- package/dist/web-guide.js +912 -324
- package/package.json +6 -3
package/dist/legal.js
CHANGED
|
@@ -55,7 +55,7 @@ handles, why, who receives it, and how long it is kept.</p>
|
|
|
55
55
|
tokens are never written to disk by the connector. Access tokens expire automatically after ~1 hour, refresh
|
|
56
56
|
tokens after ~30 days. A server restart clears all tokens.</li>
|
|
57
57
|
<li><strong>Local CLI config (stdio mode).</strong> When you run <code>npx webcake-storefront-mcp login</code>,
|
|
58
|
-
your token and session ID are saved to a local
|
|
58
|
+
your token and session ID are saved to a local file on <em>your own machine</em> (at
|
|
59
59
|
<code>~/.webcake-storefront-mcp.db</code> or similar). This file stays on your device and is not transmitted
|
|
60
60
|
anywhere by the connector.</li>
|
|
61
61
|
<li>The connector does <strong>not</strong> run an analytics database, does <strong>not</strong> sell or share
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, shared Postgres pool used to PERSIST the OAuth 2.1 Authorization Server
|
|
3
|
+
* state (clients, pending auths, codes, access + refresh tokens) so tokens
|
|
4
|
+
* survive a `serve` restart and are shared across instances behind a load
|
|
5
|
+
* balancer — unlike the caches (Redis/disposable), OAuth state is durable.
|
|
6
|
+
*
|
|
7
|
+
* Returns null when no DATABASE_URL is configured OR `pg` isn't installed — the
|
|
8
|
+
* OAuth store then falls back to in-memory maps, so single-instance `serve`,
|
|
9
|
+
* stdio/`npx`, and the offline smoke gate keep working with ZERO infra.
|
|
10
|
+
*
|
|
11
|
+
* `pg` is an OPTIONAL, CJS dependency (see package.json), required via
|
|
12
|
+
* createRequire under ESM/Node16. The pool connects lazily per query.
|
|
13
|
+
*
|
|
14
|
+
* Configure with DATABASE_URL (or WEBCAKE_POSTGRES_URL), e.g.
|
|
15
|
+
* postgres://user:pw@host:5432/webcake_storefront
|
|
16
|
+
*
|
|
17
|
+
* KEY DIFFERENCE vs. landing-mcp: the credential is a PAIR (jwt + wsid), so
|
|
18
|
+
* oauth_codes / oauth_access_tokens / oauth_refresh_tokens carry two columns
|
|
19
|
+
* (`jwt text NOT NULL` and `wsid text`) instead of a single `ljwt` column.
|
|
20
|
+
*/
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
let cached; // undefined = not yet resolved
|
|
24
|
+
function redactUrl(u) {
|
|
25
|
+
try {
|
|
26
|
+
const x = new URL(u);
|
|
27
|
+
if (x.password)
|
|
28
|
+
x.password = "***";
|
|
29
|
+
return x.toString();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "postgres";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns the shared Postgres pool, or null if Postgres isn't configured/available.
|
|
37
|
+
* Memoized: resolves the pool (or its absence) exactly once per process.
|
|
38
|
+
*/
|
|
39
|
+
export function getPg() {
|
|
40
|
+
if (cached !== undefined)
|
|
41
|
+
return cached;
|
|
42
|
+
const url = process.env.DATABASE_URL || process.env.WEBCAKE_POSTGRES_URL;
|
|
43
|
+
if (!url)
|
|
44
|
+
return (cached = null);
|
|
45
|
+
try {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const { Pool } = require("pg");
|
|
48
|
+
const pool = new Pool({
|
|
49
|
+
connectionString: url,
|
|
50
|
+
max: Number(process.env.WEBCAKE_PG_POOL_MAX) || 5,
|
|
51
|
+
// Managed Postgres (Supabase, Neon, …) often requires TLS; allow opting in
|
|
52
|
+
// without verifying the chain via WEBCAKE_PG_SSL=1.
|
|
53
|
+
ssl: /^(1|true|yes|on)$/i.test(process.env.WEBCAKE_PG_SSL ?? "")
|
|
54
|
+
? { rejectUnauthorized: false }
|
|
55
|
+
: undefined,
|
|
56
|
+
});
|
|
57
|
+
pool.on("error", (e) => console.error("[pg] pool error:", e?.message ?? e));
|
|
58
|
+
console.error(`[pg] OAuth store backend: ${redactUrl(url)}`);
|
|
59
|
+
cached = pool;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error("[pg] unavailable, using in-memory OAuth store:", e?.message ?? e);
|
|
63
|
+
cached = null;
|
|
64
|
+
}
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create the OAuth tables if absent. Idempotent and memoized to a single
|
|
69
|
+
* in-flight promise per process, so concurrent callers share one round-trip. On
|
|
70
|
+
* any failure it logs and resolves false; the caller degrades to in-memory.
|
|
71
|
+
*
|
|
72
|
+
* Storefront schema: codes/tokens carry TWO credential columns:
|
|
73
|
+
* jwt text NOT NULL — the user's WebCake JWT
|
|
74
|
+
* wsid text — the WebCake session/workspace ID (may be empty)
|
|
75
|
+
*/
|
|
76
|
+
let schemaReady;
|
|
77
|
+
export function ensureOAuthSchema(pool) {
|
|
78
|
+
if (schemaReady)
|
|
79
|
+
return schemaReady;
|
|
80
|
+
schemaReady = (async () => {
|
|
81
|
+
try {
|
|
82
|
+
await pool.query(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
84
|
+
client_id text PRIMARY KEY,
|
|
85
|
+
client_name text,
|
|
86
|
+
redirect_uris jsonb NOT NULL,
|
|
87
|
+
created_at bigint NOT NULL
|
|
88
|
+
);
|
|
89
|
+
CREATE TABLE IF NOT EXISTS oauth_pending (
|
|
90
|
+
state text PRIMARY KEY,
|
|
91
|
+
client_id text NOT NULL,
|
|
92
|
+
redirect_uri text NOT NULL,
|
|
93
|
+
code_challenge text NOT NULL,
|
|
94
|
+
client_state text,
|
|
95
|
+
scope text,
|
|
96
|
+
expires_at bigint NOT NULL
|
|
97
|
+
);
|
|
98
|
+
CREATE TABLE IF NOT EXISTS oauth_codes (
|
|
99
|
+
code text PRIMARY KEY,
|
|
100
|
+
client_id text NOT NULL,
|
|
101
|
+
redirect_uri text NOT NULL,
|
|
102
|
+
code_challenge text NOT NULL,
|
|
103
|
+
scope text,
|
|
104
|
+
jwt text NOT NULL,
|
|
105
|
+
wsid text,
|
|
106
|
+
expires_at bigint NOT NULL
|
|
107
|
+
);
|
|
108
|
+
CREATE TABLE IF NOT EXISTS oauth_access_tokens (
|
|
109
|
+
token text PRIMARY KEY,
|
|
110
|
+
jwt text NOT NULL,
|
|
111
|
+
wsid text,
|
|
112
|
+
scope text,
|
|
113
|
+
expires_at bigint NOT NULL
|
|
114
|
+
);
|
|
115
|
+
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
|
|
116
|
+
token text PRIMARY KEY,
|
|
117
|
+
jwt text NOT NULL,
|
|
118
|
+
wsid text,
|
|
119
|
+
client_id text NOT NULL,
|
|
120
|
+
scope text,
|
|
121
|
+
expires_at bigint NOT NULL
|
|
122
|
+
);
|
|
123
|
+
`);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
console.error("[pg] OAuth schema init failed, using in-memory:", e?.message ?? e);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
return schemaReady;
|
|
132
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, shared ioredis client used as a SHORT-TTL CACHE for OAuth access-token
|
|
3
|
+
* lookups (access_token → {jwt,wsid}). Returns null when no REDIS_URL is
|
|
4
|
+
* configured OR ioredis isn't installed — every caller then falls back to going
|
|
5
|
+
* directly to Postgres (or the in-memory map), so stdio/`npx` users and the
|
|
6
|
+
* offline `npm run smoke` gate keep working with ZERO infra.
|
|
7
|
+
*
|
|
8
|
+
* The cache is intentionally disposable: losing Redis (restart, eviction,
|
|
9
|
+
* expiry) just adds one Postgres round-trip per /mcp request — never a failure.
|
|
10
|
+
* We never block startup on the connection and tolerate command errors by
|
|
11
|
+
* degrading to the source-of-truth store on a per-call basis.
|
|
12
|
+
*
|
|
13
|
+
* ioredis is an OPTIONAL, CJS dependency (see package.json), so we require it via
|
|
14
|
+
* createRequire under ESM/Node16. `new Redis(url)` returns immediately and
|
|
15
|
+
* connects in the background; commands queue until the socket is up.
|
|
16
|
+
*
|
|
17
|
+
* Configure with REDIS_URL (or WEBCAKE_REDIS_URL), e.g. redis://default:pw@host:6379/0
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
let cached; // undefined = not yet resolved
|
|
22
|
+
function redactUrl(u) {
|
|
23
|
+
try {
|
|
24
|
+
const x = new URL(u);
|
|
25
|
+
if (x.password)
|
|
26
|
+
x.password = "***";
|
|
27
|
+
return x.toString();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "redis";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns the shared Redis client, or null if Redis isn't configured/available.
|
|
35
|
+
* Memoized: resolves the connection (or its absence) exactly once per process.
|
|
36
|
+
*/
|
|
37
|
+
export function getRedis() {
|
|
38
|
+
if (cached !== undefined)
|
|
39
|
+
return cached;
|
|
40
|
+
const url = process.env.REDIS_URL || process.env.WEBCAKE_REDIS_URL;
|
|
41
|
+
if (!url)
|
|
42
|
+
return (cached = null);
|
|
43
|
+
try {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
|
+
const mod = require("ioredis");
|
|
46
|
+
const Redis = mod.default ?? mod;
|
|
47
|
+
const client = new Redis(url, {
|
|
48
|
+
maxRetriesPerRequest: 2,
|
|
49
|
+
enableOfflineQueue: true,
|
|
50
|
+
// Never let a connection blip crash the process — log and keep retrying.
|
|
51
|
+
retryStrategy: (times) => Math.min(times * 200, 3000),
|
|
52
|
+
});
|
|
53
|
+
client.on("error", (e) => console.error("[redis] error:", e?.message ?? e));
|
|
54
|
+
console.error(`[redis] OAuth token cache: ${redactUrl(url)}`);
|
|
55
|
+
cached = client;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error("[redis] unavailable, using direct store lookups:", e?.message ?? e);
|
|
59
|
+
cached = null;
|
|
60
|
+
}
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
/** True when a Redis cache backend is configured (used for log/diagnostics). */
|
|
64
|
+
export function redisEnabled() {
|
|
65
|
+
return getRedis() !== null;
|
|
66
|
+
}
|
package/dist/tools/context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getConfig, setConfig } from "../db.js";
|
|
3
|
-
/** Read all saved credentials from
|
|
3
|
+
/** Read all saved credentials from the local config file for startup */
|
|
4
4
|
export function getSavedConfig() {
|
|
5
5
|
return {
|
|
6
6
|
token: getConfig("token") || "",
|
|
@@ -118,7 +118,7 @@ Get token and session_id from browser DevTools → Network tab → copy from any
|
|
|
118
118
|
api.baseUrl = oldBaseUrl;
|
|
119
119
|
throw new Error("Authentication failed — credentials were NOT changed. Make sure token and session_id are both correct.");
|
|
120
120
|
}
|
|
121
|
-
// Persist all to
|
|
121
|
+
// Persist all to the local config file
|
|
122
122
|
if (token)
|
|
123
123
|
setConfig("token", token);
|
|
124
124
|
if (session_id)
|
package/dist/tools/images.js
CHANGED
|
@@ -606,7 +606,7 @@ If alt_path is omitted, it is auto-detected via the same probe used by list_imag
|
|
|
606
606
|
}));
|
|
607
607
|
// ── Alt cache tools ──
|
|
608
608
|
server.tool("get_cached_image_alts", `Look up cached alt descriptions for image URLs. URLs are matched by normalized form (query string stripped, lowercase). Use BEFORE running read_image/OCR — skip already-described URLs.
|
|
609
|
-
When MONGO_URI is set, misses are then checked against MongoDB and successful hits are backfilled into the local
|
|
609
|
+
When MONGO_URI is set, misses are then checked against MongoDB and successful hits are backfilled into the local cache for fast re-lookup.`, {
|
|
610
610
|
urls: z.array(z.string()).min(1).describe("Image URLs to look up"),
|
|
611
611
|
}, ({ urls }) => handle(async () => {
|
|
612
612
|
const hits = [];
|
|
@@ -693,7 +693,7 @@ When MONGO_URI is set, misses are then checked against MongoDB and successful hi
|
|
|
693
693
|
return { total, count: rows.length, entries: rows };
|
|
694
694
|
}));
|
|
695
695
|
// ── Mongo sync (active when MONGO_URI is set) ──
|
|
696
|
-
server.tool("sync_image_alts_to_mongo", `Push local
|
|
696
|
+
server.tool("sync_image_alts_to_mongo", `Push local alt cache entries up to MongoDB. Bulk upsert keyed by url_key. Use when you want to back up local-only entries to the shared central store, or after a session of heavy AI describes.
|
|
697
697
|
Requires MONGO_URI env var.`, {
|
|
698
698
|
limit: z.number().default(1000).describe("Max entries to push per call"),
|
|
699
699
|
offset: z.number().default(0).describe("Offset into local cache"),
|
|
@@ -706,7 +706,7 @@ Requires MONGO_URI env var.`, {
|
|
|
706
706
|
const res = await mongoUpsertAlts(rows.map((r) => ({ url_key: r.url_key, url: r.url, alt: r.alt, source: r.source })));
|
|
707
707
|
return { pushed: rows.length, ...res, total_local: countImageAlts() };
|
|
708
708
|
}));
|
|
709
|
-
server.tool("sync_image_alts_from_mongo", `Pull MongoDB alt entries down into local
|
|
709
|
+
server.tool("sync_image_alts_from_mongo", `Pull MongoDB alt entries down into local cache. Useful when starting on a new machine/site to warm the local cache from the central store.
|
|
710
710
|
Requires MONGO_URI env var.`, {
|
|
711
711
|
limit: z.number().default(1000).describe("Max entries to pull"),
|
|
712
712
|
offset: z.number().default(0).describe("Offset into Mongo collection"),
|