@stacksjs/push 0.75.221 → 0.75.223

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.
@@ -3,8 +3,16 @@ export declare function configure(options: APNSConfig): void;
3
3
  export declare function resetConfiguration(): void;
4
4
  export declare function buildProviderToken(teamId: string, keyId: string, privateKey: string, now?: unknown): string;
5
5
  export declare function buildPayload(message: APNSMessage): string;
6
- /** Apple device tokens come from the native bridge, independently of Firebase. */
7
- export declare function send(to: string | string[], message: APNSMessage): Promise<PushResult>;
6
+ /**
7
+ * Apple device tokens come from the native bridge, independently of Firebase.
8
+ *
9
+ * `credentials` are this send's own, for a server that sends for more than
10
+ * one app: a white-label app has its own bundle ID as the topic, and one
11
+ * published on another team signs with that team's key. Each field it sets
12
+ * wins over `configure()` and `config/services.ts`; what it leaves out comes
13
+ * from them.
14
+ */
15
+ export declare function send(to: string | string[], message: APNSMessage, credentials?: APNSConfig): Promise<PushResult>;
8
16
  export declare interface APNSConfig {
9
17
  teamId?: string
10
18
  keyId?: string
@@ -41,12 +41,15 @@ export declare function buildServiceAccountAssertion(clientEmail: string, privat
41
41
  export declare function clearAccessTokens(): void;
42
42
  /**
43
43
  * Send push notification via FCM v1 API (using service account)
44
+ *
45
+ * `credentialsOverride`: this send's own project and service account (see
46
+ * `credentialsFor`), for an app other than the one `config/services.ts` names.
44
47
  */
45
- export declare function send(message: FCMMessage): Promise<PushResult>;
48
+ export declare function send(message: FCMMessage, credentialsOverride?: FCMConfig): Promise<PushResult>;
46
49
  /**
47
50
  * Send to multiple tokens
48
51
  */
49
- export declare function sendMulticast(tokens: string[], message: Omit<FCMMessage, 'to'>): Promise<PushResult[]>;
52
+ export declare function sendMulticast(tokens: string[], message: Omit<FCMMessage, 'to'>, credentials?: FCMConfig): Promise<PushResult[]>;
50
53
  /**
51
54
  * Send to a topic
52
55
  */
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export declare interface PushNotification {
37
37
  }
38
38
  export declare interface SendOptions {
39
39
  driver?: PushDriver
40
+ apns?: apns.APNSConfig
41
+ fcm?: fcm.FCMConfig
40
42
  }
41
43
  export type PushDriver = 'expo' | 'fcm' | 'apns';
42
44
  export * from './drivers/index';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- var me=Object.defineProperty;var he=(e)=>e;function be(e,t){this[e]=he.bind(null,t)}var B=(e,t)=>{for(var r in t)me(e,r,{get:t[r],enumerable:!0,configurable:!0,set:be.bind(t,r)})};var Y={};B(Y,{Send:()=>I,configure:()=>N,default:()=>Pe,getReceipts:()=>G,isExpoPushToken:()=>K,resetConfiguration:()=>ve,resolveAccessToken:()=>z,send:()=>I,sendBatch:()=>X});var ae={};B(ae,{Send:()=>E,buildServiceAccountAssertion:()=>ee,clearAccessTokens:()=>ke,configure:()=>O,default:()=>Ee,normalizePrivateKey:()=>Q,resetConfiguration:()=>Se,resolveConfig:()=>D,send:()=>E,sendMulticast:()=>_,sendToTopic:()=>re,subscribeToTopic:()=>oe,unsubscribeFromTopic:()=>ie});var pe={};B(pe,{buildPayload:()=>ue,buildProviderToken:()=>ce,configure:()=>W,resetConfiguration:()=>Ie,send:()=>U});import{log as k}from"@stacksjs/cli";async function w(){let{awaitConfig:e}=await import("@stacksjs/config");return(await e()).services??{}}function p(e){let t=e?.trim();return t?t:void 0}var xe="https://exp.host/--/api/v2/push/send",T={};function N(e){T={...T,...e}}function ve(){T={}}async function z(){if(p(T.accessToken))return p(T.accessToken);try{return p((await w()).expo?.accessToken)}catch(e){k.debug(`Expo: could not read config/services.ts: ${e instanceof Error?e.message:String(e)}`);return}}async function q(e={}){let t={Accept:"application/json","Content-Type":"application/json",...e},r=await z();if(r)t.Authorization=`Bearer ${r}`;return t}function K(e){return/^Expo(?:nent)?PushToken\[.+\]$/.test(e)||/^[a-zA-Z0-9-_]+$/.test(e)}async function I(e){let t=Array.isArray(e.to)?e.to:[e.to],r=t.filter((n)=>!K(n));if(r.length>0)k.warn(`Invalid Expo push tokens found: ${r.join(", ")}`);let s=t.filter(K);if(s.length===0)return{success:!1,provider:"expo",message:"No valid Expo push tokens provided"};try{let n=s.map((u)=>({to:u,title:e.title,body:e.body,data:e.data,sound:e.sound??"default",badge:e.badge,channelId:e.channelId,priority:e.priority??"high",ttl:e.ttl})),i=4096;for(let u of n){let l=new TextEncoder().encode(JSON.stringify(u)).length;if(l>4096)return{success:!1,provider:"expo",message:`Expo message payload (${l} bytes) exceeds 4 KiB per-message limit`}}let o=await fetch(xe,{method:"POST",headers:await q({"Accept-Encoding":"gzip, deflate"}),body:JSON.stringify(n)});if(!o.ok){let u=await o.text();return k.error(`Expo push failed: ${u}`),{success:!1,provider:"expo",message:`HTTP ${o.status}: ${u}`}}let c=(await o.json()).data,f=c.filter((u)=>u.status==="error");if(f.length>0)k.warn(`Some push notifications failed: ${JSON.stringify(f)}`);let g=c.filter((u)=>u.status==="ok").length;return k.info(`Expo push sent: ${g}/${c.length} successful`),{success:f.length===0,provider:"expo",message:`Sent ${g}/${c.length} notifications`,messageId:c.find((u)=>u.id)?.id,data:{tickets:c}}}catch(n){let i=n instanceof Error?n:Error(String(n));return k.error(`Expo push error: ${i.message}`),{success:!1,provider:"expo",message:i.message}}}async function G(e){let t=await fetch("https://exp.host/--/api/v2/push/getReceipts",{method:"POST",headers:await q(),body:JSON.stringify({ids:e})});if(!t.ok)throw Error(`Failed to get receipts: ${t.status}`);return(await t.json()).data}async function X(e,t=100){let r=[];for(let s=0;s<e.length;s+=t){let n=e.slice(s,s+t),i=await Promise.all(n.map((o)=>I(o)));r.push(...i)}return r}var Pe={send:I,sendBatch:X,getReceipts:G,isExpoPushToken:K,configure:N};import{log as R}from"@stacksjs/cli";var Z="https://fcm.googleapis.com/v1/projects",m={};function O(e){m={...m,...e}}function Se(){m={},j.clear()}function Q(e){return e.includes("\\n")?e.replace(/\\n/g,`
3
- `):e}async function D(){if(m.projectId&&m.serviceAccount)return m;let e={};try{e=await w()}catch(n){R.debug(`FCM: could not read config/services.ts: ${n instanceof Error?n.message:String(n)}`)}let t=e.fcm??{},r=p(t.clientEmail),s=p(t.privateKey);return{projectId:m.projectId??p(t.projectId),serviceAccount:m.serviceAccount??(r&&s?{clientEmail:r,privateKey:Q(s)}:void 0)}}function J(e){return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async function ee(e,t,r){let s={alg:"RS256",typ:"JWT"},n={iss:e,scope:"https://www.googleapis.com/auth/firebase.messaging",aud:"https://oauth2.googleapis.com/token",iat:r,exp:r+3600},i=new TextEncoder,o=J(JSON.stringify(s)),a=J(JSON.stringify(n)),c=`${o}.${a}`,f=t.replace(/-----BEGIN PRIVATE KEY-----/,"").replace(/-----END PRIVATE KEY-----/,"").replace(/\s/g,""),g=Uint8Array.from(atob(f),(P)=>P.charCodeAt(0)),u=await crypto.subtle.importKey("pkcs8",g,{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["sign"]),l=await crypto.subtle.sign("RSASSA-PKCS1-v1_5",u,i.encode(c)),b=J(String.fromCharCode(...new Uint8Array(l)));return`${o}.${a}.${b}`}var j=new Map,we=60000;function ke(){j.clear()}async function te(e){if(!e.serviceAccount)throw Error("Service account not configured for FCM v1 API");let{clientEmail:t,privateKey:r}=e.serviceAccount,s=j.get(t);if(s&&s.expiresAt-we>Date.now())return s.token;let n=await ee(t,r,Math.floor(Date.now()/1000)),i=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=${n}`});if(!i.ok)throw Error(`Failed to get access token: ${i.status}`);let o=await i.json(),a=Number(o.expires_in);return j.set(t,{token:o.access_token,expiresAt:Date.now()+(Number.isFinite(a)&&a>0?a:3600)*1000}),o.access_token}async function E(e){let t=await D();if(!t.serviceAccount||!t.projectId)return{success:!1,provider:"fcm",message:"FCM service account and projectId not configured - set FCM_PROJECT_ID, FCM_CLIENT_EMAIL and FCM_PRIVATE_KEY, or call configureFCM()"};try{let r=await te(t),s=`${Z}/${t.projectId}/messages:send`,n={message:{notification:e.notification,data:e.data,android:{priority:e.priority??"high",ttl:e.ttl?`${e.ttl}s`:void 0,collapseKey:e.collapseKey},apns:{headers:{"apns-priority":e.priority==="high"?"10":"5"}}}};if(e.to)n.message.token=e.to;else if(e.topic)n.message.topic=e.topic;else if(e.condition)n.message.condition=e.condition;let i=await fetch(s,{method:"POST",headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:JSON.stringify(n)});if(!i.ok){let a=await i.text();return R.error(`FCM v1 push failed: ${a}`),{success:!1,provider:"fcm",message:`HTTP ${i.status}: ${a}`}}let o=await i.json();return R.info(`FCM v1 push sent: ${o.name}`),{success:!0,provider:"fcm",message:"Notification sent successfully",messageId:o.name}}catch(r){let s=r instanceof Error?r:Error(String(r));return R.error(`FCM v1 push error: ${s.message}`),{success:!1,provider:"fcm",message:s.message}}}async function _(e,t){return Promise.all(e.map((r)=>E({...t,to:r})))}async function re(e,t){return E({...t,topic:e})}async function se(e,t,r,s){let n=await te(e),i=`${Z}/${e.projectId}/registrations/${encodeURIComponent(t)}/topicSubscriptions`,o=s?`${i}?topic_name=${encodeURIComponent(r)}`:`${i}/${encodeURIComponent(r)}`,a=await fetch(o,{method:s?"POST":"DELETE",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"},body:"{}"});if(a.ok||s&&a.status===409)return!0;return R.error(`FCM topic ${s?"subscribe":"unsubscribe"} failed for topic ${r}: HTTP ${a.status}`),!1}async function ne(e){let t=await D();if(!t.serviceAccount||!t.projectId)throw Error(`FCM service account and projectId required for ${e}`);return t}async function oe(e,t){let r=await ne("topic subscription");return(await Promise.all(e.map((n)=>se(r,n,t,!0)))).every(Boolean)}async function ie(e,t){let r=await ne("topic unsubscription");return(await Promise.all(e.map((n)=>se(r,n,t,!1)))).every(Boolean)}var Ee={send:E,sendMulticast:_,sendToTopic:re,subscribeToTopic:oe,unsubscribeFromTopic:ie,configure:O};import{createPrivateKey as Ce,sign as Ae}from"crypto";import Te,{constants as $e}from"http2";var v={},h;function W(e){v={...e},h=void 0}function Ie(){v={},h=void 0}function ce(e,t,r,s=Math.floor(Date.now()/1000)){let n=Buffer.from(JSON.stringify({alg:"ES256",kid:t})).toString("base64url"),i=Buffer.from(JSON.stringify({iss:e,iat:s})).toString("base64url"),o=`${n}.${i}`,a=Ce(r.replace(/\\n/g,`
4
- `));if(a.asymmetricKeyType!=="ec"||a.asymmetricKeyDetails?.namedCurve!=="prime256v1")throw Error("APNs requires an ES256 P-256 signing key");return`${o}.${Ae("sha256",Buffer.from(o),{key:a,dsaEncoding:"ieee-p1363"}).toString("base64url")}`}function ue(e){let t=JSON.stringify({...e.data,aps:{alert:{title:e.title??"",body:e.body},...e.sound!==null?{sound:e.sound??"default"}:{},...e.badge!==void 0?{badge:e.badge}:{}}});if(Buffer.byteLength(t)>4096)throw Error("APNs notification exceeds 4096 bytes");return t}async function U(e,t){let r=Array.isArray(e)?e:[e];if(!r.length||r.some((c)=>!/^[a-f\d]{64}$/i.test(c)))return{success:!1,provider:"apns",message:"A valid Apple device token is required"};let s=(await w()).apns,n=p(v.teamId)??p(s?.teamId),i=p(v.keyId)??p(s?.keyId),o=p(v.privateKey)??p(s?.privateKey),a=p(v.topic)??p(s?.topic);if(!n||!i||!o||!a)return{success:!1,provider:"apns",message:"APNs requires teamId, keyId, privateKey and topic"};try{let c=ue(t),f=Math.floor(Date.now()/1000),g=`${n}:${i}:${o}`;if(!h||h.identity!==g||f-h.issuedAt>=3000||f<h.issuedAt)h={identity:g,token:ce(n,i,o,f),issuedAt:f};let u=h.token,l=Te.connect(v.sandbox??s?.sandbox?"https://api.sandbox.push.apple.com":"https://api.push.apple.com");l.on("error",()=>{});try{let b=await Promise.all(r.map((S)=>new Promise((M)=>{let y=l.request({":method":"POST",":path":`/3/device/${S}`,authorization:`bearer ${u}`,"apns-topic":a,"apns-push-type":"alert","apns-priority":t.priority==="normal"?"5":"10","content-type":"application/json",...t.collapseId?{"apns-collapse-id":t.collapseId}:{}}),A=0,H="",L;y.setEncoding("utf8");let ye=setTimeout(()=>{y.close($e.NGHTTP2_CANCEL),M({success:!1,status:0,reason:"Timeout"})},15000);y.on("close",()=>{clearTimeout(ye),M({success:!1,status:A,reason:"TransportClosed"})}),y.on("response",(x)=>{A=Number(x[":status"]),L=String(x["apns-id"]??"")||void 0}),y.on("data",(x)=>{H+=x}),y.on("error",()=>M({success:!1,status:A,reason:"TransportError"})),y.on("end",()=>{let x;try{x=JSON.parse(H).reason}catch{}M({success:A===200,status:A,reason:x,messageId:L})}),y.end(c)}))),P=b.filter((S)=>S.success).length;return{success:P===r.length,provider:"apns",message:`Apple accepted ${P}/${r.length} notifications`,messageId:b[0]?.messageId,data:{results:b}}}finally{l.destroy()}}catch(c){return{success:!1,provider:"apns",message:c instanceof Error?c.message:"APNs delivery failed"}}}import{Buffer as d}from"buffer";import{createCipheriv as Re,createECDH as Me,createHmac as de,createPrivateKey as Ke,createSign as Ne,generateKeyPairSync as je,randomBytes as Oe}from"crypto";var fe="prime256v1";function C(e){return e.toString("base64url")}function F(e){return d.from(e,"base64url")}function _e(){let{publicKey:e,privateKey:t}=je("ec",{namedCurve:fe}),r=e.export({type:"spki",format:"der"}),s=t.export({format:"jwk"});return{publicKey:C(r.subarray(r.length-65)),privateKey:String(s.d)}}function Fe(e){let t=new URL(e);return`${t.protocol}//${t.host}`}function le(e){let r=Math.floor(e.nowSeconds??Date.now()/1000)+Math.min(e.expiresInSeconds??43200,86400),s=C(d.from(JSON.stringify({typ:"JWT",alg:"ES256"}))),n=C(d.from(JSON.stringify({aud:Fe(e.endpoint),exp:r,sub:e.subject}))),i=`${s}.${n}`,o=Ke({key:{kty:"EC",crv:"P-256",d:e.vapid.privateKey,x:C(F(e.vapid.publicKey).subarray(1,33)),y:C(F(e.vapid.publicKey).subarray(33,65))},format:"jwk"}),a=Ne("SHA256");a.update(i);let c=a.sign({key:o,dsaEncoding:"ieee-p1363"});return{Authorization:`vapid t=${i}.${C(c)}, k=${e.vapid.publicKey}`,"Crypto-Key":`p256ecdsa=${e.vapid.publicKey}`}}function V(e,t,r,s){let n=de("sha256",e).update(t).digest();return de("sha256",n).update(d.concat([r,d.from([1])])).digest().subarray(0,s)}function ge(e,t,r=Oe(16)){let s=F(e.keys.p256dh),n=F(e.keys.auth),i=Me(fe);i.generateKeys();let o=i.getPublicKey(),a=i.computeSecret(s),c=d.concat([d.from("WebPush: info\x00"),s,o]),f=V(n,a,c,32),g=V(r,f,d.from("Content-Encoding: aes128gcm\x00"),16),u=V(r,f,d.from("Content-Encoding: nonce\x00"),12),l=Re("aes-128-gcm",g,u),b=d.concat([d.from(t,"utf8"),d.from([2])]),P=d.concat([l.update(b),l.final(),l.getAuthTag()]),S=d.alloc(4);return S.writeUInt32BE(4096,0),d.concat([r,S,d.from([o.length]),o,P])}async function Be(e){let{subscription:t,vapid:r,subject:s}=e;if(!t?.endpoint)return{success:!1,status:0,expired:!1,error:"no endpoint"};let n={...le({endpoint:t.endpoint,vapid:r,subject:s}),TTL:String(e.ttl??86400),Urgency:e.urgency??"normal"};if(e.topic)n.Topic=e.topic;let i;if(e.payload){if(!t.keys?.p256dh||!t.keys?.auth)return{success:!1,status:0,expired:!0,error:"subscription has no keys"};i=ge(t,e.payload),n["Content-Encoding"]="aes128gcm",n["Content-Type"]="application/octet-stream",n["Content-Length"]=String(i.length)}try{let o=await fetch(t.endpoint,{method:"POST",headers:n,body:i?new Uint8Array(i):void 0}),a=o.status===404||o.status===410;if(o.status>=200&&o.status<300)return{success:!0,status:o.status,expired:!1};return{success:!1,status:o.status,expired:a,error:(await o.text().catch(()=>"")).slice(0,500)||`push service answered ${o.status}`}}catch(o){return{success:!1,status:0,expired:!1,error:o instanceof Error?o.message:String(o)}}}async function Ze(e,t,r={}){let s=r.driver??"expo";if(s==="apns")return U(e,t);if(s==="expo")return I({to:e,title:t.title,body:t.body,data:t.data,badge:t.badge,sound:t.sound,priority:t.priority});if(s==="fcm"){let n=Array.isArray(e)?e:[e];if(n.length===1)return E({to:n[0],notification:{title:t.title??"",body:t.body},data:t.data,priority:t.priority==="default"?"normal":t.priority});let i=await _(n,{notification:{title:t.title??"",body:t.body},data:t.data,priority:t.priority==="default"?"normal":t.priority});return{success:i.every((a)=>a.success),provider:"fcm",message:`Sent to ${i.filter((a)=>a.success).length}/${i.length} devices`}}return{success:!1,provider:s,message:`Unknown push driver: ${s}`}}function Qe(e){O(e)}function et(e){N(e)}function tt(e){W(e)}export{pe as apns,le as buildVapidHeaders,tt as configureAPNS,et as configureExpo,Qe as configureFCM,ge as encryptPayload,Y as expo,ae as fcm,_e as generateVapidKeys,Ze as send,Be as sendWebPush};
2
+ var xe=Object.defineProperty;var ve=(e)=>e;function Pe(e,t){this[e]=ve.bind(null,t)}var D=(e,t)=>{for(var r in t)xe(e,r,{get:t[r],enumerable:!0,configurable:!0,set:Pe.bind(t,r)})};var te={};D(te,{Send:()=>T,configure:()=>K,default:()=>we,getReceipts:()=>Q,isExpoPushToken:()=>R,resetConfiguration:()=>Ce,resolveAccessToken:()=>Y,send:()=>T,sendBatch:()=>ee});var pe={};D(pe,{Send:()=>C,buildServiceAccountAssertion:()=>se,clearAccessTokens:()=>Ae,configure:()=>j,default:()=>$e,normalizePrivateKey:()=>U,resetConfiguration:()=>Ee,resolveConfig:()=>V,send:()=>C,sendMulticast:()=>F,sendToTopic:()=>oe,subscribeToTopic:()=>ce,unsubscribeFromTopic:()=>ue});var ge={};D(ge,{buildPayload:()=>fe,buildProviderToken:()=>de,configure:()=>H,resetConfiguration:()=>Ne,send:()=>L});import{log as S}from"@stacksjs/cli";async function P(){let{awaitConfig:e}=await import("@stacksjs/config");return(await e()).services??{}}function c(e){let t=e?.trim();return t?t:void 0}var Se="https://exp.host/--/api/v2/push/send",A={};function K(e){A={...A,...e}}function Ce(){A={}}async function Y(){if(c(A.accessToken))return c(A.accessToken);try{return c((await P()).expo?.accessToken)}catch(e){S.debug(`Expo: could not read config/services.ts: ${e instanceof Error?e.message:String(e)}`);return}}async function Z(e={}){let t={Accept:"application/json","Content-Type":"application/json",...e},r=await Y();if(r)t.Authorization=`Bearer ${r}`;return t}function R(e){return/^Expo(?:nent)?PushToken\[.+\]$/.test(e)||/^[a-zA-Z0-9-_]+$/.test(e)}async function T(e){let t=Array.isArray(e.to)?e.to:[e.to],r=t.filter((n)=>!R(n));if(r.length>0)S.warn(`Invalid Expo push tokens found: ${r.join(", ")}`);let s=t.filter(R);if(s.length===0)return{success:!1,provider:"expo",message:"No valid Expo push tokens provided"};try{let n=s.map((u)=>({to:u,title:e.title,body:e.body,data:e.data,sound:e.sound??"default",badge:e.badge,channelId:e.channelId,priority:e.priority??"high",ttl:e.ttl})),i=4096;for(let u of n){let g=new TextEncoder().encode(JSON.stringify(u)).length;if(g>4096)return{success:!1,provider:"expo",message:`Expo message payload (${g} bytes) exceeds 4 KiB per-message limit`}}let o=await fetch(Se,{method:"POST",headers:await Z({"Accept-Encoding":"gzip, deflate"}),body:JSON.stringify(n)});if(!o.ok){let u=await o.text();return S.error(`Expo push failed: ${u}`),{success:!1,provider:"expo",message:`HTTP ${o.status}: ${u}`}}let p=(await o.json()).data,l=p.filter((u)=>u.status==="error");if(l.length>0)S.warn(`Some push notifications failed: ${JSON.stringify(l)}`);let f=p.filter((u)=>u.status==="ok").length;return S.info(`Expo push sent: ${f}/${p.length} successful`),{success:l.length===0,provider:"expo",message:`Sent ${f}/${p.length} notifications`,messageId:p.find((u)=>u.id)?.id,data:{tickets:p}}}catch(n){let i=n instanceof Error?n:Error(String(n));return S.error(`Expo push error: ${i.message}`),{success:!1,provider:"expo",message:i.message}}}async function Q(e){let t=await fetch("https://exp.host/--/api/v2/push/getReceipts",{method:"POST",headers:await Z(),body:JSON.stringify({ids:e})});if(!t.ok)throw Error(`Failed to get receipts: ${t.status}`);return(await t.json()).data}async function ee(e,t=100){let r=[];for(let s=0;s<e.length;s+=t){let n=e.slice(s,s+t),i=await Promise.all(n.map((o)=>T(o)));r.push(...i)}return r}var we={send:T,sendBatch:ee,getReceipts:Q,isExpoPushToken:R,configure:K};import{log as I}from"@stacksjs/cli";var re="https://fcm.googleapis.com/v1/projects",h={};function j(e){h={...h,...e}}function Ee(){h={},N.clear()}function U(e){return e.includes("\\n")?e.replace(/\\n/g,`
3
+ `):e}async function V(){if(h.projectId&&h.serviceAccount)return h;let e={};try{e=await P()}catch(n){I.debug(`FCM: could not read config/services.ts: ${n instanceof Error?n.message:String(n)}`)}let t=e.fcm??{},r=c(t.clientEmail),s=c(t.privateKey);return{projectId:h.projectId??c(t.projectId),serviceAccount:h.serviceAccount??(r&&s?{clientEmail:r,privateKey:U(s)}:void 0)}}function W(e){return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async function se(e,t,r){let s={alg:"RS256",typ:"JWT"},n={iss:e,scope:"https://www.googleapis.com/auth/firebase.messaging",aud:"https://oauth2.googleapis.com/token",iat:r,exp:r+3600},i=new TextEncoder,o=W(JSON.stringify(s)),a=W(JSON.stringify(n)),p=`${o}.${a}`,l=t.replace(/-----BEGIN PRIVATE KEY-----/,"").replace(/-----END PRIVATE KEY-----/,"").replace(/\s/g,""),f=Uint8Array.from(atob(l),(E)=>E.charCodeAt(0)),u=await crypto.subtle.importKey("pkcs8",f,{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["sign"]),g=await crypto.subtle.sign("RSASSA-PKCS1-v1_5",u,i.encode(p)),y=W(String.fromCharCode(...new Uint8Array(g)));return`${o}.${a}.${y}`}var N=new Map,ke=60000;function Ae(){N.clear()}async function ne(e){if(!e.serviceAccount)throw Error("Service account not configured for FCM v1 API");let{clientEmail:t,privateKey:r}=e.serviceAccount,s=N.get(t);if(s&&s.expiresAt-ke>Date.now())return s.token;let n=await se(t,r,Math.floor(Date.now()/1000)),i=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=${n}`});if(!i.ok)throw Error(`Failed to get access token: ${i.status}`);let o=await i.json(),a=Number(o.expires_in);return N.set(t,{token:o.access_token,expiresAt:Date.now()+(Number.isFinite(a)&&a>0?a:3600)*1000}),o.access_token}async function Te(e){let t=c(e?.projectId),r=c(e?.serviceAccount?.clientEmail),s=c(e?.serviceAccount?.privateKey);if(t&&r&&s)return{projectId:t,serviceAccount:{clientEmail:r,privateKey:U(s)}};return V()}async function C(e,t){let r=await Te(t);if(!r.serviceAccount||!r.projectId)return{success:!1,provider:"fcm",message:"FCM service account and projectId not configured - set FCM_PROJECT_ID, FCM_CLIENT_EMAIL and FCM_PRIVATE_KEY, or call configureFCM()"};try{let s=await ne(r),n=`${re}/${r.projectId}/messages:send`,i={message:{notification:e.notification,data:e.data,android:{priority:e.priority??"high",ttl:e.ttl?`${e.ttl}s`:void 0,collapseKey:e.collapseKey},apns:{headers:{"apns-priority":e.priority==="high"?"10":"5"}}}};if(e.to)i.message.token=e.to;else if(e.topic)i.message.topic=e.topic;else if(e.condition)i.message.condition=e.condition;let o=await fetch(n,{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:JSON.stringify(i)});if(!o.ok){let p=await o.text();return I.error(`FCM v1 push failed: ${p}`),{success:!1,provider:"fcm",message:`HTTP ${o.status}: ${p}`}}let a=await o.json();return I.info(`FCM v1 push sent: ${a.name}`),{success:!0,provider:"fcm",message:"Notification sent successfully",messageId:a.name}}catch(s){let n=s instanceof Error?s:Error(String(s));return I.error(`FCM v1 push error: ${n.message}`),{success:!1,provider:"fcm",message:n.message}}}async function F(e,t,r){return Promise.all(e.map((s)=>C({...t,to:s},r)))}async function oe(e,t){return C({...t,topic:e})}async function ie(e,t,r,s){let n=await ne(e),i=`${re}/${e.projectId}/registrations/${encodeURIComponent(t)}/topicSubscriptions`,o=s?`${i}?topic_name=${encodeURIComponent(r)}`:`${i}/${encodeURIComponent(r)}`,a=await fetch(o,{method:s?"POST":"DELETE",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"},body:"{}"});if(a.ok||s&&a.status===409)return!0;return I.error(`FCM topic ${s?"subscribe":"unsubscribe"} failed for topic ${r}: HTTP ${a.status}`),!1}async function ae(e){let t=await V();if(!t.serviceAccount||!t.projectId)throw Error(`FCM service account and projectId required for ${e}`);return t}async function ce(e,t){let r=await ae("topic subscription");return(await Promise.all(e.map((n)=>ie(r,n,t,!0)))).every(Boolean)}async function ue(e,t){let r=await ae("topic unsubscription");return(await Promise.all(e.map((n)=>ie(r,n,t,!1)))).every(Boolean)}var $e={send:C,sendMulticast:F,sendToTopic:oe,subscribeToTopic:ce,unsubscribeFromTopic:ue,configure:j};import{createPrivateKey as Ie,sign as Me}from"crypto";import Re,{constants as Ke}from"http2";var x={},O=new Map;function H(e){x={...e},O.clear()}function Ne(){x={},O.clear()}function de(e,t,r,s=Math.floor(Date.now()/1000)){let n=Buffer.from(JSON.stringify({alg:"ES256",kid:t})).toString("base64url"),i=Buffer.from(JSON.stringify({iss:e,iat:s})).toString("base64url"),o=`${n}.${i}`,a=Ie(r.replace(/\\n/g,`
4
+ `));if(a.asymmetricKeyType!=="ec"||a.asymmetricKeyDetails?.namedCurve!=="prime256v1")throw Error("APNs requires an ES256 P-256 signing key");return`${o}.${Me("sha256",Buffer.from(o),{key:a,dsaEncoding:"ieee-p1363"}).toString("base64url")}`}function fe(e){let t=JSON.stringify({...e.data,aps:{alert:{title:e.title??"",body:e.body},...e.sound!==null?{sound:e.sound??"default"}:{},...e.badge!==void 0?{badge:e.badge}:{}}});if(Buffer.byteLength(t)>4096)throw Error("APNs notification exceeds 4096 bytes");return t}async function L(e,t,r={}){let s=Array.isArray(e)?e:[e];if(!s.length||s.some((f)=>!/^[a-f\d]{64}$/i.test(f)))return{success:!1,provider:"apns",message:"A valid Apple device token is required"};let n=(await P()).apns,i=c(r.teamId)??c(x.teamId)??c(n?.teamId),o=c(r.keyId)??c(x.keyId)??c(n?.keyId),a=c(r.privateKey)??c(x.privateKey)??c(n?.privateKey),p=c(r.topic)??c(x.topic)??c(n?.topic),l=r.sandbox??x.sandbox??n?.sandbox;if(!i||!o||!a||!p)return{success:!1,provider:"apns",message:"APNs requires teamId, keyId, privateKey and topic"};try{let f=fe(t),u=Math.floor(Date.now()/1000),g=`${i}:${o}:${a}`,y=O.get(g);if(!y||u-y.issuedAt>=3000||u<y.issuedAt)y={token:de(i,o,a,u),issuedAt:u},O.set(g,y);let E=y.token,v=Re.connect(l?"https://api.sandbox.push.apple.com":"https://api.push.apple.com");v.on("error",()=>{});try{let B=await Promise.all(s.map((J)=>new Promise((M)=>{let m=v.request({":method":"POST",":path":`/3/device/${J}`,authorization:`bearer ${E}`,"apns-topic":p,"apns-push-type":"alert","apns-priority":t.priority==="normal"?"5":"10","content-type":"application/json",...t.collapseId?{"apns-collapse-id":t.collapseId}:{}}),k=0,G="",X;m.setEncoding("utf8");let be=setTimeout(()=>{m.close(Ke.NGHTTP2_CANCEL),M({success:!1,status:0,reason:"Timeout"})},15000);m.on("close",()=>{clearTimeout(be),M({success:!1,status:k,reason:"TransportClosed"})}),m.on("response",(b)=>{k=Number(b[":status"]),X=String(b["apns-id"]??"")||void 0}),m.on("data",(b)=>{G+=b}),m.on("error",()=>M({success:!1,status:k,reason:"TransportError"})),m.on("end",()=>{let b;try{b=JSON.parse(G).reason}catch{}M({success:k===200,status:k,reason:b,messageId:X})}),m.end(f)}))),q=B.filter((J)=>J.success).length;return{success:q===s.length,provider:"apns",message:`Apple accepted ${q}/${s.length} notifications`,messageId:B[0]?.messageId,data:{results:B}}}finally{v.destroy()}}catch(f){return{success:!1,provider:"apns",message:f instanceof Error?f.message:"APNs delivery failed"}}}import{Buffer as d}from"buffer";import{createCipheriv as je,createECDH as Fe,createHmac as le,createPrivateKey as Oe,createSign as _e,generateKeyPairSync as Be,randomBytes as Je}from"crypto";var ye="prime256v1";function w(e){return e.toString("base64url")}function _(e){return d.from(e,"base64url")}function De(){let{publicKey:e,privateKey:t}=Be("ec",{namedCurve:ye}),r=e.export({type:"spki",format:"der"}),s=t.export({format:"jwk"});return{publicKey:w(r.subarray(r.length-65)),privateKey:String(s.d)}}function We(e){let t=new URL(e);return`${t.protocol}//${t.host}`}function me(e){let r=Math.floor(e.nowSeconds??Date.now()/1000)+Math.min(e.expiresInSeconds??43200,86400),s=w(d.from(JSON.stringify({typ:"JWT",alg:"ES256"}))),n=w(d.from(JSON.stringify({aud:We(e.endpoint),exp:r,sub:e.subject}))),i=`${s}.${n}`,o=Oe({key:{kty:"EC",crv:"P-256",d:e.vapid.privateKey,x:w(_(e.vapid.publicKey).subarray(1,33)),y:w(_(e.vapid.publicKey).subarray(33,65))},format:"jwk"}),a=_e("SHA256");a.update(i);let p=a.sign({key:o,dsaEncoding:"ieee-p1363"});return{Authorization:`vapid t=${i}.${w(p)}, k=${e.vapid.publicKey}`,"Crypto-Key":`p256ecdsa=${e.vapid.publicKey}`}}function z(e,t,r,s){let n=le("sha256",e).update(t).digest();return le("sha256",n).update(d.concat([r,d.from([1])])).digest().subarray(0,s)}function he(e,t,r=Je(16)){let s=_(e.keys.p256dh),n=_(e.keys.auth),i=Fe(ye);i.generateKeys();let o=i.getPublicKey(),a=i.computeSecret(s),p=d.concat([d.from("WebPush: info\x00"),s,o]),l=z(n,a,p,32),f=z(r,l,d.from("Content-Encoding: aes128gcm\x00"),16),u=z(r,l,d.from("Content-Encoding: nonce\x00"),12),g=je("aes-128-gcm",f,u),y=d.concat([d.from(t,"utf8"),d.from([2])]),E=d.concat([g.update(y),g.final(),g.getAuthTag()]),v=d.alloc(4);return v.writeUInt32BE(4096,0),d.concat([r,v,d.from([o.length]),o,E])}async function Ue(e){let{subscription:t,vapid:r,subject:s}=e;if(!t?.endpoint)return{success:!1,status:0,expired:!1,error:"no endpoint"};let n={...me({endpoint:t.endpoint,vapid:r,subject:s}),TTL:String(e.ttl??86400),Urgency:e.urgency??"normal"};if(e.topic)n.Topic=e.topic;let i;if(e.payload){if(!t.keys?.p256dh||!t.keys?.auth)return{success:!1,status:0,expired:!0,error:"subscription has no keys"};i=he(t,e.payload),n["Content-Encoding"]="aes128gcm",n["Content-Type"]="application/octet-stream",n["Content-Length"]=String(i.length)}try{let o=await fetch(t.endpoint,{method:"POST",headers:n,body:i?new Uint8Array(i):void 0}),a=o.status===404||o.status===410;if(o.status>=200&&o.status<300)return{success:!0,status:o.status,expired:!1};return{success:!1,status:o.status,expired:a,error:(await o.text().catch(()=>"")).slice(0,500)||`push service answered ${o.status}`}}catch(o){return{success:!1,status:0,expired:!1,error:o instanceof Error?o.message:String(o)}}}async function rt(e,t,r={}){let s=r.driver??"expo";if(s==="apns")return L(e,t,r.apns);if(s==="expo")return T({to:e,title:t.title,body:t.body,data:t.data,badge:t.badge,sound:t.sound,priority:t.priority});if(s==="fcm"){let n=Array.isArray(e)?e:[e];if(n.length===1)return C({to:n[0],notification:{title:t.title??"",body:t.body},data:t.data,priority:t.priority==="default"?"normal":t.priority},r.fcm);let i=await F(n,{notification:{title:t.title??"",body:t.body},data:t.data,priority:t.priority==="default"?"normal":t.priority},r.fcm);return{success:i.every((a)=>a.success),provider:"fcm",message:`Sent to ${i.filter((a)=>a.success).length}/${i.length} devices`}}return{success:!1,provider:s,message:`Unknown push driver: ${s}`}}function st(e){j(e)}function nt(e){K(e)}function ot(e){H(e)}export{ge as apns,me as buildVapidHeaders,ot as configureAPNS,nt as configureExpo,st as configureFCM,he as encryptPayload,te as expo,pe as fcm,De as generateVapidKeys,rt as send,Ue as sendWebPush};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/push",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.221",
5
+ "version": "0.75.223",
6
6
  "description": "The Stacks Push integration",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -56,11 +56,11 @@
56
56
  "prepublishOnly": "bun run build"
57
57
  },
58
58
  "dependencies": {
59
- "@stacksjs/cli": "0.75.221",
60
- "@stacksjs/config": "0.75.221"
59
+ "@stacksjs/cli": "0.75.223",
60
+ "@stacksjs/config": "0.75.223"
61
61
  },
62
62
  "devDependencies": {
63
- "@stacksjs/error-handling": "0.75.221",
63
+ "@stacksjs/error-handling": "0.75.223",
64
64
  "better-dx": "^0.2.24"
65
65
  }
66
66
  }