web-comments 0.0.1 → 0.1.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 +188 -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-6VWNCJ3A.js +72 -0
- package/dist/chunks/chunk-GHNSLFY2.js +72 -0
- package/dist/chunks/chunk-LBIURYPH.js +192 -0
- package/dist/chunks/chunk-PLAC65HG.js +71 -0
- package/dist/chunks/chunk-WSFCRVEQ.js +9 -0
- package/dist/index.js +44 -0
- package/dist/react.js +19 -0
- package/dist/server.js +501 -0
- package/dist/store/d1.js +16 -0
- package/dist/store/drizzle/mysql.js +13 -0
- package/dist/store/drizzle/pg.js +13 -0
- package/dist/store/drizzle/sqlite.js +13 -0
- package/dist/store/drizzle.js +10 -0
- package/dist/store/memory.js +75 -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/react.d.ts +8 -0
- package/dist/types/server/auth.d.ts +16 -0
- package/dist/types/server/index.d.ts +71 -0
- package/dist/types/server/store.d.ts +59 -0
- package/dist/types/server/validate.d.ts +20 -0
- package/dist/types/shared/types.d.ts +108 -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 +506 -0
- package/dist/types/store/drizzle/pg.d.ts +476 -0
- package/dist/types/store/drizzle/sqlite.d.ts +506 -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 +44 -0
- package/package.json +132 -6
- package/types/widget.global.d.ts +5 -0
- package/index.d.ts +0 -2
- package/index.js +0 -6
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,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,71 @@
|
|
|
1
|
+
import type { Comment, Identity, Thread } from "../shared/types";
|
|
2
|
+
import { type AuthPlugin } from "./auth";
|
|
3
|
+
import type { AssetStore, CommentsStore } from "./store";
|
|
4
|
+
export type * from "../shared/types";
|
|
5
|
+
export type { AuthPlugin } from "./auth";
|
|
6
|
+
export type { AssetStore, CommentsStore, StoredScreenshot, StoredThread, ThreadChanges, } from "./store";
|
|
7
|
+
export type CommentsEvent = {
|
|
8
|
+
type: "thread.created";
|
|
9
|
+
thread: Thread;
|
|
10
|
+
actor: Identity;
|
|
11
|
+
} | {
|
|
12
|
+
type: "comment.created";
|
|
13
|
+
thread: Thread;
|
|
14
|
+
comment: Comment;
|
|
15
|
+
actor: Identity;
|
|
16
|
+
} | {
|
|
17
|
+
type: "thread.resolved" | "thread.reopened" | "thread.moved";
|
|
18
|
+
thread: Thread;
|
|
19
|
+
actor: Identity;
|
|
20
|
+
} | {
|
|
21
|
+
type: "comment.deleted";
|
|
22
|
+
threadId: string;
|
|
23
|
+
commentId: string;
|
|
24
|
+
actor: Identity;
|
|
25
|
+
} | {
|
|
26
|
+
type: "thread.deleted";
|
|
27
|
+
threadId: string;
|
|
28
|
+
actor: Identity;
|
|
29
|
+
};
|
|
30
|
+
export interface CommentsOptions<Env extends object> {
|
|
31
|
+
/** Tried in order; the first plugin that recognises the request wins. */
|
|
32
|
+
auth: AuthPlugin<Env>[];
|
|
33
|
+
/** Where comments are kept, e.g. `(env) => d1Store(env.DB)`. Any CommentsStore works. */
|
|
34
|
+
store: (env: Env) => CommentsStore;
|
|
35
|
+
/** Where screenshots are kept, e.g. `(env) => r2Assets(env.SCREENSHOTS)`. Without it, threads keep none. */
|
|
36
|
+
assets?: (env: Env) => AssetStore;
|
|
37
|
+
/** Who may use comments once identified. Everyone identified, by default. */
|
|
38
|
+
authorize?: (who: Identity, request: Request, env: Env) => boolean | Promise<boolean>;
|
|
39
|
+
/** Where the API is mounted. Default "/__comments". */
|
|
40
|
+
basePath?: string;
|
|
41
|
+
/** Keeps several sites apart in one store. Default "default". */
|
|
42
|
+
site?: (request: Request, env: Env) => string;
|
|
43
|
+
/** Origins other than the API's own that may call it with credentials. */
|
|
44
|
+
allowedOrigins?: string[];
|
|
45
|
+
/** The widget bundle (dist/widget.global.js) to serve at `{basePath}/widget.js`. */
|
|
46
|
+
widgetScript?: string;
|
|
47
|
+
/** Largest screenshot accepted, in bytes. Default 5 MB. */
|
|
48
|
+
maxScreenshotBytes?: number;
|
|
49
|
+
/** Called after every change, for integrations. Errors are logged, never returned. */
|
|
50
|
+
onEvent?: (event: CommentsEvent, env: Env) => void | Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
interface ExecutionContextLike {
|
|
53
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
54
|
+
}
|
|
55
|
+
type Vars = {
|
|
56
|
+
who: Identity;
|
|
57
|
+
site: string;
|
|
58
|
+
store: CommentsStore;
|
|
59
|
+
assets: AssetStore | null;
|
|
60
|
+
};
|
|
61
|
+
export declare function newId(): string;
|
|
62
|
+
export declare function createComments<Env extends object = object>(options: CommentsOptions<Env>): {
|
|
63
|
+
app: import("hono/hono-base").HonoBase<{
|
|
64
|
+
Bindings: Env;
|
|
65
|
+
Variables: Vars;
|
|
66
|
+
}, import("hono/types").BlankSchema, string, string>;
|
|
67
|
+
/** Answers requests under basePath; anything else gets a 404. */
|
|
68
|
+
fetch(request: Request, env: Env, ctx?: ExecutionContextLike): Promise<Response>;
|
|
69
|
+
/** Whether a request belongs to the comments API, to route it from a larger worker. */
|
|
70
|
+
matches(request: Request): boolean;
|
|
71
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Comment, Identity, ScreenshotKind, Thread, ThreadStatus } from "../shared/types";
|
|
2
|
+
/**
|
|
3
|
+
* Where comments are kept. The service owns the rules (who may do what, ids,
|
|
4
|
+
* timestamps, what deleting the first comment means); a store only saves and
|
|
5
|
+
* loads. Implement it over anything: the built-in ones are Drizzle (SQLite/D1,
|
|
6
|
+
* Postgres, MySQL) and an in-memory store.
|
|
7
|
+
*
|
|
8
|
+
* Every lookup is scoped to a site, so one store can serve several.
|
|
9
|
+
*/
|
|
10
|
+
export interface CommentsStore {
|
|
11
|
+
/** Gets storage ready (e.g. creates tables). Called once before first use; optional. */
|
|
12
|
+
setup?(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Threads with their comments (oldest first). With a page: that page's
|
|
15
|
+
* threads, oldest first. Without: the site's most recent, newest first.
|
|
16
|
+
*/
|
|
17
|
+
listThreads(site: string, query: {
|
|
18
|
+
page?: string;
|
|
19
|
+
limit: number;
|
|
20
|
+
}): Promise<StoredThread[]>;
|
|
21
|
+
getThread(site: string, id: string): Promise<StoredThread | null>;
|
|
22
|
+
/** Saves a new thread together with the comment that opens it. */
|
|
23
|
+
insertThread(thread: StoredThread, first: Comment): Promise<void>;
|
|
24
|
+
insertComment(site: string, comment: Comment): Promise<void>;
|
|
25
|
+
updateThread(site: string, id: string, changes: ThreadChanges): Promise<void>;
|
|
26
|
+
getComment(site: string, id: string): Promise<Comment | null>;
|
|
27
|
+
deleteComment(site: string, id: string): Promise<void>;
|
|
28
|
+
/** Deletes a thread and all its comments. */
|
|
29
|
+
deleteThread(site: string, id: string): Promise<void>;
|
|
30
|
+
/** Remembers who has been here (for @mentions); optional. */
|
|
31
|
+
saveUser?(who: Identity, seenAt: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** A screenshot as stored: the asset key, not a URL. */
|
|
34
|
+
export interface StoredScreenshot {
|
|
35
|
+
key: string;
|
|
36
|
+
contentType: string;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}
|
|
40
|
+
/** A thread as a store keeps it. */
|
|
41
|
+
export interface StoredThread extends Omit<Thread, "screenshots"> {
|
|
42
|
+
site: string;
|
|
43
|
+
screenshots: Partial<Record<ScreenshotKind, StoredScreenshot>>;
|
|
44
|
+
}
|
|
45
|
+
export type ThreadChanges = Partial<Pick<StoredThread, "status" | "anchor" | "screenshots" | "updatedAt" | "resolvedAt" | "resolvedBy">>;
|
|
46
|
+
export type { ThreadStatus };
|
|
47
|
+
/**
|
|
48
|
+
* Where screenshots are kept. Implement it over anything that can hold bytes
|
|
49
|
+
* under a key; the built-ins are R2, S3 (and S3-compatible) and in-memory.
|
|
50
|
+
* The API serves them to people it has identified, so the bucket can stay private.
|
|
51
|
+
*/
|
|
52
|
+
export interface AssetStore {
|
|
53
|
+
put(key: string, body: ArrayBuffer, contentType: string): Promise<void>;
|
|
54
|
+
get(key: string): Promise<{
|
|
55
|
+
body: ReadableStream | ArrayBuffer;
|
|
56
|
+
contentType: string;
|
|
57
|
+
} | null>;
|
|
58
|
+
delete?(key: string): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Anchor, CreateThreadInput, ScreenshotKind, ThreadContext, ThreadStatus } from "../shared/types";
|
|
2
|
+
export declare class InvalidInput extends Error {
|
|
3
|
+
}
|
|
4
|
+
export declare function commentBody(value: unknown): string;
|
|
5
|
+
export declare function reply(value: unknown): string;
|
|
6
|
+
export declare function anchor(value: unknown): Anchor;
|
|
7
|
+
export declare function context(value: unknown): ThreadContext;
|
|
8
|
+
export declare const SCREENSHOT_KINDS: readonly ScreenshotKind[];
|
|
9
|
+
/** A new thread: what's said and where, plus the size of each screenshot sent with it. */
|
|
10
|
+
export declare function createThread(value: unknown): CreateThreadInput & {
|
|
11
|
+
screenshots: Partial<Record<ScreenshotKind, {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}>>;
|
|
15
|
+
};
|
|
16
|
+
/** A thread change: resolve or reopen it, move its pin, or both. */
|
|
17
|
+
export declare function threadUpdate(value: unknown): {
|
|
18
|
+
status?: ThreadStatus;
|
|
19
|
+
anchor?: Anchor;
|
|
20
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Who is making a request, as an auth plugin reports it. */
|
|
2
|
+
export interface Identity {
|
|
3
|
+
/** Stable and prefixed by its source, e.g. "access:<sub>" or "token:<name>". */
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
avatarUrl?: string;
|
|
8
|
+
kind: "human" | "agent";
|
|
9
|
+
}
|
|
10
|
+
/** The author of a comment as it was when the comment was written. */
|
|
11
|
+
export interface Author {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
avatarUrl?: string;
|
|
15
|
+
kind: "human" | "agent";
|
|
16
|
+
}
|
|
17
|
+
/** Where a comment sits on the page. */
|
|
18
|
+
export interface Anchor {
|
|
19
|
+
/** CSS selector of the element the comment was dropped on. */
|
|
20
|
+
selector: string;
|
|
21
|
+
/** Start of the element's text when the comment was made, to find it again if the selector breaks. */
|
|
22
|
+
text?: string;
|
|
23
|
+
/** Lowercase tag name of the element. */
|
|
24
|
+
tag: string;
|
|
25
|
+
/** Point inside the element's box, as fractions of its width and height (0–1). */
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
/** Viewport width when the comment was made, for context. */
|
|
29
|
+
viewportWidth: number;
|
|
30
|
+
}
|
|
31
|
+
/** What the commenter was looking at, recorded with the first comment. */
|
|
32
|
+
export interface ThreadContext {
|
|
33
|
+
element: {
|
|
34
|
+
/** The element's outerHTML, cut to a few kilobytes. */
|
|
35
|
+
html: string;
|
|
36
|
+
/** Its box in the viewport when the comment was made, in CSS pixels. */
|
|
37
|
+
rect: {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
page: {
|
|
45
|
+
title: string;
|
|
46
|
+
viewport: {
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
};
|
|
50
|
+
scroll: {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
};
|
|
54
|
+
devicePixelRatio: number;
|
|
55
|
+
userAgent: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Which screenshots a thread keeps: the element, and the viewport around it. */
|
|
59
|
+
export type ScreenshotKind = "element" | "page";
|
|
60
|
+
export interface Screenshot {
|
|
61
|
+
/** Where the API serves it (it checks who is asking first). */
|
|
62
|
+
url: string;
|
|
63
|
+
contentType: string;
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
}
|
|
67
|
+
export type ThreadStatus = "open" | "resolved";
|
|
68
|
+
export interface Comment {
|
|
69
|
+
id: string;
|
|
70
|
+
threadId: string;
|
|
71
|
+
body: string;
|
|
72
|
+
author: Author;
|
|
73
|
+
createdAt: string;
|
|
74
|
+
}
|
|
75
|
+
export interface Thread {
|
|
76
|
+
id: string;
|
|
77
|
+
/** The page key the widget filed it under (by default the path). */
|
|
78
|
+
page: string;
|
|
79
|
+
/** Full address the comment was made on. */
|
|
80
|
+
url: string;
|
|
81
|
+
anchor: Anchor;
|
|
82
|
+
/** Null for threads made by clients that send none. */
|
|
83
|
+
context: ThreadContext | null;
|
|
84
|
+
screenshots: Partial<Record<ScreenshotKind, Screenshot>>;
|
|
85
|
+
status: ThreadStatus;
|
|
86
|
+
author: Author;
|
|
87
|
+
createdAt: string;
|
|
88
|
+
updatedAt: string;
|
|
89
|
+
resolvedAt: string | null;
|
|
90
|
+
resolvedBy: Author | null;
|
|
91
|
+
/** Oldest first. The first comment opens the thread. */
|
|
92
|
+
comments: Comment[];
|
|
93
|
+
}
|
|
94
|
+
export interface ApiError {
|
|
95
|
+
error: {
|
|
96
|
+
code: string;
|
|
97
|
+
message: string;
|
|
98
|
+
};
|
|
99
|
+
/** Present on 401 when an auth plugin knows where to sign in. */
|
|
100
|
+
loginUrl?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface CreateThreadInput {
|
|
103
|
+
page: string;
|
|
104
|
+
url: string;
|
|
105
|
+
anchor: Anchor;
|
|
106
|
+
context?: ThreadContext;
|
|
107
|
+
body: string;
|
|
108
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
2
|
+
import type { CommentsStore } from "../server/store";
|
|
3
|
+
/**
|
|
4
|
+
* Comments in a Cloudflare D1 database, creating the tables on first use:
|
|
5
|
+
* `store: (env) => d1Store(env.DB)`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function d1Store(binding: Parameters<typeof drizzle>[0], options?: {
|
|
8
|
+
createTables?: boolean;
|
|
9
|
+
}): CommentsStore;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CommentsStore } from "../../server/store";
|
|
2
|
+
export type Dialect = "sqlite" | "pg" | "mysql";
|
|
3
|
+
export interface DrizzleStoreOptions {
|
|
4
|
+
/**
|
|
5
|
+
* A Drizzle database from any driver of the dialect: `drizzle(env.DB)` for
|
|
6
|
+
* D1, `drizzle(client)` for libSQL, PGlite, Neon, postgres.js,
|
|
7
|
+
* PlanetScale, mysql2, ...
|
|
8
|
+
*/
|
|
9
|
+
db: unknown;
|
|
10
|
+
dialect: Dialect;
|
|
11
|
+
/**
|
|
12
|
+
* Create the tables on first use if they're missing. Default true; turn it
|
|
13
|
+
* off when you migrate them yourself (the schemas are exported from
|
|
14
|
+
* web-comments/store/drizzle/{sqlite,pg,mysql}).
|
|
15
|
+
*/
|
|
16
|
+
createTables?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A CommentsStore over Drizzle, for SQLite (D1, libSQL/Turso, ...), Postgres
|
|
20
|
+
* (PlanetScale Postgres, Neon, Supabase, ...) and MySQL (PlanetScale, ...).
|
|
21
|
+
*/
|
|
22
|
+
export declare function drizzleStore(options: DrizzleStoreOptions): CommentsStore;
|