webcake-storefront-mcp 1.0.3 → 1.1.1

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/http.js CHANGED
@@ -1,13 +1,34 @@
1
1
  // Remote MCP over Streamable-HTTP. Each client session carries its own credentials,
2
2
  // supplied per-request via headers (x-webcake-jwt / x-webcake-site-id / x-webcake-api-url)
3
3
  // or query params (?jwt=&site_id=&api_url=) for clients that can't set custom headers.
4
+ //
5
+ // Also serves: a marketing landing page at /, /privacy, /terms, /favicon.ico,
6
+ // /favicon.svg, /health, and a full OAuth 2.1 flow so the claude.ai connector can
7
+ // authenticate. See src/auth/oauth-server.ts for the OAuth AS implementation.
4
8
  import { createServer as createHttpServer } from "node:http";
5
9
  import { randomUUID } from "node:crypto";
6
10
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
11
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
8
12
  import { createServer } from "./server.js";
9
- import { makeApi } from "./config.js";
13
+ import { makeApi, resolveEnv, ENVIRONMENTS, DEFAULT_ENV } from "./config.js";
14
+ import { landingHtml, faviconSvg, ogImageSvg, normalizeLang } from "./web-guide.js";
15
+ import { privacyHtml, termsHtml } from "./legal.js";
16
+ import { registerClient, startAuthorize, completeAuthorize, exchangeToken, resolveAccessToken, revokeToken, authServerMetadata, protectedResourceMetadata, } from "./auth/oauth-server.js";
10
17
  const MCP_PATH = "/mcp";
18
+ // OAuth 2.1 endpoints.
19
+ const WELL_KNOWN_PR = "/.well-known/oauth-protected-resource";
20
+ const WELL_KNOWN_AS = "/.well-known/oauth-authorization-server";
21
+ const OAUTH_REGISTER = "/register";
22
+ const OAUTH_AUTHORIZE = "/authorize";
23
+ const OAUTH_CALLBACK = "/oauth/callback";
24
+ const OAUTH_TOKEN = "/token";
25
+ const OAUTH_REVOKE = "/revoke";
26
+ // OAuth enforcement is ON by default: a request with NO credential gets a
27
+ // 401 + WWW-Authenticate so Claude/ChatGPT kick off the OAuth flow.
28
+ // Opt out with WEBCAKE_OAUTH=0 (or false/no/off).
29
+ const OAUTH_ENFORCED = !/^(0|false|no|off)$/i.test(process.env.WEBCAKE_OAUTH ?? "");
30
+ // Social/search crawlers fetch the root with Accept: */* but should still get HTML.
31
+ const BOT_UA = /facebookexternalhit|facebot|twitterbot|linkedinbot|slackbot|slack-imgproxy|telegrambot|whatsapp|discordbot|pinterest|redditbot|googlebot|bingbot|applebot|yandexbot|baiduspider|embedly|quora link preview|outbrain|vkshare|w3c_validator|skypeuripreview|zalo/i;
11
32
  const QUERY_TO_HEADER = {
12
33
  jwt: "x-webcake-jwt",
13
34
  token: "x-webcake-jwt",
@@ -28,8 +49,10 @@ function applyQueryAuth(req) {
28
49
  const params = new URLSearchParams((req.url ?? "").slice(q + 1));
29
50
  for (const [param, head] of Object.entries(QUERY_TO_HEADER)) {
30
51
  const value = params.get(param);
31
- if (value && req.headers[head] == null)
52
+ if (value && req.headers[head] == null) {
32
53
  req.headers[head] = value;
54
+ req.rawHeaders.push(head, value);
55
+ }
33
56
  }
34
57
  }
35
58
  function apiFromRequest(req) {
@@ -59,23 +82,285 @@ function readBody(req) {
59
82
  req.on("error", reject);
60
83
  });
61
84
  }
62
- function rpcError(res, status, message) {
85
+ async function readRawBody(req) {
86
+ const chunks = [];
87
+ for await (const c of req)
88
+ chunks.push(c);
89
+ return Buffer.concat(chunks).toString("utf8").trim();
90
+ }
91
+ function parseBodyParams(raw, contentType) {
92
+ if (!raw)
93
+ return {};
94
+ if (contentType.includes("application/json")) {
95
+ try {
96
+ const o = JSON.parse(raw);
97
+ return o && typeof o === "object" ? o : {};
98
+ }
99
+ catch {
100
+ return {};
101
+ }
102
+ }
103
+ const out = {};
104
+ for (const [k, v] of new URLSearchParams(raw))
105
+ out[k] = v;
106
+ return out;
107
+ }
108
+ function sendJson(res, status, body) {
63
109
  res.writeHead(status, { "content-type": "application/json" });
64
- res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message }, id: null }));
110
+ res.end(JSON.stringify(body));
111
+ }
112
+ function rpcError(res, status, message) {
113
+ sendJson(res, status, { jsonrpc: "2.0", error: { code: -32000, message }, id: null });
114
+ }
115
+ function oauthError(res, status, error, description) {
116
+ res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
117
+ res.end(JSON.stringify({ error, error_description: description }));
118
+ }
119
+ function htmlError(res, status, message) {
120
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
121
+ res.end(`<!doctype html><meta charset="utf-8"><body style="font-family:system-ui;padding:40px;max-width:520px;margin:auto"><h2>WebCake Storefront MCP</h2><p>${message}</p></body>`);
122
+ }
123
+ /** The public origin of this server, honouring reverse-proxy headers. */
124
+ function publicBase(req) {
125
+ const fwdHost = req.headers["x-forwarded-host"];
126
+ const host = (Array.isArray(fwdHost) ? fwdHost[0] : fwdHost) || req.headers.host || "localhost";
127
+ const fwdProto = req.headers["x-forwarded-proto"];
128
+ const isLocal = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i.test(host);
129
+ const proto = (Array.isArray(fwdProto) ? fwdProto[0] : fwdProto)?.split(",")[0] || (isLocal ? "http" : "https");
130
+ return `${proto}://${host}`;
131
+ }
132
+ /** The storefront consent page URL — /mcp-storefront on the builder app. */
133
+ function storefrontConsentUrl() {
134
+ const preset = resolveEnv(process.env.WEBCAKE_ENV) ?? ENVIRONMENTS[DEFAULT_ENV];
135
+ const appUrl = (process.env.WEBCAKE_APP_URL || preset.appUrl).replace(/\/$/, "");
136
+ return `${appUrl}/mcp-storefront`;
137
+ }
138
+ /** Extract the Bearer token from the Authorization header, if any. */
139
+ function bearerFrom(req) {
140
+ const auth = req.headers["authorization"];
141
+ const v = Array.isArray(auth) ? auth[0] : auth;
142
+ if (!v || !/^Bearer\s+/i.test(v))
143
+ return undefined;
144
+ return v.replace(/^Bearer\s+/i, "").trim() || undefined;
145
+ }
146
+ /**
147
+ * Handle every OAuth 2.1 endpoint. Returns true when the request was handled.
148
+ *
149
+ * KEY ADAPTATION vs. landing-mcp: the credential is a PAIR { jwt, wsid }.
150
+ * The /oauth/callback receives both `token` (jwt) and `wsid` from the consent
151
+ * page redirect_uri. Both are stored and later injected as x-webcake-jwt AND
152
+ * x-webcake-session-id headers onto /mcp requests.
153
+ */
154
+ async function handleOAuth(req, res, path) {
155
+ const issuer = publicBase(req);
156
+ // ---- Metadata ----
157
+ if (req.method === "GET" && path === WELL_KNOWN_PR) {
158
+ res.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*" });
159
+ res.end(JSON.stringify(protectedResourceMetadata(`${issuer}${MCP_PATH}`, issuer)));
160
+ return true;
161
+ }
162
+ if (req.method === "GET" && path === WELL_KNOWN_AS) {
163
+ res.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*" });
164
+ res.end(JSON.stringify(authServerMetadata(issuer)));
165
+ return true;
166
+ }
167
+ // ---- Dynamic Client Registration ----
168
+ if (path === OAUTH_REGISTER) {
169
+ if (req.method === "OPTIONS") {
170
+ res.writeHead(204, { "access-control-allow-origin": "*", "access-control-allow-headers": "*", "access-control-allow-methods": "POST,OPTIONS" });
171
+ res.end();
172
+ return true;
173
+ }
174
+ if (req.method !== "POST") {
175
+ oauthError(res, 405, "invalid_request", "Use POST.");
176
+ return true;
177
+ }
178
+ const raw = await readRawBody(req);
179
+ const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
180
+ const result = await registerClient(body);
181
+ if (!result.ok) {
182
+ oauthError(res, 400, result.error, result.error_description);
183
+ return true;
184
+ }
185
+ res.writeHead(201, { "content-type": "application/json", "access-control-allow-origin": "*", "cache-control": "no-store" });
186
+ res.end(JSON.stringify({
187
+ client_id: result.client.client_id,
188
+ client_id_issued_at: Math.floor(result.client.created_at / 1000),
189
+ redirect_uris: result.client.redirect_uris,
190
+ token_endpoint_auth_method: "none",
191
+ grant_types: ["authorization_code", "refresh_token"],
192
+ response_types: ["code"],
193
+ }));
194
+ return true;
195
+ }
196
+ // ---- Authorize: validate + redirect to storefront consent page ----
197
+ if (req.method === "GET" && path === OAUTH_AUTHORIZE) {
198
+ const sp = new URL(req.url ?? "/", "http://x").searchParams;
199
+ const result = await startAuthorize({
200
+ client_id: sp.get("client_id"),
201
+ redirect_uri: sp.get("redirect_uri"),
202
+ response_type: sp.get("response_type"),
203
+ code_challenge: sp.get("code_challenge"),
204
+ code_challenge_method: sp.get("code_challenge_method"),
205
+ state: sp.get("state"),
206
+ scope: sp.get("scope"),
207
+ });
208
+ if (!result.ok) {
209
+ if (result.redirectable) {
210
+ const r = new URL(sp.get("redirect_uri"));
211
+ r.searchParams.set("error", result.error);
212
+ r.searchParams.set("error_description", result.error_description);
213
+ const st = sp.get("state");
214
+ if (st)
215
+ r.searchParams.set("state", st);
216
+ res.writeHead(302, { location: r.toString() });
217
+ res.end();
218
+ return true;
219
+ }
220
+ htmlError(res, 400, result.error_description);
221
+ return true;
222
+ }
223
+ // Build the consent URL: /mcp-storefront?redirect_uri=<callback>&state=<internalState>
224
+ // The SPA will redirect back to callback with ?token=<jwt>&wsid=<wsid>&state=<internalState>
225
+ const callback = `${issuer}${OAUTH_CALLBACK}`;
226
+ const consentBase = storefrontConsentUrl();
227
+ const loginUrl = `${consentBase}?redirect_uri=${encodeURIComponent(callback)}&state=${encodeURIComponent(result.internalState)}`;
228
+ res.writeHead(302, { location: loginUrl });
229
+ res.end();
230
+ return true;
231
+ }
232
+ // ---- Callback: SPA sent back token (jwt) + wsid + state ----
233
+ if (req.method === "GET" && path === OAUTH_CALLBACK) {
234
+ const sp = new URL(req.url ?? "/", "http://x").searchParams;
235
+ // Accept both 'token' and 'jwt' from the SPA (login.ts uses both aliases).
236
+ const jwt = sp.get("token") || sp.get("jwt");
237
+ const wsid = sp.get("wsid") || sp.get("session_id") || "";
238
+ const done = await completeAuthorize(sp.get("state"), jwt, wsid);
239
+ if (!done.ok) {
240
+ htmlError(res, 400, done.error_description);
241
+ return true;
242
+ }
243
+ const r = new URL(done.redirectUri);
244
+ r.searchParams.set("code", done.code);
245
+ if (done.state)
246
+ r.searchParams.set("state", done.state);
247
+ res.writeHead(302, { location: r.toString() });
248
+ res.end();
249
+ return true;
250
+ }
251
+ // ---- Token ----
252
+ if (path === OAUTH_TOKEN) {
253
+ if (req.method === "OPTIONS") {
254
+ res.writeHead(204, { "access-control-allow-origin": "*", "access-control-allow-headers": "*", "access-control-allow-methods": "POST,OPTIONS" });
255
+ res.end();
256
+ return true;
257
+ }
258
+ if (req.method !== "POST") {
259
+ oauthError(res, 405, "invalid_request", "Use POST.");
260
+ return true;
261
+ }
262
+ const raw = await readRawBody(req);
263
+ const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
264
+ const result = await exchangeToken(body);
265
+ if (!result.ok) {
266
+ oauthError(res, result.status, result.error, result.error_description);
267
+ return true;
268
+ }
269
+ res.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*", "cache-control": "no-store" });
270
+ res.end(JSON.stringify(result.body));
271
+ return true;
272
+ }
273
+ // ---- Revoke ----
274
+ if (path === OAUTH_REVOKE) {
275
+ if (req.method !== "POST") {
276
+ oauthError(res, 405, "invalid_request", "Use POST.");
277
+ return true;
278
+ }
279
+ const raw = await readRawBody(req);
280
+ const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
281
+ revokeToken(body.token);
282
+ res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
283
+ res.end("{}");
284
+ return true;
285
+ }
286
+ return false;
65
287
  }
66
288
  export async function startHttpServer(port) {
67
289
  const transports = new Map();
68
290
  const httpServer = createHttpServer(async (req, res) => {
69
291
  const path = (req.url ?? "").split("?")[0];
70
- if (path === "/health") {
71
- res.writeHead(200, { "content-type": "application/json" });
72
- res.end(JSON.stringify({ ok: true }));
292
+ // ---- Favicon / brand icon ----
293
+ if (req.method === "GET" && (path === "/favicon.ico" || path === "/favicon.svg")) {
294
+ res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "public, max-age=86400" });
295
+ res.end(faviconSvg());
296
+ return;
297
+ }
298
+ // ---- OG social card ----
299
+ if (req.method === "GET" && path === "/og.svg") {
300
+ res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "public, max-age=86400" });
301
+ res.end(ogImageSvg());
302
+ return;
303
+ }
304
+ // ---- Legal pages ----
305
+ if (req.method === "GET" && (path === "/privacy" || path === "/privacy-policy")) {
306
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "public, max-age=3600" });
307
+ res.end(privacyHtml());
308
+ return;
309
+ }
310
+ if (req.method === "GET" && (path === "/terms" || path === "/tos")) {
311
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "public, max-age=3600" });
312
+ res.end(termsHtml());
73
313
  return;
74
314
  }
315
+ // ---- Health + landing page ----
316
+ if (req.method === "GET" && (path === "/" || path === "/health")) {
317
+ if (path === "/") {
318
+ const accept = String(req.headers["accept"] ?? "");
319
+ const ua = String(req.headers["user-agent"] ?? "");
320
+ if (accept.includes("text/html") || BOT_UA.test(ua)) {
321
+ const lang = normalizeLang(new URL(req.url ?? "/", "http://x").searchParams.get("lang"));
322
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
323
+ res.end(landingHtml(publicBase(req), lang));
324
+ return;
325
+ }
326
+ }
327
+ sendJson(res, 200, { ok: true, server: "webcake-storefront", transport: "streamable-http", endpoint: MCP_PATH });
328
+ return;
329
+ }
330
+ // ---- OAuth 2.1 endpoints (always served) ----
331
+ if (await handleOAuth(req, res, path))
332
+ return;
333
+ // ---- 404 for anything other than /mcp ----
75
334
  if (path !== MCP_PATH) {
76
- return rpcError(res, 404, `Not found. Send MCP requests to ${MCP_PATH}.`);
335
+ rpcError(res, 404, `Not found. Send MCP requests to ${MCP_PATH}.`);
336
+ return;
77
337
  }
338
+ // ---- /mcp handling ----
339
+ // Accept credentials via query params for clients that can't set headers.
78
340
  applyQueryAuth(req);
341
+ // Resolve an OAuth Bearer access token to { jwt, wsid } and inject both headers
342
+ // so apiFromRequest picks them up — existing header/query paths remain untouched.
343
+ const bearer = bearerFrom(req);
344
+ const oauthCred = await resolveAccessToken(bearer);
345
+ if (oauthCred) {
346
+ if (req.headers["x-webcake-jwt"] == null) {
347
+ req.headers["x-webcake-jwt"] = oauthCred.jwt;
348
+ req.rawHeaders.push("x-webcake-jwt", oauthCred.jwt);
349
+ }
350
+ if (oauthCred.wsid && req.headers["x-webcake-session-id"] == null) {
351
+ req.headers["x-webcake-session-id"] = oauthCred.wsid;
352
+ req.rawHeaders.push("x-webcake-session-id", oauthCred.wsid);
353
+ }
354
+ }
355
+ // Enforce OAuth when enabled: a request with no recognised credential gets 401.
356
+ if (OAUTH_ENFORCED && !oauthCred && req.headers["x-webcake-jwt"] == null) {
357
+ res.writeHead(401, {
358
+ "www-authenticate": `Bearer resource_metadata="${publicBase(req)}${WELL_KNOWN_PR}"`,
359
+ "content-type": "application/json",
360
+ });
361
+ res.end(JSON.stringify({ error: "invalid_token", error_description: "Authentication required — connect via OAuth." }));
362
+ return;
363
+ }
79
364
  const sidHeader = header(req, "mcp-session-id");
80
365
  try {
81
366
  // Reuse an existing session.
@@ -105,9 +390,10 @@ export async function startHttpServer(port) {
105
390
  await transport.handleRequest(req, res, body);
106
391
  return;
107
392
  }
108
- return rpcError(res, 400, "Bad Request: send an initialize request first (no valid mcp-session-id).");
393
+ rpcError(res, 400, "Bad Request: send an initialize request first (no valid mcp-session-id).");
394
+ return;
109
395
  }
110
- return rpcError(res, 400, "Bad Request: missing or unknown mcp-session-id.");
396
+ rpcError(res, 400, "Bad Request: missing or unknown mcp-session-id.");
111
397
  }
112
398
  catch (e) {
113
399
  const msg = e instanceof Error ? e.message : String(e);
package/dist/legal.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Privacy Policy + Terms of Service pages for the WebCake Storefront MCP connector.
3
+ *
4
+ * Hosted at /privacy (also /privacy-policy) and /terms (also /tos) so the
5
+ * Claude Connectors Directory submission can point at stable, self-hosted URLs.
6
+ * Plain self-contained HTML — no external deps, served by http.ts.
7
+ */
8
+ const CONTACT_EMAIL = process.env.WEBCAKE_SUPPORT_EMAIL || "vuluu040320@gmail.com";
9
+ const LAST_UPDATED = "2026-06-23";
10
+ function page(title, bodyHtml) {
11
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8">
12
+ <meta name="viewport" content="width=device-width,initial-scale=1"><title>${title} — WebCake Storefront MCP</title>
13
+ <style>
14
+ :root{color-scheme:light dark}
15
+ body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;line-height:1.65;color:#1e293b;background:#f8fafc}
16
+ @media(prefers-color-scheme:dark){body{color:#e2e8f0;background:#0f172a}}
17
+ main{max-width:760px;margin:0 auto;padding:48px 24px 80px}
18
+ h1{font-size:1.9rem;margin:0 0 4px}
19
+ h2{font-size:1.2rem;margin:32px 0 8px}
20
+ .meta{color:#64748b;font-size:.9rem;margin-bottom:28px}
21
+ a{color:#6d5efc}
22
+ code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:rgba(127,127,127,.15);padding:1px 5px;border-radius:4px}
23
+ ul{padding-left:22px}
24
+ footer{margin-top:48px;padding-top:20px;border-top:1px solid rgba(127,127,127,.25);color:#64748b;font-size:.85rem}
25
+ </style></head>
26
+ <body><main>${bodyHtml}
27
+ <footer>WebCake Storefront MCP &middot; <a href="https://webcake.io">webcake.io</a> &middot; <a href="https://github.com/vuluu2k/webcake-storefront-mcp">source</a> &middot; Contact: <a href="mailto:${CONTACT_EMAIL}">${CONTACT_EMAIL}</a></footer>
28
+ </main></body></html>`;
29
+ }
30
+ export function privacyHtml() {
31
+ return page("Privacy Policy", `<h1>Privacy Policy</h1>
32
+ <div class="meta">Last updated: ${LAST_UPDATED}</div>
33
+ <p>WebCake Storefront MCP ("the connector") is a Model Context Protocol server that lets an AI assistant
34
+ build and manage storefront pages, products, articles, and orders in your
35
+ <a href="https://webcake.io">WebCake / StoreCake</a> account. This policy explains what data the connector
36
+ handles, why, who receives it, and how long it is kept.</p>
37
+
38
+ <h2>Categories of personal data we access</h2>
39
+ <ul>
40
+ <li><strong>Your WebCake identity, JWT, and session ID.</strong> When you connect via OAuth, you log in to
41
+ WebCake through the browser and the connector receives your bearer JWT and workspace session ID (<code>wsid</code>).
42
+ These are used <em>solely</em> to call the WebCake / StoreCake backend API on your behalf. They are never
43
+ shared with the AI assistant or any third party.</li>
44
+ <li><strong>Storefront content you ask the assistant to create or edit.</strong> Page source, product details,
45
+ article text, customer look-ups, and order data flow through the connector to your WebCake account.
46
+ <em>Purpose:</em> to carry out the actions you request.</li>
47
+ <li><strong>Images.</strong> When you ask the assistant to resize or process images, they are handled transiently
48
+ in memory and forwarded to the WebCake CDN. No image is retained on the connector after the request completes.</li>
49
+ </ul>
50
+
51
+ <h2>What we store and for how long</h2>
52
+ <ul>
53
+ <li><strong>OAuth tokens (in-process memory only).</strong> The connector holds your JWT and session ID in an
54
+ in-memory token store <strong>only for the lifetime of the server process</strong>. There is no database;
55
+ tokens are never written to disk by the connector. Access tokens expire automatically after ~1 hour, refresh
56
+ tokens after ~30 days. A server restart clears all tokens.</li>
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
59
+ <code>~/.webcake-storefront-mcp.db</code> or similar). This file stays on your device and is not transmitted
60
+ anywhere by the connector.</li>
61
+ <li>The connector does <strong>not</strong> run an analytics database, does <strong>not</strong> sell or share
62
+ data, and does <strong>not</strong> perform tracking or behavioral profiling.</li>
63
+ </ul>
64
+
65
+ <h2>Categories of recipients</h2>
66
+ <p>Your data is shared only with the services required to fulfil your request:</p>
67
+ <ul>
68
+ <li><strong>WebCake / StoreCake API</strong> (<code>api.storefront.webcake.io</code>) — stores and serves
69
+ your storefront pages, products, and orders; governed by WebCake's own terms.</li>
70
+ <li><strong>WebCake CDN</strong> — hosts images and published page assets that you explicitly create or upload.</li>
71
+ </ul>
72
+
73
+ <h2>Data we do NOT collect</h2>
74
+ <p>The connector never asks for or stores payment-card data, passwords, health data, government identifiers,
75
+ or MFA/OTP codes. It operates only on the storefront content you explicitly instruct the assistant to
76
+ create — it does <strong>not</strong> read, reconstruct, or infer your conversation history.</p>
77
+
78
+ <h2>Data retention &amp; deletion</h2>
79
+ <p>OAuth tokens expire automatically or are cleared on server restart. You can revoke access at any time by
80
+ disconnecting the connector in Claude / ChatGPT settings, or by running <code>npx webcake-storefront-mcp logout</code>.
81
+ Storefront content you create lives in your WebCake account and is managed there. To request deletion of
82
+ anything else, contact us below.</p>
83
+
84
+ <h2>Security</h2>
85
+ <p>All traffic uses HTTPS. The connector implements OAuth 2.1 with PKCE; your raw WebCake token is never
86
+ exposed to the AI assistant — only resolved server-side per request via an opaque OAuth access token.</p>
87
+
88
+ <h2>Contact</h2>
89
+ <p>Questions or requests: <a href="mailto:${CONTACT_EMAIL}">${CONTACT_EMAIL}</a> or open an issue at
90
+ <a href="https://github.com/vuluu2k/webcake-storefront-mcp/issues">github.com/vuluu2k/webcake-storefront-mcp</a>.</p>`);
91
+ }
92
+ export function termsHtml() {
93
+ return page("Terms of Service", `<h1>Terms of Service</h1>
94
+ <div class="meta">Last updated: ${LAST_UPDATED}</div>
95
+ <p>By connecting to and using the WebCake Storefront MCP connector ("the service") you agree to these terms.</p>
96
+
97
+ <h2>What the service does</h2>
98
+ <p>The service exposes tools that let an AI assistant create, update, and manage storefront pages, products,
99
+ articles, customers, and orders in your WebCake / StoreCake account. It acts on your behalf using credentials
100
+ you authorize through the WebCake OAuth login flow.</p>
101
+
102
+ <h2>Your responsibilities</h2>
103
+ <ul>
104
+ <li>You must have a valid WebCake / StoreCake account and the necessary permissions for any sites you target.</li>
105
+ <li>You are responsible for all content you generate and publish through the service, and for complying with
106
+ WebCake's terms of service and applicable law.</li>
107
+ <li>Do not use the service to create, distribute, or publish unlawful, infringing, or harmful content.</li>
108
+ <li>Do not attempt to use the service to access accounts or data you are not authorized to access.</li>
109
+ </ul>
110
+
111
+ <h2>Availability &amp; changes</h2>
112
+ <p>The service is provided "as is" without warranty of any kind. We may update, suspend, or discontinue it at
113
+ any time and may change these terms; continued use after a change constitutes acceptance of the new terms.</p>
114
+
115
+ <h2>Limitation of liability</h2>
116
+ <p>To the fullest extent permitted by applicable law, the operators of the connector are not liable for
117
+ indirect, incidental, or consequential damages arising from use of the service. The service relies on
118
+ third-party platforms (WebCake, the AI assistant host) whose own terms also apply.</p>
119
+
120
+ <h2>Intellectual property</h2>
121
+ <p>The connector is open-source software released under the MIT License. The WebCake trademarks and platform
122
+ are owned by their respective holders.</p>
123
+
124
+ <h2>Contact</h2>
125
+ <p><a href="mailto:${CONTACT_EMAIL}">${CONTACT_EMAIL}</a> &middot;
126
+ <a href="https://github.com/vuluu2k/webcake-storefront-mcp/issues">GitHub Issues</a></p>`);
127
+ }
@@ -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
+ }