@stacksjs/push 0.70.306 → 0.70.310

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.
@@ -1,2 +1,3 @@
1
1
  export * as expo from './expo';
2
2
  export * as fcm from './fcm';
3
+ export * from './web-push';
@@ -0,0 +1,73 @@
1
+ import { Buffer } from 'node:buffer';
2
+ /**
3
+ * A fresh VAPID keypair.
4
+ *
5
+ * Generated per installation rather than shipped. A shared key across every
6
+ * self-hosted instance would let any one of them send push notifications
7
+ * claiming to be any other, and would make a single revocation break everybody.
8
+ */
9
+ export declare function generateVapidKeys(): VapidKeys;
10
+ /**
11
+ * The `Authorization` and `Crypto-Key` headers RFC 8292 asks for.
12
+ *
13
+ * The JWT is scoped to the push service's origin and expires, so a leaked one
14
+ * is useful to one service for a limited time rather than being a permanent
15
+ * credential for everything.
16
+ */
17
+ export declare function buildVapidHeaders(options: {
18
+ endpoint: string
19
+ vapid: VapidKeys
20
+ subject: string
21
+ expiresInSeconds?: number
22
+ /** Passed in so this is testable without pretending it is a particular day. */
23
+ nowSeconds?: number
24
+ }): Record<string, string>;
25
+ /**
26
+ * Encrypt a payload for one subscription, in the `aes128gcm` content encoding.
27
+ *
28
+ * The wire format is a header the receiver needs to decrypt - salt, record
29
+ * size, and our ephemeral public key - followed by the ciphertext. The browser
30
+ * has everything else already.
31
+ *
32
+ * A fresh ephemeral keypair per message, which is what makes this forward
33
+ * secret: recovering the server's VAPID key later does not decrypt anything
34
+ * already sent, because that key is not involved in the encryption at all.
35
+ */
36
+ export declare function encryptPayload(subscription: WebPushSubscription, payload: string, salt?: Buffer): Buffer;
37
+ /**
38
+ * Send one notification to one browser.
39
+ *
40
+ * Returns rather than throws, and says whether the endpoint is *gone*. That
41
+ * distinction is the whole reason this returns a shape rather than a boolean:
42
+ * a caller that cannot tell "try again later" from "this browser is never
43
+ * coming back" either retries forever against a dead endpoint or deletes
44
+ * somebody's subscription because their network blipped.
45
+ */
46
+ export declare function sendWebPush(options: SendWebPushOptions): Promise<WebPushResult>;
47
+ /** A browser's subscription, exactly as `PushSubscription.toJSON()` gives it. */
48
+ export declare interface WebPushSubscription {
49
+ endpoint: string
50
+ keys: {
51
+ p256dh: string
52
+ auth: string
53
+ }
54
+ }
55
+ export declare interface VapidKeys {
56
+ publicKey: string
57
+ privateKey: string
58
+ }
59
+ export declare interface WebPushResult {
60
+ success: boolean
61
+ status: number
62
+ expired: boolean
63
+ error?: string
64
+ }
65
+ export declare interface SendWebPushOptions {
66
+ subscription: WebPushSubscription
67
+ payload?: string
68
+ vapid: VapidKeys
69
+ subject: string
70
+ ttl?: number
71
+ urgency?: 'very-low' | 'low' | 'normal' | 'high'
72
+ topic?: string
73
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import * as expo from './drivers/expo';
2
2
  import * as fcm from './drivers/fcm';
3
3
  import type { PushResult } from '@stacksjs/types';
4
+ export type {
5
+ SendWebPushOptions,
6
+ VapidKeys,
7
+ WebPushResult,
8
+ WebPushSubscription,
9
+ } from './drivers/web-push';
4
10
  /**
5
11
  * Send a push notification using the specified driver
6
12
  */
@@ -22,4 +28,19 @@ export declare interface SendOptions {
22
28
  }
23
29
  export type PushDriver = 'expo' | 'fcm';
24
30
  export * from './drivers/index';
31
+ /*
32
+ * Named rather than `export * from './drivers/index'`.
33
+ *
34
+ * The declaration build rewrites a star re-export into imports of whatever the
35
+ * file happens to use, and everything else silently vanishes from the published
36
+ * types - so `sendWebPush` worked at runtime and did not exist to TypeScript.
37
+ * Naming them states the package's surface rather than inferring it, and it
38
+ * survives the build.
39
+ */
40
+ export {
41
+ buildVapidHeaders,
42
+ encryptPayload,
43
+ generateVapidKeys,
44
+ sendWebPush,
45
+ } from './drivers/web-push';
25
46
  export { expo, fcm };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var y=Object.defineProperty;var h=(G)=>G;function T(G,J){this[G]=h.bind(null,J)}var z=(G,J)=>{for(var N in J)y(G,N,{get:J[N],enumerable:!0,configurable:!0,set:T.bind(J,N)})};var E={};z(E,{sendBatch:()=>C,send:()=>j,isExpoPushToken:()=>w,getReceipts:()=>B,default:()=>u,Send:()=>j});import{log as X}from"@stacksjs/cli";var d="https://exp.host/--/api/v2/push/send";function w(G){return/^Expo(?:nent)?PushToken\[.+\]$/.test(G)||/^[a-zA-Z0-9-_]+$/.test(G)}async function j(G){let J=Array.isArray(G.to)?G.to:[G.to],N=J.filter((W)=>!w(W));if(N.length>0)X.warn(`Invalid Expo push tokens found: ${N.join(", ")}`);let Q=J.filter(w);if(Q.length===0)return{success:!1,provider:"expo",message:"No valid Expo push tokens provided"};try{let W=Q.map(($)=>({to:$,title:G.title,body:G.body,data:G.data,sound:G.sound??"default",badge:G.badge,channelId:G.channelId,priority:G.priority??"high",ttl:G.ttl})),Z=4096;for(let $ of W){let K=new TextEncoder().encode(JSON.stringify($)).length;if(K>4096)return{success:!1,provider:"expo",message:`Expo message payload (${K} bytes) exceeds 4 KiB per-message limit`}}let q=await fetch(d,{method:"POST",headers:{Accept:"application/json","Accept-Encoding":"gzip, deflate","Content-Type":"application/json"},body:JSON.stringify(W)});if(!q.ok){let $=await q.text();return X.error(`Expo push failed: ${$}`),{success:!1,provider:"expo",message:`HTTP ${q.status}: ${$}`}}let A=(await q.json()).data,V=A.filter(($)=>$.status==="error");if(V.length>0)X.warn(`Some push notifications failed: ${JSON.stringify(V)}`);let I=A.filter(($)=>$.status==="ok").length;return X.info(`Expo push sent: ${I}/${A.length} successful`),{success:V.length===0,provider:"expo",message:`Sent ${I}/${A.length} notifications`,messageId:A.find(($)=>$.id)?.id,data:{tickets:A}}}catch(W){let Z=W instanceof Error?W:Error(String(W));return X.error(`Expo push error: ${Z.message}`),{success:!1,provider:"expo",message:Z.message}}}async function B(G){let J=await fetch("https://exp.host/--/api/v2/push/getReceipts",{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify({ids:G})});if(!J.ok)throw Error(`Failed to get receipts: ${J.status}`);return(await J.json()).data}async function C(G,J=100){let N=[];for(let Q=0;Q<G.length;Q+=J){let W=G.slice(Q,Q+J),Z=await Promise.all(W.map((q)=>j(q)));N.push(...Z)}return N}var u={send:j,sendBatch:C,getReceipts:B,isExpoPushToken:w};var M={};z(M,{unsubscribeFromTopic:()=>b,subscribeToTopic:()=>S,sendToTopic:()=>L,sendMulticast:()=>U,sendLegacy:()=>P,send:()=>O,default:()=>f,configure:()=>F,Send:()=>O});import{log as H}from"@stacksjs/cli";var l="https://fcm.googleapis.com/fcm/send",p="https://fcm.googleapis.com/v1/projects",Y={};function F(G){Y={...Y,...G}}async function k(){if(!Y.serviceAccount)throw Error("Service account not configured for FCM v1 API");let{clientEmail:G,privateKey:J}=Y.serviceAccount,N=Math.floor(Date.now()/1000),Q={alg:"RS256",typ:"JWT"},W={iss:G,scope:"https://www.googleapis.com/auth/firebase.messaging",aud:"https://oauth2.googleapis.com/token",iat:N,exp:N+3600},Z=new TextEncoder,q=btoa(JSON.stringify(Q)),D=btoa(JSON.stringify(W)),A=`${q}.${D}`,V=J.replace(/-----BEGIN PRIVATE KEY-----/,"").replace(/-----END PRIVATE KEY-----/,"").replace(/\n/g,""),I=Uint8Array.from(atob(V),(x)=>x.charCodeAt(0)),$=await crypto.subtle.importKey("pkcs8",I,{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["sign"]),K=await crypto.subtle.sign("RSASSA-PKCS1-v1_5",$,Z.encode(A)),R=btoa(String.fromCharCode(...new Uint8Array(K))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,""),v=`${q}.${D}.${R}`,_=await fetch("https://oauth2.googleapis.com/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${v}`});if(!_.ok)throw Error(`Failed to get access token: ${_.status}`);return(await _.json()).access_token}async function P(G){if(!Y.serverKey)return{success:!1,provider:"fcm",message:"FCM server key not configured"};try{let J=await fetch(l,{method:"POST",headers:{Authorization:`key=${Y.serverKey}`,"Content-Type":"application/json"},body:JSON.stringify({to:G.to,registration_ids:G.registrationIds,topic:G.topic?`/topics/${G.topic}`:void 0,condition:G.condition,notification:G.notification,data:G.data,priority:G.priority??"high",time_to_live:G.ttl,collapse_key:G.collapseKey})});if(!J.ok){let Q=await J.text();return H.error(`FCM push failed: ${Q}`),{success:!1,provider:"fcm",message:`HTTP ${J.status}: ${Q}`}}let N=await J.json();if(N.failure>0)H.warn(`FCM: ${N.failure} notifications failed`);return H.info(`FCM push sent: ${N.success} successful, ${N.failure} failed`),{success:N.failure===0,provider:"fcm",message:`Sent ${N.success} notifications`,messageId:N.results?.[0]?.messageId,data:N}}catch(J){let N=J instanceof Error?J:Error(String(J));return H.error(`FCM push error: ${N.message}`),{success:!1,provider:"fcm",message:N.message}}}async function O(G){if(!Y.serviceAccount||!Y.projectId)return P(G);try{let J=await k(),N=`${p}/${Y.projectId}/messages:send`,Q={message:{notification:G.notification,data:G.data,android:{priority:G.priority??"high",ttl:G.ttl?`${G.ttl}s`:void 0,collapseKey:G.collapseKey},apns:{headers:{"apns-priority":G.priority==="high"?"10":"5"}}}};if(G.to)Q.message.token=G.to;else if(G.topic)Q.message.topic=G.topic;else if(G.condition)Q.message.condition=G.condition;let W=await fetch(N,{method:"POST",headers:{Authorization:`Bearer ${J}`,"Content-Type":"application/json"},body:JSON.stringify(Q)});if(!W.ok){let q=await W.text();return H.error(`FCM v1 push failed: ${q}`),{success:!1,provider:"fcm",message:`HTTP ${W.status}: ${q}`}}let Z=await W.json();return H.info(`FCM v1 push sent: ${Z.name}`),{success:!0,provider:"fcm",message:"Notification sent successfully",messageId:Z.name}}catch(J){let N=J instanceof Error?J:Error(String(J));return H.error(`FCM v1 push error: ${N.message}`),{success:!1,provider:"fcm",message:N.message}}}async function U(G,J){if(Y.serviceAccount&&Y.projectId)return Promise.all(G.map((N)=>O({...J,to:N})));return[await P({...J,registrationIds:G})]}async function L(G,J){return O({...J,topic:G})}async function S(G,J){if(!Y.serverKey)throw Error("FCM server key required for topic subscription");return(await fetch("https://iid.googleapis.com/iid/v1:batchAdd",{method:"POST",headers:{Authorization:`key=${Y.serverKey}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${J}`,registration_tokens:G})})).ok}async function b(G,J){if(!Y.serverKey)throw Error("FCM server key required for topic unsubscription");return(await fetch("https://iid.googleapis.com/iid/v1:batchRemove",{method:"POST",headers:{Authorization:`key=${Y.serverKey}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${J}`,registration_tokens:G})})).ok}var f={send:O,sendLegacy:P,sendMulticast:U,sendToTopic:L,subscribeToTopic:S,unsubscribeFromTopic:b,configure:F};async function a(G,J,N={}){let Q=N.driver??"expo";if(Q==="expo")return j({to:G,title:J.title,body:J.body,data:J.data,badge:J.badge,sound:J.sound,priority:J.priority});if(Q==="fcm"){let W=Array.isArray(G)?G:[G];if(W.length===1)return O({to:W[0],notification:{title:J.title??"",body:J.body},data:J.data,priority:J.priority==="default"?"normal":J.priority});let Z=await U(W,{notification:{title:J.title??"",body:J.body},data:J.data,priority:J.priority==="default"?"normal":J.priority});return{success:Z.every((D)=>D.success),provider:"fcm",message:`Sent to ${Z.filter((D)=>D.success).length}/${Z.length} devices`}}return{success:!1,provider:Q,message:`Unknown push driver: ${Q}`}}function t(G){F(G)}export{a as send,M as fcm,E as expo,t as configureFCM};
2
+ var m=Object.defineProperty;var p=(G)=>G;function g(G,Q){this[G]=p.bind(null,Q)}var w=(G,Q)=>{for(var W in Q)m(G,W,{get:Q[W],enumerable:!0,configurable:!0,set:g.bind(Q,W)})};var v={};w(v,{sendBatch:()=>T,send:()=>L,isExpoPushToken:()=>E,getReceipts:()=>x,default:()=>a,Send:()=>L});import{log as I}from"@stacksjs/cli";var c="https://exp.host/--/api/v2/push/send";function E(G){return/^Expo(?:nent)?PushToken\[.+\]$/.test(G)||/^[a-zA-Z0-9-_]+$/.test(G)}async function L(G){let Q=Array.isArray(G.to)?G.to:[G.to],W=Q.filter((Z)=>!E(Z));if(W.length>0)I.warn(`Invalid Expo push tokens found: ${W.join(", ")}`);let Y=Q.filter(E);if(Y.length===0)return{success:!1,provider:"expo",message:"No valid Expo push tokens provided"};try{let Z=Y.map((A)=>({to:A,title:G.title,body:G.body,data:G.data,sound:G.sound??"default",badge:G.badge,channelId:G.channelId,priority:G.priority??"high",ttl:G.ttl})),q=4096;for(let A of Z){let V=new TextEncoder().encode(JSON.stringify(A)).length;if(V>4096)return{success:!1,provider:"expo",message:`Expo message payload (${V} bytes) exceeds 4 KiB per-message limit`}}let $=await fetch(c,{method:"POST",headers:{Accept:"application/json","Accept-Encoding":"gzip, deflate","Content-Type":"application/json"},body:JSON.stringify(Z)});if(!$.ok){let A=await $.text();return I.error(`Expo push failed: ${A}`),{success:!1,provider:"expo",message:`HTTP ${$.status}: ${A}`}}let N=(await $.json()).data,O=N.filter((A)=>A.status==="error");if(O.length>0)I.warn(`Some push notifications failed: ${JSON.stringify(O)}`);let F=N.filter((A)=>A.status==="ok").length;return I.info(`Expo push sent: ${F}/${N.length} successful`),{success:O.length===0,provider:"expo",message:`Sent ${F}/${N.length} notifications`,messageId:N.find((A)=>A.id)?.id,data:{tickets:N}}}catch(Z){let q=Z instanceof Error?Z:Error(String(Z));return I.error(`Expo push error: ${q.message}`),{success:!1,provider:"expo",message:q.message}}}async function x(G){let Q=await fetch("https://exp.host/--/api/v2/push/getReceipts",{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify({ids:G})});if(!Q.ok)throw Error(`Failed to get receipts: ${Q.status}`);return(await Q.json()).data}async function T(G,Q=100){let W=[];for(let Y=0;Y<G.length;Y+=Q){let Z=G.slice(Y,Y+Q),q=await Promise.all(Z.map(($)=>L($)));W.push(...q)}return W}var a={send:L,sendBatch:T,getReceipts:x,isExpoPushToken:E};var b={};w(b,{unsubscribeFromTopic:()=>h,subscribeToTopic:()=>y,sendToTopic:()=>B,sendMulticast:()=>C,sendLegacy:()=>z,send:()=>U,default:()=>i,configure:()=>K,Send:()=>U});import{log as j}from"@stacksjs/cli";var r="https://fcm.googleapis.com/fcm/send",t="https://fcm.googleapis.com/v1/projects",X={};function K(G){X={...X,...G}}async function n(){if(!X.serviceAccount)throw Error("Service account not configured for FCM v1 API");let{clientEmail:G,privateKey:Q}=X.serviceAccount,W=Math.floor(Date.now()/1000),Y={alg:"RS256",typ:"JWT"},Z={iss:G,scope:"https://www.googleapis.com/auth/firebase.messaging",aud:"https://oauth2.googleapis.com/token",iat:W,exp:W+3600},q=new TextEncoder,$=btoa(JSON.stringify(Y)),D=btoa(JSON.stringify(Z)),N=`${$}.${D}`,O=Q.replace(/-----BEGIN PRIVATE KEY-----/,"").replace(/-----END PRIVATE KEY-----/,"").replace(/\n/g,""),F=Uint8Array.from(atob(O),(f)=>f.charCodeAt(0)),A=await crypto.subtle.importKey("pkcs8",F,{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["sign"]),V=await crypto.subtle.sign("RSASSA-PKCS1-v1_5",A,q.encode(N)),R=btoa(String.fromCharCode(...new Uint8Array(V))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,""),S=`${$}.${D}.${R}`,H=await fetch("https://oauth2.googleapis.com/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${S}`});if(!H.ok)throw Error(`Failed to get access token: ${H.status}`);return(await H.json()).access_token}async function z(G){if(!X.serverKey)return{success:!1,provider:"fcm",message:"FCM server key not configured"};try{let Q=await fetch(r,{method:"POST",headers:{Authorization:`key=${X.serverKey}`,"Content-Type":"application/json"},body:JSON.stringify({to:G.to,registration_ids:G.registrationIds,topic:G.topic?`/topics/${G.topic}`:void 0,condition:G.condition,notification:G.notification,data:G.data,priority:G.priority??"high",time_to_live:G.ttl,collapse_key:G.collapseKey})});if(!Q.ok){let Y=await Q.text();return j.error(`FCM push failed: ${Y}`),{success:!1,provider:"fcm",message:`HTTP ${Q.status}: ${Y}`}}let W=await Q.json();if(W.failure>0)j.warn(`FCM: ${W.failure} notifications failed`);return j.info(`FCM push sent: ${W.success} successful, ${W.failure} failed`),{success:W.failure===0,provider:"fcm",message:`Sent ${W.success} notifications`,messageId:W.results?.[0]?.messageId,data:W}}catch(Q){let W=Q instanceof Error?Q:Error(String(Q));return j.error(`FCM push error: ${W.message}`),{success:!1,provider:"fcm",message:W.message}}}async function U(G){if(!X.serviceAccount||!X.projectId)return z(G);try{let Q=await n(),W=`${t}/${X.projectId}/messages:send`,Y={message:{notification:G.notification,data:G.data,android:{priority:G.priority??"high",ttl:G.ttl?`${G.ttl}s`:void 0,collapseKey:G.collapseKey},apns:{headers:{"apns-priority":G.priority==="high"?"10":"5"}}}};if(G.to)Y.message.token=G.to;else if(G.topic)Y.message.topic=G.topic;else if(G.condition)Y.message.condition=G.condition;let Z=await fetch(W,{method:"POST",headers:{Authorization:`Bearer ${Q}`,"Content-Type":"application/json"},body:JSON.stringify(Y)});if(!Z.ok){let $=await Z.text();return j.error(`FCM v1 push failed: ${$}`),{success:!1,provider:"fcm",message:`HTTP ${Z.status}: ${$}`}}let q=await Z.json();return j.info(`FCM v1 push sent: ${q.name}`),{success:!0,provider:"fcm",message:"Notification sent successfully",messageId:q.name}}catch(Q){let W=Q instanceof Error?Q:Error(String(Q));return j.error(`FCM v1 push error: ${W.message}`),{success:!1,provider:"fcm",message:W.message}}}async function C(G,Q){if(X.serviceAccount&&X.projectId)return Promise.all(G.map((W)=>U({...Q,to:W})));return[await z({...Q,registrationIds:G})]}async function B(G,Q){return U({...Q,topic:G})}async function y(G,Q){if(!X.serverKey)throw Error("FCM server key required for topic subscription");return(await fetch("https://iid.googleapis.com/iid/v1:batchAdd",{method:"POST",headers:{Authorization:`key=${X.serverKey}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${Q}`,registration_tokens:G})})).ok}async function h(G,Q){if(!X.serverKey)throw Error("FCM server key required for topic unsubscription");return(await fetch("https://iid.googleapis.com/iid/v1:batchRemove",{method:"POST",headers:{Authorization:`key=${X.serverKey}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${Q}`,registration_tokens:G})})).ok}var i={send:U,sendLegacy:z,sendMulticast:C,sendToTopic:B,subscribeToTopic:y,unsubscribeFromTopic:h,configure:K};import{Buffer as J}from"buffer";import{createCipheriv as o,createECDH as s,createHmac as k,createPrivateKey as e,createSign as GG,generateKeyPairSync as QG,randomBytes as WG}from"crypto";var d="prime256v1";function _(G){return G.toString("base64url")}function M(G){return J.from(G,"base64url")}function YG(){let{publicKey:G,privateKey:Q}=QG("ec",{namedCurve:d}),W=G.export({type:"spki",format:"der"}),Y=Q.export({format:"jwk"});return{publicKey:_(W.subarray(W.length-65)),privateKey:String(Y.d)}}function ZG(G){let Q=new URL(G);return`${Q.protocol}//${Q.host}`}function l(G){let W=Math.floor(G.nowSeconds??Date.now()/1000)+Math.min(G.expiresInSeconds??43200,86400),Y=_(J.from(JSON.stringify({typ:"JWT",alg:"ES256"}))),Z=_(J.from(JSON.stringify({aud:ZG(G.endpoint),exp:W,sub:G.subject}))),q=`${Y}.${Z}`,$=e({key:{kty:"EC",crv:"P-256",d:G.vapid.privateKey,x:_(M(G.vapid.publicKey).subarray(1,33)),y:_(M(G.vapid.publicKey).subarray(33,65))},format:"jwk"}),D=GG("SHA256");D.update(q);let N=D.sign({key:$,dsaEncoding:"ieee-p1363"});return{Authorization:`vapid t=${q}.${_(N)}, k=${G.vapid.publicKey}`,"Crypto-Key":`p256ecdsa=${G.vapid.publicKey}`}}function P(G,Q,W,Y){let Z=k("sha256",G).update(Q).digest();return k("sha256",Z).update(J.concat([W,J.from([1])])).digest().subarray(0,Y)}function u(G,Q,W=WG(16)){let Y=M(G.keys.p256dh),Z=M(G.keys.auth),q=s(d);q.generateKeys();let $=q.getPublicKey(),D=q.computeSecret(Y),N=J.concat([J.from("WebPush: info\x00"),Y,$]),O=P(Z,D,N,32),F=P(W,O,J.from("Content-Encoding: aes128gcm\x00"),16),A=P(W,O,J.from("Content-Encoding: nonce\x00"),12),V=o("aes-128-gcm",F,A),R=J.concat([J.from(Q,"utf8"),J.from([2])]),S=J.concat([V.update(R),V.final(),V.getAuthTag()]),H=J.alloc(4);return H.writeUInt32BE(4096,0),J.concat([W,H,J.from([$.length]),$,S])}async function $G(G){let{subscription:Q,vapid:W,subject:Y}=G;if(!Q?.endpoint)return{success:!1,status:0,expired:!1,error:"no endpoint"};let Z={...l({endpoint:Q.endpoint,vapid:W,subject:Y}),TTL:String(G.ttl??86400),Urgency:G.urgency??"normal"};if(G.topic)Z.Topic=G.topic;let q;if(G.payload){if(!Q.keys?.p256dh||!Q.keys?.auth)return{success:!1,status:0,expired:!0,error:"subscription has no keys"};q=u(Q,G.payload),Z["Content-Encoding"]="aes128gcm",Z["Content-Type"]="application/octet-stream",Z["Content-Length"]=String(q.length)}try{let $=await fetch(Q.endpoint,{method:"POST",headers:Z,body:q}),D=$.status===404||$.status===410;if($.status>=200&&$.status<300)return{success:!0,status:$.status,expired:!1};return{success:!1,status:$.status,expired:D,error:(await $.text().catch(()=>"")).slice(0,500)||`push service answered ${$.status}`}}catch($){return{success:!1,status:0,expired:!1,error:$ instanceof Error?$.message:String($)}}}async function VG(G,Q,W={}){let Y=W.driver??"expo";if(Y==="expo")return L({to:G,title:Q.title,body:Q.body,data:Q.data,badge:Q.badge,sound:Q.sound,priority:Q.priority});if(Y==="fcm"){let Z=Array.isArray(G)?G:[G];if(Z.length===1)return U({to:Z[0],notification:{title:Q.title??"",body:Q.body},data:Q.data,priority:Q.priority==="default"?"normal":Q.priority});let q=await C(Z,{notification:{title:Q.title??"",body:Q.body},data:Q.data,priority:Q.priority==="default"?"normal":Q.priority});return{success:q.every((D)=>D.success),provider:"fcm",message:`Sent to ${q.filter((D)=>D.success).length}/${q.length} devices`}}return{success:!1,provider:Y,message:`Unknown push driver: ${Y}`}}function jG(G){K(G)}export{$G as sendWebPush,VG as send,YG as generateVapidKeys,b as fcm,v as expo,u as encryptPayload,jG as configureFCM,l as buildVapidHeaders};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/push",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.306",
5
+ "version": "0.70.310",
6
6
  "description": "The Stacks Push integration",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -56,9 +56,9 @@
56
56
  "prepublishOnly": "bun run build"
57
57
  },
58
58
  "devDependencies": {
59
- "@stacksjs/cli": "0.70.306",
60
- "@stacksjs/config": "0.70.306",
59
+ "@stacksjs/cli": "0.70.310",
60
+ "@stacksjs/config": "0.70.310",
61
61
  "better-dx": "^0.2.17",
62
- "@stacksjs/error-handling": "0.70.306"
62
+ "@stacksjs/error-handling": "0.70.310"
63
63
  }
64
64
  }