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.
- package/README.md +11 -0
- package/dist/chunks/{chunk-RRSB326L.js → chunk-HN5V6BS6.js} +42 -9
- package/dist/connectors/linear.js +367 -87
- package/dist/connectors/mcp.js +96 -8
- package/dist/index.js +19 -19
- package/dist/server.js +1 -1
- package/dist/types/connectors/linear/icon.d.ts +1 -0
- package/dist/types/connectors/linear/index.d.ts +7 -4
- package/dist/types/connectors/mcp/icon.d.ts +1 -0
- package/dist/types/connectors/mcp/oauth.d.ts +13 -0
- package/dist/types/server/connector.d.ts +19 -1
- package/dist/types/server/service.d.ts +2 -0
- package/dist/types/shared/types.d.ts +55 -0
- package/dist/types/widget/api.d.ts +10 -1
- package/dist/types/widget/connectors.d.ts +13 -0
- package/dist/widget.global.js +19 -19
- package/package.json +1 -1
package/dist/connectors/mcp.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CommentsError
|
|
3
|
-
} from "../chunks/chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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})`,
|