@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,86 @@
1
+ import type { CommunicationConfig } from './types.js';
2
+ import type { LumenDb } from '../db/index.js';
3
+ import type { StorageAdapter } from '../storage/adapters/types.js';
4
+ /** Options for creating a communication socket handler */
5
+ export interface CommunicationHandlerOptions {
6
+ /** Communication config overrides */
7
+ config?: Partial<CommunicationConfig>;
8
+ /** Extract userId from socket handshake headers/query. Default: reads X-User-Id header or userId query param */
9
+ getUserId?: (headers: Record<string, any>, query: Record<string, any>) => string | undefined;
10
+ /** LumenJS database instance — if provided, messages are persisted */
11
+ db?: LumenDb;
12
+ /**
13
+ * Storage adapter for chat file uploads.
14
+ * Falls back to the global singleton (useStorage()) if not provided.
15
+ * Required for file:request-upload support.
16
+ */
17
+ storage?: StorageAdapter;
18
+ }
19
+ /**
20
+ * Creates a LumenJS-compatible socket handler for communication.
21
+ *
22
+ * Usage in a page file:
23
+ * ```ts
24
+ * import { createCommunicationHandler } from '@nuraly/lumenjs/dist/communication/server.js';
25
+ * export const socket = createCommunicationHandler();
26
+ * ```
27
+ */
28
+ export declare function createCommunicationHandler(options?: CommunicationHandlerOptions): (ctx: {
29
+ on: (event: string, handler: (...args: any[]) => void) => void;
30
+ push: (data: any) => void;
31
+ room: {
32
+ join: (name: string) => void;
33
+ leave: (name: string) => void;
34
+ broadcast: (name: string, data: any) => void;
35
+ broadcastAll: (name: string, data: any) => void;
36
+ };
37
+ params: Record<string, string>;
38
+ headers: Record<string, any>;
39
+ locale?: string;
40
+ socket: any;
41
+ }) => (() => void) | undefined;
42
+ /**
43
+ * Creates reusable API handler functions for communication REST endpoints.
44
+ *
45
+ * Usage in an API route:
46
+ * ```ts
47
+ * import { createCommunicationApiHandlers } from '@nuraly/lumenjs/dist/communication/server.js';
48
+ * import { useDb } from '@nuraly/lumenjs/dist/db/index.js';
49
+ *
50
+ * const communication = createCommunicationApiHandlers(useDb());
51
+ *
52
+ * export function GET(req) {
53
+ * return communication.getConversations(req.query.userId);
54
+ * }
55
+ * ```
56
+ */
57
+ export declare function createCommunicationApiHandlers(db: LumenDb): {
58
+ /** List conversations for a user */
59
+ getConversations(userId: string, opts?: {
60
+ limit?: number;
61
+ offset?: number;
62
+ }): Promise<any[]>;
63
+ /** Get paginated message history for a conversation */
64
+ getMessages(conversationId: string, opts?: {
65
+ limit?: number;
66
+ before?: string;
67
+ }): Promise<any[]>;
68
+ /** Create a new conversation */
69
+ createConversation(data: {
70
+ type: "direct" | "group";
71
+ name?: string;
72
+ participantIds: string[];
73
+ }): Promise<any>;
74
+ /** Search messages by content */
75
+ searchMessages(query: string, opts?: {
76
+ conversationId?: string;
77
+ limit?: number;
78
+ }): Promise<any[]>;
79
+ /** Get a single message by ID */
80
+ getMessage(messageId: string): Promise<any>;
81
+ /** Delete a message (soft delete by setting content to empty) */
82
+ deleteMessage(messageId: string, userId: string): Promise<{
83
+ changes: number;
84
+ lastInsertRowid: number | bigint;
85
+ }>;
86
+ };
@@ -0,0 +1,212 @@
1
+ import { useCommunicationStore } from './store.js';
2
+ import { handleConversationCreate, handleConversationJoin, handleConversationLeave, handleConversationArchive, handleConversationMute, handleConversationPin, handleMessageSend, handleMessageRead, handleMessageReact, handleMessageEdit, handleMessageDelete, handleMessageForward, handleTypingStart, handleTypingStop, handlePresenceUpdate, handleConnect, handleDisconnect, handleFileUploadRequest, } from './handlers.js';
3
+ import { handleCallInitiate, handleCallRespond, handleCallHangup, handleCallMediaToggle, handleCallAddParticipant, handleCallRemoveParticipant, handleSignalOffer, handleSignalAnswer, handleSignalIceCandidate, handleSignalIceRestart, handleCallQualityReport, } from './signaling.js';
4
+ import { handleUploadKeys, handleRequestKeys, handleSessionInit, } from './encryption.js';
5
+ import { useStorage } from '../storage/index.js';
6
+ const defaultGetUserId = (headers, query) => {
7
+ return headers['x-user-id'] || query.userId || undefined;
8
+ };
9
+ /**
10
+ * Creates a LumenJS-compatible socket handler for communication.
11
+ *
12
+ * Usage in a page file:
13
+ * ```ts
14
+ * import { createCommunicationHandler } from '@nuraly/lumenjs/dist/communication/server.js';
15
+ * export const socket = createCommunicationHandler();
16
+ * ```
17
+ */
18
+ export function createCommunicationHandler(options = {}) {
19
+ const getUserId = options.getUserId || defaultGetUserId;
20
+ const store = useCommunicationStore();
21
+ const resolvedConfig = { ...options.config };
22
+ // Apply configurable typing timeout to the store
23
+ if (resolvedConfig.typingTimeoutMs != null) {
24
+ store.typingTimeoutMs = resolvedConfig.typingTimeoutMs;
25
+ }
26
+ return (ctx) => {
27
+ const query = ctx.socket?.handshake?.query || {};
28
+ const userId = getUserId(ctx.headers, query);
29
+ if (!userId) {
30
+ console.warn('[LumenJS:Communication] No userId found in socket handshake. Connection rejected.');
31
+ ctx.socket?.disconnect?.();
32
+ return;
33
+ }
34
+ const socketId = ctx.socket?.id || crypto.randomUUID();
35
+ // Register socket and check if this is the user's first connection
36
+ const isFirstSocket = !store.isUserOnline(userId);
37
+ store.mapUserSocket(userId, socketId);
38
+ store.setPresence(userId, 'online');
39
+ // Build handler context
40
+ const handlerCtx = {
41
+ userId,
42
+ store,
43
+ config: resolvedConfig,
44
+ push: ctx.push,
45
+ broadcastAll: ctx.room.broadcastAll,
46
+ broadcast: ctx.room.broadcast,
47
+ joinRoom: ctx.room.join,
48
+ leaveRoom: ctx.room.leave,
49
+ emitToUser: (targetUserId, data) => {
50
+ const sockets = store.getSocketsForUser(targetUserId);
51
+ const io = ctx.socket?.nsp;
52
+ if (io) {
53
+ for (const sid of sockets) {
54
+ io.to(sid).emit('nk:data', data);
55
+ }
56
+ }
57
+ },
58
+ db: options.db,
59
+ storage: options.storage ?? useStorage() ?? undefined,
60
+ };
61
+ // Build signaling context
62
+ const signalingCtx = {
63
+ userId,
64
+ store,
65
+ emitToSocket: (sid, data) => {
66
+ // For targeted emit, we use the raw socket.io namespace
67
+ const io = ctx.socket?.nsp;
68
+ if (io) {
69
+ io.to(sid).emit('nk:data', data);
70
+ }
71
+ },
72
+ broadcastAll: ctx.room.broadcastAll,
73
+ };
74
+ // Broadcast online status if this is the user's first socket
75
+ if (isFirstSocket) {
76
+ handleConnect(handlerCtx);
77
+ }
78
+ // ── Payload Validation ────────────────────────────────────
79
+ /** Validate socket event payload is a non-null object with expected string fields. */
80
+ function validated(data, requiredStrings, handler) {
81
+ if (!data || typeof data !== 'object')
82
+ return;
83
+ for (const field of requiredStrings) {
84
+ if (typeof data[field] !== 'string' || data[field].length === 0)
85
+ return;
86
+ }
87
+ handler(data);
88
+ }
89
+ // ── Chat Events ──────────────────────────────────────────
90
+ ctx.on('conversation:create', (data) => validated(data, ['type'], () => handleConversationCreate(handlerCtx, data)));
91
+ ctx.on('conversation:join', (data) => validated(data, ['conversationId'], () => handleConversationJoin(handlerCtx, data)));
92
+ ctx.on('conversation:leave', (data) => validated(data, ['conversationId'], () => handleConversationLeave(handlerCtx, data)));
93
+ ctx.on('conversation:archive', (data) => validated(data, ['conversationId'], () => handleConversationArchive(handlerCtx, data)));
94
+ ctx.on('conversation:mute', (data) => validated(data, ['conversationId'], () => handleConversationMute(handlerCtx, data)));
95
+ ctx.on('conversation:pin', (data) => validated(data, ['conversationId'], () => handleConversationPin(handlerCtx, data)));
96
+ ctx.on('message:send', (data) => validated(data, ['conversationId', 'content'], () => handleMessageSend(handlerCtx, data)));
97
+ ctx.on('message:react', (data) => validated(data, ['messageId', 'conversationId', 'emoji'], () => handleMessageReact(handlerCtx, data)));
98
+ ctx.on('message:edit', (data) => validated(data, ['messageId', 'conversationId', 'content'], () => handleMessageEdit(handlerCtx, data)));
99
+ ctx.on('message:delete', (data) => validated(data, ['messageId', 'conversationId'], () => handleMessageDelete(handlerCtx, data)));
100
+ ctx.on('message:read', (data) => validated(data, ['messageId', 'conversationId'], () => handleMessageRead(handlerCtx, data)));
101
+ ctx.on('message:forward', (data) => validated(data, ['messageId', 'fromConversationId', 'toConversationId'], () => handleMessageForward(handlerCtx, data)));
102
+ ctx.on('typing:start', (data) => validated(data, ['conversationId'], () => handleTypingStart(handlerCtx, data)));
103
+ ctx.on('typing:stop', (data) => validated(data, ['conversationId'], () => handleTypingStop(handlerCtx, data)));
104
+ ctx.on('presence:update', (data) => validated(data, ['status'], () => handlePresenceUpdate(handlerCtx, data)));
105
+ // ── Call Events ──────────────────────────────────────────
106
+ ctx.on('call:initiate', (data) => handleCallInitiate(signalingCtx, data));
107
+ ctx.on('call:respond', (data) => handleCallRespond(signalingCtx, data));
108
+ ctx.on('call:hangup', (data) => handleCallHangup(signalingCtx, data));
109
+ ctx.on('call:media-toggle', (data) => handleCallMediaToggle(signalingCtx, data));
110
+ ctx.on('call:add-participant', (data) => handleCallAddParticipant(signalingCtx, data));
111
+ ctx.on('call:remove-participant', (data) => handleCallRemoveParticipant(signalingCtx, data));
112
+ // ── WebRTC Signaling ─────────────────────────────────────
113
+ ctx.on('signal:offer', (data) => handleSignalOffer(signalingCtx, data));
114
+ ctx.on('signal:answer', (data) => handleSignalAnswer(signalingCtx, data));
115
+ ctx.on('signal:ice-candidate', (data) => handleSignalIceCandidate(signalingCtx, data));
116
+ ctx.on('signal:ice-restart', (data) => handleSignalIceRestart(signalingCtx, data));
117
+ ctx.on('call:quality-report', (data) => handleCallQualityReport(signalingCtx, data));
118
+ // ── E2E Encryption ─────────────────────────────────────────
119
+ const encryptionCtx = {
120
+ userId,
121
+ store,
122
+ push: ctx.push,
123
+ emitToUser: (targetUserId, data) => {
124
+ const sockets = store.getSocketsForUser(targetUserId);
125
+ const io = ctx.socket?.nsp;
126
+ if (io) {
127
+ for (const sid of sockets) {
128
+ io.to(sid).emit('nk:data', data);
129
+ }
130
+ }
131
+ },
132
+ db: options.db,
133
+ };
134
+ ctx.on('encryption:upload-keys', (data) => handleUploadKeys(encryptionCtx, data));
135
+ ctx.on('encryption:request-keys', (data) => handleRequestKeys(encryptionCtx, data));
136
+ ctx.on('encryption:session-init', (data) => handleSessionInit(encryptionCtx, data));
137
+ // ── File Upload ───────────────────────────────────────────
138
+ ctx.on('file:request-upload', (data) => handleFileUploadRequest(handlerCtx, data));
139
+ // ── Cleanup on Disconnect ────────────────────────────────
140
+ return () => {
141
+ handleDisconnect(handlerCtx, socketId);
142
+ };
143
+ };
144
+ }
145
+ /**
146
+ * Creates reusable API handler functions for communication REST endpoints.
147
+ *
148
+ * Usage in an API route:
149
+ * ```ts
150
+ * import { createCommunicationApiHandlers } from '@nuraly/lumenjs/dist/communication/server.js';
151
+ * import { useDb } from '@nuraly/lumenjs/dist/db/index.js';
152
+ *
153
+ * const communication = createCommunicationApiHandlers(useDb());
154
+ *
155
+ * export function GET(req) {
156
+ * return communication.getConversations(req.query.userId);
157
+ * }
158
+ * ```
159
+ */
160
+ export function createCommunicationApiHandlers(db) {
161
+ return {
162
+ /** List conversations for a user */
163
+ async getConversations(userId, opts) {
164
+ const limit = opts?.limit || 50;
165
+ const offset = opts?.offset || 0;
166
+ return db.all(`SELECT c.*,
167
+ (SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id
168
+ AND m.id NOT IN (SELECT message_id FROM read_receipts WHERE user_id = ?)) as unread_count
169
+ FROM conversations c
170
+ JOIN conversation_participants cp ON cp.conversation_id = c.id
171
+ WHERE cp.user_id = ?
172
+ ORDER BY c.updated_at DESC
173
+ LIMIT ? OFFSET ?`, userId, userId, limit, offset);
174
+ },
175
+ /** Get paginated message history for a conversation */
176
+ async getMessages(conversationId, opts) {
177
+ const limit = opts?.limit || 50;
178
+ if (opts?.before) {
179
+ return db.all(`SELECT * FROM messages WHERE conversation_id = ? AND created_at < ?
180
+ ORDER BY created_at DESC LIMIT ?`, conversationId, opts.before, limit);
181
+ }
182
+ return db.all(`SELECT * FROM messages WHERE conversation_id = ?
183
+ ORDER BY created_at DESC LIMIT ?`, conversationId, limit);
184
+ },
185
+ /** Create a new conversation */
186
+ async createConversation(data) {
187
+ const now = new Date().toISOString();
188
+ const convId = crypto.randomUUID();
189
+ await db.run(`INSERT INTO conversations (id, type, name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`, convId, data.type, data.name || null, now, now);
190
+ for (const uid of data.participantIds) {
191
+ await db.run(`INSERT INTO conversation_participants (conversation_id, user_id, role, joined_at) VALUES (?, ?, 'member', ?)`, convId, uid, now);
192
+ }
193
+ return db.get(`SELECT * FROM conversations WHERE id = ?`, convId);
194
+ },
195
+ /** Search messages by content */
196
+ async searchMessages(query, opts) {
197
+ const limit = opts?.limit || 20;
198
+ if (opts?.conversationId) {
199
+ return db.all(`SELECT * FROM messages WHERE conversation_id = ? AND content LIKE ? ORDER BY created_at DESC LIMIT ?`, opts.conversationId, `%${query}%`, limit);
200
+ }
201
+ return db.all(`SELECT * FROM messages WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?`, `%${query}%`, limit);
202
+ },
203
+ /** Get a single message by ID */
204
+ async getMessage(messageId) {
205
+ return db.get(`SELECT * FROM messages WHERE id = ?`, messageId);
206
+ },
207
+ /** Delete a message (soft delete by setting content to empty) */
208
+ async deleteMessage(messageId, userId) {
209
+ return db.run(`UPDATE messages SET content = '', type = 'system', updated_at = ? WHERE id = ? AND sender_id = ?`, new Date().toISOString(), messageId, userId);
210
+ },
211
+ };
212
+ }
@@ -0,0 +1,43 @@
1
+ import type { CallType, CallEndReason, SignalOffer, SignalIceCandidate, SignalIceRestart, ConnectionQualityReport } from './types.js';
2
+ import type { CommunicationStore } from './store.js';
3
+ /** Context for signaling handlers */
4
+ export interface SignalingContext {
5
+ userId: string;
6
+ store: CommunicationStore;
7
+ /** Emit to a specific socket by ID */
8
+ emitToSocket: (socketId: string, data: any) => void;
9
+ /** Broadcast to all sockets in a room */
10
+ broadcastAll: (room: string, data: any) => void;
11
+ }
12
+ export declare function handleCallInitiate(ctx: SignalingContext, data: {
13
+ conversationId: string;
14
+ type: CallType;
15
+ calleeIds: string[];
16
+ }): void;
17
+ export declare function handleCallRespond(ctx: SignalingContext, data: {
18
+ callId: string;
19
+ action: 'accept' | 'reject';
20
+ }): void;
21
+ export declare function handleCallHangup(ctx: SignalingContext, data: {
22
+ callId: string;
23
+ reason: CallEndReason;
24
+ }): void;
25
+ export declare function handleCallMediaToggle(ctx: SignalingContext, data: {
26
+ callId: string;
27
+ audio?: boolean;
28
+ video?: boolean;
29
+ screenShare?: boolean;
30
+ }): void;
31
+ export declare function handleCallAddParticipant(ctx: SignalingContext, data: {
32
+ callId: string;
33
+ userId: string;
34
+ }): void;
35
+ export declare function handleCallRemoveParticipant(ctx: SignalingContext, data: {
36
+ callId: string;
37
+ userId: string;
38
+ }): void;
39
+ export declare function handleSignalOffer(ctx: SignalingContext, data: SignalOffer): void;
40
+ export declare function handleSignalAnswer(ctx: SignalingContext, data: SignalOffer): void;
41
+ export declare function handleSignalIceCandidate(ctx: SignalingContext, data: SignalIceCandidate): void;
42
+ export declare function handleSignalIceRestart(ctx: SignalingContext, data: SignalIceRestart): void;
43
+ export declare function handleCallQualityReport(ctx: SignalingContext, data: ConnectionQualityReport): void;
@@ -0,0 +1,271 @@
1
+ /** Emit data to all sockets belonging to a user */
2
+ function emitToUser(ctx, targetUserId, data) {
3
+ const sockets = ctx.store.getSocketsForUser(targetUserId);
4
+ for (const sid of sockets) {
5
+ ctx.emitToSocket(sid, data);
6
+ }
7
+ }
8
+ // ── Call Lifecycle ───────────────────────────────────────────────
9
+ export function handleCallInitiate(ctx, data) {
10
+ // Check if caller is already in a call
11
+ const existingCall = ctx.store.getActiveCallForUser(ctx.userId);
12
+ if (existingCall) {
13
+ emitToUser(ctx, ctx.userId, {
14
+ event: 'call:state-changed',
15
+ data: { callId: existingCall.id, state: existingCall.state, error: 'already_in_call' },
16
+ });
17
+ return;
18
+ }
19
+ const callId = crypto.randomUUID();
20
+ const now = new Date().toISOString();
21
+ const call = {
22
+ id: callId,
23
+ conversationId: data.conversationId,
24
+ type: data.type,
25
+ state: 'initiating',
26
+ callerId: ctx.userId,
27
+ calleeIds: data.calleeIds,
28
+ startedAt: now,
29
+ participants: [{
30
+ userId: ctx.userId,
31
+ joinedAt: now,
32
+ audioMuted: false,
33
+ videoMuted: data.type === 'audio',
34
+ screenSharing: false,
35
+ }],
36
+ };
37
+ ctx.store.addCall(call);
38
+ // Notify caller that call is initiating
39
+ emitToUser(ctx, ctx.userId, {
40
+ event: 'call:state-changed',
41
+ data: { callId, state: 'initiating' },
42
+ });
43
+ // Notify each callee with incoming call
44
+ for (const calleeId of data.calleeIds) {
45
+ // Check if callee is busy (exclude the current call being initiated)
46
+ const calleeActiveCall = ctx.store.getActiveCallForUser(calleeId);
47
+ if (calleeActiveCall && calleeActiveCall.id !== callId) {
48
+ emitToUser(ctx, ctx.userId, {
49
+ event: 'call:state-changed',
50
+ data: { callId, state: 'ended', endReason: 'busy' },
51
+ });
52
+ ctx.store.updateCallState(callId, 'ended', 'busy');
53
+ ctx.store.removeCall(callId);
54
+ return;
55
+ }
56
+ emitToUser(ctx, calleeId, { event: 'call:incoming', data: call });
57
+ }
58
+ // Update state to ringing
59
+ ctx.store.updateCallState(callId, 'ringing');
60
+ }
61
+ export function handleCallRespond(ctx, data) {
62
+ const call = ctx.store.getCall(data.callId);
63
+ if (!call)
64
+ return;
65
+ if (data.action === 'reject') {
66
+ ctx.store.updateCallState(data.callId, 'ended', 'rejected');
67
+ // Notify all parties
68
+ emitToUser(ctx, call.callerId, {
69
+ event: 'call:state-changed',
70
+ data: { callId: data.callId, state: 'ended', endReason: 'rejected' },
71
+ });
72
+ emitToUser(ctx, ctx.userId, {
73
+ event: 'call:state-changed',
74
+ data: { callId: data.callId, state: 'ended', endReason: 'rejected' },
75
+ });
76
+ ctx.store.removeCall(data.callId);
77
+ return;
78
+ }
79
+ // Accept — add callee as participant
80
+ const now = new Date().toISOString();
81
+ const participant = {
82
+ userId: ctx.userId,
83
+ joinedAt: now,
84
+ audioMuted: false,
85
+ videoMuted: call.type === 'audio',
86
+ screenSharing: false,
87
+ };
88
+ ctx.store.addCallParticipant(data.callId, participant);
89
+ ctx.store.updateCallState(data.callId, 'connecting');
90
+ // Notify caller that callee accepted
91
+ emitToUser(ctx, call.callerId, {
92
+ event: 'call:participant-joined',
93
+ data: { callId: data.callId, participant },
94
+ });
95
+ emitToUser(ctx, call.callerId, {
96
+ event: 'call:state-changed',
97
+ data: { callId: data.callId, state: 'connecting' },
98
+ });
99
+ // Notify callee of connecting state
100
+ emitToUser(ctx, ctx.userId, {
101
+ event: 'call:state-changed',
102
+ data: { callId: data.callId, state: 'connecting' },
103
+ });
104
+ }
105
+ export function handleCallHangup(ctx, data) {
106
+ const call = ctx.store.getCall(data.callId);
107
+ if (!call)
108
+ return;
109
+ // Remove the user who hung up
110
+ ctx.store.removeCallParticipant(data.callId, ctx.userId);
111
+ // Notify other participants
112
+ for (const p of call.participants) {
113
+ if (p.userId === ctx.userId)
114
+ continue;
115
+ emitToUser(ctx, p.userId, {
116
+ event: 'call:participant-left',
117
+ data: { callId: data.callId, userId: ctx.userId },
118
+ });
119
+ }
120
+ // Also notify caller/callees who may not be in participants yet (e.g., still ringing)
121
+ const allUsers = new Set([call.callerId, ...call.calleeIds]);
122
+ allUsers.delete(ctx.userId);
123
+ // If no participants left (or only one), end the call
124
+ const remainingParticipants = call.participants.filter(p => p.userId !== ctx.userId);
125
+ if (remainingParticipants.length <= 1) {
126
+ ctx.store.updateCallState(data.callId, 'ended', data.reason);
127
+ for (const uid of allUsers) {
128
+ emitToUser(ctx, uid, {
129
+ event: 'call:state-changed',
130
+ data: { callId: data.callId, state: 'ended', endReason: data.reason },
131
+ });
132
+ }
133
+ ctx.store.removeCall(data.callId);
134
+ }
135
+ }
136
+ export function handleCallMediaToggle(ctx, data) {
137
+ const call = ctx.store.getCall(data.callId);
138
+ if (!call)
139
+ return;
140
+ const participant = call.participants.find(p => p.userId === ctx.userId);
141
+ if (!participant)
142
+ return;
143
+ if (data.audio !== undefined)
144
+ participant.audioMuted = !data.audio;
145
+ if (data.video !== undefined)
146
+ participant.videoMuted = !data.video;
147
+ if (data.screenShare !== undefined)
148
+ participant.screenSharing = data.screenShare;
149
+ // Notify all other participants
150
+ for (const p of call.participants) {
151
+ if (p.userId === ctx.userId)
152
+ continue;
153
+ emitToUser(ctx, p.userId, {
154
+ event: 'call:media-changed',
155
+ data: {
156
+ callId: data.callId,
157
+ userId: ctx.userId,
158
+ audio: data.audio,
159
+ video: data.video,
160
+ screenShare: data.screenShare,
161
+ },
162
+ });
163
+ }
164
+ }
165
+ // ── Mid-Call Participant Management ──────────────────────────────
166
+ export function handleCallAddParticipant(ctx, data) {
167
+ const call = ctx.store.getCall(data.callId);
168
+ if (!call)
169
+ return;
170
+ // Only existing participants can add someone
171
+ const isParticipant = call.participants.some(p => p.userId === ctx.userId);
172
+ if (!isParticipant)
173
+ return;
174
+ // Don't add someone already in the call
175
+ if (call.participants.some(p => p.userId === data.userId))
176
+ return;
177
+ // Check if the target user is already in another call
178
+ const targetActiveCall = ctx.store.getActiveCallForUser(data.userId);
179
+ if (targetActiveCall) {
180
+ emitToUser(ctx, ctx.userId, {
181
+ event: 'call:state-changed',
182
+ data: { callId: data.callId, state: call.state, error: 'user_busy' },
183
+ });
184
+ return;
185
+ }
186
+ // Add to calleeIds so they are tracked
187
+ if (!call.calleeIds.includes(data.userId)) {
188
+ call.calleeIds.push(data.userId);
189
+ }
190
+ // Send incoming call notification to the new user
191
+ emitToUser(ctx, data.userId, { event: 'call:incoming', data: call });
192
+ }
193
+ export function handleCallRemoveParticipant(ctx, data) {
194
+ const call = ctx.store.getCall(data.callId);
195
+ if (!call)
196
+ return;
197
+ // Only the caller (owner) or admins can kick participants
198
+ if (ctx.userId !== call.callerId)
199
+ return;
200
+ // Can't remove yourself (use hangup instead)
201
+ if (data.userId === ctx.userId)
202
+ return;
203
+ // Check the target is actually in the call
204
+ const targetParticipant = call.participants.find(p => p.userId === data.userId);
205
+ if (!targetParticipant)
206
+ return;
207
+ ctx.store.removeCallParticipant(data.callId, data.userId);
208
+ // Notify all remaining participants
209
+ for (const p of call.participants) {
210
+ if (p.userId === data.userId)
211
+ continue;
212
+ emitToUser(ctx, p.userId, {
213
+ event: 'call:participant-left',
214
+ data: { callId: data.callId, userId: data.userId },
215
+ });
216
+ }
217
+ // Notify the removed user
218
+ emitToUser(ctx, data.userId, {
219
+ event: 'call:participant-left',
220
+ data: { callId: data.callId, userId: data.userId },
221
+ });
222
+ emitToUser(ctx, data.userId, {
223
+ event: 'call:state-changed',
224
+ data: { callId: data.callId, state: 'ended', endReason: 'completed' },
225
+ });
226
+ }
227
+ // ── WebRTC Signal Relay ─────────────────────────────────────────
228
+ /** Verify the sender is part of an active call with the target user. */
229
+ function verifyCallMembership(ctx, fromUserId, toUserId) {
230
+ // Ensure fromUserId matches the authenticated user
231
+ if (fromUserId !== ctx.userId)
232
+ return false;
233
+ // Check both users are participants in the same active call
234
+ const call = ctx.store.getActiveCallForUser(ctx.userId);
235
+ if (!call)
236
+ return false;
237
+ const isTarget = call.callerId === toUserId || call.calleeIds.includes(toUserId) ||
238
+ call.participants.some(p => p.userId === toUserId);
239
+ return isTarget;
240
+ }
241
+ export function handleSignalOffer(ctx, data) {
242
+ if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
243
+ return;
244
+ emitToUser(ctx, data.toUserId, { event: 'signal:offer', data });
245
+ }
246
+ export function handleSignalAnswer(ctx, data) {
247
+ if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
248
+ return;
249
+ emitToUser(ctx, data.toUserId, { event: 'signal:answer', data });
250
+ }
251
+ export function handleSignalIceCandidate(ctx, data) {
252
+ if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
253
+ return;
254
+ emitToUser(ctx, data.toUserId, { event: 'signal:ice-candidate', data });
255
+ }
256
+ export function handleSignalIceRestart(ctx, data) {
257
+ if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
258
+ return;
259
+ emitToUser(ctx, data.toUserId, { event: 'signal:ice-restart', data });
260
+ }
261
+ export function handleCallQualityReport(ctx, data) {
262
+ const call = ctx.store.getCall(data.callId);
263
+ if (!call)
264
+ return;
265
+ // Broadcast quality report to all other participants in the call
266
+ for (const p of call.participants) {
267
+ if (p.userId === ctx.userId)
268
+ continue;
269
+ emitToUser(ctx, p.userId, { event: 'call:quality-changed', data });
270
+ }
271
+ }