web-comments 1.0.1 → 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
  }
@@ -357,6 +385,17 @@ async function authorize(request, context, options) {
357
385
  if (resource !== at.resource) return fail("invalid_target", `the resource is ${at.resource}`);
358
386
  const who = await context.identify(request);
359
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
+ }
360
399
  const login = context.loginUrl(request);
361
400
  if (login) return Response.redirect(new URL(login, request.url).href, 302);
362
401
  return page(
@@ -436,7 +475,14 @@ async function decide(request, context, options) {
436
475
  );
437
476
  }
438
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
+ });
439
484
  const grant = {
485
+ grantId,
440
486
  identity: who,
441
487
  clientId: consent.clientId,
442
488
  clientName: clientName(client),
@@ -496,7 +542,7 @@ async function token(request, context, options) {
496
542
  const refresh = params.get("refresh_token") ?? "";
497
543
  const key = `token:${await sha256(refresh)}`;
498
544
  const record = refresh.startsWith(TOKEN_PREFIX.refresh) ? await readJson(storage, key) : null;
499
- 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)) {
500
546
  return oauthError("invalid_grant", "the refresh token is unknown, used or expired");
501
547
  }
502
548
  await storage.delete(key);
@@ -545,7 +591,7 @@ async function handleOAuth(request, path, context, options) {
545
591
  }
546
592
 
547
593
  // src/connectors/mcp/index.ts
548
- var VERSION = "1.0.1";
594
+ var VERSION = "1.0.2";
549
595
  var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
550
596
  function mcp(options = {}) {
551
597
  const oauth = {
@@ -571,6 +617,42 @@ function mcp(options = {}) {
571
617
  }
572
618
  if (path === "/") return serve(request, context, oauth);
573
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
+ }
574
656
  }
575
657
  };
576
658
  }
@@ -624,6 +706,7 @@ async function authenticate(request, context) {
624
706
  if (!grant || grant.site !== context.site || grant.resource !== endpoints(context.url).resource) {
625
707
  return null;
626
708
  }
709
+ if (!await context.allowed(grant.identity, request)) return null;
627
710
  return {
628
711
  ...grant.identity,
629
712
  name: `${grant.identity.name} (${grant.clientName})`,