@productbrain/mcp 0.0.1-beta.180 → 0.0.1-beta.184
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 +594 -121
- package/dist/http.js.map +1 -1
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -32,10 +32,10 @@ function baseUrl(req) {
|
|
|
32
32
|
var app = express();
|
|
33
33
|
app.set("trust proxy", 1);
|
|
34
34
|
app.use(express.json());
|
|
35
|
-
var ALLOWED_ORIGINS = process.env.CORS_ORIGINS
|
|
35
|
+
var ALLOWED_ORIGINS = (process.env.CORS_ORIGINS ?? "https://claude.ai").split(",").map((o) => o.trim()).filter(Boolean);
|
|
36
36
|
app.use((_req, res, next) => {
|
|
37
37
|
const origin = _req.headers.origin;
|
|
38
|
-
if (
|
|
38
|
+
if (origin && ALLOWED_ORIGINS.includes(origin)) {
|
|
39
39
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
40
40
|
}
|
|
41
41
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
@@ -168,145 +168,589 @@ function esc(s) {
|
|
|
168
168
|
}
|
|
169
169
|
function authPageShell(title, bodyContent, headExtra = "") {
|
|
170
170
|
return `<!DOCTYPE html>
|
|
171
|
-
<html lang="en"><head>
|
|
171
|
+
<html lang="en" data-theme="parchment-dark"><head>
|
|
172
172
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
173
|
-
<title>${title} \u2014 Product Brain</title>
|
|
173
|
+
<title>${esc(title)} \u2014 Product Brain</title>
|
|
174
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
175
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
176
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,600&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;700&display=swap">
|
|
174
177
|
${headExtra}
|
|
175
178
|
<style>
|
|
176
|
-
:root{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
:root{
|
|
180
|
+
--bg:#1a1917;--bg-warm:#201f1c;--surface:#262521;
|
|
181
|
+
--fg1:#e4e0d8;--fg2:#c4bfb4;--fg3:#9a9589;--fg4:#6a6560;--fg-bright:#ffffff;
|
|
182
|
+
--border:rgba(255,255,255,0.07);--border-light:rgba(255,255,255,0.04);
|
|
183
|
+
--accent:#c9b99a;
|
|
184
|
+
--btn-bg:#ffffff;--btn-fg:#1a1917;--btn-hover:#e4e0d8;
|
|
185
|
+
--green:#4ade80;--rose:#ef4444;
|
|
186
|
+
--ghost:rgba(38,37,33,0.55);
|
|
187
|
+
--radius-md:7px;--radius-lg:10px;
|
|
188
|
+
--font-display:"Source Serif 4",ui-serif,Georgia,serif;
|
|
189
|
+
--font-body:"IBM Plex Sans",ui-sans-serif,system-ui,sans-serif;
|
|
190
|
+
--font-mono:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace;
|
|
191
|
+
}
|
|
192
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
193
|
+
html,body{height:100%}
|
|
194
|
+
body{
|
|
195
|
+
font-family:var(--font-body);font-size:13px;line-height:1.45;
|
|
196
|
+
color:var(--fg1);background:var(--bg);
|
|
197
|
+
min-height:100vh;display:grid;place-items:center;padding:24px;
|
|
198
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
199
|
+
position:relative;overflow:hidden;
|
|
200
|
+
}
|
|
201
|
+
body::before{
|
|
202
|
+
content:"";position:fixed;inset:0;
|
|
203
|
+
background:radial-gradient(900px 600px at 50% 50%,rgba(228,224,216,0.025),transparent 60%);
|
|
204
|
+
pointer-events:none;z-index:0;
|
|
205
|
+
}
|
|
206
|
+
.top-mark{
|
|
207
|
+
position:fixed;top:22px;left:24px;display:flex;align-items:center;gap:8px;
|
|
208
|
+
z-index:5;opacity:0.7;
|
|
209
|
+
}
|
|
210
|
+
.top-mark .m{
|
|
211
|
+
width:16px;height:16px;border-radius:4px;background:#1c1e24;
|
|
212
|
+
border:1px solid rgba(255,255,255,0.06);display:inline-grid;place-items:center;
|
|
213
|
+
}
|
|
214
|
+
.top-mark .m .core{width:5px;height:5px;border-radius:50%;background:var(--accent)}
|
|
215
|
+
.top-mark .name{
|
|
216
|
+
font-family:var(--font-mono);font-size:10.5px;letter-spacing:0.22em;
|
|
217
|
+
text-transform:uppercase;color:var(--fg4);font-weight:500;
|
|
218
|
+
}
|
|
219
|
+
.stage{
|
|
220
|
+
width:100%;max-width:460px;text-align:center;
|
|
221
|
+
position:relative;z-index:1;
|
|
222
|
+
display:grid;
|
|
223
|
+
}
|
|
224
|
+
.panel{
|
|
225
|
+
grid-area:1/1;
|
|
226
|
+
transition:opacity 280ms ease-out,transform 380ms cubic-bezier(.2,.6,.2,1),filter 380ms ease-out;
|
|
227
|
+
}
|
|
228
|
+
.panel[hidden]{
|
|
229
|
+
display:block !important;
|
|
230
|
+
opacity:0;transform:scale(0.96) translateY(-2px);filter:blur(6px);
|
|
231
|
+
pointer-events:none;
|
|
232
|
+
}
|
|
233
|
+
.panel:not([hidden]){opacity:1;transform:scale(1);filter:none;pointer-events:auto}
|
|
234
|
+
|
|
235
|
+
/* eyebrows */
|
|
236
|
+
.eyebrow{
|
|
237
|
+
font-family:var(--font-mono);font-size:10px;font-weight:700;
|
|
238
|
+
letter-spacing:0.28em;text-transform:uppercase;color:var(--fg4);
|
|
239
|
+
margin-bottom:24px;display:inline-flex;align-items:center;gap:7px;
|
|
240
|
+
}
|
|
241
|
+
.eyebrow .dot{width:5px;height:5px;border-radius:50%;background:currentColor}
|
|
242
|
+
.eyebrow.danger{color:var(--rose)}
|
|
243
|
+
.eyebrow.success{color:var(--green)}
|
|
244
|
+
.eyebrow.success .dot{box-shadow:0 0 0 3px rgba(74,222,128,0.18)}
|
|
245
|
+
|
|
246
|
+
/* form input */
|
|
247
|
+
.input-wrap{
|
|
248
|
+
display:flex;align-items:center;background:rgba(0,0,0,0.22);
|
|
249
|
+
border:1px solid var(--border);border-radius:var(--radius-md);
|
|
250
|
+
transition:border-color 200ms ease-out,box-shadow 200ms ease-out;
|
|
251
|
+
}
|
|
252
|
+
.input-wrap:focus-within{
|
|
253
|
+
border-color:rgba(228,224,216,0.45);
|
|
254
|
+
box-shadow:0 0 0 3px rgba(228,224,216,0.10);
|
|
255
|
+
}
|
|
256
|
+
.input-wrap.has-error{
|
|
257
|
+
border-color:rgba(239,68,68,0.55);
|
|
258
|
+
box-shadow:0 0 0 3px rgba(239,68,68,0.12);
|
|
259
|
+
animation:shake 360ms cubic-bezier(.36,.07,.19,.97);
|
|
260
|
+
}
|
|
261
|
+
@keyframes shake{
|
|
262
|
+
10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}
|
|
263
|
+
30%,50%,70%{transform:translateX(-4px)}40%,60%{transform:translateX(4px)}
|
|
264
|
+
}
|
|
265
|
+
.input-prefix{
|
|
266
|
+
font-family:var(--font-mono);font-size:14px;color:var(--fg3);
|
|
267
|
+
padding-left:16px;user-select:none;
|
|
268
|
+
}
|
|
269
|
+
.input{
|
|
270
|
+
flex:1;min-width:0;background:transparent;border:0;outline:none;
|
|
271
|
+
padding:16px 14px 16px 4px;
|
|
272
|
+
font-family:var(--font-mono);font-size:14px;color:var(--fg1);letter-spacing:0.02em;
|
|
273
|
+
}
|
|
274
|
+
.input::placeholder{color:var(--fg4)}
|
|
275
|
+
.hint{
|
|
276
|
+
margin-top:10px;font-family:var(--font-mono);font-size:10.5px;
|
|
277
|
+
letter-spacing:0.16em;text-transform:uppercase;
|
|
278
|
+
text-align:left;padding-left:4px;height:14px;color:var(--fg4);
|
|
279
|
+
transition:color 160ms ease-out;
|
|
280
|
+
}
|
|
281
|
+
.hint.is-error{color:var(--rose)}
|
|
282
|
+
|
|
283
|
+
/* primary button */
|
|
284
|
+
.btn-primary{
|
|
285
|
+
width:100%;height:48px;margin-top:14px;
|
|
286
|
+
border:0;border-radius:var(--radius-md);
|
|
287
|
+
background:var(--btn-bg);color:var(--btn-fg);
|
|
288
|
+
font-family:var(--font-body);font-size:14.5px;font-weight:600;letter-spacing:-0.005em;
|
|
289
|
+
cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:10px;
|
|
290
|
+
transition:background 140ms ease-out,transform 80ms ease-out,opacity 200ms ease-out;
|
|
291
|
+
position:relative;overflow:hidden;
|
|
292
|
+
}
|
|
293
|
+
.btn-primary:hover:not([disabled]){background:var(--btn-hover)}
|
|
294
|
+
.btn-primary:active:not([disabled]){transform:translateY(1px)}
|
|
295
|
+
.btn-primary[disabled]{opacity:0.45;cursor:default}
|
|
296
|
+
.spin{
|
|
297
|
+
width:14px;height:14px;border-radius:50%;
|
|
298
|
+
border:1.5px solid currentColor;border-top-color:transparent;
|
|
299
|
+
animation:spin 700ms linear infinite;opacity:0.85;
|
|
300
|
+
}
|
|
301
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
302
|
+
|
|
303
|
+
/* secondary link */
|
|
304
|
+
.small-link{
|
|
305
|
+
margin-top:18px;font-family:var(--font-mono);font-size:11px;
|
|
306
|
+
letter-spacing:0.18em;text-transform:uppercase;color:var(--fg4);
|
|
307
|
+
}
|
|
308
|
+
.small-link a{
|
|
309
|
+
color:var(--fg3);text-decoration:none;border-bottom:1px dotted currentColor;
|
|
310
|
+
padding-bottom:1px;transition:color 140ms;
|
|
311
|
+
}
|
|
312
|
+
.small-link a:hover{color:var(--fg1)}
|
|
313
|
+
|
|
314
|
+
/* orb */
|
|
315
|
+
.orb-wrap{
|
|
316
|
+
position:relative;width:160px;height:160px;margin:0 auto 36px;
|
|
317
|
+
display:grid;place-items:center;
|
|
318
|
+
}
|
|
319
|
+
.orb-ring{position:absolute;border-radius:50%}
|
|
320
|
+
.orb-ring.r1{inset:0;border:1px solid rgba(228,224,216,0.06);animation:drift1 40s linear infinite}
|
|
321
|
+
.orb-ring.r2{inset:18px;border:1px dashed rgba(228,224,216,0.10);animation:drift2 28s linear infinite}
|
|
322
|
+
.orb-ring.r3{inset:38px;border:1px solid rgba(74,222,128,0.20);animation:ringPulse 3.4s ease-in-out infinite}
|
|
323
|
+
@keyframes drift1{to{transform:rotate(360deg)}}
|
|
324
|
+
@keyframes drift2{to{transform:rotate(-360deg)}}
|
|
325
|
+
@keyframes ringPulse{0%,100%{opacity:0.6}50%{opacity:1}}
|
|
326
|
+
|
|
327
|
+
.orb-wrap.is-verifying .orb-ring.r3{
|
|
328
|
+
border-color:rgba(228,224,216,0.20);
|
|
329
|
+
animation:ringPulse 1.1s ease-in-out infinite;
|
|
330
|
+
}
|
|
331
|
+
.orb-wrap.is-verifying .orb-core{
|
|
332
|
+
box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06);
|
|
333
|
+
animation:corePulseNeutral 1.6s ease-in-out infinite;
|
|
334
|
+
}
|
|
335
|
+
.orb-wrap.is-verifying .orb-dot{background:var(--fg3);box-shadow:0 0 10px rgba(228,224,216,0.4)}
|
|
336
|
+
@keyframes corePulseNeutral{
|
|
337
|
+
0%,100%{box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06)}
|
|
338
|
+
50%{box-shadow:0 0 0 9px rgba(228,224,216,0.07),0 0 32px rgba(228,224,216,0.18),inset 0 0 22px rgba(228,224,216,0.10)}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.orb-wrap.is-error .orb-ring.r3{border-color:rgba(239,68,68,0.30);animation:none;opacity:1}
|
|
342
|
+
.orb-wrap.is-error .orb-ring.r1,.orb-wrap.is-error .orb-ring.r2{animation-play-state:paused}
|
|
343
|
+
.orb-wrap.is-error .orb-core{
|
|
344
|
+
box-shadow:0 0 0 6px rgba(239,68,68,0.05),0 0 22px rgba(239,68,68,0.20),inset 0 0 16px rgba(239,68,68,0.08);
|
|
345
|
+
animation:none;
|
|
346
|
+
}
|
|
347
|
+
.orb-wrap.is-error .orb-dot{background:var(--rose);box-shadow:0 0 10px rgba(239,68,68,0.6)}
|
|
348
|
+
|
|
349
|
+
.sat-orbit{position:absolute;inset:0;pointer-events:none}
|
|
350
|
+
.sat-orbit.o1{animation:drift1 40s linear infinite}
|
|
351
|
+
.sat-orbit.o2{animation:drift2 56s linear infinite}
|
|
352
|
+
.sat{
|
|
353
|
+
position:absolute;top:50%;left:50%;
|
|
354
|
+
font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.14em;
|
|
355
|
+
background:var(--bg);padding:2px 6px;border-radius:3px;
|
|
356
|
+
border:1px solid var(--border-light);
|
|
357
|
+
transform-origin:0 0;opacity:0;
|
|
358
|
+
}
|
|
359
|
+
.panel:not([hidden])[data-state="connected"] .sat{animation:satIn 600ms ease-out forwards}
|
|
360
|
+
@keyframes satIn{from{opacity:0}to{opacity:1}}
|
|
361
|
+
.sat span{display:inline-block;animation:counter 40s linear infinite}
|
|
362
|
+
.sat-orbit.o2 .sat span{animation:counter2 56s linear infinite}
|
|
363
|
+
@keyframes counter{to{transform:rotate(-360deg)}}
|
|
364
|
+
@keyframes counter2{to{transform:rotate(360deg)}}
|
|
365
|
+
|
|
366
|
+
.orb-core{
|
|
367
|
+
position:relative;width:60px;height:60px;border-radius:50%;
|
|
368
|
+
background:radial-gradient(circle at 50% 45%,#1a1a1a 0%,#0c0c0c 60%,#050505 100%);
|
|
369
|
+
border:1px solid rgba(255,255,255,0.06);
|
|
370
|
+
display:grid;place-items:center;
|
|
371
|
+
box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 28px rgba(74,222,128,0.20),inset 0 0 18px rgba(74,222,128,0.08);
|
|
372
|
+
animation:corePulse 3.4s ease-in-out infinite;
|
|
373
|
+
}
|
|
374
|
+
@keyframes corePulse{
|
|
375
|
+
0%,100%{box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 24px rgba(74,222,128,0.18),inset 0 0 16px rgba(74,222,128,0.06)}
|
|
376
|
+
50%{box-shadow:0 0 0 9px rgba(74,222,128,0.06),0 0 40px rgba(74,222,128,0.32),inset 0 0 22px rgba(74,222,128,0.14)}
|
|
377
|
+
}
|
|
378
|
+
.orb-dot{width:10px;height:10px;border-radius:50%;background:#4ade80;box-shadow:0 0 12px rgba(74,222,128,0.7)}
|
|
379
|
+
|
|
380
|
+
.core-shockwave{
|
|
381
|
+
position:absolute;inset:0;border-radius:50%;
|
|
382
|
+
border:1px solid rgba(74,222,128,0.6);
|
|
383
|
+
opacity:0;pointer-events:none;
|
|
384
|
+
}
|
|
385
|
+
.panel:not([hidden])[data-state="connected"] .core-shockwave{animation:shock 1100ms ease-out 200ms}
|
|
386
|
+
@keyframes shock{
|
|
387
|
+
0%{opacity:0.7;transform:scale(0.4);border-width:2px}
|
|
388
|
+
100%{opacity:0;transform:scale(2.4);border-width:1px}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* titles */
|
|
392
|
+
.ok-title{
|
|
393
|
+
font-family:var(--font-display);font-weight:600;
|
|
394
|
+
font-size:38px;line-height:1.05;letter-spacing:-0.025em;
|
|
395
|
+
color:var(--fg-bright);opacity:0;
|
|
396
|
+
}
|
|
397
|
+
.panel:not([hidden]) .ok-title{animation:rise 600ms ease-out 380ms forwards}
|
|
398
|
+
.ok-sub{
|
|
399
|
+
margin-top:16px;font-size:14.5px;line-height:1.55;color:var(--fg3);
|
|
400
|
+
opacity:0;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;
|
|
401
|
+
}
|
|
402
|
+
.panel:not([hidden]) .ok-sub{animation:rise 600ms ease-out 520ms forwards}
|
|
403
|
+
.ok-actions{
|
|
404
|
+
margin-top:24px;display:flex;flex-direction:column;align-items:center;gap:14px;
|
|
405
|
+
opacity:0;
|
|
406
|
+
}
|
|
407
|
+
.panel:not([hidden]) .ok-actions{animation:rise 600ms ease-out 660ms forwards}
|
|
408
|
+
.ok-foot{
|
|
409
|
+
margin-top:36px;font-family:var(--font-mono);font-size:10px;
|
|
410
|
+
letter-spacing:0.22em;text-transform:uppercase;color:var(--fg4);opacity:0;
|
|
411
|
+
}
|
|
412
|
+
.panel:not([hidden]) .ok-foot{animation:rise 600ms ease-out 820ms forwards}
|
|
413
|
+
.ws-name{font-size:13px;color:var(--accent);letter-spacing:0.04em;font-weight:500}
|
|
414
|
+
|
|
415
|
+
.cmd{
|
|
416
|
+
display:inline-flex;align-items:center;gap:8px;
|
|
417
|
+
font-family:var(--font-mono);font-size:13px;color:var(--fg1);
|
|
418
|
+
background:rgba(255,255,255,0.05);border:1px solid var(--border);
|
|
419
|
+
padding:6px 10px 6px 12px;border-radius:6px;letter-spacing:0.02em;
|
|
420
|
+
cursor:pointer;user-select:none;
|
|
421
|
+
transition:background 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;
|
|
422
|
+
}
|
|
423
|
+
.cmd:hover{background:rgba(255,255,255,0.09);border-color:rgba(255,255,255,0.18)}
|
|
424
|
+
.cmd.is-copied{color:var(--green);border-color:rgba(74,222,128,0.3);background:rgba(74,222,128,0.06)}
|
|
425
|
+
.cmd .cmd-icon{
|
|
426
|
+
width:12px;height:12px;color:var(--fg3);display:inline-grid;place-items:center;
|
|
427
|
+
transition:color 140ms;
|
|
428
|
+
}
|
|
429
|
+
.cmd.is-copied .cmd-icon{color:var(--green)}
|
|
430
|
+
.cmd .cmd-icon svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
431
|
+
|
|
432
|
+
.return-link{
|
|
433
|
+
font-family:var(--font-mono);font-size:11px;letter-spacing:0.18em;
|
|
434
|
+
text-transform:uppercase;color:var(--fg3);text-decoration:none;
|
|
435
|
+
border-bottom:1px dotted currentColor;padding-bottom:1px;
|
|
436
|
+
transition:color 140ms;
|
|
437
|
+
}
|
|
438
|
+
.return-link:hover{color:var(--fg1)}
|
|
439
|
+
|
|
440
|
+
/* error specifics */
|
|
441
|
+
.err-title{
|
|
442
|
+
font-family:var(--font-display);font-weight:600;
|
|
443
|
+
font-size:32px;line-height:1.1;letter-spacing:-0.02em;
|
|
444
|
+
color:var(--fg1);margin-bottom:12px;
|
|
445
|
+
}
|
|
446
|
+
.err-msg{
|
|
447
|
+
font-size:14.5px;line-height:1.55;color:var(--fg3);
|
|
448
|
+
margin-bottom:28px;
|
|
449
|
+
}
|
|
450
|
+
.err-msg code{
|
|
451
|
+
font-family:var(--font-mono);font-size:12px;
|
|
452
|
+
background:rgba(255,255,255,0.06);padding:1px 5px;border-radius:4px;color:var(--fg2);
|
|
453
|
+
}
|
|
454
|
+
.err-actions{display:flex;gap:8px}
|
|
455
|
+
.btn-secondary{
|
|
456
|
+
flex:1;height:44px;border-radius:var(--radius-md);
|
|
457
|
+
background:transparent;color:var(--fg2);
|
|
458
|
+
border:1px solid var(--border);
|
|
459
|
+
font-family:var(--font-body);font-size:13.5px;font-weight:500;
|
|
460
|
+
cursor:pointer;text-decoration:none;
|
|
461
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
462
|
+
transition:background 140ms,color 140ms,border-color 140ms;
|
|
463
|
+
}
|
|
464
|
+
.btn-secondary:hover{background:rgba(255,255,255,0.04);color:var(--fg1);border-color:rgba(255,255,255,0.14)}
|
|
465
|
+
|
|
466
|
+
/* verifying */
|
|
467
|
+
.verifying-eyebrow{
|
|
468
|
+
font-family:var(--font-mono);font-size:10px;
|
|
469
|
+
letter-spacing:0.28em;text-transform:uppercase;color:var(--fg3);
|
|
470
|
+
font-weight:700;margin-bottom:14px;
|
|
471
|
+
}
|
|
472
|
+
.verifying-title{
|
|
473
|
+
font-family:var(--font-display);font-weight:600;
|
|
474
|
+
font-size:28px;line-height:1.1;color:var(--fg1);margin-bottom:8px;
|
|
475
|
+
letter-spacing:-0.02em;
|
|
476
|
+
}
|
|
477
|
+
.verifying-sub{font-size:13px;color:var(--fg3);min-height:1.45em}
|
|
478
|
+
|
|
479
|
+
/* form heading */
|
|
480
|
+
.form-title{
|
|
481
|
+
font-family:var(--font-display);font-weight:600;
|
|
482
|
+
font-size:32px;line-height:1.1;letter-spacing:-0.02em;
|
|
483
|
+
color:var(--fg1);margin-bottom:10px;
|
|
484
|
+
}
|
|
485
|
+
.form-sub{font-size:13.5px;color:var(--fg3);margin-bottom:28px;line-height:1.55}
|
|
486
|
+
|
|
487
|
+
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
488
|
+
|
|
489
|
+
@media (prefers-reduced-motion: reduce){
|
|
490
|
+
*,*::before,*::after{
|
|
491
|
+
animation-duration:0.01ms !important;animation-iteration-count:1 !important;
|
|
492
|
+
transition-duration:0.01ms !important;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
181
495
|
</style>
|
|
182
496
|
</head><body>
|
|
183
|
-
<div class="
|
|
497
|
+
<div class="top-mark">
|
|
498
|
+
<span class="m"><span class="core"></span></span>
|
|
499
|
+
<span class="name">Product Brain</span>
|
|
500
|
+
</div>
|
|
501
|
+
<div class="stage">${bodyContent}</div>
|
|
184
502
|
</body></html>`;
|
|
185
503
|
}
|
|
504
|
+
function providerDisplayName(clientName) {
|
|
505
|
+
const name = (clientName ?? "").trim();
|
|
506
|
+
if (!name) return "your agent";
|
|
507
|
+
return name.length > 40 ? name.slice(0, 40) + "\u2026" : name;
|
|
508
|
+
}
|
|
509
|
+
function successPanelInner(workspaceName, redirectUrl, providerName) {
|
|
510
|
+
return `
|
|
511
|
+
<div class="orb-wrap">
|
|
512
|
+
<div class="orb-ring r1"></div>
|
|
513
|
+
<div class="orb-ring r2"></div>
|
|
514
|
+
<div class="orb-ring r3"></div>
|
|
515
|
+
<div class="sat-orbit o1">
|
|
516
|
+
<span class="sat" style="transform:rotate(20deg) translate(78px) rotate(-20deg);color:#4ade80;animation-delay:600ms"><span>DEC</span></span>
|
|
517
|
+
<span class="sat" style="transform:rotate(140deg) translate(78px) rotate(-140deg);color:#c9b99a;animation-delay:720ms"><span>WP</span></span>
|
|
518
|
+
<span class="sat" style="transform:rotate(260deg) translate(78px) rotate(-260deg);color:#f59e0b;animation-delay:840ms"><span>TEN</span></span>
|
|
519
|
+
</div>
|
|
520
|
+
<div class="sat-orbit o2">
|
|
521
|
+
<span class="sat" style="transform:rotate(80deg) translate(54px) rotate(-80deg);color:#60a5fa;animation-delay:960ms"><span>STD</span></span>
|
|
522
|
+
<span class="sat" style="transform:rotate(220deg) translate(54px) rotate(-220deg);color:#a78bfa;animation-delay:1080ms"><span>INS</span></span>
|
|
523
|
+
</div>
|
|
524
|
+
<div class="core-shockwave"></div>
|
|
525
|
+
<div class="orb-core"><div class="orb-dot"></div></div>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="eyebrow success"><span class="dot"></span>Connected</div>
|
|
528
|
+
<h1 class="ok-title">Product Brain is live.</h1>
|
|
529
|
+
<p class="ok-sub">
|
|
530
|
+
<span class="ws-name" data-field="ws-name">${esc(workspaceName)}</span>
|
|
531
|
+
</p>
|
|
532
|
+
<div class="ok-actions">
|
|
533
|
+
<button class="cmd" type="button" data-cmd-pill data-redirect="${esc(redirectUrl)}" aria-label="Copy 'Start PB' and return to ${esc(providerName)}">
|
|
534
|
+
<span data-cmd-text>Start PB</span>
|
|
535
|
+
<span class="cmd-icon" aria-hidden="true">
|
|
536
|
+
<svg data-cmd-svg viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>
|
|
537
|
+
</span>
|
|
538
|
+
</button>
|
|
539
|
+
<a class="return-link" href="${esc(redirectUrl)}" data-return-link>Return to <span data-provider>${esc(providerName)}</span> →</a>
|
|
540
|
+
</div>
|
|
541
|
+
<p class="ok-foot">Then say <span style="font-family:var(--font-mono);color:var(--fg3)">Start PB</span> in <span data-provider>${esc(providerName)}</span></p>`;
|
|
542
|
+
}
|
|
543
|
+
function errorPanelInner(title, trustedDetailHtml, retryUrl) {
|
|
544
|
+
return `
|
|
545
|
+
<div class="orb-wrap is-error">
|
|
546
|
+
<div class="orb-ring r1"></div>
|
|
547
|
+
<div class="orb-ring r2"></div>
|
|
548
|
+
<div class="orb-ring r3"></div>
|
|
549
|
+
<div class="orb-core"><div class="orb-dot"></div></div>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="eyebrow danger"><span class="dot"></span>Couldn't connect</div>
|
|
552
|
+
<h2 class="err-title" data-field="err-title">${esc(title)}</h2>
|
|
553
|
+
<p class="err-msg" data-field="err-msg">${trustedDetailHtml}</p>
|
|
554
|
+
<div class="err-actions">
|
|
555
|
+
<a href="${esc(retryUrl)}" class="btn-secondary" data-retry-link>← Try again</a>
|
|
556
|
+
<a href="https://productbrain.io" target="_blank" rel="noopener noreferrer" class="btn-secondary">Get an API key →</a>
|
|
557
|
+
</div>`;
|
|
558
|
+
}
|
|
559
|
+
var cmdScript = `
|
|
560
|
+
(function(){
|
|
561
|
+
function bindCmd(pill){
|
|
562
|
+
if(!pill||pill.__bound)return;pill.__bound=true;
|
|
563
|
+
var textEl=pill.querySelector('[data-cmd-text]');
|
|
564
|
+
var svgEl=pill.querySelector('[data-cmd-svg]');
|
|
565
|
+
var redirectUrl=pill.getAttribute('data-redirect')||'';
|
|
566
|
+
pill.addEventListener('click',function(){
|
|
567
|
+
var done=function(){
|
|
568
|
+
pill.classList.add('is-copied');
|
|
569
|
+
if(textEl)textEl.textContent='Copied';
|
|
570
|
+
if(svgEl)svgEl.innerHTML='<polyline points="4 12 10 18 20 6"/>';
|
|
571
|
+
setTimeout(function(){if(redirectUrl)window.location.href=redirectUrl},900);
|
|
572
|
+
};
|
|
573
|
+
try{
|
|
574
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){
|
|
575
|
+
navigator.clipboard.writeText('Start PB').then(done,done);
|
|
576
|
+
}else{done()}
|
|
577
|
+
}catch(e){done()}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
document.querySelectorAll('[data-cmd-pill]').forEach(bindCmd);
|
|
581
|
+
})();
|
|
582
|
+
`;
|
|
186
583
|
function authorizeFormPage(params) {
|
|
187
584
|
const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;
|
|
585
|
+
const providerName = providerDisplayName(params.client_name);
|
|
188
586
|
const body = `
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
input
|
|
198
|
-
input
|
|
199
|
-
input
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<div class="brand"><div class="brand-icon">⬡</div><span class="brand-name">Product Brain</span></div>
|
|
218
|
-
<h1>Connect to Claude</h1>
|
|
219
|
-
<p class="sub">Enter your API key to give Claude access to your workspace.</p>
|
|
220
|
-
<form method="POST" action="/authorize" id="f">
|
|
221
|
-
<input type="hidden" name="redirect_uri" value="${esc(redirect_uri)}">
|
|
222
|
-
<input type="hidden" name="code_challenge" value="${esc(code_challenge)}">
|
|
223
|
-
<input type="hidden" name="code_challenge_method" value="${esc(code_challenge_method)}">
|
|
224
|
-
<input type="hidden" name="state" value="${esc(state)}">
|
|
225
|
-
<input type="hidden" name="client_id" value="${esc(client_id)}">
|
|
226
|
-
<label for="k">API Key</label>
|
|
227
|
-
<div class="inp-wrap">
|
|
228
|
-
<input type="password" id="k" name="api_key" placeholder="pb_sk_\u2026" required autofocus autocomplete="off">
|
|
229
|
-
</div>
|
|
230
|
-
<div class="field-row">
|
|
231
|
-
<span class="hint">Starts with <code>pb_sk_</code></span>
|
|
232
|
-
<a href="https://productbrain.io" target="_blank" rel="noopener noreferrer" class="get-key">No key? Get one →</a>
|
|
587
|
+
<!-- \u2500\u2500\u2500 CONNECT \u2500\u2500\u2500 -->
|
|
588
|
+
<div class="panel" id="p-connect" data-state="connect">
|
|
589
|
+
<div class="eyebrow">Connect Product Brain</div>
|
|
590
|
+
<h1 class="form-title">Paste your API key</h1>
|
|
591
|
+
<p class="form-sub">Give <span data-provider>${esc(providerName)}</span> access to your workspace memory.</p>
|
|
592
|
+
<form method="POST" action="/authorize" id="f" autocomplete="off">
|
|
593
|
+
<input type="hidden" name="redirect_uri" value="${esc(redirect_uri)}">
|
|
594
|
+
<input type="hidden" name="code_challenge" value="${esc(code_challenge)}">
|
|
595
|
+
<input type="hidden" name="code_challenge_method" value="${esc(code_challenge_method)}">
|
|
596
|
+
<input type="hidden" name="state" value="${esc(state)}">
|
|
597
|
+
<input type="hidden" name="client_id" value="${esc(client_id)}">
|
|
598
|
+
<div class="input-wrap" id="iw">
|
|
599
|
+
<span class="input-prefix">pb_sk_</span>
|
|
600
|
+
<input type="password" id="k" name="api_key" class="input" placeholder="\u2026" required autofocus spellcheck="false">
|
|
601
|
+
</div>
|
|
602
|
+
<div class="hint" id="hint">Your key starts with pb_sk_</div>
|
|
603
|
+
<button type="submit" class="btn-primary" id="sb" disabled><span id="bt">Connect</span></button>
|
|
604
|
+
</form>
|
|
605
|
+
<div class="small-link"><a href="https://productbrain.io" target="_blank" rel="noopener noreferrer">No key? Generate one →</a></div>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<!-- \u2500\u2500\u2500 VERIFYING \u2500\u2500\u2500 -->
|
|
609
|
+
<div class="panel" id="p-verifying" data-state="verifying" hidden>
|
|
610
|
+
<div class="orb-wrap is-verifying">
|
|
611
|
+
<div class="orb-ring r1"></div>
|
|
612
|
+
<div class="orb-ring r2"></div>
|
|
613
|
+
<div class="orb-ring r3"></div>
|
|
614
|
+
<div class="orb-core"><div class="orb-dot"></div></div>
|
|
233
615
|
</div>
|
|
234
|
-
<
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
<div class="
|
|
241
|
-
|
|
616
|
+
<div class="verifying-eyebrow">Handshake</div>
|
|
617
|
+
<h2 class="verifying-title">Verifying key\u2026</h2>
|
|
618
|
+
<p class="verifying-sub" id="verify-sub">Checking workspace \xB7 \u2026</p>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<!-- \u2500\u2500\u2500 CONNECTED (filled by JS from JSON response) \u2500\u2500\u2500 -->
|
|
622
|
+
<div class="panel" id="p-connected" data-state="connected" hidden></div>
|
|
623
|
+
|
|
624
|
+
<!-- \u2500\u2500\u2500 ERROR (filled by JS from JSON response) \u2500\u2500\u2500 -->
|
|
625
|
+
<div class="panel" id="p-error" data-state="error" hidden></div>
|
|
626
|
+
|
|
627
|
+
<template id="tpl-connected">${successPanelInner("__WS__", "__URL__", "__PROVIDER__")}</template>
|
|
628
|
+
<template id="tpl-error">${errorPanelInner("__TITLE__", "__DETAIL__", "__RETRY__")}</template>
|
|
629
|
+
|
|
242
630
|
<script>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
f.
|
|
631
|
+
${cmdScript}
|
|
632
|
+
(function(){
|
|
633
|
+
var f=document.getElementById('f'),k=document.getElementById('k'),iw=document.getElementById('iw'),hint=document.getElementById('hint'),sb=document.getElementById('sb'),bt=document.getElementById('bt');
|
|
634
|
+
var pConnect=document.getElementById('p-connect'),pVerify=document.getElementById('p-verifying'),pOk=document.getElementById('p-connected'),pErr=document.getElementById('p-error');
|
|
635
|
+
var verifySub=document.getElementById('verify-sub');
|
|
636
|
+
|
|
637
|
+
function show(panel){
|
|
638
|
+
[pConnect,pVerify,pOk,pErr].forEach(function(p){
|
|
639
|
+
if(p===panel){p.removeAttribute('hidden')}else{p.setAttribute('hidden','')}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function syncInput(){
|
|
644
|
+
var v=k.value;
|
|
645
|
+
if(v.indexOf('pb_sk_')===0)k.value=v.slice(6);
|
|
646
|
+
sb.disabled=!k.value.trim();
|
|
647
|
+
iw.classList.remove('has-error');
|
|
648
|
+
hint.classList.remove('is-error');
|
|
649
|
+
hint.textContent='Your key starts with pb_sk_';
|
|
650
|
+
}
|
|
651
|
+
k.addEventListener('input',syncInput);
|
|
652
|
+
k.addEventListener('paste',function(){setTimeout(syncInput,0)});
|
|
653
|
+
k.addEventListener('keydown',function(e){
|
|
654
|
+
if(e.key==='Escape'){k.value='';syncInput()}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
function showError(title,detailHtml){
|
|
658
|
+
var tpl=document.getElementById('tpl-error');
|
|
659
|
+
var html=tpl.innerHTML
|
|
660
|
+
.replace('__TITLE__',title.replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]}))
|
|
661
|
+
.replace('__DETAIL__',detailHtml)
|
|
662
|
+
.replace('__RETRY__','#');
|
|
663
|
+
pErr.innerHTML=html;
|
|
664
|
+
var retry=pErr.querySelector('[data-retry-link]');
|
|
665
|
+
if(retry){retry.addEventListener('click',function(e){e.preventDefault();show(pConnect);k.focus();k.select()})}
|
|
666
|
+
show(pErr);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function showSuccess(workspaceName,redirectUrl,providerName){
|
|
670
|
+
var tpl=document.getElementById('tpl-connected');
|
|
671
|
+
var safeWs=String(workspaceName||'').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});
|
|
672
|
+
var safeProv=String(providerName||'your agent').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});
|
|
673
|
+
var safeUrl=String(redirectUrl||'').replace(/"/g,'"').replace(/[<>]/g,function(c){return{'<':'<','>':'>'}[c]});
|
|
674
|
+
var html=tpl.innerHTML.split('__WS__').join(safeWs).split('__URL__').join(safeUrl).split('__PROVIDER__').join(safeProv);
|
|
675
|
+
pOk.innerHTML=html;
|
|
676
|
+
pOk.querySelectorAll('[data-cmd-pill]').forEach(function(pill){
|
|
677
|
+
pill.__bound=false;
|
|
678
|
+
});
|
|
679
|
+
// Re-run binder
|
|
680
|
+
var s=document.createElement('script');s.textContent=${JSON.stringify(cmdScript)};document.body.appendChild(s);s.remove();
|
|
681
|
+
show(pOk);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
f.addEventListener('submit',function(e){
|
|
685
|
+
e.preventDefault();
|
|
686
|
+
var v=k.value.trim();
|
|
687
|
+
if(!v){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Paste your key first';return}
|
|
688
|
+
sb.disabled=true;bt.textContent='Verifying';
|
|
689
|
+
show(pVerify);
|
|
690
|
+
|
|
691
|
+
var steps=['Checking workspace \xB7 \u2026','Loading chain \xB7 \u2026','Establishing memory \xB7 \u2026'];
|
|
692
|
+
var i=0;verifySub.textContent=steps[0];
|
|
693
|
+
var ti=setInterval(function(){i++;if(i>=steps.length){clearInterval(ti);return}verifySub.textContent=steps[i]},700);
|
|
694
|
+
|
|
695
|
+
var minDelay=new Promise(function(r){setTimeout(r,1100)});
|
|
696
|
+
var fd=new FormData(f);
|
|
697
|
+
var body=new URLSearchParams();
|
|
698
|
+
fd.forEach(function(val,key){body.append(key,String(val))});
|
|
699
|
+
|
|
700
|
+
var req=fetch('/authorize',{
|
|
701
|
+
method:'POST',
|
|
702
|
+
headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'},
|
|
703
|
+
body:body.toString(),
|
|
704
|
+
credentials:'same-origin'
|
|
705
|
+
}).then(function(r){return r.json().then(function(j){return{status:r.status,body:j}})});
|
|
706
|
+
|
|
707
|
+
Promise.all([req,minDelay]).then(function(arr){
|
|
708
|
+
clearInterval(ti);
|
|
709
|
+
var res=arr[0];
|
|
710
|
+
if(res.body&&res.body.ok){
|
|
711
|
+
showSuccess(res.body.workspaceName,res.body.redirectUrl,res.body.providerName);
|
|
712
|
+
}else{
|
|
713
|
+
showError(res.body&&res.body.title||'Couldn\\'t connect',res.body&&res.body.detail||'Try again, or generate a new key.');
|
|
714
|
+
sb.disabled=false;bt.textContent='Connect';
|
|
715
|
+
}
|
|
716
|
+
}).catch(function(){
|
|
717
|
+
clearInterval(ti);
|
|
718
|
+
showError('Network error','We couldn\\'t reach Product Brain. Check your connection and try again.');
|
|
719
|
+
sb.disabled=false;bt.textContent='Connect';
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
k.focus();
|
|
724
|
+
})();
|
|
246
725
|
</script>`;
|
|
247
|
-
return authPageShell("Connect
|
|
726
|
+
return authPageShell("Connect Product Brain", body);
|
|
248
727
|
}
|
|
249
|
-
function authorizeSuccessPage(workspaceName, redirectUrl) {
|
|
250
|
-
const safeUrl = JSON.stringify(redirectUrl).replace(/<\/script>/gi, "<\\/script>");
|
|
728
|
+
function authorizeSuccessPage(workspaceName, redirectUrl, providerName) {
|
|
251
729
|
const body = `
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
.ring{width:60px;height:60px;background:rgba(52,211,153,.1);border:1px solid rgba(52,211,153,.25);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.4rem;animation:pop .5s cubic-bezier(.175,.885,.32,1.275) .15s both}
|
|
255
|
-
@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
|
|
256
|
-
svg{animation:draw .6s ease .5s both}
|
|
257
|
-
@keyframes draw{from{stroke-dashoffset:30}to{stroke-dashoffset:0}}
|
|
258
|
-
h1{font-size:1.3rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.3rem}
|
|
259
|
-
.ws{font-size:.8rem;color:var(--ok);font-weight:500;margin-bottom:1.5rem}
|
|
260
|
-
.redirect-msg{font-size:.78rem;color:var(--muted);margin-bottom:1.4rem}
|
|
261
|
-
.dots::after{content:'';animation:dots 1.4s steps(4,end) infinite}
|
|
262
|
-
@keyframes dots{0%{content:''}25%{content:'.'}50%{content:'..'}75%{content:'...'}}
|
|
263
|
-
.continue{display:inline-flex;align-items:center;gap:.4rem;padding:.6rem 1.2rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.84rem;font-weight:600;cursor:pointer;text-decoration:none;transition:background .15s}
|
|
264
|
-
.continue:hover{background:var(--accent-h)}
|
|
265
|
-
@keyframes fadein{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
266
|
-
.card{animation:fadein .35s ease}
|
|
267
|
-
</style>
|
|
268
|
-
<div class="ring">
|
|
269
|
-
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="30"><polyline points="20 6 9 17 4 12"/></svg>
|
|
730
|
+
<div class="panel" data-state="connected">
|
|
731
|
+
${successPanelInner(workspaceName, redirectUrl, providerName)}
|
|
270
732
|
</div>
|
|
271
|
-
<
|
|
272
|
-
<p class="ws">${esc(workspaceName)}</p>
|
|
273
|
-
<p class="redirect-msg">Returning to Claude<span class="dots"></span></p>
|
|
274
|
-
<a href="${esc(redirectUrl)}" class="continue">Continue to Claude →</a>
|
|
275
|
-
<script>setTimeout(function(){window.location.href=${safeUrl}},1800)</script>`;
|
|
733
|
+
<script>${cmdScript}</script>`;
|
|
276
734
|
return authPageShell("Connected", body);
|
|
277
735
|
}
|
|
278
736
|
function authorizeErrorPage(title, trustedDetailHtml, retryUrl) {
|
|
279
737
|
const body = `
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
.x-ring{width:52px;height:52px;background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.25rem;font-size:1.4rem;animation:shake .4s ease .1s}
|
|
283
|
-
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
|
|
284
|
-
h1{font-size:1.2rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem;color:var(--err)}
|
|
285
|
-
.detail{font-size:.8rem;color:var(--muted);line-height:1.6;margin-bottom:1.5rem}
|
|
286
|
-
.detail code{font-size:.75rem;background:rgba(255,255,255,.06);padding:.1rem .3rem;border-radius:4px}
|
|
287
|
-
.actions{display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}
|
|
288
|
-
.btn-retry{display:inline-flex;align-items:center;padding:.55rem 1.1rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.82rem;font-weight:600;cursor:pointer;text-decoration:none;transition:background .15s}
|
|
289
|
-
.btn-retry:hover{background:var(--accent-h)}
|
|
290
|
-
.btn-key{display:inline-flex;align-items:center;padding:.55rem 1rem;background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:10px;font-size:.82rem;text-decoration:none;transition:border-color .15s,color .15s}
|
|
291
|
-
.btn-key:hover{border-color:var(--accent);color:var(--text)}
|
|
292
|
-
</style>
|
|
293
|
-
<div class="x-ring">✕</div>
|
|
294
|
-
<h1>${esc(title)}</h1>
|
|
295
|
-
<p class="detail">${trustedDetailHtml}</p>
|
|
296
|
-
<div class="actions">
|
|
297
|
-
<a href="${esc(retryUrl)}" class="btn-retry">← Try again</a>
|
|
298
|
-
<a href="https://productbrain.io" target="_blank" rel="noopener noreferrer" class="btn-key">Get an API key →</a>
|
|
738
|
+
<div class="panel" data-state="error">
|
|
739
|
+
${errorPanelInner(title, trustedDetailHtml, retryUrl)}
|
|
299
740
|
</div>`;
|
|
300
741
|
return authPageShell("Connection error", body);
|
|
301
742
|
}
|
|
302
743
|
app.get("/authorize", authLimiter, (req, res) => {
|
|
303
744
|
const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;
|
|
745
|
+
const cid = String(client_id ?? "");
|
|
746
|
+
const clientName = cid && registeredClients.has(cid) ? registeredClients.get(cid).client_name : void 0;
|
|
304
747
|
res.type("html").send(authorizeFormPage({
|
|
305
748
|
redirect_uri: String(redirect_uri ?? ""),
|
|
306
749
|
code_challenge: String(code_challenge ?? ""),
|
|
307
750
|
code_challenge_method: String(code_challenge_method ?? "S256"),
|
|
308
751
|
state: String(state ?? ""),
|
|
309
|
-
client_id:
|
|
752
|
+
client_id: cid,
|
|
753
|
+
client_name: clientName
|
|
310
754
|
}));
|
|
311
755
|
});
|
|
312
756
|
app.post(
|
|
@@ -315,6 +759,7 @@ app.post(
|
|
|
315
759
|
express.urlencoded({ extended: false }),
|
|
316
760
|
async (req, res) => {
|
|
317
761
|
const { api_key, redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;
|
|
762
|
+
const wantsJson = String(req.headers["accept"] ?? "").includes("application/json");
|
|
318
763
|
const retryParams = new URLSearchParams({
|
|
319
764
|
redirect_uri: redirect_uri ?? "",
|
|
320
765
|
code_challenge: code_challenge ?? "",
|
|
@@ -323,21 +768,44 @@ app.post(
|
|
|
323
768
|
...client_id ? { client_id } : {}
|
|
324
769
|
}).toString();
|
|
325
770
|
const retryUrl = `/authorize?${retryParams}`;
|
|
771
|
+
function sendError(title, trustedDetailHtml, status = 400) {
|
|
772
|
+
if (wantsJson) {
|
|
773
|
+
res.status(status).json({ ok: false, title, detail: trustedDetailHtml });
|
|
774
|
+
} else {
|
|
775
|
+
res.status(status).type("html").send(authorizeErrorPage(title, trustedDetailHtml, retryUrl));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
326
778
|
if (!api_key?.startsWith("pb_sk_")) {
|
|
327
|
-
|
|
779
|
+
sendError(
|
|
328
780
|
"Invalid key format",
|
|
329
|
-
"API keys start with <code>pb_sk_</code>. Check your key and try again."
|
|
330
|
-
|
|
331
|
-
));
|
|
781
|
+
"API keys start with <code>pb_sk_</code>. Check your key and try again."
|
|
782
|
+
);
|
|
332
783
|
return;
|
|
333
784
|
}
|
|
334
|
-
if (!client_id
|
|
785
|
+
if (!client_id) {
|
|
335
786
|
res.status(400).json({
|
|
336
787
|
error: "invalid_request",
|
|
337
|
-
error_description: "
|
|
788
|
+
error_description: "client_id is required"
|
|
338
789
|
});
|
|
339
790
|
return;
|
|
340
791
|
}
|
|
792
|
+
if (!registeredClients.has(client_id)) {
|
|
793
|
+
if (typeof redirect_uri === "string" && redirect_uri.startsWith("https://")) {
|
|
794
|
+
registeredClients.set(client_id, {
|
|
795
|
+
client_id,
|
|
796
|
+
redirect_uris: [redirect_uri],
|
|
797
|
+
registeredAt: Date.now()
|
|
798
|
+
});
|
|
799
|
+
process.stderr.write(`[authorize] auto-re-registered stale client_id after restart
|
|
800
|
+
`);
|
|
801
|
+
} else {
|
|
802
|
+
res.status(400).json({
|
|
803
|
+
error: "invalid_request",
|
|
804
|
+
error_description: "Unknown client_id and redirect_uri is not a valid https URL"
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
341
809
|
const client = registeredClients.get(client_id);
|
|
342
810
|
if (!client.redirect_uris.includes(redirect_uri)) {
|
|
343
811
|
res.status(400).json({
|
|
@@ -374,11 +842,11 @@ app.post(
|
|
|
374
842
|
}
|
|
375
843
|
if (!foundUrl) {
|
|
376
844
|
if (anyDefinitiveReject) {
|
|
377
|
-
|
|
845
|
+
sendError(
|
|
378
846
|
"Key not recognized",
|
|
379
847
|
"This API key wasn't found in Product Brain. Check your API Keys in Studio and try again.",
|
|
380
|
-
|
|
381
|
-
)
|
|
848
|
+
401
|
|
849
|
+
);
|
|
382
850
|
return;
|
|
383
851
|
}
|
|
384
852
|
process.stderr.write("[authorize] key-check unavailable \u2014 proceeding without validation\n");
|
|
@@ -399,7 +867,12 @@ app.post(
|
|
|
399
867
|
url.searchParams.set("code", code);
|
|
400
868
|
if (state) url.searchParams.set("state", state);
|
|
401
869
|
const redirectUrl = url.toString();
|
|
402
|
-
|
|
870
|
+
const providerName = providerDisplayName(client.client_name);
|
|
871
|
+
if (wantsJson) {
|
|
872
|
+
res.json({ ok: true, workspaceName, redirectUrl, providerName });
|
|
873
|
+
} else {
|
|
874
|
+
res.type("html").send(authorizeSuccessPage(workspaceName, redirectUrl, providerName));
|
|
875
|
+
}
|
|
403
876
|
}
|
|
404
877
|
);
|
|
405
878
|
function issueTokens(apiKey) {
|
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\n * HTTP transport entry point for Product Brain MCP.\n *\n * Serves the MCP protocol over Streamable HTTP for web clients\n * (Claude web app, API consumers) that can't spawn local processes.\n *\n * Implements the full MCP OAuth 2.1 spec (Nov 2025):\n * 1. Protected Resource Metadata (/.well-known/oauth-protected-resource)\n * 2. Authorization Server Metadata (/.well-known/oauth-authorization-server)\n * 3. Dynamic Client Registration (POST /register)\n * 4. Authorization Code + PKCE (GET/POST /authorize)\n * 5. Token Exchange (POST /oauth/token)\n *\n * Env:\n * CONVEX_SITE_URL — Convex deployment URL (defaults to cloud)\n * PORT / MCP_PORT — Listen port (default 3000)\n * CORS_ORIGINS — Comma-separated allowed origins (default: all)\n * PB_MODULES — Comma-separated modules (default: core,gitchain,arch)\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport express from \"express\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { isInitializeRequest } from \"@modelcontextprotocol/sdk/types.js\";\nimport rateLimit from \"express-rate-limit\";\n\nimport { bootstrapHttp, DEFAULT_CLOUD_URL } from \"./client.js\";\nimport { runWithAuth, hashKey, getKeyState } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\nfunction baseUrl(req: any): string {\n const proto = req.headers[\"x-forwarded-proto\"] ?? req.protocol ?? \"http\";\n const host = req.headers.host ?? `localhost:${PORT}`;\n return `${proto}://${host}`;\n}\n\n// ── Express App ─────────────────────────────────────────────────────────\n\nconst app = express();\n// Required when behind a reverse proxy (e.g. Railway): rate limiter uses X-Forwarded-For\n// and throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR if trust proxy is false.\napp.set(\"trust proxy\", 1);\napp.use(express.json());\n\n// CORS — fail-closed; requires CORS_ORIGINS to be explicitly configured.\nconst ALLOWED_ORIGINS = process.env.CORS_ORIGINS\n ?.split(\",\")\n .map((o) => o.trim())\n .filter(Boolean);\n\napp.use((_req: any, res: any, next: any) => {\n const origin = _req.headers.origin;\n if (ALLOWED_ORIGINS && origin && ALLOWED_ORIGINS.includes(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, Mcp-Session-Id, Last-Event-Id\",\n );\n res.setHeader(\"Access-Control-Expose-Headers\", \"Mcp-Session-Id\");\n if (_req.method === \"OPTIONS\") {\n res.status(204).end();\n return;\n }\n next();\n});\n\n// ── OAuth: Protected Resource Metadata (RFC 9728) ────────────────────────\n// Step 1 of MCP auth: Claude fetches this to discover the authorization server.\n\napp.get(\"/.well-known/oauth-protected-resource\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n resource: base,\n authorization_servers: [base],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n bearer_methods_supported: [\"header\"],\n });\n});\n\n// ── OAuth: Authorization Server Metadata (RFC 8414) ──────────────────────\n// Step 2: Claude fetches this to discover authorize, token, and register endpoints.\n\napp.get(\"/.well-known/oauth-authorization-server\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n issuer: base,\n authorization_endpoint: `${base}/authorize`,\n token_endpoint: `${base}/oauth/token`,\n registration_endpoint: `${base}/register`,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n });\n});\n\n// ── OAuth: Rate Limiting (Fix 2) ─────────────────────────────────────────\n// Separate, stricter limiter for auth endpoints to prevent brute-force and\n// enumeration attacks on the OAuth flow.\n\nconst authLimiter = rateLimit({\n windowMs: 60_000,\n max: 20,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many auth requests. Try again later.\" },\n});\n\n// ── OAuth: Dynamic Client Registration (RFC 7591) ────────────────────────\n// Step 3: Claude registers itself as a client before starting the auth flow.\n\ninterface RegisteredClient {\n client_id: string;\n redirect_uris: string[];\n client_name?: string;\n registeredAt: number;\n}\n\nconst registeredClients = new Map<string, RegisteredClient>();\n// Fix 4 — Cap client registrations at 500 to prevent unbounded memory growth.\nconst MAX_REGISTERED_CLIENTS = 500;\n\napp.post(\n \"/register\",\n authLimiter,\n express.json(),\n (req: any, res: any) => {\n // Fix 4 — Reject registration when cap is reached.\n if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {\n res.status(503).json({\n error: \"server_error\",\n error_description: \"Registration limit reached. Try again later.\",\n });\n return;\n }\n\n const { redirect_uris, client_name } = req.body;\n\n if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {\n res.status(400).json({\n error: \"invalid_client_metadata\",\n error_description: \"redirect_uris is required\",\n });\n return;\n }\n\n const clientId = `pb_client_${randomUUID()}`;\n const client: RegisteredClient = {\n client_id: clientId,\n redirect_uris,\n client_name,\n registeredAt: Date.now(),\n };\n registeredClients.set(clientId, client);\n\n res.status(201).json({\n client_id: clientId,\n client_name: client_name ?? \"MCP Client\",\n redirect_uris,\n grant_types: [\"authorization_code\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n });\n },\n);\n\n// ── OAuth: Authorization Code + PKCE ─────────────────────────────────────\n// Step 4: User enters their pb_sk_* key, server generates a one-time code.\n\ninterface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\nconst pendingCodes = new Map<string, PendingAuth>();\n\n// Refresh token store — declared here so the cleanup interval can reference it.\nconst ACCESS_TOKEN_TTL = 3600; // 1 hour\nconst ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1000;\nconst REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60_000; // 90 days\n\ninterface RefreshEntry {\n apiKey: string;\n createdAt: number;\n}\n\nconst refreshTokens = new Map<string, RefreshEntry>();\nconst MAX_REFRESH_TOKENS = 2000;\n// Per-key cap mirrors MAX_SESSIONS_PER_KEY (see ~line 524): prevents any single\n// API key from monopolising the global refresh-token budget. PR #34 review Finding 3.\nconst MAX_REFRESH_TOKENS_PER_KEY = 20;\n\n// Fix 1 — Opaque access token store.\n// Maps pb_at_<uuid> → { apiKey, createdAt } so the raw pb_sk_* key is never\n// exposed through the OAuth flow. Capped at 1000 entries with LRU eviction.\ninterface AccessTokenEntry {\n apiKey: string;\n createdAt: number;\n}\nconst accessTokens = new Map<string, AccessTokenEntry>();\nconst MAX_ACCESS_TOKENS = 1000;\n\nsetInterval(() => {\n const now = Date.now();\n for (const [code, auth] of pendingCodes) {\n if (now > auth.expiresAt) pendingCodes.delete(code);\n }\n for (const [id, client] of registeredClients) {\n if (now - client.registeredAt > 24 * 60 * 60_000) registeredClients.delete(id);\n }\n for (const [token, entry] of refreshTokens) {\n if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);\n }\n if (refreshTokens.size > MAX_REFRESH_TOKENS) {\n const sorted = [...refreshTokens.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);\n for (let i = 0; i < sorted.length - MAX_REFRESH_TOKENS; i++) {\n refreshTokens.delete(sorted[i][0]);\n }\n }\n // Fix 1 — Evict expired opaque access tokens.\n for (const [token, entry] of accessTokens) {\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);\n }\n // Fix 5 — Clean up stale auth failure tracking entries.\n for (const [ip, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(ip);\n }\n }\n // Cap authFailures map size.\n if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {\n const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);\n for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {\n authFailures.delete(sorted[i][0]);\n }\n }\n}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"'<>]/g, (c) =>\n ({ \"&\": \"&\", '\"': \""\", \"'\": \"'\", \"<\": \"<\", \">\": \">\" })[c]!,\n );\n}\n\n// ── Authorize Page Templates ─────────────────────────────────────────────\n// Zen-themed OAuth pages: warm dark palette, dot-grid background, subtle animations.\n\nfunction authPageShell(title: string, bodyContent: string, headExtra = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${title} — Product Brain</title>\n${headExtra}\n<style>\n:root{--bg:#0d0c10;--surface:#18161e;--border:#2d2840;--border-focus:#7c3aed;--text:#e8e4f0;--muted:#7b7590;--accent:#7c3aed;--accent-h:#6d28d9;--err:#f87171;--ok:#34d399}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:-apple-system,BlinkMacSystemFont,'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1.5rem}\nbody::before{content:'';position:fixed;inset:0;background-image:radial-gradient(circle,#3d3560 1px,transparent 1px);background-size:28px 28px;opacity:.22;pointer-events:none;z-index:0}\n.card{position:relative;z-index:1;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:2rem 2rem 1.75rem;max-width:380px;width:100%}\n</style>\n</head><body>\n<div class=\"card\">${bodyContent}</div>\n</body></html>`;\n}\n\nfunction authorizeFormPage(params: {\n redirect_uri: string;\n code_challenge: string;\n code_challenge_method: string;\n state: string;\n client_id: string;\n}): string {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;\n const body = `\n<style>\n.brand{display:flex;align-items:center;gap:.55rem;margin-bottom:1.5rem}\n.brand-icon{width:30px;height:30px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0}\n.brand-name{font-size:.88rem;font-weight:600;letter-spacing:-.01em}\nh1{font-size:1.3rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.35rem;line-height:1.3}\n.sub{font-size:.8rem;color:var(--muted);margin-bottom:1.75rem;line-height:1.55}\nlabel{display:block;font-size:.72rem;font-weight:600;color:#a89fc4;margin-bottom:.4rem;letter-spacing:.04em;text-transform:uppercase}\n.inp-wrap{position:relative}\ninput[type=password]{width:100%;padding:.65rem .85rem;background:var(--bg);border:1px solid var(--border);border-radius:10px;color:var(--text);font:.875rem/1.4 'JetBrains Mono','Fira Code',ui-monospace,monospace;outline:none;transition:border-color .15s,box-shadow .15s}\ninput:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,58,237,.15)}\ninput.err{border-color:var(--err);box-shadow:0 0 0 3px rgba(248,113,113,.12)}\n.field-row{display:flex;align-items:center;justify-content:space-between;margin-top:.4rem}\n.hint{font-size:.73rem;color:var(--muted)}\n.hint code{font-size:.72rem;background:rgba(255,255,255,.06);padding:.1rem .3rem;border-radius:4px}\n.get-key{font-size:.73rem;color:var(--accent);text-decoration:none;opacity:.85;transition:opacity .1s}\n.get-key:hover{opacity:1;text-decoration:underline}\n.err-msg{font-size:.73rem;color:var(--err);margin-top:.35rem;display:none}\n.err-msg.show{display:block}\n.btn{width:100%;padding:.7rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.88rem;font-weight:600;cursor:pointer;margin-top:1.25rem;letter-spacing:-.01em;transition:background .15s,transform .1s;display:flex;align-items:center;justify-content:center;gap:.5rem}\n.btn:hover{background:var(--accent-h)}\n.btn:active{transform:scale(.98)}\n.btn:disabled{opacity:.6;cursor:not-allowed;transform:none}\n.spinner{width:15px;height:15px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;display:none}\n@keyframes spin{to{transform:rotate(360deg)}}\n.divider{height:1px;background:var(--border);margin:1.5rem -2rem}\n.footer{font-size:.7rem;color:var(--muted);text-align:center;padding-top:.5rem;line-height:1.6}\n.footer a{color:var(--muted);text-decoration:underline}\n</style>\n<div class=\"brand\"><div class=\"brand-icon\">⬡</div><span class=\"brand-name\">Product Brain</span></div>\n<h1>Connect to Claude</h1>\n<p class=\"sub\">Enter your API key to give Claude access to your workspace.</p>\n<form method=\"POST\" action=\"/authorize\" id=\"f\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${esc(redirect_uri)}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${esc(code_challenge)}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${esc(code_challenge_method)}\">\n <input type=\"hidden\" name=\"state\" value=\"${esc(state)}\">\n <input type=\"hidden\" name=\"client_id\" value=\"${esc(client_id)}\">\n <label for=\"k\">API Key</label>\n <div class=\"inp-wrap\">\n <input type=\"password\" id=\"k\" name=\"api_key\" placeholder=\"pb_sk_…\" required autofocus autocomplete=\"off\">\n </div>\n <div class=\"field-row\">\n <span class=\"hint\">Starts with <code>pb_sk_</code></span>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"get-key\">No key? Get one →</a>\n </div>\n <p class=\"err-msg\" id=\"em\">Key must start with <code>pb_sk_</code></p>\n <button type=\"submit\" class=\"btn\" id=\"sb\">\n <span id=\"bt\">Authorize</span>\n <div class=\"spinner\" id=\"sp\"></div>\n </button>\n</form>\n<div class=\"divider\"></div>\n<p class=\"footer\">Product Brain gives Claude access to your knowledge graph.<br><a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\">Learn more</a></p>\n<script>\nvar f=document.getElementById('f'),k=document.getElementById('k'),em=document.getElementById('em'),sb=document.getElementById('sb'),bt=document.getElementById('bt'),sp=document.getElementById('sp');\nk.addEventListener('input',function(){var v=k.value.trim();if(v&&!v.startsWith('pb_sk_')){k.classList.add('err');em.classList.add('show')}else{k.classList.remove('err');em.classList.remove('show')}});\nf.addEventListener('submit',function(e){var v=k.value.trim();if(!v.startsWith('pb_sk_')){e.preventDefault();k.classList.add('err');em.classList.add('show');k.focus();return}sb.disabled=true;bt.textContent='Verifying…';sp.style.display='block'});\n</script>`;\n return authPageShell(\"Connect to Claude\", body);\n}\n\nfunction authorizeSuccessPage(workspaceName: string, redirectUrl: string): string {\n // Escape </script> sequences so a crafted redirect_uri can't break script context.\n const safeUrl = JSON.stringify(redirectUrl).replace(/<\\/script>/gi, \"<\\\\/script>\");\n const body = `\n<style>\n.card{text-align:center;padding:2.5rem 2rem}\n.ring{width:60px;height:60px;background:rgba(52,211,153,.1);border:1px solid rgba(52,211,153,.25);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.4rem;animation:pop .5s cubic-bezier(.175,.885,.32,1.275) .15s both}\n@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}\nsvg{animation:draw .6s ease .5s both}\n@keyframes draw{from{stroke-dashoffset:30}to{stroke-dashoffset:0}}\nh1{font-size:1.3rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.3rem}\n.ws{font-size:.8rem;color:var(--ok);font-weight:500;margin-bottom:1.5rem}\n.redirect-msg{font-size:.78rem;color:var(--muted);margin-bottom:1.4rem}\n.dots::after{content:'';animation:dots 1.4s steps(4,end) infinite}\n@keyframes dots{0%{content:''}25%{content:'.'}50%{content:'..'}75%{content:'...'}}\n.continue{display:inline-flex;align-items:center;gap:.4rem;padding:.6rem 1.2rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.84rem;font-weight:600;cursor:pointer;text-decoration:none;transition:background .15s}\n.continue:hover{background:var(--accent-h)}\n@keyframes fadein{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}\n.card{animation:fadein .35s ease}\n</style>\n<div class=\"ring\">\n <svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#34d399\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-dasharray=\"30\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n</div>\n<h1>Claude is connected</h1>\n<p class=\"ws\">${esc(workspaceName)}</p>\n<p class=\"redirect-msg\">Returning to Claude<span class=\"dots\"></span></p>\n<a href=\"${esc(redirectUrl)}\" class=\"continue\">Continue to Claude →</a>\n<script>setTimeout(function(){window.location.href=${safeUrl}},1800)</script>`;\n return authPageShell(\"Connected\", body);\n}\n\n// security: trustedDetailHtml must be a hardcoded literal — never pass user-controlled data.\nfunction authorizeErrorPage(title: string, trustedDetailHtml: string, retryUrl: string): string {\n const body = `\n<style>\n.card{text-align:center;padding:2.25rem 2rem}\n.x-ring{width:52px;height:52px;background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.25rem;font-size:1.4rem;animation:shake .4s ease .1s}\n@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}\nh1{font-size:1.2rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem;color:var(--err)}\n.detail{font-size:.8rem;color:var(--muted);line-height:1.6;margin-bottom:1.5rem}\n.detail code{font-size:.75rem;background:rgba(255,255,255,.06);padding:.1rem .3rem;border-radius:4px}\n.actions{display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}\n.btn-retry{display:inline-flex;align-items:center;padding:.55rem 1.1rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.82rem;font-weight:600;cursor:pointer;text-decoration:none;transition:background .15s}\n.btn-retry:hover{background:var(--accent-h)}\n.btn-key{display:inline-flex;align-items:center;padding:.55rem 1rem;background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:10px;font-size:.82rem;text-decoration:none;transition:border-color .15s,color .15s}\n.btn-key:hover{border-color:var(--accent);color:var(--text)}\n</style>\n<div class=\"x-ring\">✕</div>\n<h1>${esc(title)}</h1>\n<p class=\"detail\">${trustedDetailHtml}</p>\n<div class=\"actions\">\n <a href=\"${esc(retryUrl)}\" class=\"btn-retry\">← Try again</a>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-key\">Get an API key →</a>\n</div>`;\n return authPageShell(\"Connection error\", body);\n}\n\napp.get(\"/authorize\", authLimiter, (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;\n res.type(\"html\").send(authorizeFormPage({\n redirect_uri: String(redirect_uri ?? \"\"),\n code_challenge: String(code_challenge ?? \"\"),\n code_challenge_method: String(code_challenge_method ?? \"S256\"),\n state: String(state ?? \"\"),\n client_id: String(client_id ?? \"\"),\n }));\n});\n\napp.post(\n \"/authorize\",\n authLimiter,\n express.urlencoded({ extended: false }),\n async (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;\n\n // Build \"retry\" URL so error pages can link back to the form.\n const retryParams = new URLSearchParams({\n redirect_uri: redirect_uri ?? \"\",\n code_challenge: code_challenge ?? \"\",\n code_challenge_method: code_challenge_method ?? \"S256\",\n ...(state ? { state } : {}),\n ...(client_id ? { client_id } : {}),\n }).toString();\n const retryUrl = `/authorize?${retryParams}`;\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n res.type(\"html\").send(authorizeErrorPage(\n \"Invalid key format\",\n \"API keys start with <code>pb_sk_</code>. Check your key and try again.\",\n retryUrl,\n ));\n return;\n }\n\n // Validate redirect_uri against the registered client's allowed redirects.\n // Open redirect prevention: never trust a request-supplied redirect_uri without\n // checking it was pre-registered during dynamic client registration (RFC 7591).\n if (!client_id || !registeredClients.has(client_id)) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"Unknown or missing client_id\",\n });\n return;\n }\n\n const client = registeredClients.get(client_id)!;\n if (!client.redirect_uris.includes(redirect_uri)) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"redirect_uri does not match any registered redirect for this client\",\n });\n return;\n }\n\n // Validate key against Convex before issuing the code.\n // DEC-789 S2: probe primary then fallback URLs so DEV keys work against PROD Railway MCP.\n let workspaceName = \"Your Workspace\";\n try {\n const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\\/$/, \"\");\n const fallbackUrls = (process.env.CONVEX_FALLBACK_URLS ?? \"\")\n .split(\",\").map((u) => u.trim().replace(/\\/$/, \"\")).filter(Boolean);\n const candidates = [primaryUrl, ...fallbackUrls];\n\n let foundUrl: string | undefined;\n let anyDefinitiveReject = false;\n\n for (const url of candidates) {\n let checkData: { ok: boolean; workspaceName?: string; deploymentUrl?: string } | null = null;\n try {\n const checkRes = await fetch(`${url}/api/key-check`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${api_key}`, \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(5000),\n });\n checkData = await checkRes.json() as { ok: boolean; workspaceName?: string; deploymentUrl?: string };\n } catch {\n // This candidate unreachable — try next.\n continue;\n }\n if (checkData.ok) {\n if (checkData.workspaceName) workspaceName = checkData.workspaceName;\n foundUrl = checkData.deploymentUrl ?? url;\n break;\n }\n anyDefinitiveReject = true;\n }\n\n if (!foundUrl) {\n if (anyDefinitiveReject) {\n res.type(\"html\").send(authorizeErrorPage(\n \"Key not recognized\",\n \"This API key wasn't found in Product Brain. Check your API Keys in Studio and try again.\",\n retryUrl,\n ));\n return;\n }\n // All candidates unreachable (network errors only) — fail-open.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n } else {\n getKeyState(api_key).deploymentUrl = foundUrl;\n }\n } catch {\n // Outer guard: fail-open so auth works even if the entire block throws.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n }\n\n const code = randomUUID();\n pendingCodes.set(code, {\n apiKey: api_key,\n codeChallenge: code_challenge,\n redirectUri: redirect_uri,\n expiresAt: Date.now() + 5 * 60_000,\n });\n\n const url = new URL(redirect_uri);\n url.searchParams.set(\"code\", code);\n if (state) url.searchParams.set(\"state\", state);\n const redirectUrl = url.toString();\n\n // Return a success page that auto-redirects to Claude after 1.8s.\n res.type(\"html\").send(authorizeSuccessPage(workspaceName, redirectUrl));\n },\n);\n\n// ── OAuth: Token Exchange ────────────────────────────────────────────────\n// Step 5: Claude exchanges the authorization code (with PKCE verifier) for a token.\n// Supports both authorization_code and refresh_token grants.\n\nfunction issueTokens(apiKey: string): object {\n // Return the pb_sk_* key directly as the access_token so connections\n // survive server restarts. Railway deploys on every git push, wiping\n // in-memory Maps. pb_sk_* keys are long-lived (valid until explicitly\n // revoked in Studio); actual validity is enforced by Convex per tool call.\n // extractBearerKey() already handles pb_sk_* with zero Map lookup.\n //\n // NOTE: pb_at_* tokens issued before this change are still resolved by\n // the accessTokens Map for backward compat (until clients re-auth).\n const now = Date.now();\n\n const refreshToken = `pb_rt_${randomUUID()}`;\n\n // Per-key cap (Finding 3, PR #34): before the global backstop kicks in, ensure\n // no single apiKey holds more than MAX_REFRESH_TOKENS_PER_KEY entries. Mirrors\n // MAX_SESSIONS_PER_KEY. Prevents one abusive/leaked key from evicting every\n // other tenant's refresh tokens via the global FIFO backstop below.\n let perKeyCount = 0;\n let oldestKeyForApiKey: string | null = null;\n let oldestAtForApiKey = Infinity;\n for (const [k, v] of refreshTokens) {\n if (v.apiKey === apiKey) {\n perKeyCount++;\n if (v.createdAt < oldestAtForApiKey) {\n oldestAtForApiKey = v.createdAt;\n oldestKeyForApiKey = k;\n }\n }\n }\n if (perKeyCount >= MAX_REFRESH_TOKENS_PER_KEY && oldestKeyForApiKey) {\n refreshTokens.delete(oldestKeyForApiKey);\n }\n\n // Global backstop — unchanged behaviour.\n if (refreshTokens.size >= MAX_REFRESH_TOKENS) {\n let oldestKey: string | null = null;\n let oldestAt = Infinity;\n for (const [k, v] of refreshTokens) {\n if (v.createdAt < oldestAt) {\n oldestAt = v.createdAt;\n oldestKey = k;\n }\n }\n if (oldestKey) refreshTokens.delete(oldestKey);\n }\n refreshTokens.set(refreshToken, { apiKey, createdAt: now });\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n // 1-year TTL: actual validity enforced by Convex, not by expiry clock.\n // Long TTL prevents unnecessary refresh cycles after restarts.\n expires_in: 365 * 24 * 3600,\n refresh_token: refreshToken,\n };\n}\n\napp.post(\n \"/oauth/token\",\n authLimiter,\n express.urlencoded({ extended: false }),\n express.json(),\n (req: any, res: any) => {\n const { grant_type, code, code_verifier, redirect_uri, refresh_token } =\n req.body;\n\n if (grant_type === \"refresh_token\") {\n const entry = refreshTokens.get(refresh_token);\n if (!entry) {\n res.status(400).json({ error: \"invalid_grant\", error_description: \"Invalid refresh token\" });\n return;\n }\n if (Date.now() - entry.createdAt > REFRESH_TOKEN_TTL_MS) {\n refreshTokens.delete(refresh_token);\n res.status(400).json({ error: \"invalid_grant\", error_description: \"Refresh token expired\" });\n return;\n }\n // Rotate: revoke old, issue new pair\n const apiKey = entry.apiKey;\n refreshTokens.delete(refresh_token);\n res.json(issueTokens(apiKey));\n return;\n }\n\n if (grant_type !== \"authorization_code\") {\n res.status(400).json({ error: \"unsupported_grant_type\" });\n return;\n }\n\n const pending = pendingCodes.get(code);\n if (!pending || pending.redirectUri !== redirect_uri) {\n res.status(400).json({ error: \"invalid_grant\" });\n return;\n }\n\n // PKCE S256 validation\n const challenge = createHash(\"sha256\")\n .update(code_verifier ?? \"\")\n .digest(\"base64url\");\n if (challenge !== pending.codeChallenge) {\n pendingCodes.delete(code);\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"PKCE verification failed\",\n });\n return;\n }\n\n pendingCodes.delete(code);\n res.json(issueTokens(pending.apiKey));\n },\n);\n\n// ── Rate Limiting ────────────────────────────────────────────────────────\n\nconst mcpLimiter = rateLimit({\n windowMs: 60_000,\n max: 120,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many requests. Try again later.\" },\n});\n\n// ── Auth Failure Backoff (Fix 5) ──────────────────────────────────────────\n// Per-IP progressive lockout for failed API key auth attempts to prevent\n// brute-force attacks through the MCP endpoints.\n\ninterface AuthFailureRecord {\n count: number;\n firstFailure: number;\n blockedUntil: number;\n}\n\nconst authFailures = new Map<string, AuthFailureRecord>();\nconst AUTH_FAILURE_MAX = 10;\nconst AUTH_FAILURE_WINDOW_MS = 5 * 60_000; // 5 minutes\nconst AUTH_BLOCK_DURATION_MS = 15 * 60_000; // 15 minutes\nconst MAX_AUTH_FAILURE_ENTRIES = 10_000;\n\nfunction checkAuthBlock(ip: string): boolean {\n const rec = authFailures.get(ip);\n if (!rec) return false;\n return rec.blockedUntil > Date.now();\n}\n\nfunction recordAuthFailure(ip: string): void {\n const now = Date.now();\n const rec = authFailures.get(ip);\n\n if (!rec) {\n authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });\n return;\n }\n\n // Reset window if the first failure is outside the tracking window.\n if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {\n rec.count = 1;\n rec.firstFailure = now;\n rec.blockedUntil = 0;\n } else {\n rec.count++;\n if (rec.count >= AUTH_FAILURE_MAX) {\n rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;\n }\n }\n}\n\n// ── Health Check ─────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: any, res: any) => {\n res.json({ status: \"ok\", version: SERVER_VERSION, transport: \"http\" });\n});\n\n// ── Session Management ──────────────────────────────────────────────────\n\ninterface SessionEntry {\n transport: StreamableHTTPServerTransport;\n lastAccess: number;\n // Fix 3 — short hash of the API key that created this session. Used to\n // detect session hijacking when subsequent requests arrive with a different key.\n keyHash: string;\n}\n\nconst sessions = new Map<string, SessionEntry>();\nconst SESSION_TTL_MS = 30 * 60 * 1000;\nconst MAX_SESSIONS = 200;\n// Fix 6 — prevent a single API key from monopolising all session slots.\nconst MAX_SESSIONS_PER_KEY = 5;\n\nfunction evictStaleSessions(): void {\n const now = Date.now();\n for (const [id, entry] of sessions) {\n if (now - entry.lastAccess > SESSION_TTL_MS) {\n logSessionLifecycle(\"session_deleted\", id, \"ttl\");\n entry.transport.close().catch(() => {});\n sessions.delete(id);\n }\n }\n if (sessions.size > MAX_SESSIONS) {\n const sorted = [...sessions.entries()].sort(\n (a, b) => a[1].lastAccess - b[1].lastAccess,\n );\n for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {\n logSessionLifecycle(\"session_deleted\", sorted[i][0], \"eviction\");\n sorted[i][1].transport.close().catch(() => {});\n sessions.delete(sorted[i][0]);\n }\n }\n}\n\nsetInterval(evictStaleSessions, 60_000);\n\n// ── Auth Helpers ─────────────────────────────────────────────────────────\n\nfunction extractBearerKey(req: any): string | null {\n const header = req.headers?.authorization;\n if (typeof header !== \"string\" || !header.startsWith(\"Bearer \")) return null;\n const token = header.slice(7).trim();\n\n // Fix 1 — Support both direct API keys (stdio/backward compat) and opaque\n // OAuth access tokens issued by issueTokens().\n if (token.startsWith(\"pb_sk_\")) {\n // Direct API key — accepted for stdio and backward compatibility.\n return token;\n }\n if (token.startsWith(\"pb_at_\")) {\n // Opaque OAuth access token — resolve to the underlying API key.\n const entry = accessTokens.get(token);\n if (!entry) return null;\n const now = Date.now();\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {\n // Expired — remove and reject.\n accessTokens.delete(token);\n return null;\n }\n return entry.apiKey;\n }\n return null;\n}\n\nfunction send401(req: any, res: any): void {\n const base = baseUrl(req);\n res\n .status(401)\n .set(\n \"WWW-Authenticate\",\n `Bearer resource_metadata=\"${base}/.well-known/oauth-protected-resource\"`,\n )\n .json({ error: \"unauthorized\" });\n}\n\nfunction logRequest(\n method: string,\n outcome: \"ok\" | \"auth_fail\" | \"error\",\n sessionId?: string,\n durationMs?: number,\n): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n const dur = durationMs != null ? ` duration=${durationMs}ms` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}\\n`);\n}\n\nfunction logSessionLifecycle(\n event: \"session_created\" | \"session_deleted\",\n sessionId: string,\n reason?: \"ttl\" | \"eviction\" | \"onclose\",\n): void {\n const ts = new Date().toISOString();\n const r = reason ? ` reason=${reason}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}\\n`);\n}\n\n// ── MCP Handlers ────────────────────────────────────────────────────────\n\napp.post(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"POST\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const reqStart = Date.now();\n\n try {\n await runWithAuth({ apiKey }, async () => {\n if (sessionId && sessions.has(sessionId)) {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", sessionId, Date.now() - reqStart);\n } else if (!sessionId && isInitializeRequest(req.body)) {\n // Fix 6 — Enforce per-key session cap before creating a new session.\n const keyH = hashKey(apiKey);\n let keySessionCount = 0;\n for (const entry of sessions.values()) {\n if (entry.keyHash === keyH) keySessionCount++;\n }\n if (keySessionCount >= MAX_SESSIONS_PER_KEY) {\n res.status(429).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Too many sessions for this API key\" },\n id: null,\n });\n return;\n }\n\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (sid: string) => {\n // Fix 3 — Store a key hash with the session entry.\n sessions.set(sid, { transport, lastAccess: Date.now(), keyHash: keyH });\n logSessionLifecycle(\"session_created\", sid);\n },\n });\n\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) {\n logSessionLifecycle(\"session_deleted\", sid, \"onclose\");\n sessions.delete(sid);\n }\n };\n\n const server = createProductBrainServer();\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", transport.sessionId ?? undefined, Date.now() - reqStart);\n } else {\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)\\n`,\n );\n res.status(400).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Bad Request: no valid session ID provided\" },\n id: null,\n });\n }\n });\n } catch (err: any) {\n logRequest(\"POST\", \"error\", sessionId, Date.now() - reqStart);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n }\n});\n\napp.get(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"GET\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).send(\"Invalid or missing session ID\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res);\n logRequest(\"GET\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"GET\", \"error\", sessionId);\n }\n});\n\napp.delete(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"DELETE\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).send(\"Invalid or missing session ID\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n await entry.transport.handleRequest(req, res);\n logRequest(\"DELETE\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"DELETE\", \"error\", sessionId);\n }\n});\n\n// ── Start ───────────────────────────────────────────────────────────────\n\nprocess.on(\"unhandledRejection\", (reason) => {\n const msg = reason instanceof Error ? reason.message : String(reason);\n console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);\n});\n\nprocess.on(\"uncaughtException\", (err) => {\n console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);\n gracefulShutdown();\n});\n\nlet shuttingDown = false;\nasync function gracefulShutdown() {\n if (shuttingDown) return;\n shuttingDown = true;\n setTimeout(() => process.exit(1), 3_000).unref();\n console.log(\"Shutting down...\");\n for (const [, entry] of sessions) {\n await entry.transport.close().catch(() => {});\n }\n try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\n// Bind all interfaces — Railway/Cloudflare reach the container on its non-loopback IP.\n// Loopback-only (127.0.0.1) causes edge 502: the proxy never connects to localhost inside the pod.\nconst LISTEN_HOST = \"0.0.0.0\";\nconst httpServer = app.listen(PORT, LISTEN_HOST, () => {\n console.log(\n `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`,\n );\n});\nhttpServer.on(\"error\", (err) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n"],"mappings":"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","names":["url"]}
|
|
1
|
+
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\n * HTTP transport entry point for Product Brain MCP.\n *\n * Serves the MCP protocol over Streamable HTTP for web clients\n * (Claude web app, API consumers) that can't spawn local processes.\n *\n * Implements the full MCP OAuth 2.1 spec (Nov 2025):\n * 1. Protected Resource Metadata (/.well-known/oauth-protected-resource)\n * 2. Authorization Server Metadata (/.well-known/oauth-authorization-server)\n * 3. Dynamic Client Registration (POST /register)\n * 4. Authorization Code + PKCE (GET/POST /authorize)\n * 5. Token Exchange (POST /oauth/token)\n *\n * Env:\n * CONVEX_SITE_URL — Convex deployment URL (defaults to cloud)\n * PORT / MCP_PORT — Listen port (default 3000)\n * CORS_ORIGINS — Comma-separated allowed origins (default: all)\n * PB_MODULES — Comma-separated modules (default: core,gitchain,arch)\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport express from \"express\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { isInitializeRequest } from \"@modelcontextprotocol/sdk/types.js\";\nimport rateLimit from \"express-rate-limit\";\n\nimport { bootstrapHttp, DEFAULT_CLOUD_URL } from \"./client.js\";\nimport { runWithAuth, hashKey, getKeyState } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\nfunction baseUrl(req: any): string {\n const proto = req.headers[\"x-forwarded-proto\"] ?? req.protocol ?? \"http\";\n const host = req.headers.host ?? `localhost:${PORT}`;\n return `${proto}://${host}`;\n}\n\n// ── Express App ─────────────────────────────────────────────────────────\n\nconst app = express();\n// Required when behind a reverse proxy (e.g. Railway): rate limiter uses X-Forwarded-For\n// and throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR if trust proxy is false.\napp.set(\"trust proxy\", 1);\napp.use(express.json());\n\n// CORS — defaults to https://claude.ai (per REMOTE.md). Override via CORS_ORIGINS env var.\nconst ALLOWED_ORIGINS = (process.env.CORS_ORIGINS ?? \"https://claude.ai\")\n .split(\",\")\n .map((o) => o.trim())\n .filter(Boolean);\n\napp.use((_req: any, res: any, next: any) => {\n const origin = _req.headers.origin;\n if (origin && ALLOWED_ORIGINS.includes(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, Mcp-Session-Id, Last-Event-Id\",\n );\n res.setHeader(\"Access-Control-Expose-Headers\", \"Mcp-Session-Id\");\n if (_req.method === \"OPTIONS\") {\n res.status(204).end();\n return;\n }\n next();\n});\n\n// ── OAuth: Protected Resource Metadata (RFC 9728) ────────────────────────\n// Step 1 of MCP auth: Claude fetches this to discover the authorization server.\n\napp.get(\"/.well-known/oauth-protected-resource\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n resource: base,\n authorization_servers: [base],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n bearer_methods_supported: [\"header\"],\n });\n});\n\n// ── OAuth: Authorization Server Metadata (RFC 8414) ──────────────────────\n// Step 2: Claude fetches this to discover authorize, token, and register endpoints.\n\napp.get(\"/.well-known/oauth-authorization-server\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n issuer: base,\n authorization_endpoint: `${base}/authorize`,\n token_endpoint: `${base}/oauth/token`,\n registration_endpoint: `${base}/register`,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n });\n});\n\n// ── OAuth: Rate Limiting (Fix 2) ─────────────────────────────────────────\n// Separate, stricter limiter for auth endpoints to prevent brute-force and\n// enumeration attacks on the OAuth flow.\n\nconst authLimiter = rateLimit({\n windowMs: 60_000,\n max: 20,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many auth requests. Try again later.\" },\n});\n\n// ── OAuth: Dynamic Client Registration (RFC 7591) ────────────────────────\n// Step 3: Claude registers itself as a client before starting the auth flow.\n\ninterface RegisteredClient {\n client_id: string;\n redirect_uris: string[];\n client_name?: string;\n registeredAt: number;\n}\n\nconst registeredClients = new Map<string, RegisteredClient>();\n// Fix 4 — Cap client registrations at 500 to prevent unbounded memory growth.\nconst MAX_REGISTERED_CLIENTS = 500;\n\napp.post(\n \"/register\",\n authLimiter,\n express.json(),\n (req: any, res: any) => {\n // Fix 4 — Reject registration when cap is reached.\n if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {\n res.status(503).json({\n error: \"server_error\",\n error_description: \"Registration limit reached. Try again later.\",\n });\n return;\n }\n\n const { redirect_uris, client_name } = req.body;\n\n if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {\n res.status(400).json({\n error: \"invalid_client_metadata\",\n error_description: \"redirect_uris is required\",\n });\n return;\n }\n\n const clientId = `pb_client_${randomUUID()}`;\n const client: RegisteredClient = {\n client_id: clientId,\n redirect_uris,\n client_name,\n registeredAt: Date.now(),\n };\n registeredClients.set(clientId, client);\n\n res.status(201).json({\n client_id: clientId,\n client_name: client_name ?? \"MCP Client\",\n redirect_uris,\n grant_types: [\"authorization_code\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n });\n },\n);\n\n// ── OAuth: Authorization Code + PKCE ─────────────────────────────────────\n// Step 4: User enters their pb_sk_* key, server generates a one-time code.\n\ninterface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\nconst pendingCodes = new Map<string, PendingAuth>();\n\n// Refresh token store — declared here so the cleanup interval can reference it.\nconst ACCESS_TOKEN_TTL = 3600; // 1 hour\nconst ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1000;\nconst REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60_000; // 90 days\n\ninterface RefreshEntry {\n apiKey: string;\n createdAt: number;\n}\n\nconst refreshTokens = new Map<string, RefreshEntry>();\nconst MAX_REFRESH_TOKENS = 2000;\n// Per-key cap mirrors MAX_SESSIONS_PER_KEY (see ~line 524): prevents any single\n// API key from monopolising the global refresh-token budget. PR #34 review Finding 3.\nconst MAX_REFRESH_TOKENS_PER_KEY = 20;\n\n// Fix 1 — Opaque access token store.\n// Maps pb_at_<uuid> → { apiKey, createdAt } so the raw pb_sk_* key is never\n// exposed through the OAuth flow. Capped at 1000 entries with LRU eviction.\ninterface AccessTokenEntry {\n apiKey: string;\n createdAt: number;\n}\nconst accessTokens = new Map<string, AccessTokenEntry>();\nconst MAX_ACCESS_TOKENS = 1000;\n\nsetInterval(() => {\n const now = Date.now();\n for (const [code, auth] of pendingCodes) {\n if (now > auth.expiresAt) pendingCodes.delete(code);\n }\n for (const [id, client] of registeredClients) {\n if (now - client.registeredAt > 24 * 60 * 60_000) registeredClients.delete(id);\n }\n for (const [token, entry] of refreshTokens) {\n if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);\n }\n if (refreshTokens.size > MAX_REFRESH_TOKENS) {\n const sorted = [...refreshTokens.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);\n for (let i = 0; i < sorted.length - MAX_REFRESH_TOKENS; i++) {\n refreshTokens.delete(sorted[i][0]);\n }\n }\n // Fix 1 — Evict expired opaque access tokens.\n for (const [token, entry] of accessTokens) {\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);\n }\n // Fix 5 — Clean up stale auth failure tracking entries.\n for (const [ip, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(ip);\n }\n }\n // Cap authFailures map size.\n if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {\n const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);\n for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {\n authFailures.delete(sorted[i][0]);\n }\n }\n}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"'<>]/g, (c) =>\n ({ \"&\": \"&\", '\"': \""\", \"'\": \"'\", \"<\": \"<\", \">\": \">\" })[c]!,\n );\n}\n\n// ── Authorize Page Templates ─────────────────────────────────────────────\n// Parchment-dark OAuth pages — brand tokens from DEC-419 (brand-tokens.json).\n// Monochrome-first per DEC-417, warm temperature per DEC-418.\n// Provider-agnostic: copy interpolates registered client_name (RFC 7591),\n// never hardcodes \"Claude\" — any MCP client may reach this page.\n\nfunction authPageShell(title: string, bodyContent: string, headExtra = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"parchment-dark\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${esc(title)} — Product Brain</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,600&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;700&display=swap\">\n${headExtra}\n<style>\n:root{\n --bg:#1a1917;--bg-warm:#201f1c;--surface:#262521;\n --fg1:#e4e0d8;--fg2:#c4bfb4;--fg3:#9a9589;--fg4:#6a6560;--fg-bright:#ffffff;\n --border:rgba(255,255,255,0.07);--border-light:rgba(255,255,255,0.04);\n --accent:#c9b99a;\n --btn-bg:#ffffff;--btn-fg:#1a1917;--btn-hover:#e4e0d8;\n --green:#4ade80;--rose:#ef4444;\n --ghost:rgba(38,37,33,0.55);\n --radius-md:7px;--radius-lg:10px;\n --font-display:\"Source Serif 4\",ui-serif,Georgia,serif;\n --font-body:\"IBM Plex Sans\",ui-sans-serif,system-ui,sans-serif;\n --font-mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Menlo,monospace;\n}\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%}\nbody{\n font-family:var(--font-body);font-size:13px;line-height:1.45;\n color:var(--fg1);background:var(--bg);\n min-height:100vh;display:grid;place-items:center;padding:24px;\n -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;\n position:relative;overflow:hidden;\n}\nbody::before{\n content:\"\";position:fixed;inset:0;\n background:radial-gradient(900px 600px at 50% 50%,rgba(228,224,216,0.025),transparent 60%);\n pointer-events:none;z-index:0;\n}\n.top-mark{\n position:fixed;top:22px;left:24px;display:flex;align-items:center;gap:8px;\n z-index:5;opacity:0.7;\n}\n.top-mark .m{\n width:16px;height:16px;border-radius:4px;background:#1c1e24;\n border:1px solid rgba(255,255,255,0.06);display:inline-grid;place-items:center;\n}\n.top-mark .m .core{width:5px;height:5px;border-radius:50%;background:var(--accent)}\n.top-mark .name{\n font-family:var(--font-mono);font-size:10.5px;letter-spacing:0.22em;\n text-transform:uppercase;color:var(--fg4);font-weight:500;\n}\n.stage{\n width:100%;max-width:460px;text-align:center;\n position:relative;z-index:1;\n display:grid;\n}\n.panel{\n grid-area:1/1;\n transition:opacity 280ms ease-out,transform 380ms cubic-bezier(.2,.6,.2,1),filter 380ms ease-out;\n}\n.panel[hidden]{\n display:block !important;\n opacity:0;transform:scale(0.96) translateY(-2px);filter:blur(6px);\n pointer-events:none;\n}\n.panel:not([hidden]){opacity:1;transform:scale(1);filter:none;pointer-events:auto}\n\n/* eyebrows */\n.eyebrow{\n font-family:var(--font-mono);font-size:10px;font-weight:700;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg4);\n margin-bottom:24px;display:inline-flex;align-items:center;gap:7px;\n}\n.eyebrow .dot{width:5px;height:5px;border-radius:50%;background:currentColor}\n.eyebrow.danger{color:var(--rose)}\n.eyebrow.success{color:var(--green)}\n.eyebrow.success .dot{box-shadow:0 0 0 3px rgba(74,222,128,0.18)}\n\n/* form input */\n.input-wrap{\n display:flex;align-items:center;background:rgba(0,0,0,0.22);\n border:1px solid var(--border);border-radius:var(--radius-md);\n transition:border-color 200ms ease-out,box-shadow 200ms ease-out;\n}\n.input-wrap:focus-within{\n border-color:rgba(228,224,216,0.45);\n box-shadow:0 0 0 3px rgba(228,224,216,0.10);\n}\n.input-wrap.has-error{\n border-color:rgba(239,68,68,0.55);\n box-shadow:0 0 0 3px rgba(239,68,68,0.12);\n animation:shake 360ms cubic-bezier(.36,.07,.19,.97);\n}\n@keyframes shake{\n 10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}\n 30%,50%,70%{transform:translateX(-4px)}40%,60%{transform:translateX(4px)}\n}\n.input-prefix{\n font-family:var(--font-mono);font-size:14px;color:var(--fg3);\n padding-left:16px;user-select:none;\n}\n.input{\n flex:1;min-width:0;background:transparent;border:0;outline:none;\n padding:16px 14px 16px 4px;\n font-family:var(--font-mono);font-size:14px;color:var(--fg1);letter-spacing:0.02em;\n}\n.input::placeholder{color:var(--fg4)}\n.hint{\n margin-top:10px;font-family:var(--font-mono);font-size:10.5px;\n letter-spacing:0.16em;text-transform:uppercase;\n text-align:left;padding-left:4px;height:14px;color:var(--fg4);\n transition:color 160ms ease-out;\n}\n.hint.is-error{color:var(--rose)}\n\n/* primary button */\n.btn-primary{\n width:100%;height:48px;margin-top:14px;\n border:0;border-radius:var(--radius-md);\n background:var(--btn-bg);color:var(--btn-fg);\n font-family:var(--font-body);font-size:14.5px;font-weight:600;letter-spacing:-0.005em;\n cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:10px;\n transition:background 140ms ease-out,transform 80ms ease-out,opacity 200ms ease-out;\n position:relative;overflow:hidden;\n}\n.btn-primary:hover:not([disabled]){background:var(--btn-hover)}\n.btn-primary:active:not([disabled]){transform:translateY(1px)}\n.btn-primary[disabled]{opacity:0.45;cursor:default}\n.spin{\n width:14px;height:14px;border-radius:50%;\n border:1.5px solid currentColor;border-top-color:transparent;\n animation:spin 700ms linear infinite;opacity:0.85;\n}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n/* secondary link */\n.small-link{\n margin-top:18px;font-family:var(--font-mono);font-size:11px;\n letter-spacing:0.18em;text-transform:uppercase;color:var(--fg4);\n}\n.small-link a{\n color:var(--fg3);text-decoration:none;border-bottom:1px dotted currentColor;\n padding-bottom:1px;transition:color 140ms;\n}\n.small-link a:hover{color:var(--fg1)}\n\n/* orb */\n.orb-wrap{\n position:relative;width:160px;height:160px;margin:0 auto 36px;\n display:grid;place-items:center;\n}\n.orb-ring{position:absolute;border-radius:50%}\n.orb-ring.r1{inset:0;border:1px solid rgba(228,224,216,0.06);animation:drift1 40s linear infinite}\n.orb-ring.r2{inset:18px;border:1px dashed rgba(228,224,216,0.10);animation:drift2 28s linear infinite}\n.orb-ring.r3{inset:38px;border:1px solid rgba(74,222,128,0.20);animation:ringPulse 3.4s ease-in-out infinite}\n@keyframes drift1{to{transform:rotate(360deg)}}\n@keyframes drift2{to{transform:rotate(-360deg)}}\n@keyframes ringPulse{0%,100%{opacity:0.6}50%{opacity:1}}\n\n.orb-wrap.is-verifying .orb-ring.r3{\n border-color:rgba(228,224,216,0.20);\n animation:ringPulse 1.1s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-core{\n box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06);\n animation:corePulseNeutral 1.6s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-dot{background:var(--fg3);box-shadow:0 0 10px rgba(228,224,216,0.4)}\n@keyframes corePulseNeutral{\n 0%,100%{box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(228,224,216,0.07),0 0 32px rgba(228,224,216,0.18),inset 0 0 22px rgba(228,224,216,0.10)}\n}\n\n.orb-wrap.is-error .orb-ring.r3{border-color:rgba(239,68,68,0.30);animation:none;opacity:1}\n.orb-wrap.is-error .orb-ring.r1,.orb-wrap.is-error .orb-ring.r2{animation-play-state:paused}\n.orb-wrap.is-error .orb-core{\n box-shadow:0 0 0 6px rgba(239,68,68,0.05),0 0 22px rgba(239,68,68,0.20),inset 0 0 16px rgba(239,68,68,0.08);\n animation:none;\n}\n.orb-wrap.is-error .orb-dot{background:var(--rose);box-shadow:0 0 10px rgba(239,68,68,0.6)}\n\n.sat-orbit{position:absolute;inset:0;pointer-events:none}\n.sat-orbit.o1{animation:drift1 40s linear infinite}\n.sat-orbit.o2{animation:drift2 56s linear infinite}\n.sat{\n position:absolute;top:50%;left:50%;\n font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.14em;\n background:var(--bg);padding:2px 6px;border-radius:3px;\n border:1px solid var(--border-light);\n transform-origin:0 0;opacity:0;\n}\n.panel:not([hidden])[data-state=\"connected\"] .sat{animation:satIn 600ms ease-out forwards}\n@keyframes satIn{from{opacity:0}to{opacity:1}}\n.sat span{display:inline-block;animation:counter 40s linear infinite}\n.sat-orbit.o2 .sat span{animation:counter2 56s linear infinite}\n@keyframes counter{to{transform:rotate(-360deg)}}\n@keyframes counter2{to{transform:rotate(360deg)}}\n\n.orb-core{\n position:relative;width:60px;height:60px;border-radius:50%;\n background:radial-gradient(circle at 50% 45%,#1a1a1a 0%,#0c0c0c 60%,#050505 100%);\n border:1px solid rgba(255,255,255,0.06);\n display:grid;place-items:center;\n box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 28px rgba(74,222,128,0.20),inset 0 0 18px rgba(74,222,128,0.08);\n animation:corePulse 3.4s ease-in-out infinite;\n}\n@keyframes corePulse{\n 0%,100%{box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 24px rgba(74,222,128,0.18),inset 0 0 16px rgba(74,222,128,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(74,222,128,0.06),0 0 40px rgba(74,222,128,0.32),inset 0 0 22px rgba(74,222,128,0.14)}\n}\n.orb-dot{width:10px;height:10px;border-radius:50%;background:#4ade80;box-shadow:0 0 12px rgba(74,222,128,0.7)}\n\n.core-shockwave{\n position:absolute;inset:0;border-radius:50%;\n border:1px solid rgba(74,222,128,0.6);\n opacity:0;pointer-events:none;\n}\n.panel:not([hidden])[data-state=\"connected\"] .core-shockwave{animation:shock 1100ms ease-out 200ms}\n@keyframes shock{\n 0%{opacity:0.7;transform:scale(0.4);border-width:2px}\n 100%{opacity:0;transform:scale(2.4);border-width:1px}\n}\n\n/* titles */\n.ok-title{\n font-family:var(--font-display);font-weight:600;\n font-size:38px;line-height:1.05;letter-spacing:-0.025em;\n color:var(--fg-bright);opacity:0;\n}\n.panel:not([hidden]) .ok-title{animation:rise 600ms ease-out 380ms forwards}\n.ok-sub{\n margin-top:16px;font-size:14.5px;line-height:1.55;color:var(--fg3);\n opacity:0;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;\n}\n.panel:not([hidden]) .ok-sub{animation:rise 600ms ease-out 520ms forwards}\n.ok-actions{\n margin-top:24px;display:flex;flex-direction:column;align-items:center;gap:14px;\n opacity:0;\n}\n.panel:not([hidden]) .ok-actions{animation:rise 600ms ease-out 660ms forwards}\n.ok-foot{\n margin-top:36px;font-family:var(--font-mono);font-size:10px;\n letter-spacing:0.22em;text-transform:uppercase;color:var(--fg4);opacity:0;\n}\n.panel:not([hidden]) .ok-foot{animation:rise 600ms ease-out 820ms forwards}\n.ws-name{font-size:13px;color:var(--accent);letter-spacing:0.04em;font-weight:500}\n\n.cmd{\n display:inline-flex;align-items:center;gap:8px;\n font-family:var(--font-mono);font-size:13px;color:var(--fg1);\n background:rgba(255,255,255,0.05);border:1px solid var(--border);\n padding:6px 10px 6px 12px;border-radius:6px;letter-spacing:0.02em;\n cursor:pointer;user-select:none;\n transition:background 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;\n}\n.cmd:hover{background:rgba(255,255,255,0.09);border-color:rgba(255,255,255,0.18)}\n.cmd.is-copied{color:var(--green);border-color:rgba(74,222,128,0.3);background:rgba(74,222,128,0.06)}\n.cmd .cmd-icon{\n width:12px;height:12px;color:var(--fg3);display:inline-grid;place-items:center;\n transition:color 140ms;\n}\n.cmd.is-copied .cmd-icon{color:var(--green)}\n.cmd .cmd-icon svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}\n\n.return-link{\n font-family:var(--font-mono);font-size:11px;letter-spacing:0.18em;\n text-transform:uppercase;color:var(--fg3);text-decoration:none;\n border-bottom:1px dotted currentColor;padding-bottom:1px;\n transition:color 140ms;\n}\n.return-link:hover{color:var(--fg1)}\n\n/* error specifics */\n.err-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:12px;\n}\n.err-msg{\n font-size:14.5px;line-height:1.55;color:var(--fg3);\n margin-bottom:28px;\n}\n.err-msg code{\n font-family:var(--font-mono);font-size:12px;\n background:rgba(255,255,255,0.06);padding:1px 5px;border-radius:4px;color:var(--fg2);\n}\n.err-actions{display:flex;gap:8px}\n.btn-secondary{\n flex:1;height:44px;border-radius:var(--radius-md);\n background:transparent;color:var(--fg2);\n border:1px solid var(--border);\n font-family:var(--font-body);font-size:13.5px;font-weight:500;\n cursor:pointer;text-decoration:none;\n display:inline-flex;align-items:center;justify-content:center;\n transition:background 140ms,color 140ms,border-color 140ms;\n}\n.btn-secondary:hover{background:rgba(255,255,255,0.04);color:var(--fg1);border-color:rgba(255,255,255,0.14)}\n\n/* verifying */\n.verifying-eyebrow{\n font-family:var(--font-mono);font-size:10px;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg3);\n font-weight:700;margin-bottom:14px;\n}\n.verifying-title{\n font-family:var(--font-display);font-weight:600;\n font-size:28px;line-height:1.1;color:var(--fg1);margin-bottom:8px;\n letter-spacing:-0.02em;\n}\n.verifying-sub{font-size:13px;color:var(--fg3);min-height:1.45em}\n\n/* form heading */\n.form-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:10px;\n}\n.form-sub{font-size:13.5px;color:var(--fg3);margin-bottom:28px;line-height:1.55}\n\n@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n\n@media (prefers-reduced-motion: reduce){\n *,*::before,*::after{\n animation-duration:0.01ms !important;animation-iteration-count:1 !important;\n transition-duration:0.01ms !important;\n }\n}\n</style>\n</head><body>\n<div class=\"top-mark\">\n <span class=\"m\"><span class=\"core\"></span></span>\n <span class=\"name\">Product Brain</span>\n</div>\n<div class=\"stage\">${bodyContent}</div>\n</body></html>`;\n}\n\n// Provider-agnostic display name. Trims and clamps to keep page chrome stable.\nfunction providerDisplayName(clientName: string | undefined): string {\n const name = (clientName ?? \"\").trim();\n if (!name) return \"your agent\";\n return name.length > 40 ? name.slice(0, 40) + \"…\" : name;\n}\n\n// Inner HTML for the success panel. Used both by the JSON path (cloned client-side\n// from a <template>) and by the no-JS server-rendered fallback page.\nfunction successPanelInner(workspaceName: string, redirectUrl: string, providerName: string): string {\n return `\n<div class=\"orb-wrap\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"sat-orbit o1\">\n <span class=\"sat\" style=\"transform:rotate(20deg) translate(78px) rotate(-20deg);color:#4ade80;animation-delay:600ms\"><span>DEC</span></span>\n <span class=\"sat\" style=\"transform:rotate(140deg) translate(78px) rotate(-140deg);color:#c9b99a;animation-delay:720ms\"><span>WP</span></span>\n <span class=\"sat\" style=\"transform:rotate(260deg) translate(78px) rotate(-260deg);color:#f59e0b;animation-delay:840ms\"><span>TEN</span></span>\n </div>\n <div class=\"sat-orbit o2\">\n <span class=\"sat\" style=\"transform:rotate(80deg) translate(54px) rotate(-80deg);color:#60a5fa;animation-delay:960ms\"><span>STD</span></span>\n <span class=\"sat\" style=\"transform:rotate(220deg) translate(54px) rotate(-220deg);color:#a78bfa;animation-delay:1080ms\"><span>INS</span></span>\n </div>\n <div class=\"core-shockwave\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow success\"><span class=\"dot\"></span>Connected</div>\n<h1 class=\"ok-title\">Product Brain is live.</h1>\n<p class=\"ok-sub\">\n <span class=\"ws-name\" data-field=\"ws-name\">${esc(workspaceName)}</span>\n</p>\n<div class=\"ok-actions\">\n <button class=\"cmd\" type=\"button\" data-cmd-pill data-redirect=\"${esc(redirectUrl)}\" aria-label=\"Copy 'Start PB' and return to ${esc(providerName)}\">\n <span data-cmd-text>Start PB</span>\n <span class=\"cmd-icon\" aria-hidden=\"true\">\n <svg data-cmd-svg viewBox=\"0 0 24 24\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V6a2 2 0 0 1 2-2h9\"/></svg>\n </span>\n </button>\n <a class=\"return-link\" href=\"${esc(redirectUrl)}\" data-return-link>Return to <span data-provider>${esc(providerName)}</span> →</a>\n</div>\n<p class=\"ok-foot\">Then say <span style=\"font-family:var(--font-mono);color:var(--fg3)\">Start PB</span> in <span data-provider>${esc(providerName)}</span></p>`;\n}\n\nfunction errorPanelInner(title: string, trustedDetailHtml: string, retryUrl: string): string {\n return `\n<div class=\"orb-wrap is-error\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow danger\"><span class=\"dot\"></span>Couldn't connect</div>\n<h2 class=\"err-title\" data-field=\"err-title\">${esc(title)}</h2>\n<p class=\"err-msg\" data-field=\"err-msg\">${trustedDetailHtml}</p>\n<div class=\"err-actions\">\n <a href=\"${esc(retryUrl)}\" class=\"btn-secondary\" data-retry-link>← Try again</a>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-secondary\">Get an API key →</a>\n</div>`;\n}\n\nconst cmdScript = `\n(function(){\n function bindCmd(pill){\n if(!pill||pill.__bound)return;pill.__bound=true;\n var textEl=pill.querySelector('[data-cmd-text]');\n var svgEl=pill.querySelector('[data-cmd-svg]');\n var redirectUrl=pill.getAttribute('data-redirect')||'';\n pill.addEventListener('click',function(){\n var done=function(){\n pill.classList.add('is-copied');\n if(textEl)textEl.textContent='Copied';\n if(svgEl)svgEl.innerHTML='<polyline points=\"4 12 10 18 20 6\"/>';\n setTimeout(function(){if(redirectUrl)window.location.href=redirectUrl},900);\n };\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){\n navigator.clipboard.writeText('Start PB').then(done,done);\n }else{done()}\n }catch(e){done()}\n });\n }\n document.querySelectorAll('[data-cmd-pill]').forEach(bindCmd);\n})();\n`;\n\nfunction authorizeFormPage(params: {\n redirect_uri: string;\n code_challenge: string;\n code_challenge_method: string;\n state: string;\n client_id: string;\n client_name?: string;\n}): string {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;\n const providerName = providerDisplayName(params.client_name);\n const body = `\n<!-- ─── CONNECT ─── -->\n<div class=\"panel\" id=\"p-connect\" data-state=\"connect\">\n <div class=\"eyebrow\">Connect Product Brain</div>\n <h1 class=\"form-title\">Paste your API key</h1>\n <p class=\"form-sub\">Give <span data-provider>${esc(providerName)}</span> access to your workspace memory.</p>\n <form method=\"POST\" action=\"/authorize\" id=\"f\" autocomplete=\"off\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${esc(redirect_uri)}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${esc(code_challenge)}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${esc(code_challenge_method)}\">\n <input type=\"hidden\" name=\"state\" value=\"${esc(state)}\">\n <input type=\"hidden\" name=\"client_id\" value=\"${esc(client_id)}\">\n <div class=\"input-wrap\" id=\"iw\">\n <span class=\"input-prefix\">pb_sk_</span>\n <input type=\"password\" id=\"k\" name=\"api_key\" class=\"input\" placeholder=\"…\" required autofocus spellcheck=\"false\">\n </div>\n <div class=\"hint\" id=\"hint\">Your key starts with pb_sk_</div>\n <button type=\"submit\" class=\"btn-primary\" id=\"sb\" disabled><span id=\"bt\">Connect</span></button>\n </form>\n <div class=\"small-link\"><a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\">No key? Generate one →</a></div>\n</div>\n\n<!-- ─── VERIFYING ─── -->\n<div class=\"panel\" id=\"p-verifying\" data-state=\"verifying\" hidden>\n <div class=\"orb-wrap is-verifying\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n </div>\n <div class=\"verifying-eyebrow\">Handshake</div>\n <h2 class=\"verifying-title\">Verifying key…</h2>\n <p class=\"verifying-sub\" id=\"verify-sub\">Checking workspace · …</p>\n</div>\n\n<!-- ─── CONNECTED (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-connected\" data-state=\"connected\" hidden></div>\n\n<!-- ─── ERROR (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-error\" data-state=\"error\" hidden></div>\n\n<template id=\"tpl-connected\">${successPanelInner(\"__WS__\", \"__URL__\", \"__PROVIDER__\")}</template>\n<template id=\"tpl-error\">${errorPanelInner(\"__TITLE__\", \"__DETAIL__\", \"__RETRY__\")}</template>\n\n<script>\n${cmdScript}\n(function(){\n var f=document.getElementById('f'),k=document.getElementById('k'),iw=document.getElementById('iw'),hint=document.getElementById('hint'),sb=document.getElementById('sb'),bt=document.getElementById('bt');\n var pConnect=document.getElementById('p-connect'),pVerify=document.getElementById('p-verifying'),pOk=document.getElementById('p-connected'),pErr=document.getElementById('p-error');\n var verifySub=document.getElementById('verify-sub');\n\n function show(panel){\n [pConnect,pVerify,pOk,pErr].forEach(function(p){\n if(p===panel){p.removeAttribute('hidden')}else{p.setAttribute('hidden','')}\n });\n }\n\n function syncInput(){\n var v=k.value;\n if(v.indexOf('pb_sk_')===0)k.value=v.slice(6);\n sb.disabled=!k.value.trim();\n iw.classList.remove('has-error');\n hint.classList.remove('is-error');\n hint.textContent='Your key starts with pb_sk_';\n }\n k.addEventListener('input',syncInput);\n k.addEventListener('paste',function(){setTimeout(syncInput,0)});\n k.addEventListener('keydown',function(e){\n if(e.key==='Escape'){k.value='';syncInput()}\n });\n\n function showError(title,detailHtml){\n var tpl=document.getElementById('tpl-error');\n var html=tpl.innerHTML\n .replace('__TITLE__',title.replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]}))\n .replace('__DETAIL__',detailHtml)\n .replace('__RETRY__','#');\n pErr.innerHTML=html;\n var retry=pErr.querySelector('[data-retry-link]');\n if(retry){retry.addEventListener('click',function(e){e.preventDefault();show(pConnect);k.focus();k.select()})}\n show(pErr);\n }\n\n function showSuccess(workspaceName,redirectUrl,providerName){\n var tpl=document.getElementById('tpl-connected');\n var safeWs=String(workspaceName||'').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});\n var safeProv=String(providerName||'your agent').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});\n var safeUrl=String(redirectUrl||'').replace(/\"/g,'"').replace(/[<>]/g,function(c){return{'<':'<','>':'>'}[c]});\n var html=tpl.innerHTML.split('__WS__').join(safeWs).split('__URL__').join(safeUrl).split('__PROVIDER__').join(safeProv);\n pOk.innerHTML=html;\n pOk.querySelectorAll('[data-cmd-pill]').forEach(function(pill){\n pill.__bound=false;\n });\n // Re-run binder\n var s=document.createElement('script');s.textContent=${JSON.stringify(cmdScript)};document.body.appendChild(s);s.remove();\n show(pOk);\n }\n\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var v=k.value.trim();\n if(!v){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Paste your key first';return}\n sb.disabled=true;bt.textContent='Verifying';\n show(pVerify);\n\n var steps=['Checking workspace · …','Loading chain · …','Establishing memory · …'];\n var i=0;verifySub.textContent=steps[0];\n var ti=setInterval(function(){i++;if(i>=steps.length){clearInterval(ti);return}verifySub.textContent=steps[i]},700);\n\n var minDelay=new Promise(function(r){setTimeout(r,1100)});\n var fd=new FormData(f);\n var body=new URLSearchParams();\n fd.forEach(function(val,key){body.append(key,String(val))});\n\n var req=fetch('/authorize',{\n method:'POST',\n headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'},\n body:body.toString(),\n credentials:'same-origin'\n }).then(function(r){return r.json().then(function(j){return{status:r.status,body:j}})});\n\n Promise.all([req,minDelay]).then(function(arr){\n clearInterval(ti);\n var res=arr[0];\n if(res.body&&res.body.ok){\n showSuccess(res.body.workspaceName,res.body.redirectUrl,res.body.providerName);\n }else{\n showError(res.body&&res.body.title||'Couldn\\\\'t connect',res.body&&res.body.detail||'Try again, or generate a new key.');\n sb.disabled=false;bt.textContent='Connect';\n }\n }).catch(function(){\n clearInterval(ti);\n showError('Network error','We couldn\\\\'t reach Product Brain. Check your connection and try again.');\n sb.disabled=false;bt.textContent='Connect';\n });\n });\n\n k.focus();\n})();\n</script>`;\n return authPageShell(\"Connect Product Brain\", body);\n}\n\nfunction authorizeSuccessPage(workspaceName: string, redirectUrl: string, providerName: string): string {\n const body = `\n<div class=\"panel\" data-state=\"connected\">\n${successPanelInner(workspaceName, redirectUrl, providerName)}\n</div>\n<script>${cmdScript}</script>`;\n return authPageShell(\"Connected\", body);\n}\n\n// security: trustedDetailHtml must be a hardcoded literal — never pass user-controlled data.\nfunction authorizeErrorPage(title: string, trustedDetailHtml: string, retryUrl: string): string {\n const body = `\n<div class=\"panel\" data-state=\"error\">\n${errorPanelInner(title, trustedDetailHtml, retryUrl)}\n</div>`;\n return authPageShell(\"Connection error\", body);\n}\n\napp.get(\"/authorize\", authLimiter, (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;\n const cid = String(client_id ?? \"\");\n const clientName = cid && registeredClients.has(cid)\n ? registeredClients.get(cid)!.client_name\n : undefined;\n res.type(\"html\").send(authorizeFormPage({\n redirect_uri: String(redirect_uri ?? \"\"),\n code_challenge: String(code_challenge ?? \"\"),\n code_challenge_method: String(code_challenge_method ?? \"S256\"),\n state: String(state ?? \"\"),\n client_id: cid,\n client_name: clientName,\n }));\n});\n\napp.post(\n \"/authorize\",\n authLimiter,\n express.urlencoded({ extended: false }),\n async (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;\n\n // Negotiate response shape: fetch path sets Accept: application/json,\n // form-post no-JS path gets the standard rebranded HTML page.\n const wantsJson = String(req.headers[\"accept\"] ?? \"\").includes(\"application/json\");\n\n // Build \"retry\" URL so error pages can link back to the form.\n const retryParams = new URLSearchParams({\n redirect_uri: redirect_uri ?? \"\",\n code_challenge: code_challenge ?? \"\",\n code_challenge_method: code_challenge_method ?? \"S256\",\n ...(state ? { state } : {}),\n ...(client_id ? { client_id } : {}),\n }).toString();\n const retryUrl = `/authorize?${retryParams}`;\n\n function sendError(title: string, trustedDetailHtml: string, status = 400): void {\n if (wantsJson) {\n res.status(status).json({ ok: false, title, detail: trustedDetailHtml });\n } else {\n res.status(status).type(\"html\").send(authorizeErrorPage(title, trustedDetailHtml, retryUrl));\n }\n }\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n sendError(\n \"Invalid key format\",\n \"API keys start with <code>pb_sk_</code>. Check your key and try again.\",\n );\n return;\n }\n\n // Validate redirect_uri against the registered client's allowed redirects.\n // Open redirect prevention: never trust a request-supplied redirect_uri without\n // checking it was pre-registered during dynamic client registration (RFC 7591).\n //\n // When client_id is provided but unknown (e.g. server restart wiped the in-memory Map\n // after claude.ai cached its client_id from a previous session), auto-re-register using\n // the supplied redirect_uri rather than hard-rejecting. The API key validation below is\n // the primary security gate; basic HTTPS validation guards against degenerate redirect URIs.\n if (!client_id) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"client_id is required\",\n });\n return;\n }\n if (!registeredClients.has(client_id)) {\n if (typeof redirect_uri === \"string\" && redirect_uri.startsWith(\"https://\")) {\n registeredClients.set(client_id, {\n client_id,\n redirect_uris: [redirect_uri],\n registeredAt: Date.now(),\n });\n process.stderr.write(`[authorize] auto-re-registered stale client_id after restart\\n`);\n } else {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"Unknown client_id and redirect_uri is not a valid https URL\",\n });\n return;\n }\n }\n\n const client = registeredClients.get(client_id)!;\n if (!client.redirect_uris.includes(redirect_uri)) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"redirect_uri does not match any registered redirect for this client\",\n });\n return;\n }\n\n // Validate key against Convex before issuing the code.\n // DEC-789 S2: probe primary then fallback URLs so DEV keys work against PROD Railway MCP.\n let workspaceName = \"Your Workspace\";\n try {\n const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\\/$/, \"\");\n const fallbackUrls = (process.env.CONVEX_FALLBACK_URLS ?? \"\")\n .split(\",\").map((u) => u.trim().replace(/\\/$/, \"\")).filter(Boolean);\n const candidates = [primaryUrl, ...fallbackUrls];\n\n let foundUrl: string | undefined;\n let anyDefinitiveReject = false;\n\n for (const url of candidates) {\n let checkData: { ok: boolean; workspaceName?: string; deploymentUrl?: string } | null = null;\n try {\n const checkRes = await fetch(`${url}/api/key-check`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${api_key}`, \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(5000),\n });\n checkData = await checkRes.json() as { ok: boolean; workspaceName?: string; deploymentUrl?: string };\n } catch {\n // This candidate unreachable — try next.\n continue;\n }\n if (checkData.ok) {\n if (checkData.workspaceName) workspaceName = checkData.workspaceName;\n foundUrl = checkData.deploymentUrl ?? url;\n break;\n }\n anyDefinitiveReject = true;\n }\n\n if (!foundUrl) {\n if (anyDefinitiveReject) {\n sendError(\n \"Key not recognized\",\n \"This API key wasn't found in Product Brain. Check your API Keys in Studio and try again.\",\n 401,\n );\n return;\n }\n // All candidates unreachable (network errors only) — fail-open.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n } else {\n getKeyState(api_key).deploymentUrl = foundUrl;\n }\n } catch {\n // Outer guard: fail-open so auth works even if the entire block throws.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n }\n\n const code = randomUUID();\n pendingCodes.set(code, {\n apiKey: api_key,\n codeChallenge: code_challenge,\n redirectUri: redirect_uri,\n expiresAt: Date.now() + 5 * 60_000,\n });\n\n const url = new URL(redirect_uri);\n url.searchParams.set(\"code\", code);\n if (state) url.searchParams.set(\"state\", state);\n const redirectUrl = url.toString();\n const providerName = providerDisplayName(client.client_name);\n\n if (wantsJson) {\n res.json({ ok: true, workspaceName, redirectUrl, providerName });\n } else {\n // No-JS path: server-rendered success page. User clicks the pill or the\n // \"Return to {provider}\" link to complete the redirect — no auto-bounce.\n res.type(\"html\").send(authorizeSuccessPage(workspaceName, redirectUrl, providerName));\n }\n },\n);\n\n// ── OAuth: Token Exchange ────────────────────────────────────────────────\n// Step 5: Claude exchanges the authorization code (with PKCE verifier) for a token.\n// Supports both authorization_code and refresh_token grants.\n\nfunction issueTokens(apiKey: string): object {\n // Return the pb_sk_* key directly as the access_token so connections\n // survive server restarts. Railway deploys on every git push, wiping\n // in-memory Maps. pb_sk_* keys are long-lived (valid until explicitly\n // revoked in Studio); actual validity is enforced by Convex per tool call.\n // extractBearerKey() already handles pb_sk_* with zero Map lookup.\n //\n // NOTE: pb_at_* tokens issued before this change are still resolved by\n // the accessTokens Map for backward compat (until clients re-auth).\n const now = Date.now();\n\n const refreshToken = `pb_rt_${randomUUID()}`;\n\n // Per-key cap (Finding 3, PR #34): before the global backstop kicks in, ensure\n // no single apiKey holds more than MAX_REFRESH_TOKENS_PER_KEY entries. Mirrors\n // MAX_SESSIONS_PER_KEY. Prevents one abusive/leaked key from evicting every\n // other tenant's refresh tokens via the global FIFO backstop below.\n let perKeyCount = 0;\n let oldestKeyForApiKey: string | null = null;\n let oldestAtForApiKey = Infinity;\n for (const [k, v] of refreshTokens) {\n if (v.apiKey === apiKey) {\n perKeyCount++;\n if (v.createdAt < oldestAtForApiKey) {\n oldestAtForApiKey = v.createdAt;\n oldestKeyForApiKey = k;\n }\n }\n }\n if (perKeyCount >= MAX_REFRESH_TOKENS_PER_KEY && oldestKeyForApiKey) {\n refreshTokens.delete(oldestKeyForApiKey);\n }\n\n // Global backstop — unchanged behaviour.\n if (refreshTokens.size >= MAX_REFRESH_TOKENS) {\n let oldestKey: string | null = null;\n let oldestAt = Infinity;\n for (const [k, v] of refreshTokens) {\n if (v.createdAt < oldestAt) {\n oldestAt = v.createdAt;\n oldestKey = k;\n }\n }\n if (oldestKey) refreshTokens.delete(oldestKey);\n }\n refreshTokens.set(refreshToken, { apiKey, createdAt: now });\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n // 1-year TTL: actual validity enforced by Convex, not by expiry clock.\n // Long TTL prevents unnecessary refresh cycles after restarts.\n expires_in: 365 * 24 * 3600,\n refresh_token: refreshToken,\n };\n}\n\napp.post(\n \"/oauth/token\",\n authLimiter,\n express.urlencoded({ extended: false }),\n express.json(),\n (req: any, res: any) => {\n const { grant_type, code, code_verifier, redirect_uri, refresh_token } =\n req.body;\n\n if (grant_type === \"refresh_token\") {\n const entry = refreshTokens.get(refresh_token);\n if (!entry) {\n res.status(400).json({ error: \"invalid_grant\", error_description: \"Invalid refresh token\" });\n return;\n }\n if (Date.now() - entry.createdAt > REFRESH_TOKEN_TTL_MS) {\n refreshTokens.delete(refresh_token);\n res.status(400).json({ error: \"invalid_grant\", error_description: \"Refresh token expired\" });\n return;\n }\n // Rotate: revoke old, issue new pair\n const apiKey = entry.apiKey;\n refreshTokens.delete(refresh_token);\n res.json(issueTokens(apiKey));\n return;\n }\n\n if (grant_type !== \"authorization_code\") {\n res.status(400).json({ error: \"unsupported_grant_type\" });\n return;\n }\n\n const pending = pendingCodes.get(code);\n if (!pending || pending.redirectUri !== redirect_uri) {\n res.status(400).json({ error: \"invalid_grant\" });\n return;\n }\n\n // PKCE S256 validation\n const challenge = createHash(\"sha256\")\n .update(code_verifier ?? \"\")\n .digest(\"base64url\");\n if (challenge !== pending.codeChallenge) {\n pendingCodes.delete(code);\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"PKCE verification failed\",\n });\n return;\n }\n\n pendingCodes.delete(code);\n res.json(issueTokens(pending.apiKey));\n },\n);\n\n// ── Rate Limiting ────────────────────────────────────────────────────────\n\nconst mcpLimiter = rateLimit({\n windowMs: 60_000,\n max: 120,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many requests. Try again later.\" },\n});\n\n// ── Auth Failure Backoff (Fix 5) ──────────────────────────────────────────\n// Per-IP progressive lockout for failed API key auth attempts to prevent\n// brute-force attacks through the MCP endpoints.\n\ninterface AuthFailureRecord {\n count: number;\n firstFailure: number;\n blockedUntil: number;\n}\n\nconst authFailures = new Map<string, AuthFailureRecord>();\nconst AUTH_FAILURE_MAX = 10;\nconst AUTH_FAILURE_WINDOW_MS = 5 * 60_000; // 5 minutes\nconst AUTH_BLOCK_DURATION_MS = 15 * 60_000; // 15 minutes\nconst MAX_AUTH_FAILURE_ENTRIES = 10_000;\n\nfunction checkAuthBlock(ip: string): boolean {\n const rec = authFailures.get(ip);\n if (!rec) return false;\n return rec.blockedUntil > Date.now();\n}\n\nfunction recordAuthFailure(ip: string): void {\n const now = Date.now();\n const rec = authFailures.get(ip);\n\n if (!rec) {\n authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });\n return;\n }\n\n // Reset window if the first failure is outside the tracking window.\n if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {\n rec.count = 1;\n rec.firstFailure = now;\n rec.blockedUntil = 0;\n } else {\n rec.count++;\n if (rec.count >= AUTH_FAILURE_MAX) {\n rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;\n }\n }\n}\n\n// ── Health Check ─────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: any, res: any) => {\n res.json({ status: \"ok\", version: SERVER_VERSION, transport: \"http\" });\n});\n\n// ── Session Management ──────────────────────────────────────────────────\n\ninterface SessionEntry {\n transport: StreamableHTTPServerTransport;\n lastAccess: number;\n // Fix 3 — short hash of the API key that created this session. Used to\n // detect session hijacking when subsequent requests arrive with a different key.\n keyHash: string;\n}\n\nconst sessions = new Map<string, SessionEntry>();\nconst SESSION_TTL_MS = 30 * 60 * 1000;\nconst MAX_SESSIONS = 200;\n// Fix 6 — prevent a single API key from monopolising all session slots.\nconst MAX_SESSIONS_PER_KEY = 5;\n\nfunction evictStaleSessions(): void {\n const now = Date.now();\n for (const [id, entry] of sessions) {\n if (now - entry.lastAccess > SESSION_TTL_MS) {\n logSessionLifecycle(\"session_deleted\", id, \"ttl\");\n entry.transport.close().catch(() => {});\n sessions.delete(id);\n }\n }\n if (sessions.size > MAX_SESSIONS) {\n const sorted = [...sessions.entries()].sort(\n (a, b) => a[1].lastAccess - b[1].lastAccess,\n );\n for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {\n logSessionLifecycle(\"session_deleted\", sorted[i][0], \"eviction\");\n sorted[i][1].transport.close().catch(() => {});\n sessions.delete(sorted[i][0]);\n }\n }\n}\n\nsetInterval(evictStaleSessions, 60_000);\n\n// ── Auth Helpers ─────────────────────────────────────────────────────────\n\nfunction extractBearerKey(req: any): string | null {\n const header = req.headers?.authorization;\n if (typeof header !== \"string\" || !header.startsWith(\"Bearer \")) return null;\n const token = header.slice(7).trim();\n\n // Fix 1 — Support both direct API keys (stdio/backward compat) and opaque\n // OAuth access tokens issued by issueTokens().\n if (token.startsWith(\"pb_sk_\")) {\n // Direct API key — accepted for stdio and backward compatibility.\n return token;\n }\n if (token.startsWith(\"pb_at_\")) {\n // Opaque OAuth access token — resolve to the underlying API key.\n const entry = accessTokens.get(token);\n if (!entry) return null;\n const now = Date.now();\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {\n // Expired — remove and reject.\n accessTokens.delete(token);\n return null;\n }\n return entry.apiKey;\n }\n return null;\n}\n\nfunction send401(req: any, res: any): void {\n const base = baseUrl(req);\n res\n .status(401)\n .set(\n \"WWW-Authenticate\",\n `Bearer resource_metadata=\"${base}/.well-known/oauth-protected-resource\"`,\n )\n .json({ error: \"unauthorized\" });\n}\n\nfunction logRequest(\n method: string,\n outcome: \"ok\" | \"auth_fail\" | \"error\",\n sessionId?: string,\n durationMs?: number,\n): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n const dur = durationMs != null ? ` duration=${durationMs}ms` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}\\n`);\n}\n\nfunction logSessionLifecycle(\n event: \"session_created\" | \"session_deleted\",\n sessionId: string,\n reason?: \"ttl\" | \"eviction\" | \"onclose\",\n): void {\n const ts = new Date().toISOString();\n const r = reason ? ` reason=${reason}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}\\n`);\n}\n\n// ── MCP Handlers ────────────────────────────────────────────────────────\n\napp.post(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"POST\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const reqStart = Date.now();\n\n try {\n await runWithAuth({ apiKey }, async () => {\n if (sessionId && sessions.has(sessionId)) {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", sessionId, Date.now() - reqStart);\n } else if (!sessionId && isInitializeRequest(req.body)) {\n // Fix 6 — Enforce per-key session cap before creating a new session.\n const keyH = hashKey(apiKey);\n let keySessionCount = 0;\n for (const entry of sessions.values()) {\n if (entry.keyHash === keyH) keySessionCount++;\n }\n if (keySessionCount >= MAX_SESSIONS_PER_KEY) {\n res.status(429).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Too many sessions for this API key\" },\n id: null,\n });\n return;\n }\n\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (sid: string) => {\n // Fix 3 — Store a key hash with the session entry.\n sessions.set(sid, { transport, lastAccess: Date.now(), keyHash: keyH });\n logSessionLifecycle(\"session_created\", sid);\n },\n });\n\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) {\n logSessionLifecycle(\"session_deleted\", sid, \"onclose\");\n sessions.delete(sid);\n }\n };\n\n const server = createProductBrainServer();\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", transport.sessionId ?? undefined, Date.now() - reqStart);\n } else {\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)\\n`,\n );\n res.status(400).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Bad Request: no valid session ID provided\" },\n id: null,\n });\n }\n });\n } catch (err: any) {\n logRequest(\"POST\", \"error\", sessionId, Date.now() - reqStart);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n }\n});\n\napp.get(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"GET\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).send(\"Invalid or missing session ID\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res);\n logRequest(\"GET\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"GET\", \"error\", sessionId);\n }\n});\n\napp.delete(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"DELETE\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).send(\"Invalid or missing session ID\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n await entry.transport.handleRequest(req, res);\n logRequest(\"DELETE\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"DELETE\", \"error\", sessionId);\n }\n});\n\n// ── Start ───────────────────────────────────────────────────────────────\n\nprocess.on(\"unhandledRejection\", (reason) => {\n const msg = reason instanceof Error ? reason.message : String(reason);\n console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);\n});\n\nprocess.on(\"uncaughtException\", (err) => {\n console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);\n gracefulShutdown();\n});\n\nlet shuttingDown = false;\nasync function gracefulShutdown() {\n if (shuttingDown) return;\n shuttingDown = true;\n setTimeout(() => process.exit(1), 3_000).unref();\n console.log(\"Shutting down...\");\n for (const [, entry] of sessions) {\n await entry.transport.close().catch(() => {});\n }\n try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\n// Bind all interfaces — Railway/Cloudflare reach the container on its non-loopback IP.\n// Loopback-only (127.0.0.1) causes edge 502: the proxy never connects to localhost inside the pod.\nconst LISTEN_HOST = \"0.0.0.0\";\nconst httpServer = app.listen(PORT, LISTEN_HOST, () => {\n console.log(\n `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`,\n );\n});\nhttpServer.on(\"error\", (err) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n"],"mappings":"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","names":["url"]}
|