@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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { s as shooterDataDir } from './shooter-home-4f_HkdGI.js';
|
|
6
|
+
|
|
7
|
+
function isDeviceRecord(v) {
|
|
8
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const r = v;
|
|
12
|
+
const isNullableString = (x) => x === null || typeof x === "string";
|
|
13
|
+
return typeof r.id === "string" && typeof r.token === "string" && (r.platform === "ios" || r.platform === "android") && (r.appEnv === "sandbox" || r.appEnv === "production") && typeof r.registeredAt === "string" && typeof r.lastSeenAt === "string" && typeof r.isActive === "boolean" && Number.isInteger(r.failureCount) && r.failureCount >= 0 && isNullableString(r.bundleId) && isNullableString(r.deviceId) && isNullableString(r.friendlyName);
|
|
14
|
+
}
|
|
15
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
16
|
+
const MAX_TOKEN_LENGTH = 512;
|
|
17
|
+
const MAX_DEVICE_ID_LENGTH = 256;
|
|
18
|
+
const MAX_NAME_LENGTH = 256;
|
|
19
|
+
const MAX_BUNDLE_ID_LENGTH = 256;
|
|
20
|
+
class DeviceTokenStore {
|
|
21
|
+
dataDir;
|
|
22
|
+
db;
|
|
23
|
+
constructor(dataDir = shooterDataDir()) {
|
|
24
|
+
this.dataDir = dataDir;
|
|
25
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
26
|
+
this.db = new Database(path.join(dataDir, "shooter.db"));
|
|
27
|
+
this.db.pragma("journal_mode = WAL");
|
|
28
|
+
this.db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS device_tokens (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
token TEXT NOT NULL,
|
|
32
|
+
platform TEXT NOT NULL CHECK(platform IN ('ios', 'android')),
|
|
33
|
+
app_env TEXT NOT NULL DEFAULT 'sandbox'
|
|
34
|
+
CHECK(app_env IN ('sandbox', 'production')),
|
|
35
|
+
device_id TEXT,
|
|
36
|
+
friendly_name TEXT,
|
|
37
|
+
bundle_id TEXT,
|
|
38
|
+
registered_at TEXT NOT NULL,
|
|
39
|
+
last_seen_at TEXT NOT NULL,
|
|
40
|
+
failure_count INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
42
|
+
UNIQUE(token),
|
|
43
|
+
UNIQUE(device_id, platform)
|
|
44
|
+
);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_device_tokens_active
|
|
46
|
+
ON device_tokens(platform, is_active);
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
close() {
|
|
50
|
+
this.db.close();
|
|
51
|
+
}
|
|
52
|
+
/** Soft-delete a device by id (sets is_active = 0; auditable until cleanup). */
|
|
53
|
+
deleteById(id) {
|
|
54
|
+
return this.db.prepare("UPDATE device_tokens SET is_active = 0 WHERE id = ?").run(id).changes;
|
|
55
|
+
}
|
|
56
|
+
getByToken(token) {
|
|
57
|
+
const row = this.db.prepare("SELECT * FROM device_tokens WHERE token = ?").get(token);
|
|
58
|
+
return row ? rowToRecord(row) : null;
|
|
59
|
+
}
|
|
60
|
+
/** All active tokens for a platform, most-recently-seen first. */
|
|
61
|
+
listActive(platform) {
|
|
62
|
+
const rows = this.db.prepare(
|
|
63
|
+
"SELECT * FROM device_tokens WHERE platform = ? AND is_active = 1 ORDER BY last_seen_at DESC"
|
|
64
|
+
).all(platform);
|
|
65
|
+
return rows.map(rowToRecord);
|
|
66
|
+
}
|
|
67
|
+
/** Active tokens for a platform filtered by APNs gateway env. */
|
|
68
|
+
listActiveForEnv(platform, appEnv) {
|
|
69
|
+
const rows = this.db.prepare(
|
|
70
|
+
`SELECT * FROM device_tokens
|
|
71
|
+
WHERE platform = ? AND app_env = ? AND is_active = 1
|
|
72
|
+
ORDER BY last_seen_at DESC`
|
|
73
|
+
).all(platform, appEnv);
|
|
74
|
+
return rows.map(rowToRecord);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Import tokens from the legacy device-tokens.json and the setup-wizard
|
|
78
|
+
* device-token-seeds.json, then rename the consumed files. Idempotent: the
|
|
79
|
+
* rename prevents re-import, and INSERT OR IGNORE prevents UNIQUE(token) dups.
|
|
80
|
+
*/
|
|
81
|
+
migrate(now = /* @__PURE__ */ new Date()) {
|
|
82
|
+
this.importFile(path.join(this.dataDir, "device-tokens.json"), ".migrated", now);
|
|
83
|
+
this.importFile(path.join(this.dataDir, "device-token-seeds.json"), ".processed", now);
|
|
84
|
+
}
|
|
85
|
+
/** Soft-delete a set of dead tokens (lazy pruning after a failed delivery). */
|
|
86
|
+
pruneByTokens(tokens) {
|
|
87
|
+
if (tokens.length === 0) {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
const placeholders = tokens.map(() => "?").join(", ");
|
|
91
|
+
return this.db.prepare(`UPDATE device_tokens SET is_active = 0 WHERE token IN (${placeholders})`).run(...tokens).changes;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Hard-delete inactive rows whose last_seen_at is older than 30 days.
|
|
95
|
+
* Active rows are NEVER deleted by age — a device that is still on but
|
|
96
|
+
* hasn't re-registered should keep receiving notifications.
|
|
97
|
+
*/
|
|
98
|
+
startupCleanup(now = /* @__PURE__ */ new Date()) {
|
|
99
|
+
const cutoff = new Date(now.getTime() - THIRTY_DAYS_MS).toISOString();
|
|
100
|
+
return this.db.prepare("DELETE FROM device_tokens WHERE is_active = 0 AND last_seen_at < ?").run(cutoff).changes;
|
|
101
|
+
}
|
|
102
|
+
/** Bump last_seen_at for tokens that just delivered successfully. */
|
|
103
|
+
touchLastSeen(tokens, now = /* @__PURE__ */ new Date()) {
|
|
104
|
+
if (tokens.length === 0) {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
const placeholders = tokens.map(() => "?").join(", ");
|
|
108
|
+
return this.db.prepare(`UPDATE device_tokens SET last_seen_at = ? WHERE token IN (${placeholders})`).run(now.toISOString(), ...tokens).changes;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Register or refresh a device. Three cases, resolved in one transaction:
|
|
112
|
+
* 1. Known device (deviceId matches an existing row) → rotate token in place.
|
|
113
|
+
* If the incoming token is currently held by a DIFFERENT row, APNs has
|
|
114
|
+
* recycled it to us, so that stale row is hard-deleted first — otherwise
|
|
115
|
+
* the rotation UPDATE would violate UNIQUE(token). (A soft-delete would
|
|
116
|
+
* NOT free the constraint: the token value stays in the inactive row.)
|
|
117
|
+
* 2. New legacy device (no deviceId) → deactivate prior null-device rows for
|
|
118
|
+
* the platform (can't tell them apart) then insert/refresh by token.
|
|
119
|
+
* 3. New device (deviceId not yet known) → insert; on token conflict, the
|
|
120
|
+
* ownership WHERE guard gates the WHOLE update: an unknown device cannot
|
|
121
|
+
* steal OR clobber the metadata of a token already owned by another active
|
|
122
|
+
* device (the conflict resolves to a no-op, leaving that row untouched).
|
|
123
|
+
*/
|
|
124
|
+
upsert(input, now = /* @__PURE__ */ new Date()) {
|
|
125
|
+
const ts = now.toISOString();
|
|
126
|
+
const appEnv = input.appEnv ?? (process.env.APNS_PRODUCTION === "true" ? "production" : "sandbox");
|
|
127
|
+
const deviceId = input.deviceId ?? null;
|
|
128
|
+
const friendlyName = input.friendlyName ?? null;
|
|
129
|
+
const bundleId = input.bundleId ?? null;
|
|
130
|
+
const { platform } = input;
|
|
131
|
+
const token = input.token.trim();
|
|
132
|
+
if (token.length === 0) {
|
|
133
|
+
throw new Error("DeviceTokenStore.upsert: token must be a non-empty string");
|
|
134
|
+
}
|
|
135
|
+
if (token.length > MAX_TOKEN_LENGTH) {
|
|
136
|
+
throw new Error(`DeviceTokenStore.upsert: token exceeds ${MAX_TOKEN_LENGTH} chars`);
|
|
137
|
+
}
|
|
138
|
+
if (deviceId !== null && deviceId.length > MAX_DEVICE_ID_LENGTH) {
|
|
139
|
+
throw new Error(`DeviceTokenStore.upsert: deviceId exceeds ${MAX_DEVICE_ID_LENGTH} chars`);
|
|
140
|
+
}
|
|
141
|
+
if (friendlyName !== null && friendlyName.length > MAX_NAME_LENGTH) {
|
|
142
|
+
throw new Error(`DeviceTokenStore.upsert: friendlyName exceeds ${MAX_NAME_LENGTH} chars`);
|
|
143
|
+
}
|
|
144
|
+
if (bundleId !== null && bundleId.length > MAX_BUNDLE_ID_LENGTH) {
|
|
145
|
+
throw new Error(`DeviceTokenStore.upsert: bundleId exceeds ${MAX_BUNDLE_ID_LENGTH} chars`);
|
|
146
|
+
}
|
|
147
|
+
this.db.transaction(() => {
|
|
148
|
+
if (deviceId !== null) {
|
|
149
|
+
const ownRow = this.db.prepare("SELECT id FROM device_tokens WHERE device_id = ? AND platform = ?").get(deviceId, platform);
|
|
150
|
+
if (ownRow) {
|
|
151
|
+
this.db.prepare("DELETE FROM device_tokens WHERE token = ? AND id != ?").run(token, ownRow.id);
|
|
152
|
+
this.db.prepare(
|
|
153
|
+
`UPDATE device_tokens SET
|
|
154
|
+
token = ?,
|
|
155
|
+
last_seen_at = ?,
|
|
156
|
+
app_env = ?,
|
|
157
|
+
failure_count = 0,
|
|
158
|
+
is_active = 1,
|
|
159
|
+
friendly_name = COALESCE(?, friendly_name),
|
|
160
|
+
bundle_id = COALESCE(?, bundle_id)
|
|
161
|
+
WHERE id = ?`
|
|
162
|
+
).run(token, ts, appEnv, friendlyName, bundleId, ownRow.id);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
this.db.prepare(
|
|
167
|
+
`UPDATE device_tokens SET is_active = 0
|
|
168
|
+
WHERE platform = ? AND device_id IS NULL AND token != ? AND is_active = 1`
|
|
169
|
+
).run(platform, token);
|
|
170
|
+
}
|
|
171
|
+
this.db.prepare(
|
|
172
|
+
`INSERT INTO device_tokens
|
|
173
|
+
(id, token, platform, app_env, device_id, friendly_name, bundle_id,
|
|
174
|
+
registered_at, last_seen_at, failure_count, is_active)
|
|
175
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1)
|
|
176
|
+
ON CONFLICT(token) DO UPDATE SET
|
|
177
|
+
last_seen_at = excluded.last_seen_at,
|
|
178
|
+
app_env = excluded.app_env,
|
|
179
|
+
failure_count = 0,
|
|
180
|
+
is_active = 1,
|
|
181
|
+
friendly_name = COALESCE(excluded.friendly_name, device_tokens.friendly_name),
|
|
182
|
+
bundle_id = COALESCE(excluded.bundle_id, device_tokens.bundle_id),
|
|
183
|
+
device_id = excluded.device_id
|
|
184
|
+
WHERE device_tokens.device_id IS NULL
|
|
185
|
+
OR device_tokens.device_id = excluded.device_id
|
|
186
|
+
OR device_tokens.is_active = 0`
|
|
187
|
+
).run(randomUUID(), token, platform, appEnv, deviceId, friendlyName, bundleId, ts, ts);
|
|
188
|
+
})();
|
|
189
|
+
const rec = this.getByToken(token);
|
|
190
|
+
if (!rec) {
|
|
191
|
+
throw new Error("DeviceTokenStore.upsert: row not found after write");
|
|
192
|
+
}
|
|
193
|
+
return rec;
|
|
194
|
+
}
|
|
195
|
+
importFile(filePath, renameSuffix, now) {
|
|
196
|
+
if (!fs.existsSync(filePath)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
201
|
+
this.importTokenMap(raw, now);
|
|
202
|
+
fs.renameSync(filePath, filePath + renameSuffix);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.warn(`[device-token-store] failed to import ${filePath}:`, err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
importTokenMap(raw, now) {
|
|
208
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const obj = raw;
|
|
212
|
+
const ts = now.toISOString();
|
|
213
|
+
const appEnv = process.env.APNS_PRODUCTION === "true" ? "production" : "sandbox";
|
|
214
|
+
const insert = this.db.prepare(
|
|
215
|
+
`INSERT OR IGNORE INTO device_tokens
|
|
216
|
+
(id, token, platform, app_env, device_id, friendly_name, bundle_id,
|
|
217
|
+
registered_at, last_seen_at, failure_count, is_active)
|
|
218
|
+
VALUES (?, ?, ?, ?, NULL, NULL, NULL, ?, ?, 0, 1)`
|
|
219
|
+
);
|
|
220
|
+
for (const platform of ["ios", "android"]) {
|
|
221
|
+
const value = obj[platform];
|
|
222
|
+
const tokens = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
223
|
+
for (const tok of tokens) {
|
|
224
|
+
if (typeof tok !== "string") {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const normalized = tok.trim();
|
|
228
|
+
if (normalized.length > 0) {
|
|
229
|
+
insert.run(randomUUID(), normalized, platform, appEnv, ts, ts);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function rowToRecord(row) {
|
|
236
|
+
const rec = {
|
|
237
|
+
appEnv: row.app_env,
|
|
238
|
+
bundleId: row.bundle_id ?? null,
|
|
239
|
+
deviceId: row.device_id ?? null,
|
|
240
|
+
failureCount: row.failure_count,
|
|
241
|
+
friendlyName: row.friendly_name ?? null,
|
|
242
|
+
id: row.id,
|
|
243
|
+
isActive: row.is_active === 1,
|
|
244
|
+
lastSeenAt: row.last_seen_at,
|
|
245
|
+
platform: row.platform,
|
|
246
|
+
registeredAt: row.registered_at,
|
|
247
|
+
token: row.token
|
|
248
|
+
};
|
|
249
|
+
if (!isDeviceRecord(rec)) {
|
|
250
|
+
throw new Error(`DeviceTokenStore: row failed DeviceRecord validation (id=${String(row.id)})`);
|
|
251
|
+
}
|
|
252
|
+
return rec;
|
|
253
|
+
}
|
|
254
|
+
const DTS_GLOBAL_KEY = "__shooter_device_token_store";
|
|
255
|
+
const deviceTokenStore = globalThis[DTS_GLOBAL_KEY] || new DeviceTokenStore();
|
|
256
|
+
globalThis[DTS_GLOBAL_KEY] = deviceTokenStore;
|
|
257
|
+
|
|
258
|
+
export { MAX_TOKEN_LENGTH as M, MAX_DEVICE_ID_LENGTH as a, MAX_NAME_LENGTH as b, MAX_BUNDLE_ID_LENGTH as c, deviceTokenStore as d };
|
|
259
|
+
//# sourceMappingURL=device-token-store-Ct7aTeR8.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-token-store-Ct7aTeR8.js","sources":["../../../.svelte-kit/adapter-node/chunks/device-token-store.js"],"sourcesContent":["import Database from \"better-sqlite3\";\nimport { randomUUID } from \"crypto\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { s as shooterDataDir } from \"./shooter-home.js\";\nfunction isDeviceRecord(v) {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) {\n return false;\n }\n const r = v;\n const isNullableString = (x) => x === null || typeof x === \"string\";\n return typeof r.id === \"string\" && typeof r.token === \"string\" && (r.platform === \"ios\" || r.platform === \"android\") && (r.appEnv === \"sandbox\" || r.appEnv === \"production\") && typeof r.registeredAt === \"string\" && typeof r.lastSeenAt === \"string\" && typeof r.isActive === \"boolean\" && Number.isInteger(r.failureCount) && r.failureCount >= 0 && isNullableString(r.bundleId) && isNullableString(r.deviceId) && isNullableString(r.friendlyName);\n}\nconst THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;\nconst MAX_TOKEN_LENGTH = 512;\nconst MAX_DEVICE_ID_LENGTH = 256;\nconst MAX_NAME_LENGTH = 256;\nconst MAX_BUNDLE_ID_LENGTH = 256;\nclass DeviceTokenStore {\n dataDir;\n db;\n constructor(dataDir = shooterDataDir()) {\n this.dataDir = dataDir;\n fs.mkdirSync(dataDir, { recursive: true });\n this.db = new Database(path.join(dataDir, \"shooter.db\"));\n this.db.pragma(\"journal_mode = WAL\");\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS device_tokens (\n id TEXT PRIMARY KEY,\n token TEXT NOT NULL,\n platform TEXT NOT NULL CHECK(platform IN ('ios', 'android')),\n app_env TEXT NOT NULL DEFAULT 'sandbox'\n CHECK(app_env IN ('sandbox', 'production')),\n device_id TEXT,\n friendly_name TEXT,\n bundle_id TEXT,\n registered_at TEXT NOT NULL,\n last_seen_at TEXT NOT NULL,\n failure_count INTEGER NOT NULL DEFAULT 0,\n is_active INTEGER NOT NULL DEFAULT 1,\n UNIQUE(token),\n UNIQUE(device_id, platform)\n );\n CREATE INDEX IF NOT EXISTS idx_device_tokens_active\n ON device_tokens(platform, is_active);\n `);\n }\n close() {\n this.db.close();\n }\n /** Soft-delete a device by id (sets is_active = 0; auditable until cleanup). */\n deleteById(id) {\n return this.db.prepare(\"UPDATE device_tokens SET is_active = 0 WHERE id = ?\").run(id).changes;\n }\n getByToken(token) {\n const row = this.db.prepare(\"SELECT * FROM device_tokens WHERE token = ?\").get(token);\n return row ? rowToRecord(row) : null;\n }\n /** All active tokens for a platform, most-recently-seen first. */\n listActive(platform) {\n const rows = this.db.prepare(\n \"SELECT * FROM device_tokens WHERE platform = ? AND is_active = 1 ORDER BY last_seen_at DESC\"\n ).all(platform);\n return rows.map(rowToRecord);\n }\n /** Active tokens for a platform filtered by APNs gateway env. */\n listActiveForEnv(platform, appEnv) {\n const rows = this.db.prepare(\n `SELECT * FROM device_tokens\n WHERE platform = ? AND app_env = ? AND is_active = 1\n ORDER BY last_seen_at DESC`\n ).all(platform, appEnv);\n return rows.map(rowToRecord);\n }\n /**\n * Import tokens from the legacy device-tokens.json and the setup-wizard\n * device-token-seeds.json, then rename the consumed files. Idempotent: the\n * rename prevents re-import, and INSERT OR IGNORE prevents UNIQUE(token) dups.\n */\n migrate(now = /* @__PURE__ */ new Date()) {\n this.importFile(path.join(this.dataDir, \"device-tokens.json\"), \".migrated\", now);\n this.importFile(path.join(this.dataDir, \"device-token-seeds.json\"), \".processed\", now);\n }\n /** Soft-delete a set of dead tokens (lazy pruning after a failed delivery). */\n pruneByTokens(tokens) {\n if (tokens.length === 0) {\n return 0;\n }\n const placeholders = tokens.map(() => \"?\").join(\", \");\n return this.db.prepare(`UPDATE device_tokens SET is_active = 0 WHERE token IN (${placeholders})`).run(...tokens).changes;\n }\n /**\n * Hard-delete inactive rows whose last_seen_at is older than 30 days.\n * Active rows are NEVER deleted by age — a device that is still on but\n * hasn't re-registered should keep receiving notifications.\n */\n startupCleanup(now = /* @__PURE__ */ new Date()) {\n const cutoff = new Date(now.getTime() - THIRTY_DAYS_MS).toISOString();\n return this.db.prepare(\"DELETE FROM device_tokens WHERE is_active = 0 AND last_seen_at < ?\").run(cutoff).changes;\n }\n /** Bump last_seen_at for tokens that just delivered successfully. */\n touchLastSeen(tokens, now = /* @__PURE__ */ new Date()) {\n if (tokens.length === 0) {\n return 0;\n }\n const placeholders = tokens.map(() => \"?\").join(\", \");\n return this.db.prepare(`UPDATE device_tokens SET last_seen_at = ? WHERE token IN (${placeholders})`).run(now.toISOString(), ...tokens).changes;\n }\n /**\n * Register or refresh a device. Three cases, resolved in one transaction:\n * 1. Known device (deviceId matches an existing row) → rotate token in place.\n * If the incoming token is currently held by a DIFFERENT row, APNs has\n * recycled it to us, so that stale row is hard-deleted first — otherwise\n * the rotation UPDATE would violate UNIQUE(token). (A soft-delete would\n * NOT free the constraint: the token value stays in the inactive row.)\n * 2. New legacy device (no deviceId) → deactivate prior null-device rows for\n * the platform (can't tell them apart) then insert/refresh by token.\n * 3. New device (deviceId not yet known) → insert; on token conflict, the\n * ownership WHERE guard gates the WHOLE update: an unknown device cannot\n * steal OR clobber the metadata of a token already owned by another active\n * device (the conflict resolves to a no-op, leaving that row untouched).\n */\n upsert(input, now = /* @__PURE__ */ new Date()) {\n const ts = now.toISOString();\n const appEnv = input.appEnv ?? (process.env.APNS_PRODUCTION === \"true\" ? \"production\" : \"sandbox\");\n const deviceId = input.deviceId ?? null;\n const friendlyName = input.friendlyName ?? null;\n const bundleId = input.bundleId ?? null;\n const { platform } = input;\n const token = input.token.trim();\n if (token.length === 0) {\n throw new Error(\"DeviceTokenStore.upsert: token must be a non-empty string\");\n }\n if (token.length > MAX_TOKEN_LENGTH) {\n throw new Error(`DeviceTokenStore.upsert: token exceeds ${MAX_TOKEN_LENGTH} chars`);\n }\n if (deviceId !== null && deviceId.length > MAX_DEVICE_ID_LENGTH) {\n throw new Error(`DeviceTokenStore.upsert: deviceId exceeds ${MAX_DEVICE_ID_LENGTH} chars`);\n }\n if (friendlyName !== null && friendlyName.length > MAX_NAME_LENGTH) {\n throw new Error(`DeviceTokenStore.upsert: friendlyName exceeds ${MAX_NAME_LENGTH} chars`);\n }\n if (bundleId !== null && bundleId.length > MAX_BUNDLE_ID_LENGTH) {\n throw new Error(`DeviceTokenStore.upsert: bundleId exceeds ${MAX_BUNDLE_ID_LENGTH} chars`);\n }\n this.db.transaction(() => {\n if (deviceId !== null) {\n const ownRow = this.db.prepare(\"SELECT id FROM device_tokens WHERE device_id = ? AND platform = ?\").get(deviceId, platform);\n if (ownRow) {\n this.db.prepare(\"DELETE FROM device_tokens WHERE token = ? AND id != ?\").run(token, ownRow.id);\n this.db.prepare(\n `UPDATE device_tokens SET\n token = ?,\n last_seen_at = ?,\n app_env = ?,\n failure_count = 0,\n is_active = 1,\n friendly_name = COALESCE(?, friendly_name),\n bundle_id = COALESCE(?, bundle_id)\n WHERE id = ?`\n ).run(token, ts, appEnv, friendlyName, bundleId, ownRow.id);\n return;\n }\n } else {\n this.db.prepare(\n `UPDATE device_tokens SET is_active = 0\n WHERE platform = ? AND device_id IS NULL AND token != ? AND is_active = 1`\n ).run(platform, token);\n }\n this.db.prepare(\n `INSERT INTO device_tokens\n (id, token, platform, app_env, device_id, friendly_name, bundle_id,\n registered_at, last_seen_at, failure_count, is_active)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1)\n ON CONFLICT(token) DO UPDATE SET\n last_seen_at = excluded.last_seen_at,\n app_env = excluded.app_env,\n failure_count = 0,\n is_active = 1,\n friendly_name = COALESCE(excluded.friendly_name, device_tokens.friendly_name),\n bundle_id = COALESCE(excluded.bundle_id, device_tokens.bundle_id),\n device_id = excluded.device_id\n WHERE device_tokens.device_id IS NULL\n OR device_tokens.device_id = excluded.device_id\n OR device_tokens.is_active = 0`\n ).run(randomUUID(), token, platform, appEnv, deviceId, friendlyName, bundleId, ts, ts);\n })();\n const rec = this.getByToken(token);\n if (!rec) {\n throw new Error(\"DeviceTokenStore.upsert: row not found after write\");\n }\n return rec;\n }\n importFile(filePath, renameSuffix, now) {\n if (!fs.existsSync(filePath)) {\n return;\n }\n try {\n const raw = JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n this.importTokenMap(raw, now);\n fs.renameSync(filePath, filePath + renameSuffix);\n } catch (err) {\n console.warn(`[device-token-store] failed to import ${filePath}:`, err);\n }\n }\n importTokenMap(raw, now) {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n return;\n }\n const obj = raw;\n const ts = now.toISOString();\n const appEnv = process.env.APNS_PRODUCTION === \"true\" ? \"production\" : \"sandbox\";\n const insert = this.db.prepare(\n `INSERT OR IGNORE INTO device_tokens\n (id, token, platform, app_env, device_id, friendly_name, bundle_id,\n registered_at, last_seen_at, failure_count, is_active)\n VALUES (?, ?, ?, ?, NULL, NULL, NULL, ?, ?, 0, 1)`\n );\n for (const platform of [\"ios\", \"android\"]) {\n const value = obj[platform];\n const tokens = Array.isArray(value) ? value : typeof value === \"string\" ? [value] : [];\n for (const tok of tokens) {\n if (typeof tok !== \"string\") {\n continue;\n }\n const normalized = tok.trim();\n if (normalized.length > 0) {\n insert.run(randomUUID(), normalized, platform, appEnv, ts, ts);\n }\n }\n }\n }\n}\nfunction rowToRecord(row) {\n const rec = {\n appEnv: row.app_env,\n bundleId: row.bundle_id ?? null,\n deviceId: row.device_id ?? null,\n failureCount: row.failure_count,\n friendlyName: row.friendly_name ?? null,\n id: row.id,\n isActive: row.is_active === 1,\n lastSeenAt: row.last_seen_at,\n platform: row.platform,\n registeredAt: row.registered_at,\n token: row.token\n };\n if (!isDeviceRecord(rec)) {\n throw new Error(`DeviceTokenStore: row failed DeviceRecord validation (id=${String(row.id)})`);\n }\n return rec;\n}\nconst DTS_GLOBAL_KEY = \"__shooter_device_token_store\";\nconst deviceTokenStore = globalThis[DTS_GLOBAL_KEY] || new DeviceTokenStore();\nglobalThis[DTS_GLOBAL_KEY] = deviceTokenStore;\nexport {\n MAX_TOKEN_LENGTH as M,\n MAX_DEVICE_ID_LENGTH as a,\n MAX_NAME_LENGTH as b,\n MAX_BUNDLE_ID_LENGTH as c,\n deviceTokenStore as d\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,60 @@ import jwt from 'jsonwebtoken';
|
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { t as toErrorMessage } from './error-DDXB3duW.js';
|
|
6
6
|
|
|
7
|
+
const UNCONDITIONAL_STALE_REASONS = /* @__PURE__ */ new Set([
|
|
8
|
+
"DeviceTokenNotForTopic",
|
|
9
|
+
// token belongs to a different bundle id
|
|
10
|
+
"Unregistered"
|
|
11
|
+
// 410 — guarded by the re-registration timestamp check below
|
|
12
|
+
]);
|
|
13
|
+
function classifyApnsReason(httpStatus, reason, storedAppEnv, serverAppEnv) {
|
|
14
|
+
if (httpStatus === 200) {
|
|
15
|
+
return "sent";
|
|
16
|
+
}
|
|
17
|
+
if (reason === "TopicDisallowed") {
|
|
18
|
+
return "transient_error";
|
|
19
|
+
}
|
|
20
|
+
if (reason === "BadDeviceToken") {
|
|
21
|
+
return storedAppEnv === serverAppEnv ? "stale_token" : "transient_error";
|
|
22
|
+
}
|
|
23
|
+
if (reason && UNCONDITIONAL_STALE_REASONS.has(reason)) {
|
|
24
|
+
return "stale_token";
|
|
25
|
+
}
|
|
26
|
+
return "transient_error";
|
|
27
|
+
}
|
|
28
|
+
function summarizeApnsFanOut(outcomes, serverAppEnv) {
|
|
29
|
+
const results = [];
|
|
30
|
+
const staleTokens = [];
|
|
31
|
+
let totalSent = 0;
|
|
32
|
+
let totalFailed = 0;
|
|
33
|
+
for (const o of outcomes) {
|
|
34
|
+
let disposition = classifyApnsReason(o.httpStatus, o.reason, o.appEnv, serverAppEnv);
|
|
35
|
+
if (disposition === "stale_token" && o.reason === "Unregistered" && o.timestampMs > 0) {
|
|
36
|
+
const registeredMs = Date.parse(o.registeredAt);
|
|
37
|
+
if (!Number.isNaN(registeredMs) && registeredMs > o.timestampMs) {
|
|
38
|
+
disposition = "transient_error";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const success = o.httpStatus === 200;
|
|
42
|
+
if (success) {
|
|
43
|
+
totalSent += 1;
|
|
44
|
+
} else {
|
|
45
|
+
totalFailed += 1;
|
|
46
|
+
}
|
|
47
|
+
if (disposition === "stale_token") {
|
|
48
|
+
staleTokens.push(o.token);
|
|
49
|
+
}
|
|
50
|
+
results.push({
|
|
51
|
+
disposition,
|
|
52
|
+
httpStatus: o.httpStatus,
|
|
53
|
+
reason: o.reason,
|
|
54
|
+
success,
|
|
55
|
+
timestampMs: o.timestampMs,
|
|
56
|
+
token: o.token
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { results, staleTokens, totalFailed, totalSent };
|
|
60
|
+
}
|
|
7
61
|
const APNS_MAX_BYTES = 3900;
|
|
8
62
|
const ELLIPSIS = "…";
|
|
9
63
|
function fitApnsPayload(body, maxBytes = APNS_MAX_BYTES) {
|
|
@@ -40,6 +94,7 @@ const APNS_HOST_PROD = "api.push.apple.com";
|
|
|
40
94
|
const APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
41
95
|
const JWT_REFRESH_INTERVAL_MS = 30 * 60 * 1e3;
|
|
42
96
|
const REQUEST_TIMEOUT_SECONDS = 15;
|
|
97
|
+
const MAX_APNS_CONCURRENCY = 20;
|
|
43
98
|
class LibraryAPNsService {
|
|
44
99
|
bundleId;
|
|
45
100
|
cachedJwt = null;
|
|
@@ -94,24 +149,7 @@ class LibraryAPNsService {
|
|
|
94
149
|
if (!deviceToken || !payload) {
|
|
95
150
|
throw new Error("Device token and payload are required");
|
|
96
151
|
}
|
|
97
|
-
|
|
98
|
-
alert: {
|
|
99
|
-
body: payload.body ?? payload.message ?? "",
|
|
100
|
-
title: payload.title,
|
|
101
|
-
...payload.subtitle ? { subtitle: payload.subtitle } : {}
|
|
102
|
-
},
|
|
103
|
-
badge: payload.badge ?? 1,
|
|
104
|
-
sound: payload.sound ?? "default"
|
|
105
|
-
};
|
|
106
|
-
if (payload.category) {
|
|
107
|
-
aps.category = payload.category;
|
|
108
|
-
}
|
|
109
|
-
const body = { aps };
|
|
110
|
-
if (payload.data) {
|
|
111
|
-
const { aps: _ignoredAps, ...customData } = payload.data;
|
|
112
|
-
Object.assign(body, customData);
|
|
113
|
-
}
|
|
114
|
-
return this.deliver(deviceToken, body, "alert", "10");
|
|
152
|
+
return this.deliver(deviceToken, this.buildAlertBody(payload), "alert", "10");
|
|
115
153
|
}
|
|
116
154
|
/**
|
|
117
155
|
* Send a SILENT (content-available) background push to wake a backgrounded app without
|
|
@@ -133,13 +171,85 @@ class LibraryAPNsService {
|
|
|
133
171
|
}
|
|
134
172
|
return this.deliver(deviceToken, body, "background", "5");
|
|
135
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Fan out one alert push to many devices concurrently. Fits + serializes the
|
|
176
|
+
* payload once and captures the JWT once (so no call uses a mid-fan-out
|
|
177
|
+
* expired token), then classifies/aggregates results and returns the set of
|
|
178
|
+
* stale tokens for the caller to prune.
|
|
179
|
+
*/
|
|
180
|
+
async sendToMany(devices, payload, collapseId) {
|
|
181
|
+
if (!this.configured) {
|
|
182
|
+
throw new Error("APNs service not configured properly");
|
|
183
|
+
}
|
|
184
|
+
if (devices.length === 0) {
|
|
185
|
+
return { results: [], staleTokens: [], totalFailed: 0, totalSent: 0 };
|
|
186
|
+
}
|
|
187
|
+
const serverAppEnv = this.host === APNS_HOST_PROD ? "production" : "sandbox";
|
|
188
|
+
const bodyJson = JSON.stringify(fitApnsPayload(this.buildAlertBody(payload)));
|
|
189
|
+
const jwtToken = this.getJwt();
|
|
190
|
+
const outcomes = await mapLimit(
|
|
191
|
+
devices,
|
|
192
|
+
MAX_APNS_CONCURRENCY,
|
|
193
|
+
async (device) => {
|
|
194
|
+
const res = await this.deliverPreSerialized(
|
|
195
|
+
device.token,
|
|
196
|
+
bodyJson,
|
|
197
|
+
"alert",
|
|
198
|
+
"10",
|
|
199
|
+
jwtToken,
|
|
200
|
+
collapseId
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
appEnv: device.appEnv,
|
|
204
|
+
httpStatus: res.httpStatus ?? 0,
|
|
205
|
+
reason: res.error ?? null,
|
|
206
|
+
registeredAt: device.registeredAt,
|
|
207
|
+
timestampMs: res.timestampMs ?? 0,
|
|
208
|
+
token: device.token
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
return summarizeApnsFanOut(outcomes, serverAppEnv);
|
|
213
|
+
}
|
|
136
214
|
shutdown() {
|
|
137
215
|
}
|
|
138
|
-
/**
|
|
216
|
+
/** Build the APNs `{ aps, ...customData }` body for an alert push. */
|
|
217
|
+
buildAlertBody(payload) {
|
|
218
|
+
const aps = {
|
|
219
|
+
alert: {
|
|
220
|
+
body: payload.body ?? payload.message ?? "",
|
|
221
|
+
title: payload.title,
|
|
222
|
+
...payload.subtitle ? { subtitle: payload.subtitle } : {}
|
|
223
|
+
},
|
|
224
|
+
badge: payload.badge ?? 1,
|
|
225
|
+
sound: payload.sound ?? "default"
|
|
226
|
+
};
|
|
227
|
+
if (payload.category) {
|
|
228
|
+
aps.category = payload.category;
|
|
229
|
+
}
|
|
230
|
+
const body = { aps };
|
|
231
|
+
if (payload.data) {
|
|
232
|
+
const { aps: _ignoredAps, ...customData } = payload.data;
|
|
233
|
+
Object.assign(body, customData);
|
|
234
|
+
}
|
|
235
|
+
return body;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Shared curl/HTTP-2 delivery. Fits + serializes the body and signs a JWT,
|
|
239
|
+
* then delegates to deliverPreSerialized. `pushType` is 'alert' | 'background'.
|
|
240
|
+
*/
|
|
139
241
|
async deliver(deviceToken, body, pushType, priority) {
|
|
140
242
|
const bodyJson = JSON.stringify(fitApnsPayload(body));
|
|
141
|
-
|
|
243
|
+
return this.deliverPreSerialized(deviceToken, bodyJson, pushType, priority, this.getJwt());
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Curl/HTTP-2 delivery from a pre-serialized body + pre-captured JWT — the hot
|
|
247
|
+
* path for sendToMany (no per-token fit or JWT sign). Returns httpStatus and,
|
|
248
|
+
* on a 410, the parsed Unregistered timestamp (ms) for the prune guard.
|
|
249
|
+
*/
|
|
250
|
+
async deliverPreSerialized(deviceToken, bodyJson, pushType, priority, jwtToken, collapseId) {
|
|
142
251
|
const url = `https://${this.host}/3/device/${deviceToken}`;
|
|
252
|
+
const safeCollapseId = collapseId ? collapseId.replace(/[^\x20-\x7E]/g, "").slice(0, 64) || void 0 : void 0;
|
|
143
253
|
const args = [
|
|
144
254
|
"-sS",
|
|
145
255
|
"--http2",
|
|
@@ -157,6 +267,7 @@ class LibraryAPNsService {
|
|
|
157
267
|
`apns-push-type: ${pushType}`,
|
|
158
268
|
"-H",
|
|
159
269
|
`apns-priority: ${priority}`,
|
|
270
|
+
...safeCollapseId ? ["-H", `apns-collapse-id: ${safeCollapseId}`] : [],
|
|
160
271
|
"-d",
|
|
161
272
|
bodyJson,
|
|
162
273
|
"-w",
|
|
@@ -172,22 +283,29 @@ class LibraryAPNsService {
|
|
|
172
283
|
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
|
173
284
|
const bodyText = stdout.replace(/\n?__SHOOTER_HTTP_STATUS__:\d+\n?$/, "").trim();
|
|
174
285
|
if (status === 200) {
|
|
175
|
-
return { failed: 0, sent: 1, success: true };
|
|
286
|
+
return { failed: 0, httpStatus: 200, sent: 1, success: true };
|
|
176
287
|
}
|
|
177
288
|
let reason = bodyText;
|
|
289
|
+
let timestampMs;
|
|
178
290
|
try {
|
|
179
291
|
const parsed = JSON.parse(bodyText);
|
|
180
|
-
if (parsed && typeof parsed === "object"
|
|
181
|
-
|
|
292
|
+
if (parsed && typeof parsed === "object") {
|
|
293
|
+
const obj = parsed;
|
|
294
|
+
if (typeof obj.reason === "string") {
|
|
295
|
+
reason = obj.reason;
|
|
296
|
+
}
|
|
297
|
+
if (typeof obj.timestamp === "number") {
|
|
298
|
+
timestampMs = obj.timestamp < 1e12 ? obj.timestamp * 1e3 : obj.timestamp;
|
|
299
|
+
}
|
|
182
300
|
}
|
|
183
301
|
} catch {
|
|
184
302
|
}
|
|
185
303
|
console.error(`[apns] Delivery failed (status=${status}): ${reason}`);
|
|
186
|
-
return { error: reason, failed: 1, sent: 0, success: false };
|
|
304
|
+
return { error: reason, failed: 1, httpStatus: status, sent: 0, success: false, timestampMs };
|
|
187
305
|
} catch (err) {
|
|
188
306
|
const msg = toErrorMessage(err).replace(/bearer\s+[A-Za-z0-9._-]+/gi, "bearer [REDACTED]").replace(/device\/[A-Fa-f0-9]+/g, "device/[REDACTED]");
|
|
189
307
|
console.error(`[apns] curl transport error: ${msg}`);
|
|
190
|
-
return { error: msg, failed: 1, sent: 0, success: false };
|
|
308
|
+
return { error: msg, failed: 1, httpStatus: 0, sent: 0, success: false };
|
|
191
309
|
}
|
|
192
310
|
}
|
|
193
311
|
getJwt() {
|
|
@@ -207,6 +325,20 @@ class LibraryAPNsService {
|
|
|
207
325
|
return token;
|
|
208
326
|
}
|
|
209
327
|
}
|
|
328
|
+
async function mapLimit(items, limit, fn) {
|
|
329
|
+
const results = new Array(items.length);
|
|
330
|
+
let next = 0;
|
|
331
|
+
const worker = async () => {
|
|
332
|
+
while (next < items.length) {
|
|
333
|
+
const idx = next;
|
|
334
|
+
next += 1;
|
|
335
|
+
results[idx] = await fn(items[idx]);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const workerCount = Math.min(limit, items.length);
|
|
339
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
340
|
+
return results;
|
|
341
|
+
}
|
|
210
342
|
|
|
211
343
|
export { LibraryAPNsService as L };
|
|
212
|
-
//# sourceMappingURL=library-apns-
|
|
344
|
+
//# sourceMappingURL=library-apns-DMlL1BAg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library-apns-DMlL1BAg.js","sources":["../../../.svelte-kit/adapter-node/chunks/library-apns.js"],"sourcesContent":["import { b as private_env } from \"./shared-server.js\";\nimport { execFile, execFileSync } from \"child_process\";\nimport jwt from \"jsonwebtoken\";\nimport { promisify } from \"util\";\nimport { t as toErrorMessage } from \"./error.js\";\nconst UNCONDITIONAL_STALE_REASONS = /* @__PURE__ */ new Set([\n \"DeviceTokenNotForTopic\",\n // token belongs to a different bundle id\n \"Unregistered\"\n // 410 — guarded by the re-registration timestamp check below\n]);\nfunction classifyApnsReason(httpStatus, reason, storedAppEnv, serverAppEnv) {\n if (httpStatus === 200) {\n return \"sent\";\n }\n if (reason === \"TopicDisallowed\") {\n return \"transient_error\";\n }\n if (reason === \"BadDeviceToken\") {\n return storedAppEnv === serverAppEnv ? \"stale_token\" : \"transient_error\";\n }\n if (reason && UNCONDITIONAL_STALE_REASONS.has(reason)) {\n return \"stale_token\";\n }\n return \"transient_error\";\n}\nfunction summarizeApnsFanOut(outcomes, serverAppEnv) {\n const results = [];\n const staleTokens = [];\n let totalSent = 0;\n let totalFailed = 0;\n for (const o of outcomes) {\n let disposition = classifyApnsReason(o.httpStatus, o.reason, o.appEnv, serverAppEnv);\n if (disposition === \"stale_token\" && o.reason === \"Unregistered\" && o.timestampMs > 0) {\n const registeredMs = Date.parse(o.registeredAt);\n if (!Number.isNaN(registeredMs) && registeredMs > o.timestampMs) {\n disposition = \"transient_error\";\n }\n }\n const success = o.httpStatus === 200;\n if (success) {\n totalSent += 1;\n } else {\n totalFailed += 1;\n }\n if (disposition === \"stale_token\") {\n staleTokens.push(o.token);\n }\n results.push({\n disposition,\n httpStatus: o.httpStatus,\n reason: o.reason,\n success,\n timestampMs: o.timestampMs,\n token: o.token\n });\n }\n return { results, staleTokens, totalFailed, totalSent };\n}\nconst APNS_MAX_BYTES = 3900;\nconst ELLIPSIS = \"…\";\nfunction fitApnsPayload(body, maxBytes = APNS_MAX_BYTES) {\n if (payloadBytes(body) <= maxBytes) {\n return body;\n }\n const aps = body.aps;\n const alert = aps?.alert;\n if (!alert) {\n return body;\n }\n for (const field of [\"body\", \"subtitle\"]) {\n if (payloadBytes(body) <= maxBytes) {\n break;\n }\n const value = alert[field];\n if (typeof value !== \"string\" || value.length === 0) {\n continue;\n }\n let text = value;\n while (payloadBytes(body) > maxBytes && text.length > 0) {\n const cut = Math.max(1, Math.ceil(text.length * 0.12));\n text = text.slice(0, text.length - cut);\n alert[field] = text.length > 0 ? text + ELLIPSIS : void 0;\n }\n }\n return body;\n}\nfunction payloadBytes(body) {\n return Buffer.byteLength(JSON.stringify(body), \"utf8\");\n}\nconst execFileAsync = promisify(execFile);\nconst APNS_HOST_PROD = \"api.push.apple.com\";\nconst APNS_HOST_SANDBOX = \"api.sandbox.push.apple.com\";\nconst JWT_REFRESH_INTERVAL_MS = 30 * 60 * 1e3;\nconst REQUEST_TIMEOUT_SECONDS = 15;\nconst MAX_APNS_CONCURRENCY = 20;\nclass LibraryAPNsService {\n bundleId;\n cachedJwt = null;\n cachedJwtAt = 0;\n configured = false;\n host = \"\";\n keyId;\n privateKey;\n teamId;\n constructor() {\n this.keyId = private_env.APNS_KEY_ID;\n this.teamId = private_env.APNS_TEAM_ID;\n this.bundleId = private_env.APNS_BUNDLE_ID;\n this.privateKey = private_env.APNS_KEY;\n if (!this.keyId || !this.teamId || !this.bundleId || !this.privateKey) {\n console.error(\n \"[apns] Missing required configuration (APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, or APNS_KEY)\"\n );\n this.configured = false;\n return;\n }\n const production = private_env.APNS_PRODUCTION === \"true\";\n this.host = production ? APNS_HOST_PROD : APNS_HOST_SANDBOX;\n try {\n execFileSync(\"curl\", [\"--version\"], { stdio: \"ignore\" });\n } catch (error) {\n console.error(\n \"[apns] curl binary not found — install curl to enable APNs delivery:\",\n toErrorMessage(error)\n );\n this.configured = false;\n return;\n }\n try {\n this.getJwt();\n this.configured = true;\n console.log(\n `[apns] Provider initialized (${production ? \"production\" : \"sandbox\"} mode, curl transport)`\n );\n } catch (error) {\n console.error(\"[apns] Failed to initialize:\", toErrorMessage(error));\n this.configured = false;\n }\n }\n isConfigured() {\n return this.configured;\n }\n async sendNotification(deviceToken, payload) {\n if (!this.configured) {\n throw new Error(\"APNs service not configured properly\");\n }\n if (!deviceToken || !payload) {\n throw new Error(\"Device token and payload are required\");\n }\n return this.deliver(deviceToken, this.buildAlertBody(payload), \"alert\", \"10\");\n }\n /**\n * Send a SILENT (content-available) background push to wake a backgrounded app without\n * showing an alert — used to wake the phone-resident agent loop so it can run a burst.\n * Uses apns-push-type:background + apns-priority:5 (required by APNs for silent pushes).\n * iOS throttles these (a few per hour); delivery is best-effort.\n */\n async sendSilentNotification(deviceToken, data) {\n if (!this.configured) {\n throw new Error(\"APNs service not configured properly\");\n }\n if (!deviceToken) {\n throw new Error(\"Device token is required\");\n }\n const body = { aps: { \"content-available\": 1 } };\n if (data) {\n const { aps: _ignoredAps, ...customData } = data;\n Object.assign(body, customData);\n }\n return this.deliver(deviceToken, body, \"background\", \"5\");\n }\n /**\n * Fan out one alert push to many devices concurrently. Fits + serializes the\n * payload once and captures the JWT once (so no call uses a mid-fan-out\n * expired token), then classifies/aggregates results and returns the set of\n * stale tokens for the caller to prune.\n */\n async sendToMany(devices, payload, collapseId) {\n if (!this.configured) {\n throw new Error(\"APNs service not configured properly\");\n }\n if (devices.length === 0) {\n return { results: [], staleTokens: [], totalFailed: 0, totalSent: 0 };\n }\n const serverAppEnv = this.host === APNS_HOST_PROD ? \"production\" : \"sandbox\";\n const bodyJson = JSON.stringify(fitApnsPayload(this.buildAlertBody(payload)));\n const jwtToken = this.getJwt();\n const outcomes = await mapLimit(\n devices,\n MAX_APNS_CONCURRENCY,\n async (device) => {\n const res = await this.deliverPreSerialized(\n device.token,\n bodyJson,\n \"alert\",\n \"10\",\n jwtToken,\n collapseId\n );\n return {\n appEnv: device.appEnv,\n httpStatus: res.httpStatus ?? 0,\n reason: res.error ?? null,\n registeredAt: device.registeredAt,\n timestampMs: res.timestampMs ?? 0,\n token: device.token\n };\n }\n );\n return summarizeApnsFanOut(outcomes, serverAppEnv);\n }\n shutdown() {\n }\n /** Build the APNs `{ aps, ...customData }` body for an alert push. */\n buildAlertBody(payload) {\n const aps = {\n alert: {\n body: payload.body ?? payload.message ?? \"\",\n title: payload.title,\n ...payload.subtitle ? { subtitle: payload.subtitle } : {}\n },\n badge: payload.badge ?? 1,\n sound: payload.sound ?? \"default\"\n };\n if (payload.category) {\n aps.category = payload.category;\n }\n const body = { aps };\n if (payload.data) {\n const { aps: _ignoredAps, ...customData } = payload.data;\n Object.assign(body, customData);\n }\n return body;\n }\n /**\n * Shared curl/HTTP-2 delivery. Fits + serializes the body and signs a JWT,\n * then delegates to deliverPreSerialized. `pushType` is 'alert' | 'background'.\n */\n async deliver(deviceToken, body, pushType, priority) {\n const bodyJson = JSON.stringify(fitApnsPayload(body));\n return this.deliverPreSerialized(deviceToken, bodyJson, pushType, priority, this.getJwt());\n }\n /**\n * Curl/HTTP-2 delivery from a pre-serialized body + pre-captured JWT — the hot\n * path for sendToMany (no per-token fit or JWT sign). Returns httpStatus and,\n * on a 410, the parsed Unregistered timestamp (ms) for the prune guard.\n */\n async deliverPreSerialized(deviceToken, bodyJson, pushType, priority, jwtToken, collapseId) {\n const url = `https://${this.host}/3/device/${deviceToken}`;\n const safeCollapseId = collapseId ? collapseId.replace(/[^\\x20-\\x7E]/g, \"\").slice(0, 64) || void 0 : void 0;\n const args = [\n \"-sS\",\n \"--http2\",\n \"--max-time\",\n String(REQUEST_TIMEOUT_SECONDS),\n \"-X\",\n \"POST\",\n \"-H\",\n \"content-type: application/json\",\n \"-H\",\n `apns-topic: ${this.bundleId}`,\n \"-H\",\n `authorization: bearer ${jwtToken}`,\n \"-H\",\n `apns-push-type: ${pushType}`,\n \"-H\",\n `apns-priority: ${priority}`,\n ...safeCollapseId ? [\"-H\", `apns-collapse-id: ${safeCollapseId}`] : [],\n \"-d\",\n bodyJson,\n \"-w\",\n \"\\n__SHOOTER_HTTP_STATUS__:%{http_code}\\n\",\n url\n ];\n try {\n const { stdout } = await execFileAsync(\"curl\", args, {\n maxBuffer: 1024 * 1024,\n timeout: (REQUEST_TIMEOUT_SECONDS + 5) * 1e3\n });\n const statusMatch = /__SHOOTER_HTTP_STATUS__:(\\d+)/.exec(stdout);\n const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;\n const bodyText = stdout.replace(/\\n?__SHOOTER_HTTP_STATUS__:\\d+\\n?$/, \"\").trim();\n if (status === 200) {\n return { failed: 0, httpStatus: 200, sent: 1, success: true };\n }\n let reason = bodyText;\n let timestampMs;\n try {\n const parsed = JSON.parse(bodyText);\n if (parsed && typeof parsed === \"object\") {\n const obj = parsed;\n if (typeof obj.reason === \"string\") {\n reason = obj.reason;\n }\n if (typeof obj.timestamp === \"number\") {\n timestampMs = obj.timestamp < 1e12 ? obj.timestamp * 1e3 : obj.timestamp;\n }\n }\n } catch {\n }\n console.error(`[apns] Delivery failed (status=${status}): ${reason}`);\n return { error: reason, failed: 1, httpStatus: status, sent: 0, success: false, timestampMs };\n } catch (err) {\n const msg = toErrorMessage(err).replace(/bearer\\s+[A-Za-z0-9._-]+/gi, \"bearer [REDACTED]\").replace(/device\\/[A-Fa-f0-9]+/g, \"device/[REDACTED]\");\n console.error(`[apns] curl transport error: ${msg}`);\n return { error: msg, failed: 1, httpStatus: 0, sent: 0, success: false };\n }\n }\n getJwt() {\n const now = Date.now();\n if (this.cachedJwt && now - this.cachedJwtAt < JWT_REFRESH_INTERVAL_MS) {\n return this.cachedJwt;\n }\n if (!this.privateKey || !this.keyId || !this.teamId) {\n throw new Error(\"APNs credentials missing\");\n }\n const token = jwt.sign({ iat: Math.floor(now / 1e3), iss: this.teamId }, this.privateKey, {\n algorithm: \"ES256\",\n header: { alg: \"ES256\", kid: this.keyId }\n });\n this.cachedJwt = token;\n this.cachedJwtAt = now;\n return token;\n }\n}\nasync function mapLimit(items, limit, fn) {\n const results = new Array(items.length);\n let next = 0;\n const worker = async () => {\n while (next < items.length) {\n const idx = next;\n next += 1;\n results[idx] = await fn(items[idx]);\n }\n };\n const workerCount = Math.min(limit, items.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\nexport {\n LibraryAPNsService as L\n};\n"],"names":[],"mappings":"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"}
|
package/build/server/index.js
CHANGED
|
@@ -1351,7 +1351,7 @@ const options = {
|
|
|
1351
1351
|
<div class="error">
|
|
1352
1352
|
<span class="status">` + status + '</span>\n <div class="message">\n <h1>' + message + "</h1>\n </div>\n </div>\n </body>\n</html>\n"
|
|
1353
1353
|
},
|
|
1354
|
-
version_hash: "
|
|
1354
|
+
version_hash: "1f9h0ew"
|
|
1355
1355
|
};
|
|
1356
1356
|
async function get_hooks() {
|
|
1357
1357
|
let handle;
|