@vex-chat/spire 2.4.0 → 2.5.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/dist/ClientManager.js +8 -1
- package/dist/ClientManager.js.map +1 -1
- package/dist/Database.d.ts +3 -2
- package/dist/Database.js +7 -1
- package/dist/Database.js.map +1 -1
- package/dist/NotificationService.d.ts +2 -0
- package/dist/NotificationService.js +321 -4
- package/dist/NotificationService.js.map +1 -1
- package/dist/Spire.js +21 -5
- package/dist/Spire.js.map +1 -1
- package/dist/server/callWake.d.ts +13 -0
- package/dist/server/callWake.js +18 -0
- package/dist/server/callWake.js.map +1 -0
- package/package.json +4 -4
- package/src/ClientManager.ts +23 -9
- package/src/Database.ts +12 -3
- package/src/NotificationService.ts +428 -4
- package/src/Spire.ts +52 -21
- package/src/__tests__/notifyFanout.spec.ts +136 -0
- package/src/server/callWake.ts +30 -0
|
@@ -8,6 +8,8 @@ import type { ClientManager } from "../ClientManager.ts";
|
|
|
8
8
|
import type { Database, NotificationSubscription } from "../Database.ts";
|
|
9
9
|
import type { BaseMsg } from "@vex-chat/types";
|
|
10
10
|
|
|
11
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
12
|
+
|
|
11
13
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
14
|
|
|
13
15
|
import { NotificationService } from "../NotificationService.ts";
|
|
@@ -535,6 +537,140 @@ describe("Spire notify fanout", () => {
|
|
|
535
537
|
});
|
|
536
538
|
});
|
|
537
539
|
|
|
540
|
+
it("sends Expo callWake pushes with opaque call data", async () => {
|
|
541
|
+
const callSubscription: NotificationSubscription = {
|
|
542
|
+
...subscription,
|
|
543
|
+
events: ["callWake"],
|
|
544
|
+
subscriptionID: "sub-call",
|
|
545
|
+
};
|
|
546
|
+
const fetchMock = vi.fn().mockResolvedValueOnce({
|
|
547
|
+
json: () =>
|
|
548
|
+
Promise.resolve({
|
|
549
|
+
data: [{ id: "receipt-a", status: "ok" }],
|
|
550
|
+
}),
|
|
551
|
+
ok: true,
|
|
552
|
+
});
|
|
553
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
554
|
+
|
|
555
|
+
const { db } = createSpireHarness([], [callSubscription]);
|
|
556
|
+
const service = new NotificationService(db, [], () => {});
|
|
557
|
+
|
|
558
|
+
await service["notifyPush"]({
|
|
559
|
+
data: {
|
|
560
|
+
callID: "call-1",
|
|
561
|
+
expiresAt: "2026-06-05T12:01:00.000Z",
|
|
562
|
+
mailID: "mail-1",
|
|
563
|
+
mailNonce: "abcd",
|
|
564
|
+
},
|
|
565
|
+
deviceID: callSubscription.deviceID,
|
|
566
|
+
event: "callWake",
|
|
567
|
+
transmissionID: "00000000-0000-0000-0000-000000000017",
|
|
568
|
+
userID: callSubscription.userID,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const init = fetchMock.mock.calls[0]?.[1] as
|
|
572
|
+
| undefined
|
|
573
|
+
| { body?: unknown };
|
|
574
|
+
const messages = JSON.parse(String(init?.body)) as Array<{
|
|
575
|
+
body?: string;
|
|
576
|
+
data?: Record<string, unknown>;
|
|
577
|
+
title?: string;
|
|
578
|
+
}>;
|
|
579
|
+
expect(messages[0]?.title).toBe("Incoming Vex call");
|
|
580
|
+
expect(messages[0]?.body).toBe("Incoming voice call.");
|
|
581
|
+
expect(messages[0]?.data).toMatchObject({
|
|
582
|
+
callID: "call-1",
|
|
583
|
+
event: "callWake",
|
|
584
|
+
expiresAt: "2026-06-05T12:01:00.000Z",
|
|
585
|
+
mailID: "mail-1",
|
|
586
|
+
mailNonce: "abcd",
|
|
587
|
+
transmissionID: "00000000-0000-0000-0000-000000000017",
|
|
588
|
+
});
|
|
589
|
+
expect(messages[0]?.data).not.toHaveProperty("callerUserID");
|
|
590
|
+
expect(messages[0]?.data).not.toHaveProperty("sdp");
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("sends FCM call pushes as high-priority opaque data", async () => {
|
|
594
|
+
const { privateKey } = generateKeyPairSync("rsa", {
|
|
595
|
+
modulusLength: 2048,
|
|
596
|
+
});
|
|
597
|
+
const fcmSubscription: NotificationSubscription = {
|
|
598
|
+
...subscription,
|
|
599
|
+
channel: "fcmCall",
|
|
600
|
+
events: ["callWake"],
|
|
601
|
+
platform: "android",
|
|
602
|
+
subscriptionID: "sub-fcm",
|
|
603
|
+
token: "fcm-token",
|
|
604
|
+
};
|
|
605
|
+
const fetchMock = vi
|
|
606
|
+
.fn()
|
|
607
|
+
.mockResolvedValueOnce({
|
|
608
|
+
json: () => Promise.resolve({ access_token: "access-token" }),
|
|
609
|
+
ok: true,
|
|
610
|
+
})
|
|
611
|
+
.mockResolvedValueOnce({
|
|
612
|
+
ok: true,
|
|
613
|
+
});
|
|
614
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
615
|
+
|
|
616
|
+
process.env["SPIRE_FCM_CLIENT_EMAIL"] = "spire@example.invalid";
|
|
617
|
+
process.env["SPIRE_FCM_PRIVATE_KEY"] = privateKey.export({
|
|
618
|
+
format: "pem",
|
|
619
|
+
type: "pkcs8",
|
|
620
|
+
});
|
|
621
|
+
process.env["SPIRE_FCM_PROJECT_ID"] = "vex-test";
|
|
622
|
+
try {
|
|
623
|
+
const { db } = createSpireHarness([], [fcmSubscription]);
|
|
624
|
+
const service = new NotificationService(db, [], () => {});
|
|
625
|
+
|
|
626
|
+
await service["notifyPush"]({
|
|
627
|
+
data: {
|
|
628
|
+
callID: "call-fcm",
|
|
629
|
+
expiresAt: "2026-06-05T12:01:00.000Z",
|
|
630
|
+
mailID: "mail-fcm",
|
|
631
|
+
mailNonce: "beef",
|
|
632
|
+
},
|
|
633
|
+
deviceID: fcmSubscription.deviceID,
|
|
634
|
+
event: "callWake",
|
|
635
|
+
transmissionID: "00000000-0000-0000-0000-000000000018",
|
|
636
|
+
userID: fcmSubscription.userID,
|
|
637
|
+
});
|
|
638
|
+
} finally {
|
|
639
|
+
delete process.env["SPIRE_FCM_CLIENT_EMAIL"];
|
|
640
|
+
delete process.env["SPIRE_FCM_PRIVATE_KEY"];
|
|
641
|
+
delete process.env["SPIRE_FCM_PROJECT_ID"];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
645
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
646
|
+
"https://oauth2.googleapis.com/token",
|
|
647
|
+
);
|
|
648
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
649
|
+
"https://fcm.googleapis.com/v1/projects/vex-test/messages:send",
|
|
650
|
+
);
|
|
651
|
+
const init = fetchMock.mock.calls[1]?.[1] as
|
|
652
|
+
| undefined
|
|
653
|
+
| { body?: unknown };
|
|
654
|
+
const body = JSON.parse(String(init?.body)) as {
|
|
655
|
+
message?: {
|
|
656
|
+
android?: { priority?: string; ttl?: string };
|
|
657
|
+
data?: Record<string, string>;
|
|
658
|
+
token?: string;
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
expect(body.message?.token).toBe("fcm-token");
|
|
662
|
+
expect(body.message?.android?.priority).toBe("HIGH");
|
|
663
|
+
expect(body.message?.data).toMatchObject({
|
|
664
|
+
callID: "call-fcm",
|
|
665
|
+
event: "callWake",
|
|
666
|
+
mailID: "mail-fcm",
|
|
667
|
+
mailNonce: "beef",
|
|
668
|
+
transmissionID: "00000000-0000-0000-0000-000000000018",
|
|
669
|
+
});
|
|
670
|
+
expect(body.message?.data).not.toHaveProperty("callerUserID");
|
|
671
|
+
expect(body.message?.data).not.toHaveProperty("sdp");
|
|
672
|
+
});
|
|
673
|
+
|
|
538
674
|
it("requests the default sound for iOS visible Expo pushes only", async () => {
|
|
539
675
|
const iosSubscription: NotificationSubscription = {
|
|
540
676
|
...subscription,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MailWS } from "@vex-chat/types";
|
|
8
|
+
|
|
9
|
+
import { XUtils } from "@vex-chat/crypto";
|
|
10
|
+
|
|
11
|
+
export interface CallWakeDispatchData {
|
|
12
|
+
callID: string;
|
|
13
|
+
expiresAt?: string | undefined;
|
|
14
|
+
mailID: string;
|
|
15
|
+
mailNonce: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function callWakeDispatchData(
|
|
19
|
+
mail: MailWS,
|
|
20
|
+
): CallWakeDispatchData | null {
|
|
21
|
+
if (mail.notify?.event !== "callWake") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
callID: mail.notify.callID,
|
|
26
|
+
...(mail.notify.expiresAt ? { expiresAt: mail.notify.expiresAt } : {}),
|
|
27
|
+
mailID: mail.mailID,
|
|
28
|
+
mailNonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
29
|
+
};
|
|
30
|
+
}
|