@devchitchat/chat 3.0.61 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +66 -2
  2. package/index.js +132 -50
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -30,11 +30,74 @@ The system should just create a default hub and channel. That way, on bootstrap,
30
30
 
31
31
  # Getting Started
32
32
 
33
- ## Prerequisites
33
+ ## Install as a package
34
+
35
+ ```bash
36
+ bun add @devchitchat/chat
37
+ ```
38
+
39
+ Then start it from your own server file:
40
+
41
+ ```js
42
+ // server.mjs
43
+ import { start } from '@devchitchat/chat'
44
+
45
+ const server = await start({
46
+ port: 3000,
47
+ basePath: '/chat', // mount at example.com/chat — omit for root
48
+ dbPath: './data/chat.db',
49
+ })
50
+
51
+ console.log(`chat listening on port ${server.port}`)
52
+ ```
53
+
54
+ All config options and their defaults:
55
+
56
+ | Option | Env var fallback | Default |
57
+ |---|---|---|
58
+ | `port` | `PORT` | `3000` |
59
+ | `dbPath` | `DB_PATH` | `./data/chat.db` |
60
+ | `basePath` | `BASE_PATH` | `""` (root) |
61
+ | `dev` | `NODE_ENV !== 'production'` | `true` |
62
+ | `tlsCert` | `TLS_CERT` | `./certs/dev-cert.pem` |
63
+ | `tlsKey` | `TLS_KEY` | `./certs/dev-key.pem` |
64
+
65
+ Every option falls back to its environment variable, so you can configure via env instead of passing a config object:
66
+
67
+ ```bash
68
+ BASE_PATH=/chat PORT=3000 DB_PATH=./data/chat.db bun server.mjs
69
+ ```
70
+
71
+ `start()` returns the [Bun server instance](https://bun.sh/docs/api/http#bun-serve) — you can inspect `server.port`, stop it with `server.stop()`, etc.
72
+
73
+ Migrations run automatically on every call to `start()` — no separate migration step needed.
74
+
75
+ ---
76
+
77
+ ## Run standalone
78
+
79
+ To run the chat app directly without writing a wrapper:
80
+
81
+ ```bash
82
+ bunx devchitchat
83
+ ```
84
+
85
+ Or install globally:
86
+
87
+ ```bash
88
+ bun install -g @devchitchat/chat
89
+ devchitchat
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Run from source
95
+
96
+ ### Prerequisites
34
97
 
35
98
  - [Bun](https://bun.sh) v1.0 or later
36
99
 
37
- ## Install
100
+ ### Install
38
101
 
39
102
  ```bash
40
103
  bun install
@@ -121,6 +184,7 @@ Copy that URL and open it in a browser to create the first account, which become
121
184
  |---|---|---|
122
185
  | `PORT` | `3000` | Port the server listens on |
123
186
  | `DB_PATH` | `data/chat.db` | Path to the SQLite database file |
187
+ | `BASE_PATH` | `""` | URL subpath to mount the app at (e.g. `/chat`) |
124
188
  | `NODE_ENV` | `development` | Set to `production` in production |
125
189
  | `TLS_CERT` | `certs/dev-cert.pem` | Path to the TLS certificate |
126
190
  | `TLS_KEY` | `certs/dev-key.pem` | Path to the TLS private key |
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { createServer } from '@devchitchat/index97'
2
+ import { createServer, createRoutes } from '@devchitchat/index97'
3
3
  import { openDatabase } from './src/db/openDb.js'
4
4
  import { initDb } from './src/db/initDb.js'
5
5
  import { runMigrations } from './src/db/runMigrations.js'
@@ -12,26 +12,16 @@ import { UploadService } from './src/services/UploadService.js'
12
12
  import { LocalFileStore } from './src/adapters/LocalFileStore.js'
13
13
  import { SqliteUploadRepository } from './src/adapters/SqliteUploadRepository.js'
14
14
 
15
- /**
16
- * Start the devchitchat server.
17
- *
18
- * @param {object} config
19
- * @param {number} [config.port] - Port to listen on. Default: process.env.PORT ?? 3000
20
- * @param {string} [config.dbPath] - Path to the SQLite database file. Default: process.env.DB_PATH ?? './data/chat.db'
21
- * @param {string} [config.basePath] - URL subpath to mount the app at, e.g. '/chat'. Default: process.env.BASE_PATH ?? ''
22
- * @param {boolean} [config.dev] - Enable dev mode. Default: process.env.NODE_ENV !== 'production'
23
- * @param {string} [config.tlsCert] - Path to TLS certificate. Default: process.env.TLS_CERT ?? './certs/dev-cert.pem'
24
- * @param {string} [config.tlsKey] - Path to TLS private key. Default: process.env.TLS_KEY ?? './certs/dev-key.pem'
25
- * @returns {Promise<import('bun').Server>}
26
- */
27
- export async function start(config = {}) {
15
+ const CHAT_PAGES_DIR = import.meta.dir + '/pages'
16
+ const CHAT_PUBLIC_DIR = CHAT_PAGES_DIR + '/public'
17
+ const CHAT_CSP = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:"
18
+ const CHAT_PERMISSIONS_POLICY = 'camera=(self), microphone=(self), display-capture=(self)'
19
+
20
+ async function _setupServices(config = {}) {
28
21
  const {
29
- port = Number(process.env.PORT ?? 3000),
30
22
  dbPath = process.env.DB_PATH ?? './data/chat.db',
31
23
  basePath = (process.env.BASE_PATH ?? '').replace(/\/$/, ''),
32
24
  dev = process.env.NODE_ENV !== 'production',
33
- tlsCert = process.env.TLS_CERT ?? './certs/dev-cert.pem',
34
- tlsKey = process.env.TLS_KEY ?? './certs/dev-key.pem',
35
25
  } = config
36
26
 
37
27
  const p = path => `${basePath}${path}`
@@ -50,7 +40,6 @@ export async function start(config = {}) {
50
40
  })
51
41
  chat.messageService.setUploadService(uploadService)
52
42
 
53
- // Wire service context so page handlers (pages/**/*.js) can access services
54
43
  initContext({
55
44
  auth: chat.auth,
56
45
  hubService: chat.hubService,
@@ -67,26 +56,133 @@ export async function start(config = {}) {
67
56
  logger,
68
57
  })
69
58
 
59
+ return { chat, db, logger, p, basePath, dev }
60
+ }
61
+
62
+ /**
63
+ * Return chat routes and websocket handler for embedding in an existing server.
64
+ *
65
+ * Use this when you want to run the chat app on the same port as another app.
66
+ * Pass the returned routes and websocket to your own createServer() call, then
67
+ * call onServerReady() with the server instance once it is created.
68
+ *
69
+ * @example
70
+ * import { createServer } from '@devchitchat/index97'
71
+ * import { setup as setupChat } from '@devchitchat/chat'
72
+ *
73
+ * const chat = await setupChat({ basePath: '/chat', dbPath: './data/chat.db' })
74
+ *
75
+ * const server = await createServer({
76
+ * pagesDir: new URL('./pages', import.meta.url).pathname,
77
+ * port: 3000,
78
+ * routes: chat.routes,
79
+ * websocket: chat.websocket,
80
+ * })
81
+ *
82
+ * chat.onServerReady(server)
83
+ *
84
+ * @param {object} config
85
+ * @param {string} [config.basePath] - URL prefix for all chat routes (e.g. '/chat')
86
+ * @param {string} [config.dbPath] - Path to the SQLite database file
87
+ * @param {boolean} [config.dev] - Enable dev mode
88
+ * @returns {Promise<{ routes: object, websocket: object, onServerReady: Function }>}
89
+ */
90
+ export async function setup(config = {}) {
91
+ const { chat, logger, p, basePath, dev } = await _setupServices(config)
92
+
93
+ // Discover and prefix all file-based chat page routes
94
+ const routes = await createRoutes({
95
+ pagesDir: CHAT_PAGES_DIR,
96
+ prefix: basePath,
97
+ dev,
98
+ csp: CHAT_CSP,
99
+ permissionsPolicy: CHAT_PERMISSIONS_POLICY,
100
+ })
101
+
102
+ // Explicit protocol routes
103
+ routes[p('/ws')] = (req, server) => {
104
+ const session = sessionFromRequest(req)
105
+ if (server.upgrade(req, {
106
+ data: session ? { userId: session.user.user_id, sessionId: session.session_id, displayName: session.user.display_name } : {}
107
+ })) return
108
+ return new Response('WebSocket upgrade required', { status: 426 })
109
+ }
110
+
111
+ routes[p('/vendor/rdbl.js')] = () =>
112
+ new Response(Bun.file(new URL(import.meta.resolve('@devchitchat/rdbljs/src/rdbl.js'))), {
113
+ headers: { 'Content-Type': 'text/javascript' },
114
+ })
115
+
116
+ routes[p('/sw.js')] = () => new Response(
117
+ Bun.file(CHAT_PAGES_DIR + '/public/sw.js'),
118
+ { headers: { 'Content-Type': 'application/javascript; charset=utf-8', 'Service-Worker-Allowed': `${basePath}/`, 'Cache-Control': 'no-cache, no-store' } }
119
+ )
120
+
121
+ routes[p('/manifest.json')] = () => new Response(
122
+ JSON.stringify({
123
+ name: 'devchitchat', short_name: 'devchitchat',
124
+ start_url: `${basePath}/`, scope: `${basePath}/`,
125
+ display: 'standalone', background_color: '#1a1b1e', theme_color: '#141517',
126
+ icons: [{ src: `${basePath}/icon.png`, sizes: '300x300', type: 'image/png', purpose: 'any maskable' }]
127
+ }),
128
+ { headers: { 'Content-Type': 'application/manifest+json', 'Cache-Control': 'no-cache' } }
129
+ )
130
+
131
+ // Static files from pages/public/ — register each as an explicit route so the
132
+ // host server's fetch handler (which only knows its own public dir) can serve them
133
+ const glob = new Bun.Glob('**/*')
134
+ for await (const file of glob.scan({ cwd: CHAT_PUBLIC_DIR, onlyFiles: true })) {
135
+ if (file === '.DS_Store') continue
136
+ const pattern = basePath + '/' + file
137
+ const filePath = CHAT_PUBLIC_DIR + '/' + file
138
+ routes[pattern] = () => new Response(Bun.file(filePath))
139
+ }
140
+
141
+ return {
142
+ routes,
143
+ websocket: chat.websocket,
144
+ onServerReady(server) {
145
+ chat.attachServer(server)
146
+ logger.info('server.ready', { basePath })
147
+ },
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Start the devchitchat server as a standalone process.
153
+ *
154
+ * @param {object} config
155
+ * @param {number} [config.port] - Port to listen on. Default: process.env.PORT ?? 3000
156
+ * @param {string} [config.dbPath] - Path to the SQLite database file. Default: process.env.DB_PATH ?? './data/chat.db'
157
+ * @param {string} [config.basePath] - URL subpath to mount the app at, e.g. '/chat'. Default: process.env.BASE_PATH ?? ''
158
+ * @param {boolean} [config.dev] - Enable dev mode. Default: process.env.NODE_ENV !== 'production'
159
+ * @param {string} [config.tlsCert] - Path to TLS certificate. Default: process.env.TLS_CERT ?? './certs/dev-cert.pem'
160
+ * @param {string} [config.tlsKey] - Path to TLS private key. Default: process.env.TLS_KEY ?? './certs/dev-key.pem'
161
+ * @returns {Promise<import('bun').Server>}
162
+ */
163
+ export async function start(config = {}) {
164
+ const {
165
+ port = Number(process.env.PORT ?? 3000),
166
+ tlsCert = process.env.TLS_CERT ?? './certs/dev-cert.pem',
167
+ tlsKey = process.env.TLS_KEY ?? './certs/dev-key.pem',
168
+ } = config
169
+
170
+ const { chat, db, logger, p, basePath, dev } = await _setupServices(config)
171
+
70
172
  async function getTlsIfAvailable() {
71
173
  const cert = Bun.file(tlsCert)
72
- if (await cert.exists()) {
73
- return { cert, key: Bun.file(tlsKey) }
74
- }
174
+ if (await cert.exists()) return { cert, key: Bun.file(tlsKey) }
75
175
  return null
76
176
  }
77
177
 
78
178
  const server = await createServer({
79
- pagesDir: import.meta.dir + '/pages',
179
+ pagesDir: CHAT_PAGES_DIR,
80
180
  port,
81
181
  dev,
82
182
  basePath,
83
- // Allow camera/mic/display for WebRTC
84
- permissionsPolicy: 'camera=(self), microphone=(self), display-capture=(self)',
85
- // CSP: allow WebSocket connections to self
86
- csp: "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:",
87
- // So the SSE doesn't timeout.
183
+ permissionsPolicy: CHAT_PERMISSIONS_POLICY,
184
+ csp: CHAT_CSP,
88
185
  idleTimeout: 0,
89
- // WebSocket upgrade route — authenticate via session cookie before the first message
90
186
  routes: {
91
187
  [p('/ws')]: (req, server) => {
92
188
  const session = sessionFromRequest(req)
@@ -95,35 +191,24 @@ export async function start(config = {}) {
95
191
  })) return
96
192
  return new Response('WebSocket upgrade required', { status: 426 })
97
193
  },
98
- [p('/vendor/rdbl.js')]: () => {
99
- return new Response(Bun.file(new URL(import.meta.resolve('@devchitchat/rdbljs/src/rdbl.js'))), {
194
+ [p('/vendor/rdbl.js')]: () =>
195
+ new Response(Bun.file(new URL(import.meta.resolve('@devchitchat/rdbljs/src/rdbl.js'))), {
100
196
  headers: { 'Content-Type': 'text/javascript' },
101
- })
102
- },
103
- // Service worker at basePath scope so it can receive push events for all app pages
197
+ }),
104
198
  [p('/sw.js')]: () => new Response(
105
- Bun.file(import.meta.dir + '/pages/public/sw.js'),
199
+ Bun.file(CHAT_PAGES_DIR + '/public/sw.js'),
106
200
  { headers: { 'Content-Type': 'application/javascript; charset=utf-8', 'Service-Worker-Allowed': `${basePath}/`, 'Cache-Control': 'no-cache, no-store' } }
107
201
  ),
108
- // Dynamic PWA manifest — start_url and icon.src must reflect basePath
109
202
  [p('/manifest.json')]: () => new Response(
110
203
  JSON.stringify({
111
- name: 'devchitchat',
112
- short_name: 'devchitchat',
113
- start_url: `${basePath}/`,
114
- scope: `${basePath}/`,
115
- display: 'standalone',
116
- background_color: '#1a1b1e',
117
- theme_color: '#141517',
118
- icons: [
119
- { src: `${basePath}/icon.png`, sizes: '300x300', type: 'image/png', purpose: 'any maskable' }
120
- ]
204
+ name: 'devchitchat', short_name: 'devchitchat',
205
+ start_url: `${basePath}/`, scope: `${basePath}/`,
206
+ display: 'standalone', background_color: '#1a1b1e', theme_color: '#141517',
207
+ icons: [{ src: `${basePath}/icon.png`, sizes: '300x300', type: 'image/png', purpose: 'any maskable' }]
121
208
  }),
122
209
  { headers: { 'Content-Type': 'application/manifest+json', 'Cache-Control': 'no-cache' } }
123
210
  ),
124
211
  },
125
-
126
- // Bun native WebSocket handler (new index97 passthrough)
127
212
  websocket: chat.websocket,
128
213
  tls: await getTlsIfAvailable(),
129
214
  onShutdown: (server) => {
@@ -134,11 +219,8 @@ export async function start(config = {}) {
134
219
  },
135
220
  })
136
221
 
137
- // Give ChatServer a reference to the Bun server so it can publish to topics
138
222
  chat.attachServer(server)
139
-
140
223
  logger.info('server.ready', { port: server.port, dev, basePath })
141
-
142
224
  return server
143
225
  }
144
226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devchitchat/chat",
3
- "version": "3.0.61",
3
+ "version": "4.0.0",
4
4
  "description": "A small chat app. p2p video and screenshare.",
5
5
  "scripts": {
6
6
  "dev": "bun --watch index.js",
@@ -33,7 +33,7 @@
33
33
  "styles.css"
34
34
  ],
35
35
  "dependencies": {
36
- "@devchitchat/index97": "^2.1.2",
36
+ "@devchitchat/index97": "^3.0.0",
37
37
  "@devchitchat/rdbljs": "^2.0.1"
38
38
  },
39
39
  "main": "index.js",