@lunora/notify 1.0.0-alpha.13 → 1.0.0-alpha.14
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 +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/buildEngine-BV6_77DO.mjs +1 -0
- package/dist/packem_shared/createNotify-DSUQo7VL.mjs +1 -0
- package/dist/packem_shared/{d1SubscriptionStore-67jpK0Vx.mjs → d1SubscriptionStore-BcpvEOmO.mjs} +1 -1
- package/dist/packem_shared/fcmId-BQZrXP9X.mjs +1 -0
- package/dist/packem_shared/{memorySubscriptionStore-FKZR4-fB.mjs → memorySubscriptionStore-0VBjZg3X.mjs} +1 -1
- package/dist/packem_shared/ssrf-host-B_g2AcI7.mjs +1 -0
- package/package.json +1 -1
- package/dist/packem_shared/buildEngine-BZKpZXQ6.mjs +0 -1
- package/dist/packem_shared/createNotify-DmK_8uPL.mjs +0 -1
- package/dist/packem_shared/fcmId-CFZ8IquF.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -467,6 +467,11 @@ declare const createNotify: (definition: NotifyDefinition, env: NotifyEnv, optio
|
|
|
467
467
|
};
|
|
468
468
|
/** Options for {@link routingPushProvider}. */
|
|
469
469
|
interface RoutingPushOptions {
|
|
470
|
+
/**
|
|
471
|
+
* The definition's exact-origin allowlist, when configured. Its presence
|
|
472
|
+
* disables the send-time rebinding re-check (see {@link assertPushTargetResolvable}).
|
|
473
|
+
*/
|
|
474
|
+
allowedPushOrigins?: string[];
|
|
470
475
|
fcm?: Provider<unknown, PushPayload>;
|
|
471
476
|
webPush?: Provider<unknown, PushPayload>;
|
|
472
477
|
}
|
|
@@ -480,6 +485,8 @@ interface RoutingPushOptions {
|
|
|
480
485
|
declare const routingPushProvider: (options: RoutingPushOptions) => Provider<unknown, PushPayload>;
|
|
481
486
|
/** A resolved, ready-to-wire set of channel configs (edge-safe channels only). */
|
|
482
487
|
interface ResolvedProviders {
|
|
488
|
+
/** The definition's `allowedPushOrigins`, threaded to the push router's send-time SSRF guard. */
|
|
489
|
+
allowedPushOrigins?: string[];
|
|
483
490
|
chat?: Provider;
|
|
484
491
|
fcm?: FcmConfig;
|
|
485
492
|
inApp?: Provider;
|
|
@@ -649,9 +656,10 @@ interface NormalizeOptions {
|
|
|
649
656
|
*
|
|
650
657
|
* When unset, the default posture applies: `https:` scheme + a host the
|
|
651
658
|
* {@link assertPushEndpoint} STRING classifier does not flag as
|
|
652
|
-
* private/loopback
|
|
653
|
-
*
|
|
654
|
-
*
|
|
659
|
+
* private/loopback, plus a resolved-address re-check at send time. Setting
|
|
660
|
+
* this allowlist replaces both with an exact-origin match — the hard
|
|
661
|
+
* guarantee, and the only one that also covers an internal push service you
|
|
662
|
+
* deliberately want to reach.
|
|
655
663
|
*/
|
|
656
664
|
allowedPushOrigins?: string[];
|
|
657
665
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -467,6 +467,11 @@ declare const createNotify: (definition: NotifyDefinition, env: NotifyEnv, optio
|
|
|
467
467
|
};
|
|
468
468
|
/** Options for {@link routingPushProvider}. */
|
|
469
469
|
interface RoutingPushOptions {
|
|
470
|
+
/**
|
|
471
|
+
* The definition's exact-origin allowlist, when configured. Its presence
|
|
472
|
+
* disables the send-time rebinding re-check (see {@link assertPushTargetResolvable}).
|
|
473
|
+
*/
|
|
474
|
+
allowedPushOrigins?: string[];
|
|
470
475
|
fcm?: Provider<unknown, PushPayload>;
|
|
471
476
|
webPush?: Provider<unknown, PushPayload>;
|
|
472
477
|
}
|
|
@@ -480,6 +485,8 @@ interface RoutingPushOptions {
|
|
|
480
485
|
declare const routingPushProvider: (options: RoutingPushOptions) => Provider<unknown, PushPayload>;
|
|
481
486
|
/** A resolved, ready-to-wire set of channel configs (edge-safe channels only). */
|
|
482
487
|
interface ResolvedProviders {
|
|
488
|
+
/** The definition's `allowedPushOrigins`, threaded to the push router's send-time SSRF guard. */
|
|
489
|
+
allowedPushOrigins?: string[];
|
|
483
490
|
chat?: Provider;
|
|
484
491
|
fcm?: FcmConfig;
|
|
485
492
|
inApp?: Provider;
|
|
@@ -649,9 +656,10 @@ interface NormalizeOptions {
|
|
|
649
656
|
*
|
|
650
657
|
* When unset, the default posture applies: `https:` scheme + a host the
|
|
651
658
|
* {@link assertPushEndpoint} STRING classifier does not flag as
|
|
652
|
-
* private/loopback
|
|
653
|
-
*
|
|
654
|
-
*
|
|
659
|
+
* private/loopback, plus a resolved-address re-check at send time. Setting
|
|
660
|
+
* this allowlist replaces both with an exact-origin match — the hard
|
|
661
|
+
* guarantee, and the only one that also covers an internal push service you
|
|
662
|
+
* deliberately want to reach.
|
|
655
663
|
*/
|
|
656
664
|
allowedPushOrigins?: string[];
|
|
657
665
|
}
|
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-
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
package/dist/packem_shared/{d1SubscriptionStore-67jpK0Vx.mjs → d1SubscriptionStore-BcpvEOmO.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LunoraError as h}from"@lunora/errors";import{legacyIdFor as S}from"./fcmId-
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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 +1 @@
|
|
|
1
|
-
import{legacyIdFor as d}from"./fcmId-
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/notify",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.14",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createNotification as s}from"@visulima/notification";import{retryMiddleware as a,circuitBreakerMiddleware as n}from"@visulima/notification/middleware";import{fcmProvider as c}from"@visulima/notification/providers/fcm";import{webPushProvider as u}from"@visulima/notification/providers/web-push";const d=i=>{if(typeof i!="string")return typeof i=="object"&&i!==null&&"endpoint"in i;if(!i.startsWith("{"))return!1;try{const r=JSON.parse(i);return typeof r.endpoint=="string"&&typeof r.keys=="object"}catch{return!1}},f=i=>{const r=o=>{const e=d(o),t=e?i.webPush:i.fcm;if(t===void 0)throw new Error(e?"@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 t};return{channel:"push",id:"lunora-push-router",initialize:async()=>{await i.webPush?.initialize(),await i.fcm?.initialize()},isAvailable:()=>(i.webPush??i.fcm)!==void 0,send:o=>{const e=Array.isArray(o.to)?o.to[0]:o.to;return r(e).send(o)}}},w=i=>{const r=i.webPush===void 0?void 0:u(i.webPush),o=i.fcm===void 0?void 0:c(i.fcm),e={};(r!==void 0||o!==void 0)&&(e.push=f({fcm:o,webPush:r})),i.chat!==void 0&&(e.chat=i.chat),i.inApp!==void 0&&(e.inapp=i.inApp),i.webhook!==void 0&&(e.webhook=i.webhook);const t=s(e);return t.use(a()).use(n()),t};export{w as buildEngine,f as routingPushProvider};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as k}from"@lunora/errors";import{buildEngine as R}from"./buildEngine-BZKpZXQ6.mjs";import{memorySubscriptionStore as W}from"./memorySubscriptionStore-FKZR4-fB.mjs";import{normalizeRegisterInput as Q,targetOf as U,isGoneError as j}from"./fcmId-CFZ8IquF.mjs";const z=250,g=(o,l)=>typeof o=="function"?o(l):o,b=o=>o.successful?void 0:o.errorMessages.join("; "),G=(o,l)=>o.successful?"accepted":j(l)?"gone":"failed",L=async(o,l,c)=>{const d=Array.from({length:o.length});let h=0;const p=async()=>{for(;h<o.length;){const f=h;h+=1,d[f]=await c(o[f])}};return await Promise.all(Array.from({length:Math.min(l,o.length)},()=>p())),d},_=(o,l)=>({chat:g(o.chat,l),fcm:g(o.fcm,l),inApp:g(o.inApp,l),webhook:g(o.webhook,l),webPush:g(o.webPush,l)}),E=new WeakMap,q=(o,l)=>{let c=E.get(o);c===void 0&&(c=new WeakMap,E.set(o,c));let d=c.get(l);return d===void 0&&(d={warnedNoPushOriginAllowlist:!1,warnedNoStore:!1},c.set(l,d)),d},V=(o,l,c={})=>{const d=q(o,l);let h;c.engine===void 0?(d.engine??=R(_(o,l)),h=d.engine):h=c.engine,d.store??=o.store?.(l);let{store:p}=d;p===void 0&&(d.fallbackStore??=W(),!c.silent&&!d.warnedNoStore&&(d.warnedNoStore=!0,console.warn("@lunora/notify: no `store` configured — using a non-durable in-memory subscription store. Configure `store: (env) => d1SubscriptionStore(env.DB)` for production.")),p=d.fallbackStore);const f=p,M=Math.max(1,c.concurrency??10),S=Math.max(1,c.broadcastPageSize??z),{log:T,metrics:P}=c,w=(t,e,r,i=1)=>{P?.count("notify.send",i,{channel:t,provider:e??t,status:r})},v=(t,e,r)=>{T?.warn(`notify ${t} delivery failed`,{channel:t,provider:e??t,status:"failed",...r})},N=(t,e)=>{P?.count("notify.skipped",1,{channel:t,reason:e})},$=()=>{const t=o.allowedPushOrigins!==void 0&&o.allowedPushOrigins.length>0;c.silent||t||d.warnedNoPushOriginAllowlist||(d.warnedNoPushOriginAllowlist=!0,console.warn("@lunora/notify: Web Push registered without `allowedPushOrigins` — the endpoint host is validated by a string classifier that does NOT resolve DNS, so a public hostname resolving to a private/internal IP (e.g. `https://127.0.0.1.nip.io/…`) is NOT blocked. Set `allowedPushOrigins` to the exact push-service origins to close DNS rebinding."))},I=async t=>(await f.list(t)).map(({keys:e,token:r,...i})=>i),C=async t=>{if(typeof t!="string")return t;const e=await f.get(t);if(e===void 0)throw new k("BAD_REQUEST",`@lunora/notify: no registered subscription with id "${t}"`);return e},O=async(t,e,r)=>{let i,s,n;try{i=await h.sendToChannel("push",{...e,to:U(t)}),s=b(i),n=G(i,s)}catch(u){n="failed",s=u instanceof Error?u.message:String(u)}try{n==="accepted"?await f.markStatus(t.id,"ok"):n==="gone"?await f.delete(t.id):await f.markStatus(t.id,"failed",s)}catch{}return n==="failed"&&v("push",t.kind,{error:s,subscriptionId:t.id,userId:t.userId??null}),r&&w("push",t.kind,n),{error:s,receipt:i,status:n}},D=async(t,e)=>{const r=await L(e,M,async n=>{const{error:u,status:a}=await O(n,t,!1);return{error:u,kind:n.kind,status:a,subscription:n}}),i=new Map;for(const{kind:n,status:u}of r){const a=`${n} ${u}`,m=i.get(a);m===void 0?i.set(a,{count:1,kind:n,status:u}):m.count+=1}for(const{count:n,kind:u,status:a}of i.values())w("push",u,a,n);const s=r.map(({error:n,status:u,subscription:a})=>u==="accepted"?{id:a.id,status:"ok"}:u==="gone"?{error:n,id:a.id,status:"expired"}:{error:n,id:a.id,status:"failed"});return{failed:s.filter(n=>n.status==="failed").length,outcomes:s,pruned:s.filter(n=>n.status==="expired").length,sent:s.filter(n=>n.status==="ok").length,total:s.length}},A=async(t,e)=>{if(e?.limit!==void 0&&e.limit<=0)return{nextCursor:void 0,result:{failed:0,outcomes:[],pruned:0,sent:0,total:0}};const r=e?.limit!==void 0&&e.limit>0?Math.trunc(e.limit):void 0,i=r===void 0?S:Math.min(r,S),s=await f.list({after:e?.after,kind:e?.kind,limit:i+1,userId:e?.userId}),n=e?.after===void 0?s:s.filter(B=>B.id>e.after),u=n.length>i,a=u?n.slice(0,i):n;a.length===0&&e?.after===void 0&&N("push","no-subscriptions-matched");const m=await D(t,a);return{nextCursor:u?a[a.length-1]?.id:void 0,result:m}},x={broadcast:async(t,e)=>{const r={failed:0,outcomes:[],pruned:0,sent:0,total:0};let i=e?.after;const s=e?.limit;if(s!==void 0&&s<=0)return r;for(;;){const n=s===void 0?{...e,after:i}:{...e,after:i,limit:s-r.total},{nextCursor:u,result:a}=await A(t,n);if(r.failed+=a.failed,r.pruned+=a.pruned,r.sent+=a.sent,r.total+=a.total,r.outcomes.push(...a.outcomes),s!==void 0&&r.total>=s||u===void 0||u===i)break;i=u}return r},broadcastPage:A,list:t=>I(t),register:t=>("token"in t||$(),f.put(Q(t,void 0,{allowedPushOrigins:o.allowedPushOrigins}))),send:async(t,e)=>{const{error:r,receipt:i}=await O(await C(t),e,!0);if(i===void 0)throw new k("INTERNAL",`@lunora/notify: push send failed: ${r??"unknown error"}`);return i},unregister:t=>f.delete(t)},y=async(t,e)=>{if(h.getProvider(t)===void 0)throw N(t,"channel-not-configured"),new k("BAD_REQUEST",`@lunora/notify: the "${t}" channel is not configured in defineNotify(...)`);const r=await h.sendToChannel(t,e),i=r.successful?"accepted":"failed";return w(t,r.provider,i),i==="failed"&&v(t,r.provider,{error:b(r)}),r};return{notify:{chat:t=>y("chat",t),inApp:t=>y("inapp",t),push:x,send:async t=>{const e=await h.send(t);for(const r of e){const i=r.channel??"unknown",s=r.successful?"accepted":"failed";w(i,r.provider,s),s==="failed"&&v(i,r.provider,{error:b(r)})}return e},webhook:t=>y("webhook",t)},push:x}};export{V as createNotify};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as a}from"@lunora/errors";const k=/^\d{1,3}$/u,A=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,I=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,v=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,O=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,D=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,R=/^\[|\]$/gu,_=/\.$/u,c=t=>{const e=t.split(".");if(e.length!==4)return;const n=e.map(r=>k.test(r)?Number(r):-1);if(!n.some(r=>r<0||r>255))return[n[0],n[1],n[2],n[3]]},f=([t,e])=>t===0||t===10||t===127||t===100&&e>=64&&e<=127||t===169&&e===254||t===172&&e>=16&&e<=31||t===192&&e===168||t>=224,h=(t,e)=>{const n=Number.parseInt(t??"",16),r=Number.parseInt(e??"",16);return!Number.isFinite(n)||!Number.isFinite(r)?!0:f([Math.floor(n/256),n%256,Math.floor(r/256),r%256])},N=t=>{const e=t.toLowerCase(),n=A.exec(e);if(n)return h(n[1],n[2]);const r=I.exec(e);if(r){const i=c(r[1]??"");return i===void 0||f(i)}const o=v.exec(e);if(o){const i=c(o[1]??"");return i===void 0||f(i)}const s=O.exec(e);return s?h(s[1],s[2]):D.test(e)||e.startsWith("2002:")||e.startsWith("2001:0:")?!0:e==="::"||e==="::1"||e.startsWith("fc")||e.startsWith("fd")||e.startsWith("fe8")||e.startsWith("fe9")||e.startsWith("fea")||e.startsWith("feb")},W=t=>t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal")||t.endsWith(".home.arpa"),B=t=>t.replaceAll(R,"").replace(_,"").toLowerCase(),P=t=>{const e=B(t);if(e.includes(":"))return N(e);const n=c(e);return n===void 0?W(e):f(n)},g=4096,w=t=>{if(t===void 0)return;const e=typeof t=="object"&&t!==null?Object.getPrototypeOf(t):void 0;if(!(typeof t=="object"&&t!==null&&!Array.isArray(t)&&(e===Object.prototype||e===null)))throw new a("BAD_REQUEST","@lunora/notify: register() `metadata` must be a plain object");let n;try{n=JSON.stringify(t)}catch(o){throw new a("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is not JSON-serialisable: ${o instanceof Error?o.message:String(o)}`)}const r=new TextEncoder().encode(n).length;if(r>g)throw new a("BAD_REQUEST",`@lunora/notify: register() \`metadata\` is ${r.toString()} bytes, exceeding the ${g.toString()}-byte cap`);return t},d=t=>t.toString(16).padStart(4,"0"),b=t=>{let e=8997,n=33826,r=40164,o=52210;for(let s=0;s<t.length;s+=1){const i=t.codePointAt(s)??0;e^=i&65535,n^=i>>>16&65535;const u=e*435,m=n*435,$=r*435+e*256,E=o*435+n*256,l=m+(u>>>16),p=$+(l>>>16),S=E+(p>>>16);e=u&65535,n=l&65535,r=p&65535,o=S&65535}return d(o)+d(r)+d(n)+d(e)},T=t=>`wp2_${b(t)}`,U=t=>`fcm2_${b(t)}`,y=t=>{let e=2166136261;for(let n=0;n<t.length;n+=1)e^=t.codePointAt(n)??0,e=Math.imul(e,16777619);return(e>>>0).toString(16).padStart(8,"0")},x=t=>`wp_${y(t)}`,Q=t=>`fcm_${y(t)}`,C=t=>t.kind==="fcm"?t.token===void 0?void 0:Q(t.token):t.endpoint===void 0?void 0:x(t.endpoint),F=t=>{if(typeof t!="string")return t??{};try{return JSON.parse(t)}catch(e){throw new a("BAD_REQUEST",`@lunora/notify: register() web-push subscription is not valid JSON: ${e instanceof Error?e.message:String(e)}`)}},j=(t,e)=>{let n;try{n=new URL(t)}catch{throw new a("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must be an absolute https URL (got "${t}")`)}if(n.protocol!=="https:")throw new a("BAD_REQUEST",`@lunora/notify: register() web-push \`endpoint\` must use https (got "${n.protocol}")`);if(e!==void 0&&e.length>0){if(!e.includes(n.origin))throw new a("FORBIDDEN",`@lunora/notify: register() web-push endpoint origin "${n.origin}" is not in the configured allowedPushOrigins allowlist`);return}if(P(n.hostname))throw new a("FORBIDDEN",`@lunora/notify: register() web-push endpoint host "${n.hostname}" is a private/internal address; configure allowedPushOrigins to permit a specific origin`)},z=(t,e=Date.now(),n={})=>{if("token"in t){const{token:u}=t;if(typeof u!="string"||u==="")throw new a("BAD_REQUEST","@lunora/notify: register() fcm input requires a non-empty `token`");return{createdAt:e,id:U(u),kind:"fcm",lastSeenAt:e,metadata:w(t.metadata),token:u,userId:t.userId??null}}const r=F(t.subscription),{endpoint:o}=r,s=r.keys?.p256dh,i=r.keys?.auth;if(typeof o!="string"||o===""||typeof s!="string"||typeof i!="string")throw new a("BAD_REQUEST","@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");return j(o,n.allowedPushOrigins),{createdAt:e,endpoint:o,id:T(o),keys:{auth:i,p256dh:s},kind:"web-push",lastSeenAt:e,metadata:w(t.metadata),userId:t.userId??null}},G=t=>t.kind==="fcm"?t.token??"":JSON.stringify({endpoint:t.endpoint,keys:t.keys}),J=/\bhttp\s*4(?:04|10)\b/iu,L=/\b(?:unregistered|not[\s-]?registered|registration-token-not-registered)\b/iu,M=/\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu,H=t=>t===void 0?!1:J.test(t)||L.test(t)||M.test(t);export{U as fcmId,H as isGoneError,Q as legacyFcmId,C as legacyIdFor,x as legacyWebPushId,z as normalizeRegisterInput,G as targetOf,T as webPushId};
|