@lunora/notify 1.0.0-alpha.6 → 1.0.0-alpha.60

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 CHANGED
@@ -1 +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-iBbs9Bew.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-DMwYIM0o.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-BwssRgeI.mjs";import{memorySubscriptionStore as N}from"./packem_shared/memorySubscriptionStore-BxKdyxjC.mjs";import{fcmId as g,isGoneError as y,normalizeRegisterInput as v,targetOf as B,webPushId as F}from"./packem_shared/fcmId-CLRyQJVw.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};
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-BbhPScGH.mjs";import{defineNotify as n,isNotifyDefinition as m}from"./packem_shared/defineNotify-DVPpVkU0.mjs";import{createNotify as u}from"./packem_shared/createNotify-BQp_YxUK.mjs";import{buildEngine as E,routingPushProvider as d}from"./packem_shared/buildEngine-BACMHt3p.mjs";import{enqueuePushBroadcast as P,runPushBroadcastPage as c}from"./packem_shared/enqueuePushBroadcast-BnuMQfuG.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-DoInbQak.mjs";import{memorySubscriptionStore as h}from"./packem_shared/memorySubscriptionStore-C7hyd6ii.mjs";import{f as _,i as y,n as v,t as w,w as B}from"./packem_shared/normalize-BdOnBfM2.mjs";export{e as FCM_ENV_KEYS,t as WEB_PUSH_ENV_KEYS,E as buildEngine,u as createNotify,b as d1SubscriptionStore,n as defineNotify,P as enqueuePushBroadcast,i as fcmFromEnv,_ as fcmId,y as isGoneError,m as isNotifyDefinition,h as memorySubscriptionStore,v as normalizeRegisterInput,d as routingPushProvider,c as runPushBroadcastPage,w as targetOf,f as webPushFromEnv,B as webPushId};
@@ -0,0 +1 @@
1
+ const r={privateKey:"VAPID_PRIVATE_KEY",publicKey:"VAPID_PUBLIC_KEY",subject:"VAPID_SUBJECT"},i={accessToken:"FCM_ACCESS_TOKEN",projectId:"FCM_PROJECT_ID"},t=(c,o)=>{const e=c[o];return typeof e=="string"&&e!==""?e:void 0},d=(c,o)=>{const e=t(c,r.publicKey),s=t(c,r.privateKey),n=t(c,r.subject);if(!(e===void 0||s===void 0||n===void 0))return{vapidPrivateKey:s,vapidPublicKey:e,vapidSubject:n,...o}},E=(c,o)=>{const e=t(c,i.projectId);return e===void 0?void 0:{accessToken:t(c,i.accessToken),projectId:e,...o}};export{i as FCM_ENV_KEYS,r as WEB_PUSH_ENV_KEYS,E as fcmFromEnv,d as webPushFromEnv};
@@ -0,0 +1 @@
1
+ import{LunoraError as l,isLunoraError as S}from"@lunora/errors";import{createNotification as C}from"@visulima/notification";import{retryMiddleware as $}from"@visulima/notification/middleware";import{createFcmProvider as O}from"@visulima/notification/providers/fcm";import{createWebPushProvider as L}from"@visulima/notification/providers/web-push";import{a as N,p as y,b as U,d as H,i as M}from"./normalize-BdOnBfM2.mjs";const z=(e,t)=>{if(e.size<t)return;const r=e.keys().next().value;r!==void 0&&e.delete(r)},B="https://cloudflare-dns.com/dns-query",v=1,m=28,F=2e3,V=(e,t)=>{if(t===v){const r=y(e);return r===void 0||U(r)}return H(e.toLowerCase())},g=async(e,t,r)=>{try{const s=await fetch(`${B}?name=${encodeURIComponent(e)}&type=${String(t)}`,{headers:{accept:"application/dns-json"},signal:AbortSignal.timeout(r)});return s.ok?(await s.json()).Answer??[]:void 0}catch{return}},j=async(e,t=F)=>{const r=N(e);if(r.includes(":")||y(r)!==void 0)return{kind:"unknown"};const[s,n]=await Promise.all([g(r,v,t),g(r,m,t)]);if(s===void 0&&n===void 0)return{kind:"unknown"};for(const o of[...s??[],...n??[]])if((o.type===v||o.type===m)&&V(o.data,o.type))return{address:o.data,kind:"private"};return{kind:"public"}},E=e=>{let t=e;if(typeof e=="string"){if(!e.startsWith("{"))return;try{t=JSON.parse(e)}catch{return}}const r=t?.endpoint;return typeof r=="string"?r:void 0},Y=e=>E(e)===void 0?"fcm":"web-push",h="lunora-push-router",p=new Map,G=256,K=async(e,t)=>{if(t!==void 0&&t.length>0)return;let r;try{({hostname:r}=new URL(e))}catch{return}const s=p.get(r),n=s??j(r),o=await n;if(s===void 0&&o.kind!=="unknown"&&(z(p,G),p.set(r,n)),o.kind==="private")throw new l("FORBIDDEN",`@lunora/notify: web-push endpoint host "${r}" resolves to a private/internal address (${o.address}); refusing to send (DNS-rebinding guard)`)},W=(e,t)=>{const r=[e.provider,t.provider].filter(n=>n!==void 0),s=[...e.recipients??[],...t.recipients??[]];return{...e,messageId:[e.messageId,t.messageId].join(","),response:[e.response,t.response],sent:e.sent&&t.sent,timestamp:new Date(Math.max(e.timestamp.getTime(),t.timestamp.getTime())),...r.length>0?{provider:r.join(",")}:{},...s.length>0?{recipients:s}:{}}},b=e=>e instanceof Error?e.message:typeof e=="string"?e:void 0,A="PUSH_PARTIALLY_DELIVERED",R=e=>S(e)&&e.code===A,q=e=>{if(!R(e))return;const{data:t}=e;return t==="fcm"||t==="web-push"?t:void 0},x=(e,t)=>{if(!e.success||!t.success){if(e.success||t.success){const[s,n]=e.success?["web-push","fcm"]:["fcm","web-push"],o=e.success?t.error:e.error;return{error:new l(A,`@lunora/notify: push partially delivered — the ${s} group was accepted, the ${n} group failed (${b(o)??"unknown error"}). Not retried: a retry would re-send to the ${s} targets that already received it. Re-send to the ${n} targets only.`,{cause:o,data:n}),success:!1}}return{error:new AggregateError([e.error,t.error],"@lunora/notify: both push target groups failed"),success:!1}}const r=e.data===void 0||t.data===void 0?e.data??t.data:W(e.data,t.data);return r===void 0?{success:!0}:{data:r,success:!0}},P=e=>M(b(e)),f=5,J=3e4,Q=e=>{if(e.provider!==h)return[e.provider];const{to:t}=e.payload,r=Array.isArray(t)?t:[t],s=new Set(r.map(n=>Y(n)));return s.size===0?[e.provider]:[...s].map(n=>`${e.provider}:${n}`)},X=(e,t)=>e.failures>=f&&t-e.openedAt<J,Z=(e,t,r)=>{const s=q(e),n=s===void 0?void 0:`${h}:${s}`,o=n!==void 0&&t.includes(n);for(const[d,a]of r.entries())o&&t[d]!==n?a.failures=0:(a.failures+=1,a.failures>=f&&(a.openedAt=Date.now()))},ee=()=>{const e=new Map,t=r=>{const s=e.get(r);if(s!==void 0)return s;const n={failures:0,openedAt:0};return e.set(r,n),n};return async(r,s)=>{const n=Q(r),o=n.map(c=>t(c)),d=Date.now();if(o.every(c=>X(c,d)))return{error:new l("SERVICE_UNAVAILABLE",`@lunora/notify: circuit open for "${n.join('", "')}" after ${f.toString()} consecutive failures`),success:!1};for(const c of o)c.failures>=f&&(c.failures=f-1);const a=await s(r);if(a.success)for(const c of o)c.failures=0;else P(a.error)||Z(a.error,n,o);return a}},te=e=>{const t=r=>{const s=r===void 0?e.fcm:e.webPush;if(s===void 0)throw new Error(r===void 0?"@lunora/notify: received an FCM token target but no `fcm` channel is configured":"@lunora/notify: received a web-push target but no `webPush` channel is configured");return s};return{channel:"push",id:h,initialize:async()=>{await e.webPush?.initialize(),await e.fcm?.initialize()},isAvailable:()=>(e.webPush??e.fcm)!==void 0,send:async r=>{const s=Array.isArray(r.to)?r.to:[r.to];if(s.length===0)throw new l("BAD_REQUEST","@lunora/notify: push send has no recipients — `to` is an empty array");const n=s.map(i=>E(i));for(const i of n)i!==void 0&&await K(i,e.allowedPushOrigins);const o=s.filter((i,u)=>n[u]!==void 0),d=s.filter((i,u)=>n[u]===void 0),a=n.find(i=>i!==void 0);if(d.length===0&&a!==void 0)return t(a).send(r);if(o.length===0)return t(void 0).send(r);const c=i=>({...r,to:i.length===1&&i[0]!==void 0?i[0]:i}),I=t(a),T=t(void 0),w=async(i,u)=>i.send(c(u)),D=await Promise.allSettled([w(I,o),w(T,d)]),[_,k]=D.map(i=>i.status==="fulfilled"?i.value:{error:i.reason,success:!1});return x(_,k)}}},re=(e,t={})=>e.use($({baseDelay:t.retryBaseDelay,shouldRetry:r=>!P(r)&&!R(r)})).use(ee()),de=e=>{const t=e.webPush===void 0?void 0:L(e.webPush),r=e.fcm===void 0?void 0:O(e.fcm),s={};return(t!==void 0||r!==void 0)&&(s.push=te({allowedPushOrigins:e.allowedPushOrigins,fcm:r,webPush:t})),e.chat!==void 0&&(s.chat=e.chat),e.inApp!==void 0&&(s.inapp=e.inApp),e.webhook!==void 0&&(s.webhook=e.webhook),re(C(s))};export{re as attachResilience,de as buildEngine,te as routingPushProvider};
@@ -0,0 +1 @@
1
+ import{LunoraError as v}from"@lunora/errors";import{buildEngine as z}from"./buildEngine-BACMHt3p.mjs";import{memorySubscriptionStore as F}from"./memorySubscriptionStore-C7hyd6ii.mjs";import{n as R,t as B,i as L}from"./normalize-BdOnBfM2.mjs";const W=250,w=(o,l)=>typeof o=="function"?o(l):o,S=o=>o.successful?void 0:o.errorMessages.join("; "),U=(o,l,d)=>o.successful?"accepted":L(l,d)?"gone":"failed",j=async(o,l,d)=>{const u=Array.from({length:o.length});let h=0;const g=async()=>{for(;h<o.length;){const f=h;h+=1,u[f]=await d(o[f])}};return await Promise.all(Array.from({length:Math.min(l,o.length)},()=>g())),u},G=(o,l)=>({allowedPushOrigins:o.allowedPushOrigins,chat:w(o.chat,l),fcm:w(o.fcm,l),inApp:w(o.inApp,l),webhook:w(o.webhook,l),webPush:w(o.webPush,l)}),x=new WeakMap,Q=(o,l)=>{let d=x.get(o);d===void 0&&(d=new WeakMap,x.set(o,d));let u=d.get(l);return u===void 0&&(u={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},d.set(l,u)),u},Y=(o,l,d={})=>{const u=Q(o,l);let h;d.engine===void 0?(u.engine??=z(G(o,l)),h=u.engine):h=d.engine,u.store??=o.store?.(l);let{store:g}=u;g===void 0&&(u.fallbackStore??=F(),!d.silent&&!u.warnedNoStore&&(u.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.")),g=u.fallbackStore);const f=g,C=Math.max(1,d.concurrency??o.concurrency??10),b=Math.max(1,d.broadcastPageSize??o.broadcastPageSize??W),{log:M,metrics:P}=d,p=(e,t,r,n=1)=>{P?.count("notify.send",n,{channel:e,provider:t??e,status:r})},y=(e,t,r)=>{M?.warn(`notify ${e} delivery failed`,{channel:e,provider:t??e,status:"failed",...r})},A=(e,t)=>{P?.count("notify.skipped",1,{channel:e,reason:t})},I=()=>{const e=o.allowedPushOrigins!==void 0&&o.allowedPushOrigins.length>0;d.silent||e||u.warnedNoPushOriginAllowlist||(u.warnedNoPushOriginAllowlist=!0,console.warn("@lunora/notify: Web Push registered without `allowedPushOrigins` — endpoints are guarded by a string classifier at register time and a best-effort DNS re-check at send time, both of which are defeatable. Set `allowedPushOrigins` to the exact push-service origins for a hard guarantee."))},T=async e=>(await f.list(e)).map(({keys:r,token:n,...a})=>a),_=async e=>{if(typeof e!="string")return e;const t=await f.get(e);if(t===void 0)throw new v("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${e}"`);return t},O=async(e,t,r)=>{let n,a,s;try{n=await h.sendToChannel("push",{...t,to:B(e)}),a=S(n),s=U(n,a,e.kind)}catch(c){s="failed",a=c instanceof Error?c.message:String(c)}try{s==="accepted"?await f.markStatus(e.id,"ok"):s==="gone"?await f.delete(e.id):await f.markStatus(e.id,"failed",a)}catch{}return s==="failed"&&y("push",e.kind,{error:a,subscriptionId:e.id,userId:e.userId??null}),r&&p("push",e.kind,s),{error:a,receipt:n,status:s}},D=async(e,t)=>{const r=await j(t,C,async s=>{const{error:c,status:i}=await O(s,e,!1);return{error:c,kind:s.kind,status:i,subscription:s}}),n=new Map;for(const{kind:s,status:c}of r){const i=`${s} ${c}`,m=n.get(i);m===void 0?n.set(i,{count:1,kind:s,status:c}):m.count+=1}for(const{count:s,kind:c,status:i}of n.values())p("push",c,i,s);const a=r.map(({error:s,status:c,subscription:i})=>c==="accepted"?{id:i.id,status:"ok"}:c==="gone"?{error:s,id:i.id,status:"expired"}:{error:s,id:i.id,status:"failed"});return{failed:a.filter(s=>s.status==="failed").length,outcomes:a,pruned:a.filter(s=>s.status==="expired").length,sent:a.filter(s=>s.status==="ok").length,total:a.length}},E=async(e,t)=>{if(t?.limit!==void 0&&t.limit<=0)return{nextCursor:void 0,result:{failed:0,outcomes:[],pruned:0,sent:0,total:0}};const r=t?.limit!==void 0&&t.limit>0?Math.trunc(t.limit):void 0,n=r===void 0?b:Math.min(r,b),a=await f.list({after:t?.after,kind:t?.kind,limit:n+1,userId:t?.userId}),s=t?.after===void 0?a:a.filter($=>$.id>t.after),c=s.length>n,i=c?s.slice(0,n):s;i.length===0&&t?.after===void 0&&A("push","no-subscriptions-matched");const m=await D(e,i);return{nextCursor:c?i[i.length-1]?.id:void 0,result:m}},N={broadcast:async(e,t)=>{const r={failed:0,outcomes:[],pruned:0,sent:0,total:0};let n=t?.after;const a=t?.limit;if(a!==void 0&&a<=0)return r;for(;;){const s=a===void 0?{...t,after:n}:{...t,after:n,limit:a-r.total},{nextCursor:c,result:i}=await E(e,s);if(r.failed+=i.failed,r.pruned+=i.pruned,r.sent+=i.sent,r.total+=i.total,r.outcomes.push(...i.outcomes),a!==void 0&&r.total>=a||c===void 0||c===n)break;n=c}return r},broadcastPage:E,list:e=>T(e),register:e=>("token"in e||I(),f.put(R(e,void 0,{allowedPushOrigins:o.allowedPushOrigins}))),send:async(e,t)=>{const{error:r,receipt:n}=await O(await _(e),t,!0);if(n===void 0)throw new v("INTERNAL",`@lunora/notify: push send failed: ${r??"unknown error"}`);return n},unregister:async(e,t)=>{await f.deleteOwned(e,t.userId??null)}},k=async(e,t)=>{if(h.getProvider(e)===void 0)throw A(e,"channel-not-configured"),new v("BAD_REQUEST",`@lunora/notify: the "${e}" channel is not configured in defineNotify(...)`);const r=await h.sendToChannel(e,t),n=r.successful?"accepted":"failed";return p(e,r.provider,n),n==="failed"&&y(e,r.provider,{error:S(r)}),r};return{notify:{chat:e=>k("chat",e),inApp:e=>k("inapp",e),push:N,send:async e=>{const t=await h.send(e);for(const r of t){const n=r.channel??"unknown",a=r.successful?"accepted":"failed";p(n,r.provider,a),a==="failed"&&y(n,r.provider,{error:S(r)})}return t},webhook:e=>k("webhook",e)},push:N}};export{Y as createNotify};
@@ -0,0 +1 @@
1
+ import{LunoraError as h}from"@lunora/errors";import{c as S,l as N}from"./normalize-BdOnBfM2.mjs";const R=e=>/^[A-Z_]\w*$/i.test(e),T=e=>{const i={createdAt:e.created_at,id:e.id,kind:e.kind,lastSeenAt:e.last_seen_at,userId:e.user_id};if(e.endpoint!==null&&(i.endpoint=e.endpoint),e.p256dh!==null&&e.auth!==null&&(i.keys={auth:e.auth,p256dh:e.p256dh}),e.token!==null&&(i.token=e.token),e.last_status!==null&&(i.lastStatus=e.last_status),e.last_error!==null&&(i.lastError=e.last_error),e.metadata!==null)try{i.metadata=JSON.parse(e.metadata)}catch{}return i},A=(e,i={})=>{const n=i.tableName??"lunora_push_subscriptions";if(!R(n))throw new h("BAD_REQUEST",`@lunora/notify: d1SubscriptionStore tableName "${n}" is not a bare SQL identifier`);let s;const r=()=>(s===void 0&&(s=e.prepare(`CREATE TABLE IF NOT EXISTS ${n} (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(()=>e.prepare(`CREATE INDEX IF NOT EXISTS ${n}_user_id_idx ON ${n} (user_id)`).run()).then(()=>e.prepare(`CREATE INDEX IF NOT EXISTS ${n}_kind_idx ON ${n} (kind)`).run()).then(()=>{}),s.catch(()=>{s=void 0})),s),E=async t=>{await r();const a=await e.prepare(`SELECT * FROM ${n} WHERE id = ?1`).bind(t).first();return a===null?void 0:T(a)};return{delete:async t=>{await r(),await e.prepare(`DELETE FROM ${n} WHERE id = ?1`).bind(t).run()},deleteOwned:async(t,a)=>(await r(),await(a===null?e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id IS NULL RETURNING id`).bind(t):e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id = ?2 RETURNING id`).bind(t,a)).first()!==null),get:E,list:async t=>{await r();const a=[],d=[];t?.kind!==void 0&&(d.push(t.kind),a.push(`kind = ?${d.length.toString()}`)),t?.userId!==void 0&&(t.userId===null?a.push("user_id IS NULL"):(d.push(t.userId),a.push(`user_id = ?${d.length.toString()}`))),t?.after!==void 0&&(d.push(t.after),a.push(`id > ?${d.length.toString()}`));const l=a.length===0?"":` WHERE ${a.join(" AND ")}`,u=" ORDER BY id ASC";let o="";t?.limit!==void 0&&t.limit>0&&(d.push(Math.trunc(t.limit)),o=` LIMIT ?${d.length.toString()}`);const{results:_}=await e.prepare(`SELECT * FROM ${n}${l}${u}${o}`).bind(...d).all();return _.map(p=>T(p))},markStatus:async(t,a,d)=>{await r(),await e.prepare(`UPDATE ${n} SET last_status = ?2, last_error = ?3, last_seen_at = ?4 WHERE id = ?1`).bind(t,a,d??null,Date.now()).run()},put:async t=>{await r(),await e.prepare(`INSERT INTO ${n} (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 WHERE ${n}.user_id IS NULL OR ${n}.user_id = ?7`).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 a=await E(t.id),d=S(a,t);if(d!==void 0)throw d;const l=N(t);if(l!==void 0&&l!==t.id){const u=t.userId??null;await(u===null?e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND user_id IS NULL`).bind(l):e.prepare(`DELETE FROM ${n} WHERE id = ?1 AND (user_id IS NULL OR user_id = ?2)`).bind(l,u)).run()}return a??t}}};export{A as d1SubscriptionStore};
@@ -0,0 +1 @@
1
+ const t=e=>{if(e.webPush!==void 0&&typeof e.webPush!="function"&&typeof e.webPush!="object")throw new TypeError("defineNotify: `webPush` must be a WebPushConfig object or an `(env) => WebPushConfig` function");if(e.fcm!==void 0&&typeof e.fcm!="function"&&typeof e.fcm!="object")throw new TypeError("defineNotify: `fcm` must be an FcmConfig object or an `(env) => FcmConfig` function");if(e.store!==void 0&&typeof e.store!="function")throw new TypeError("defineNotify: `store` must be a function `(env) => SubscriptionStore` when provided");if(e.allowedPushOrigins!==void 0&&(!Array.isArray(e.allowedPushOrigins)||e.allowedPushOrigins.some(o=>typeof o!="string")))throw new TypeError('defineNotify: `allowedPushOrigins` must be an array of origin strings (e.g. ["https://fcm.googleapis.com"]) when provided');for(const o of["broadcastPageSize","concurrency"]){const r=e[o];if(r!==void 0&&(!Number.isInteger(r)||r<1))throw new TypeError(`defineNotify: \`${o}\` must be a positive integer when provided`)}if(e.webPush===void 0&&e.fcm===void 0)throw new TypeError("defineNotify: configure at least one push channel — `webPush` and/or `fcm`");return{...e,isLunoraNotify:!0}},i=e=>typeof e=="object"&&e!==null&&e.isLunoraNotify===!0;export{t as defineNotify,i as isNotifyDefinition};
@@ -0,0 +1 @@
1
+ import{LunoraError as d}from"@lunora/errors";import{i as c,k as f}from"./normalize-BdOnBfM2.mjs";const u=r=>r.filter(t=>t.status==="failed").map(t=>t.id),v=(r,t)=>r.send({...t,type:"lunora.push.broadcast"}),g=(r,t,s)=>{if(t===void 0)return;if(r?.limit===void 0)return{...r,after:t};const e=r.limit-s;return e>0?{...r,after:t,limit:e}:void 0},h=/no registered subscription/u,p=async(r,t,s)=>{try{const e=await r.send(s,t);if(e.successful)return{id:s,status:"ok"};const n=e.errorMessages.join("; ");return{error:n,id:s,status:c(n,f(s))?"expired":"failed"}}catch(e){const n=e instanceof Error?e.message:String(e);return{error:n,id:s,status:h.test(n)?"expired":"failed"}}},y=async(r,t,s)=>{const e=[];for(const o of s)e.push(await p(r,t,o));const n=u(e),a=e.filter(o=>o.status==="expired").length,i=e.length-n.length-a,l={failed:n.length,outcomes:e,pruned:a,sent:i,total:e.length};if(i===0&&n.length>0)throw new d("INTERNAL",`@lunora/notify: push retry failed for ${n.length.toString()} of ${e.length.toString()} subscription(s) — throwing so the queue retries and eventually dead-letters them`);return{failedIds:n,nextFilter:void 0,result:l}},w=async(r,t)=>{if(t.retryIds!==void 0&&t.retryIds.length>0)return y(r,t.payload,t.retryIds);const s=await r.broadcastPage(t.payload,t.filter);return{failedIds:u(s.result.outcomes),nextFilter:g(t.filter,s.nextCursor,s.result.total),result:s.result}};export{v as enqueuePushBroadcast,w as runPushBroadcastPage};
@@ -0,0 +1 @@
1
+ import{c as e,f as r,i as c,k as d,e as l,l as I,g as f,n as g,t as i,w as o}from"./normalize-BdOnBfM2.mjs";export{e as claimRefusal,r as fcmId,c as isGoneError,d as kindOfId,l as legacyFcmId,I as legacyIdFor,f as legacyWebPushId,g as normalizeRegisterInput,i as targetOf,o as webPushId};
@@ -0,0 +1 @@
1
+ import{c as n,l as a}from"./normalize-BdOnBfM2.mjs";const l=(t,e)=>t.id<e.id?-1:t.id>e.id?1:0,u=(t,e)=>e===void 0?!0:!(e.kind!==void 0&&t.kind!==e.kind||e.userId!==void 0&&(t.userId??null)!==e.userId),m=()=>{const t=new Map;return{delete:e=>(t.delete(e),Promise.resolve()),deleteOwned:(e,r)=>{const o=t.get(e);return o===void 0||(o.userId??null)!==r?Promise.resolve(!1):(t.delete(e),Promise.resolve(!0))},get:e=>Promise.resolve(t.get(e)),list:e=>{const r=[];for(const d of t.values())u(d,e)&&r.push(d);r.sort(l);const o=e?.after===void 0?r:r.filter(d=>d.id>e.after),s=e?.limit!==void 0&&e.limit>0?o.slice(0,Math.trunc(e.limit)):o;return Promise.resolve(s)},markStatus:(e,r,o)=>{const s=t.get(e);return s!==void 0&&t.set(e,{...s,lastError:o,lastSeenAt:Date.now(),lastStatus:r}),Promise.resolve()},put:e=>{const r=t.get(e.id),o=n(r,e);if(o!==void 0)return Promise.reject(o);const s=r===void 0?e:{...r,...e,createdAt:r.createdAt};t.set(s.id,s);const d=a(e),i=d===void 0||d===e.id?void 0:t.get(d);return i!==void 0&&n(i,e)===void 0&&t.delete(i.id),Promise.resolve(s)}}};export{m as memorySubscriptionStore};
@@ -0,0 +1 @@
1
+ import{LunoraError as c}from"@lunora/errors";const T=/^\d{1,3}$/u,S=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,I=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,P=/^::(\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,N=/^\[|\]$/gu,k=/\.$/u,h=t=>{const e=t.split(".");if(e.length!==4)return;const n=e.map(r=>T.test(r)?Number(r):-1);if(!n.some(r=>r<0||r>255))return[n[0],n[1],n[2],n[3]]},l=([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,E=(t,e)=>{const n=Number.parseInt(t??"",16),r=Number.parseInt(e??"",16);return!Number.isFinite(n)||!Number.isFinite(r)?!0:l([Math.floor(n/256),n%256,Math.floor(r/256),r%256])},$=t=>{const e=t.toLowerCase(),n=S.exec(e);if(n)return E(n[1],n[2]);const r=I.exec(e);if(r){const i=h(r[1]??"");return i===void 0||l(i)}const s=P.exec(e);if(s){const i=h(s[1]??"");return i===void 0||l(i)}const o=O.exec(e);return o?E(o[1],o[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")},B=t=>t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal")||t.endsWith(".home.arpa"),R=t=>t.replaceAll(N,"").replace(k,"").toLowerCase(),W=t=>{const e=R(t);if(e.includes(":"))return $(e);const n=h(e);return n===void 0?B(e):l(n)},x=2166136261,F=16777619,M=(t,e=x)=>{let n=e;for(let r=0;r<t.length;r+=1)n^=t.charCodeAt(r),n=Math.imul(n,F);return(n>>>0).toString(16).padStart(8,"0")},u=t=>t.toString(16).padStart(4,"0"),b=t=>{let e=8997,n=33826,r=40164,s=52210;for(let o=0;o<t.length;o+=1){e^=t.charCodeAt(o);const i=e*435,d=n*435,a=r*435+e*256,v=s*435+n*256,p=d+(i>>>16),g=a+(p>>>16),A=v+(g>>>16);e=i&65535,n=p&65535,r=g&65535,s=A&65535}return u(s)+u(r)+u(n)+u(e)},y=4096,H=2048,L=2048,_=512,f=(t,e,n)=>{const r=new TextEncoder().encode(t).length;if(r>e)throw new c("BAD_REQUEST",`@lunora/notify: register() \`${n}\` is ${r.toString()} bytes, exceeding the ${e.toString()}-byte cap`)},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 c("BAD_REQUEST","@lunora/notify: register() `metadata` must be a plain object");let r;try{r=JSON.stringify(t)}catch(o){throw new c("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is not JSON-serialisable: ${o instanceof Error?o.message:String(o)}`)}const s=new TextEncoder().encode(r).length;if(s>y)throw new c("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is ${s.toString()} bytes, exceeding the ${y.toString()}-byte cap`);return t},U=t=>`wp2_${b(t)}`,C=t=>`fcm2_${b(t)}`,K=t=>t.startsWith("fcm2_")||t.startsWith("fcm_")?"fcm":t.startsWith("wp2_")||t.startsWith("wp_")?"web-push":void 0,m=t=>M(t),Q=t=>`wp_${m(t)}`,V=t=>`fcm_${m(t)}`,q=t=>t.kind==="fcm"?t.token===void 0?void 0:V(t.token):t.endpoint===void 0?void 0:Q(t.endpoint),Z=(t,e)=>{const n=t?.userId??null;if(!(t===void 0||n===null||n===(e.userId??null)))return new c("FORBIDDEN",`@lunora/notify: subscription "${e.id}" is registered to a different user; a device must be unregistered by its owner before another account can claim it`)},X=t=>{if(typeof t!="string")return t??{};try{return JSON.parse(t)}catch(e){throw new c("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 c("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must be an absolute https URL (got "${t}")`)}if(n.protocol!=="https:")throw new c("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 c("FORBIDDEN",`@lunora/notify: register() web-push endpoint origin "${n.origin}" is not in the configured allowedPushOrigins allowlist`);return}if(W(n.hostname))throw new c("FORBIDDEN",`@lunora/notify: register() web-push endpoint host "${n.hostname}" is a private/internal address; configure allowedPushOrigins to permit a specific origin`)},tt=(t,e=Date.now(),n={})=>{if(t.kind===void 0?t.token!==void 0:t.kind==="fcm"){const{token:a}=t;if(typeof a!="string"||a==="")throw new c("BAD_REQUEST","@lunora/notify: register() fcm input requires a non-empty `token`");return f(a,L,"token"),{createdAt:e,id:C(a),kind:"fcm",lastSeenAt:e,metadata:w(t.metadata),token:a,userId:t.userId??null}}const s=X(t.subscription),{endpoint:o}=s,i=s.keys?.p256dh,d=s.keys?.auth;if(typeof o!="string"||o===""||typeof i!="string"||typeof d!="string")throw new c("BAD_REQUEST","@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");return f(o,H,"endpoint"),f(d,_,"keys.auth"),f(i,_,"keys.p256dh"),j(o,n.allowedPushOrigins),{createdAt:e,endpoint:o,id:U(o),keys:{auth:d,p256dh:i},kind:"web-push",lastSeenAt:e,metadata:w(t.metadata),userId:t.userId??null}},et=t=>t.kind==="fcm"?t.token??"":JSON.stringify({endpoint:t.endpoint,keys:t.keys}),G=/\bhttp\s*4(?:04|10)\b/iu,J=/\b(?:unregistered|not[\s-]?registered|registration-token-not-registered|requested entity was not found)\b/iu,Y=/\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu,nt=(t,e)=>t===void 0?!1:G.test(t)||Y.test(t)?!0:e!=="web-push"&&J.test(t);export{R as a,l as b,Z as c,$ as d,V as e,C as f,Q as g,nt as i,K as k,q as l,tt as n,h as p,et as t,U as w};
package/dist/web.d.mts CHANGED
@@ -1,18 +1,3 @@
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
1
  interface SerializedPushSubscription {
17
2
  endpoint: string;
18
3
  expirationTime: number | null;
@@ -21,6 +6,29 @@ interface SerializedPushSubscription {
21
6
  p256dh: string;
22
7
  };
23
8
  }
9
+ /**
10
+ * What {@link subscribeToPush} returns: the (new or reused) subscription, plus
11
+ * the endpoint of the one it replaced, when it replaced one.
12
+ */
13
+ interface SubscribeToPushResult {
14
+ /**
15
+ * The endpoint of the subscription this call dropped — set ONLY on the
16
+ * VAPID-rotation path, where the stale subscription is unsubscribed and a
17
+ * new one minted under the current key.
18
+ *
19
+ * Send it to the server and unregister it — owner-scoped, since this is a
20
+ * caller-supplied key
21
+ * (`ctx.push.unregister(webPushId(replacedEndpoint), { userId: ctx.auth?.userId })`). The new subscription
22
+ * carries a NEW endpoint, hence a new store id, so it never upserts over the
23
+ * old row — and `403 VapidPkHashMismatch`, which every send to that row now
24
+ * answers, is correctly not a "gone" signal, so nothing prunes it either.
25
+ * Dropped instead of returned, the row is billed a POST and a write on every
26
+ * later broadcast, forever.
27
+ */
28
+ replacedEndpoint?: string;
29
+ /** The active subscription, in the serialisable shape `ctx.push.register` accepts. */
30
+ subscription: SerializedPushSubscription;
31
+ }
24
32
  /** Options for {@link subscribeToPush}. */
25
33
  interface SubscribeToPushOptions {
26
34
  /**
@@ -39,14 +47,20 @@ interface SubscribeToPushOptions {
39
47
  */
40
48
  vapidPublicKey: string;
41
49
  }
42
- /** Whether the current browser supports the Web Push flow (service workers + Push API). */
50
+ /**
51
+ * Whether the current browser supports the Web Push flow (service workers +
52
+ * Push API + the Notifications API). `Notification` is part of the check because
53
+ * {@link subscribeToPush} calls `Notification.requestPermission()` — without it
54
+ * a browser missing the API got a bare `ReferenceError` rather than the
55
+ * "not supported" error this predicate exists to produce.
56
+ */
43
57
  declare const isPushSupported: () => boolean;
44
58
  /**
45
59
  * Register (or reuse) a service worker and subscribe the browser to Web Push,
46
60
  * returning the subscription in serialisable form. Reuses an existing subscription
47
61
  * when present. Throws if push is unsupported or the user denies permission.
48
62
  */
49
- declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SerializedPushSubscription>;
63
+ declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SubscribeToPushResult>;
50
64
  /** Unsubscribe the browser's current Web Push subscription. Returns whether one was removed. */
51
65
  declare const unsubscribeFromPush: () => Promise<boolean>;
52
- export { type SerializedPushSubscription, type SubscribeToPushOptions, isPushSupported, subscribeToPush, unsubscribeFromPush };
66
+ export { type SerializedPushSubscription, type SubscribeToPushOptions, type SubscribeToPushResult, isPushSupported, subscribeToPush, unsubscribeFromPush };
package/dist/web.d.ts CHANGED
@@ -1,18 +1,3 @@
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
1
  interface SerializedPushSubscription {
17
2
  endpoint: string;
18
3
  expirationTime: number | null;
@@ -21,6 +6,29 @@ interface SerializedPushSubscription {
21
6
  p256dh: string;
22
7
  };
23
8
  }
9
+ /**
10
+ * What {@link subscribeToPush} returns: the (new or reused) subscription, plus
11
+ * the endpoint of the one it replaced, when it replaced one.
12
+ */
13
+ interface SubscribeToPushResult {
14
+ /**
15
+ * The endpoint of the subscription this call dropped — set ONLY on the
16
+ * VAPID-rotation path, where the stale subscription is unsubscribed and a
17
+ * new one minted under the current key.
18
+ *
19
+ * Send it to the server and unregister it — owner-scoped, since this is a
20
+ * caller-supplied key
21
+ * (`ctx.push.unregister(webPushId(replacedEndpoint), { userId: ctx.auth?.userId })`). The new subscription
22
+ * carries a NEW endpoint, hence a new store id, so it never upserts over the
23
+ * old row — and `403 VapidPkHashMismatch`, which every send to that row now
24
+ * answers, is correctly not a "gone" signal, so nothing prunes it either.
25
+ * Dropped instead of returned, the row is billed a POST and a write on every
26
+ * later broadcast, forever.
27
+ */
28
+ replacedEndpoint?: string;
29
+ /** The active subscription, in the serialisable shape `ctx.push.register` accepts. */
30
+ subscription: SerializedPushSubscription;
31
+ }
24
32
  /** Options for {@link subscribeToPush}. */
25
33
  interface SubscribeToPushOptions {
26
34
  /**
@@ -39,14 +47,20 @@ interface SubscribeToPushOptions {
39
47
  */
40
48
  vapidPublicKey: string;
41
49
  }
42
- /** Whether the current browser supports the Web Push flow (service workers + Push API). */
50
+ /**
51
+ * Whether the current browser supports the Web Push flow (service workers +
52
+ * Push API + the Notifications API). `Notification` is part of the check because
53
+ * {@link subscribeToPush} calls `Notification.requestPermission()` — without it
54
+ * a browser missing the API got a bare `ReferenceError` rather than the
55
+ * "not supported" error this predicate exists to produce.
56
+ */
43
57
  declare const isPushSupported: () => boolean;
44
58
  /**
45
59
  * Register (or reuse) a service worker and subscribe the browser to Web Push,
46
60
  * returning the subscription in serialisable form. Reuses an existing subscription
47
61
  * when present. Throws if push is unsupported or the user denies permission.
48
62
  */
49
- declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SerializedPushSubscription>;
63
+ declare const subscribeToPush: (options: SubscribeToPushOptions) => Promise<SubscribeToPushResult>;
50
64
  /** Unsubscribe the browser's current Web Push subscription. Returns whether one was removed. */
51
65
  declare const unsubscribeFromPush: () => Promise<boolean>;
52
- export { type SerializedPushSubscription, type SubscribeToPushOptions, isPushSupported, subscribeToPush, unsubscribeFromPush };
66
+ export { type SerializedPushSubscription, type SubscribeToPushOptions, type SubscribeToPushResult, isPushSupported, subscribeToPush, unsubscribeFromPush };
package/dist/web.mjs CHANGED
@@ -1 +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};
1
+ const p=r=>{const e=atob(r),t=new Uint8Array(e.length);for(let i=0;i<e.length;i+=1)t[i]=e.codePointAt(i)??0;return t},b=r=>{const e=r.replaceAll("-","+").replaceAll("_","/"),t=e+"=".repeat((4-e.length%4)%4);return p(t)},a=r=>b(r),d=(r,e)=>r.length===e.length&&r.every((t,i)=>t===e[i]),g=(r,e)=>{const t=r.options.applicationServerKey;return t===null?!1:d(new Uint8Array(t),a(e))},s=globalThis,c=()=>s.navigator?.serviceWorker!==void 0&&s.PushManager!==void 0&&s.Notification!==void 0,v=async r=>{if(!c())throw new Error("@lunora/notify: Web Push is not supported in this browser (needs service workers + PushManager + Notification)");let e;if(r.serviceWorkerUrl===void 0)e=await navigator.serviceWorker.ready;else{const u=r.scope===void 0?void 0:{scope:r.scope};e=await navigator.serviceWorker.register(r.serviceWorkerUrl,u)}const t=await Notification.requestPermission();if(t!=="granted")throw new Error(`@lunora/notify: notification permission was not granted (got "${t}")`);const i=await e.pushManager.getSubscription();let n=null,o;i!==null&&(g(i,r.vapidPublicKey)?n=i:(o=i.endpoint,await i.unsubscribe()));const l=n??await e.pushManager.subscribe({applicationServerKey:a(r.vapidPublicKey),userVisibleOnly:!0});return{replacedEndpoint:o,subscription:l.toJSON()}},f=async()=>{if(!c())return!1;const e=await(await navigator.serviceWorker.ready).pushManager.getSubscription();return e===null?!1:e.unsubscribe()};export{c as isPushSupported,v as subscribeToPush,f as unsubscribeFromPush};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/notify",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.60",
4
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
6
  "cloudflare",
@@ -49,8 +49,8 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@lunora/errors": "1.0.0-alpha.9",
53
- "@visulima/notification": "1.0.5"
52
+ "@lunora/errors": "1.0.0-alpha.39",
53
+ "@visulima/notification": "1.1.1"
54
54
  },
55
55
  "engines": {
56
56
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
1
- import{LunoraError as v}from"@lunora/errors";import{buildEngine as R}from"./buildEngine-Jlzf2Jzn.mjs";import{memorySubscriptionStore as x}from"./memorySubscriptionStore-BxKdyxjC.mjs";import{normalizeRegisterInput as C,targetOf as W,isGoneError as B}from"./fcmId-CLRyQJVw.mjs";const w=(t,s)=>typeof t=="function"?t(s):t,b=t=>t.successful?void 0:t.errorMessages.join("; "),L=(t,s)=>t.successful?"accepted":B(s)?"gone":"failed",_=async(t,s,l)=>{const a=Array.from({length:t.length});let f=0;const g=async()=>{for(;f<t.length;){const c=f;f+=1,a[c]=await l(t[c])}};return await Promise.all(Array.from({length:Math.min(s,t.length)},()=>g())),a},j=(t,s)=>({chat:w(t.chat,s),fcm:w(t.fcm,s),inApp:w(t.inApp,s),webhook:w(t.webhook,s),webPush:w(t.webPush,s)}),E=new WeakMap,Q=(t,s)=>{let l=E.get(t);l===void 0&&(l=new WeakMap,E.set(t,l));let a=l.get(s);return a===void 0&&(a={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},l.set(s,a)),a},F=(t,s,l={})=>{const a=Q(t,s);let f;l.engine===void 0?(a.engine??=R(j(t,s)),f=a.engine):f=l.engine,a.store??=t.store?.(s);let{store:g}=a;g===void 0&&(a.fallbackStore??=x(),!l.silent&&!a.warnedNoStore&&(a.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.")),g=a.fallbackStore);const c=g,T=Math.max(1,l.concurrency??10),{log:$,metrics:S}=l,y=(e,o,n,i=1)=>{S?.count("notify.send",i,{channel:e,provider:o??e,status:n})},m=(e,o,n)=>{$?.warn(`notify ${e} delivery failed`,{channel:e,provider:o??e,status:"failed",...n})},P=(e,o)=>{S?.count("notify.skipped",1,{channel:e,reason:o})},I=()=>{const e=t.allowedPushOrigins!==void 0&&t.allowedPushOrigins.length>0;l.silent||e||a.warnedNoPushOriginAllowlist||(a.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."))},D=async e=>(await c.list(e)).map(({keys:o,token:n,...i})=>i),M=async e=>{if(typeof e!="string")return e;const o=await c.get(e);if(o===void 0)throw new v("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${e}"`);return o},N=async(e,o,n)=>{let i,u,d;try{i=await f.sendToChannel("push",{...o,to:W(e)}),u=b(i),d=L(i,u)}catch(r){d="failed",u=r instanceof Error?r.message:String(r)}try{d==="accepted"?await c.markStatus(e.id,"ok"):d==="gone"?await c.delete(e.id):await c.markStatus(e.id,"failed",u)}catch{}return d==="failed"&&m("push",e.kind,{error:u,subscriptionId:e.id,userId:e.userId??null}),n&&y("push",e.kind,d),{error:u,receipt:i,status:d}},O={broadcast:async(e,o)=>{const n=await c.list(o);n.length===0&&P("push","no-subscriptions-matched");const i=await _(n,T,async r=>{const{error:p,status:h}=await N(r,e,!1);return{error:p,kind:r.kind,status:h,subscription:r}}),u=new Map;for(const{kind:r,status:p}of i){const h=`${r}\0${p}`,A=u.get(h);A===void 0?u.set(h,{count:1,kind:r,status:p}):A.count+=1}for(const{count:r,kind:p,status:h}of u.values())y("push",p,h,r);const d=i.map(({error:r,status:p,subscription:h})=>p==="accepted"?{id:h.id,status:"ok"}:p==="gone"?{error:r,id:h.id,status:"expired"}:{error:r,id:h.id,status:"failed"});return{failed:d.filter(r=>r.status==="failed").length,outcomes:d,pruned:d.filter(r=>r.status==="expired").length,sent:d.filter(r=>r.status==="ok").length,total:d.length}},list:e=>D(e),register:e=>("token"in e||I(),c.put(C(e,void 0,{allowedPushOrigins:t.allowedPushOrigins}))),send:async(e,o)=>{const{error:n,receipt:i}=await N(await M(e),o,!0);if(i===void 0)throw new v("INTERNAL",`@lunora/notify: push send failed: ${n??"unknown error"}`);return i},unregister:e=>c.delete(e)},k=async(e,o)=>{if(f.getProvider(e)===void 0)throw P(e,"channel-not-configured"),new v("BAD_REQUEST",`@lunora/notify: the "${e}" channel is not configured in defineNotify(...)`);const n=await f.sendToChannel(e,o),i=n.successful?"accepted":"failed";return y(e,n.provider,i),i==="failed"&&m(e,n.provider,{error:b(n)}),n};return{notify:{chat:e=>k("chat",e),inApp:e=>k("inapp",e),push:O,send:async e=>{const o=await f.send(e);for(const n of o){const i=n.channel??"unknown",u=n.successful?"accepted":"failed";y(i,n.provider,u),u==="failed"&&m(i,n.provider,{error:b(n)})}return o},webhook:e=>k("webhook",e)},push:O}};export{F as createNotify};
@@ -1 +0,0 @@
1
- import{LunoraError as _}from"@lunora/errors";import{legacyIdFor as h}from"./fcmId-CLRyQJVw.mjs";const o=a=>{const i={createdAt:a.created_at,id:a.id,kind:a.kind,lastSeenAt:a.last_seen_at,userId:a.user_id};if(a.endpoint!==null&&(i.endpoint=a.endpoint),a.p256dh!==null&&a.auth!==null&&(i.keys={auth:a.auth,p256dh:a.p256dh}),a.token!==null&&(i.token=a.token),a.last_status!==null&&(i.lastStatus=a.last_status),a.last_error!==null&&(i.lastError=a.last_error),a.metadata!==null)try{i.metadata=JSON.parse(a.metadata)}catch{}return i},S=/^[A-Za-z_]\w*$/u,I=(a,i={})=>{const r=i.tableName??"lunora_push_subscriptions";if(!S.test(r))throw new _("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=[],e=[];t?.kind!==void 0&&(e.push(t.kind),n.push(`kind = ?${e.length.toString()}`)),t?.userId!==void 0&&(t.userId===null?n.push("user_id IS NULL"):(e.push(t.userId),n.push(`user_id = ?${e.length.toString()}`)));const E=n.length===0?"":` WHERE ${n.join(" AND ")}`;let u="";t?.limit!==void 0&&t.limit>0&&(e.push(Math.trunc(t.limit)),u=` LIMIT ?${e.length.toString()}`);const{results:p}=await a.prepare(`SELECT * FROM ${r}${E}${u}`).bind(...e).all();return p.map(T=>o(T))},markStatus:async(t,n,e)=>{await d(),await a.prepare(`UPDATE ${r} SET last_status = ?2, last_error = ?3, last_seen_at = ?4 WHERE id = ?1`).bind(t,n,e??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=h(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{I as d1SubscriptionStore};
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
1
- import{LunoraError as a}from"@lunora/errors";const e=(o,r)=>o.send({...r,type:"lunora.push.broadcast"}),s=async(o,r)=>{const t=await o.broadcast(r.payload,r.filter);if(t.failed>0)throw new a("INTERNAL",`@lunora/notify: push broadcast had ${t.failed.toString()} transient failure(s) of ${t.total.toString()} subscription(s) (${t.sent.toString()} sent, ${t.pruned.toString()} pruned) — throwing so the queue retries`);return t};export{e as enqueuePushBroadcast,s as runPushBroadcastJob};
@@ -1 +0,0 @@
1
- import{LunoraError as u}from"@lunora/errors";const k=/^\d{1,3}$/u,E=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,I=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,S=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,v=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,A=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,W=/^\[|\]$/gu,N=/\.$/u,l=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,p=(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])},R=t=>{const e=t.toLowerCase(),n=E.exec(e);if(n)return p(n[1],n[2]);const r=I.exec(e);if(r){const o=l(r[1]??"");return o===void 0||f(o)}const i=S.exec(e);if(i){const o=l(i[1]??"");return o===void 0||f(o)}const s=v.exec(e);return s?p(s[1],s[2]):A.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")},_=t=>t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal")||t.endsWith(".home.arpa"),D=t=>t.replaceAll(W,"").replace(N,"").toLowerCase(),O=t=>{const e=D(t);if(e.includes(":"))return R(e);const n=l(e);return n===void 0?_(e):f(n)},d=t=>t.toString(16).padStart(4,"0"),g=t=>{let e=8997,n=33826,r=40164,i=52210;for(let s=0;s<t.length;s+=1){const o=t.codePointAt(s)??0;e^=o&65535,n^=o>>>16&65535;const a=e*435,w=n*435,b=r*435+e*256,y=i*435+n*256,c=w+(a>>>16),h=b+(c>>>16),$=y+(h>>>16);e=a&65535,n=c&65535,r=h&65535,i=$&65535}return d(i)+d(r)+d(n)+d(e)},P=t=>`wp2_${g(t)}`,B=t=>`fcm2_${g(t)}`,m=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")},U=t=>`wp_${m(t)}`,x=t=>`fcm_${m(t)}`,q=t=>t.kind==="fcm"?t.token===void 0?void 0:x(t.token):t.endpoint===void 0?void 0:U(t.endpoint),F=t=>{if(typeof t!="string")return t??{};try{return JSON.parse(t)}catch(e){throw new u("BAD_REQUEST",`@lunora/notify: register() web-push subscription is not valid JSON: ${e instanceof Error?e.message:String(e)}`)}},T=(t,e)=>{let n;try{n=new URL(t)}catch{throw new u("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must be an absolute https URL (got "${t}")`)}if(n.protocol!=="https:")throw new u("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 u("FORBIDDEN",`@lunora/notify: register() web-push endpoint origin "${n.origin}" is not in the configured allowedPushOrigins allowlist`);return}if(O(n.hostname))throw new u("FORBIDDEN",`@lunora/notify: register() web-push endpoint host "${n.hostname}" is a private/internal address; configure allowedPushOrigins to permit a specific origin`)},C=(t,e=Date.now(),n={})=>{if("token"in t){const{token:a}=t;if(typeof a!="string"||a==="")throw new u("BAD_REQUEST","@lunora/notify: register() fcm input requires a non-empty `token`");return{createdAt:e,id:B(a),kind:"fcm",lastSeenAt:e,metadata:t.metadata,token:a,userId:t.userId??null}}const r=F(t.subscription),{endpoint:i}=r,s=r.keys?.p256dh,o=r.keys?.auth;if(typeof i!="string"||i===""||typeof s!="string"||typeof o!="string")throw new u("BAD_REQUEST","@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");return T(i,n.allowedPushOrigins),{createdAt:e,endpoint:i,id:P(i),keys:{auth:o,p256dh:s},kind:"web-push",lastSeenAt:e,metadata:t.metadata,userId:t.userId??null}},z=t=>t.kind==="fcm"?t.token??"":JSON.stringify({endpoint:t.endpoint,keys:t.keys}),L=/\bhttp\s*4(?:04|10)\b/iu,Q=/\b(?:unregistered|not[\s-]?registered|registration-token-not-registered)\b/iu,J=/\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu,G=t=>t===void 0?!1:L.test(t)||Q.test(t)||J.test(t);export{B as fcmId,G as isGoneError,x as legacyFcmId,q as legacyIdFor,U as legacyWebPushId,C as normalizeRegisterInput,z as targetOf,P as webPushId};
@@ -1 +0,0 @@
1
- import{legacyIdFor as i}from"./fcmId-CLRyQJVw.mjs";const d=(e,t)=>t===void 0?!0:!(t.kind!==void 0&&e.kind!==t.kind||t.userId!==void 0&&(e.userId??null)!==t.userId),l=()=>{const e=new Map;return{delete:t=>(e.delete(t),Promise.resolve()),get:t=>Promise.resolve(e.get(t)),list:t=>{const r=[];for(const o of e.values())d(o,t)&&r.push(o);const s=t?.limit!==void 0&&t.limit>0?r.slice(0,Math.trunc(t.limit)):r;return Promise.resolve(s)},markStatus:(t,r,s)=>{const o=e.get(t);return o!==void 0&&e.set(t,{...o,lastError:s,lastSeenAt:Date.now(),lastStatus:r}),Promise.resolve()},put:t=>{const r=i(t);r!==void 0&&r!==t.id&&e.delete(r);const s=e.get(t.id),o=s===void 0?t:{...s,...t,createdAt:s.createdAt};return e.set(o.id,o),Promise.resolve(o)}}};export{l as memorySubscriptionStore};