@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.
- package/dist/drivers/index.d.ts +1 -0
- package/dist/drivers/web-push.d.ts +73 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1 -1
- package/package.json +4 -4
package/dist/drivers/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
60
|
-
"@stacksjs/config": "0.70.
|
|
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.
|
|
62
|
+
"@stacksjs/error-handling": "0.70.310"
|
|
63
63
|
}
|
|
64
64
|
}
|