@lunora/notify 0.0.0 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{FCM_ENV_KEYS as e,WEB_PUSH_ENV_KEYS as t,fcmFromEnv as i,webPushFromEnv as f}from"./packem_shared/FCM_ENV_KEYS-D0tvRTLQ.mjs";import{defineNotify as n,isNotifyDefinition as u}from"./packem_shared/defineNotify-DLEy53HJ.mjs";import{createNotify as p}from"./packem_shared/createNotify-jNlehALj.mjs";import{buildEngine as d,routingPushProvider as x}from"./packem_shared/buildEngine-Jlzf2Jzn.mjs";import{enqueuePushBroadcast as c,runPushBroadcastJob as P}from"./packem_shared/enqueuePushBroadcast-tf1LqgzA.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-67jpK0Vx.mjs";import{memorySubscriptionStore as N}from"./packem_shared/memorySubscriptionStore-FKZR4-fB.mjs";import{fcmId as g,isGoneError as y,normalizeRegisterInput as v,targetOf as B,webPushId as F}from"./packem_shared/fcmId-CFZ8IquF.mjs";export{e as FCM_ENV_KEYS,t as WEB_PUSH_ENV_KEYS,d as buildEngine,p as createNotify,b as d1SubscriptionStore,n as defineNotify,c as enqueuePushBroadcast,i as fcmFromEnv,g as fcmId,y as isGoneError,u as isNotifyDefinition,N as memorySubscriptionStore,v as normalizeRegisterInput,x as routingPushProvider,P as runPushBroadcastJob,B as targetOf,f as webPushFromEnv,F as webPushId};
@@ -0,0 +1 @@
1
+ const s={privateKey:"VAPID_PRIVATE_KEY",publicKey:"VAPID_PUBLIC_KEY",subject:"VAPID_SUBJECT"},v={accessToken:"FCM_ACCESS_TOKEN",projectId:"FCM_PROJECT_ID"},t=(c,o)=>{const e=c[o];return typeof e=="string"&&e!==""?e:void 0},E=(c,o)=>{const e=t(c,s.publicKey),i=t(c,s.privateKey),r=t(c,s.subject);if(!(e===void 0||i===void 0||r===void 0))return{vapidPrivateKey:i,vapidPublicKey:e,vapidSubject:r,...o}},_=(c,o)=>{const e=t(c,v.projectId);return e===void 0?void 0:{accessToken:t(c,v.accessToken),projectId:e,...o}};export{v as FCM_ENV_KEYS,s as WEB_PUSH_ENV_KEYS,_ as fcmFromEnv,E as webPushFromEnv};
@@ -0,0 +1 @@
1
+ import{createNotification as s}from"@visulima/notification";import{retryMiddleware as a,circuitBreakerMiddleware as c}from"@visulima/notification/middleware";import{fcmProvider as u}from"@visulima/notification/providers/fcm";import{webPushProvider as d}from"@visulima/notification/providers/web-push";const i=e=>{if(typeof e!="string")return typeof e=="object"&&e!==null&&"endpoint"in e;if(!e.startsWith("{"))return!1;try{const t=JSON.parse(e);return typeof t.endpoint=="string"&&typeof t.keys=="object"}catch{return!1}},f=e=>{const t=o=>{const r=i(o)?e.webPush:e.fcm;if(r===void 0)throw new Error(i(o)?"@lunora/notify: received a web-push target but no `webPush` channel is configured":"@lunora/notify: received an FCM token target but no `fcm` channel is configured");return r};return{channel:"push",id:"lunora-push-router",initialize:async()=>{await e.webPush?.initialize(),await e.fcm?.initialize()},isAvailable:()=>(e.webPush??e.fcm)!==void 0,send:o=>{const r=Array.isArray(o.to)?o.to[0]:o.to;return t(r).send(o)}}},w=e=>{const t=e.webPush===void 0?void 0:d(e.webPush),o=e.fcm===void 0?void 0:u(e.fcm),r={};(t!==void 0||o!==void 0)&&(r.push=f({fcm:o,webPush:t})),e.chat!==void 0&&(r.chat=e.chat),e.inApp!==void 0&&(r.inapp=e.inApp),e.webhook!==void 0&&(r.webhook=e.webhook);const n=s(r);return n.use(a()).use(c()),n};export{w as buildEngine,f as routingPushProvider};
@@ -0,0 +1 @@
1
+ import{LunoraError as k}from"@lunora/errors";import{buildEngine as R}from"./buildEngine-Jlzf2Jzn.mjs";import{memorySubscriptionStore as W}from"./memorySubscriptionStore-FKZR4-fB.mjs";import{normalizeRegisterInput as Q,targetOf as U,isGoneError as j}from"./fcmId-CFZ8IquF.mjs";const z=250,g=(o,l)=>typeof o=="function"?o(l):o,b=o=>o.successful?void 0:o.errorMessages.join("; "),G=(o,l)=>o.successful?"accepted":j(l)?"gone":"failed",L=async(o,l,c)=>{const d=Array.from({length:o.length});let h=0;const p=async()=>{for(;h<o.length;){const f=h;h+=1,d[f]=await c(o[f])}};return await Promise.all(Array.from({length:Math.min(l,o.length)},()=>p())),d},_=(o,l)=>({chat:g(o.chat,l),fcm:g(o.fcm,l),inApp:g(o.inApp,l),webhook:g(o.webhook,l),webPush:g(o.webPush,l)}),E=new WeakMap,q=(o,l)=>{let c=E.get(o);c===void 0&&(c=new WeakMap,E.set(o,c));let d=c.get(l);return d===void 0&&(d={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},c.set(l,d)),d},V=(o,l,c={})=>{const d=q(o,l);let h;c.engine===void 0?(d.engine??=R(_(o,l)),h=d.engine):h=c.engine,d.store??=o.store?.(l);let{store:p}=d;p===void 0&&(d.fallbackStore??=W(),!c.silent&&!d.warnedNoStore&&(d.warnedNoStore=!0,console.warn("@lunora/notify: no `store` configured — using a non-durable in-memory subscription store. Configure `store: (env) => d1SubscriptionStore(env.DB)` for production.")),p=d.fallbackStore);const f=p,M=Math.max(1,c.concurrency??10),S=Math.max(1,c.broadcastPageSize??z),{log:T,metrics:P}=c,w=(t,e,r,i=1)=>{P?.count("notify.send",i,{channel:t,provider:e??t,status:r})},v=(t,e,r)=>{T?.warn(`notify ${t} delivery failed`,{channel:t,provider:e??t,status:"failed",...r})},N=(t,e)=>{P?.count("notify.skipped",1,{channel:t,reason:e})},$=()=>{const t=o.allowedPushOrigins!==void 0&&o.allowedPushOrigins.length>0;c.silent||t||d.warnedNoPushOriginAllowlist||(d.warnedNoPushOriginAllowlist=!0,console.warn("@lunora/notify: Web Push registered without `allowedPushOrigins` — the endpoint host is validated by a string classifier that does NOT resolve DNS, so a public hostname resolving to a private/internal IP (e.g. `https://127.0.0.1.nip.io/…`) is NOT blocked. Set `allowedPushOrigins` to the exact push-service origins to close DNS rebinding."))},I=async t=>(await f.list(t)).map(({keys:e,token:r,...i})=>i),C=async t=>{if(typeof t!="string")return t;const e=await f.get(t);if(e===void 0)throw new k("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${t}"`);return e},O=async(t,e,r)=>{let i,s,n;try{i=await h.sendToChannel("push",{...e,to:U(t)}),s=b(i),n=G(i,s)}catch(u){n="failed",s=u instanceof Error?u.message:String(u)}try{n==="accepted"?await f.markStatus(t.id,"ok"):n==="gone"?await f.delete(t.id):await f.markStatus(t.id,"failed",s)}catch{}return n==="failed"&&v("push",t.kind,{error:s,subscriptionId:t.id,userId:t.userId??null}),r&&w("push",t.kind,n),{error:s,receipt:i,status:n}},D=async(t,e)=>{const r=await L(e,M,async n=>{const{error:u,status:a}=await O(n,t,!1);return{error:u,kind:n.kind,status:a,subscription:n}}),i=new Map;for(const{kind:n,status:u}of r){const a=`${n} ${u}`,m=i.get(a);m===void 0?i.set(a,{count:1,kind:n,status:u}):m.count+=1}for(const{count:n,kind:u,status:a}of i.values())w("push",u,a,n);const s=r.map(({error:n,status:u,subscription:a})=>u==="accepted"?{id:a.id,status:"ok"}:u==="gone"?{error:n,id:a.id,status:"expired"}:{error:n,id:a.id,status:"failed"});return{failed:s.filter(n=>n.status==="failed").length,outcomes:s,pruned:s.filter(n=>n.status==="expired").length,sent:s.filter(n=>n.status==="ok").length,total:s.length}},A=async(t,e)=>{if(e?.limit!==void 0&&e.limit<=0)return{nextCursor:void 0,result:{failed:0,outcomes:[],pruned:0,sent:0,total:0}};const r=e?.limit!==void 0&&e.limit>0?Math.trunc(e.limit):void 0,i=r===void 0?S:Math.min(r,S),s=await f.list({after:e?.after,kind:e?.kind,limit:i+1,userId:e?.userId}),n=e?.after===void 0?s:s.filter(B=>B.id>e.after),u=n.length>i,a=u?n.slice(0,i):n;a.length===0&&e?.after===void 0&&N("push","no-subscriptions-matched");const m=await D(t,a);return{nextCursor:u?a[a.length-1]?.id:void 0,result:m}},x={broadcast:async(t,e)=>{const r={failed:0,outcomes:[],pruned:0,sent:0,total:0};let i=e?.after;const s=e?.limit;if(s!==void 0&&s<=0)return r;for(;;){const n=s===void 0?{...e,after:i}:{...e,after:i,limit:s-r.total},{nextCursor:u,result:a}=await A(t,n);if(r.failed+=a.failed,r.pruned+=a.pruned,r.sent+=a.sent,r.total+=a.total,r.outcomes.push(...a.outcomes),s!==void 0&&r.total>=s||u===void 0||u===i)break;i=u}return r},broadcastPage:A,list:t=>I(t),register:t=>("token"in t||$(),f.put(Q(t,void 0,{allowedPushOrigins:o.allowedPushOrigins}))),send:async(t,e)=>{const{error:r,receipt:i}=await O(await C(t),e,!0);if(i===void 0)throw new k("INTERNAL",`@lunora/notify: push send failed: ${r??"unknown error"}`);return i},unregister:t=>f.delete(t)},y=async(t,e)=>{if(h.getProvider(t)===void 0)throw N(t,"channel-not-configured"),new k("BAD_REQUEST",`@lunora/notify: the "${t}" channel is not configured in defineNotify(...)`);const r=await h.sendToChannel(t,e),i=r.successful?"accepted":"failed";return w(t,r.provider,i),i==="failed"&&v(t,r.provider,{error:b(r)}),r};return{notify:{chat:t=>y("chat",t),inApp:t=>y("inapp",t),push:x,send:async t=>{const e=await h.send(t);for(const r of e){const i=r.channel??"unknown",s=r.successful?"accepted":"failed";w(i,r.provider,s),s==="failed"&&v(i,r.provider,{error:b(r)})}return e},webhook:t=>y("webhook",t)},push:x}};export{V as createNotify};
@@ -0,0 +1 @@
1
+ import{LunoraError as h}from"@lunora/errors";import{legacyIdFor as S}from"./fcmId-CFZ8IquF.mjs";const o=a=>{const e={createdAt:a.created_at,id:a.id,kind:a.kind,lastSeenAt:a.last_seen_at,userId:a.user_id};if(a.endpoint!==null&&(e.endpoint=a.endpoint),a.p256dh!==null&&a.auth!==null&&(e.keys={auth:a.auth,p256dh:a.p256dh}),a.token!==null&&(e.token=a.token),a.last_status!==null&&(e.lastStatus=a.last_status),a.last_error!==null&&(e.lastError=a.last_error),a.metadata!==null)try{e.metadata=JSON.parse(a.metadata)}catch{}return e},c=/^[A-Za-z_]\w*$/u,k=(a,e={})=>{const r=e.tableName??"lunora_push_subscriptions";if(!c.test(r))throw new h("BAD_REQUEST",`@lunora/notify: d1SubscriptionStore tableName "${r}" is not a bare SQL identifier`);let s;const d=()=>(s===void 0&&(s=a.prepare(`CREATE TABLE IF NOT EXISTS ${r} (id TEXT PRIMARY KEY, kind TEXT NOT NULL, endpoint TEXT, p256dh TEXT, auth TEXT, token TEXT, user_id TEXT, metadata TEXT, created_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL, last_status TEXT, last_error TEXT)`).run().then(()=>a.prepare(`CREATE INDEX IF NOT EXISTS ${r}_user_id_idx ON ${r} (user_id)`).run()).then(()=>a.prepare(`CREATE INDEX IF NOT EXISTS ${r}_kind_idx ON ${r} (kind)`).run()).then(()=>{}),s.catch(()=>{s=void 0})),s),l=async t=>{await d();const n=await a.prepare(`SELECT * FROM ${r} WHERE id = ?1`).bind(t).first();return n===null?void 0:o(n)};return{delete:async t=>{await d(),await a.prepare(`DELETE FROM ${r} WHERE id = ?1`).bind(t).run()},get:l,list:async t=>{await d();const n=[],i=[];t?.kind!==void 0&&(i.push(t.kind),n.push(`kind = ?${i.length.toString()}`)),t?.userId!==void 0&&(t.userId===null?n.push("user_id IS NULL"):(i.push(t.userId),n.push(`user_id = ?${i.length.toString()}`))),t?.after!==void 0&&(i.push(t.after),n.push(`id > ?${i.length.toString()}`));const E=n.length===0?"":` WHERE ${n.join(" AND ")}`,p=" ORDER BY id ASC";let u="";t?.limit!==void 0&&t.limit>0&&(i.push(Math.trunc(t.limit)),u=` LIMIT ?${i.length.toString()}`);const{results:T}=await a.prepare(`SELECT * FROM ${r}${E}${p}${u}`).bind(...i).all();return T.map(_=>o(_))},markStatus:async(t,n,i)=>{await d(),await a.prepare(`UPDATE ${r} SET last_status = ?2, last_error = ?3, last_seen_at = ?4 WHERE id = ?1`).bind(t,n,i??null,Date.now()).run()},put:async t=>{await d(),await a.prepare(`INSERT INTO ${r} (id, kind, endpoint, p256dh, auth, token, user_id, metadata, created_at, last_seen_at, last_status, last_error) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT(id) DO UPDATE SET kind = ?2, endpoint = ?3, p256dh = ?4, auth = ?5, token = ?6, user_id = ?7, metadata = ?8, last_seen_at = ?10`).bind(t.id,t.kind,t.endpoint??null,t.keys?.p256dh??null,t.keys?.auth??null,t.token??null,t.userId??null,t.metadata===void 0?null:JSON.stringify(t.metadata),t.createdAt,t.lastSeenAt,t.lastStatus??null,t.lastError??null).run();const n=S(t);return n!==void 0&&n!==t.id&&await a.prepare(`DELETE FROM ${r} WHERE id = ?1`).bind(n).run(),await l(t.id)??t}}};export{k as d1SubscriptionStore};
@@ -0,0 +1 @@
1
+ const e=o=>{if(o.webPush!==void 0&&typeof o.webPush!="function"&&typeof o.webPush!="object")throw new TypeError("defineNotify: `webPush` must be a WebPushConfig object or an `(env) => WebPushConfig` function");if(o.fcm!==void 0&&typeof o.fcm!="function"&&typeof o.fcm!="object")throw new TypeError("defineNotify: `fcm` must be an FcmConfig object or an `(env) => FcmConfig` function");if(o.store!==void 0&&typeof o.store!="function")throw new TypeError("defineNotify: `store` must be a function `(env) => SubscriptionStore` when provided");if(o.allowedPushOrigins!==void 0&&(!Array.isArray(o.allowedPushOrigins)||o.allowedPushOrigins.some(i=>typeof i!="string")))throw new TypeError('defineNotify: `allowedPushOrigins` must be an array of origin strings (e.g. ["https://fcm.googleapis.com"]) when provided');if(o.webPush===void 0&&o.fcm===void 0)throw new TypeError("defineNotify: configure at least one push channel — `webPush` and/or `fcm`");return{...o,isLunoraNotify:!0}},n=o=>typeof o=="object"&&o!==null&&o.isLunoraNotify===!0;export{e as defineNotify,n as isNotifyDefinition};
@@ -0,0 +1 @@
1
+ import{LunoraError as s}from"@lunora/errors";const o=(e,r)=>e.send({...r,type:"lunora.push.broadcast"}),n=async(e,r)=>{const t=await e.broadcastPage(r.payload,r.filter);if(t.result.failed>0)throw new s("INTERNAL",`@lunora/notify: push broadcast page had ${t.result.failed.toString()} transient failure(s) of ${t.result.total.toString()} subscription(s) (${t.result.sent.toString()} sent, ${t.result.pruned.toString()} pruned) — throwing so the queue retries this page`);return t};export{o as enqueuePushBroadcast,n as runPushBroadcastJob};
@@ -0,0 +1 @@
1
+ import{LunoraError as a}from"@lunora/errors";const k=/^\d{1,3}$/u,A=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,I=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,v=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,O=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,D=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,R=/^\[|\]$/gu,_=/\.$/u,c=t=>{const e=t.split(".");if(e.length!==4)return;const n=e.map(r=>k.test(r)?Number(r):-1);if(!n.some(r=>r<0||r>255))return[n[0],n[1],n[2],n[3]]},f=([t,e])=>t===0||t===10||t===127||t===100&&e>=64&&e<=127||t===169&&e===254||t===172&&e>=16&&e<=31||t===192&&e===168||t>=224,h=(t,e)=>{const n=Number.parseInt(t??"",16),r=Number.parseInt(e??"",16);return!Number.isFinite(n)||!Number.isFinite(r)?!0:f([Math.floor(n/256),n%256,Math.floor(r/256),r%256])},N=t=>{const e=t.toLowerCase(),n=A.exec(e);if(n)return h(n[1],n[2]);const r=I.exec(e);if(r){const i=c(r[1]??"");return i===void 0||f(i)}const o=v.exec(e);if(o){const i=c(o[1]??"");return i===void 0||f(i)}const s=O.exec(e);return s?h(s[1],s[2]):D.test(e)||e.startsWith("2002:")||e.startsWith("2001:0:")?!0:e==="::"||e==="::1"||e.startsWith("fc")||e.startsWith("fd")||e.startsWith("fe8")||e.startsWith("fe9")||e.startsWith("fea")||e.startsWith("feb")},W=t=>t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal")||t.endsWith(".home.arpa"),B=t=>t.replaceAll(R,"").replace(_,"").toLowerCase(),P=t=>{const e=B(t);if(e.includes(":"))return N(e);const n=c(e);return n===void 0?W(e):f(n)},g=4096,w=t=>{if(t===void 0)return;const e=typeof t=="object"&&t!==null?Object.getPrototypeOf(t):void 0;if(!(typeof t=="object"&&t!==null&&!Array.isArray(t)&&(e===Object.prototype||e===null)))throw new a("BAD_REQUEST","@lunora/notify: register() `metadata` must be a plain object");let n;try{n=JSON.stringify(t)}catch(o){throw new a("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is not JSON-serialisable: ${o instanceof Error?o.message:String(o)}`)}const r=new TextEncoder().encode(n).length;if(r>g)throw new a("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is ${r.toString()} bytes, exceeding the ${g.toString()}-byte cap`);return t},d=t=>t.toString(16).padStart(4,"0"),b=t=>{let e=8997,n=33826,r=40164,o=52210;for(let s=0;s<t.length;s+=1){const i=t.codePointAt(s)??0;e^=i&65535,n^=i>>>16&65535;const u=e*435,m=n*435,$=r*435+e*256,E=o*435+n*256,l=m+(u>>>16),p=$+(l>>>16),S=E+(p>>>16);e=u&65535,n=l&65535,r=p&65535,o=S&65535}return d(o)+d(r)+d(n)+d(e)},T=t=>`wp2_${b(t)}`,U=t=>`fcm2_${b(t)}`,y=t=>{let e=2166136261;for(let n=0;n<t.length;n+=1)e^=t.codePointAt(n)??0,e=Math.imul(e,16777619);return(e>>>0).toString(16).padStart(8,"0")},x=t=>`wp_${y(t)}`,Q=t=>`fcm_${y(t)}`,C=t=>t.kind==="fcm"?t.token===void 0?void 0:Q(t.token):t.endpoint===void 0?void 0:x(t.endpoint),F=t=>{if(typeof t!="string")return t??{};try{return JSON.parse(t)}catch(e){throw new a("BAD_REQUEST",`@lunora/notify: register() web-push subscription is not valid JSON: ${e instanceof Error?e.message:String(e)}`)}},j=(t,e)=>{let n;try{n=new URL(t)}catch{throw new a("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must be an absolute https URL (got "${t}")`)}if(n.protocol!=="https:")throw new a("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must use https (got "${n.protocol}")`);if(e!==void 0&&e.length>0){if(!e.includes(n.origin))throw new a("FORBIDDEN",`@lunora/notify: register() web-push endpoint origin "${n.origin}" is not in the configured allowedPushOrigins allowlist`);return}if(P(n.hostname))throw new a("FORBIDDEN",`@lunora/notify: register() web-push endpoint host "${n.hostname}" is a private/internal address; configure allowedPushOrigins to permit a specific origin`)},z=(t,e=Date.now(),n={})=>{if("token"in t){const{token:u}=t;if(typeof u!="string"||u==="")throw new a("BAD_REQUEST","@lunora/notify: register() fcm input requires a non-empty `token`");return{createdAt:e,id:U(u),kind:"fcm",lastSeenAt:e,metadata:w(t.metadata),token:u,userId:t.userId??null}}const r=F(t.subscription),{endpoint:o}=r,s=r.keys?.p256dh,i=r.keys?.auth;if(typeof o!="string"||o===""||typeof s!="string"||typeof i!="string")throw new a("BAD_REQUEST","@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");return j(o,n.allowedPushOrigins),{createdAt:e,endpoint:o,id:T(o),keys:{auth:i,p256dh:s},kind:"web-push",lastSeenAt:e,metadata:w(t.metadata),userId:t.userId??null}},G=t=>t.kind==="fcm"?t.token??"":JSON.stringify({endpoint:t.endpoint,keys:t.keys}),J=/\bhttp\s*4(?:04|10)\b/iu,L=/\b(?:unregistered|not[\s-]?registered|registration-token-not-registered)\b/iu,M=/\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu,H=t=>t===void 0?!1:J.test(t)||L.test(t)||M.test(t);export{U as fcmId,H as isGoneError,Q as legacyFcmId,C as legacyIdFor,x as legacyWebPushId,z as normalizeRegisterInput,G as targetOf,T as webPushId};
@@ -0,0 +1 @@
1
+ import{legacyIdFor as d}from"./fcmId-CFZ8IquF.mjs";const n=(r,t)=>r.id<t.id?-1:r.id>t.id?1:0,l=(r,t)=>t===void 0?!0:!(t.kind!==void 0&&r.kind!==t.kind||t.userId!==void 0&&(r.userId??null)!==t.userId),u=()=>{const r=new Map;return{delete:t=>(r.delete(t),Promise.resolve()),get:t=>Promise.resolve(r.get(t)),list:t=>{const e=[];for(const s of r.values())l(s,t)&&e.push(s);e.sort(n);const o=t?.after===void 0?e:e.filter(s=>s.id>t.after),i=t?.limit!==void 0&&t.limit>0?o.slice(0,Math.trunc(t.limit)):o;return Promise.resolve(i)},markStatus:(t,e,o)=>{const i=r.get(t);return i!==void 0&&r.set(t,{...i,lastError:o,lastSeenAt:Date.now(),lastStatus:e}),Promise.resolve()},put:t=>{const e=d(t);e!==void 0&&e!==t.id&&r.delete(e);const o=r.get(t.id),i=o===void 0?t:{...o,...t,createdAt:o.createdAt};return r.set(i.id,i),Promise.resolve(i)}}};export{u as memorySubscriptionStore};
package/dist/web.d.mts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@lunora/notify/web` — the browser-side helper for registering a service-worker
3
+ * Web Push subscription. Runs in the browser (Push API + `navigator.serviceWorker`),
4
+ * has no server imports, and returns a plain, JSON-serialisable subscription you
5
+ * hand to a Lunora mutation calling `ctx.push.register({ subscription, userId })`.
6
+ *
7
+ * ```ts
8
+ * import { subscribeToPush } from "@lunora/notify/web";
9
+ *
10
+ * const subscription = await subscribeToPush({ serviceWorkerUrl: "/sw.js", vapidPublicKey });
11
+ * await client.mutation("registerDevice", { subscription });
12
+ * ```
13
+ * @packageDocumentation
14
+ */
15
+ /** A plain, JSON-serialisable Web Push subscription (the shape `ctx.push.register` accepts). */
16
+ interface SerializedPushSubscription {
17
+ endpoint: string;
18
+ expirationTime: number | null;
19
+ keys: {
20
+ auth: string;
21
+ p256dh: string;
22
+ };
23
+ }
24
+ /** Options for {@link subscribeToPush}. */
25
+ interface SubscribeToPushOptions {
26
+ /**
27
+ * Service-worker registration scope (passed to `register`). Only used when
28
+ * `serviceWorkerUrl` is provided.
29
+ */
30
+ scope?: string;
31
+ /**
32
+ * URL of the service worker script to register. Omit to reuse the page's
33
+ * already-active registration (`navigator.serviceWorker.ready`).
34
+ */
35
+ serviceWorkerUrl?: string;
36
+ /**
37
+ * The application server VAPID **public** key (base64url) — must match the
38
+ * `VAPID_PUBLIC_KEY` the server signs with. Required for a new subscription.
39
+ */
40
+ vapidPublicKey: string;
41
+ }
42
+ /** Whether the current browser supports the Web Push flow (service workers + Push API). */
43
+ declare const isPushSupported: () => boolean;
44
+ /**
45
+ * Register (or reuse) a service worker and subscribe the browser to Web Push,
46
+ * returning the subscription in serialisable form. Reuses an existing subscription
47
+ * when present. Throws if push is unsupported or the user denies permission.
48
+ */
49
+ declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SerializedPushSubscription>;
50
+ /** Unsubscribe the browser's current Web Push subscription. Returns whether one was removed. */
51
+ declare const unsubscribeFromPush: () => Promise<boolean>;
52
+ export { type SerializedPushSubscription, type SubscribeToPushOptions, isPushSupported, subscribeToPush, unsubscribeFromPush };
package/dist/web.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@lunora/notify/web` — the browser-side helper for registering a service-worker
3
+ * Web Push subscription. Runs in the browser (Push API + `navigator.serviceWorker`),
4
+ * has no server imports, and returns a plain, JSON-serialisable subscription you
5
+ * hand to a Lunora mutation calling `ctx.push.register({ subscription, userId })`.
6
+ *
7
+ * ```ts
8
+ * import { subscribeToPush } from "@lunora/notify/web";
9
+ *
10
+ * const subscription = await subscribeToPush({ serviceWorkerUrl: "/sw.js", vapidPublicKey });
11
+ * await client.mutation("registerDevice", { subscription });
12
+ * ```
13
+ * @packageDocumentation
14
+ */
15
+ /** A plain, JSON-serialisable Web Push subscription (the shape `ctx.push.register` accepts). */
16
+ interface SerializedPushSubscription {
17
+ endpoint: string;
18
+ expirationTime: number | null;
19
+ keys: {
20
+ auth: string;
21
+ p256dh: string;
22
+ };
23
+ }
24
+ /** Options for {@link subscribeToPush}. */
25
+ interface SubscribeToPushOptions {
26
+ /**
27
+ * Service-worker registration scope (passed to `register`). Only used when
28
+ * `serviceWorkerUrl` is provided.
29
+ */
30
+ scope?: string;
31
+ /**
32
+ * URL of the service worker script to register. Omit to reuse the page's
33
+ * already-active registration (`navigator.serviceWorker.ready`).
34
+ */
35
+ serviceWorkerUrl?: string;
36
+ /**
37
+ * The application server VAPID **public** key (base64url) — must match the
38
+ * `VAPID_PUBLIC_KEY` the server signs with. Required for a new subscription.
39
+ */
40
+ vapidPublicKey: string;
41
+ }
42
+ /** Whether the current browser supports the Web Push flow (service workers + Push API). */
43
+ declare const isPushSupported: () => boolean;
44
+ /**
45
+ * Register (or reuse) a service worker and subscribe the browser to Web Push,
46
+ * returning the subscription in serialisable form. Reuses an existing subscription
47
+ * when present. Throws if push is unsupported or the user denies permission.
48
+ */
49
+ declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SerializedPushSubscription>;
50
+ /** Unsubscribe the browser's current Web Push subscription. Returns whether one was removed. */
51
+ declare const unsubscribeFromPush: () => Promise<boolean>;
52
+ export { type SerializedPushSubscription, type SubscribeToPushOptions, isPushSupported, subscribeToPush, unsubscribeFromPush };
package/dist/web.mjs ADDED
@@ -0,0 +1 @@
1
+ const l=r=>{const i="=".repeat((4-r.length%4)%4),n=(r+i).replaceAll("-","+").replaceAll("_","/"),e=atob(n),a=new Uint8Array(e.length);for(let s=0;s<e.length;s+=1)a[s]=e.codePointAt(s)??0;return a},c=(r,i)=>r.length===i.length&&r.every((n,e)=>n===i[e]),t=(r,i)=>{const n=r.options.applicationServerKey;return n===null?!1:c(new Uint8Array(n),l(i))},o=globalThis,u=()=>o.navigator?.serviceWorker!==void 0&&o.PushManager!==void 0,p=async r=>{if(!u())throw new Error("@lunora/notify: Web Push is not supported in this browser (needs service workers + PushManager)");let i;if(r.serviceWorkerUrl===void 0)i=await navigator.serviceWorker.ready;else{const a=r.scope===void 0?void 0:{scope:r.scope};i=await navigator.serviceWorker.register(r.serviceWorkerUrl,a)}const n=await Notification.requestPermission();if(n!=="granted")throw new Error(`@lunora/notify: notification permission was not granted (got "${n}")`);const e=await i.pushManager.getSubscription();return e!==null&&!t(e,r.vapidPublicKey)&&await e.unsubscribe(),((e!==null&&t(e,r.vapidPublicKey)?e:null)??await i.pushManager.subscribe({applicationServerKey:l(r.vapidPublicKey),userVisibleOnly:!0})).toJSON()},g=async()=>{if(!u())return!1;const r=await(await navigator.serviceWorker.ready).pushManager.getSubscription();return r===null?!1:r.unsubscribe()};export{u as isPushSupported,p as subscribeToPush,g as unsubscribeFromPush};
package/package.json CHANGED
@@ -1,10 +1,58 @@
1
1
  {
2
2
  "name": "@lunora/notify",
3
- "version": "0.0.0",
4
- "description": "OIDC trusted publishing setup package for @lunora/notify",
3
+ "version": "1.0.0-alpha.10",
4
+ "description": "Multi-channel notifications for Lunora ctx.notify / ctx.push over @visulima/notification: edge-safe Web Push + FCM, plus chat, in-app inbox and webhook channels, with subscription storage and queue-backed fan-out",
5
5
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
6
+ "cloudflare",
7
+ "fcm",
8
+ "lunora",
9
+ "notifications",
10
+ "push",
11
+ "vapid",
12
+ "web-push",
13
+ "workers"
14
+ ],
15
+ "homepage": "https://lunora.sh",
16
+ "bugs": "https://github.com/anolilab/lunora/issues",
17
+ "license": "FSL-1.1-Apache-2.0",
18
+ "author": {
19
+ "name": "Daniel Bannert",
20
+ "email": "d.bannert@anolilab.de"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/anolilab/lunora.git",
25
+ "directory": "packages/notify"
26
+ },
27
+ "files": [
28
+ "./dist",
29
+ "README.md",
30
+ "LICENSE.md"
31
+ ],
32
+ "type": "module",
33
+ "sideEffects": false,
34
+ "main": "./dist/index.mjs",
35
+ "module": "./dist/index.mjs",
36
+ "types": "./dist/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.mjs"
41
+ },
42
+ "./web": {
43
+ "types": "./dist/web.d.ts",
44
+ "import": "./dist/web.mjs"
45
+ },
46
+ "./package.json": "./package.json"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "dependencies": {
52
+ "@lunora/errors": "1.0.0-alpha.13",
53
+ "@visulima/notification": "1.0.5"
54
+ },
55
+ "engines": {
56
+ "node": "^22.15.0 || >=24.11.0"
57
+ }
58
+ }