OverviewFilesCommitsIssuesDocsReleasesPeers

keet-cli commits / 4501800

Use shared keet-daemon package for identity registry and vault support
Replace inline daemon code with thin wrapper importing from keet-daemon shared module. This gives keet-cli identity registry, vault encryption, multi-identity support, and all security audit fixes from godot-keet. Fixes the identity divergence bug where keet-cli used default/ storage instead of identity-0/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4501800aea184746418363f4f1de00e036ce3173
by plankatron <fleeky@gmail.com> · 2026-02-19T13:33:18.000Z

Changed files (5) +4 -1293

.pear-git/ci.yaml +2 -0
……@@ -2,3 +2,5 @@ steps:
22 - name: copy-deps
33 run: cp -r /storage/projects/code/hypercore-kb/hyper-fleeky/keet-cli/node_modules .
44 timeout: 120
5 - name: copy-shared-daemon
6 run: cp -r /storage/projects/code/hypercore-kb/hyper-fleeky/packages/keet-daemon node_modules/keet-daemon
daemon.js +2 -45
……@@ -1,45 +1,2 @@
1import KeetCoreHyperDB from '@holepunchto/keet-core-hyperdb'
2import path from 'path'
3import os from 'os'
4import fs from 'fs'
5import { Router } from './lib/router.js'
6import { UdsTransport } from './lib/uds-transport.js'
7
8export async function startDaemon (opts = {}) {
9 const storage = opts.storage || path.join(os.homedir(), '.config', 'keet-cli', 'default')
10 const pidFile = opts.pidFile || '/tmp/keet-daemon.pid'
11
12 console.log('[keet-daemon] Starting... storage:', storage)
13
14 // Init Keet core (holds exclusive RocksDB lock)
15 const KeetCore = KeetCoreHyperDB.default || KeetCoreHyperDB
16 const { keet: core } = KeetCore.from(storage, {
17 experimental: false,
18 devMirrors: false,
19 devUserRegistry: false,
20 swarming: true,
21 production: true,
22 onerror: err => console.error('[keet-daemon] Core error:', err)
23 })
24
25 const backend = core.backendAPI
26 await backend.ready()
27 console.log('[keet-daemon] Backend ready')
28
29 // Router + UDS transport (no HTTP — see STRIP_HTTP.md)
30 const router = new Router(backend, core)
31 const socketPath = opts.socketPath || '/tmp/keet-daemon.sock'
32 const uds = new UdsTransport(router, { socketPath })
33 await uds.start()
34
35 // Write PID file
36 try {
37 fs.writeFileSync(pidFile, String(Bare.pid))
38 } catch (err) {
39 console.error('[keet-daemon] Warning: could not write PID file:', err.message)
40 }
41
42 console.log(`[keet-daemon] Ready — UDS ${socketPath}, PID ${Bare.pid}`)
43
44 return { router, uds, core, pidFile }
45}
1import { startDaemon } from 'keet-daemon'
2export { startDaemon }
lib/format.js +0 -38
……@@ -1,38 +0,0 @@
1export function getGenericEventName (code) {
2 const events = {
3 1: 'MEMBER_JOINED',
4 2: 'CALL_STARTED',
5 3: 'AVATAR_CHANGED',
6 4: 'INVITATION_CREATED',
7 5: 'MEMBER_LEFT',
8 6: 'TITLE_CHANGED',
9 7: 'MEMBER_REMOVED',
10 8: 'CALL_HANGUP',
11 9: 'CALL_MISSED'
12 }
13 return events[code] || `EVENT_${code}`
14}
15
16export function formatMessage (msg) {
17 const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '??:??'
18 const sender = msg.memberId || 'unknown'
19 const text = msg.chat?.text || msg.text || msg.message?.text || ''
20 const deleted = msg.deleted ? ' [DELETED]' : ''
21 const isSystem = msg.message?.generic ? ` [${getGenericEventName(msg.message.generic)}]` : ''
22 if (!text && !isSystem) return null
23 return `[${time}] ${sender}: ${text}${deleted}${isSystem}`
24}
25
26export function formatChatEvent (event) {
27 if (!event) return null
28 if (event.chat) {
29 const time = event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : '??:??'
30 const sender = event.memberId || 'unknown'
31 const text = event.chat.text || ''
32 const deleted = event.deleted ? ' [DELETED]' : ''
33 return `[${time}] ${sender}: ${text}${deleted}`
34 } else if (event.message?.generic) {
35 return `[system] ${getGenericEventName(event.message.generic)}`
36 }
37 return `[event] ${JSON.stringify(event)}`
38}
lib/router.js +0 -920
……@@ -1,920 +0,0 @@
1export class Router {
2 constructor (backend, core) {
3 this.backend = backend
4 this.core = core
5 this._startTime = Date.now()
6 }
7
8 async handle (method, params = {}) {
9 try {
10 const data = await this._dispatch(method, params)
11 return { ok: true, data }
12 } catch (err) {
13 return { ok: false, error: err.message || String(err) }
14 }
15 }
16
17 async _dispatch (method, params) {
18 switch (method) {
19 case 'identity': return this._identity()
20 case 'identity.recovery': return this._identityRecovery()
21 case 'identity.restore': return this._identityRestore(params)
22 case 'identity.setName': return this._identitySetName(params)
23 case 'rooms': return this._rooms()
24 case 'room': return this._room(params)
25 case 'room.create': return this._roomCreate(params)
26 case 'room.join': return this._roomJoin(params)
27 case 'room.invite': return this._roomInvite(params)
28 case 'room.leave': return this._roomLeave(params)
29 case 'send': return this._send(params)
30 case 'read': return this._read(params)
31 case 'members': return this._members(params)
32 case 'members.role': return this._changeMemberRole(params)
33 case 'dm.request': return this._dmRequest(params)
34 case 'watch': return this._watch(params)
35 case 'call.authorize': return this._callAuthorize(params)
36 case 'call.deauthorize': return this._callDeauthorize(params)
37 case 'call.join': return this._callJoinPeer(params)
38 case 'call.leave': return this._callLeavePeer(params)
39 case 'call.presence': return this._callPresence(params)
40 case 'call.setPresent': return this._callSetPresent(params)
41 case 'call.setNotPresent': return this._callSetNotPresent(params)
42 case 'call.tracks': return this._callGetTracks(params)
43 case 'call.requestTrack': return this._callRequestTrack(params)
44 case 'call.unrequestTrack': return this._callUnrequestTrack(params)
45 case 'call.shareTrack': return this._callShareTrack(params)
46 case 'call.unshareTrack': return this._callUnshareTrack(params)
47 case 'call.signal': return this._callSignalMedia(params)
48 // call.raw removed — arbitrary function execution (SECURITY_AUDIT_V2 D-F3)
49 case 'msg.edit': return this._editMessage(params)
50 case 'msg.delete': return this._deleteMessage(params)
51 case 'msg.pin': return this._pinMessage(params)
52 case 'msg.unpin': return this._unpinMessage(params)
53 case 'msg.pinned': return this._getPinned(params)
54 case 'msg.search': return this._searchMessages(params)
55 case 'react': return this._addReaction(params)
56 case 'unreact': return this._removeReaction(params)
57 case 'reactions': return this._getReactions(params)
58 case 'mute': return this._muteMember(params)
59 case 'unmute': return this._unmuteMember(params)
60 case 'block': return this._blockMember(params)
61 case 'unblock': return this._unblockMember(params)
62 case 'kick': return this._removeMember(params)
63 case 'blocked': return this._getBlocked(params)
64 // raw removed — arbitrary function execution (SECURITY_AUDIT_V2 D-F3)
65 case 'status': return this._status()
66 case 'shutdown': return this._shutdown()
67
68 // --- Introspection ---
69 case 'introspect': return this._introspect(params)
70
71 // --- Identity & Username ---
72 case 'identity.confirm': return this._identityConfirm()
73 case 'identity.store': return this._identityStore(params)
74 case 'identity.register': return this._identityRegister(params)
75 case 'identity.checkName': return this._identityCheckName(params)
76 case 'identity.lookup': return this._identityLookup(params)
77 case 'identity.lookupMember': return this._identityLookupMember(params)
78
79 // --- Room Config & Management ---
80 case 'room.config': return this._roomConfig(params)
81 case 'room.config.set': return this._roomConfigSet(params)
82 case 'room.bookmark': return this._roomBookmark(params)
83 case 'room.unbookmark': return this._roomUnbookmark(params)
84 case 'room.preferences': return this._roomPreferences(params)
85 case 'room.preferences.set': return this._roomPreferencesSet(params)
86 case 'room.search': return this._roomSearch(params)
87 case 'room.unread': return this._roomUnread(params)
88
89 // --- DM Management ---
90 case 'dm.list': return this._dmList()
91 case 'dm.requests': return this._dmRequests(params)
92 case 'dm.accept': return this._dmAccept(params)
93 case 'dm.reject': return this._dmReject(params)
94
95 // --- File Management ---
96 case 'file.send': return this._fileSend(params)
97 case 'file.list': return this._fileList(params)
98 case 'file.info': return this._fileInfo(params)
99 case 'file.download': return this._fileDownload(params)
100 case 'file.cancel': return this._fileCancel(params)
101 case 'file.remove': return this._fileRemove(params)
102 case 'file.clear': return this._fileClear(params)
103 case 'file.fetch': return this._fileFetch(params)
104 case 'file.preview': return this._filePreview(params)
105 case 'file.subscribe': return { ok: true }
106 case 'file.unsubscribe': return { ok: true }
107 case 'file.path': return this._filePath(params)
108
109 // --- Advanced Members ---
110 case 'members.active': return this._membersActive(params)
111 case 'members.mods': return this._membersMods(params)
112 case 'members.count': return this._membersCount(params)
113 case 'members.connected': return this._membersConnected(params)
114 case 'members.search': return this._membersSearch(params)
115
116 // --- Contacts ---
117 case 'contact.info': return this._contactInfo(params)
118 case 'contact.self': return this._contactSelf()
119 case 'dm.fromContact': return this._dmFromContact(params)
120
121 // --- Sub-APIs ---
122 case 'network': return this._networkQuery()
123 case 'preferences': return this._preferencesQuery()
124 case 'preferences.set': return this._preferencesSet(params)
125 case 'debug.stats': return this._debugStats(params)
126
127 // --- System ---
128 case 'version': return this._getVersion()
129 case 'stats': return this._getStats()
130
131 default: throw new Error(`Unknown method: ${method}`)
132 }
133 }
134
135 // --- Identity ---
136
137 async _identity () {
138 const identity = await this.backend.api.core.getIdentity()
139 return identity || null
140 }
141
142 async _identityRecovery () {
143 const phrase = await this.backend.api.core.getRecoveryPhrase()
144 return { phrase }
145 }
146
147 async _identityRestore (params) {
148 if (!params.phrase) throw new Error('Missing required param: phrase')
149 await this.backend.api.core.restoreIdentity(params.phrase)
150 return { ok: true }
151 }
152
153 async _identitySetName (params) {
154 if (!params.name) throw new Error('Missing required param: name')
155 await this.backend.api.core.updateIdentityProfile({ displayName: params.name })
156 return { ok: true, name: params.name }
157 }
158
159 // --- Rooms ---
160
161 async _rooms () {
162 const result = await this.backend.api.core.getRecentRooms()
163 const rooms = Array.isArray(result) ? result : (result?.rooms || result?.data || [])
164 return rooms.map(room => ({
165 roomId: room.roomId || room.id || room.key || '?',
166 title: room.config?.title || room.title || '(untitled)'
167 }))
168 }
169
170 async _room (params) {
171 if (!params.roomId) throw new Error('Missing required param: roomId')
172 return this.backend.api.core.getRoomInfo(params.roomId)
173 }
174
175 async _roomCreate (params) {
176 if (!params.title) throw new Error('Missing required param: title')
177 return this.backend.api.core.createRoom({ title: params.title })
178 }
179
180 async _roomJoin (params) {
181 if (!params.invite) throw new Error('Missing required param: invite')
182 let invite = params.invite.trim()
183
184 // Strip pear://keet/ or punch:// prefixes — keet-core expects raw invite token
185 if (invite.startsWith('pear://keet/')) invite = invite.slice('pear://keet/'.length)
186 else if (invite.startsWith('punch://')) invite = invite.slice('punch://'.length)
187
188 try {
189 const result = await this.backend.api.core.startPairingRoom(invite)
190 return { status: 'pairing', ...(result || {}) }
191 } catch (err) {
192 // Fallback to pairRoom if startPairingRoom fails
193 try {
194 const result = await this.backend.api.core.pairRoom(invite)
195 return { status: 'joined', ...(result || {}) }
196 } catch {
197 throw err
198 }
199 }
200 }
201
202 async _roomInvite (params) {
203 if (!params.roomId) throw new Error('Missing required param: roomId')
204 const opts = {}
205 if (params.role !== undefined) opts.role = params.role
206 const result = await this.backend.api.core.createInvitation(params.roomId, opts)
207 if (typeof result === 'string') {
208 return { token: result }
209 }
210 return result
211 }
212
213 async _roomLeave (params) {
214 if (!params.roomId) throw new Error('Missing required param: roomId')
215 await this.backend.api.core.leaveRoom(params.roomId)
216 return { ok: true }
217 }
218
219 // --- Messages ---
220
221 async _send (params) {
222 if (!params.roomId) throw new Error('Missing required param: roomId')
223 if (!params.text) throw new Error('Missing required param: text')
224 const result = await this.backend.api.core.addChatMessage(params.roomId, params.text, {})
225 return result || { ok: true }
226 }
227
228 async _read (params) {
229 if (!params.roomId) throw new Error('Missing required param: roomId')
230 const limit = params.limit || 20
231 const result = await this.backend.api.core.getChatMessages(params.roomId, {
232 limit,
233 reverse: true
234 })
235 const messages = Array.isArray(result) ? result : (result?.messages || result?.data || result?.events || [])
236
237 // Resolve member display names
238 const nameMap = await this._getMemberNames(params.roomId)
239
240 return [...(messages || [])].reverse().map(msg => {
241 const memberId = msg.memberId || 'unknown'
242 const generic = msg.message?.generic || null
243 // Normalize system: only set if there's an actual event (non-null/non-false value)
244 let system = null
245 if (generic && typeof generic === 'object') {
246 for (const v of Object.values(generic)) {
247 if (v !== null && v !== false) { system = generic; break }
248 }
249 } else if (generic) {
250 system = generic
251 }
252 // Extract file attachments (encoded file structure from keet-core API)
253 // Encoded files have: metadata.{name,mimetype,size,dimensions}, entry.{httpLink, preview.small.httpLink},
254 // pointer.externalBlob.id (z32 key). Raw files have: name, mimeType, size at top level.
255 const rawFiles = msg.message?.files || msg.files || []
256 const files = Array.isArray(rawFiles) ? rawFiles.map(f => {
257 const meta = f.metadata || {}
258 const entry = f.entry || {}
259 const name = meta.name || f.name || f.fileName || 'file'
260 return {
261 id: name, // Use filename as canonical ID (most reliable across encoded/raw formats)
262 name,
263 type: meta.mimetype || f.mimeType || '',
264 size: meta.size || f.size || 0,
265 dimensions: meta.dimensions || f.dimensions || null,
266 httpLink: entry.httpLink || f.httpLink || '',
267 preview: entry.preview ? {
268 small: entry.preview.small?.httpLink || '',
269 medium: entry.preview.medium?.httpLink || ''
270 } : null
271 }
272 }) : []
273
274 return {
275 memberId,
276 name: nameMap[memberId] || memberId.slice(0, 8),
277 text: msg.chat?.text || msg.text || msg.message?.text || '',
278 timestamp: msg.timestamp || null,
279 deleted: msg.deleted || false,
280 system,
281 files: files.length > 0 ? files : null,
282 seq: msg.seq ?? msg.clock ?? null,
283 id: msg.id || null
284 }
285 }).filter(m => m.text || m.system || m.files)
286 }
287
288 async _getMemberNames (roomId) {
289 try {
290 const result = await this.backend.api.core.getMembers(roomId)
291 const members = Array.isArray(result) ? result : (result?.members || result?.data || [])
292 const map = {}
293 for (const m of (members || [])) {
294 const id = m.memberId || m.id
295 if (id) map[id] = m.displayName || m.name || id.slice(0, 8)
296 }
297 return map
298 } catch {
299 return {}
300 }
301 }
302
303 async _watch () {
304 // watch is handled at the transport level (SSE / socket push)
305 throw new Error('watch must be handled by transport layer')
306 }
307
308 // --- Members ---
309
310 async _changeMemberRole (params) {
311 if (!params.roomId) throw new Error('Missing required param: roomId')
312 if (!params.memberId) throw new Error('Missing required param: memberId')
313 if (params.role === undefined) throw new Error('Missing required param: role (1=admin, 2=mod, 3=peer)')
314 await this.backend.api.core.changeMemberRole(params.roomId, {
315 memberId: params.memberId,
316 role: params.role
317 })
318 return { ok: true }
319 }
320
321 async _members (params) {
322 if (!params.roomId) throw new Error('Missing required param: roomId')
323 const result = await this.backend.api.core.getMembers(params.roomId)
324 const members = Array.isArray(result) ? result : (result?.members || result?.data || [])
325 return (members || []).map(m => {
326 let role = 'peer'
327 if (m.status?.isAdmin) role = 'admin'
328 else if (m.status?.isModerator) role = 'mod'
329 else if (m.role === 1) role = 'admin'
330 else if (m.role === 2) role = 'mod'
331 else if (m.role === 4) role = 'read-only'
332 return {
333 memberId: m.memberId || m.id || '?',
334 name: m.displayName || m.name || m.memberId || '?',
335 role,
336 online: m.status?.isActive || m.online || false,
337 canDM: m.canDM || false
338 }
339 })
340 }
341
342 // --- DMs ---
343
344 async _dmRequest (params) {
345 if (!params.roomId) throw new Error('Missing required param: roomId')
346 if (!params.memberId) throw new Error('Missing required param: memberId')
347 const message = params.message || ''
348 const result = await this.backend.api.core.sendDMRequest(
349 params.roomId,
350 params.memberId,
351 { message, receivers: [params.memberId] }
352 )
353 return result || { ok: true }
354 }
355
356 // --- Message Operations ---
357
358 async _editMessage (params) {
359 if (!params.roomId) throw new Error('Missing required param: roomId')
360 if (!params.messageId) throw new Error('Missing required param: messageId')
361 if (!params.text) throw new Error('Missing required param: text')
362 await this.backend.api.core.updateChatMessage(params.roomId, params.messageId, params.text)
363 return { ok: true }
364 }
365
366 async _deleteMessage (params) {
367 if (!params.roomId) throw new Error('Missing required param: roomId')
368 if (!params.messageId) throw new Error('Missing required param: messageId')
369 await this.backend.api.core.removeChatMessage(params.roomId, params.messageId)
370 return { ok: true }
371 }
372
373 async _pinMessage (params) {
374 if (!params.roomId) throw new Error('Missing required param: roomId')
375 if (!params.messageId) throw new Error('Missing required param: messageId')
376 await this.backend.api.core.pinChatMessage(params.roomId, params.messageId)
377 return { ok: true }
378 }
379
380 async _unpinMessage (params) {
381 if (!params.roomId) throw new Error('Missing required param: roomId')
382 if (!params.messageId) throw new Error('Missing required param: messageId')
383 await this.backend.api.core.unpinChatMessage(params.roomId, params.messageId)
384 return { ok: true }
385 }
386
387 async _getPinned (params) {
388 if (!params.roomId) throw new Error('Missing required param: roomId')
389 return this.backend.api.core.getPinnedChatMessages(params.roomId)
390 }
391
392 async _searchMessages (params) {
393 if (!params.roomId) throw new Error('Missing required param: roomId')
394 if (!params.query) throw new Error('Missing required param: query')
395 return this.backend.api.core.searchChatMessages(params.roomId, { query: params.query })
396 }
397
398 // --- Reactions ---
399
400 async _addReaction (params) {
401 if (!params.roomId) throw new Error('Missing required param: roomId')
402 if (!params.messageId) throw new Error('Missing required param: messageId')
403 if (!params.emoji) throw new Error('Missing required param: emoji')
404 await this.backend.api.core.addReaction(params.roomId, params.messageId, params.emoji)
405 return { ok: true }
406 }
407
408 async _removeReaction (params) {
409 if (!params.roomId) throw new Error('Missing required param: roomId')
410 if (!params.messageId) throw new Error('Missing required param: messageId')
411 if (!params.emoji) throw new Error('Missing required param: emoji')
412 await this.backend.api.core.removeReaction(params.roomId, params.messageId, params.emoji)
413 return { ok: true }
414 }
415
416 async _getReactions (params) {
417 if (!params.roomId) throw new Error('Missing required param: roomId')
418 if (!params.messageId) throw new Error('Missing required param: messageId')
419 return this.backend.api.core.getReactions(params.roomId, params.messageId)
420 }
421
422 // --- Moderation ---
423
424 async _muteMember (params) {
425 if (!params.roomId) throw new Error('Missing required param: roomId')
426 if (!params.memberId) throw new Error('Missing required param: memberId')
427 await this.backend.api.core.muteMember(params.roomId, params.memberId)
428 return { ok: true }
429 }
430
431 async _unmuteMember (params) {
432 if (!params.roomId) throw new Error('Missing required param: roomId')
433 if (!params.memberId) throw new Error('Missing required param: memberId')
434 await this.backend.api.core.unmuteMember(params.roomId, params.memberId)
435 return { ok: true }
436 }
437
438 async _blockMember (params) {
439 if (!params.memberId) throw new Error('Missing required param: memberId')
440 await this.backend.api.core.blockMember(params.memberId)
441 return { ok: true }
442 }
443
444 async _unblockMember (params) {
445 if (!params.memberId) throw new Error('Missing required param: memberId')
446 await this.backend.api.core.unblockMember(params.memberId)
447 return { ok: true }
448 }
449
450 async _removeMember (params) {
451 if (!params.roomId) throw new Error('Missing required param: roomId')
452 if (!params.memberId) throw new Error('Missing required param: memberId')
453 await this.backend.api.core.removeMember(params.roomId, params.memberId)
454 return { ok: true }
455 }
456
457 async _getBlocked (params) {
458 return this.backend.api.core.getBlockedMembers()
459 }
460
461 // --- Calls ---
462
463 _callApi () {
464 const api = this.backend.api.call
465 if (!api) throw new Error('Call API not available')
466 return api
467 }
468
469 async _callAuthorize (params) {
470 if (!params.roomId) throw new Error('Missing required param: roomId')
471 await this._callApi().authorize(params.roomId)
472 return { ok: true }
473 }
474
475 async _callDeauthorize (params) {
476 if (!params.roomId) throw new Error('Missing required param: roomId')
477 await this._callApi().deauthorize(params.roomId)
478 return { ok: true }
479 }
480
481 async _callJoinPeer (params) {
482 if (!params.roomId) throw new Error('Missing required param: roomId')
483 if (!params.peerId) throw new Error('Missing required param: peerId')
484 await this._callApi().joinPeer(params.roomId, params.peerId)
485 return { ok: true }
486 }
487
488 async _callLeavePeer (params) {
489 if (!params.roomId) throw new Error('Missing required param: roomId')
490 if (!params.peerId) throw new Error('Missing required param: peerId')
491 await this._callApi().leavePeer(params.roomId, params.peerId)
492 return { ok: true }
493 }
494
495 async _callPresence (params) {
496 if (!params.roomId) throw new Error('Missing required param: roomId')
497 return this._callApi().getPresence(params.roomId)
498 }
499
500 async _callSetPresent (params) {
501 if (!params.roomId) throw new Error('Missing required param: roomId')
502 await this._callApi().setPresent(params.roomId)
503 return { ok: true }
504 }
505
506 async _callSetNotPresent (params) {
507 if (!params.roomId) throw new Error('Missing required param: roomId')
508 await this._callApi().setNotPresent(params.roomId)
509 return { ok: true }
510 }
511
512 async _callGetTracks (params) {
513 if (!params.roomId) throw new Error('Missing required param: roomId')
514 return this._callApi().getAvailableTracks(params.roomId)
515 }
516
517 async _callRequestTrack (params) {
518 if (!params.roomId) throw new Error('Missing required param: roomId')
519 if (!params.peerId) throw new Error('Missing required param: peerId')
520 if (!params.trackKind) throw new Error('Missing required param: trackKind (audio|video|screen)')
521 await this._callApi().requestTrack(params.roomId, params.peerId, params.trackKind)
522 return { ok: true }
523 }
524
525 async _callUnrequestTrack (params) {
526 if (!params.roomId) throw new Error('Missing required param: roomId')
527 if (!params.peerId) throw new Error('Missing required param: peerId')
528 if (!params.trackKind) throw new Error('Missing required param: trackKind')
529 await this._callApi().unrequestTrack(params.roomId, params.peerId, params.trackKind)
530 return { ok: true }
531 }
532
533 async _callShareTrack (params) {
534 if (!params.roomId) throw new Error('Missing required param: roomId')
535 if (!params.peerId) throw new Error('Missing required param: peerId')
536 if (!params.trackKind) throw new Error('Missing required param: trackKind')
537 await this._callApi().shareTrack(params.roomId, params.peerId, params.trackKind, params.trackData || null)
538 return { ok: true }
539 }
540
541 async _callUnshareTrack (params) {
542 if (!params.roomId) throw new Error('Missing required param: roomId')
543 if (!params.peerId) throw new Error('Missing required param: peerId')
544 if (!params.trackKind) throw new Error('Missing required param: trackKind')
545 await this._callApi().unshareTrack(params.roomId, params.peerId, params.trackKind)
546 return { ok: true }
547 }
548
549 async _callSignalMedia (params) {
550 if (!params.roomId) throw new Error('Missing required param: roomId')
551 if (!params.peerId) throw new Error('Missing required param: peerId')
552 if (!params.signal) throw new Error('Missing required param: signal')
553 await this._callApi().signalMedia(params.roomId, params.peerId, params.signal)
554 return { ok: true }
555 }
556
557 // call.raw and raw removed — arbitrary function execution vectors (SECURITY_AUDIT_V2 D-F3)
558
559 // --- Streaming ---
560
561 subscribe (roomId) {
562 return this.backend.api.core.subscribeChatMessages(roomId)
563 }
564
565 // --- System ---
566
567 async _status () {
568 const identity = await this.backend.api.core.getIdentity()
569 const rooms = await this._rooms()
570 return {
571 uptime: Math.floor((Date.now() - this._startTime) / 1000),
572 rooms: rooms.length,
573 identity: identity ? { memberId: identity.memberId, name: identity.displayName } : null
574 }
575 }
576
577 async _shutdown () {
578 setTimeout(async () => {
579 await this.core.close()
580 Bare.exit(0)
581 }, 500)
582 return { ok: true, message: 'shutting down' }
583 }
584
585 // --- Introspection ---
586
587 async _introspect (params) {
588 const target = params.api || 'core'
589 const api = target === 'core' ? this.backend.api.core
590 : target === 'call' ? this.backend.api.call
591 : target === 'preferences' ? this.backend.api.preferences
592 : target === 'network' ? this.backend.api.network
593 : target === 'debug' ? this.backend.api.debug
594 : null
595 if (!api) throw new Error(`Unknown API: ${target}`)
596 const methods = []
597 for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(api))) {
598 if (key === 'constructor') continue
599 if (typeof api[key] === 'function') methods.push(key)
600 }
601 for (const key of Object.keys(api)) {
602 if (typeof api[key] === 'function' && !methods.includes(key)) methods.push(key)
603 }
604 return { api: target, methods: methods.sort(), count: methods.length }
605 }
606
607 // --- Identity & Username ---
608
609 async _identityConfirm () {
610 await this.backend.api.core.confirmRecoveryPhrase()
611 return { ok: true }
612 }
613
614 async _identityStore (params) {
615 if (!params.phrase) throw new Error('Missing required param: phrase')
616 await this.backend.api.core.storeRecoveryPhrase(params.phrase)
617 return { ok: true }
618 }
619
620 async _identityRegister (params) {
621 if (!params.username) throw new Error('Missing required param: username')
622 const result = await this.backend.api.core.registerUsername(params.username)
623 return { ok: true, result }
624 }
625
626 async _identityCheckName (params) {
627 if (!params.username) throw new Error('Missing required param: username')
628 const available = await this.backend.api.core.checkUsername(params.username)
629 return { username: params.username, available }
630 }
631
632 async _identityLookup (params) {
633 if (!params.username) throw new Error('Missing required param: username')
634 return this.backend.api.core.lookupUsername(params.username)
635 }
636
637 async _identityLookupMember (params) {
638 if (!params.roomId) throw new Error('Missing required param: roomId')
639 if (!params.memberId) throw new Error('Missing required param: memberId')
640 return this.backend.api.core.lookupUserByMemberId(params.roomId, params.memberId)
641 }
642
643 // --- Room Config & Management ---
644
645 async _roomConfig (params) {
646 if (!params.roomId) throw new Error('Missing required param: roomId')
647 return this.backend.api.core.getConfig(params.roomId)
648 }
649
650 async _roomConfigSet (params) {
651 if (!params.roomId) throw new Error('Missing required param: roomId')
652 if (!params.key) throw new Error('Missing required param: key')
653 if (params.value === undefined) throw new Error('Missing required param: value')
654 await this.backend.api.core.updateConfig(params.roomId, params.key, params.value)
655 return { ok: true }
656 }
657
658 async _roomBookmark (params) {
659 if (!params.roomId) throw new Error('Missing required param: roomId')
660 await this.backend.api.core.bookmark(params.roomId)
661 return { ok: true }
662 }
663
664 async _roomUnbookmark (params) {
665 if (!params.roomId) throw new Error('Missing required param: roomId')
666 await this.backend.api.core.unbookmark(params.roomId)
667 return { ok: true }
668 }
669
670 async _roomPreferences (params) {
671 if (!params.roomId) throw new Error('Missing required param: roomId')
672 return this.backend.api.core.getRoomPreferences(params.roomId)
673 }
674
675 async _roomPreferencesSet (params) {
676 if (!params.roomId) throw new Error('Missing required param: roomId')
677 const { roomId, ...prefs } = params
678 await this.backend.api.core.updateRoomPreferences(roomId, prefs)
679 return { ok: true }
680 }
681
682 async _roomSearch (params) {
683 if (!params.query) throw new Error('Missing required param: query')
684 return this.backend.api.core.searchRooms(params.query)
685 }
686
687 async _roomUnread (params) {
688 if (!params.roomId) throw new Error('Missing required param: roomId')
689 if (params.unread === undefined) throw new Error('Missing required param: unread')
690 await this.backend.api.core.setRoomUnread(params.roomId, params.unread)
691 return { ok: true }
692 }
693
694 // --- DM Management ---
695
696 async _dmList () {
697 // No getDmList — use getDmRequestsByStatus(1) for accepted DMs
698 return this.backend.api.core.getDmRequestsByStatus(1)
699 }
700
701 async _dmRequests (params) {
702 const status = params.status || 3 // default: pending
703 return this.backend.api.core.getDmRequestsByStatus(status)
704 }
705
706 async _dmAccept (params) {
707 if (!params.roomId) throw new Error('Missing required param: roomId')
708 await this.backend.api.core.acceptDmRequest(params.roomId)
709 return { ok: true }
710 }
711
712 async _dmReject (params) {
713 if (!params.roomId) throw new Error('Missing required param: roomId')
714 await this.backend.api.core.rejectDmRequest(params.roomId)
715 return { ok: true }
716 }
717
718 // --- File Management ---
719
720 async _fileSend (params) {
721 if (!params.roomId) throw new Error('Missing required param: roomId')
722 if (!params.file) throw new Error('Missing required param: file')
723 return this.backend.api.core.sendFile(params.roomId, params.file)
724 }
725
726 async _fileList (params) {
727 if (!params.roomId) throw new Error('Missing required param: roomId')
728 return this.backend.api.core.getLocalFiles(params.roomId)
729 }
730
731 async _fileInfo (params) {
732 if (!params.roomId) throw new Error('Missing required param: roomId')
733 if (!params.fileId) throw new Error('Missing required param: fileId')
734 return this.backend.api.core.getFileEntry(params.roomId, params.fileId)
735 }
736
737 async _fileDownload (params) {
738 if (!params.roomId) throw new Error('Missing required param: roomId')
739 if (!params.fileId) throw new Error('Missing required param: fileId')
740 await this.backend.api.core.startDownload(params.roomId, params.fileId)
741 return { ok: true }
742 }
743
744 async _fileCancel (params) {
745 if (!params.roomId) throw new Error('Missing required param: roomId')
746 if (!params.fileId) throw new Error('Missing required param: fileId')
747 await this.backend.api.core.cancelDownload(params.roomId, params.fileId)
748 return { ok: true }
749 }
750
751 async _fileRemove (params) {
752 if (!params.roomId) throw new Error('Missing required param: roomId')
753 if (!params.fileId) throw new Error('Missing required param: fileId')
754 await this.backend.api.core.removeFile(params.roomId, params.fileId)
755 return { ok: true }
756 }
757
758 async _fileClear (params) {
759 if (!params.roomId) throw new Error('Missing required param: roomId')
760 if (!params.fileId) throw new Error('Missing required param: fileId')
761 await this.backend.api.core.clearFile(params.roomId, params.fileId)
762 return { ok: true }
763 }
764
765 async _fileFetch (params) {
766 if (!params.url) throw new Error('Missing required param: url (httpLink URL from file preview)')
767 const { default: fetch } = await import('bare-fetch')
768 const resp = await fetch(params.url)
769 if (!resp.ok) throw new Error(`Fetch failed: ${resp.status} ${resp.statusText}`)
770 const buf = Buffer.from(await resp.arrayBuffer())
771 const contentType = resp.headers.get('content-type') || 'application/octet-stream'
772 return { base64: buf.toString('base64'), contentType, size: buf.length }
773 }
774
775 async _filePath (params) {
776 if (!params.roomId) throw new Error('Missing required param: roomId')
777 if (!params.fileId) throw new Error('Missing required param: fileId')
778 const result = await this.backend.api.core.getFileEntry(params.roomId, params.fileId)
779 return result || {}
780 }
781
782 async _filePreview (params) {
783 if (!params.roomId) throw new Error('Missing required param: roomId')
784 if (!params.fileId) throw new Error('Missing required param: fileId')
785
786 // getLocalFiles returns raw DB entries (without httpLinks)
787 const files = await this.backend.api.core.getLocalFiles(params.roomId)
788 const entries = Array.isArray(files) ? files : (files?.data || files?.entries || [])
789
790 console.log('[keet-daemon] file.preview: looking for "%s" among %d local files', params.fileId, entries.length)
791 if (entries.length > 0) {
792 const sample = entries[0]
793 const sObj = sample.file || sample
794 console.log('[keet-daemon] sample entry keys:', Object.keys(sObj).join(','),
795 'metadata.name:', sObj?.metadata?.name,
796 'pointer.externalBlob:', sObj?.pointer?.externalBlob ? 'yes' : 'no')
797 }
798
799 // Match file by FILENAME (primary) or blob ID (fallback)
800 const targetId = String(params.fileId)
801 const entry = entries.find(f => {
802 const fileObj = f.file || f
803 // Try filename match first (most reliable — Godot sends filename as fileId)
804 const name = fileObj?.metadata?.name || ''
805 if (name && name === targetId) return true
806 // Fallback: match by blob ID (z32 or hex)
807 const blobId = fileObj?.pointer?.externalBlob?.id
808 if (!blobId) return false
809 const idStr = Buffer.isBuffer(blobId) ? blobId.toString('hex') : String(blobId)
810 return idStr === targetId
811 })
812
813 if (!entry) {
814 const names = entries.slice(0, 5).map(f => (f.file || f)?.metadata?.name || '?').join(', ')
815 throw new Error('File not found: ' + params.fileId + ' (have: ' + names + ')')
816 }
817
818 // Generate httpLink using FileManager's blob server
819 const fileObj = entry.file || entry
820 let httpLink = ''
821 try {
822 httpLink = String(await this.core.files.getFileLink(entry.roomKey, fileObj) || '')
823 } catch (err) {
824 console.error('[keet-daemon] getFileLink error:', err.message)
825 }
826
827 console.log('[keet-daemon] file.preview: found "%s" -> httpLink=%s', params.fileId, (httpLink || '').slice(0, 80))
828
829 const meta = fileObj?.metadata || {}
830 return {
831 name: meta.name || 'file',
832 type: meta.mimetype || '',
833 size: meta.size || 0,
834 httpLink,
835 dimensions: meta.dimensions || null
836 }
837 }
838
839 // --- Advanced Members ---
840
841 async _membersActive (params) {
842 if (!params.roomId) throw new Error('Missing required param: roomId')
843 return this.backend.api.core.getActiveMembers(params.roomId)
844 }
845
846 async _membersMods (params) {
847 if (!params.roomId) throw new Error('Missing required param: roomId')
848 return this.backend.api.core.getModerators(params.roomId)
849 }
850
851 async _membersCount (params) {
852 if (!params.roomId) throw new Error('Missing required param: roomId')
853 return this.backend.api.core.getMemberCount(params.roomId)
854 }
855
856 async _membersConnected (params) {
857 if (!params.roomId) throw new Error('Missing required param: roomId')
858 return this.backend.api.core.getConnectedMembers(params.roomId)
859 }
860
861 async _membersSearch (params) {
862 if (!params.roomId) throw new Error('Missing required param: roomId')
863 if (!params.query) throw new Error('Missing required param: query')
864 return this.backend.api.core.getSearchMembers(params.roomId, params.query)
865 }
866
867 // --- Contacts ---
868
869 async _contactInfo (params) {
870 if (!params.memberId) throw new Error('Missing required param: memberId')
871 return this.backend.api.core.getContactInfo(params.memberId)
872 }
873
874 async _contactSelf () {
875 return this.backend.api.core.getSelfContactInfo()
876 }
877
878 async _dmFromContact (params) {
879 if (!params.memberId) throw new Error('Missing required param: memberId')
880 return this.backend.api.core.sendDMFromContact(params.memberId)
881 }
882
883 // --- Sub-APIs ---
884
885 async _networkQuery () {
886 const api = this.backend.api.network
887 if (!api) throw new Error('Network API not available')
888 return api.query()
889 }
890
891 async _preferencesQuery () {
892 const api = this.backend.api.preferences
893 if (!api) throw new Error('Preferences API not available')
894 return api.query()
895 }
896
897 async _preferencesSet (params) {
898 const api = this.backend.api.preferences
899 if (!api) throw new Error('Preferences API not available')
900 await api.update(params)
901 return { ok: true }
902 }
903
904 async _debugStats (params) {
905 const api = this.backend.api.debug
906 if (!api) throw new Error('Debug API not available')
907 if (!params.roomId) throw new Error('Missing required param: roomId')
908 return api.getCurrentRoomStats(params.roomId)
909 }
910
911 // --- System (extended) ---
912
913 async _getVersion () {
914 return this.backend.api.core.getVersion()
915 }
916
917 async _getStats () {
918 return this.backend.api.core.getStats()
919 }
920}
lib/uds-transport.js +0 -290
……@@ -1,290 +0,0 @@
1import Pipe from 'bare-pipe'
2import FramedStream from 'framed-stream'
3import b4a from 'b4a'
4import fs from 'fs'
5
6export class UdsTransport {
7 constructor (router, opts = {}) {
8 this.router = router
9 this.socketPath = opts.socketPath || '/tmp/keet-daemon.sock'
10 this.server = null
11 this._clients = new Set()
12 }
13
14 start () {
15 return new Promise((resolve, reject) => {
16 // Remove stale socket file
17 try { fs.unlinkSync(this.socketPath) } catch {}
18
19 this.server = Pipe.createServer((rawPipe) => {
20 this._onConnection(rawPipe)
21 })
22
23 this.server.on('error', (err) => {
24 console.error('[keet-daemon] UDS server error:', err.message)
25 reject(err)
26 })
27
28 this.server.listen(this.socketPath, () => {
29 // Owner-only permissions — prevents other users from connecting (SECURITY_AUDIT_V2 D-F6)
30 try { fs.chmodSync(this.socketPath, 0o600) } catch {}
31 console.log(`[keet-daemon] UDS listening on ${this.socketPath} (mode 0600)`)
32 resolve()
33 })
34 })
35 }
36
37 stop () {
38 for (const client of this._clients) {
39 client.destroy()
40 }
41 this._clients.clear()
42 if (this.server) this.server.close()
43 try { fs.unlinkSync(this.socketPath) } catch {}
44 }
45
46 _onConnection (rawPipe) {
47 const framed = new FramedStream(rawPipe)
48 const client = {
49 framed,
50 rawPipe,
51 subscriptions: new Map(), // roomId -> { stream, highSeq, initialized }
52 destroy () {
53 // Clean up all subscriptions
54 for (const [, sub] of this.subscriptions) {
55 if (sub.stream && typeof sub.stream.destroy === 'function') {
56 sub.stream.destroy()
57 }
58 }
59 this.subscriptions.clear()
60 framed.destroy()
61 }
62 }
63
64 this._clients.add(client)
65
66 framed.on('data', (buf) => {
67 this._onFrame(client, buf)
68 })
69
70 framed.on('error', (err) => {
71 if (err.message !== 'Stream destroyed') {
72 console.error('[keet-daemon] UDS client error:', err.message)
73 }
74 })
75
76 framed.on('close', () => {
77 client.destroy()
78 this._clients.delete(client)
79 })
80 }
81
82 async _onFrame (client, buf) {
83 let msg
84 try {
85 msg = JSON.parse(b4a.toString(buf, 'utf8'))
86 } catch {
87 this._sendFrame(client, { id: null, ok: false, error: 'Invalid JSON' })
88 return
89 }
90
91 const { id, method, params } = msg
92
93 if (!method) {
94 this._sendFrame(client, { id, ok: false, error: 'Missing "method"' })
95 return
96 }
97
98 // Handle subscribe/unsubscribe at transport level
99 if (method === 'subscribe') {
100 await this._handleSubscribe(client, id, params || {})
101 return
102 }
103
104 if (method === 'unsubscribe') {
105 this._handleUnsubscribe(client, id, params || {})
106 return
107 }
108
109 try {
110 const result = await this.router.handle(method, params || {})
111 this._sendFrame(client, { id, ...result })
112 } catch (err) {
113 this._sendFrame(client, { id, ok: false, error: err.message || String(err) })
114 }
115 }
116
117 async _handleSubscribe (client, id, params) {
118 const { roomId } = params
119
120 if (!roomId) {
121 this._sendFrame(client, { id, ok: false, error: 'Missing roomId' })
122 return
123 }
124
125 // If already subscribed, unsubscribe first
126 if (client.subscriptions.has(roomId)) {
127 const old = client.subscriptions.get(roomId)
128 if (old.stream && typeof old.stream.destroy === 'function') {
129 old.stream.destroy()
130 }
131 client.subscriptions.delete(roomId)
132 }
133
134 // Resolve member names
135 const nameMap = await this.router._getMemberNames(roomId)
136
137 // Get subscription stream
138 const stream = this.router.subscribe(roomId)
139
140 const sub = { stream, highSeq: -1, initialized: false }
141 client.subscriptions.set(roomId, sub)
142
143 // Confirm subscription
144 this._sendFrame(client, { id, ok: true, data: { subscribed: roomId } })
145
146 if (!stream) return
147
148 if (typeof stream[Symbol.asyncIterator] === 'function') {
149 ;(async () => {
150 try {
151 for await (const snapshot of stream) {
152 if (!snapshot) continue
153 const messages = Array.isArray(snapshot) ? snapshot : [snapshot]
154
155 if (!sub.initialized) {
156 // First snapshot = current state — record high water mark
157 for (const event of messages) {
158 const seq = event.seq ?? event.clock ?? -1
159 if (seq > sub.highSeq) sub.highSeq = seq
160 }
161 sub.initialized = true
162 continue
163 }
164
165 // Emit only newer messages
166 for (const event of messages) {
167 const seq = event.seq ?? event.clock ?? -1
168 if (seq <= sub.highSeq) continue
169 sub.highSeq = seq
170 const msg = normalizeEvent(event, nameMap)
171 if (msg) {
172 this._sendFrame(client, { event: 'message', roomId, data: msg })
173 }
174 }
175 }
176 } catch (err) {
177 if (err.message !== 'Stream destroyed') {
178 this._sendFrame(client, { event: 'error', roomId, data: { error: err.message } })
179 }
180 }
181 })()
182 } else if (stream && typeof stream.on === 'function') {
183 stream.on('data', (snapshot) => {
184 if (!snapshot) return
185 const messages = Array.isArray(snapshot) ? snapshot : [snapshot]
186 if (!sub.initialized) {
187 for (const event of messages) {
188 const seq = event.seq ?? event.clock ?? -1
189 if (seq > sub.highSeq) sub.highSeq = seq
190 }
191 sub.initialized = true
192 return
193 }
194 for (const event of messages) {
195 const seq = event.seq ?? event.clock ?? -1
196 if (seq <= sub.highSeq) continue
197 sub.highSeq = seq
198 const msg = normalizeEvent(event, nameMap)
199 if (msg) {
200 this._sendFrame(client, { event: 'message', roomId, data: msg })
201 }
202 }
203 })
204 stream.on('error', (err) => {
205 this._sendFrame(client, { event: 'error', roomId, data: { error: err.message } })
206 })
207 stream.on('close', () => {
208 client.subscriptions.delete(roomId)
209 })
210 }
211 }
212
213 _handleUnsubscribe (client, id, params) {
214 const { roomId } = params
215
216 if (!roomId) {
217 this._sendFrame(client, { id, ok: false, error: 'Missing roomId' })
218 return
219 }
220
221 const sub = client.subscriptions.get(roomId)
222 if (sub) {
223 if (sub.stream && typeof sub.stream.destroy === 'function') {
224 sub.stream.destroy()
225 }
226 client.subscriptions.delete(roomId)
227 }
228
229 this._sendFrame(client, { id, ok: true, data: { unsubscribed: roomId } })
230 }
231
232 _sendFrame (client, obj) {
233 try {
234 const json = JSON.stringify(obj)
235 client.framed.write(b4a.from(json, 'utf8'))
236 } catch {}
237 }
238}
239
240function normalizeEvent (event, nameMap) {
241 if (!event || typeof event !== 'object') return null
242
243 const text = event.chat?.text || event.text || ''
244 const generic = event.message?.generic || null
245
246 let system = null
247 if (generic && typeof generic === 'object') {
248 for (const v of Object.values(generic)) {
249 if (v !== null && v !== false) { system = generic; break }
250 }
251 } else if (generic) {
252 system = generic
253 }
254
255 // Extract file attachments (encoded file structure from keet-core API)
256 const rawFiles = event.message?.files || event.files || []
257 const files = Array.isArray(rawFiles) ? rawFiles.map(f => {
258 const meta = f.metadata || {}
259 const entry = f.entry || {}
260 const name = meta.name || f.name || f.fileName || 'file'
261 return {
262 id: name,
263 name,
264 type: meta.mimetype || f.mimeType || '',
265 size: meta.size || f.size || 0,
266 dimensions: meta.dimensions || f.dimensions || null,
267 httpLink: entry.httpLink || f.httpLink || '',
268 preview: entry.preview ? {
269 small: entry.preview.small?.httpLink || '',
270 medium: entry.preview.medium?.httpLink || ''
271 } : null
272 }
273 }) : []
274
275 if (!text && !system && files.length === 0) return null
276
277 const memberId = event.memberId || 'unknown'
278
279 return {
280 memberId,
281 name: (nameMap && nameMap[memberId]) || memberId.slice(0, 8),
282 text,
283 timestamp: event.timestamp || Date.now(),
284 deleted: event.deleted || false,
285 system,
286 files: files.length > 0 ? files : null,
287 seq: event.seq ?? event.clock ?? null,
288 id: event.id || null
289 }
290}