@vex-chat/spire 3.0.0 → 4.0.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.
Files changed (82) hide show
  1. package/README.md +9 -8
  2. package/dist/Database.d.ts +12 -11
  3. package/dist/Database.js +161 -118
  4. package/dist/Database.js.map +1 -1
  5. package/dist/Spire.d.ts +4 -3
  6. package/dist/Spire.js +176 -92
  7. package/dist/Spire.js.map +1 -1
  8. package/dist/db/schema.d.ts +0 -2
  9. package/dist/migrations/2026-04-06_initial-schema.js +4 -5
  10. package/dist/migrations/2026-04-06_initial-schema.js.map +1 -1
  11. package/dist/migrations/{2026-04-14_argon2id-password-hashing.d.ts → 2026-07-14_query-indexes.d.ts} +1 -1
  12. package/dist/migrations/2026-07-14_query-indexes.js +31 -0
  13. package/dist/migrations/2026-07-14_query-indexes.js.map +1 -0
  14. package/dist/server/avatar.js +30 -4
  15. package/dist/server/avatar.js.map +1 -1
  16. package/dist/server/errors.js +8 -0
  17. package/dist/server/errors.js.map +1 -1
  18. package/dist/server/file.js +35 -12
  19. package/dist/server/file.js.map +1 -1
  20. package/dist/server/index.d.ts +8 -0
  21. package/dist/server/index.js +157 -41
  22. package/dist/server/index.js.map +1 -1
  23. package/dist/server/passkey.js +41 -33
  24. package/dist/server/passkey.js.map +1 -1
  25. package/dist/server/passkeyDevices.js +1 -0
  26. package/dist/server/passkeyDevices.js.map +1 -1
  27. package/dist/server/{passkeySecondFactor.d.ts → password.d.ts} +1 -1
  28. package/dist/server/password.js +58 -0
  29. package/dist/server/password.js.map +1 -0
  30. package/dist/server/permissions.d.ts +1 -0
  31. package/dist/server/permissions.js +11 -2
  32. package/dist/server/permissions.js.map +1 -1
  33. package/dist/server/rateLimit.d.ts +11 -0
  34. package/dist/server/rateLimit.js +43 -4
  35. package/dist/server/rateLimit.js.map +1 -1
  36. package/dist/server/user.d.ts +1 -1
  37. package/dist/server/user.js +53 -17
  38. package/dist/server/user.js.map +1 -1
  39. package/dist/types/express.d.ts +3 -4
  40. package/dist/types/express.js.map +1 -1
  41. package/dist/utils/authJwt.d.ts +8 -0
  42. package/dist/utils/authJwt.js +25 -0
  43. package/dist/utils/authJwt.js.map +1 -0
  44. package/dist/utils/loadEnv.js +4 -0
  45. package/dist/utils/loadEnv.js.map +1 -1
  46. package/dist/utils/preKeySignature.d.ts +1 -1
  47. package/dist/utils/preKeySignature.js +7 -11
  48. package/dist/utils/preKeySignature.js.map +1 -1
  49. package/package.json +7 -7
  50. package/src/Database.ts +211 -152
  51. package/src/Spire.ts +418 -294
  52. package/src/__tests__/Database.spec.ts +126 -3
  53. package/src/__tests__/connectAuth.spec.ts +146 -4
  54. package/src/__tests__/deviceTokenRevalidation.spec.ts +57 -3
  55. package/src/__tests__/passkeyDevices.spec.ts +94 -0
  56. package/src/__tests__/passkeys.spec.ts +9 -54
  57. package/src/__tests__/password.spec.ts +223 -0
  58. package/src/__tests__/permissions.spec.ts +50 -0
  59. package/src/__tests__/preKeySignature.spec.ts +20 -3
  60. package/src/db/schema.ts +0 -2
  61. package/src/migrations/2026-04-06_initial-schema.ts +4 -5
  62. package/src/migrations/2026-07-14_query-indexes.ts +34 -0
  63. package/src/server/avatar.ts +29 -4
  64. package/src/server/errors.ts +7 -0
  65. package/src/server/file.ts +34 -13
  66. package/src/server/index.ts +286 -111
  67. package/src/server/passkey.ts +51 -35
  68. package/src/server/passkeyDevices.ts +1 -0
  69. package/src/server/password.ts +96 -0
  70. package/src/server/permissions.ts +20 -2
  71. package/src/server/rateLimit.ts +47 -4
  72. package/src/server/user.ts +58 -25
  73. package/src/types/express.ts +3 -4
  74. package/src/utils/authJwt.ts +34 -0
  75. package/src/utils/loadEnv.ts +4 -0
  76. package/src/utils/preKeySignature.ts +12 -15
  77. package/dist/migrations/2026-04-14_argon2id-password-hashing.js +0 -17
  78. package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +0 -1
  79. package/dist/server/passkeySecondFactor.js +0 -20
  80. package/dist/server/passkeySecondFactor.js.map +0 -1
  81. package/src/migrations/2026-04-14_argon2id-password-hashing.ts +0 -24
  82. package/src/server/passkeySecondFactor.ts +0 -27
@@ -10,6 +10,7 @@ const REQUIRED_ENV_VARS = ["DB_TYPE", "JWT_SECRET", "SPK"] as const;
10
10
  const NORMALIZED_ENV_VARS = [
11
11
  ...REQUIRED_ENV_VARS,
12
12
  "API_PORT",
13
+ "SPIRE_TRUST_PROXY_HOPS",
13
14
  "SPIRE_FIPS",
14
15
  "SPIRE_PASSKEY_RP_ID",
15
16
  "SPIRE_PASSKEY_RP_NAME",
@@ -82,6 +83,9 @@ export function validateSpireRuntimeEnv(
82
83
  "JWT_SECRET must be set. Generate one with `pnpm --filter @vex-chat/spire gen-spk` or `gen-spk-fips`.",
83
84
  );
84
85
  }
86
+ if (jwtSecret.length < 32) {
87
+ throw new Error("JWT_SECRET must contain at least 32 characters.");
88
+ }
85
89
  if (jwtSecret === spk) {
86
90
  throw new Error("JWT_SECRET must be separate from SPK.");
87
91
  }
@@ -6,13 +6,7 @@
6
6
 
7
7
  import type { DevicePayload, PreKeysWS } from "@vex-chat/types";
8
8
 
9
- import {
10
- getCryptoProfile,
11
- xConcat,
12
- xConstants,
13
- xEncode,
14
- XUtils,
15
- } from "@vex-chat/crypto";
9
+ import { xPreKeySignaturePayload, XUtils } from "@vex-chat/crypto";
16
10
 
17
11
  import { spireXSignOpenAsync } from "./spireXSignOpenAsync.ts";
18
12
 
@@ -24,6 +18,7 @@ export async function verifyDevicePayloadPreKeySignature(
24
18
  XUtils.decodeHex(payload.preKey),
25
19
  XUtils.decodeHex(payload.preKeySignature),
26
20
  payload.signKey,
21
+ "signed",
27
22
  );
28
23
  } catch {
29
24
  return false;
@@ -33,20 +28,21 @@ export async function verifyDevicePayloadPreKeySignature(
33
28
  export async function verifyPreKeyWsSignature(
34
29
  preKey: PreKeysWS,
35
30
  signKeyHex: string,
31
+ kind: "one-time" | "signed",
36
32
  ): Promise<boolean> {
37
- return verifySignedPreKey(preKey.publicKey, preKey.signature, signKeyHex);
38
- }
39
-
40
- function preKeySignPayload(publicKey: Uint8Array): Uint8Array {
41
- return getCryptoProfile() === "fips"
42
- ? xConcat(new Uint8Array([0xa1]), publicKey)
43
- : xEncode(xConstants.CURVE, publicKey);
33
+ return verifySignedPreKey(
34
+ preKey.publicKey,
35
+ preKey.signature,
36
+ signKeyHex,
37
+ kind,
38
+ );
44
39
  }
45
40
 
46
41
  async function verifySignedPreKey(
47
42
  publicKey: Uint8Array,
48
43
  signature: Uint8Array,
49
44
  signKeyHex: string,
45
+ kind: "one-time" | "signed",
50
46
  ): Promise<boolean> {
51
47
  try {
52
48
  const opened = await spireXSignOpenAsync(
@@ -54,7 +50,8 @@ async function verifySignedPreKey(
54
50
  XUtils.decodeHex(signKeyHex),
55
51
  );
56
52
  return Boolean(
57
- opened && XUtils.bytesEqual(opened, preKeySignPayload(publicKey)),
53
+ opened &&
54
+ XUtils.bytesEqual(opened, xPreKeySignaturePayload(publicKey, kind)),
58
55
  );
59
56
  } catch {
60
57
  return false;
@@ -1,17 +0,0 @@
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 { sql } from "kysely";
7
- export async function down(db) {
8
- await db.schema.alterTable("users").dropColumn("hashAlgo").execute();
9
- }
10
- export async function up(db) {
11
- await db.schema
12
- .alterTable("users")
13
- .addColumn("hashAlgo", "varchar(16)", (cb) => cb.defaultTo("pbkdf2").notNull())
14
- .execute();
15
- await sql `UPDATE users SET hashAlgo = 'pbkdf2' WHERE hashAlgo = 'pbkdf2'`.execute(db);
16
- }
17
- //# sourceMappingURL=2026-04-14_argon2id-password-hashing.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"2026-04-14_argon2id-password-hashing.js","sourceRoot":"","sources":["../../src/migrations/2026-04-14_argon2id-password-hashing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAe,GAAG,EAAE,MAAM,QAAQ,CAAC;AAE1C,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,EAAmB;IAC1C,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC;AACzE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,EAAmB;IACxC,MAAM,EAAE,CAAC,MAAM;SACV,UAAU,CAAC,OAAO,CAAC;SACnB,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,CACzC,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,CACnC;SACA,OAAO,EAAE,CAAC;IAEf,MAAM,GAAG,CAAA,gEAAgE,CAAC,OAAO,CAC7E,EAAE,CACL,CAAC;AACN,CAAC"}
@@ -1,20 +0,0 @@
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
- export async function passkeySecondFactorError(db, userID, passkeyID, mismatchError) {
7
- const passkeys = await db.retrievePasskeysByUser(userID);
8
- if (passkeys.length === 0) {
9
- return null;
10
- }
11
- if (!passkeyID) {
12
- return "Passkey verification required.";
13
- }
14
- const passkey = await db.retrievePasskeyInternal(passkeyID);
15
- if (!passkey || passkey.userID !== userID) {
16
- return mismatchError;
17
- }
18
- return null;
19
- }
20
- //# sourceMappingURL=passkeySecondFactor.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"passkeySecondFactor.js","sourceRoot":"","sources":["../../src/server/passkeySecondFactor.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC1C,EAAY,EACZ,MAAc,EACd,SAA6B,EAC7B,aAAqB;IAErB,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,CAAC,SAAS,EAAE,CAAC;QACb,OAAO,gCAAgC,CAAC;IAC5C,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAC5D,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QACxC,OAAO,aAAa,CAAC;IACzB,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC"}
@@ -1,24 +0,0 @@
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 Kysely, sql } from "kysely";
8
-
9
- export async function down(db: Kysely<unknown>): Promise<void> {
10
- await db.schema.alterTable("users").dropColumn("hashAlgo").execute();
11
- }
12
-
13
- export async function up(db: Kysely<unknown>): Promise<void> {
14
- await db.schema
15
- .alterTable("users")
16
- .addColumn("hashAlgo", "varchar(16)", (cb) =>
17
- cb.defaultTo("pbkdf2").notNull(),
18
- )
19
- .execute();
20
-
21
- await sql`UPDATE users SET hashAlgo = 'pbkdf2' WHERE hashAlgo = 'pbkdf2'`.execute(
22
- db,
23
- );
24
- }
@@ -1,27 +0,0 @@
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
-
9
- export async function passkeySecondFactorError(
10
- db: Database,
11
- userID: string,
12
- passkeyID: string | undefined,
13
- mismatchError: string,
14
- ): Promise<null | string> {
15
- const passkeys = await db.retrievePasskeysByUser(userID);
16
- if (passkeys.length === 0) {
17
- return null;
18
- }
19
- if (!passkeyID) {
20
- return "Passkey verification required.";
21
- }
22
- const passkey = await db.retrievePasskeyInternal(passkeyID);
23
- if (!passkey || passkey.userID !== userID) {
24
- return mismatchError;
25
- }
26
- return null;
27
- }