@juspay/shooter 1.24.1 → 1.25.0
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/.claude/hooks/notifier.cjs +32 -4
- package/bin/lib/service-manager.cjs +148 -0
- package/bin/shooter.cjs +151 -62
- package/build/client/_app/immutable/assets/{4.D4EDSN4H.css → 4.ChO_hlLs.css} +1 -1
- package/build/client/_app/immutable/assets/4.ChO_hlLs.css.br +0 -0
- package/build/client/_app/immutable/assets/4.ChO_hlLs.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{DjWRwZyr.js → BstJSK2K.js} +1 -1
- package/build/client/_app/immutable/chunks/BstJSK2K.js.br +0 -0
- package/build/client/_app/immutable/chunks/BstJSK2K.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CfzjLyJm.js → DINqYbXU.js} +1 -1
- package/build/client/_app/immutable/chunks/DINqYbXU.js.br +0 -0
- package/build/client/_app/immutable/chunks/DINqYbXU.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ssAhjWfF.js +3 -0
- package/build/client/_app/immutable/chunks/ssAhjWfF.js.br +0 -0
- package/build/client/_app/immutable/chunks/ssAhjWfF.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.9F0rhLIY.js → app.BPK9s2o5.js} +2 -2
- package/build/client/_app/immutable/entry/app.BPK9s2o5.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BPK9s2o5.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DFPHuGE3.js +1 -0
- package/build/client/_app/immutable/entry/start.DFPHuGE3.js.br +2 -0
- package/build/client/_app/immutable/entry/start.DFPHuGE3.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.MlMcxYLT.js → 0.BEbzRcwm.js} +1 -1
- package/build/client/_app/immutable/nodes/0.BEbzRcwm.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.BEbzRcwm.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1._Bn-HQ9b.js → 1.C_V0SOr9.js} +1 -1
- package/build/client/_app/immutable/nodes/1.C_V0SOr9.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.C_V0SOr9.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{10.CsX0V4R9.js → 10.6V-37_Rl.js} +1 -1
- package/build/client/_app/immutable/nodes/10.6V-37_Rl.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.6V-37_Rl.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{11.lM_b6yUv.js → 11.Lzf-KC6L.js} +1 -1
- package/build/client/_app/immutable/nodes/11.Lzf-KC6L.js.br +0 -0
- package/build/client/_app/immutable/nodes/11.Lzf-KC6L.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.sAnHf5go.js → 2.Cl2dMP2L.js} +1 -1
- package/build/client/_app/immutable/nodes/2.Cl2dMP2L.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.Cl2dMP2L.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.DEHWta3G.js → 3.CuX2eSna.js} +1 -1
- package/build/client/_app/immutable/nodes/3.CuX2eSna.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CuX2eSna.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.2DvqoOaB.js +17 -0
- package/build/client/_app/immutable/nodes/4.2DvqoOaB.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.2DvqoOaB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.-gDjaLSz.js → 6.C7e6zQyP.js} +1 -1
- package/build/client/_app/immutable/nodes/6.C7e6zQyP.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.C7e6zQyP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.R-emDnvX.js → 7.1ygvNTnO.js} +1 -1
- package/build/client/_app/immutable/nodes/7.1ygvNTnO.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.1ygvNTnO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.DXNcFetv.js → 8.CBVmgOk0.js} +1 -1
- package/build/client/_app/immutable/nodes/8.CBVmgOk0.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.CBVmgOk0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.Brv6N-Ji.js → 9.B-_ZFZhj.js} +1 -1
- package/build/client/_app/immutable/nodes/9.B-_ZFZhj.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.B-_ZFZhj.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-CezVlDLP.js → 0-BHU07xt9.js} +2 -2
- package/build/server/chunks/{0-CezVlDLP.js.map → 0-BHU07xt9.js.map} +1 -1
- package/build/server/chunks/{1-CAwGzW00.js → 1-OVOd8GUH.js} +2 -2
- package/build/server/chunks/{1-CAwGzW00.js.map → 1-OVOd8GUH.js.map} +1 -1
- package/build/server/chunks/{10-pqorW2OP.js → 10-CRHtvb_u.js} +2 -2
- package/build/server/chunks/{10-pqorW2OP.js.map → 10-CRHtvb_u.js.map} +1 -1
- package/build/server/chunks/{11-Byxg_lSY.js → 11-CdSex9j1.js} +2 -2
- package/build/server/chunks/{11-Byxg_lSY.js.map → 11-CdSex9j1.js.map} +1 -1
- package/build/server/chunks/{2-Bbck5mN2.js → 2-bb78aIZ6.js} +2 -2
- package/build/server/chunks/{2-Bbck5mN2.js.map → 2-bb78aIZ6.js.map} +1 -1
- package/build/server/chunks/{3-BBFbA1P8.js → 3-CsTC6Lrn.js} +2 -2
- package/build/server/chunks/{3-BBFbA1P8.js.map → 3-CsTC6Lrn.js.map} +1 -1
- package/build/server/chunks/{4-w2W_T8ax.js → 4-DTTu8_Gr.js} +4 -4
- package/build/server/chunks/{4-w2W_T8ax.js.map → 4-DTTu8_Gr.js.map} +1 -1
- package/build/server/chunks/{6-BI3p-t3i.js → 6-BUgQGB_4.js} +2 -2
- package/build/server/chunks/{6-BI3p-t3i.js.map → 6-BUgQGB_4.js.map} +1 -1
- package/build/server/chunks/{7-6XKPVOSu.js → 7-CsQZnkcG.js} +2 -2
- package/build/server/chunks/{7-6XKPVOSu.js.map → 7-CsQZnkcG.js.map} +1 -1
- package/build/server/chunks/{8-VOztiJG_.js → 8-DcIuPdyW.js} +2 -2
- package/build/server/chunks/{8-VOztiJG_.js.map → 8-DcIuPdyW.js.map} +1 -1
- package/build/server/chunks/{9-DNUlBLEz.js → 9-D8bkM1uj.js} +2 -2
- package/build/server/chunks/{9-DNUlBLEz.js.map → 9-D8bkM1uj.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-DDE2nChH.js → _page.svelte-BBbaKwNz.js} +101 -27
- package/build/server/chunks/_page.svelte-BBbaKwNz.js.map +1 -0
- package/build/server/chunks/{_server.ts-DfscXcFe.js → _server.ts-B7BLxK5u.js} +233 -186
- package/build/server/chunks/_server.ts-B7BLxK5u.js.map +1 -0
- package/build/server/chunks/{_server.ts-EJVmhLtg.js → _server.ts-BSS8cO80.js} +15 -3
- package/build/server/chunks/_server.ts-BSS8cO80.js.map +1 -0
- package/build/server/chunks/_server.ts-DNTxPoxO.js +115 -0
- package/build/server/chunks/_server.ts-DNTxPoxO.js.map +1 -0
- package/build/server/chunks/{_server.ts-D9_hkPQ6.js → _server.ts-DUb7fbuW.js} +17 -3
- package/build/server/chunks/_server.ts-DUb7fbuW.js.map +1 -0
- package/build/server/chunks/{_server.ts-v7TaT83B.js → _server.ts-FdKi8RwL.js} +7 -3
- package/build/server/chunks/_server.ts-FdKi8RwL.js.map +1 -0
- package/build/server/chunks/device-format-DTgEz4Yr.js +29 -0
- package/build/server/chunks/device-format-DTgEz4Yr.js.map +1 -0
- package/build/server/chunks/device-token-store-Ct7aTeR8.js +259 -0
- package/build/server/chunks/device-token-store-Ct7aTeR8.js.map +1 -0
- package/build/server/chunks/{library-apns-D8RPINlv.js → library-apns-DMlL1BAg.js} +158 -26
- package/build/server/chunks/library-apns-DMlL1BAg.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +17 -17
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/server.ts +15 -1
- package/src/app.d.ts +4 -0
- package/src/lib/modules/server/apn/apns-classify.ts +103 -0
- package/src/lib/modules/server/apn/library-apns.ts +151 -35
- package/src/lib/modules/server/apn/notify-fanout.ts +40 -0
- package/src/lib/modules/server/fcm/fcm-classify.ts +61 -0
- package/src/lib/modules/server/fcm/fcm-service.ts +128 -29
- package/src/lib/modules/server/push/device-format.ts +42 -0
- package/src/lib/modules/server/push/device-token-store.ts +354 -0
- package/src/lib/types/apn.ts +4 -0
- package/src/lib/types/device.ts +156 -0
- package/src/lib/types/index.ts +1 -0
- package/src/routes/api/debug/+server.ts +13 -1
- package/src/routes/api/device-token/+server.ts +122 -37
- package/src/routes/api/health/+server.ts +16 -2
- package/src/routes/api/notify/+server.ts +175 -168
- package/src/routes/api/qr-config/+server.ts +9 -2
- package/src/routes/config/+page.svelte +182 -44
- package/build/client/_app/immutable/assets/4.D4EDSN4H.css.br +0 -0
- package/build/client/_app/immutable/assets/4.D4EDSN4H.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BzW0vlVX.js +0 -3
- package/build/client/_app/immutable/chunks/BzW0vlVX.js.br +0 -0
- package/build/client/_app/immutable/chunks/BzW0vlVX.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CfzjLyJm.js.br +0 -0
- package/build/client/_app/immutable/chunks/CfzjLyJm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DjWRwZyr.js.br +0 -0
- package/build/client/_app/immutable/chunks/DjWRwZyr.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.9F0rhLIY.js.br +0 -0
- package/build/client/_app/immutable/entry/app.9F0rhLIY.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DGFWmhrj.js +0 -1
- package/build/client/_app/immutable/entry/start.DGFWmhrj.js.br +0 -2
- package/build/client/_app/immutable/entry/start.DGFWmhrj.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.MlMcxYLT.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.MlMcxYLT.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1._Bn-HQ9b.js.br +0 -0
- package/build/client/_app/immutable/nodes/1._Bn-HQ9b.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.CsX0V4R9.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.CsX0V4R9.js.gz +0 -0
- package/build/client/_app/immutable/nodes/11.lM_b6yUv.js.br +0 -0
- package/build/client/_app/immutable/nodes/11.lM_b6yUv.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.sAnHf5go.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.sAnHf5go.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DEHWta3G.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DEHWta3G.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.C9fv_m2R.js +0 -16
- package/build/client/_app/immutable/nodes/4.C9fv_m2R.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.C9fv_m2R.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.-gDjaLSz.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.-gDjaLSz.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.R-emDnvX.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.R-emDnvX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.DXNcFetv.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.DXNcFetv.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.Brv6N-Ji.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.Brv6N-Ji.js.gz +0 -0
- package/build/server/chunks/_page.svelte-DDE2nChH.js.map +0 -1
- package/build/server/chunks/_server.ts-D2RS8TFd.js +0 -72
- package/build/server/chunks/_server.ts-D2RS8TFd.js.map +0 -1
- package/build/server/chunks/_server.ts-D9_hkPQ6.js.map +0 -1
- package/build/server/chunks/_server.ts-DfscXcFe.js.map +0 -1
- package/build/server/chunks/_server.ts-EJVmhLtg.js.map +0 -1
- package/build/server/chunks/_server.ts-v7TaT83B.js.map +0 -1
- package/build/server/chunks/library-apns-D8RPINlv.js.map +0 -1
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
APNsFanOutResult,
|
|
3
|
+
AppEnv,
|
|
4
|
+
DeviceRecord,
|
|
5
|
+
FCMFanOutResult,
|
|
6
|
+
NotificationData,
|
|
7
|
+
OptionChoice,
|
|
8
|
+
ResponseKind,
|
|
9
|
+
} from '$lib/types';
|
|
2
10
|
|
|
3
11
|
import { env } from '$env/dynamic/private';
|
|
4
|
-
import { readPersistedDeviceToken, resolveDeviceToken } from '$lib/modules/server/apn/device-token';
|
|
5
12
|
import { LibraryAPNsService } from '$lib/modules/server/apn/library-apns';
|
|
6
13
|
import { addNotification, getNotifications } from '$lib/modules/server/apn/notification-history';
|
|
14
|
+
import { selectPlatforms, summarizeNotifyDelivery } from '$lib/modules/server/apn/notify-fanout';
|
|
7
15
|
import { createPendingRequest } from '$lib/modules/server/apn/pending-requests';
|
|
8
16
|
import { validateAuth } from '$lib/modules/server/auth';
|
|
9
|
-
import { isFCMConfigured,
|
|
17
|
+
import { isFCMConfigured, sendFCMNotificationMulti } from '$lib/modules/server/fcm/fcm-service.js';
|
|
18
|
+
import { deviceTokenStore } from '$lib/modules/server/push/device-token-store';
|
|
10
19
|
import { toErrorMessage } from '$lib/modules/server/utils/error';
|
|
11
20
|
import { broadcastEvent } from '$lib/modules/server/ws/server';
|
|
12
21
|
import { json } from '@sveltejs/kit';
|
|
@@ -22,6 +31,73 @@ function getAPNsClient(): LibraryAPNsService {
|
|
|
22
31
|
return apnsSingleton;
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
const EMPTY_APNS_RESULT: APNsFanOutResult = {
|
|
35
|
+
results: [],
|
|
36
|
+
staleTokens: [],
|
|
37
|
+
totalFailed: 0,
|
|
38
|
+
totalSent: 0,
|
|
39
|
+
};
|
|
40
|
+
const EMPTY_FCM_RESULT: FCMFanOutResult = {
|
|
41
|
+
failureCount: 0,
|
|
42
|
+
results: [],
|
|
43
|
+
staleTokens: [],
|
|
44
|
+
successCount: 0,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Android tokens to deliver to: active registry rows, else the ANDROID_DEVICE_TOKEN seed. */
|
|
48
|
+
function resolveAndroidTokens(): string[] {
|
|
49
|
+
const rows = deviceTokenStore.listActive('android');
|
|
50
|
+
if (rows.length > 0) {
|
|
51
|
+
return rows.map((r) => r.token);
|
|
52
|
+
}
|
|
53
|
+
const seed = env.ANDROID_DEVICE_TOKEN?.trim();
|
|
54
|
+
return seed ? [seed] : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* iOS devices to deliver to: an explicit request override, else all active
|
|
59
|
+
* registry rows for the server's gateway, else the legacy DEVICE_TOKEN env seed
|
|
60
|
+
* (so .env-only deployments keep working without ever writing to the DB).
|
|
61
|
+
*/
|
|
62
|
+
function resolveIosDevices(override?: string): DeviceRecord[] {
|
|
63
|
+
const appEnv = serverApnEnv();
|
|
64
|
+
if (override) {
|
|
65
|
+
return [syntheticSeedDevice(override, 'ios', appEnv)];
|
|
66
|
+
}
|
|
67
|
+
const rows = deviceTokenStore.listActiveForEnv('ios', appEnv);
|
|
68
|
+
if (rows.length > 0) {
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
const seed = env.DEVICE_TOKEN?.trim();
|
|
72
|
+
return seed ? [syntheticSeedDevice(seed, 'ios', appEnv)] : [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function serverApnEnv(): AppEnv {
|
|
76
|
+
return env.APNS_PRODUCTION === 'true' ? 'production' : 'sandbox';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A throwaway DeviceRecord wrapping a legacy env-seed or request-override token. */
|
|
80
|
+
function syntheticSeedDevice(
|
|
81
|
+
token: string,
|
|
82
|
+
platform: 'android' | 'ios',
|
|
83
|
+
appEnv: AppEnv
|
|
84
|
+
): DeviceRecord {
|
|
85
|
+
const nowIso = new Date().toISOString();
|
|
86
|
+
return {
|
|
87
|
+
appEnv,
|
|
88
|
+
bundleId: null,
|
|
89
|
+
deviceId: null,
|
|
90
|
+
failureCount: 0,
|
|
91
|
+
friendlyName: null,
|
|
92
|
+
id: `seed-${platform}`,
|
|
93
|
+
isActive: true,
|
|
94
|
+
lastSeenAt: nowIso,
|
|
95
|
+
platform,
|
|
96
|
+
registeredAt: nowIso,
|
|
97
|
+
token,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
25
101
|
// NOTIFICATION DEDUPLICATION CACHE
|
|
26
102
|
const notificationCache = new Map<string, number>();
|
|
27
103
|
const DEDUP_WINDOW = 10000; // 10 seconds deduplication window
|
|
@@ -136,6 +212,13 @@ function intelligentNotificationFilter(
|
|
|
136
212
|
): { reason: string; send: boolean } {
|
|
137
213
|
const source = data?.source || 'unknown';
|
|
138
214
|
|
|
215
|
+
// Never filter a bidirectional (waitForResponse) request: the caller blocks
|
|
216
|
+
// on polling, and a filtered 200 would read as success:true and hang it for
|
|
217
|
+
// the full permission timeout. These are also dedup-exempt below.
|
|
218
|
+
if (waitForResponse) {
|
|
219
|
+
return { reason: 'Bidirectional request — never filtered', send: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
139
222
|
// Check for duplicate notifications first
|
|
140
223
|
if (isDuplicateNotification(title, message, data, waitForResponse)) {
|
|
141
224
|
return {
|
|
@@ -265,7 +348,7 @@ function releaseNotification(title: string, message: string, data?: Notification
|
|
|
265
348
|
// helpers. This handler grew past the 300-line guideline organically; PR-2
|
|
266
349
|
// adds 4 lines for dynamic-options fields. A dedicated cleanup PR is the
|
|
267
350
|
// right place to split it, not a feature PR.
|
|
268
|
-
|
|
351
|
+
|
|
269
352
|
export const POST: RequestHandler = async ({ request }) => {
|
|
270
353
|
try {
|
|
271
354
|
// Use singleton APNs client to reuse HTTP/2 connection
|
|
@@ -420,175 +503,99 @@ export const POST: RequestHandler = async ({ request }) => {
|
|
|
420
503
|
title,
|
|
421
504
|
};
|
|
422
505
|
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
506
|
+
// Multi-device fan-out. DEVICE_PLATFORM is now a FILTER (unset → both
|
|
507
|
+
// platforms), not a binary switch. A request-scoped deviceToken override
|
|
508
|
+
// (config "send test") bypasses the registry and targets one token.
|
|
509
|
+
const override = requestDeviceToken?.trim() || undefined;
|
|
510
|
+
const { doAndroid, doIos } = selectPlatforms(env.DEVICE_PLATFORM);
|
|
511
|
+
|
|
512
|
+
// An override is a single token of unknown platform; honour the platform
|
|
513
|
+
// FILTER (unset → try both) rather than assuming iOS, so an Android override
|
|
514
|
+
// is not silently dropped when DEVICE_PLATFORM is unset. Firing a token at
|
|
515
|
+
// the wrong gateway is harmless here because override sends never prune (see
|
|
516
|
+
// the pruning guard below).
|
|
517
|
+
const iosDevices: DeviceRecord[] = doIos
|
|
518
|
+
? override
|
|
519
|
+
? resolveIosDevices(override)
|
|
520
|
+
: resolveIosDevices()
|
|
521
|
+
: [];
|
|
522
|
+
|
|
523
|
+
const androidTokens: string[] = doAndroid
|
|
524
|
+
? override
|
|
525
|
+
? [override]
|
|
526
|
+
: resolveAndroidTokens()
|
|
527
|
+
: [];
|
|
528
|
+
|
|
529
|
+
// Collapse permission pushes by requestId so a re-notify replaces (not
|
|
530
|
+
// stacks) the prompt on every device, and answering on one clears it.
|
|
531
|
+
const collapseId = waitForResponse ? canonicalRequestId : undefined;
|
|
532
|
+
|
|
533
|
+
const [apnsResult, fcmResult] = await Promise.all([
|
|
534
|
+
iosDevices.length > 0 && apnsClient.isConfigured()
|
|
535
|
+
? apnsClient.sendToMany(iosDevices, payload, collapseId)
|
|
536
|
+
: Promise.resolve(EMPTY_APNS_RESULT),
|
|
537
|
+
androidTokens.length > 0 && isFCMConfigured()
|
|
538
|
+
? sendFCMNotificationMulti(androidTokens, payload)
|
|
539
|
+
: Promise.resolve(EMPTY_FCM_RESULT),
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
const summary = summarizeNotifyDelivery(apnsResult, fcmResult);
|
|
543
|
+
|
|
544
|
+
// Lazy prune dead tokens; bump last-seen for the ones that delivered.
|
|
545
|
+
// Never prune on an override send: the override token is a one-off explicit
|
|
546
|
+
// target (not necessarily a registry row), and firing it at the wrong
|
|
547
|
+
// gateway must not soft-delete a real device that happens to hold it.
|
|
548
|
+
const pruned =
|
|
549
|
+
!override && summary.staleTokens.length
|
|
550
|
+
? deviceTokenStore.pruneByTokens(summary.staleTokens)
|
|
551
|
+
: 0;
|
|
552
|
+
if (summary.succeededTokens.length > 0) {
|
|
553
|
+
deviceTokenStore.touchLastSeen(summary.succeededTokens);
|
|
554
|
+
}
|
|
459
555
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (waitForResponse) {
|
|
467
|
-
createPendingRequest(canonicalRequestId, {
|
|
468
|
-
options,
|
|
469
|
-
question: question ?? null,
|
|
470
|
-
responseKind: responseKind ?? 'hook',
|
|
471
|
-
sessionId: (data?.sessionId as string) || '',
|
|
472
|
-
toolInput: (data?.toolInput as Record<string, unknown>) || {},
|
|
473
|
-
toolName: (data?.toolName as string) || '',
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
addNotification(buildNotificationRecord(canonicalRequestId, title, message, 'sent', data));
|
|
478
|
-
|
|
479
|
-
return json({
|
|
480
|
-
message: 'Notification sent successfully',
|
|
481
|
-
requestId: canonicalRequestId,
|
|
482
|
-
result: { messageId: fcmResult.messageId },
|
|
483
|
-
success: true,
|
|
484
|
-
timestamp: new Date().toISOString(),
|
|
485
|
-
});
|
|
486
|
-
} else {
|
|
487
|
-
console.error(`[notify] FCM delivery failed: ${fcmResult.error}`);
|
|
488
|
-
releaseNotification(title, message, data); // free the reserved dedup slot for a retry
|
|
489
|
-
|
|
490
|
-
addNotification(
|
|
491
|
-
buildNotificationRecord(
|
|
492
|
-
canonicalRequestId,
|
|
493
|
-
title,
|
|
494
|
-
message,
|
|
495
|
-
'failed',
|
|
496
|
-
data,
|
|
497
|
-
fcmResult.error
|
|
498
|
-
)
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
return json(
|
|
502
|
-
{
|
|
503
|
-
details: fcmResult.error,
|
|
504
|
-
error: 'Failed to send notification',
|
|
505
|
-
requestId: canonicalRequestId,
|
|
506
|
-
timestamp: new Date().toISOString(),
|
|
507
|
-
},
|
|
508
|
-
{ status: 500 }
|
|
509
|
-
);
|
|
510
|
-
}
|
|
556
|
+
// Keep the dedup reservation only on a clean, fully-successful delivery;
|
|
557
|
+
// otherwise release it so a legitimate retry is not blocked (a transient
|
|
558
|
+
// failure for one device must not poison the cache for everyone).
|
|
559
|
+
if (summary.delivered && summary.failed === 0) {
|
|
560
|
+
recordNotification(title, message, data);
|
|
511
561
|
} else {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return json(
|
|
515
|
-
{
|
|
516
|
-
details: 'Missing APNS_KEY, APNS_KEY_ID, or APNS_TEAM_ID environment variables',
|
|
517
|
-
error: 'APNs client not configured',
|
|
518
|
-
},
|
|
519
|
-
{ status: 500 }
|
|
520
|
-
);
|
|
521
|
-
}
|
|
562
|
+
releaseNotification(title, message, data);
|
|
563
|
+
}
|
|
522
564
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
565
|
+
// Register the pending request for bidirectional polling only if at least
|
|
566
|
+
// one device received the push — otherwise no one can answer.
|
|
567
|
+
if (waitForResponse && summary.delivered) {
|
|
568
|
+
createPendingRequest(canonicalRequestId, {
|
|
569
|
+
options,
|
|
570
|
+
question: question ?? null,
|
|
571
|
+
responseKind: responseKind ?? 'hook',
|
|
572
|
+
sessionId: (data?.sessionId as string) || '',
|
|
573
|
+
toolInput: (data?.toolInput as Record<string, unknown>) || {},
|
|
574
|
+
toolName: (data?.toolName as string) || '',
|
|
575
|
+
});
|
|
576
|
+
}
|
|
532
577
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
578
|
+
addNotification(
|
|
579
|
+
buildNotificationRecord(
|
|
580
|
+
canonicalRequestId,
|
|
581
|
+
title,
|
|
582
|
+
message,
|
|
583
|
+
summary.delivered ? 'sent' : 'failed',
|
|
584
|
+
data,
|
|
585
|
+
summary.delivered ? null : 'No registered device accepted the notification'
|
|
586
|
+
)
|
|
587
|
+
);
|
|
543
588
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
options,
|
|
555
|
-
question: question ?? null,
|
|
556
|
-
responseKind: responseKind ?? 'hook',
|
|
557
|
-
sessionId: (data?.sessionId as string) || '',
|
|
558
|
-
toolInput: (data?.toolInput as Record<string, unknown>) || {},
|
|
559
|
-
toolName: (data?.toolName as string) || '',
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
addNotification(buildNotificationRecord(canonicalRequestId, title, message, 'sent', data));
|
|
564
|
-
|
|
565
|
-
return json({
|
|
566
|
-
message: 'Notification sent successfully',
|
|
567
|
-
requestId: canonicalRequestId,
|
|
568
|
-
result,
|
|
569
|
-
success: true,
|
|
570
|
-
timestamp: new Date().toISOString(),
|
|
571
|
-
});
|
|
572
|
-
} catch (notificationError) {
|
|
573
|
-
const notifErrMsg = toErrorMessage(notificationError);
|
|
574
|
-
console.error(`[notify] APNs delivery failed: ${notifErrMsg}`);
|
|
575
|
-
releaseNotification(title, message, data); // free the reserved dedup slot for a retry
|
|
576
|
-
|
|
577
|
-
addNotification(
|
|
578
|
-
buildNotificationRecord(canonicalRequestId, title, message, 'failed', data, notifErrMsg)
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
return json(
|
|
582
|
-
{
|
|
583
|
-
details: notifErrMsg,
|
|
584
|
-
error: 'Failed to send notification',
|
|
585
|
-
requestId: canonicalRequestId,
|
|
586
|
-
timestamp: new Date().toISOString(),
|
|
587
|
-
},
|
|
588
|
-
{ status: 500 }
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
589
|
+
// 200 even when nothing was delivered (success:false) so the notifier hook
|
|
590
|
+
// fast-fails instead of hanging for the full permission timeout.
|
|
591
|
+
return json({
|
|
592
|
+
failed: summary.failed,
|
|
593
|
+
pruned,
|
|
594
|
+
requestId: canonicalRequestId,
|
|
595
|
+
sent: summary.sent,
|
|
596
|
+
success: summary.delivered,
|
|
597
|
+
timestamp: new Date().toISOString(),
|
|
598
|
+
});
|
|
592
599
|
} catch (error) {
|
|
593
600
|
console.error('Notification error:', error);
|
|
594
601
|
return json(
|
|
@@ -19,9 +19,16 @@ export const GET: RequestHandler = async ({ request, url }) => {
|
|
|
19
19
|
|
|
20
20
|
// Use a configured server URL from env when available (trusted), otherwise
|
|
21
21
|
// fall back to url.origin (derived from the incoming request URL by SvelteKit).
|
|
22
|
-
|
|
22
|
+
// Strip any trailing slash so registerUrl never becomes "https://x//api/...".
|
|
23
|
+
const serverUrl = (env.ORIGIN?.trim() || url.origin).replace(/\/+$/, '');
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
// registerUrl lets a scanning app POST its push token straight to the
|
|
26
|
+
// device registry after pairing (multi-device auto-registration).
|
|
27
|
+
const configPayload = JSON.stringify({
|
|
28
|
+
apiKey,
|
|
29
|
+
registerUrl: `${serverUrl}/api/device-token`,
|
|
30
|
+
serverUrl,
|
|
31
|
+
});
|
|
25
32
|
|
|
26
33
|
try {
|
|
27
34
|
const dataUrl = await QRCode.toDataURL(configPayload, {
|