webcake-landing-mcp 1.0.77 → 1.0.79
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/auth/oauth-server.js +221 -63
- package/dist/changelog.json +14 -14
- package/dist/http.js +6 -6
- package/dist/legal.js +30 -16
- package/dist/persistence/draft-cache.js +75 -10
- package/dist/persistence/postgres.js +120 -0
- package/dist/persistence/redis.js +64 -0
- package/dist/persistence/rehost-cache.js +45 -0
- package/dist/persistence/rehost.js +3 -3
- package/dist/persistence/webcake-client.js +7 -9
- package/dist/smoke.js +33 -33
- package/dist/tools/persistence.js +50 -50
- package/package.json +5 -1
|
@@ -15,49 +15,215 @@
|
|
|
15
15
|
* - Token endpoint (POST /token) authorization_code + refresh_token
|
|
16
16
|
* - Authorization Server + Protected Resource metadata (the /.well-known docs)
|
|
17
17
|
*
|
|
18
|
-
* Access tokens are OPAQUE random strings mapped to the user's `ljwt` in
|
|
18
|
+
* Access tokens are OPAQUE random strings mapped to the user's `ljwt` in the
|
|
19
19
|
* store (so they can be revoked and the ljwt never leaves the server). The HTTP
|
|
20
20
|
* layer resolves a Bearer access token to its ljwt and injects it as the normal
|
|
21
21
|
* `x-webcake-jwt` header, so the rest of the server (persistence/config.ts) is
|
|
22
22
|
* UNCHANGED and the legacy `?jwt=` / `x-webcake-jwt` paths keep working untouched.
|
|
23
23
|
*
|
|
24
|
-
* STORE:
|
|
25
|
-
*
|
|
24
|
+
* STORE: Postgres when DATABASE_URL is set (tokens survive a `serve` restart and
|
|
25
|
+
* are shared across instances behind a load balancer), else in-memory maps
|
|
26
|
+
* (single-instance `serve`, stdio/`npx`, offline tests). Both implement the same
|
|
27
|
+
* async `OAuthStore` interface; the rest of this module is backend-agnostic. All
|
|
28
|
+
* exported state functions are async — callers (src/http.ts) `await` them.
|
|
26
29
|
*/
|
|
27
30
|
import { randomBytes, createHash } from "node:crypto";
|
|
31
|
+
import { getPg, ensureOAuthSchema } from "../persistence/postgres.js";
|
|
28
32
|
// ---- TTLs (override via env where useful) ---------------------------------
|
|
29
33
|
const TEN_MIN = 10 * 60 * 1000;
|
|
30
34
|
const ACCESS_TTL = Number(process.env.WEBCAKE_OAUTH_ACCESS_TTL_MS) || 60 * 60 * 1000; // 1h
|
|
31
35
|
const REFRESH_TTL = Number(process.env.WEBCAKE_OAUTH_REFRESH_TTL_MS) || 30 * 24 * 60 * 60 * 1000; // 30d
|
|
32
36
|
const CODE_TTL = TEN_MIN;
|
|
33
37
|
const PENDING_TTL = TEN_MIN;
|
|
34
|
-
// ---- In-memory maps -------------------------------------------------------
|
|
35
|
-
const clients = new Map();
|
|
36
|
-
const pending = new Map(); // key: internal state we send to /mcp-connect
|
|
37
|
-
const codes = new Map(); // key: authorization code
|
|
38
|
-
const accessTokens = new Map(); // key: access token
|
|
39
|
-
const refreshTokens = new Map(); // key: refresh token
|
|
40
38
|
function now() {
|
|
41
39
|
return Date.now();
|
|
42
40
|
}
|
|
43
41
|
function token(bytes = 32) {
|
|
44
42
|
return randomBytes(bytes).toString("base64url");
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
// ---- In-memory store (default; no DATABASE_URL) ---------------------------
|
|
45
|
+
class MemoryStore {
|
|
46
|
+
clients = new Map();
|
|
47
|
+
pending = new Map();
|
|
48
|
+
codes = new Map();
|
|
49
|
+
access = new Map();
|
|
50
|
+
refresh = new Map();
|
|
51
|
+
/** Lazy sweep of anything expired — cheap, called on the hot paths. */
|
|
52
|
+
sweep() {
|
|
53
|
+
const t = now();
|
|
54
|
+
for (const [k, v] of this.pending)
|
|
55
|
+
if (v.expiresAt < t)
|
|
56
|
+
this.pending.delete(k);
|
|
57
|
+
for (const [k, v] of this.codes)
|
|
58
|
+
if (v.expiresAt < t)
|
|
59
|
+
this.codes.delete(k);
|
|
60
|
+
for (const [k, v] of this.access)
|
|
61
|
+
if (v.expiresAt < t)
|
|
62
|
+
this.access.delete(k);
|
|
63
|
+
for (const [k, v] of this.refresh)
|
|
64
|
+
if (v.expiresAt < t)
|
|
65
|
+
this.refresh.delete(k);
|
|
66
|
+
}
|
|
67
|
+
async putClient(c) {
|
|
68
|
+
this.clients.set(c.client_id, c);
|
|
69
|
+
}
|
|
70
|
+
async getClient(id) {
|
|
71
|
+
return this.clients.get(id);
|
|
72
|
+
}
|
|
73
|
+
async putPending(state, p) {
|
|
74
|
+
this.sweep();
|
|
75
|
+
this.pending.set(state, p);
|
|
76
|
+
}
|
|
77
|
+
async takePending(state) {
|
|
78
|
+
this.sweep();
|
|
79
|
+
const p = this.pending.get(state);
|
|
80
|
+
this.pending.delete(state);
|
|
81
|
+
return p && p.expiresAt >= now() ? p : undefined;
|
|
82
|
+
}
|
|
83
|
+
async putCode(code, c) {
|
|
84
|
+
this.codes.set(code, c);
|
|
85
|
+
}
|
|
86
|
+
async takeCode(code) {
|
|
87
|
+
this.sweep();
|
|
88
|
+
const c = this.codes.get(code);
|
|
89
|
+
this.codes.delete(code);
|
|
90
|
+
return c && c.expiresAt >= now() ? c : undefined;
|
|
91
|
+
}
|
|
92
|
+
async putAccess(t, a) {
|
|
93
|
+
this.access.set(t, a);
|
|
94
|
+
}
|
|
95
|
+
async getAccess(t) {
|
|
96
|
+
const a = this.access.get(t);
|
|
97
|
+
if (a && a.expiresAt < now()) {
|
|
98
|
+
this.access.delete(t);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return a;
|
|
102
|
+
}
|
|
103
|
+
async putRefresh(t, r) {
|
|
104
|
+
this.refresh.set(t, r);
|
|
105
|
+
}
|
|
106
|
+
async takeRefresh(t) {
|
|
107
|
+
this.sweep();
|
|
108
|
+
const r = this.refresh.get(t);
|
|
109
|
+
this.refresh.delete(t);
|
|
110
|
+
return r && r.expiresAt >= now() ? r : undefined;
|
|
111
|
+
}
|
|
112
|
+
async revoke(t) {
|
|
113
|
+
this.access.delete(t);
|
|
114
|
+
this.refresh.delete(t);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ---- Postgres store (when DATABASE_URL is set) ----------------------------
|
|
118
|
+
class PgStore {
|
|
119
|
+
pool;
|
|
120
|
+
constructor(pool) {
|
|
121
|
+
this.pool = pool;
|
|
122
|
+
}
|
|
123
|
+
async putClient(c) {
|
|
124
|
+
await this.pool.query(`INSERT INTO oauth_clients (client_id, client_name, redirect_uris, created_at)
|
|
125
|
+
VALUES ($1,$2,$3,$4)
|
|
126
|
+
ON CONFLICT (client_id) DO UPDATE SET client_name=$2, redirect_uris=$3`, [c.client_id, c.client_name ?? null, JSON.stringify(c.redirect_uris), c.created_at]);
|
|
127
|
+
}
|
|
128
|
+
async getClient(id) {
|
|
129
|
+
const { rows } = await this.pool.query(`SELECT client_id, client_name, redirect_uris, created_at FROM oauth_clients WHERE client_id=$1`, [id]);
|
|
130
|
+
const r = rows[0];
|
|
131
|
+
if (!r)
|
|
132
|
+
return undefined;
|
|
133
|
+
return {
|
|
134
|
+
client_id: r.client_id,
|
|
135
|
+
client_name: r.client_name ?? undefined,
|
|
136
|
+
redirect_uris: Array.isArray(r.redirect_uris) ? r.redirect_uris : JSON.parse(r.redirect_uris),
|
|
137
|
+
created_at: Number(r.created_at),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async putPending(state, p) {
|
|
141
|
+
await this.pool.query(`INSERT INTO oauth_pending (state, client_id, redirect_uri, code_challenge, client_state, scope, expires_at)
|
|
142
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7)`, [state, p.client_id, p.redirect_uri, p.code_challenge, p.state ?? null, p.scope ?? null, p.expiresAt]);
|
|
143
|
+
}
|
|
144
|
+
async takePending(state) {
|
|
145
|
+
const { rows } = await this.pool.query(`DELETE FROM oauth_pending WHERE state=$1 RETURNING client_id, redirect_uri, code_challenge, client_state, scope, expires_at`, [state]);
|
|
146
|
+
const r = rows[0];
|
|
147
|
+
if (!r || Number(r.expires_at) < now())
|
|
148
|
+
return undefined;
|
|
149
|
+
return {
|
|
150
|
+
client_id: r.client_id,
|
|
151
|
+
redirect_uri: r.redirect_uri,
|
|
152
|
+
code_challenge: r.code_challenge,
|
|
153
|
+
state: r.client_state ?? undefined,
|
|
154
|
+
scope: r.scope ?? undefined,
|
|
155
|
+
expiresAt: Number(r.expires_at),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async putCode(code, c) {
|
|
159
|
+
await this.pool.query(`INSERT INTO oauth_codes (code, client_id, redirect_uri, code_challenge, scope, ljwt, expires_at)
|
|
160
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7)`, [code, c.client_id, c.redirect_uri, c.code_challenge, c.scope ?? null, c.ljwt, c.expiresAt]);
|
|
161
|
+
}
|
|
162
|
+
async takeCode(code) {
|
|
163
|
+
const { rows } = await this.pool.query(`DELETE FROM oauth_codes WHERE code=$1 RETURNING client_id, redirect_uri, code_challenge, scope, ljwt, expires_at`, [code]);
|
|
164
|
+
const r = rows[0];
|
|
165
|
+
if (!r || Number(r.expires_at) < now())
|
|
166
|
+
return undefined;
|
|
167
|
+
return {
|
|
168
|
+
client_id: r.client_id,
|
|
169
|
+
redirect_uri: r.redirect_uri,
|
|
170
|
+
code_challenge: r.code_challenge,
|
|
171
|
+
scope: r.scope ?? undefined,
|
|
172
|
+
ljwt: r.ljwt,
|
|
173
|
+
expiresAt: Number(r.expires_at),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async putAccess(t, a) {
|
|
177
|
+
await this.pool.query(`INSERT INTO oauth_access_tokens (token, ljwt, scope, expires_at) VALUES ($1,$2,$3,$4)`, [t, a.ljwt, a.scope ?? null, a.expiresAt]);
|
|
178
|
+
}
|
|
179
|
+
async getAccess(t) {
|
|
180
|
+
const { rows } = await this.pool.query(`SELECT ljwt, scope, expires_at FROM oauth_access_tokens WHERE token=$1`, [t]);
|
|
181
|
+
const r = rows[0];
|
|
182
|
+
if (!r)
|
|
183
|
+
return undefined;
|
|
184
|
+
if (Number(r.expires_at) < now()) {
|
|
185
|
+
await this.pool.query(`DELETE FROM oauth_access_tokens WHERE token=$1`, [t]);
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return { ljwt: r.ljwt, scope: r.scope ?? undefined, expiresAt: Number(r.expires_at) };
|
|
189
|
+
}
|
|
190
|
+
async putRefresh(t, r) {
|
|
191
|
+
await this.pool.query(`INSERT INTO oauth_refresh_tokens (token, ljwt, client_id, scope, expires_at) VALUES ($1,$2,$3,$4,$5)`, [t, r.ljwt, r.client_id, r.scope ?? null, r.expiresAt]);
|
|
192
|
+
}
|
|
193
|
+
async takeRefresh(t) {
|
|
194
|
+
const { rows } = await this.pool.query(`DELETE FROM oauth_refresh_tokens WHERE token=$1 RETURNING ljwt, client_id, scope, expires_at`, [t]);
|
|
195
|
+
const r = rows[0];
|
|
196
|
+
if (!r || Number(r.expires_at) < now())
|
|
197
|
+
return undefined;
|
|
198
|
+
return {
|
|
199
|
+
ljwt: r.ljwt,
|
|
200
|
+
client_id: r.client_id,
|
|
201
|
+
scope: r.scope ?? undefined,
|
|
202
|
+
expiresAt: Number(r.expires_at),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async revoke(t) {
|
|
206
|
+
await this.pool.query(`DELETE FROM oauth_access_tokens WHERE token=$1`, [t]);
|
|
207
|
+
await this.pool.query(`DELETE FROM oauth_refresh_tokens WHERE token=$1`, [t]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ---- Backend selection (memoized) -----------------------------------------
|
|
211
|
+
const memoryStore = new MemoryStore();
|
|
212
|
+
let storePromise;
|
|
213
|
+
/** Resolve the active store ONCE: Postgres if configured + schema ready, else memory. */
|
|
214
|
+
function getStore() {
|
|
215
|
+
if (storePromise)
|
|
216
|
+
return storePromise;
|
|
217
|
+
storePromise = (async () => {
|
|
218
|
+
const pool = getPg();
|
|
219
|
+
if (pool && (await ensureOAuthSchema(pool)))
|
|
220
|
+
return new PgStore(pool);
|
|
221
|
+
return memoryStore;
|
|
222
|
+
})().catch((e) => {
|
|
223
|
+
console.error("[oauth] store init failed, using in-memory:", e?.message ?? e);
|
|
224
|
+
return memoryStore;
|
|
225
|
+
});
|
|
226
|
+
return storePromise;
|
|
61
227
|
}
|
|
62
228
|
// ---- PKCE -----------------------------------------------------------------
|
|
63
229
|
/** base64url( SHA256(verifier) ) — the S256 transform. */
|
|
@@ -76,7 +242,7 @@ export function verifyPkce(verifier, challenge) {
|
|
|
76
242
|
diff |= a.charCodeAt(i) ^ challenge.charCodeAt(i);
|
|
77
243
|
return diff === 0;
|
|
78
244
|
}
|
|
79
|
-
export function registerClient(body) {
|
|
245
|
+
export async function registerClient(body) {
|
|
80
246
|
const uris = Array.isArray(body?.redirect_uris)
|
|
81
247
|
? body.redirect_uris.filter((u) => typeof u === "string" && /^https?:\/\//i.test(u))
|
|
82
248
|
: [];
|
|
@@ -89,11 +255,13 @@ export function registerClient(body) {
|
|
|
89
255
|
redirect_uris: uris,
|
|
90
256
|
created_at: now(),
|
|
91
257
|
};
|
|
92
|
-
|
|
258
|
+
(await getStore()).putClient(client).catch((e) => console.error("[oauth] putClient:", e?.message ?? e));
|
|
93
259
|
return { ok: true, client };
|
|
94
260
|
}
|
|
95
|
-
export function getClient(clientId) {
|
|
96
|
-
|
|
261
|
+
export async function getClient(clientId) {
|
|
262
|
+
if (!clientId)
|
|
263
|
+
return undefined;
|
|
264
|
+
return (await getStore()).getClient(clientId);
|
|
97
265
|
}
|
|
98
266
|
/**
|
|
99
267
|
* Validate an /authorize request and park it. Returns an `internalState` to send
|
|
@@ -101,9 +269,9 @@ export function getClient(clientId) {
|
|
|
101
269
|
* `redirectable: false` means the error must be shown as a page (we can't trust
|
|
102
270
|
* the redirect_uri); `true` means it's safe to bounce the error to the client.
|
|
103
271
|
*/
|
|
104
|
-
export function startAuthorize(p) {
|
|
105
|
-
|
|
106
|
-
const client = getClient(p.client_id);
|
|
272
|
+
export async function startAuthorize(p) {
|
|
273
|
+
const store = await getStore();
|
|
274
|
+
const client = p.client_id ? await store.getClient(p.client_id) : undefined;
|
|
107
275
|
if (!client) {
|
|
108
276
|
return { ok: false, error: "invalid_client", error_description: "Unknown client_id. Register first via /register.", redirectable: false };
|
|
109
277
|
}
|
|
@@ -118,7 +286,7 @@ export function startAuthorize(p) {
|
|
|
118
286
|
return { ok: false, error: "invalid_request", error_description: "PKCE with code_challenge_method=S256 is required.", redirectable: true };
|
|
119
287
|
}
|
|
120
288
|
const internalState = token(24);
|
|
121
|
-
|
|
289
|
+
await store.putPending(internalState, {
|
|
122
290
|
client_id: client.client_id,
|
|
123
291
|
redirect_uri: p.redirect_uri,
|
|
124
292
|
code_challenge: p.code_challenge,
|
|
@@ -134,18 +302,17 @@ export function startAuthorize(p) {
|
|
|
134
302
|
* parked PKCE challenge, and return where to redirect the user (the client's
|
|
135
303
|
* redirect_uri with ?code=&state=).
|
|
136
304
|
*/
|
|
137
|
-
export function completeAuthorize(internalState, ljwt) {
|
|
138
|
-
|
|
139
|
-
|
|
305
|
+
export async function completeAuthorize(internalState, ljwt) {
|
|
306
|
+
const store = await getStore();
|
|
307
|
+
const p = internalState ? await store.takePending(internalState) : undefined;
|
|
308
|
+
if (!p) {
|
|
140
309
|
return { ok: false, error: "invalid_request", error_description: "Authorization session expired or unknown — restart the connection." };
|
|
141
310
|
}
|
|
142
|
-
const p = pending.get(internalState);
|
|
143
|
-
pending.delete(internalState);
|
|
144
311
|
if (!ljwt) {
|
|
145
312
|
return { ok: false, error: "access_denied", error_description: "No Webcake token returned from login." };
|
|
146
313
|
}
|
|
147
314
|
const code = token(32);
|
|
148
|
-
|
|
315
|
+
await store.putCode(code, {
|
|
149
316
|
client_id: p.client_id,
|
|
150
317
|
redirect_uri: p.redirect_uri,
|
|
151
318
|
code_challenge: p.code_challenge,
|
|
@@ -155,21 +322,20 @@ export function completeAuthorize(internalState, ljwt) {
|
|
|
155
322
|
});
|
|
156
323
|
return { ok: true, redirectUri: p.redirect_uri, code, state: p.state };
|
|
157
324
|
}
|
|
158
|
-
function issueTokens(ljwt, client_id, scope) {
|
|
325
|
+
async function issueTokens(store, ljwt, client_id, scope) {
|
|
159
326
|
const access = token(32);
|
|
160
327
|
const refresh = token(32);
|
|
161
|
-
|
|
162
|
-
|
|
328
|
+
await store.putAccess(access, { ljwt, scope, expiresAt: now() + ACCESS_TTL });
|
|
329
|
+
await store.putRefresh(refresh, { ljwt, client_id, scope, expiresAt: now() + REFRESH_TTL });
|
|
163
330
|
return { access_token: access, token_type: "Bearer", expires_in: Math.floor(ACCESS_TTL / 1000), refresh_token: refresh, scope };
|
|
164
331
|
}
|
|
165
|
-
export function exchangeToken(p) {
|
|
166
|
-
|
|
332
|
+
export async function exchangeToken(p) {
|
|
333
|
+
const store = await getStore();
|
|
167
334
|
if (p.grant_type === "authorization_code") {
|
|
168
|
-
|
|
335
|
+
const c = p.code ? await store.takeCode(p.code) : undefined; // one-time use
|
|
336
|
+
if (!c) {
|
|
169
337
|
return { ok: false, status: 400, error: "invalid_grant", error_description: "Unknown or expired authorization code." };
|
|
170
338
|
}
|
|
171
|
-
const c = codes.get(p.code);
|
|
172
|
-
codes.delete(p.code); // one-time use
|
|
173
339
|
if (c.client_id !== p.client_id) {
|
|
174
340
|
return { ok: false, status: 400, error: "invalid_grant", error_description: "client_id does not match the authorization code." };
|
|
175
341
|
}
|
|
@@ -179,38 +345,30 @@ export function exchangeToken(p) {
|
|
|
179
345
|
if (!p.code_verifier || !verifyPkce(p.code_verifier, c.code_challenge)) {
|
|
180
346
|
return { ok: false, status: 400, error: "invalid_grant", error_description: "PKCE verification failed." };
|
|
181
347
|
}
|
|
182
|
-
return { ok: true, body: issueTokens(c.ljwt, c.client_id, c.scope) };
|
|
348
|
+
return { ok: true, body: await issueTokens(store, c.ljwt, c.client_id, c.scope) };
|
|
183
349
|
}
|
|
184
350
|
if (p.grant_type === "refresh_token") {
|
|
185
|
-
|
|
351
|
+
const r = p.refresh_token ? await store.takeRefresh(p.refresh_token) : undefined; // rotate
|
|
352
|
+
if (!r) {
|
|
186
353
|
return { ok: false, status: 400, error: "invalid_grant", error_description: "Unknown or expired refresh token." };
|
|
187
354
|
}
|
|
188
|
-
|
|
189
|
-
refreshTokens.delete(p.refresh_token); // rotate
|
|
190
|
-
return { ok: true, body: issueTokens(r.ljwt, r.client_id, r.scope) };
|
|
355
|
+
return { ok: true, body: await issueTokens(store, r.ljwt, r.client_id, r.scope) };
|
|
191
356
|
}
|
|
192
357
|
return { ok: false, status: 400, error: "unsupported_grant_type", error_description: "grant_type must be authorization_code or refresh_token." };
|
|
193
358
|
}
|
|
194
359
|
// ---- Resource-server side: resolve a Bearer access token to its ljwt -------
|
|
195
360
|
/** Returns the user's ljwt for a valid, unexpired access token, else undefined. */
|
|
196
|
-
export function resolveAccessToken(accessToken) {
|
|
361
|
+
export async function resolveAccessToken(accessToken) {
|
|
197
362
|
if (!accessToken)
|
|
198
363
|
return undefined;
|
|
199
|
-
const a =
|
|
200
|
-
|
|
201
|
-
return undefined;
|
|
202
|
-
if (a.expiresAt < now()) {
|
|
203
|
-
accessTokens.delete(accessToken);
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
return a.ljwt;
|
|
364
|
+
const a = await (await getStore()).getAccess(accessToken);
|
|
365
|
+
return a?.ljwt;
|
|
207
366
|
}
|
|
208
367
|
/** Revoke an access or refresh token (best-effort; for /revoke). */
|
|
209
|
-
export function revokeToken(t) {
|
|
368
|
+
export async function revokeToken(t) {
|
|
210
369
|
if (!t)
|
|
211
370
|
return;
|
|
212
|
-
|
|
213
|
-
refreshTokens.delete(t);
|
|
371
|
+
await (await getStore()).revoke(t);
|
|
214
372
|
}
|
|
215
373
|
// ---- Metadata documents (RFC 8414 / RFC 9728) -----------------------------
|
|
216
374
|
/** /.well-known/oauth-authorization-server */
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.79",
|
|
4
|
+
"d": "15/06/2026",
|
|
5
|
+
"type": "Changed",
|
|
6
|
+
"en": "The Privacy Policy served at /privacy is updated with GDPR-style data-category headings, per-category purpose statements, documentation of…",
|
|
7
|
+
"vi": "Trang Privacy Policy tại /privacy được cập nhật với các tiêu đề phân loại dữ liệu theo chuẩn GDPR, ghi rõ mục đích xử lý từng loại dữ liệu, bổ sung…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.0.78",
|
|
11
|
+
"d": "15/06/2026",
|
|
12
|
+
"type": "Added",
|
|
13
|
+
"en": "The OAuth token store (clients, authorization codes, access and refresh tokens) now uses Postgres when DATABASE_URL, WEBCAKE_POSTGRES_URL, or…",
|
|
14
|
+
"vi": "Kho lưu trữ OAuth token (clients, authorization code, access và refresh token) nay sử dụng Postgres khi DATABASE_URL, WEBCAKE_POSTGRES_URL, hoặc…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.0.77",
|
|
4
18
|
"d": "15/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "ingest_html and ingest_url now extract Tailwind gradient utilities (bg-gradient-to-*/from-*/via-*/to-*) from the page's class attributes, resolve…",
|
|
28
42
|
"vi": "ingest_html và ingest_url nay trích xuất các utility gradient Tailwind (bg-gradient-to-*/from-*/via-*/to-*) từ các thuộc tính class của trang,…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.0.73",
|
|
32
|
-
"d": "13/06/2026",
|
|
33
|
-
"type": "Added",
|
|
34
|
-
"en": "ingest_html and ingest_url now extract the full design system from a tailwind.config script block when present (Google Stitch and other Tailwind-CDN…",
|
|
35
|
-
"vi": "ingest_html và ingest_url nay trích xuất toàn bộ design system từ block script tailwind.config khi có (trang Google Stitch và các trang Tailwind-CDN…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.72",
|
|
39
|
-
"d": "13/06/2026",
|
|
40
|
-
"type": "Fixed",
|
|
41
|
-
"en": "When cloning a LadiPage or Webcake-published page via ingest_html / ingest_url, html-box elements' passthrough HTML content now has its…",
|
|
42
|
-
"vi": "Khi clone trang LadiPage hoặc Webcake-published qua ingest_html / ingest_url, nội dung HTML passthrough của các phần tử html-box nay được đổi tên…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/http.js
CHANGED
|
@@ -189,7 +189,7 @@ async function handleOAuth(req, res, path) {
|
|
|
189
189
|
return oauthError(res, 405, "invalid_request", "Use POST."), true;
|
|
190
190
|
const raw = await readRawBody(req);
|
|
191
191
|
const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
|
|
192
|
-
const result = registerClient(body);
|
|
192
|
+
const result = await registerClient(body);
|
|
193
193
|
if (!result.ok)
|
|
194
194
|
return oauthError(res, 400, result.error, result.error_description), true;
|
|
195
195
|
res.writeHead(201, { "content-type": "application/json", "access-control-allow-origin": "*", "cache-control": "no-store" });
|
|
@@ -206,7 +206,7 @@ async function handleOAuth(req, res, path) {
|
|
|
206
206
|
// ---- Authorize: validate + delegate to the SPA login, parking the request ----
|
|
207
207
|
if (req.method === "GET" && path === OAUTH_AUTHORIZE) {
|
|
208
208
|
const sp = new URL(req.url ?? "/", "http://x").searchParams;
|
|
209
|
-
const result = startAuthorize({
|
|
209
|
+
const result = await startAuthorize({
|
|
210
210
|
client_id: sp.get("client_id"),
|
|
211
211
|
redirect_uri: sp.get("redirect_uri"),
|
|
212
212
|
response_type: sp.get("response_type"),
|
|
@@ -238,7 +238,7 @@ async function handleOAuth(req, res, path) {
|
|
|
238
238
|
// ---- Login callback: the SPA handed back the user's ljwt → mint a code ----
|
|
239
239
|
if (req.method === "GET" && path === OAUTH_CALLBACK) {
|
|
240
240
|
const sp = new URL(req.url ?? "/", "http://x").searchParams;
|
|
241
|
-
const done = completeAuthorize(sp.get("state"), sp.get("token"));
|
|
241
|
+
const done = await completeAuthorize(sp.get("state"), sp.get("token"));
|
|
242
242
|
if (!done.ok)
|
|
243
243
|
return htmlError(res, 400, done.error_description), true;
|
|
244
244
|
const r = new URL(done.redirectUri);
|
|
@@ -258,7 +258,7 @@ async function handleOAuth(req, res, path) {
|
|
|
258
258
|
return oauthError(res, 405, "invalid_request", "Use POST."), true;
|
|
259
259
|
const raw = await readRawBody(req);
|
|
260
260
|
const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
|
|
261
|
-
const result = exchangeToken(body);
|
|
261
|
+
const result = await exchangeToken(body);
|
|
262
262
|
if (!result.ok)
|
|
263
263
|
return oauthError(res, result.status, result.error, result.error_description), true;
|
|
264
264
|
res.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*", "cache-control": "no-store" });
|
|
@@ -271,7 +271,7 @@ async function handleOAuth(req, res, path) {
|
|
|
271
271
|
return oauthError(res, 405, "invalid_request", "Use POST."), true;
|
|
272
272
|
const raw = await readRawBody(req);
|
|
273
273
|
const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
|
|
274
|
-
revokeToken(body.token);
|
|
274
|
+
await revokeToken(body.token);
|
|
275
275
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
276
276
|
res.end("{}");
|
|
277
277
|
return true;
|
|
@@ -393,7 +393,7 @@ export async function startHttpServer(port) {
|
|
|
393
393
|
// normal x-webcake-jwt header, so persistence/config.ts is unchanged. A legacy
|
|
394
394
|
// raw JWT sent via x-webcake-jwt / ?jwt= still wins and passes straight through.
|
|
395
395
|
const bearer = bearerFrom(req);
|
|
396
|
-
const oauthLjwt = resolveAccessToken(bearer);
|
|
396
|
+
const oauthLjwt = await resolveAccessToken(bearer);
|
|
397
397
|
if (oauthLjwt && req.headers["x-webcake-jwt"] == null) {
|
|
398
398
|
req.headers["x-webcake-jwt"] = oauthLjwt;
|
|
399
399
|
req.rawHeaders.push("x-webcake-jwt", oauthLjwt);
|
package/dist/legal.js
CHANGED
|
@@ -36,38 +36,52 @@ export function privacyHtml() {
|
|
|
36
36
|
<div class="meta">Last updated: ${LAST_UPDATED}</div>
|
|
37
37
|
<p>Webcake Landing MCP ("the connector") is a Model Context Protocol server that lets an AI assistant
|
|
38
38
|
build and edit landing pages in your <a href="https://webcake.io">Webcake</a> account. This policy explains
|
|
39
|
-
what data the connector handles and how.</p>
|
|
39
|
+
what categories of data the connector handles, why, who receives it, and how long it is kept.</p>
|
|
40
40
|
|
|
41
|
-
<h2>
|
|
41
|
+
<h2>Categories of personal data we access</h2>
|
|
42
42
|
<ul>
|
|
43
43
|
<li><strong>Your Webcake identity & access token.</strong> When you connect, you log in to Webcake and the
|
|
44
|
-
connector receives a per-user access token mapped to your Webcake landing-page credential.
|
|
44
|
+
connector receives a per-user access token mapped to your Webcake landing-page credential. <em>Purpose:</em> solely
|
|
45
45
|
to call the Webcake backend on your behalf (create, read, update, publish pages, list your organizations).</li>
|
|
46
46
|
<li><strong>Page content you ask the assistant to build or edit.</strong> The page-source JSON (text, images,
|
|
47
|
-
layout) flows through the connector to your Webcake account.</li>
|
|
47
|
+
layout) flows through the connector to your Webcake account. <em>Purpose:</em> to assemble and save the page you request.</li>
|
|
48
48
|
<li><strong>Images.</strong> External image URLs in a page are re-hosted to the Webcake CDN on save. Optional
|
|
49
|
-
stock-photo search
|
|
49
|
+
stock-photo search uses the Pexels API and icon lookup uses the Iconify API. <em>Purpose:</em> to supply the
|
|
50
|
+
visuals for your page.</li>
|
|
50
51
|
</ul>
|
|
51
52
|
|
|
52
|
-
<h2>What we store</h2>
|
|
53
|
+
<h2>What we store and for how long</h2>
|
|
53
54
|
<ul>
|
|
54
|
-
<li><strong>OAuth tokens
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
<li><strong>OAuth tokens.</strong> Stored in the connector's database (PostgreSQL) when the server is configured
|
|
56
|
+
with one — so a logged-in session survives a server restart and works across instances — otherwise only in
|
|
57
|
+
process memory. Either way they <strong>expire automatically</strong> (access tokens ~1 hour, refresh tokens
|
|
58
|
+
~30 days) and are removed on logout/revoke or expiry.</li>
|
|
59
|
+
<li><strong>Transient draft cache.</strong> If a page fails validation while being created, its draft page-source
|
|
60
|
+
is cached briefly (about 2 hours, in Redis or memory) so you can fix and retry without re-sending everything.
|
|
61
|
+
It is removed on success or expiry.</li>
|
|
62
|
+
<li>The connector does <strong>not</strong> run an analytics database, does <strong>not</strong> sell data, and
|
|
63
|
+
does <strong>not</strong> perform tracking, behavioral profiling, or advertising.</li>
|
|
59
64
|
</ul>
|
|
60
65
|
|
|
61
|
-
<h2>
|
|
66
|
+
<h2>Categories of recipients (third-party services)</h2>
|
|
67
|
+
<p>Your data is shared only with the services required to perform your request:</p>
|
|
62
68
|
<ul>
|
|
63
|
-
<li><strong>Webcake</strong> (api.webcake.io
|
|
69
|
+
<li><strong>Webcake</strong> (api.webcake.io, the Webcake CDN, and the publish/build host) — stores, renders,
|
|
70
|
+
and serves your pages; governed by Webcake's own terms.</li>
|
|
64
71
|
<li><strong>Pexels</strong> (pexels.com) — stock-photo search, only when you request images.</li>
|
|
72
|
+
<li><strong>Iconify</strong> (iconify.design) — resolves icon names to SVG, only when a page uses icons.</li>
|
|
65
73
|
</ul>
|
|
66
74
|
|
|
75
|
+
<h2>Data we do NOT collect</h2>
|
|
76
|
+
<p>The connector never asks for or stores payment-card data, health data, government identifiers, or
|
|
77
|
+
authentication secrets (passwords, API keys, MFA/OTP codes) as tool inputs. It operates only on the page
|
|
78
|
+
content you explicitly ask the assistant to build — it does <strong>not</strong> read, reconstruct, or infer
|
|
79
|
+
your full conversation or chat history.</p>
|
|
80
|
+
|
|
67
81
|
<h2>Data retention & deletion</h2>
|
|
68
|
-
<p>Tokens expire automatically as described above; you can revoke access at any time by
|
|
69
|
-
connector in Claude/ChatGPT settings, or by logging out of Webcake. Pages you create live in
|
|
70
|
-
account and are managed there. To request deletion of anything else, contact us below.</p>
|
|
82
|
+
<p>Tokens and the draft cache expire automatically as described above; you can revoke access at any time by
|
|
83
|
+
disconnecting the connector in Claude/ChatGPT settings, or by logging out of Webcake. Pages you create live in
|
|
84
|
+
your Webcake account and are managed there. To request deletion of anything else, contact us below.</p>
|
|
71
85
|
|
|
72
86
|
<h2>Security</h2>
|
|
73
87
|
<p>All traffic uses HTTPS. Authentication follows OAuth 2.1 with PKCE; the connector validates a short-lived
|