web-comments 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  CommentsError
3
- } from "../chunks/chunk-RRSB326L.js";
3
+ } from "../chunks/chunk-HN5V6BS6.js";
4
4
  import "../chunks/chunk-WSFCRVEQ.js";
5
5
 
6
+ // src/connectors/mcp/icon.ts
7
+ var icon = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIxLjIxIDAgMjEuNTggMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLjg1IDBhNC4xNiA0LjE2IDAgMCAwLTIuOTUgMS4yMTdMMS40NTYgMTAuNjZhLjgzNS44MzUgMCAwIDAgMCAxLjE4LjgzNS44MzUgMCAwIDAgMS4xOCAwbDkuNDQyLTkuNDQyYTIuNDkgMi40OSAwIDAgMSAzLjU0MSAwIDIuNDkgMi40OSAwIDAgMSAwIDMuNTQxTDguNTkgMTIuOTdsLS4xLjFhLjgzNS44MzUgMCAwIDAgMCAxLjE4LjgzNS44MzUgMCAwIDAgMS4xOCAwbC4xLS4wOTggNy4wMy03LjAzNGEyLjQ5IDIuNDkgMCAwIDEgMy41NDIgMGwuMDQ5LjA1YTIuNDkgMi40OSAwIDAgMSAwIDMuNTRsLTguNTQgOC41NGExLjk2IDEuOTYgMCAwIDAgMCAyLjc1NWwxLjc1MyAxLjc1M2EuODM1LjgzNSAwIDAgMCAxLjE4IDAgLjgzNS44MzUgMCAwIDAgMC0xLjE4bC0xLjc1My0xLjc1M2EuMjY2LjI2NiAwIDAgMSAwLS4zOTRsOC41NC04LjU0YTQuMTg1IDQuMTg1IDAgMCAwIDAtNS45bC0uMDUtLjA1YTQuMTYgNC4xNiAwIDAgMC0yLjk1LTEuMjE4Yy0uMiAwLS40MDEuMDItLjYuMDQ4YTQuMTcgNC4xNyAwIDAgMC0xLjE3LTMuNTUyQTQuMTYgNC4xNiAwIDAgMCAxMy44NSAwbTAgMy4zMzNhLjg0Ljg0IDAgMCAwLS41OS4yNDVMNi4yNzUgMTAuNTZhNC4xODYgNC4xODYgMCAwIDAgMCA1LjkwMiA0LjE4NiA0LjE4NiAwIDAgMCA1LjkwMiAwTDE5LjE2IDkuNDhhLjgzNS44MzUgMCAwIDAgMC0xLjE4LjgzNS44MzUgMCAwIDAtMS4xOCAwbC02Ljk4NSA2Ljk4NGEyLjQ5IDIuNDkgMCAwIDEtMy41NCAwIDIuNDkgMi40OSAwIDAgMSAwLTMuNTRsNi45ODMtNi45ODVhLjgzNS44MzUgMCAwIDAgMC0xLjE4Ljg0Ljg0IDAgMCAwLS41OS0uMjQ1Ii8+PC9zdmc+";
8
+
6
9
  // src/connectors/mcp/pages.ts
7
10
  function escapeHtml(value) {
8
11
  return value.replace(
@@ -151,10 +154,35 @@ async function readJson(storage, key) {
151
154
  async function grantOf(storage, token2) {
152
155
  if (!token2.startsWith(TOKEN_PREFIX.access)) return null;
153
156
  const record = await readJson(storage, `token:${await sha256(token2)}`);
154
- if (record?.kind !== "access") return null;
157
+ if (record?.kind !== "access" || !await live(storage, record.grantId)) return null;
155
158
  const { kind: _, ...grant } = record;
156
159
  return grant;
157
160
  }
161
+ async function live(storage, grantId) {
162
+ return await storage.get(`grant:${grantId}`) !== null;
163
+ }
164
+ async function grantIds(storage, identityId) {
165
+ return await readJson(storage, `grants:${identityId}`) ?? [];
166
+ }
167
+ async function addGrant(storage, identityId, grantId, record) {
168
+ await storage.set(`grant:${grantId}`, JSON.stringify(record));
169
+ const ids = await grantIds(storage, identityId);
170
+ await storage.set(`grants:${identityId}`, JSON.stringify([...ids, grantId]));
171
+ }
172
+ async function grantsOf(storage, identityId) {
173
+ const found = [];
174
+ for (const grantId of await grantIds(storage, identityId)) {
175
+ const record = await readJson(storage, `grant:${grantId}`);
176
+ if (record) found.push({ grantId, ...record });
177
+ }
178
+ return found.reverse();
179
+ }
180
+ async function revokeGrant(storage, identityId, grantId) {
181
+ const ids = await grantIds(storage, identityId);
182
+ if (!ids.includes(grantId)) return;
183
+ await storage.delete(`grant:${grantId}`);
184
+ await storage.set(`grants:${identityId}`, JSON.stringify(ids.filter((id) => id !== grantId)));
185
+ }
158
186
  function looksLikeOurToken(token2) {
159
187
  return token2.startsWith(TOKEN_PREFIX.access) || token2.startsWith(TOKEN_PREFIX.refresh);
160
188
  }
@@ -302,9 +330,16 @@ var PAGE_HEADERS = {
302
330
  "cache-control": "no-store",
303
331
  "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; frame-ancestors 'none'; base-uri 'none'",
304
332
  "x-frame-options": "DENY",
305
- "referrer-policy": "no-referrer",
333
+ // Not no-referrer: under it, browsers send the consent form's Origin as "null".
334
+ "referrer-policy": "same-origin",
306
335
  "x-content-type-options": "nosniff"
307
336
  };
337
+ function fromThisOrigin(request) {
338
+ const site = request.headers.get("sec-fetch-site");
339
+ if (site) return site === "same-origin";
340
+ const origin = request.headers.get("origin");
341
+ return origin === null || origin === new URL(request.url).origin;
342
+ }
308
343
  function page(html, status = 200) {
309
344
  return new Response(html, { status, headers: PAGE_HEADERS });
310
345
  }
@@ -350,6 +385,17 @@ async function authorize(request, context, options) {
350
385
  if (resource !== at.resource) return fail("invalid_target", `the resource is ${at.resource}`);
351
386
  const who = await context.identify(request);
352
387
  if (!who) {
388
+ const someone = await context.signedIn(request);
389
+ if (someone) {
390
+ return page(
391
+ messagePage(
392
+ options.name,
393
+ "Not allowed",
394
+ `${someone.email ?? someone.name} may not use comments on ${url.host}.`
395
+ ),
396
+ 403
397
+ );
398
+ }
353
399
  const login = context.loginUrl(request);
354
400
  if (login) return Response.redirect(new URL(login, request.url).href, 302);
355
401
  return page(
@@ -387,9 +433,7 @@ async function authorize(request, context, options) {
387
433
  }
388
434
  async function decide(request, context, options) {
389
435
  const at = endpoints(context.url);
390
- const ownOrigin = new URL(request.url).origin;
391
- const origin = request.headers.get("origin");
392
- if (origin !== null && origin !== ownOrigin || request.headers.get("sec-fetch-site") === "cross-site") {
436
+ if (!fromThisOrigin(request)) {
393
437
  return page(
394
438
  messagePage(options.name, "Not allowed", "This approval didn't come from this site."),
395
439
  403
@@ -431,7 +475,14 @@ async function decide(request, context, options) {
431
475
  );
432
476
  }
433
477
  const code = randomToken();
478
+ const grantId = randomToken(12);
479
+ await addGrant(context.storage, who.id, grantId, {
480
+ clientId: consent.clientId,
481
+ clientName: clientName(client),
482
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
483
+ });
434
484
  const grant = {
485
+ grantId,
435
486
  identity: who,
436
487
  clientId: consent.clientId,
437
488
  clientName: clientName(client),
@@ -491,7 +542,7 @@ async function token(request, context, options) {
491
542
  const refresh = params.get("refresh_token") ?? "";
492
543
  const key = `token:${await sha256(refresh)}`;
493
544
  const record = refresh.startsWith(TOKEN_PREFIX.refresh) ? await readJson(storage, key) : null;
494
- if (record?.kind !== "refresh" || record.clientId !== clientId || record.site !== context.site) {
545
+ if (record?.kind !== "refresh" || record.clientId !== clientId || record.site !== context.site || !await live(storage, record.grantId)) {
495
546
  return oauthError("invalid_grant", "the refresh token is unknown, used or expired");
496
547
  }
497
548
  await storage.delete(key);
@@ -540,7 +591,7 @@ async function handleOAuth(request, path, context, options) {
540
591
  }
541
592
 
542
593
  // src/connectors/mcp/index.ts
543
- var VERSION = "1.0.0";
594
+ var VERSION = "1.0.2";
544
595
  var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
545
596
  function mcp(options = {}) {
546
597
  const oauth = {
@@ -566,6 +617,42 @@ function mcp(options = {}) {
566
617
  }
567
618
  if (path === "/") return serve(request, context, oauth);
568
619
  return await handleOAuth(request, path, context, oauth) ?? Response.json({ error: "not_found" }, { status: 404, headers: CORS });
620
+ },
621
+ // The toolbar's MCP panel: how to connect an agent, and the ones connected.
622
+ async panel(who, context) {
623
+ const url = `${context.origin}${context.basePath}/mcp`;
624
+ const grants = await grantsOf(context.storage, who.id);
625
+ return {
626
+ title: "MCP",
627
+ icon,
628
+ blocks: [
629
+ {
630
+ type: "text",
631
+ text: "Let an agent (Claude Code, Cursor, \u2026) read the comments here with their screenshots, reply, and resolve them, as you. You approve it in the browser the first time."
632
+ },
633
+ { type: "code", label: "Server URL", value: url },
634
+ {
635
+ type: "code",
636
+ label: "Claude Code",
637
+ value: `claude mcp add --transport http comments ${url}`
638
+ },
639
+ {
640
+ type: "list",
641
+ label: "Your agents",
642
+ empty: "No agent connected yet.",
643
+ items: grants.map((grant) => ({
644
+ title: grant.clientName,
645
+ description: `Connected ${new Date(grant.createdAt).toLocaleDateString("en", { month: "short", day: "numeric", year: "numeric" })}`,
646
+ button: { label: "Disconnect", action: "revoke", input: { grant: grant.grantId } }
647
+ }))
648
+ }
649
+ ]
650
+ };
651
+ },
652
+ async action(action, who, input, context) {
653
+ if (action === "revoke" && typeof input.grant === "string") {
654
+ await revokeGrant(context.storage, who.id, input.grant);
655
+ }
569
656
  }
570
657
  };
571
658
  }
@@ -619,6 +706,7 @@ async function authenticate(request, context) {
619
706
  if (!grant || grant.site !== context.site || grant.resource !== endpoints(context.url).resource) {
620
707
  return null;
621
708
  }
709
+ if (!await context.allowed(grant.identity, request)) return null;
622
710
  return {
623
711
  ...grant.identity,
624
712
  name: `${grant.identity.name} (${grant.clientName})`,