@vex-chat/spire 3.0.1 → 4.1.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/README.md +9 -8
- package/dist/Database.d.ts +20 -11
- package/dist/Database.js +229 -118
- package/dist/Database.js.map +1 -1
- package/dist/Spire.d.ts +4 -2
- package/dist/Spire.js +155 -72
- package/dist/Spire.js.map +1 -1
- package/dist/db/schema.d.ts +0 -2
- package/dist/migrations/2026-04-06_initial-schema.js +4 -5
- package/dist/migrations/2026-04-06_initial-schema.js.map +1 -1
- package/dist/migrations/{2026-04-14_argon2id-password-hashing.d.ts → 2026-07-14_query-indexes.d.ts} +1 -1
- package/dist/migrations/2026-07-14_query-indexes.js +31 -0
- package/dist/migrations/2026-07-14_query-indexes.js.map +1 -0
- package/dist/server/avatar.js +33 -6
- package/dist/server/avatar.js.map +1 -1
- package/dist/server/cliPasskeyPage.js +109 -11
- package/dist/server/cliPasskeyPage.js.map +1 -1
- package/dist/server/errors.js +8 -0
- package/dist/server/errors.js.map +1 -1
- package/dist/server/file.js +35 -12
- package/dist/server/file.js.map +1 -1
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +319 -56
- package/dist/server/index.js.map +1 -1
- package/dist/server/passkey.js +367 -29
- package/dist/server/passkey.js.map +1 -1
- package/dist/server/passkeyDevices.js +1 -0
- package/dist/server/passkeyDevices.js.map +1 -1
- package/dist/server/password.d.ts +7 -0
- package/dist/server/password.js +58 -0
- package/dist/server/password.js.map +1 -0
- package/dist/server/permissions.d.ts +1 -0
- package/dist/server/permissions.js +11 -2
- package/dist/server/permissions.js.map +1 -1
- package/dist/server/rateLimit.d.ts +11 -0
- package/dist/server/rateLimit.js +43 -4
- package/dist/server/rateLimit.js.map +1 -1
- package/dist/server/serverIcon.d.ts +10 -0
- package/dist/server/serverIcon.js +158 -0
- package/dist/server/serverIcon.js.map +1 -0
- package/dist/server/user.d.ts +1 -1
- package/dist/server/user.js +40 -9
- package/dist/server/user.js.map +1 -1
- package/dist/types/express.d.ts +3 -4
- package/dist/types/express.js.map +1 -1
- package/dist/utils/authJwt.d.ts +8 -0
- package/dist/utils/authJwt.js +25 -0
- package/dist/utils/authJwt.js.map +1 -0
- package/dist/utils/loadEnv.js +4 -0
- package/dist/utils/loadEnv.js.map +1 -1
- package/dist/utils/preKeySignature.d.ts +1 -1
- package/dist/utils/preKeySignature.js +7 -11
- package/dist/utils/preKeySignature.js.map +1 -1
- package/package.json +7 -7
- package/src/Database.ts +294 -152
- package/src/Spire.ts +412 -285
- package/src/__tests__/Database.spec.ts +126 -3
- package/src/__tests__/browserPasskeyAuthentication.spec.ts +192 -0
- package/src/__tests__/browserPasskeyRegistration.spec.ts +182 -0
- package/src/__tests__/cliPasskeyPage.spec.ts +6 -0
- package/src/__tests__/connectAuth.spec.ts +146 -4
- package/src/__tests__/deviceTokenRevalidation.spec.ts +57 -3
- package/src/__tests__/passkeyDevices.spec.ts +94 -0
- package/src/__tests__/passkeys.spec.ts +7 -2
- package/src/__tests__/password.spec.ts +223 -0
- package/src/__tests__/permissions.spec.ts +50 -0
- package/src/__tests__/preKeySignature.spec.ts +20 -3
- package/src/__tests__/serverManagement.spec.ts +262 -0
- package/src/db/schema.ts +0 -2
- package/src/migrations/2026-04-06_initial-schema.ts +4 -5
- package/src/migrations/2026-07-14_query-indexes.ts +34 -0
- package/src/server/avatar.ts +32 -6
- package/src/server/cliPasskeyPage.ts +109 -11
- package/src/server/errors.ts +7 -0
- package/src/server/file.ts +34 -13
- package/src/server/index.ts +494 -139
- package/src/server/passkey.ts +531 -31
- package/src/server/passkeyDevices.ts +1 -0
- package/src/server/password.ts +96 -0
- package/src/server/permissions.ts +20 -2
- package/src/server/rateLimit.ts +47 -4
- package/src/server/serverIcon.ts +199 -0
- package/src/server/user.ts +44 -9
- package/src/types/express.ts +3 -4
- package/src/utils/authJwt.ts +34 -0
- package/src/utils/loadEnv.ts +4 -0
- package/src/utils/preKeySignature.ts +12 -15
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js +0 -17
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +0 -1
- package/src/migrations/2026-04-14_argon2id-password-hashing.ts +0 -24
|
@@ -58,6 +58,7 @@ export const getPasskeyDeviceRouter = (db, notify, disconnectDevices) => {
|
|
|
58
58
|
// "I lost my phone, sign in with the passkey, wipe the
|
|
59
59
|
// old device, then recover onto a new one."
|
|
60
60
|
await db.deleteDevice(deviceID);
|
|
61
|
+
disconnectDevices?.([deviceID]);
|
|
61
62
|
// Tell whoever's online that the device-list shape
|
|
62
63
|
// changed; clients use this to refresh the Settings →
|
|
63
64
|
// Devices view in real time.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkeyDevices.js","sourceRoot":"","sources":["../../src/server/passkeyDevices.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,EACH,8BAA8B,EAC9B,8BAA8B,GACjC,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAClC,EAAY,EACZ,MAMS,EACT,iBAAiD,EACnD,EAAE;IACA,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,CAAC,GAAG,CACN,2BAA2B,EAC3B,cAAc,EACd,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACf,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACnC,IAAI,WAAW,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAChC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACvD,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC,CACJ,CAAC;IAEF,MAAM,CAAC,MAAM,CACT,qCAAqC,EACrC,cAAc,EACd,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACf,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC3C,IAAI,WAAW,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAChC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACjD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;YACrC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,uDAAuD;QACvD,qDAAqD;QACrD,uDAAuD;QACvD,yDAAyD;QACzD,uDAAuD;QACvD,4CAA4C;QAC5C,MAAM,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAChC,mDAAmD;QACnD,sDAAsD;QACtD,6BAA6B;QAC7B,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;QACzD,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC,CACJ,CAAC;IAEF,MAAM,CAAC,IAAI,CACP,uDAAuD,EACvD,cAAc,EACd,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACf,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC7C,IAAI,WAAW,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAChC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC;QACzC,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,oDAAoD;QACpD,0DAA0D;QAC1D,4DAA4D;QAC5D,yDAAyD;QACzD,yDAAyD;QACzD,MAAM,MAAM,GAAG,MAAM,8BAA8B,CAAC;YAChD,mBAAmB,EAAE,SAAS;YAC9B,EAAE;YACF,MAAM;YACN,SAAS;YACT,MAAM;SACT,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YACvB,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;YACzD,iBAAiB,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;YAC7C,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YAC1C,OAAO;QACX,CAAC;QACD,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAC5D,CAAC,CACJ,CAAC;IAEF,MAAM,CAAC,IAAI,CACP,sDAAsD,EACtD,cAAc,EACd,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACf,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC7C,IAAI,WAAW,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAChC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,8BAA8B,CAAC;YAChD,MAAM,EAAE,QAAQ;YAChB,EAAE;YACF,MAAM;YACN,SAAS;YACT,MAAM;SACT,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YACvB,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAC5D,CAAC,CACJ,CAAC;IAEF,OAAO,MAAM,CAAC;AAClB,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"passkeyDevices.js","sourceRoot":"","sources":["../../src/server/passkeyDevices.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
import type { Database } from "../Database.ts";
|
|
7
|
+
export declare const getPasswordRouter: (db: Database) => import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
import express from "express";
|
|
7
|
+
import { PasswordUpdatePayloadSchema } from "@vex-chat/types";
|
|
8
|
+
import { hashPasswordArgon2, validateAccountPassword, verifyPassword, } from "../Database.js";
|
|
9
|
+
import { AppError } from "./errors.js";
|
|
10
|
+
import { passwordUpdateLimiter } from "./rateLimit.js";
|
|
11
|
+
import { getParam, getUser } from "./utils.js";
|
|
12
|
+
import { protectAnyAuth } from "./index.js";
|
|
13
|
+
export const getPasswordRouter = (db) => {
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
router.patch("/user/:id/password", protectAnyAuth, passwordUpdateLimiter, async (req, res) => {
|
|
16
|
+
const authenticatedUser = getUser(req);
|
|
17
|
+
const userID = getParam(req, "id");
|
|
18
|
+
if (authenticatedUser.userID !== userID) {
|
|
19
|
+
throw new AppError(403, "Not authorized for this account");
|
|
20
|
+
}
|
|
21
|
+
const payload = PasswordUpdatePayloadSchema.parse(req.body);
|
|
22
|
+
const user = await db.retrieveUser(userID);
|
|
23
|
+
if (!user) {
|
|
24
|
+
throw new AppError(404, "Account not found");
|
|
25
|
+
}
|
|
26
|
+
const passwordError = validateAccountPassword(payload.newPassword, user.username);
|
|
27
|
+
if (passwordError) {
|
|
28
|
+
throw new AppError(400, passwordError);
|
|
29
|
+
}
|
|
30
|
+
if (req.passkey) {
|
|
31
|
+
const passkey = await db.retrievePasskeyInternal(req.passkey.passkeyID);
|
|
32
|
+
if (!passkey || passkey.userID !== userID) {
|
|
33
|
+
throw new AppError(401, "Passkey authentication required");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
if (!req.device ||
|
|
38
|
+
req.device.owner !== userID ||
|
|
39
|
+
!payload.currentPassword) {
|
|
40
|
+
throw new AppError(401, "Current-password authentication required");
|
|
41
|
+
}
|
|
42
|
+
const current = await verifyPassword(payload.currentPassword, user);
|
|
43
|
+
if (!current.valid) {
|
|
44
|
+
throw new AppError(401, "Current password is incorrect");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const reused = await verifyPassword(payload.newPassword, user);
|
|
48
|
+
if (reused.valid) {
|
|
49
|
+
throw new AppError(409, "New password must be different from the current password");
|
|
50
|
+
}
|
|
51
|
+
const passwordHash = await hashPasswordArgon2(payload.newPassword);
|
|
52
|
+
await db.rehashPassword(userID, passwordHash);
|
|
53
|
+
res.setHeader("Cache-Control", "no-store");
|
|
54
|
+
res.sendStatus(204);
|
|
55
|
+
});
|
|
56
|
+
return router;
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/server/password.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
6
|
import type { Permission } from "@vex-chat/types";
|
|
7
|
+
export declare function canDeletePermission(permissions: Permission[], actorUserID: string, target: Permission, adminPowerLevel: number): boolean;
|
|
7
8
|
/**
|
|
8
9
|
* Check whether any permission in the list covers the given resource
|
|
9
10
|
* (any power level).
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
|
+
export function canDeletePermission(permissions, actorUserID, target, adminPowerLevel) {
|
|
7
|
+
if (target.userID === actorUserID) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return permissions.some((permission) => permission.userID === actorUserID &&
|
|
11
|
+
permission.resourceID === target.resourceID &&
|
|
12
|
+
permission.powerLevel >= adminPowerLevel &&
|
|
13
|
+
permission.powerLevel > target.powerLevel);
|
|
14
|
+
}
|
|
6
15
|
/**
|
|
7
16
|
* Check whether any permission in the list covers the given resource
|
|
8
17
|
* (any power level).
|
|
@@ -15,13 +24,13 @@ export function hasAnyPermission(permissions, resourceID) {
|
|
|
15
24
|
* on the given resource.
|
|
16
25
|
*/
|
|
17
26
|
export function hasPermission(permissions, resourceID, minPowerLevel) {
|
|
18
|
-
return permissions.some((p) => p.resourceID === resourceID && p.powerLevel
|
|
27
|
+
return permissions.some((p) => p.resourceID === resourceID && p.powerLevel >= minPowerLevel);
|
|
19
28
|
}
|
|
20
29
|
/**
|
|
21
30
|
* Check whether any permission in the list grants at least `minPowerLevel`
|
|
22
31
|
* on the given resource AND belongs to the specified user.
|
|
23
32
|
*/
|
|
24
33
|
export function userHasPermission(permissions, userID, minPowerLevel) {
|
|
25
|
-
return permissions.some((p) => p.userID === userID && p.powerLevel
|
|
34
|
+
return permissions.some((p) => p.userID === userID && p.powerLevel >= minPowerLevel);
|
|
26
35
|
}
|
|
27
36
|
//# sourceMappingURL=permissions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/server/permissions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC5B,WAAyB,EACzB,UAAkB;IAElB,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC;AAChE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CACzB,WAAyB,EACzB,UAAkB,EAClB,aAAqB;IAErB,OAAO,WAAW,CAAC,IAAI,CACnB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,IAAI,CAAC,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/server/permissions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,UAAU,mBAAmB,CAC/B,WAAyB,EACzB,WAAmB,EACnB,MAAkB,EAClB,eAAuB;IAEvB,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,WAAW,CAAC,IAAI,CACnB,CAAC,UAAU,EAAE,EAAE,CACX,UAAU,CAAC,MAAM,KAAK,WAAW;QACjC,UAAU,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU;QAC3C,UAAU,CAAC,UAAU,IAAI,eAAe;QACxC,UAAU,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAChD,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC5B,WAAyB,EACzB,UAAkB;IAElB,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC;AAChE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CACzB,WAAyB,EACzB,UAAkB,EAClB,aAAqB;IAErB,OAAO,WAAW,CAAC,IAAI,CACnB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,IAAI,CAAC,CAAC,UAAU,IAAI,aAAa,CACtE,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC7B,WAAyB,EACzB,MAAc,EACd,aAAqB;IAErB,OAAO,WAAW,CAAC,IAAI,CACnB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,UAAU,IAAI,aAAa,CAC9D,CAAC;AACN,CAAC"}
|
|
@@ -34,6 +34,17 @@ export declare const globalLimiter: import("express-rate-limit").RateLimitReques
|
|
|
34
34
|
* (CWE-307) without harming UX.
|
|
35
35
|
*/
|
|
36
36
|
export declare const authLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
37
|
+
/**
|
|
38
|
+
* Cross-IP password defense. This account bucket complements the IP bucket so
|
|
39
|
+
* a distributed password spray cannot make unlimited guesses at one account.
|
|
40
|
+
*/
|
|
41
|
+
export declare const accountAuthLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
42
|
+
/**
|
|
43
|
+
* Limit repeated password replacement attempts per authenticated account.
|
|
44
|
+
* This is intentionally tighter than sign-in because each failed request has
|
|
45
|
+
* already passed bearer/device validation and still consumes an Argon2 check.
|
|
46
|
+
*/
|
|
47
|
+
export declare const passwordUpdateLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
37
48
|
/**
|
|
38
49
|
* Upload endpoint limiter. Applied per-route to /file and /avatar
|
|
39
50
|
* POSTs, BEFORE multer parses the multipart body. Caps the number of
|
package/dist/server/rateLimit.js
CHANGED
|
@@ -59,14 +59,13 @@ function disableRateLimitsByEnv() {
|
|
|
59
59
|
* versions silently collapsed all IPv4-mapped IPv6 addresses into
|
|
60
60
|
* one bucket, which let attackers bypass the limiter).
|
|
61
61
|
*
|
|
62
|
-
* `trust proxy` must
|
|
63
|
-
* `req.ip`
|
|
62
|
+
* `trust proxy` must match the real deployment topology (see Spire.ts) so
|
|
63
|
+
* `req.ip` cannot be spoofed and does not collapse all traffic onto a proxy.
|
|
64
64
|
*/
|
|
65
65
|
/**
|
|
66
66
|
* Bucket requests by the real client IP, IPv6-safe.
|
|
67
67
|
*
|
|
68
|
-
* `req.ip`
|
|
69
|
-
* set to `1` in Spire's constructor. We still run it through
|
|
68
|
+
* `req.ip` reflects the explicitly configured proxy hop count. We still run it through
|
|
70
69
|
* `ipKeyGenerator` so IPv4-mapped IPv6 (`::ffff:1.2.3.4`) doesn't
|
|
71
70
|
* collide with unrelated IPv6 addresses in the same /56.
|
|
72
71
|
*/
|
|
@@ -104,6 +103,46 @@ export const authLimiter = rateLimit({
|
|
|
104
103
|
standardHeaders: "draft-7",
|
|
105
104
|
windowMs: 15 * 60 * 1000,
|
|
106
105
|
});
|
|
106
|
+
/**
|
|
107
|
+
* Cross-IP password defense. This account bucket complements the IP bucket so
|
|
108
|
+
* a distributed password spray cannot make unlimited guesses at one account.
|
|
109
|
+
*/
|
|
110
|
+
export const accountAuthLimiter = rateLimit({
|
|
111
|
+
keyGenerator: (req) => {
|
|
112
|
+
const body = req.body;
|
|
113
|
+
const username = typeof body === "object" &&
|
|
114
|
+
body !== null &&
|
|
115
|
+
"username" in body &&
|
|
116
|
+
typeof body.username === "string"
|
|
117
|
+
? body.username.trim().toLowerCase().slice(0, 64)
|
|
118
|
+
: "";
|
|
119
|
+
return username.length > 0
|
|
120
|
+
? `account:${username}`
|
|
121
|
+
: `invalid:${keyByIp(req)}`;
|
|
122
|
+
},
|
|
123
|
+
legacyHeaders: false,
|
|
124
|
+
limit: 100,
|
|
125
|
+
skip: devApiKeySkipsRateLimits,
|
|
126
|
+
skipSuccessfulRequests: true,
|
|
127
|
+
standardHeaders: "draft-7",
|
|
128
|
+
windowMs: 15 * 60 * 1000,
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* Limit repeated password replacement attempts per authenticated account.
|
|
132
|
+
* This is intentionally tighter than sign-in because each failed request has
|
|
133
|
+
* already passed bearer/device validation and still consumes an Argon2 check.
|
|
134
|
+
*/
|
|
135
|
+
export const passwordUpdateLimiter = rateLimit({
|
|
136
|
+
keyGenerator: (req) => req.user?.userID
|
|
137
|
+
? `password:${req.user.userID}`
|
|
138
|
+
: `invalid:${keyByIp(req)}`,
|
|
139
|
+
legacyHeaders: false,
|
|
140
|
+
limit: 10,
|
|
141
|
+
skip: devApiKeySkipsRateLimits,
|
|
142
|
+
skipSuccessfulRequests: true,
|
|
143
|
+
standardHeaders: "draft-7",
|
|
144
|
+
windowMs: 15 * 60 * 1000,
|
|
145
|
+
});
|
|
107
146
|
/**
|
|
108
147
|
* Upload endpoint limiter. Applied per-route to /file and /avatar
|
|
109
148
|
* POSTs, BEFORE multer parses the multipart body. Caps the number of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/server/rateLimit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,SAAS,EAAE,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE/D,yFAAyF;AACzF,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAMlD,MAAM,UAAU,gBAAgB,CAAC,GAAiB;IAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC5D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,MAAM,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,UAAU,CAAC,MAAM,EAAE,CAAC;QACvD,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,IAAI,CAAC;QACD,OAAO,eAAe,CAClB,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,EAC9B,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAClC,CAAC;IACN,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAiB;IACtD,IAAI,sBAAsB,EAAE,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,sBAAsB;IAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3E,OAAO,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,MAAM,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/server/rateLimit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
import type { Database } from "../Database.ts";
|
|
7
|
+
type NotifyServerChange = (serverID: string) => Promise<void>;
|
|
8
|
+
export declare function deleteServerIconFile(iconID: string): Promise<void>;
|
|
9
|
+
export declare const getServerIconRouter: (db: Database, notifyServerChange: NotifyServerChange) => import("express-serve-static-core").Router;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as fsp from "node:fs/promises";
|
|
8
|
+
import express from "express";
|
|
9
|
+
import { XUtils } from "@vex-chat/crypto";
|
|
10
|
+
import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
|
|
11
|
+
import multer from "multer";
|
|
12
|
+
import { z } from "zod/v4";
|
|
13
|
+
import { POWER_LEVELS } from "../ClientManager.js";
|
|
14
|
+
import { msgpack } from "../utils/msgpack.js";
|
|
15
|
+
import { hasPermission } from "./permissions.js";
|
|
16
|
+
import { uploadLimiter } from "./rateLimit.js";
|
|
17
|
+
import { getParam, getUser } from "./utils.js";
|
|
18
|
+
import { ALLOWED_IMAGE_TYPES, protect } from "./index.js";
|
|
19
|
+
const SERVER_ICON_DIR = "server-icons";
|
|
20
|
+
const MAX_SERVER_ICON_BYTES = 5 * 1024 * 1024;
|
|
21
|
+
const safePathParam = z.string().regex(/^[a-zA-Z0-9._-]+$/);
|
|
22
|
+
const serverIconUpload = multer({
|
|
23
|
+
limits: { fields: 1, files: 1, fileSize: MAX_SERVER_ICON_BYTES, parts: 2 },
|
|
24
|
+
});
|
|
25
|
+
const serverIconJsonPayload = z.object({
|
|
26
|
+
file: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(Math.ceil((MAX_SERVER_ICON_BYTES * 4) / 3) + 4),
|
|
30
|
+
});
|
|
31
|
+
export async function deleteServerIconFile(iconID) {
|
|
32
|
+
const safeID = safePathParam.safeParse(iconID);
|
|
33
|
+
if (!safeID.success)
|
|
34
|
+
return;
|
|
35
|
+
await fsp
|
|
36
|
+
.unlink(`${SERVER_ICON_DIR}/${safeID.data}`)
|
|
37
|
+
.catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
export const getServerIconRouter = (db, notifyServerChange) => {
|
|
40
|
+
const router = express.Router();
|
|
41
|
+
router.get("/:iconID", async (req, res) => {
|
|
42
|
+
const safeID = safePathParam.safeParse(getParam(req, "iconID"));
|
|
43
|
+
if (!safeID.success) {
|
|
44
|
+
res.sendStatus(400);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const filePath = `${SERVER_ICON_DIR}/${safeID.data}`;
|
|
48
|
+
const typeDetails = await fileTypeFromFile(filePath).catch(() => null);
|
|
49
|
+
if (!typeDetails) {
|
|
50
|
+
res.sendStatus(404);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.set("Content-Type", typeDetails.mime);
|
|
54
|
+
res.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
55
|
+
res.set("Cross-Origin-Resource-Policy", "cross-origin");
|
|
56
|
+
const stream = fs.createReadStream(filePath);
|
|
57
|
+
stream.on("error", () => {
|
|
58
|
+
if (!res.headersSent)
|
|
59
|
+
res.sendStatus(500);
|
|
60
|
+
else
|
|
61
|
+
res.destroy();
|
|
62
|
+
});
|
|
63
|
+
stream.pipe(res);
|
|
64
|
+
});
|
|
65
|
+
router.post("/:serverID/json", uploadLimiter, protect, async (req, res) => {
|
|
66
|
+
const parsed = serverIconJsonPayload.safeParse(req.body);
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
res.status(400).json({
|
|
69
|
+
error: "Invalid server icon payload",
|
|
70
|
+
issues: parsed.error.issues,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let buffer;
|
|
75
|
+
try {
|
|
76
|
+
buffer = Buffer.from(XUtils.decodeBase64(parsed.data.file));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
res.status(400).json({ error: "Icon must be valid base64." });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await saveIcon(req, res, db, notifyServerChange, buffer);
|
|
83
|
+
});
|
|
84
|
+
router.post("/:serverID", uploadLimiter, protect, serverIconUpload.single("icon"), async (req, res) => {
|
|
85
|
+
if (!req.file) {
|
|
86
|
+
res.sendStatus(400);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await saveIcon(req, res, db, notifyServerChange, req.file.buffer);
|
|
90
|
+
});
|
|
91
|
+
router.delete("/:serverID", protect, async (req, res) => {
|
|
92
|
+
const serverID = getParam(req, "serverID");
|
|
93
|
+
if (!(await canManageServer(db, getUser(req).userID, serverID))) {
|
|
94
|
+
res.sendStatus(403);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const existing = await db.retrieveServer(serverID);
|
|
98
|
+
if (!existing) {
|
|
99
|
+
res.sendStatus(404);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const updated = await db.updateServer(serverID, { icon: null });
|
|
103
|
+
if (!updated) {
|
|
104
|
+
res.sendStatus(404);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (existing.icon)
|
|
108
|
+
await deleteServerIconFile(existing.icon);
|
|
109
|
+
await notifyServerChange(serverID);
|
|
110
|
+
res.send(msgpack.encode(updated));
|
|
111
|
+
});
|
|
112
|
+
return router;
|
|
113
|
+
};
|
|
114
|
+
async function canManageServer(db, userID, serverID) {
|
|
115
|
+
const permissions = await db.retrievePermissions(userID, "server");
|
|
116
|
+
return hasPermission(permissions, serverID, POWER_LEVELS.CREATE);
|
|
117
|
+
}
|
|
118
|
+
async function saveIcon(req, res, db, notifyServerChange, buffer) {
|
|
119
|
+
const serverID = getParam(req, "serverID");
|
|
120
|
+
if (!(await canManageServer(db, getUser(req).userID, serverID))) {
|
|
121
|
+
res.sendStatus(403);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (buffer.byteLength > MAX_SERVER_ICON_BYTES) {
|
|
125
|
+
res.sendStatus(413);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const typeDetails = await fileTypeFromBuffer(buffer);
|
|
129
|
+
if (!ALLOWED_IMAGE_TYPES.includes(typeDetails?.mime ?? "")) {
|
|
130
|
+
res.status(400).json({
|
|
131
|
+
error: "Unsupported icon type. Use JPEG, PNG, GIF, APNG, AVIF, or WebP.",
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const existing = await db.retrieveServer(serverID);
|
|
136
|
+
if (!existing) {
|
|
137
|
+
res.sendStatus(404);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const iconID = crypto.randomUUID();
|
|
141
|
+
const filePath = `${SERVER_ICON_DIR}/${iconID}`;
|
|
142
|
+
try {
|
|
143
|
+
await fsp.writeFile(filePath, buffer, { flag: "wx" });
|
|
144
|
+
const updated = await db.updateServer(serverID, { icon: iconID });
|
|
145
|
+
if (!updated) {
|
|
146
|
+
throw new Error("Server disappeared while its icon was updating.");
|
|
147
|
+
}
|
|
148
|
+
if (existing.icon)
|
|
149
|
+
await deleteServerIconFile(existing.icon);
|
|
150
|
+
await notifyServerChange(serverID);
|
|
151
|
+
res.send(msgpack.encode(updated));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
await fsp.unlink(filePath).catch(() => undefined);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=serverIcon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverIcon.js","sourceRoot":"","sources":["../../src/server/serverIcon.ts"],"names":[],"mappings":"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"}
|
package/dist/server/user.d.ts
CHANGED
|
@@ -56,4 +56,4 @@ export declare function resolveDeviceEnrollmentRequest(args: {
|
|
|
56
56
|
kind: "err";
|
|
57
57
|
status: number;
|
|
58
58
|
}>;
|
|
59
|
-
export declare const getUserRouter: (db: Database, tokenValidator: (key: string, scope: TokenScopes) => boolean, notify: (userID: string, event: string, transmissionID: string, data?: unknown, deviceID?: string) => void) => import("express-serve-static-core").Router;
|
|
59
|
+
export declare const getUserRouter: (db: Database, tokenValidator: (key: string, scope: TokenScopes) => boolean, notify: (userID: string, event: string, transmissionID: string, data?: unknown, deviceID?: string) => void, disconnectDevices?: (deviceIDs: string[]) => void) => import("express-serve-static-core").Router;
|
package/dist/server/user.js
CHANGED
|
@@ -10,7 +10,9 @@ import { DevicePayloadSchema, TokenScopes } from "@vex-chat/types";
|
|
|
10
10
|
import { generateRegistrationOptions, verifyRegistrationResponse, } from "@simplewebauthn/server";
|
|
11
11
|
import { stringify } from "uuid";
|
|
12
12
|
import { z } from "zod/v4";
|
|
13
|
+
import { MAX_ACTIVE_DEVICES_PER_USER } from "../Database.js";
|
|
13
14
|
import { msgpack } from "../utils/msgpack.js";
|
|
15
|
+
import { verifyDevicePayloadPreKeySignature } from "../utils/preKeySignature.js";
|
|
14
16
|
import { spireXSignOpenAsync } from "../utils/spireXSignOpenAsync.js";
|
|
15
17
|
import { AppError } from "./errors.js";
|
|
16
18
|
import { censorUser, getParam, getUser } from "./utils.js";
|
|
@@ -312,7 +314,7 @@ async function tryGetVerifiedPendingEnrollmentForSigned(req, signed) {
|
|
|
312
314
|
}
|
|
313
315
|
return { ok: true, pending };
|
|
314
316
|
}
|
|
315
|
-
export const getUserRouter = (db, tokenValidator, notify) => {
|
|
317
|
+
export const getUserRouter = (db, tokenValidator, notify, disconnectDevices) => {
|
|
316
318
|
const router = express.Router();
|
|
317
319
|
// Unauthenticated status poll for the *requesting* device on a pending
|
|
318
320
|
// device-enrollment request. The new device cannot use the protected
|
|
@@ -390,7 +392,7 @@ export const getUserRouter = (db, tokenValidator, notify) => {
|
|
|
390
392
|
authenticatorSelection: {
|
|
391
393
|
requireResidentKey: false,
|
|
392
394
|
residentKey: "preferred",
|
|
393
|
-
userVerification: "
|
|
395
|
+
userVerification: "required",
|
|
394
396
|
},
|
|
395
397
|
excludeCredentials: [],
|
|
396
398
|
rpID,
|
|
@@ -467,16 +469,18 @@ export const getUserRouter = (db, tokenValidator, notify) => {
|
|
|
467
469
|
expectedChallenge: passkeyRegistration.challenge,
|
|
468
470
|
expectedOrigin,
|
|
469
471
|
expectedRPID: rpID,
|
|
470
|
-
requireUserVerification:
|
|
472
|
+
requireUserVerification: true,
|
|
471
473
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- structurally validated by simplewebauthn below
|
|
472
474
|
response: parsed.data
|
|
473
475
|
.response,
|
|
474
476
|
});
|
|
475
477
|
}
|
|
476
478
|
catch (err) {
|
|
479
|
+
const requestId = crypto.randomUUID();
|
|
477
480
|
const message = err instanceof Error ? err.message : String(err);
|
|
481
|
+
console.warn(`[spire] pending-device WebAuthn registration failed requestId=${requestId} message=${message}`);
|
|
478
482
|
res.status(400).send({
|
|
479
|
-
error: "Passkey attestation
|
|
483
|
+
error: "Passkey attestation could not be verified.",
|
|
480
484
|
});
|
|
481
485
|
return;
|
|
482
486
|
}
|
|
@@ -589,16 +593,28 @@ export const getUserRouter = (db, tokenValidator, notify) => {
|
|
|
589
593
|
});
|
|
590
594
|
router.get("/:id/permissions", protect, async (req, res) => {
|
|
591
595
|
const userDetails = getUser(req);
|
|
596
|
+
if (getParam(req, "id") !== userDetails.userID) {
|
|
597
|
+
res.sendStatus(403);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
592
600
|
const permissions = await db.retrievePermissions(userDetails.userID, "all");
|
|
593
601
|
res.send(msgpack.encode(permissions));
|
|
594
602
|
});
|
|
595
603
|
router.get("/:id/servers", protect, async (req, res) => {
|
|
596
604
|
const userDetails = getUser(req);
|
|
605
|
+
if (getParam(req, "id") !== userDetails.userID) {
|
|
606
|
+
res.sendStatus(403);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
597
609
|
const servers = await db.retrieveServers(userDetails.userID);
|
|
598
610
|
res.send(msgpack.encode(servers));
|
|
599
611
|
});
|
|
600
612
|
router.get("/:id/servers/bootstrap", protect, async (req, res) => {
|
|
601
613
|
const userDetails = getUser(req);
|
|
614
|
+
if (getParam(req, "id") !== userDetails.userID) {
|
|
615
|
+
res.sendStatus(403);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
602
618
|
const payload = await db.retrieveServerChannelBootstrap(userDetails.userID);
|
|
603
619
|
res.send(msgpack.encode(payload));
|
|
604
620
|
});
|
|
@@ -609,8 +625,12 @@ export const getUserRouter = (db, tokenValidator, notify) => {
|
|
|
609
625
|
return;
|
|
610
626
|
}
|
|
611
627
|
const userDetails = getUser(req);
|
|
612
|
-
|
|
613
|
-
|
|
628
|
+
const currentDevice = req.device;
|
|
629
|
+
if (getParam(req, "userID") !== userDetails.userID ||
|
|
630
|
+
userDetails.userID !== device.owner ||
|
|
631
|
+
!currentDevice ||
|
|
632
|
+
currentDevice.owner !== userDetails.userID) {
|
|
633
|
+
res.sendStatus(403);
|
|
614
634
|
return;
|
|
615
635
|
}
|
|
616
636
|
const deviceList = await db.retrieveUserDeviceList([
|
|
@@ -623,6 +643,7 @@ export const getUserRouter = (db, tokenValidator, notify) => {
|
|
|
623
643
|
return;
|
|
624
644
|
}
|
|
625
645
|
await db.deleteDevice(device.deviceID);
|
|
646
|
+
disconnectDevices?.([device.deviceID]);
|
|
626
647
|
res.sendStatus(200);
|
|
627
648
|
});
|
|
628
649
|
router.post("/:id/devices", protect, async (req, res) => {
|
|
@@ -652,9 +673,21 @@ export const getUserRouter = (db, tokenValidator, notify) => {
|
|
|
652
673
|
return;
|
|
653
674
|
}
|
|
654
675
|
if (tokenValidator(stringify(token), TokenScopes.Device)) {
|
|
676
|
+
if (!(await verifyDevicePayloadPreKeySignature(deviceData))) {
|
|
677
|
+
res.status(400).send({
|
|
678
|
+
error: "Signed prekey signature is invalid.",
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
655
682
|
const userDevices = await db.retrieveUserDeviceList([
|
|
656
683
|
userDetails.userID,
|
|
657
684
|
]);
|
|
685
|
+
if (userDevices.length >= MAX_ACTIVE_DEVICES_PER_USER) {
|
|
686
|
+
res.status(409).send({
|
|
687
|
+
error: `Each account is limited to ${String(MAX_ACTIVE_DEVICES_PER_USER)} active devices. Remove an old device before adding another.`,
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
658
691
|
if (userDevices.length === 0) {
|
|
659
692
|
try {
|
|
660
693
|
const device = await db.createDevice(userDetails.userID, deviceData);
|
|
@@ -765,9 +798,7 @@ export const getUserRouter = (db, tokenValidator, notify) => {
|
|
|
765
798
|
});
|
|
766
799
|
return;
|
|
767
800
|
}
|
|
768
|
-
|
|
769
|
-
// Older clients may still satisfy this with an approval-side passkey.
|
|
770
|
-
const approvedByPasskeyID = pending.requesterPasskeyID ?? req.passkey?.passkeyID;
|
|
801
|
+
const approvedByPasskeyID = pending.requesterPasskeyID;
|
|
771
802
|
if (approvedByPasskeyID) {
|
|
772
803
|
const passkey = await db.retrievePasskeyInternal(approvedByPasskeyID);
|
|
773
804
|
if (!passkey || passkey.userID !== userID) {
|