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