@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.
- package/README.md +62 -282
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +82 -0
- package/dist/auth/native-auth.js +340 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +121 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +124 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +18 -0
- package/dist/build/build-server.js +107 -0
- package/dist/build/build.js +60 -123
- package/dist/build/scan.d.ts +18 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -4
- package/dist/build/serve-loaders.js +26 -18
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +341 -18
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/index.d.ts +17 -13
- package/dist/db/index.js +205 -26
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +11 -0
- package/dist/dev-server/config.js +40 -20
- package/dist/dev-server/index-html.d.ts +4 -0
- package/dist/dev-server/index-html.js +21 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
- package/dist/dev-server/server.js +242 -70
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +117 -50
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +113 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +99 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +613 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +18 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +76 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +164 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +34 -2
- package/dist/runtime/router.d.ts +19 -2
- package/dist/runtime/router.js +237 -43
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +91 -0
- package/dist/runtime/webrtc.js +428 -0
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +15 -0
- package/dist/shared/utils.d.ts +33 -7
- package/dist/shared/utils.js +164 -27
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +119 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +45 -7
- package/templates/blog/api/posts.ts +4 -18
- package/templates/blog/data/migrations/001_init.sql +6 -5
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +48 -22
- package/templates/blog/pages/posts/[slug].ts +45 -20
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +8 -5
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +54 -23
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ── Typing ──────────────────────────────────────────────────────
|
|
2
|
+
export function handleTypingStart(ctx, data) {
|
|
3
|
+
ctx.store.setTyping(data.conversationId, ctx.userId, () => {
|
|
4
|
+
// Auto-expire: broadcast typing stopped
|
|
5
|
+
ctx.broadcastAll(`conv:${data.conversationId}`, {
|
|
6
|
+
event: 'typing:update',
|
|
7
|
+
data: { conversationId: data.conversationId, userId: ctx.userId, isTyping: false },
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
ctx.broadcast(`conv:${data.conversationId}`, {
|
|
11
|
+
event: 'typing:update',
|
|
12
|
+
data: { conversationId: data.conversationId, userId: ctx.userId, isTyping: true },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function handleTypingStop(ctx, data) {
|
|
16
|
+
ctx.store.clearTyping(data.conversationId, ctx.userId);
|
|
17
|
+
ctx.broadcast(`conv:${data.conversationId}`, {
|
|
18
|
+
event: 'typing:update',
|
|
19
|
+
data: { conversationId: data.conversationId, userId: ctx.userId, isTyping: false },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// ── Presence ────────────────────────────────────────────────────
|
|
23
|
+
/** Broadcast online status to all conversation rooms when a user connects */
|
|
24
|
+
export function handleConnect(ctx) {
|
|
25
|
+
const conversations = ctx.store.getUserConversations(ctx.userId);
|
|
26
|
+
if (conversations.size === 0)
|
|
27
|
+
return;
|
|
28
|
+
const entry = ctx.store.getPresence(ctx.userId);
|
|
29
|
+
if (!entry)
|
|
30
|
+
return;
|
|
31
|
+
const payload = {
|
|
32
|
+
event: 'presence:changed',
|
|
33
|
+
data: { userId: ctx.userId, status: entry.status, lastSeen: entry.lastSeen },
|
|
34
|
+
};
|
|
35
|
+
for (const convId of conversations) {
|
|
36
|
+
ctx.broadcastAll(`conv:${convId}`, payload);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function handlePresenceUpdate(ctx, data) {
|
|
40
|
+
const entry = ctx.store.setPresence(ctx.userId, data.status);
|
|
41
|
+
const payload = {
|
|
42
|
+
event: 'presence:changed',
|
|
43
|
+
data: { userId: ctx.userId, status: entry.status, lastSeen: entry.lastSeen },
|
|
44
|
+
};
|
|
45
|
+
// Broadcast to all conversation rooms the user has joined
|
|
46
|
+
for (const convId of ctx.store.getUserConversations(ctx.userId)) {
|
|
47
|
+
ctx.broadcastAll(`conv:${convId}`, payload);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Called on disconnect to clean up user state */
|
|
51
|
+
export function handleDisconnect(ctx, socketId) {
|
|
52
|
+
const userId = ctx.store.unmapUserSocket(socketId);
|
|
53
|
+
if (!userId)
|
|
54
|
+
return;
|
|
55
|
+
// Clear all typing indicators for this user
|
|
56
|
+
const clearedConversations = ctx.store.clearAllTypingForUser(userId);
|
|
57
|
+
for (const convId of clearedConversations) {
|
|
58
|
+
ctx.broadcastAll(`conv:${convId}`, {
|
|
59
|
+
event: 'typing:update',
|
|
60
|
+
data: { conversationId: convId, userId, isTyping: false },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// If user has no more connected sockets, set presence to offline and broadcast
|
|
64
|
+
if (!ctx.store.isUserOnline(userId)) {
|
|
65
|
+
const entry = ctx.store.setPresence(userId, 'offline');
|
|
66
|
+
const payload = {
|
|
67
|
+
event: 'presence:changed',
|
|
68
|
+
data: { userId, status: 'offline', lastSeen: entry.lastSeen },
|
|
69
|
+
};
|
|
70
|
+
// Broadcast offline to all conversation rooms before cleaning up membership
|
|
71
|
+
for (const convId of ctx.store.getUserConversations(userId)) {
|
|
72
|
+
ctx.broadcastAll(`conv:${convId}`, payload);
|
|
73
|
+
}
|
|
74
|
+
ctx.store.removeUserFromAllConversations(userId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { HandlerContext } from './handlers/context.js';
|
|
2
|
+
export { handleConversationCreate, handleConversationJoin, handleConversationLeave, handleConversationArchive, handleConversationMute, handleConversationPin, } from './handlers/conversation.js';
|
|
3
|
+
export { handleMessageSend, handleMessageRead, handleMessageReact, handleMessageEdit, handleMessageDelete, handleMessageForward, } from './handlers/messaging.js';
|
|
4
|
+
export { handleTypingStart, handleTypingStop, handleConnect, handlePresenceUpdate, handleDisconnect, } from './handlers/presence.js';
|
|
5
|
+
export { handleFileUploadRequest } from './handlers/file-upload.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Barrel re-export — keeps the public API identical for all existing consumers.
|
|
2
|
+
export { handleConversationCreate, handleConversationJoin, handleConversationLeave, handleConversationArchive, handleConversationMute, handleConversationPin, } from './handlers/conversation.js';
|
|
3
|
+
export { handleMessageSend, handleMessageRead, handleMessageReact, handleMessageEdit, handleMessageDelete, handleMessageForward, } from './handlers/messaging.js';
|
|
4
|
+
export { handleTypingStart, handleTypingStop, handleConnect, handlePresenceUpdate, handleDisconnect, } from './handlers/presence.js';
|
|
5
|
+
export { handleFileUploadRequest } from './handlers/file-upload.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { CommunicationConfig, RTCIceServerConfig, MediaConstraintDefaults, MediaTrackConstraintSet, Conversation, Participant, PresenceStatus, PresenceUpdate, Message, MessageStatus, MessageAttachment, ReadReceipt, TypingIndicator, MessageForward, CallType, CallState, CallEndReason, Call, CallParticipant, SignalOffer, SignalIceCandidate, SignalIceRestart, ConnectionQualityReport, CallInitiate, CallResponse, CallHangup, CallMediaToggle, CommunicationClientEvents, CommunicationServerEvents, RateLimitConfig, FileUploadConfig, ReconnectionConfig, EncryptionConfig, PreKey, KeyBundle, EncryptedEnvelope, KeyExchangeRequest, KeyExchangeResponse, NkCommunication, } from './types.js';
|
|
2
|
+
export { CommunicationStore, useCommunicationStore } from './store.js';
|
|
3
|
+
export type { PresenceEntry } from './store.js';
|
|
4
|
+
export type { HandlerContext } from './handlers.js';
|
|
5
|
+
export type { SignalingContext } from './signaling.js';
|
|
6
|
+
export type { EncryptionContext } from './encryption.js';
|
|
7
|
+
export { createCommunicationHandler, createCommunicationApiHandlers } from './server.js';
|
|
8
|
+
export type { CommunicationHandlerOptions } from './server.js';
|
|
9
|
+
export { ensureCommunicationTables } from './schema.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ── Types ─────────────────────────────────────────────────────────
|
|
2
|
+
// ── Store ─────────────────────────────────────────────────────────
|
|
3
|
+
export { CommunicationStore, useCommunicationStore } from './store.js';
|
|
4
|
+
// ── Server (main entry points) ────────────────────────────────────
|
|
5
|
+
export { createCommunicationHandler, createCommunicationApiHandlers } from './server.js';
|
|
6
|
+
// ── Schema ────────────────────────────────────────────────────────
|
|
7
|
+
export { ensureCommunicationTables } from './schema.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface LinkPreview {
|
|
2
|
+
url: string;
|
|
3
|
+
title: string | null;
|
|
4
|
+
description: string | null;
|
|
5
|
+
image: string | null;
|
|
6
|
+
domain: string;
|
|
7
|
+
}
|
|
8
|
+
interface Db {
|
|
9
|
+
get<T = any>(sql: string, ...params: any[]): Promise<T | undefined>;
|
|
10
|
+
run(sql: string, ...params: any[]): Promise<any>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fetch Open Graph metadata from a URL and cache it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchLinkPreview(url: string, db?: Db): Promise<LinkPreview | null>;
|
|
16
|
+
/** Extract URLs from message text */
|
|
17
|
+
export declare function extractUrls(text: string): string[];
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
function isPrivateUrl(urlStr) {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(urlStr);
|
|
5
|
+
// Only allow http/https schemes
|
|
6
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
7
|
+
return true;
|
|
8
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
9
|
+
// Loopback and unspecified addresses
|
|
10
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '0.0.0.0')
|
|
11
|
+
return true;
|
|
12
|
+
// Single-label hostnames (no dots)
|
|
13
|
+
if (!hostname.includes('.'))
|
|
14
|
+
return true;
|
|
15
|
+
// Cloud metadata endpoints
|
|
16
|
+
if (hostname === 'metadata.google.internal' || hostname === 'metadata.internal')
|
|
17
|
+
return true;
|
|
18
|
+
// IPv6 addresses — block all private/reserved ranges
|
|
19
|
+
if (hostname.startsWith('[')) {
|
|
20
|
+
const ipv6 = hostname.slice(1, -1).toLowerCase();
|
|
21
|
+
if (ipv6 === '::1' || ipv6 === '::' || ipv6.startsWith('fe80:') || ipv6.startsWith('fd') || ipv6.startsWith('fc'))
|
|
22
|
+
return true;
|
|
23
|
+
return true; // Block all bracketed IPv6 for safety
|
|
24
|
+
}
|
|
25
|
+
// IPv4 private ranges
|
|
26
|
+
const parts = hostname.split('.').map(Number);
|
|
27
|
+
if (parts.length === 4 && parts.every(n => !isNaN(n))) {
|
|
28
|
+
if (parts[0] === 10)
|
|
29
|
+
return true;
|
|
30
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
31
|
+
return true;
|
|
32
|
+
if (parts[0] === 192 && parts[1] === 168)
|
|
33
|
+
return true;
|
|
34
|
+
if (parts[0] === 169 && parts[1] === 254)
|
|
35
|
+
return true; // link-local + cloud metadata
|
|
36
|
+
if (parts[0] === 0)
|
|
37
|
+
return true;
|
|
38
|
+
if (parts[0] === 127)
|
|
39
|
+
return true; // full loopback range
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fetch Open Graph metadata from a URL and cache it.
|
|
49
|
+
*/
|
|
50
|
+
export async function fetchLinkPreview(url, db) {
|
|
51
|
+
if (isPrivateUrl(url))
|
|
52
|
+
return null;
|
|
53
|
+
const urlHash = crypto.createHash('sha256').update(url).digest('hex').slice(0, 16);
|
|
54
|
+
// Check cache
|
|
55
|
+
if (db) {
|
|
56
|
+
const cached = await db.get('SELECT * FROM link_previews WHERE url_hash = ?', urlHash);
|
|
57
|
+
if (cached) {
|
|
58
|
+
return { url: cached.url, title: cached.title, description: cached.description, image: cached.image, domain: cached.domain };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
headers: { 'User-Agent': 'LumenJS LinkPreview/1.0' },
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
redirect: 'follow',
|
|
68
|
+
});
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
// Re-check final URL after redirects to prevent SSRF via redirect
|
|
71
|
+
if (res.url && res.url !== url && isPrivateUrl(res.url))
|
|
72
|
+
return null;
|
|
73
|
+
if (!res.ok)
|
|
74
|
+
return null;
|
|
75
|
+
const contentType = res.headers.get('content-type') || '';
|
|
76
|
+
if (!contentType.includes('text/html'))
|
|
77
|
+
return null;
|
|
78
|
+
const html = await res.text();
|
|
79
|
+
const domain = new URL(url).hostname;
|
|
80
|
+
const title = extractMeta(html, 'og:title') || extractTag(html, 'title');
|
|
81
|
+
const description = extractMeta(html, 'og:description') || extractMeta(html, 'description');
|
|
82
|
+
const image = extractMeta(html, 'og:image');
|
|
83
|
+
const preview = { url, title, description, image, domain };
|
|
84
|
+
// Cache
|
|
85
|
+
if (db) {
|
|
86
|
+
await db.run('INSERT OR REPLACE INTO link_previews (url_hash, url, title, description, image, domain) VALUES (?, ?, ?, ?, ?, ?)', urlHash, url, title, description, image, domain);
|
|
87
|
+
}
|
|
88
|
+
return preview;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Extract URLs from message text */
|
|
95
|
+
export function extractUrls(text) {
|
|
96
|
+
const regex = /https?:\/\/[^\s<>"')\]]+/g;
|
|
97
|
+
const matches = text.match(regex);
|
|
98
|
+
return matches ? [...new Set(matches)].slice(0, 3) : [];
|
|
99
|
+
}
|
|
100
|
+
function extractMeta(html, name) {
|
|
101
|
+
// Match <meta property="og:title" content="..."> or <meta name="description" content="...">
|
|
102
|
+
const regex = new RegExp(`<meta[^>]+(?:property|name)=["']${name}["'][^>]+content=["']([^"']+)["']`, 'i');
|
|
103
|
+
const match = html.match(regex);
|
|
104
|
+
if (match)
|
|
105
|
+
return match[1];
|
|
106
|
+
// Try reversed order: content before property
|
|
107
|
+
const regex2 = new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${name}["']`, 'i');
|
|
108
|
+
const match2 = html.match(regex2);
|
|
109
|
+
return match2 ? match2[1] : null;
|
|
110
|
+
}
|
|
111
|
+
function extractTag(html, tag) {
|
|
112
|
+
const regex = new RegExp(`<${tag}[^>]*>([^<]+)</${tag}>`, 'i');
|
|
113
|
+
const match = html.match(regex);
|
|
114
|
+
return match ? match[1].trim() : null;
|
|
115
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Communication module DB schema.
|
|
3
|
+
* Call ensureCommunicationTables(db) to create all required tables.
|
|
4
|
+
*/
|
|
5
|
+
interface Db {
|
|
6
|
+
exec(sql: string): Promise<void>;
|
|
7
|
+
isPg?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function ensureCommunicationTables(db: Db): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Communication module DB schema.
|
|
3
|
+
* Call ensureCommunicationTables(db) to create all required tables.
|
|
4
|
+
*/
|
|
5
|
+
export async function ensureCommunicationTables(db) {
|
|
6
|
+
if (db.isPg)
|
|
7
|
+
return;
|
|
8
|
+
await db.exec(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
type TEXT NOT NULL CHECK(type IN ('direct', 'group')),
|
|
12
|
+
name TEXT,
|
|
13
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS conversation_participants (
|
|
18
|
+
conversation_id TEXT NOT NULL,
|
|
19
|
+
user_id TEXT NOT NULL,
|
|
20
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
21
|
+
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
22
|
+
PRIMARY KEY (conversation_id, user_id),
|
|
23
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
conversation_id TEXT NOT NULL,
|
|
29
|
+
sender_id TEXT NOT NULL,
|
|
30
|
+
content TEXT NOT NULL DEFAULT '',
|
|
31
|
+
type TEXT NOT NULL DEFAULT 'text',
|
|
32
|
+
reply_to TEXT,
|
|
33
|
+
attachment TEXT,
|
|
34
|
+
status TEXT NOT NULL DEFAULT 'sent',
|
|
35
|
+
encrypted INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
updated_at TEXT,
|
|
38
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS read_receipts (
|
|
42
|
+
message_id INTEGER NOT NULL,
|
|
43
|
+
user_id TEXT NOT NULL,
|
|
44
|
+
read_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
45
|
+
PRIMARY KEY (message_id, user_id),
|
|
46
|
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS encryption_keys (
|
|
50
|
+
user_id TEXT PRIMARY KEY,
|
|
51
|
+
identity_key TEXT NOT NULL,
|
|
52
|
+
signed_pre_key_id INTEGER NOT NULL,
|
|
53
|
+
signed_pre_key TEXT NOT NULL,
|
|
54
|
+
signed_pre_key_signature TEXT NOT NULL,
|
|
55
|
+
uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS encryption_prekeys (
|
|
59
|
+
user_id TEXT NOT NULL,
|
|
60
|
+
key_id INTEGER NOT NULL,
|
|
61
|
+
public_key TEXT NOT NULL,
|
|
62
|
+
PRIMARY KEY (user_id, key_id),
|
|
63
|
+
FOREIGN KEY (user_id) REFERENCES encryption_keys(user_id) ON DELETE CASCADE
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS message_reactions (
|
|
67
|
+
message_id INTEGER NOT NULL,
|
|
68
|
+
user_id TEXT NOT NULL,
|
|
69
|
+
emoji TEXT NOT NULL,
|
|
70
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
71
|
+
PRIMARY KEY (message_id, user_id, emoji),
|
|
72
|
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
filename TEXT NOT NULL,
|
|
78
|
+
mimetype TEXT NOT NULL,
|
|
79
|
+
size INTEGER NOT NULL,
|
|
80
|
+
url TEXT NOT NULL,
|
|
81
|
+
thumbnail_url TEXT,
|
|
82
|
+
uploaded_by TEXT NOT NULL,
|
|
83
|
+
encrypted INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS link_previews (
|
|
88
|
+
url_hash TEXT PRIMARY KEY,
|
|
89
|
+
url TEXT NOT NULL,
|
|
90
|
+
title TEXT,
|
|
91
|
+
description TEXT,
|
|
92
|
+
image TEXT,
|
|
93
|
+
domain TEXT,
|
|
94
|
+
fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id, created_at);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_participants_user ON conversation_participants(user_id);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_reactions_message ON message_reactions(message_id);
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
@@ -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
|
+
}
|