@vex-chat/spire 0.7.4 → 1.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 (202) hide show
  1. package/README.md +82 -26
  2. package/dist/ClientManager.d.ts +24 -25
  3. package/dist/ClientManager.js +232 -509
  4. package/dist/ClientManager.js.map +1 -0
  5. package/dist/Database.d.ts +49 -41
  6. package/dist/Database.js +698 -716
  7. package/dist/Database.js.map +1 -0
  8. package/dist/Spire.d.ts +23 -15
  9. package/dist/Spire.js +518 -218
  10. package/dist/Spire.js.map +1 -0
  11. package/dist/__tests__/Database.spec.js +113 -73
  12. package/dist/__tests__/Database.spec.js.map +1 -0
  13. package/dist/db/schema.d.ts +134 -0
  14. package/dist/db/schema.js +2 -0
  15. package/dist/db/schema.js.map +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +3 -5
  18. package/dist/index.js.map +1 -0
  19. package/dist/middleware/validate.d.ts +12 -0
  20. package/dist/middleware/validate.js +35 -0
  21. package/dist/middleware/validate.js.map +1 -0
  22. package/dist/migrations/2026-04-06_initial-schema.d.ts +3 -0
  23. package/dist/migrations/2026-04-06_initial-schema.js +192 -0
  24. package/dist/migrations/2026-04-06_initial-schema.js.map +1 -0
  25. package/dist/run.js +26 -21
  26. package/dist/run.js.map +1 -0
  27. package/dist/server/avatar.d.ts +3 -4
  28. package/dist/server/avatar.js +85 -67
  29. package/dist/server/avatar.js.map +1 -0
  30. package/dist/server/errors.d.ts +59 -0
  31. package/dist/server/errors.js +94 -0
  32. package/dist/server/errors.js.map +1 -0
  33. package/dist/server/file.d.ts +3 -4
  34. package/dist/server/file.js +101 -61
  35. package/dist/server/file.js.map +1 -0
  36. package/dist/server/index.d.ts +9 -6
  37. package/dist/server/index.js +595 -70
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/invite.d.ts +4 -5
  40. package/dist/server/invite.js +21 -103
  41. package/dist/server/invite.js.map +1 -0
  42. package/dist/server/openapi.d.ts +2 -0
  43. package/dist/server/openapi.js +40 -0
  44. package/dist/server/openapi.js.map +1 -0
  45. package/dist/server/permissions.d.ts +16 -0
  46. package/dist/server/permissions.js +22 -0
  47. package/dist/server/permissions.js.map +1 -0
  48. package/dist/server/rateLimit.d.ts +28 -0
  49. package/dist/server/rateLimit.js +58 -0
  50. package/dist/server/rateLimit.js.map +1 -0
  51. package/dist/server/user.d.ts +4 -7
  52. package/dist/server/user.js +66 -76
  53. package/dist/server/user.js.map +1 -0
  54. package/dist/server/utils.d.ts +35 -7
  55. package/dist/server/utils.js +50 -6
  56. package/dist/server/utils.js.map +1 -0
  57. package/dist/types/express.d.ts +20 -0
  58. package/dist/types/express.js +2 -0
  59. package/dist/types/express.js.map +1 -0
  60. package/dist/utils/createLogger.js +13 -19
  61. package/dist/utils/createLogger.js.map +1 -0
  62. package/dist/utils/createUint8UUID.js +6 -10
  63. package/dist/utils/createUint8UUID.js.map +1 -0
  64. package/dist/utils/jwtSecret.d.ts +7 -0
  65. package/dist/utils/jwtSecret.js +15 -0
  66. package/dist/utils/jwtSecret.js.map +1 -0
  67. package/dist/utils/loadEnv.js +7 -22
  68. package/dist/utils/loadEnv.js.map +1 -0
  69. package/dist/utils/msgpack.d.ts +2 -0
  70. package/dist/utils/msgpack.js +4 -0
  71. package/dist/utils/msgpack.js.map +1 -0
  72. package/package.json +91 -63
  73. package/src/ClientManager.ts +434 -0
  74. package/src/Database.ts +925 -0
  75. package/src/Spire.ts +878 -0
  76. package/src/__tests__/Database.spec.ts +167 -0
  77. package/src/ambient-modules.d.ts +1 -0
  78. package/src/db/schema.ts +165 -0
  79. package/src/index.ts +3 -0
  80. package/src/middleware/validate.ts +38 -0
  81. package/src/migrations/2026-04-06_initial-schema.ts +218 -0
  82. package/src/run.ts +37 -0
  83. package/src/server/avatar.ts +141 -0
  84. package/src/server/errors.ts +133 -0
  85. package/src/server/file.ts +172 -0
  86. package/src/server/index.ts +855 -0
  87. package/src/server/invite.ts +65 -0
  88. package/src/server/openapi.ts +51 -0
  89. package/src/server/permissions.ts +40 -0
  90. package/src/server/rateLimit.ts +86 -0
  91. package/src/server/user.ts +125 -0
  92. package/src/server/utils.ts +59 -0
  93. package/src/types/express.ts +23 -0
  94. package/src/utils/createLogger.ts +47 -0
  95. package/src/utils/createUint8UUID.ts +9 -0
  96. package/src/utils/jwtSecret.ts +16 -0
  97. package/src/utils/loadEnv.ts +15 -0
  98. package/src/utils/msgpack.ts +4 -0
  99. package/avatars/169d76cb-6e7c-4e24-8224-017673eed8ff +0 -0
  100. package/avatars/1cae8d0b-0c6b-4c73-b25c-2d2349a57122 +0 -0
  101. package/avatars/1d87bc2b-71fc-4818-8004-40d04093e5fc +0 -0
  102. package/avatars/1f2c3d62-8b4d-465a-9895-51a24caef00d +0 -0
  103. package/avatars/245ee7fc-1004-41ab-adab-a04c9ceb9d7a +0 -0
  104. package/avatars/2465c28c-bdaf-4fa2-b42a-d054f04dc39b +0 -0
  105. package/avatars/3900a674-a2dd-4996-a61d-8c29b3270f41 +0 -0
  106. package/avatars/3c3b9c77-ea50-45e7-bb25-65d6f3d2a219 +0 -0
  107. package/avatars/414a2ff4-ad2f-4a7d-aa27-3b09ad3522b2 +0 -0
  108. package/avatars/522fe504-f0ad-4ed4-9dc6-e5dc2338e531 +0 -0
  109. package/avatars/53e5eb29-e7d1-4d58-add9-d44f39f0cfb7 +0 -0
  110. package/avatars/54d3f757-1038-41c8-bfb9-efd37b6e8ebe +0 -0
  111. package/avatars/623e86d7-c49c-46f6-9b76-ca70c9dbc43b +0 -0
  112. package/avatars/66e2abae-60f5-4dd1-b9fb-297a4bedfeb0 +0 -0
  113. package/avatars/6f37980e-f6fa-4d6d-9206-24050b403f45 +0 -0
  114. package/avatars/80138ece-eb5c-4b20-817b-903d6b0f54ae +0 -0
  115. package/avatars/841c77d3-37c4-431b-be22-672888062874 +0 -0
  116. package/avatars/88051a61-2bda-4750-95a3-5fb0b4918149 +0 -0
  117. package/avatars/89540973-c421-4bf1-8b89-9f1eaa51c1b5 +0 -0
  118. package/avatars/8a802fad-8c99-4942-8f80-47cec600149c +0 -0
  119. package/avatars/90531d8a-907a-4a1a-ac45-c85e4acb0df9 +0 -0
  120. package/avatars/9b7d0da9-b8d6-4801-b128-9993f79f464a +0 -0
  121. package/avatars/9bc456f1-c4c4-48a1-b9e6-fd44dd744a72 +0 -0
  122. package/avatars/9cf878bf-7430-49ec-a47a-78f3c93793dd +0 -0
  123. package/avatars/9ee82847-6ad3-45e5-92b9-f474a6c54d96 +0 -0
  124. package/avatars/ab44c857-d81d-4c88-85db-32f9532e5376 +0 -0
  125. package/avatars/b396a8d2-dc14-48d2-aac4-3755dc637051 +0 -0
  126. package/avatars/b6ac11c5-a8b2-4e0a-995a-9c87f1e58787 +0 -0
  127. package/avatars/b79d6855-b738-434c-be32-809637e62b9b +0 -0
  128. package/avatars/bbcd0188-d6a5-48ae-90fb-be5ff30599ab +0 -0
  129. package/avatars/bc7a9e0e-4720-4a6e-a90d-c11fec94d380 +0 -0
  130. package/avatars/c1c4889f-8383-4041-8bdd-9fded4046f37 +0 -0
  131. package/avatars/c4c7203c-d93a-4749-ade2-17053acf1d2a +0 -0
  132. package/avatars/ca974a70-0a23-4668-8b80-c4304dc7f793 +0 -0
  133. package/avatars/cf119a0d-eb3f-4bed-905b-f14a876c3535 +0 -0
  134. package/avatars/d464b03d-30c2-49e3-a666-80aefa8a1b35 +0 -0
  135. package/avatars/da0eee89-82f0-4d45-ab48-7d2786b634c5 +0 -0
  136. package/avatars/de4c77a5-68e9-4bb2-b40d-d964bf377d61 +0 -0
  137. package/avatars/dea95395-7d0b-42aa-a9ed-40c7d4fb4c48 +0 -0
  138. package/avatars/edb30749-59ba-4aa2-9c52-0fb22048f4cf +0 -0
  139. package/avatars/f17c245a-af7d-445b-9365-49f7f54b1eeb +0 -0
  140. package/avatars/f1ee6a35-b262-4dbf-99f5-3d011e3b98ec +0 -0
  141. package/avatars/f802bdd0-345d-41f6-9184-0f30e1258fb3 +0 -0
  142. package/dist/migrations/20210103192527_users.d.ts +0 -3
  143. package/dist/migrations/20210103192527_users.js +0 -30
  144. package/dist/migrations/20210103193502_mail.d.ts +0 -3
  145. package/dist/migrations/20210103193502_mail.js +0 -35
  146. package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
  147. package/dist/migrations/20210103193525_preKeys.js +0 -30
  148. package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
  149. package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
  150. package/dist/migrations/20210103193615_servers.d.ts +0 -3
  151. package/dist/migrations/20210103193615_servers.js +0 -28
  152. package/dist/migrations/20210103193729_channels.d.ts +0 -3
  153. package/dist/migrations/20210103193729_channels.js +0 -28
  154. package/dist/migrations/20210103193749_permissions.d.ts +0 -3
  155. package/dist/migrations/20210103193749_permissions.js +0 -30
  156. package/dist/migrations/20210103193801_files.d.ts +0 -3
  157. package/dist/migrations/20210103193801_files.js +0 -28
  158. package/files/00ea7368-45a7-4f6a-a199-974da14be1a0 +0 -0
  159. package/files/039503d2-a170-4962-b921-c97994ba64ff +0 -0
  160. package/files/14b6fa02-4cbb-40df-be4f-a07187cb619e +0 -0
  161. package/files/15c04cb1-dc6a-4f19-aa6f-4a3b92d05bf7 +0 -0
  162. package/files/2a8d411c-8b92-4532-b84d-d64c638d6293 +0 -0
  163. package/files/37de2cd3-08a8-4044-9c37-e13386765f3d +0 -0
  164. package/files/42452029-284e-4c81-9f18-feb6ce309eed +0 -0
  165. package/files/43d09f2f-29c8-415f-8c4a-23f2a32eb79e +0 -0
  166. package/files/52992923-33ab-44a1-8118-605e9b4856a7 +0 -0
  167. package/files/53180681-36e2-49c0-8382-94dca0da09bf +0 -0
  168. package/files/5a56cd7b-1d04-4619-b60f-1e5515b9164a +0 -0
  169. package/files/5ced7676-20c4-4219-a4f2-70a25eb7eea8 +0 -0
  170. package/files/60e787ff-8ec5-444d-b963-0aaf5313b53e +0 -0
  171. package/files/67a17729-fcb7-4339-9499-1fc08fea72ca +0 -0
  172. package/files/68d09565-908d-4a67-8f09-e183f8708eb4 +0 -0
  173. package/files/70e587c5-56e8-47f0-bd36-691efcb0cc2e +0 -4
  174. package/files/7a227619-b715-4e2c-a79d-b2158d56a799 +0 -0
  175. package/files/7e3cc3ea-b706-4835-994d-65d33acaf369 +0 -0
  176. package/files/816f72a0-65dc-40c1-8862-1d22065cca3c +0 -0
  177. package/files/8bf84972-5086-4631-a752-093d7b1a098b +0 -0
  178. package/files/8c46e3bc-3f2e-441d-b8cc-17428fc3d219 +0 -0
  179. package/files/8eb83364-8826-4eee-895a-ba5cd3ab85e9 +0 -0
  180. package/files/8faefaea-14e3-49e4-ac74-9710622bfae9 +0 -0
  181. package/files/91af08c2-9dca-41f4-b6c6-b7bf33505df9 +0 -0
  182. package/files/9349dffa-35dd-49a0-a651-10b438038f95 +0 -0
  183. package/files/b0a12a41-6283-4f27-b2c8-66774991d96c +0 -0
  184. package/files/b28afb08-d18b-48a8-93c3-6a59c66d8fee +0 -0
  185. package/files/be60fe01-1578-4363-a908-41500092b77d +0 -0
  186. package/files/cf7b90e3-734a-4453-9ff3-9a331404b5ef +0 -0
  187. package/files/d1be30aa-8ff4-41c1-b360-f65922029047 +0 -0
  188. package/files/d2d31a57-a413-443c-9b97-fa9ca394ef0b +0 -0
  189. package/files/d357f223-b786-478d-a113-b53cb62acdab +0 -0
  190. package/files/d4a69ee0-05c4-4ff0-8753-624cdd0f24e9 +0 -0
  191. package/files/d57f411b-1874-4f00-a6b0-2f86de6b229d +0 -0
  192. package/files/d85aaa33-7f70-4a70-b3d2-cad667beb38c +0 -0
  193. package/files/db2fc236-dbe0-4d24-bfaf-884829fd090a +0 -0
  194. package/files/df2e20ec-6dcc-4402-94ee-7d1163f84197 +0 -0
  195. package/files/e0880ab5-6d49-4dc0-a5ec-0d3793a8a7b8 +0 -0
  196. package/files/e59ee9c6-5aea-48ea-b3d9-94a324dc2260 +0 -0
  197. package/files/e6d7aad6-4220-4ce7-85f8-89d0b1f0cc89 +0 -0
  198. package/files/f0bfc98c-3534-459d-931a-7c6e77bde153 +0 -0
  199. package/files/f8443740-050c-4714-9bf6-334783ae6ffd +0 -0
  200. package/files/fc405ae7-f9fb-455d-ac8c-0ca0d88d4115 +0 -0
  201. package/jest.config.js +0 -13
  202. package/spire.sqlite +0 -0
@@ -0,0 +1,167 @@
1
+ import type { SpireOptions } from "../Spire.ts";
2
+ import type { PreKeysWS } from "@vex-chat/types";
3
+
4
+ import { XUtils } from "@vex-chat/crypto";
5
+
6
+ import * as uuid from "uuid";
7
+ import { describe, expect, it, vi } from "vitest";
8
+ import winston from "winston";
9
+
10
+ import { Database } from "../Database.ts";
11
+
12
+ // vi.mock is hoisted above all imports automatically.
13
+ // Minimal stubs for uuid functions used by spire src: v4, parse, stringify.
14
+ vi.mock("uuid", () => ({
15
+ parse: (s: string) => {
16
+ const matches = s.replace(/-/g, "").match(/.{2}/g);
17
+ if (!matches) throw new Error("Invalid UUID");
18
+ return Uint8Array.from(matches.map((b) => parseInt(b, 16)));
19
+ },
20
+ stringify: (b: Uint8Array) => {
21
+ const hex = Array.from(b)
22
+ .map((x) => x.toString(16).padStart(2, "0"))
23
+ .join("");
24
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
25
+ },
26
+ v4: vi.fn(() => "93ce482b-a0f2-4f6e-b1df-3aed61073552"),
27
+ validate: () => true,
28
+ }));
29
+
30
+ /** Winston logger stub — Database.close() calls `.info`, and `{}` breaks that. */
31
+ function silentLogger(): winston.Logger {
32
+ const noop = vi.fn();
33
+ return {
34
+ debug: noop,
35
+ error: noop,
36
+ info: noop,
37
+ log: noop,
38
+ verbose: noop,
39
+ warn: noop,
40
+ } as unknown as winston.Logger;
41
+ }
42
+
43
+ describe("Database", () => {
44
+ // Reusable test data
45
+ const keyID = "de459e05-aa63-4dfa-97b4-ed43d5c7a5f7";
46
+ const userID = "4e67b90f-cbf8-44bc-8ce3-d3b248f033f1";
47
+ const deviceID = "23cb0b27-7d0c-43b2-87e1-c2b93e0095e5";
48
+
49
+ const publicKey = XUtils.decodeHex(
50
+ "30c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
51
+ );
52
+ const signature = XUtils.decodeHex(
53
+ "dd0665079426c3efcf4dce9b1487e4aca132f8147581b3294c3f23ddd2b4ba8240a10082bd06805d7eb320d91af971da3306e11b60073ccc3d829710f5036004000030c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
54
+ );
55
+
56
+ const testSQLPreKey = {
57
+ deviceID,
58
+ index: 1,
59
+ keyID,
60
+ publicKey:
61
+ "30c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
62
+ signature:
63
+ "dd0665079426c3efcf4dce9b1487e4aca132f8147581b3294c3f23ddd2b4ba8240a10082bd06805d7eb320d91af971da3306e11b60073ccc3d829710f5036004000030c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
64
+ userID,
65
+ };
66
+
67
+ const testWSPreKey: PreKeysWS = {
68
+ deviceID,
69
+ index: 1,
70
+ publicKey,
71
+ signature,
72
+ };
73
+
74
+ const options: SpireOptions = {
75
+ dbType: "sqlite3mem",
76
+ };
77
+
78
+ describe("saveOTK", () => {
79
+ it("takes a userId and one time key, adds a keyId and saves it to oneTimeKey table", async () => {
80
+ expect.assertions(1);
81
+
82
+ vi.mocked(uuid.v4).mockReturnValueOnce(keyID);
83
+ vi.spyOn(winston, "createLogger").mockReturnValueOnce(
84
+ silentLogger(),
85
+ );
86
+
87
+ const provider = new Database(options);
88
+ await new Promise<void>((resolve, reject) => {
89
+ provider.once("ready", () => {
90
+ void (async () => {
91
+ try {
92
+ await provider.saveOTK(
93
+ testSQLPreKey.userID,
94
+ testSQLPreKey.deviceID,
95
+ [
96
+ {
97
+ deviceID,
98
+ index: 1,
99
+ publicKey,
100
+ signature,
101
+ },
102
+ ],
103
+ );
104
+ const oneTimeKey = await provider.getOTK(deviceID);
105
+ expect(oneTimeKey).toEqual(testWSPreKey);
106
+ await provider.close();
107
+ resolve();
108
+ } catch (e: unknown) {
109
+ reject(
110
+ e instanceof Error ? e : new Error(String(e)),
111
+ );
112
+ }
113
+ })();
114
+ });
115
+ });
116
+ });
117
+ });
118
+
119
+ describe("getPreKeys", () => {
120
+ it("returns a preKey by deviceID if said preKey exists.", async () => {
121
+ expect.assertions(1);
122
+
123
+ const provider = new Database(options);
124
+ await new Promise<void>((resolve, reject) => {
125
+ provider.once("ready", () => {
126
+ void (async () => {
127
+ try {
128
+ await provider["db"]
129
+ .insertInto("preKeys")
130
+ .values(testSQLPreKey)
131
+ .execute();
132
+ const result = await provider.getPreKeys(deviceID);
133
+ expect(result).toEqual(testWSPreKey);
134
+ await provider.close();
135
+ resolve();
136
+ } catch (e: unknown) {
137
+ reject(
138
+ e instanceof Error ? e : new Error(String(e)),
139
+ );
140
+ }
141
+ })();
142
+ });
143
+ });
144
+ });
145
+
146
+ it("return null if there are no preKeys with deviceID param", async () => {
147
+ expect.assertions(1);
148
+ const provider = new Database(options);
149
+ await new Promise<void>((resolve, reject) => {
150
+ provider.once("ready", () => {
151
+ void (async () => {
152
+ try {
153
+ const result = await provider.getPreKeys(deviceID);
154
+ expect(result).toBeNull();
155
+ await provider.close();
156
+ resolve();
157
+ } catch (e: unknown) {
158
+ reject(
159
+ e instanceof Error ? e : new Error(String(e)),
160
+ );
161
+ }
162
+ })();
163
+ });
164
+ });
165
+ });
166
+ });
167
+ });
@@ -0,0 +1 @@
1
+ declare module "ed2curve";
@@ -0,0 +1,165 @@
1
+ import type { Insertable, Selectable, Updateable } from "kysely";
2
+
3
+ // ── Table interfaces ────────────────────────────────────────────────────
4
+
5
+ export type ChannelRow = Selectable<ChannelsTable>;
6
+
7
+ export interface ChannelsTable {
8
+ channelID: string;
9
+ name: string;
10
+ serverID: string;
11
+ }
12
+
13
+ export type ChannelUpdate = Updateable<ChannelsTable>;
14
+
15
+ export type DeviceRow = Selectable<DevicesTable>;
16
+
17
+ export interface DevicesTable {
18
+ deleted: number;
19
+ deviceID: string;
20
+ lastLogin: string;
21
+ name: string;
22
+ owner: string;
23
+ signKey: string;
24
+ }
25
+
26
+ export type DeviceUpdate = Updateable<DevicesTable>;
27
+
28
+ export type EmojiRow = Selectable<EmojisTable>;
29
+
30
+ export interface EmojisTable {
31
+ emojiID: string;
32
+ name: string;
33
+ owner: string;
34
+ }
35
+
36
+ export type EmojiUpdate = Updateable<EmojisTable>;
37
+
38
+ export type FileRow = Selectable<FilesTable>;
39
+
40
+ export interface FilesTable {
41
+ fileID: string;
42
+ nonce: string;
43
+ owner: string;
44
+ }
45
+
46
+ export type FileUpdate = Updateable<FilesTable>;
47
+
48
+ // ── Database schema ─────────────────────────────────────────────────────
49
+
50
+ export type InviteRow = Selectable<InvitesTable>;
51
+
52
+ // ── Row utility types ───────────────────────────────────────────────────
53
+
54
+ export interface InvitesTable {
55
+ expiration: string;
56
+ inviteID: string;
57
+ owner: string;
58
+ serverID: string;
59
+ }
60
+ export type InviteUpdate = Updateable<InvitesTable>;
61
+ export type MailRow = Selectable<MailTable>;
62
+
63
+ export interface MailTable {
64
+ authorID: string;
65
+ cipher: string;
66
+ extra: null | string;
67
+ forward: number;
68
+ group: null | string;
69
+ header: string;
70
+ mailID: string;
71
+ mailType: number;
72
+ nonce: string;
73
+ readerID: string;
74
+ recipient: string;
75
+ sender: string;
76
+ time: string;
77
+ }
78
+ export type MailUpdate = Updateable<MailTable>;
79
+ export type NewChannel = Insertable<ChannelsTable>;
80
+
81
+ export type NewDevice = Insertable<DevicesTable>;
82
+ export type NewEmoji = Insertable<EmojisTable>;
83
+ export type NewFile = Insertable<FilesTable>;
84
+
85
+ export type NewInvite = Insertable<InvitesTable>;
86
+ export type NewMail = Insertable<MailTable>;
87
+ export type NewOneTimeKey = Insertable<OneTimeKeysTable>;
88
+
89
+ export type NewPermission = Insertable<PermissionsTable>;
90
+ export type NewPreKey = Insertable<PreKeysTable>;
91
+ export type NewServer = Insertable<ServersTable>;
92
+
93
+ export type NewServiceMetric = Insertable<ServiceMetricsTable>;
94
+ export type NewUser = Insertable<UsersTable>;
95
+ export type OneTimeKeyRow = Selectable<OneTimeKeysTable>;
96
+
97
+ export interface OneTimeKeysTable {
98
+ deviceID: string;
99
+ index: number;
100
+ keyID: string;
101
+ publicKey: string;
102
+ signature: string;
103
+ userID: string;
104
+ }
105
+ export type OneTimeKeyUpdate = Updateable<OneTimeKeysTable>;
106
+ export type PermissionRow = Selectable<PermissionsTable>;
107
+
108
+ export interface PermissionsTable {
109
+ permissionID: string;
110
+ powerLevel: number;
111
+ resourceID: string;
112
+ resourceType: string;
113
+ userID: string;
114
+ }
115
+ export type PermissionUpdate = Updateable<PermissionsTable>;
116
+ export type PreKeyRow = Selectable<PreKeysTable>;
117
+
118
+ export interface PreKeysTable {
119
+ deviceID: string;
120
+ index: number;
121
+ keyID: string;
122
+ publicKey: string;
123
+ signature: string;
124
+ userID: string;
125
+ }
126
+ export type PreKeyUpdate = Updateable<PreKeysTable>;
127
+ export interface ServerDatabase {
128
+ channels: ChannelsTable;
129
+ devices: DevicesTable;
130
+ emojis: EmojisTable;
131
+ files: FilesTable;
132
+ invites: InvitesTable;
133
+ mail: MailTable;
134
+ oneTimeKeys: OneTimeKeysTable;
135
+ permissions: PermissionsTable;
136
+ preKeys: PreKeysTable;
137
+ servers: ServersTable;
138
+ service_metrics: ServiceMetricsTable;
139
+ users: UsersTable;
140
+ }
141
+
142
+ export type ServerRow = Selectable<ServersTable>;
143
+ export interface ServersTable {
144
+ icon: null | string;
145
+ name: string;
146
+ serverID: string;
147
+ }
148
+ export type ServerUpdate = Updateable<ServersTable>;
149
+
150
+ export type ServiceMetricRow = Selectable<ServiceMetricsTable>;
151
+ export interface ServiceMetricsTable {
152
+ metric_key: string;
153
+ metric_value: number;
154
+ }
155
+ export type ServiceMetricUpdate = Updateable<ServiceMetricsTable>;
156
+
157
+ export type UserRow = Selectable<UsersTable>;
158
+ export interface UsersTable {
159
+ lastSeen: string;
160
+ passwordHash: string;
161
+ passwordSalt: string;
162
+ userID: string;
163
+ username: string;
164
+ }
165
+ export type UserUpdate = Updateable<UsersTable>;
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Spire } from "./Spire.ts";
2
+
3
+ export { Spire };
@@ -0,0 +1,38 @@
1
+ import type { NextFunction, Request, Response } from "express";
2
+ import type { z } from "zod/v4";
3
+
4
+ /**
5
+ * Express middleware that validates req.body against a Zod schema.
6
+ * On failure, returns 400 with validation errors.
7
+ */
8
+ export function validateBody(schema: z.ZodType) {
9
+ return (req: Request, res: Response, next: NextFunction): void => {
10
+ const result = schema.safeParse(req.body);
11
+ if (!result.success) {
12
+ res.status(400).json({
13
+ error: "Validation failed",
14
+ issues: result.error.issues,
15
+ });
16
+ return;
17
+ }
18
+ req.body = result.data;
19
+ next();
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Validates a single URL param as a non-empty string.
25
+ * For UUIDs, use validateUuidParam.
26
+ */
27
+ export function validateParam(name: string) {
28
+ return (req: Request, res: Response, next: NextFunction): void => {
29
+ const value = req.params[name];
30
+ if (!value || typeof value !== "string" || value.trim() === "") {
31
+ res.status(400).json({
32
+ error: `Missing or empty parameter: ${name}`,
33
+ });
34
+ return;
35
+ }
36
+ next();
37
+ };
38
+ }
@@ -0,0 +1,218 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function down(db: Kysely<unknown>): Promise<void> {
4
+ const tables = [
5
+ "service_metrics",
6
+ "invites",
7
+ "emojis",
8
+ "files",
9
+ "permissions",
10
+ "channels",
11
+ "servers",
12
+ "oneTimeKeys",
13
+ "preKeys",
14
+ "mail",
15
+ "devices",
16
+ "users",
17
+ ] as const;
18
+
19
+ for (const table of tables) {
20
+ await db.schema.dropTable(table).ifExists().execute();
21
+ }
22
+ }
23
+
24
+ export async function up(db: Kysely<unknown>): Promise<void> {
25
+ await db.schema
26
+ .createTable("users")
27
+ .ifNotExists()
28
+ .addColumn("userID", "varchar(255)", (cb) => cb.primaryKey())
29
+ .addColumn("username", "varchar(255)", (cb) => cb.unique())
30
+ .addColumn("passwordHash", "text")
31
+ .addColumn("passwordSalt", "text")
32
+ .addColumn("lastSeen", "text")
33
+ .execute();
34
+
35
+ await db.schema
36
+ .createTable("devices")
37
+ .ifNotExists()
38
+ .addColumn("deviceID", "varchar(255)", (cb) => cb.primaryKey())
39
+ .addColumn("signKey", "varchar(255)", (cb) => cb.unique())
40
+ .addColumn("owner", "varchar(255)")
41
+ .addColumn("name", "varchar(255)")
42
+ .addColumn("lastLogin", "text")
43
+ .addColumn("deleted", "integer", (cb) => cb.defaultTo(0))
44
+ .execute();
45
+
46
+ await db.schema
47
+ .createTable("mail")
48
+ .ifNotExists()
49
+ .addColumn("nonce", "varchar(255)", (cb) => cb.primaryKey())
50
+ .addColumn("recipient", "varchar(255)")
51
+ .addColumn("mailID", "varchar(255)")
52
+ .addColumn("sender", "varchar(255)")
53
+ .addColumn("header", "text")
54
+ .addColumn("cipher", "text")
55
+ .addColumn("group", "varchar(255)")
56
+ .addColumn("extra", "text")
57
+ .addColumn("mailType", "integer")
58
+ .addColumn("time", "text")
59
+ .addColumn("forward", "integer", (cb) => cb.defaultTo(0))
60
+ .addColumn("authorID", "varchar(255)")
61
+ .addColumn("readerID", "varchar(255)")
62
+ .execute();
63
+
64
+ await db.schema
65
+ .createIndex("mail_recipient_idx")
66
+ .ifNotExists()
67
+ .on("mail")
68
+ .column("recipient")
69
+ .execute();
70
+
71
+ await db.schema
72
+ .createTable("preKeys")
73
+ .ifNotExists()
74
+ .addColumn("keyID", "varchar(255)", (cb) => cb.primaryKey())
75
+ .addColumn("userID", "varchar(255)")
76
+ .addColumn("deviceID", "varchar(255)", (cb) => cb.unique())
77
+ .addColumn("publicKey", "text")
78
+ .addColumn("signature", "text")
79
+ .addColumn("index", "integer")
80
+ .execute();
81
+
82
+ await db.schema
83
+ .createIndex("preKeys_userID_idx")
84
+ .ifNotExists()
85
+ .on("preKeys")
86
+ .column("userID")
87
+ .execute();
88
+
89
+ await db.schema
90
+ .createIndex("preKeys_deviceID_idx")
91
+ .ifNotExists()
92
+ .on("preKeys")
93
+ .column("deviceID")
94
+ .execute();
95
+
96
+ await db.schema
97
+ .createTable("oneTimeKeys")
98
+ .ifNotExists()
99
+ .addColumn("keyID", "varchar(255)", (cb) => cb.primaryKey())
100
+ .addColumn("userID", "varchar(255)")
101
+ .addColumn("deviceID", "varchar(255)")
102
+ .addColumn("publicKey", "text")
103
+ .addColumn("signature", "text")
104
+ .addColumn("index", "integer")
105
+ .execute();
106
+
107
+ await db.schema
108
+ .createIndex("oneTimeKeys_userID_idx")
109
+ .ifNotExists()
110
+ .on("oneTimeKeys")
111
+ .column("userID")
112
+ .execute();
113
+
114
+ await db.schema
115
+ .createIndex("oneTimeKeys_deviceID_idx")
116
+ .ifNotExists()
117
+ .on("oneTimeKeys")
118
+ .column("deviceID")
119
+ .execute();
120
+
121
+ await db.schema
122
+ .createTable("servers")
123
+ .ifNotExists()
124
+ .addColumn("serverID", "varchar(255)", (cb) => cb.primaryKey())
125
+ .addColumn("name", "varchar(255)")
126
+ .addColumn("icon", "varchar(255)")
127
+ .execute();
128
+
129
+ await db.schema
130
+ .createTable("channels")
131
+ .ifNotExists()
132
+ .addColumn("channelID", "varchar(255)", (cb) => cb.primaryKey())
133
+ .addColumn("serverID", "varchar(255)")
134
+ .addColumn("name", "varchar(255)")
135
+ .execute();
136
+
137
+ await db.schema
138
+ .createTable("permissions")
139
+ .ifNotExists()
140
+ .addColumn("permissionID", "varchar(255)", (cb) => cb.primaryKey())
141
+ .addColumn("userID", "varchar(255)")
142
+ .addColumn("resourceType", "varchar(255)")
143
+ .addColumn("resourceID", "varchar(255)")
144
+ .addColumn("powerLevel", "integer")
145
+ .execute();
146
+
147
+ await db.schema
148
+ .createIndex("permissions_userID_idx")
149
+ .ifNotExists()
150
+ .on("permissions")
151
+ .column("userID")
152
+ .execute();
153
+
154
+ await db.schema
155
+ .createIndex("permissions_resourceID_idx")
156
+ .ifNotExists()
157
+ .on("permissions")
158
+ .column("resourceID")
159
+ .execute();
160
+
161
+ await db.schema
162
+ .createTable("files")
163
+ .ifNotExists()
164
+ .addColumn("fileID", "varchar(255)", (cb) => cb.primaryKey())
165
+ .addColumn("owner", "varchar(255)")
166
+ .addColumn("nonce", "varchar(255)")
167
+ .execute();
168
+
169
+ await db.schema
170
+ .createIndex("files_owner_idx")
171
+ .ifNotExists()
172
+ .on("files")
173
+ .column("owner")
174
+ .execute();
175
+
176
+ await db.schema
177
+ .createTable("emojis")
178
+ .ifNotExists()
179
+ .addColumn("emojiID", "varchar(255)", (cb) => cb.primaryKey())
180
+ .addColumn("owner", "varchar(255)")
181
+ .addColumn("name", "varchar(255)")
182
+ .execute();
183
+
184
+ await db.schema
185
+ .createIndex("emojis_owner_idx")
186
+ .ifNotExists()
187
+ .on("emojis")
188
+ .column("owner")
189
+ .execute();
190
+
191
+ await db.schema
192
+ .createTable("invites")
193
+ .ifNotExists()
194
+ .addColumn("inviteID", "varchar(255)", (cb) => cb.primaryKey())
195
+ .addColumn("serverID", "varchar(255)")
196
+ .addColumn("owner", "varchar(255)")
197
+ .addColumn("expiration", "text")
198
+ .execute();
199
+
200
+ await db.schema
201
+ .createIndex("invites_serverID_idx")
202
+ .ifNotExists()
203
+ .on("invites")
204
+ .column("serverID")
205
+ .execute();
206
+
207
+ await db.schema
208
+ .createTable("service_metrics")
209
+ .ifNotExists()
210
+ .addColumn("metric_key", "varchar(255)", (cb) => cb.primaryKey())
211
+ .addColumn("metric_value", "bigint", (cb) => cb.notNull().defaultTo(0))
212
+ .execute();
213
+
214
+ // Seed the requests_total metric row
215
+ await sql`INSERT OR IGNORE INTO service_metrics (metric_key, metric_value) VALUES ('requests_total', 0)`.execute(
216
+ db,
217
+ );
218
+ }
package/src/run.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { SpireOptions } from "./Spire.ts";
2
+
3
+ import { Spire } from "./Spire.ts";
4
+ import { loadEnv } from "./utils/loadEnv.ts";
5
+
6
+ function main() {
7
+ // load the environment variables — loadEnv() exits if required vars are missing
8
+ loadEnv();
9
+
10
+ const spk = process.env["SPK"];
11
+ if (!spk) {
12
+ throw new Error("SPK must be set (loadEnv should have caught this).");
13
+ }
14
+
15
+ const apiPort = process.env["API_PORT"];
16
+ const dbType = parseDbType(process.env["DB_TYPE"]);
17
+
18
+ new Spire(spk, {
19
+ ...(apiPort !== undefined ? { apiPort: Number(apiPort) } : {}),
20
+ ...(dbType !== undefined ? { dbType } : {}),
21
+ logLevel: "info",
22
+ });
23
+ }
24
+
25
+ function parseDbType(value: string | undefined): SpireOptions["dbType"] {
26
+ switch (value) {
27
+ case "mysql":
28
+ case "sqlite":
29
+ case "sqlite3":
30
+ case "sqlite3mem":
31
+ return value;
32
+ default:
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ main();