agentic-comments 0.4.0 → 0.5.1

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
@@ -18,8 +18,6 @@ copy of `yjs`, because the package checks the Yjs types of your document. Instal
18
18
  npm install agentic-comments yjs
19
19
  ```
20
20
 
21
- To build from source, see Develop.
22
-
23
21
  ## Quick start
24
22
 
25
23
  ```ts
@@ -165,10 +163,6 @@ machine. It cannot check a lock held in another container or on another machine.
165
163
  share a volume can have the same process id, and each then clears the other's lock, so they can lose writes. A save that
166
164
  would pass 50 MB is refused.
167
165
 
168
- If the version you install has no `agentic-comments` command, build from source (see [Develop](#develop)).
169
- In the cloned repo, run `npm run build` and use `node dist/cli.js` where these examples say
170
- `npx agentic-comments`.
171
-
172
166
  ```sh
173
167
  export AGENTIC_COMMENTS_FILE=./comments.ydoc
174
168
  export AGENTIC_COMMENTS_AS=human:ada # kind:name. The operator sets this.
@@ -180,10 +174,10 @@ npx agentic-comments accept <id>
180
174
  ```
181
175
 
182
176
  - Commands: `init`, `list`, `show`, `comment`, `reply`, `resolve`, `reopen`, `relocate`, `suggestions`,
183
- `suggest`, `accept`, `reject`, `mcp`. Run `agentic-comments --help` for flags.
177
+ `suggest`, `accept`, `reject`, `mcp`. Run `npx agentic-comments --help` for flags.
184
178
  - Every result is JSON. An error is JSON on stderr with a `code`, and the exit code is 1. A usage error
185
179
  exits 2.
186
- - The principal comes from `--as` or `AGENTIC_COMMENTS_AS`. Scopes come only from
180
+ - The principal comes from `--as` or `AGENTIC_COMMENTS_AS`, as `kind:name`. The name starts with A-Z a-z 0-9 `_` `-`, then uses A-Z a-z 0-9 `.` `_` `-`, and is at most 128 characters. Scopes come only from
187
181
  `AGENTIC_COMMENTS_SCOPES`, a comma list such as `comments:accept`. No flag takes a scope or an author,
188
182
  and a flag that does not apply to a command is refused. `AGENTIC_COMMENTS_LOCK_TIMEOUT_MS` sets the wait
189
183
  for a lock, in whole milliseconds.
@@ -203,9 +197,13 @@ npx agentic-comments accept <id>
203
197
  document in a D1 database through the `Store` port of `agentic-identity`. It checks every request with the
204
198
  `Auth` port, so the host picks who the caller is and no request carries an author.
205
199
 
206
- Set the principals as a secret. It is JSON from a bearer token (32 bytes or more) to `kind:name`, or to
207
- `{ "as": "kind:name", "scopes": [...] }`. One bad entry refuses the whole table. A request with no valid
208
- token gets `401`.
200
+ Set the principals as a secret. It is JSON from a bearer token to `kind:name`, or to
201
+ `{ "as": "kind:name", "scopes": [...] }`. A token is 32 to 512 bytes, from A-Z a-z 0-9 `_` `-`, so a base64 token with `+` or `/` is refused.
202
+ A name starts with A-Z a-z 0-9 `_` `-`, then uses A-Z a-z 0-9 `.` `_` `-`, and is at most 128 characters.
203
+ One bad entry refuses the whole table. A request with no valid token gets `401`.
204
+ A request whose `Authorization` header is not `Bearer` followed by 1 to 512 bytes of A-Z a-z 0-9 `_` `-` gets `401`, before any custom `Auth` runs.
205
+ A write with no `Authorization` header (a session cookie, for example) gets `403` unless its `Origin` is in the `origins`
206
+ option of `createApp`. The default lists none.
209
207
 
210
208
  ```sh
211
209
  wrangler secret put AGENTIC_COMMENTS_TOKENS
@@ -243,6 +241,34 @@ curl -H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
243
241
  http://127.0.0.1:8787/api/v1/docs/d1/threads
244
242
  ```
245
243
 
244
+ ## HTTP client
245
+
246
+ `agentic-comments/client` is a typed client for the Worker routes. It uses only `fetch`, so it runs in browsers,
247
+ Workers, and Node.
248
+
249
+ ```ts
250
+ import { createClient, CommentsApiError } from "agentic-comments/client";
251
+
252
+ const comments = createClient({ baseUrl: "https://comments.example.com", token });
253
+ await comments.createDocument({ id: "d1", text: "The quick brown fox" });
254
+ const thread = await comments.addThread("d1", { anchor: { type: "text", quote: "fox" }, body: "Which fox?" });
255
+ await comments.reply("d1", thread.id, { body: "This one." });
256
+ try {
257
+ await comments.accept("d1", "missing");
258
+ } catch (error) {
259
+ if (error instanceof CommentsApiError) console.error(error.code, error.status);
260
+ }
261
+ ```
262
+
263
+ - The methods match the routes: `me`, `createDocument`, `getDocument`, `listThreads`, `getThread`, `addThread`, `reply`,
264
+ `resolve`, `reopen`, `relocate`, `listSuggestions`, `suggest`, `accept`, `reject`. Each takes an optional `{ signal }`.
265
+ - `baseUrl` is the Worker URL, `http` or `https`, with no query, hash, or credentials.
266
+ - The client never sends an author. The Worker stamps one from the token.
267
+ - A failed call throws `CommentsApiError`. `code` is the API error code, or `UNKNOWN` when the reply is not an API
268
+ error. `status` is the HTTP status. A `200` reply with the wrong shape also fails as `UNKNOWN`.
269
+ - The client never follows a redirect and never retries. A write that you abort after the client sends it can still apply:
270
+ read the thread before you try again.
271
+
246
272
  ## Live demo
247
273
 
248
274
  A demo Worker runs at <https://agentic-comments-demo.hello-fd1.workers.dev>. It is a staging site. Check it with
@@ -244,6 +244,7 @@ var FileStore = class {
244
244
  };
245
245
 
246
246
  // src/operator.ts
247
+ import { PrincipalParseError, parsePrincipal } from "agentic-identity";
247
248
  function operatorActor(as, scopes) {
248
249
  const value = as?.trim();
249
250
  const cut = value === void 0 ? -1 : value.indexOf(":");
@@ -251,7 +252,13 @@ function operatorActor(as, scopes) {
251
252
  throw new CommentsError("INVALID_AUTHOR", `set the principal as kind:name, for example human:ada. Kinds: ${PRINCIPAL_KINDS.join(", ")}`);
252
253
  }
253
254
  const list = scopes === void 0 || scopes.trim() === "" ? [] : scopes.split(",").map((s) => s.trim());
254
- return stamp({ kind: value.slice(0, cut), name: value.slice(cut + 1), scopes: list });
255
+ try {
256
+ const { kind, id } = parsePrincipal({ kind: value.slice(0, cut), id: value.slice(cut + 1) });
257
+ return stamp({ kind, name: id, scopes: list });
258
+ } catch (error) {
259
+ if (!(error instanceof PrincipalParseError)) throw error;
260
+ throw new CommentsError("INVALID_AUTHOR", `the principal needs a known kind (${PRINCIPAL_KINDS.join(", ")}) and a name of A-Z a-z 0-9 . _ -`);
261
+ }
255
262
  }
256
263
 
257
264
  export {
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.4.0" : readPackageVersion();
3
+ var VERSION = true ? "0.5.1" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-DRACF66Y.js";
3
+ } from "./chunk-REFQNIEP.js";
4
4
  import {
5
5
  CommentsService
6
6
  } from "./chunk-NQWRFKJ6.js";
package/dist/cli.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  FileStore,
4
4
  lockTimeoutFrom,
5
5
  operatorActor
6
- } from "./chunk-HXXJNFEO.js";
6
+ } from "./chunk-QJXI3P5D.js";
7
7
  import {
8
8
  VERSION
9
- } from "./chunk-DRACF66Y.js";
9
+ } from "./chunk-REFQNIEP.js";
10
10
  import {
11
11
  CommentsService,
12
12
  TEXT_ROOT
@@ -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-ACC4QIEM.js");
158
+ const { startMcp } = await import("./mcp-XUV3RPOO.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
  }
@@ -0,0 +1,72 @@
1
+ import { L as ListFilter, A as AnchorSpec } from './service-hpWbnSmc.js';
2
+ import { C as CommentsErrorCode } from './errors-CNgWMVQT.js';
3
+ import { PrincipalKind } from 'agentic-identity';
4
+ export { PrincipalKind } from 'agentic-identity';
5
+ import { T as Thread, N as NewReply, M as Message, S as Suggestion } from './suggestions-CvJwo6r4.js';
6
+ import 'yjs';
7
+
8
+ /** Codes the comment rules return, plus the ones the Worker and its auth return. */
9
+ type ApiErrorCode = CommentsErrorCode | "UNAUTHENTICATED" | "NOT_FOUND" | "INTERNAL" | "UNKNOWN";
10
+ /** Thrown for every reply that is not a success. `code` is the API error code and `status` is the HTTP status. */
11
+ declare class CommentsApiError extends Error {
12
+ readonly code: ApiErrorCode;
13
+ readonly status: number;
14
+ constructor(code: ApiErrorCode, status: number, message: string);
15
+ }
16
+ interface ClientOptions {
17
+ /** The Worker origin, for example https://comments.example.com. The client adds /api/v1. */
18
+ baseUrl: string;
19
+ /** A bearer credential that the Worker's auth accepts. */
20
+ token: string;
21
+ fetch?: typeof fetch;
22
+ }
23
+ /**
24
+ * Every method takes an optional signal. An aborted call rejects with the signal reason, not a
25
+ * CommentsApiError. A write that was aborted after it was sent may still have applied: read the
26
+ * thread before you try again.
27
+ */
28
+ interface CallOptions {
29
+ signal?: AbortSignal;
30
+ }
31
+ interface Me {
32
+ kind: PrincipalKind;
33
+ name: string;
34
+ scopes: string[];
35
+ }
36
+ type ThreadFilter = ListFilter;
37
+ type SuggestionListFilter = Pick<ListFilter, "status" | "anchorState">;
38
+ interface CommentsClient {
39
+ me(options?: CallOptions): Promise<Me>;
40
+ createDocument(input: {
41
+ id: string;
42
+ text?: string;
43
+ }, options?: CallOptions): Promise<{
44
+ id: string;
45
+ }>;
46
+ getDocument(docId: string, options?: CallOptions): Promise<{
47
+ id: string;
48
+ text: string;
49
+ }>;
50
+ listThreads(docId: string, filter?: ThreadFilter, options?: CallOptions): Promise<Thread[]>;
51
+ getThread(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
52
+ addThread(docId: string, input: {
53
+ anchor: AnchorSpec;
54
+ body: string;
55
+ mentions?: string[];
56
+ }, options?: CallOptions): Promise<Thread>;
57
+ reply(docId: string, threadId: string, input: NewReply, options?: CallOptions): Promise<Message>;
58
+ resolve(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
59
+ reopen(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
60
+ relocate(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
61
+ listSuggestions(docId: string, filter?: SuggestionListFilter, options?: CallOptions): Promise<Suggestion[]>;
62
+ suggest(docId: string, input: {
63
+ anchor: AnchorSpec;
64
+ change: unknown;
65
+ summary?: string;
66
+ }, options?: CallOptions): Promise<Suggestion>;
67
+ accept(docId: string, suggestionId: string, options?: CallOptions): Promise<Suggestion>;
68
+ reject(docId: string, suggestionId: string, options?: CallOptions): Promise<Suggestion>;
69
+ }
70
+ declare function createClient(options: ClientOptions): CommentsClient;
71
+
72
+ export { AnchorSpec, type ApiErrorCode, type CallOptions, type ClientOptions, CommentsApiError, type CommentsClient, CommentsErrorCode, ListFilter, type Me, Message, NewReply, Suggestion, type SuggestionListFilter, Thread, type ThreadFilter, createClient };
package/dist/client.js ADDED
@@ -0,0 +1,135 @@
1
+ // src/client.ts
2
+ var CODES = {
3
+ INVALID_AUTHOR: true,
4
+ INVALID_INPUT: true,
5
+ INVALID_ANCHOR: true,
6
+ NO_THREAD: true,
7
+ THREAD_RESOLVED: true,
8
+ CORRUPT: true,
9
+ NO_SUGGESTION: true,
10
+ FORBIDDEN: true,
11
+ ALREADY_DECIDED: true,
12
+ APPLY_FAILED: true,
13
+ RELOCATE_FAILED: true,
14
+ STORE_MISSING: true,
15
+ STORE_EXISTS: true,
16
+ STORE_CORRUPT: true,
17
+ STORE_LOCKED: true,
18
+ STORE_FULL: true,
19
+ NO_DOCUMENT: true,
20
+ DOCUMENT_EXISTS: true,
21
+ CONFLICT: true,
22
+ MISCONFIGURED: true,
23
+ USAGE: true,
24
+ UNAUTHENTICATED: true,
25
+ NOT_FOUND: true,
26
+ INTERNAL: true,
27
+ UNKNOWN: true
28
+ };
29
+ var isKnownCode = (v) => typeof v === "string" && Object.hasOwn(CODES, v);
30
+ var CommentsApiError = class extends Error {
31
+ code;
32
+ status;
33
+ constructor(code, status, message) {
34
+ super(message);
35
+ this.name = "CommentsApiError";
36
+ this.code = code;
37
+ this.status = status;
38
+ }
39
+ };
40
+ var KINDS = { human: true, agent_of_human: true, agent_of_agent: true, agent: true, user_token: true };
41
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
42
+ var isString = (v) => typeof v === "string";
43
+ var SHAPES = {
44
+ me: (v) => isObject(v) && (typeof v.kind === "string" && Object.hasOwn(KINDS, v.kind)) && isString(v.name) && Array.isArray(v.scopes),
45
+ created: (v) => isObject(v) && isString(v.id),
46
+ document: (v) => isObject(v) && isString(v.id) && isString(v.text),
47
+ thread: (v) => isObject(v) && isString(v.id) && isObject(v.anchor) && (v.status === "open" || v.status === "resolved") && Array.isArray(v.messages),
48
+ threads: (v) => Array.isArray(v) && v.every(SHAPES.thread),
49
+ message: (v) => isObject(v) && isString(v.id) && isString(v.body) && isObject(v.author),
50
+ suggestion: (v) => isObject(v) && isString(v.id) && isObject(v.anchor) && isString(v.status) && isObject(v.author),
51
+ suggestions: (v) => Array.isArray(v) && v.every(SHAPES.suggestion)
52
+ };
53
+ function createClient(options) {
54
+ if (typeof options.baseUrl !== "string" || options.baseUrl === "") throw new TypeError("createClient needs a baseUrl");
55
+ if (typeof options.token !== "string" || options.token === "") throw new TypeError("createClient needs a token");
56
+ let origin;
57
+ try {
58
+ origin = new URL(options.baseUrl);
59
+ } catch {
60
+ throw new TypeError("baseUrl must be an absolute URL");
61
+ }
62
+ if (origin.protocol !== "https:" && origin.protocol !== "http:") throw new TypeError("baseUrl must be an http or https URL");
63
+ if (origin.search !== "" || origin.hash !== "" || origin.username !== "" || origin.password !== "") {
64
+ throw new TypeError("baseUrl must have no query, hash, or credentials");
65
+ }
66
+ const root = `${origin.origin}${origin.pathname.replace(/\/+$/, "")}/api/v1`;
67
+ const token = options.token;
68
+ const send = (input, init) => {
69
+ const run = options.fetch ?? globalThis.fetch;
70
+ return run(input, init);
71
+ };
72
+ const seg = (value) => {
73
+ if (typeof value !== "string" || /^(?:\.|%2e){0,2}$/i.test(value)) throw new TypeError("an id must be a non-empty string that is not . or ..");
74
+ return encodeURIComponent(value);
75
+ };
76
+ const query = (filter) => {
77
+ const params = new URLSearchParams();
78
+ for (const [key, value] of Object.entries(filter ?? {})) if (value !== void 0) params.set(key, String(value));
79
+ const text = params.toString();
80
+ return text === "" ? "" : `?${text}`;
81
+ };
82
+ async function call(method, path, body, callOptions, shape) {
83
+ const headers = { authorization: `Bearer ${token}` };
84
+ if (body !== void 0) headers["content-type"] = "application/json";
85
+ const response = await send(`${root}${path}`, {
86
+ method,
87
+ headers,
88
+ body: body === void 0 ? void 0 : JSON.stringify(body),
89
+ signal: callOptions?.signal,
90
+ // Never follow a redirect: it would send a write to another host.
91
+ redirect: "manual"
92
+ });
93
+ const text = await response.text();
94
+ let parsed;
95
+ try {
96
+ parsed = text === "" ? void 0 : JSON.parse(text);
97
+ } catch {
98
+ parsed = void 0;
99
+ }
100
+ if (!response.ok) {
101
+ const error = parsed?.error;
102
+ const code = isKnownCode(error?.code) ? error.code : "UNKNOWN";
103
+ const message = typeof error?.message === "string" ? error.message : `HTTP ${response.status}`;
104
+ throw new CommentsApiError(code, response.status, message);
105
+ }
106
+ if (!SHAPES[shape](parsed)) throw new CommentsApiError("UNKNOWN", response.status, `the reply is not a ${shape}`);
107
+ return parsed;
108
+ }
109
+ const doc = (id) => `/docs/${seg(id)}`;
110
+ const thread = (id, threadId) => `${doc(id)}/threads/${seg(threadId)}`;
111
+ const suggestion = (id, suggestionId) => `${doc(id)}/suggestions/${seg(suggestionId)}`;
112
+ const methods = {
113
+ me: (o) => call("GET", "/me", void 0, o, "me"),
114
+ createDocument: (input, o) => call("POST", "/docs", input, o, "created"),
115
+ getDocument: (id, o) => call("GET", doc(id), void 0, o, "document"),
116
+ listThreads: (id, filter, o) => call("GET", `${doc(id)}/threads${query(filter)}`, void 0, o, "threads"),
117
+ getThread: (id, threadId, o) => call("GET", thread(id, threadId), void 0, o, "thread"),
118
+ addThread: (id, input, o) => call("POST", `${doc(id)}/threads`, input, o, "thread"),
119
+ reply: (id, threadId, input, o) => call("POST", `${thread(id, threadId)}/replies`, input, o, "message"),
120
+ resolve: (id, threadId, o) => call("POST", `${thread(id, threadId)}/resolve`, void 0, o, "thread"),
121
+ reopen: (id, threadId, o) => call("POST", `${thread(id, threadId)}/reopen`, void 0, o, "thread"),
122
+ relocate: (id, threadId, o) => call("POST", `${thread(id, threadId)}/relocate`, void 0, o, "thread"),
123
+ listSuggestions: (id, filter, o) => call("GET", `${doc(id)}/suggestions${query(filter)}`, void 0, o, "suggestions"),
124
+ suggest: (id, input, o) => call("POST", `${doc(id)}/suggestions`, input, o, "suggestion"),
125
+ accept: (id, suggestionId, o) => call("POST", `${suggestion(id, suggestionId)}/accept`, void 0, o, "suggestion"),
126
+ reject: (id, suggestionId, o) => call("POST", `${suggestion(id, suggestionId)}/reject`, void 0, o, "suggestion")
127
+ };
128
+ return Object.fromEntries(
129
+ Object.entries(methods).map(([name, method]) => [name, async (...args) => method(...args)])
130
+ );
131
+ }
132
+ export {
133
+ CommentsApiError,
134
+ createClient
135
+ };
@@ -0,0 +1,7 @@
1
+ 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";
2
+ declare class CommentsError extends Error {
3
+ readonly code: CommentsErrorCode;
4
+ constructor(code: CommentsErrorCode, message: string);
5
+ }
6
+
7
+ export { type CommentsErrorCode as C, CommentsError as a };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,11 @@
1
- import { a as Author, C as Comments, b as Suggestions } from './suggestions-oj8fd-nG.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, R as RelativePositionJson, m as RevertContext, n as SlideAnchor, o as StampInput, S as Suggestion, p as SuggestionFilter, q as SuggestionHost, r as TextAnchor, T as Thread, s as ThreadFilter, t as authorLabel, u as stamp, v as textAnchor, w as textOf } from './suggestions-oj8fd-nG.js';
1
+ import { a as Author, C as Comments, b as Suggestions } from './suggestions-CvJwo6r4.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, R as RelativePositionJson, m as RevertContext, n as SlideAnchor, o as StampInput, S as Suggestion, p as SuggestionFilter, q as SuggestionHost, r as TextAnchor, T as Thread, s as ThreadFilter, t as authorLabel, u as stamp, v as textAnchor, w as textOf } from './suggestions-CvJwo6r4.js';
3
+ export { a as CommentsError, C as CommentsErrorCode } from './errors-CNgWMVQT.js';
3
4
  export { PRINCIPAL_KINDS, PrincipalKind } from 'agentic-identity';
4
5
  import 'yjs';
5
6
 
6
7
  declare const VERSION: string;
7
8
 
8
- 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";
9
- declare class CommentsError extends Error {
10
- readonly code: CommentsErrorCode;
11
- constructor(code: CommentsErrorCode, message: string);
12
- }
13
-
14
9
  type CommentEventType = "thread.created" | "message.added" | "mention" | "thread.resolved" | "thread.reopened" | "suggestion.created" | "suggestion.accepted" | "suggestion.rejected";
15
10
  /**
16
11
  * One change, in terms a notifier can route. It holds ids and never the text of a comment, a suggestion, or
@@ -103,4 +98,4 @@ declare class Notifications {
103
98
  private decided;
104
99
  }
105
100
 
106
- export { Author, type CommentEvent, type CommentEventType, type CommentListener, Comments, CommentsError, type CommentsErrorCode, Notifications, type NotificationsOptions, Suggestions, VERSION };
101
+ export { Author, type CommentEvent, type CommentEventType, type CommentListener, Comments, Notifications, type NotificationsOptions, Suggestions, VERSION };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-DRACF66Y.js";
3
+ } from "./chunk-REFQNIEP.js";
4
4
  import {
5
5
  ACCEPT_SCOPE,
6
6
  Actor,
package/dist/local.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as Y from 'yjs';
2
- export { A as AnchorSpec, C as CommentsService, L as ListFilter } from './service-Dxw9TVxY.js';
3
- import { q as SuggestionHost, A as Actor } from './suggestions-oj8fd-nG.js';
2
+ export { A as AnchorSpec, C as CommentsService, L as ListFilter } from './service-hpWbnSmc.js';
3
+ import { q as SuggestionHost, A as Actor } from './suggestions-CvJwo6r4.js';
4
4
  import 'agentic-identity';
5
5
 
6
6
  /** The most bytes a saved document holds. A save over this is refused, so a file never grows past what a load accepts. */
package/dist/local.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  MAX_FILE,
4
4
  lockTimeoutFrom,
5
5
  operatorActor
6
- } from "./chunk-HXXJNFEO.js";
6
+ } from "./chunk-QJXI3P5D.js";
7
7
  import {
8
8
  CommentsService,
9
9
  TEXT_ROOT,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcp
4
- } from "./chunk-R4LH36VG.js";
5
- import "./chunk-DRACF66Y.js";
4
+ } from "./chunk-TFRLOBJW.js";
5
+ import "./chunk-REFQNIEP.js";
6
6
  import "./chunk-NQWRFKJ6.js";
7
7
  import "./chunk-BSNBDKR2.js";
8
8
  export {
package/dist/mcp-bin.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  FileStore,
4
4
  lockTimeoutFrom,
5
5
  operatorActor
6
- } from "./chunk-HXXJNFEO.js";
6
+ } from "./chunk-QJXI3P5D.js";
7
7
  import {
8
8
  startMcp
9
- } from "./chunk-R4LH36VG.js";
9
+ } from "./chunk-TFRLOBJW.js";
10
10
  import {
11
11
  VERSION
12
- } from "./chunk-DRACF66Y.js";
12
+ } from "./chunk-REFQNIEP.js";
13
13
  import "./chunk-NQWRFKJ6.js";
14
14
  import "./chunk-BSNBDKR2.js";
15
15
 
@@ -1,5 +1,5 @@
1
1
  import * as Y from 'yjs';
2
- import { A as Actor, T as Thread, M as Message, S as Suggestion } from './suggestions-oj8fd-nG.js';
2
+ import { A as Actor, T as Thread, M as Message, S as Suggestion } from './suggestions-CvJwo6r4.js';
3
3
 
4
4
  /** Where a comment or suggestion points, in terms a person or agent can write down. */
5
5
  type AnchorSpec = {
package/dist/worker.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Hono } from 'hono';
2
2
  import { Auth, Store, PrincipalContext, Region } from 'agentic-identity';
3
+ import { RequirePrincipalOptions } from 'agentic-identity/http';
3
4
  import { D1Like } from 'agentic-identity/cloudflare';
4
- import { A as Actor } from './suggestions-oj8fd-nG.js';
5
- import { C as CommentsService } from './service-Dxw9TVxY.js';
5
+ import { A as Actor } from './suggestions-CvJwo6r4.js';
6
+ import { C as CommentsService } from './service-hpWbnSmc.js';
6
7
  import 'yjs';
7
8
 
8
9
  interface WorkerEnv {
@@ -13,7 +14,7 @@ interface WorkerEnv {
13
14
  /** D1 database: each document, with its threads, as a Yjs update. */
14
15
  DB?: D1Like;
15
16
  /**
16
- * JSON object: bearer token (32 bytes or more) to `kind:name`, or to `{ "as": "kind:name", "scopes": ["comments:write"] }`.
17
+ * JSON object: bearer token (32 to 512 bytes of A-Z a-z 0-9 _ -) to `kind:name` (the name uses A-Z a-z 0-9 . _ -), or to `{ "as": "kind:name", "scopes": ["comments:write"] }`.
17
18
  * Set it as a secret. A short token or a bad entry refuses the whole config.
18
19
  */
19
20
  AGENTIC_COMMENTS_TOKENS?: string;
@@ -39,6 +40,8 @@ type DocsSource = CommentsDocs | ((env: WorkerEnv) => Promise<CommentsDocs>);
39
40
  interface AppOptions {
40
41
  /** The credential check. The default reads `AGENTIC_COMMENTS_TOKENS`. Pass any `Auth` from agentic-identity to replace it. */
41
42
  auth?: Auth | ((env: WorkerEnv) => Auth);
43
+ /** Origins that may send a write with a session cookie. A bearer token needs none. Passed to `requirePrincipal`. */
44
+ origins?: RequirePrincipalOptions["origins"];
42
45
  }
43
46
  type AppEnv = {
44
47
  Bindings: WorkerEnv;
package/dist/worker.js CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  import {
6
6
  ACCEPT_SCOPE,
7
7
  CommentsError,
8
- PRINCIPAL_KINDS,
9
8
  exact,
10
9
  invalid,
11
10
  stamp
@@ -13,7 +12,8 @@ import {
13
12
 
14
13
  // src/worker.ts
15
14
  import { Hono } from "hono";
16
- import { StoreConflictError, assertRegion, isScope, parseRegion, hasScope } from "agentic-identity";
15
+ import { StoreConflictError, assertRegion, isScope, parseRegion, hasScope, parsePrincipal } from "agentic-identity";
16
+ import { requirePrincipal } from "agentic-identity/http";
17
17
  import { D1_SCHEMA, createD1Store } from "agentic-identity/cloudflare";
18
18
  import * as Y from "yjs";
19
19
  var READ = "comments:read";
@@ -22,6 +22,7 @@ var WRITE = "comments:write";
22
22
  var ADMIN = "comments:admin";
23
23
  var DEFAULT_SCOPES = [COMMENT];
24
24
  var MIN_TOKEN_BYTES = 32;
25
+ var TOKEN_SHAPE = /^[A-Za-z0-9_-]{1,512}$/;
25
26
  var warned = /* @__PURE__ */ new Set();
26
27
  function refuse(reason) {
27
28
  if (!warned.has(reason)) {
@@ -40,9 +41,13 @@ function contextOf(entry) {
40
41
  const who = typeof entry === "string" ? entry : object?.as;
41
42
  const cut = typeof who === "string" ? who.indexOf(":") : -1;
42
43
  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;
44
+ let kind;
45
+ let name;
46
+ try {
47
+ ({ kind, id: name } = parsePrincipal({ kind: who.slice(0, cut), id: who.slice(cut + 1) }));
48
+ } catch {
49
+ return void 0;
50
+ }
46
51
  const given = object?.scopes;
47
52
  if (given !== void 0 && !(Array.isArray(given) && given.every(isScope))) return void 0;
48
53
  try {
@@ -61,6 +66,7 @@ function createTokenAuth(raw) {
61
66
  for (const [token, entry] of Object.entries(parsed)) {
62
67
  const context = contextOf(entry);
63
68
  if (new TextEncoder().encode(token).length < MIN_TOKEN_BYTES) throw new Error(`a token is shorter than ${MIN_TOKEN_BYTES} bytes`);
69
+ if (!TOKEN_SHAPE.test(token)) throw new Error("a token holds a character outside A-Z a-z 0-9 _ - or is over 512 bytes");
64
70
  if (!context) throw new Error("an entry is not a valid kind:name or { as, scopes }");
65
71
  table.set(token, context);
66
72
  }
@@ -186,8 +192,12 @@ function createApp(source, options = {}) {
186
192
  app2.get("/health", (c) => c.json({ ok: true, pack: "agentic-comments" }));
187
193
  app2.use("/api/v1/*", async (c, next) => {
188
194
  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);
195
+ const context = await requirePrincipal(c.req.raw, auth, { origins: options.origins });
196
+ if (context instanceof Response) {
197
+ if (context.status === 401) return Response.json({ error: { code: "UNAUTHENTICATED", message: "send a valid bearer token" } }, { status: 401, headers: { "www-authenticate": "Bearer" } });
198
+ if (context.status === 403) return fail("FORBIDDEN", "this credential may not do this", 403);
199
+ return fail("INTERNAL", "the credential check is unavailable", context.status);
200
+ }
191
201
  c.set("context", context);
192
202
  await next();
193
203
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-comments",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Comment threads and suggestions anchored to any content, from people and agents.",
5
5
  "keywords": [
6
6
  "comments",
@@ -48,6 +48,10 @@
48
48
  "types": "./dist/anchoring.d.ts",
49
49
  "import": "./dist/anchoring.js"
50
50
  },
51
+ "./client": {
52
+ "types": "./dist/client.d.ts",
53
+ "import": "./dist/client.js"
54
+ },
51
55
  "./local": {
52
56
  "types": "./dist/local.d.ts",
53
57
  "import": "./dist/local.js"
@@ -72,7 +76,7 @@
72
76
  },
73
77
  "dependencies": {
74
78
  "@modelcontextprotocol/sdk": "^1.32.2",
75
- "agentic-identity": "^0.4.0",
79
+ "agentic-identity": "^0.6.0",
76
80
  "hono": "^4.13.14",
77
81
  "zod": "^3.25.76"
78
82
  },
@@ -1,6 +1,35 @@
1
1
  import * as Y from 'yjs';
2
2
  import { PrincipalKind } from 'agentic-identity';
3
3
 
4
+ /** What a thread keeps about who wrote a message. Scopes never reach storage. */
5
+ interface Author {
6
+ kind: PrincipalKind;
7
+ name: string;
8
+ }
9
+ interface StampInput {
10
+ kind: PrincipalKind;
11
+ name: string;
12
+ /** Rights the host granted this principal, such as `comments:accept`. Used to decide, never stored. */
13
+ scopes?: string[];
14
+ }
15
+ /**
16
+ * The only author a write accepts. The host makes one from the principal it authenticated, and an
17
+ * operation never reads an author from its input. An object that `stamp()` did not make is refused,
18
+ * and a stamped one cannot change.
19
+ */
20
+ declare class Actor implements Author {
21
+ readonly kind: PrincipalKind;
22
+ readonly name: string;
23
+ readonly scopes: readonly string[];
24
+ private constructor();
25
+ static of(input: StampInput): Actor;
26
+ static is(value: unknown): value is Actor;
27
+ }
28
+ /** Make the author for one authenticated principal. Throws when the kind or name is missing or wrong. */
29
+ declare const stamp: (input: StampInput) => Actor;
30
+ /** The label a UI shows. Agent work stays marked as machine-made. */
31
+ declare function authorLabel(author: Author): "person" | "person via agent" | "agent";
32
+
4
33
  /** A Yjs relative position as JSON. It names an item, not an index, so it follows its text. */
5
34
  interface RelativePositionJson {
6
35
  type?: {
@@ -66,35 +95,6 @@ declare function textOf(type: Y.Text | Y.XmlText): string;
66
95
  /** Pin a range of a Yjs text. Later edits by any replica do not move the anchor off its text. */
67
96
  declare function textAnchor(type: Y.Text | Y.XmlText, from: number, to: number): TextAnchor;
68
97
 
69
- /** What a thread keeps about who wrote a message. Scopes never reach storage. */
70
- interface Author {
71
- kind: PrincipalKind;
72
- name: string;
73
- }
74
- interface StampInput {
75
- kind: PrincipalKind;
76
- name: string;
77
- /** Rights the host granted this principal, such as `comments:accept`. Used to decide, never stored. */
78
- scopes?: string[];
79
- }
80
- /**
81
- * The only author a write accepts. The host makes one from the principal it authenticated, and an
82
- * operation never reads an author from its input. An object that `stamp()` did not make is refused,
83
- * and a stamped one cannot change.
84
- */
85
- declare class Actor implements Author {
86
- readonly kind: PrincipalKind;
87
- readonly name: string;
88
- readonly scopes: readonly string[];
89
- private constructor();
90
- static of(input: StampInput): Actor;
91
- static is(value: unknown): value is Actor;
92
- }
93
- /** Make the author for one authenticated principal. Throws when the kind or name is missing or wrong. */
94
- declare const stamp: (input: StampInput) => Actor;
95
- /** The label a UI shows. Agent work stays marked as machine-made. */
96
- declare function authorLabel(author: Author): "person" | "person via agent" | "agent";
97
-
98
98
  interface Message {
99
99
  id: string;
100
100
  at: string;