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/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 SQLite file on <em>your own machine</em> (at
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
+ }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { getConfig, setConfig } from "../db.js";
3
- /** Read all saved credentials from SQLite for startup */
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 SQLite
121
+ // Persist all to the local config file
122
122
  if (token)
123
123
  setConfig("token", token);
124
124
  if (session_id)
@@ -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 SQLite cache for fast re-lookup.`, {
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 SQLite 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.
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 SQLite cache. Useful when starting on a new machine/site to warm the local cache from the central store.
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"),