@vex-chat/spire 1.10.1 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
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 { Database } from "../Database.ts";
8
+ import type { Device, MailWS } from "@vex-chat/types";
9
+
10
+ export interface ValidatedMailIngress {
11
+ readonly recipientDevice: Device;
12
+ }
13
+
14
+ export class MailIngressValidationError extends Error {
15
+ public readonly status: number;
16
+
17
+ public constructor(status: number, message: string) {
18
+ super(message);
19
+ this.name = "MailIngressValidationError";
20
+ this.status = status;
21
+ }
22
+ }
23
+
24
+ export async function validateMailIngress(
25
+ db: Pick<Database, "retrieveDevice">,
26
+ mail: MailWS,
27
+ authenticatedDeviceID: string,
28
+ authenticatedUserID: string,
29
+ ): Promise<ValidatedMailIngress> {
30
+ if (mail.sender !== authenticatedDeviceID) {
31
+ throw new MailIngressValidationError(
32
+ 403,
33
+ "Mail sender does not match the authenticated device.",
34
+ );
35
+ }
36
+
37
+ if (mail.authorID !== authenticatedUserID) {
38
+ throw new MailIngressValidationError(
39
+ 403,
40
+ "Mail author does not match the authenticated user.",
41
+ );
42
+ }
43
+
44
+ const recipientDevice = await db.retrieveDevice(mail.recipient);
45
+ if (recipientDevice === null) {
46
+ throw new MailIngressValidationError(
47
+ 400,
48
+ "No associated user record found for recipient device.",
49
+ );
50
+ }
51
+
52
+ if (mail.readerID !== recipientDevice.owner) {
53
+ throw new MailIngressValidationError(
54
+ 400,
55
+ "Mail reader does not match the recipient device owner.",
56
+ );
57
+ }
58
+
59
+ return { recipientDevice };
60
+ }
@@ -132,3 +132,24 @@ export const uploadLimiter = rateLimit({
132
132
  standardHeaders: "draft-7",
133
133
  windowMs: 60 * 1000,
134
134
  });
135
+
136
+ /**
137
+ * Key-bundle retrieval dispenses OTKs, so it gets its own tighter bucket keyed
138
+ * by authenticated caller and target device. This makes OTK-drain attempts hit
139
+ * a per-pair budget without penalizing unrelated senders.
140
+ */
141
+ export const keyBundleLimiter = rateLimit({
142
+ keyGenerator: (req) => {
143
+ const caller =
144
+ req.user?.userID ??
145
+ ipKeyGenerator(req.ip ?? req.socket.remoteAddress ?? "");
146
+ const target =
147
+ typeof req.params["id"] === "string" ? req.params["id"] : "unknown";
148
+ return `${caller}:${target}`;
149
+ },
150
+ legacyHeaders: false,
151
+ limit: 30,
152
+ skip: devApiKeySkipsRateLimits,
153
+ standardHeaders: "draft-7",
154
+ windowMs: 15 * 60 * 1000,
155
+ });