easy-ping 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +15 -4
  2. package/dist/{adapter-f0OR2DhY.d.cts → adapter-BfLe2J0t.d.cts} +10 -0
  3. package/dist/{adapter-f0OR2DhY.d.ts → adapter-BfLe2J0t.d.ts} +10 -0
  4. package/dist/adapters/drizzle.cjs +9 -4
  5. package/dist/adapters/drizzle.cjs.map +1 -1
  6. package/dist/adapters/drizzle.d.cts +35 -1
  7. package/dist/adapters/drizzle.d.ts +35 -1
  8. package/dist/adapters/drizzle.js +10 -5
  9. package/dist/adapters/drizzle.js.map +1 -1
  10. package/dist/adapters/mongodb.cjs +9 -4
  11. package/dist/adapters/mongodb.cjs.map +1 -1
  12. package/dist/adapters/mongodb.d.cts +2 -2
  13. package/dist/adapters/mongodb.d.ts +2 -2
  14. package/dist/adapters/mongodb.js +8 -4
  15. package/dist/adapters/mongodb.js.map +1 -1
  16. package/dist/adapters/postgres.cjs +136 -3
  17. package/dist/adapters/postgres.cjs.map +1 -1
  18. package/dist/adapters/postgres.d.cts +45 -2
  19. package/dist/adapters/postgres.d.ts +45 -2
  20. package/dist/adapters/postgres.js +38 -5
  21. package/dist/adapters/postgres.js.map +1 -1
  22. package/dist/{chunk-OZSOIX23.js → chunk-3Y2MBUAW.js} +53 -12
  23. package/dist/chunk-3Y2MBUAW.js.map +1 -0
  24. package/dist/chunk-CYKAM7LE.js +226 -0
  25. package/dist/chunk-CYKAM7LE.js.map +1 -0
  26. package/dist/chunk-DG3ZL5NP.js +315 -0
  27. package/dist/chunk-DG3ZL5NP.js.map +1 -0
  28. package/dist/{chunk-NYDVNMKG.js → chunk-PSJHXZPR.js} +4 -3
  29. package/dist/chunk-PSJHXZPR.js.map +1 -0
  30. package/dist/{chunk-BV3U542P.js → chunk-W6I3B2CI.js} +2 -2
  31. package/dist/chunk-W6I3B2CI.js.map +1 -0
  32. package/dist/chunk-YEMRRMJQ.js +199 -0
  33. package/dist/chunk-YEMRRMJQ.js.map +1 -0
  34. package/dist/client.cjs +3 -5
  35. package/dist/client.cjs.map +1 -1
  36. package/dist/client.js +3 -5
  37. package/dist/client.js.map +1 -1
  38. package/dist/index.cjs +377 -73
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +88 -22
  41. package/dist/index.d.ts +88 -22
  42. package/dist/index.js +119 -351
  43. package/dist/index.js.map +1 -1
  44. package/dist/node.cjs +27 -6
  45. package/dist/node.cjs.map +1 -1
  46. package/dist/node.d.cts +8 -2
  47. package/dist/node.d.ts +8 -2
  48. package/dist/node.js +26 -6
  49. package/dist/node.js.map +1 -1
  50. package/dist/{plugin-Bqj9xAOS.d.cts → plugin-C9Hoxus2.d.ts} +38 -10
  51. package/dist/{plugin-bTJKmnlq.d.ts → plugin-CiMsGlQe.d.cts} +38 -10
  52. package/dist/plugins/digests.d.cts +2 -2
  53. package/dist/plugins/digests.d.ts +2 -2
  54. package/dist/plugins/preferences-client.cjs.map +1 -1
  55. package/dist/plugins/preferences-client.d.cts +2 -2
  56. package/dist/plugins/preferences-client.d.ts +2 -2
  57. package/dist/plugins/preferences-client.js +1 -1
  58. package/dist/plugins/preferences.cjs +100 -15
  59. package/dist/plugins/preferences.cjs.map +1 -1
  60. package/dist/plugins/preferences.d.cts +4 -2
  61. package/dist/plugins/preferences.d.ts +4 -2
  62. package/dist/plugins/preferences.js +35 -7
  63. package/dist/plugins/preferences.js.map +1 -1
  64. package/dist/plugins/push.cjs +222 -61
  65. package/dist/plugins/push.cjs.map +1 -1
  66. package/dist/plugins/push.d.cts +81 -3
  67. package/dist/plugins/push.d.ts +81 -3
  68. package/dist/plugins/push.js +164 -62
  69. package/dist/plugins/push.js.map +1 -1
  70. package/dist/providers/web-push.cjs +37 -11
  71. package/dist/providers/web-push.cjs.map +1 -1
  72. package/dist/providers/web-push.d.cts +9 -3
  73. package/dist/providers/web-push.d.ts +9 -3
  74. package/dist/providers/web-push.js +37 -12
  75. package/dist/providers/web-push.js.map +1 -1
  76. package/dist/react.cjs +3 -5
  77. package/dist/react.cjs.map +1 -1
  78. package/dist/react.js +3 -5
  79. package/dist/react.js.map +1 -1
  80. package/dist/schema.cjs +2 -1
  81. package/dist/schema.cjs.map +1 -1
  82. package/dist/schema.d.cts +7 -2
  83. package/dist/schema.d.ts +7 -2
  84. package/dist/schema.js +2 -198
  85. package/dist/schema.js.map +1 -1
  86. package/dist/testing.cjs +29 -0
  87. package/dist/testing.cjs.map +1 -1
  88. package/dist/testing.d.cts +1 -1
  89. package/dist/testing.d.ts +1 -1
  90. package/dist/testing.js +29 -0
  91. package/dist/testing.js.map +1 -1
  92. package/package.json +1 -1
  93. package/dist/chunk-BV3U542P.js.map +0 -1
  94. package/dist/chunk-NYDVNMKG.js.map +0 -1
  95. package/dist/chunk-OZSOIX23.js.map +0 -1
  96. package/dist/chunk-W3BMEPPQ.js +0 -50
  97. package/dist/chunk-W3BMEPPQ.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/base64url.ts","../../src/providers/web-push/crypto.ts","../../src/providers/web-push/index.ts"],"names":[],"mappings":"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","file":"web-push.cjs","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","import { decodeBase64UrlBytes, encodeBase64Url } from \"../../core/base64url\";\n\n/**\n * VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291 over RFC 8188),\n * on Web Crypto.\n *\n * The `web-push` package is node:crypto only, so it cannot run on Cloudflare\n * Workers or Vercel Edge — the runtimes this library targets. Everything here\n * uses crypto.subtle and therefore runs anywhere the rest of the package does.\n */\n\nconst utf8 = (value: string) => new TextEncoder().encode(value);\n\nfunction concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nfunction requireBytes(value: string, label: string, length?: number): Uint8Array<ArrayBuffer> {\n const bytes = decodeBase64UrlBytes(value);\n if (!bytes) throw new Error(`${label} is not valid base64url`);\n if (length !== undefined && bytes.length !== length) {\n throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);\n }\n return bytes;\n}\n\n/** Web Crypto's HKDF does extract and expand in one call. */\nasync function hkdf(\n // Uint8Array<ArrayBuffer>, not bare Uint8Array: the latter widens to\n // ArrayBufferLike, which Web Crypto will not accept as a BufferSource.\n salt: Uint8Array<ArrayBuffer>,\n ikm: Uint8Array<ArrayBuffer>,\n info: Uint8Array<ArrayBuffer>,\n length: number,\n): Promise<Uint8Array<ArrayBuffer>> {\n const key = await crypto.subtle.importKey(\"raw\", ikm, \"HKDF\", false, [\"deriveBits\"]);\n const bits = await crypto.subtle.deriveBits(\n { name: \"HKDF\", hash: \"SHA-256\", salt, info },\n key,\n length * 8,\n );\n return new Uint8Array(bits);\n}\n\nexport const RECORD_SIZE = 4096;\n/** One record: record size less the padding delimiter and the GCM tag. */\nexport const MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;\n\nexport type EncryptedPush = {\n body: Uint8Array<ArrayBuffer>;\n salt: Uint8Array<ArrayBuffer>;\n serverPublicKey: Uint8Array<ArrayBuffer>;\n};\n\n/**\n * RFC 8291 §3.4 then RFC 8188 §2.\n *\n * `salt` and `serverKeys` are injectable so tests can pin them; production\n * always generates fresh ones, and reusing a salt with the same key would be\n * catastrophic for GCM.\n */\nexport async function encryptPayload(\n plaintext: Uint8Array<ArrayBuffer>,\n userAgentPublicKey: string,\n authSecret: string,\n overrides?: { salt?: Uint8Array<ArrayBuffer>; serverKeys?: CryptoKeyPair },\n): Promise<EncryptedPush> {\n if (plaintext.length > MAX_PAYLOAD_BYTES) {\n throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);\n }\n\n const uaPublic = requireBytes(userAgentPublicKey, \"p256dh\", 65);\n const auth = requireBytes(authSecret, \"auth\", 16);\n\n const salt = overrides?.salt\n ? new Uint8Array(overrides.salt)\n : crypto.getRandomValues(new Uint8Array(16));\n\n const serverKeys =\n overrides?.serverKeys ??\n (await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\"deriveBits\"]));\n\n const serverPublic = new Uint8Array(\n await crypto.subtle.exportKey(\"raw\", serverKeys.publicKey),\n ) as Uint8Array<ArrayBuffer>;\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n uaPublic,\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, serverKeys.privateKey, 256),\n );\n\n // The receiver's key comes first; swapping the order yields a key the\n // browser cannot derive, and the push simply never decrypts.\n const keyInfo = concat(utf8(\"WebPush: info\"), new Uint8Array([0]), uaPublic, serverPublic);\n const ikm = await hkdf(auth, shared, keyInfo, 32);\n\n const cek = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: aes128gcm\"), new Uint8Array([0])),\n 16,\n );\n const nonce = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: nonce\"), new Uint8Array([0])),\n 12,\n );\n\n const aesKey = await crypto.subtle.importKey(\"raw\", cek, \"AES-GCM\", false, [\"encrypt\"]);\n\n // 0x02 marks the final record. 0x01 would mean \"more records follow\" and the\n // receiver would wait for one that never arrives.\n const padded = concat(plaintext, new Uint8Array([2]));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-GCM\", iv: nonce }, aesKey, padded),\n );\n\n // RFC 8188 header: salt(16) | rs(4, big endian) | idlen(1) | keyid\n const header = new Uint8Array(21 + serverPublic.length);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);\n header[20] = serverPublic.length;\n header.set(serverPublic, 21);\n\n return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };\n}\n\nexport type VapidKeys = { publicKey: string; privateKey: string };\n\n/** A VAPID keypair in the base64url form every push library uses. */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const keys = await crypto.subtle.generateKey({ name: \"ECDSA\", namedCurve: \"P-256\" }, true, [\n \"sign\",\n \"verify\",\n ]);\n\n const publicKey = new Uint8Array(await crypto.subtle.exportKey(\"raw\", keys.publicKey));\n const jwk = await crypto.subtle.exportKey(\"jwk\", keys.privateKey);\n\n if (!jwk.d) throw new Error(\"generated key has no private component\");\n return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };\n}\n\n/**\n * VAPID keys are distributed as a raw public point plus a bare scalar, but Web\n * Crypto will only import a private EC key as JWK — which needs x and y. They\n * are recovered by slicing the uncompressed public point.\n */\nasync function importVapidKey(keys: VapidKeys): Promise<CryptoKey> {\n const publicKey = requireBytes(keys.publicKey, \"vapid publicKey\", 65);\n if (publicKey[0] !== 0x04) throw new Error(\"vapid publicKey must be an uncompressed EC point\");\n\n return crypto.subtle.importKey(\n \"jwk\",\n {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.slice(1, 33)),\n y: encodeBase64Url(publicKey.slice(33, 65)),\n d: keys.privateKey,\n ext: true,\n },\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n}\n\nexport type VapidOptions = {\n subject: string;\n keys: VapidKeys;\n /** Injectable for tests; VAPID tokens are short-lived. */\n now?: Date;\n ttlSeconds?: number;\n};\n\n/** RFC 8292 §2: an ES256 JWT scoped to the push service origin. */\nexport async function vapidAuthorization(endpoint: string, options: VapidOptions): Promise<string> {\n if (!/^(mailto:|https:)/.test(options.subject)) {\n throw new Error('vapid subject must be a \"mailto:\" or \"https:\" URL');\n }\n\n const now = options.now ?? new Date();\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const claims = {\n // Origin only: a token scoped to the full endpoint is rejected.\n aud: new URL(endpoint).origin,\n exp: Math.floor(now.getTime() / 1000) + (options.ttlSeconds ?? 12 * 3600),\n sub: options.subject,\n };\n\n const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(\n JSON.stringify(claims),\n )}`;\n\n const signature = await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n await importVapidKey(options.keys),\n utf8(signingInput),\n );\n\n // Web Crypto emits raw r||s, which is what JWS ES256 wants — no DER unwrap.\n const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;\n return `vapid t=${jwt}, k=${options.keys.publicKey}`;\n}\n","import type { PushMessage, PushProvider, PushSendResult } from \"../../plugins/push\";\nimport { encryptPayload, MAX_PAYLOAD_BYTES, type VapidKeys, vapidAuthorization } from \"./crypto\";\n\nexport type WebPushOptions = {\n /** \"mailto:you@example.com\" or an https URL. Push services require it. */\n subject: string;\n vapid: VapidKeys;\n /** How long the service should hold an undelivered message. */\n ttlSeconds?: number;\n urgency?: \"very-low\" | \"low\" | \"normal\" | \"high\";\n fetch?: typeof globalThis.fetch;\n};\n\n/**\n * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.\n *\n * Endpoints that report themselves gone are surfaced as `expired`, which the\n * push plugin turns into an immediate row deletion. Everything transient is\n * `retryable`; anything the push service will reject identically forever is\n * neither, so it fails on the first attempt.\n */\nexport function webPush(options: WebPushOptions): PushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const ttl = options.ttlSeconds ?? 12 * 3600;\n\n return {\n name: \"web-push\",\n\n async send(message: PushMessage): Promise<PushSendResult> {\n const payload = new TextEncoder().encode(\n JSON.stringify({\n title: message.title,\n body: message.body,\n data: message.data ?? {},\n }),\n );\n\n if (payload.length > MAX_PAYLOAD_BYTES) {\n // Trimming silently would deliver a truncated notification; a bigger\n // payload will never fit, so retrying cannot help either.\n throw new Error(\n `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`,\n );\n }\n\n const encrypted = await encryptPayload(\n payload,\n message.subscription.keys.p256dh,\n message.subscription.keys.auth,\n );\n\n const authorization = await vapidAuthorization(message.subscription.endpoint, {\n subject: options.subject,\n keys: options.vapid,\n });\n\n let response: Response;\n try {\n response = await doFetch(message.subscription.endpoint, {\n method: \"POST\",\n headers: {\n Authorization: authorization,\n \"Content-Encoding\": \"aes128gcm\",\n \"Content-Type\": \"application/octet-stream\",\n TTL: String(ttl),\n ...(options.urgency ? { Urgency: options.urgency } : {}),\n },\n body: encrypted.body,\n ...(message.signal ? { signal: message.signal } : {}),\n });\n } catch (error) {\n // Never reached the service: DNS, socket, timeout.\n throw new Error(error instanceof Error ? error.message : String(error));\n }\n\n // 201 is the documented success; 200/202 appear in the wild.\n if (response.ok) return {};\n\n // The subscription is permanently gone and must be pruned.\n if (response.status === 404 || response.status === 410) return { expired: true };\n\n if (response.status === 429 || response.status === 408 || response.status >= 500) {\n return { retryable: true };\n }\n\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : \"\"}`,\n );\n },\n };\n}\n\nexport type { VapidKeys } from \"./crypto\";\nexport { generateVapidKeys, MAX_PAYLOAD_BYTES } from \"./crypto\";\n"]}
1
+ {"version":3,"sources":["../../src/core/base64url.ts","../../src/providers/web-push/crypto.ts","../../src/providers/web-push/index.ts"],"names":[],"mappings":"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","file":"web-push.cjs","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","import { decodeBase64UrlBytes, encodeBase64Url } from \"../../core/base64url\";\n\n/**\n * VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291 over RFC 8188),\n * on Web Crypto.\n *\n * The `web-push` package is node:crypto only, so it cannot run on Cloudflare\n * Workers or Vercel Edge — the runtimes this library targets. Everything here\n * uses crypto.subtle and therefore runs anywhere the rest of the package does.\n */\n\nconst utf8 = (value: string) => new TextEncoder().encode(value);\n\nfunction concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\n/** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */\nexport class InvalidSubscriptionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidSubscriptionError\";\n }\n}\n\nfunction requireBytes(\n value: string,\n label: string,\n length?: number,\n Failure: new (message: string) => Error = Error,\n): Uint8Array<ArrayBuffer> {\n const bytes = decodeBase64UrlBytes(value);\n if (!bytes) throw new Failure(`${label} is not valid base64url`);\n if (length !== undefined && bytes.length !== length) {\n throw new Failure(`${label} must be ${length} bytes, got ${bytes.length}`);\n }\n return bytes;\n}\n\n/** Web Crypto's HKDF does extract and expand in one call. */\nasync function hkdf(\n // Uint8Array<ArrayBuffer>, not bare Uint8Array: the latter widens to\n // ArrayBufferLike, which Web Crypto will not accept as a BufferSource.\n salt: Uint8Array<ArrayBuffer>,\n ikm: Uint8Array<ArrayBuffer>,\n info: Uint8Array<ArrayBuffer>,\n length: number,\n): Promise<Uint8Array<ArrayBuffer>> {\n const key = await crypto.subtle.importKey(\"raw\", ikm, \"HKDF\", false, [\"deriveBits\"]);\n const bits = await crypto.subtle.deriveBits(\n { name: \"HKDF\", hash: \"SHA-256\", salt, info },\n key,\n length * 8,\n );\n return new Uint8Array(bits);\n}\n\nexport const RECORD_SIZE = 4096;\n/** One record: record size less the padding delimiter and the GCM tag. */\nexport const MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;\n\nexport type EncryptedPush = {\n body: Uint8Array<ArrayBuffer>;\n salt: Uint8Array<ArrayBuffer>;\n serverPublicKey: Uint8Array<ArrayBuffer>;\n};\n\n/**\n * RFC 8291 §3.4 then RFC 8188 §2.\n *\n * `salt` and `serverKeys` are injectable so tests can pin them; production\n * always generates fresh ones, and reusing a salt with the same key would be\n * catastrophic for GCM.\n */\nexport async function encryptPayload(\n plaintext: Uint8Array<ArrayBuffer>,\n userAgentPublicKey: string,\n authSecret: string,\n overrides?: { salt?: Uint8Array<ArrayBuffer>; serverKeys?: CryptoKeyPair },\n): Promise<EncryptedPush> {\n if (plaintext.length > MAX_PAYLOAD_BYTES) {\n throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);\n }\n\n const uaPublic = requireBytes(userAgentPublicKey, \"p256dh\", 65, InvalidSubscriptionError);\n const auth = requireBytes(authSecret, \"auth\", 16, InvalidSubscriptionError);\n if (uaPublic[0] !== 0x04) {\n throw new InvalidSubscriptionError(\"p256dh must be an uncompressed EC point\");\n }\n\n const salt = overrides?.salt\n ? new Uint8Array(overrides.salt)\n : crypto.getRandomValues(new Uint8Array(16));\n\n const serverKeys =\n overrides?.serverKeys ??\n (await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\"deriveBits\"]));\n\n const serverPublic = new Uint8Array(\n await crypto.subtle.exportKey(\"raw\", serverKeys.publicKey),\n ) as Uint8Array<ArrayBuffer>;\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n uaPublic,\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, serverKeys.privateKey, 256),\n );\n\n // The receiver's key comes first; swapping the order yields a key the\n // browser cannot derive, and the push simply never decrypts.\n const keyInfo = concat(utf8(\"WebPush: info\"), new Uint8Array([0]), uaPublic, serverPublic);\n const ikm = await hkdf(auth, shared, keyInfo, 32);\n\n const cek = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: aes128gcm\"), new Uint8Array([0])),\n 16,\n );\n const nonce = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: nonce\"), new Uint8Array([0])),\n 12,\n );\n\n const aesKey = await crypto.subtle.importKey(\"raw\", cek, \"AES-GCM\", false, [\"encrypt\"]);\n\n // 0x02 marks the final record. 0x01 would mean \"more records follow\" and the\n // receiver would wait for one that never arrives.\n const padded = concat(plaintext, new Uint8Array([2]));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-GCM\", iv: nonce }, aesKey, padded),\n );\n\n // RFC 8188 header: salt(16) | rs(4, big endian) | idlen(1) | keyid\n const header = new Uint8Array(21 + serverPublic.length);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);\n header[20] = serverPublic.length;\n header.set(serverPublic, 21);\n\n return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };\n}\n\nexport type VapidKeys = { publicKey: string; privateKey: string };\n\n/** A VAPID keypair in the base64url form every push library uses. */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const keys = await crypto.subtle.generateKey({ name: \"ECDSA\", namedCurve: \"P-256\" }, true, [\n \"sign\",\n \"verify\",\n ]);\n\n const publicKey = new Uint8Array(await crypto.subtle.exportKey(\"raw\", keys.publicKey));\n const jwk = await crypto.subtle.exportKey(\"jwk\", keys.privateKey);\n\n if (!jwk.d) throw new Error(\"generated key has no private component\");\n return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };\n}\n\n/**\n * VAPID keys are distributed as a raw public point plus a bare scalar, but Web\n * Crypto will only import a private EC key as JWK — which needs x and y. They\n * are recovered by slicing the uncompressed public point.\n */\nasync function importVapidKey(keys: VapidKeys): Promise<CryptoKey> {\n const publicKey = requireBytes(keys.publicKey, \"vapid publicKey\", 65);\n if (publicKey[0] !== 0x04) throw new Error(\"vapid publicKey must be an uncompressed EC point\");\n\n return crypto.subtle.importKey(\n \"jwk\",\n {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.slice(1, 33)),\n y: encodeBase64Url(publicKey.slice(33, 65)),\n d: keys.privateKey,\n ext: true,\n },\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n}\n\nexport type VapidOptions = {\n subject: string;\n keys: VapidKeys;\n /** Injectable for tests; VAPID tokens are short-lived. */\n now?: Date;\n ttlSeconds?: number;\n};\n\n/** RFC 8292 §2: an ES256 JWT scoped to the push service origin. */\nexport async function vapidAuthorization(endpoint: string, options: VapidOptions): Promise<string> {\n if (!/^(mailto:|https:)/.test(options.subject)) {\n throw new Error('vapid subject must be a \"mailto:\" or \"https:\" URL');\n }\n\n const now = options.now ?? new Date();\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const claims = {\n // Origin only: a token scoped to the full endpoint is rejected.\n aud: new URL(endpoint).origin,\n exp: Math.floor(now.getTime() / 1000) + (options.ttlSeconds ?? 12 * 3600),\n sub: options.subject,\n };\n\n const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(\n JSON.stringify(claims),\n )}`;\n\n const signature = await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n await importVapidKey(options.keys),\n utf8(signingInput),\n );\n\n // Web Crypto emits raw r||s, which is what JWS ES256 wants — no DER unwrap.\n const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;\n return `vapid t=${jwt}, k=${options.keys.publicKey}`;\n}\n","import type { PushMessage, PushProvider, PushSendResult } from \"../../plugins/push\";\nimport {\n encryptPayload,\n InvalidSubscriptionError,\n MAX_PAYLOAD_BYTES,\n type VapidKeys,\n vapidAuthorization,\n} from \"./crypto\";\n\nexport type WebPushOptions = {\n /** \"mailto:you@example.com\" or an https URL. Push services require it. */\n subject: string;\n vapid: VapidKeys;\n /** How long the service should hold an undelivered message. */\n ttlSeconds?: number;\n urgency?: \"very-low\" | \"low\" | \"normal\" | \"high\";\n fetch?: typeof globalThis.fetch;\n /** Lets an http endpoint through. For a local fake push service only; never in production. */\n allowInsecureEndpoints?: boolean;\n};\n\n/**\n * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.\n *\n * Endpoints that report themselves gone are surfaced as `expired`, which the\n * push plugin turns into an immediate row deletion. Everything transient is\n * `retryable`; anything the push service will reject identically forever is\n * neither, so it fails on the first attempt.\n */\nexport function webPush(options: WebPushOptions): PushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const ttl = options.ttlSeconds ?? 12 * 3600;\n\n return {\n name: \"web-push\",\n\n async send(message: PushMessage): Promise<PushSendResult> {\n const payload = new TextEncoder().encode(\n JSON.stringify({\n title: message.title,\n body: message.body,\n data: message.data ?? {},\n }),\n );\n\n if (payload.length > MAX_PAYLOAD_BYTES) {\n // Trimming silently would deliver a truncated notification; a bigger\n // payload will never fit, so retrying cannot help either.\n throw new Error(\n `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`,\n );\n }\n\n // A subscription that cannot be encrypted for, or whose endpoint is not\n // an https URL, will never deliver. Report it so the row is pruned\n // instead of retried five times.\n let endpoint: URL;\n try {\n endpoint = new URL(message.subscription.endpoint);\n } catch {\n return { invalid: true };\n }\n const secure = endpoint.protocol === \"https:\";\n if (!secure && !(options.allowInsecureEndpoints && endpoint.protocol === \"http:\")) {\n return { invalid: true };\n }\n\n let encrypted: Awaited<ReturnType<typeof encryptPayload>>;\n try {\n encrypted = await encryptPayload(\n payload,\n message.subscription.keys.p256dh,\n message.subscription.keys.auth,\n );\n } catch (error) {\n if (error instanceof InvalidSubscriptionError) return { invalid: true };\n throw error;\n }\n\n const authorization = await vapidAuthorization(endpoint.href, {\n subject: options.subject,\n keys: options.vapid,\n });\n\n let response: Response;\n try {\n response = await doFetch(message.subscription.endpoint, {\n method: \"POST\",\n headers: {\n Authorization: authorization,\n \"Content-Encoding\": \"aes128gcm\",\n \"Content-Type\": \"application/octet-stream\",\n TTL: String(ttl),\n ...(options.urgency ? { Urgency: options.urgency } : {}),\n },\n body: encrypted.body,\n ...(message.signal ? { signal: message.signal } : {}),\n });\n } catch (error) {\n // Never reached the service: DNS, socket, timeout.\n throw new Error(error instanceof Error ? error.message : String(error));\n }\n\n // 201 is the documented success; 200/202 appear in the wild.\n if (response.ok) return {};\n\n // The subscription is permanently gone and must be pruned.\n if (response.status === 404 || response.status === 410) return { expired: true };\n\n if (response.status === 429 || response.status === 408 || response.status >= 500) {\n return { retryable: true };\n }\n\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : \"\"}`,\n );\n },\n };\n}\n\nexport type { VapidKeys } from \"./crypto\";\nexport { generateVapidKeys, InvalidSubscriptionError, MAX_PAYLOAD_BYTES } from \"./crypto\";\n"]}
@@ -1,7 +1,11 @@
1
1
  import { PushProvider } from '../plugins/push.cjs';
2
- import '../plugin-Bqj9xAOS.cjs';
3
- import '../adapter-f0OR2DhY.cjs';
2
+ import '../plugin-CiMsGlQe.cjs';
3
+ import '../adapter-BfLe2J0t.cjs';
4
4
 
5
+ /** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */
6
+ declare class InvalidSubscriptionError extends Error {
7
+ constructor(message: string);
8
+ }
5
9
  /** One record: record size less the padding delimiter and the GCM tag. */
6
10
  declare const MAX_PAYLOAD_BYTES: number;
7
11
  type VapidKeys = {
@@ -19,6 +23,8 @@ type WebPushOptions = {
19
23
  ttlSeconds?: number;
20
24
  urgency?: "very-low" | "low" | "normal" | "high";
21
25
  fetch?: typeof globalThis.fetch;
26
+ /** Lets an http endpoint through. For a local fake push service only; never in production. */
27
+ allowInsecureEndpoints?: boolean;
22
28
  };
23
29
  /**
24
30
  * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.
@@ -30,4 +36,4 @@ type WebPushOptions = {
30
36
  */
31
37
  declare function webPush(options: WebPushOptions): PushProvider;
32
38
 
33
- export { MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
39
+ export { InvalidSubscriptionError, MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
@@ -1,7 +1,11 @@
1
1
  import { PushProvider } from '../plugins/push.js';
2
- import '../plugin-bTJKmnlq.js';
3
- import '../adapter-f0OR2DhY.js';
2
+ import '../plugin-C9Hoxus2.js';
3
+ import '../adapter-BfLe2J0t.js';
4
4
 
5
+ /** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */
6
+ declare class InvalidSubscriptionError extends Error {
7
+ constructor(message: string);
8
+ }
5
9
  /** One record: record size less the padding delimiter and the GCM tag. */
6
10
  declare const MAX_PAYLOAD_BYTES: number;
7
11
  type VapidKeys = {
@@ -19,6 +23,8 @@ type WebPushOptions = {
19
23
  ttlSeconds?: number;
20
24
  urgency?: "very-low" | "low" | "normal" | "high";
21
25
  fetch?: typeof globalThis.fetch;
26
+ /** Lets an http endpoint through. For a local fake push service only; never in production. */
27
+ allowInsecureEndpoints?: boolean;
22
28
  };
23
29
  /**
24
30
  * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.
@@ -30,4 +36,4 @@ type WebPushOptions = {
30
36
  */
31
37
  declare function webPush(options: WebPushOptions): PushProvider;
32
38
 
33
- export { MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
39
+ export { InvalidSubscriptionError, MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
@@ -12,11 +12,17 @@ function concat(...parts) {
12
12
  }
13
13
  return out;
14
14
  }
15
- function requireBytes(value, label, length) {
15
+ var InvalidSubscriptionError = class extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "InvalidSubscriptionError";
19
+ }
20
+ };
21
+ function requireBytes(value, label, length, Failure = Error) {
16
22
  const bytes = decodeBase64UrlBytes(value);
17
- if (!bytes) throw new Error(`${label} is not valid base64url`);
23
+ if (!bytes) throw new Failure(`${label} is not valid base64url`);
18
24
  if (length !== void 0 && bytes.length !== length) {
19
- throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);
25
+ throw new Failure(`${label} must be ${length} bytes, got ${bytes.length}`);
20
26
  }
21
27
  return bytes;
22
28
  }
@@ -35,8 +41,11 @@ async function encryptPayload(plaintext, userAgentPublicKey, authSecret, overrid
35
41
  if (plaintext.length > MAX_PAYLOAD_BYTES) {
36
42
  throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);
37
43
  }
38
- const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65);
39
- const auth = requireBytes(authSecret, "auth", 16);
44
+ const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65, InvalidSubscriptionError);
45
+ const auth = requireBytes(authSecret, "auth", 16, InvalidSubscriptionError);
46
+ if (uaPublic[0] !== 4) {
47
+ throw new InvalidSubscriptionError("p256dh must be an uncompressed EC point");
48
+ }
40
49
  const salt = overrides?.salt ? new Uint8Array(overrides.salt) : crypto.getRandomValues(new Uint8Array(16));
41
50
  const serverKeys = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
42
51
  const serverPublic = new Uint8Array(
@@ -149,12 +158,28 @@ function webPush(options) {
149
158
  `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`
150
159
  );
151
160
  }
152
- const encrypted = await encryptPayload(
153
- payload,
154
- message.subscription.keys.p256dh,
155
- message.subscription.keys.auth
156
- );
157
- const authorization = await vapidAuthorization(message.subscription.endpoint, {
161
+ let endpoint;
162
+ try {
163
+ endpoint = new URL(message.subscription.endpoint);
164
+ } catch {
165
+ return { invalid: true };
166
+ }
167
+ const secure = endpoint.protocol === "https:";
168
+ if (!secure && !(options.allowInsecureEndpoints && endpoint.protocol === "http:")) {
169
+ return { invalid: true };
170
+ }
171
+ let encrypted;
172
+ try {
173
+ encrypted = await encryptPayload(
174
+ payload,
175
+ message.subscription.keys.p256dh,
176
+ message.subscription.keys.auth
177
+ );
178
+ } catch (error) {
179
+ if (error instanceof InvalidSubscriptionError) return { invalid: true };
180
+ throw error;
181
+ }
182
+ const authorization = await vapidAuthorization(endpoint.href, {
158
183
  subject: options.subject,
159
184
  keys: options.vapid
160
185
  });
@@ -188,6 +213,6 @@ function webPush(options) {
188
213
  };
189
214
  }
190
215
 
191
- export { MAX_PAYLOAD_BYTES, generateVapidKeys, webPush };
216
+ export { InvalidSubscriptionError, MAX_PAYLOAD_BYTES, generateVapidKeys, webPush };
192
217
  //# sourceMappingURL=web-push.js.map
193
218
  //# sourceMappingURL=web-push.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/web-push/crypto.ts","../../src/providers/web-push/index.ts"],"names":[],"mappings":"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","file":"web-push.js","sourcesContent":["import { decodeBase64UrlBytes, encodeBase64Url } from \"../../core/base64url\";\n\n/**\n * VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291 over RFC 8188),\n * on Web Crypto.\n *\n * The `web-push` package is node:crypto only, so it cannot run on Cloudflare\n * Workers or Vercel Edge — the runtimes this library targets. Everything here\n * uses crypto.subtle and therefore runs anywhere the rest of the package does.\n */\n\nconst utf8 = (value: string) => new TextEncoder().encode(value);\n\nfunction concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nfunction requireBytes(value: string, label: string, length?: number): Uint8Array<ArrayBuffer> {\n const bytes = decodeBase64UrlBytes(value);\n if (!bytes) throw new Error(`${label} is not valid base64url`);\n if (length !== undefined && bytes.length !== length) {\n throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);\n }\n return bytes;\n}\n\n/** Web Crypto's HKDF does extract and expand in one call. */\nasync function hkdf(\n // Uint8Array<ArrayBuffer>, not bare Uint8Array: the latter widens to\n // ArrayBufferLike, which Web Crypto will not accept as a BufferSource.\n salt: Uint8Array<ArrayBuffer>,\n ikm: Uint8Array<ArrayBuffer>,\n info: Uint8Array<ArrayBuffer>,\n length: number,\n): Promise<Uint8Array<ArrayBuffer>> {\n const key = await crypto.subtle.importKey(\"raw\", ikm, \"HKDF\", false, [\"deriveBits\"]);\n const bits = await crypto.subtle.deriveBits(\n { name: \"HKDF\", hash: \"SHA-256\", salt, info },\n key,\n length * 8,\n );\n return new Uint8Array(bits);\n}\n\nexport const RECORD_SIZE = 4096;\n/** One record: record size less the padding delimiter and the GCM tag. */\nexport const MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;\n\nexport type EncryptedPush = {\n body: Uint8Array<ArrayBuffer>;\n salt: Uint8Array<ArrayBuffer>;\n serverPublicKey: Uint8Array<ArrayBuffer>;\n};\n\n/**\n * RFC 8291 §3.4 then RFC 8188 §2.\n *\n * `salt` and `serverKeys` are injectable so tests can pin them; production\n * always generates fresh ones, and reusing a salt with the same key would be\n * catastrophic for GCM.\n */\nexport async function encryptPayload(\n plaintext: Uint8Array<ArrayBuffer>,\n userAgentPublicKey: string,\n authSecret: string,\n overrides?: { salt?: Uint8Array<ArrayBuffer>; serverKeys?: CryptoKeyPair },\n): Promise<EncryptedPush> {\n if (plaintext.length > MAX_PAYLOAD_BYTES) {\n throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);\n }\n\n const uaPublic = requireBytes(userAgentPublicKey, \"p256dh\", 65);\n const auth = requireBytes(authSecret, \"auth\", 16);\n\n const salt = overrides?.salt\n ? new Uint8Array(overrides.salt)\n : crypto.getRandomValues(new Uint8Array(16));\n\n const serverKeys =\n overrides?.serverKeys ??\n (await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\"deriveBits\"]));\n\n const serverPublic = new Uint8Array(\n await crypto.subtle.exportKey(\"raw\", serverKeys.publicKey),\n ) as Uint8Array<ArrayBuffer>;\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n uaPublic,\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, serverKeys.privateKey, 256),\n );\n\n // The receiver's key comes first; swapping the order yields a key the\n // browser cannot derive, and the push simply never decrypts.\n const keyInfo = concat(utf8(\"WebPush: info\"), new Uint8Array([0]), uaPublic, serverPublic);\n const ikm = await hkdf(auth, shared, keyInfo, 32);\n\n const cek = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: aes128gcm\"), new Uint8Array([0])),\n 16,\n );\n const nonce = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: nonce\"), new Uint8Array([0])),\n 12,\n );\n\n const aesKey = await crypto.subtle.importKey(\"raw\", cek, \"AES-GCM\", false, [\"encrypt\"]);\n\n // 0x02 marks the final record. 0x01 would mean \"more records follow\" and the\n // receiver would wait for one that never arrives.\n const padded = concat(plaintext, new Uint8Array([2]));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-GCM\", iv: nonce }, aesKey, padded),\n );\n\n // RFC 8188 header: salt(16) | rs(4, big endian) | idlen(1) | keyid\n const header = new Uint8Array(21 + serverPublic.length);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);\n header[20] = serverPublic.length;\n header.set(serverPublic, 21);\n\n return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };\n}\n\nexport type VapidKeys = { publicKey: string; privateKey: string };\n\n/** A VAPID keypair in the base64url form every push library uses. */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const keys = await crypto.subtle.generateKey({ name: \"ECDSA\", namedCurve: \"P-256\" }, true, [\n \"sign\",\n \"verify\",\n ]);\n\n const publicKey = new Uint8Array(await crypto.subtle.exportKey(\"raw\", keys.publicKey));\n const jwk = await crypto.subtle.exportKey(\"jwk\", keys.privateKey);\n\n if (!jwk.d) throw new Error(\"generated key has no private component\");\n return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };\n}\n\n/**\n * VAPID keys are distributed as a raw public point plus a bare scalar, but Web\n * Crypto will only import a private EC key as JWK — which needs x and y. They\n * are recovered by slicing the uncompressed public point.\n */\nasync function importVapidKey(keys: VapidKeys): Promise<CryptoKey> {\n const publicKey = requireBytes(keys.publicKey, \"vapid publicKey\", 65);\n if (publicKey[0] !== 0x04) throw new Error(\"vapid publicKey must be an uncompressed EC point\");\n\n return crypto.subtle.importKey(\n \"jwk\",\n {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.slice(1, 33)),\n y: encodeBase64Url(publicKey.slice(33, 65)),\n d: keys.privateKey,\n ext: true,\n },\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n}\n\nexport type VapidOptions = {\n subject: string;\n keys: VapidKeys;\n /** Injectable for tests; VAPID tokens are short-lived. */\n now?: Date;\n ttlSeconds?: number;\n};\n\n/** RFC 8292 §2: an ES256 JWT scoped to the push service origin. */\nexport async function vapidAuthorization(endpoint: string, options: VapidOptions): Promise<string> {\n if (!/^(mailto:|https:)/.test(options.subject)) {\n throw new Error('vapid subject must be a \"mailto:\" or \"https:\" URL');\n }\n\n const now = options.now ?? new Date();\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const claims = {\n // Origin only: a token scoped to the full endpoint is rejected.\n aud: new URL(endpoint).origin,\n exp: Math.floor(now.getTime() / 1000) + (options.ttlSeconds ?? 12 * 3600),\n sub: options.subject,\n };\n\n const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(\n JSON.stringify(claims),\n )}`;\n\n const signature = await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n await importVapidKey(options.keys),\n utf8(signingInput),\n );\n\n // Web Crypto emits raw r||s, which is what JWS ES256 wants — no DER unwrap.\n const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;\n return `vapid t=${jwt}, k=${options.keys.publicKey}`;\n}\n","import type { PushMessage, PushProvider, PushSendResult } from \"../../plugins/push\";\nimport { encryptPayload, MAX_PAYLOAD_BYTES, type VapidKeys, vapidAuthorization } from \"./crypto\";\n\nexport type WebPushOptions = {\n /** \"mailto:you@example.com\" or an https URL. Push services require it. */\n subject: string;\n vapid: VapidKeys;\n /** How long the service should hold an undelivered message. */\n ttlSeconds?: number;\n urgency?: \"very-low\" | \"low\" | \"normal\" | \"high\";\n fetch?: typeof globalThis.fetch;\n};\n\n/**\n * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.\n *\n * Endpoints that report themselves gone are surfaced as `expired`, which the\n * push plugin turns into an immediate row deletion. Everything transient is\n * `retryable`; anything the push service will reject identically forever is\n * neither, so it fails on the first attempt.\n */\nexport function webPush(options: WebPushOptions): PushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const ttl = options.ttlSeconds ?? 12 * 3600;\n\n return {\n name: \"web-push\",\n\n async send(message: PushMessage): Promise<PushSendResult> {\n const payload = new TextEncoder().encode(\n JSON.stringify({\n title: message.title,\n body: message.body,\n data: message.data ?? {},\n }),\n );\n\n if (payload.length > MAX_PAYLOAD_BYTES) {\n // Trimming silently would deliver a truncated notification; a bigger\n // payload will never fit, so retrying cannot help either.\n throw new Error(\n `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`,\n );\n }\n\n const encrypted = await encryptPayload(\n payload,\n message.subscription.keys.p256dh,\n message.subscription.keys.auth,\n );\n\n const authorization = await vapidAuthorization(message.subscription.endpoint, {\n subject: options.subject,\n keys: options.vapid,\n });\n\n let response: Response;\n try {\n response = await doFetch(message.subscription.endpoint, {\n method: \"POST\",\n headers: {\n Authorization: authorization,\n \"Content-Encoding\": \"aes128gcm\",\n \"Content-Type\": \"application/octet-stream\",\n TTL: String(ttl),\n ...(options.urgency ? { Urgency: options.urgency } : {}),\n },\n body: encrypted.body,\n ...(message.signal ? { signal: message.signal } : {}),\n });\n } catch (error) {\n // Never reached the service: DNS, socket, timeout.\n throw new Error(error instanceof Error ? error.message : String(error));\n }\n\n // 201 is the documented success; 200/202 appear in the wild.\n if (response.ok) return {};\n\n // The subscription is permanently gone and must be pruned.\n if (response.status === 404 || response.status === 410) return { expired: true };\n\n if (response.status === 429 || response.status === 408 || response.status >= 500) {\n return { retryable: true };\n }\n\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : \"\"}`,\n );\n },\n };\n}\n\nexport type { VapidKeys } from \"./crypto\";\nexport { generateVapidKeys, MAX_PAYLOAD_BYTES } from \"./crypto\";\n"]}
1
+ {"version":3,"sources":["../../src/providers/web-push/crypto.ts","../../src/providers/web-push/index.ts"],"names":[],"mappings":"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","file":"web-push.js","sourcesContent":["import { decodeBase64UrlBytes, encodeBase64Url } from \"../../core/base64url\";\n\n/**\n * VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291 over RFC 8188),\n * on Web Crypto.\n *\n * The `web-push` package is node:crypto only, so it cannot run on Cloudflare\n * Workers or Vercel Edge — the runtimes this library targets. Everything here\n * uses crypto.subtle and therefore runs anywhere the rest of the package does.\n */\n\nconst utf8 = (value: string) => new TextEncoder().encode(value);\n\nfunction concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\n/** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */\nexport class InvalidSubscriptionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidSubscriptionError\";\n }\n}\n\nfunction requireBytes(\n value: string,\n label: string,\n length?: number,\n Failure: new (message: string) => Error = Error,\n): Uint8Array<ArrayBuffer> {\n const bytes = decodeBase64UrlBytes(value);\n if (!bytes) throw new Failure(`${label} is not valid base64url`);\n if (length !== undefined && bytes.length !== length) {\n throw new Failure(`${label} must be ${length} bytes, got ${bytes.length}`);\n }\n return bytes;\n}\n\n/** Web Crypto's HKDF does extract and expand in one call. */\nasync function hkdf(\n // Uint8Array<ArrayBuffer>, not bare Uint8Array: the latter widens to\n // ArrayBufferLike, which Web Crypto will not accept as a BufferSource.\n salt: Uint8Array<ArrayBuffer>,\n ikm: Uint8Array<ArrayBuffer>,\n info: Uint8Array<ArrayBuffer>,\n length: number,\n): Promise<Uint8Array<ArrayBuffer>> {\n const key = await crypto.subtle.importKey(\"raw\", ikm, \"HKDF\", false, [\"deriveBits\"]);\n const bits = await crypto.subtle.deriveBits(\n { name: \"HKDF\", hash: \"SHA-256\", salt, info },\n key,\n length * 8,\n );\n return new Uint8Array(bits);\n}\n\nexport const RECORD_SIZE = 4096;\n/** One record: record size less the padding delimiter and the GCM tag. */\nexport const MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;\n\nexport type EncryptedPush = {\n body: Uint8Array<ArrayBuffer>;\n salt: Uint8Array<ArrayBuffer>;\n serverPublicKey: Uint8Array<ArrayBuffer>;\n};\n\n/**\n * RFC 8291 §3.4 then RFC 8188 §2.\n *\n * `salt` and `serverKeys` are injectable so tests can pin them; production\n * always generates fresh ones, and reusing a salt with the same key would be\n * catastrophic for GCM.\n */\nexport async function encryptPayload(\n plaintext: Uint8Array<ArrayBuffer>,\n userAgentPublicKey: string,\n authSecret: string,\n overrides?: { salt?: Uint8Array<ArrayBuffer>; serverKeys?: CryptoKeyPair },\n): Promise<EncryptedPush> {\n if (plaintext.length > MAX_PAYLOAD_BYTES) {\n throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);\n }\n\n const uaPublic = requireBytes(userAgentPublicKey, \"p256dh\", 65, InvalidSubscriptionError);\n const auth = requireBytes(authSecret, \"auth\", 16, InvalidSubscriptionError);\n if (uaPublic[0] !== 0x04) {\n throw new InvalidSubscriptionError(\"p256dh must be an uncompressed EC point\");\n }\n\n const salt = overrides?.salt\n ? new Uint8Array(overrides.salt)\n : crypto.getRandomValues(new Uint8Array(16));\n\n const serverKeys =\n overrides?.serverKeys ??\n (await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\"deriveBits\"]));\n\n const serverPublic = new Uint8Array(\n await crypto.subtle.exportKey(\"raw\", serverKeys.publicKey),\n ) as Uint8Array<ArrayBuffer>;\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n uaPublic,\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, serverKeys.privateKey, 256),\n );\n\n // The receiver's key comes first; swapping the order yields a key the\n // browser cannot derive, and the push simply never decrypts.\n const keyInfo = concat(utf8(\"WebPush: info\"), new Uint8Array([0]), uaPublic, serverPublic);\n const ikm = await hkdf(auth, shared, keyInfo, 32);\n\n const cek = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: aes128gcm\"), new Uint8Array([0])),\n 16,\n );\n const nonce = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: nonce\"), new Uint8Array([0])),\n 12,\n );\n\n const aesKey = await crypto.subtle.importKey(\"raw\", cek, \"AES-GCM\", false, [\"encrypt\"]);\n\n // 0x02 marks the final record. 0x01 would mean \"more records follow\" and the\n // receiver would wait for one that never arrives.\n const padded = concat(plaintext, new Uint8Array([2]));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-GCM\", iv: nonce }, aesKey, padded),\n );\n\n // RFC 8188 header: salt(16) | rs(4, big endian) | idlen(1) | keyid\n const header = new Uint8Array(21 + serverPublic.length);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);\n header[20] = serverPublic.length;\n header.set(serverPublic, 21);\n\n return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };\n}\n\nexport type VapidKeys = { publicKey: string; privateKey: string };\n\n/** A VAPID keypair in the base64url form every push library uses. */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const keys = await crypto.subtle.generateKey({ name: \"ECDSA\", namedCurve: \"P-256\" }, true, [\n \"sign\",\n \"verify\",\n ]);\n\n const publicKey = new Uint8Array(await crypto.subtle.exportKey(\"raw\", keys.publicKey));\n const jwk = await crypto.subtle.exportKey(\"jwk\", keys.privateKey);\n\n if (!jwk.d) throw new Error(\"generated key has no private component\");\n return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };\n}\n\n/**\n * VAPID keys are distributed as a raw public point plus a bare scalar, but Web\n * Crypto will only import a private EC key as JWK — which needs x and y. They\n * are recovered by slicing the uncompressed public point.\n */\nasync function importVapidKey(keys: VapidKeys): Promise<CryptoKey> {\n const publicKey = requireBytes(keys.publicKey, \"vapid publicKey\", 65);\n if (publicKey[0] !== 0x04) throw new Error(\"vapid publicKey must be an uncompressed EC point\");\n\n return crypto.subtle.importKey(\n \"jwk\",\n {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.slice(1, 33)),\n y: encodeBase64Url(publicKey.slice(33, 65)),\n d: keys.privateKey,\n ext: true,\n },\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n}\n\nexport type VapidOptions = {\n subject: string;\n keys: VapidKeys;\n /** Injectable for tests; VAPID tokens are short-lived. */\n now?: Date;\n ttlSeconds?: number;\n};\n\n/** RFC 8292 §2: an ES256 JWT scoped to the push service origin. */\nexport async function vapidAuthorization(endpoint: string, options: VapidOptions): Promise<string> {\n if (!/^(mailto:|https:)/.test(options.subject)) {\n throw new Error('vapid subject must be a \"mailto:\" or \"https:\" URL');\n }\n\n const now = options.now ?? new Date();\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const claims = {\n // Origin only: a token scoped to the full endpoint is rejected.\n aud: new URL(endpoint).origin,\n exp: Math.floor(now.getTime() / 1000) + (options.ttlSeconds ?? 12 * 3600),\n sub: options.subject,\n };\n\n const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(\n JSON.stringify(claims),\n )}`;\n\n const signature = await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n await importVapidKey(options.keys),\n utf8(signingInput),\n );\n\n // Web Crypto emits raw r||s, which is what JWS ES256 wants — no DER unwrap.\n const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;\n return `vapid t=${jwt}, k=${options.keys.publicKey}`;\n}\n","import type { PushMessage, PushProvider, PushSendResult } from \"../../plugins/push\";\nimport {\n encryptPayload,\n InvalidSubscriptionError,\n MAX_PAYLOAD_BYTES,\n type VapidKeys,\n vapidAuthorization,\n} from \"./crypto\";\n\nexport type WebPushOptions = {\n /** \"mailto:you@example.com\" or an https URL. Push services require it. */\n subject: string;\n vapid: VapidKeys;\n /** How long the service should hold an undelivered message. */\n ttlSeconds?: number;\n urgency?: \"very-low\" | \"low\" | \"normal\" | \"high\";\n fetch?: typeof globalThis.fetch;\n /** Lets an http endpoint through. For a local fake push service only; never in production. */\n allowInsecureEndpoints?: boolean;\n};\n\n/**\n * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.\n *\n * Endpoints that report themselves gone are surfaced as `expired`, which the\n * push plugin turns into an immediate row deletion. Everything transient is\n * `retryable`; anything the push service will reject identically forever is\n * neither, so it fails on the first attempt.\n */\nexport function webPush(options: WebPushOptions): PushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const ttl = options.ttlSeconds ?? 12 * 3600;\n\n return {\n name: \"web-push\",\n\n async send(message: PushMessage): Promise<PushSendResult> {\n const payload = new TextEncoder().encode(\n JSON.stringify({\n title: message.title,\n body: message.body,\n data: message.data ?? {},\n }),\n );\n\n if (payload.length > MAX_PAYLOAD_BYTES) {\n // Trimming silently would deliver a truncated notification; a bigger\n // payload will never fit, so retrying cannot help either.\n throw new Error(\n `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`,\n );\n }\n\n // A subscription that cannot be encrypted for, or whose endpoint is not\n // an https URL, will never deliver. Report it so the row is pruned\n // instead of retried five times.\n let endpoint: URL;\n try {\n endpoint = new URL(message.subscription.endpoint);\n } catch {\n return { invalid: true };\n }\n const secure = endpoint.protocol === \"https:\";\n if (!secure && !(options.allowInsecureEndpoints && endpoint.protocol === \"http:\")) {\n return { invalid: true };\n }\n\n let encrypted: Awaited<ReturnType<typeof encryptPayload>>;\n try {\n encrypted = await encryptPayload(\n payload,\n message.subscription.keys.p256dh,\n message.subscription.keys.auth,\n );\n } catch (error) {\n if (error instanceof InvalidSubscriptionError) return { invalid: true };\n throw error;\n }\n\n const authorization = await vapidAuthorization(endpoint.href, {\n subject: options.subject,\n keys: options.vapid,\n });\n\n let response: Response;\n try {\n response = await doFetch(message.subscription.endpoint, {\n method: \"POST\",\n headers: {\n Authorization: authorization,\n \"Content-Encoding\": \"aes128gcm\",\n \"Content-Type\": \"application/octet-stream\",\n TTL: String(ttl),\n ...(options.urgency ? { Urgency: options.urgency } : {}),\n },\n body: encrypted.body,\n ...(message.signal ? { signal: message.signal } : {}),\n });\n } catch (error) {\n // Never reached the service: DNS, socket, timeout.\n throw new Error(error instanceof Error ? error.message : String(error));\n }\n\n // 201 is the documented success; 200/202 appear in the wild.\n if (response.ok) return {};\n\n // The subscription is permanently gone and must be pruned.\n if (response.status === 404 || response.status === 410) return { expired: true };\n\n if (response.status === 429 || response.status === 408 || response.status >= 500) {\n return { retryable: true };\n }\n\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : \"\"}`,\n );\n },\n };\n}\n\nexport type { VapidKeys } from \"./crypto\";\nexport { generateVapidKeys, InvalidSubscriptionError, MAX_PAYLOAD_BYTES } from \"./crypto\";\n"]}
package/dist/react.cjs CHANGED
@@ -66,15 +66,13 @@ function createNotifyClient(options = {}) {
66
66
  }
67
67
  async function refresh() {
68
68
  const startedAt = generation;
69
- const [page, count] = await Promise.all([
70
- request(`/?limit=${limit}`),
71
- request("/count")
72
- ]);
69
+ const page = await request(`/?limit=${limit}`);
70
+ const unseenCount = page.unseenCount ?? (await request("/count")).unseen;
73
71
  if (generation !== startedAt) return;
74
72
  set({
75
73
  notifications: page.notifications,
76
74
  nextCursor: page.nextCursor,
77
- unseenCount: count.unseen,
75
+ unseenCount,
78
76
  isLoading: false,
79
77
  error: null
80
78
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/index.ts","../src/client/index.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n createNotifyClient,\n type NotifyClient,\n type NotifyClientOptions,\n type NotifyState,\n} from \"../client\";\n\nexport type UseNotificationsResult = NotifyState & {\n markAsRead: (id: string) => Promise<void>;\n markAllRead: () => Promise<void>;\n markSeen: () => Promise<void>;\n loadMore: () => Promise<void>;\n refresh: () => Promise<void>;\n};\n\n/**\n * All state and optimistic-rollback logic lives in the framework-agnostic\n * client; this binding only wires it to React's lifecycle.\n */\nexport function useNotifications(\n options: NotifyClientOptions & { client?: NotifyClient } = {},\n): UseNotificationsResult {\n const { client: provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs } = options;\n\n // Callbacks are usually inline literals. Held in a ref and called through a\n // stable wrapper so a new function identity each render does not rebuild the\n // client and restart polling.\n const callbacksRef = useRef(options);\n callbacksRef.current = options;\n\n const client = useMemo(\n () =>\n provided ??\n createNotifyClient({\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),\n isDocumentHidden: () =>\n callbacksRef.current.isDocumentHidden?.() ??\n (typeof document !== \"undefined\" && document.visibilityState === \"hidden\"),\n }),\n [provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs],\n );\n\n const [state, setState] = useState<NotifyState>(() => client.getState());\n\n useEffect(() => client.subscribe(setState), [client]);\n\n return {\n ...state,\n markAsRead: useCallback((id: string) => client.markAsRead(id), [client]),\n markAllRead: useCallback(() => client.markAllRead(), [client]),\n markSeen: useCallback(() => client.markSeen(), [client]),\n loadMore: useCallback(() => client.loadMore(), [client]),\n refresh: useCallback(() => client.refresh(), [client]),\n };\n}\n\nexport type {\n NotificationView,\n NotifyClient,\n NotifyClientOptions,\n NotifyState,\n} from \"../client\";\nexport { createNotifyClient } from \"../client\";\n","export type NotificationView = {\n id: string;\n userId: string;\n type: string;\n payload: unknown;\n actorId: string | null;\n groupKey: string | null;\n seenAt: string | null;\n readAt: string | null;\n createdAt: string;\n};\n\nexport type NotifyState = {\n notifications: readonly NotificationView[];\n /** Badge count — cleared when the dropdown opens. Server-authoritative. */\n unseenCount: number;\n /** Items still bold. Derived from what is loaded, not a server total. */\n unreadCount: number;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n pollIntervalMs?: number | undefined;\n maxPollIntervalMs?: number | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n};\n\nconst INITIAL: NotifyState = {\n notifications: [],\n unseenCount: 0,\n unreadCount: 0,\n nextCursor: null,\n isLoading: true,\n error: null,\n};\n\nconst countUnread = (rows: readonly NotificationView[]) =>\n rows.reduce((total, row) => (row.readAt === null ? total + 1 : total), 0);\n\nexport function createNotifyClient(options: NotifyClientOptions = {}) {\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));\n const limit = options.limit ?? 20;\n const pollIntervalMs = options.pollIntervalMs ?? 15_000;\n const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 60_000;\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n\n // Bumped per mutation: an in-flight poll returns pre-mutation data, which\n // would silently revert the optimistic update.\n let generation = 0;\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n for (const listener of listeners) listener(state);\n }\n\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: { \"content-type\": \"application/json\", ...init?.headers },\n });\n if (!response.ok)\n throw new Error(`${init?.method ?? \"GET\"} ${path} failed: ${response.status}`);\n return (await response.json()) as T;\n }\n\n async function refresh(): Promise<void> {\n const startedAt = generation;\n\n const [page, count] = await Promise.all([\n request<{ notifications: NotificationView[]; nextCursor: string | null }>(`/?limit=${limit}`),\n request<{ unseen: number }>(\"/count\"),\n ]);\n\n // A mutation landed while this was in flight, so the response is already\n // stale. Dropping it is correct: the mutation's own optimistic state is\n // newer, and the next poll will reconcile.\n if (generation !== startedAt) return;\n\n set({\n notifications: page.notifications,\n nextCursor: page.nextCursor,\n unseenCount: count.unseen,\n isLoading: false,\n error: null,\n });\n }\n\n let loadingMore: Promise<void> | undefined;\n\n async function loadMore(): Promise<void> {\n if (!state.nextCursor) return;\n\n // Two clicks before the first response lands would both read the same\n // cursor and append the same page twice. Share the in-flight request.\n if (loadingMore) return loadingMore;\n\n const cursor = state.nextCursor;\n\n loadingMore = (async () => {\n try {\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n }>(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);\n\n // Guard against a duplicate arriving anyway — a concurrent refresh may\n // already have pulled some of these into the list.\n const known = new Set(state.notifications.map((row) => row.id));\n\n set({\n notifications: [\n ...state.notifications,\n ...page.notifications.filter((row) => !known.has(row.id)),\n ],\n nextCursor: page.nextCursor,\n });\n } finally {\n loadingMore = undefined;\n }\n })();\n\n return loadingMore;\n }\n\n /** Applies a patch immediately and reverts it if the request fails. */\n async function optimistic(\n apply: (rows: readonly NotificationView[]) => readonly NotificationView[],\n patch: Partial<NotifyState>,\n send: () => Promise<unknown>,\n ): Promise<void> {\n const previous = state;\n generation += 1;\n set({ notifications: apply(state.notifications), ...patch });\n\n try {\n await send();\n } catch (error) {\n generation += 1;\n state = previous;\n for (const listener of listeners) listener(state);\n throw error;\n }\n }\n\n const stamp = () => new Date().toISOString();\n\n async function markAsRead(id: string): Promise<void> {\n await optimistic(\n (rows) =>\n rows.map((row) => (row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row)),\n {},\n () => request(\"/read\", { method: \"POST\", body: JSON.stringify({ ids: [id] }) }),\n );\n }\n\n async function markAllRead(): Promise<void> {\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n );\n }\n\n async function markSeen(): Promise<void> {\n // Only what the user could actually have seen. Sending no cutoff would\n // mark notifications that landed after the last poll as seen too.\n const newest = state.notifications[0]?.createdAt;\n\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),\n { unseenCount: 0 },\n () =>\n request(\"/seen\", {\n method: \"POST\",\n body: JSON.stringify(newest ? { before: newest } : {}),\n }),\n );\n }\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n async function tick() {\n // A hidden tab still holds an open bell. Polling it burns requests for a\n // UI nobody is looking at.\n if (isHidden()) {\n schedule(pollIntervalMs);\n return;\n }\n\n try {\n await refresh();\n consecutiveFailures = 0;\n } catch (error) {\n consecutiveFailures += 1;\n set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });\n }\n\n // Back off on failure so an outage does not turn every open tab into a\n // retry loop against a struggling server.\n schedule(\n consecutiveFailures === 0\n ? pollIntervalMs\n : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs),\n );\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One poller regardless of how many components subscribe — otherwise two\n // bells, or a StrictMode double-mount, double the request rate.\n if (listeners.size === 1) void tick();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n getState: () => state,\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/react/index.ts","../src/client/index.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n createNotifyClient,\n type NotifyClient,\n type NotifyClientOptions,\n type NotifyState,\n} from \"../client\";\n\nexport type UseNotificationsResult = NotifyState & {\n markAsRead: (id: string) => Promise<void>;\n markAllRead: () => Promise<void>;\n markSeen: () => Promise<void>;\n loadMore: () => Promise<void>;\n refresh: () => Promise<void>;\n};\n\n/**\n * All state and optimistic-rollback logic lives in the framework-agnostic\n * client; this binding only wires it to React's lifecycle.\n */\nexport function useNotifications(\n options: NotifyClientOptions & { client?: NotifyClient } = {},\n): UseNotificationsResult {\n const { client: provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs } = options;\n\n // Callbacks are usually inline literals. Held in a ref and called through a\n // stable wrapper so a new function identity each render does not rebuild the\n // client and restart polling.\n const callbacksRef = useRef(options);\n callbacksRef.current = options;\n\n const client = useMemo(\n () =>\n provided ??\n createNotifyClient({\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),\n isDocumentHidden: () =>\n callbacksRef.current.isDocumentHidden?.() ??\n (typeof document !== \"undefined\" && document.visibilityState === \"hidden\"),\n }),\n [provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs],\n );\n\n const [state, setState] = useState<NotifyState>(() => client.getState());\n\n useEffect(() => client.subscribe(setState), [client]);\n\n return {\n ...state,\n markAsRead: useCallback((id: string) => client.markAsRead(id), [client]),\n markAllRead: useCallback(() => client.markAllRead(), [client]),\n markSeen: useCallback(() => client.markSeen(), [client]),\n loadMore: useCallback(() => client.loadMore(), [client]),\n refresh: useCallback(() => client.refresh(), [client]),\n };\n}\n\nexport type {\n NotificationView,\n NotifyClient,\n NotifyClientOptions,\n NotifyState,\n} from \"../client\";\nexport { createNotifyClient } from \"../client\";\n","export type NotificationView = {\n id: string;\n userId: string;\n type: string;\n payload: unknown;\n actorId: string | null;\n groupKey: string | null;\n seenAt: string | null;\n readAt: string | null;\n createdAt: string;\n};\n\nexport type NotifyState = {\n notifications: readonly NotificationView[];\n /** Badge count — cleared when the dropdown opens. Server-authoritative. */\n unseenCount: number;\n /** Items still bold. Derived from what is loaded, not a server total. */\n unreadCount: number;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n pollIntervalMs?: number | undefined;\n maxPollIntervalMs?: number | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n};\n\nconst INITIAL: NotifyState = {\n notifications: [],\n unseenCount: 0,\n unreadCount: 0,\n nextCursor: null,\n isLoading: true,\n error: null,\n};\n\nconst countUnread = (rows: readonly NotificationView[]) =>\n rows.reduce((total, row) => (row.readAt === null ? total + 1 : total), 0);\n\nexport function createNotifyClient(options: NotifyClientOptions = {}) {\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));\n const limit = options.limit ?? 20;\n const pollIntervalMs = options.pollIntervalMs ?? 15_000;\n const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 60_000;\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n\n // Bumped per mutation: an in-flight poll returns pre-mutation data, which\n // would silently revert the optimistic update.\n let generation = 0;\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n for (const listener of listeners) listener(state);\n }\n\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: { \"content-type\": \"application/json\", ...init?.headers },\n });\n if (!response.ok)\n throw new Error(`${init?.method ?? \"GET\"} ${path} failed: ${response.status}`);\n return (await response.json()) as T;\n }\n\n async function refresh(): Promise<void> {\n const startedAt = generation;\n\n // One request, not two. The feed carries unseenCount for a first page,\n // which is every poll. A server too old to send it still works: the\n // fallback asks /count the way this always used to.\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n unseenCount?: number;\n }>(`/?limit=${limit}`);\n\n const unseenCount = page.unseenCount ?? (await request<{ unseen: number }>(\"/count\")).unseen;\n\n // A mutation landed while this was in flight, so the response is already\n // stale. Dropping it is correct: the mutation's own optimistic state is\n // newer, and the next poll will reconcile.\n if (generation !== startedAt) return;\n\n set({\n notifications: page.notifications,\n nextCursor: page.nextCursor,\n unseenCount,\n isLoading: false,\n error: null,\n });\n }\n\n let loadingMore: Promise<void> | undefined;\n\n async function loadMore(): Promise<void> {\n if (!state.nextCursor) return;\n\n // Two clicks before the first response lands would both read the same\n // cursor and append the same page twice. Share the in-flight request.\n if (loadingMore) return loadingMore;\n\n const cursor = state.nextCursor;\n\n loadingMore = (async () => {\n try {\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n }>(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);\n\n // Guard against a duplicate arriving anyway — a concurrent refresh may\n // already have pulled some of these into the list.\n const known = new Set(state.notifications.map((row) => row.id));\n\n set({\n notifications: [\n ...state.notifications,\n ...page.notifications.filter((row) => !known.has(row.id)),\n ],\n nextCursor: page.nextCursor,\n });\n } finally {\n loadingMore = undefined;\n }\n })();\n\n return loadingMore;\n }\n\n /** Applies a patch immediately and reverts it if the request fails. */\n async function optimistic(\n apply: (rows: readonly NotificationView[]) => readonly NotificationView[],\n patch: Partial<NotifyState>,\n send: () => Promise<unknown>,\n ): Promise<void> {\n const previous = state;\n generation += 1;\n set({ notifications: apply(state.notifications), ...patch });\n\n try {\n await send();\n } catch (error) {\n generation += 1;\n state = previous;\n for (const listener of listeners) listener(state);\n throw error;\n }\n }\n\n const stamp = () => new Date().toISOString();\n\n async function markAsRead(id: string): Promise<void> {\n await optimistic(\n (rows) =>\n rows.map((row) => (row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row)),\n {},\n () => request(\"/read\", { method: \"POST\", body: JSON.stringify({ ids: [id] }) }),\n );\n }\n\n async function markAllRead(): Promise<void> {\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n );\n }\n\n async function markSeen(): Promise<void> {\n // Only what the user could actually have seen. Sending no cutoff would\n // mark notifications that landed after the last poll as seen too.\n const newest = state.notifications[0]?.createdAt;\n\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),\n { unseenCount: 0 },\n () =>\n request(\"/seen\", {\n method: \"POST\",\n body: JSON.stringify(newest ? { before: newest } : {}),\n }),\n );\n }\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n async function tick() {\n // A hidden tab still holds an open bell. Polling it burns requests for a\n // UI nobody is looking at.\n if (isHidden()) {\n schedule(pollIntervalMs);\n return;\n }\n\n try {\n await refresh();\n consecutiveFailures = 0;\n } catch (error) {\n consecutiveFailures += 1;\n set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });\n }\n\n // Back off on failure so an outage does not turn every open tab into a\n // retry loop against a struggling server.\n schedule(\n consecutiveFailures === 0\n ? pollIntervalMs\n : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs),\n );\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One poller regardless of how many components subscribe — otherwise two\n // bells, or a StrictMode double-mount, double the request rate.\n if (listeners.size === 1) void tick();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n getState: () => state,\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"],"mappings":"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","names":[]}
package/dist/react.js CHANGED
@@ -42,15 +42,13 @@ function createNotifyClient(options = {}) {
42
42
  }
43
43
  async function refresh() {
44
44
  const startedAt = generation;
45
- const [page, count] = await Promise.all([
46
- request(`/?limit=${limit}`),
47
- request("/count")
48
- ]);
45
+ const page = await request(`/?limit=${limit}`);
46
+ const unseenCount = page.unseenCount ?? (await request("/count")).unseen;
49
47
  if (generation !== startedAt) return;
50
48
  set({
51
49
  notifications: page.notifications,
52
50
  nextCursor: page.nextCursor,
53
- unseenCount: count.unseen,
51
+ unseenCount,
54
52
  isLoading: false,
55
53
  error: null
56
54
  });
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/index.ts","../src/client/index.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n createNotifyClient,\n type NotifyClient,\n type NotifyClientOptions,\n type NotifyState,\n} from \"../client\";\n\nexport type UseNotificationsResult = NotifyState & {\n markAsRead: (id: string) => Promise<void>;\n markAllRead: () => Promise<void>;\n markSeen: () => Promise<void>;\n loadMore: () => Promise<void>;\n refresh: () => Promise<void>;\n};\n\n/**\n * All state and optimistic-rollback logic lives in the framework-agnostic\n * client; this binding only wires it to React's lifecycle.\n */\nexport function useNotifications(\n options: NotifyClientOptions & { client?: NotifyClient } = {},\n): UseNotificationsResult {\n const { client: provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs } = options;\n\n // Callbacks are usually inline literals. Held in a ref and called through a\n // stable wrapper so a new function identity each render does not rebuild the\n // client and restart polling.\n const callbacksRef = useRef(options);\n callbacksRef.current = options;\n\n const client = useMemo(\n () =>\n provided ??\n createNotifyClient({\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),\n isDocumentHidden: () =>\n callbacksRef.current.isDocumentHidden?.() ??\n (typeof document !== \"undefined\" && document.visibilityState === \"hidden\"),\n }),\n [provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs],\n );\n\n const [state, setState] = useState<NotifyState>(() => client.getState());\n\n useEffect(() => client.subscribe(setState), [client]);\n\n return {\n ...state,\n markAsRead: useCallback((id: string) => client.markAsRead(id), [client]),\n markAllRead: useCallback(() => client.markAllRead(), [client]),\n markSeen: useCallback(() => client.markSeen(), [client]),\n loadMore: useCallback(() => client.loadMore(), [client]),\n refresh: useCallback(() => client.refresh(), [client]),\n };\n}\n\nexport type {\n NotificationView,\n NotifyClient,\n NotifyClientOptions,\n NotifyState,\n} from \"../client\";\nexport { createNotifyClient } from \"../client\";\n","export type NotificationView = {\n id: string;\n userId: string;\n type: string;\n payload: unknown;\n actorId: string | null;\n groupKey: string | null;\n seenAt: string | null;\n readAt: string | null;\n createdAt: string;\n};\n\nexport type NotifyState = {\n notifications: readonly NotificationView[];\n /** Badge count — cleared when the dropdown opens. Server-authoritative. */\n unseenCount: number;\n /** Items still bold. Derived from what is loaded, not a server total. */\n unreadCount: number;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n pollIntervalMs?: number | undefined;\n maxPollIntervalMs?: number | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n};\n\nconst INITIAL: NotifyState = {\n notifications: [],\n unseenCount: 0,\n unreadCount: 0,\n nextCursor: null,\n isLoading: true,\n error: null,\n};\n\nconst countUnread = (rows: readonly NotificationView[]) =>\n rows.reduce((total, row) => (row.readAt === null ? total + 1 : total), 0);\n\nexport function createNotifyClient(options: NotifyClientOptions = {}) {\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));\n const limit = options.limit ?? 20;\n const pollIntervalMs = options.pollIntervalMs ?? 15_000;\n const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 60_000;\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n\n // Bumped per mutation: an in-flight poll returns pre-mutation data, which\n // would silently revert the optimistic update.\n let generation = 0;\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n for (const listener of listeners) listener(state);\n }\n\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: { \"content-type\": \"application/json\", ...init?.headers },\n });\n if (!response.ok)\n throw new Error(`${init?.method ?? \"GET\"} ${path} failed: ${response.status}`);\n return (await response.json()) as T;\n }\n\n async function refresh(): Promise<void> {\n const startedAt = generation;\n\n const [page, count] = await Promise.all([\n request<{ notifications: NotificationView[]; nextCursor: string | null }>(`/?limit=${limit}`),\n request<{ unseen: number }>(\"/count\"),\n ]);\n\n // A mutation landed while this was in flight, so the response is already\n // stale. Dropping it is correct: the mutation's own optimistic state is\n // newer, and the next poll will reconcile.\n if (generation !== startedAt) return;\n\n set({\n notifications: page.notifications,\n nextCursor: page.nextCursor,\n unseenCount: count.unseen,\n isLoading: false,\n error: null,\n });\n }\n\n let loadingMore: Promise<void> | undefined;\n\n async function loadMore(): Promise<void> {\n if (!state.nextCursor) return;\n\n // Two clicks before the first response lands would both read the same\n // cursor and append the same page twice. Share the in-flight request.\n if (loadingMore) return loadingMore;\n\n const cursor = state.nextCursor;\n\n loadingMore = (async () => {\n try {\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n }>(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);\n\n // Guard against a duplicate arriving anyway — a concurrent refresh may\n // already have pulled some of these into the list.\n const known = new Set(state.notifications.map((row) => row.id));\n\n set({\n notifications: [\n ...state.notifications,\n ...page.notifications.filter((row) => !known.has(row.id)),\n ],\n nextCursor: page.nextCursor,\n });\n } finally {\n loadingMore = undefined;\n }\n })();\n\n return loadingMore;\n }\n\n /** Applies a patch immediately and reverts it if the request fails. */\n async function optimistic(\n apply: (rows: readonly NotificationView[]) => readonly NotificationView[],\n patch: Partial<NotifyState>,\n send: () => Promise<unknown>,\n ): Promise<void> {\n const previous = state;\n generation += 1;\n set({ notifications: apply(state.notifications), ...patch });\n\n try {\n await send();\n } catch (error) {\n generation += 1;\n state = previous;\n for (const listener of listeners) listener(state);\n throw error;\n }\n }\n\n const stamp = () => new Date().toISOString();\n\n async function markAsRead(id: string): Promise<void> {\n await optimistic(\n (rows) =>\n rows.map((row) => (row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row)),\n {},\n () => request(\"/read\", { method: \"POST\", body: JSON.stringify({ ids: [id] }) }),\n );\n }\n\n async function markAllRead(): Promise<void> {\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n );\n }\n\n async function markSeen(): Promise<void> {\n // Only what the user could actually have seen. Sending no cutoff would\n // mark notifications that landed after the last poll as seen too.\n const newest = state.notifications[0]?.createdAt;\n\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),\n { unseenCount: 0 },\n () =>\n request(\"/seen\", {\n method: \"POST\",\n body: JSON.stringify(newest ? { before: newest } : {}),\n }),\n );\n }\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n async function tick() {\n // A hidden tab still holds an open bell. Polling it burns requests for a\n // UI nobody is looking at.\n if (isHidden()) {\n schedule(pollIntervalMs);\n return;\n }\n\n try {\n await refresh();\n consecutiveFailures = 0;\n } catch (error) {\n consecutiveFailures += 1;\n set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });\n }\n\n // Back off on failure so an outage does not turn every open tab into a\n // retry loop against a struggling server.\n schedule(\n consecutiveFailures === 0\n ? pollIntervalMs\n : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs),\n );\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One poller regardless of how many components subscribe — otherwise two\n // bells, or a StrictMode double-mount, double the request rate.\n if (listeners.size === 1) void tick();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n getState: () => state,\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/react/index.ts","../src/client/index.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n createNotifyClient,\n type NotifyClient,\n type NotifyClientOptions,\n type NotifyState,\n} from \"../client\";\n\nexport type UseNotificationsResult = NotifyState & {\n markAsRead: (id: string) => Promise<void>;\n markAllRead: () => Promise<void>;\n markSeen: () => Promise<void>;\n loadMore: () => Promise<void>;\n refresh: () => Promise<void>;\n};\n\n/**\n * All state and optimistic-rollback logic lives in the framework-agnostic\n * client; this binding only wires it to React's lifecycle.\n */\nexport function useNotifications(\n options: NotifyClientOptions & { client?: NotifyClient } = {},\n): UseNotificationsResult {\n const { client: provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs } = options;\n\n // Callbacks are usually inline literals. Held in a ref and called through a\n // stable wrapper so a new function identity each render does not rebuild the\n // client and restart polling.\n const callbacksRef = useRef(options);\n callbacksRef.current = options;\n\n const client = useMemo(\n () =>\n provided ??\n createNotifyClient({\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),\n isDocumentHidden: () =>\n callbacksRef.current.isDocumentHidden?.() ??\n (typeof document !== \"undefined\" && document.visibilityState === \"hidden\"),\n }),\n [provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs],\n );\n\n const [state, setState] = useState<NotifyState>(() => client.getState());\n\n useEffect(() => client.subscribe(setState), [client]);\n\n return {\n ...state,\n markAsRead: useCallback((id: string) => client.markAsRead(id), [client]),\n markAllRead: useCallback(() => client.markAllRead(), [client]),\n markSeen: useCallback(() => client.markSeen(), [client]),\n loadMore: useCallback(() => client.loadMore(), [client]),\n refresh: useCallback(() => client.refresh(), [client]),\n };\n}\n\nexport type {\n NotificationView,\n NotifyClient,\n NotifyClientOptions,\n NotifyState,\n} from \"../client\";\nexport { createNotifyClient } from \"../client\";\n","export type NotificationView = {\n id: string;\n userId: string;\n type: string;\n payload: unknown;\n actorId: string | null;\n groupKey: string | null;\n seenAt: string | null;\n readAt: string | null;\n createdAt: string;\n};\n\nexport type NotifyState = {\n notifications: readonly NotificationView[];\n /** Badge count — cleared when the dropdown opens. Server-authoritative. */\n unseenCount: number;\n /** Items still bold. Derived from what is loaded, not a server total. */\n unreadCount: number;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n pollIntervalMs?: number | undefined;\n maxPollIntervalMs?: number | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n};\n\nconst INITIAL: NotifyState = {\n notifications: [],\n unseenCount: 0,\n unreadCount: 0,\n nextCursor: null,\n isLoading: true,\n error: null,\n};\n\nconst countUnread = (rows: readonly NotificationView[]) =>\n rows.reduce((total, row) => (row.readAt === null ? total + 1 : total), 0);\n\nexport function createNotifyClient(options: NotifyClientOptions = {}) {\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));\n const limit = options.limit ?? 20;\n const pollIntervalMs = options.pollIntervalMs ?? 15_000;\n const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 60_000;\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n\n // Bumped per mutation: an in-flight poll returns pre-mutation data, which\n // would silently revert the optimistic update.\n let generation = 0;\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n for (const listener of listeners) listener(state);\n }\n\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: { \"content-type\": \"application/json\", ...init?.headers },\n });\n if (!response.ok)\n throw new Error(`${init?.method ?? \"GET\"} ${path} failed: ${response.status}`);\n return (await response.json()) as T;\n }\n\n async function refresh(): Promise<void> {\n const startedAt = generation;\n\n // One request, not two. The feed carries unseenCount for a first page,\n // which is every poll. A server too old to send it still works: the\n // fallback asks /count the way this always used to.\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n unseenCount?: number;\n }>(`/?limit=${limit}`);\n\n const unseenCount = page.unseenCount ?? (await request<{ unseen: number }>(\"/count\")).unseen;\n\n // A mutation landed while this was in flight, so the response is already\n // stale. Dropping it is correct: the mutation's own optimistic state is\n // newer, and the next poll will reconcile.\n if (generation !== startedAt) return;\n\n set({\n notifications: page.notifications,\n nextCursor: page.nextCursor,\n unseenCount,\n isLoading: false,\n error: null,\n });\n }\n\n let loadingMore: Promise<void> | undefined;\n\n async function loadMore(): Promise<void> {\n if (!state.nextCursor) return;\n\n // Two clicks before the first response lands would both read the same\n // cursor and append the same page twice. Share the in-flight request.\n if (loadingMore) return loadingMore;\n\n const cursor = state.nextCursor;\n\n loadingMore = (async () => {\n try {\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n }>(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);\n\n // Guard against a duplicate arriving anyway — a concurrent refresh may\n // already have pulled some of these into the list.\n const known = new Set(state.notifications.map((row) => row.id));\n\n set({\n notifications: [\n ...state.notifications,\n ...page.notifications.filter((row) => !known.has(row.id)),\n ],\n nextCursor: page.nextCursor,\n });\n } finally {\n loadingMore = undefined;\n }\n })();\n\n return loadingMore;\n }\n\n /** Applies a patch immediately and reverts it if the request fails. */\n async function optimistic(\n apply: (rows: readonly NotificationView[]) => readonly NotificationView[],\n patch: Partial<NotifyState>,\n send: () => Promise<unknown>,\n ): Promise<void> {\n const previous = state;\n generation += 1;\n set({ notifications: apply(state.notifications), ...patch });\n\n try {\n await send();\n } catch (error) {\n generation += 1;\n state = previous;\n for (const listener of listeners) listener(state);\n throw error;\n }\n }\n\n const stamp = () => new Date().toISOString();\n\n async function markAsRead(id: string): Promise<void> {\n await optimistic(\n (rows) =>\n rows.map((row) => (row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row)),\n {},\n () => request(\"/read\", { method: \"POST\", body: JSON.stringify({ ids: [id] }) }),\n );\n }\n\n async function markAllRead(): Promise<void> {\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n );\n }\n\n async function markSeen(): Promise<void> {\n // Only what the user could actually have seen. Sending no cutoff would\n // mark notifications that landed after the last poll as seen too.\n const newest = state.notifications[0]?.createdAt;\n\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),\n { unseenCount: 0 },\n () =>\n request(\"/seen\", {\n method: \"POST\",\n body: JSON.stringify(newest ? { before: newest } : {}),\n }),\n );\n }\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n async function tick() {\n // A hidden tab still holds an open bell. Polling it burns requests for a\n // UI nobody is looking at.\n if (isHidden()) {\n schedule(pollIntervalMs);\n return;\n }\n\n try {\n await refresh();\n consecutiveFailures = 0;\n } catch (error) {\n consecutiveFailures += 1;\n set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });\n }\n\n // Back off on failure so an outage does not turn every open tab into a\n // retry loop against a struggling server.\n schedule(\n consecutiveFailures === 0\n ? pollIntervalMs\n : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs),\n );\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One poller regardless of how many components subscribe — otherwise two\n // bells, or a StrictMode double-mount, double the request rate.\n if (listeners.size === 1) void tick();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n getState: () => state,\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"],"mappings":"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","names":[]}
package/dist/schema.cjs CHANGED
@@ -53,7 +53,8 @@ var coreSchema = {
53
53
  type: { type: "string", required: true },
54
54
  channel: { type: "string", required: true },
55
55
  enabled: { type: "boolean", required: true, default: true },
56
- frequency: { type: "string", required: true, default: "instant" }
56
+ frequency: { type: "string", required: true, default: "instant" },
57
+ updatedAt: { type: "date", required: true, defaultNow: true }
57
58
  },
58
59
  primaryKey: ["userId", "type", "channel"]
59
60
  }