@myscheme/voice-navigation-sdk 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -17
- package/dist/actions.d.ts +1 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +51 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -1
- package/dist/languages.d.ts +17 -0
- package/dist/languages.d.ts.map +1 -0
- package/dist/languages.js +86 -0
- package/dist/microphone-handler.d.ts +9 -2
- package/dist/microphone-handler.d.ts.map +1 -1
- package/dist/microphone-handler.js +104 -6
- package/dist/navigation-controller.d.ts +5 -0
- package/dist/navigation-controller.d.ts.map +1 -1
- package/dist/navigation-controller.js +149 -35
- package/dist/server/azure-speech-handler.d.ts +3 -2
- package/dist/server/azure-speech-handler.d.ts.map +1 -1
- package/dist/server/azure-speech-handler.js +26 -2
- package/dist/server/bedrock-embedding-handler.d.ts +2 -1
- package/dist/server/bedrock-embedding-handler.d.ts.map +1 -1
- package/dist/server/bedrock-embedding-handler.js +73 -19
- package/dist/server/bedrock-handler.d.ts +2 -1
- package/dist/server/bedrock-handler.d.ts.map +1 -1
- package/dist/server/bedrock-handler.js +124 -19
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/opensearch-env-handler.d.ts +2 -1
- package/dist/server/opensearch-env-handler.d.ts.map +1 -1
- package/dist/server/opensearch-env-handler.js +23 -53
- package/dist/server/sarvam-stt-relay.d.ts +8 -0
- package/dist/server/sarvam-stt-relay.d.ts.map +1 -0
- package/dist/server/sarvam-stt-relay.js +111 -0
- package/dist/server/security.d.ts +26 -0
- package/dist/server/security.d.ts.map +1 -0
- package/dist/server/security.js +89 -0
- package/dist/services/bedrock.d.ts.map +1 -1
- package/dist/services/bedrock.js +13 -0
- package/dist/services/sarvam-speech.d.ts +47 -0
- package/dist/services/sarvam-speech.d.ts.map +1 -0
- package/dist/services/sarvam-speech.js +418 -0
- package/dist/services/voice-feedback.d.ts +7 -2
- package/dist/services/voice-feedback.d.ts.map +1 -1
- package/dist/services/voice-feedback.js +45 -5
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bedrock-embedding-handler.d.ts","sourceRoot":"","sources":["../../src/server/bedrock-embedding-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bedrock-embedding-handler.d.ts","sourceRoot":"","sources":["../../src/server/bedrock-embedding-handler.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEnE,KAAK,cAAc,GAAG,eAAe,GAAG;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC1C,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;CAC7D,CAAC;AA0OF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAMD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,QAAQ,CAAC,CAsDnB"}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
|
+
import { evaluateGate } from "./security.js";
|
|
3
|
+
const MAX_EMBEDDING_TEXTS = 16;
|
|
4
|
+
const MAX_EMBEDDING_TEXT_CHARS = 2000;
|
|
5
|
+
const ALLOWED_INPUT_TYPES = new Set([
|
|
6
|
+
"search_query",
|
|
7
|
+
"search_document",
|
|
8
|
+
"classification",
|
|
9
|
+
"clustering",
|
|
10
|
+
]);
|
|
11
|
+
function sanitizeEmbeddingInput(text, inputType) {
|
|
12
|
+
const rawTexts = Array.isArray(text) ? text : text != null ? [text] : [];
|
|
13
|
+
const texts = [];
|
|
14
|
+
for (const entry of rawTexts.slice(0, MAX_EMBEDDING_TEXTS)) {
|
|
15
|
+
if (typeof entry === "string" && entry.trim().length > 0) {
|
|
16
|
+
texts.push(entry.slice(0, MAX_EMBEDDING_TEXT_CHARS));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (texts.length === 0) {
|
|
20
|
+
return { ok: false };
|
|
21
|
+
}
|
|
22
|
+
const resolvedType = typeof inputType === "string" && ALLOWED_INPUT_TYPES.has(inputType)
|
|
23
|
+
? inputType
|
|
24
|
+
: "search_query";
|
|
25
|
+
return { ok: true, texts, inputType: resolvedType };
|
|
26
|
+
}
|
|
2
27
|
function sign(key, msg) {
|
|
3
28
|
return crypto.createHmac("sha256", key).update(msg, "utf8").digest();
|
|
4
29
|
}
|
|
@@ -141,43 +166,72 @@ export async function bedrockEmbeddingHandler(req, res) {
|
|
|
141
166
|
res.status(405).json({ error: "Method not allowed" });
|
|
142
167
|
return;
|
|
143
168
|
}
|
|
144
|
-
const {
|
|
145
|
-
|
|
169
|
+
const gate = evaluateGate(req, { maxRequests: 30, windowMs: 60 * 1000, bucket: "embed" });
|
|
170
|
+
if (!gate.ok) {
|
|
171
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const { text, inputType } = req.body || {};
|
|
175
|
+
const sanitized = sanitizeEmbeddingInput(text, inputType);
|
|
176
|
+
if (!sanitized.ok) {
|
|
177
|
+
res.status(400).json({ error: "Invalid text parameter" });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const result = await generateBedrockEmbedding(sanitized.texts, sanitized.inputType);
|
|
146
181
|
if (result.success && result.data) {
|
|
147
182
|
res.status(result.statusCode).json(result.data);
|
|
148
183
|
}
|
|
149
184
|
else {
|
|
150
|
-
res
|
|
185
|
+
res
|
|
186
|
+
.status(result.statusCode >= 500 ? 502 : result.statusCode)
|
|
187
|
+
.json({ error: "Embedding request failed" });
|
|
151
188
|
}
|
|
152
189
|
}
|
|
153
190
|
export async function BedrockEmbeddingPOST(request) {
|
|
191
|
+
const headers = { "Content-Type": "application/json" };
|
|
192
|
+
const gateReq = {
|
|
193
|
+
method: request.method,
|
|
194
|
+
headers: {
|
|
195
|
+
origin: request.headers.get("origin") ?? undefined,
|
|
196
|
+
referer: request.headers.get("referer") ?? undefined,
|
|
197
|
+
host: request.headers.get("host") ?? undefined,
|
|
198
|
+
"x-forwarded-for": request.headers.get("x-forwarded-for") ?? undefined,
|
|
199
|
+
"x-real-ip": request.headers.get("x-real-ip") ?? undefined,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const gate = evaluateGate(gateReq, { maxRequests: 30, windowMs: 60 * 1000, bucket: "embed" });
|
|
203
|
+
if (!gate.ok) {
|
|
204
|
+
return new Response(JSON.stringify({ error: gate.error }), {
|
|
205
|
+
status: gate.status ?? 403,
|
|
206
|
+
headers,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
154
209
|
try {
|
|
155
210
|
const body = await request.json();
|
|
156
|
-
const { text, inputType
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (result.success && result.data) {
|
|
162
|
-
return new Response(JSON.stringify(result.data), {
|
|
163
|
-
status: result.statusCode,
|
|
211
|
+
const { text, inputType } = body || {};
|
|
212
|
+
const sanitized = sanitizeEmbeddingInput(text, inputType);
|
|
213
|
+
if (!sanitized.ok) {
|
|
214
|
+
return new Response(JSON.stringify({ error: "Invalid text parameter" }), {
|
|
215
|
+
status: 400,
|
|
164
216
|
headers,
|
|
165
217
|
});
|
|
166
218
|
}
|
|
167
|
-
|
|
168
|
-
|
|
219
|
+
const result = await generateBedrockEmbedding(sanitized.texts, sanitized.inputType);
|
|
220
|
+
if (result.success && result.data) {
|
|
221
|
+
return new Response(JSON.stringify(result.data), {
|
|
169
222
|
status: result.statusCode,
|
|
170
223
|
headers,
|
|
171
224
|
});
|
|
172
225
|
}
|
|
226
|
+
return new Response(JSON.stringify({ error: "Embedding request failed" }), {
|
|
227
|
+
status: result.statusCode >= 500 ? 502 : result.statusCode,
|
|
228
|
+
headers,
|
|
229
|
+
});
|
|
173
230
|
}
|
|
174
|
-
catch
|
|
175
|
-
return new Response(JSON.stringify({
|
|
176
|
-
error: "Invalid request",
|
|
177
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
178
|
-
}), {
|
|
231
|
+
catch {
|
|
232
|
+
return new Response(JSON.stringify({ error: "Invalid request" }), {
|
|
179
233
|
status: 400,
|
|
180
|
-
headers
|
|
234
|
+
headers,
|
|
181
235
|
});
|
|
182
236
|
}
|
|
183
237
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bedrock-handler.d.ts","sourceRoot":"","sources":["../../src/server/bedrock-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bedrock-handler.d.ts","sourceRoot":"","sources":["../../src/server/bedrock-handler.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEnE,KAAK,cAAc,GAAG,eAAe,GAAG;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC1C,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;CAC7D,CAAC;AAyRF,wBAAsB,cAAc,CAClC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAMD,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmDrE"}
|
|
@@ -1,4 +1,82 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
|
+
import { evaluateGate } from "./security.js";
|
|
3
|
+
const MAX_OUTPUT_TOKENS = 512;
|
|
4
|
+
const DEFAULT_OUTPUT_TOKENS = 256;
|
|
5
|
+
const MAX_MESSAGES = 6;
|
|
6
|
+
const MAX_SYSTEM_CHARS = 20000;
|
|
7
|
+
const MAX_TEXT_CHARS = 20000;
|
|
8
|
+
const MAX_TOTAL_INPUT_CHARS = 40000;
|
|
9
|
+
const clampNumber = (value, min, max, fallback) => {
|
|
10
|
+
const num = typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
11
|
+
return Math.min(max, Math.max(min, Math.floor(num)));
|
|
12
|
+
};
|
|
13
|
+
const sanitizeContent = (content) => {
|
|
14
|
+
if (typeof content === "string") {
|
|
15
|
+
return [{ type: "text", text: content.slice(0, MAX_TEXT_CHARS) }];
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(content)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const parts = [];
|
|
21
|
+
for (const part of content) {
|
|
22
|
+
if (part && typeof part === "object" && part.type === "text") {
|
|
23
|
+
const text = part.text;
|
|
24
|
+
if (typeof text === "string") {
|
|
25
|
+
parts.push({ type: "text", text: text.slice(0, MAX_TEXT_CHARS) });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return parts;
|
|
30
|
+
};
|
|
31
|
+
function sanitizeClaudeRequest(input) {
|
|
32
|
+
const raw = input && typeof input === "object" && "body" in input
|
|
33
|
+
? input.body
|
|
34
|
+
: input;
|
|
35
|
+
if (!raw || typeof raw !== "object") {
|
|
36
|
+
return { ok: false };
|
|
37
|
+
}
|
|
38
|
+
const source = raw;
|
|
39
|
+
const rawMessages = Array.isArray(source.messages) ? source.messages : [];
|
|
40
|
+
if (rawMessages.length === 0) {
|
|
41
|
+
return { ok: false };
|
|
42
|
+
}
|
|
43
|
+
let totalChars = 0;
|
|
44
|
+
const messages = [];
|
|
45
|
+
for (const message of rawMessages.slice(0, MAX_MESSAGES)) {
|
|
46
|
+
if (!message || typeof message !== "object") {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const role = message.role;
|
|
50
|
+
if (role !== "user" && role !== "assistant") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const content = sanitizeContent(message.content);
|
|
54
|
+
if (content.length === 0) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (const part of content) {
|
|
58
|
+
totalChars += part.text.length;
|
|
59
|
+
}
|
|
60
|
+
messages.push({ role, content });
|
|
61
|
+
}
|
|
62
|
+
if (messages.length === 0 || totalChars > MAX_TOTAL_INPUT_CHARS) {
|
|
63
|
+
return { ok: false };
|
|
64
|
+
}
|
|
65
|
+
const body = {
|
|
66
|
+
anthropic_version: typeof source.anthropic_version === "string"
|
|
67
|
+
? source.anthropic_version
|
|
68
|
+
: "bedrock-2023-05-31",
|
|
69
|
+
max_tokens: clampNumber(source.max_tokens, 1, MAX_OUTPUT_TOKENS, DEFAULT_OUTPUT_TOKENS),
|
|
70
|
+
messages,
|
|
71
|
+
};
|
|
72
|
+
if (typeof source.system === "string") {
|
|
73
|
+
body.system = source.system.slice(0, MAX_SYSTEM_CHARS);
|
|
74
|
+
}
|
|
75
|
+
if (source.temperature != null) {
|
|
76
|
+
body.temperature = clampNumber(source.temperature, 0, 1, 0);
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, body };
|
|
79
|
+
}
|
|
2
80
|
function sign(key, msg) {
|
|
3
81
|
return crypto.createHmac("sha256", key).update(msg, "utf8").digest();
|
|
4
82
|
}
|
|
@@ -129,43 +207,70 @@ export async function bedrockHandler(req, res) {
|
|
|
129
207
|
res.status(405).json({ error: "Method not allowed" });
|
|
130
208
|
return;
|
|
131
209
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
210
|
+
const gate = evaluateGate(req, { maxRequests: 30, windowMs: 60 * 1000, bucket: "bedrock" });
|
|
211
|
+
if (!gate.ok) {
|
|
212
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const sanitized = sanitizeClaudeRequest(req.body);
|
|
216
|
+
if (!sanitized.ok) {
|
|
217
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const result = await invokeBedrock(sanitized.body);
|
|
134
221
|
if (result.success && result.data) {
|
|
135
222
|
res.status(result.statusCode).json(result.data);
|
|
136
223
|
}
|
|
137
224
|
else {
|
|
138
|
-
res
|
|
225
|
+
res
|
|
226
|
+
.status(result.statusCode >= 500 ? 502 : result.statusCode)
|
|
227
|
+
.json({ error: "Bedrock request failed" });
|
|
139
228
|
}
|
|
140
229
|
}
|
|
141
230
|
export async function BedrockPOST(request) {
|
|
231
|
+
const headers = { "Content-Type": "application/json" };
|
|
232
|
+
const gateReq = {
|
|
233
|
+
method: request.method,
|
|
234
|
+
headers: {
|
|
235
|
+
origin: request.headers.get("origin") ?? undefined,
|
|
236
|
+
referer: request.headers.get("referer") ?? undefined,
|
|
237
|
+
host: request.headers.get("host") ?? undefined,
|
|
238
|
+
"x-forwarded-for": request.headers.get("x-forwarded-for") ?? undefined,
|
|
239
|
+
"x-real-ip": request.headers.get("x-real-ip") ?? undefined,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
const gate = evaluateGate(gateReq, { maxRequests: 30, windowMs: 60 * 1000, bucket: "bedrock" });
|
|
243
|
+
if (!gate.ok) {
|
|
244
|
+
return new Response(JSON.stringify({ error: gate.error }), {
|
|
245
|
+
status: gate.status ?? 403,
|
|
246
|
+
headers,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
142
249
|
try {
|
|
143
250
|
const body = await request.json();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
if (result.success && result.data) {
|
|
150
|
-
return new Response(JSON.stringify(result.data), {
|
|
151
|
-
status: result.statusCode,
|
|
251
|
+
const sanitized = sanitizeClaudeRequest(body);
|
|
252
|
+
if (!sanitized.ok) {
|
|
253
|
+
return new Response(JSON.stringify({ error: "Invalid request body" }), {
|
|
254
|
+
status: 400,
|
|
152
255
|
headers,
|
|
153
256
|
});
|
|
154
257
|
}
|
|
155
|
-
|
|
156
|
-
|
|
258
|
+
const result = await invokeBedrock(sanitized.body);
|
|
259
|
+
if (result.success && result.data) {
|
|
260
|
+
return new Response(JSON.stringify(result.data), {
|
|
157
261
|
status: result.statusCode,
|
|
158
262
|
headers,
|
|
159
263
|
});
|
|
160
264
|
}
|
|
265
|
+
return new Response(JSON.stringify({ error: "Bedrock request failed" }), {
|
|
266
|
+
status: result.statusCode >= 500 ? 502 : result.statusCode,
|
|
267
|
+
headers,
|
|
268
|
+
});
|
|
161
269
|
}
|
|
162
|
-
catch
|
|
163
|
-
return new Response(JSON.stringify({
|
|
164
|
-
error: "Invalid request",
|
|
165
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
166
|
-
}), {
|
|
270
|
+
catch {
|
|
271
|
+
return new Response(JSON.stringify({ error: "Invalid request" }), {
|
|
167
272
|
status: 400,
|
|
168
|
-
headers
|
|
273
|
+
headers,
|
|
169
274
|
});
|
|
170
275
|
}
|
|
171
276
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -7,4 +7,9 @@ export { BedrockPOST } from "./bedrock-handler.js";
|
|
|
7
7
|
export { BedrockEmbeddingPOST } from "./bedrock-embedding-handler.js";
|
|
8
8
|
export { createOpenSearchProxyHandler, DEFAULT_OPENSEARCH_PROXY_PATH, } from "./opensearch-handler.js";
|
|
9
9
|
export type { VectorSearchProxyRequest, VectorSearchProxyResponse, } from "./opensearch-handler.js";
|
|
10
|
+
export { attachSarvamSttRelay } from "./sarvam-stt-relay.js";
|
|
11
|
+
export type { SarvamSttRelayOptions } from "./sarvam-stt-relay.js";
|
|
12
|
+
export { evaluateGate, isOriginAllowed, getClientId, checkRateLimit, isTrustProxyEnabled, parseAllowedOrigins, } from "./security.js";
|
|
13
|
+
export type { GateRequestLike, GateOptions, GateResult } from "./security.js";
|
|
14
|
+
export { SARVAM_TTS_LANGUAGES, SARVAM_STT_LANGUAGES, DEFAULT_LANGUAGE, } from "../languages.js";
|
|
10
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAGtE,OAAO,EACL,4BAA4B,EAC5B,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAGtE,OAAO,EACL,4BAA4B,EAC5B,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAInE,OAAO,EACL,YAAY,EACZ,eAAe,EACf,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG9E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -6,3 +6,6 @@ export { POST } from "./azure-speech-handler.js";
|
|
|
6
6
|
export { BedrockPOST } from "./bedrock-handler.js";
|
|
7
7
|
export { BedrockEmbeddingPOST } from "./bedrock-embedding-handler.js";
|
|
8
8
|
export { createOpenSearchProxyHandler, DEFAULT_OPENSEARCH_PROXY_PATH, } from "./opensearch-handler.js";
|
|
9
|
+
export { attachSarvamSttRelay } from "./sarvam-stt-relay.js";
|
|
10
|
+
export { evaluateGate, isOriginAllowed, getClientId, checkRateLimit, isTrustProxyEnabled, parseAllowedOrigins, } from "./security.js";
|
|
11
|
+
export { SARVAM_TTS_LANGUAGES, SARVAM_STT_LANGUAGES, DEFAULT_LANGUAGE, } from "../languages.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opensearch-env-handler.d.ts","sourceRoot":"","sources":["../../src/server/opensearch-env-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"opensearch-env-handler.d.ts","sourceRoot":"","sources":["../../src/server/opensearch-env-handler.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGnE,KAAK,cAAc,GAAG,eAAe,GAAG;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3D,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAC;IAC7C,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC,CAAC;AAMF,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC1C,IAAI,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAC1B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;CAC7D,CAAC;AAuCF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CA8Hf;AAMD,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CA8Ef"}
|
|
@@ -1,32 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const now = Date.now();
|
|
4
|
-
const record = rateLimitMap.get(identifier);
|
|
5
|
-
if (!record || now > record.resetTime) {
|
|
6
|
-
rateLimitMap.set(identifier, {
|
|
7
|
-
count: 1,
|
|
8
|
-
resetTime: now + windowMs,
|
|
9
|
-
});
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
if (record.count >= maxRequests) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
record.count++;
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
function getClientIdentifier(req) {
|
|
19
|
-
const forwardedFor = req.headers?.["x-forwarded-for"];
|
|
20
|
-
if (forwardedFor) {
|
|
21
|
-
const ip = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
22
|
-
return ip.split(",")[0].trim();
|
|
23
|
-
}
|
|
24
|
-
const realIp = req.headers?.["x-real-ip"];
|
|
25
|
-
if (realIp) {
|
|
26
|
-
return Array.isArray(realIp) ? realIp[0] : realIp;
|
|
27
|
-
}
|
|
28
|
-
return req.socket?.remoteAddress || "unknown";
|
|
29
|
-
}
|
|
1
|
+
import { evaluateGate } from "./security.js";
|
|
2
|
+
const SEARCH_SOURCE_FIELDS = ["name", "path", "keywords", "description"];
|
|
30
3
|
let opensearchClient = null;
|
|
31
4
|
async function getOpenSearchClient() {
|
|
32
5
|
if (!opensearchClient) {
|
|
@@ -56,12 +29,13 @@ export async function openSearchSearchHandler(req, res) {
|
|
|
56
29
|
res.status(405).json({ error: "Method not allowed" });
|
|
57
30
|
return;
|
|
58
31
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
32
|
+
const gate = evaluateGate(req, {
|
|
33
|
+
maxRequests: 20,
|
|
34
|
+
windowMs: 60 * 1000,
|
|
35
|
+
bucket: "ossearch",
|
|
36
|
+
});
|
|
37
|
+
if (!gate.ok) {
|
|
38
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
65
39
|
return;
|
|
66
40
|
}
|
|
67
41
|
const { query, vector, size = 10 } = req.body || {};
|
|
@@ -122,14 +96,15 @@ export async function openSearchSearchHandler(req, res) {
|
|
|
122
96
|
body: {
|
|
123
97
|
size: Math.min(size, 100),
|
|
124
98
|
query: {
|
|
125
|
-
|
|
99
|
+
simple_query_string: {
|
|
126
100
|
query: query,
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
fields: ["name^3", "keywords^2", "description", "text"],
|
|
102
|
+
default_operator: "and",
|
|
103
|
+
flags: "NONE",
|
|
129
104
|
lenient: true,
|
|
130
105
|
},
|
|
131
106
|
},
|
|
132
|
-
_source:
|
|
107
|
+
_source: SEARCH_SOURCE_FIELDS,
|
|
133
108
|
},
|
|
134
109
|
});
|
|
135
110
|
}
|
|
@@ -146,10 +121,7 @@ export async function openSearchSearchHandler(req, res) {
|
|
|
146
121
|
}
|
|
147
122
|
catch (error) {
|
|
148
123
|
console.error("[VoiceNavigation] OpenSearch search error:", error);
|
|
149
|
-
res.status(500).json({
|
|
150
|
-
error: "Search failed",
|
|
151
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
152
|
-
});
|
|
124
|
+
res.status(500).json({ error: "Search failed" });
|
|
153
125
|
}
|
|
154
126
|
}
|
|
155
127
|
export async function openSearchSuggestHandler(req, res) {
|
|
@@ -158,12 +130,13 @@ export async function openSearchSuggestHandler(req, res) {
|
|
|
158
130
|
res.status(405).json({ error: "Method not allowed" });
|
|
159
131
|
return;
|
|
160
132
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
133
|
+
const gate = evaluateGate(req, {
|
|
134
|
+
maxRequests: 30,
|
|
135
|
+
windowMs: 60 * 1000,
|
|
136
|
+
bucket: "ossuggest",
|
|
137
|
+
});
|
|
138
|
+
if (!gate.ok) {
|
|
139
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
167
140
|
return;
|
|
168
141
|
}
|
|
169
142
|
const { query } = req.body || {};
|
|
@@ -213,9 +186,6 @@ export async function openSearchSuggestHandler(req, res) {
|
|
|
213
186
|
}
|
|
214
187
|
catch (error) {
|
|
215
188
|
console.error("[VoiceNavigation] OpenSearch suggest error:", error);
|
|
216
|
-
res.status(500).json({
|
|
217
|
-
error: "Suggestion failed",
|
|
218
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
219
|
-
});
|
|
189
|
+
res.status(500).json({ error: "Suggestion failed" });
|
|
220
190
|
}
|
|
221
191
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Server as HttpServer } from "http";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
export interface SarvamSttRelayOptions {
|
|
4
|
+
path?: string;
|
|
5
|
+
connectTimeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function attachSarvamSttRelay(server: HttpServer, options?: SarvamSttRelayOptions): WebSocketServer;
|
|
8
|
+
//# sourceMappingURL=sarvam-stt-relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sarvam-stt-relay.d.ts","sourceRoot":"","sources":["../../src/server/sarvam-stt-relay.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAmB,MAAM,MAAM,CAAC;AAElE,OAAO,EAA8B,eAAe,EAAE,MAAM,IAAI,CAAC;AAiDjE,MAAM,WAAW,qBAAqB;IAEpC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAUD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,qBAA0B,GAClC,eAAe,CA2HjB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { WebSocket as NodeWebSocket, WebSocketServer } from "ws";
|
|
2
|
+
import { isOriginAllowed, getClientId, checkRateLimit } from "./security.js";
|
|
3
|
+
import { SARVAM_STT_LANGUAGES, DEFAULT_LANGUAGE } from "../languages.js";
|
|
4
|
+
const ALLOWED_STT_MODELS = new Set(["saaras:v3", "saarika:v2.5"]);
|
|
5
|
+
const ALLOWED_SAMPLE_RATES = new Set(["8000", "16000"]);
|
|
6
|
+
const ALLOWED_BOOLEANS = new Set(["true", "false"]);
|
|
7
|
+
function buildSafeUpstreamQuery(clientParams) {
|
|
8
|
+
const out = new URLSearchParams();
|
|
9
|
+
const language = clientParams.get("language-code") ?? "";
|
|
10
|
+
out.set("language-code", SARVAM_STT_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE);
|
|
11
|
+
const model = clientParams.get("model") ?? "";
|
|
12
|
+
out.set("model", ALLOWED_STT_MODELS.has(model) ? model : "saaras:v3");
|
|
13
|
+
const sampleRate = clientParams.get("sample_rate") ?? "";
|
|
14
|
+
out.set("sample_rate", ALLOWED_SAMPLE_RATES.has(sampleRate) ? sampleRate : "16000");
|
|
15
|
+
out.set("input_audio_codec", "wav");
|
|
16
|
+
for (const key of ["high_vad_sensitivity", "vad_signals", "flush_signal"]) {
|
|
17
|
+
const value = clientParams.get(key);
|
|
18
|
+
if (value !== null && ALLOWED_BOOLEANS.has(value)) {
|
|
19
|
+
out.set(key, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out.toString();
|
|
23
|
+
}
|
|
24
|
+
function rejectUpgrade(socket, status, reason) {
|
|
25
|
+
try {
|
|
26
|
+
socket.write(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\n\r\n`);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
}
|
|
30
|
+
socket.destroy();
|
|
31
|
+
}
|
|
32
|
+
const DEFAULT_PATH = "/api/speech/sarvam-stt";
|
|
33
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 10000;
|
|
34
|
+
const SARVAM_STT_WS_URL = "wss://api.sarvam.ai/speech-to-text/ws";
|
|
35
|
+
export function attachSarvamSttRelay(server, options = {}) {
|
|
36
|
+
const path = options.path ?? DEFAULT_PATH;
|
|
37
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
38
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
39
|
+
server.on("upgrade", (req, socket, head) => {
|
|
40
|
+
const requestUrl = new URL(req.url ?? "", "http://localhost");
|
|
41
|
+
if (requestUrl.pathname !== path) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!isOriginAllowed(req)) {
|
|
45
|
+
rejectUpgrade(socket, 403, "Forbidden");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!checkRateLimit(`stt:${getClientId(req)}`, 15, 60 * 1000)) {
|
|
49
|
+
rejectUpgrade(socket, 429, "Too Many Requests");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const apiKey = process.env.SARVAM_API_KEY;
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
console.error("[VoiceNavigation] Missing SARVAM_API_KEY. Set it in your .env file to enable the Sarvam STT relay.");
|
|
55
|
+
socket.destroy();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const safeQuery = buildSafeUpstreamQuery(requestUrl.searchParams);
|
|
59
|
+
const upstream = new NodeWebSocket(`${SARVAM_STT_WS_URL}?${safeQuery}`, [`api-subscription-key.${apiKey}`], { headers: { "Api-Subscription-Key": apiKey } });
|
|
60
|
+
const connectTimeout = setTimeout(() => {
|
|
61
|
+
upstream.terminate();
|
|
62
|
+
}, connectTimeoutMs);
|
|
63
|
+
upstream.once("open", () => {
|
|
64
|
+
clearTimeout(connectTimeout);
|
|
65
|
+
wss.handleUpgrade(req, socket, head, (clientSocket) => {
|
|
66
|
+
let closedByClient = false;
|
|
67
|
+
let closedByUpstream = false;
|
|
68
|
+
upstream.on("message", (data, isBinary) => {
|
|
69
|
+
if (clientSocket.readyState === clientSocket.OPEN) {
|
|
70
|
+
clientSocket.send(data, { binary: isBinary });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
upstream.on("close", () => {
|
|
74
|
+
closedByUpstream = true;
|
|
75
|
+
if (!closedByClient &&
|
|
76
|
+
clientSocket.readyState === clientSocket.OPEN) {
|
|
77
|
+
clientSocket.close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
upstream.on("error", (error) => {
|
|
81
|
+
console.error("[VoiceNavigation] Sarvam STT upstream error:", error);
|
|
82
|
+
if (clientSocket.readyState === clientSocket.OPEN) {
|
|
83
|
+
clientSocket.close();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
clientSocket.on("message", (data, isBinary) => {
|
|
87
|
+
if (upstream.readyState === upstream.OPEN) {
|
|
88
|
+
upstream.send(data, { binary: isBinary });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
clientSocket.on("close", () => {
|
|
92
|
+
closedByClient = true;
|
|
93
|
+
if (!closedByUpstream && upstream.readyState === upstream.OPEN) {
|
|
94
|
+
upstream.close();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
clientSocket.on("error", () => {
|
|
98
|
+
if (upstream.readyState === upstream.OPEN) {
|
|
99
|
+
upstream.close();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
upstream.once("error", (error) => {
|
|
105
|
+
clearTimeout(connectTimeout);
|
|
106
|
+
console.error("[VoiceNavigation] Failed to connect to Sarvam STT upstream:", error);
|
|
107
|
+
socket.destroy();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return wss;
|
|
111
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface GateRequestLike {
|
|
2
|
+
method?: string;
|
|
3
|
+
headers?: {
|
|
4
|
+
[key: string]: string | string[] | undefined;
|
|
5
|
+
};
|
|
6
|
+
socket?: {
|
|
7
|
+
remoteAddress?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface GateOptions {
|
|
11
|
+
maxRequests: number;
|
|
12
|
+
windowMs: number;
|
|
13
|
+
bucket?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface GateResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
status?: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function isTrustProxyEnabled(): boolean;
|
|
21
|
+
export declare function parseAllowedOrigins(): string[];
|
|
22
|
+
export declare function getClientId(req: GateRequestLike): string;
|
|
23
|
+
export declare function isOriginAllowed(req: GateRequestLike): boolean;
|
|
24
|
+
export declare function checkRateLimit(identifier: string, maxRequests: number, windowMs: number): boolean;
|
|
25
|
+
export declare function evaluateGate(req: GateRequestLike, options: GateOptions): GateResult;
|
|
26
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/server/security.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3D,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAE1B,WAAW,EAAE,MAAM,CAAC;IAEpB,QAAQ,EAAE,MAAM,CAAC;IAKjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4BD,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAGD,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAK9C;AAOD,wBAAgB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAYxD;AAQD,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAqB7D;AAMD,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CA0BT;AAMD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,GACnB,UAAU,CAWZ"}
|