@vex-chat/spire 1.11.4 → 2.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 (49) hide show
  1. package/README.md +9 -7
  2. package/dist/ClientManager.d.ts +2 -1
  3. package/dist/ClientManager.js +4 -1
  4. package/dist/ClientManager.js.map +1 -1
  5. package/dist/Database.d.ts +12 -0
  6. package/dist/Database.js +100 -16
  7. package/dist/Database.js.map +1 -1
  8. package/dist/NotificationService.d.ts +4 -0
  9. package/dist/NotificationService.js +54 -10
  10. package/dist/NotificationService.js.map +1 -1
  11. package/dist/Spire.d.ts +3 -0
  12. package/dist/Spire.js +158 -4
  13. package/dist/Spire.js.map +1 -1
  14. package/dist/migrations/2026-05-03_passkeys.js +2 -2
  15. package/dist/migrations/2026-05-03_passkeys.js.map +1 -1
  16. package/dist/server/index.d.ts +2 -2
  17. package/dist/server/index.js +18 -4
  18. package/dist/server/index.js.map +1 -1
  19. package/dist/server/passkey.js +24 -16
  20. package/dist/server/passkey.js.map +1 -1
  21. package/dist/server/passkeyDevices.d.ts +2 -2
  22. package/dist/server/passkeyDevices.js +14 -15
  23. package/dist/server/passkeyDevices.js.map +1 -1
  24. package/dist/server/rateLimit.d.ts +6 -2
  25. package/dist/server/rateLimit.js +13 -10
  26. package/dist/server/rateLimit.js.map +1 -1
  27. package/dist/server/user.d.ts +14 -0
  28. package/dist/server/user.js +48 -0
  29. package/dist/server/user.js.map +1 -1
  30. package/dist/utils/loadEnv.d.ts +3 -0
  31. package/dist/utils/loadEnv.js +63 -3
  32. package/dist/utils/loadEnv.js.map +1 -1
  33. package/package.json +6 -6
  34. package/src/ClientManager.ts +7 -0
  35. package/src/Database.ts +136 -16
  36. package/src/NotificationService.ts +84 -11
  37. package/src/Spire.ts +233 -2
  38. package/src/__tests__/Database.spec.ts +191 -2
  39. package/src/__tests__/loadEnv.spec.ts +91 -0
  40. package/src/__tests__/notifyFanout.spec.ts +109 -0
  41. package/src/__tests__/passkeys.spec.ts +46 -0
  42. package/src/__tests__/rateLimit.spec.ts +60 -0
  43. package/src/migrations/2026-05-03_passkeys.ts +2 -2
  44. package/src/server/index.ts +26 -3
  45. package/src/server/passkey.ts +25 -17
  46. package/src/server/passkeyDevices.ts +17 -14
  47. package/src/server/rateLimit.ts +18 -10
  48. package/src/server/user.ts +66 -0
  49. package/src/utils/loadEnv.ts +79 -3
@@ -6,16 +6,92 @@
6
6
 
7
7
  import { config } from "dotenv";
8
8
 
9
+ const REQUIRED_ENV_VARS = ["DB_TYPE", "JWT_SECRET", "SPK"] as const;
10
+ const NORMALIZED_ENV_VARS = [
11
+ ...REQUIRED_ENV_VARS,
12
+ "API_PORT",
13
+ "SPIRE_FIPS",
14
+ "SPIRE_PASSKEY_RP_ID",
15
+ "SPIRE_PASSKEY_RP_NAME",
16
+ "SPIRE_PASSKEY_ORIGINS",
17
+ "SPIRE_PASSKEY_IOS_APP_IDS",
18
+ "SPIRE_PASSKEY_ANDROID_PACKAGE",
19
+ "SPIRE_PASSKEY_ANDROID_FINGERPRINTS",
20
+ ] as const;
21
+ const HEX_BYTES_RE = /^(?:[0-9a-fA-F]{2})+$/;
22
+ const TWEETNACL_SPK_HEX_LENGTH = 128;
23
+
24
+ export function isSpireFipsEnabled(value: string | undefined): boolean {
25
+ const normalized = value == null ? "" : normalizeEnvValue(value);
26
+ return normalized === "1" || normalized === "true";
27
+ }
28
+
9
29
  /* Populate process.env with vars from .env and verify required vars are present. */
10
30
  export function loadEnv(): void {
11
31
  config();
12
- const requiredEnvVars: string[] = ["DB_TYPE", "JWT_SECRET", "SPK"];
13
- for (const required of requiredEnvVars) {
14
- if (process.env[required] === undefined) {
32
+ normalizeConfiguredEnv();
33
+ for (const required of REQUIRED_ENV_VARS) {
34
+ if (!process.env[required]) {
15
35
  process.stderr.write(
16
36
  `Required environment variable '${required}' is not set. Please consult the README.\n`,
17
37
  );
18
38
  process.exit(1);
19
39
  }
20
40
  }
41
+ validateSpireRuntimeEnv(process.env);
42
+ }
43
+
44
+ export function normalizeEnvValue(value: string): string {
45
+ const trimmed = value.trim();
46
+ if (trimmed.length < 2) {
47
+ return trimmed;
48
+ }
49
+ const first = trimmed[0];
50
+ const last = trimmed[trimmed.length - 1];
51
+ if ((first === `"` && last === `"`) || (first === `'` && last === `'`)) {
52
+ return trimmed.slice(1, -1);
53
+ }
54
+ return trimmed;
55
+ }
56
+
57
+ export function validateSpireRuntimeEnv(
58
+ env: Record<string, string | undefined>,
59
+ ): void {
60
+ const spk = normalizeEnvValue(env["SPK"] ?? "");
61
+ const jwtSecret = normalizeEnvValue(env["JWT_SECRET"] ?? "");
62
+ if (!HEX_BYTES_RE.test(spk)) {
63
+ throw new Error(
64
+ "SPK must be an even-length hex string. Generate one with `pnpm --filter @vex-chat/spire gen-spk` or `gen-spk-fips`.",
65
+ );
66
+ }
67
+
68
+ if (isSpireFipsEnabled(env["SPIRE_FIPS"])) {
69
+ if (spk.length === TWEETNACL_SPK_HEX_LENGTH) {
70
+ throw new Error(
71
+ "SPIRE_FIPS=true requires an SPK from `pnpm --filter @vex-chat/spire gen-spk-fips`; the configured SPK looks like a tweetnacl key.",
72
+ );
73
+ }
74
+ } else if (spk.length !== TWEETNACL_SPK_HEX_LENGTH) {
75
+ throw new Error(
76
+ "SPIRE_FIPS is not enabled, so SPK must be 128 hex characters from `pnpm --filter @vex-chat/spire gen-spk`.",
77
+ );
78
+ }
79
+
80
+ if (jwtSecret.length === 0) {
81
+ throw new Error(
82
+ "JWT_SECRET must be set. Generate one with `pnpm --filter @vex-chat/spire gen-spk` or `gen-spk-fips`.",
83
+ );
84
+ }
85
+ if (jwtSecret === spk) {
86
+ throw new Error("JWT_SECRET must be separate from SPK.");
87
+ }
88
+ }
89
+
90
+ function normalizeConfiguredEnv(): void {
91
+ for (const name of NORMALIZED_ENV_VARS) {
92
+ const value = process.env[name];
93
+ if (value != null) {
94
+ process.env[name] = normalizeEnvValue(value);
95
+ }
96
+ }
21
97
  }