@stacksjs/push 0.70.23 → 0.70.26

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.js CHANGED
@@ -1 +1 @@
1
- var p=Object.defineProperty;var i=(r,o)=>{for(var t in o)p(r,t,{get:o[t],enumerable:!0,configurable:!0,set:(e)=>o[t]=()=>e})};var n={};i(n,{expoWip:()=>a});var a=1;var s={};i(s,{fcmWip:()=>c});var c=1;export{s as fcm,n as expo};
1
+ var y=Object.defineProperty;var h=(G)=>G;function T(G,J){this[G]=h.bind(null,J)}var C=(G,J)=>{for(var N in J)y(G,N,{get:J[N],enumerable:!0,configurable:!0,set:T.bind(J,N)})};var z={};C(z,{sendBatch:()=>L,send:()=>j,isExpoPushToken:()=>w,getReceipts:()=>E,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 E(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 L(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:L,getReceipts:E,isExpoPushToken:w};var B={};C(B,{unsubscribeFromTopic:()=>M,subscribeToTopic:()=>b,sendToTopic:()=>S,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),(v)=>v.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,""),x=`${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=${x}`});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 S(G,J){return O({...J,topic:G})}async function b(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 M(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:S,subscribeToTopic:b,unsubscribeFromTopic:M,configure:F};async function t(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 n(G){F(G)}export{t as send,B as fcm,z as expo,n as configureFCM};
@@ -0,0 +1,45 @@
1
+ import type { PushResult } from '@stacksjs/types';
2
+ /**
3
+ * Validates an Expo push token
4
+ */
5
+ export declare function isExpoPushToken(token: string): boolean;
6
+ /**
7
+ * Send a push notification via Expo Push Service
8
+ */
9
+ export declare function send(message: ExpoPushMessage): Promise<PushResult>;
10
+ /**
11
+ * Get push notification receipts from Expo
12
+ */
13
+ export declare function getReceipts(ticketIds: string[]): Promise<Record<string, ExpoPushReceipt>>;
14
+ /**
15
+ * Send multiple push notifications in chunks (Expo recommends max 100 per request)
16
+ */
17
+ export declare function sendBatch(messages: ExpoPushMessage[], chunkSize?: number): Promise<PushResult[]>;
18
+ export declare interface ExpoPushMessage {
19
+ to: string | string[]
20
+ title?: string
21
+ body: string
22
+ data?: Record<string, any>
23
+ sound?: 'default' | null
24
+ badge?: number
25
+ channelId?: string
26
+ priority?: 'default' | 'normal' | 'high'
27
+ ttl?: number
28
+ }
29
+ export declare interface ExpoPushTicket {
30
+ id?: string
31
+ status: 'ok' | 'error'
32
+ message?: string
33
+ details?: {
34
+ error?: 'DeviceNotRegistered' | 'MessageTooBig' | 'MessageRateExceeded' | 'InvalidCredentials'
35
+ }
36
+ }
37
+ export declare interface ExpoPushReceipt {
38
+ status: 'ok' | 'error'
39
+ message?: string
40
+ details?: {
41
+ error?: string
42
+ }
43
+ }
44
+ export { send as Send };
45
+ export default { send, sendBatch, getReceipts, isExpoPushToken };
@@ -0,0 +1,76 @@
1
+ import type { PushResult } from '@stacksjs/types';
2
+ /**
3
+ * Configure FCM with server key or service account
4
+ */
5
+ export declare function configure(options: FCMConfig): void;
6
+ /**
7
+ * Send push notification via FCM Legacy API (using server key)
8
+ */
9
+ export declare function sendLegacy(message: FCMMessage): Promise<PushResult>;
10
+ /**
11
+ * Send push notification via FCM v1 API (using service account)
12
+ */
13
+ export declare function send(message: FCMMessage): Promise<PushResult>;
14
+ /**
15
+ * Send to multiple tokens
16
+ */
17
+ export declare function sendMulticast(tokens: string[], message: Omit<FCMMessage, 'to' | 'registrationIds'>): Promise<PushResult[]>;
18
+ /**
19
+ * Send to a topic
20
+ */
21
+ export declare function sendToTopic(topic: string, message: Omit<FCMMessage, 'to' | 'registrationIds' | 'topic'>): Promise<PushResult>;
22
+ /**
23
+ * Subscribe tokens to a topic
24
+ */
25
+ export declare function subscribeToTopic(tokens: string[], topic: string): Promise<boolean>;
26
+ /**
27
+ * Unsubscribe tokens from a topic
28
+ */
29
+ export declare function unsubscribeFromTopic(tokens: string[], topic: string): Promise<boolean>;
30
+ export declare interface FCMMessage {
31
+ to?: string
32
+ registrationIds?: string[]
33
+ topic?: string
34
+ condition?: string
35
+ notification?: {
36
+ title: string
37
+ body: string
38
+ icon?: string
39
+ image?: string
40
+ sound?: string
41
+ badge?: string
42
+ clickAction?: string
43
+ tag?: string
44
+ }
45
+ data?: Record<string, string>
46
+ priority?: 'high' | 'normal'
47
+ ttl?: number
48
+ collapseKey?: string
49
+ }
50
+ export declare interface FCMConfig {
51
+ serverKey?: string
52
+ projectId?: string
53
+ serviceAccount?: {
54
+ clientEmail: string
55
+ privateKey: string
56
+ }
57
+ }
58
+ export declare interface FCMResponse {
59
+ multicastId?: number
60
+ success: number
61
+ failure: number
62
+ results?: Array<{
63
+ messageId?: string
64
+ error?: string
65
+ }>
66
+ }
67
+ export { send as Send };
68
+ export default {
69
+ send,
70
+ sendLegacy,
71
+ sendMulticast,
72
+ sendToTopic,
73
+ subscribeToTopic,
74
+ unsubscribeFromTopic,
75
+ configure,
76
+ };
@@ -0,0 +1,2 @@
1
+ export * as expo from './expo';
2
+ export * as fcm from './fcm';
@@ -0,0 +1,25 @@
1
+ import * as expo from './drivers/expo';
2
+ import * as fcm from './drivers/fcm';
3
+ import type { PushResult } from '@stacksjs/types';
4
+ /**
5
+ * Send a push notification using the specified driver
6
+ */
7
+ export declare function send(to: string | string[], notification: PushNotification, options?: SendOptions): Promise<PushResult>;
8
+ /**
9
+ * Configure FCM with credentials
10
+ */
11
+ export declare function configureFCM(config: fcm.FCMConfig): void;
12
+ export declare interface PushNotification {
13
+ title?: string
14
+ body: string
15
+ data?: Record<string, any>
16
+ badge?: number
17
+ sound?: 'default' | null
18
+ priority?: 'high' | 'normal' | 'default'
19
+ }
20
+ export declare interface SendOptions {
21
+ driver?: PushDriver
22
+ }
23
+ export type PushDriver = 'expo' | 'fcm';
24
+ export * from './drivers';
25
+ export { expo, fcm };
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@stacksjs/push",
3
3
  "type": "module",
4
- "version": "0.70.23",
4
+ "version": "0.70.26",
5
5
  "description": "The Stacks Push integration",
6
6
  "author": "Chris Breuer",
7
- "contributors": ["Chris Breuer <chris@stacksjs.org>"],
7
+ "contributors": [
8
+ "Chris Breuer <chris@stacksjs.com>"
9
+ ],
8
10
  "license": "MIT",
9
11
  "funding": "https://github.com/sponsors/chrisbbreuer",
10
12
  "homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/push#readme",
@@ -16,27 +18,37 @@
16
18
  "bugs": {
17
19
  "url": "https://github.com/stacksjs/stacks/issues"
18
20
  },
19
- "keywords": ["push", "aws", "stacks"],
21
+ "keywords": [
22
+ "push",
23
+ "aws",
24
+ "stacks"
25
+ ],
20
26
  "exports": {
21
27
  ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "bun": "./src/index.ts",
22
30
  "import": "./dist/index.js"
23
31
  },
24
32
  "./*": {
33
+ "bun": "./src/*",
25
34
  "import": "./dist/*"
26
35
  }
27
36
  },
28
37
  "module": "dist/index.js",
29
38
  "types": "dist/index.d.ts",
30
- "files": ["README.md", "dist"],
39
+ "files": [
40
+ "README.md",
41
+ "dist"
42
+ ],
31
43
  "scripts": {
32
44
  "build": "bun build.ts",
33
45
  "typecheck": "bun tsc --noEmit",
34
46
  "prepublishOnly": "bun run build"
35
47
  },
36
48
  "devDependencies": {
37
- "@stacksjs/cli": "0.70.22",
38
- "@stacksjs/config": "0.70.22",
39
- "@stacksjs/development": "0.70.22",
40
- "@stacksjs/error-handling": "0.70.22"
49
+ "@stacksjs/cli": "0.70.23",
50
+ "@stacksjs/config": "0.70.23",
51
+ "better-dx": "^0.2.12",
52
+ "@stacksjs/error-handling": "0.70.23"
41
53
  }
42
54
  }
package/dist/expo.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const expoWip: 1;
package/dist/fcm.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const fcmWip: 1;
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export * from './drivers'