@nuraly/lumenjs 0.1.3 → 0.2.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 (306) 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 +82 -0
  11. package/dist/auth/native-auth.js +340 -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 +121 -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/totp.d.ts +22 -0
  31. package/dist/auth/routes/totp.js +232 -0
  32. package/dist/auth/routes/utils.d.ts +7 -0
  33. package/dist/auth/routes/utils.js +35 -0
  34. package/dist/auth/routes/verify.d.ts +3 -0
  35. package/dist/auth/routes/verify.js +26 -0
  36. package/dist/auth/routes.d.ts +8 -0
  37. package/dist/auth/routes.js +124 -0
  38. package/dist/auth/session.d.ts +8 -0
  39. package/dist/auth/session.js +54 -0
  40. package/dist/auth/token.d.ts +33 -0
  41. package/dist/auth/token.js +90 -0
  42. package/dist/auth/types.d.ts +156 -0
  43. package/dist/auth/types.js +2 -0
  44. package/dist/build/build-client.d.ts +15 -0
  45. package/dist/build/build-client.js +45 -0
  46. package/dist/build/build-prerender.d.ts +11 -0
  47. package/dist/build/build-prerender.js +159 -0
  48. package/dist/build/build-server.d.ts +18 -0
  49. package/dist/build/build-server.js +107 -0
  50. package/dist/build/build.js +60 -123
  51. package/dist/build/scan.d.ts +18 -0
  52. package/dist/build/scan.js +77 -6
  53. package/dist/build/serve-api.js +8 -2
  54. package/dist/build/serve-loaders.d.ts +4 -4
  55. package/dist/build/serve-loaders.js +26 -18
  56. package/dist/build/serve-ssr.js +38 -11
  57. package/dist/build/serve-static.js +3 -3
  58. package/dist/build/serve.js +341 -18
  59. package/dist/cli.js +37 -6
  60. package/dist/communication/encryption.d.ts +35 -0
  61. package/dist/communication/encryption.js +90 -0
  62. package/dist/communication/handlers/context.d.ts +27 -0
  63. package/dist/communication/handlers/context.js +1 -0
  64. package/dist/communication/handlers/conversation.d.ts +24 -0
  65. package/dist/communication/handlers/conversation.js +113 -0
  66. package/dist/communication/handlers/file-upload.d.ts +17 -0
  67. package/dist/communication/handlers/file-upload.js +62 -0
  68. package/dist/communication/handlers/messaging.d.ts +30 -0
  69. package/dist/communication/handlers/messaging.js +237 -0
  70. package/dist/communication/handlers/presence.d.ts +15 -0
  71. package/dist/communication/handlers/presence.js +76 -0
  72. package/dist/communication/handlers.d.ts +5 -0
  73. package/dist/communication/handlers.js +5 -0
  74. package/dist/communication/index.d.ts +9 -0
  75. package/dist/communication/index.js +7 -0
  76. package/dist/communication/link-preview.d.ts +18 -0
  77. package/dist/communication/link-preview.js +115 -0
  78. package/dist/communication/schema.d.ts +10 -0
  79. package/dist/communication/schema.js +101 -0
  80. package/dist/communication/server.d.ts +86 -0
  81. package/dist/communication/server.js +212 -0
  82. package/dist/communication/signaling.d.ts +43 -0
  83. package/dist/communication/signaling.js +271 -0
  84. package/dist/communication/store.d.ts +71 -0
  85. package/dist/communication/store.js +289 -0
  86. package/dist/communication/types.d.ts +454 -0
  87. package/dist/communication/types.js +1 -0
  88. package/dist/create.d.ts +1 -0
  89. package/dist/create.js +55 -0
  90. package/dist/db/auto-migrate.d.ts +3 -0
  91. package/dist/db/auto-migrate.js +100 -0
  92. package/dist/db/client.d.ts +3 -0
  93. package/dist/db/client.js +18 -0
  94. package/dist/db/index.d.ts +17 -13
  95. package/dist/db/index.js +205 -26
  96. package/dist/db/seed.d.ts +12 -0
  97. package/dist/db/seed.js +88 -0
  98. package/dist/db/table.d.ts +10 -0
  99. package/dist/db/table.js +12 -0
  100. package/dist/dev-server/config.d.ts +11 -0
  101. package/dist/dev-server/config.js +40 -20
  102. package/dist/dev-server/index-html.d.ts +4 -0
  103. package/dist/dev-server/index-html.js +21 -6
  104. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  105. package/dist/dev-server/nuralyui-aliases.js +115 -94
  106. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  107. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  108. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  109. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  110. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  111. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  113. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  114. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  115. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  116. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  117. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  118. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  119. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  120. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  121. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  122. package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
  123. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  125. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  126. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  127. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  128. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  129. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
  130. package/dist/dev-server/server.js +242 -70
  131. package/dist/dev-server/ssr-render.d.ts +2 -1
  132. package/dist/dev-server/ssr-render.js +117 -50
  133. package/dist/editor/ai/backend.d.ts +20 -0
  134. package/dist/editor/ai/backend.js +113 -0
  135. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  136. package/dist/editor/ai/claude-code-client.js +145 -0
  137. package/dist/editor/ai/deepseek-client.d.ts +7 -0
  138. package/dist/editor/ai/deepseek-client.js +113 -0
  139. package/dist/editor/ai/opencode-client.d.ts +14 -0
  140. package/dist/editor/ai/opencode-client.js +99 -0
  141. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  142. package/dist/editor/ai/snapshot-store.js +35 -0
  143. package/dist/editor/ai/types.d.ts +30 -0
  144. package/dist/editor/ai/types.js +136 -0
  145. package/dist/editor/ai-chat-panel.d.ts +13 -0
  146. package/dist/editor/ai-chat-panel.js +613 -0
  147. package/dist/editor/ai-markdown.d.ts +10 -0
  148. package/dist/editor/ai-markdown.js +70 -0
  149. package/dist/editor/ai-project-panel.d.ts +11 -0
  150. package/dist/editor/ai-project-panel.js +332 -0
  151. package/dist/editor/ast-modification.d.ts +11 -0
  152. package/dist/editor/ast-modification.js +1 -0
  153. package/dist/editor/ast-service.d.ts +30 -0
  154. package/dist/editor/ast-service.js +180 -0
  155. package/dist/editor/css-rules.d.ts +54 -0
  156. package/dist/editor/css-rules.js +423 -0
  157. package/dist/editor/editor-api-client.d.ts +51 -0
  158. package/dist/editor/editor-api-client.js +162 -0
  159. package/dist/editor/editor-bridge.d.ts +1 -0
  160. package/dist/editor/editor-bridge.js +18 -8
  161. package/dist/editor/editor-toolbar.d.ts +14 -0
  162. package/dist/editor/editor-toolbar.js +115 -0
  163. package/dist/editor/file-editor.d.ts +9 -0
  164. package/dist/editor/file-editor.js +236 -0
  165. package/dist/editor/file-service.d.ts +16 -0
  166. package/dist/editor/file-service.js +52 -0
  167. package/dist/editor/i18n-key-gen.d.ts +1 -0
  168. package/dist/editor/i18n-key-gen.js +7 -0
  169. package/dist/editor/inline-text-edit.d.ts +5 -0
  170. package/dist/editor/inline-text-edit.js +173 -92
  171. package/dist/editor/overlay-events.d.ts +5 -0
  172. package/dist/editor/overlay-events.js +364 -0
  173. package/dist/editor/overlay-hmr.d.ts +2 -0
  174. package/dist/editor/overlay-hmr.js +76 -0
  175. package/dist/editor/overlay-selection.d.ts +29 -0
  176. package/dist/editor/overlay-selection.js +148 -0
  177. package/dist/editor/overlay-utils.d.ts +12 -0
  178. package/dist/editor/overlay-utils.js +59 -0
  179. package/dist/editor/properties-panel-persist.d.ts +14 -0
  180. package/dist/editor/properties-panel-persist.js +70 -0
  181. package/dist/editor/properties-panel-rows.d.ts +10 -0
  182. package/dist/editor/properties-panel-rows.js +349 -0
  183. package/dist/editor/properties-panel-styles.d.ts +4 -0
  184. package/dist/editor/properties-panel-styles.js +174 -0
  185. package/dist/editor/properties-panel.d.ts +4 -0
  186. package/dist/editor/properties-panel.js +148 -0
  187. package/dist/editor/property-registry.d.ts +16 -0
  188. package/dist/editor/property-registry.js +303 -0
  189. package/dist/editor/standalone-file-panel.d.ts +0 -0
  190. package/dist/editor/standalone-file-panel.js +1 -0
  191. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  192. package/dist/editor/standalone-overlay-dom.js +1 -0
  193. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  194. package/dist/editor/standalone-overlay-styles.js +1 -0
  195. package/dist/editor/standalone-overlay.d.ts +1 -0
  196. package/dist/editor/standalone-overlay.js +76 -0
  197. package/dist/editor/syntax-highlighter.d.ts +4 -0
  198. package/dist/editor/syntax-highlighter.js +81 -0
  199. package/dist/editor/text-toolbar.d.ts +11 -0
  200. package/dist/editor/text-toolbar.js +327 -0
  201. package/dist/editor/toolbar-styles.d.ts +4 -0
  202. package/dist/editor/toolbar-styles.js +198 -0
  203. package/dist/email/index.d.ts +32 -0
  204. package/dist/email/index.js +154 -0
  205. package/dist/email/providers/resend.d.ts +2 -0
  206. package/dist/email/providers/resend.js +24 -0
  207. package/dist/email/providers/sendgrid.d.ts +2 -0
  208. package/dist/email/providers/sendgrid.js +31 -0
  209. package/dist/email/providers/smtp.d.ts +13 -0
  210. package/dist/email/providers/smtp.js +125 -0
  211. package/dist/email/template-engine.d.ts +18 -0
  212. package/dist/email/template-engine.js +116 -0
  213. package/dist/email/templates/base.d.ts +9 -0
  214. package/dist/email/templates/base.js +65 -0
  215. package/dist/email/templates/password-reset.d.ts +5 -0
  216. package/dist/email/templates/password-reset.js +15 -0
  217. package/dist/email/templates/verify-email.d.ts +5 -0
  218. package/dist/email/templates/verify-email.js +15 -0
  219. package/dist/email/templates/welcome.d.ts +5 -0
  220. package/dist/email/templates/welcome.js +13 -0
  221. package/dist/email/types.d.ts +49 -0
  222. package/dist/email/types.js +1 -0
  223. package/dist/llms/generate.d.ts +46 -0
  224. package/dist/llms/generate.js +185 -0
  225. package/dist/permissions/guard.d.ts +28 -0
  226. package/dist/permissions/guard.js +30 -0
  227. package/dist/permissions/index.d.ts +6 -0
  228. package/dist/permissions/index.js +3 -0
  229. package/dist/permissions/service.d.ts +80 -0
  230. package/dist/permissions/service.js +210 -0
  231. package/dist/permissions/tables.d.ts +5 -0
  232. package/dist/permissions/tables.js +68 -0
  233. package/dist/permissions/types.d.ts +33 -0
  234. package/dist/permissions/types.js +1 -0
  235. package/dist/runtime/app-shell.d.ts +1 -1
  236. package/dist/runtime/app-shell.js +164 -0
  237. package/dist/runtime/auth.d.ts +10 -0
  238. package/dist/runtime/auth.js +30 -0
  239. package/dist/runtime/communication.d.ts +137 -0
  240. package/dist/runtime/communication.js +228 -0
  241. package/dist/runtime/error-boundary.d.ts +23 -0
  242. package/dist/runtime/error-boundary.js +120 -0
  243. package/dist/runtime/i18n.d.ts +6 -1
  244. package/dist/runtime/i18n.js +42 -21
  245. package/dist/runtime/island.d.ts +16 -0
  246. package/dist/runtime/island.js +80 -0
  247. package/dist/runtime/router-data.d.ts +3 -0
  248. package/dist/runtime/router-data.js +102 -17
  249. package/dist/runtime/router-hydration.js +34 -2
  250. package/dist/runtime/router.d.ts +19 -2
  251. package/dist/runtime/router.js +237 -43
  252. package/dist/runtime/socket-client.d.ts +2 -0
  253. package/dist/runtime/socket-client.js +30 -0
  254. package/dist/runtime/webrtc.d.ts +91 -0
  255. package/dist/runtime/webrtc.js +428 -0
  256. package/dist/shared/dom-shims.js +4 -2
  257. package/dist/shared/graceful-shutdown.d.ts +8 -0
  258. package/dist/shared/graceful-shutdown.js +36 -0
  259. package/dist/shared/health.d.ts +8 -0
  260. package/dist/shared/health.js +25 -0
  261. package/dist/shared/llms-txt.d.ts +31 -0
  262. package/dist/shared/llms-txt.js +85 -0
  263. package/dist/shared/logger.d.ts +32 -0
  264. package/dist/shared/logger.js +93 -0
  265. package/dist/shared/meta.d.ts +27 -0
  266. package/dist/shared/meta.js +71 -0
  267. package/dist/shared/middleware-runner.d.ts +9 -0
  268. package/dist/shared/middleware-runner.js +29 -0
  269. package/dist/shared/rate-limit.d.ts +18 -0
  270. package/dist/shared/rate-limit.js +71 -0
  271. package/dist/shared/request-id.d.ts +5 -0
  272. package/dist/shared/request-id.js +18 -0
  273. package/dist/shared/route-matching.js +16 -1
  274. package/dist/shared/security-headers.d.ts +18 -0
  275. package/dist/shared/security-headers.js +38 -0
  276. package/dist/shared/socket-io-setup.d.ts +11 -0
  277. package/dist/shared/socket-io-setup.js +51 -0
  278. package/dist/shared/types.d.ts +15 -0
  279. package/dist/shared/utils.d.ts +33 -7
  280. package/dist/shared/utils.js +164 -27
  281. package/dist/storage/adapters/local.d.ts +44 -0
  282. package/dist/storage/adapters/local.js +85 -0
  283. package/dist/storage/adapters/s3.d.ts +32 -0
  284. package/dist/storage/adapters/s3.js +119 -0
  285. package/dist/storage/adapters/types.d.ts +53 -0
  286. package/dist/storage/adapters/types.js +1 -0
  287. package/dist/storage/index.d.ts +76 -0
  288. package/dist/storage/index.js +83 -0
  289. package/package.json +45 -7
  290. package/templates/blog/api/posts.ts +4 -18
  291. package/templates/blog/data/migrations/001_init.sql +6 -5
  292. package/templates/blog/lumenjs.config.ts +3 -0
  293. package/templates/blog/package.json +14 -0
  294. package/templates/blog/pages/_layout.ts +25 -0
  295. package/templates/blog/pages/index.ts +48 -22
  296. package/templates/blog/pages/posts/[slug].ts +45 -20
  297. package/templates/blog/pages/tag/[tag].ts +44 -0
  298. package/templates/dashboard/api/stats.ts +8 -5
  299. package/templates/dashboard/lumenjs.config.ts +3 -0
  300. package/templates/dashboard/package.json +14 -0
  301. package/templates/dashboard/pages/_layout.ts +25 -0
  302. package/templates/dashboard/pages/index.ts +54 -23
  303. package/templates/dashboard/pages/settings/index.ts +29 -0
  304. package/templates/default/lumenjs.config.ts +3 -0
  305. package/templates/default/package.json +14 -0
  306. package/templates/default/pages/index.ts +24 -0
@@ -0,0 +1,35 @@
1
+ import type { KeyBundle, EncryptedEnvelope } from './types.js';
2
+ import type { CommunicationStore } from './store.js';
3
+ import type { LumenDb } from '../db/index.js';
4
+ /** Context for encryption handlers */
5
+ export interface EncryptionContext {
6
+ userId: string;
7
+ store: CommunicationStore;
8
+ /** Emit data to the current socket */
9
+ push: (data: any) => void;
10
+ /** Emit to a specific user's sockets */
11
+ emitToUser: (targetUserId: string, data: any) => void;
12
+ /** LumenJS database instance (optional) */
13
+ db?: LumenDb;
14
+ }
15
+ /**
16
+ * Handle a client uploading their public key bundle.
17
+ * Server stores the bundle — it never sees private keys.
18
+ */
19
+ export declare function handleUploadKeys(ctx: EncryptionContext, data: KeyBundle): Promise<void>;
20
+ /**
21
+ * Handle a client requesting another user's key bundle for session setup.
22
+ * Pops one one-time pre-key (consumed once).
23
+ */
24
+ export declare function handleRequestKeys(ctx: EncryptionContext, data: {
25
+ recipientId: string;
26
+ }): Promise<void>;
27
+ /**
28
+ * Relay a session initialization message to the recipient.
29
+ * The server never reads the envelope — just forwards it.
30
+ */
31
+ export declare function handleSessionInit(ctx: EncryptionContext, data: {
32
+ recipientId: string;
33
+ sessionId: string;
34
+ envelope: EncryptedEnvelope;
35
+ }): void;
@@ -0,0 +1,90 @@
1
+ // ── Key Upload ──────────────────────────────────────────────────
2
+ /**
3
+ * Handle a client uploading their public key bundle.
4
+ * Server stores the bundle — it never sees private keys.
5
+ */
6
+ export async function handleUploadKeys(ctx, data) {
7
+ // Ensure the bundle belongs to the sender
8
+ const bundle = {
9
+ ...data,
10
+ userId: ctx.userId,
11
+ uploadedAt: new Date().toISOString(),
12
+ };
13
+ // Store in memory
14
+ ctx.store.setKeyBundle(ctx.userId, bundle);
15
+ // Persist to DB if available
16
+ if (ctx.db) {
17
+ await ctx.db.run(`INSERT OR REPLACE INTO encryption_keys (user_id, identity_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, uploaded_at)
18
+ VALUES (?, ?, ?, ?, ?, ?)`, ctx.userId, bundle.identityKey, bundle.signedPreKey.keyId, bundle.signedPreKey.publicKey, bundle.signedPreKeySignature, bundle.uploadedAt);
19
+ // Replace one-time pre-keys
20
+ await ctx.db.run(`DELETE FROM encryption_prekeys WHERE user_id = ?`, ctx.userId);
21
+ for (const otk of bundle.oneTimePreKeys) {
22
+ await ctx.db.run(`INSERT INTO encryption_prekeys (user_id, key_id, public_key) VALUES (?, ?, ?)`, ctx.userId, otk.keyId, otk.publicKey);
23
+ }
24
+ }
25
+ ctx.push({ event: 'encryption:keys-uploaded', data: { userId: ctx.userId, keyCount: bundle.oneTimePreKeys.length } });
26
+ }
27
+ // ── Key Request ─────────────────────────────────────────────────
28
+ /**
29
+ * Handle a client requesting another user's key bundle for session setup.
30
+ * Pops one one-time pre-key (consumed once).
31
+ */
32
+ export async function handleRequestKeys(ctx, data) {
33
+ let bundle = ctx.store.getKeyBundle(data.recipientId);
34
+ // Try loading from DB if not in memory
35
+ if (!bundle && ctx.db) {
36
+ const row = await ctx.db.get(`SELECT * FROM encryption_keys WHERE user_id = ?`, data.recipientId);
37
+ if (row) {
38
+ const prekeys = await ctx.db.all(`SELECT key_id, public_key FROM encryption_prekeys WHERE user_id = ? ORDER BY key_id`, data.recipientId);
39
+ bundle = {
40
+ userId: data.recipientId,
41
+ identityKey: row.identity_key,
42
+ signedPreKey: { keyId: row.signed_pre_key_id, publicKey: row.signed_pre_key },
43
+ signedPreKeySignature: row.signed_pre_key_signature,
44
+ oneTimePreKeys: prekeys.map((pk) => ({ keyId: pk.key_id, publicKey: pk.public_key })),
45
+ uploadedAt: row.uploaded_at,
46
+ };
47
+ ctx.store.setKeyBundle(data.recipientId, bundle);
48
+ }
49
+ }
50
+ if (!bundle) {
51
+ ctx.push({ event: 'encryption:keys-response', data: { error: 'no_keys', recipientId: data.recipientId } });
52
+ return;
53
+ }
54
+ // Pop one one-time pre-key (consumed once per session)
55
+ const oneTimePreKey = ctx.store.popOneTimePreKey(data.recipientId);
56
+ // Remove from DB too
57
+ if (oneTimePreKey && ctx.db) {
58
+ await ctx.db.run(`DELETE FROM encryption_prekeys WHERE user_id = ? AND key_id = ?`, data.recipientId, oneTimePreKey.keyId);
59
+ }
60
+ const response = {
61
+ recipientId: data.recipientId,
62
+ identityKey: bundle.identityKey,
63
+ signedPreKey: bundle.signedPreKey,
64
+ signedPreKeySignature: bundle.signedPreKeySignature,
65
+ oneTimePreKey,
66
+ };
67
+ ctx.push({ event: 'encryption:keys-response', data: response });
68
+ // Notify recipient if their one-time pre-keys are depleted
69
+ if (ctx.store.getOneTimePreKeyCount(data.recipientId) === 0) {
70
+ ctx.emitToUser(data.recipientId, {
71
+ event: 'encryption:keys-depleted',
72
+ data: { userId: data.recipientId },
73
+ });
74
+ }
75
+ }
76
+ // ── Session Init Relay ──────────────────────────────────────────
77
+ /**
78
+ * Relay a session initialization message to the recipient.
79
+ * The server never reads the envelope — just forwards it.
80
+ */
81
+ export function handleSessionInit(ctx, data) {
82
+ ctx.emitToUser(data.recipientId, {
83
+ event: 'encryption:session-init',
84
+ data: {
85
+ senderId: ctx.userId,
86
+ sessionId: data.sessionId,
87
+ envelope: data.envelope,
88
+ },
89
+ });
90
+ }
@@ -0,0 +1,27 @@
1
+ import type { CommunicationConfig } from '../types.js';
2
+ import type { CommunicationStore } from '../store.js';
3
+ import type { LumenDb } from '../../db/index.js';
4
+ import type { StorageAdapter } from '../../storage/adapters/types.js';
5
+ /** Context passed to every handler from the socket connection */
6
+ export interface HandlerContext {
7
+ userId: string;
8
+ store: CommunicationStore;
9
+ /** Resolved communication config */
10
+ config: CommunicationConfig;
11
+ /** Emit data to the current socket */
12
+ push: (data: any) => void;
13
+ /** Broadcast to all sockets in a room */
14
+ broadcastAll: (room: string, data: any) => void;
15
+ /** Broadcast to all sockets in a room except the sender */
16
+ broadcast: (room: string, data: any) => void;
17
+ /** Join a socket room */
18
+ joinRoom: (room: string) => void;
19
+ /** Leave a socket room */
20
+ leaveRoom: (room: string) => void;
21
+ /** Emit data to all sockets for a specific user */
22
+ emitToUser?: (userId: string, data: any) => void;
23
+ /** LumenJS database instance (optional — only if app has DB set up) */
24
+ db?: LumenDb;
25
+ /** Storage adapter for file uploads (optional — only if storage is configured) */
26
+ storage?: StorageAdapter;
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { HandlerContext } from './context.js';
2
+ export declare function handleConversationCreate(ctx: HandlerContext, data: {
3
+ type: 'direct' | 'group';
4
+ name?: string;
5
+ participantIds: string[];
6
+ }): Promise<void>;
7
+ export declare function handleConversationJoin(ctx: HandlerContext, data: {
8
+ conversationId: string;
9
+ }): Promise<void>;
10
+ export declare function handleConversationLeave(ctx: HandlerContext, data: {
11
+ conversationId: string;
12
+ }): void;
13
+ export declare function handleConversationArchive(ctx: HandlerContext, data: {
14
+ conversationId: string;
15
+ archived: boolean;
16
+ }): Promise<void>;
17
+ export declare function handleConversationMute(ctx: HandlerContext, data: {
18
+ conversationId: string;
19
+ muted: boolean;
20
+ }): Promise<void>;
21
+ export declare function handleConversationPin(ctx: HandlerContext, data: {
22
+ conversationId: string;
23
+ pinned: boolean;
24
+ }): Promise<void>;
@@ -0,0 +1,113 @@
1
+ // ── Conversation ────────────────────────────────────────────────
2
+ export async function handleConversationCreate(ctx, data) {
3
+ const now = new Date().toISOString();
4
+ let conversation;
5
+ if (ctx.db) {
6
+ const convId = crypto.randomUUID();
7
+ await ctx.db.run(`INSERT INTO conversations (id, type, name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`, convId, data.type, data.name || null, now, now);
8
+ // Add the creator as a participant with 'owner' role
9
+ const allParticipantIds = [ctx.userId, ...data.participantIds.filter(id => id !== ctx.userId)];
10
+ for (const uid of allParticipantIds) {
11
+ const role = uid === ctx.userId ? 'owner' : 'member';
12
+ await ctx.db.run(`INSERT INTO conversation_participants (conversation_id, user_id, role, joined_at) VALUES (?, ?, ?, ?)`, convId, uid, role, now);
13
+ }
14
+ conversation = {
15
+ id: convId,
16
+ type: data.type,
17
+ name: data.name,
18
+ participants: allParticipantIds.map(uid => ({
19
+ userId: uid,
20
+ displayName: '',
21
+ role: uid === ctx.userId ? 'owner' : 'member',
22
+ joinedAt: now,
23
+ presence: ctx.store.getPresence(uid)?.status || 'offline',
24
+ })),
25
+ createdAt: now,
26
+ updatedAt: now,
27
+ unreadCount: 0,
28
+ };
29
+ }
30
+ else {
31
+ const allParticipantIds = [ctx.userId, ...data.participantIds.filter(id => id !== ctx.userId)];
32
+ conversation = {
33
+ id: crypto.randomUUID(),
34
+ type: data.type,
35
+ name: data.name,
36
+ participants: allParticipantIds.map(uid => ({
37
+ userId: uid,
38
+ displayName: '',
39
+ role: uid === ctx.userId ? 'owner' : 'member',
40
+ joinedAt: now,
41
+ presence: ctx.store.getPresence(uid)?.status || 'offline',
42
+ })),
43
+ createdAt: now,
44
+ updatedAt: now,
45
+ unreadCount: 0,
46
+ };
47
+ }
48
+ // Auto-join the creator to the conversation room
49
+ ctx.joinRoom(`conv:${conversation.id}`);
50
+ // Notify the creator
51
+ ctx.push({ event: 'conversation:new', data: conversation });
52
+ // Notify other participants (they haven't joined the room yet)
53
+ if (ctx.emitToUser) {
54
+ for (const uid of data.participantIds) {
55
+ if (uid !== ctx.userId) {
56
+ ctx.emitToUser(uid, { event: 'conversation:new', data: conversation });
57
+ }
58
+ }
59
+ }
60
+ }
61
+ export async function handleConversationJoin(ctx, data) {
62
+ // Verify the user is an actual participant of this conversation
63
+ if (ctx.db) {
64
+ const row = await ctx.db.get('SELECT 1 FROM conversation_participants WHERE conversation_id = ? AND user_id = ?', data.conversationId, ctx.userId);
65
+ if (!row) {
66
+ ctx.push({ event: 'error', data: { code: 'FORBIDDEN', message: 'Not a participant of this conversation' } });
67
+ return;
68
+ }
69
+ }
70
+ ctx.joinRoom(`conv:${data.conversationId}`);
71
+ ctx.store.addConversationMember(data.conversationId, ctx.userId);
72
+ ctx.store.joinConversation(ctx.userId, data.conversationId);
73
+ }
74
+ export function handleConversationLeave(ctx, data) {
75
+ ctx.leaveRoom(`conv:${data.conversationId}`);
76
+ ctx.store.removeConversationMember(data.conversationId, ctx.userId);
77
+ ctx.store.leaveConversation(ctx.userId, data.conversationId);
78
+ }
79
+ export async function handleConversationArchive(ctx, data) {
80
+ // Verify the user is a participant
81
+ if (ctx.db) {
82
+ const row = await ctx.db.get('SELECT 1 FROM conversation_participants WHERE conversation_id = ? AND user_id = ?', data.conversationId, ctx.userId);
83
+ if (!row) {
84
+ ctx.push({ event: 'error', data: { code: 'FORBIDDEN', message: 'Not a participant of this conversation' } });
85
+ return;
86
+ }
87
+ await ctx.db.run(`UPDATE conversations SET archived = ?, updated_at = ? WHERE id = ?`, data.archived ? 1 : 0, new Date().toISOString(), data.conversationId);
88
+ }
89
+ ctx.broadcastAll(`conv:${data.conversationId}`, {
90
+ event: 'conversation:updated',
91
+ data: { id: data.conversationId, archived: data.archived },
92
+ });
93
+ }
94
+ export async function handleConversationMute(ctx, data) {
95
+ if (ctx.db) {
96
+ await ctx.db.run(`UPDATE conversation_participants SET muted = ?, updated_at = ? WHERE conversation_id = ? AND user_id = ?`, data.muted ? 1 : 0, new Date().toISOString(), data.conversationId, ctx.userId);
97
+ }
98
+ // Only notify the requesting user — mute is per-user
99
+ ctx.push({
100
+ event: 'conversation:updated',
101
+ data: { id: data.conversationId, muted: data.muted },
102
+ });
103
+ }
104
+ export async function handleConversationPin(ctx, data) {
105
+ if (ctx.db) {
106
+ await ctx.db.run(`UPDATE conversation_participants SET pinned = ?, updated_at = ? WHERE conversation_id = ? AND user_id = ?`, data.pinned ? 1 : 0, new Date().toISOString(), data.conversationId, ctx.userId);
107
+ }
108
+ // Only notify the requesting user — pin is per-user
109
+ ctx.push({
110
+ event: 'conversation:updated',
111
+ data: { id: data.conversationId, pinned: data.pinned },
112
+ });
113
+ }
@@ -0,0 +1,17 @@
1
+ import type { HandlerContext } from './context.js';
2
+ /**
3
+ * Handle a client requesting a presigned upload URL for a chat file attachment.
4
+ *
5
+ * E2E security model:
6
+ * 1. Client encrypts the file locally (AES-256-GCM with a random key).
7
+ * 2. This handler returns a presigned PUT URL — the server never sees the plaintext.
8
+ * 3. Client PUTs the encrypted blob directly to storage (S3 / local).
9
+ * 4. The file encryption key is included inside the Signal message envelope,
10
+ * encrypted per-recipient. The server cannot decrypt either the file or the key.
11
+ */
12
+ export declare function handleFileUploadRequest(ctx: HandlerContext, data: {
13
+ conversationId: string;
14
+ mimeType: string;
15
+ size: number;
16
+ fileName: string;
17
+ }): Promise<void>;
@@ -0,0 +1,62 @@
1
+ // ── File Upload (Presigned URL) ──────────────────────────────────
2
+ /**
3
+ * Handle a client requesting a presigned upload URL for a chat file attachment.
4
+ *
5
+ * E2E security model:
6
+ * 1. Client encrypts the file locally (AES-256-GCM with a random key).
7
+ * 2. This handler returns a presigned PUT URL — the server never sees the plaintext.
8
+ * 3. Client PUTs the encrypted blob directly to storage (S3 / local).
9
+ * 4. The file encryption key is included inside the Signal message envelope,
10
+ * encrypted per-recipient. The server cannot decrypt either the file or the key.
11
+ */
12
+ export async function handleFileUploadRequest(ctx, data) {
13
+ if (!ctx.storage) {
14
+ ctx.push({
15
+ event: 'file:upload-error',
16
+ data: { code: 'STORAGE_NOT_CONFIGURED', message: 'Storage is not configured on this server.' },
17
+ });
18
+ return;
19
+ }
20
+ // Validate against file upload config
21
+ if (ctx.config.fileUpload) {
22
+ const { maxFileSize, allowedMimeTypes } = ctx.config.fileUpload;
23
+ if (data.size > maxFileSize) {
24
+ ctx.push({
25
+ event: 'file:upload-error',
26
+ data: {
27
+ code: 'FILE_TOO_LARGE',
28
+ message: `File size ${data.size} exceeds maximum allowed size of ${maxFileSize} bytes.`,
29
+ },
30
+ });
31
+ return;
32
+ }
33
+ if (allowedMimeTypes && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(data.mimeType)) {
34
+ ctx.push({
35
+ event: 'file:upload-error',
36
+ data: {
37
+ code: 'MIME_TYPE_NOT_ALLOWED',
38
+ message: `MIME type '${data.mimeType}' is not allowed.`,
39
+ },
40
+ });
41
+ return;
42
+ }
43
+ }
44
+ // Generate a unique file key scoped to the conversation
45
+ const fileId = crypto.randomUUID();
46
+ const ext = data.fileName.includes('.') ? `.${data.fileName.split('.').pop()}` : '';
47
+ const key = `conversations/${data.conversationId}/${fileId}${ext}`;
48
+ const presigned = await ctx.storage.presignPut(key, {
49
+ mimeType: data.mimeType,
50
+ expiresIn: 300, // 5 minutes to complete the upload
51
+ maxSize: ctx.config.fileUpload?.maxFileSize,
52
+ });
53
+ ctx.push({
54
+ event: 'file:upload-ready',
55
+ data: {
56
+ fileId,
57
+ uploadUrl: presigned.uploadUrl,
58
+ key: presigned.key,
59
+ expiresAt: presigned.expiresAt,
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,30 @@
1
+ import type { Message, MessageAttachment, MessageForward, EncryptedEnvelope } from '../types.js';
2
+ import type { HandlerContext } from './context.js';
3
+ export declare function handleMessageSend(ctx: HandlerContext, data: {
4
+ conversationId: string;
5
+ content: string;
6
+ type: Message['type'];
7
+ replyTo?: string;
8
+ attachment?: MessageAttachment;
9
+ encrypted?: boolean;
10
+ envelope?: EncryptedEnvelope;
11
+ }): Promise<void>;
12
+ export declare function handleMessageRead(ctx: HandlerContext, data: {
13
+ conversationId: string;
14
+ messageId: string;
15
+ }): Promise<void>;
16
+ export declare function handleMessageReact(ctx: HandlerContext, data: {
17
+ messageId: string;
18
+ conversationId: string;
19
+ emoji: string;
20
+ }): Promise<void>;
21
+ export declare function handleMessageEdit(ctx: HandlerContext, data: {
22
+ messageId: string;
23
+ conversationId: string;
24
+ content: string;
25
+ }): Promise<void>;
26
+ export declare function handleMessageDelete(ctx: HandlerContext, data: {
27
+ messageId: string;
28
+ conversationId: string;
29
+ }): Promise<void>;
30
+ export declare function handleMessageForward(ctx: HandlerContext, data: MessageForward): Promise<void>;
@@ -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;