agentic-notify 0.2.1 → 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.2.1.** 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
 
@@ -59,10 +58,13 @@ CLI, SDK and MCP talk to that Worker:
59
58
  export NOTIFY_URL=https://notify.example.org NOTIFY_TOKEN=...
60
59
  npx agentic-notify send --to human:ana --type mention --resource doc:d1 --body "Look at this"
61
60
  npx agentic-notify list --unread
62
- npx agentic-notify mcp # tools: list_notifications, read_notification, mark_read,
63
- # mark_all_read, get_preferences, set_preferences, send_notification
61
+ npx agentic-notify mcp # tools: ping, list_notifications, read_notification, mark_read,
62
+ # mark_all_read, get_preferences, set_preferences, unread_count, digest,
63
+ # export_data, erase_data, send_notification
64
64
  ```
65
65
 
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.
67
+
66
68
  ```ts
67
69
  import { createClient } from "agentic-notify/sdk";
68
70
  const client = createClient({ baseUrl: process.env.NOTIFY_URL!, token: process.env.NOTIFY_TOKEN! });
@@ -71,7 +73,16 @@ console.log(await client.unreadCount());
71
73
 
72
74
  ## HTTP API
73
75
 
74
- 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.
75
86
 
76
87
  | Route | Use |
77
88
  | --- | --- |
@@ -84,7 +95,22 @@ All routes sit under `/api/v1` and need `Authorization: Bearer <token>`. `GET /h
84
95
 
85
96
  ## Identity
86
97
 
87
- 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.
88
114
 
89
115
  ## Privacy
90
116
 
@@ -94,6 +120,24 @@ Auth is a port: `PrincipalResolver` turns a request into `{ kind: "human" | "age
94
120
  - Logs hold ids, channel names and error names only. They never hold bodies or addresses.
95
121
  - Erase removes a recipient's own notifications and preferences. Notifications that name the person only as actor stay with their recipients.
96
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
+
97
141
  ## Limits
98
142
 
99
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.2.1",
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-PHY45IVQ.js";
13
+ } from "./chunk-EMQ7EWWD.js";
14
14
 
15
15
  // src/cli.ts
16
16
  import { realpathSync } from "fs";
@@ -1,7 +1,7 @@
1
- import { R as RecipientExport } from './gdpr-BAg1U1w6.js';
1
+ import { R as RecipientExport } from './gdpr-CUq5MUwo.js';
2
2
  import { Store } from 'agentic-identity';
3
- import { S as SendInput, N as Notification, C as Channel, R as Recipient, D as Digest, P as Preferences } from './types-anA8DMQH.js';
4
- import { D as DeliveryChannel, L as Logger } from './types-BdX7UTFu.js';
3
+ import { S as SendInput, N as Notification, C as Channel, R as Recipient, D as Digest, P as Preferences } from './types-C842YAMe.js';
4
+ import { D as DeliveryChannel, L as Logger } from './types-DONKNIOo.js';
5
5
 
6
6
  interface NotifierOptions {
7
7
  store: Store;
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { R as Recipient, P as Preferences, N as Notification } from './types-anA8DMQH.js';
1
+ import { R as Recipient, P as Preferences, N as Notification } from './types-C842YAMe.js';
2
2
 
3
3
  interface RecipientExport {
4
4
  recipient: Recipient;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
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-xe2IAqWe.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
- import { R as Recipient } from './types-anA8DMQH.js';
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-anA8DMQH.js';
5
- import { D as DeliveryChannel } from './types-BdX7UTFu.js';
6
- export { a as DeliveryError, L as Logger } from './types-BdX7UTFu.js';
7
- export { E as EmailChannelOptions, a as EmailMessage, b as EmailPort, c as createEmailChannel } from './testing-46doJP5Y.js';
3
+ import { R as Recipient } from './types-C842YAMe.js';
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';
5
+ import { D as DeliveryChannel } from './types-DONKNIOo.js';
6
+ export { a as DeliveryError, L as Logger } from './types-DONKNIOo.js';
7
+ export { E as EmailChannelOptions, a as EmailMessage, b as EmailPort, c as createEmailChannel } from './testing-B5slvYDV.js';
8
8
  export { D1Like, D1_SCHEMA, createD1Store } from 'agentic-identity/cloudflare';
9
- export { R as RecipientExport } from './gdpr-BAg1U1w6.js';
9
+ export { R as RecipientExport } from './gdpr-CUq5MUwo.js';
10
10
  import 'zod';
11
11
  import 'agentic-identity/testing';
12
12
 
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-PHY45IVQ.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-PHY45IVQ.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.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { Client } from './sdk.js';
3
- import './types-anA8DMQH.js';
3
+ import './types-C842YAMe.js';
4
4
  import 'zod';
5
- import './gdpr-BAg1U1w6.js';
5
+ import './gdpr-CUq5MUwo.js';
6
6
 
7
7
  /** Without a client, only `ping` works and the other tools report that setup is missing. */
8
8
  declare function createMcpServer(client?: Client): McpServer;
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-PHY45IVQ.js";
16
+ } from "./chunk-EMQ7EWWD.js";
17
17
 
18
18
  // src/mcp.ts
19
19
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -72,6 +72,26 @@ function createMcpServer(client) {
72
72
  inputSchema: preferencesSchema.shape,
73
73
  annotations: write
74
74
  }, (a) => need((c) => c.setPreferences(a))());
75
+ server.registerTool(
76
+ "unread_count",
77
+ { description: "Count your unread notifications.", inputSchema: {}, annotations: read },
78
+ need((c) => c.unreadCount())
79
+ );
80
+ server.registerTool("digest", {
81
+ description: "Group your unread notifications by type. Optional since: epoch milliseconds.",
82
+ inputSchema: { since: z.number().int().min(0).optional() },
83
+ annotations: read
84
+ }, ({ since }) => need((c) => c.digest(since))());
85
+ server.registerTool(
86
+ "export_data",
87
+ { description: "Export all data held about you.", inputSchema: {}, annotations: read },
88
+ need((c) => c.exportData())
89
+ );
90
+ server.registerTool("erase_data", {
91
+ description: "Permanently delete all data held about you. Pass confirm: true. This cannot be undone.",
92
+ inputSchema: { confirm: z.literal(true) },
93
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
94
+ }, () => need((c) => c.erase())());
75
95
  server.registerTool("send_notification", {
76
96
  description: "Send a notification. You must hold the send right. You are the actor.",
77
97
  inputSchema: { recipient: recipientSchema, type: typeNameSchema, resource: resourceRefSchema, body: z.string().max(2e3) },
package/dist/sdk.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { N as Notification, D as Digest, P as Preferences, R as Recipient, a as ResourceRef } from './types-anA8DMQH.js';
2
- import { R as RecipientExport } from './gdpr-BAg1U1w6.js';
1
+ import { N as Notification, D as Digest, P as Preferences, R as Recipient, a as ResourceRef } from './types-C842YAMe.js';
2
+ import { R as RecipientExport } from './gdpr-CUq5MUwo.js';
3
3
  import 'zod';
4
4
 
5
5
  declare class ApiError extends Error {
@@ -46,17 +46,17 @@ declare function createClient(options: ClientOptions): {
46
46
  type: string;
47
47
  id: string;
48
48
  recipient: {
49
- kind: "human" | "agent";
50
49
  id: string;
50
+ kind: "human" | "agent";
51
51
  };
52
52
  resource: {
53
- kind: string;
54
53
  id: string;
54
+ kind: string;
55
55
  url?: string | undefined;
56
56
  };
57
57
  actor: {
58
- kind: "human" | "agent";
59
58
  id: string;
59
+ kind: "human" | "agent";
60
60
  };
61
61
  body: string;
62
62
  createdAt: number;
@@ -66,17 +66,17 @@ declare function createClient(options: ClientOptions): {
66
66
  type: string;
67
67
  id: string;
68
68
  recipient: {
69
- kind: "human" | "agent";
70
69
  id: string;
70
+ kind: "human" | "agent";
71
71
  };
72
72
  resource: {
73
- kind: string;
74
73
  id: string;
74
+ kind: string;
75
75
  url?: string | undefined;
76
76
  };
77
77
  actor: {
78
- kind: "human" | "agent";
79
78
  id: string;
79
+ kind: "human" | "agent";
80
80
  };
81
81
  body: string;
82
82
  createdAt: number;
@@ -1,6 +1,6 @@
1
1
  import 'agentic-identity/testing';
2
- import { R as Recipient } from './types-anA8DMQH.js';
3
- import { D as DeliveryChannel } from './types-BdX7UTFu.js';
2
+ import { R as Recipient } from './types-C842YAMe.js';
3
+ import { D as DeliveryChannel } from './types-DONKNIOo.js';
4
4
 
5
5
  interface EmailMessage {
6
6
  to: string;
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { runStoreContract } from 'agentic-identity/testing';
2
- export { T as TestEmailAdapter } from './testing-46doJP5Y.js';
3
- import './types-anA8DMQH.js';
2
+ export { T as TestEmailAdapter } from './testing-B5slvYDV.js';
3
+ import './types-C842YAMe.js';
4
4
  import 'zod';
5
- import './types-BdX7UTFu.js';
5
+ import './types-DONKNIOo.js';
@@ -1,4 +1,4 @@
1
- import { N as Notification } from './types-anA8DMQH.js';
1
+ import { N as Notification } from './types-C842YAMe.js';
2
2
 
3
3
  /** A delivery channel other than the inbox. */
4
4
  interface DeliveryChannel {
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-xe2IAqWe.js';
3
- import { R as Recipient } from './types-anA8DMQH.js';
1
+ import { Auth, PrincipalContext, Store } from 'agentic-identity';
2
+ import { N as Notifier, c as createDispatcher } from './dispatch-CfflYJ-R.js';
3
+ import { R as Recipient } from './types-C842YAMe.js';
4
4
  import { D1Like } from 'agentic-identity/cloudflare';
5
- import './gdpr-BAg1U1w6.js';
6
- import 'agentic-identity';
7
- import './types-BdX7UTFu.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;
5
+ import './gdpr-CUq5MUwo.js';
6
+ import './types-DONKNIOo.js';
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-PHY45IVQ.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.2.1",
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
  },
@@ -6,11 +6,11 @@ declare const recipientSchema: z.ZodObject<{
6
6
  kind: z.ZodEnum<["human", "agent"]>;
7
7
  id: z.ZodString;
8
8
  }, "strict", z.ZodTypeAny, {
9
- kind: "human" | "agent";
10
9
  id: string;
11
- }, {
12
10
  kind: "human" | "agent";
11
+ }, {
13
12
  id: string;
13
+ kind: "human" | "agent";
14
14
  }>;
15
15
  type Recipient = z.infer<typeof recipientSchema>;
16
16
  declare const resourceRefSchema: z.ZodObject<{
@@ -18,12 +18,12 @@ declare const resourceRefSchema: z.ZodObject<{
18
18
  id: z.ZodString;
19
19
  url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
20
20
  }, "strict", z.ZodTypeAny, {
21
- kind: string;
22
21
  id: string;
22
+ kind: string;
23
23
  url?: string | undefined;
24
24
  }, {
25
- kind: string;
26
25
  id: string;
26
+ kind: string;
27
27
  url?: string | undefined;
28
28
  }>;
29
29
  type ResourceRef = z.infer<typeof resourceRefSchema>;
@@ -31,11 +31,11 @@ declare const actorSchema: z.ZodObject<{
31
31
  kind: z.ZodEnum<["human", "agent"]>;
32
32
  id: z.ZodString;
33
33
  }, "strict", z.ZodTypeAny, {
34
- kind: "human" | "agent";
35
34
  id: string;
36
- }, {
37
35
  kind: "human" | "agent";
36
+ }, {
38
37
  id: string;
38
+ kind: "human" | "agent";
39
39
  }>;
40
40
  type Actor = z.infer<typeof actorSchema>;
41
41
  declare const notificationSchema: z.ZodObject<{
@@ -44,11 +44,11 @@ declare const notificationSchema: z.ZodObject<{
44
44
  kind: z.ZodEnum<["human", "agent"]>;
45
45
  id: z.ZodString;
46
46
  }, "strict", z.ZodTypeAny, {
47
- kind: "human" | "agent";
48
47
  id: string;
49
- }, {
50
48
  kind: "human" | "agent";
49
+ }, {
51
50
  id: string;
51
+ kind: "human" | "agent";
52
52
  }>;
53
53
  type: z.ZodString;
54
54
  resource: z.ZodObject<{
@@ -56,23 +56,23 @@ declare const notificationSchema: z.ZodObject<{
56
56
  id: z.ZodString;
57
57
  url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
58
58
  }, "strict", z.ZodTypeAny, {
59
- kind: string;
60
59
  id: string;
60
+ kind: string;
61
61
  url?: string | undefined;
62
62
  }, {
63
- kind: string;
64
63
  id: string;
64
+ kind: string;
65
65
  url?: string | undefined;
66
66
  }>;
67
67
  actor: z.ZodObject<{
68
68
  kind: z.ZodEnum<["human", "agent"]>;
69
69
  id: z.ZodString;
70
70
  }, "strict", z.ZodTypeAny, {
71
- kind: "human" | "agent";
72
71
  id: string;
73
- }, {
74
72
  kind: "human" | "agent";
73
+ }, {
75
74
  id: string;
75
+ kind: "human" | "agent";
76
76
  }>;
77
77
  body: z.ZodString;
78
78
  createdAt: z.ZodNumber;
@@ -81,17 +81,17 @@ declare const notificationSchema: z.ZodObject<{
81
81
  type: string;
82
82
  id: string;
83
83
  recipient: {
84
- kind: "human" | "agent";
85
84
  id: string;
85
+ kind: "human" | "agent";
86
86
  };
87
87
  resource: {
88
- kind: string;
89
88
  id: string;
89
+ kind: string;
90
90
  url?: string | undefined;
91
91
  };
92
92
  actor: {
93
- kind: "human" | "agent";
94
93
  id: string;
94
+ kind: "human" | "agent";
95
95
  };
96
96
  body: string;
97
97
  createdAt: number;
@@ -100,17 +100,17 @@ declare const notificationSchema: z.ZodObject<{
100
100
  type: string;
101
101
  id: string;
102
102
  recipient: {
103
- kind: "human" | "agent";
104
103
  id: string;
104
+ kind: "human" | "agent";
105
105
  };
106
106
  resource: {
107
- kind: string;
108
107
  id: string;
108
+ kind: string;
109
109
  url?: string | undefined;
110
110
  };
111
111
  actor: {
112
- kind: "human" | "agent";
113
112
  id: string;
113
+ kind: "human" | "agent";
114
114
  };
115
115
  body: string;
116
116
  createdAt: number;
@@ -122,11 +122,11 @@ declare const sendInputSchema: z.ZodObject<{
122
122
  kind: z.ZodEnum<["human", "agent"]>;
123
123
  id: z.ZodString;
124
124
  }, "strict", z.ZodTypeAny, {
125
- kind: "human" | "agent";
126
125
  id: string;
127
- }, {
128
126
  kind: "human" | "agent";
127
+ }, {
129
128
  id: string;
129
+ kind: "human" | "agent";
130
130
  }>;
131
131
  type: z.ZodString;
132
132
  resource: z.ZodObject<{
@@ -134,55 +134,55 @@ declare const sendInputSchema: z.ZodObject<{
134
134
  id: z.ZodString;
135
135
  url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
136
136
  }, "strict", z.ZodTypeAny, {
137
- kind: string;
138
137
  id: string;
138
+ kind: string;
139
139
  url?: string | undefined;
140
140
  }, {
141
- kind: string;
142
141
  id: string;
142
+ kind: string;
143
143
  url?: string | undefined;
144
144
  }>;
145
145
  actor: z.ZodObject<{
146
146
  kind: z.ZodEnum<["human", "agent"]>;
147
147
  id: z.ZodString;
148
148
  }, "strict", z.ZodTypeAny, {
149
- kind: "human" | "agent";
150
149
  id: string;
151
- }, {
152
150
  kind: "human" | "agent";
151
+ }, {
153
152
  id: string;
153
+ kind: "human" | "agent";
154
154
  }>;
155
155
  body: z.ZodString;
156
156
  }, "strict", z.ZodTypeAny, {
157
157
  type: string;
158
158
  recipient: {
159
- kind: "human" | "agent";
160
159
  id: string;
160
+ kind: "human" | "agent";
161
161
  };
162
162
  resource: {
163
- kind: string;
164
163
  id: string;
164
+ kind: string;
165
165
  url?: string | undefined;
166
166
  };
167
167
  actor: {
168
- kind: "human" | "agent";
169
168
  id: string;
169
+ kind: "human" | "agent";
170
170
  };
171
171
  body: string;
172
172
  }, {
173
173
  type: string;
174
174
  recipient: {
175
- kind: "human" | "agent";
176
175
  id: string;
176
+ kind: "human" | "agent";
177
177
  };
178
178
  resource: {
179
- kind: string;
180
179
  id: string;
180
+ kind: string;
181
181
  url?: string | undefined;
182
182
  };
183
183
  actor: {
184
- kind: "human" | "agent";
185
184
  id: string;
185
+ kind: "human" | "agent";
186
186
  };
187
187
  body: string;
188
188
  }>;