@growth-labs/push 0.1.3
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/README.md +106 -0
- package/dist/client/push-optin.d.ts +24 -0
- package/dist/client/push-optin.d.ts.map +1 -0
- package/dist/client/push-optin.js +122 -0
- package/dist/client/push-optin.js.map +1 -0
- package/dist/client/push-sw.d.ts +3 -0
- package/dist/client/push-sw.d.ts.map +1 -0
- package/dist/client/push-sw.js +36 -0
- package/dist/client/push-sw.js.map +1 -0
- package/dist/db/schema.d.ts +178 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +14 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/push.d.ts +2 -0
- package/dist/middleware/push.d.ts.map +1 -0
- package/dist/middleware/push.js +10 -0
- package/dist/middleware/push.js.map +1 -0
- package/dist/options.d.ts +108 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +36 -0
- package/dist/options.js.map +1 -0
- package/dist/routes/subscribe.d.ts +3 -0
- package/dist/routes/subscribe.d.ts.map +1 -0
- package/dist/routes/subscribe.js +68 -0
- package/dist/routes/subscribe.js.map +1 -0
- package/dist/routes/unsubscribe.d.ts +3 -0
- package/dist/routes/unsubscribe.d.ts.map +1 -0
- package/dist/routes/unsubscribe.js +24 -0
- package/dist/routes/unsubscribe.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/analytics.d.ts +47 -0
- package/dist/utils/analytics.d.ts.map +1 -0
- package/dist/utils/analytics.js +78 -0
- package/dist/utils/analytics.js.map +1 -0
- package/dist/utils/cleanup.d.ts +12 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +18 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/composer.d.ts +95 -0
- package/dist/utils/composer.d.ts.map +1 -0
- package/dist/utils/composer.js +98 -0
- package/dist/utils/composer.js.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/queue.d.ts +40 -0
- package/dist/utils/queue.d.ts.map +1 -0
- package/dist/utils/queue.js +69 -0
- package/dist/utils/queue.js.map +1 -0
- package/dist/utils/scheduled.d.ts +105 -0
- package/dist/utils/scheduled.d.ts.map +1 -0
- package/dist/utils/scheduled.js +75 -0
- package/dist/utils/scheduled.js.map +1 -0
- package/dist/utils/send.d.ts +23 -0
- package/dist/utils/send.d.ts.map +1 -0
- package/dist/utils/send.js +99 -0
- package/dist/utils/send.js.map +1 -0
- package/dist/utils/subscriptions.d.ts +21 -0
- package/dist/utils/subscriptions.d.ts.map +1 -0
- package/dist/utils/subscriptions.js +84 -0
- package/dist/utils/subscriptions.js.map +1 -0
- package/dist/utils/turnstile.d.ts +5 -0
- package/dist/utils/turnstile.d.ts.map +1 -0
- package/dist/utils/turnstile.js +22 -0
- package/dist/utils/turnstile.js.map +1 -0
- package/dist/utils/vapid.d.ts +9 -0
- package/dist/utils/vapid.d.ts.map +1 -0
- package/dist/utils/vapid.js +23 -0
- package/dist/utils/vapid.js.map +1 -0
- package/package.json +77 -0
- package/src/client/push-optin.ts +189 -0
- package/src/client/push-sw.d.ts +3 -0
- package/src/client/push-sw.d.ts.map +1 -0
- package/src/client/push-sw.js +25 -0
- package/src/client/push-sw.js.map +1 -0
- package/src/client/push-sw.ts +45 -0
- package/src/components/PushOptIn.astro +53 -0
- package/src/components/index.ts +1 -0
- package/src/virtual.d.ts +5 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const pushOptionsSchema: z.ZodObject<{
|
|
3
|
+
vapidPublicKey: z.ZodString;
|
|
4
|
+
vapidPrivateKey: z.ZodString;
|
|
5
|
+
vapidSubject: z.ZodString;
|
|
6
|
+
d1Binding: z.ZodDefault<z.ZodString>;
|
|
7
|
+
queueBinding: z.ZodDefault<z.ZodString>;
|
|
8
|
+
defaults: z.ZodDefault<z.ZodObject<{
|
|
9
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
10
|
+
ttl: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
urgency: z.ZodDefault<z.ZodEnum<["very-low", "low", "normal", "high"]>>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
ttl: number;
|
|
14
|
+
urgency: "very-low" | "low" | "normal" | "high";
|
|
15
|
+
icon?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
icon?: string | undefined;
|
|
18
|
+
ttl?: number | undefined;
|
|
19
|
+
urgency?: "very-low" | "low" | "normal" | "high" | undefined;
|
|
20
|
+
}>>;
|
|
21
|
+
queue: z.ZodDefault<z.ZodObject<{
|
|
22
|
+
batchSize: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
maxRetries: z.ZodDefault<z.ZodNumber>;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
batchSize: number;
|
|
26
|
+
maxRetries: number;
|
|
27
|
+
}, {
|
|
28
|
+
batchSize?: number | undefined;
|
|
29
|
+
maxRetries?: number | undefined;
|
|
30
|
+
}>>;
|
|
31
|
+
turnstile: z.ZodObject<{
|
|
32
|
+
secretKey: z.ZodString;
|
|
33
|
+
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
secretKey: string;
|
|
35
|
+
}, {
|
|
36
|
+
secretKey: string;
|
|
37
|
+
}>;
|
|
38
|
+
routes: z.ZodDefault<z.ZodObject<{
|
|
39
|
+
subscribe: z.ZodDefault<z.ZodString>;
|
|
40
|
+
unsubscribe: z.ZodDefault<z.ZodString>;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
subscribe: string;
|
|
43
|
+
unsubscribe: string;
|
|
44
|
+
}, {
|
|
45
|
+
subscribe?: string | undefined;
|
|
46
|
+
unsubscribe?: string | undefined;
|
|
47
|
+
}>>;
|
|
48
|
+
cleanup: z.ZodDefault<z.ZodObject<{
|
|
49
|
+
staleAfterDays: z.ZodDefault<z.ZodNumber>;
|
|
50
|
+
}, "strip", z.ZodTypeAny, {
|
|
51
|
+
staleAfterDays: number;
|
|
52
|
+
}, {
|
|
53
|
+
staleAfterDays?: number | undefined;
|
|
54
|
+
}>>;
|
|
55
|
+
}, "strip", z.ZodTypeAny, {
|
|
56
|
+
vapidPublicKey: string;
|
|
57
|
+
vapidPrivateKey: string;
|
|
58
|
+
vapidSubject: string;
|
|
59
|
+
d1Binding: string;
|
|
60
|
+
queueBinding: string;
|
|
61
|
+
defaults: {
|
|
62
|
+
ttl: number;
|
|
63
|
+
urgency: "very-low" | "low" | "normal" | "high";
|
|
64
|
+
icon?: string | undefined;
|
|
65
|
+
};
|
|
66
|
+
queue: {
|
|
67
|
+
batchSize: number;
|
|
68
|
+
maxRetries: number;
|
|
69
|
+
};
|
|
70
|
+
turnstile: {
|
|
71
|
+
secretKey: string;
|
|
72
|
+
};
|
|
73
|
+
routes: {
|
|
74
|
+
subscribe: string;
|
|
75
|
+
unsubscribe: string;
|
|
76
|
+
};
|
|
77
|
+
cleanup: {
|
|
78
|
+
staleAfterDays: number;
|
|
79
|
+
};
|
|
80
|
+
}, {
|
|
81
|
+
vapidPublicKey: string;
|
|
82
|
+
vapidPrivateKey: string;
|
|
83
|
+
vapidSubject: string;
|
|
84
|
+
turnstile: {
|
|
85
|
+
secretKey: string;
|
|
86
|
+
};
|
|
87
|
+
d1Binding?: string | undefined;
|
|
88
|
+
queueBinding?: string | undefined;
|
|
89
|
+
defaults?: {
|
|
90
|
+
icon?: string | undefined;
|
|
91
|
+
ttl?: number | undefined;
|
|
92
|
+
urgency?: "very-low" | "low" | "normal" | "high" | undefined;
|
|
93
|
+
} | undefined;
|
|
94
|
+
queue?: {
|
|
95
|
+
batchSize?: number | undefined;
|
|
96
|
+
maxRetries?: number | undefined;
|
|
97
|
+
} | undefined;
|
|
98
|
+
routes?: {
|
|
99
|
+
subscribe?: string | undefined;
|
|
100
|
+
unsubscribe?: string | undefined;
|
|
101
|
+
} | undefined;
|
|
102
|
+
cleanup?: {
|
|
103
|
+
staleAfterDays?: number | undefined;
|
|
104
|
+
} | undefined;
|
|
105
|
+
}>;
|
|
106
|
+
export type PushOptions = z.input<typeof pushOptionsSchema>;
|
|
107
|
+
export type ResolvedPushOptions = z.output<typeof pushOptionsSchema>;
|
|
108
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuC5B,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAC3D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAA"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const pushOptionsSchema = z.object({
|
|
3
|
+
vapidPublicKey: z.string().min(1),
|
|
4
|
+
vapidPrivateKey: z.string().min(1),
|
|
5
|
+
vapidSubject: z.string().min(1),
|
|
6
|
+
d1Binding: z.string().default('SITE_DB'),
|
|
7
|
+
queueBinding: z.string().default('PUSH_QUEUE'),
|
|
8
|
+
defaults: z
|
|
9
|
+
.object({
|
|
10
|
+
icon: z.string().optional(),
|
|
11
|
+
ttl: z.number().int().positive().default(86400),
|
|
12
|
+
urgency: z.enum(['very-low', 'low', 'normal', 'high']).default('normal'),
|
|
13
|
+
})
|
|
14
|
+
.default({}),
|
|
15
|
+
queue: z
|
|
16
|
+
.object({
|
|
17
|
+
batchSize: z.number().int().positive().default(100),
|
|
18
|
+
maxRetries: z.number().int().nonnegative().default(3),
|
|
19
|
+
})
|
|
20
|
+
.default({}),
|
|
21
|
+
turnstile: z.object({
|
|
22
|
+
secretKey: z.string().min(1),
|
|
23
|
+
}),
|
|
24
|
+
routes: z
|
|
25
|
+
.object({
|
|
26
|
+
subscribe: z.string().default('/api/push/subscribe'),
|
|
27
|
+
unsubscribe: z.string().default('/api/push/unsubscribe'),
|
|
28
|
+
})
|
|
29
|
+
.default({}),
|
|
30
|
+
cleanup: z
|
|
31
|
+
.object({
|
|
32
|
+
staleAfterDays: z.number().int().positive().default(90),
|
|
33
|
+
})
|
|
34
|
+
.default({}),
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAClC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAE/B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC;IACxC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IAE9C,QAAQ,EAAE,CAAC;SACT,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC3B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;QAC/C,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;KACxE,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;IAEb,KAAK,EAAE,CAAC;SACN,MAAM,CAAC;QACP,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;QACnD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;KACrD,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;IAEb,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC;QACnB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC5B,CAAC;IAEF,MAAM,EAAE,CAAC;SACP,MAAM,CAAC;QACP,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,qBAAqB,CAAC;QACpD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC;KACxD,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;IAEb,OAAO,EAAE,CAAC;SACR,MAAM,CAAC;QACP,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;KACvD,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;CACb,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAKrC,eAAO,MAAM,IAAI,EAAE,QA4ElB,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import config from 'virtual:growth-labs/push/config';
|
|
2
|
+
import { trackPushSubscription } from '../utils/analytics.js';
|
|
3
|
+
import { findSubscriptionByEndpoint, saveSubscription } from '../utils/subscriptions.js';
|
|
4
|
+
import { verifyTurnstile } from '../utils/turnstile.js';
|
|
5
|
+
export const POST = async ({ request, locals }) => {
|
|
6
|
+
const body = (await request.json());
|
|
7
|
+
const turnstileToken = body.turnstileToken;
|
|
8
|
+
if (!turnstileToken) {
|
|
9
|
+
return new Response(JSON.stringify({ error: 'Missing Turnstile token' }), {
|
|
10
|
+
status: 400,
|
|
11
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const runtime = locals.runtime;
|
|
15
|
+
const turnstileSecret = runtime.env[config.turnstile.secretKey] ?? runtime.env.TURNSTILE_SECRET;
|
|
16
|
+
const turnstileValid = await verifyTurnstile(turnstileToken, turnstileSecret, request.headers.get('CF-Connecting-IP'));
|
|
17
|
+
if (!turnstileValid) {
|
|
18
|
+
return new Response(JSON.stringify({ error: 'Bot detected' }), {
|
|
19
|
+
status: 403,
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const sub = (body.subscription ?? {});
|
|
24
|
+
const endpoint = sub.endpoint;
|
|
25
|
+
const keys = sub.keys;
|
|
26
|
+
if (!endpoint || !keys?.p256dh || !keys?.auth) {
|
|
27
|
+
return new Response(JSON.stringify({ error: 'Invalid subscription' }), {
|
|
28
|
+
status: 400,
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const db = runtime.env[config.d1Binding];
|
|
33
|
+
const existing = await findSubscriptionByEndpoint(db, endpoint);
|
|
34
|
+
if (existing) {
|
|
35
|
+
await saveSubscription(db, {
|
|
36
|
+
...existing,
|
|
37
|
+
keyP256dh: keys.p256dh,
|
|
38
|
+
keyAuth: keys.auth,
|
|
39
|
+
failureCount: 0,
|
|
40
|
+
lastDeliveryAt: null,
|
|
41
|
+
});
|
|
42
|
+
return new Response(JSON.stringify({ ok: true, updated: true }), {
|
|
43
|
+
status: 200,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
await saveSubscription(db, {
|
|
48
|
+
id: crypto.randomUUID(),
|
|
49
|
+
endpoint,
|
|
50
|
+
keyP256dh: keys.p256dh,
|
|
51
|
+
keyAuth: keys.auth,
|
|
52
|
+
topics: null,
|
|
53
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
54
|
+
lastDeliveryAt: null,
|
|
55
|
+
failureCount: 0,
|
|
56
|
+
userAgent: request.headers.get('User-Agent'),
|
|
57
|
+
country: request.headers.get('CF-IPCountry') ?? null,
|
|
58
|
+
});
|
|
59
|
+
trackPushSubscription('subscribed', {
|
|
60
|
+
country: request.headers.get('CF-IPCountry') ?? undefined,
|
|
61
|
+
userAgent: request.headers.get('User-Agent') ?? undefined,
|
|
62
|
+
}).catch(() => { });
|
|
63
|
+
return new Response(JSON.stringify({ ok: true, created: true }), {
|
|
64
|
+
status: 201,
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=subscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,iCAAiC,CAAA;AAEpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AACxF,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD,MAAM,CAAC,MAAM,IAAI,GAAa,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE;IAC3D,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAA4B,CAAA;IAE9D,MAAM,cAAc,GAAG,IAAI,CAAC,cAAoC,CAAA;IAChE,IAAI,CAAC,cAAc,EAAE,CAAC;QACrB,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,EAAE;YACzE,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAC/C,CAAC,CAAA;IACH,CAAC;IAED,MAAM,OAAO,GACZ,MACA,CAAC,OAAO,CAAA;IACT,MAAM,eAAe,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAY,IAAK,OAAO,CAAC,GAAG,CAAC,gBAA2B,CAAA;IAChG,MAAM,cAAc,GAAG,MAAM,eAAe,CAC3C,cAAc,EACd,eAAe,EACf,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CACvC,CAAA;IACD,IAAI,CAAC,cAAc,EAAE,CAAC;QACrB,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE;YAC9D,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAC/C,CAAC,CAAA;IACH,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAA4B,CAAA;IAChE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAA8B,CAAA;IACnD,MAAM,IAAI,GAAG,GAAG,CAAC,IAA0C,CAAA;IAC3D,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QAC/C,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,EAAE;YACtE,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAC/C,CAAC,CAAA;IACH,CAAC;IAED,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACtD,MAAM,QAAQ,GAAG,MAAM,0BAA0B,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IAC/D,IAAI,QAAQ,EAAE,CAAC;QACd,MAAM,gBAAgB,CAAC,EAAE,EAAE;YAC1B,GAAG,QAAQ;YACX,SAAS,EAAE,IAAI,CAAC,MAAM;YACtB,OAAO,EAAE,IAAI,CAAC,IAAI;YAClB,YAAY,EAAE,CAAC;YACf,cAAc,EAAE,IAAI;SACpB,CAAC,CAAA;QACF,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE;YAChE,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAC/C,CAAC,CAAA;IACH,CAAC;IAED,MAAM,gBAAgB,CAAC,EAAE,EAAE;QAC1B,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;QACvB,QAAQ;QACR,SAAS,EAAE,IAAI,CAAC,MAAM;QACtB,OAAO,EAAE,IAAI,CAAC,IAAI;QAClB,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACxC,cAAc,EAAE,IAAI;QACpB,YAAY,EAAE,CAAC;QACf,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QAC5C,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI;KACpD,CAAC,CAAA;IAEF,qBAAqB,CAAC,YAAY,EAAE;QACnC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,SAAS;QACzD,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,SAAS;KACzD,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAElB,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE;QAChE,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAC/C,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAIrC,eAAO,MAAM,IAAI,EAAE,QAyBlB,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import config from 'virtual:growth-labs/push/config';
|
|
2
|
+
import { trackPushSubscription } from '../utils/analytics.js';
|
|
3
|
+
import { deleteSubscriptionByEndpoint } from '../utils/subscriptions.js';
|
|
4
|
+
export const POST = async ({ request, locals }) => {
|
|
5
|
+
const body = (await request.json());
|
|
6
|
+
const endpoint = body.endpoint;
|
|
7
|
+
if (!endpoint) {
|
|
8
|
+
return new Response(JSON.stringify({ error: 'Missing endpoint' }), {
|
|
9
|
+
status: 400,
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const runtime = locals.runtime;
|
|
14
|
+
const db = runtime.env[config.d1Binding];
|
|
15
|
+
const deleted = await deleteSubscriptionByEndpoint(db, endpoint);
|
|
16
|
+
if (deleted) {
|
|
17
|
+
trackPushSubscription('unsubscribed').catch(() => { });
|
|
18
|
+
}
|
|
19
|
+
return new Response(JSON.stringify({ ok: true, deleted }), {
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=unsubscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,iCAAiC,CAAA;AAEpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAA;AAExE,MAAM,CAAC,MAAM,IAAI,GAAa,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE;IAC3D,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAA4B,CAAA;IAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAA8B,CAAA;IAEpD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,EAAE;YAClE,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAC/C,CAAC,CAAA;IACH,CAAC;IAED,MAAM,OAAO,GACZ,MACA,CAAC,OAAO,CAAA;IACT,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACtD,MAAM,OAAO,GAAG,MAAM,4BAA4B,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IAEhE,IAAI,OAAO,EAAE,CAAC;QACb,qBAAqB,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACtD,CAAC;IAED,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE;QAC1D,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAC/C,CAAC,CAAA;AACH,CAAC,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Push notification payload — what the user sees */
|
|
2
|
+
export interface PushPayload {
|
|
3
|
+
title: string;
|
|
4
|
+
body: string;
|
|
5
|
+
icon?: string;
|
|
6
|
+
image?: string;
|
|
7
|
+
url: string;
|
|
8
|
+
action?: {
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
};
|
|
12
|
+
tag?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Per-notification delivery options */
|
|
15
|
+
export interface PushSendOptions {
|
|
16
|
+
ttl?: number;
|
|
17
|
+
urgency?: 'very-low' | 'low' | 'normal' | 'high';
|
|
18
|
+
}
|
|
19
|
+
/** A push subscription as stored in D1 / received from the browser */
|
|
20
|
+
export interface PushSubscriptionRecord {
|
|
21
|
+
id: string;
|
|
22
|
+
endpoint: string;
|
|
23
|
+
keyP256dh: string;
|
|
24
|
+
keyAuth: string;
|
|
25
|
+
topics: string | null;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
lastDeliveryAt: number | null;
|
|
28
|
+
failureCount: number;
|
|
29
|
+
userAgent: string | null;
|
|
30
|
+
country: string | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Queue message shape for batch sends.
|
|
34
|
+
*
|
|
35
|
+
* VAPID credentials are NOT included. The Queue consumer Worker reads them
|
|
36
|
+
* from its own env bindings (Cloudflare Secrets). This keeps the private key
|
|
37
|
+
* in exactly one place and avoids serializing it into every Queue message.
|
|
38
|
+
*/
|
|
39
|
+
export interface PushQueueMessage {
|
|
40
|
+
subscriptions: PushSubscriptionRecord[];
|
|
41
|
+
payload: PushPayload;
|
|
42
|
+
options: PushSendOptions;
|
|
43
|
+
}
|
|
44
|
+
/** Result of a single push send attempt */
|
|
45
|
+
export interface PushSendResult {
|
|
46
|
+
subscriptionId: string;
|
|
47
|
+
success: boolean;
|
|
48
|
+
statusCode?: number;
|
|
49
|
+
error?: string;
|
|
50
|
+
expired?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Batch send result */
|
|
53
|
+
export interface PushBatchResult {
|
|
54
|
+
sent: number;
|
|
55
|
+
failed: number;
|
|
56
|
+
expired: number;
|
|
57
|
+
results: PushSendResult[];
|
|
58
|
+
}
|
|
59
|
+
/** What the middleware puts on context.locals */
|
|
60
|
+
export interface PushLocals {
|
|
61
|
+
push: {
|
|
62
|
+
vapidPublicKey: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Serializable config exported by the virtual module */
|
|
66
|
+
export interface PushConfig {
|
|
67
|
+
vapidPublicKey: string;
|
|
68
|
+
d1Binding: string;
|
|
69
|
+
queueBinding: string;
|
|
70
|
+
defaults: {
|
|
71
|
+
icon?: string;
|
|
72
|
+
ttl: number;
|
|
73
|
+
urgency: 'very-low' | 'low' | 'normal' | 'high';
|
|
74
|
+
};
|
|
75
|
+
queue: {
|
|
76
|
+
batchSize: number;
|
|
77
|
+
maxRetries: number;
|
|
78
|
+
};
|
|
79
|
+
turnstile: {
|
|
80
|
+
secretKey: string;
|
|
81
|
+
};
|
|
82
|
+
routes: {
|
|
83
|
+
subscribe: string;
|
|
84
|
+
unsubscribe: string;
|
|
85
|
+
};
|
|
86
|
+
cleanup: {
|
|
87
|
+
staleAfterDays: number;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE;QACR,KAAK,EAAE,MAAM,CAAA;QACb,GAAG,EAAE,MAAM,CAAA;KACX,CAAA;IACD,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ;AAED,wCAAwC;AACxC,MAAM,WAAW,eAAe;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;CAChD;AAED,sEAAsE;AACtE,MAAM,WAAW,sBAAsB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,sBAAsB,EAAE,CAAA;IACvC,OAAO,EAAE,WAAW,CAAA;IACpB,OAAO,EAAE,eAAe,CAAA;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC9B,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,wBAAwB;AACxB,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,cAAc,EAAE,CAAA;CACzB;AAED,iDAAiD;AACjD,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE;QACL,cAAc,EAAE,MAAM,CAAA;KACtB,CAAA;CACD;AAED,yDAAyD;AACzD,MAAM,WAAW,UAAU;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;KAC/C,CAAA;IACD,KAAK,EAAE;QACN,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,EAAE,MAAM,CAAA;KAClB,CAAA;IACD,SAAS,EAAE;QACV,SAAS,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,MAAM,EAAE;QACP,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;KACnB,CAAA;IACD,OAAO,EAAE;QACR,cAAc,EAAE,MAAM,CAAA;KACtB,CAAA;CACD"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PushBatchResult, PushPayload } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Server-side analytics helper for push events.
|
|
4
|
+
*
|
|
5
|
+
* Uses optional peer dependency on @growth-labs/analytics.
|
|
6
|
+
* If analytics is not installed, events are silently dropped.
|
|
7
|
+
* This follows the project convention: optional peer dependency + dynamic import with try/catch.
|
|
8
|
+
*/
|
|
9
|
+
export declare function trackPushEvent(eventName: string, data: Record<string, unknown>): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Track a push notification send completion.
|
|
12
|
+
* Call after a batch send completes (in queue consumer or direct send).
|
|
13
|
+
*/
|
|
14
|
+
export declare function trackPushSend(payload: PushPayload, result: PushBatchResult): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Track a push subscription event (subscribe or unsubscribe).
|
|
17
|
+
* Call from route handlers after successful subscription changes.
|
|
18
|
+
*/
|
|
19
|
+
export declare function trackPushSubscription(action: 'subscribed' | 'unsubscribed', metadata?: {
|
|
20
|
+
endpoint?: string;
|
|
21
|
+
country?: string;
|
|
22
|
+
userAgent?: string;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Track a push cleanup event.
|
|
26
|
+
* Call after subscription cleanup runs.
|
|
27
|
+
*/
|
|
28
|
+
export declare function trackPushCleanup(removed: number): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Emit a client-side analytics event using the gl:* CustomEvent pattern.
|
|
31
|
+
* This is meant to be called from browser-side code (service worker or page).
|
|
32
|
+
*
|
|
33
|
+
* Packages emit CustomEvent on document with gl: prefix.
|
|
34
|
+
* Analytics listens for all gl:* events. No hard dependencies between packages.
|
|
35
|
+
*
|
|
36
|
+
* Usage in service worker notificationclick handler:
|
|
37
|
+
* ```javascript
|
|
38
|
+
* // In the page after notification click navigation
|
|
39
|
+
* document.dispatchEvent(new CustomEvent('gl:push_notification_clicked', {
|
|
40
|
+
* detail: { tag: 'article-123', url: '/articles/123' }
|
|
41
|
+
* }))
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* This function is a convenience wrapper for use in page scripts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function emitPushAnalyticsEvent(eventName: string, detail?: Record<string, unknown>): void;
|
|
47
|
+
//# sourceMappingURL=analytics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAK/D;;;;;;GAMG;AACH,wBAAsB,cAAc,CACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAShG;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAC1C,MAAM,EAAE,YAAY,GAAG,cAAc,EACrC,QAAQ,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACxE,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACrC,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CASN"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Module name kept in a variable so TypeScript doesn't try to resolve it at compile time */
|
|
2
|
+
const ANALYTICS_MODULE = '@growth-labs/analytics';
|
|
3
|
+
/**
|
|
4
|
+
* Server-side analytics helper for push events.
|
|
5
|
+
*
|
|
6
|
+
* Uses optional peer dependency on @growth-labs/analytics.
|
|
7
|
+
* If analytics is not installed, events are silently dropped.
|
|
8
|
+
* This follows the project convention: optional peer dependency + dynamic import with try/catch.
|
|
9
|
+
*/
|
|
10
|
+
export async function trackPushEvent(eventName, data) {
|
|
11
|
+
try {
|
|
12
|
+
const analytics = (await import(/* @vite-ignore */ ANALYTICS_MODULE));
|
|
13
|
+
if (analytics && typeof analytics.trackServerEvent === 'function') {
|
|
14
|
+
await analytics.trackServerEvent(eventName, data);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// @growth-labs/analytics not installed — silently skip
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Track a push notification send completion.
|
|
23
|
+
* Call after a batch send completes (in queue consumer or direct send).
|
|
24
|
+
*/
|
|
25
|
+
export async function trackPushSend(payload, result) {
|
|
26
|
+
await trackPushEvent('push_notification_sent', {
|
|
27
|
+
title: payload.title,
|
|
28
|
+
tag: payload.tag,
|
|
29
|
+
url: payload.url,
|
|
30
|
+
sent: result.sent,
|
|
31
|
+
failed: result.failed,
|
|
32
|
+
expired: result.expired,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Track a push subscription event (subscribe or unsubscribe).
|
|
37
|
+
* Call from route handlers after successful subscription changes.
|
|
38
|
+
*/
|
|
39
|
+
export async function trackPushSubscription(action, metadata = {}) {
|
|
40
|
+
await trackPushEvent(`push_${action}`, {
|
|
41
|
+
country: metadata.country,
|
|
42
|
+
userAgent: metadata.userAgent,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Track a push cleanup event.
|
|
47
|
+
* Call after subscription cleanup runs.
|
|
48
|
+
*/
|
|
49
|
+
export async function trackPushCleanup(removed) {
|
|
50
|
+
await trackPushEvent('push_cleanup', { removed });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Emit a client-side analytics event using the gl:* CustomEvent pattern.
|
|
54
|
+
* This is meant to be called from browser-side code (service worker or page).
|
|
55
|
+
*
|
|
56
|
+
* Packages emit CustomEvent on document with gl: prefix.
|
|
57
|
+
* Analytics listens for all gl:* events. No hard dependencies between packages.
|
|
58
|
+
*
|
|
59
|
+
* Usage in service worker notificationclick handler:
|
|
60
|
+
* ```javascript
|
|
61
|
+
* // In the page after notification click navigation
|
|
62
|
+
* document.dispatchEvent(new CustomEvent('gl:push_notification_clicked', {
|
|
63
|
+
* detail: { tag: 'article-123', url: '/articles/123' }
|
|
64
|
+
* }))
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* This function is a convenience wrapper for use in page scripts.
|
|
68
|
+
*/
|
|
69
|
+
export function emitPushAnalyticsEvent(eventName, detail = {}) {
|
|
70
|
+
// Check for browser environment — this function is a no-op on the server
|
|
71
|
+
const doc = typeof globalThis !== 'undefined'
|
|
72
|
+
? globalThis.document
|
|
73
|
+
: undefined;
|
|
74
|
+
if (doc) {
|
|
75
|
+
doc.dispatchEvent(new CustomEvent(`gl:${eventName}`, { detail }));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,MAAM,gBAAgB,GAAG,wBAAwB,CAAA;AAEjD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,SAAiB,EACjB,IAA6B;IAE7B,IAAI,CAAC;QACJ,MAAM,SAAS,GAAG,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAA4B,CAAA;QAChG,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;YACnE,MACC,SAAS,CAAC,gBACV,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;QACnB,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,uDAAuD;IACxD,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAoB,EAAE,MAAuB;IAChF,MAAM,cAAc,CAAC,wBAAwB,EAAE;QAC9C,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;KACvB,CAAC,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,MAAqC,EACrC,WAAwE,EAAE;IAE1E,MAAM,cAAc,CAAC,QAAQ,MAAM,EAAE,EAAE;QACtC,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,SAAS,EAAE,QAAQ,CAAC,SAAS;KAC7B,CAAC,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAe;IACrD,MAAM,cAAc,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CACrC,SAAiB,EACjB,SAAkC,EAAE;IAEpC,yEAAyE;IACzE,MAAM,GAAG,GACR,OAAO,UAAU,KAAK,WAAW;QAChC,CAAC,CAAE,UAAiF,CAAC,QAAQ;QAC7F,CAAC,CAAC,SAAS,CAAA;IACb,IAAI,GAAG,EAAE,CAAC;QACT,GAAG,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,MAAM,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;IAClE,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface CleanupResult {
|
|
2
|
+
removed: number;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Remove stale or high-failure subscriptions.
|
|
6
|
+
* Call from a cron trigger or maintenance script.
|
|
7
|
+
*/
|
|
8
|
+
export declare function cleanupStaleSubscriptions(db: D1Database, options: {
|
|
9
|
+
staleAfterDays: number;
|
|
10
|
+
}): Promise<CleanupResult>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../src/utils/cleanup.ts"],"names":[],"mappings":"AAAA,UAAU,aAAa;IACtB,OAAO,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,EAAE,EAAE,UAAU,EACd,OAAO,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GACjC,OAAO,CAAC,aAAa,CAAC,CAgBxB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove stale or high-failure subscriptions.
|
|
3
|
+
* Call from a cron trigger or maintenance script.
|
|
4
|
+
*/
|
|
5
|
+
export async function cleanupStaleSubscriptions(db, options) {
|
|
6
|
+
const cutoff = Math.floor(Date.now() / 1000) - options.staleAfterDays * 86400;
|
|
7
|
+
const result = await db
|
|
8
|
+
.prepare(`DELETE FROM gl_push_subscriptions
|
|
9
|
+
WHERE (last_delivery_at IS NOT NULL AND last_delivery_at < ?)
|
|
10
|
+
OR (last_delivery_at IS NULL AND created_at < ?)
|
|
11
|
+
OR failure_count >= 10`)
|
|
12
|
+
.bind(cutoff, cutoff)
|
|
13
|
+
.run();
|
|
14
|
+
return {
|
|
15
|
+
removed: result.meta?.changes ?? 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/utils/cleanup.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC9C,EAAc,EACd,OAAmC;IAEnC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,cAAc,GAAG,KAAK,CAAA;IAE7E,MAAM,MAAM,GAAG,MAAM,EAAE;SACrB,OAAO,CACP;;;4BAGyB,CACzB;SACA,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;SACpB,GAAG,EAAE,CAAA;IAEP,OAAO;QACN,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC;KAClC,CAAA;AACF,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { PushPayload } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A push notification template with placeholder support.
|
|
4
|
+
* Placeholders use {{key}} syntax and are replaced at compose time.
|
|
5
|
+
*/
|
|
6
|
+
export interface PushTemplate {
|
|
7
|
+
/** Template name for identification */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Title with optional {{placeholders}} */
|
|
10
|
+
title: string;
|
|
11
|
+
/** Body with optional {{placeholders}} */
|
|
12
|
+
body: string;
|
|
13
|
+
/** Default icon URL (can be overridden at compose time) */
|
|
14
|
+
icon?: string;
|
|
15
|
+
/** Default image URL with optional {{placeholders}} */
|
|
16
|
+
image?: string;
|
|
17
|
+
/** URL pattern with optional {{placeholders}} */
|
|
18
|
+
url: string;
|
|
19
|
+
/** Optional action button */
|
|
20
|
+
action?: {
|
|
21
|
+
title: string;
|
|
22
|
+
url: string;
|
|
23
|
+
};
|
|
24
|
+
/** Optional dedup tag with {{placeholders}} */
|
|
25
|
+
tag?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Variables to interpolate into a template */
|
|
28
|
+
export type TemplateVariables = Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Compose a PushPayload from a template and variables.
|
|
31
|
+
*
|
|
32
|
+
* Templates define the structure with {{placeholder}} tokens.
|
|
33
|
+
* Variables fill in the dynamic parts at send time.
|
|
34
|
+
*
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const template: PushTemplate = {
|
|
37
|
+
* name: 'new-article',
|
|
38
|
+
* title: 'New on {{siteName}}',
|
|
39
|
+
* body: '{{articleTitle}}',
|
|
40
|
+
* url: 'https://{{domain}}/articles/{{slug}}',
|
|
41
|
+
* icon: '/icons/notification-192.png',
|
|
42
|
+
* tag: 'article-{{slug}}',
|
|
43
|
+
* action: {
|
|
44
|
+
* title: 'Read article',
|
|
45
|
+
* url: 'https://{{domain}}/articles/{{slug}}',
|
|
46
|
+
* },
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* const payload = composePushPayload(template, {
|
|
50
|
+
* siteName: 'FEDweek',
|
|
51
|
+
* domain: 'fedweek.com',
|
|
52
|
+
* articleTitle: 'Federal Pay Freeze Announced',
|
|
53
|
+
* slug: 'pay-freeze-2026',
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function composePushPayload(template: PushTemplate, variables?: TemplateVariables, overrides?: Partial<PushPayload>): PushPayload;
|
|
58
|
+
/**
|
|
59
|
+
* Built-in templates for common notification patterns.
|
|
60
|
+
* Consumers can use these directly or define their own.
|
|
61
|
+
*/
|
|
62
|
+
export declare const templates: {
|
|
63
|
+
/** New content published notification */
|
|
64
|
+
readonly newContent: {
|
|
65
|
+
readonly name: "new-content";
|
|
66
|
+
readonly title: "New on {{siteName}}";
|
|
67
|
+
readonly body: "{{contentTitle}}";
|
|
68
|
+
readonly url: "{{contentUrl}}";
|
|
69
|
+
readonly tag: "content-{{contentId}}";
|
|
70
|
+
readonly action: {
|
|
71
|
+
readonly title: "Read now";
|
|
72
|
+
readonly url: "{{contentUrl}}";
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/** Breaking news / high-urgency notification */
|
|
76
|
+
readonly breaking: {
|
|
77
|
+
readonly name: "breaking";
|
|
78
|
+
readonly title: "Breaking: {{headline}}";
|
|
79
|
+
readonly body: "{{summary}}";
|
|
80
|
+
readonly url: "{{contentUrl}}";
|
|
81
|
+
readonly tag: "breaking-{{contentId}}";
|
|
82
|
+
readonly action: {
|
|
83
|
+
readonly title: "Read more";
|
|
84
|
+
readonly url: "{{contentUrl}}";
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/** Generic notification with just title and body */
|
|
88
|
+
readonly generic: {
|
|
89
|
+
readonly name: "generic";
|
|
90
|
+
readonly title: "{{title}}";
|
|
91
|
+
readonly body: "{{body}}";
|
|
92
|
+
readonly url: "{{url}}";
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=composer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/utils/composer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACR,KAAK,EAAE,MAAM,CAAA;QACb,GAAG,EAAE,MAAM,CAAA;KACX,CAAA;IACD,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ;AAED,+CAA+C;AAC/C,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAYtD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,YAAY,EACtB,SAAS,GAAE,iBAAsB,EACjC,SAAS,GAAE,OAAO,CAAC,WAAW,CAAM,GAClC,WAAW,CA2Bb;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS;IACrB,yCAAyC;;;;;;;;;;;;IAazC,gDAAgD;;;;;;;;;;;;IAahD,oDAAoD;;;;;;;CAOJ,CAAA"}
|