@nuraly/lumenjs 0.1.2 → 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 (337) hide show
  1. package/README.md +76 -235
  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 +52 -120
  49. package/dist/build/scan.d.ts +19 -0
  50. package/dist/build/scan.js +77 -6
  51. package/dist/build/serve-api.js +8 -2
  52. package/dist/build/serve-loaders.d.ts +4 -2
  53. package/dist/build/serve-loaders.js +128 -10
  54. package/dist/build/serve-ssr.js +38 -11
  55. package/dist/build/serve-static.js +3 -3
  56. package/dist/build/serve.js +229 -14
  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/context.d.ts +2 -0
  93. package/dist/db/context.js +9 -0
  94. package/dist/db/index.d.ts +23 -0
  95. package/dist/db/index.js +258 -0
  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 +14 -0
  101. package/dist/dev-server/config.js +26 -9
  102. package/dist/dev-server/index-html.d.ts +3 -0
  103. package/dist/dev-server/index-html.js +18 -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.d.ts +0 -1
  122. package/dist/dev-server/plugins/vite-plugin-loaders.js +311 -42
  123. package/dist/dev-server/plugins/vite-plugin-routes.js +18 -6
  124. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  125. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  126. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  127. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  128. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  129. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  130. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
  131. package/dist/dev-server/server.js +128 -12
  132. package/dist/dev-server/ssr-render.d.ts +2 -1
  133. package/dist/dev-server/ssr-render.js +107 -48
  134. package/dist/editor/ai/backend.d.ts +20 -0
  135. package/dist/editor/ai/backend.js +104 -0
  136. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  137. package/dist/editor/ai/claude-code-client.js +145 -0
  138. package/dist/editor/ai/opencode-client.d.ts +14 -0
  139. package/dist/editor/ai/opencode-client.js +125 -0
  140. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  141. package/dist/editor/ai/snapshot-store.js +35 -0
  142. package/dist/editor/ai/types.d.ts +30 -0
  143. package/dist/editor/ai/types.js +136 -0
  144. package/dist/editor/ai-chat-panel.d.ts +13 -0
  145. package/dist/editor/ai-chat-panel.js +587 -0
  146. package/dist/editor/ai-markdown.d.ts +10 -0
  147. package/dist/editor/ai-markdown.js +70 -0
  148. package/dist/editor/ai-project-panel.d.ts +11 -0
  149. package/dist/editor/ai-project-panel.js +332 -0
  150. package/dist/editor/ast-modification.d.ts +11 -0
  151. package/dist/editor/ast-modification.js +1 -0
  152. package/dist/editor/ast-service.d.ts +30 -0
  153. package/dist/editor/ast-service.js +180 -0
  154. package/dist/editor/css-rules.d.ts +54 -0
  155. package/dist/editor/css-rules.js +423 -0
  156. package/dist/editor/editor-api-client.d.ts +51 -0
  157. package/dist/editor/editor-api-client.js +162 -0
  158. package/dist/editor/editor-bridge.d.ts +1 -0
  159. package/dist/editor/editor-bridge.js +17 -8
  160. package/dist/editor/editor-toolbar.d.ts +14 -0
  161. package/dist/editor/editor-toolbar.js +115 -0
  162. package/dist/editor/file-editor.d.ts +9 -0
  163. package/dist/editor/file-editor.js +236 -0
  164. package/dist/editor/file-service.d.ts +16 -0
  165. package/dist/editor/file-service.js +52 -0
  166. package/dist/editor/i18n-key-gen.d.ts +1 -0
  167. package/dist/editor/i18n-key-gen.js +7 -0
  168. package/dist/editor/inline-text-edit.d.ts +5 -0
  169. package/dist/editor/inline-text-edit.js +173 -92
  170. package/dist/editor/overlay-events.d.ts +5 -0
  171. package/dist/editor/overlay-events.js +364 -0
  172. package/dist/editor/overlay-hmr.d.ts +2 -0
  173. package/dist/editor/overlay-hmr.js +75 -0
  174. package/dist/editor/overlay-selection.d.ts +29 -0
  175. package/dist/editor/overlay-selection.js +148 -0
  176. package/dist/editor/overlay-utils.d.ts +12 -0
  177. package/dist/editor/overlay-utils.js +59 -0
  178. package/dist/editor/properties-panel-persist.d.ts +14 -0
  179. package/dist/editor/properties-panel-persist.js +70 -0
  180. package/dist/editor/properties-panel-rows.d.ts +10 -0
  181. package/dist/editor/properties-panel-rows.js +349 -0
  182. package/dist/editor/properties-panel-styles.d.ts +4 -0
  183. package/dist/editor/properties-panel-styles.js +174 -0
  184. package/dist/editor/properties-panel.d.ts +4 -0
  185. package/dist/editor/properties-panel.js +148 -0
  186. package/dist/editor/property-registry.d.ts +16 -0
  187. package/dist/editor/property-registry.js +303 -0
  188. package/dist/editor/standalone-file-panel.d.ts +0 -0
  189. package/dist/editor/standalone-file-panel.js +1 -0
  190. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  191. package/dist/editor/standalone-overlay-dom.js +1 -0
  192. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  193. package/dist/editor/standalone-overlay-styles.js +1 -0
  194. package/dist/editor/standalone-overlay.d.ts +1 -0
  195. package/dist/editor/standalone-overlay.js +76 -0
  196. package/dist/editor/syntax-highlighter.d.ts +4 -0
  197. package/dist/editor/syntax-highlighter.js +81 -0
  198. package/dist/editor/text-toolbar.d.ts +11 -0
  199. package/dist/editor/text-toolbar.js +327 -0
  200. package/dist/editor/toolbar-styles.d.ts +4 -0
  201. package/dist/editor/toolbar-styles.js +198 -0
  202. package/dist/email/index.d.ts +32 -0
  203. package/dist/email/index.js +154 -0
  204. package/dist/email/providers/resend.d.ts +2 -0
  205. package/dist/email/providers/resend.js +24 -0
  206. package/dist/email/providers/sendgrid.d.ts +2 -0
  207. package/dist/email/providers/sendgrid.js +31 -0
  208. package/dist/email/providers/smtp.d.ts +13 -0
  209. package/dist/email/providers/smtp.js +125 -0
  210. package/dist/email/template-engine.d.ts +18 -0
  211. package/dist/email/template-engine.js +116 -0
  212. package/dist/email/templates/base.d.ts +9 -0
  213. package/dist/email/templates/base.js +65 -0
  214. package/dist/email/templates/password-reset.d.ts +5 -0
  215. package/dist/email/templates/password-reset.js +15 -0
  216. package/dist/email/templates/verify-email.d.ts +5 -0
  217. package/dist/email/templates/verify-email.js +15 -0
  218. package/dist/email/templates/welcome.d.ts +5 -0
  219. package/dist/email/templates/welcome.js +13 -0
  220. package/dist/email/types.d.ts +49 -0
  221. package/dist/email/types.js +1 -0
  222. package/dist/llms/generate.d.ts +46 -0
  223. package/dist/llms/generate.js +185 -0
  224. package/dist/permissions/guard.d.ts +28 -0
  225. package/dist/permissions/guard.js +30 -0
  226. package/dist/permissions/index.d.ts +6 -0
  227. package/dist/permissions/index.js +3 -0
  228. package/dist/permissions/service.d.ts +80 -0
  229. package/dist/permissions/service.js +210 -0
  230. package/dist/permissions/tables.d.ts +5 -0
  231. package/dist/permissions/tables.js +68 -0
  232. package/dist/permissions/types.d.ts +33 -0
  233. package/dist/permissions/types.js +1 -0
  234. package/dist/runtime/app-shell.js +163 -0
  235. package/dist/runtime/auth.d.ts +10 -0
  236. package/dist/runtime/auth.js +30 -0
  237. package/dist/runtime/communication.d.ts +137 -0
  238. package/dist/runtime/communication.js +228 -0
  239. package/dist/runtime/error-boundary.d.ts +23 -0
  240. package/dist/runtime/error-boundary.js +120 -0
  241. package/dist/runtime/i18n.d.ts +6 -1
  242. package/dist/runtime/i18n.js +42 -21
  243. package/dist/runtime/router-data.d.ts +5 -0
  244. package/dist/runtime/router-data.js +121 -16
  245. package/dist/runtime/router-hydration.js +25 -0
  246. package/dist/runtime/router.d.ts +21 -1
  247. package/dist/runtime/router.js +221 -39
  248. package/dist/runtime/socket-client.d.ts +2 -0
  249. package/dist/runtime/socket-client.js +30 -0
  250. package/dist/runtime/webrtc.d.ts +47 -0
  251. package/dist/runtime/webrtc.js +178 -0
  252. package/dist/shared/graceful-shutdown.d.ts +8 -0
  253. package/dist/shared/graceful-shutdown.js +36 -0
  254. package/dist/shared/health.d.ts +8 -0
  255. package/dist/shared/health.js +25 -0
  256. package/dist/shared/llms-txt.d.ts +31 -0
  257. package/dist/shared/llms-txt.js +85 -0
  258. package/dist/shared/logger.d.ts +32 -0
  259. package/dist/shared/logger.js +93 -0
  260. package/dist/shared/meta.d.ts +27 -0
  261. package/dist/shared/meta.js +71 -0
  262. package/dist/shared/middleware-runner.d.ts +9 -0
  263. package/dist/shared/middleware-runner.js +29 -0
  264. package/dist/shared/rate-limit.d.ts +18 -0
  265. package/dist/shared/rate-limit.js +71 -0
  266. package/dist/shared/request-id.d.ts +5 -0
  267. package/dist/shared/request-id.js +18 -0
  268. package/dist/shared/route-matching.js +16 -1
  269. package/dist/shared/security-headers.d.ts +18 -0
  270. package/dist/shared/security-headers.js +38 -0
  271. package/dist/shared/socket-io-setup.d.ts +11 -0
  272. package/dist/shared/socket-io-setup.js +51 -0
  273. package/dist/shared/types.d.ts +16 -0
  274. package/dist/shared/utils.d.ts +37 -7
  275. package/dist/shared/utils.js +175 -26
  276. package/dist/storage/adapters/local.d.ts +44 -0
  277. package/dist/storage/adapters/local.js +85 -0
  278. package/dist/storage/adapters/s3.d.ts +32 -0
  279. package/dist/storage/adapters/s3.js +116 -0
  280. package/dist/storage/adapters/types.d.ts +53 -0
  281. package/dist/storage/adapters/types.js +1 -0
  282. package/dist/storage/index.d.ts +76 -0
  283. package/dist/storage/index.js +83 -0
  284. package/package.json +20 -1
  285. package/templates/blog/api/posts.ts +6 -0
  286. package/templates/blog/data/migrations/001_init.sql +13 -0
  287. package/templates/blog/lumenjs.config.ts +3 -0
  288. package/templates/blog/package.json +14 -0
  289. package/templates/blog/pages/_layout.ts +25 -0
  290. package/templates/blog/pages/index.ts +65 -0
  291. package/templates/blog/pages/posts/[slug].ts +60 -0
  292. package/templates/blog/pages/tag/[tag].ts +44 -0
  293. package/templates/dashboard/api/stats.ts +10 -0
  294. package/templates/dashboard/data/migrations/001_init.sql +13 -0
  295. package/templates/dashboard/lumenjs.config.ts +3 -0
  296. package/templates/dashboard/package.json +14 -0
  297. package/templates/dashboard/pages/_layout.ts +25 -0
  298. package/templates/dashboard/pages/index.ts +72 -0
  299. package/templates/dashboard/pages/settings/index.ts +29 -0
  300. package/templates/default/lumenjs.config.ts +3 -0
  301. package/templates/default/package.json +14 -0
  302. package/templates/default/pages/index.ts +24 -0
  303. package/templates/social/api/posts/[id].ts +14 -0
  304. package/templates/social/api/posts.ts +11 -0
  305. package/templates/social/api/profile/[username].ts +10 -0
  306. package/templates/social/api/upload.ts +19 -0
  307. package/templates/social/data/migrations/001_init.sql +78 -0
  308. package/templates/social/data/migrations/002_add_image_url.sql +1 -0
  309. package/templates/social/data/migrations/003_auth.sql +7 -0
  310. package/templates/social/docs/architecture.md +76 -0
  311. package/templates/social/docs/components.md +100 -0
  312. package/templates/social/docs/data.md +89 -0
  313. package/templates/social/docs/pages.md +96 -0
  314. package/templates/social/docs/theming.md +52 -0
  315. package/templates/social/lib/media.ts +130 -0
  316. package/templates/social/lumenjs.auth.ts +21 -0
  317. package/templates/social/lumenjs.config.ts +3 -0
  318. package/templates/social/package.json +5 -0
  319. package/templates/social/pages/_layout.ts +239 -0
  320. package/templates/social/pages/apps/[id].ts +173 -0
  321. package/templates/social/pages/apps/index.ts +116 -0
  322. package/templates/social/pages/auth/login.ts +92 -0
  323. package/templates/social/pages/bookmarks.ts +57 -0
  324. package/templates/social/pages/explore.ts +73 -0
  325. package/templates/social/pages/index.ts +351 -0
  326. package/templates/social/pages/messages.ts +298 -0
  327. package/templates/social/pages/new.ts +77 -0
  328. package/templates/social/pages/notifications.ts +73 -0
  329. package/templates/social/pages/post/[id].ts +124 -0
  330. package/templates/social/pages/profile/[username].ts +100 -0
  331. package/templates/social/pages/settings/accessibility.ts +153 -0
  332. package/templates/social/pages/settings/account.ts +260 -0
  333. package/templates/social/pages/settings/help.ts +141 -0
  334. package/templates/social/pages/settings/language.ts +103 -0
  335. package/templates/social/pages/settings/privacy.ts +183 -0
  336. package/templates/social/pages/settings/security.ts +133 -0
  337. package/templates/social/pages/settings.ts +185 -0
@@ -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
+ }
@@ -0,0 +1,71 @@
1
+ import type { PresenceStatus, Call, CallState, CallEndReason, CallParticipant, KeyBundle, PreKey, RateLimitConfig } from './types.js';
2
+ export interface PresenceEntry {
3
+ userId: string;
4
+ status: PresenceStatus;
5
+ lastSeen: string;
6
+ socketIds: Set<string>;
7
+ }
8
+ export declare class CommunicationStore {
9
+ /** userId → presence info */
10
+ private presence;
11
+ /** conversationId → map of userId → typing timer */
12
+ private typing;
13
+ /** callId → active call */
14
+ private calls;
15
+ /** userId → set of socket IDs (multi-device support) */
16
+ private userSockets;
17
+ /** socketId → userId (reverse lookup) */
18
+ private socketUser;
19
+ /** userId → set of conversation IDs the user has joined */
20
+ private userConversations;
21
+ /** userId → public key bundle for E2E encryption */
22
+ private keyBundles;
23
+ /** userId → rate limit tracking */
24
+ private rateLimits;
25
+ /** conversationId → set of user IDs currently in the room */
26
+ private conversationMembers;
27
+ /** Configurable typing timeout in ms */
28
+ typingTimeoutMs: number;
29
+ /** Max call age before auto-cleanup (1 hour) */
30
+ private static CALL_TTL_MS;
31
+ constructor();
32
+ mapUserSocket(userId: string, socketId: string): void;
33
+ unmapUserSocket(socketId: string): string | undefined;
34
+ getSocketsForUser(userId: string): Set<string>;
35
+ getUserForSocket(socketId: string): string | undefined;
36
+ isUserOnline(userId: string): boolean;
37
+ joinConversation(userId: string, conversationId: string): void;
38
+ leaveConversation(userId: string, conversationId: string): void;
39
+ getUserConversations(userId: string): Set<string>;
40
+ setPresence(userId: string, status: PresenceStatus): PresenceEntry;
41
+ getPresence(userId: string): PresenceEntry | undefined;
42
+ removePresence(userId: string): void;
43
+ setTyping(conversationId: string, userId: string, onExpire: () => void): void;
44
+ clearTyping(conversationId: string, userId: string): void;
45
+ clearAllTypingForUser(userId: string): string[];
46
+ getTypingUsers(conversationId: string): string[];
47
+ addCall(call: Call): void;
48
+ getCall(callId: string): Call | undefined;
49
+ updateCallState(callId: string, state: CallState, endReason?: CallEndReason): Call | undefined;
50
+ addCallParticipant(callId: string, participant: CallParticipant): Call | undefined;
51
+ removeCallParticipant(callId: string, userId: string): Call | undefined;
52
+ removeCall(callId: string): void;
53
+ getActiveCallForUser(userId: string): Call | undefined;
54
+ addConversationMember(conversationId: string, userId: string): void;
55
+ removeConversationMember(conversationId: string, userId: string): void;
56
+ getConversationMembers(conversationId: string): Set<string>;
57
+ removeUserFromAllConversations(userId: string): string[];
58
+ setKeyBundle(userId: string, bundle: KeyBundle): void;
59
+ getKeyBundle(userId: string): KeyBundle | undefined;
60
+ /** Pop one one-time pre-key (consumed once per session setup). Returns undefined if depleted. */
61
+ popOneTimePreKey(userId: string): PreKey | undefined;
62
+ /** Check how many one-time pre-keys remain */
63
+ getOneTimePreKeyCount(userId: string): number;
64
+ removeKeyBundle(userId: string): void;
65
+ /**
66
+ * Check whether a user is rate-limited. If not, records the message timestamp.
67
+ * Returns true if the message is allowed, false if rate-limited.
68
+ */
69
+ checkRateLimit(userId: string, config: RateLimitConfig): boolean;
70
+ }
71
+ export declare function useCommunicationStore(): CommunicationStore;
@@ -0,0 +1,289 @@
1
+ // ── CommunicationStore ────────────────────────────────────────────
2
+ const DEFAULT_TYPING_TIMEOUT_MS = 5000;
3
+ export class CommunicationStore {
4
+ /** Max call age before auto-cleanup (1 hour) */
5
+ static { this.CALL_TTL_MS = 60 * 60 * 1000; }
6
+ constructor() {
7
+ /** userId → presence info */
8
+ this.presence = new Map();
9
+ /** conversationId → map of userId → typing timer */
10
+ this.typing = new Map();
11
+ /** callId → active call */
12
+ this.calls = new Map();
13
+ /** userId → set of socket IDs (multi-device support) */
14
+ this.userSockets = new Map();
15
+ /** socketId → userId (reverse lookup) */
16
+ this.socketUser = new Map();
17
+ /** userId → set of conversation IDs the user has joined */
18
+ this.userConversations = new Map();
19
+ /** userId → public key bundle for E2E encryption */
20
+ this.keyBundles = new Map();
21
+ /** userId → rate limit tracking */
22
+ this.rateLimits = new Map();
23
+ /** conversationId → set of user IDs currently in the room */
24
+ this.conversationMembers = new Map();
25
+ /** Configurable typing timeout in ms */
26
+ this.typingTimeoutMs = DEFAULT_TYPING_TIMEOUT_MS;
27
+ // Periodically clean up stale calls that were never properly ended
28
+ setInterval(() => {
29
+ const now = Date.now();
30
+ for (const [id, call] of this.calls) {
31
+ const startedAt = call.startedAt ? new Date(call.startedAt).getTime() : 0;
32
+ if (now - startedAt > CommunicationStore.CALL_TTL_MS) {
33
+ this.calls.delete(id);
34
+ }
35
+ }
36
+ }, 5 * 60 * 1000).unref(); // every 5 min, unref so it doesn't keep process alive
37
+ }
38
+ // ── User-Socket Mapping ───────────────────────────────────────
39
+ mapUserSocket(userId, socketId) {
40
+ let sockets = this.userSockets.get(userId);
41
+ if (!sockets) {
42
+ sockets = new Set();
43
+ this.userSockets.set(userId, sockets);
44
+ }
45
+ sockets.add(socketId);
46
+ this.socketUser.set(socketId, userId);
47
+ }
48
+ unmapUserSocket(socketId) {
49
+ const userId = this.socketUser.get(socketId);
50
+ if (!userId)
51
+ return undefined;
52
+ this.socketUser.delete(socketId);
53
+ const sockets = this.userSockets.get(userId);
54
+ if (sockets) {
55
+ sockets.delete(socketId);
56
+ if (sockets.size === 0)
57
+ this.userSockets.delete(userId);
58
+ }
59
+ return userId;
60
+ }
61
+ getSocketsForUser(userId) {
62
+ return this.userSockets.get(userId) || new Set();
63
+ }
64
+ getUserForSocket(socketId) {
65
+ return this.socketUser.get(socketId);
66
+ }
67
+ isUserOnline(userId) {
68
+ const sockets = this.userSockets.get(userId);
69
+ return !!sockets && sockets.size > 0;
70
+ }
71
+ // ── Conversation Membership ────────────────────────────────────
72
+ joinConversation(userId, conversationId) {
73
+ let convs = this.userConversations.get(userId);
74
+ if (!convs) {
75
+ convs = new Set();
76
+ this.userConversations.set(userId, convs);
77
+ }
78
+ convs.add(conversationId);
79
+ }
80
+ leaveConversation(userId, conversationId) {
81
+ const convs = this.userConversations.get(userId);
82
+ if (!convs)
83
+ return;
84
+ convs.delete(conversationId);
85
+ if (convs.size === 0)
86
+ this.userConversations.delete(userId);
87
+ }
88
+ getUserConversations(userId) {
89
+ return this.userConversations.get(userId) || new Set();
90
+ }
91
+ // ── Presence ──────────────────────────────────────────────────
92
+ setPresence(userId, status) {
93
+ const existing = this.presence.get(userId);
94
+ const entry = {
95
+ userId,
96
+ status,
97
+ lastSeen: new Date().toISOString(),
98
+ socketIds: existing?.socketIds || this.getSocketsForUser(userId),
99
+ };
100
+ this.presence.set(userId, entry);
101
+ return entry;
102
+ }
103
+ getPresence(userId) {
104
+ return this.presence.get(userId);
105
+ }
106
+ removePresence(userId) {
107
+ this.presence.delete(userId);
108
+ }
109
+ // ── Typing ────────────────────────────────────────────────────
110
+ setTyping(conversationId, userId, onExpire) {
111
+ let convTyping = this.typing.get(conversationId);
112
+ if (!convTyping) {
113
+ convTyping = new Map();
114
+ this.typing.set(conversationId, convTyping);
115
+ }
116
+ const existing = convTyping.get(userId);
117
+ if (existing)
118
+ clearTimeout(existing.timer);
119
+ const timer = setTimeout(() => {
120
+ this.clearTyping(conversationId, userId);
121
+ onExpire();
122
+ }, this.typingTimeoutMs);
123
+ convTyping.set(userId, { userId, timer });
124
+ }
125
+ clearTyping(conversationId, userId) {
126
+ const convTyping = this.typing.get(conversationId);
127
+ if (!convTyping)
128
+ return;
129
+ const entry = convTyping.get(userId);
130
+ if (entry) {
131
+ clearTimeout(entry.timer);
132
+ convTyping.delete(userId);
133
+ }
134
+ if (convTyping.size === 0)
135
+ this.typing.delete(conversationId);
136
+ }
137
+ clearAllTypingForUser(userId) {
138
+ const cleared = [];
139
+ for (const [convId, convTyping] of this.typing) {
140
+ if (convTyping.has(userId)) {
141
+ const entry = convTyping.get(userId);
142
+ clearTimeout(entry.timer);
143
+ convTyping.delete(userId);
144
+ cleared.push(convId);
145
+ }
146
+ if (convTyping.size === 0)
147
+ this.typing.delete(convId);
148
+ }
149
+ return cleared;
150
+ }
151
+ getTypingUsers(conversationId) {
152
+ const convTyping = this.typing.get(conversationId);
153
+ if (!convTyping)
154
+ return [];
155
+ return Array.from(convTyping.keys());
156
+ }
157
+ // ── Calls ─────────────────────────────────────────────────────
158
+ addCall(call) {
159
+ this.calls.set(call.id, call);
160
+ }
161
+ getCall(callId) {
162
+ return this.calls.get(callId);
163
+ }
164
+ updateCallState(callId, state, endReason) {
165
+ const call = this.calls.get(callId);
166
+ if (!call)
167
+ return undefined;
168
+ call.state = state;
169
+ if (endReason)
170
+ call.endReason = endReason;
171
+ if (state === 'connected' && !call.answeredAt)
172
+ call.answeredAt = new Date().toISOString();
173
+ if (state === 'ended')
174
+ call.endedAt = new Date().toISOString();
175
+ return call;
176
+ }
177
+ addCallParticipant(callId, participant) {
178
+ const call = this.calls.get(callId);
179
+ if (!call)
180
+ return undefined;
181
+ const existing = call.participants.findIndex(p => p.userId === participant.userId);
182
+ if (existing >= 0)
183
+ call.participants[existing] = participant;
184
+ else
185
+ call.participants.push(participant);
186
+ return call;
187
+ }
188
+ removeCallParticipant(callId, userId) {
189
+ const call = this.calls.get(callId);
190
+ if (!call)
191
+ return undefined;
192
+ call.participants = call.participants.filter(p => p.userId !== userId);
193
+ return call;
194
+ }
195
+ removeCall(callId) {
196
+ this.calls.delete(callId);
197
+ }
198
+ getActiveCallForUser(userId) {
199
+ for (const call of this.calls.values()) {
200
+ if (call.state === 'ended')
201
+ continue;
202
+ if (call.callerId === userId || call.calleeIds.includes(userId))
203
+ return call;
204
+ }
205
+ return undefined;
206
+ }
207
+ // ── Conversation Membership ─────────────────────────────────────
208
+ addConversationMember(conversationId, userId) {
209
+ let members = this.conversationMembers.get(conversationId);
210
+ if (!members) {
211
+ members = new Set();
212
+ this.conversationMembers.set(conversationId, members);
213
+ }
214
+ members.add(userId);
215
+ }
216
+ removeConversationMember(conversationId, userId) {
217
+ const members = this.conversationMembers.get(conversationId);
218
+ if (!members)
219
+ return;
220
+ members.delete(userId);
221
+ if (members.size === 0)
222
+ this.conversationMembers.delete(conversationId);
223
+ }
224
+ getConversationMembers(conversationId) {
225
+ return this.conversationMembers.get(conversationId) || new Set();
226
+ }
227
+ removeUserFromAllConversations(userId) {
228
+ const removed = [];
229
+ for (const [convId, members] of this.conversationMembers) {
230
+ if (members.has(userId)) {
231
+ members.delete(userId);
232
+ removed.push(convId);
233
+ }
234
+ if (members.size === 0)
235
+ this.conversationMembers.delete(convId);
236
+ }
237
+ return removed;
238
+ }
239
+ // ── E2E Encryption Key Bundles ──────────────────────────────────
240
+ setKeyBundle(userId, bundle) {
241
+ this.keyBundles.set(userId, bundle);
242
+ }
243
+ getKeyBundle(userId) {
244
+ return this.keyBundles.get(userId);
245
+ }
246
+ /** Pop one one-time pre-key (consumed once per session setup). Returns undefined if depleted. */
247
+ popOneTimePreKey(userId) {
248
+ const bundle = this.keyBundles.get(userId);
249
+ if (!bundle || bundle.oneTimePreKeys.length === 0)
250
+ return undefined;
251
+ return bundle.oneTimePreKeys.shift();
252
+ }
253
+ /** Check how many one-time pre-keys remain */
254
+ getOneTimePreKeyCount(userId) {
255
+ const bundle = this.keyBundles.get(userId);
256
+ return bundle?.oneTimePreKeys.length || 0;
257
+ }
258
+ removeKeyBundle(userId) {
259
+ this.keyBundles.delete(userId);
260
+ }
261
+ // ── Rate Limiting ──────────────────────────────────────────────
262
+ /**
263
+ * Check whether a user is rate-limited. If not, records the message timestamp.
264
+ * Returns true if the message is allowed, false if rate-limited.
265
+ */
266
+ checkRateLimit(userId, config) {
267
+ const now = Date.now();
268
+ const windowStart = now - config.windowSeconds * 1000;
269
+ let entry = this.rateLimits.get(userId);
270
+ if (!entry) {
271
+ entry = { timestamps: [] };
272
+ this.rateLimits.set(userId, entry);
273
+ }
274
+ // Prune timestamps outside the current window
275
+ entry.timestamps = entry.timestamps.filter(t => t > windowStart);
276
+ if (entry.timestamps.length >= config.maxMessages) {
277
+ return false;
278
+ }
279
+ entry.timestamps.push(now);
280
+ return true;
281
+ }
282
+ }
283
+ // ── Singleton ─────────────────────────────────────────────────────
284
+ let _instance = null;
285
+ export function useCommunicationStore() {
286
+ if (!_instance)
287
+ _instance = new CommunicationStore();
288
+ return _instance;
289
+ }