feat(26-04): create push_subscriptions schema, migration, pushService, and push routes
- Add push_subscriptions pgTable with endpoint, p256dh, auth, userId, companyId, deviceLabel - Add 0055_create_push_subscriptions.sql migration with CREATE TABLE and endpoint index - Export pushSubscriptions from schema/index.ts - Create pushService with initVapid, getVapidPublicKey, saveSubscription, removeSubscription, sendPushToAll - sendPushToAll auto-deletes stale subscriptions on 410/404 response - Create pushRoutes: GET /vapid-public-key, POST /subscribe, DELETE /subscribe - Mount /api/push routes and call initVapid() in app.ts with graceful skip - Install web-push and @types/web-push
This commit is contained in:
parent
77117d9fc0
commit
3f1535f295
8 changed files with 277 additions and 0 deletions
|
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE IF NOT EXISTS "push_subscriptions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"endpoint" text NOT NULL,
|
||||
"p256dh" text NOT NULL,
|
||||
"auth" text NOT NULL,
|
||||
"user_id" uuid,
|
||||
"company_id" uuid,
|
||||
"device_label" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "push_sub_endpoint_idx" ON "push_subscriptions" ("endpoint");
|
||||
|
|
@ -61,3 +61,4 @@ export { chatMessages } from "./chat_messages.js";
|
|||
export { chatMessageBookmarks } from "./chat_message_bookmarks.js";
|
||||
export { chatFiles } from "./chat_files.js";
|
||||
export { chatFileReferences } from "./chat_file_references.js";
|
||||
export { pushSubscriptions } from "./push_subscriptions.js";
|
||||
|
|
|
|||
18
packages/db/src/schema/push_subscriptions.ts
Normal file
18
packages/db/src/schema/push_subscriptions.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
|
||||
|
||||
export const pushSubscriptions = pgTable(
|
||||
"push_subscriptions",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
endpoint: text("endpoint").notNull(),
|
||||
p256dh: text("p256dh").notNull(),
|
||||
auth: text("auth").notNull(),
|
||||
userId: uuid("user_id"),
|
||||
companyId: uuid("company_id"),
|
||||
deviceLabel: text("device_label"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
endpointIdx: index("push_sub_endpoint_idx").on(table.endpoint),
|
||||
}),
|
||||
);
|
||||
84
pnpm-lock.yaml
generated
84
pnpm-lock.yaml
generated
|
|
@ -485,6 +485,9 @@ importers:
|
|||
'@paperclipai/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/shared
|
||||
'@types/web-push':
|
||||
specifier: ^3.6.4
|
||||
version: 3.6.4
|
||||
ajv:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
|
|
@ -539,6 +542,9 @@ importers:
|
|||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
web-push:
|
||||
specifier: ^3.6.7
|
||||
version: 3.6.7
|
||||
ws:
|
||||
specifier: ^8.19.0
|
||||
version: 8.19.0
|
||||
|
|
@ -3599,6 +3605,9 @@ packages:
|
|||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/web-push@3.6.4':
|
||||
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
|
|
@ -3700,6 +3709,9 @@ packages:
|
|||
asap@2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
|
||||
asn1.js@5.4.1:
|
||||
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -3798,6 +3810,9 @@ packages:
|
|||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
bn.js@4.12.3:
|
||||
resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==}
|
||||
|
||||
body-parser@2.2.2:
|
||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3810,6 +3825,9 @@ packages:
|
|||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
|
|
@ -4383,6 +4401,9 @@ packages:
|
|||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
|
|
@ -4665,6 +4686,10 @@ packages:
|
|||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
http_ece@1.2.0:
|
||||
resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
|
@ -4800,6 +4825,12 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
jws@4.0.1:
|
||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||
|
||||
katex@0.16.37:
|
||||
resolution: {integrity: sha512-TIGjO2cCGYono+uUzgkE7RFF329mLLWGuHUlSr6cwIVj9O8f0VQZ783rsanmJpFUo32vvtj7XT04NGRPh+SZFg==}
|
||||
hasBin: true
|
||||
|
|
@ -4835,6 +4866,7 @@ packages:
|
|||
|
||||
libsql@0.5.29:
|
||||
resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==}
|
||||
cpu: [x64, arm64, wasm32, arm]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
lightningcss-android-arm64@1.30.2:
|
||||
|
|
@ -5168,6 +5200,9 @@ packages:
|
|||
engines: {node: '>=4.0.0'}
|
||||
hasBin: true
|
||||
|
||||
minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
|
|
@ -6116,6 +6151,11 @@ packages:
|
|||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
web-push@3.6.7:
|
||||
resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
|
||||
engines: {node: '>= 16'}
|
||||
hasBin: true
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
|
@ -9645,6 +9685,10 @@ snapshots:
|
|||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/web-push@3.6.4':
|
||||
dependencies:
|
||||
'@types/node': 25.2.3
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.2.3
|
||||
|
|
@ -9759,6 +9803,13 @@ snapshots:
|
|||
|
||||
asap@2.0.6: {}
|
||||
|
||||
asn1.js@5.4.1:
|
||||
dependencies:
|
||||
bn.js: 4.12.3
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
async-exit-hook@2.0.1: {}
|
||||
|
|
@ -9808,6 +9859,8 @@ snapshots:
|
|||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
bn.js@4.12.3: {}
|
||||
|
||||
body-parser@2.2.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
|
|
@ -9832,6 +9885,8 @@ snapshots:
|
|||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@6.0.3:
|
||||
|
|
@ -10328,6 +10383,10 @@ snapshots:
|
|||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
electron-to-chromium@1.5.286: {}
|
||||
|
|
@ -10741,6 +10800,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http_ece@1.2.0: {}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
|
|
@ -10858,6 +10919,17 @@ snapshots:
|
|||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.1:
|
||||
dependencies:
|
||||
jwa: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
katex@0.16.37:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
|
|
@ -11517,6 +11589,8 @@ snapshots:
|
|||
|
||||
mime@2.6.0: {}
|
||||
|
||||
minimalistic-assert@1.0.1: {}
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mkdirp@0.5.6:
|
||||
|
|
@ -12675,6 +12749,16 @@ snapshots:
|
|||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
web-push@3.6.7:
|
||||
dependencies:
|
||||
asn1.js: 5.4.1
|
||||
http_ece: 1.2.0
|
||||
https-proxy-agent: 7.0.6
|
||||
jws: 4.0.1
|
||||
minimist: 1.2.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"@paperclipai/db": "workspace:*",
|
||||
"@paperclipai/plugin-sdk": "workspace:*",
|
||||
"@paperclipai/shared": "workspace:*",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"ajv": "^8.18.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"better-auth": "1.4.18",
|
||||
|
|
@ -75,6 +76,7 @@
|
|||
"pino-http": "^10.4.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"sharp": "^0.34.5",
|
||||
"web-push": "^3.6.7",
|
||||
"ws": "^8.19.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import { instanceSettingsRoutes } from "./routes/instance-settings.js";
|
|||
import { llmRoutes } from "./routes/llms.js";
|
||||
import { assetRoutes } from "./routes/assets.js";
|
||||
import { chatFileRoutes } from "./routes/chat-files.js";
|
||||
import { pushRoutes } from "./routes/push.js";
|
||||
import { initVapid } from "./services/pushService.js";
|
||||
import { accessRoutes } from "./routes/access.js";
|
||||
import { pluginRoutes } from "./routes/plugins.js";
|
||||
import { pluginUiStaticRoutes } from "./routes/plugin-ui-static.js";
|
||||
|
|
@ -150,6 +152,7 @@ export async function createApp(
|
|||
api.use(agentRoutes(db));
|
||||
api.use(assetRoutes(db, opts.storageService));
|
||||
api.use(chatFileRoutes(db, opts.storageService));
|
||||
api.use("/push", pushRoutes(db));
|
||||
api.use(projectRoutes(db));
|
||||
api.use(issueRoutes(db, opts.storageService));
|
||||
api.use(routineRoutes(db));
|
||||
|
|
@ -294,6 +297,13 @@ export async function createApp(
|
|||
|
||||
app.use(errorHandler);
|
||||
|
||||
// Initialize VAPID for push notifications (graceful skip if keys not set)
|
||||
try {
|
||||
initVapid();
|
||||
} catch (err) {
|
||||
logger.warn({ err }, "VAPID init skipped — push notifications unavailable");
|
||||
}
|
||||
|
||||
jobCoordinator.start();
|
||||
scheduler.start();
|
||||
void toolDispatcher.initialize().catch((err) => {
|
||||
|
|
|
|||
59
server/src/routes/push.ts
Normal file
59
server/src/routes/push.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Router } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { getVapidPublicKey, saveSubscription, removeSubscription } from "../services/pushService.js";
|
||||
|
||||
export function pushRoutes(db: Db): Router {
|
||||
const router = Router();
|
||||
|
||||
// GET /api/push/vapid-public-key
|
||||
router.get("/vapid-public-key", (_req, res) => {
|
||||
const publicKey = getVapidPublicKey();
|
||||
if (!publicKey) {
|
||||
res.status(404).json({ error: "VAPID not configured" });
|
||||
return;
|
||||
}
|
||||
res.json({ publicKey });
|
||||
});
|
||||
|
||||
// POST /api/push/subscribe
|
||||
router.post("/subscribe", async (req, res) => {
|
||||
const { endpoint, keys, userId, companyId, deviceLabel } = req.body as {
|
||||
endpoint: string;
|
||||
keys: { p256dh: string; auth: string };
|
||||
userId?: string;
|
||||
companyId?: string;
|
||||
deviceLabel?: string;
|
||||
};
|
||||
|
||||
if (!endpoint || !keys?.p256dh || !keys?.auth) {
|
||||
res.status(400).json({ error: "Missing required subscription fields" });
|
||||
return;
|
||||
}
|
||||
|
||||
const id = await saveSubscription(db, {
|
||||
endpoint,
|
||||
p256dh: keys.p256dh,
|
||||
auth: keys.auth,
|
||||
userId,
|
||||
companyId,
|
||||
deviceLabel,
|
||||
});
|
||||
|
||||
res.status(201).json({ id });
|
||||
});
|
||||
|
||||
// DELETE /api/push/subscribe
|
||||
router.delete("/subscribe", async (req, res) => {
|
||||
const { endpoint } = req.body as { endpoint: string };
|
||||
|
||||
if (!endpoint) {
|
||||
res.status(400).json({ error: "Missing endpoint" });
|
||||
return;
|
||||
}
|
||||
|
||||
await removeSubscription(db, endpoint);
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
91
server/src/services/pushService.ts
Normal file
91
server/src/services/pushService.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import webPush from "web-push";
|
||||
import { eq, and, isNotNull } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { pushSubscriptions } from "@paperclipai/db";
|
||||
import { logger } from "../middleware/logger.js";
|
||||
|
||||
export function initVapid(): void {
|
||||
const publicKey = process.env.VAPID_PUBLIC_KEY;
|
||||
const privateKey = process.env.VAPID_PRIVATE_KEY;
|
||||
if (!publicKey || !privateKey) {
|
||||
logger.warn("VAPID keys not configured — push notifications disabled");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
webPush.setVapidDetails(
|
||||
process.env.VAPID_SUBJECT ?? "mailto:admin@nexus.local",
|
||||
publicKey,
|
||||
privateKey,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn({ err }, "Failed to initialize VAPID — push notifications disabled");
|
||||
}
|
||||
}
|
||||
|
||||
export function getVapidPublicKey(): string | null {
|
||||
return process.env.VAPID_PUBLIC_KEY ?? null;
|
||||
}
|
||||
|
||||
export async function saveSubscription(
|
||||
db: Db,
|
||||
params: {
|
||||
endpoint: string;
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
userId?: string;
|
||||
companyId?: string;
|
||||
deviceLabel?: string;
|
||||
},
|
||||
): Promise<string> {
|
||||
const id = crypto.randomUUID();
|
||||
await db.insert(pushSubscriptions).values({
|
||||
id,
|
||||
endpoint: params.endpoint,
|
||||
p256dh: params.p256dh,
|
||||
auth: params.auth,
|
||||
userId: params.userId ?? null,
|
||||
companyId: params.companyId ?? null,
|
||||
deviceLabel: params.deviceLabel ?? null,
|
||||
}).onConflictDoNothing();
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function removeSubscription(db: Db, endpoint: string): Promise<void> {
|
||||
await db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint));
|
||||
}
|
||||
|
||||
export async function sendPushToAll(
|
||||
db: Db,
|
||||
companyId: string | null,
|
||||
payload: { title: string; body: string; icon?: string; data?: Record<string, string> },
|
||||
): Promise<void> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(pushSubscriptions)
|
||||
.where(
|
||||
companyId
|
||||
? and(eq(pushSubscriptions.companyId, companyId), isNotNull(pushSubscriptions.endpoint))
|
||||
: isNotNull(pushSubscriptions.endpoint),
|
||||
);
|
||||
|
||||
const payloadStr = JSON.stringify(payload);
|
||||
|
||||
await Promise.allSettled(
|
||||
rows.map(async (row) => {
|
||||
try {
|
||||
await webPush.sendNotification(
|
||||
{ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
|
||||
payloadStr,
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
const status = (err as { statusCode?: number }).statusCode;
|
||||
if (status === 410 || status === 404) {
|
||||
// Stale subscription — remove it
|
||||
await removeSubscription(db, row.endpoint).catch(() => {});
|
||||
} else {
|
||||
logger.warn({ err, endpoint: row.endpoint }, "Push notification send failed");
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue