@vex-chat/spire 4.0.0 → 4.1.1
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/Database.d.ts +8 -0
- package/dist/Database.js +68 -0
- package/dist/Database.js.map +1 -1
- package/dist/Spire.js +1 -1
- package/dist/Spire.js.map +1 -1
- package/dist/server/avatar.js +3 -2
- package/dist/server/avatar.js.map +1 -1
- package/dist/server/cliPasskeyPage.js +580 -91
- package/dist/server/cliPasskeyPage.js.map +1 -1
- package/dist/server/index.js +163 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/passkey.js +326 -3
- package/dist/server/passkey.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/package.json +2 -2
- package/src/Database.ts +83 -0
- package/src/Spire.ts +1 -1
- package/src/__tests__/browserPasskeyAuthentication.spec.ts +192 -0
- package/src/__tests__/browserPasskeyRegistration.spec.ts +182 -0
- package/src/__tests__/cliPasskeyPage.spec.ts +17 -1
- package/src/__tests__/serverManagement.spec.ts +262 -0
- package/src/server/avatar.ts +3 -2
- package/src/server/cliPasskeyPage.ts +580 -91
- package/src/server/index.ts +211 -31
- package/src/server/passkey.ts +480 -3
- package/src/server/serverIcon.ts +199 -0
package/src/server/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
userHasPermission,
|
|
54
54
|
} from "./permissions.ts";
|
|
55
55
|
import { globalLimiter, keyBundleLimiter, uploadLimiter } from "./rateLimit.ts";
|
|
56
|
+
import { deleteServerIconFile, getServerIconRouter } from "./serverIcon.ts";
|
|
56
57
|
import { getUserRouter } from "./user.ts";
|
|
57
58
|
import { censorUser, getParam, getUser } from "./utils.ts";
|
|
58
59
|
import { getWellKnownRouter } from "./wellKnown.ts";
|
|
@@ -66,6 +67,7 @@ export const ALLOWED_IMAGE_TYPES = [
|
|
|
66
67
|
"image/gif",
|
|
67
68
|
"image/apng",
|
|
68
69
|
"image/avif",
|
|
70
|
+
"image/webp",
|
|
69
71
|
];
|
|
70
72
|
|
|
71
73
|
// ── Zod schemas for trust-boundary validation ──────────────────────────
|
|
@@ -75,7 +77,15 @@ const invitePayload = z.object({
|
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
const channelPayload = z.object({
|
|
78
|
-
name: z.string().min(1).max(
|
|
80
|
+
name: z.string().trim().min(1).max(100),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const serverUpdatePayload = z.object({
|
|
84
|
+
name: z.string().trim().min(1).max(100),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const permissionRolePayload = z.object({
|
|
88
|
+
powerLevel: z.union([z.literal(0), z.literal(50), z.literal(100)]),
|
|
79
89
|
});
|
|
80
90
|
|
|
81
91
|
const deviceListPayload = z.array(z.string().min(1).max(128)).max(256);
|
|
@@ -283,7 +293,7 @@ export const msgpackParser: express.RequestHandler = (req, res, next) => {
|
|
|
283
293
|
next();
|
|
284
294
|
};
|
|
285
295
|
|
|
286
|
-
const directories = ["files", "avatars"];
|
|
296
|
+
const directories = ["files", "avatars", "server-icons"];
|
|
287
297
|
for (const dir of directories) {
|
|
288
298
|
if (!fs.existsSync(dir)) {
|
|
289
299
|
fs.mkdirSync(dir);
|
|
@@ -304,6 +314,20 @@ export const initApp = (
|
|
|
304
314
|
) => void,
|
|
305
315
|
disconnectDevices?: (deviceIDs: string[]) => void,
|
|
306
316
|
) => {
|
|
317
|
+
const notifyServerChange = async (
|
|
318
|
+
serverID: string,
|
|
319
|
+
additionalUserIDs: readonly string[] = [],
|
|
320
|
+
): Promise<void> => {
|
|
321
|
+
const affectedUsers = await db.retrieveAffectedUsers(serverID);
|
|
322
|
+
const userIDs = new Set([
|
|
323
|
+
...affectedUsers.map((user) => user.userID),
|
|
324
|
+
...additionalUserIDs,
|
|
325
|
+
]);
|
|
326
|
+
for (const userID of userIDs) {
|
|
327
|
+
notify(userID, "serverChange", crypto.randomUUID(), serverID);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
307
331
|
// INIT ROUTERS
|
|
308
332
|
const userRouter = getUserRouter(
|
|
309
333
|
db,
|
|
@@ -318,6 +342,7 @@ export const initApp = (
|
|
|
318
342
|
const inviteRouter = getInviteRouter(db, tokenValidator, notify);
|
|
319
343
|
const passkeyRouter = getPasskeyRouter(db);
|
|
320
344
|
const passwordRouter = getPasswordRouter(db);
|
|
345
|
+
const serverIconRouter = getServerIconRouter(db, notifyServerChange);
|
|
321
346
|
const passkeyDeviceRouter = getPasskeyDeviceRouter(
|
|
322
347
|
db,
|
|
323
348
|
notify,
|
|
@@ -419,6 +444,54 @@ export const initApp = (
|
|
|
419
444
|
}
|
|
420
445
|
});
|
|
421
446
|
|
|
447
|
+
api.patch("/server/:id", protect, async (req, res) => {
|
|
448
|
+
const parsed = serverUpdatePayload.safeParse(req.body);
|
|
449
|
+
if (!parsed.success) {
|
|
450
|
+
res.status(400).json({
|
|
451
|
+
error: "Invalid server update",
|
|
452
|
+
issues: parsed.error.issues,
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const serverID = getParam(req, "id");
|
|
458
|
+
const permissions = await db.retrievePermissions(
|
|
459
|
+
getUser(req).userID,
|
|
460
|
+
"server",
|
|
461
|
+
);
|
|
462
|
+
if (!hasPermission(permissions, serverID, POWER_LEVELS.CREATE)) {
|
|
463
|
+
res.sendStatus(403);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const server = await db.updateServer(serverID, parsed.data);
|
|
468
|
+
if (!server) {
|
|
469
|
+
res.sendStatus(404);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
await notifyServerChange(serverID);
|
|
473
|
+
res.send(msgpack.encode(server));
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
api.post("/servers", protect, async (req, res) => {
|
|
477
|
+
const parsed = serverUpdatePayload.safeParse(req.body);
|
|
478
|
+
if (!parsed.success) {
|
|
479
|
+
res.status(400).json({
|
|
480
|
+
error: "Invalid server",
|
|
481
|
+
issues: parsed.error.issues,
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const server = await db.createServer(
|
|
487
|
+
parsed.data.name,
|
|
488
|
+
getUser(req).userID,
|
|
489
|
+
);
|
|
490
|
+
res.send(msgpack.encode(server));
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Compatibility route for clients released before server names moved into
|
|
494
|
+
// the request body. New clients use POST /servers so Unicode names work.
|
|
422
495
|
api.post("/server/:name", protect, async (req, res) => {
|
|
423
496
|
const userDetails = getUser(req);
|
|
424
497
|
const encodedName = getParam(req, "name");
|
|
@@ -537,11 +610,22 @@ export const initApp = (
|
|
|
537
610
|
"server",
|
|
538
611
|
);
|
|
539
612
|
if (hasPermission(permissions, serverID, POWER_LEVELS.DELETE)) {
|
|
613
|
+
const server = await db.retrieveServer(serverID);
|
|
614
|
+
if (!server) {
|
|
615
|
+
res.sendStatus(404);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const affectedUsers = await db.retrieveAffectedUsers(serverID);
|
|
540
619
|
await db.deleteServer(serverID);
|
|
620
|
+
if (server.icon) await deleteServerIconFile(server.icon);
|
|
621
|
+
await notifyServerChange(
|
|
622
|
+
serverID,
|
|
623
|
+
affectedUsers.map((user) => user.userID),
|
|
624
|
+
);
|
|
541
625
|
res.sendStatus(200);
|
|
542
626
|
return;
|
|
543
627
|
}
|
|
544
|
-
res.sendStatus(
|
|
628
|
+
res.sendStatus(403);
|
|
545
629
|
});
|
|
546
630
|
|
|
547
631
|
api.post("/server/:id/channels", protect, async (req, res) => {
|
|
@@ -564,20 +648,10 @@ export const initApp = (
|
|
|
564
648
|
if (hasPermission(permissions, serverID, POWER_LEVELS.CREATE)) {
|
|
565
649
|
const channel = await db.createChannel(name, serverID);
|
|
566
650
|
res.send(msgpack.encode(channel));
|
|
567
|
-
|
|
568
|
-
const affectedUsers = await db.retrieveAffectedUsers(serverID);
|
|
569
|
-
// tell everyone about server change
|
|
570
|
-
for (const user of affectedUsers) {
|
|
571
|
-
notify(
|
|
572
|
-
user.userID,
|
|
573
|
-
"serverChange",
|
|
574
|
-
crypto.randomUUID(),
|
|
575
|
-
serverID,
|
|
576
|
-
);
|
|
577
|
-
}
|
|
651
|
+
await notifyServerChange(serverID);
|
|
578
652
|
return;
|
|
579
653
|
}
|
|
580
|
-
res.sendStatus(
|
|
654
|
+
res.sendStatus(403);
|
|
581
655
|
});
|
|
582
656
|
|
|
583
657
|
api.get("/server/:id/channels", protect, async (req, res) => {
|
|
@@ -623,6 +697,42 @@ export const initApp = (
|
|
|
623
697
|
res.send(msgpack.encode(permissions));
|
|
624
698
|
});
|
|
625
699
|
|
|
700
|
+
api.patch("/channel/:id", protect, async (req, res) => {
|
|
701
|
+
const parsed = channelPayload.safeParse(req.body);
|
|
702
|
+
if (!parsed.success) {
|
|
703
|
+
res.status(400).json({
|
|
704
|
+
error: "Invalid channel update",
|
|
705
|
+
issues: parsed.error.issues,
|
|
706
|
+
});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const channelID = getParam(req, "id");
|
|
711
|
+
const channel = await db.retrieveChannel(channelID);
|
|
712
|
+
if (!channel) {
|
|
713
|
+
res.sendStatus(404);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const permissions = await db.retrievePermissions(
|
|
717
|
+
getUser(req).userID,
|
|
718
|
+
"server",
|
|
719
|
+
);
|
|
720
|
+
if (
|
|
721
|
+
!hasPermission(permissions, channel.serverID, POWER_LEVELS.DELETE)
|
|
722
|
+
) {
|
|
723
|
+
res.sendStatus(403);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const updated = await db.updateChannel(channelID, parsed.data.name);
|
|
728
|
+
if (!updated) {
|
|
729
|
+
res.sendStatus(404);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
await notifyServerChange(channel.serverID);
|
|
733
|
+
res.send(msgpack.encode(updated));
|
|
734
|
+
});
|
|
735
|
+
|
|
626
736
|
api.delete("/channel/:id", protect, async (req, res) => {
|
|
627
737
|
const channelID = getParam(req, "id");
|
|
628
738
|
const userDetails = getUser(req);
|
|
@@ -643,26 +753,19 @@ export const initApp = (
|
|
|
643
753
|
permission.resourceID === channel.serverID &&
|
|
644
754
|
permission.powerLevel >= POWER_LEVELS.DELETE
|
|
645
755
|
) {
|
|
646
|
-
await db.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
);
|
|
653
|
-
// tell everyone about server change
|
|
654
|
-
for (const user of affectedUsers) {
|
|
655
|
-
notify(
|
|
656
|
-
user.userID,
|
|
657
|
-
"serverChange",
|
|
658
|
-
crypto.randomUUID(),
|
|
659
|
-
channel.serverID,
|
|
660
|
-
);
|
|
756
|
+
const deleted = await db.deleteChannelIfNotLast(channelID);
|
|
757
|
+
if (!deleted) {
|
|
758
|
+
res.status(409).json({
|
|
759
|
+
error: "A server must have at least one channel.",
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
661
762
|
}
|
|
763
|
+
await notifyServerChange(channel.serverID);
|
|
764
|
+
res.sendStatus(200);
|
|
662
765
|
return;
|
|
663
766
|
}
|
|
664
767
|
}
|
|
665
|
-
res.sendStatus(
|
|
768
|
+
res.sendStatus(403);
|
|
666
769
|
});
|
|
667
770
|
|
|
668
771
|
api.get("/channel/:id", protect, async (req, res) => {
|
|
@@ -682,6 +785,56 @@ export const initApp = (
|
|
|
682
785
|
}
|
|
683
786
|
});
|
|
684
787
|
|
|
788
|
+
api.patch("/permission/:permissionID", protect, async (req, res) => {
|
|
789
|
+
const parsed = permissionRolePayload.safeParse(req.body);
|
|
790
|
+
if (!parsed.success) {
|
|
791
|
+
res.status(400).json({
|
|
792
|
+
error: "Invalid role",
|
|
793
|
+
issues: parsed.error.issues,
|
|
794
|
+
});
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const target = await db.retrievePermission(
|
|
799
|
+
getParam(req, "permissionID"),
|
|
800
|
+
);
|
|
801
|
+
if (!target) {
|
|
802
|
+
res.sendStatus(404);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (target.resourceType !== "server") {
|
|
806
|
+
res.sendStatus(400);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const actor = getUser(req);
|
|
811
|
+
if (target.userID === actor.userID) {
|
|
812
|
+
res.status(400).json({
|
|
813
|
+
error: "Use another owner to change your role.",
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const actorPermissions = await db.retrievePermissions(
|
|
818
|
+
actor.userID,
|
|
819
|
+
"server",
|
|
820
|
+
);
|
|
821
|
+
if (!hasPermission(actorPermissions, target.resourceID, 100)) {
|
|
822
|
+
res.sendStatus(403);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const updated = await db.updatePermissionPowerLevel(
|
|
827
|
+
target.permissionID,
|
|
828
|
+
parsed.data.powerLevel,
|
|
829
|
+
);
|
|
830
|
+
if (!updated) {
|
|
831
|
+
res.sendStatus(404);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
await notifyServerChange(target.resourceID);
|
|
835
|
+
res.send(msgpack.encode(updated));
|
|
836
|
+
});
|
|
837
|
+
|
|
685
838
|
api.delete("/permission/:permissionID", protect, async (req, res) => {
|
|
686
839
|
const permissionID = getParam(req, "permissionID");
|
|
687
840
|
const userDetails = getUser(req);
|
|
@@ -704,7 +857,32 @@ export const initApp = (
|
|
|
704
857
|
POWER_LEVELS.DELETE,
|
|
705
858
|
)
|
|
706
859
|
) {
|
|
860
|
+
if (
|
|
861
|
+
permToDelete.resourceType === "server" &&
|
|
862
|
+
permToDelete.powerLevel >= 100
|
|
863
|
+
) {
|
|
864
|
+
const resourcePermissions =
|
|
865
|
+
await db.retrievePermissionsByResourceID(
|
|
866
|
+
permToDelete.resourceID,
|
|
867
|
+
);
|
|
868
|
+
const hasAnotherOwner = resourcePermissions.some(
|
|
869
|
+
(permission) =>
|
|
870
|
+
permission.permissionID !== permToDelete.permissionID &&
|
|
871
|
+
permission.powerLevel >= 100,
|
|
872
|
+
);
|
|
873
|
+
if (!hasAnotherOwner) {
|
|
874
|
+
res.status(409).json({
|
|
875
|
+
error: "Delete the server instead of leaving it without an owner.",
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
707
880
|
await db.deletePermission(permToDelete.permissionID);
|
|
881
|
+
if (permToDelete.resourceType === "server") {
|
|
882
|
+
await notifyServerChange(permToDelete.resourceID, [
|
|
883
|
+
permToDelete.userID,
|
|
884
|
+
]);
|
|
885
|
+
}
|
|
708
886
|
res.sendStatus(200);
|
|
709
887
|
return;
|
|
710
888
|
}
|
|
@@ -1199,6 +1377,8 @@ export const initApp = (
|
|
|
1199
1377
|
|
|
1200
1378
|
api.use("/avatar", avatarRouter);
|
|
1201
1379
|
|
|
1380
|
+
api.use("/server-icon", serverIconRouter);
|
|
1381
|
+
|
|
1202
1382
|
api.use("/invite", inviteRouter);
|
|
1203
1383
|
|
|
1204
1384
|
setupDocs(api);
|