agentic-notify 0.0.0-stage → 0.1.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.
@@ -0,0 +1,155 @@
1
+ import {
2
+ TestEmailAdapter
3
+ } from "./chunk-KJKM56FT.js";
4
+ import {
5
+ encodeCursor
6
+ } from "./chunk-GJFAXI2K.js";
7
+
8
+ // 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
+ }
152
+ export {
153
+ TestEmailAdapter,
154
+ storeContract
155
+ };
@@ -0,0 +1,69 @@
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 };
@@ -0,0 +1,217 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const CHANNELS: readonly ["inbox", "webhook", "email"];
4
+ type Channel = (typeof CHANNELS)[number];
5
+ declare const recipientSchema: z.ZodObject<{
6
+ kind: z.ZodEnum<["human", "agent"]>;
7
+ id: z.ZodString;
8
+ }, "strict", z.ZodTypeAny, {
9
+ id: string;
10
+ kind: "human" | "agent";
11
+ }, {
12
+ id: string;
13
+ kind: "human" | "agent";
14
+ }>;
15
+ type Recipient = z.infer<typeof recipientSchema>;
16
+ declare const resourceRefSchema: z.ZodObject<{
17
+ kind: z.ZodString;
18
+ id: z.ZodString;
19
+ url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
20
+ }, "strict", z.ZodTypeAny, {
21
+ id: string;
22
+ kind: string;
23
+ url?: string | undefined;
24
+ }, {
25
+ id: string;
26
+ kind: string;
27
+ url?: string | undefined;
28
+ }>;
29
+ type ResourceRef = z.infer<typeof resourceRefSchema>;
30
+ declare const actorSchema: z.ZodObject<{
31
+ kind: z.ZodEnum<["human", "agent"]>;
32
+ id: z.ZodString;
33
+ }, "strict", z.ZodTypeAny, {
34
+ id: string;
35
+ kind: "human" | "agent";
36
+ }, {
37
+ id: string;
38
+ kind: "human" | "agent";
39
+ }>;
40
+ type Actor = z.infer<typeof actorSchema>;
41
+ declare const notificationSchema: z.ZodObject<{
42
+ id: z.ZodString;
43
+ recipient: z.ZodObject<{
44
+ kind: z.ZodEnum<["human", "agent"]>;
45
+ id: z.ZodString;
46
+ }, "strict", z.ZodTypeAny, {
47
+ id: string;
48
+ kind: "human" | "agent";
49
+ }, {
50
+ id: string;
51
+ kind: "human" | "agent";
52
+ }>;
53
+ type: z.ZodString;
54
+ resource: z.ZodObject<{
55
+ kind: z.ZodString;
56
+ id: z.ZodString;
57
+ url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
58
+ }, "strict", z.ZodTypeAny, {
59
+ id: string;
60
+ kind: string;
61
+ url?: string | undefined;
62
+ }, {
63
+ id: string;
64
+ kind: string;
65
+ url?: string | undefined;
66
+ }>;
67
+ actor: z.ZodObject<{
68
+ kind: z.ZodEnum<["human", "agent"]>;
69
+ id: z.ZodString;
70
+ }, "strict", z.ZodTypeAny, {
71
+ id: string;
72
+ kind: "human" | "agent";
73
+ }, {
74
+ id: string;
75
+ kind: "human" | "agent";
76
+ }>;
77
+ body: z.ZodString;
78
+ createdAt: z.ZodNumber;
79
+ readAt: z.ZodNullable<z.ZodNumber>;
80
+ }, "strict", z.ZodTypeAny, {
81
+ type: string;
82
+ id: string;
83
+ recipient: {
84
+ id: string;
85
+ kind: "human" | "agent";
86
+ };
87
+ resource: {
88
+ id: string;
89
+ kind: string;
90
+ url?: string | undefined;
91
+ };
92
+ actor: {
93
+ id: string;
94
+ kind: "human" | "agent";
95
+ };
96
+ body: string;
97
+ createdAt: number;
98
+ readAt: number | null;
99
+ }, {
100
+ type: string;
101
+ id: string;
102
+ recipient: {
103
+ id: string;
104
+ kind: "human" | "agent";
105
+ };
106
+ resource: {
107
+ id: string;
108
+ kind: string;
109
+ url?: string | undefined;
110
+ };
111
+ actor: {
112
+ id: string;
113
+ kind: "human" | "agent";
114
+ };
115
+ body: string;
116
+ createdAt: number;
117
+ readAt: number | null;
118
+ }>;
119
+ type Notification = z.infer<typeof notificationSchema>;
120
+ declare const sendInputSchema: z.ZodObject<{
121
+ recipient: z.ZodObject<{
122
+ kind: z.ZodEnum<["human", "agent"]>;
123
+ id: z.ZodString;
124
+ }, "strict", z.ZodTypeAny, {
125
+ id: string;
126
+ kind: "human" | "agent";
127
+ }, {
128
+ id: string;
129
+ kind: "human" | "agent";
130
+ }>;
131
+ type: z.ZodString;
132
+ resource: z.ZodObject<{
133
+ kind: z.ZodString;
134
+ id: z.ZodString;
135
+ url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
136
+ }, "strict", z.ZodTypeAny, {
137
+ id: string;
138
+ kind: string;
139
+ url?: string | undefined;
140
+ }, {
141
+ id: string;
142
+ kind: string;
143
+ url?: string | undefined;
144
+ }>;
145
+ actor: z.ZodObject<{
146
+ kind: z.ZodEnum<["human", "agent"]>;
147
+ id: z.ZodString;
148
+ }, "strict", z.ZodTypeAny, {
149
+ id: string;
150
+ kind: "human" | "agent";
151
+ }, {
152
+ id: string;
153
+ kind: "human" | "agent";
154
+ }>;
155
+ body: z.ZodString;
156
+ }, "strict", z.ZodTypeAny, {
157
+ type: string;
158
+ recipient: {
159
+ id: string;
160
+ kind: "human" | "agent";
161
+ };
162
+ resource: {
163
+ id: string;
164
+ kind: string;
165
+ url?: string | undefined;
166
+ };
167
+ actor: {
168
+ id: string;
169
+ kind: "human" | "agent";
170
+ };
171
+ body: string;
172
+ }, {
173
+ type: string;
174
+ recipient: {
175
+ id: string;
176
+ kind: "human" | "agent";
177
+ };
178
+ resource: {
179
+ id: string;
180
+ kind: string;
181
+ url?: string | undefined;
182
+ };
183
+ actor: {
184
+ id: string;
185
+ kind: "human" | "agent";
186
+ };
187
+ body: string;
188
+ }>;
189
+ type SendInput = z.infer<typeof sendInputSchema>;
190
+ declare const preferencesSchema: z.ZodObject<{
191
+ channels: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodArray<z.ZodEnum<["inbox", "webhook", "email"]>, "many">, ("inbox" | "webhook" | "email")[], ("inbox" | "webhook" | "email")[]>>;
192
+ muted: z.ZodBoolean;
193
+ mutedTypes: z.ZodArray<z.ZodString, "many">;
194
+ }, "strict", z.ZodTypeAny, {
195
+ channels: Record<string, ("inbox" | "webhook" | "email")[]>;
196
+ muted: boolean;
197
+ mutedTypes: string[];
198
+ }, {
199
+ channels: Record<string, ("inbox" | "webhook" | "email")[]>;
200
+ muted: boolean;
201
+ mutedTypes: string[];
202
+ }>;
203
+ type Preferences = z.infer<typeof preferencesSchema>;
204
+ declare const DEFAULT_CHANNELS: readonly Channel[];
205
+ declare function defaultPreferences(): Preferences;
206
+ declare function recipientKey(recipient: Recipient): string;
207
+ interface DigestGroup {
208
+ type: string;
209
+ count: number;
210
+ latest: Notification;
211
+ }
212
+ interface Digest {
213
+ total: number;
214
+ groups: DigestGroup[];
215
+ }
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 };
@@ -0,0 +1,61 @@
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';
6
+
7
+ /** Who is calling. `kind` has no default: a missing kind is an error. */
8
+ declare const principalSchema: z.ZodObject<{
9
+ kind: z.ZodEnum<["human", "agent"]>;
10
+ id: z.ZodString;
11
+ /** Allowed to send notifications to others. */
12
+ canSend: z.ZodOptional<z.ZodBoolean>;
13
+ }, "strict", z.ZodTypeAny, {
14
+ id: string;
15
+ kind: "human" | "agent";
16
+ canSend?: boolean | undefined;
17
+ }, {
18
+ id: string;
19
+ kind: "human" | "agent";
20
+ canSend?: boolean | undefined;
21
+ }>;
22
+ type Principal = z.infer<typeof principalSchema>;
23
+ /** Port: turn a request into a principal, or null for "not signed in". Plug in any identity system. */
24
+ type PrincipalResolver = (request: Request) => Promise<Principal | null> | Principal | null;
25
+
26
+ interface AppOptions {
27
+ notifier: Notifier;
28
+ /** When set, sends also go to webhook and email channels. */
29
+ dispatcher?: ReturnType<typeof createDispatcher>;
30
+ resolvePrincipal: PrincipalResolver;
31
+ /** May this principal send to this recipient? Default: only principals with `canSend: true`. */
32
+ authorizeSend?: (principal: Principal, recipient: Recipient) => Promise<boolean> | boolean;
33
+ }
34
+
35
+ interface WorkerEnv {
36
+ NOTIFY_NAME?: string;
37
+ /** D1 binding. */
38
+ DB?: D1Like;
39
+ /** Secret. JSON map of bearer token (32 bytes or more) to principal. */
40
+ NOTIFY_TOKENS?: string;
41
+ /** Retention window in days. Unset keeps notifications. */
42
+ NOTIFY_RETENTION_DAYS?: string;
43
+ }
44
+ interface WorkerConfig {
45
+ /** Build the principal resolver from the environment. Null means not configured. */
46
+ resolver(env: WorkerEnv): Promise<PrincipalResolver | null>;
47
+ authorizeSend?: AppOptions["authorizeSend"];
48
+ }
49
+ /** Compose the Worker. Bring your own identity system through `resolver`. */
50
+ declare function createWorker(config: WorkerConfig): {
51
+ fetch(request: Request, env: WorkerEnv): Promise<Response>;
52
+ /** Delete notifications past the retention window. Wire to a cron trigger. */
53
+ scheduled(_event: unknown, env: WorkerEnv): Promise<void>;
54
+ };
55
+ declare const _default: {
56
+ fetch(request: Request, env: WorkerEnv): Promise<Response>;
57
+ /** Delete notifications past the retention window. Wire to a cron trigger. */
58
+ scheduled(_event: unknown, env: WorkerEnv): Promise<void>;
59
+ };
60
+
61
+ export { type WorkerConfig, type WorkerEnv, createWorker, _default as default };
package/dist/worker.js ADDED
@@ -0,0 +1,170 @@
1
+ import {
2
+ D1Store,
3
+ NotFoundError,
4
+ VERSION,
5
+ createNotifier,
6
+ migrateD1,
7
+ preferencesSchema,
8
+ recipientSchema,
9
+ resourceRefSchema,
10
+ typeNameSchema
11
+ } from "./chunk-FMU2ED2H.js";
12
+ import "./chunk-GJFAXI2K.js";
13
+
14
+ // src/api.ts
15
+ import { Hono } from "hono";
16
+ import { bodyLimit } from "hono/body-limit";
17
+ import { ZodError, z as z2 } from "zod";
18
+
19
+ // src/auth.ts
20
+ import { z } from "zod";
21
+ var principalSchema = z.object({
22
+ kind: z.enum(["human", "agent"]),
23
+ id: z.string().min(1).max(200),
24
+ /** Allowed to send notifications to others. */
25
+ canSend: z.boolean().optional()
26
+ }).strict();
27
+ var MIN_TOKEN_BYTES = 32;
28
+ var enc = new TextEncoder();
29
+ async function digest(token) {
30
+ const d = await crypto.subtle.digest("SHA-256", enc.encode(token));
31
+ return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join("");
32
+ }
33
+ async function createStaticTokenResolver(tokens) {
34
+ const byDigest = /* @__PURE__ */ new Map();
35
+ for (const [token, principal] of Object.entries(tokens)) {
36
+ if (enc.encode(token).length < MIN_TOKEN_BYTES) throw new Error(`tokens must hold at least ${MIN_TOKEN_BYTES} bytes`);
37
+ byDigest.set(await digest(token), principalSchema.parse(principal));
38
+ }
39
+ if (byDigest.size === 0) throw new Error("no tokens configured");
40
+ return async (request) => {
41
+ const m = /^Bearer ([^\s]+)$/.exec(request.headers.get("authorization") ?? "");
42
+ return m ? byDigest.get(await digest(m[1])) ?? null : null;
43
+ };
44
+ }
45
+
46
+ // src/api.ts
47
+ var sendBody = z2.object({ recipient: recipientSchema, type: typeNameSchema, resource: resourceRefSchema, body: z2.string().max(2e3) }).strict();
48
+ var num = z2.coerce.number().int().nonnegative();
49
+ function createApp(options) {
50
+ const { notifier } = options;
51
+ const authorizeSend = options.authorizeSend ?? ((p) => p.canSend === true);
52
+ const app = new Hono();
53
+ app.get("/health", (c) => c.json({ ok: true }));
54
+ const api = new Hono();
55
+ api.use("*", bodyLimit({ maxSize: 64 * 1024, onError: (c) => c.json({ error: "too_large" }, 413) }));
56
+ api.use("*", async (c, next) => {
57
+ const raw = await options.resolvePrincipal(c.req.raw);
58
+ const parsed = principalSchema.safeParse(raw);
59
+ if (!parsed.success) return c.json({ error: "unauthorized" }, 401);
60
+ c.set("principal", parsed.data);
61
+ c.set("me", { kind: parsed.data.kind, id: parsed.data.id });
62
+ await next();
63
+ });
64
+ async function json2(c) {
65
+ try {
66
+ return await c.req.json();
67
+ } catch {
68
+ throw new ZodError([{ code: "custom", path: [], message: "body must be JSON" }]);
69
+ }
70
+ }
71
+ api.get("/notifications", async (c) => {
72
+ const q = c.req.query();
73
+ const limit = q.limit === void 0 ? void 0 : z2.coerce.number().int().min(1).parse(q.limit);
74
+ try {
75
+ return c.json(await notifier.list(c.var.me, { unreadOnly: q.unread === "1" || q.unread === "true", limit, cursor: q.cursor }));
76
+ } catch (error) {
77
+ if (error instanceof RangeError) throw new ZodError([{ code: "custom", path: ["cursor"], message: "bad cursor" }]);
78
+ throw error;
79
+ }
80
+ });
81
+ api.post("/notifications", async (c) => {
82
+ const input = sendBody.parse(await json2(c));
83
+ if (!await authorizeSend(c.var.principal, input.recipient)) return c.json({ error: "forbidden" }, 403);
84
+ const full = { ...input, actor: c.var.me };
85
+ if (options.dispatcher) return c.json(await options.dispatcher.notify(full), 201);
86
+ const sent = await notifier.send(full);
87
+ const deliveries = sent.channels.filter((channel) => channel !== "inbox").map((channel) => ({ channel, ok: false, skipped: "no_channel" }));
88
+ return c.json({ ...sent, deliveries }, 201);
89
+ });
90
+ api.post("/notifications/read-all", async (c) => c.json({ marked: await notifier.markAllRead(c.var.me) }));
91
+ api.get("/notifications/:id", async (c) => c.json(await notifier.get(c.var.me, c.req.param("id"))));
92
+ api.post("/notifications/:id/read", async (c) => c.json(await notifier.markRead(c.var.me, c.req.param("id"))));
93
+ api.get("/unread-count", async (c) => c.json({ count: await notifier.unreadCount(c.var.me) }));
94
+ api.get("/digest", async (c) => {
95
+ const since = c.req.query("since");
96
+ return c.json(await notifier.digest(c.var.me, since === void 0 ? 0 : num.parse(since)));
97
+ });
98
+ api.get("/preferences", async (c) => c.json(await notifier.getPreferences(c.var.me)));
99
+ api.put("/preferences", async (c) => c.json(await notifier.setPreferences(c.var.me, preferencesSchema.parse(await json2(c)))));
100
+ api.get("/me/export", async (c) => c.json(await notifier.exportData(c.var.me)));
101
+ api.delete("/me", async (c) => c.json(await notifier.erase(c.var.me)));
102
+ app.route("/api/v1", api);
103
+ app.notFound((c) => c.json({ error: "not_found" }, 404));
104
+ app.onError((error, c) => {
105
+ if (error instanceof NotFoundError) return c.json({ error: "not_found" }, 404);
106
+ if (error instanceof ZodError) {
107
+ return c.json({ error: "invalid_request", issues: error.issues.map((i) => ({ path: i.path.join("."), message: i.message })) }, 400);
108
+ }
109
+ console.error("internal error", error instanceof Error ? error.name : "unknown");
110
+ return c.json({ error: "internal_error" }, 500);
111
+ });
112
+ return app;
113
+ }
114
+
115
+ // src/worker.ts
116
+ function json(value, status = 200) {
117
+ return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
118
+ }
119
+ function createWorker(config) {
120
+ const apps = /* @__PURE__ */ new WeakMap();
121
+ async function buildNotifier(env) {
122
+ if (!env.DB) return null;
123
+ await migrateD1(env.DB);
124
+ const days = env.NOTIFY_RETENTION_DAYS === void 0 ? void 0 : Number(env.NOTIFY_RETENTION_DAYS);
125
+ return createNotifier({
126
+ store: new D1Store(env.DB),
127
+ retentionMs: days === void 0 ? void 0 : days * 864e5
128
+ });
129
+ }
130
+ async function build(env) {
131
+ const resolvePrincipal = await config.resolver(env);
132
+ const notifier = resolvePrincipal ? await buildNotifier(env) : null;
133
+ if (!resolvePrincipal || !notifier) return null;
134
+ return createApp({ notifier, resolvePrincipal, authorizeSend: config.authorizeSend });
135
+ }
136
+ return {
137
+ async fetch(request, env) {
138
+ const url = new URL(request.url);
139
+ if ((request.method === "GET" || request.method === "HEAD") && url.pathname === "/health") {
140
+ return json({ ok: true, name: env.NOTIFY_NAME ?? "agentic-notify", version: VERSION });
141
+ }
142
+ if (!url.pathname.startsWith("/api/")) return json({ error: "not_found" }, 404);
143
+ let app;
144
+ try {
145
+ let p = apps.get(env);
146
+ if (!p) apps.set(env, p = build(env));
147
+ app = await p;
148
+ } catch {
149
+ apps.delete(env);
150
+ return json({ error: "not_configured" }, 503);
151
+ }
152
+ if (!app) return json({ error: "not_configured" }, 503);
153
+ return app.fetch(request);
154
+ },
155
+ /** Delete notifications past the retention window. Wire to a cron trigger. */
156
+ async scheduled(_event, env) {
157
+ await (await buildNotifier(env))?.sweep();
158
+ }
159
+ };
160
+ }
161
+ var worker_default = createWorker({
162
+ async resolver(env) {
163
+ if (!env.NOTIFY_TOKENS) return null;
164
+ return createStaticTokenResolver(JSON.parse(env.NOTIFY_TOKENS));
165
+ }
166
+ });
167
+ export {
168
+ createWorker,
169
+ worker_default as default
170
+ };