agentic-comments 0.2.0 → 0.3.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 CHANGED
@@ -11,7 +11,7 @@ Comment threads and suggestions anchored to any content, from people and agents.
11
11
 
12
12
  ## Install
13
13
 
14
- Requires Node.js 20 or later. `yjs` is a peer dependency. Your app and this package must use the same
14
+ Requires Node.js 22 or later. `yjs` is a peer dependency. Your app and this package must use the same
15
15
  copy of `yjs`, because the package checks the Yjs types of your document. Install both:
16
16
 
17
17
  ```sh
@@ -170,6 +170,69 @@ npx agentic-comments accept <id>
170
170
  `{"value": <JSON>}`.
171
171
  - From code: `import { FileStore, CommentsService, operatorActor } from "agentic-comments/local"`.
172
172
 
173
+ ## HTTP API
174
+
175
+ `agentic-comments/worker` is a Cloudflare Worker (Hono) with REST routes under `/api/v1`. It keeps each
176
+ document in a D1 database through the `Store` port of `agentic-identity`. It checks every request with the
177
+ `Auth` port, so the host picks who the caller is and no request carries an author.
178
+
179
+ Set the principals as a secret. It is JSON from a bearer token (32 bytes or more) to `kind:name`, or to
180
+ `{ "as": "kind:name", "scopes": [...] }`. One bad entry refuses the whole table. A request with no valid
181
+ token gets `401`.
182
+
183
+ ```sh
184
+ wrangler secret put AGENTIC_COMMENTS_TOKENS
185
+ # {"<openssl rand -hex 32>": {"as": "human:ada", "scopes": ["comments:write"]}, "<another>": "agent_of_human:scribe"}
186
+ ```
187
+
188
+ Also set `AGENTIC_COMMENTS_REGION` to `eu` or `us`. Without it every document route fails with `500 MISCONFIGURED`. Use `unknown` with
189
+ `AGENTIC_COMMENTS_UNSAFE_REGION=1` only under `wrangler dev`. The `wrangler.jsonc` in the repo shows the D1 binding.
190
+
191
+ | Scope | Allows |
192
+ | --- | --- |
193
+ | `comments:read` | Read documents, threads, and suggestions. |
194
+ | `comments:comment` | Also add threads, reply, resolve, reopen, and suggest. This is the default for a token with no scopes. |
195
+ | `comments:write` | Also create documents, relocate, and accept or reject. |
196
+ | `comments:admin` | Also let an agent accept or reject (the `comments:accept` right). |
197
+
198
+ | Route | Action |
199
+ | --- | --- |
200
+ | `GET /api/v1/me` | Who the token stands for. |
201
+ | `POST /api/v1/docs` `{id, text?}` | Create a document. |
202
+ | `GET /api/v1/docs/:doc` | Read the content text. |
203
+ | `GET`, `POST /api/v1/docs/:doc/threads` | List (`?status=`, `?anchorState=`, `?mentioning=`) or add. |
204
+ | `GET /api/v1/docs/:doc/threads/:id` | Read one thread. |
205
+ | `POST /api/v1/docs/:doc/threads/:id/replies` | Reply. |
206
+ | `POST .../threads/:id/resolve`, `reopen`, `relocate` | Change a thread. |
207
+ | `GET`, `POST /api/v1/docs/:doc/suggestions` | List or suggest. |
208
+ | `POST .../suggestions/:id/accept`, `reject` | Decide. An agent never decides its own suggestion. |
209
+
210
+ Bodies are the same as the MCP tool inputs. An error is `{"error": {"code", "message"}}`. A body with an
211
+ `author` key is refused with `400`.
212
+
213
+ ```sh
214
+ curl -H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
215
+ -d '{"anchor":{"type":"text","quote":"brave"},"body":"Word choice?"}' \
216
+ http://127.0.0.1:8787/api/v1/docs/d1/threads
217
+ ```
218
+
219
+ ## Live demo
220
+
221
+ A demo Worker runs at <https://agentic-comments-demo.hello-fd1.workers.dev>. It is a staging site. Check it with
222
+ `GET /health`. Every `/api/v1` route needs a bearer token, and the demo does not publish one, so a call
223
+ with no token gets `401`. The data lives in a D1 database in the EU (`weur`), and the Worker sets the
224
+ region to `eu`.
225
+
226
+ To deploy your own copy, log in with `wrangler login` or set `CLOUDFLARE_API_TOKEN`, then:
227
+
228
+ ```sh
229
+ npx wrangler d1 create agentic-comments-demo --location weur # once. Put the id in wrangler.demo.jsonc.
230
+ npx wrangler secret put AGENTIC_COMMENTS_TOKENS --config wrangler.demo.jsonc # see HTTP API
231
+ npm run deploy:demo
232
+ ```
233
+
234
+ `wrangler.demo.jsonc` sets no secret. Deploys run by hand, and no CI job holds a Cloudflare token.
235
+
173
236
  ## Notifications
174
237
 
175
238
  `Notifications` turns changes into events that your app can send to a notifier. It watches the data, so it
@@ -225,7 +288,7 @@ const stop = notifications.subscribe(async (event) => {
225
288
 
226
289
  ## Limits
227
290
 
228
- - No HTTP API yet. The file store is for one machine. It is not a sync server.
291
+ - The file store is for one machine. It is not a sync server. The Worker keeps each document as one D1 row and refuses a document past 600,000 bytes of state.
229
292
  - `add` checks that a text anchor points into this document and matches its quote.
230
293
  - The library trusts the host to call `stamp()` only for a principal it authenticated.
231
294
  - A reply to a resolved thread is refused on the replica that sees the resolve. A reply and a
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.2.0" : readPackageVersion();
3
+ var VERSION = true ? "0.3.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
@@ -158,9 +158,6 @@ function authorLabel(author) {
158
158
  return author.kind === "human" ? "person" : author.kind === "user_token" ? "person via agent" : "agent";
159
159
  }
160
160
 
161
- // src/suggestions.ts
162
- import * as Y2 from "yjs";
163
-
164
161
  // src/validate.ts
165
162
  var invalid = (message) => {
166
163
  throw new CommentsError("INVALID_INPUT", message);
@@ -182,6 +179,7 @@ var strictCopy = (value) => JSON.parse(
182
179
  );
183
180
 
184
181
  // src/suggestions.ts
182
+ import * as Y2 from "yjs";
185
183
  var ACCEPT_SCOPE = "comments:accept";
186
184
  var MAX_CHANGE = 64e3;
187
185
  var MAX_RECEIPT = 16e3;
@@ -7,7 +7,7 @@ import {
7
7
  textAnchor,
8
8
  textOf,
9
9
  textRange
10
- } from "./chunk-C3P5TAC3.js";
10
+ } from "./chunk-GSAJOCDV.js";
11
11
 
12
12
  // src/local-host.ts
13
13
  import * as Y from "yjs";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-RWMBGIAS.js";
3
+ } from "./chunk-GMA5I5L6.js";
4
4
  import {
5
5
  CommentsService
6
- } from "./chunk-ZCM2UDLN.js";
6
+ } from "./chunk-IB6ANO32.js";
7
7
  import {
8
8
  CommentsError
9
- } from "./chunk-C3P5TAC3.js";
9
+ } from "./chunk-GSAJOCDV.js";
10
10
 
11
11
  // src/mcp.ts
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -2,7 +2,7 @@ import {
2
2
  CommentsError,
3
3
  PRINCIPAL_KINDS,
4
4
  stamp
5
- } from "./chunk-C3P5TAC3.js";
5
+ } from "./chunk-GSAJOCDV.js";
6
6
 
7
7
  // src/file-store.ts
8
8
  import { createHash } from "crypto";
package/dist/cli.js CHANGED
@@ -3,17 +3,17 @@ import {
3
3
  FileStore,
4
4
  lockTimeoutFrom,
5
5
  operatorActor
6
- } from "./chunk-C43HLFQE.js";
6
+ } from "./chunk-YU5U4P22.js";
7
7
  import {
8
8
  VERSION
9
- } from "./chunk-RWMBGIAS.js";
9
+ } from "./chunk-GMA5I5L6.js";
10
10
  import {
11
11
  CommentsService,
12
12
  TEXT_ROOT
13
- } from "./chunk-ZCM2UDLN.js";
13
+ } from "./chunk-IB6ANO32.js";
14
14
  import {
15
15
  CommentsError
16
- } from "./chunk-C3P5TAC3.js";
16
+ } from "./chunk-GSAJOCDV.js";
17
17
 
18
18
  // src/cli.ts
19
19
  import { readFileSync, realpathSync } from "fs";
@@ -155,7 +155,7 @@ async function runCli(argv, io) {
155
155
  return 0;
156
156
  }
157
157
  if (p.command === "mcp") {
158
- const { startMcp } = await import("./mcp-BOJM5NE6.js");
158
+ const { startMcp } = await import("./mcp-YERA77HT.js");
159
159
  await startMcp(store, operatorActor(p.flags["--as"] ?? io.env.AGENTIC_COMMENTS_AS, io.env.AGENTIC_COMMENTS_SCOPES));
160
160
  return 0;
161
161
  }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { A as Author, C as Comments, S as Suggestions } from './suggestions-U8tXRLZ5.js';
2
- export { a as ACCEPT_SCOPE, b as Actor, c as Anchor, d as AnchorKind, e as AnchorState, f as ApplyContext, g as CanvasAnchor, h as CellAnchor, i as CommentsOptions, M as Message, N as NewReply, j as NewSuggestion, k as NewThread, P as PRINCIPAL_KINDS, l as PrincipalKind, R as RelativePositionJson, m as RevertContext, n as SlideAnchor, o as StampInput, p as Suggestion, q as SuggestionFilter, r as SuggestionHost, T as TextAnchor, s as Thread, t as ThreadFilter, u as authorLabel, v as stamp, w as textAnchor, x as textOf } from './suggestions-U8tXRLZ5.js';
1
+ import { a as Author, C as Comments, b as Suggestions } from './suggestions-D4ZJFzzR.js';
2
+ export { c as ACCEPT_SCOPE, A as Actor, d as Anchor, e as AnchorKind, f as AnchorState, g as ApplyContext, h as CanvasAnchor, i as CellAnchor, j as CommentsOptions, M as Message, N as NewReply, k as NewSuggestion, l as NewThread, P as PRINCIPAL_KINDS, m as PrincipalKind, R as RelativePositionJson, n as RevertContext, o as SlideAnchor, p as StampInput, S as Suggestion, q as SuggestionFilter, r as SuggestionHost, s as TextAnchor, T as Thread, t as ThreadFilter, u as authorLabel, v as stamp, w as textAnchor, x as textOf } from './suggestions-D4ZJFzzR.js';
3
3
  import 'yjs';
4
4
 
5
5
  declare const VERSION: string;
6
6
 
7
- type CommentsErrorCode = "INVALID_AUTHOR" | "INVALID_INPUT" | "INVALID_ANCHOR" | "NO_THREAD" | "THREAD_RESOLVED" | "CORRUPT" | "NO_SUGGESTION" | "FORBIDDEN" | "ALREADY_DECIDED" | "APPLY_FAILED" | "RELOCATE_FAILED" | "STORE_MISSING" | "STORE_EXISTS" | "STORE_CORRUPT" | "STORE_LOCKED" | "STORE_FULL" | "USAGE";
7
+ type CommentsErrorCode = "INVALID_AUTHOR" | "INVALID_INPUT" | "INVALID_ANCHOR" | "NO_THREAD" | "THREAD_RESOLVED" | "CORRUPT" | "NO_SUGGESTION" | "FORBIDDEN" | "ALREADY_DECIDED" | "APPLY_FAILED" | "RELOCATE_FAILED" | "STORE_MISSING" | "STORE_EXISTS" | "STORE_CORRUPT" | "STORE_LOCKED" | "STORE_FULL" | "NO_DOCUMENT" | "DOCUMENT_EXISTS" | "CONFLICT" | "MISCONFIGURED" | "USAGE";
8
8
  declare class CommentsError extends Error {
9
9
  readonly code: CommentsErrorCode;
10
10
  constructor(code: CommentsErrorCode, message: string);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-RWMBGIAS.js";
3
+ } from "./chunk-GMA5I5L6.js";
4
4
  import {
5
5
  ACCEPT_SCOPE,
6
6
  Actor,
@@ -14,7 +14,7 @@ import {
14
14
  textAnchor,
15
15
  textOf,
16
16
  validDate
17
- } from "./chunk-C3P5TAC3.js";
17
+ } from "./chunk-GSAJOCDV.js";
18
18
 
19
19
  // src/notifications.ts
20
20
  import * as Y from "yjs";
package/dist/local.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as Y from 'yjs';
2
- import { b as Actor, s as Thread, M as Message, p as Suggestion, r as SuggestionHost } from './suggestions-U8tXRLZ5.js';
2
+ export { A as AnchorSpec, C as CommentsService, L as ListFilter } from './service-BmMQMT_8.js';
3
+ import { r as SuggestionHost, A as Actor } from './suggestions-D4ZJFzzR.js';
3
4
 
4
5
  /** The most bytes a saved document holds. A save over this is refused, so a file never grows past what a load accepts. */
5
6
  declare const MAX_FILE: number;
@@ -35,63 +36,6 @@ declare class FileStore {
35
36
  write<T>(fn: (ydoc: Y.Doc) => T): T;
36
37
  }
37
38
 
38
- /** Where a comment or suggestion points, in terms a person or agent can write down. */
39
- type AnchorSpec = {
40
- type: "text";
41
- quote: string;
42
- occurrence?: number;
43
- } | {
44
- type: "cell";
45
- key: string;
46
- } | {
47
- type: "slide";
48
- elementId: string;
49
- } | {
50
- type: "canvas";
51
- elementId: string;
52
- };
53
- interface ListFilter {
54
- status?: string;
55
- anchorState?: string;
56
- mentioning?: string;
57
- }
58
- /**
59
- * Every action the CLI and MCP server offer, on a local file store. Each call takes the actor the operator
60
- * stamped, and no input carries an author, so both faces return the same shapes and obey the same rules.
61
- */
62
- declare class CommentsService {
63
- private readonly store;
64
- private readonly actor;
65
- constructor(store: FileStore, actor: Actor);
66
- private open;
67
- private read;
68
- private write;
69
- threads(filter?: ListFilter): Thread[];
70
- thread(id: string): Thread;
71
- comment(input: {
72
- anchor: AnchorSpec;
73
- body: string;
74
- mentions?: string[];
75
- }): Thread;
76
- reply(threadId: string, input: {
77
- body: string;
78
- mentions?: string[];
79
- }): Message;
80
- resolve(threadId: string): Thread;
81
- reopen(threadId: string): Thread;
82
- relocate(threadId: string): Thread;
83
- suggestions(filter?: ListFilter): Suggestion[];
84
- suggest(input: {
85
- anchor: AnchorSpec;
86
- change: unknown;
87
- summary?: string;
88
- }): Suggestion;
89
- accept(suggestionId: string): Suggestion;
90
- reject(suggestionId: string): Suggestion;
91
- /** The content text of the store. */
92
- text(): string;
93
- }
94
-
95
39
  /** The root Y.Text that a local store keeps its content in. */
96
40
  declare const TEXT_ROOT = "text";
97
41
  /** The root Y.Map that holds values for cell, slide, canvas, and custom anchors. */
@@ -109,4 +53,4 @@ declare const localHost: SuggestionHost;
109
53
  */
110
54
  declare function operatorActor(as: string | undefined, scopes: string | undefined): Actor;
111
55
 
112
- export { type AnchorSpec, CommentsService, FileStore, type FileStoreOptions, type ListFilter, MAX_FILE, TEXT_ROOT, VALUES_ROOT, localHost, lockTimeoutFrom, operatorActor };
56
+ export { FileStore, type FileStoreOptions, MAX_FILE, TEXT_ROOT, VALUES_ROOT, localHost, lockTimeoutFrom, operatorActor };
package/dist/local.js CHANGED
@@ -3,14 +3,14 @@ import {
3
3
  MAX_FILE,
4
4
  lockTimeoutFrom,
5
5
  operatorActor
6
- } from "./chunk-C43HLFQE.js";
6
+ } from "./chunk-YU5U4P22.js";
7
7
  import {
8
8
  CommentsService,
9
9
  TEXT_ROOT,
10
10
  VALUES_ROOT,
11
11
  localHost
12
- } from "./chunk-ZCM2UDLN.js";
13
- import "./chunk-C3P5TAC3.js";
12
+ } from "./chunk-IB6ANO32.js";
13
+ import "./chunk-GSAJOCDV.js";
14
14
  export {
15
15
  CommentsService,
16
16
  FileStore,
@@ -0,0 +1,11 @@
1
+ import {
2
+ createMcpServer,
3
+ startMcp
4
+ } from "./chunk-TTTX3R6M.js";
5
+ import "./chunk-GMA5I5L6.js";
6
+ import "./chunk-IB6ANO32.js";
7
+ import "./chunk-GSAJOCDV.js";
8
+ export {
9
+ createMcpServer,
10
+ startMcp
11
+ };
package/dist/mcp-bin.js CHANGED
@@ -3,19 +3,38 @@ import {
3
3
  FileStore,
4
4
  lockTimeoutFrom,
5
5
  operatorActor
6
- } from "./chunk-C43HLFQE.js";
6
+ } from "./chunk-YU5U4P22.js";
7
7
  import {
8
8
  startMcp
9
- } from "./chunk-UE5ULEXU.js";
10
- import "./chunk-RWMBGIAS.js";
11
- import "./chunk-ZCM2UDLN.js";
12
- import "./chunk-C3P5TAC3.js";
9
+ } from "./chunk-TTTX3R6M.js";
10
+ import {
11
+ VERSION
12
+ } from "./chunk-GMA5I5L6.js";
13
+ import "./chunk-IB6ANO32.js";
14
+ import "./chunk-GSAJOCDV.js";
13
15
 
14
16
  // src/mcp-bin.ts
15
- try {
16
- const env = process.env;
17
- await startMcp(new FileStore(env.AGENTIC_COMMENTS_FILE ?? "./comments.ydoc", { lockTimeoutMs: lockTimeoutFrom(env) }), operatorActor(env.AGENTIC_COMMENTS_AS, env.AGENTIC_COMMENTS_SCOPES));
18
- } catch (error) {
19
- console.error(JSON.stringify({ error: { code: error.code ?? "INTERNAL", message: error instanceof Error ? error.message : String(error) } }));
20
- process.exit(1);
17
+ var MCP_HELP = `agentic-comments-mcp ${VERSION}
18
+
19
+ Start the MCP server on stdio. Set the principal and scopes in the environment:
20
+
21
+ AGENTIC_COMMENTS_AS=kind:name Required. For example agent_of_human:scribe.
22
+ AGENTIC_COMMENTS_SCOPES=a,b Optional. A comma list, such as comments:accept.
23
+ AGENTIC_COMMENTS_FILE=./comments.ydoc The store file. The default is ./comments.ydoc.
24
+ AGENTIC_COMMENTS_LOCK_TIMEOUT_MS=5000 How long a write waits for another process.
25
+
26
+ Usage: agentic-comments-mcp [--help | --version]`;
27
+ var args = process.argv.slice(2);
28
+ if (args.includes("--help") || args.includes("-h")) {
29
+ console.log(MCP_HELP);
30
+ } else if (args.includes("--version") || args.includes("-v")) {
31
+ console.log(VERSION);
32
+ } else {
33
+ try {
34
+ const env = process.env;
35
+ await startMcp(new FileStore(env.AGENTIC_COMMENTS_FILE ?? "./comments.ydoc", { lockTimeoutMs: lockTimeoutFrom(env) }), operatorActor(env.AGENTIC_COMMENTS_AS, env.AGENTIC_COMMENTS_SCOPES));
36
+ } catch (error) {
37
+ console.error(JSON.stringify({ error: { code: error.code ?? "INTERNAL", message: error instanceof Error ? error.message : String(error) } }));
38
+ process.exit(1);
39
+ }
21
40
  }
@@ -0,0 +1,66 @@
1
+ import * as Y from 'yjs';
2
+ import { A as Actor, T as Thread, M as Message, S as Suggestion } from './suggestions-D4ZJFzzR.js';
3
+
4
+ /** Where a comment or suggestion points, in terms a person or agent can write down. */
5
+ type AnchorSpec = {
6
+ type: "text";
7
+ quote: string;
8
+ occurrence?: number;
9
+ } | {
10
+ type: "cell";
11
+ key: string;
12
+ } | {
13
+ type: "slide";
14
+ elementId: string;
15
+ } | {
16
+ type: "canvas";
17
+ elementId: string;
18
+ };
19
+ /** Where a service reads and writes its document: a file store, or a document the host loaded. */
20
+ interface YStore {
21
+ read<T>(fn: (ydoc: Y.Doc) => T): T;
22
+ write<T>(fn: (ydoc: Y.Doc) => T): T;
23
+ }
24
+ interface ListFilter {
25
+ status?: string;
26
+ anchorState?: string;
27
+ mentioning?: string;
28
+ }
29
+ /**
30
+ * Every action the CLI and MCP server offer, on a local file store. Each call takes the actor the operator
31
+ * stamped, and no input carries an author, so both faces return the same shapes and obey the same rules.
32
+ */
33
+ declare class CommentsService {
34
+ private readonly store;
35
+ private readonly actor;
36
+ constructor(store: YStore, actor: Actor);
37
+ private open;
38
+ private read;
39
+ private write;
40
+ threads(filter?: ListFilter): Thread[];
41
+ thread(id: string): Thread;
42
+ comment(input: {
43
+ anchor: AnchorSpec;
44
+ body: string;
45
+ mentions?: string[];
46
+ }): Thread;
47
+ reply(threadId: string, input: {
48
+ body: string;
49
+ mentions?: string[];
50
+ }): Message;
51
+ resolve(threadId: string): Thread;
52
+ reopen(threadId: string): Thread;
53
+ relocate(threadId: string): Thread;
54
+ suggestions(filter?: ListFilter): Suggestion[];
55
+ suggest(input: {
56
+ anchor: AnchorSpec;
57
+ change: unknown;
58
+ summary?: string;
59
+ }): Suggestion;
60
+ accept(suggestionId: string): Suggestion;
61
+ reject(suggestionId: string): Suggestion;
62
+ /** The content text of the store. */
63
+ text(): string;
64
+ }
65
+
66
+ export { type AnchorSpec as A, CommentsService as C, type ListFilter as L };
@@ -331,4 +331,4 @@ declare class Suggestions {
331
331
  settle(): void;
332
332
  }
333
333
 
334
- export { type Author as A, Comments as C, type Message as M, type NewReply as N, PRINCIPAL_KINDS as P, type RelativePositionJson as R, Suggestions as S, type TextAnchor as T, ACCEPT_SCOPE as a, Actor as b, type Anchor as c, type AnchorKind as d, type AnchorState as e, type ApplyContext as f, type CanvasAnchor as g, type CellAnchor as h, type CommentsOptions as i, type NewSuggestion as j, type NewThread as k, type PrincipalKind as l, type RevertContext as m, type SlideAnchor as n, type StampInput as o, type Suggestion as p, type SuggestionFilter as q, type SuggestionHost as r, type Thread as s, type ThreadFilter as t, authorLabel as u, stamp as v, textAnchor as w, textOf as x };
334
+ export { Actor as A, Comments as C, type Message as M, type NewReply as N, PRINCIPAL_KINDS as P, type RelativePositionJson as R, type Suggestion as S, type Thread as T, type Author as a, Suggestions as b, ACCEPT_SCOPE as c, type Anchor as d, type AnchorKind as e, type AnchorState as f, type ApplyContext as g, type CanvasAnchor as h, type CellAnchor as i, type CommentsOptions as j, type NewSuggestion as k, type NewThread as l, type PrincipalKind as m, type RevertContext as n, type SlideAnchor as o, type StampInput as p, type SuggestionFilter as q, type SuggestionHost as r, type TextAnchor as s, type ThreadFilter as t, authorLabel as u, stamp as v, textAnchor as w, textOf as x };
@@ -0,0 +1,58 @@
1
+ import { Hono } from 'hono';
2
+ import { Auth, Store, PrincipalContext, Region } from 'agentic-identity';
3
+ import { D1Like } from 'agentic-identity/cloudflare';
4
+ import { A as Actor } from './suggestions-D4ZJFzzR.js';
5
+ import { C as CommentsService } from './service-BmMQMT_8.js';
6
+ import 'yjs';
7
+
8
+ interface WorkerEnv {
9
+ /** Where the data lives: "eu" or "us". It is a claim you make at deploy time, with no default. "unknown" needs `AGENTIC_COMMENTS_UNSAFE_REGION=1` too. */
10
+ AGENTIC_COMMENTS_REGION?: string;
11
+ /** Set to "1" to accept the region "unknown" under `wrangler dev`. Never set it in production. */
12
+ AGENTIC_COMMENTS_UNSAFE_REGION?: string;
13
+ /** D1 database: each document, with its threads, as a Yjs update. */
14
+ DB?: D1Like;
15
+ /**
16
+ * JSON object: bearer token (32 bytes or more) to `kind:name`, or to `{ "as": "kind:name", "scopes": ["comments:write"] }`.
17
+ * Set it as a secret. A short token or a bad entry refuses the whole config.
18
+ */
19
+ AGENTIC_COMMENTS_TOKENS?: string;
20
+ }
21
+ /**
22
+ * An `Auth` over a table of bearer tokens. One bad entry (a token under 32 bytes, a value that is not a valid principal)
23
+ * refuses the whole table, so a typo never leaves a weak token live. The compare visits every entry.
24
+ */
25
+ declare function createTokenAuth(raw: string | undefined): Auth;
26
+ /** The documents, on the `Store` port. Each write loads the document, runs the change, and saves in one transaction. */
27
+ declare class CommentsDocs {
28
+ private readonly store;
29
+ constructor(store: Store);
30
+ private id;
31
+ private load;
32
+ private open;
33
+ create(id: string, text: string, actor: Actor): Promise<void>;
34
+ read<T>(id: string, actor: Actor, run: (service: CommentsService) => T): Promise<T>;
35
+ write<T>(id: string, actor: Actor, run: (service: CommentsService) => T): Promise<T>;
36
+ }
37
+ /** The docs, or a function that makes them from the Worker bindings. */
38
+ type DocsSource = CommentsDocs | ((env: WorkerEnv) => Promise<CommentsDocs>);
39
+ interface AppOptions {
40
+ /** The credential check. The default reads `AGENTIC_COMMENTS_TOKENS`. Pass any `Auth` from agentic-identity to replace it. */
41
+ auth?: Auth | ((env: WorkerEnv) => Auth);
42
+ }
43
+ type AppEnv = {
44
+ Bindings: WorkerEnv;
45
+ Variables: {
46
+ context: PrincipalContext;
47
+ };
48
+ };
49
+ declare function createApp(source: DocsSource, options?: AppOptions): Hono<AppEnv>;
50
+ /** The region claim, or a MISCONFIGURED error. */
51
+ declare function regionOf(env: WorkerEnv): Region;
52
+ /** The documents over D1. */
53
+ declare function docsFor(env: WorkerEnv): Promise<CommentsDocs>;
54
+ declare const _default: {
55
+ fetch: (request: Request, env: WorkerEnv, ctx: unknown) => Response | Promise<Response>;
56
+ };
57
+
58
+ export { type AppOptions, CommentsDocs, type DocsSource, type WorkerEnv, createApp, createTokenAuth, _default as default, docsFor, regionOf };
package/dist/worker.js ADDED
@@ -0,0 +1,287 @@
1
+ import {
2
+ CommentsService,
3
+ TEXT_ROOT
4
+ } from "./chunk-IB6ANO32.js";
5
+ import {
6
+ ACCEPT_SCOPE,
7
+ CommentsError,
8
+ PRINCIPAL_KINDS,
9
+ exact,
10
+ invalid,
11
+ stamp
12
+ } from "./chunk-GSAJOCDV.js";
13
+
14
+ // src/worker.ts
15
+ import { Hono } from "hono";
16
+ import { StoreConflictError, assertRegion, isScope, parseRegion, hasScope } from "agentic-identity";
17
+ import { D1_SCHEMA, createD1Store } from "agentic-identity/cloudflare";
18
+ import * as Y from "yjs";
19
+ var READ = "comments:read";
20
+ var COMMENT = "comments:comment";
21
+ var WRITE = "comments:write";
22
+ var ADMIN = "comments:admin";
23
+ var DEFAULT_SCOPES = [COMMENT];
24
+ var MIN_TOKEN_BYTES = 32;
25
+ var warned = /* @__PURE__ */ new Set();
26
+ function refuse(reason) {
27
+ if (!warned.has(reason)) {
28
+ warned.add(reason);
29
+ console.error(`AGENTIC_COMMENTS_TOKENS is refused: ${reason}`);
30
+ }
31
+ return void 0;
32
+ }
33
+ function same(a, b) {
34
+ let diff = a.length ^ b.length;
35
+ for (let i = 0; i < Math.max(a.length, b.length); i++) diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
36
+ return diff === 0;
37
+ }
38
+ function contextOf(entry) {
39
+ const object = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : void 0;
40
+ const who = typeof entry === "string" ? entry : object?.as;
41
+ const cut = typeof who === "string" ? who.indexOf(":") : -1;
42
+ if (typeof who !== "string" || cut < 1 || cut === who.length - 1) return void 0;
43
+ const kind = who.slice(0, cut);
44
+ const name = who.slice(cut + 1);
45
+ if (!PRINCIPAL_KINDS.includes(kind)) return void 0;
46
+ const given = object?.scopes;
47
+ if (given !== void 0 && !(Array.isArray(given) && given.every(isScope))) return void 0;
48
+ try {
49
+ stamp({ kind, name });
50
+ } catch {
51
+ return void 0;
52
+ }
53
+ return { principalId: name, kind, chain: [{ id: name, kind }], scopes: given ?? [...DEFAULT_SCOPES] };
54
+ }
55
+ function createTokenAuth(raw) {
56
+ let table;
57
+ try {
58
+ const parsed = JSON.parse(raw ?? "{}");
59
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { authenticate: async () => (refuse("not a JSON object"), null) };
60
+ table = /* @__PURE__ */ new Map();
61
+ for (const [token, entry] of Object.entries(parsed)) {
62
+ const context = contextOf(entry);
63
+ if (new TextEncoder().encode(token).length < MIN_TOKEN_BYTES) throw new Error(`a token is shorter than ${MIN_TOKEN_BYTES} bytes`);
64
+ if (!context) throw new Error("an entry is not a valid kind:name or { as, scopes }");
65
+ table.set(token, context);
66
+ }
67
+ } catch (error) {
68
+ table = void 0;
69
+ refuse(error instanceof SyntaxError ? "not valid JSON" : error.message);
70
+ }
71
+ const known = table;
72
+ return {
73
+ async authenticate(request) {
74
+ const bearer = /^Bearer (.+)$/.exec(request.headers.get("authorization") ?? "")?.[1];
75
+ if (bearer === void 0 || !known) return null;
76
+ let found = null;
77
+ for (const [token, context] of known) if (same(token, bearer)) found = context;
78
+ return found;
79
+ }
80
+ };
81
+ }
82
+ function actorOf(context) {
83
+ const scopes = hasScope(context.scopes, ADMIN) ? [ACCEPT_SCOPE] : [];
84
+ return stamp({ kind: context.kind, name: context.principalId, scopes });
85
+ }
86
+ var DOCS = "comments_docs";
87
+ var DOC_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
88
+ var MAX_STATE = 6e5;
89
+ var MAX_BODY = 256 * 1024;
90
+ var toBase64 = (bytes) => {
91
+ let out = "";
92
+ for (let i = 0; i < bytes.length; i += 32768) out += String.fromCharCode(...bytes.subarray(i, i + 32768));
93
+ return btoa(out);
94
+ };
95
+ var fromBase64 = (text) => Uint8Array.from(atob(text), (ch) => ch.charCodeAt(0));
96
+ var CommentsDocs = class {
97
+ constructor(store) {
98
+ this.store = store;
99
+ }
100
+ store;
101
+ id(id) {
102
+ if (!DOC_ID.test(id)) invalid(`invalid document id "${id}": use 1 to 64 of a-z, 0-9, "-" and "_"`);
103
+ return id;
104
+ }
105
+ load(row, id) {
106
+ if (row === null) throw new CommentsError("NO_DOCUMENT", `no document with id "${id}"`);
107
+ const ydoc = new Y.Doc();
108
+ try {
109
+ if (typeof row.state !== "string") throw new Error("no state");
110
+ Y.applyUpdate(ydoc, fromBase64(row.state));
111
+ } catch {
112
+ throw new CommentsError("CORRUPT", `document "${id}" is damaged`);
113
+ }
114
+ return ydoc;
115
+ }
116
+ open(ydoc, actor) {
117
+ const fixed = { read: (fn) => fn(ydoc), write: (fn) => fn(ydoc) };
118
+ return new CommentsService(fixed, actor);
119
+ }
120
+ async create(id, text, actor) {
121
+ this.id(id);
122
+ const ydoc = new Y.Doc();
123
+ ydoc.getText(TEXT_ROOT).insert(0, text);
124
+ const state = Y.encodeStateAsUpdate(ydoc);
125
+ if (state.length > MAX_STATE) throw new CommentsError("STORE_FULL", `the text is too large for one document (${MAX_STATE} bytes of state)`);
126
+ await this.store.transaction(async (tx) => {
127
+ if (await tx.get(DOCS, id) !== null) throw new CommentsError("DOCUMENT_EXISTS", `document "${id}" already exists`);
128
+ await tx.put(DOCS, id, { state: toBase64(state), createdBy: { kind: actor.kind, name: actor.name }, createdAt: Date.now() });
129
+ });
130
+ }
131
+ async read(id, actor, run) {
132
+ return run(this.open(this.load(await this.store.get(DOCS, this.id(id)), id), actor));
133
+ }
134
+ async write(id, actor, run) {
135
+ this.id(id);
136
+ return this.store.transaction(async (tx) => {
137
+ const row = await tx.get(DOCS, id);
138
+ const ydoc = this.load(row, id);
139
+ const result = run(this.open(ydoc, actor));
140
+ const state = Y.encodeStateAsUpdate(ydoc);
141
+ if (state.length > MAX_STATE) throw new CommentsError("STORE_FULL", `document "${id}" would pass ${MAX_STATE} bytes of state`);
142
+ await tx.put(DOCS, id, { state: toBase64(state), createdBy: row?.createdBy ?? null, createdAt: row?.createdAt ?? null });
143
+ return result;
144
+ });
145
+ }
146
+ };
147
+ var STATUS = {
148
+ INVALID_AUTHOR: 400,
149
+ INVALID_INPUT: 400,
150
+ INVALID_ANCHOR: 400,
151
+ NO_THREAD: 404,
152
+ NO_SUGGESTION: 404,
153
+ NO_DOCUMENT: 404,
154
+ THREAD_RESOLVED: 409,
155
+ ALREADY_DECIDED: 409,
156
+ DOCUMENT_EXISTS: 409,
157
+ CONFLICT: 409,
158
+ APPLY_FAILED: 409,
159
+ RELOCATE_FAILED: 409,
160
+ FORBIDDEN: 403,
161
+ STORE_FULL: 413,
162
+ CORRUPT: 500,
163
+ MISCONFIGURED: 500
164
+ };
165
+ var fail = (code, message, status) => Response.json({ error: { code, message } }, { status });
166
+ async function body(c) {
167
+ if (Number(c.req.header("content-length") ?? 0) > MAX_BODY) invalid(`the body is over ${MAX_BODY} bytes`);
168
+ const text = await c.req.text();
169
+ if (text.length > MAX_BODY) invalid(`the body is over ${MAX_BODY} bytes`);
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(text);
173
+ } catch {
174
+ parsed = void 0;
175
+ }
176
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) invalid("the body must be a JSON object");
177
+ return parsed;
178
+ }
179
+ function createApp(source, options = {}) {
180
+ const app2 = new Hono();
181
+ const docsOf = (c) => typeof source === "function" ? source(c.env ?? {}) : Promise.resolve(source);
182
+ app2.use("*", async (c, next) => {
183
+ await next();
184
+ c.header("x-content-type-options", "nosniff");
185
+ });
186
+ app2.get("/health", (c) => c.json({ ok: true, pack: "agentic-comments" }));
187
+ app2.use("/api/v1/*", async (c, next) => {
188
+ const auth = typeof options.auth === "function" ? options.auth(c.env ?? {}) : options.auth ?? createTokenAuth(c.env?.AGENTIC_COMMENTS_TOKENS);
189
+ const context = await auth.authenticate(c.req.raw);
190
+ if (!context) return fail("UNAUTHENTICATED", "send a valid bearer token", 401);
191
+ c.set("context", context);
192
+ await next();
193
+ });
194
+ app2.get("/api/v1/me", (c) => {
195
+ const { principalId, kind, scopes } = c.get("context");
196
+ return c.json({ kind, name: principalId, scopes });
197
+ });
198
+ const act = (c, need, write, run) => (async () => {
199
+ const context = c.get("context");
200
+ if (!hasScope(context.scopes, need)) throw new CommentsError("FORBIDDEN", `this token needs the ${need} scope`);
201
+ const docs = await docsOf(c);
202
+ const actor = actorOf(context);
203
+ const id = c.req.param("doc") ?? "";
204
+ return c.json(await (write ? docs.write(id, actor, run) : docs.read(id, actor, run)));
205
+ })();
206
+ const doc = "/api/v1/docs/:doc";
207
+ app2.post("/api/v1/docs", async (c) => {
208
+ const given = exact(await body(c), ["id", "text"], "a document");
209
+ if (typeof given.id !== "string") invalid("a document needs an id");
210
+ if (given.text !== void 0 && typeof given.text !== "string") invalid("text must be a string");
211
+ const context = c.get("context");
212
+ if (!hasScope(context.scopes, WRITE)) throw new CommentsError("FORBIDDEN", `this token needs the ${WRITE} scope`);
213
+ await (await docsOf(c)).create(given.id, given.text ?? "", actorOf(context));
214
+ return c.json({ id: given.id }, 201);
215
+ });
216
+ app2.get(doc, (c) => act(c, READ, false, (s) => ({ id: c.req.param("doc"), text: s.text() })));
217
+ app2.get(`${doc}/threads`, (c) => act(c, READ, false, (s) => s.threads(c.req.query())));
218
+ app2.get(`${doc}/threads/:id`, (c) => act(c, READ, false, (s) => s.thread(c.req.param("id"))));
219
+ app2.post(`${doc}/threads`, async (c) => {
220
+ const input = await body(c);
221
+ return act(c, COMMENT, true, (s) => s.comment(input));
222
+ });
223
+ app2.post(`${doc}/threads/:id/replies`, async (c) => {
224
+ const input = await body(c);
225
+ return act(c, COMMENT, true, (s) => s.reply(c.req.param("id"), input));
226
+ });
227
+ for (const action of ["resolve", "reopen"]) {
228
+ app2.post(`${doc}/threads/:id/${action}`, (c) => act(c, COMMENT, true, (s) => s[action](c.req.param("id"))));
229
+ }
230
+ app2.post(`${doc}/threads/:id/relocate`, (c) => act(c, WRITE, true, (s) => s.relocate(c.req.param("id"))));
231
+ app2.get(`${doc}/suggestions`, (c) => act(c, READ, false, (s) => s.suggestions(c.req.query())));
232
+ app2.post(`${doc}/suggestions`, async (c) => {
233
+ const input = await body(c);
234
+ return act(c, COMMENT, true, (s) => s.suggest(input));
235
+ });
236
+ for (const action of ["accept", "reject"]) {
237
+ app2.post(`${doc}/suggestions/:id/${action}`, (c) => act(c, WRITE, true, (s) => s[action](c.req.param("id"))));
238
+ }
239
+ app2.notFound(() => fail("NOT_FOUND", "no such route", 404));
240
+ app2.onError((error) => {
241
+ if (error instanceof CommentsError) return fail(error.code, error.message, STATUS[error.code] ?? 400);
242
+ if (error instanceof StoreConflictError) return fail("CONFLICT", "the document kept changing; try again", 409);
243
+ console.error(error instanceof Error ? error.name : "error");
244
+ return fail("INTERNAL", "the server could not finish the request", 500);
245
+ });
246
+ return app2;
247
+ }
248
+ var schemas = /* @__PURE__ */ new WeakMap();
249
+ function regionOf(env) {
250
+ let region;
251
+ try {
252
+ region = parseRegion(env.AGENTIC_COMMENTS_REGION);
253
+ } catch {
254
+ throw new CommentsError("MISCONFIGURED", 'set AGENTIC_COMMENTS_REGION to "eu" or "us" ("unknown" only for local development)');
255
+ }
256
+ if (region === "unknown" && env.AGENTIC_COMMENTS_UNSAFE_REGION !== "1") {
257
+ throw new CommentsError("MISCONFIGURED", 'the region "unknown" turns the residency check off; set AGENTIC_COMMENTS_UNSAFE_REGION=1 to accept that for local development');
258
+ }
259
+ return region;
260
+ }
261
+ async function docsFor(env) {
262
+ if (!env.DB) throw new CommentsError("MISCONFIGURED", "bind a D1 database as DB");
263
+ const region = regionOf(env);
264
+ const db = env.DB;
265
+ let ready = schemas.get(db);
266
+ if (!ready) {
267
+ ready = db.prepare(D1_SCHEMA).run().catch((error) => {
268
+ schemas.delete(db);
269
+ throw error;
270
+ });
271
+ schemas.set(db, ready);
272
+ }
273
+ await ready;
274
+ const store = createD1Store(db, { region });
275
+ assertRegion(region, store);
276
+ return new CommentsDocs(store);
277
+ }
278
+ var app = createApp(docsFor);
279
+ var worker_default = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
280
+ export {
281
+ CommentsDocs,
282
+ createApp,
283
+ createTokenAuth,
284
+ worker_default as default,
285
+ docsFor,
286
+ regionOf
287
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-comments",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Comment threads and suggestions anchored to any content, from people and agents.",
5
5
  "keywords": [
6
6
  "comments",
@@ -26,7 +26,7 @@
26
26
  "type": "module",
27
27
  "private": false,
28
28
  "engines": {
29
- "node": ">=20"
29
+ "node": ">=22"
30
30
  },
31
31
  "files": [
32
32
  "dist",
@@ -47,6 +47,10 @@
47
47
  "./local": {
48
48
  "types": "./dist/local.d.ts",
49
49
  "import": "./dist/local.js"
50
+ },
51
+ "./worker": {
52
+ "types": "./dist/worker.d.ts",
53
+ "import": "./dist/worker.js"
50
54
  }
51
55
  },
52
56
  "bin": {
@@ -57,12 +61,15 @@
57
61
  "build": "tsup",
58
62
  "typecheck": "tsc --noEmit",
59
63
  "test": "vitest run",
64
+ "deploy:demo": "npm run build && WRANGLER_SEND_METRICS=false wrangler deploy --config wrangler.demo.jsonc",
60
65
  "clean": "rm -rf dist",
61
66
  "prepack": "npm run build",
62
67
  "ci:local": "npm run build && npm run typecheck && npm test"
63
68
  },
64
69
  "dependencies": {
65
70
  "@modelcontextprotocol/sdk": "^1.32.2",
71
+ "agentic-identity": "^0.4.0",
72
+ "hono": "^4.13.14",
66
73
  "zod": "^3.25.76"
67
74
  },
68
75
  "peerDependencies": {
@@ -73,6 +80,7 @@
73
80
  "tsup": "^8.0.0",
74
81
  "typescript": "^5.6.0",
75
82
  "vitest": "^3.0.0",
83
+ "wrangler": "^4.155.0",
76
84
  "yjs": "^13.6.0"
77
85
  }
78
86
  }
@@ -1,11 +0,0 @@
1
- import {
2
- createMcpServer,
3
- startMcp
4
- } from "./chunk-UE5ULEXU.js";
5
- import "./chunk-RWMBGIAS.js";
6
- import "./chunk-ZCM2UDLN.js";
7
- import "./chunk-C3P5TAC3.js";
8
- export {
9
- createMcpServer,
10
- startMcp
11
- };