web-comments 0.0.1 → 1.0.0
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 +263 -6
- package/dist/auth/bearer-token.js +43 -0
- package/dist/auth/cloudflare-access.js +90 -0
- package/dist/auth/dev-user.js +21 -0
- package/dist/auth/session-endpoint.js +47 -0
- package/dist/chunks/chunk-644PTNSO.js +101 -0
- package/dist/chunks/chunk-7K7K7M5O.js +101 -0
- package/dist/chunks/chunk-RRSB326L.js +720 -0
- package/dist/chunks/chunk-T4WN3STA.js +239 -0
- package/dist/chunks/chunk-WSFCRVEQ.js +9 -0
- package/dist/chunks/chunk-WY5E2C47.js +99 -0
- package/dist/connectors/linear.js +318 -0
- package/dist/connectors/mcp.js +890 -0
- package/dist/index.js +52 -0
- package/dist/react.js +19 -0
- package/dist/server.js +11 -0
- package/dist/store/d1.js +16 -0
- package/dist/store/drizzle/mysql.js +17 -0
- package/dist/store/drizzle/pg.js +17 -0
- package/dist/store/drizzle/sqlite.js +17 -0
- package/dist/store/drizzle.js +10 -0
- package/dist/store/memory.js +109 -0
- package/dist/store/r2.js +25 -0
- package/dist/store/s3.js +43 -0
- package/dist/types/auth/bearer-token.d.ts +13 -0
- package/dist/types/auth/cloudflare-access.d.ts +33 -0
- package/dist/types/auth/dev-user.d.ts +6 -0
- package/dist/types/auth/session-endpoint.d.ts +25 -0
- package/dist/types/connectors/linear/index.d.ts +46 -0
- package/dist/types/connectors/mcp/index.d.ts +21 -0
- package/dist/types/connectors/mcp/oauth.d.ts +50 -0
- package/dist/types/connectors/mcp/pages.d.ts +14 -0
- package/dist/types/react.d.ts +8 -0
- package/dist/types/server/auth.d.ts +16 -0
- package/dist/types/server/connector.d.ts +102 -0
- package/dist/types/server/events.d.ts +31 -0
- package/dist/types/server/index.d.ts +50 -0
- package/dist/types/server/service.d.ts +53 -0
- package/dist/types/server/store.d.ts +85 -0
- package/dist/types/server/validate.d.ts +20 -0
- package/dist/types/shared/types.d.ts +117 -0
- package/dist/types/store/d1.d.ts +9 -0
- package/dist/types/store/drizzle/index.d.ts +22 -0
- package/dist/types/store/drizzle/mysql.d.ts +700 -0
- package/dist/types/store/drizzle/pg.d.ts +659 -0
- package/dist/types/store/drizzle/sqlite.d.ts +700 -0
- package/dist/types/store/memory.d.ts +10 -0
- package/dist/types/store/r2.d.ts +20 -0
- package/dist/types/store/s3.d.ts +21 -0
- package/dist/types/widget/anchor.d.ts +9 -0
- package/dist/types/widget/api.d.ts +43 -0
- package/dist/types/widget/app.d.ts +29 -0
- package/dist/types/widget/capture.d.ts +33 -0
- package/dist/types/widget/format.d.ts +15 -0
- package/dist/types/widget/index.d.ts +41 -0
- package/dist/types/widget/styles.d.ts +6 -0
- package/dist/types/widget/thread.d.ts +32 -0
- package/dist/types/widget/tracker.d.ts +68 -0
- package/dist/widget.global.js +52 -0
- package/package.json +144 -6
- package/types/widget.global.d.ts +5 -0
- package/index.d.ts +0 -2
- package/index.js +0 -6
package/dist/react.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/react.ts
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { mountComments } from "web-comments";
|
|
4
|
+
function Comments(props) {
|
|
5
|
+
const latest = useRef(props);
|
|
6
|
+
latest.current = props;
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const handle = mountComments({
|
|
9
|
+
...latest.current,
|
|
10
|
+
page: () => latest.current.page?.() ?? location.pathname,
|
|
11
|
+
headers: async () => await latest.current.headers?.() ?? {}
|
|
12
|
+
});
|
|
13
|
+
return () => handle.unmount();
|
|
14
|
+
}, []);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
Comments
|
|
19
|
+
};
|
package/dist/server.js
ADDED
package/dist/store/d1.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
drizzleStore
|
|
3
|
+
} from "../chunks/chunk-T4WN3STA.js";
|
|
4
|
+
import "../chunks/chunk-7K7K7M5O.js";
|
|
5
|
+
import "../chunks/chunk-WY5E2C47.js";
|
|
6
|
+
import "../chunks/chunk-644PTNSO.js";
|
|
7
|
+
import "../chunks/chunk-WSFCRVEQ.js";
|
|
8
|
+
|
|
9
|
+
// src/store/d1.ts
|
|
10
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
11
|
+
function d1Store(binding, options = {}) {
|
|
12
|
+
return drizzleStore({ db: drizzle(binding), dialect: "sqlite", ...options });
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
d1Store
|
|
16
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
comments,
|
|
3
|
+
createTablesSql,
|
|
4
|
+
links,
|
|
5
|
+
threads,
|
|
6
|
+
users,
|
|
7
|
+
values
|
|
8
|
+
} from "../../chunks/chunk-WY5E2C47.js";
|
|
9
|
+
import "../../chunks/chunk-WSFCRVEQ.js";
|
|
10
|
+
export {
|
|
11
|
+
comments,
|
|
12
|
+
createTablesSql,
|
|
13
|
+
links,
|
|
14
|
+
threads,
|
|
15
|
+
users,
|
|
16
|
+
values
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
comments,
|
|
3
|
+
createTablesSql,
|
|
4
|
+
links,
|
|
5
|
+
threads,
|
|
6
|
+
users,
|
|
7
|
+
values
|
|
8
|
+
} from "../../chunks/chunk-7K7K7M5O.js";
|
|
9
|
+
import "../../chunks/chunk-WSFCRVEQ.js";
|
|
10
|
+
export {
|
|
11
|
+
comments,
|
|
12
|
+
createTablesSql,
|
|
13
|
+
links,
|
|
14
|
+
threads,
|
|
15
|
+
users,
|
|
16
|
+
values
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
comments,
|
|
3
|
+
createTablesSql,
|
|
4
|
+
links,
|
|
5
|
+
threads,
|
|
6
|
+
users,
|
|
7
|
+
values
|
|
8
|
+
} from "../../chunks/chunk-644PTNSO.js";
|
|
9
|
+
import "../../chunks/chunk-WSFCRVEQ.js";
|
|
10
|
+
export {
|
|
11
|
+
comments,
|
|
12
|
+
createTablesSql,
|
|
13
|
+
links,
|
|
14
|
+
threads,
|
|
15
|
+
users,
|
|
16
|
+
values
|
|
17
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import "../chunks/chunk-WSFCRVEQ.js";
|
|
2
|
+
|
|
3
|
+
// src/store/memory.ts
|
|
4
|
+
function memoryStore() {
|
|
5
|
+
const threads = /* @__PURE__ */ new Map();
|
|
6
|
+
const comments = /* @__PURE__ */ new Map();
|
|
7
|
+
const links = /* @__PURE__ */ new Map();
|
|
8
|
+
const values = /* @__PURE__ */ new Map();
|
|
9
|
+
const linkKey = (site, connector, externalId) => JSON.stringify([site, connector, externalId]);
|
|
10
|
+
function withComments(thread) {
|
|
11
|
+
const own = [...comments.values()].filter((c) => c.threadId === thread.id).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
12
|
+
return structuredClone({ ...thread, comments: own });
|
|
13
|
+
}
|
|
14
|
+
function find(site, id) {
|
|
15
|
+
const thread = threads.get(id);
|
|
16
|
+
return thread && thread.site === site ? thread : void 0;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
async listThreads(site, { page, limit }) {
|
|
20
|
+
const list = [...threads.values()].filter(
|
|
21
|
+
(t) => t.site === site && (page === void 0 || t.page === page)
|
|
22
|
+
);
|
|
23
|
+
list.sort(
|
|
24
|
+
(a, b) => page === void 0 ? b.createdAt.localeCompare(a.createdAt) : a.createdAt.localeCompare(b.createdAt)
|
|
25
|
+
);
|
|
26
|
+
return list.slice(0, limit).map(withComments);
|
|
27
|
+
},
|
|
28
|
+
async getThread(site, id) {
|
|
29
|
+
const thread = find(site, id);
|
|
30
|
+
return thread ? withComments(thread) : null;
|
|
31
|
+
},
|
|
32
|
+
async insertThread(thread, first) {
|
|
33
|
+
const { comments: _, ...rest } = structuredClone(thread);
|
|
34
|
+
threads.set(thread.id, rest);
|
|
35
|
+
comments.set(first.id, structuredClone(first));
|
|
36
|
+
},
|
|
37
|
+
async insertComment(site, comment) {
|
|
38
|
+
if (find(site, comment.threadId)) comments.set(comment.id, structuredClone(comment));
|
|
39
|
+
},
|
|
40
|
+
async updateThread(site, id, changes) {
|
|
41
|
+
const thread = find(site, id);
|
|
42
|
+
if (thread) Object.assign(thread, structuredClone(changes));
|
|
43
|
+
},
|
|
44
|
+
async getComment(site, id) {
|
|
45
|
+
const comment = comments.get(id);
|
|
46
|
+
return comment && find(site, comment.threadId) ? structuredClone(comment) : null;
|
|
47
|
+
},
|
|
48
|
+
async deleteComment(site, id) {
|
|
49
|
+
const comment = comments.get(id);
|
|
50
|
+
if (comment && find(site, comment.threadId)) comments.delete(id);
|
|
51
|
+
},
|
|
52
|
+
async deleteThread(site, id) {
|
|
53
|
+
if (!find(site, id)) return;
|
|
54
|
+
threads.delete(id);
|
|
55
|
+
for (const [key, comment] of comments) if (comment.threadId === id) comments.delete(key);
|
|
56
|
+
for (const [key, link] of links) {
|
|
57
|
+
if (link.site === site && link.threadId === id) links.delete(key);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async saveLink(site, link) {
|
|
61
|
+
links.set(linkKey(site, link.connector, link.externalId), { ...structuredClone(link), site });
|
|
62
|
+
},
|
|
63
|
+
async findLink(site, connector, externalId) {
|
|
64
|
+
const link = links.get(linkKey(site, connector, externalId));
|
|
65
|
+
if (!link) return null;
|
|
66
|
+
const { site: _, ...rest } = link;
|
|
67
|
+
return structuredClone(rest);
|
|
68
|
+
},
|
|
69
|
+
async listLinks(site, threadIds) {
|
|
70
|
+
const wanted = new Set(threadIds);
|
|
71
|
+
return [...links.values()].filter((link) => link.site === site && wanted.has(link.threadId)).map(({ site: _, ...rest }) => structuredClone(rest));
|
|
72
|
+
},
|
|
73
|
+
async getValue(name) {
|
|
74
|
+
const entry = values.get(name);
|
|
75
|
+
if (!entry) return null;
|
|
76
|
+
if (entry.expiresAt !== null && entry.expiresAt <= (/* @__PURE__ */ new Date()).toISOString()) {
|
|
77
|
+
values.delete(name);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return entry.value;
|
|
81
|
+
},
|
|
82
|
+
async setValue(name, value, expiresAt) {
|
|
83
|
+
values.set(name, { value, expiresAt });
|
|
84
|
+
},
|
|
85
|
+
async deleteValue(name) {
|
|
86
|
+
values.delete(name);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function memoryAssets() {
|
|
91
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
92
|
+
return {
|
|
93
|
+
async put(key, body, contentType) {
|
|
94
|
+
blobs.set(key, { body: body.slice(0), contentType });
|
|
95
|
+
},
|
|
96
|
+
async get(key) {
|
|
97
|
+
const blob = blobs.get(key);
|
|
98
|
+
return blob ? { body: blob.body.slice(0), contentType: blob.contentType } : null;
|
|
99
|
+
},
|
|
100
|
+
async delete(key) {
|
|
101
|
+
blobs.delete(key);
|
|
102
|
+
},
|
|
103
|
+
size: () => blobs.size
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
memoryAssets,
|
|
108
|
+
memoryStore
|
|
109
|
+
};
|
package/dist/store/r2.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "../chunks/chunk-WSFCRVEQ.js";
|
|
2
|
+
|
|
3
|
+
// src/store/r2.ts
|
|
4
|
+
function r2Assets(bucket, options = {}) {
|
|
5
|
+
const prefix = options.prefix ?? "";
|
|
6
|
+
return {
|
|
7
|
+
async put(key, body, contentType) {
|
|
8
|
+
await bucket.put(prefix + key, body, { httpMetadata: { contentType } });
|
|
9
|
+
},
|
|
10
|
+
async get(key) {
|
|
11
|
+
const object = await bucket.get(prefix + key);
|
|
12
|
+
if (!object) return null;
|
|
13
|
+
return {
|
|
14
|
+
body: object.body,
|
|
15
|
+
contentType: object.httpMetadata?.contentType ?? "application/octet-stream"
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
async delete(key) {
|
|
19
|
+
await bucket.delete(prefix + key);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
r2Assets
|
|
25
|
+
};
|
package/dist/store/s3.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import "../chunks/chunk-WSFCRVEQ.js";
|
|
2
|
+
|
|
3
|
+
// src/store/s3.ts
|
|
4
|
+
import { AwsClient } from "aws4fetch";
|
|
5
|
+
function s3Assets(options) {
|
|
6
|
+
const client = new AwsClient({
|
|
7
|
+
accessKeyId: options.accessKeyId,
|
|
8
|
+
secretAccessKey: options.secretAccessKey,
|
|
9
|
+
region: options.region ?? "auto",
|
|
10
|
+
service: "s3"
|
|
11
|
+
});
|
|
12
|
+
const base = `${options.endpoint.replace(/\/+$/, "")}/${encodeURIComponent(options.bucket)}/`;
|
|
13
|
+
const urlFor = (key) => base + `${options.prefix ?? ""}${key}`.split("/").map(encodeURIComponent).join("/");
|
|
14
|
+
return {
|
|
15
|
+
async put(key, body, contentType) {
|
|
16
|
+
const response = await client.fetch(urlFor(key), {
|
|
17
|
+
method: "PUT",
|
|
18
|
+
body,
|
|
19
|
+
headers: { "content-type": contentType }
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) throw new Error(`S3 PUT ${key} answered ${response.status}`);
|
|
22
|
+
},
|
|
23
|
+
async get(key) {
|
|
24
|
+
const response = await client.fetch(urlFor(key));
|
|
25
|
+
if (response.status === 404) return null;
|
|
26
|
+
if (!response.ok || !response.body)
|
|
27
|
+
throw new Error(`S3 GET ${key} answered ${response.status}`);
|
|
28
|
+
return {
|
|
29
|
+
body: response.body,
|
|
30
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream"
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
async delete(key) {
|
|
34
|
+
const response = await client.fetch(urlFor(key), { method: "DELETE" });
|
|
35
|
+
if (!response.ok && response.status !== 404) {
|
|
36
|
+
throw new Error(`S3 DELETE ${key} answered ${response.status}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
s3Assets
|
|
43
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuthPlugin, Identity } from "../server/auth";
|
|
2
|
+
export interface BearerTokenEntry {
|
|
3
|
+
/** At least 24 characters. Generate one with `openssl rand -base64 32`. */
|
|
4
|
+
token: string;
|
|
5
|
+
name: string;
|
|
6
|
+
kind?: Identity["kind"];
|
|
7
|
+
email?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Identifies callers by `Authorization: Bearer <token>`: agents, MCP clients,
|
|
11
|
+
* scripts. Tokens usually live in a secret, so they may come from env.
|
|
12
|
+
*/
|
|
13
|
+
export declare function bearerToken<Env = unknown>(tokens: BearerTokenEntry[] | ((env: Env) => BearerTokenEntry[])): AuthPlugin<Env>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type JWTVerifyGetKey } from "jose";
|
|
2
|
+
import type { AuthPlugin, Identity } from "../server/auth";
|
|
3
|
+
export interface CloudflareAccessOptions {
|
|
4
|
+
/** Your Zero Trust team domain, e.g. "acme.cloudflareaccess.com". */
|
|
5
|
+
teamDomain: string;
|
|
6
|
+
/** The Access application's Audience (AUD) tag. Several, if one Worker sits behind more than one app. */
|
|
7
|
+
audience: string | string[];
|
|
8
|
+
/**
|
|
9
|
+
* Service tokens that may call the API, keyed by their Client ID. Access
|
|
10
|
+
* lets them through as tokens, not people; this names them and marks them
|
|
11
|
+
* as agents unless told otherwise.
|
|
12
|
+
*/
|
|
13
|
+
serviceTokens?: Record<string, {
|
|
14
|
+
name: string;
|
|
15
|
+
kind?: Identity["kind"];
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Ask Access for the person's display name (one request per person per
|
|
19
|
+
* hour). Without it the name comes from the email address.
|
|
20
|
+
*/
|
|
21
|
+
fetchIdentity?: boolean;
|
|
22
|
+
/** Replaces the team's published signing keys. For tests. */
|
|
23
|
+
keys?: JWTVerifyGetKey;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Identifies people (and service tokens) that Cloudflare Access let through.
|
|
27
|
+
* Access puts a signed JWT on every request it forwards; this checks the
|
|
28
|
+
* signature, issuer and audience before trusting it. Options may come from
|
|
29
|
+
* env, as Worker configuration usually does.
|
|
30
|
+
*/
|
|
31
|
+
export declare function cloudflareAccess<Env = unknown>(options: CloudflareAccessOptions | ((env: Env) => CloudflareAccessOptions)): AuthPlugin<Env>;
|
|
32
|
+
/** "ada.lovelace@example.com" → "Ada Lovelace" */
|
|
33
|
+
export declare function nameFromEmail(email: string): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AuthPlugin, Identity } from "../server/auth";
|
|
2
|
+
/**
|
|
3
|
+
* Treats every request as one fixed person. For local development only:
|
|
4
|
+
* anyone who can reach the API becomes that person.
|
|
5
|
+
*/
|
|
6
|
+
export declare function devUser(identity?: Partial<Identity>): AuthPlugin;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AuthPlugin, Identity } from "../server/auth";
|
|
2
|
+
export interface SessionEndpointOptions<Env> {
|
|
3
|
+
/**
|
|
4
|
+
* Your app's "who is signed in?" endpoint. It gets the request's
|
|
5
|
+
* credentials (see `forward`) and answers with the person, or 401.
|
|
6
|
+
*/
|
|
7
|
+
url: string | URL | ((request: Request, env: Env) => string | URL);
|
|
8
|
+
/** Request headers passed along to it. Default: cookie and authorization. */
|
|
9
|
+
forward?: string[];
|
|
10
|
+
/** Turns its JSON answer into an Identity. Return null to turn the person away. */
|
|
11
|
+
identity: (body: unknown, request: Request) => Identity | null | Promise<Identity | null>;
|
|
12
|
+
/** How long an answer is reused for the same credentials, in ms. Default 30 000; 0 turns it off. */
|
|
13
|
+
cacheFor?: number;
|
|
14
|
+
/** Where to send people who aren't signed in. */
|
|
15
|
+
loginUrl?: (request: Request) => string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Identifies people by asking an endpoint your app already has, with the
|
|
19
|
+
* request's own cookies or token: any sign-in system that can say who is
|
|
20
|
+
* signed in works, without web-comments knowing how it signs people in.
|
|
21
|
+
*
|
|
22
|
+
* A 401, 403 or redirect means "not signed in" (the next plugin gets a go);
|
|
23
|
+
* any other failure is an error rather than a silent sign-out.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sessionEndpoint<Env = unknown>(options: SessionEndpointOptions<Env>): AuthPlugin<Env>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Connector, Thread } from "../../server";
|
|
2
|
+
export interface LinearOptions {
|
|
3
|
+
/**
|
|
4
|
+
* How to call Linear:
|
|
5
|
+
* - `{ apiKey }`: a personal API key (lin_api_...). Issues and comments are
|
|
6
|
+
* that person's; who said what is written into them.
|
|
7
|
+
* - `{ accessToken }`: an OAuth token.
|
|
8
|
+
* - `{ clientId, clientSecret }`: an OAuth app with client credentials
|
|
9
|
+
* enabled. Issues and comments are the app's, shown under each
|
|
10
|
+
* commenter's name and picture.
|
|
11
|
+
*/
|
|
12
|
+
auth: {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
} | {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
} | {
|
|
17
|
+
clientId: string;
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
};
|
|
20
|
+
/** The team new issues go to: its key ("ENG") or id. */
|
|
21
|
+
team: string;
|
|
22
|
+
/**
|
|
23
|
+
* The signing secret of a Linear webhook (Issues and Comments) pointed at
|
|
24
|
+
* `{basePath}/linear/webhook`. With it, Linear's side comes back: comments
|
|
25
|
+
* there become replies, and finishing or reopening the issue resolves or
|
|
26
|
+
* reopens the thread.
|
|
27
|
+
*/
|
|
28
|
+
webhookSecret?: string;
|
|
29
|
+
/** Labels to put on new issues, by id. */
|
|
30
|
+
labelIds?: string[];
|
|
31
|
+
/** The project new issues go into, by id. */
|
|
32
|
+
projectId?: string;
|
|
33
|
+
/** Which threads become issues. All of them, by default. */
|
|
34
|
+
filter?: (thread: Thread) => boolean;
|
|
35
|
+
/** Linear's GraphQL endpoint; for tests. */
|
|
36
|
+
apiUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every thread becomes a Linear issue, with its screenshots, and stays in
|
|
40
|
+
* step with it: replies go both ways, and resolving one resolves the other.
|
|
41
|
+
* Options can depend on the environment; returning null leaves Linear out
|
|
42
|
+
* of that one (say, until its secrets are set).
|
|
43
|
+
*
|
|
44
|
+
* connectors: [linear((env) => ({ auth: { apiKey: env.LINEAR_API_KEY }, team: "ENG", webhookSecret: env.LINEAR_WEBHOOK_SECRET }))]
|
|
45
|
+
*/
|
|
46
|
+
export declare function linear<Env = unknown>(options: LinearOptions | ((env: Env) => LinearOptions | null)): Connector<Env>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Connector } from "../../server";
|
|
2
|
+
export interface McpOptions {
|
|
3
|
+
/** What the server calls itself, to MCP clients and on the approval page. Default "web-comments". */
|
|
4
|
+
name?: string;
|
|
5
|
+
/** How long an access token lasts, in seconds. Default an hour. */
|
|
6
|
+
accessTokenTtl?: number;
|
|
7
|
+
/** How long a refresh token lasts unused, in seconds. Default 30 days. */
|
|
8
|
+
refreshTokenTtl?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An MCP server for comments, at `{basePath}/mcp`: agents list threads, read
|
|
12
|
+
* one with its screenshots and the element it's on, reply, and resolve or
|
|
13
|
+
* reopen it. MCP clients sign in with OAuth: the person approving one is
|
|
14
|
+
* whoever the API already recognises in their browser, and the agent then
|
|
15
|
+
* acts as them. Requests the auth plugins recognise directly (a
|
|
16
|
+
* `bearerToken`, an Access service token) are let in too.
|
|
17
|
+
*
|
|
18
|
+
* connectors: [mcp()]
|
|
19
|
+
* claude mcp add --transport http comments https://app.example.com/__comments/mcp
|
|
20
|
+
*/
|
|
21
|
+
export declare function mcp<Env = unknown>(options?: McpOptions): Connector<Env>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ConnectorRequestContext, ConnectorStorage, Identity } from "../../server";
|
|
2
|
+
export declare const SCOPE = "comments";
|
|
3
|
+
export interface OAuthOptions {
|
|
4
|
+
/** What people approving a client are told they're signing in to. */
|
|
5
|
+
name: string;
|
|
6
|
+
accessTokenTtl: number;
|
|
7
|
+
refreshTokenTtl: number;
|
|
8
|
+
}
|
|
9
|
+
/** What a token stands for. */
|
|
10
|
+
export interface Grant {
|
|
11
|
+
identity: Identity;
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientName: string;
|
|
14
|
+
site: string;
|
|
15
|
+
resource: string;
|
|
16
|
+
scope: string;
|
|
17
|
+
}
|
|
18
|
+
/** Where everything is, for one request's origin. */
|
|
19
|
+
export declare function endpoints(base: string): {
|
|
20
|
+
resource: string;
|
|
21
|
+
issuer: string;
|
|
22
|
+
authorize: string;
|
|
23
|
+
token: string;
|
|
24
|
+
register: string;
|
|
25
|
+
revoke: string;
|
|
26
|
+
jwks: string;
|
|
27
|
+
resourceMetadata: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const CORS: {
|
|
30
|
+
"access-control-allow-origin": string;
|
|
31
|
+
"access-control-allow-methods": string;
|
|
32
|
+
"access-control-allow-headers": string;
|
|
33
|
+
"access-control-expose-headers": string;
|
|
34
|
+
"access-control-max-age": string;
|
|
35
|
+
};
|
|
36
|
+
/** RFC 9728: which authorization server guards the MCP endpoint. */
|
|
37
|
+
export declare function protectedResourceMetadata(base: string): Response;
|
|
38
|
+
/**
|
|
39
|
+
* RFC 8414, also served as OpenID discovery, which is the one place under
|
|
40
|
+
* the API's own path a client looks. This server issues no ID tokens; the
|
|
41
|
+
* JWKS and ID token fields are there because OIDC discovery parsers require
|
|
42
|
+
* them.
|
|
43
|
+
*/
|
|
44
|
+
export declare function authorizationServerMetadata(base: string, oidc: boolean): Response;
|
|
45
|
+
export declare function jwks(): Response;
|
|
46
|
+
/** The grant behind an access token, or null if it isn't a live one. */
|
|
47
|
+
export declare function grantOf(storage: ConnectorStorage, token: string): Promise<Grant | null>;
|
|
48
|
+
export declare function looksLikeOurToken(token: string): boolean;
|
|
49
|
+
/** Answers the OAuth paths under the connector's base; null for anything else. */
|
|
50
|
+
export declare function handleOAuth(request: Request, path: string, context: ConnectorRequestContext, options: OAuthOptions): Promise<Response | null>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Identity } from "../../server";
|
|
2
|
+
export declare function consentPage(input: {
|
|
3
|
+
product: string;
|
|
4
|
+
host: string;
|
|
5
|
+
app: string;
|
|
6
|
+
appHost: string;
|
|
7
|
+
who: Identity;
|
|
8
|
+
action: string;
|
|
9
|
+
consentId: string;
|
|
10
|
+
}): string;
|
|
11
|
+
export declare function messagePage(product: string, title: string, message: string, link?: {
|
|
12
|
+
label: string;
|
|
13
|
+
href: string;
|
|
14
|
+
}): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type MountOptions } from "web-comments";
|
|
2
|
+
export type { CommentsHandle, MountOptions } from "web-comments";
|
|
3
|
+
/**
|
|
4
|
+
* Mounts the comments toolbar while rendered. Renders nothing itself.
|
|
5
|
+
* `page` and `headers` may change between renders; the other options are
|
|
6
|
+
* read once, when it mounts.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Comments(props: MountOptions): null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Identity } from "../shared/types";
|
|
2
|
+
export type { Identity };
|
|
3
|
+
/**
|
|
4
|
+
* Answers "who is this?" for a request. Return null when the request isn't
|
|
5
|
+
* yours to judge (no header, no cookie), so the next plugin can try.
|
|
6
|
+
* Throw only for real failures; a bad credential should also return null.
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthPlugin<Env = unknown> {
|
|
9
|
+
name: string;
|
|
10
|
+
authenticate(request: Request, env: Env): Promise<Identity | null>;
|
|
11
|
+
/** Where to send someone no plugin recognised. */
|
|
12
|
+
loginUrl?(request: Request): string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/** Runs the plugins in order and returns the first identity any of them reports. */
|
|
15
|
+
export declare function identify<Env>(plugins: readonly AuthPlugin<Env>[], request: Request, env: Env): Promise<Identity | null>;
|
|
16
|
+
export declare function loginUrlFor<Env>(plugins: readonly AuthPlugin<Env>[], request: Request): string | undefined;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Comment, Identity, ScreenshotKind, Thread, ThreadStatus } from "../shared/types";
|
|
2
|
+
import type { CommentsEvent } from "./events";
|
|
3
|
+
import type { ThreadLink } from "./store";
|
|
4
|
+
/**
|
|
5
|
+
* Takes comments somewhere else, or lets something else in: a Linear issue
|
|
6
|
+
* per thread, an MCP server for agents, a Slack channel, ... Connectors see
|
|
7
|
+
* every change as it happens, can answer requests of their own under
|
|
8
|
+
* `{basePath}/{name}`, and act on comments by the same rules as the API.
|
|
9
|
+
*
|
|
10
|
+
* Built in: `web-comments/connectors/linear` and `web-comments/connectors/mcp`.
|
|
11
|
+
*/
|
|
12
|
+
export interface Connector<Env = unknown> {
|
|
13
|
+
/** Lowercase, unique: it names the connector's routes, links and stored state. */
|
|
14
|
+
name: string;
|
|
15
|
+
/**
|
|
16
|
+
* After every change, once the response has gone out. Changes this
|
|
17
|
+
* connector made itself (through `as()`) aren't sent back to it.
|
|
18
|
+
*/
|
|
19
|
+
onEvent?(event: CommentsEvent, context: ConnectorContext<Env>): void | Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Requests under `{basePath}/{name}` (and `rootPaths`). Nobody is signed in
|
|
22
|
+
* for them: check the caller yourself (a webhook's signature, a token), or
|
|
23
|
+
* ask `context.identify()` for the visitor the API itself would see.
|
|
24
|
+
*/
|
|
25
|
+
fetch?(request: Request, context: ConnectorRequestContext<Env>): Response | Promise<Response>;
|
|
26
|
+
/**
|
|
27
|
+
* Paths at the root of the origin this connector answers too, such as
|
|
28
|
+
* OAuth's `/.well-known/...` documents. They only arrive if the host
|
|
29
|
+
* sends them (see `matches()`).
|
|
30
|
+
*/
|
|
31
|
+
rootPaths?(basePath: string): string[];
|
|
32
|
+
}
|
|
33
|
+
export interface ConnectorContext<Env = unknown> {
|
|
34
|
+
env: Env;
|
|
35
|
+
site: string;
|
|
36
|
+
/** Where the API is mounted, e.g. "/__comments". */
|
|
37
|
+
basePath: string;
|
|
38
|
+
/**
|
|
39
|
+
* Acts on comments as someone, by the API's rules (only authors delete
|
|
40
|
+
* their comments, and so on). Changes are announced like the API's own,
|
|
41
|
+
* with this connector as their `source`.
|
|
42
|
+
*/
|
|
43
|
+
as(actor: Identity): CommentsActions;
|
|
44
|
+
/** Links between threads and what this connector keeps elsewhere. */
|
|
45
|
+
links: {
|
|
46
|
+
find(externalId: string): Promise<ThreadLink | null>;
|
|
47
|
+
forThread(threadId: string): Promise<ThreadLink[]>;
|
|
48
|
+
save(link: {
|
|
49
|
+
externalId: string;
|
|
50
|
+
threadId: string;
|
|
51
|
+
commentId?: string | null;
|
|
52
|
+
url?: string | null;
|
|
53
|
+
title?: string | null;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
/** State of this connector's own, by key (kept apart from other connectors'). */
|
|
57
|
+
storage: ConnectorStorage;
|
|
58
|
+
/** A thread's screenshot, straight from the asset store. */
|
|
59
|
+
screenshot(thread: Thread, kind: ScreenshotKind): Promise<{
|
|
60
|
+
body: ArrayBuffer;
|
|
61
|
+
contentType: string;
|
|
62
|
+
} | null>;
|
|
63
|
+
/** The page the thread is on, with the thread open. */
|
|
64
|
+
threadUrl(thread: Thread): string;
|
|
65
|
+
/** Keeps the request alive for work that finishes after the response. */
|
|
66
|
+
waitUntil(work: Promise<unknown>): void;
|
|
67
|
+
}
|
|
68
|
+
export interface ConnectorRequestContext<Env = unknown> extends ConnectorContext<Env> {
|
|
69
|
+
/** This connector's own address: `{origin}{basePath}/{name}`. */
|
|
70
|
+
url: string;
|
|
71
|
+
/**
|
|
72
|
+
* Who a request is from by the API's own sign-in (its auth plugins and
|
|
73
|
+
* `authorize`), or null: e.g. the person approving an OAuth client.
|
|
74
|
+
*/
|
|
75
|
+
identify(request: Request): Promise<Identity | null>;
|
|
76
|
+
/** Where the auth plugins send someone to sign in, if they know. */
|
|
77
|
+
loginUrl(request: Request): string | undefined;
|
|
78
|
+
}
|
|
79
|
+
export interface ConnectorStorage {
|
|
80
|
+
get(key: string): Promise<string | null>;
|
|
81
|
+
set(key: string, value: string, options?: {
|
|
82
|
+
ttlSeconds?: number;
|
|
83
|
+
}): Promise<void>;
|
|
84
|
+
delete(key: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
/** What a connector may do with comments, as whoever it acts for. */
|
|
87
|
+
export interface CommentsActions {
|
|
88
|
+
/** Threads on one page (oldest first), or the site's most recent (newest first). */
|
|
89
|
+
listThreads(query?: {
|
|
90
|
+
page?: string;
|
|
91
|
+
status?: ThreadStatus;
|
|
92
|
+
limit?: number;
|
|
93
|
+
}): Promise<Thread[]>;
|
|
94
|
+
getThread(id: string): Promise<Thread | null>;
|
|
95
|
+
reply(threadId: string, body: string): Promise<{
|
|
96
|
+
thread: Thread;
|
|
97
|
+
comment: Comment;
|
|
98
|
+
}>;
|
|
99
|
+
setStatus(threadId: string, status: ThreadStatus): Promise<Thread>;
|
|
100
|
+
/** Deleting the comment that opens a thread deletes the thread. */
|
|
101
|
+
deleteComment(commentId: string): Promise<void>;
|
|
102
|
+
}
|