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 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, first release.** Works today: the notification model, preferences and mute, digest, an in-memory store and a D1 store (both pass one contract suite), 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.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, MemoryStore } from "agentic-notify";
32
+ import { createNotifier, createMemoryStore } from "agentic-notify";
22
33
 
23
- const notifier = createNotifier({ store: new MemoryStore() });
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.1.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.doc;
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
- for (const id2 of ids) await store.delete(NOTES, id2);
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.doc.createdAt === "number" && i.doc.createdAt < cutoff);
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
- for (const item of old) {
198
- await store.delete(NOTES, item.id);
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.doc.createdAt === "number")) return removed;
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
- if (!await store.create(NOTES, notification.id, toDoc(notification))) {
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: opts.limit,
288
+ limit,
269
289
  cursor: opts.cursor
270
290
  });
271
- return { items: page.items.map((i) => fromDoc(i.doc)), nextCursor: page.nextCursor };
291
+ return { items: page.items.map((i) => fromDoc(i.data)), nextCursor: page.nextCursor ?? null };
272
292
  }
273
- async function load(recipient, id2) {
274
- const r = recipientSchema.parse(recipient);
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
- return fromDoc(await load(recipient, id2));
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
- for (let attempt = 0; attempt < 5; attempt++) {
284
- const doc = await load(recipient, id2);
285
- const n = fromDoc(doc);
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
- if (await store.replaceIf(NOTES, id2, doc, toDoc(next))) return { notification: next, won: true };
289
- }
290
- throw new Error("could not mark read: too much contention");
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-6CKFBZOF.js";
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-FMU2ED2H.js";
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-6CKFBZOF.js";
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-FMU2ED2H.js";
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-9I4eV86m.js';
2
- import { S as Store, D as DeliveryChannel, L as Logger, b as Doc, Q as Query, P as Page } from './types-Dkst26cb.js';
3
- import { S as SendInput, N as Notification, b as Channel, R as Recipient, D as Digest, P as Preferences } from './types-GuAgyMOc.js';
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
- /** The slice of the Cloudflare D1 binding this adapter uses. */
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 };
@@ -1,4 +1,4 @@
1
- import { R as Recipient, P as Preferences, N as Notification } from './types-GuAgyMOc.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,11 +1,14 @@
1
- export { D as D1Like, a as D1Store, b as DispatchResult, c as DispatcherOptions, L as ListOptions, d as ListResult, N as NotFoundError, e as Notifier, f as NotifierOptions, S as SendResult, g as createDispatcher, h as createNotifier, m as migrateD1 } from './d1-CUcNko0m.js';
2
- import { D as DeliveryChannel } from './types-Dkst26cb.js';
3
- export { a as DeliveryError, b as Doc, L as Logger, M as MemoryStore, P as Page, Q as Query, c as Scalar, S as Store } from './types-Dkst26cb.js';
4
- import { R as Recipient } from './types-GuAgyMOc.js';
5
- export { A as Actor, C as CHANNELS, b 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-GuAgyMOc.js';
6
- export { E as EmailChannelOptions, a as EmailMessage, b as EmailPort, c as createEmailChannel } from './email-UqtllxjF.js';
7
- export { R as RecipientExport } from './gdpr-9I4eV86m.js';
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-6CKFBZOF.js";
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-FMU2ED2H.js";
24
- import {
25
- MemoryStore
26
- } from "./chunk-GJFAXI2K.js";
24
+ } from "./chunk-6QCYA3X7.js";
27
25
  export {
28
26
  CHANNELS,
29
- D1Store,
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-DEPJ3SHO.js";
5
- import "./chunk-6CKFBZOF.js";
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-FMU2ED2H.js";
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-GuAgyMOc.js';
3
+ import './types-C842YAMe.js';
4
4
  import 'zod';
5
- import './gdpr-9I4eV86m.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
@@ -1,12 +1,11 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcp
4
- } from "./chunk-DEPJ3SHO.js";
5
- import "./chunk-6CKFBZOF.js";
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-FMU2ED2H.js";
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-GuAgyMOc.js';
2
- import { R as RecipientExport } from './gdpr-9I4eV86m.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 {
@@ -1,5 +1,6 @@
1
- import { R as Recipient } from './types-GuAgyMOc.js';
2
- import { D as DeliveryChannel } from './types-Dkst26cb.js';
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
- import { S as Store } from './types-Dkst26cb.js';
2
- export { T as TestEmailAdapter } from './email-UqtllxjF.js';
3
- import './types-GuAgyMOc.js';
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 { describe, expect, test } from "vitest";
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
- storeContract
9
+ runStoreContract
155
10
  };
@@ -214,4 +214,4 @@ interface Digest {
214
214
  groups: DigestGroup[];
215
215
  }
216
216
 
217
- export { type Actor as A, CHANNELS 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, type Channel as b, DEFAULT_CHANNELS as c, type DigestGroup as d, defaultPreferences as e, recipientKey as r };
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 { e as Notifier, g as createDispatcher, D as D1Like } from './d1-CUcNko0m.js';
3
- import { R as Recipient } from './types-GuAgyMOc.js';
4
- import './gdpr-9I4eV86m.js';
5
- import './types-Dkst26cb.js';
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
- D1Store,
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-FMU2ED2H.js";
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 migrateD1(env.DB);
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: new D1Store(env.DB),
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.1.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"
@@ -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
- };
@@ -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 };