@goodandready/dsh-messenger-gateway 0.4.9 → 0.4.11
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/lib/ask.js +3 -1
- package/lib/documents.js +33 -7
- package/lib/file-manager.js +53 -1
- package/lib/http.js +18 -5
- package/lib/index.js +5 -5
- package/lib/locales/index.js +11 -2
- package/lib/messenger-api.js +23 -1
- package/lib/outbound.js +51 -6
- package/lib/personas.js +3 -4
- package/lib/scheduler.js +12 -4
- package/lib/settings-bridge.js +2 -2
- package/lib/storage-atomic.js +17 -0
- package/lib/telegram-errors.js +0 -0
- package/lib/telegram-format.js +23 -2
- package/lib/updater.js +11 -6
- package/package.json +1 -1
package/lib/ask.js
CHANGED
|
@@ -56,7 +56,9 @@ export function buildInlineKeyboard(token, buttons) {
|
|
|
56
56
|
|
|
57
57
|
export function normalizeAskOptions(options = []) {
|
|
58
58
|
if (Array.isArray(options)) {
|
|
59
|
-
const flat =
|
|
59
|
+
const flat = typeof options.flat === 'function'
|
|
60
|
+
? options.flat()
|
|
61
|
+
: options.reduce((acc, item) => acc.concat(item), [])
|
|
60
62
|
return flat.map((opt, i) => {
|
|
61
63
|
if (typeof opt === 'string') return { id: String(i + 1), text: opt, selected: false }
|
|
62
64
|
const id = String(opt?.id ?? opt?.buttonId ?? i + 1).trim()
|
package/lib/documents.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { open } from 'node:fs/promises'
|
|
2
2
|
import { extname } from 'node:path'
|
|
3
3
|
import { inflateRawSync } from 'node:zlib'
|
|
4
4
|
|
|
@@ -132,7 +132,36 @@ export async function parseDocument(filePath, opts = {}) {
|
|
|
132
132
|
const ext = extname(filePath).toLowerCase()
|
|
133
133
|
|
|
134
134
|
try {
|
|
135
|
-
const
|
|
135
|
+
const handle = await open(filePath, 'r')
|
|
136
|
+
let rawBuffer
|
|
137
|
+
let truncated = false
|
|
138
|
+
let originalLength = 0
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const st = await handle.stat()
|
|
142
|
+
originalLength = st.size
|
|
143
|
+
|
|
144
|
+
if (TEXT_INJECT_EXTS.has(ext)) {
|
|
145
|
+
const toRead = Math.min(st.size, maxBytes)
|
|
146
|
+
const buf = Buffer.alloc(toRead)
|
|
147
|
+
const { bytesRead } = await handle.read(buf, 0, toRead, 0)
|
|
148
|
+
rawBuffer = buf.subarray(0, bytesRead)
|
|
149
|
+
truncated = st.size > toRead
|
|
150
|
+
} else if (ext === '.pdf' || ext === '.docx') {
|
|
151
|
+
const maxBinary = Math.max(maxBytes * 4, 10 * 1024 * 1024)
|
|
152
|
+
if (st.size > maxBinary) {
|
|
153
|
+
return { parsed: false, error: `File size (${st.size} bytes) exceeds safety parse limit` }
|
|
154
|
+
}
|
|
155
|
+
const buf = Buffer.alloc(st.size)
|
|
156
|
+
const { bytesRead } = await handle.read(buf, 0, st.size, 0)
|
|
157
|
+
rawBuffer = buf.subarray(0, bytesRead)
|
|
158
|
+
} else {
|
|
159
|
+
return { parsed: false, type: 'unsupported' }
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
await handle.close()
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
let text = ''
|
|
137
166
|
let parsedType = 'text'
|
|
138
167
|
|
|
@@ -145,15 +174,11 @@ export async function parseDocument(filePath, opts = {}) {
|
|
|
145
174
|
} else if (TEXT_INJECT_EXTS.has(ext)) {
|
|
146
175
|
parsedType = ext.slice(1)
|
|
147
176
|
text = rawBuffer.toString('utf8')
|
|
148
|
-
} else {
|
|
149
|
-
return { parsed: false, type: 'unsupported' }
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
text = text.trim()
|
|
153
180
|
if (!text) return { parsed: false, type: parsedType }
|
|
154
181
|
|
|
155
|
-
const originalLength = text.length
|
|
156
|
-
let truncated = false
|
|
157
182
|
if (text.length > maxBytes) {
|
|
158
183
|
text = text.slice(0, maxBytes)
|
|
159
184
|
truncated = true
|
|
@@ -164,9 +189,10 @@ export async function parseDocument(filePath, opts = {}) {
|
|
|
164
189
|
type: parsedType,
|
|
165
190
|
text,
|
|
166
191
|
truncated,
|
|
167
|
-
originalLength,
|
|
192
|
+
originalLength: originalLength || text.length,
|
|
168
193
|
}
|
|
169
194
|
} catch (err) {
|
|
170
195
|
return { parsed: false, error: err.message }
|
|
171
196
|
}
|
|
172
197
|
}
|
|
198
|
+
|
package/lib/file-manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs'
|
|
1
2
|
import { readdir, stat, readFile } from 'node:fs/promises'
|
|
2
|
-
import { resolve, normalize, relative, join, extname, isAbsolute } from 'node:path'
|
|
3
|
+
import { resolve, normalize, relative, join, extname, isAbsolute, sep, dirname } from 'node:path'
|
|
3
4
|
|
|
4
5
|
export function formatFileSize(bytes) {
|
|
5
6
|
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
|
|
@@ -9,6 +10,17 @@ export function formatFileSize(bytes) {
|
|
|
9
10
|
return `${val} ${units[i]}`
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
function isSubpath(parent, child) {
|
|
14
|
+
const normParent = normalize(parent)
|
|
15
|
+
const normChild = normalize(child)
|
|
16
|
+
if (process.platform === 'win32') {
|
|
17
|
+
const p = normParent.toLowerCase()
|
|
18
|
+
const c = normChild.toLowerCase()
|
|
19
|
+
return c === p || c.startsWith(p.endsWith(sep) ? p : p + sep)
|
|
20
|
+
}
|
|
21
|
+
return normChild === normParent || normChild.startsWith(normParent.endsWith(sep) ? normParent : normParent + sep)
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export function resolveSafePath(baseCwd, userPath = '.') {
|
|
13
25
|
const base = resolve(normalize(baseCwd || process.cwd()))
|
|
14
26
|
const cleanUser = String(userPath || '').trim() || '.'
|
|
@@ -21,6 +33,46 @@ export function resolveSafePath(baseCwd, userPath = '.') {
|
|
|
21
33
|
err.code = 'ERR_PATH_TRAVERSAL'
|
|
22
34
|
throw err
|
|
23
35
|
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const realBase = realpathSync(base)
|
|
39
|
+
try {
|
|
40
|
+
const realTarget = realpathSync(target)
|
|
41
|
+
if (!isSubpath(realBase, realTarget)) {
|
|
42
|
+
const err = new Error('Access denied: symlink resolves out of workspace')
|
|
43
|
+
err.code = 'ERR_PATH_TRAVERSAL'
|
|
44
|
+
throw err
|
|
45
|
+
}
|
|
46
|
+
} catch (targetErr) {
|
|
47
|
+
if (targetErr?.code === 'ERR_PATH_TRAVERSAL') throw targetErr
|
|
48
|
+
if (targetErr?.code === 'ENOENT') {
|
|
49
|
+
let curr = dirname(target)
|
|
50
|
+
while (curr === base || isSubpath(base, curr)) {
|
|
51
|
+
try {
|
|
52
|
+
const realCurr = realpathSync(curr)
|
|
53
|
+
if (!isSubpath(realBase, realCurr)) {
|
|
54
|
+
const err = new Error('Access denied: symlink ancestor resolves out of workspace')
|
|
55
|
+
err.code = 'ERR_PATH_TRAVERSAL'
|
|
56
|
+
throw err
|
|
57
|
+
}
|
|
58
|
+
break
|
|
59
|
+
} catch (ancestorErr) {
|
|
60
|
+
if (ancestorErr?.code === 'ERR_PATH_TRAVERSAL') throw ancestorErr
|
|
61
|
+
if (ancestorErr?.code === 'ENOENT') {
|
|
62
|
+
const parent = dirname(curr)
|
|
63
|
+
if (parent === curr) break
|
|
64
|
+
curr = parent
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (baseErr) {
|
|
73
|
+
if (baseErr?.code === 'ERR_PATH_TRAVERSAL') throw baseErr
|
|
74
|
+
}
|
|
75
|
+
|
|
24
76
|
return { base, target, relativePath: rel || '.' }
|
|
25
77
|
}
|
|
26
78
|
|
package/lib/http.js
CHANGED
|
@@ -36,7 +36,7 @@ export function isLoopback(value) {
|
|
|
36
36
|
|| address.startsWith('::ffff:127.')
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function isTrustedSettingsRequest(req) {
|
|
39
|
+
export function isTrustedSettingsRequest(req, expectedSecret) {
|
|
40
40
|
const remote = req?.socket?.remoteAddress
|
|
41
41
|
const host = String(req?.headers?.host || '')
|
|
42
42
|
if (!host) return false
|
|
@@ -44,6 +44,21 @@ export function isTrustedSettingsRequest(req) {
|
|
|
44
44
|
const site = String(req?.headers?.['sec-fetch-site'] || '').toLowerCase()
|
|
45
45
|
if (site === 'cross-site') return false
|
|
46
46
|
|
|
47
|
+
const secret = String(expectedSecret || '').trim()
|
|
48
|
+
if (secret) {
|
|
49
|
+
const authHeader = req?.headers?.authorization || ''
|
|
50
|
+
const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''
|
|
51
|
+
const tokenHeader = req?.headers?.['x-webhook-secret'] || ''
|
|
52
|
+
const provided = bearer || tokenHeader
|
|
53
|
+
if (provided && timingSafeCompare(provided, secret)) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (remote !== undefined && !isLoopback(remote)) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
const origin = String(req?.headers?.origin || '')
|
|
48
63
|
if (origin) {
|
|
49
64
|
try {
|
|
@@ -67,7 +82,6 @@ export function isTrustedSettingsRequest(req) {
|
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
if (site === 'same-origin') {
|
|
70
|
-
if (remote !== undefined && !isLoopback(remote)) return false
|
|
71
85
|
return true
|
|
72
86
|
}
|
|
73
87
|
|
|
@@ -78,10 +92,9 @@ export function isAuthorizedMessengerRequest(req, webhooksConfig) {
|
|
|
78
92
|
const site = String(req?.headers?.['sec-fetch-site'] || '').toLowerCase()
|
|
79
93
|
if (site === 'cross-site') return false
|
|
80
94
|
|
|
81
|
-
if (isTrustedSettingsRequest(req)) return true
|
|
82
|
-
if (isLoopback(req?.socket?.remoteAddress)) return true
|
|
83
|
-
|
|
84
95
|
const secret = String(webhooksConfig?.secret || '').trim()
|
|
96
|
+
if (isTrustedSettingsRequest(req, secret)) return true
|
|
97
|
+
if (isLoopback(req?.socket?.remoteAddress)) return true
|
|
85
98
|
if (secret) {
|
|
86
99
|
const authHeader = req?.headers?.authorization || ''
|
|
87
100
|
const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''
|
package/lib/index.js
CHANGED
|
@@ -380,7 +380,7 @@ export function apply(ctx, config) {
|
|
|
380
380
|
kind: 'exact', path: '/dsh-messenger-gateway/smoke',
|
|
381
381
|
handler: async (req, res) => {
|
|
382
382
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
383
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
383
|
+
if (!isTrustedSettingsRequest(req, source().webhooks?.secret)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
384
384
|
const gw = getGw()
|
|
385
385
|
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
386
386
|
const parsed = await readJsonSafe(req, res, 65536)
|
|
@@ -457,7 +457,7 @@ export function apply(ctx, config) {
|
|
|
457
457
|
handler: async (req, res) => {
|
|
458
458
|
if (req.method === 'GET') return writeJson(res, 200, { ok: true, config: publicConfig(source()) })
|
|
459
459
|
if (req.method !== 'PUT') return writeJson(res, 405, { ok: false, error: 'GET or PUT' })
|
|
460
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
460
|
+
if (!isTrustedSettingsRequest(req, source().webhooks?.secret)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
461
461
|
if (!settingsApi?.isReady?.()) return writeJson(res, 503, { ok: false, error: 'settings not ready' })
|
|
462
462
|
const parsedConfig = await readJsonSafe(req, res, 1_048_576)
|
|
463
463
|
if (!parsedConfig.ok) return
|
|
@@ -499,7 +499,7 @@ export function apply(ctx, config) {
|
|
|
499
499
|
kind: 'exact', path: '/dsh-messenger-gateway/pairing',
|
|
500
500
|
handler: async (req, res) => {
|
|
501
501
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
502
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
502
|
+
if (!isTrustedSettingsRequest(req, source().webhooks?.secret)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
503
503
|
const gw = getGw()
|
|
504
504
|
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
505
505
|
writeJson(res, 200, {
|
|
@@ -514,7 +514,7 @@ export function apply(ctx, config) {
|
|
|
514
514
|
kind: 'exact', path: '/dsh-messenger-gateway/pairing/approve',
|
|
515
515
|
handler: async (req, res) => {
|
|
516
516
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
517
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
517
|
+
if (!isTrustedSettingsRequest(req, source().webhooks?.secret)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
518
518
|
const gw = getGw()
|
|
519
519
|
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
520
520
|
const parsed = await readJsonSafe(req, res, 65536)
|
|
@@ -528,7 +528,7 @@ export function apply(ctx, config) {
|
|
|
528
528
|
kind: 'exact', path: '/dsh-messenger-gateway/pairing/reject',
|
|
529
529
|
handler: async (req, res) => {
|
|
530
530
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
531
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
531
|
+
if (!isTrustedSettingsRequest(req, source().webhooks?.secret)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
532
532
|
const gw = getGw()
|
|
533
533
|
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
534
534
|
const parsed = await readJsonSafe(req, res, 65536)
|
package/lib/locales/index.js
CHANGED
|
@@ -9,11 +9,20 @@ export function getLocaleDictionary(locale = 'en') {
|
|
|
9
9
|
return en
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function
|
|
12
|
+
export function escapeHtml(text) {
|
|
13
|
+
return String(text ?? '')
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function t(key, params = {}, locale = 'en', options = {}) {
|
|
13
20
|
const dict = getLocaleDictionary(locale)
|
|
14
21
|
let template = dict[key] || en[key] || key
|
|
22
|
+
const escape = options.escape !== false
|
|
15
23
|
for (const [k, v] of Object.entries(params)) {
|
|
16
|
-
|
|
24
|
+
const val = escape ? escapeHtml(v) : String(v)
|
|
25
|
+
template = template.replaceAll(`{${k}}`, val)
|
|
17
26
|
}
|
|
18
27
|
return template
|
|
19
28
|
}
|
package/lib/messenger-api.js
CHANGED
|
@@ -110,7 +110,29 @@ export function normalizeFiles(files) {
|
|
|
110
110
|
err.status = 400
|
|
111
111
|
throw err
|
|
112
112
|
}
|
|
113
|
-
if (file.bytes)
|
|
113
|
+
if (file.bytes !== undefined) {
|
|
114
|
+
let buf = null
|
|
115
|
+
if (Buffer.isBuffer(file.bytes) || file.bytes instanceof Uint8Array) {
|
|
116
|
+
buf = Buffer.from(file.bytes)
|
|
117
|
+
} else if (Array.isArray(file.bytes)) {
|
|
118
|
+
buf = Buffer.from(file.bytes)
|
|
119
|
+
} else if (file.bytes && typeof file.bytes === 'object' && file.bytes.type === 'Buffer' && Array.isArray(file.bytes.data)) {
|
|
120
|
+
buf = Buffer.from(file.bytes.data)
|
|
121
|
+
} else if (typeof file.bytes === 'string') {
|
|
122
|
+
buf = Buffer.from(file.bytes, 'utf8')
|
|
123
|
+
}
|
|
124
|
+
if (!buf) {
|
|
125
|
+
const err = new Error(`files[${i}].bytes must be a Buffer, Uint8Array, or array of bytes`)
|
|
126
|
+
err.status = 400
|
|
127
|
+
throw err
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
bytes: buf,
|
|
131
|
+
mime: file.mime || 'application/octet-stream',
|
|
132
|
+
kind: file.kind || 'document',
|
|
133
|
+
name: file.name || 'file',
|
|
134
|
+
}
|
|
135
|
+
}
|
|
114
136
|
if (typeof file.dataBase64 === 'string') {
|
|
115
137
|
return {
|
|
116
138
|
bytes: Buffer.from(file.dataBase64, 'base64'),
|
package/lib/outbound.js
CHANGED
|
@@ -72,12 +72,54 @@ export async function readAttachmentBytes(ctx, ref) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export async function fetchInternalImage(baseUrl, path, signal) {
|
|
75
|
+
export async function fetchInternalImage(baseUrl, path, signal, opts = {}) {
|
|
76
|
+
const { timeoutMs = 15_000, maxBytes = 20 * 1024 * 1024 } = opts
|
|
77
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
78
|
+
const effectiveSignal = signal
|
|
79
|
+
? (typeof AbortSignal.any === 'function' ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal)
|
|
80
|
+
: timeoutSignal
|
|
81
|
+
|
|
76
82
|
const url = new URL(path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`)
|
|
77
|
-
const res = await fetch(url, { signal })
|
|
83
|
+
const res = await fetch(url, { signal: effectiveSignal })
|
|
78
84
|
if (!res.ok) throw new Error(`image fetch HTTP ${res.status}`)
|
|
79
|
-
|
|
80
|
-
const
|
|
85
|
+
|
|
86
|
+
const clHeader = res.headers?.get?.('content-length')
|
|
87
|
+
const cl = Number(clHeader)
|
|
88
|
+
if (Number.isFinite(cl) && cl > maxBytes) {
|
|
89
|
+
throw new Error(`image size (${cl} bytes) exceeds limit of ${maxBytes} bytes`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let bytes
|
|
93
|
+
if (res.body && typeof res.body.getReader === 'function') {
|
|
94
|
+
const reader = res.body.getReader()
|
|
95
|
+
const chunks = []
|
|
96
|
+
let total = 0
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read()
|
|
99
|
+
if (done) break
|
|
100
|
+
total += value.length
|
|
101
|
+
if (total > maxBytes) {
|
|
102
|
+
reader.cancel().catch(() => {})
|
|
103
|
+
throw new Error(`image size exceeds limit of ${maxBytes} bytes`)
|
|
104
|
+
}
|
|
105
|
+
chunks.push(value)
|
|
106
|
+
}
|
|
107
|
+
const combined = new Uint8Array(total)
|
|
108
|
+
let offset = 0
|
|
109
|
+
for (const chunk of chunks) {
|
|
110
|
+
combined.set(chunk, offset)
|
|
111
|
+
offset += chunk.length
|
|
112
|
+
}
|
|
113
|
+
bytes = combined
|
|
114
|
+
} else {
|
|
115
|
+
const ab = await res.arrayBuffer()
|
|
116
|
+
if (ab.byteLength > maxBytes) {
|
|
117
|
+
throw new Error(`image size exceeds limit of ${maxBytes} bytes`)
|
|
118
|
+
}
|
|
119
|
+
bytes = new Uint8Array(ab)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const mime = res.headers?.get?.('content-type') || 'image/png'
|
|
81
123
|
const fromQuery = url.searchParams.get('id') || url.searchParams.get('name')
|
|
82
124
|
const name = fromQuery ? `image-${fromQuery.slice(0, 32)}.png` : 'image.png'
|
|
83
125
|
return { bytes, mime, kind: 'photo', name }
|
|
@@ -87,7 +129,7 @@ const DEFAULT_MAX_OUTBOUND_FILES = 10
|
|
|
87
129
|
|
|
88
130
|
/** Build Telegram-ready files from assistant image blocks and inline plugin URLs. */
|
|
89
131
|
export async function buildOutboundFiles(ctx, baseUrl, collector, opts = {}) {
|
|
90
|
-
const { signal, logger, maxFiles = DEFAULT_MAX_OUTBOUND_FILES } = opts
|
|
132
|
+
const { signal, logger, maxFiles = DEFAULT_MAX_OUTBOUND_FILES, maxImageBytes, imageTimeoutMs } = opts
|
|
91
133
|
const files = []
|
|
92
134
|
const refs = dedupeAttachmentRefs(collector.images)
|
|
93
135
|
for (const ref of refs) {
|
|
@@ -104,7 +146,10 @@ export async function buildOutboundFiles(ctx, baseUrl, collector, opts = {}) {
|
|
|
104
146
|
if (files.length >= maxFiles) break
|
|
105
147
|
if (signal?.aborted) break
|
|
106
148
|
try {
|
|
107
|
-
files.push(await fetchInternalImage(baseUrl, path, signal
|
|
149
|
+
files.push(await fetchInternalImage(baseUrl, path, signal, {
|
|
150
|
+
maxBytes: maxImageBytes,
|
|
151
|
+
timeoutMs: imageTimeoutMs,
|
|
152
|
+
}))
|
|
108
153
|
} catch (e) {
|
|
109
154
|
logger?.warn?.(`outbound url image: ${e.message}`)
|
|
110
155
|
}
|
package/lib/personas.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { writeJsonAtomicSync } from './storage-atomic.js'
|
|
3
3
|
|
|
4
4
|
export const BUILTIN_PERSONAS = {
|
|
5
5
|
default: {
|
|
@@ -87,8 +87,7 @@ export function createPersonaStore(filePath, { logger } = {}) {
|
|
|
87
87
|
|
|
88
88
|
function save() {
|
|
89
89
|
try {
|
|
90
|
-
|
|
91
|
-
writeFileSync(filePath, JSON.stringify(cache, null, 2), 'utf8')
|
|
90
|
+
writeJsonAtomicSync(filePath, cache)
|
|
92
91
|
} catch (err) {
|
|
93
92
|
logger?.warn?.('[dsh-messenger-gateway] personas save error:', err?.message || err)
|
|
94
93
|
}
|
package/lib/scheduler.js
CHANGED
|
@@ -42,10 +42,16 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
42
42
|
async function load() {
|
|
43
43
|
try {
|
|
44
44
|
const raw = await readFile(filePath, 'utf8')
|
|
45
|
-
|
|
45
|
+
const parsed = JSON.parse(raw)
|
|
46
|
+
if (Array.isArray(parsed)) {
|
|
47
|
+
tasks = parsed
|
|
48
|
+
} else {
|
|
49
|
+
logger?.warn?.(`[dsh-messenger-gateway] scheduler invalid tasks payload in ${filePath}: expected array, got ${typeof parsed}; resetting to empty list`)
|
|
50
|
+
tasks = []
|
|
51
|
+
}
|
|
46
52
|
} catch (err) {
|
|
47
53
|
if (err?.code !== 'ENOENT') {
|
|
48
|
-
logger?.
|
|
54
|
+
logger?.warn?.('[dsh-messenger-gateway] scheduler load error:', err?.message || err)
|
|
49
55
|
}
|
|
50
56
|
tasks = []
|
|
51
57
|
}
|
|
@@ -56,6 +62,7 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
56
62
|
|
|
57
63
|
async function save() {
|
|
58
64
|
if (!filePath) return
|
|
65
|
+
if (!Array.isArray(tasks)) tasks = []
|
|
59
66
|
try {
|
|
60
67
|
const now = Date.now()
|
|
61
68
|
tasks = tasks.filter((t) => {
|
|
@@ -71,6 +78,7 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
71
78
|
|
|
72
79
|
async function checkDue() {
|
|
73
80
|
if (!loaded) await load()
|
|
81
|
+
if (!Array.isArray(tasks)) tasks = []
|
|
74
82
|
const now = Date.now()
|
|
75
83
|
const due = tasks.filter((t) => t.status === 'pending' && t.dueAt <= now)
|
|
76
84
|
|
|
@@ -99,10 +107,10 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
99
107
|
function start(intervalMs = 5000) {
|
|
100
108
|
if (timer) clearInterval(timer)
|
|
101
109
|
timer = setInterval(() => {
|
|
102
|
-
checkDue().catch((err) => { logger?.
|
|
110
|
+
checkDue().catch((err) => { logger?.warn?.('[dsh-messenger-gateway] scheduler checkDue error:', err?.message || err) })
|
|
103
111
|
}, intervalMs)
|
|
104
112
|
timer.unref?.()
|
|
105
|
-
checkDue().catch((err) => { logger?.
|
|
113
|
+
checkDue().catch((err) => { logger?.warn?.('[dsh-messenger-gateway] scheduler checkDue error:', err?.message || err) })
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
function stop() {
|
package/lib/settings-bridge.js
CHANGED
|
@@ -78,7 +78,7 @@ export function setupSettings(ctx, { namespace, Config, resolveConfig, baseConfi
|
|
|
78
78
|
source = () => currentConfig
|
|
79
79
|
|
|
80
80
|
const persist = async (next) => {
|
|
81
|
-
const payload =
|
|
81
|
+
const payload = resolveConfig(next)
|
|
82
82
|
if (typeof service.replace === 'function') {
|
|
83
83
|
await service.replace(namespace, payload, getRevision())
|
|
84
84
|
} else if (typeof service.update === 'function') {
|
|
@@ -88,7 +88,7 @@ export function setupSettings(ctx, { namespace, Config, resolveConfig, baseConfi
|
|
|
88
88
|
} else {
|
|
89
89
|
throw new Error('settings service cannot persist configuration')
|
|
90
90
|
}
|
|
91
|
-
currentConfig =
|
|
91
|
+
currentConfig = payload
|
|
92
92
|
sync()
|
|
93
93
|
for (const cb of watchers) {
|
|
94
94
|
try { cb(currentConfig) } catch (err) { logger?.debug?.(`${namespace}: watcher error:`, err?.message || err) }
|
package/lib/storage-atomic.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeFile, rename, mkdir, unlink } from 'node:fs/promises'
|
|
2
|
+
import { writeFileSync, renameSync, mkdirSync, unlinkSync } from 'node:fs'
|
|
2
3
|
import { dirname } from 'node:path'
|
|
3
4
|
import { randomUUID } from 'node:crypto'
|
|
4
5
|
|
|
@@ -18,4 +19,20 @@ export async function writeJsonAtomic(filePath, data) {
|
|
|
18
19
|
await unlink(tmpPath).catch(() => {})
|
|
19
20
|
throw err
|
|
20
21
|
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Synchronous atomic JSON write via temporary file and rename.
|
|
26
|
+
*/
|
|
27
|
+
export function writeJsonAtomicSync(filePath, data) {
|
|
28
|
+
const dir = dirname(filePath)
|
|
29
|
+
mkdirSync(dir, { recursive: true })
|
|
30
|
+
const tmpPath = `${filePath}.${randomUUID().slice(0, 8)}.tmp`
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8')
|
|
33
|
+
renameSync(tmpPath, filePath)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
try { unlinkSync(tmpPath) } catch { /* ignore */ }
|
|
36
|
+
throw err
|
|
37
|
+
}
|
|
21
38
|
}
|
package/lib/telegram-errors.js
CHANGED
|
File without changes
|
package/lib/telegram-format.js
CHANGED
|
@@ -7,12 +7,33 @@ export function escapeHtml(text) {
|
|
|
7
7
|
|
|
8
8
|
const PH = '\uE000'
|
|
9
9
|
|
|
10
|
+
const SAFE_SCHEMES = new Set(['https:', 'http:', 'tg:'])
|
|
11
|
+
|
|
12
|
+
export function isSafeUrl(rawUrl) {
|
|
13
|
+
if (!rawUrl || typeof rawUrl !== 'string') return false
|
|
14
|
+
const clean = rawUrl.replace(/&/g, '&').trim()
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(clean)
|
|
17
|
+
return SAFE_SCHEMES.has(parsed.protocol.toLowerCase())
|
|
18
|
+
} catch {
|
|
19
|
+
if (/^tg:\/\//i.test(clean)) return true
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatSafeLink(label, url) {
|
|
25
|
+
if (!isSafeUrl(url)) {
|
|
26
|
+
return `${label} (${url})`
|
|
27
|
+
}
|
|
28
|
+
return `<a href="${url.replace(/"/g, '%22')}">${label}</a>`
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
// Render inline markdown (bold/italic/code/links) inside already-HTML-escaped text.
|
|
11
32
|
function renderInline(t) {
|
|
12
33
|
let s = escapeHtml(t)
|
|
13
34
|
s = s.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
14
35
|
s = s.replace(/`([^`\n]+)`/g, '<code>$1</code>')
|
|
15
|
-
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) =>
|
|
36
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => formatSafeLink(label, url))
|
|
16
37
|
s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<i>$2</i>')
|
|
17
38
|
return s
|
|
18
39
|
}
|
|
@@ -123,7 +144,7 @@ export function markdownToTelegramHtml(markdown) {
|
|
|
123
144
|
text = convertDetails(text, stash)
|
|
124
145
|
text = convertTaskLists(text, stash)
|
|
125
146
|
text = escapeHtml(text)
|
|
126
|
-
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) =>
|
|
147
|
+
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => formatSafeLink(label, url))
|
|
127
148
|
text = text.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
128
149
|
text = text.replace(/__([^_\n]+)__/g, '<b>$1</b>')
|
|
129
150
|
text = text.replace(/~~([^~\n]+)~~/g, '<s>$1</s>')
|
package/lib/updater.js
CHANGED
|
@@ -181,15 +181,20 @@ export async function getUpdateStatus(options, target = runtime()) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
export function buildUpdateArgs(target, packageSpec, options = {}) {
|
|
185
|
+
return [
|
|
186
|
+
target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
|
|
187
|
+
packageSpec,
|
|
188
|
+
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
|
|
184
192
|
export async function installExact(target, packageSpec, options = {}) {
|
|
185
193
|
if (target.cliEntry === undefined) throw new Error('Automatic update CLI entry is unavailable in this runtime.')
|
|
186
194
|
return new Promise((resolvePromise, reject) => {
|
|
187
|
-
const args =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
191
|
-
]
|
|
192
|
-
const child = spawn(process.execPath, args, {
|
|
195
|
+
const args = buildUpdateArgs(target, packageSpec, options)
|
|
196
|
+
const spawnFn = options.spawn ?? spawn
|
|
197
|
+
const child = spawnFn(process.execPath, args, {
|
|
193
198
|
cwd: target.profileDir,
|
|
194
199
|
windowsHide: true,
|
|
195
200
|
stdio: ['ignore', 'pipe', 'pipe'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-messenger-gateway",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "Telegram messenger bridge for DeepSeek Harness: sessions, steer, homes, inline asks, notify bridge, and optional TTS voice notes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|