@goodandready/dsh-tts 0.4.34 → 0.4.35
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/CHANGELOG.md +9 -0
- package/lib/http-util.js +95 -34
- package/lib/routes.js +49 -50
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.35 (2026-10-20)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Authentication Guard for Administrative and Credential Routes** (#210):
|
|
7
|
+
- Enforced DSH core connection authentication (`connection.requestRejection(req)`) across all `/dsh-tts/*` HTTP endpoints.
|
|
8
|
+
- Fail-closed: Remote LAN clients without an authenticated operator session or token are rejected with 401/403 and cannot manage credentials or mutate plugin configuration (`PUT/DELETE /dsh-tts/credential`, `PUT /dsh-tts/config`, `/dsh-tts/models/install`, `/dsh-tts/models/delete`, `/dsh-tts/cache/clear`).
|
|
9
|
+
- Permitted authenticated operators via DSH connection service to manage provider credentials and configuration from LAN or remote interfaces.
|
|
10
|
+
- Added helpers `getClientIp(req)`, `isLanAddress(address)`, and `getRejectionStatus(req, options)` with comprehensive unit test coverage.
|
|
11
|
+
|
|
3
12
|
## 0.4.34 (2026-10-17)
|
|
4
13
|
|
|
5
14
|
- **Per-Reply Speak Action** (#208, GitHub PR #2):
|
package/lib/http-util.js
CHANGED
|
@@ -4,7 +4,7 @@ export function writeJson(res, code, body) {
|
|
|
4
4
|
try {
|
|
5
5
|
res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
|
|
6
6
|
res.end(JSON.stringify(body))
|
|
7
|
-
} catch { /* socket closed */ }
|
|
7
|
+
} catch (_err) { /* socket closed */ }
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function readBody(req, maxBytes) {
|
|
@@ -23,15 +23,36 @@ export function readBody(req, maxBytes) {
|
|
|
23
23
|
|
|
24
24
|
export function isLoopback(address) {
|
|
25
25
|
if (!address || typeof address !== 'string') return false
|
|
26
|
+
const clean = address.toLowerCase().replace(/^\[|\]$/g, '')
|
|
26
27
|
return (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
clean === '127.0.0.1' ||
|
|
29
|
+
clean === '::1' ||
|
|
30
|
+
clean === '::ffff:127.0.0.1' ||
|
|
31
|
+
clean.startsWith('127.') ||
|
|
32
|
+
clean === 'localhost'
|
|
32
33
|
)
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export function isLanAddress(address) {
|
|
37
|
+
if (!address || typeof address !== 'string') return false
|
|
38
|
+
const clean = address.toLowerCase().replace(/^\[|\]$/g, '')
|
|
39
|
+
const ipv4 = clean.startsWith('::ffff:') ? clean.slice(7) : clean
|
|
40
|
+
return (
|
|
41
|
+
ipv4.startsWith('192.168.') ||
|
|
42
|
+
ipv4.startsWith('10.') ||
|
|
43
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ipv4)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getClientIp(request) {
|
|
48
|
+
if (!request) return ''
|
|
49
|
+
const remote = request.socket?.remoteAddress || request.connection?.remoteAddress
|
|
50
|
+
if (remote) {
|
|
51
|
+
return remote.startsWith('::ffff:') ? remote.slice(7) : remote
|
|
52
|
+
}
|
|
53
|
+
return ''
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
function parseCookies(cookieHeader) {
|
|
36
57
|
if (!cookieHeader || typeof cookieHeader !== 'string') return {}
|
|
37
58
|
const out = {}
|
|
@@ -47,66 +68,106 @@ function parseCookies(cookieHeader) {
|
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
71
|
+
* Returns undefined if the request is permitted, or an HTTP error status code (401, 403) if rejected.
|
|
72
|
+
*
|
|
73
|
+
* Evaluation order:
|
|
74
|
+
* 1. Base validation (fail-closed on missing headers or cross-site Sec-Fetch-Site).
|
|
75
|
+
* 2. Origin vs Host verification.
|
|
76
|
+
* 3. DSH connection service authentication (connection.requestRejection(request)).
|
|
77
|
+
* 4. Fallback authentication when connection service is absent (env token, loopback, etc.).
|
|
78
|
+
*
|
|
79
|
+
* @param {object} request Node HTTP request
|
|
80
|
+
* @param {object} [options]
|
|
81
|
+
* @param {object} [options.connection] DSH connection service
|
|
82
|
+
* @param {boolean} [options.requireAuth] require authenticated operator (fail-closed for remote)
|
|
83
|
+
* @param {boolean} [options.isRemote] mark in-memory/mock request as remote
|
|
84
|
+
* @returns {number|undefined} undefined if allowed, 401 or 403 if rejected
|
|
53
85
|
*/
|
|
54
|
-
export function
|
|
55
|
-
if (!request || !request.headers) return
|
|
86
|
+
export function getRejectionStatus(request, options = {}) {
|
|
87
|
+
if (!request || !request.headers) return 403
|
|
56
88
|
|
|
57
|
-
// Fail-closed: unconditionally reject cross-site and non-same-origin Sec-Fetch-Site (e.g. same-site, cross-site)
|
|
89
|
+
// 1. Fail-closed: unconditionally reject cross-site and non-same-origin Sec-Fetch-Site (e.g. same-site, cross-site)
|
|
58
90
|
const secFetch = request.headers['sec-fetch-site']
|
|
59
|
-
if (secFetch && secFetch !== 'same-origin') {
|
|
60
|
-
return
|
|
91
|
+
if (secFetch && secFetch !== 'same-origin' && secFetch !== 'none') {
|
|
92
|
+
return 403
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
// If origin is present, ensure it matches request host
|
|
95
|
+
// 2. If origin is present, ensure it matches request host
|
|
64
96
|
const origin = request.headers['origin']
|
|
65
97
|
const host = request.headers['host']
|
|
66
98
|
if (origin && host) {
|
|
67
99
|
try {
|
|
68
100
|
const originHost = new URL(origin).host
|
|
69
|
-
if (originHost !== host) return
|
|
70
|
-
} catch {
|
|
71
|
-
return
|
|
101
|
+
if (originHost !== host) return 403
|
|
102
|
+
} catch (_err) {
|
|
103
|
+
return 403
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. If DSH connection service is available, invoke requestRejection(req)
|
|
108
|
+
const conn = options.connection
|
|
109
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
110
|
+
const rejection = conn.requestRejection(request)
|
|
111
|
+
if (rejection !== undefined && rejection !== null && rejection !== false) {
|
|
112
|
+
return typeof rejection === 'number' ? rejection : 401
|
|
72
113
|
}
|
|
114
|
+
// Admitted by DSH connection service
|
|
115
|
+
return undefined
|
|
73
116
|
}
|
|
74
117
|
|
|
75
|
-
// Verify explicit authorization token against DSH environment tokens if defined
|
|
118
|
+
// 4. Verify explicit authorization token against DSH environment tokens if defined
|
|
76
119
|
const expectedToken = process.env.DSH_AUTH_TOKEN || process.env.DSH_TOKEN
|
|
77
120
|
const rawAuth = request.headers['authorization'] || request.headers['x-dsh-auth']
|
|
78
121
|
const cookieHeader = request.headers['cookie']
|
|
79
122
|
|
|
80
123
|
if (expectedToken) {
|
|
124
|
+
let token = ''
|
|
81
125
|
if (typeof rawAuth === 'string') {
|
|
82
|
-
|
|
83
|
-
if (token && token === expectedToken) return true
|
|
126
|
+
token = rawAuth.startsWith('Bearer ') ? rawAuth.slice(7).trim() : rawAuth.trim()
|
|
84
127
|
}
|
|
85
|
-
if (typeof cookieHeader === 'string') {
|
|
128
|
+
if (!token && typeof cookieHeader === 'string') {
|
|
86
129
|
const cookies = parseCookies(cookieHeader)
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
token = cookies['token'] || cookies['dsh_token'] || ''
|
|
131
|
+
}
|
|
132
|
+
if (token && token === expectedToken) {
|
|
133
|
+
return undefined
|
|
89
134
|
}
|
|
135
|
+
return 401
|
|
90
136
|
}
|
|
91
137
|
|
|
92
|
-
// Remote IP
|
|
93
|
-
const remoteAddr = request
|
|
138
|
+
// 5. Remote IP check
|
|
139
|
+
const remoteAddr = getClientIp(request)
|
|
94
140
|
if (remoteAddr) {
|
|
95
|
-
|
|
141
|
+
if (isLoopback(remoteAddr)) {
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
// Remote client (LAN or WAN) without connection auth or token:
|
|
145
|
+
// Fail-closed. For write / sensitive endpoints requireAuth returns 401, otherwise 403.
|
|
146
|
+
return options.requireAuth ? 401 : 403
|
|
96
147
|
}
|
|
97
148
|
|
|
98
|
-
// Internal/mock caller without socket:
|
|
149
|
+
// 6. Internal/mock caller without socket:
|
|
150
|
+
if (options.requireAuth && options.isRemote) {
|
|
151
|
+
return 401
|
|
152
|
+
}
|
|
99
153
|
if (secFetch === 'same-origin') {
|
|
100
|
-
return
|
|
154
|
+
return undefined
|
|
101
155
|
}
|
|
102
|
-
|
|
103
156
|
if (origin && host) {
|
|
104
157
|
try {
|
|
105
|
-
|
|
106
|
-
} catch {
|
|
107
|
-
return
|
|
158
|
+
if (new URL(origin).host === host) return undefined
|
|
159
|
+
} catch (_err) {
|
|
160
|
+
return 403
|
|
108
161
|
}
|
|
109
162
|
}
|
|
110
163
|
|
|
111
|
-
return
|
|
164
|
+
return 403
|
|
112
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Strict fail-closed check for administrative/settings requests.
|
|
169
|
+
* Backward-compatible boolean wrapper around getRejectionStatus.
|
|
170
|
+
*/
|
|
171
|
+
export function isTrustedSettingsRequest(request, options = {}) {
|
|
172
|
+
return getRejectionStatus(request, options) === undefined
|
|
173
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MODEL_SPECS } from './engines/manager.js'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import { registerPluginUpdater } from './updater.js'
|
|
4
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './http-util.js'
|
|
4
|
+
import { writeJson, readBody, getRejectionStatus, isTrustedSettingsRequest } from './http-util.js'
|
|
5
5
|
import { addStreamSubscriber, removeStreamSubscriber, streamSubscriberCount } from './stream-hub.js'
|
|
6
6
|
import { pendingKeyWrites, stripSecretsFromConfig, needsApiKey } from './keys.js'
|
|
7
7
|
import { PROVIDER_KEYS } from './providers.js'
|
|
@@ -29,15 +29,43 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
29
29
|
validateConfig,
|
|
30
30
|
breakerSnapshot,
|
|
31
31
|
} = api
|
|
32
|
+
|
|
33
|
+
const resolveConnection = () => {
|
|
34
|
+
try {
|
|
35
|
+
if (api && api.connection) return api.connection
|
|
36
|
+
} catch (_err) { /* connection resolution fallback */ }
|
|
37
|
+
try {
|
|
38
|
+
if (ctx && typeof ctx.get === 'function') {
|
|
39
|
+
const c = ctx.get('connection')
|
|
40
|
+
if (c) return c
|
|
41
|
+
}
|
|
42
|
+
} catch (_err) { /* connection get fallback */ }
|
|
43
|
+
try {
|
|
44
|
+
if (ctx) return Reflect.get(ctx, 'connection') || ctx.connection
|
|
45
|
+
} catch (_err) { /* connection reflect fallback */ }
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rejectIfUntrusted = (req, res, extraOptions = {}) => {
|
|
50
|
+
const conn = resolveConnection()
|
|
51
|
+
const status = getRejectionStatus(req, { connection: conn, ...extraOptions })
|
|
52
|
+
if (status !== undefined) {
|
|
53
|
+
const code = status === 401 ? 'unauthorized' : 'forbidden'
|
|
54
|
+
const message = status === 401
|
|
55
|
+
? 'unauthorized: session or token required'
|
|
56
|
+
: (extraOptions.message || 'same-origin only')
|
|
57
|
+
writeJson(res, status, { ok: false, error: { code, message } })
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
ctx.effect(() => ctx.webServer.register({
|
|
33
64
|
kind: 'exact',
|
|
34
65
|
path: '/dsh-tts/status',
|
|
35
66
|
handler: async (req, res) => {
|
|
36
67
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
37
|
-
if (
|
|
38
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
39
|
-
return
|
|
40
|
-
}
|
|
68
|
+
if (rejectIfUntrusted(req, res)) return
|
|
41
69
|
const cfg = live()
|
|
42
70
|
writeJson(res, 200, {
|
|
43
71
|
ok: true,
|
|
@@ -67,7 +95,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
67
95
|
path: '/dsh-tts/stream',
|
|
68
96
|
handler: async (req, res) => {
|
|
69
97
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
70
|
-
if (
|
|
98
|
+
if (rejectIfUntrusted(req, res)) return
|
|
71
99
|
try {
|
|
72
100
|
res.writeHead(200, {
|
|
73
101
|
'Content-Type': 'text/event-stream',
|
|
@@ -88,7 +116,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
88
116
|
path: '/dsh-tts/models/status',
|
|
89
117
|
handler: async (req, res) => {
|
|
90
118
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
91
|
-
if (
|
|
119
|
+
if (rejectIfUntrusted(req, res)) return
|
|
92
120
|
try {
|
|
93
121
|
const list = await modelManager.listStatus()
|
|
94
122
|
writeJson(res, 200, { ok: true, models: list })
|
|
@@ -103,10 +131,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
103
131
|
path: '/dsh-tts/models/install',
|
|
104
132
|
handler: async (req, res) => {
|
|
105
133
|
if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
|
|
106
|
-
if (
|
|
107
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
108
|
-
return
|
|
109
|
-
}
|
|
134
|
+
if (rejectIfUntrusted(req, res, { requireAuth: true })) return
|
|
110
135
|
let body = {}
|
|
111
136
|
try {
|
|
112
137
|
const raw = await readBody(req, 16 * 1024)
|
|
@@ -147,10 +172,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
147
172
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'DELETE or POST only' } })
|
|
148
173
|
return
|
|
149
174
|
}
|
|
150
|
-
if (
|
|
151
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
152
|
-
return
|
|
153
|
-
}
|
|
175
|
+
if (rejectIfUntrusted(req, res, { requireAuth: true })) return
|
|
154
176
|
let body = {}
|
|
155
177
|
try {
|
|
156
178
|
const raw = await readBody(req, 16 * 1024)
|
|
@@ -179,10 +201,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
179
201
|
path: '/dsh-tts/integrations',
|
|
180
202
|
handler: async (req, res) => {
|
|
181
203
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
182
|
-
if (
|
|
183
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
184
|
-
return
|
|
185
|
-
}
|
|
204
|
+
if (rejectIfUntrusted(req, res)) return
|
|
186
205
|
function isPluginOrServicePresent(id) {
|
|
187
206
|
if (!ctx) return false
|
|
188
207
|
try {
|
|
@@ -246,10 +265,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
246
265
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'DELETE only' } })
|
|
247
266
|
return
|
|
248
267
|
}
|
|
249
|
-
if (
|
|
250
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
251
|
-
return
|
|
252
|
-
}
|
|
268
|
+
if (rejectIfUntrusted(req, res, { requireAuth: true })) return
|
|
253
269
|
const removed = await speechCache.clear()
|
|
254
270
|
writeJson(res, 200, { ok: true, removed, cleared: removed })
|
|
255
271
|
},
|
|
@@ -263,10 +279,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
263
279
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
264
280
|
return
|
|
265
281
|
}
|
|
266
|
-
if (
|
|
267
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
268
|
-
return
|
|
269
|
-
}
|
|
282
|
+
if (rejectIfUntrusted(req, res, { requireAuth: true })) return
|
|
270
283
|
const removed = await speechCache.clear()
|
|
271
284
|
writeJson(res, 200, { ok: true, removed, cleared: removed })
|
|
272
285
|
},
|
|
@@ -280,10 +293,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
280
293
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
281
294
|
return
|
|
282
295
|
}
|
|
283
|
-
if (
|
|
284
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
285
|
-
return
|
|
286
|
-
}
|
|
296
|
+
if (rejectIfUntrusted(req, res)) return
|
|
287
297
|
let body = {}
|
|
288
298
|
try {
|
|
289
299
|
const raw = await readBody(req, 64 * 1024)
|
|
@@ -313,10 +323,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
313
323
|
path: '/dsh-tts/stats',
|
|
314
324
|
handler: async (req, res) => {
|
|
315
325
|
if (req.method === 'GET') {
|
|
316
|
-
if (
|
|
317
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
318
|
-
return
|
|
319
|
-
}
|
|
326
|
+
if (rejectIfUntrusted(req, res)) return
|
|
320
327
|
writeJson(res, 200, { ok: true, total: stats.total, cacheHits: stats.cacheHits, errors: stats.errors, providers: stats.providers, breaker: typeof breakerSnapshot === 'function' ? breakerSnapshot() : [] })
|
|
321
328
|
return
|
|
322
329
|
}
|
|
@@ -324,10 +331,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
324
331
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or DELETE' } })
|
|
325
332
|
return
|
|
326
333
|
}
|
|
327
|
-
if (
|
|
328
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
329
|
-
return
|
|
330
|
-
}
|
|
334
|
+
if (rejectIfUntrusted(req, res, { requireAuth: true })) return
|
|
331
335
|
stats.total = 0
|
|
332
336
|
stats.cacheHits = 0
|
|
333
337
|
stats.errors = 0
|
|
@@ -340,11 +344,8 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
340
344
|
kind: 'exact',
|
|
341
345
|
path: '/dsh-tts/config',
|
|
342
346
|
handler: async (req, res) => {
|
|
343
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
344
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'dsh-tts: settings writes are same-origin only' } })
|
|
345
|
-
return
|
|
346
|
-
}
|
|
347
347
|
if (req.method === 'GET') {
|
|
348
|
+
if (rejectIfUntrusted(req, res, { message: 'dsh-tts: settings writes are same-origin only' })) return
|
|
348
349
|
writeJson(res, 200, await configResponse())
|
|
349
350
|
return
|
|
350
351
|
}
|
|
@@ -352,6 +353,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
352
353
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or PUT' } })
|
|
353
354
|
return
|
|
354
355
|
}
|
|
356
|
+
if (rejectIfUntrusted(req, res, { requireAuth: true, message: 'dsh-tts: settings writes are same-origin only' })) return
|
|
355
357
|
const settingsApi = typeof getSettingsApi === 'function' ? getSettingsApi() : null
|
|
356
358
|
if (!settingsApi) {
|
|
357
359
|
writeJson(res, 503, { ok: false, error: { code: 'settings', message: 'settings not ready' } })
|
|
@@ -423,10 +425,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
423
425
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'PUT or DELETE only' } })
|
|
424
426
|
return
|
|
425
427
|
}
|
|
426
|
-
if (
|
|
427
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'dsh-tts: keys are same-origin only' } })
|
|
428
|
-
return
|
|
429
|
-
}
|
|
428
|
+
if (rejectIfUntrusted(req, res, { requireAuth: true, message: 'dsh-tts: keys are same-origin only' })) return
|
|
430
429
|
let payload = {}
|
|
431
430
|
if (req.method === 'PUT' || (req.headers['content-length'] && Number(req.headers['content-length']) > 0)) {
|
|
432
431
|
let raw
|
|
@@ -463,7 +462,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
463
462
|
path: '/dsh-tts/pending',
|
|
464
463
|
handler: async (req, res) => {
|
|
465
464
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
466
|
-
if (
|
|
465
|
+
if (rejectIfUntrusted(req, res)) return
|
|
467
466
|
const url = new URL(req.url || '/', 'http://127.0.0.1')
|
|
468
467
|
const after = url.searchParams.get('after') || ''
|
|
469
468
|
let items = pending.slice()
|
|
@@ -480,7 +479,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
480
479
|
path: '/dsh-tts/speak',
|
|
481
480
|
handler: async (req, res) => {
|
|
482
481
|
if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
|
|
483
|
-
if (
|
|
482
|
+
if (rejectIfUntrusted(req, res)) return
|
|
484
483
|
let raw
|
|
485
484
|
try { raw = await readBody(req, 256 * 1024) } catch (e) {
|
|
486
485
|
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } }); return
|
|
@@ -531,4 +530,4 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
531
530
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
532
531
|
mutex: updaterMutex,
|
|
533
532
|
}), 'dsh-tts: updater route /api/dsh-tts/update')
|
|
534
|
-
}
|
|
533
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.35",
|
|
4
4
|
"description": "Text-to-speech for DeepSeek Harness: speak agent replies in the Web UI with a provider fallback chain (OpenAI, ElevenLabs, Google, Azure, Groq, Deepgram, OpenRouter, Edge, Piper, eSpeak).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|