agentic-notify 0.1.0 → 0.2.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 +16 -3
- package/dist/{chunk-6CKFBZOF.js → chunk-3N77SYRA.js} +8 -1
- package/dist/{chunk-FMU2ED2H.js → chunk-6QCYA3X7.js} +57 -141
- package/dist/{chunk-DEPJ3SHO.js → chunk-POSHKDNY.js} +2 -2
- package/dist/cli.js +2 -3
- package/dist/{d1-CUcNko0m.d.ts → dispatch-LBnkLOQg.d.ts} +5 -34
- package/dist/{gdpr-9I4eV86m.d.ts → gdpr-CUq5MUwo.d.ts} +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -10
- package/dist/mcp-bin.js +3 -4
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +3 -4
- package/dist/sdk.d.ts +2 -2
- package/dist/{email-UqtllxjF.d.ts → testing-B5slvYDV.d.ts} +3 -2
- package/dist/testing.d.ts +4 -12
- package/dist/testing.js +2 -147
- package/dist/{types-GuAgyMOc.d.ts → types-C842YAMe.d.ts} +1 -1
- package/dist/types-DONKNIOo.d.ts +20 -0
- package/dist/worker.d.ts +10 -6
- package/dist/worker.js +14 -6
- package/package.json +5 -2
- package/dist/chunk-GJFAXI2K.js +0 -104
- package/dist/types-Dkst26cb.d.ts +0 -69
package/README.md
CHANGED
|
@@ -5,7 +5,18 @@
|
|
|
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.1.0,
|
|
8
|
+
**Status: 0.2.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.
|
|
9
|
+
|
|
10
|
+
## Live demo
|
|
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.
|
|
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):
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx wrangler secret put NOTIFY_TOKENS --config wrangler.demo.jsonc
|
|
18
|
+
WRANGLER_SEND_METRICS=false npm run deploy:demo
|
|
19
|
+
```
|
|
9
20
|
|
|
10
21
|
## Install
|
|
11
22
|
|
|
@@ -18,9 +29,9 @@ npm install agentic-notify
|
|
|
18
29
|
Library, with the memory store:
|
|
19
30
|
|
|
20
31
|
```ts
|
|
21
|
-
import { createNotifier,
|
|
32
|
+
import { createNotifier, createMemoryStore } from "agentic-notify";
|
|
22
33
|
|
|
23
|
-
const notifier = createNotifier({ store:
|
|
34
|
+
const notifier = createNotifier({ store: createMemoryStore() });
|
|
24
35
|
await notifier.send({
|
|
25
36
|
recipient: { kind: "agent", id: "helper" },
|
|
26
37
|
type: "approval_request",
|
|
@@ -78,6 +89,8 @@ Auth is a port: `PrincipalResolver` turns a request into `{ kind: "human" | "age
|
|
|
78
89
|
## Privacy
|
|
79
90
|
|
|
80
91
|
- Export and erase work per recipient. `retentionMs` plus `sweep()` (or a cron trigger on the Worker, with `NOTIFY_RETENTION_DAYS`) deletes old notifications.
|
|
92
|
+
- Erase and sweep delete in batches of 100 inside one transaction each, so a large erase does not cost one query per row.
|
|
93
|
+
- The Worker reads `NOTIFY_REGION` (`eu`, `us` or `unknown`, default `eu`). The D1 adapter refuses a database that does not report a serving region in that area. For local runs without a real D1, set `NOTIFY_REGION=unknown`.
|
|
81
94
|
- Logs hold ids, channel names and error names only. They never hold bodies or addresses.
|
|
82
95
|
- Erase removes a recipient's own notifications and preferences. Notifications that name the person only as actor stay with their recipients.
|
|
83
96
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createMemoryStore } from "agentic-identity";
|
|
3
|
+
|
|
1
4
|
// src/delivery/dispatch.ts
|
|
2
5
|
function createDispatcher(options) {
|
|
3
6
|
return {
|
|
@@ -148,6 +151,7 @@ function createWebhookChannel(options) {
|
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
// src/index.ts
|
|
154
|
+
import { createD1Store, D1_SCHEMA } from "agentic-identity/cloudflare";
|
|
151
155
|
var PACKAGE_NAME = "agentic-notify";
|
|
152
156
|
|
|
153
157
|
export {
|
|
@@ -157,5 +161,8 @@ export {
|
|
|
157
161
|
signPayload,
|
|
158
162
|
verifySignature,
|
|
159
163
|
createWebhookChannel,
|
|
160
|
-
PACKAGE_NAME
|
|
164
|
+
PACKAGE_NAME,
|
|
165
|
+
createMemoryStore,
|
|
166
|
+
createD1Store,
|
|
167
|
+
D1_SCHEMA
|
|
161
168
|
};
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
canonicalJson,
|
|
3
|
-
clampLimit,
|
|
4
|
-
decodeCursor,
|
|
5
|
-
encodeCursor
|
|
6
|
-
} from "./chunk-GJFAXI2K.js";
|
|
7
|
-
|
|
8
1
|
// package.json
|
|
9
2
|
var package_default = {
|
|
10
3
|
name: "agentic-notify",
|
|
11
|
-
version: "0.
|
|
4
|
+
version: "0.2.0",
|
|
12
5
|
description: "Notifications for humans and agents: in-app inbox, webhook and email delivery, with SDK, CLI, MCP server and Worker API.",
|
|
13
6
|
keywords: [
|
|
14
7
|
"notifications",
|
|
@@ -69,10 +62,12 @@ var package_default = {
|
|
|
69
62
|
clean: "rm -rf dist",
|
|
70
63
|
prepack: "npm run build",
|
|
71
64
|
"ci:local": "npm run build && npm run typecheck && npm run check:worker && npm test",
|
|
65
|
+
"deploy:demo": "wrangler deploy --config wrangler.demo.jsonc",
|
|
72
66
|
"check:worker": "esbuild src/worker.ts --bundle --platform=neutral --format=esm --log-level=warning --outfile=node_modules/.cache/worker-check.js"
|
|
73
67
|
},
|
|
74
68
|
dependencies: {
|
|
75
69
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
70
|
+
"agentic-identity": "^0.4.0",
|
|
76
71
|
hono: "^4.13.14",
|
|
77
72
|
zod: "^3.25.0"
|
|
78
73
|
},
|
|
@@ -82,7 +77,8 @@ var package_default = {
|
|
|
82
77
|
miniflare: "^4.20260730.0",
|
|
83
78
|
tsup: "^8.0.0",
|
|
84
79
|
typescript: "^5.6.0",
|
|
85
|
-
vitest: "^3.0.0"
|
|
80
|
+
vitest: "^3.0.0",
|
|
81
|
+
wrangler: "^4.155.0"
|
|
86
82
|
},
|
|
87
83
|
peerDependencies: {
|
|
88
84
|
vitest: ">=3"
|
|
@@ -142,12 +138,18 @@ function recipientKey(recipient) {
|
|
|
142
138
|
return `${recipient.kind}:${recipient.id}`;
|
|
143
139
|
}
|
|
144
140
|
|
|
145
|
-
// src/core/service.ts
|
|
146
|
-
import { z as z2 } from "zod";
|
|
147
|
-
|
|
148
141
|
// src/core/gdpr.ts
|
|
149
142
|
var NOTES = "notifications";
|
|
150
143
|
var PREFS = "preferences";
|
|
144
|
+
var DELETE_BATCH = 100;
|
|
145
|
+
async function deleteAll(store, ids) {
|
|
146
|
+
for (let i = 0; i < ids.length; i += DELETE_BATCH) {
|
|
147
|
+
const batch = ids.slice(i, i + DELETE_BATCH);
|
|
148
|
+
await store.transaction(async (tx) => {
|
|
149
|
+
for (const id2 of batch) await tx.delete(NOTES, id2);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
151
153
|
async function* notificationDocs(store, recipient, ordered = true) {
|
|
152
154
|
let cursor;
|
|
153
155
|
do {
|
|
@@ -165,7 +167,7 @@ async function* notificationDocs(store, recipient, ordered = true) {
|
|
|
165
167
|
async function exportRecipient(store, recipient) {
|
|
166
168
|
const notifications = [];
|
|
167
169
|
for await (const item of notificationDocs(store, recipient, false)) {
|
|
168
|
-
const { recipientKey: _drop, ...rest } = item.
|
|
170
|
+
const { recipientKey: _drop, ...rest } = item.data;
|
|
169
171
|
notifications.push(notificationSchema.parse(rest));
|
|
170
172
|
}
|
|
171
173
|
notifications.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
@@ -179,7 +181,7 @@ async function eraseRecipient(store, recipient) {
|
|
|
179
181
|
const ids = [];
|
|
180
182
|
for await (const item of notificationDocs(store, recipient, false)) ids.push(item.id);
|
|
181
183
|
if (ids.length === 0) break;
|
|
182
|
-
|
|
184
|
+
await deleteAll(store, ids);
|
|
183
185
|
removed += ids.length;
|
|
184
186
|
}
|
|
185
187
|
await store.delete(PREFS, recipientKey(recipient));
|
|
@@ -192,20 +194,19 @@ async function sweepBefore(store, cutoff) {
|
|
|
192
194
|
const gone = /* @__PURE__ */ new Set();
|
|
193
195
|
for (; ; ) {
|
|
194
196
|
const page = await store.query(NOTES, { orderBy: { field: "createdAt", direction: "asc" }, limit });
|
|
195
|
-
const old = page.items.filter((i) => typeof i.
|
|
197
|
+
const old = page.items.filter((i) => typeof i.data.createdAt === "number" && i.data.createdAt < cutoff);
|
|
196
198
|
if (old.some((i) => gone.has(i.id))) throw new Error("retention sweep did not finish: deletes had no effect");
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
gone.add(item.id);
|
|
200
|
-
}
|
|
199
|
+
await deleteAll(store, old.map((i) => i.id));
|
|
200
|
+
for (const item of old) gone.add(item.id);
|
|
201
201
|
removed += old.length;
|
|
202
202
|
if (old.length > 0) continue;
|
|
203
|
-
if (page.items.length < limit || page.items.some((i) => typeof i.
|
|
203
|
+
if (page.items.length < limit || page.items.some((i) => typeof i.data.createdAt === "number")) return removed;
|
|
204
204
|
throw new Error("retention sweep blocked: a full page of notifications has no usable createdAt");
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// src/core/service.ts
|
|
209
|
+
import { z as z2 } from "zod";
|
|
209
210
|
var NotFoundError = class extends Error {
|
|
210
211
|
code = "NOT_FOUND";
|
|
211
212
|
constructor(message = "notification not found") {
|
|
@@ -220,6 +221,22 @@ function fromDoc(doc) {
|
|
|
220
221
|
const { recipientKey: _drop, ...rest } = doc;
|
|
221
222
|
return notificationSchema.parse(rest);
|
|
222
223
|
}
|
|
224
|
+
var DEFAULT_LIMIT = 50;
|
|
225
|
+
var MAX_LIMIT = 200;
|
|
226
|
+
function clampLimit(limit) {
|
|
227
|
+
if (limit === void 0) return DEFAULT_LIMIT;
|
|
228
|
+
if (!Number.isInteger(limit) || limit < 1) throw new RangeError("limit must be an integer of 1 or more");
|
|
229
|
+
return Math.min(limit, MAX_LIMIT);
|
|
230
|
+
}
|
|
231
|
+
function checkCursor(cursor) {
|
|
232
|
+
try {
|
|
233
|
+
const bytes = Uint8Array.from(atob(cursor.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
234
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
235
|
+
if (Array.isArray(parsed) && parsed.length === 2 && Number.isFinite(parsed[0]) && typeof parsed[1] === "string") return;
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
throw new RangeError("bad cursor");
|
|
239
|
+
}
|
|
223
240
|
function createNotifier(options) {
|
|
224
241
|
const { store } = options;
|
|
225
242
|
const now = options.now ?? (() => Date.now());
|
|
@@ -253,41 +270,43 @@ function createNotifier(options) {
|
|
|
253
270
|
readAt: null
|
|
254
271
|
});
|
|
255
272
|
if (!channels.includes("inbox")) return { notification, stored: false, channels, muted: false };
|
|
256
|
-
|
|
257
|
-
throw new Error("notification id collision");
|
|
258
|
-
|
|
273
|
+
await store.transaction(async (tx) => {
|
|
274
|
+
if (await tx.get(NOTES, notification.id)) throw new Error("notification id collision");
|
|
275
|
+
await tx.put(NOTES, notification.id, toDoc(notification));
|
|
276
|
+
});
|
|
259
277
|
return { notification, stored: true, channels, muted: false };
|
|
260
278
|
}
|
|
261
279
|
async function list(recipient, opts = {}) {
|
|
262
280
|
const r = recipientSchema.parse(recipient);
|
|
263
281
|
const where = { recipientKey: recipientKey(r) };
|
|
264
282
|
if (opts.unreadOnly) where.readAt = null;
|
|
283
|
+
if (opts.cursor !== void 0) checkCursor(opts.cursor);
|
|
284
|
+
const limit = clampLimit(opts.limit);
|
|
265
285
|
const page = await store.query(NOTES, {
|
|
266
286
|
where,
|
|
267
287
|
orderBy: { field: "createdAt", direction: "desc" },
|
|
268
|
-
limit
|
|
288
|
+
limit,
|
|
269
289
|
cursor: opts.cursor
|
|
270
290
|
});
|
|
271
|
-
return { items: page.items.map((i) => fromDoc(i.
|
|
291
|
+
return { items: page.items.map((i) => fromDoc(i.data)), nextCursor: page.nextCursor ?? null };
|
|
272
292
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const doc = await store.get(NOTES, id2);
|
|
276
|
-
if (!doc || doc.recipientKey !== recipientKey(r)) throw new NotFoundError();
|
|
293
|
+
function check(doc, recipient) {
|
|
294
|
+
if (!doc || doc.recipientKey !== recipientKey(recipient)) throw new NotFoundError();
|
|
277
295
|
return doc;
|
|
278
296
|
}
|
|
279
297
|
async function get(recipient, id2) {
|
|
280
|
-
|
|
298
|
+
const r = recipientSchema.parse(recipient);
|
|
299
|
+
return fromDoc(check(await store.get(NOTES, id2), r));
|
|
281
300
|
}
|
|
282
301
|
async function markReadOnce(recipient, id2) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const n = fromDoc(
|
|
302
|
+
const r = recipientSchema.parse(recipient);
|
|
303
|
+
return store.transaction(async (tx) => {
|
|
304
|
+
const n = fromDoc(check(await tx.get(NOTES, id2), r));
|
|
286
305
|
if (n.readAt !== null) return { notification: n, won: false };
|
|
287
306
|
const next = notificationSchema.parse({ ...n, readAt: now() });
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
307
|
+
await tx.put(NOTES, id2, toDoc(next));
|
|
308
|
+
return { notification: next, won: true };
|
|
309
|
+
});
|
|
291
310
|
}
|
|
292
311
|
async function markRead(recipient, id2) {
|
|
293
312
|
return (await markReadOnce(recipient, id2)).notification;
|
|
@@ -366,108 +385,6 @@ function createNotifier(options) {
|
|
|
366
385
|
};
|
|
367
386
|
}
|
|
368
387
|
|
|
369
|
-
// src/adapters/d1.ts
|
|
370
|
-
var FIELD = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
371
|
-
var SCHEMA = `CREATE TABLE IF NOT EXISTS notify_docs (
|
|
372
|
-
collection TEXT NOT NULL,
|
|
373
|
-
id TEXT NOT NULL,
|
|
374
|
-
doc TEXT NOT NULL,
|
|
375
|
-
PRIMARY KEY (collection, id)
|
|
376
|
-
)`;
|
|
377
|
-
function field(name) {
|
|
378
|
-
if (!FIELD.test(name)) throw new RangeError("bad field name");
|
|
379
|
-
return `json_extract(doc, '$.${name}')`;
|
|
380
|
-
}
|
|
381
|
-
function bindable(v) {
|
|
382
|
-
return typeof v === "boolean" ? v ? 1 : 0 : v;
|
|
383
|
-
}
|
|
384
|
-
var TYPES = {
|
|
385
|
-
number: ["integer", "real"],
|
|
386
|
-
string: ["text"],
|
|
387
|
-
boolean: ["true", "false"]
|
|
388
|
-
};
|
|
389
|
-
var INDEXES = [
|
|
390
|
-
`CREATE INDEX IF NOT EXISTS notify_by_recipient ON notify_docs (collection, json_extract(doc, '$.recipientKey'), json_extract(doc, '$.createdAt'), id)`,
|
|
391
|
-
`CREATE INDEX IF NOT EXISTS notify_by_created ON notify_docs (collection, json_extract(doc, '$.createdAt'), id)`
|
|
392
|
-
];
|
|
393
|
-
async function migrateD1(db) {
|
|
394
|
-
for (const sql of [SCHEMA, ...INDEXES]) await db.prepare(sql).bind().run();
|
|
395
|
-
}
|
|
396
|
-
var D1Store = class {
|
|
397
|
-
constructor(db) {
|
|
398
|
-
this.db = db;
|
|
399
|
-
}
|
|
400
|
-
db;
|
|
401
|
-
async get(collection, id2) {
|
|
402
|
-
const row = await this.db.prepare("SELECT doc FROM notify_docs WHERE collection = ? AND id = ?").bind(collection, id2).first();
|
|
403
|
-
return row ? JSON.parse(row.doc) : null;
|
|
404
|
-
}
|
|
405
|
-
async put(collection, id2, doc) {
|
|
406
|
-
await this.db.prepare("INSERT OR REPLACE INTO notify_docs (collection, id, doc) VALUES (?, ?, ?)").bind(collection, id2, canonicalJson(doc)).run();
|
|
407
|
-
}
|
|
408
|
-
async create(collection, id2, doc) {
|
|
409
|
-
const res = await this.db.prepare("INSERT OR IGNORE INTO notify_docs (collection, id, doc) VALUES (?, ?, ?)").bind(collection, id2, canonicalJson(doc)).run();
|
|
410
|
-
return (res.meta.changes ?? 0) > 0;
|
|
411
|
-
}
|
|
412
|
-
async replaceIf(collection, id2, expected, next) {
|
|
413
|
-
const res = await this.db.prepare("UPDATE notify_docs SET doc = ? WHERE collection = ? AND id = ? AND doc = ?").bind(canonicalJson(next), collection, id2, canonicalJson(expected)).run();
|
|
414
|
-
return (res.meta.changes ?? 0) > 0;
|
|
415
|
-
}
|
|
416
|
-
async delete(collection, id2) {
|
|
417
|
-
await this.db.prepare("DELETE FROM notify_docs WHERE collection = ? AND id = ?").bind(collection, id2).run();
|
|
418
|
-
}
|
|
419
|
-
async query(collection, query = {}) {
|
|
420
|
-
const limit = clampLimit(query.limit);
|
|
421
|
-
const desc = query.orderBy?.direction === "desc";
|
|
422
|
-
const key = query.orderBy ? field(query.orderBy.field) : "id";
|
|
423
|
-
const conds = ["collection = ?"];
|
|
424
|
-
const args = [collection];
|
|
425
|
-
for (const [name, value] of Object.entries(query.where ?? {})) {
|
|
426
|
-
if (value === null) conds.push(`${field(name)} IS NULL`);
|
|
427
|
-
else {
|
|
428
|
-
const types = TYPES[typeof value] ?? [];
|
|
429
|
-
conds.push(`${field(name)} = ? AND json_type(doc, '$.${name}') IN (${types.map(() => "?").join(",")})`);
|
|
430
|
-
args.push(bindable(value), ...types);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (query.cursor !== void 0) {
|
|
434
|
-
const [cv, cid] = decodeCursor(query.cursor);
|
|
435
|
-
if (query.orderBy && cv !== null) {
|
|
436
|
-
const allowed = TYPES[typeof cv] ?? [];
|
|
437
|
-
const marks = allowed.map(() => "?").join(",");
|
|
438
|
-
const bad = await this.db.prepare(
|
|
439
|
-
`SELECT 1 AS x FROM notify_docs WHERE ${conds.join(" AND ")} AND ${key} IS NOT NULL AND json_type(doc, '$.${query.orderBy.field}') NOT IN (${marks}) LIMIT 1`
|
|
440
|
-
).bind(...args, ...allowed).first();
|
|
441
|
-
if (bad) throw new RangeError("bad cursor");
|
|
442
|
-
}
|
|
443
|
-
const idCmp = desc ? "<" : ">";
|
|
444
|
-
if (!query.orderBy) {
|
|
445
|
-
conds.push(`id ${idCmp} ?`);
|
|
446
|
-
args.push(cid);
|
|
447
|
-
} else if (cv === null) {
|
|
448
|
-
conds.push(desc ? `(${key} IS NULL AND id < ?)` : `(${key} IS NOT NULL OR id > ?)`);
|
|
449
|
-
args.push(cid);
|
|
450
|
-
} else if (desc) {
|
|
451
|
-
conds.push(`(${key} < ? OR ${key} IS NULL OR (${key} = ? AND id < ?))`);
|
|
452
|
-
args.push(bindable(cv), bindable(cv), cid);
|
|
453
|
-
} else {
|
|
454
|
-
conds.push(`(${key} > ? OR (${key} = ? AND id > ?))`);
|
|
455
|
-
args.push(bindable(cv), bindable(cv), cid);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
const dir = desc ? "DESC" : "ASC";
|
|
459
|
-
const sql = `SELECT id, doc FROM notify_docs WHERE ${conds.join(" AND ")} ORDER BY ${key} ${dir}, id ${dir} LIMIT ?`;
|
|
460
|
-
const { results } = await this.db.prepare(sql).bind(...args, limit + 1).all();
|
|
461
|
-
const rows = results.slice(0, limit).map((r) => ({ id: r.id, doc: JSON.parse(r.doc) }));
|
|
462
|
-
const last = rows[rows.length - 1];
|
|
463
|
-
const lastKey = last && query.orderBy ? last.doc[query.orderBy.field] ?? null : last?.id ?? null;
|
|
464
|
-
return {
|
|
465
|
-
items: rows,
|
|
466
|
-
nextCursor: results.length > limit && last ? encodeCursor(lastKey, last.id) : null
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
|
|
471
388
|
export {
|
|
472
389
|
VERSION,
|
|
473
390
|
CHANNELS,
|
|
@@ -478,8 +395,7 @@ export {
|
|
|
478
395
|
DEFAULT_CHANNELS,
|
|
479
396
|
defaultPreferences,
|
|
480
397
|
recipientKey,
|
|
398
|
+
NOTES,
|
|
481
399
|
NotFoundError,
|
|
482
|
-
createNotifier
|
|
483
|
-
migrateD1,
|
|
484
|
-
D1Store
|
|
400
|
+
createNotifier
|
|
485
401
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PACKAGE_NAME
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3N77SYRA.js";
|
|
4
4
|
import {
|
|
5
5
|
ApiError
|
|
6
6
|
} from "./chunk-FJPEUDTG.js";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
recipientSchema,
|
|
11
11
|
resourceRefSchema,
|
|
12
12
|
typeNameSchema
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-6QCYA3X7.js";
|
|
14
14
|
|
|
15
15
|
// src/mcp.ts
|
|
16
16
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
PACKAGE_NAME
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3N77SYRA.js";
|
|
5
5
|
import {
|
|
6
6
|
ApiError,
|
|
7
7
|
createClient
|
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
import "./chunk-KJKM56FT.js";
|
|
10
10
|
import {
|
|
11
11
|
VERSION
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-GJFAXI2K.js";
|
|
12
|
+
} from "./chunk-6QCYA3X7.js";
|
|
14
13
|
|
|
15
14
|
// src/cli.ts
|
|
16
15
|
import { realpathSync } from "fs";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { R as RecipientExport } from './gdpr-
|
|
2
|
-
import {
|
|
3
|
-
import { S as SendInput, N as Notification,
|
|
1
|
+
import { R as RecipientExport } from './gdpr-CUq5MUwo.js';
|
|
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-C842YAMe.js';
|
|
4
|
+
import { D as DeliveryChannel, L as Logger } from './types-DONKNIOo.js';
|
|
4
5
|
|
|
5
6
|
interface NotifierOptions {
|
|
6
7
|
store: Store;
|
|
@@ -73,34 +74,4 @@ declare function createDispatcher(options: DispatcherOptions): {
|
|
|
73
74
|
notify(input: SendInput): Promise<DispatchResult>;
|
|
74
75
|
};
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
interface D1Like {
|
|
78
|
-
prepare(sql: string): {
|
|
79
|
-
bind(...values: unknown[]): {
|
|
80
|
-
first<T = unknown>(): Promise<T | null>;
|
|
81
|
-
all<T = unknown>(): Promise<{
|
|
82
|
-
results: T[];
|
|
83
|
-
}>;
|
|
84
|
-
run(): Promise<{
|
|
85
|
-
meta: {
|
|
86
|
-
changes?: number;
|
|
87
|
-
};
|
|
88
|
-
}>;
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/** Create the table and indexes. Safe to run on every start. */
|
|
93
|
-
declare function migrateD1(db: D1Like): Promise<void>;
|
|
94
|
-
/** Store adapter for Cloudflare D1. Run `migrateD1` once first. */
|
|
95
|
-
declare class D1Store implements Store {
|
|
96
|
-
private readonly db;
|
|
97
|
-
constructor(db: D1Like);
|
|
98
|
-
get(collection: string, id: string): Promise<Doc | null>;
|
|
99
|
-
put(collection: string, id: string, doc: Doc): Promise<void>;
|
|
100
|
-
create(collection: string, id: string, doc: Doc): Promise<boolean>;
|
|
101
|
-
replaceIf(collection: string, id: string, expected: Doc, next: Doc): Promise<boolean>;
|
|
102
|
-
delete(collection: string, id: string): Promise<void>;
|
|
103
|
-
query(collection: string, query?: Query): Promise<Page>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export { type D1Like as D, type ListOptions as L, NotFoundError as N, type SendResult as S, D1Store as a, type DispatchResult as b, type DispatcherOptions as c, type ListResult as d, type Notifier as e, type NotifierOptions as f, createDispatcher as g, createNotifier as h, migrateD1 as m };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
export { D as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
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';
|
|
2
|
+
export { Doc, Page, Query, Scalar, Store, StoreOps, createMemoryStore } from 'agentic-identity';
|
|
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
|
+
export { D1Like, D1_SCHEMA, createD1Store } from 'agentic-identity/cloudflare';
|
|
9
|
+
export { R as RecipientExport } from './gdpr-CUq5MUwo.js';
|
|
8
10
|
import 'zod';
|
|
11
|
+
import 'agentic-identity/testing';
|
|
9
12
|
|
|
10
13
|
declare const VERSION: string;
|
|
11
14
|
|
package/dist/index.js
CHANGED
|
@@ -1,45 +1,43 @@
|
|
|
1
1
|
import {
|
|
2
|
+
D1_SCHEMA,
|
|
2
3
|
DeliveryError,
|
|
3
4
|
PACKAGE_NAME,
|
|
4
5
|
SIGNATURE_HEADER,
|
|
6
|
+
createD1Store,
|
|
5
7
|
createDispatcher,
|
|
8
|
+
createMemoryStore,
|
|
6
9
|
createWebhookChannel,
|
|
7
10
|
signPayload,
|
|
8
11
|
verifySignature
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-3N77SYRA.js";
|
|
10
13
|
import {
|
|
11
14
|
createEmailChannel
|
|
12
15
|
} from "./chunk-KJKM56FT.js";
|
|
13
16
|
import {
|
|
14
17
|
CHANNELS,
|
|
15
|
-
D1Store,
|
|
16
18
|
DEFAULT_CHANNELS,
|
|
17
19
|
NotFoundError,
|
|
18
20
|
VERSION,
|
|
19
21
|
createNotifier,
|
|
20
22
|
defaultPreferences,
|
|
21
|
-
migrateD1,
|
|
22
23
|
recipientKey
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import {
|
|
25
|
-
MemoryStore
|
|
26
|
-
} from "./chunk-GJFAXI2K.js";
|
|
24
|
+
} from "./chunk-6QCYA3X7.js";
|
|
27
25
|
export {
|
|
28
26
|
CHANNELS,
|
|
29
|
-
|
|
27
|
+
D1_SCHEMA,
|
|
30
28
|
DEFAULT_CHANNELS,
|
|
31
29
|
DeliveryError,
|
|
32
|
-
MemoryStore,
|
|
33
30
|
NotFoundError,
|
|
34
31
|
PACKAGE_NAME,
|
|
35
32
|
SIGNATURE_HEADER,
|
|
36
33
|
VERSION,
|
|
34
|
+
createD1Store,
|
|
37
35
|
createDispatcher,
|
|
38
36
|
createEmailChannel,
|
|
37
|
+
createMemoryStore,
|
|
39
38
|
createNotifier,
|
|
40
39
|
createWebhookChannel,
|
|
41
40
|
defaultPreferences,
|
|
42
|
-
migrateD1,
|
|
43
41
|
recipientKey,
|
|
44
42
|
signPayload,
|
|
45
43
|
verifySignature
|
package/dist/mcp-bin.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startMcp
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-POSHKDNY.js";
|
|
5
|
+
import "./chunk-3N77SYRA.js";
|
|
6
6
|
import {
|
|
7
7
|
createClient
|
|
8
8
|
} from "./chunk-FJPEUDTG.js";
|
|
9
9
|
import "./chunk-KJKM56FT.js";
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-GJFAXI2K.js";
|
|
10
|
+
import "./chunk-6QCYA3X7.js";
|
|
12
11
|
|
|
13
12
|
// src/mcp-bin.ts
|
|
14
13
|
var { NOTIFY_URL, NOTIFY_TOKEN } = process.env;
|
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-
|
|
3
|
+
import './types-C842YAMe.js';
|
|
4
4
|
import 'zod';
|
|
5
|
-
import './gdpr-
|
|
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
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcp
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-POSHKDNY.js";
|
|
5
|
+
import "./chunk-3N77SYRA.js";
|
|
6
6
|
import "./chunk-FJPEUDTG.js";
|
|
7
7
|
import "./chunk-KJKM56FT.js";
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-GJFAXI2K.js";
|
|
8
|
+
import "./chunk-6QCYA3X7.js";
|
|
10
9
|
export {
|
|
11
10
|
createMcpServer,
|
|
12
11
|
startMcp
|
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-
|
|
2
|
-
import { R as RecipientExport } from './gdpr-
|
|
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 {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import 'agentic-identity/testing';
|
|
2
|
+
import { R as Recipient } from './types-C842YAMe.js';
|
|
3
|
+
import { D as DeliveryChannel } from './types-DONKNIOo.js';
|
|
3
4
|
|
|
4
5
|
interface EmailMessage {
|
|
5
6
|
to: string;
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export { T as TestEmailAdapter } from './
|
|
3
|
-
import './types-
|
|
1
|
+
export { runStoreContract } from 'agentic-identity/testing';
|
|
2
|
+
export { T as TestEmailAdapter } from './testing-B5slvYDV.js';
|
|
3
|
+
import './types-C842YAMe.js';
|
|
4
4
|
import 'zod';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Contract suite for the Store port. Run it on every adapter.
|
|
8
|
-
* `make` must return a fresh store. When two calls share one backing
|
|
9
|
-
* database, the concurrency tests use both handles.
|
|
10
|
-
*/
|
|
11
|
-
declare function storeContract(name: string, make: () => Promise<Store>, makePeer?: (s: Store) => Promise<Store>): void;
|
|
12
|
-
|
|
13
|
-
export { storeContract };
|
|
5
|
+
import './types-DONKNIOo.js';
|
package/dist/testing.js
CHANGED
|
@@ -1,155 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TestEmailAdapter
|
|
3
3
|
} from "./chunk-KJKM56FT.js";
|
|
4
|
-
import {
|
|
5
|
-
encodeCursor
|
|
6
|
-
} from "./chunk-GJFAXI2K.js";
|
|
7
4
|
|
|
8
5
|
// src/testing.ts
|
|
9
|
-
import {
|
|
10
|
-
function storeContract(name, make, makePeer) {
|
|
11
|
-
describe(`Store contract: ${name}`, () => {
|
|
12
|
-
test("get returns null for a missing doc, put then get round-trips", async () => {
|
|
13
|
-
const s = await make();
|
|
14
|
-
expect(await s.get("c", "a")).toBeNull();
|
|
15
|
-
await s.put("c", "a", { n: 1, s: "x", t: true, z: null });
|
|
16
|
-
expect(await s.get("c", "a")).toEqual({ n: 1, s: "x", t: true, z: null });
|
|
17
|
-
});
|
|
18
|
-
test("collections are separate", async () => {
|
|
19
|
-
const s = await make();
|
|
20
|
-
await s.put("c1", "a", { v: 1 });
|
|
21
|
-
expect(await s.get("c2", "a")).toBeNull();
|
|
22
|
-
});
|
|
23
|
-
test("create refuses an existing id and keeps the old doc", async () => {
|
|
24
|
-
const s = await make();
|
|
25
|
-
expect(await s.create("c", "a", { v: 1 })).toBe(true);
|
|
26
|
-
expect(await s.create("c", "a", { v: 2 })).toBe(false);
|
|
27
|
-
expect(await s.get("c", "a")).toEqual({ v: 1 });
|
|
28
|
-
});
|
|
29
|
-
test("only one of two racing creates wins", async () => {
|
|
30
|
-
const s = await make();
|
|
31
|
-
const peer = makePeer ? await makePeer(s) : s;
|
|
32
|
-
const results = await Promise.all([s.create("c", "a", { v: 1 }), peer.create("c", "a", { v: 2 })]);
|
|
33
|
-
expect(results.filter(Boolean)).toHaveLength(1);
|
|
34
|
-
});
|
|
35
|
-
test("replaceIf swaps only when the doc is unchanged", async () => {
|
|
36
|
-
const s = await make();
|
|
37
|
-
await s.put("c", "a", { v: 1 });
|
|
38
|
-
expect(await s.replaceIf("c", "a", { v: 9 }, { v: 2 })).toBe(false);
|
|
39
|
-
expect(await s.replaceIf("c", "a", { v: 1 }, { v: 2 })).toBe(true);
|
|
40
|
-
expect(await s.get("c", "a")).toEqual({ v: 2 });
|
|
41
|
-
expect(await s.replaceIf("c", "missing", { v: 1 }, { v: 2 })).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
test("replaceIf equality ignores key order", async () => {
|
|
44
|
-
const s = await make();
|
|
45
|
-
await s.put("c", "a", { a: 1, b: { x: 1, y: 2 } });
|
|
46
|
-
expect(await s.replaceIf("c", "a", { b: { y: 2, x: 1 }, a: 1 }, { a: 2 })).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
test("only one of two racing replaceIf calls wins", async () => {
|
|
49
|
-
const s = await make();
|
|
50
|
-
const peer = makePeer ? await makePeer(s) : s;
|
|
51
|
-
await s.put("c", "a", { v: 0 });
|
|
52
|
-
const results = await Promise.all([
|
|
53
|
-
s.replaceIf("c", "a", { v: 0 }, { v: 1 }),
|
|
54
|
-
peer.replaceIf("c", "a", { v: 0 }, { v: 2 })
|
|
55
|
-
]);
|
|
56
|
-
expect(results.filter(Boolean)).toHaveLength(1);
|
|
57
|
-
});
|
|
58
|
-
test("delete removes a doc and ignores a missing one", async () => {
|
|
59
|
-
const s = await make();
|
|
60
|
-
await s.put("c", "a", { v: 1 });
|
|
61
|
-
await s.delete("c", "a");
|
|
62
|
-
await s.delete("c", "a");
|
|
63
|
-
expect(await s.get("c", "a")).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
test("query filters on equality, including null", async () => {
|
|
66
|
-
const s = await make();
|
|
67
|
-
await s.put("c", "a", { k: "x", r: null });
|
|
68
|
-
await s.put("c", "b", { k: "x", r: 5 });
|
|
69
|
-
await s.put("c", "c", { k: "y", r: null });
|
|
70
|
-
const ids = async (where) => (await s.query("c", { where, orderBy: { field: "k", direction: "asc" } })).items.map((i) => i.id).sort();
|
|
71
|
-
expect(await ids({ k: "x" })).toEqual(["a", "b"]);
|
|
72
|
-
expect(await ids({ k: "x", r: null })).toEqual(["a"]);
|
|
73
|
-
expect(await ids({ r: 5 })).toEqual(["b"]);
|
|
74
|
-
expect(await ids({ k: "none" })).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
test("equality filters respect the value type", async () => {
|
|
77
|
-
const s = await make();
|
|
78
|
-
await s.put("c", "t", { f: true, o: { a: 1 } });
|
|
79
|
-
await s.put("c", "u", { f: 1, o: '{"a":1}' });
|
|
80
|
-
await s.put("c", "v", { f: "1", o: [1] });
|
|
81
|
-
const ids = async (where) => (await s.query("c", { where })).items.map((i) => i.id).sort();
|
|
82
|
-
expect(await ids({ f: true })).toEqual(["t"]);
|
|
83
|
-
expect(await ids({ f: 1 })).toEqual(["u"]);
|
|
84
|
-
expect(await ids({ f: "1" })).toEqual(["v"]);
|
|
85
|
-
expect(await ids({ o: '{"a":1}' })).toEqual(["u"]);
|
|
86
|
-
});
|
|
87
|
-
test("query orders by one field and breaks ties by id", async () => {
|
|
88
|
-
const s = await make();
|
|
89
|
-
for (const [id, n] of [["b", 2], ["a", 2], ["c", 1], ["d", 3]]) await s.put("c", id, { n });
|
|
90
|
-
const order = async (direction) => (await s.query("c", { orderBy: { field: "n", direction } })).items.map((i) => i.id);
|
|
91
|
-
expect(await order("asc")).toEqual(["c", "a", "b", "d"]);
|
|
92
|
-
expect(await order("desc")).toEqual(["d", "b", "a", "c"]);
|
|
93
|
-
});
|
|
94
|
-
test("cursor paging walks every doc once, in both directions", async () => {
|
|
95
|
-
const s = await make();
|
|
96
|
-
for (let i = 0; i < 7; i++) await s.put("c", `id${i}`, { n: i % 3 });
|
|
97
|
-
for (const direction of ["asc", "desc"]) {
|
|
98
|
-
const seen = [];
|
|
99
|
-
let cursor;
|
|
100
|
-
let pages = 0;
|
|
101
|
-
do {
|
|
102
|
-
const page = await s.query("c", { orderBy: { field: "n", direction }, limit: 3, cursor });
|
|
103
|
-
seen.push(...page.items.map((i) => i.id));
|
|
104
|
-
cursor = page.nextCursor ?? void 0;
|
|
105
|
-
pages++;
|
|
106
|
-
} while (cursor);
|
|
107
|
-
const whole = (await s.query("c", { orderBy: { field: "n", direction }, limit: 50 })).items.map((i) => i.id);
|
|
108
|
-
expect(pages).toBe(3);
|
|
109
|
-
expect(seen).toEqual(whole);
|
|
110
|
-
expect(new Set(seen).size).toBe(7);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
test("cursor paging works with a filter and with no orderBy", async () => {
|
|
114
|
-
const s = await make();
|
|
115
|
-
for (let i = 0; i < 6; i++) await s.put("c", `id${i}`, { g: i % 2 === 0 ? "even" : "odd", n: i });
|
|
116
|
-
const walk = async (query) => {
|
|
117
|
-
const seen = [];
|
|
118
|
-
let cursor;
|
|
119
|
-
do {
|
|
120
|
-
const page = await s.query("c", { ...query, limit: 2, cursor });
|
|
121
|
-
seen.push(...page.items.map((i) => i.id));
|
|
122
|
-
cursor = page.nextCursor ?? void 0;
|
|
123
|
-
} while (cursor);
|
|
124
|
-
return seen;
|
|
125
|
-
};
|
|
126
|
-
expect(await walk({ where: { g: "even" }, orderBy: { field: "n", direction: "desc" } })).toEqual(["id4", "id2", "id0"]);
|
|
127
|
-
expect(await walk({})).toEqual(["id0", "id1", "id2", "id3", "id4", "id5"]);
|
|
128
|
-
});
|
|
129
|
-
test("a cursor of the wrong type for the sort field is rejected", async () => {
|
|
130
|
-
const s = await make();
|
|
131
|
-
await s.put("c", "a", { n: 1 });
|
|
132
|
-
await s.put("c", "b", { n: 2 });
|
|
133
|
-
await expect(s.query("c", { orderBy: { field: "n", direction: "asc" }, cursor: encodeCursor("abc", "a") })).rejects.toThrow();
|
|
134
|
-
});
|
|
135
|
-
test("bad limit and bad cursor are rejected", async () => {
|
|
136
|
-
const s = await make();
|
|
137
|
-
await expect(s.query("c", { limit: 0 })).rejects.toThrow();
|
|
138
|
-
await expect(s.query("c", { cursor: "%%%" })).rejects.toThrow();
|
|
139
|
-
});
|
|
140
|
-
test("stored docs are copies", async () => {
|
|
141
|
-
const s = await make();
|
|
142
|
-
const doc = { nested: { v: 1 } };
|
|
143
|
-
await s.put("c", "a", doc);
|
|
144
|
-
doc.nested.v = 2;
|
|
145
|
-
const got = await s.get("c", "a");
|
|
146
|
-
expect(got.nested.v).toBe(1);
|
|
147
|
-
got.nested.v = 3;
|
|
148
|
-
expect((await s.get("c", "a")).nested.v).toBe(1);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
}
|
|
6
|
+
import { runStoreContract } from "agentic-identity/testing";
|
|
152
7
|
export {
|
|
153
8
|
TestEmailAdapter,
|
|
154
|
-
|
|
9
|
+
runStoreContract
|
|
155
10
|
};
|
|
@@ -214,4 +214,4 @@ interface Digest {
|
|
|
214
214
|
groups: DigestGroup[];
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
export { type Actor as A,
|
|
217
|
+
export { type Actor as A, type Channel as C, type Digest as D, type Notification as N, type Preferences as P, type Recipient as R, type SendInput as S, type ResourceRef as a, CHANNELS as b, DEFAULT_CHANNELS as c, type DigestGroup as d, defaultPreferences as e, recipientKey as r };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { N as Notification } from './types-C842YAMe.js';
|
|
2
|
+
|
|
3
|
+
/** A delivery channel other than the inbox. */
|
|
4
|
+
interface DeliveryChannel {
|
|
5
|
+
/** Resolves "skipped" when the recipient has no target (no endpoint, no address). Throws on failure. */
|
|
6
|
+
deliver(notification: Notification): Promise<"sent" | "skipped">;
|
|
7
|
+
}
|
|
8
|
+
declare class DeliveryError extends Error {
|
|
9
|
+
readonly attempts: number;
|
|
10
|
+
readonly code = "DELIVERY_FAILED";
|
|
11
|
+
constructor(message: string, attempts: number, options?: {
|
|
12
|
+
cause?: unknown;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Logger that takes ids and counts only. Never pass content or addresses. */
|
|
16
|
+
interface Logger {
|
|
17
|
+
warn(message: string, fields?: Record<string, string | number>): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { type DeliveryChannel as D, type Logger as L, DeliveryError as a };
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import { R as Recipient } from './types-
|
|
4
|
-
import '
|
|
5
|
-
import './
|
|
2
|
+
import { c as Notifier, e as createDispatcher } from './dispatch-LBnkLOQg.js';
|
|
3
|
+
import { R as Recipient } from './types-C842YAMe.js';
|
|
4
|
+
import { D1Like } from 'agentic-identity/cloudflare';
|
|
5
|
+
import './gdpr-CUq5MUwo.js';
|
|
6
|
+
import 'agentic-identity';
|
|
7
|
+
import './types-DONKNIOo.js';
|
|
6
8
|
|
|
7
9
|
/** Who is calling. `kind` has no default: a missing kind is an error. */
|
|
8
10
|
declare const principalSchema: z.ZodObject<{
|
|
@@ -11,12 +13,12 @@ declare const principalSchema: z.ZodObject<{
|
|
|
11
13
|
/** Allowed to send notifications to others. */
|
|
12
14
|
canSend: z.ZodOptional<z.ZodBoolean>;
|
|
13
15
|
}, "strict", z.ZodTypeAny, {
|
|
14
|
-
id: string;
|
|
15
16
|
kind: "human" | "agent";
|
|
17
|
+
id: string;
|
|
16
18
|
canSend?: boolean | undefined;
|
|
17
19
|
}, {
|
|
18
|
-
id: string;
|
|
19
20
|
kind: "human" | "agent";
|
|
21
|
+
id: string;
|
|
20
22
|
canSend?: boolean | undefined;
|
|
21
23
|
}>;
|
|
22
24
|
type Principal = z.infer<typeof principalSchema>;
|
|
@@ -38,6 +40,8 @@ interface WorkerEnv {
|
|
|
38
40
|
DB?: D1Like;
|
|
39
41
|
/** Secret. JSON map of bearer token (32 bytes or more) to principal. */
|
|
40
42
|
NOTIFY_TOKENS?: string;
|
|
43
|
+
/** Data region of the store: "eu", "us" or "unknown". Default "eu". */
|
|
44
|
+
NOTIFY_REGION?: string;
|
|
41
45
|
/** Retention window in days. Unset keeps notifications. */
|
|
42
46
|
NOTIFY_RETENTION_DAYS?: string;
|
|
43
47
|
}
|
package/dist/worker.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
NOTES,
|
|
3
3
|
NotFoundError,
|
|
4
4
|
VERSION,
|
|
5
5
|
createNotifier,
|
|
6
|
-
migrateD1,
|
|
7
6
|
preferencesSchema,
|
|
8
7
|
recipientSchema,
|
|
9
8
|
resourceRefSchema,
|
|
10
9
|
typeNameSchema
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-GJFAXI2K.js";
|
|
10
|
+
} from "./chunk-6QCYA3X7.js";
|
|
13
11
|
|
|
14
12
|
// src/api.ts
|
|
15
13
|
import { Hono } from "hono";
|
|
@@ -113,17 +111,27 @@ function createApp(options) {
|
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
// src/worker.ts
|
|
114
|
+
import { parseRegion } from "agentic-identity";
|
|
115
|
+
import { createD1Store, D1_SCHEMA } from "agentic-identity/cloudflare";
|
|
116
116
|
function json(value, status = 200) {
|
|
117
117
|
return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
|
|
118
118
|
}
|
|
119
|
+
async function hasLegacyRows(db) {
|
|
120
|
+
const table = await db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'notify_docs'").bind().first();
|
|
121
|
+
if (!table) return false;
|
|
122
|
+
return await db.prepare("SELECT 1 AS found FROM notify_docs LIMIT 1").bind().first() !== null;
|
|
123
|
+
}
|
|
119
124
|
function createWorker(config) {
|
|
120
125
|
const apps = /* @__PURE__ */ new WeakMap();
|
|
121
126
|
async function buildNotifier(env) {
|
|
122
127
|
if (!env.DB) return null;
|
|
123
|
-
await
|
|
128
|
+
if (await hasLegacyRows(env.DB)) throw new Error("notify_docs holds rows that were not copied to store_docs");
|
|
129
|
+
await env.DB.prepare(D1_SCHEMA).bind().run();
|
|
124
130
|
const days = env.NOTIFY_RETENTION_DAYS === void 0 ? void 0 : Number(env.NOTIFY_RETENTION_DAYS);
|
|
131
|
+
const store = createD1Store(env.DB, { region: parseRegion(env.NOTIFY_REGION ?? "eu") });
|
|
132
|
+
await store.query(NOTES, { limit: 1 });
|
|
125
133
|
return createNotifier({
|
|
126
|
-
store
|
|
134
|
+
store,
|
|
127
135
|
retentionMs: days === void 0 ? void 0 : days * 864e5
|
|
128
136
|
});
|
|
129
137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-notify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -61,10 +61,12 @@
|
|
|
61
61
|
"clean": "rm -rf dist",
|
|
62
62
|
"prepack": "npm run build",
|
|
63
63
|
"ci:local": "npm run build && npm run typecheck && npm run check:worker && npm test",
|
|
64
|
+
"deploy:demo": "wrangler deploy --config wrangler.demo.jsonc",
|
|
64
65
|
"check:worker": "esbuild src/worker.ts --bundle --platform=neutral --format=esm --log-level=warning --outfile=node_modules/.cache/worker-check.js"
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
67
68
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
69
|
+
"agentic-identity": "^0.4.0",
|
|
68
70
|
"hono": "^4.13.14",
|
|
69
71
|
"zod": "^3.25.0"
|
|
70
72
|
},
|
|
@@ -74,7 +76,8 @@
|
|
|
74
76
|
"miniflare": "^4.20260730.0",
|
|
75
77
|
"tsup": "^8.0.0",
|
|
76
78
|
"typescript": "^5.6.0",
|
|
77
|
-
"vitest": "^3.0.0"
|
|
79
|
+
"vitest": "^3.0.0",
|
|
80
|
+
"wrangler": "^4.155.0"
|
|
78
81
|
},
|
|
79
82
|
"peerDependencies": {
|
|
80
83
|
"vitest": ">=3"
|
package/dist/chunk-GJFAXI2K.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// src/core/store.ts
|
|
2
|
-
var MAX_LIMIT = 200;
|
|
3
|
-
var DEFAULT_LIMIT = 50;
|
|
4
|
-
function clampLimit(limit) {
|
|
5
|
-
if (limit === void 0) return DEFAULT_LIMIT;
|
|
6
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
7
|
-
throw new RangeError("limit must be an integer of 1 or more");
|
|
8
|
-
}
|
|
9
|
-
return Math.min(limit, MAX_LIMIT);
|
|
10
|
-
}
|
|
11
|
-
function encodeCursor(value, id) {
|
|
12
|
-
return btoa(unescape(encodeURIComponent(JSON.stringify([value, id]))));
|
|
13
|
-
}
|
|
14
|
-
function decodeCursor(cursor) {
|
|
15
|
-
try {
|
|
16
|
-
const parsed = JSON.parse(decodeURIComponent(escape(atob(cursor))));
|
|
17
|
-
if (Array.isArray(parsed) && parsed.length === 2 && typeof parsed[1] === "string" && (parsed[0] === null || ["string", "number", "boolean"].includes(typeof parsed[0]))) {
|
|
18
|
-
return [parsed[0], parsed[1]];
|
|
19
|
-
}
|
|
20
|
-
} catch {
|
|
21
|
-
}
|
|
22
|
-
throw new RangeError("bad cursor");
|
|
23
|
-
}
|
|
24
|
-
function compare(a, b) {
|
|
25
|
-
if (a === b) return 0;
|
|
26
|
-
if (a === null) return -1;
|
|
27
|
-
if (b === null) return 1;
|
|
28
|
-
return a < b ? -1 : 1;
|
|
29
|
-
}
|
|
30
|
-
function canonicalJson(value) {
|
|
31
|
-
return JSON.stringify(
|
|
32
|
-
value,
|
|
33
|
-
(_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : v
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
function sameDoc(a, b) {
|
|
37
|
-
return canonicalJson(a) === canonicalJson(b);
|
|
38
|
-
}
|
|
39
|
-
var MemoryStore = class {
|
|
40
|
-
data = /* @__PURE__ */ new Map();
|
|
41
|
-
coll(name) {
|
|
42
|
-
let c = this.data.get(name);
|
|
43
|
-
if (!c) {
|
|
44
|
-
c = /* @__PURE__ */ new Map();
|
|
45
|
-
this.data.set(name, c);
|
|
46
|
-
}
|
|
47
|
-
return c;
|
|
48
|
-
}
|
|
49
|
-
async get(collection, id) {
|
|
50
|
-
const doc = this.coll(collection).get(id);
|
|
51
|
-
return doc ? structuredClone(doc) : null;
|
|
52
|
-
}
|
|
53
|
-
async put(collection, id, doc) {
|
|
54
|
-
this.coll(collection).set(id, structuredClone(doc));
|
|
55
|
-
}
|
|
56
|
-
async create(collection, id, doc) {
|
|
57
|
-
const c = this.coll(collection);
|
|
58
|
-
if (c.has(id)) return false;
|
|
59
|
-
c.set(id, structuredClone(doc));
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
async replaceIf(collection, id, expected, next) {
|
|
63
|
-
const c = this.coll(collection);
|
|
64
|
-
const current = c.get(id);
|
|
65
|
-
if (!current || !sameDoc(current, expected)) return false;
|
|
66
|
-
c.set(id, structuredClone(next));
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
async delete(collection, id) {
|
|
70
|
-
this.coll(collection).delete(id);
|
|
71
|
-
}
|
|
72
|
-
async query(collection, query = {}) {
|
|
73
|
-
const limit = clampLimit(query.limit);
|
|
74
|
-
const field = query.orderBy?.field;
|
|
75
|
-
const dir = query.orderBy?.direction === "desc" ? -1 : 1;
|
|
76
|
-
const where = Object.entries(query.where ?? {});
|
|
77
|
-
const key = (id, doc) => field ? doc[field] ?? null : id;
|
|
78
|
-
let rows = [...this.coll(collection)].filter(([, doc]) => where.every(([k, v]) => (doc[k] ?? null) === v)).sort(([ia, a], [ib, b]) => dir * (compare(key(ia, a), key(ib, b)) || compare(ia, ib)));
|
|
79
|
-
if (query.cursor !== void 0) {
|
|
80
|
-
const [cv, cid] = decodeCursor(query.cursor);
|
|
81
|
-
const kind = (v) => v === null ? "null" : typeof v;
|
|
82
|
-
if (rows.some(([id, doc]) => key(id, doc) !== null && cv !== null && kind(key(id, doc)) !== kind(cv))) {
|
|
83
|
-
throw new RangeError("bad cursor");
|
|
84
|
-
}
|
|
85
|
-
rows = rows.filter(([id, doc]) => dir * (compare(key(id, doc), cv) || compare(id, cid)) > 0);
|
|
86
|
-
}
|
|
87
|
-
const slice = rows.slice(0, limit + 1);
|
|
88
|
-
const hasMore = slice.length > limit;
|
|
89
|
-
const items = slice.slice(0, limit).map(([id, doc]) => ({ id, doc: structuredClone(doc) }));
|
|
90
|
-
const last = items[items.length - 1];
|
|
91
|
-
return {
|
|
92
|
-
items,
|
|
93
|
-
nextCursor: hasMore && last ? encodeCursor(key(last.id, last.doc), last.id) : null
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export {
|
|
99
|
-
clampLimit,
|
|
100
|
-
encodeCursor,
|
|
101
|
-
decodeCursor,
|
|
102
|
-
canonicalJson,
|
|
103
|
-
MemoryStore
|
|
104
|
-
};
|
package/dist/types-Dkst26cb.d.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { N as Notification } from './types-GuAgyMOc.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Store port. Documents are flat JSON objects in named collections.
|
|
5
|
-
* The shape fits document stores and SQL alike: equality filters, one
|
|
6
|
-
* orderBy, a limit, cursor paging. No joins.
|
|
7
|
-
*/
|
|
8
|
-
type Scalar = string | number | boolean | null;
|
|
9
|
-
type Doc = Record<string, unknown>;
|
|
10
|
-
interface Query {
|
|
11
|
-
/** Equality filters on top-level scalar fields. */
|
|
12
|
-
where?: Record<string, Scalar>;
|
|
13
|
-
/** One sort field. Ties break by id, in the same direction. */
|
|
14
|
-
orderBy?: {
|
|
15
|
-
field: string;
|
|
16
|
-
direction: "asc" | "desc";
|
|
17
|
-
};
|
|
18
|
-
limit?: number;
|
|
19
|
-
/** Opaque value from a previous page. */
|
|
20
|
-
cursor?: string;
|
|
21
|
-
}
|
|
22
|
-
interface Page {
|
|
23
|
-
items: {
|
|
24
|
-
id: string;
|
|
25
|
-
doc: Doc;
|
|
26
|
-
}[];
|
|
27
|
-
nextCursor: string | null;
|
|
28
|
-
}
|
|
29
|
-
interface Store {
|
|
30
|
-
get(collection: string, id: string): Promise<Doc | null>;
|
|
31
|
-
/** Insert or replace. */
|
|
32
|
-
put(collection: string, id: string, doc: Doc): Promise<void>;
|
|
33
|
-
/** Insert only. Returns false when the id exists. Atomic. */
|
|
34
|
-
create(collection: string, id: string, doc: Doc): Promise<boolean>;
|
|
35
|
-
/** Replace only when the stored doc still equals `expected`. Equality is structural: key order does not matter. Atomic. */
|
|
36
|
-
replaceIf(collection: string, id: string, expected: Doc, next: Doc): Promise<boolean>;
|
|
37
|
-
delete(collection: string, id: string): Promise<void>;
|
|
38
|
-
query(collection: string, query?: Query): Promise<Page>;
|
|
39
|
-
}
|
|
40
|
-
/** In-memory adapter. Also the reference for the contract suite. */
|
|
41
|
-
declare class MemoryStore implements Store {
|
|
42
|
-
private readonly data;
|
|
43
|
-
private coll;
|
|
44
|
-
get(collection: string, id: string): Promise<Doc | null>;
|
|
45
|
-
put(collection: string, id: string, doc: Doc): Promise<void>;
|
|
46
|
-
create(collection: string, id: string, doc: Doc): Promise<boolean>;
|
|
47
|
-
replaceIf(collection: string, id: string, expected: Doc, next: Doc): Promise<boolean>;
|
|
48
|
-
delete(collection: string, id: string): Promise<void>;
|
|
49
|
-
query(collection: string, query?: Query): Promise<Page>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** A delivery channel other than the inbox. */
|
|
53
|
-
interface DeliveryChannel {
|
|
54
|
-
/** Resolves "skipped" when the recipient has no target (no endpoint, no address). Throws on failure. */
|
|
55
|
-
deliver(notification: Notification): Promise<"sent" | "skipped">;
|
|
56
|
-
}
|
|
57
|
-
declare class DeliveryError extends Error {
|
|
58
|
-
readonly attempts: number;
|
|
59
|
-
readonly code = "DELIVERY_FAILED";
|
|
60
|
-
constructor(message: string, attempts: number, options?: {
|
|
61
|
-
cause?: unknown;
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
/** Logger that takes ids and counts only. Never pass content or addresses. */
|
|
65
|
-
interface Logger {
|
|
66
|
-
warn(message: string, fields?: Record<string, string | number>): void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export { type DeliveryChannel as D, type Logger as L, MemoryStore as M, type Page as P, type Query as Q, type Store as S, DeliveryError as a, type Doc as b, type Scalar as c };
|