agentic-notify 0.3.0 → 0.4.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
@@ -5,16 +5,15 @@
5
5
 
6
6
  Notifications for humans and agents: in-app inbox, email and webhook delivery, with a CLI, API, MCP server and SDK.
7
7
 
8
- **Status: 0.3.0.** Breaking for 0.1.0 code and databases, see the CHANGELOG upgrade steps. Works today: the notification model, preferences and mute, digest, a store port from agentic-identity with its memory and D1 adapters, signed webhook and email delivery through a dispatcher, an HTTP API in a Worker, an SDK, a CLI, MCP tools, per-recipient export and erase, and a retention sweep. Not done: a real email provider adapter, webhook endpoint management, and a web UI.
8
+ **Status: 0.4.0.** Breaking for Worker auth, see "Migrating from `NOTIFY_TOKENS`" below. Breaking for 0.1.0 code and databases, see the CHANGELOG upgrade steps. Works today: the notification model, preferences and mute, digest, a store port from agentic-identity with its memory and D1 adapters, signed webhook and email delivery through a dispatcher, an HTTP API in a Worker, an SDK, a CLI, MCP tools, per-recipient export and erase, and a retention sweep. Not done: a real email provider adapter, webhook endpoint management, and a web UI.
9
9
 
10
10
  ## Live demo
11
11
 
12
- A demo Worker runs at https://agentic-notify-demo.hello-fd1.workers.dev. `GET /health` answers 200. The `/api/v1` routes need a bearer token, so without one they answer 401. The demo keeps its data in the EU and sweeps notifications after 7 days.
12
+ A demo Worker runs at https://agentic-notify-demo.hello-fd1.workers.dev. `GET /health` answers 200. The `/api/v1` routes need a key, so without one they answer 401. The demo keeps its data in the EU and sweeps notifications after 7 days.
13
13
 
14
- Redeploy your own copy with `wrangler.demo.jsonc` (create the D1 database with `--location weur`, put its id in the file, set the `NOTIFY_TOKENS` secret):
14
+ Redeploy your own copy with `wrangler.demo.jsonc` (create the D1 database with `--location weur`, put its id in the file, then add principals and keys, see Identity):
15
15
 
16
16
  ```sh
17
- npx wrangler secret put NOTIFY_TOKENS --config wrangler.demo.jsonc
18
17
  WRANGLER_SEND_METRICS=false npm run deploy:demo
19
18
  ```
20
19
 
@@ -42,16 +41,16 @@ await notifier.send({
42
41
  const { items } = await notifier.list({ kind: "agent", id: "helper" });
43
42
  ```
44
43
 
45
- Worker (D1 binding `DB`, secret `NOTIFY_TOKENS`):
44
+ Worker (D1 binding `DB`):
46
45
 
47
46
  ```sh
48
47
  # Create the database first: npx wrangler d1 create agentic-notify --location weur
49
- # NOTIFY_TOKENS is JSON: { "<token of 32+ bytes>": { "kind": "human", "id": "ana", "canSend": true } }
50
- npx wrangler secret put NOTIFY_TOKENS
51
48
  npx wrangler deploy
52
49
  ```
53
50
 
54
- Without a D1 binding or `NOTIFY_TOKENS`, every `/api/v1` call answers 503. Nobody gets in by default.
51
+ The Worker reads keys and principals from the same D1 database, with `agentic-identity`. Issue a key for each caller with `agentic-identity` (for example `issueCredential`, or its Worker), into this database. See Identity below.
52
+
53
+ Without a D1 binding, every `/api/v1` call answers 503. A request without a valid key answers 401, on unknown paths too. Nobody gets in by default.
55
54
 
56
55
  CLI, SDK and MCP talk to that Worker:
57
56
 
@@ -64,7 +63,7 @@ npx agentic-notify mcp # tools: ping, list_notifications, read_notification
64
63
  # export_data, erase_data, send_notification
65
64
  ```
66
65
 
67
- The MCP server needs `NOTIFY_URL` and `NOTIFY_TOKEN`. Without them only `ping` works. `erase_data` needs `confirm: true`.
66
+ The MCP server needs `NOTIFY_URL` and `NOTIFY_TOKEN`. Without them only `ping` works. `erase_data` needs `confirm: true` and a key with `notify:admin`. `send_notification` needs `notify-send:write`. A tool that lacks its scope reports 403.
68
67
 
69
68
  ```ts
70
69
  import { createClient } from "agentic-notify/sdk";
@@ -74,7 +73,16 @@ console.log(await client.unreadCount());
74
73
 
75
74
  ## HTTP API
76
75
 
77
- All routes sit under `/api/v1` and need `Authorization: Bearer <token>`. `GET /health` is open.
76
+ All routes sit under `/api/v1` and need `Authorization: Bearer <key>`. `GET /health` is open. A missing or bad key answers 401. A good key without the scope of the route answers 403. Each route needs one scope:
77
+
78
+ | Scope | Routes |
79
+ | --- | --- |
80
+ | `notify:read` | `GET /notifications`, `GET /notifications/:id`, `GET /unread-count`, `GET /digest`, `GET /preferences`, `GET /me/export` |
81
+ | `notify:write` | `POST /notifications/:id/read`, `POST /notifications/read-all`, `PUT /preferences` |
82
+ | `notify-send:write` | `POST /notifications` |
83
+ | `notify:admin` | `DELETE /me` |
84
+
85
+ A scope with a higher action covers the lower ones, so `notify:admin` covers `notify:write` and `notify:read`. `*:admin` covers all. `notify-send` is a separate resource, so a key that can only send cannot read.
78
86
 
79
87
  | Route | Use |
80
88
  | --- | --- |
@@ -87,7 +95,22 @@ All routes sit under `/api/v1` and need `Authorization: Bearer <token>`. `GET /h
87
95
 
88
96
  ## Identity
89
97
 
90
- Auth is a port: `PrincipalResolver` turns a request into `{ kind: "human" | "agent", id }` or null. `createStaticTokenResolver` is a small example. To use another identity system, call `createWorker({ resolver })` with your own resolver. A caller reads and changes only its own notifications.
98
+ The Worker authenticates with `agentic-identity`. `requirePrincipal` from `agentic-identity/http` reads the credential, and the `Auth` port checks that it is not expired or revoked. The default `Auth` takes a bearer key, and also a `session` cookie that `agentic-identity` sets. A write with a cookie is refused, because no origin is allowed here. Pass your own `Auth` to accept bearer keys only. The default `Auth` is `createStoreAuth` over the same D1 store. Each route checks one scope (see HTTP API). A delegated agent holds only the scopes that its person also holds.
99
+
100
+ A person, and a `user_token` of that person, use the inbox of the person. An agent (`agent`, `agent_of_human` or `agent_of_agent`) uses the inbox of its own id. A caller reads and changes only its own notifications.
101
+
102
+ To use another identity system, call `createWorker({ auth: (env, store) => myAuth })`, where `auth` builds your own `Auth` from the Worker env and the store. Import `createWorker` from `agentic-notify/worker`.
103
+
104
+ ### Migrating from `NOTIFY_TOKENS`
105
+
106
+ Before 0.4.0, the Worker read a static `NOTIFY_TOKENS` secret: a map of token to `{ kind, id, canSend }`. The tokens had no scopes. The Worker no longer reads it. Steps:
107
+
108
+ 1. Delete the `NOTIFY_TOKENS` secret (`npx wrangler secret delete NOTIFY_TOKENS`).
109
+ 2. Make each caller a principal in the D1 database, and issue it a key with `agentic-identity`. The old `{ "kind": "human" }` becomes kind `human`, and `{ "kind": "agent" }` becomes kind `agent`.
110
+ 3. Give each principal scopes. An old token could read, change and erase its own inbox. To keep that, use `notify:admin`. For less, use `notify:write` (it covers `notify:read`) or `notify:read`. For the old `canSend: true`, add `notify-send:write`. It is a separate resource, so it does not cover `notify:read`: a caller that sends and reads needs both.
111
+ 4. Give the new key to the CLI, SDK and MCP server as `NOTIFY_TOKEN`.
112
+
113
+ Library users: `createWorker` takes `auth` in place of `resolver`. `authorizeSend` still works as an extra check after the scope. Its first argument is now a `PrincipalContext` (`principalId` in place of `id`, no `canSend`). Its default changed: it was "only `canSend: true`", and now every caller with `notify-send:write` may send unless you pass your own.
91
114
 
92
115
  ## Privacy
93
116
 
@@ -97,6 +120,24 @@ Auth is a port: `PrincipalResolver` turns a request into `{ kind: "human" | "age
97
120
  - Logs hold ids, channel names and error names only. They never hold bodies or addresses.
98
121
  - Erase removes a recipient's own notifications and preferences. Notifications that name the person only as actor stay with their recipients.
99
122
 
123
+ ## Testing
124
+
125
+ `agentic-notify/testing` helps you test a store adapter and your own code.
126
+
127
+ `runStoreContract(make)` runs the store contract suite. Call it at the top level of a vitest test
128
+ file, and install `vitest` 3 or later. The factory makes a new store for each call. A store made
129
+ without `shared` must start empty. Stores made with the same `shared` value must see the same data.
130
+ So the factory passes `options` to the store:
131
+
132
+ ```ts
133
+ import { createMemoryStore } from "agentic-notify";
134
+ import { runStoreContract } from "agentic-notify/testing";
135
+
136
+ runStoreContract((options) => createMemoryStore(options));
137
+ ```
138
+
139
+ `TestEmailAdapter` records each message in memory and can fail a set number of sends.
140
+
100
141
  ## Limits
101
142
 
102
143
  - The default Worker serves the inbox. To fan out to webhook and email, compose `createApp` with `createDispatcher` in your own Worker.
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "agentic-notify",
4
- version: "0.3.0",
4
+ version: "0.4.0",
5
5
  description: "Notifications for humans and agents: in-app inbox, webhook and email delivery, with SDK, CLI, MCP server and Worker API.",
6
6
  keywords: [
7
7
  "notifications",
@@ -67,7 +67,7 @@ var package_default = {
67
67
  },
68
68
  dependencies: {
69
69
  "@modelcontextprotocol/sdk": "^1.12.0",
70
- "agentic-identity": "^0.4.0",
70
+ "agentic-identity": "^0.6.0",
71
71
  hono: "^4.13.14",
72
72
  zod: "^3.25.0"
73
73
  },
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import "./chunk-KJKM56FT.js";
10
10
  import "./chunk-ARL7FN6Y.js";
11
11
  import {
12
12
  VERSION
13
- } from "./chunk-O574VONP.js";
13
+ } from "./chunk-EMQ7EWWD.js";
14
14
 
15
15
  // src/cli.ts
16
16
  import { realpathSync } from "fs";
@@ -74,4 +74,4 @@ declare function createDispatcher(options: DispatcherOptions): {
74
74
  notify(input: SendInput): Promise<DispatchResult>;
75
75
  };
76
76
 
77
- export { type DispatchResult as D, type ListOptions as L, NotFoundError as N, type SendResult as S, type DispatcherOptions as a, type ListResult as b, type Notifier as c, type NotifierOptions as d, createDispatcher as e, createNotifier as f };
77
+ export { type DispatchResult as D, type ListOptions as L, type Notifier as N, type SendResult as S, type DispatcherOptions as a, type ListResult as b, createDispatcher as c, NotFoundError as d, type NotifierOptions as e, createNotifier as f };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { D as DispatchResult, a as DispatcherOptions, L as ListOptions, b as ListResult, N as NotFoundError, c as Notifier, d as NotifierOptions, S as SendResult, e as createDispatcher, f as createNotifier } from './dispatch-LBnkLOQg.js';
1
+ export { D as DispatchResult, a as DispatcherOptions, L as ListOptions, b as ListResult, d as NotFoundError, N as Notifier, e as NotifierOptions, S as SendResult, c as createDispatcher, f as createNotifier } from './dispatch-CfflYJ-R.js';
2
2
  export { Doc, Page, Query, Scalar, Store, StoreOps, createMemoryStore } from 'agentic-identity';
3
3
  import { R as Recipient } from './types-C842YAMe.js';
4
4
  export { A as Actor, b as CHANNELS, C as Channel, c as DEFAULT_CHANNELS, D as Digest, d as DigestGroup, N as Notification, P as Preferences, a as ResourceRef, S as SendInput, e as defaultPreferences, r as recipientKey } from './types-C842YAMe.js';
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  } from "./chunk-ARL7FN6Y.js";
24
24
  import {
25
25
  VERSION
26
- } from "./chunk-O574VONP.js";
26
+ } from "./chunk-EMQ7EWWD.js";
27
27
  export {
28
28
  CHANNELS,
29
29
  D1_SCHEMA,
package/dist/mcp-bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  VERSION
4
- } from "./chunk-O574VONP.js";
4
+ } from "./chunk-EMQ7EWWD.js";
5
5
 
6
6
  // src/mcp-bin.ts
7
7
  var HELP = `agentic-notify-mcp ${VERSION}
package/dist/mcp.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-ARL7FN6Y.js";
14
14
  import {
15
15
  VERSION
16
- } from "./chunk-O574VONP.js";
16
+ } from "./chunk-EMQ7EWWD.js";
17
17
 
18
18
  // src/mcp.ts
19
19
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/dist/worker.d.ts CHANGED
@@ -1,56 +1,39 @@
1
- import { z } from 'zod';
2
- import { c as Notifier, e as createDispatcher } from './dispatch-LBnkLOQg.js';
1
+ import { Auth, PrincipalContext, Store } from 'agentic-identity';
2
+ import { N as Notifier, c as createDispatcher } from './dispatch-CfflYJ-R.js';
3
3
  import { R as Recipient } from './types-C842YAMe.js';
4
4
  import { D1Like } from 'agentic-identity/cloudflare';
5
5
  import './gdpr-CUq5MUwo.js';
6
- import 'agentic-identity';
7
6
  import './types-DONKNIOo.js';
8
-
9
- /** Who is calling. `kind` has no default: a missing kind is an error. */
10
- declare const principalSchema: z.ZodObject<{
11
- kind: z.ZodEnum<["human", "agent"]>;
12
- id: z.ZodString;
13
- /** Allowed to send notifications to others. */
14
- canSend: z.ZodOptional<z.ZodBoolean>;
15
- }, "strict", z.ZodTypeAny, {
16
- kind: "human" | "agent";
17
- id: string;
18
- canSend?: boolean | undefined;
19
- }, {
20
- kind: "human" | "agent";
21
- id: string;
22
- canSend?: boolean | undefined;
23
- }>;
24
- type Principal = z.infer<typeof principalSchema>;
25
- /** Port: turn a request into a principal, or null for "not signed in". Plug in any identity system. */
26
- type PrincipalResolver = (request: Request) => Promise<Principal | null> | Principal | null;
7
+ import 'zod';
27
8
 
28
9
  interface AppOptions {
29
10
  notifier: Notifier;
30
11
  /** When set, sends also go to webhook and email channels. */
31
12
  dispatcher?: ReturnType<typeof createDispatcher>;
32
- resolvePrincipal: PrincipalResolver;
33
- /** May this principal send to this recipient? Default: only principals with `canSend: true`. */
34
- authorizeSend?: (principal: Principal, recipient: Recipient) => Promise<boolean> | boolean;
13
+ /** Reads the credential on a request. Each route then checks one scope. */
14
+ auth: Auth;
15
+ /** Extra check on a send, after the `notify-send:write` scope. Default: allow. */
16
+ authorizeSend?: (principal: PrincipalContext, recipient: Recipient) => Promise<boolean> | boolean;
35
17
  }
36
18
 
37
19
  interface WorkerEnv {
38
20
  NOTIFY_NAME?: string;
39
21
  /** D1 binding. */
40
22
  DB?: D1Like;
41
- /** Secret. JSON map of bearer token (32 bytes or more) to principal. */
42
- NOTIFY_TOKENS?: string;
43
23
  /** Data region of the store: "eu", "us" or "unknown". Default "eu". */
44
24
  NOTIFY_REGION?: string;
45
25
  /** Retention window in days. Unset keeps notifications. */
46
26
  NOTIFY_RETENTION_DAYS?: string;
47
27
  }
48
28
  interface WorkerConfig {
49
- /** Build the principal resolver from the environment. Null means not configured. */
50
- resolver(env: WorkerEnv): Promise<PrincipalResolver | null>;
29
+ /**
30
+ * Build the `Auth` port. Default: `createStoreAuth` over the same store, so keys that
31
+ * agentic-identity issues into the database work here. Each route checks a scope.
32
+ */
33
+ auth?(env: WorkerEnv, store: Store): Auth;
51
34
  authorizeSend?: AppOptions["authorizeSend"];
52
35
  }
53
- /** Compose the Worker. Bring your own identity system through `resolver`. */
36
+ /** Compose the Worker. Bring your own identity system through `auth`. */
54
37
  declare function createWorker(config: WorkerConfig): {
55
38
  fetch(request: Request, env: WorkerEnv): Promise<Response>;
56
39
  /** Delete notifications past the retention window. Wire to a cron trigger. */
package/dist/worker.js CHANGED
@@ -9,58 +9,60 @@ import {
9
9
  } from "./chunk-ARL7FN6Y.js";
10
10
  import {
11
11
  VERSION
12
- } from "./chunk-O574VONP.js";
12
+ } from "./chunk-EMQ7EWWD.js";
13
13
 
14
14
  // src/api.ts
15
15
  import { Hono } from "hono";
16
16
  import { bodyLimit } from "hono/body-limit";
17
- import { ZodError, z as z2 } from "zod";
17
+ import { ZodError, z } from "zod";
18
+ import { requirePrincipal } from "agentic-identity/http";
18
19
 
19
20
  // src/auth.ts
20
- import { z } from "zod";
21
- var principalSchema = z.object({
22
- kind: z.enum(["human", "agent"]),
23
- id: z.string().min(1).max(200),
24
- /** Allowed to send notifications to others. */
25
- canSend: z.boolean().optional()
26
- }).strict();
27
- var MIN_TOKEN_BYTES = 32;
28
- var enc = new TextEncoder();
29
- async function digest(token) {
30
- const d = await crypto.subtle.digest("SHA-256", enc.encode(token));
31
- return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join("");
32
- }
33
- async function createStaticTokenResolver(tokens) {
34
- const byDigest = /* @__PURE__ */ new Map();
35
- for (const [token, principal] of Object.entries(tokens)) {
36
- if (enc.encode(token).length < MIN_TOKEN_BYTES) throw new Error(`tokens must hold at least ${MIN_TOKEN_BYTES} bytes`);
37
- byDigest.set(await digest(token), principalSchema.parse(principal));
21
+ var SCOPES = {
22
+ /** List, read, count, digest, read preferences, export. */
23
+ read: "notify:read",
24
+ /** Mark read, change preferences. */
25
+ write: "notify:write",
26
+ /** Erase all data held about the caller. */
27
+ erase: "notify:admin",
28
+ /** Notify someone else. */
29
+ send: "notify-send:write"
30
+ };
31
+ function recipientOf(context) {
32
+ switch (context.kind) {
33
+ case "human":
34
+ return { kind: "human", id: context.principalId };
35
+ case "user_token": {
36
+ const root = context.chain[0];
37
+ if (!root || root.kind !== "human") throw new RangeError("a user token needs a person at the root of its chain");
38
+ return { kind: "human", id: root.id };
39
+ }
40
+ case "agent":
41
+ case "agent_of_human":
42
+ case "agent_of_agent":
43
+ return { kind: "agent", id: context.principalId };
44
+ default:
45
+ throw new RangeError("unknown principal kind");
38
46
  }
39
- if (byDigest.size === 0) throw new Error("no tokens configured");
40
- return async (request) => {
41
- const m = /^Bearer ([^\s]+)$/.exec(request.headers.get("authorization") ?? "");
42
- return m ? byDigest.get(await digest(m[1])) ?? null : null;
43
- };
44
47
  }
45
48
 
46
49
  // src/api.ts
47
- var sendBody = z2.object({ recipient: recipientSchema, type: typeNameSchema, resource: resourceRefSchema, body: z2.string().max(2e3) }).strict();
48
- var num = z2.coerce.number().int().nonnegative();
50
+ var sendBody = z.object({ recipient: recipientSchema, type: typeNameSchema, resource: resourceRefSchema, body: z.string().max(2e3) }).strict();
51
+ var num = z.coerce.number().int().nonnegative();
49
52
  function createApp(options) {
50
53
  const { notifier } = options;
51
- const authorizeSend = options.authorizeSend ?? ((p) => p.canSend === true);
54
+ const authorizeSend = options.authorizeSend ?? (() => true);
52
55
  const app = new Hono();
53
56
  app.get("/health", (c) => c.json({ ok: true }));
54
57
  const api = new Hono();
55
58
  api.use("*", bodyLimit({ maxSize: 64 * 1024, onError: (c) => c.json({ error: "too_large" }, 413) }));
56
- api.use("*", async (c, next) => {
57
- const raw = await options.resolvePrincipal(c.req.raw);
58
- const parsed = principalSchema.safeParse(raw);
59
- if (!parsed.success) return c.json({ error: "unauthorized" }, 401);
60
- c.set("principal", parsed.data);
61
- c.set("me", { kind: parsed.data.kind, id: parsed.data.id });
59
+ const guard = (scope) => async (c, next) => {
60
+ const who = await requirePrincipal(c.req.raw, options.auth, { scope });
61
+ if (who instanceof Response) return who;
62
+ c.set("principal", who);
63
+ c.set("me", recipientOf(who));
62
64
  await next();
63
- });
65
+ };
64
66
  async function json2(c) {
65
67
  try {
66
68
  return await c.req.json();
@@ -68,9 +70,9 @@ function createApp(options) {
68
70
  throw new ZodError([{ code: "custom", path: [], message: "body must be JSON" }]);
69
71
  }
70
72
  }
71
- api.get("/notifications", async (c) => {
73
+ api.get("/notifications", guard(SCOPES.read), async (c) => {
72
74
  const q = c.req.query();
73
- const limit = q.limit === void 0 ? void 0 : z2.coerce.number().int().min(1).parse(q.limit);
75
+ const limit = q.limit === void 0 ? void 0 : z.coerce.number().int().min(1).parse(q.limit);
74
76
  try {
75
77
  return c.json(await notifier.list(c.var.me, { unreadOnly: q.unread === "1" || q.unread === "true", limit, cursor: q.cursor }));
76
78
  } catch (error) {
@@ -78,7 +80,7 @@ function createApp(options) {
78
80
  throw error;
79
81
  }
80
82
  });
81
- api.post("/notifications", async (c) => {
83
+ api.post("/notifications", guard(SCOPES.send), async (c) => {
82
84
  const input = sendBody.parse(await json2(c));
83
85
  if (!await authorizeSend(c.var.principal, input.recipient)) return c.json({ error: "forbidden" }, 403);
84
86
  const full = { ...input, actor: c.var.me };
@@ -87,18 +89,22 @@ function createApp(options) {
87
89
  const deliveries = sent.channels.filter((channel) => channel !== "inbox").map((channel) => ({ channel, ok: false, skipped: "no_channel" }));
88
90
  return c.json({ ...sent, deliveries }, 201);
89
91
  });
90
- api.post("/notifications/read-all", async (c) => c.json({ marked: await notifier.markAllRead(c.var.me) }));
91
- api.get("/notifications/:id", async (c) => c.json(await notifier.get(c.var.me, c.req.param("id"))));
92
- api.post("/notifications/:id/read", async (c) => c.json(await notifier.markRead(c.var.me, c.req.param("id"))));
93
- api.get("/unread-count", async (c) => c.json({ count: await notifier.unreadCount(c.var.me) }));
94
- api.get("/digest", async (c) => {
92
+ api.post("/notifications/read-all", guard(SCOPES.write), async (c) => c.json({ marked: await notifier.markAllRead(c.var.me) }));
93
+ api.get("/notifications/:id", guard(SCOPES.read), async (c) => c.json(await notifier.get(c.var.me, c.req.param("id"))));
94
+ api.post("/notifications/:id/read", guard(SCOPES.write), async (c) => c.json(await notifier.markRead(c.var.me, c.req.param("id"))));
95
+ api.get("/unread-count", guard(SCOPES.read), async (c) => c.json({ count: await notifier.unreadCount(c.var.me) }));
96
+ api.get("/digest", guard(SCOPES.read), async (c) => {
95
97
  const since = c.req.query("since");
96
98
  return c.json(await notifier.digest(c.var.me, since === void 0 ? 0 : num.parse(since)));
97
99
  });
98
- api.get("/preferences", async (c) => c.json(await notifier.getPreferences(c.var.me)));
99
- api.put("/preferences", async (c) => c.json(await notifier.setPreferences(c.var.me, preferencesSchema.parse(await json2(c)))));
100
- api.get("/me/export", async (c) => c.json(await notifier.exportData(c.var.me)));
101
- api.delete("/me", async (c) => c.json(await notifier.erase(c.var.me)));
100
+ api.get("/preferences", guard(SCOPES.read), async (c) => c.json(await notifier.getPreferences(c.var.me)));
101
+ api.put("/preferences", guard(SCOPES.write), async (c) => c.json(await notifier.setPreferences(c.var.me, preferencesSchema.parse(await json2(c)))));
102
+ api.get("/me/export", guard(SCOPES.read), async (c) => c.json(await notifier.exportData(c.var.me)));
103
+ api.delete("/me", guard(SCOPES.erase), async (c) => c.json(await notifier.erase(c.var.me)));
104
+ api.all("*", async (c) => {
105
+ const who = await requirePrincipal(c.req.raw, options.auth);
106
+ return who instanceof Response ? who : c.json({ error: "not_found" }, 404);
107
+ });
102
108
  app.route("/api/v1", api);
103
109
  app.notFound((c) => c.json({ error: "not_found" }, 404));
104
110
  app.onError((error, c) => {
@@ -113,7 +119,7 @@ function createApp(options) {
113
119
  }
114
120
 
115
121
  // src/worker.ts
116
- import { parseRegion } from "agentic-identity";
122
+ import { createStoreAuth, parseRegion } from "agentic-identity";
117
123
  import { createD1Store, D1_SCHEMA } from "agentic-identity/cloudflare";
118
124
  function json(value, status = 200) {
119
125
  return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
@@ -132,16 +138,17 @@ function createWorker(config) {
132
138
  const days = env.NOTIFY_RETENTION_DAYS === void 0 ? void 0 : Number(env.NOTIFY_RETENTION_DAYS);
133
139
  const store = createD1Store(env.DB, { region: parseRegion(env.NOTIFY_REGION ?? "eu") });
134
140
  await store.query(NOTES, { limit: 1 });
135
- return createNotifier({
141
+ const notifier = createNotifier({
136
142
  store,
137
143
  retentionMs: days === void 0 ? void 0 : days * 864e5
138
144
  });
145
+ return { notifier, store };
139
146
  }
140
147
  async function build(env) {
141
- const resolvePrincipal = await config.resolver(env);
142
- const notifier = resolvePrincipal ? await buildNotifier(env) : null;
143
- if (!resolvePrincipal || !notifier) return null;
144
- return createApp({ notifier, resolvePrincipal, authorizeSend: config.authorizeSend });
148
+ const built = await buildNotifier(env);
149
+ if (!built) return null;
150
+ const auth = config.auth ? config.auth(env, built.store) : createStoreAuth(built.store);
151
+ return createApp({ notifier: built.notifier, auth, authorizeSend: config.authorizeSend });
145
152
  }
146
153
  return {
147
154
  async fetch(request, env) {
@@ -164,16 +171,11 @@ function createWorker(config) {
164
171
  },
165
172
  /** Delete notifications past the retention window. Wire to a cron trigger. */
166
173
  async scheduled(_event, env) {
167
- await (await buildNotifier(env))?.sweep();
174
+ await (await buildNotifier(env))?.notifier.sweep();
168
175
  }
169
176
  };
170
177
  }
171
- var worker_default = createWorker({
172
- async resolver(env) {
173
- if (!env.NOTIFY_TOKENS) return null;
174
- return createStaticTokenResolver(JSON.parse(env.NOTIFY_TOKENS));
175
- }
176
- });
178
+ var worker_default = createWorker({});
177
179
  export {
178
180
  createWorker,
179
181
  worker_default as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-notify",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Notifications for humans and agents: in-app inbox, webhook and email delivery, with SDK, CLI, MCP server and Worker API.",
5
5
  "keywords": [
6
6
  "notifications",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@modelcontextprotocol/sdk": "^1.12.0",
69
- "agentic-identity": "^0.4.0",
69
+ "agentic-identity": "^0.6.0",
70
70
  "hono": "^4.13.14",
71
71
  "zod": "^3.25.0"
72
72
  },