easy-ping 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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/dist/adapter-f0OR2DhY.d.cts +200 -0
  4. package/dist/adapter-f0OR2DhY.d.ts +200 -0
  5. package/dist/adapters/drizzle.cjs +440 -0
  6. package/dist/adapters/drizzle.cjs.map +1 -0
  7. package/dist/adapters/drizzle.d.cts +991 -0
  8. package/dist/adapters/drizzle.d.ts +991 -0
  9. package/dist/adapters/drizzle.js +409 -0
  10. package/dist/adapters/drizzle.js.map +1 -0
  11. package/dist/adapters/mongodb.cjs +458 -0
  12. package/dist/adapters/mongodb.cjs.map +1 -0
  13. package/dist/adapters/mongodb.d.cts +32 -0
  14. package/dist/adapters/mongodb.d.ts +32 -0
  15. package/dist/adapters/mongodb.js +376 -0
  16. package/dist/adapters/mongodb.js.map +1 -0
  17. package/dist/browser.cjs +96 -0
  18. package/dist/browser.cjs.map +1 -0
  19. package/dist/browser.d.cts +45 -0
  20. package/dist/browser.d.ts +45 -0
  21. package/dist/browser.js +89 -0
  22. package/dist/browser.js.map +1 -0
  23. package/dist/chunk-BV3U542P.js +11 -0
  24. package/dist/chunk-BV3U542P.js.map +1 -0
  25. package/dist/chunk-NYDVNMKG.js +62 -0
  26. package/dist/chunk-NYDVNMKG.js.map +1 -0
  27. package/dist/chunk-OZSOIX23.js +120 -0
  28. package/dist/chunk-OZSOIX23.js.map +1 -0
  29. package/dist/chunk-PTGHMCCG.js +23 -0
  30. package/dist/chunk-PTGHMCCG.js.map +1 -0
  31. package/dist/chunk-W3BMEPPQ.js +50 -0
  32. package/dist/chunk-W3BMEPPQ.js.map +1 -0
  33. package/dist/client.cjs +160 -0
  34. package/dist/client.cjs.map +1 -0
  35. package/dist/client.d.cts +42 -0
  36. package/dist/client.d.ts +42 -0
  37. package/dist/client.js +158 -0
  38. package/dist/client.js.map +1 -0
  39. package/dist/index.cjs +920 -0
  40. package/dist/index.cjs.map +1 -0
  41. package/dist/index.d.cts +203 -0
  42. package/dist/index.d.ts +203 -0
  43. package/dist/index.js +723 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/node.cjs +49 -0
  46. package/dist/node.cjs.map +1 -0
  47. package/dist/node.d.cts +8 -0
  48. package/dist/node.d.ts +8 -0
  49. package/dist/node.js +46 -0
  50. package/dist/node.js.map +1 -0
  51. package/dist/plugin-Bqj9xAOS.d.cts +217 -0
  52. package/dist/plugin-bTJKmnlq.d.ts +217 -0
  53. package/dist/plugins/digests.cjs +206 -0
  54. package/dist/plugins/digests.cjs.map +1 -0
  55. package/dist/plugins/digests.d.cts +68 -0
  56. package/dist/plugins/digests.d.ts +68 -0
  57. package/dist/plugins/digests.js +201 -0
  58. package/dist/plugins/digests.js.map +1 -0
  59. package/dist/plugins/preferences-client.cjs +23 -0
  60. package/dist/plugins/preferences-client.cjs.map +1 -0
  61. package/dist/plugins/preferences-client.d.cts +29 -0
  62. package/dist/plugins/preferences-client.d.ts +29 -0
  63. package/dist/plugins/preferences-client.js +18 -0
  64. package/dist/plugins/preferences-client.js.map +1 -0
  65. package/dist/plugins/preferences-react.cjs +125 -0
  66. package/dist/plugins/preferences-react.cjs.map +1 -0
  67. package/dist/plugins/preferences-react.d.cts +40 -0
  68. package/dist/plugins/preferences-react.d.ts +40 -0
  69. package/dist/plugins/preferences-react.js +101 -0
  70. package/dist/plugins/preferences-react.js.map +1 -0
  71. package/dist/plugins/preferences.cjs +147 -0
  72. package/dist/plugins/preferences.cjs.map +1 -0
  73. package/dist/plugins/preferences.d.cts +22 -0
  74. package/dist/plugins/preferences.d.ts +22 -0
  75. package/dist/plugins/preferences.js +119 -0
  76. package/dist/plugins/preferences.js.map +1 -0
  77. package/dist/plugins/push.cjs +146 -0
  78. package/dist/plugins/push.cjs.map +1 -0
  79. package/dist/plugins/push.d.cts +60 -0
  80. package/dist/plugins/push.d.ts +60 -0
  81. package/dist/plugins/push.js +144 -0
  82. package/dist/plugins/push.js.map +1 -0
  83. package/dist/provider-CA45WVqt.d.cts +23 -0
  84. package/dist/provider-CA45WVqt.d.ts +23 -0
  85. package/dist/providers/resend.cjs +69 -0
  86. package/dist/providers/resend.cjs.map +1 -0
  87. package/dist/providers/resend.d.cts +20 -0
  88. package/dist/providers/resend.d.ts +20 -0
  89. package/dist/providers/resend.js +66 -0
  90. package/dist/providers/resend.js.map +1 -0
  91. package/dist/providers/web-push.cjs +211 -0
  92. package/dist/providers/web-push.cjs.map +1 -0
  93. package/dist/providers/web-push.d.cts +33 -0
  94. package/dist/providers/web-push.d.ts +33 -0
  95. package/dist/providers/web-push.js +193 -0
  96. package/dist/providers/web-push.js.map +1 -0
  97. package/dist/react.cjs +218 -0
  98. package/dist/react.cjs.map +1 -0
  99. package/dist/react.d.cts +57 -0
  100. package/dist/react.d.ts +57 -0
  101. package/dist/react.js +193 -0
  102. package/dist/react.js.map +1 -0
  103. package/dist/schema.cjs +263 -0
  104. package/dist/schema.cjs.map +1 -0
  105. package/dist/schema.d.cts +180 -0
  106. package/dist/schema.d.ts +180 -0
  107. package/dist/schema.js +200 -0
  108. package/dist/schema.js.map +1 -0
  109. package/dist/testing.cjs +342 -0
  110. package/dist/testing.cjs.map +1 -0
  111. package/dist/testing.d.cts +38 -0
  112. package/dist/testing.d.ts +38 -0
  113. package/dist/testing.js +340 -0
  114. package/dist/testing.js.map +1 -0
  115. package/package.json +168 -0
@@ -0,0 +1,89 @@
1
+ // src/browser/index.ts
2
+ var PushUnsupportedError = class extends Error {
3
+ constructor(reason) {
4
+ super(`web push is unavailable: ${reason}`);
5
+ this.name = "PushUnsupportedError";
6
+ }
7
+ };
8
+ var PushPermissionError = class extends Error {
9
+ constructor(permission) {
10
+ super(`notification permission was ${permission}`);
11
+ this.permission = permission;
12
+ this.name = "PushPermissionError";
13
+ }
14
+ permission;
15
+ };
16
+ function isPushSupported() {
17
+ return typeof navigator !== "undefined" && "serviceWorker" in navigator && typeof PushManager !== "undefined" && typeof Notification !== "undefined";
18
+ }
19
+ function decodeVapidKey(publicKey) {
20
+ const padded = publicKey.padEnd(publicKey.length + (4 - publicKey.length % 4) % 4, "=");
21
+ let binary;
22
+ try {
23
+ binary = atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
24
+ } catch {
25
+ throw new Error("VAPID publicKey is not valid base64url");
26
+ }
27
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
28
+ if (bytes.length !== 65 || bytes[0] !== 4) {
29
+ throw new Error(
30
+ `VAPID publicKey must be a 65-byte uncompressed EC point, got ${bytes.length} bytes`
31
+ );
32
+ }
33
+ return bytes;
34
+ }
35
+ async function resolveRegistration(options) {
36
+ if (options.registration) return options.registration;
37
+ if (!isPushSupported()) throw new PushUnsupportedError("this browser has no PushManager");
38
+ const path = options.serviceWorkerPath ?? "/sw.js";
39
+ const registration = await navigator.serviceWorker.register(
40
+ path,
41
+ options.scope ? { scope: options.scope } : void 0
42
+ );
43
+ await navigator.serviceWorker.ready;
44
+ return registration;
45
+ }
46
+ async function subscribeToPush(options) {
47
+ const registration = await resolveRegistration(options);
48
+ if (typeof Notification !== "undefined") {
49
+ const permission = Notification.permission === "default" ? await Notification.requestPermission() : Notification.permission;
50
+ if (permission !== "granted") throw new PushPermissionError(permission);
51
+ }
52
+ const existing = await registration.pushManager.getSubscription();
53
+ const subscription = existing ?? await registration.pushManager.subscribe({
54
+ // Required by Chrome; a silent push is rejected outright.
55
+ userVisibleOnly: true,
56
+ applicationServerKey: decodeVapidKey(options.publicKey)
57
+ });
58
+ const payload = subscription.toJSON();
59
+ const doFetch = options.fetch ?? globalThis.fetch;
60
+ const baseUrl = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
61
+ const response = await doFetch(`${baseUrl}/push/devices`, {
62
+ method: "POST",
63
+ headers: { "content-type": "application/json" },
64
+ credentials: "same-origin",
65
+ body: JSON.stringify({ endpoint: payload.endpoint, keys: payload.keys })
66
+ });
67
+ if (!response.ok) {
68
+ throw new Error(`registering the push device failed: ${response.status}`);
69
+ }
70
+ return payload;
71
+ }
72
+ async function unsubscribeFromPush(options) {
73
+ const registration = await resolveRegistration({ ...options});
74
+ const subscription = await registration.pushManager.getSubscription();
75
+ if (!subscription) return false;
76
+ const doFetch = options.fetch ?? globalThis.fetch;
77
+ const baseUrl = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
78
+ await doFetch(`${baseUrl}/push/devices/remove`, {
79
+ method: "POST",
80
+ headers: { "content-type": "application/json" },
81
+ credentials: "same-origin",
82
+ body: JSON.stringify({ endpoint: subscription.endpoint })
83
+ }).catch(() => void 0);
84
+ return subscription.unsubscribe();
85
+ }
86
+
87
+ export { PushPermissionError, PushUnsupportedError, decodeVapidKey, isPushSupported, subscribeToPush, unsubscribeFromPush };
88
+ //# sourceMappingURL=browser.js.map
89
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/browser/index.ts"],"names":[],"mappings":"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","file":"browser.js","sourcesContent":["/**\n * The browser half of web push.\n *\n * Everything here is injectable so it can be tested in Node: the real\n * navigator, Notification and fetch are only reached through the defaults.\n */\n\nexport type PushSubscriptionPayload = {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n};\n\nexport type SubscribeOptions = {\n /** VAPID public key, base64url — the same one the server signs with. */\n publicKey: string;\n baseUrl?: string;\n serviceWorkerPath?: string;\n scope?: string;\n fetch?: typeof globalThis.fetch;\n /** Pre-resolved registration, so tests need no service worker. */\n registration?: ServiceWorkerRegistration;\n};\n\nexport class PushUnsupportedError extends Error {\n constructor(reason: string) {\n super(`web push is unavailable: ${reason}`);\n this.name = \"PushUnsupportedError\";\n }\n}\n\nexport class PushPermissionError extends Error {\n constructor(readonly permission: NotificationPermission) {\n super(`notification permission was ${permission}`);\n this.name = \"PushPermissionError\";\n }\n}\n\nexport function isPushSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n typeof PushManager !== \"undefined\" &&\n typeof Notification !== \"undefined\"\n );\n}\n\n/**\n * applicationServerKey must be raw bytes; handing the browser the base64url\n * string it came in as fails with an opaque InvalidCharacterError.\n */\nexport function decodeVapidKey(publicKey: string): Uint8Array<ArrayBuffer> {\n const padded = publicKey.padEnd(publicKey.length + ((4 - (publicKey.length % 4)) % 4), \"=\");\n\n let binary: string;\n try {\n binary = atob(padded.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n } catch {\n // atob throws a bare \"Invalid character\", which says nothing about which\n // of the several keys in a push setup is wrong.\n throw new Error(\"VAPID publicKey is not valid base64url\");\n }\n\n const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n if (bytes.length !== 65 || bytes[0] !== 0x04) {\n throw new Error(\n `VAPID publicKey must be a 65-byte uncompressed EC point, got ${bytes.length} bytes`,\n );\n }\n return bytes;\n}\n\nasync function resolveRegistration(options: SubscribeOptions): Promise<ServiceWorkerRegistration> {\n if (options.registration) return options.registration;\n if (!isPushSupported()) throw new PushUnsupportedError(\"this browser has no PushManager\");\n\n const path = options.serviceWorkerPath ?? \"/sw.js\";\n const registration = await navigator.serviceWorker.register(\n path,\n options.scope ? { scope: options.scope } : undefined,\n );\n\n // subscribe() before the worker is active rejects.\n await navigator.serviceWorker.ready;\n return registration;\n}\n\n/**\n * Registers the worker, asks permission, subscribes, and hands the\n * subscription to the server. Returns what was registered.\n */\nexport async function subscribeToPush(options: SubscribeOptions): Promise<PushSubscriptionPayload> {\n const registration = await resolveRegistration(options);\n\n if (typeof Notification !== \"undefined\") {\n const permission =\n Notification.permission === \"default\"\n ? await Notification.requestPermission()\n : Notification.permission;\n\n if (permission !== \"granted\") throw new PushPermissionError(permission);\n }\n\n const existing = await registration.pushManager.getSubscription();\n const subscription =\n existing ??\n (await registration.pushManager.subscribe({\n // Required by Chrome; a silent push is rejected outright.\n userVisibleOnly: true,\n applicationServerKey: decodeVapidKey(options.publicKey),\n }));\n\n const payload = subscription.toJSON() as PushSubscriptionPayload;\n const doFetch = options.fetch ?? globalThis.fetch;\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n\n const response = await doFetch(`${baseUrl}/push/devices`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify({ endpoint: payload.endpoint, keys: payload.keys }),\n });\n\n if (!response.ok) {\n throw new Error(`registering the push device failed: ${response.status}`);\n }\n\n return payload;\n}\n\n/** Tells the server first: a browser-only unsubscribe leaves a dead row behind. */\nexport async function unsubscribeFromPush(\n options: Omit<SubscribeOptions, \"publicKey\">,\n): Promise<boolean> {\n const registration = await resolveRegistration({ ...options, publicKey: \"\" });\n const subscription = await registration.pushManager.getSubscription();\n if (!subscription) return false;\n\n const doFetch = options.fetch ?? globalThis.fetch;\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n\n await doFetch(`${baseUrl}/push/devices/remove`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify({ endpoint: subscription.endpoint }),\n }).catch(() => undefined);\n\n return subscription.unsubscribe();\n}\n"]}
@@ -0,0 +1,11 @@
1
+ // src/core/plugin.ts
2
+ function definePlugin(plugin) {
3
+ return plugin;
4
+ }
5
+ function defineClientPlugin(plugin) {
6
+ return plugin;
7
+ }
8
+
9
+ export { defineClientPlugin, definePlugin };
10
+ //# sourceMappingURL=chunk-BV3U542P.js.map
11
+ //# sourceMappingURL=chunk-BV3U542P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/plugin.ts"],"names":[],"mappings":";AAgLO,SAAS,aACd,MAAA,EAC8B;AAC9B,EAAA,OAAO,MAAA;AACT;AAgBO,SAAS,mBACd,MAAA,EAC6B;AAC7B,EAAA,OAAO,MAAA;AACT","file":"chunk-BV3U542P.js","sourcesContent":["import type { DeliveryOutcome } from \"./adapter\";\nimport type { Logger } from \"./errors\";\nimport type { PluginStore } from \"./store\";\nimport type { Channel, Recipient } from \"./types\";\n\nexport type Promisable<T> = T | Promise<T>;\n\nexport type PrepareContext = {\n type: string;\n recipients: readonly Recipient[];\n};\n\nexport type BeforeSendContext = {\n type: string;\n recipient: Recipient;\n payload: unknown;\n actorId: string | null;\n dedupeKey: string | null;\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type BeforeSendResult =\n | { action: \"continue\"; payload?: unknown; dedupeKey?: string }\n | { action: \"cancel\"; reason: string };\n\nexport type ChannelDecision = {\n channel: Channel;\n action: \"send\" | \"skip\" | \"defer\";\n /** defer only. Written to notification_delivery.not_before. */\n notBefore?: Date | undefined;\n};\n\nexport type ResolveChannelsContext = {\n type: string;\n recipient: Recipient;\n payload: unknown;\n decisions: readonly ChannelDecision[];\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type DeliverContext = {\n channel: Channel;\n deliveryId: string;\n attempt: number;\n recipient: Recipient;\n notification: { id: string; userId: string; type: string; payload: unknown };\n /** Aborted on the runner's timeout. */\n signal: AbortSignal;\n};\n\nexport type AfterSendContext = {\n type: string;\n payload: unknown;\n /** Committed rows. Ids exist only at this point. */\n notifications: readonly { id: string; userId: string }[];\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type AfterDeliverContext = {\n deliveryId: string;\n notificationId: string;\n userId: string;\n channel: Channel;\n outcome: DeliveryOutcome;\n attempt: number;\n isTerminal: boolean;\n};\n\n/** beforeSend and resolveChannels fail closed; afterDeliver fails open. */\nexport type PluginHooks = {\n /** Once per send, for bulk loading — the per-recipient hooks would be an N+1. */\n prepare?: (ctx: PrepareContext) => Promisable<unknown>;\n beforeSend?: (ctx: BeforeSendContext) => Promisable<BeforeSendResult>;\n resolveChannels?: (ctx: ResolveChannelsContext) => Promisable<readonly ChannelDecision[]>;\n /**\n * Once per send, after the rows are committed. The only hook that sees\n * notification ids, which digests needs to bucket an entry against one.\n * Fails open: the notification is already written.\n */\n afterSend?: (ctx: AfterSendContext) => Promisable<void>;\n /**\n * Delivers one of the plugin's declared `channels`. Core carries inApp and\n * email; everything else arrives here, which is how push, sms and slack are\n * added without core learning about them.\n */\n deliver?: (ctx: DeliverContext) => Promisable<DeliveryOutcome>;\n afterDeliver?: (ctx: AfterDeliverContext) => Promisable<void>;\n};\n\nexport type FieldType = \"string\" | \"number\" | \"boolean\" | \"date\" | \"json\";\n\nexport type FieldDeclaration = {\n type: FieldType;\n required?: boolean;\n default?: string | number | boolean;\n /** date fields only. Renders as defaultNow() / CURRENT_TIMESTAMP. */\n defaultNow?: boolean;\n};\n\nexport type IndexDeclaration = {\n on: readonly string[];\n unique?: boolean;\n name?: string;\n};\n\nexport type TableDeclaration = {\n tableName: string;\n fields: Record<string, FieldDeclaration>;\n primaryKey?: readonly string[];\n indexes?: readonly IndexDeclaration[];\n};\n\nexport type SchemaDeclaration = Record<string, TableDeclaration>;\n\nexport type RouteScope =\n | { type: \"user\" }\n | { type: \"machine\" }\n | { type: \"signed\"; purpose: string }\n | { type: \"custom\"; justification: string };\n\nexport type RouteContext = {\n request: Request;\n /** Resolved for user-scoped routes, so a handler cannot forget to scope. */\n userId: string | null;\n /** Verified before dispatch, so a handler cannot forget to check. */\n claims: { uid: string; purpose: string; exp: number; data?: Record<string, string> } | null;\n params: Record<string, string>;\n};\n\nexport type RouteDefinition = {\n path: string;\n method: \"GET\" | \"POST\";\n scope: RouteScope;\n handler: (ctx: RouteContext) => Promise<Response>;\n};\n\n/** Handed to a plugin at startup so it need not be given the database twice. */\nexport type PluginInitContext = {\n /** Scoped to the tables this plugin declares in schema(). */\n store: PluginStore;\n /** The top-level signing secret, for plugins issuing signed links. */\n secret: string;\n logger: Logger;\n /** The app's resolver, for plugins that need timezones or addresses. */\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n /**\n * The instance's own send(). A plugin composing a message (a digest, say)\n * routes it back through the pipeline rather than reaching for a provider,\n * so it inherits retry, backoff and idempotency for free.\n */\n send: (\n type: string,\n args: { to: string | readonly string[]; payload: unknown; dedupeKey?: string },\n ) => Promise<unknown>;\n};\n\nexport type EasyPingPlugin<\n TId extends string = string,\n TSchema extends SchemaDeclaration = SchemaDeclaration,\n> = {\n id: TId;\n dependsOn?: readonly string[];\n schema?: TSchema;\n routes?: readonly RouteDefinition[];\n hooks?: PluginHooks;\n /** Channels this plugin delivers. Makes them usable in a notification. */\n channels?: readonly Channel[];\n /** Called once by easyPing() before any hook or route can run. */\n init?: (ctx: PluginInitContext) => void;\n};\n\nexport type AnyPlugin = EasyPingPlugin<string, SchemaDeclaration>;\n\nexport function definePlugin<TId extends string, TSchema extends SchemaDeclaration>(\n plugin: EasyPingPlugin<TId, TSchema>,\n): EasyPingPlugin<TId, TSchema> {\n return plugin;\n}\n\nexport type ClientFetch = (path: string, init?: RequestInit) => Promise<unknown>;\n\nexport type ClientPlugin<\n TId extends string = string,\n TActions extends Record<string, unknown> = Record<string, unknown>,\n> = {\n id: TId;\n /** Phantom. Links client types to the server plugin so a mismatch is a type error. */\n $InferServerPlugin?: AnyPlugin;\n actions: (fetch: ClientFetch) => TActions;\n};\n\nexport type AnyClientPlugin = ClientPlugin<string, Record<string, unknown>>;\n\nexport function defineClientPlugin<TId extends string, TActions extends Record<string, unknown>>(\n plugin: ClientPlugin<TId, TActions>,\n): ClientPlugin<TId, TActions> {\n return plugin;\n}\n\nexport type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (\n k: infer I,\n) => void\n ? I\n : never;\n\n/** Naked type parameter, so the conditional distributes over the union. */\ntype ExtractActions<TPlugin> = TPlugin extends { actions: (fetch: never) => infer TActions }\n ? TActions\n : never;\n\n/** Indexes the tuple rather than recursing: a fold blows the instantiation depth limit. */\nexport type ActionsOf<TPlugins extends readonly AnyClientPlugin[]> = UnionToIntersection<\n ExtractActions<TPlugins[number]>\n>;\n"]}
@@ -0,0 +1,62 @@
1
+ // src/schema/declaration.ts
2
+ var toSnakeCase = (value) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
3
+ var coreSchema = {
4
+ notification: {
5
+ tableName: "notification",
6
+ fields: {
7
+ id: { type: "string", required: true },
8
+ userId: { type: "string", required: true },
9
+ type: { type: "string", required: true },
10
+ payload: { type: "json", required: true },
11
+ actorId: { type: "string" },
12
+ groupKey: { type: "string" },
13
+ dedupeKey: { type: "string" },
14
+ seenAt: { type: "date" },
15
+ readAt: { type: "date" },
16
+ archivedAt: { type: "date" },
17
+ createdAt: { type: "date", required: true, defaultNow: true }
18
+ },
19
+ primaryKey: ["id"],
20
+ indexes: [
21
+ { on: ["userId", "createdAt"], name: "notification_feed_idx" },
22
+ { on: ["userId", "groupKey"], name: "notification_group_idx" },
23
+ { on: ["userId", "dedupeKey"], unique: true, name: "notification_dedupe_idx" }
24
+ ]
25
+ },
26
+ notificationDelivery: {
27
+ tableName: "notification_delivery",
28
+ fields: {
29
+ id: { type: "string", required: true },
30
+ notificationId: { type: "string", required: true },
31
+ channel: { type: "string", required: true },
32
+ status: { type: "string", required: true, default: "pending" },
33
+ attempts: { type: "number", required: true, default: 0 },
34
+ maxAttempts: { type: "number", required: true, default: 5 },
35
+ notBefore: { type: "date", required: true, defaultNow: true },
36
+ claimedAt: { type: "date" },
37
+ claimedBy: { type: "string" },
38
+ lastError: { type: "string" },
39
+ updatedAt: { type: "date", required: true, defaultNow: true }
40
+ },
41
+ primaryKey: ["id"],
42
+ indexes: [
43
+ { on: ["status", "notBefore"], name: "delivery_claim_idx" },
44
+ { on: ["notificationId"], name: "delivery_notification_idx" }
45
+ ]
46
+ },
47
+ notificationPreference: {
48
+ tableName: "notification_preference",
49
+ fields: {
50
+ userId: { type: "string", required: true },
51
+ type: { type: "string", required: true },
52
+ channel: { type: "string", required: true },
53
+ enabled: { type: "boolean", required: true, default: true },
54
+ frequency: { type: "string", required: true, default: "instant" }
55
+ },
56
+ primaryKey: ["userId", "type", "channel"]
57
+ }
58
+ };
59
+
60
+ export { coreSchema, toSnakeCase };
61
+ //# sourceMappingURL=chunk-NYDVNMKG.js.map
62
+ //# sourceMappingURL=chunk-NYDVNMKG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/declaration.ts"],"names":[],"mappings":"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","file":"chunk-NYDVNMKG.js","sourcesContent":["import type { SchemaDeclaration } from \"../core/plugin\";\n\nexport const toSnakeCase = (value: string) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n\n/**\n * Core tables in the format plugins use. Less expressive than the hand-written\n * Drizzle schema (no partial indexes, ordering or cascade) — a drift test\n * asserts the two agree on the fields they share.\n */\nexport const coreSchema = {\n notification: {\n tableName: \"notification\",\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n payload: { type: \"json\", required: true },\n actorId: { type: \"string\" },\n groupKey: { type: \"string\" },\n dedupeKey: { type: \"string\" },\n seenAt: { type: \"date\" },\n readAt: { type: \"date\" },\n archivedAt: { type: \"date\" },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\", \"createdAt\"], name: \"notification_feed_idx\" },\n { on: [\"userId\", \"groupKey\"], name: \"notification_group_idx\" },\n { on: [\"userId\", \"dedupeKey\"], unique: true, name: \"notification_dedupe_idx\" },\n ],\n },\n\n notificationDelivery: {\n tableName: \"notification_delivery\",\n fields: {\n id: { type: \"string\", required: true },\n notificationId: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n status: { type: \"string\", required: true, default: \"pending\" },\n attempts: { type: \"number\", required: true, default: 0 },\n maxAttempts: { type: \"number\", required: true, default: 5 },\n notBefore: { type: \"date\", required: true, defaultNow: true },\n claimedAt: { type: \"date\" },\n claimedBy: { type: \"string\" },\n lastError: { type: \"string\" },\n updatedAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"status\", \"notBefore\"], name: \"delivery_claim_idx\" },\n { on: [\"notificationId\"], name: \"delivery_notification_idx\" },\n ],\n },\n\n notificationPreference: {\n tableName: \"notification_preference\",\n fields: {\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n enabled: { type: \"boolean\", required: true, default: true },\n frequency: { type: \"string\", required: true, default: \"instant\" },\n },\n primaryKey: [\"userId\", \"type\", \"channel\"],\n },\n} satisfies SchemaDeclaration;\n"]}
@@ -0,0 +1,120 @@
1
+ import { toSnakeCase } from './chunk-NYDVNMKG.js';
2
+
3
+ // src/core/errors.ts
4
+ var EasyPingError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "EasyPingError";
8
+ }
9
+ };
10
+ var ValidationError = class extends EasyPingError {
11
+ issues;
12
+ constructor(type, issues) {
13
+ super(`Payload for "${type}" failed validation: ${issues.map((i) => i.message).join("; ")}`);
14
+ this.name = "ValidationError";
15
+ this.issues = issues;
16
+ }
17
+ };
18
+ var ConfigError = class extends EasyPingError {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "ConfigError";
22
+ }
23
+ };
24
+ var consoleLogger = {
25
+ warn: (message, meta) => console.warn(`[easy-ping] ${message}`, meta ?? ""),
26
+ error: (message, meta) => console.error(`[easy-ping] ${message}`, meta ?? "")
27
+ };
28
+
29
+ // src/core/store.ts
30
+ var OPERATORS = ["in", "lt", "lte", "gt", "gte", "not"];
31
+ var isOperator = (value) => typeof value === "object" && value !== null && !(value instanceof Date) && OPERATORS.some((op) => op in value);
32
+ function createPluginStore(pluginId, schema, storage, prefix) {
33
+ const byTableName = /* @__PURE__ */ new Map();
34
+ for (const declaration of Object.values(schema ?? {})) {
35
+ byTableName.set(declaration.tableName, declaration);
36
+ }
37
+ function resolve(table) {
38
+ const declaration = byTableName.get(table);
39
+ if (!declaration) {
40
+ throw new ConfigError(
41
+ `plugin "${pluginId}" accessed table "${table}", which it does not declare in schema().`
42
+ );
43
+ }
44
+ return declaration;
45
+ }
46
+ function checkFields(table, fields, context) {
47
+ for (const field of fields) {
48
+ const spec = table.fields[field];
49
+ if (!spec) {
50
+ throw new ConfigError(
51
+ `plugin "${pluginId}" used unknown field "${field}" on "${table.tableName}" (${context}).`
52
+ );
53
+ }
54
+ if (context === "where" && spec.type === "json") {
55
+ throw new ConfigError(
56
+ `plugin "${pluginId}" cannot filter on json field "${field}"; a bare object value is indistinguishable from an operator.`
57
+ );
58
+ }
59
+ }
60
+ }
61
+ const qualified = (table) => prefix + table.tableName;
62
+ const column = (field) => storage.naming === "preserve" ? field : toSnakeCase(field);
63
+ const serialize = (table, row) => {
64
+ if (storage.serializesJson === false) return row;
65
+ const out = {};
66
+ for (const [field, value] of Object.entries(row)) {
67
+ out[field] = table.fields[field]?.type === "json" && value !== null && value !== void 0 ? JSON.stringify(value) : value;
68
+ }
69
+ return out;
70
+ };
71
+ return {
72
+ async find(table, where = {}, options = {}) {
73
+ const declaration = resolve(table);
74
+ checkFields(declaration, Object.keys(where), "where");
75
+ if (options.orderBy) checkFields(declaration, [options.orderBy.field], "orderBy");
76
+ const rows = await storage.queryTable(qualified(declaration), where, options);
77
+ return rows.map((row) => {
78
+ const mapped = {};
79
+ for (const field of Object.keys(declaration.fields)) {
80
+ const value = row[column(field)];
81
+ mapped[field] = declaration.fields[field]?.type === "json" && typeof value === "string" ? JSON.parse(value) : value;
82
+ }
83
+ return mapped;
84
+ });
85
+ },
86
+ async insert(table, rows) {
87
+ const declaration = resolve(table);
88
+ for (const row of rows) checkFields(declaration, Object.keys(row), "insert");
89
+ return storage.insertRows(
90
+ qualified(declaration),
91
+ rows.map((row) => serialize(declaration, row))
92
+ );
93
+ },
94
+ async upsert(table, rows, options) {
95
+ const declaration = resolve(table);
96
+ for (const row of rows) checkFields(declaration, Object.keys(row), "upsert");
97
+ checkFields(declaration, options.onConflict, "onConflict");
98
+ return storage.insertRows(
99
+ qualified(declaration),
100
+ rows.map((row) => serialize(declaration, row)),
101
+ options.onConflict
102
+ );
103
+ },
104
+ async update(table, where, set) {
105
+ const declaration = resolve(table);
106
+ checkFields(declaration, Object.keys(where), "where");
107
+ checkFields(declaration, Object.keys(set), "update");
108
+ return storage.updateRows(qualified(declaration), where, serialize(declaration, set));
109
+ },
110
+ async remove(table, where) {
111
+ const declaration = resolve(table);
112
+ checkFields(declaration, Object.keys(where), "where");
113
+ return storage.deleteRows(qualified(declaration), where);
114
+ }
115
+ };
116
+ }
117
+
118
+ export { ConfigError, EasyPingError, ValidationError, consoleLogger, createPluginStore, isOperator };
119
+ //# sourceMappingURL=chunk-OZSOIX23.js.map
120
+ //# sourceMappingURL=chunk-OZSOIX23.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts","../src/core/store.ts"],"names":[],"mappings":"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","file":"chunk-OZSOIX23.js","sourcesContent":["export class EasyPingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"EasyPingError\";\n }\n}\n\n/** Thrown when a payload fails its Standard Schema. Developer error. */\nexport class ValidationError extends EasyPingError {\n readonly issues: readonly { message: string; path?: string }[];\n\n constructor(type: string, issues: readonly { message: string; path?: string }[]) {\n super(`Payload for \"${type}\" failed validation: ${issues.map((i) => i.message).join(\"; \")}`);\n this.name = \"ValidationError\";\n this.issues = issues;\n }\n}\n\n/** Thrown at startup for a malformed config. Never at send time. */\nexport class ConfigError extends EasyPingError {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\nexport type Logger = {\n warn: (message: string, meta?: Record<string, unknown>) => void;\n error: (message: string, meta?: Record<string, unknown>) => void;\n};\n\nexport const consoleLogger: Logger = {\n warn: (message, meta) => console.warn(`[easy-ping] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[easy-ping] ${message}`, meta ?? \"\"),\n};\n","import { toSnakeCase } from \"../schema/declaration\";\nimport { ConfigError } from \"./errors\";\nimport type { SchemaDeclaration, TableDeclaration } from \"./plugin\";\n\nexport type Scalar = string | number | boolean | Date | null;\n\n/**\n * One operator per condition. Operators are tagged objects so a bare value is\n * always an equality test; json columns therefore cannot be filtered, since an\n * object value would be ambiguous.\n */\nexport type WhereCondition =\n | Scalar\n | { in: readonly (string | number)[] }\n | { lt: Date | number }\n | { lte: Date | number }\n | { gt: Date | number }\n | { gte: Date | number }\n | { not: Scalar };\n\nexport type WhereClause = Record<string, WhereCondition>;\n\nexport type QueryOptions = {\n limit?: number;\n orderBy?: { field: string; direction: \"asc\" | \"desc\" };\n};\n\nexport type UpsertOptions = { onConflict: readonly string[] };\n\n/**\n * Table access for plugins, scoped to the tables that plugin declared.\n *\n * Plugins could always declare tables via schema() but had no way to read\n * them, so preferences had to bolt its queries onto the core adapter. That\n * does not generalise to digests or push.\n */\nexport type PluginStore = {\n find<T = Record<string, unknown>>(\n table: string,\n where?: WhereClause,\n options?: QueryOptions,\n ): Promise<T[]>;\n insert(table: string, rows: readonly Record<string, unknown>[]): Promise<number>;\n upsert(\n table: string,\n rows: readonly Record<string, unknown>[],\n options: UpsertOptions,\n ): Promise<number>;\n update(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n remove(table: string, where: WhereClause): Promise<number>;\n};\n\nexport type TableStorage = {\n readonly naming?: \"snake_case\" | \"preserve\";\n readonly serializesJson?: boolean;\n\n queryTable(\n table: string,\n where: WhereClause,\n options: QueryOptions,\n ): Promise<Record<string, unknown>[]>;\n insertRows(\n table: string,\n rows: readonly Record<string, unknown>[],\n onConflict?: readonly string[],\n ): Promise<number>;\n updateRows(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n deleteRows(table: string, where: WhereClause): Promise<number>;\n};\n\nconst OPERATORS = [\"in\", \"lt\", \"lte\", \"gt\", \"gte\", \"not\"] as const;\n\nexport const isOperator = (value: unknown): boolean =>\n typeof value === \"object\" &&\n value !== null &&\n !(value instanceof Date) &&\n OPERATORS.some((op) => op in value);\n\n/**\n * Every table and column a plugin touches is checked against its own\n * declaration. Without this a plugin could read the notification table, and\n * unvalidated identifiers would reach the SQL builder.\n */\nexport function createPluginStore(\n pluginId: string,\n schema: SchemaDeclaration | undefined,\n storage: TableStorage,\n prefix: string,\n): PluginStore {\n const byTableName = new Map<string, TableDeclaration>();\n for (const declaration of Object.values(schema ?? {})) {\n byTableName.set(declaration.tableName, declaration);\n }\n\n function resolve(table: string): TableDeclaration {\n const declaration = byTableName.get(table);\n if (!declaration) {\n throw new ConfigError(\n `plugin \"${pluginId}\" accessed table \"${table}\", which it does not declare in schema().`,\n );\n }\n return declaration;\n }\n\n function checkFields(table: TableDeclaration, fields: Iterable<string>, context: string) {\n for (const field of fields) {\n const spec = table.fields[field];\n if (!spec) {\n throw new ConfigError(\n `plugin \"${pluginId}\" used unknown field \"${field}\" on \"${table.tableName}\" (${context}).`,\n );\n }\n if (context === \"where\" && spec.type === \"json\") {\n throw new ConfigError(\n `plugin \"${pluginId}\" cannot filter on json field \"${field}\"; ` +\n \"a bare object value is indistinguishable from an operator.\",\n );\n }\n }\n }\n\n const qualified = (table: TableDeclaration) => prefix + table.tableName;\n\n // A document store keeps the declared names; a SQL adapter wants columns.\n const column = (field: string) => (storage.naming === \"preserve\" ? field : toSnakeCase(field));\n\n /**\n * json columns must be handed to the driver as text; postgres-js cannot bind\n * a plain object and the insert fails outright.\n */\n const serialize = (table: TableDeclaration, row: Record<string, unknown>) => {\n if (storage.serializesJson === false) return row;\n\n const out: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(row)) {\n out[field] =\n table.fields[field]?.type === \"json\" && value !== null && value !== undefined\n ? JSON.stringify(value)\n : value;\n }\n return out;\n };\n\n return {\n async find(table, where = {}, options = {}) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n if (options.orderBy) checkFields(declaration, [options.orderBy.field], \"orderBy\");\n\n const rows = await storage.queryTable(qualified(declaration), where, options);\n\n // SELECT * returns snake_case columns; plugins declare camelCase fields\n // and type their reads that way. Without this every property is\n // undefined at runtime while typechecking perfectly.\n return rows.map((row) => {\n const mapped: Record<string, unknown> = {};\n for (const field of Object.keys(declaration.fields)) {\n const value = row[column(field)];\n // Drivers differ: some hand back parsed jsonb, some raw text.\n mapped[field] =\n declaration.fields[field]?.type === \"json\" && typeof value === \"string\"\n ? JSON.parse(value)\n : value;\n }\n return mapped;\n }) as never;\n },\n\n async insert(table, rows) {\n const declaration = resolve(table);\n for (const row of rows) checkFields(declaration, Object.keys(row), \"insert\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n );\n },\n\n async upsert(table, rows, options) {\n const declaration = resolve(table);\n for (const row of rows) checkFields(declaration, Object.keys(row), \"upsert\");\n checkFields(declaration, options.onConflict, \"onConflict\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n options.onConflict,\n );\n },\n\n async update(table, where, set) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n checkFields(declaration, Object.keys(set), \"update\");\n return storage.updateRows(qualified(declaration), where, serialize(declaration, set));\n },\n\n async remove(table, where) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n return storage.deleteRows(qualified(declaration), where);\n },\n };\n}\n"]}
@@ -0,0 +1,23 @@
1
+ // src/core/base64url.ts
2
+ function encodeBase64Url(input) {
3
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
4
+ let binary = "";
5
+ for (const byte of bytes) binary += String.fromCharCode(byte);
6
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7
+ }
8
+ function decodeBase64UrlBytes(value) {
9
+ try {
10
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
11
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function decodeBase64Url(value) {
17
+ const bytes = decodeBase64UrlBytes(value);
18
+ return bytes ? new TextDecoder().decode(bytes) : null;
19
+ }
20
+
21
+ export { decodeBase64Url, decodeBase64UrlBytes, encodeBase64Url };
22
+ //# sourceMappingURL=chunk-PTGHMCCG.js.map
23
+ //# sourceMappingURL=chunk-PTGHMCCG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/base64url.ts"],"names":[],"mappings":";AAEO,SAAS,gBAAgB,KAAA,EAAoC;AAClE,EAAA,MAAM,KAAA,GAAQ,OAAO,KAAA,KAAU,QAAA,GAAW,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA,GAAI,KAAA;AAC5E,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,EAAO,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAC5D,EAAA,OAAO,IAAA,CAAK,MAAM,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC/E;AAGO,SAAS,qBAAqB,KAAA,EAA+C;AAClF,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAC,CAAA;AAC/D,IAAA,OAAO,UAAA,CAAW,KAAK,MAAA,EAAQ,CAAC,SAAS,IAAA,CAAK,UAAA,CAAW,CAAC,CAAC,CAAA;AAAA,EAC7D,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,SAAS,gBAAgB,KAAA,EAA8B;AAC5D,EAAA,MAAM,KAAA,GAAQ,qBAAqB,KAAK,CAAA;AACxC,EAAA,OAAO,QAAQ,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA,GAAI,IAAA;AACnD","file":"chunk-PTGHMCCG.js","sourcesContent":["// Web APIs, not Buffer: this runs on Workers and Vercel Edge.\n\nexport function encodeBase64Url(input: string | Uint8Array): string {\n const bytes = typeof input === \"string\" ? new TextEncoder().encode(input) : input;\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n// Uint8Array<ArrayBuffer>: bare Uint8Array widens and is not a BufferSource.\nexport function decodeBase64UrlBytes(value: string): Uint8Array<ArrayBuffer> | null {\n try {\n const binary = atob(value.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n return Uint8Array.from(binary, (char) => char.charCodeAt(0));\n } catch {\n return null;\n }\n}\n\nexport function decodeBase64Url(value: string): string | null {\n const bytes = decodeBase64UrlBytes(value);\n return bytes ? new TextDecoder().decode(bytes) : null;\n}\n"]}
@@ -0,0 +1,50 @@
1
+ import { encodeBase64Url, decodeBase64UrlBytes, decodeBase64Url } from './chunk-PTGHMCCG.js';
2
+
3
+ // src/core/tokens.ts
4
+ var encoder = new TextEncoder();
5
+ async function hmacKey(secret) {
6
+ return crypto.subtle.importKey(
7
+ "raw",
8
+ encoder.encode(secret),
9
+ { name: "HMAC", hash: "SHA-256" },
10
+ false,
11
+ ["sign", "verify"]
12
+ );
13
+ }
14
+ async function signToken(secret, claims) {
15
+ const body = encodeBase64Url(JSON.stringify(claims));
16
+ const signature = await crypto.subtle.sign("HMAC", await hmacKey(secret), encoder.encode(body));
17
+ return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;
18
+ }
19
+ async function verifyToken(secret, token, purpose, now = /* @__PURE__ */ new Date()) {
20
+ const separator = token.lastIndexOf(".");
21
+ if (separator <= 0) return null;
22
+ const body = token.slice(0, separator);
23
+ const signature = decodeBase64UrlBytes(token.slice(separator + 1));
24
+ if (!signature) return null;
25
+ const valid = await crypto.subtle.verify(
26
+ "HMAC",
27
+ await hmacKey(secret),
28
+ signature,
29
+ encoder.encode(body)
30
+ );
31
+ if (!valid) return null;
32
+ const decoded = decodeBase64Url(body);
33
+ if (!decoded) return null;
34
+ let claims;
35
+ try {
36
+ claims = JSON.parse(decoded);
37
+ } catch {
38
+ return null;
39
+ }
40
+ if (typeof claims?.uid !== "string" || typeof claims.exp !== "number") return null;
41
+ if (claims.purpose !== purpose) return null;
42
+ if (claims.exp * 1e3 <= now.getTime()) return null;
43
+ return claims;
44
+ }
45
+ var DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
46
+ var expiresIn = (seconds, now = /* @__PURE__ */ new Date()) => Math.floor(now.getTime() / 1e3) + seconds;
47
+
48
+ export { DEFAULT_TOKEN_TTL_SECONDS, expiresIn, signToken, verifyToken };
49
+ //# sourceMappingURL=chunk-W3BMEPPQ.js.map
50
+ //# sourceMappingURL=chunk-W3BMEPPQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/tokens.ts"],"names":[],"mappings":";;;AAaA,IAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAEhC,eAAe,QAAQ,MAAA,EAAoC;AACzD,EAAA,OAAO,OAAO,MAAA,CAAO,SAAA;AAAA,IACnB,KAAA;AAAA,IACA,OAAA,CAAQ,OAAO,MAAM,CAAA;AAAA,IACrB,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,IAChC,KAAA;AAAA,IACA,CAAC,QAAQ,QAAQ;AAAA,GACnB;AACF;AAEA,eAAsB,SAAA,CAAU,QAAgB,MAAA,EAAsC;AACpF,EAAA,MAAM,IAAA,GAAO,eAAA,CAAgB,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,CAAA;AACnD,EAAA,MAAM,SAAA,GAAY,MAAM,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,MAAA,EAAQ,MAAM,OAAA,CAAQ,MAAM,CAAA,EAAG,OAAA,CAAQ,MAAA,CAAO,IAAI,CAAC,CAAA;AAC9F,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,eAAA,CAAgB,IAAI,UAAA,CAAW,SAAS,CAAC,CAAC,CAAA,CAAA;AAC9D;AAGA,eAAsB,YACpB,MAAA,EACA,KAAA,EACA,SACA,GAAA,mBAAY,IAAI,MAAK,EACQ;AAC7B,EAAA,MAAM,SAAA,GAAY,KAAA,CAAM,WAAA,CAAY,GAAG,CAAA;AACvC,EAAA,IAAI,SAAA,IAAa,GAAG,OAAO,IAAA;AAE3B,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA;AACrC,EAAA,MAAM,YAAY,oBAAA,CAAqB,KAAA,CAAM,KAAA,CAAM,SAAA,GAAY,CAAC,CAAC,CAAA;AACjE,EAAA,IAAI,CAAC,WAAW,OAAO,IAAA;AAGvB,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA;AAAA,IAChC,MAAA;AAAA,IACA,MAAM,QAAQ,MAAM,CAAA;AAAA,IACpB,SAAA;AAAA,IACA,OAAA,CAAQ,OAAO,IAAI;AAAA,GACrB;AACA,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AAEnB,EAAA,MAAM,OAAA,GAAU,gBAAgB,IAAI,CAAA;AACpC,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AAErB,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,OAAO,QAAQ,GAAA,KAAQ,QAAA,IAAY,OAAO,MAAA,CAAO,GAAA,KAAQ,UAAU,OAAO,IAAA;AAE9E,EAAA,IAAI,MAAA,CAAO,OAAA,KAAY,OAAA,EAAS,OAAO,IAAA;AACvC,EAAA,IAAI,OAAO,GAAA,GAAM,GAAA,IAAQ,GAAA,CAAI,OAAA,IAAW,OAAO,IAAA;AAE/C,EAAA,OAAO,MAAA;AACT;AAEO,IAAM,yBAAA,GAA4B,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;AAEjD,IAAM,SAAA,GAAY,CAAC,OAAA,EAAiB,GAAA,mBAAY,IAAI,IAAA,EAAK,KAC9D,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,OAAA,EAAQ,GAAI,GAAI,CAAA,GAAI","file":"chunk-W3BMEPPQ.js","sourcesContent":["import { decodeBase64Url, decodeBase64UrlBytes, encodeBase64Url } from \"./base64url\";\n\nexport type TokenClaims = {\n /** Subject — the user the token acts for. */\n uid: string;\n /** What the token is allowed to do. Verification requires an exact match. */\n purpose: string;\n /** Expiry, epoch seconds. */\n exp: number;\n /** Free-form, purpose-specific. */\n data?: Record<string, string>;\n};\n\nconst encoder = new TextEncoder();\n\nasync function hmacKey(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\", \"verify\"],\n );\n}\n\nexport async function signToken(secret: string, claims: TokenClaims): Promise<string> {\n const body = encodeBase64Url(JSON.stringify(claims));\n const signature = await crypto.subtle.sign(\"HMAC\", await hmacKey(secret), encoder.encode(body));\n return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;\n}\n\n/** null for every failure mode. Distinguishing them tells an attacker which part to fix. */\nexport async function verifyToken(\n secret: string,\n token: string,\n purpose: string,\n now: Date = new Date(),\n): Promise<TokenClaims | null> {\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) return null;\n\n const body = token.slice(0, separator);\n const signature = decodeBase64UrlBytes(token.slice(separator + 1));\n if (!signature) return null;\n\n // crypto.subtle.verify is constant-time.\n const valid = await crypto.subtle.verify(\n \"HMAC\",\n await hmacKey(secret),\n signature,\n encoder.encode(body),\n );\n if (!valid) return null;\n\n const decoded = decodeBase64Url(body);\n if (!decoded) return null;\n\n let claims: TokenClaims;\n try {\n claims = JSON.parse(decoded) as TokenClaims;\n } catch {\n return null;\n }\n\n if (typeof claims?.uid !== \"string\" || typeof claims.exp !== \"number\") return null;\n // Scoped so an unsubscribe token can never be replayed against another route.\n if (claims.purpose !== purpose) return null;\n if (claims.exp * 1000 <= now.getTime()) return null;\n\n return claims;\n}\n\nexport const DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;\n\nexport const expiresIn = (seconds: number, now: Date = new Date()): number =>\n Math.floor(now.getTime() / 1000) + seconds;\n"]}
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ // src/client/index.ts
4
+ var INITIAL = {
5
+ notifications: [],
6
+ unseenCount: 0,
7
+ unreadCount: 0,
8
+ nextCursor: null,
9
+ isLoading: true,
10
+ error: null
11
+ };
12
+ var countUnread = (rows) => rows.reduce((total, row) => row.readAt === null ? total + 1 : total, 0);
13
+ function createNotifyClient(options = {}) {
14
+ const baseUrl = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
15
+ const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));
16
+ const limit = options.limit ?? 20;
17
+ const pollIntervalMs = options.pollIntervalMs ?? 15e3;
18
+ const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 6e4;
19
+ const isHidden = options.isDocumentHidden ?? (() => typeof document !== "undefined" && document.visibilityState === "hidden");
20
+ let state = INITIAL;
21
+ const listeners = /* @__PURE__ */ new Set();
22
+ let timer;
23
+ let consecutiveFailures = 0;
24
+ let generation = 0;
25
+ function set(patch) {
26
+ state = { ...state, ...patch };
27
+ state = { ...state, unreadCount: countUnread(state.notifications) };
28
+ for (const listener of listeners) listener(state);
29
+ }
30
+ async function request(path, init) {
31
+ const response = await doFetch(`${baseUrl}${path}`, {
32
+ ...init,
33
+ headers: { "content-type": "application/json", ...init?.headers }
34
+ });
35
+ if (!response.ok)
36
+ throw new Error(`${init?.method ?? "GET"} ${path} failed: ${response.status}`);
37
+ return await response.json();
38
+ }
39
+ async function refresh() {
40
+ const startedAt = generation;
41
+ const [page, count] = await Promise.all([
42
+ request(`/?limit=${limit}`),
43
+ request("/count")
44
+ ]);
45
+ if (generation !== startedAt) return;
46
+ set({
47
+ notifications: page.notifications,
48
+ nextCursor: page.nextCursor,
49
+ unseenCount: count.unseen,
50
+ isLoading: false,
51
+ error: null
52
+ });
53
+ }
54
+ let loadingMore;
55
+ async function loadMore() {
56
+ if (!state.nextCursor) return;
57
+ if (loadingMore) return loadingMore;
58
+ const cursor = state.nextCursor;
59
+ loadingMore = (async () => {
60
+ try {
61
+ const page = await request(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);
62
+ const known = new Set(state.notifications.map((row) => row.id));
63
+ set({
64
+ notifications: [
65
+ ...state.notifications,
66
+ ...page.notifications.filter((row) => !known.has(row.id))
67
+ ],
68
+ nextCursor: page.nextCursor
69
+ });
70
+ } finally {
71
+ loadingMore = void 0;
72
+ }
73
+ })();
74
+ return loadingMore;
75
+ }
76
+ async function optimistic(apply, patch, send) {
77
+ const previous = state;
78
+ generation += 1;
79
+ set({ notifications: apply(state.notifications), ...patch });
80
+ try {
81
+ await send();
82
+ } catch (error) {
83
+ generation += 1;
84
+ state = previous;
85
+ for (const listener of listeners) listener(state);
86
+ throw error;
87
+ }
88
+ }
89
+ const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
90
+ async function markAsRead(id) {
91
+ await optimistic(
92
+ (rows) => rows.map((row) => row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row),
93
+ {},
94
+ () => request("/read", { method: "POST", body: JSON.stringify({ ids: [id] }) })
95
+ );
96
+ }
97
+ async function markAllRead() {
98
+ await optimistic(
99
+ (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),
100
+ {},
101
+ () => request("/read-all", { method: "POST" })
102
+ );
103
+ }
104
+ async function markSeen() {
105
+ const newest = state.notifications[0]?.createdAt;
106
+ await optimistic(
107
+ (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),
108
+ { unseenCount: 0 },
109
+ () => request("/seen", {
110
+ method: "POST",
111
+ body: JSON.stringify(newest ? { before: newest } : {})
112
+ })
113
+ );
114
+ }
115
+ function schedule(delay) {
116
+ clearTimeout(timer);
117
+ timer = setTimeout(tick, delay);
118
+ }
119
+ async function tick() {
120
+ if (isHidden()) {
121
+ schedule(pollIntervalMs);
122
+ return;
123
+ }
124
+ try {
125
+ await refresh();
126
+ consecutiveFailures = 0;
127
+ } catch (error) {
128
+ consecutiveFailures += 1;
129
+ set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });
130
+ }
131
+ schedule(
132
+ consecutiveFailures === 0 ? pollIntervalMs : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs)
133
+ );
134
+ }
135
+ function subscribe(listener) {
136
+ listeners.add(listener);
137
+ listener(state);
138
+ if (listeners.size === 1) void tick();
139
+ return () => {
140
+ listeners.delete(listener);
141
+ if (listeners.size === 0) {
142
+ clearTimeout(timer);
143
+ timer = void 0;
144
+ }
145
+ };
146
+ }
147
+ return {
148
+ subscribe,
149
+ refresh,
150
+ loadMore,
151
+ markAsRead,
152
+ markAllRead,
153
+ markSeen,
154
+ getState: () => state
155
+ };
156
+ }
157
+
158
+ exports.createNotifyClient = createNotifyClient;
159
+ //# sourceMappingURL=client.cjs.map
160
+ //# sourceMappingURL=client.cjs.map