@growth-labs/push 0.4.0 → 0.4.2

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.
@@ -0,0 +1,124 @@
1
+ import type { PushSubscriptionRecord } from '../types.js'
2
+
3
+ /** Insert or update a subscription */
4
+ export async function saveSubscription(db: D1Database, sub: PushSubscriptionRecord): Promise<void> {
5
+ await db
6
+ .prepare(
7
+ `INSERT INTO gl_push_subscriptions
8
+ (id, endpoint, key_p256dh, key_auth, topics, created_at, last_delivery_at, failure_count, user_agent, country)
9
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10
+ ON CONFLICT (endpoint) DO UPDATE SET
11
+ key_p256dh = excluded.key_p256dh,
12
+ key_auth = excluded.key_auth,
13
+ failure_count = excluded.failure_count,
14
+ last_delivery_at = excluded.last_delivery_at`,
15
+ )
16
+ .bind(
17
+ sub.id,
18
+ sub.endpoint,
19
+ sub.keyP256dh,
20
+ sub.keyAuth,
21
+ sub.topics,
22
+ sub.createdAt,
23
+ sub.lastDeliveryAt,
24
+ sub.failureCount,
25
+ sub.userAgent,
26
+ sub.country,
27
+ )
28
+ .run()
29
+ }
30
+
31
+ /** Find a subscription by its endpoint URL */
32
+ export async function findSubscriptionByEndpoint(
33
+ db: D1Database,
34
+ endpoint: string,
35
+ ): Promise<PushSubscriptionRecord | null> {
36
+ const result = await db
37
+ .prepare('SELECT * FROM gl_push_subscriptions WHERE endpoint = ?')
38
+ .bind(endpoint)
39
+ .first()
40
+
41
+ return result ? mapRow(result) : null
42
+ }
43
+
44
+ /** Delete a subscription by endpoint. Returns true if a row was deleted. */
45
+ export async function deleteSubscriptionByEndpoint(
46
+ db: D1Database,
47
+ endpoint: string,
48
+ ): Promise<boolean> {
49
+ const result = await db
50
+ .prepare('DELETE FROM gl_push_subscriptions WHERE endpoint = ?')
51
+ .bind(endpoint)
52
+ .run()
53
+
54
+ return (result.meta?.changes ?? 0) > 0
55
+ }
56
+
57
+ /** Delete a subscription by ID */
58
+ export async function deleteSubscriptionById(db: D1Database, id: string): Promise<void> {
59
+ await db.prepare('DELETE FROM gl_push_subscriptions WHERE id = ?').bind(id).run()
60
+ }
61
+
62
+ /** Get subscriptions with offset-based pagination. Used by enqueuePushBatchAll. */
63
+ export async function getAllSubscriptions(
64
+ db: D1Database,
65
+ options: { limit?: number; offset?: number } = {},
66
+ ): Promise<PushSubscriptionRecord[]> {
67
+ const limit = options.limit ?? 1000
68
+ const offset = options.offset ?? 0
69
+
70
+ const result = await db
71
+ .prepare('SELECT * FROM gl_push_subscriptions ORDER BY created_at ASC LIMIT ? OFFSET ?')
72
+ .bind(limit, offset)
73
+ .all()
74
+
75
+ return (result.results ?? []).map(mapRow)
76
+ }
77
+
78
+ /** Count total subscriptions */
79
+ export async function countSubscriptions(db: D1Database): Promise<number> {
80
+ const result = await db
81
+ .prepare('SELECT COUNT(*) as count FROM gl_push_subscriptions')
82
+ .first<{ count: number }>()
83
+
84
+ return result?.count ?? 0
85
+ }
86
+
87
+ /** Mark a subscription as successfully delivered */
88
+ export async function markDelivered(db: D1Database, id: string): Promise<void> {
89
+ await db
90
+ .prepare(
91
+ `UPDATE gl_push_subscriptions
92
+ SET last_delivery_at = ?, failure_count = 0
93
+ WHERE id = ?`,
94
+ )
95
+ .bind(Math.floor(Date.now() / 1000), id)
96
+ .run()
97
+ }
98
+
99
+ /** Increment failure count for a subscription */
100
+ export async function incrementFailureCount(db: D1Database, id: string): Promise<void> {
101
+ await db
102
+ .prepare(
103
+ `UPDATE gl_push_subscriptions
104
+ SET failure_count = failure_count + 1
105
+ WHERE id = ?`,
106
+ )
107
+ .bind(id)
108
+ .run()
109
+ }
110
+
111
+ function mapRow(row: Record<string, unknown>): PushSubscriptionRecord {
112
+ return {
113
+ id: row.id as string,
114
+ endpoint: row.endpoint as string,
115
+ keyP256dh: row.key_p256dh as string, // gitleaks:allow - Web Push field name, not literal secret material.
116
+ keyAuth: row.key_auth as string,
117
+ topics: row.topics as string | null,
118
+ createdAt: row.created_at as number,
119
+ lastDeliveryAt: row.last_delivery_at as number | null,
120
+ failureCount: row.failure_count as number,
121
+ userAgent: row.user_agent as string | null,
122
+ country: row.country as string | null,
123
+ }
124
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Verify a Turnstile token server-side.
3
+ */
4
+ export async function verifyTurnstile(
5
+ token: string,
6
+ secretKey: string,
7
+ remoteIp: string | null,
8
+ ): Promise<boolean> {
9
+ try {
10
+ const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
11
+ method: 'POST',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify({
14
+ secret: secretKey,
15
+ response: token,
16
+ remoteip: remoteIp,
17
+ }),
18
+ })
19
+
20
+ const result = (await response.json()) as { success: boolean }
21
+ return result.success
22
+ } catch {
23
+ return false
24
+ }
25
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Generate a new VAPID key pair using Web Crypto API.
3
+ * Run this once during project setup. Store the private key as a Cloudflare Secret.
4
+ */
5
+ export async function generateVapidKeys(): Promise<{
6
+ publicKey: string
7
+ privateKey: string
8
+ }> {
9
+ const keyPair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
10
+ 'sign',
11
+ 'verify',
12
+ ])
13
+
14
+ const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
15
+ const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey)
16
+ const publicKey = uint8ArrayToUrlSafeBase64(new Uint8Array(publicRaw))
17
+
18
+ return {
19
+ publicKey,
20
+ privateKey: JSON.stringify(privateJwk),
21
+ }
22
+ }
23
+
24
+ /** Convert a Uint8Array to URL-safe base64 (no padding) */
25
+ function uint8ArrayToUrlSafeBase64(bytes: Uint8Array): string {
26
+ const binary = String.fromCharCode(...bytes)
27
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
28
+ }