@nuraly/lumenjs 0.1.3 → 0.1.4

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 (333) hide show
  1. package/README.md +62 -282
  2. package/dist/auth/config.d.ts +23 -0
  3. package/dist/auth/config.js +115 -0
  4. package/dist/auth/guard.d.ts +12 -0
  5. package/dist/auth/guard.js +28 -0
  6. package/dist/auth/index.d.ts +3 -0
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/middleware.d.ts +23 -0
  9. package/dist/auth/middleware.js +89 -0
  10. package/dist/auth/native-auth.d.ts +73 -0
  11. package/dist/auth/native-auth.js +293 -0
  12. package/dist/auth/oidc-client.d.ts +17 -0
  13. package/dist/auth/oidc-client.js +123 -0
  14. package/dist/auth/providers/google.d.ts +23 -0
  15. package/dist/auth/providers/google.js +25 -0
  16. package/dist/auth/providers/index.d.ts +2 -0
  17. package/dist/auth/providers/index.js +1 -0
  18. package/dist/auth/routes/login.d.ts +8 -0
  19. package/dist/auth/routes/login.js +98 -0
  20. package/dist/auth/routes/logout.d.ts +4 -0
  21. package/dist/auth/routes/logout.js +79 -0
  22. package/dist/auth/routes/oidc-callback.d.ts +3 -0
  23. package/dist/auth/routes/oidc-callback.js +70 -0
  24. package/dist/auth/routes/password.d.ts +5 -0
  25. package/dist/auth/routes/password.js +149 -0
  26. package/dist/auth/routes/signup.d.ts +3 -0
  27. package/dist/auth/routes/signup.js +81 -0
  28. package/dist/auth/routes/token.d.ts +4 -0
  29. package/dist/auth/routes/token.js +70 -0
  30. package/dist/auth/routes/utils.d.ts +7 -0
  31. package/dist/auth/routes/utils.js +35 -0
  32. package/dist/auth/routes/verify.d.ts +3 -0
  33. package/dist/auth/routes/verify.js +26 -0
  34. package/dist/auth/routes.d.ts +8 -0
  35. package/dist/auth/routes.js +110 -0
  36. package/dist/auth/session.d.ts +8 -0
  37. package/dist/auth/session.js +54 -0
  38. package/dist/auth/token.d.ts +33 -0
  39. package/dist/auth/token.js +90 -0
  40. package/dist/auth/types.d.ts +156 -0
  41. package/dist/auth/types.js +2 -0
  42. package/dist/build/build-client.d.ts +15 -0
  43. package/dist/build/build-client.js +45 -0
  44. package/dist/build/build-prerender.d.ts +11 -0
  45. package/dist/build/build-prerender.js +159 -0
  46. package/dist/build/build-server.d.ts +17 -0
  47. package/dist/build/build-server.js +98 -0
  48. package/dist/build/build.js +48 -120
  49. package/dist/build/scan.d.ts +17 -0
  50. package/dist/build/scan.js +76 -6
  51. package/dist/build/serve-api.js +8 -2
  52. package/dist/build/serve-loaders.d.ts +4 -4
  53. package/dist/build/serve-loaders.js +26 -18
  54. package/dist/build/serve-ssr.js +38 -11
  55. package/dist/build/serve-static.js +3 -3
  56. package/dist/build/serve.js +218 -15
  57. package/dist/cli.js +37 -6
  58. package/dist/communication/encryption.d.ts +35 -0
  59. package/dist/communication/encryption.js +90 -0
  60. package/dist/communication/handlers/context.d.ts +27 -0
  61. package/dist/communication/handlers/context.js +1 -0
  62. package/dist/communication/handlers/conversation.d.ts +24 -0
  63. package/dist/communication/handlers/conversation.js +113 -0
  64. package/dist/communication/handlers/file-upload.d.ts +17 -0
  65. package/dist/communication/handlers/file-upload.js +62 -0
  66. package/dist/communication/handlers/messaging.d.ts +30 -0
  67. package/dist/communication/handlers/messaging.js +237 -0
  68. package/dist/communication/handlers/presence.d.ts +15 -0
  69. package/dist/communication/handlers/presence.js +76 -0
  70. package/dist/communication/handlers.d.ts +5 -0
  71. package/dist/communication/handlers.js +5 -0
  72. package/dist/communication/index.d.ts +9 -0
  73. package/dist/communication/index.js +7 -0
  74. package/dist/communication/link-preview.d.ts +18 -0
  75. package/dist/communication/link-preview.js +115 -0
  76. package/dist/communication/schema.d.ts +10 -0
  77. package/dist/communication/schema.js +101 -0
  78. package/dist/communication/server.d.ts +86 -0
  79. package/dist/communication/server.js +212 -0
  80. package/dist/communication/signaling.d.ts +43 -0
  81. package/dist/communication/signaling.js +271 -0
  82. package/dist/communication/store.d.ts +71 -0
  83. package/dist/communication/store.js +289 -0
  84. package/dist/communication/types.d.ts +454 -0
  85. package/dist/communication/types.js +1 -0
  86. package/dist/create.d.ts +1 -0
  87. package/dist/create.js +55 -0
  88. package/dist/db/auto-migrate.d.ts +3 -0
  89. package/dist/db/auto-migrate.js +100 -0
  90. package/dist/db/client.d.ts +3 -0
  91. package/dist/db/client.js +18 -0
  92. package/dist/db/index.d.ts +17 -13
  93. package/dist/db/index.js +205 -26
  94. package/dist/db/seed.d.ts +12 -0
  95. package/dist/db/seed.js +88 -0
  96. package/dist/db/table.d.ts +10 -0
  97. package/dist/db/table.js +12 -0
  98. package/dist/dev-server/config.d.ts +11 -0
  99. package/dist/dev-server/config.js +23 -20
  100. package/dist/dev-server/index-html.d.ts +3 -0
  101. package/dist/dev-server/index-html.js +18 -6
  102. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  103. package/dist/dev-server/nuralyui-aliases.js +115 -94
  104. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  105. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  106. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  107. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  108. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  109. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  110. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  111. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  113. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  114. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  115. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  116. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  117. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  118. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  119. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  120. package/dist/dev-server/plugins/vite-plugin-routes.js +15 -5
  121. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  122. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  123. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  125. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  126. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  127. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
  128. package/dist/dev-server/server.js +127 -13
  129. package/dist/dev-server/ssr-render.d.ts +2 -1
  130. package/dist/dev-server/ssr-render.js +107 -48
  131. package/dist/editor/ai/backend.d.ts +20 -0
  132. package/dist/editor/ai/backend.js +104 -0
  133. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  134. package/dist/editor/ai/claude-code-client.js +145 -0
  135. package/dist/editor/ai/opencode-client.d.ts +14 -0
  136. package/dist/editor/ai/opencode-client.js +125 -0
  137. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  138. package/dist/editor/ai/snapshot-store.js +35 -0
  139. package/dist/editor/ai/types.d.ts +30 -0
  140. package/dist/editor/ai/types.js +136 -0
  141. package/dist/editor/ai-chat-panel.d.ts +13 -0
  142. package/dist/editor/ai-chat-panel.js +587 -0
  143. package/dist/editor/ai-markdown.d.ts +10 -0
  144. package/dist/editor/ai-markdown.js +70 -0
  145. package/dist/editor/ai-project-panel.d.ts +11 -0
  146. package/dist/editor/ai-project-panel.js +332 -0
  147. package/dist/editor/ast-modification.d.ts +11 -0
  148. package/dist/editor/ast-modification.js +1 -0
  149. package/dist/editor/ast-service.d.ts +30 -0
  150. package/dist/editor/ast-service.js +180 -0
  151. package/dist/editor/css-rules.d.ts +54 -0
  152. package/dist/editor/css-rules.js +423 -0
  153. package/dist/editor/editor-api-client.d.ts +51 -0
  154. package/dist/editor/editor-api-client.js +162 -0
  155. package/dist/editor/editor-bridge.d.ts +1 -0
  156. package/dist/editor/editor-bridge.js +17 -8
  157. package/dist/editor/editor-toolbar.d.ts +14 -0
  158. package/dist/editor/editor-toolbar.js +115 -0
  159. package/dist/editor/file-editor.d.ts +9 -0
  160. package/dist/editor/file-editor.js +236 -0
  161. package/dist/editor/file-service.d.ts +16 -0
  162. package/dist/editor/file-service.js +52 -0
  163. package/dist/editor/i18n-key-gen.d.ts +1 -0
  164. package/dist/editor/i18n-key-gen.js +7 -0
  165. package/dist/editor/inline-text-edit.d.ts +5 -0
  166. package/dist/editor/inline-text-edit.js +173 -92
  167. package/dist/editor/overlay-events.d.ts +5 -0
  168. package/dist/editor/overlay-events.js +364 -0
  169. package/dist/editor/overlay-hmr.d.ts +2 -0
  170. package/dist/editor/overlay-hmr.js +75 -0
  171. package/dist/editor/overlay-selection.d.ts +29 -0
  172. package/dist/editor/overlay-selection.js +148 -0
  173. package/dist/editor/overlay-utils.d.ts +12 -0
  174. package/dist/editor/overlay-utils.js +59 -0
  175. package/dist/editor/properties-panel-persist.d.ts +14 -0
  176. package/dist/editor/properties-panel-persist.js +70 -0
  177. package/dist/editor/properties-panel-rows.d.ts +10 -0
  178. package/dist/editor/properties-panel-rows.js +349 -0
  179. package/dist/editor/properties-panel-styles.d.ts +4 -0
  180. package/dist/editor/properties-panel-styles.js +174 -0
  181. package/dist/editor/properties-panel.d.ts +4 -0
  182. package/dist/editor/properties-panel.js +148 -0
  183. package/dist/editor/property-registry.d.ts +16 -0
  184. package/dist/editor/property-registry.js +303 -0
  185. package/dist/editor/standalone-file-panel.d.ts +0 -0
  186. package/dist/editor/standalone-file-panel.js +1 -0
  187. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  188. package/dist/editor/standalone-overlay-dom.js +1 -0
  189. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  190. package/dist/editor/standalone-overlay-styles.js +1 -0
  191. package/dist/editor/standalone-overlay.d.ts +1 -0
  192. package/dist/editor/standalone-overlay.js +76 -0
  193. package/dist/editor/syntax-highlighter.d.ts +4 -0
  194. package/dist/editor/syntax-highlighter.js +81 -0
  195. package/dist/editor/text-toolbar.d.ts +11 -0
  196. package/dist/editor/text-toolbar.js +327 -0
  197. package/dist/editor/toolbar-styles.d.ts +4 -0
  198. package/dist/editor/toolbar-styles.js +198 -0
  199. package/dist/email/index.d.ts +32 -0
  200. package/dist/email/index.js +154 -0
  201. package/dist/email/providers/resend.d.ts +2 -0
  202. package/dist/email/providers/resend.js +24 -0
  203. package/dist/email/providers/sendgrid.d.ts +2 -0
  204. package/dist/email/providers/sendgrid.js +31 -0
  205. package/dist/email/providers/smtp.d.ts +13 -0
  206. package/dist/email/providers/smtp.js +125 -0
  207. package/dist/email/template-engine.d.ts +18 -0
  208. package/dist/email/template-engine.js +116 -0
  209. package/dist/email/templates/base.d.ts +9 -0
  210. package/dist/email/templates/base.js +65 -0
  211. package/dist/email/templates/password-reset.d.ts +5 -0
  212. package/dist/email/templates/password-reset.js +15 -0
  213. package/dist/email/templates/verify-email.d.ts +5 -0
  214. package/dist/email/templates/verify-email.js +15 -0
  215. package/dist/email/templates/welcome.d.ts +5 -0
  216. package/dist/email/templates/welcome.js +13 -0
  217. package/dist/email/types.d.ts +49 -0
  218. package/dist/email/types.js +1 -0
  219. package/dist/llms/generate.d.ts +46 -0
  220. package/dist/llms/generate.js +185 -0
  221. package/dist/permissions/guard.d.ts +28 -0
  222. package/dist/permissions/guard.js +30 -0
  223. package/dist/permissions/index.d.ts +6 -0
  224. package/dist/permissions/index.js +3 -0
  225. package/dist/permissions/service.d.ts +80 -0
  226. package/dist/permissions/service.js +210 -0
  227. package/dist/permissions/tables.d.ts +5 -0
  228. package/dist/permissions/tables.js +68 -0
  229. package/dist/permissions/types.d.ts +33 -0
  230. package/dist/permissions/types.js +1 -0
  231. package/dist/runtime/app-shell.js +163 -0
  232. package/dist/runtime/auth.d.ts +10 -0
  233. package/dist/runtime/auth.js +30 -0
  234. package/dist/runtime/communication.d.ts +137 -0
  235. package/dist/runtime/communication.js +228 -0
  236. package/dist/runtime/error-boundary.d.ts +23 -0
  237. package/dist/runtime/error-boundary.js +120 -0
  238. package/dist/runtime/i18n.d.ts +6 -1
  239. package/dist/runtime/i18n.js +42 -21
  240. package/dist/runtime/router-data.d.ts +3 -0
  241. package/dist/runtime/router-data.js +102 -17
  242. package/dist/runtime/router-hydration.js +25 -0
  243. package/dist/runtime/router.d.ts +16 -1
  244. package/dist/runtime/router.js +188 -42
  245. package/dist/runtime/socket-client.d.ts +2 -0
  246. package/dist/runtime/socket-client.js +30 -0
  247. package/dist/runtime/webrtc.d.ts +47 -0
  248. package/dist/runtime/webrtc.js +178 -0
  249. package/dist/shared/graceful-shutdown.d.ts +8 -0
  250. package/dist/shared/graceful-shutdown.js +36 -0
  251. package/dist/shared/health.d.ts +8 -0
  252. package/dist/shared/health.js +25 -0
  253. package/dist/shared/llms-txt.d.ts +31 -0
  254. package/dist/shared/llms-txt.js +85 -0
  255. package/dist/shared/logger.d.ts +32 -0
  256. package/dist/shared/logger.js +93 -0
  257. package/dist/shared/meta.d.ts +27 -0
  258. package/dist/shared/meta.js +71 -0
  259. package/dist/shared/middleware-runner.d.ts +9 -0
  260. package/dist/shared/middleware-runner.js +29 -0
  261. package/dist/shared/rate-limit.d.ts +18 -0
  262. package/dist/shared/rate-limit.js +71 -0
  263. package/dist/shared/request-id.d.ts +5 -0
  264. package/dist/shared/request-id.js +18 -0
  265. package/dist/shared/route-matching.js +16 -1
  266. package/dist/shared/security-headers.d.ts +18 -0
  267. package/dist/shared/security-headers.js +38 -0
  268. package/dist/shared/socket-io-setup.d.ts +11 -0
  269. package/dist/shared/socket-io-setup.js +51 -0
  270. package/dist/shared/types.d.ts +14 -0
  271. package/dist/shared/utils.d.ts +33 -7
  272. package/dist/shared/utils.js +164 -27
  273. package/dist/storage/adapters/local.d.ts +44 -0
  274. package/dist/storage/adapters/local.js +85 -0
  275. package/dist/storage/adapters/s3.d.ts +32 -0
  276. package/dist/storage/adapters/s3.js +116 -0
  277. package/dist/storage/adapters/types.d.ts +53 -0
  278. package/dist/storage/adapters/types.js +1 -0
  279. package/dist/storage/index.d.ts +76 -0
  280. package/dist/storage/index.js +83 -0
  281. package/package.json +19 -7
  282. package/templates/blog/api/posts.ts +4 -18
  283. package/templates/blog/data/migrations/001_init.sql +6 -5
  284. package/templates/blog/lumenjs.config.ts +3 -0
  285. package/templates/blog/package.json +14 -0
  286. package/templates/blog/pages/_layout.ts +25 -0
  287. package/templates/blog/pages/index.ts +48 -22
  288. package/templates/blog/pages/posts/[slug].ts +45 -20
  289. package/templates/blog/pages/tag/[tag].ts +44 -0
  290. package/templates/dashboard/api/stats.ts +8 -5
  291. package/templates/dashboard/lumenjs.config.ts +3 -0
  292. package/templates/dashboard/package.json +14 -0
  293. package/templates/dashboard/pages/_layout.ts +25 -0
  294. package/templates/dashboard/pages/index.ts +54 -23
  295. package/templates/dashboard/pages/settings/index.ts +29 -0
  296. package/templates/default/lumenjs.config.ts +3 -0
  297. package/templates/default/package.json +14 -0
  298. package/templates/default/pages/index.ts +24 -0
  299. package/templates/social/api/posts/[id].ts +14 -0
  300. package/templates/social/api/posts.ts +11 -0
  301. package/templates/social/api/profile/[username].ts +10 -0
  302. package/templates/social/api/upload.ts +19 -0
  303. package/templates/social/data/migrations/001_init.sql +78 -0
  304. package/templates/social/data/migrations/002_add_image_url.sql +1 -0
  305. package/templates/social/data/migrations/003_auth.sql +7 -0
  306. package/templates/social/docs/architecture.md +76 -0
  307. package/templates/social/docs/components.md +100 -0
  308. package/templates/social/docs/data.md +89 -0
  309. package/templates/social/docs/pages.md +96 -0
  310. package/templates/social/docs/theming.md +52 -0
  311. package/templates/social/lib/media.ts +130 -0
  312. package/templates/social/lumenjs.auth.ts +21 -0
  313. package/templates/social/lumenjs.config.ts +3 -0
  314. package/templates/social/package.json +5 -0
  315. package/templates/social/pages/_layout.ts +239 -0
  316. package/templates/social/pages/apps/[id].ts +173 -0
  317. package/templates/social/pages/apps/index.ts +116 -0
  318. package/templates/social/pages/auth/login.ts +92 -0
  319. package/templates/social/pages/bookmarks.ts +57 -0
  320. package/templates/social/pages/explore.ts +73 -0
  321. package/templates/social/pages/index.ts +351 -0
  322. package/templates/social/pages/messages.ts +298 -0
  323. package/templates/social/pages/new.ts +77 -0
  324. package/templates/social/pages/notifications.ts +73 -0
  325. package/templates/social/pages/post/[id].ts +124 -0
  326. package/templates/social/pages/profile/[username].ts +100 -0
  327. package/templates/social/pages/settings/accessibility.ts +153 -0
  328. package/templates/social/pages/settings/account.ts +260 -0
  329. package/templates/social/pages/settings/help.ts +141 -0
  330. package/templates/social/pages/settings/language.ts +103 -0
  331. package/templates/social/pages/settings/privacy.ts +183 -0
  332. package/templates/social/pages/settings/security.ts +133 -0
  333. package/templates/social/pages/settings.ts +185 -0
@@ -0,0 +1,237 @@
1
+ // ── Messages ────────────────────────────────────────────────────
2
+ export async function handleMessageSend(ctx, data) {
3
+ // ── Membership check ──────────────────────────────────────────
4
+ if (ctx.db) {
5
+ const row = await ctx.db.get('SELECT 1 FROM conversation_participants WHERE conversation_id = ? AND user_id = ?', data.conversationId, ctx.userId);
6
+ if (!row) {
7
+ ctx.push({ event: 'message:error', data: { code: 'FORBIDDEN', message: 'Not a participant of this conversation' } });
8
+ return;
9
+ }
10
+ }
11
+ else {
12
+ const members = ctx.store.getConversationMembers(data.conversationId);
13
+ if (members.size > 0 && !members.has(ctx.userId)) {
14
+ ctx.push({ event: 'message:error', data: { code: 'FORBIDDEN', message: 'Not a participant of this conversation' } });
15
+ return;
16
+ }
17
+ }
18
+ // ── Message length check ──────────────────────────────────────
19
+ const maxLen = ctx.config.maxMessageLength ?? 10_000;
20
+ if (data.content && data.content.length > maxLen) {
21
+ ctx.push({ event: 'message:error', data: { code: 'MESSAGE_TOO_LONG', message: `Message exceeds maximum length of ${maxLen} characters.` } });
22
+ return;
23
+ }
24
+ // ── Rate limit check ──────────────────────────────────────────
25
+ if (ctx.config.rateLimit) {
26
+ const allowed = ctx.store.checkRateLimit(ctx.userId, ctx.config.rateLimit);
27
+ if (!allowed) {
28
+ ctx.push({ event: 'message:error', data: { code: 'RATE_LIMITED', message: 'Message rate limit exceeded. Please wait before sending more messages.' } });
29
+ return;
30
+ }
31
+ }
32
+ // ── File upload validation ────────────────────────────────────
33
+ if (data.attachment && ctx.config.fileUpload) {
34
+ const { maxFileSize, allowedMimeTypes } = ctx.config.fileUpload;
35
+ if (data.attachment.fileSize > maxFileSize) {
36
+ ctx.push({ event: 'message:error', data: { code: 'FILE_TOO_LARGE', message: `File size ${data.attachment.fileSize} exceeds maximum allowed size of ${maxFileSize} bytes.` } });
37
+ return;
38
+ }
39
+ if (allowedMimeTypes && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(data.attachment.mimeType)) {
40
+ ctx.push({ event: 'message:error', data: { code: 'MIME_TYPE_NOT_ALLOWED', message: `MIME type '${data.attachment.mimeType}' is not allowed.` } });
41
+ return;
42
+ }
43
+ }
44
+ const now = new Date().toISOString();
45
+ let message;
46
+ // For encrypted messages, content is ciphertext — server stores it as opaque blob
47
+ const contentToStore = data.encrypted && data.envelope
48
+ ? JSON.stringify(data.envelope)
49
+ : data.content;
50
+ if (ctx.db) {
51
+ const inserted = await ctx.db.get(`INSERT INTO messages (conversation_id, sender_id, content, type, reply_to, attachment, status, encrypted, created_at)
52
+ VALUES (?, ?, ?, ?, ?, ?, 'sent', ?, ?) RETURNING id`, data.conversationId, ctx.userId, contentToStore, data.type, data.replyTo || null, data.attachment ? JSON.stringify(data.attachment) : null, data.encrypted ? 1 : 0, now);
53
+ message = {
54
+ id: String(inserted?.id ?? 0),
55
+ conversationId: data.conversationId,
56
+ senderId: ctx.userId,
57
+ content: contentToStore,
58
+ type: data.type,
59
+ createdAt: now,
60
+ replyTo: data.replyTo,
61
+ attachment: data.attachment,
62
+ status: 'sent',
63
+ readBy: [],
64
+ encrypted: data.encrypted,
65
+ envelope: data.envelope,
66
+ };
67
+ // Update conversation's last activity
68
+ await ctx.db.run(`UPDATE conversations SET updated_at = ? WHERE id = ?`, now, data.conversationId);
69
+ }
70
+ else {
71
+ // Without DB, create an in-memory message
72
+ message = {
73
+ id: crypto.randomUUID(),
74
+ conversationId: data.conversationId,
75
+ senderId: ctx.userId,
76
+ content: contentToStore,
77
+ type: data.type,
78
+ createdAt: now,
79
+ replyTo: data.replyTo,
80
+ attachment: data.attachment,
81
+ status: 'sent',
82
+ readBy: [],
83
+ encrypted: data.encrypted,
84
+ envelope: data.envelope,
85
+ };
86
+ }
87
+ // Clear typing indicator since the user just sent a message
88
+ ctx.store.clearTyping(data.conversationId, ctx.userId);
89
+ // Notify sender that the message has been persisted
90
+ ctx.push({ event: 'message:status', data: { messageId: message.id, status: 'sent' } });
91
+ // Broadcast to all participants in the conversation
92
+ ctx.broadcastAll(`conv:${data.conversationId}`, { event: 'message:new', data: message });
93
+ // Check if any recipient in the conversation has a connected socket
94
+ const members = ctx.store.getConversationMembers(data.conversationId);
95
+ const hasOnlineRecipient = Array.from(members).some((uid) => uid !== ctx.userId && ctx.store.isUserOnline(uid));
96
+ if (hasOnlineRecipient) {
97
+ if (ctx.db) {
98
+ await ctx.db.run(`UPDATE messages SET status = 'delivered' WHERE id = ? AND status = 'sent'`, message.id);
99
+ }
100
+ message.status = 'delivered';
101
+ ctx.push({ event: 'message:status', data: { messageId: message.id, status: 'delivered' } });
102
+ }
103
+ }
104
+ export async function handleMessageRead(ctx, data) {
105
+ const now = new Date().toISOString();
106
+ const receipt = { userId: ctx.userId, readAt: now };
107
+ if (ctx.db) {
108
+ // Insert read receipt (ignore duplicates)
109
+ await ctx.db.run(`INSERT OR IGNORE INTO read_receipts (message_id, user_id, read_at) VALUES (?, ?, ?)`, data.messageId, ctx.userId, now);
110
+ // Update message status to 'read' if all participants have read it
111
+ await ctx.db.run(`UPDATE messages SET status = 'read' WHERE id = ? AND status != 'read'`, data.messageId);
112
+ }
113
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
114
+ event: 'read-receipt:update',
115
+ data: { conversationId: data.conversationId, messageId: data.messageId, readBy: receipt },
116
+ });
117
+ // Emit message:status so clients can update delivery indicators
118
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
119
+ event: 'message:status',
120
+ data: { messageId: data.messageId, status: 'read' },
121
+ });
122
+ }
123
+ // ── Message Reactions ────────────────────────────────────────────
124
+ export async function handleMessageReact(ctx, data) {
125
+ if (ctx.db) {
126
+ // Toggle: if already reacted with this emoji, remove it
127
+ const existing = await ctx.db.get('SELECT 1 FROM message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ?', data.messageId, ctx.userId, data.emoji);
128
+ if (existing) {
129
+ await ctx.db.run('DELETE FROM message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ?', data.messageId, ctx.userId, data.emoji);
130
+ }
131
+ else {
132
+ await ctx.db.run('INSERT INTO message_reactions (message_id, user_id, emoji) VALUES (?, ?, ?)', data.messageId, ctx.userId, data.emoji);
133
+ }
134
+ // Fetch all reactions for this message
135
+ const reactions = await ctx.db.all('SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji', data.messageId);
136
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
137
+ event: 'message:reaction-update',
138
+ data: { messageId: data.messageId, reactions: reactions.map((r) => ({ emoji: r.emoji, count: r.count, users: r.users.split(',') })) },
139
+ });
140
+ }
141
+ else {
142
+ // Without DB, just broadcast the reaction event
143
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
144
+ event: 'message:reaction-update',
145
+ data: { messageId: data.messageId, reactions: [{ emoji: data.emoji, count: 1, users: [ctx.userId] }] },
146
+ });
147
+ }
148
+ }
149
+ // ── Message Edit ────────────────────────────────────────────────
150
+ export async function handleMessageEdit(ctx, data) {
151
+ const now = new Date().toISOString();
152
+ if (ctx.db) {
153
+ // Only allow sender to edit — return early if not the owner
154
+ const msg = await ctx.db.get('SELECT sender_id FROM messages WHERE id = ?', data.messageId);
155
+ if (!msg || msg.sender_id !== ctx.userId)
156
+ return;
157
+ await ctx.db.run('UPDATE messages SET content = ?, updated_at = ? WHERE id = ? AND sender_id = ?', data.content, now, data.messageId, ctx.userId);
158
+ }
159
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
160
+ event: 'message:updated',
161
+ data: { messageId: data.messageId, content: data.content, updatedAt: now, editedBy: ctx.userId },
162
+ });
163
+ }
164
+ // ── Message Delete ──────────────────────────────────────────────
165
+ export async function handleMessageDelete(ctx, data) {
166
+ if (ctx.db) {
167
+ // Only allow sender to delete — return early if not the owner
168
+ const msg = await ctx.db.get('SELECT sender_id FROM messages WHERE id = ?', data.messageId);
169
+ if (!msg || msg.sender_id !== ctx.userId)
170
+ return;
171
+ await ctx.db.run("UPDATE messages SET content = '', type = 'system', updated_at = ? WHERE id = ? AND sender_id = ?", new Date().toISOString(), data.messageId, ctx.userId);
172
+ }
173
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
174
+ event: 'message:deleted',
175
+ data: { messageId: data.messageId, conversationId: data.conversationId, deletedBy: ctx.userId },
176
+ });
177
+ }
178
+ // ── Message Forward ─────────────────────────────────────────────
179
+ export async function handleMessageForward(ctx, data) {
180
+ // Verify user is a participant of both source and target conversations
181
+ if (ctx.db) {
182
+ const inSource = await ctx.db.get('SELECT 1 FROM conversation_participants WHERE conversation_id = ? AND user_id = ?', data.fromConversationId, ctx.userId);
183
+ if (!inSource) {
184
+ ctx.push({ event: 'message:error', data: { code: 'FORBIDDEN', message: 'Not a participant of the source conversation' } });
185
+ return;
186
+ }
187
+ const inTarget = await ctx.db.get('SELECT 1 FROM conversation_participants WHERE conversation_id = ? AND user_id = ?', data.toConversationId, ctx.userId);
188
+ if (!inTarget) {
189
+ ctx.push({ event: 'message:error', data: { code: 'FORBIDDEN', message: 'Not a participant of the target conversation' } });
190
+ return;
191
+ }
192
+ }
193
+ const now = new Date().toISOString();
194
+ if (ctx.db) {
195
+ // Fetch the original message
196
+ const original = await ctx.db.get(`SELECT * FROM messages WHERE id = ?`, data.messageId);
197
+ if (!original)
198
+ return;
199
+ // Insert a forwarded copy into the target conversation
200
+ const fwdInserted = await ctx.db.get(`INSERT INTO messages (conversation_id, sender_id, content, type, status, encrypted, created_at, forwarded_from_conversation_id, forwarded_from_message_id)
201
+ VALUES (?, ?, ?, ?, 'sent', ?, ?, ?, ?) RETURNING id`, data.toConversationId, ctx.userId, original.content, original.type, original.encrypted || 0, now, data.fromConversationId, data.messageId);
202
+ const forwarded = {
203
+ id: String(fwdInserted?.id ?? 0),
204
+ conversationId: data.toConversationId,
205
+ senderId: ctx.userId,
206
+ content: original.content,
207
+ type: original.type,
208
+ createdAt: now,
209
+ status: 'sent',
210
+ readBy: [],
211
+ encrypted: !!original.encrypted,
212
+ forwardedFrom: {
213
+ conversationId: data.fromConversationId,
214
+ messageId: data.messageId,
215
+ },
216
+ };
217
+ await ctx.db.run(`UPDATE conversations SET updated_at = ? WHERE id = ?`, now, data.toConversationId);
218
+ ctx.broadcastAll(`conv:${data.toConversationId}`, { event: 'message:forwarded', data: forwarded });
219
+ }
220
+ else {
221
+ const forwarded = {
222
+ id: crypto.randomUUID(),
223
+ conversationId: data.toConversationId,
224
+ senderId: ctx.userId,
225
+ content: '',
226
+ type: 'text',
227
+ createdAt: now,
228
+ status: 'sent',
229
+ readBy: [],
230
+ forwardedFrom: {
231
+ conversationId: data.fromConversationId,
232
+ messageId: data.messageId,
233
+ },
234
+ };
235
+ ctx.broadcastAll(`conv:${data.toConversationId}`, { event: 'message:forwarded', data: forwarded });
236
+ }
237
+ }
@@ -0,0 +1,15 @@
1
+ import type { PresenceStatus } from '../types.js';
2
+ import type { HandlerContext } from './context.js';
3
+ export declare function handleTypingStart(ctx: HandlerContext, data: {
4
+ conversationId: string;
5
+ }): void;
6
+ export declare function handleTypingStop(ctx: HandlerContext, data: {
7
+ conversationId: string;
8
+ }): void;
9
+ /** Broadcast online status to all conversation rooms when a user connects */
10
+ export declare function handleConnect(ctx: HandlerContext): void;
11
+ export declare function handlePresenceUpdate(ctx: HandlerContext, data: {
12
+ status: PresenceStatus;
13
+ }): void;
14
+ /** Called on disconnect to clean up user state */
15
+ export declare function handleDisconnect(ctx: HandlerContext, socketId: string): void;
@@ -0,0 +1,76 @@
1
+ // ── Typing ──────────────────────────────────────────────────────
2
+ export function handleTypingStart(ctx, data) {
3
+ ctx.store.setTyping(data.conversationId, ctx.userId, () => {
4
+ // Auto-expire: broadcast typing stopped
5
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
6
+ event: 'typing:update',
7
+ data: { conversationId: data.conversationId, userId: ctx.userId, isTyping: false },
8
+ });
9
+ });
10
+ ctx.broadcast(`conv:${data.conversationId}`, {
11
+ event: 'typing:update',
12
+ data: { conversationId: data.conversationId, userId: ctx.userId, isTyping: true },
13
+ });
14
+ }
15
+ export function handleTypingStop(ctx, data) {
16
+ ctx.store.clearTyping(data.conversationId, ctx.userId);
17
+ ctx.broadcast(`conv:${data.conversationId}`, {
18
+ event: 'typing:update',
19
+ data: { conversationId: data.conversationId, userId: ctx.userId, isTyping: false },
20
+ });
21
+ }
22
+ // ── Presence ────────────────────────────────────────────────────
23
+ /** Broadcast online status to all conversation rooms when a user connects */
24
+ export function handleConnect(ctx) {
25
+ const conversations = ctx.store.getUserConversations(ctx.userId);
26
+ if (conversations.size === 0)
27
+ return;
28
+ const entry = ctx.store.getPresence(ctx.userId);
29
+ if (!entry)
30
+ return;
31
+ const payload = {
32
+ event: 'presence:changed',
33
+ data: { userId: ctx.userId, status: entry.status, lastSeen: entry.lastSeen },
34
+ };
35
+ for (const convId of conversations) {
36
+ ctx.broadcastAll(`conv:${convId}`, payload);
37
+ }
38
+ }
39
+ export function handlePresenceUpdate(ctx, data) {
40
+ const entry = ctx.store.setPresence(ctx.userId, data.status);
41
+ const payload = {
42
+ event: 'presence:changed',
43
+ data: { userId: ctx.userId, status: entry.status, lastSeen: entry.lastSeen },
44
+ };
45
+ // Broadcast to all conversation rooms the user has joined
46
+ for (const convId of ctx.store.getUserConversations(ctx.userId)) {
47
+ ctx.broadcastAll(`conv:${convId}`, payload);
48
+ }
49
+ }
50
+ /** Called on disconnect to clean up user state */
51
+ export function handleDisconnect(ctx, socketId) {
52
+ const userId = ctx.store.unmapUserSocket(socketId);
53
+ if (!userId)
54
+ return;
55
+ // Clear all typing indicators for this user
56
+ const clearedConversations = ctx.store.clearAllTypingForUser(userId);
57
+ for (const convId of clearedConversations) {
58
+ ctx.broadcastAll(`conv:${convId}`, {
59
+ event: 'typing:update',
60
+ data: { conversationId: convId, userId, isTyping: false },
61
+ });
62
+ }
63
+ // If user has no more connected sockets, set presence to offline and broadcast
64
+ if (!ctx.store.isUserOnline(userId)) {
65
+ const entry = ctx.store.setPresence(userId, 'offline');
66
+ const payload = {
67
+ event: 'presence:changed',
68
+ data: { userId, status: 'offline', lastSeen: entry.lastSeen },
69
+ };
70
+ // Broadcast offline to all conversation rooms before cleaning up membership
71
+ for (const convId of ctx.store.getUserConversations(userId)) {
72
+ ctx.broadcastAll(`conv:${convId}`, payload);
73
+ }
74
+ ctx.store.removeUserFromAllConversations(userId);
75
+ }
76
+ }
@@ -0,0 +1,5 @@
1
+ export type { HandlerContext } from './handlers/context.js';
2
+ export { handleConversationCreate, handleConversationJoin, handleConversationLeave, handleConversationArchive, handleConversationMute, handleConversationPin, } from './handlers/conversation.js';
3
+ export { handleMessageSend, handleMessageRead, handleMessageReact, handleMessageEdit, handleMessageDelete, handleMessageForward, } from './handlers/messaging.js';
4
+ export { handleTypingStart, handleTypingStop, handleConnect, handlePresenceUpdate, handleDisconnect, } from './handlers/presence.js';
5
+ export { handleFileUploadRequest } from './handlers/file-upload.js';
@@ -0,0 +1,5 @@
1
+ // Barrel re-export — keeps the public API identical for all existing consumers.
2
+ export { handleConversationCreate, handleConversationJoin, handleConversationLeave, handleConversationArchive, handleConversationMute, handleConversationPin, } from './handlers/conversation.js';
3
+ export { handleMessageSend, handleMessageRead, handleMessageReact, handleMessageEdit, handleMessageDelete, handleMessageForward, } from './handlers/messaging.js';
4
+ export { handleTypingStart, handleTypingStop, handleConnect, handlePresenceUpdate, handleDisconnect, } from './handlers/presence.js';
5
+ export { handleFileUploadRequest } from './handlers/file-upload.js';
@@ -0,0 +1,9 @@
1
+ export type { CommunicationConfig, RTCIceServerConfig, MediaConstraintDefaults, MediaTrackConstraintSet, Conversation, Participant, PresenceStatus, PresenceUpdate, Message, MessageStatus, MessageAttachment, ReadReceipt, TypingIndicator, MessageForward, CallType, CallState, CallEndReason, Call, CallParticipant, SignalOffer, SignalIceCandidate, SignalIceRestart, ConnectionQualityReport, CallInitiate, CallResponse, CallHangup, CallMediaToggle, CommunicationClientEvents, CommunicationServerEvents, RateLimitConfig, FileUploadConfig, ReconnectionConfig, EncryptionConfig, PreKey, KeyBundle, EncryptedEnvelope, KeyExchangeRequest, KeyExchangeResponse, NkCommunication, } from './types.js';
2
+ export { CommunicationStore, useCommunicationStore } from './store.js';
3
+ export type { PresenceEntry } from './store.js';
4
+ export type { HandlerContext } from './handlers.js';
5
+ export type { SignalingContext } from './signaling.js';
6
+ export type { EncryptionContext } from './encryption.js';
7
+ export { createCommunicationHandler, createCommunicationApiHandlers } from './server.js';
8
+ export type { CommunicationHandlerOptions } from './server.js';
9
+ export { ensureCommunicationTables } from './schema.js';
@@ -0,0 +1,7 @@
1
+ // ── Types ─────────────────────────────────────────────────────────
2
+ // ── Store ─────────────────────────────────────────────────────────
3
+ export { CommunicationStore, useCommunicationStore } from './store.js';
4
+ // ── Server (main entry points) ────────────────────────────────────
5
+ export { createCommunicationHandler, createCommunicationApiHandlers } from './server.js';
6
+ // ── Schema ────────────────────────────────────────────────────────
7
+ export { ensureCommunicationTables } from './schema.js';
@@ -0,0 +1,18 @@
1
+ interface LinkPreview {
2
+ url: string;
3
+ title: string | null;
4
+ description: string | null;
5
+ image: string | null;
6
+ domain: string;
7
+ }
8
+ interface Db {
9
+ get<T = any>(sql: string, ...params: any[]): Promise<T | undefined>;
10
+ run(sql: string, ...params: any[]): Promise<any>;
11
+ }
12
+ /**
13
+ * Fetch Open Graph metadata from a URL and cache it.
14
+ */
15
+ export declare function fetchLinkPreview(url: string, db?: Db): Promise<LinkPreview | null>;
16
+ /** Extract URLs from message text */
17
+ export declare function extractUrls(text: string): string[];
18
+ export {};
@@ -0,0 +1,115 @@
1
+ import crypto from 'node:crypto';
2
+ function isPrivateUrl(urlStr) {
3
+ try {
4
+ const parsed = new URL(urlStr);
5
+ // Only allow http/https schemes
6
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
7
+ return true;
8
+ const hostname = parsed.hostname.toLowerCase();
9
+ // Loopback and unspecified addresses
10
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '0.0.0.0')
11
+ return true;
12
+ // Single-label hostnames (no dots)
13
+ if (!hostname.includes('.'))
14
+ return true;
15
+ // Cloud metadata endpoints
16
+ if (hostname === 'metadata.google.internal' || hostname === 'metadata.internal')
17
+ return true;
18
+ // IPv6 addresses — block all private/reserved ranges
19
+ if (hostname.startsWith('[')) {
20
+ const ipv6 = hostname.slice(1, -1).toLowerCase();
21
+ if (ipv6 === '::1' || ipv6 === '::' || ipv6.startsWith('fe80:') || ipv6.startsWith('fd') || ipv6.startsWith('fc'))
22
+ return true;
23
+ return true; // Block all bracketed IPv6 for safety
24
+ }
25
+ // IPv4 private ranges
26
+ const parts = hostname.split('.').map(Number);
27
+ if (parts.length === 4 && parts.every(n => !isNaN(n))) {
28
+ if (parts[0] === 10)
29
+ return true;
30
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
31
+ return true;
32
+ if (parts[0] === 192 && parts[1] === 168)
33
+ return true;
34
+ if (parts[0] === 169 && parts[1] === 254)
35
+ return true; // link-local + cloud metadata
36
+ if (parts[0] === 0)
37
+ return true;
38
+ if (parts[0] === 127)
39
+ return true; // full loopback range
40
+ }
41
+ return false;
42
+ }
43
+ catch {
44
+ return true;
45
+ }
46
+ }
47
+ /**
48
+ * Fetch Open Graph metadata from a URL and cache it.
49
+ */
50
+ export async function fetchLinkPreview(url, db) {
51
+ if (isPrivateUrl(url))
52
+ return null;
53
+ const urlHash = crypto.createHash('sha256').update(url).digest('hex').slice(0, 16);
54
+ // Check cache
55
+ if (db) {
56
+ const cached = await db.get('SELECT * FROM link_previews WHERE url_hash = ?', urlHash);
57
+ if (cached) {
58
+ return { url: cached.url, title: cached.title, description: cached.description, image: cached.image, domain: cached.domain };
59
+ }
60
+ }
61
+ try {
62
+ const controller = new AbortController();
63
+ const timeout = setTimeout(() => controller.abort(), 5000);
64
+ const res = await fetch(url, {
65
+ headers: { 'User-Agent': 'LumenJS LinkPreview/1.0' },
66
+ signal: controller.signal,
67
+ redirect: 'follow',
68
+ });
69
+ clearTimeout(timeout);
70
+ // Re-check final URL after redirects to prevent SSRF via redirect
71
+ if (res.url && res.url !== url && isPrivateUrl(res.url))
72
+ return null;
73
+ if (!res.ok)
74
+ return null;
75
+ const contentType = res.headers.get('content-type') || '';
76
+ if (!contentType.includes('text/html'))
77
+ return null;
78
+ const html = await res.text();
79
+ const domain = new URL(url).hostname;
80
+ const title = extractMeta(html, 'og:title') || extractTag(html, 'title');
81
+ const description = extractMeta(html, 'og:description') || extractMeta(html, 'description');
82
+ const image = extractMeta(html, 'og:image');
83
+ const preview = { url, title, description, image, domain };
84
+ // Cache
85
+ if (db) {
86
+ await db.run('INSERT OR REPLACE INTO link_previews (url_hash, url, title, description, image, domain) VALUES (?, ?, ?, ?, ?, ?)', urlHash, url, title, description, image, domain);
87
+ }
88
+ return preview;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ /** Extract URLs from message text */
95
+ export function extractUrls(text) {
96
+ const regex = /https?:\/\/[^\s<>"')\]]+/g;
97
+ const matches = text.match(regex);
98
+ return matches ? [...new Set(matches)].slice(0, 3) : [];
99
+ }
100
+ function extractMeta(html, name) {
101
+ // Match <meta property="og:title" content="..."> or <meta name="description" content="...">
102
+ const regex = new RegExp(`<meta[^>]+(?:property|name)=["']${name}["'][^>]+content=["']([^"']+)["']`, 'i');
103
+ const match = html.match(regex);
104
+ if (match)
105
+ return match[1];
106
+ // Try reversed order: content before property
107
+ const regex2 = new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${name}["']`, 'i');
108
+ const match2 = html.match(regex2);
109
+ return match2 ? match2[1] : null;
110
+ }
111
+ function extractTag(html, tag) {
112
+ const regex = new RegExp(`<${tag}[^>]*>([^<]+)</${tag}>`, 'i');
113
+ const match = html.match(regex);
114
+ return match ? match[1].trim() : null;
115
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Communication module DB schema.
3
+ * Call ensureCommunicationTables(db) to create all required tables.
4
+ */
5
+ interface Db {
6
+ exec(sql: string): Promise<void>;
7
+ isPg?: boolean;
8
+ }
9
+ export declare function ensureCommunicationTables(db: Db): Promise<void>;
10
+ export {};
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Communication module DB schema.
3
+ * Call ensureCommunicationTables(db) to create all required tables.
4
+ */
5
+ export async function ensureCommunicationTables(db) {
6
+ if (db.isPg)
7
+ return;
8
+ await db.exec(`
9
+ CREATE TABLE IF NOT EXISTS conversations (
10
+ id TEXT PRIMARY KEY,
11
+ type TEXT NOT NULL CHECK(type IN ('direct', 'group')),
12
+ name TEXT,
13
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
14
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS conversation_participants (
18
+ conversation_id TEXT NOT NULL,
19
+ user_id TEXT NOT NULL,
20
+ role TEXT NOT NULL DEFAULT 'member',
21
+ joined_at TEXT NOT NULL DEFAULT (datetime('now')),
22
+ PRIMARY KEY (conversation_id, user_id),
23
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS messages (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ conversation_id TEXT NOT NULL,
29
+ sender_id TEXT NOT NULL,
30
+ content TEXT NOT NULL DEFAULT '',
31
+ type TEXT NOT NULL DEFAULT 'text',
32
+ reply_to TEXT,
33
+ attachment TEXT,
34
+ status TEXT NOT NULL DEFAULT 'sent',
35
+ encrypted INTEGER NOT NULL DEFAULT 0,
36
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
37
+ updated_at TEXT,
38
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS read_receipts (
42
+ message_id INTEGER NOT NULL,
43
+ user_id TEXT NOT NULL,
44
+ read_at TEXT NOT NULL DEFAULT (datetime('now')),
45
+ PRIMARY KEY (message_id, user_id),
46
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
47
+ );
48
+
49
+ CREATE TABLE IF NOT EXISTS encryption_keys (
50
+ user_id TEXT PRIMARY KEY,
51
+ identity_key TEXT NOT NULL,
52
+ signed_pre_key_id INTEGER NOT NULL,
53
+ signed_pre_key TEXT NOT NULL,
54
+ signed_pre_key_signature TEXT NOT NULL,
55
+ uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
56
+ );
57
+
58
+ CREATE TABLE IF NOT EXISTS encryption_prekeys (
59
+ user_id TEXT NOT NULL,
60
+ key_id INTEGER NOT NULL,
61
+ public_key TEXT NOT NULL,
62
+ PRIMARY KEY (user_id, key_id),
63
+ FOREIGN KEY (user_id) REFERENCES encryption_keys(user_id) ON DELETE CASCADE
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS message_reactions (
67
+ message_id INTEGER NOT NULL,
68
+ user_id TEXT NOT NULL,
69
+ emoji TEXT NOT NULL,
70
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
71
+ PRIMARY KEY (message_id, user_id, emoji),
72
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS attachments (
76
+ id TEXT PRIMARY KEY,
77
+ filename TEXT NOT NULL,
78
+ mimetype TEXT NOT NULL,
79
+ size INTEGER NOT NULL,
80
+ url TEXT NOT NULL,
81
+ thumbnail_url TEXT,
82
+ uploaded_by TEXT NOT NULL,
83
+ encrypted INTEGER NOT NULL DEFAULT 0,
84
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS link_previews (
88
+ url_hash TEXT PRIMARY KEY,
89
+ url TEXT NOT NULL,
90
+ title TEXT,
91
+ description TEXT,
92
+ image TEXT,
93
+ domain TEXT,
94
+ fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
95
+ );
96
+
97
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id, created_at);
98
+ CREATE INDEX IF NOT EXISTS idx_participants_user ON conversation_participants(user_id);
99
+ CREATE INDEX IF NOT EXISTS idx_reactions_message ON message_reactions(message_id);
100
+ `);
101
+ }