@lunora/notify 1.0.0-alpha.19 → 1.0.0-alpha.20

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.d.mts CHANGED
@@ -689,4 +689,80 @@ declare const targetOf: (subscription: StoredSubscription) => string;
689
689
  * (a cert/session expiry) can never permanently drop a valid subscription.
690
690
  */
691
691
  declare const isGoneError: (message: string | undefined) => boolean;
692
- export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions, FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore, WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast, fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf, webPushFromEnv, webPushId };
692
+ export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions,
693
+ /**
694
+ * `@lunora/notify`
695
+ *
696
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
697
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
698
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
699
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
700
+ * → `ctx.flags`).
701
+ *
702
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
703
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
704
+ * deliberately **not** on the edge facade — route heavy fan-out through
705
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
706
+ *
707
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
708
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
709
+ * @packageDocumentation
710
+ */
711
+ FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
712
+ /**
713
+ * `@lunora/notify`
714
+ *
715
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
716
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
717
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
718
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
719
+ * → `ctx.flags`).
720
+ *
721
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
722
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
723
+ * deliberately **not** on the edge facade — route heavy fan-out through
724
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
725
+ *
726
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
727
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
728
+ * @packageDocumentation
729
+ */
730
+ WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast,
731
+ /**
732
+ * `@lunora/notify`
733
+ *
734
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
735
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
736
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
737
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
738
+ * → `ctx.flags`).
739
+ *
740
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
741
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
742
+ * deliberately **not** on the edge facade — route heavy fan-out through
743
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
744
+ *
745
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
746
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
747
+ * @packageDocumentation
748
+ */
749
+ fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf,
750
+ /**
751
+ * `@lunora/notify`
752
+ *
753
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
754
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
755
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
756
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
757
+ * → `ctx.flags`).
758
+ *
759
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
760
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
761
+ * deliberately **not** on the edge facade — route heavy fan-out through
762
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
763
+ *
764
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
765
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
766
+ * @packageDocumentation
767
+ */
768
+ webPushFromEnv, webPushId };
package/dist/index.d.ts CHANGED
@@ -689,4 +689,80 @@ declare const targetOf: (subscription: StoredSubscription) => string;
689
689
  * (a cert/session expiry) can never permanently drop a valid subscription.
690
690
  */
691
691
  declare const isGoneError: (message: string | undefined) => boolean;
692
- export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions, FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore, WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast, fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf, webPushFromEnv, webPushId };
692
+ export { type BroadcastOutcome, type BroadcastResult, type CreateNotifyOptions, type D1Like, type D1PreparedLike, type D1StoreOptions,
693
+ /**
694
+ * `@lunora/notify`
695
+ *
696
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
697
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
698
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
699
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
700
+ * → `ctx.flags`).
701
+ *
702
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
703
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
704
+ * deliberately **not** on the edge facade — route heavy fan-out through
705
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
706
+ *
707
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
708
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
709
+ * @packageDocumentation
710
+ */
711
+ FCM_ENV_KEYS, type FcmConfigFactory, type LunoraNotify, type LunoraPush, type NotifyConfig, type NotifyDefinition, type NotifyDeliveryStatus, type NotifyEnv, type NotifyLogger, type NotifyMetrics, type NotifySkipReason, type PushBroadcastJob, type PushSubscriptionDevice, type PushSubscriptionsResult, type QueueProducerLike, type RegisterInput, type ResolvedProviders, type RoutingPushOptions, type StoredSubscription, type SubscriptionFilter, type SubscriptionKind, type SubscriptionStatus, type SubscriptionStore,
712
+ /**
713
+ * `@lunora/notify`
714
+ *
715
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
716
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
717
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
718
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
719
+ * → `ctx.flags`).
720
+ *
721
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
722
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
723
+ * deliberately **not** on the edge facade — route heavy fan-out through
724
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
725
+ *
726
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
727
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
728
+ * @packageDocumentation
729
+ */
730
+ WEB_PUSH_ENV_KEYS, type WebPushConfigFactory, buildEngine, createNotify, d1SubscriptionStore, defineNotify, enqueuePushBroadcast,
731
+ /**
732
+ * `@lunora/notify`
733
+ *
734
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
735
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
736
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
737
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
738
+ * → `ctx.flags`).
739
+ *
740
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
741
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
742
+ * deliberately **not** on the edge facade — route heavy fan-out through
743
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
744
+ *
745
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
746
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
747
+ * @packageDocumentation
748
+ */
749
+ fcmFromEnv, fcmId, isGoneError, isNotifyDefinition, memorySubscriptionStore, normalizeRegisterInput, routingPushProvider, runPushBroadcastJob, targetOf,
750
+ /**
751
+ * `@lunora/notify`
752
+ *
753
+ * Multi-channel notifications for Lunora, wrapping the `@visulima/notification`
754
+ * engine. `defineNotify` in `lunora/notify.ts` configures the edge-safe channels
755
+ * (Web Push + FCM, plus chat / in-app inbox / webhook); codegen wires `ctx.notify`
756
+ * and its `ctx.push` alias onto every handler ctx from it (mirroring `defineFlags`
757
+ * → `ctx.flags`).
758
+ *
759
+ * Edge-safety: Web Push (VAPID + RFC 8291) and FCM (HTTP v1) run on `fetch` + Web
760
+ * Crypto under workerd. APNs (`node:http2`) and SMS / Node-only queue adapters are
761
+ * deliberately **not** on the edge facade — route heavy fan-out through
762
+ * `@lunora/queue` (`enqueuePushBroadcast` / `runPushBroadcastJob`).
763
+ *
764
+ * - `@lunora/notify` — `defineNotify`, `createNotify`, config resolvers, subscription stores and types.
765
+ * - `@lunora/notify/web` — the browser `subscribeToPush` service-worker helper.
766
+ * @packageDocumentation
767
+ */
768
+ webPushFromEnv, webPushId };
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-DSUQo7VL.mjs";import{buildEngine as d,routingPushProvider as x}from"./packem_shared/buildEngine-BV6_77DO.mjs";import{enqueuePushBroadcast as c,runPushBroadcastJob as P}from"./packem_shared/enqueuePushBroadcast-tf1LqgzA.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-BcpvEOmO.mjs";import{memorySubscriptionStore as N}from"./packem_shared/memorySubscriptionStore-0VBjZg3X.mjs";import{fcmId as g,isGoneError as y,normalizeRegisterInput as v,targetOf as B,webPushId as F}from"./packem_shared/fcmId-BQZrXP9X.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 u}from"./packem_shared/defineNotify-CUi2k7pP.mjs";import{createNotify as p}from"./packem_shared/createNotify-GgvLs28y.mjs";import{buildEngine as d,routingPushProvider as x}from"./packem_shared/buildEngine-Cl_c6i9_.mjs";import{enqueuePushBroadcast as c,runPushBroadcastJob as P}from"./packem_shared/enqueuePushBroadcast-tf1LqgzA.mjs";import{d1SubscriptionStore as b}from"./packem_shared/d1SubscriptionStore-D5VVGBxm.mjs";import{memorySubscriptionStore as N}from"./packem_shared/memorySubscriptionStore-B60C_I7v.mjs";import{fcmId as g,isGoneError as y,normalizeRegisterInput as v,targetOf as B,webPushId as F}from"./packem_shared/fcmId-ggkSOUHd.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 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 f}from"@lunora/errors";import{createNotification as p}from"@visulima/notification";import{retryMiddleware as h,circuitBreakerMiddleware as v}from"@visulima/notification/middleware";import{fcmProvider as w}from"@visulima/notification/providers/fcm";import{webPushProvider as l}from"@visulima/notification/providers/web-push";import{n as b,p as u,a as m,b as P}from"./ssrf-host-BCpHorGa.mjs";const y=(n,i)=>{if(n.size<i)return;const t=n.keys().next().value;t!==void 0&&n.delete(t)},g="https://cloudflare-dns.com/dns-query",a=1,c=28,k=2e3,A=(n,i)=>{if(i===a){const t=u(n);return t===void 0||m(t)}return P(n.toLowerCase())},d=async(n,i,t)=>{try{const e=await fetch(`${g}?name=${encodeURIComponent(n)}&type=${String(i)}`,{headers:{accept:"application/dns-json"},signal:AbortSignal.timeout(t)});return e.ok?(await e.json()).Answer??[]:void 0}catch{return}},E=async(n,i=k)=>{const t=b(n);if(t.includes(":")||u(t)!==void 0)return{kind:"unknown"};const[e,r]=await Promise.all([d(t,a,i),d(t,c,i)]);if(e===void 0&&r===void 0)return{kind:"unknown"};for(const o of[...e??[],...r??[]])if((o.type===a||o.type===c)&&A(o.data,o.type))return{address:o.data,kind:"private"};return{kind:"public"}},I=n=>{let i=n;if(typeof n=="string"){if(!n.startsWith("{"))return;try{i=JSON.parse(n)}catch{return}}const t=i?.endpoint;return typeof t=="string"?t:void 0},s=new Map,D=256,N=async(n,i)=>{if(i!==void 0&&i.length>0)return;let t;try{({hostname:t}=new URL(n))}catch{return}const e=s.get(t),r=e??E(t),o=await r;if(e===void 0&&o.kind!=="unknown"&&(y(s,D),s.set(t,r)),o.kind==="private")throw new f("FORBIDDEN",`@lunora/notify: web-push endpoint host "${t}" resolves to a private/internal address (${o.address}); refusing to send (DNS-rebinding guard)`)},O=n=>{const i=t=>{const e=t===void 0?n.fcm:n.webPush;if(e===void 0)throw new Error(t===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 e};return{channel:"push",id:"lunora-push-router",initialize:async()=>{await n.webPush?.initialize(),await n.fcm?.initialize()},isAvailable:()=>(n.webPush??n.fcm)!==void 0,send:async t=>{const r=(Array.isArray(t.to)?t.to:[t.to]).map(o=>I(o));for(const o of r)o!==void 0&&await N(o,n.allowedPushOrigins);return i(r[0]).send(t)}}},M=n=>{const i=n.webPush===void 0?void 0:l(n.webPush),t=n.fcm===void 0?void 0:w(n.fcm),e={};(i!==void 0||t!==void 0)&&(e.push=O({allowedPushOrigins:n.allowedPushOrigins,fcm:t,webPush:i})),n.chat!==void 0&&(e.chat=n.chat),n.inApp!==void 0&&(e.inapp=n.inApp),n.webhook!==void 0&&(e.webhook=n.webhook);const r=p(e);return r.use(h()).use(v()),r};export{M as buildEngine,O as routingPushProvider};
@@ -0,0 +1 @@
1
+ import{LunoraError as v}from"@lunora/errors";import{buildEngine as F}from"./buildEngine-Cl_c6i9_.mjs";import{memorySubscriptionStore as R}from"./memorySubscriptionStore-B60C_I7v.mjs";import{normalizeRegisterInput as z,targetOf as B,isGoneError as L}from"./fcmId-ggkSOUHd.mjs";const W=250,w=(s,l)=>typeof s=="function"?s(l):s,S=s=>s.successful?void 0:s.errorMessages.join("; "),U=(s,l)=>s.successful?"accepted":L(l)?"gone":"failed",j=async(s,l,d)=>{const u=Array.from({length:s.length});let h=0;const g=async()=>{for(;h<s.length;){const f=h;h+=1,u[f]=await d(s[f])}};return await Promise.all(Array.from({length:Math.min(l,s.length)},()=>g())),u},G=(s,l)=>({allowedPushOrigins:s.allowedPushOrigins,chat:w(s.chat,l),fcm:w(s.fcm,l),inApp:w(s.inApp,l),webhook:w(s.webhook,l),webPush:w(s.webPush,l)}),x=new WeakMap,Q=(s,l)=>{let d=x.get(s);d===void 0&&(d=new WeakMap,x.set(s,d));let u=d.get(l);return u===void 0&&(u={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},d.set(l,u)),u},Y=(s,l,d={})=>{const u=Q(s,l);let h;d.engine===void 0?(u.engine??=F(G(s,l)),h=u.engine):h=d.engine,u.store??=s.store?.(l);let{store:g}=u;g===void 0&&(u.fallbackStore??=R(),!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??10),b=Math.max(1,d.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=s.allowedPushOrigins!==void 0&&s.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},E=async(e,t,r)=>{let n,a,o;try{n=await h.sendToChannel("push",{...t,to:B(e)}),a=S(n),o=U(n,a)}catch(c){o="failed",a=c instanceof Error?c.message:String(c)}try{o==="accepted"?await f.markStatus(e.id,"ok"):o==="gone"?await f.delete(e.id):await f.markStatus(e.id,"failed",a)}catch{}return o==="failed"&&y("push",e.kind,{error:a,subscriptionId:e.id,userId:e.userId??null}),r&&p("push",e.kind,o),{error:a,receipt:n,status:o}},D=async(e,t)=>{const r=await j(t,C,async o=>{const{error:c,status:i}=await E(o,e,!1);return{error:c,kind:o.kind,status:i,subscription:o}}),n=new Map;for(const{kind:o,status:c}of r){const i=`${o} ${c}`,m=n.get(i);m===void 0?n.set(i,{count:1,kind:o,status:c}):m.count+=1}for(const{count:o,kind:c,status:i}of n.values())p("push",c,i,o);const a=r.map(({error:o,status:c,subscription:i})=>c==="accepted"?{id:i.id,status:"ok"}:c==="gone"?{error:o,id:i.id,status:"expired"}:{error:o,id:i.id,status:"failed"});return{failed:a.filter(o=>o.status==="failed").length,outcomes:a,pruned:a.filter(o=>o.status==="expired").length,sent:a.filter(o=>o.status==="ok").length,total:a.length}},O=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}),o=t?.after===void 0?a:a.filter($=>$.id>t.after),c=o.length>n,i=c?o.slice(0,n):o;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 o=a===void 0?{...t,after:n}:{...t,after:n,limit:a-r.total},{nextCursor:c,result:i}=await O(e,o);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:O,list:e=>T(e),register:e=>("token"in e||I(),f.put(z(e,void 0,{allowedPushOrigins:s.allowedPushOrigins}))),send:async(e,t)=>{const{error:r,receipt:n}=await E(await _(e),t,!0);if(n===void 0)throw new v("INTERNAL",`@lunora/notify: push send failed: ${r??"unknown error"}`);return n},unregister:e=>f.delete(e)},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 p}from"@lunora/errors";import{legacyIdFor as S}from"./fcmId-ggkSOUHd.mjs";const E=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},c=/^[A-Za-z_]\w*$/u,O=(e,i={})=>{const n=i.tableName??"lunora_push_subscriptions";if(!c.test(n))throw new p("BAD_REQUEST",`@lunora/notify: d1SubscriptionStore tableName "${n}" is not a bare SQL identifier`);let r;const l=()=>(r===void 0&&(r=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(()=>{}),r.catch(()=>{r=void 0})),r),s=async t=>{await l();const a=await e.prepare(`SELECT * FROM ${n} WHERE id = ?1`).bind(t).first();return a===null?void 0:E(a)};return{delete:async t=>{await l(),await e.prepare(`DELETE FROM ${n} WHERE id = ?1`).bind(t).run()},get:s,list:async t=>{await l();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 T=a.length===0?"":` WHERE ${a.join(" AND ")}`,o=" ORDER BY id ASC";let u="";t?.limit!==void 0&&t.limit>0&&(d.push(Math.trunc(t.limit)),u=` LIMIT ?${d.length.toString()}`);const{results:_}=await e.prepare(`SELECT * FROM ${n}${T}${o}${u}`).bind(...d).all();return _.map(h=>E(h))},markStatus:async(t,a,d)=>{await l(),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 l(),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`).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=S(t);return a!==void 0&&a!==t.id&&await e.prepare(`DELETE FROM ${n} WHERE id = ?1`).bind(a).run(),await s(t.id)??t}}};export{O 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');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}},r=e=>typeof e=="object"&&e!==null&&e.isLunoraNotify===!0;export{t as defineNotify,r as isNotifyDefinition};
@@ -0,0 +1 @@
1
+ import{LunoraError as s}from"@lunora/errors";import{i as S}from"./ssrf-host-BCpHorGa.mjs";const f=4096,h=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 s("BAD_REQUEST","@lunora/notify: register() `metadata` must be a plain object");let r;try{r=JSON.stringify(t)}catch(i){throw new s("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is not JSON-serialisable: ${i instanceof Error?i.message:String(i)}`)}const o=new TextEncoder().encode(r).length;if(o>f)throw new s("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is ${o.toString()} bytes, exceeding the ${f.toString()}-byte cap`);return t},d=t=>t.toString(16).padStart(4,"0"),p=t=>{let e=8997,n=33826,r=40164,o=52210;for(let i=0;i<t.length;i+=1){const a=t.codePointAt(i)??0;e^=a&65535,n^=a>>>16&65535;const c=e*435,y=n*435,E=r*435+e*256,w=o*435+n*256,l=y+(c>>>16),u=E+(l>>>16),b=w+(u>>>16);e=c&65535,n=l&65535,r=u&65535,o=b&65535}return d(o)+d(r)+d(n)+d(e)},A=t=>`wp2_${p(t)}`,_=t=>`fcm2_${p(t)}`,g=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")},m=t=>`wp_${g(t)}`,T=t=>`fcm_${g(t)}`,I=t=>t.kind==="fcm"?t.token===void 0?void 0:T(t.token):t.endpoint===void 0?void 0:m(t.endpoint),k=t=>{if(typeof t!="string")return t??{};try{return JSON.parse(t)}catch(e){throw new s("BAD_REQUEST",`@lunora/notify: register() web-push subscription is not valid JSON: ${e instanceof Error?e.message:String(e)}`)}},O=(t,e)=>{let n;try{n=new URL(t)}catch{throw new s("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must be an absolute https URL (got "${t}")`)}if(n.protocol!=="https:")throw new s("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 s("FORBIDDEN",`@lunora/notify: register() web-push endpoint origin "${n.origin}" is not in the configured allowedPushOrigins allowlist`);return}if(S(n.hostname))throw new s("FORBIDDEN",`@lunora/notify: register() web-push endpoint host "${n.hostname}" is a private/internal address; configure allowedPushOrigins to permit a specific origin`)},N=(t,e=Date.now(),n={})=>{if("token"in t){const{token:c}=t;if(typeof c!="string"||c==="")throw new s("BAD_REQUEST","@lunora/notify: register() fcm input requires a non-empty `token`");return{createdAt:e,id:_(c),kind:"fcm",lastSeenAt:e,metadata:h(t.metadata),token:c,userId:t.userId??null}}const r=k(t.subscription),{endpoint:o}=r,i=r.keys?.p256dh,a=r.keys?.auth;if(typeof o!="string"||o===""||typeof i!="string"||typeof a!="string")throw new s("BAD_REQUEST","@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");return O(o,n.allowedPushOrigins),{createdAt:e,endpoint:o,id:A(o),keys:{auth:a,p256dh:i},kind:"web-push",lastSeenAt:e,metadata:h(t.metadata),userId:t.userId??null}},$=t=>t.kind==="fcm"?t.token??"":JSON.stringify({endpoint:t.endpoint,keys:t.keys}),v=/\bhttp\s*4(?:04|10)\b/iu,R=/\b(?:unregistered|not[\s-]?registered|registration-token-not-registered)\b/iu,D=/\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu,U=t=>t===void 0?!1:v.test(t)||R.test(t)||D.test(t);export{_ as fcmId,U as isGoneError,T as legacyFcmId,I as legacyIdFor,m as legacyWebPushId,N as normalizeRegisterInput,$ as targetOf,A as webPushId};
@@ -0,0 +1 @@
1
+ import{legacyIdFor as n}from"./fcmId-ggkSOUHd.mjs";const i=(t,e)=>t.id<e.id?-1:t.id>e.id?1:0,a=(t,e)=>e===void 0?!0:!(e.kind!==void 0&&t.kind!==e.kind||e.userId!==void 0&&(t.userId??null)!==e.userId),c=()=>{const t=new Map;return{delete:e=>(t.delete(e),Promise.resolve()),get:e=>Promise.resolve(t.get(e)),list:e=>{const r=[];for(const d of t.values())a(d,e)&&r.push(d);r.sort(i);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=n(e);r!==void 0&&r!==e.id&&t.delete(r);const o=t.get(e.id),s=o===void 0?e:{...o,...e,createdAt:o.createdAt};return t.set(s.id,s),Promise.resolve(s)}}};export{c as memorySubscriptionStore};
@@ -0,0 +1 @@
1
+ const u=/^\d{1,3}$/u,d=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,p=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,l=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,P=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,h=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,v=/^\[|\]$/gu,I=/\.$/u,a=e=>{const t=e.split(".");if(t.length!==4)return;const s=t.map(r=>u.test(r)?Number(r):-1);if(!s.some(r=>r<0||r>255))return[s[0],s[1],s[2],s[3]]},o=([e,t])=>e===0||e===10||e===127||e===100&&t>=64&&t<=127||e===169&&t===254||e===172&&t>=16&&t<=31||e===192&&t===168||e>=224,f=(e,t)=>{const s=Number.parseInt(e??"",16),r=Number.parseInt(t??"",16);return!Number.isFinite(s)||!Number.isFinite(r)?!0:o([Math.floor(s/256),s%256,Math.floor(r/256),r%256])},m=e=>{const t=e.toLowerCase(),s=d.exec(t);if(s)return f(s[1],s[2]);const r=p.exec(t);if(r){const n=a(r[1]??"");return n===void 0||o(n)}const c=l.exec(t);if(c){const n=a(c[1]??"");return n===void 0||o(n)}const i=P.exec(t);return i?f(i[1],i[2]):h.test(t)||t.startsWith("2002:")||t.startsWith("2001:0:")?!0:t==="::"||t==="::1"||t.startsWith("fc")||t.startsWith("fd")||t.startsWith("fe8")||t.startsWith("fe9")||t.startsWith("fea")||t.startsWith("feb")},_=e=>e==="localhost"||e.endsWith(".localhost")||e.endsWith(".local")||e.endsWith(".internal")||e.endsWith(".home.arpa"),E=e=>e.replaceAll(v,"").replace(I,"").toLowerCase(),T=e=>{const t=E(e);if(t.includes(":"))return m(t);const s=a(t);return s===void 0?_(t):o(s)};export{o as a,m as b,T as i,E as n,a as p};
package/dist/web.mjs CHANGED
@@ -1 +1 @@
1
- const o=r=>{const e="=".repeat((4-r.length%4)%4),s=(r+e).replaceAll("-","+").replaceAll("_","/"),i=atob(s),n=new Uint8Array(i.length);for(let t=0;t<i.length;t+=1)n[t]=i.codePointAt(t)??0;return n},u=(r,e)=>r.length===e.length&&r.every((s,i)=>s===e[i]),c=(r,e)=>{const s=r.options.applicationServerKey;return s===null?!1:u(new Uint8Array(s),o(e))},a=globalThis,l=()=>a.navigator?.serviceWorker!==void 0&&a.PushManager!==void 0,p=async r=>{if(!l())throw new Error("@lunora/notify: Web Push is not supported in this browser (needs service workers + PushManager)");let e;if(r.serviceWorkerUrl===void 0)e=await navigator.serviceWorker.ready;else{const t=r.scope===void 0?void 0:{scope:r.scope};e=await navigator.serviceWorker.register(r.serviceWorkerUrl,t)}const s=await Notification.requestPermission();if(s!=="granted")throw new Error(`@lunora/notify: notification permission was not granted (got "${s}")`);const i=await e.pushManager.getSubscription();let n=null;return i!==null&&(c(i,r.vapidPublicKey)?n=i:await i.unsubscribe()),(n??await e.pushManager.subscribe({applicationServerKey:o(r.vapidPublicKey),userVisibleOnly:!0})).toJSON()},g=async()=>{if(!l())return!1;const r=await(await navigator.serviceWorker.ready).pushManager.getSubscription();return r===null?!1:r.unsubscribe()};export{l as isPushSupported,p as subscribeToPush,g as unsubscribeFromPush};
1
+ const a=e=>{const r="=".repeat((4-e.length%4)%4),i=(e+r).replaceAll("-","+").replaceAll("_","/"),t=atob(i),s=new Uint8Array(t.length);for(let n=0;n<t.length;n+=1)s[n]=t.codePointAt(n)??0;return s},l=(e,r)=>e.length===r.length&&e.every((i,t)=>i===r[t]),g=(e,r)=>{const i=e.options.applicationServerKey;return i===null?!1:l(new Uint8Array(i),a(r))},o=globalThis,c=()=>o.navigator?.serviceWorker!==void 0&&o.PushManager!==void 0,p=async e=>{if(!c())throw new Error("@lunora/notify: Web Push is not supported in this browser (needs service workers + PushManager)");let r;if(e.serviceWorkerUrl===void 0)r=await navigator.serviceWorker.ready;else{const u=e.scope===void 0?void 0:{scope:e.scope};r=await navigator.serviceWorker.register(e.serviceWorkerUrl,u)}const i=await Notification.requestPermission();if(i!=="granted")throw new Error(`@lunora/notify: notification permission was not granted (got "${i}")`);const t=await r.pushManager.getSubscription();let s=null;return t!==null&&(g(t,e.vapidPublicKey)?s=t:await t.unsubscribe()),(s??await r.pushManager.subscribe({applicationServerKey:a(e.vapidPublicKey),userVisibleOnly:!0})).toJSON()},b=async()=>{if(!c())return!1;const r=await(await navigator.serviceWorker.ready).pushManager.getSubscription();return r===null?!1:r.unsubscribe()};export{c as isPushSupported,p as subscribeToPush,b as unsubscribeFromPush};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/notify",
3
- "version": "1.0.0-alpha.19",
3
+ "version": "1.0.0-alpha.20",
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",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@lunora/errors": "1.0.0-alpha.21",
53
- "@visulima/notification": "1.0.5"
53
+ "@visulima/notification": "1.0.12"
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{LunoraError as f}from"@lunora/errors";import{createNotification as l}from"@visulima/notification";import{retryMiddleware as h,circuitBreakerMiddleware as p}from"@visulima/notification/middleware";import{fcmProvider as w}from"@visulima/notification/providers/fcm";import{webPushProvider as v}from"@visulima/notification/providers/web-push";import{d as m,o as c,i as y,a as b}from"./ssrf-host-B_g2AcI7.mjs";const g=(t,r)=>{if(t.size<r)return;const e=t.keys().next().value;e!==void 0&&t.delete(e)},k="https://cloudflare-dns.com/dns-query",s=1,d=28,P=2e3,$=(t,r)=>{if(r===s){const e=c(t);return e===void 0||y(e)}return b(t.toLowerCase())},u=async(t,r,e)=>{try{const o=await fetch(`${k}?name=${encodeURIComponent(t)}&type=${String(r)}`,{headers:{accept:"application/dns-json"},signal:AbortSignal.timeout(e)});return o.ok?(await o.json()).Answer??[]:void 0}catch{return}},A=async(t,r=P)=>{const e=m(t);if(e.includes(":")||c(e)!==void 0)return{kind:"unknown"};const[o,i]=await Promise.all([u(e,s,r),u(e,d,r)]);if(o===void 0&&i===void 0)return{kind:"unknown"};for(const n of[...o??[],...i??[]])if((n.type===s||n.type===d)&&$(n.data,n.type))return{address:n.data,kind:"private"};return{kind:"public"}},O=t=>{let r=t;if(typeof t=="string"){if(!t.startsWith("{"))return;try{r=JSON.parse(t)}catch{return}}const e=r?.endpoint;return typeof e=="string"?e:void 0},a=new Map,z=256,E=async(t,r)=>{if(r!==void 0&&r.length>0)return;let e;try{({hostname:e}=new URL(t))}catch{return}const o=a.get(e),i=o??A(e),n=await i;if(o===void 0&&n.kind!=="unknown"&&(g(a,z),a.set(e,i)),n.kind==="private")throw new f("FORBIDDEN",`@lunora/notify: web-push endpoint host "${e}" resolves to a private/internal address (${n.address}); refusing to send (DNS-rebinding guard)`)},M=t=>{const r=e=>{const o=e===void 0?t.fcm:t.webPush;if(o===void 0)throw new Error(e===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 o};return{channel:"push",id:"lunora-push-router",initialize:async()=>{await t.webPush?.initialize(),await t.fcm?.initialize()},isAvailable:()=>(t.webPush??t.fcm)!==void 0,send:async e=>{const o=(Array.isArray(e.to)?e.to:[e.to]).map(i=>O(i));for(const i of o)i!==void 0&&await E(i,t.allowedPushOrigins);return r(o[0]).send(e)}}},j=t=>{const r=t.webPush===void 0?void 0:v(t.webPush),e=t.fcm===void 0?void 0:w(t.fcm),o={};(r!==void 0||e!==void 0)&&(o.push=M({allowedPushOrigins:t.allowedPushOrigins,fcm:e,webPush:r})),t.chat!==void 0&&(o.chat=t.chat),t.inApp!==void 0&&(o.inapp=t.inApp),t.webhook!==void 0&&(o.webhook=t.webhook);const i=l(o);return i.use(h()).use(p()),i};export{j as buildEngine,M as routingPushProvider};
@@ -1 +0,0 @@
1
- import{LunoraError as k}from"@lunora/errors";import{buildEngine as R}from"./buildEngine-BV6_77DO.mjs";import{memorySubscriptionStore as W}from"./memorySubscriptionStore-0VBjZg3X.mjs";import{normalizeRegisterInput as Q,targetOf as U,isGoneError as j}from"./fcmId-BQZrXP9X.mjs";const z=250,g=(i,u)=>typeof i=="function"?i(u):i,b=i=>i.successful?void 0:i.errorMessages.join("; "),G=(i,u)=>i.successful?"accepted":j(u)?"gone":"failed",L=async(i,u,c)=>{const d=Array.from({length:i.length});let h=0;const p=async()=>{for(;h<i.length;){const f=h;h+=1,d[f]=await c(i[f])}};return await Promise.all(Array.from({length:Math.min(u,i.length)},()=>p())),d},_=(i,u)=>({allowedPushOrigins:i.allowedPushOrigins,chat:g(i.chat,u),fcm:g(i.fcm,u),inApp:g(i.inApp,u),webhook:g(i.webhook,u),webPush:g(i.webPush,u)}),E=new WeakMap,q=(i,u)=>{let c=E.get(i);c===void 0&&(c=new WeakMap,E.set(i,c));let d=c.get(u);return d===void 0&&(d={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},c.set(u,d)),d},V=(i,u,c={})=>{const d=q(i,u);let h;c.engine===void 0?(d.engine??=R(_(i,u)),h=d.engine):h=c.engine,d.store??=i.store?.(u);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:$,metrics:P}=c,w=(e,t,n,r=1)=>{P?.count("notify.send",r,{channel:e,provider:t??e,status:n})},v=(e,t,n)=>{$?.warn(`notify ${e} delivery failed`,{channel:e,provider:t??e,status:"failed",...n})},O=(e,t)=>{P?.count("notify.skipped",1,{channel:e,reason:t})},C=()=>{const e=i.allowedPushOrigins!==void 0&&i.allowedPushOrigins.length>0;c.silent||e||d.warnedNoPushOriginAllowlist||(d.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."))},I=async e=>(await f.list(e)).map(({keys:t,token:n,...r})=>r),T=async e=>{if(typeof e!="string")return e;const t=await f.get(e);if(t===void 0)throw new k("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${e}"`);return t},A=async(e,t,n)=>{let r,s,o;try{r=await h.sendToChannel("push",{...t,to:U(e)}),s=b(r),o=G(r,s)}catch(l){o="failed",s=l instanceof Error?l.message:String(l)}try{o==="accepted"?await f.markStatus(e.id,"ok"):o==="gone"?await f.delete(e.id):await f.markStatus(e.id,"failed",s)}catch{}return o==="failed"&&v("push",e.kind,{error:s,subscriptionId:e.id,userId:e.userId??null}),n&&w("push",e.kind,o),{error:s,receipt:r,status:o}},D=async(e,t)=>{const n=await L(t,M,async o=>{const{error:l,status:a}=await A(o,e,!1);return{error:l,kind:o.kind,status:a,subscription:o}}),r=new Map;for(const{kind:o,status:l}of n){const a=`${o} ${l}`,m=r.get(a);m===void 0?r.set(a,{count:1,kind:o,status:l}):m.count+=1}for(const{count:o,kind:l,status:a}of r.values())w("push",l,a,o);const s=n.map(({error:o,status:l,subscription:a})=>l==="accepted"?{id:a.id,status:"ok"}:l==="gone"?{error:o,id:a.id,status:"expired"}:{error:o,id:a.id,status:"failed"});return{failed:s.filter(o=>o.status==="failed").length,outcomes:s,pruned:s.filter(o=>o.status==="expired").length,sent:s.filter(o=>o.status==="ok").length,total:s.length}},N=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 n=t?.limit!==void 0&&t.limit>0?Math.trunc(t.limit):void 0,r=n===void 0?S:Math.min(n,S),s=await f.list({after:t?.after,kind:t?.kind,limit:r+1,userId:t?.userId}),o=t?.after===void 0?s:s.filter(B=>B.id>t.after),l=o.length>r,a=l?o.slice(0,r):o;a.length===0&&t?.after===void 0&&O("push","no-subscriptions-matched");const m=await D(e,a);return{nextCursor:l?a[a.length-1]?.id:void 0,result:m}},x={broadcast:async(e,t)=>{const n={failed:0,outcomes:[],pruned:0,sent:0,total:0};let r=t?.after;const s=t?.limit;if(s!==void 0&&s<=0)return n;for(;;){const o=s===void 0?{...t,after:r}:{...t,after:r,limit:s-n.total},{nextCursor:l,result:a}=await N(e,o);if(n.failed+=a.failed,n.pruned+=a.pruned,n.sent+=a.sent,n.total+=a.total,n.outcomes.push(...a.outcomes),s!==void 0&&n.total>=s||l===void 0||l===r)break;r=l}return n},broadcastPage:N,list:e=>I(e),register:e=>("token"in e||C(),f.put(Q(e,void 0,{allowedPushOrigins:i.allowedPushOrigins}))),send:async(e,t)=>{const{error:n,receipt:r}=await A(await T(e),t,!0);if(r===void 0)throw new k("INTERNAL",`@lunora/notify: push send failed: ${n??"unknown error"}`);return r},unregister:e=>f.delete(e)},y=async(e,t)=>{if(h.getProvider(e)===void 0)throw O(e,"channel-not-configured"),new k("BAD_REQUEST",`@lunora/notify: the "${e}" channel is not configured in defineNotify(...)`);const n=await h.sendToChannel(e,t),r=n.successful?"accepted":"failed";return w(e,n.provider,r),r==="failed"&&v(e,n.provider,{error:b(n)}),n};return{notify:{chat:e=>y("chat",e),inApp:e=>y("inapp",e),push:x,send:async e=>{const t=await h.send(e);for(const n of t){const r=n.channel??"unknown",s=n.successful?"accepted":"failed";w(r,n.provider,s),s==="failed"&&v(r,n.provider,{error:b(n)})}return t},webhook:e=>y("webhook",e)},push:x}};export{V as createNotify};
@@ -1 +0,0 @@
1
- import{LunoraError as h}from"@lunora/errors";import{legacyIdFor as S}from"./fcmId-BQZrXP9X.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};
@@ -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 i}from"@lunora/errors";import{E}from"./ssrf-host-B_g2AcI7.mjs";const g=4096,c=e=>{if(e===void 0)return;const t=typeof e=="object"&&e!==null?Object.getPrototypeOf(e):void 0;if(!(typeof e=="object"&&e!==null&&!Array.isArray(e)&&(t===Object.prototype||t===null)))throw new i("BAD_REQUEST","@lunora/notify: register() `metadata` must be a plain object");let n;try{n=JSON.stringify(e)}catch(r){throw new i("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is not JSON-serialisable: ${r instanceof Error?r.message:String(r)}`)}const o=new TextEncoder().encode(n).length;if(o>g)throw new i("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is ${o.toString()} bytes, exceeding the ${g.toString()}-byte cap`);return e},u=e=>e.toString(16).padStart(4,"0"),f=e=>{let t=8997,n=33826,o=40164,r=52210;for(let a=0;a<e.length;a+=1){const d=e.codePointAt(a)??0;t^=d&65535,n^=d>>>16&65535;const s=t*435,y=n*435,w=o*435+t*256,b=r*435+n*256,l=y+(s>>>16),p=w+(l>>>16),m=b+(p>>>16);t=s&65535,n=l&65535,o=p&65535,r=m&65535}return u(r)+u(o)+u(n)+u(t)},S=e=>`wp2_${f(e)}`,k=e=>`fcm2_${f(e)}`,h=e=>{let t=2166136261;for(let n=0;n<e.length;n+=1)t^=e.codePointAt(n)??0,t=Math.imul(t,16777619);return(t>>>0).toString(16).padStart(8,"0")},A=e=>`wp_${h(e)}`,O=e=>`fcm_${h(e)}`,T=e=>e.kind==="fcm"?e.token===void 0?void 0:O(e.token):e.endpoint===void 0?void 0:A(e.endpoint),D=e=>{if(typeof e!="string")return e??{};try{return JSON.parse(e)}catch(t){throw new i("BAD_REQUEST",`@lunora/notify: register() web-push subscription is not valid JSON: ${t instanceof Error?t.message:String(t)}`)}},R=(e,t)=>{let n;try{n=new URL(e)}catch{throw new i("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must be an absolute https URL (got "${e}")`)}if(n.protocol!=="https:")throw new i("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must use https (got "${n.protocol}")`);if(t!==void 0&&t.length>0){if(!t.includes(n.origin))throw new i("FORBIDDEN",`@lunora/notify: register() web-push endpoint origin "${n.origin}" is not in the configured allowedPushOrigins allowlist`);return}if(E(n.hostname))throw new i("FORBIDDEN",`@lunora/notify: register() web-push endpoint host "${n.hostname}" is a private/internal address; configure allowedPushOrigins to permit a specific origin`)},U=(e,t=Date.now(),n={})=>{if("token"in e){const{token:s}=e;if(typeof s!="string"||s==="")throw new i("BAD_REQUEST","@lunora/notify: register() fcm input requires a non-empty `token`");return{createdAt:t,id:k(s),kind:"fcm",lastSeenAt:t,metadata:c(e.metadata),token:s,userId:e.userId??null}}const o=D(e.subscription),{endpoint:r}=o,a=o.keys?.p256dh,d=o.keys?.auth;if(typeof r!="string"||r===""||typeof a!="string"||typeof d!="string")throw new i("BAD_REQUEST","@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");return R(r,n.allowedPushOrigins),{createdAt:t,endpoint:r,id:S(r),keys:{auth:d,p256dh:a},kind:"web-push",lastSeenAt:t,metadata:c(e.metadata),userId:e.userId??null}},P=e=>e.kind==="fcm"?e.token??"":JSON.stringify({endpoint:e.endpoint,keys:e.keys}),I=/\bhttp\s*4(?:04|10)\b/iu,_=/\b(?:unregistered|not[\s-]?registered|registration-token-not-registered)\b/iu,$=/\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu,N=e=>e===void 0?!1:I.test(e)||_.test(e)||$.test(e);export{k as fcmId,N as isGoneError,O as legacyFcmId,T as legacyIdFor,A as legacyWebPushId,U as normalizeRegisterInput,P as targetOf,S as webPushId};
@@ -1 +0,0 @@
1
- import{legacyIdFor as d}from"./fcmId-BQZrXP9X.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};
@@ -1 +0,0 @@
1
- const c=/^\d{1,3}$/u,d=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,h=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,l=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,W=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,m=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,p=/^\[|\]$/gu,b=/\.$/u,f=s=>{const t=s.split(".");if(t.length!==4)return;const r=t.map(e=>c.test(e)?Number(e):-1);if(!r.some(e=>e<0||e>255))return[r[0],r[1],r[2],r[3]]},o=([s,t])=>s===0||s===10||s===127||s===100&&t>=64&&t<=127||s===169&&t===254||s===172&&t>=16&&t<=31||s===192&&t===168||s>=224,u=(s,t)=>{const r=Number.parseInt(s??"",16),e=Number.parseInt(t??"",16);return!Number.isFinite(r)||!Number.isFinite(e)?!0:o([Math.floor(r/256),r%256,Math.floor(e/256),e%256])},$=s=>{const t=s.toLowerCase(),r=d.exec(t);if(r)return u(r[1],r[2]);const e=h.exec(t);if(e){const n=f(e[1]??"");return n===void 0||o(n)}const i=l.exec(t);if(i){const n=f(i[1]??"");return n===void 0||o(n)}const a=W.exec(t);return a?u(a[1],a[2]):m.test(t)||t.startsWith("2002:")||t.startsWith("2001:0:")?!0:t==="::"||t==="::1"||t.startsWith("fc")||t.startsWith("fd")||t.startsWith("fe8")||t.startsWith("fe9")||t.startsWith("fea")||t.startsWith("feb")},x=s=>s==="localhost"||s.endsWith(".localhost")||s.endsWith(".local")||s.endsWith(".internal")||s.endsWith(".home.arpa"),N=s=>s.replaceAll(p,"").replace(b,"").toLowerCase(),v=s=>{const t=N(s);if(t.includes(":"))return $(t);const r=f(t);return r===void 0?x(t):o(r)};export{v as E,$ as a,N as d,o as i,f as o};