@myscheme/voice-navigation-sdk 0.1.9 → 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.
Files changed (48) hide show
  1. package/README.md +130 -17
  2. package/dist/actions.d.ts +1 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +51 -1
  5. package/dist/index.d.ts +38 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +158 -1
  8. package/dist/languages.d.ts +17 -0
  9. package/dist/languages.d.ts.map +1 -0
  10. package/dist/languages.js +86 -0
  11. package/dist/microphone-handler.d.ts +8 -2
  12. package/dist/microphone-handler.d.ts.map +1 -1
  13. package/dist/microphone-handler.js +89 -2
  14. package/dist/navigation-controller.d.ts +2 -0
  15. package/dist/navigation-controller.d.ts.map +1 -1
  16. package/dist/navigation-controller.js +58 -22
  17. package/dist/server/azure-speech-handler.d.ts +3 -2
  18. package/dist/server/azure-speech-handler.d.ts.map +1 -1
  19. package/dist/server/azure-speech-handler.js +26 -2
  20. package/dist/server/bedrock-embedding-handler.d.ts +2 -1
  21. package/dist/server/bedrock-embedding-handler.d.ts.map +1 -1
  22. package/dist/server/bedrock-embedding-handler.js +73 -19
  23. package/dist/server/bedrock-handler.d.ts +2 -1
  24. package/dist/server/bedrock-handler.d.ts.map +1 -1
  25. package/dist/server/bedrock-handler.js +124 -19
  26. package/dist/server/index.d.ts +5 -0
  27. package/dist/server/index.d.ts.map +1 -1
  28. package/dist/server/index.js +3 -0
  29. package/dist/server/opensearch-env-handler.d.ts +2 -1
  30. package/dist/server/opensearch-env-handler.d.ts.map +1 -1
  31. package/dist/server/opensearch-env-handler.js +23 -53
  32. package/dist/server/sarvam-stt-relay.d.ts +8 -0
  33. package/dist/server/sarvam-stt-relay.d.ts.map +1 -0
  34. package/dist/server/sarvam-stt-relay.js +111 -0
  35. package/dist/server/security.d.ts +26 -0
  36. package/dist/server/security.d.ts.map +1 -0
  37. package/dist/server/security.js +89 -0
  38. package/dist/services/bedrock.d.ts.map +1 -1
  39. package/dist/services/bedrock.js +13 -0
  40. package/dist/services/sarvam-speech.d.ts +47 -0
  41. package/dist/services/sarvam-speech.d.ts.map +1 -0
  42. package/dist/services/sarvam-speech.js +418 -0
  43. package/dist/services/voice-feedback.d.ts +7 -2
  44. package/dist/services/voice-feedback.d.ts.map +1 -1
  45. package/dist/services/voice-feedback.js +45 -5
  46. package/dist/types.d.ts +11 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +4 -2
@@ -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 requestBody = req.body?.body;
133
- const result = await invokeBedrock(requestBody);
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.status(result.statusCode).json(result.error);
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 requestBody = body?.body;
145
- const result = await invokeBedrock(requestBody);
146
- const headers = {
147
- "Content-Type": "application/json",
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
- else {
156
- return new Response(JSON.stringify(result.error), {
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 (error) {
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: { "Content-Type": "application/json" },
273
+ headers,
169
274
  });
170
275
  }
171
276
  }
@@ -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"}
@@ -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,4 +1,5 @@
1
- type NextApiRequest = {
1
+ import { type GateRequestLike } from "./security.js";
2
+ type NextApiRequest = GateRequestLike & {
2
3
  method?: string;
3
4
  headers?: {
4
5
  [key: string]: string | string[] | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"opensearch-env-handler.d.ts","sourceRoot":"","sources":["../../src/server/opensearch-env-handler.ts"],"names":[],"mappings":"AAuBA,KAAK,cAAc,GAAG;IACpB,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;AAEF,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;AAmFF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CA6Hf;AAMD,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CAgFf"}
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
- const rateLimitMap = new Map();
2
- function checkRateLimit(identifier, maxRequests, windowMs) {
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 clientId = getClientIdentifier(req);
60
- if (!checkRateLimit(clientId, 20, 60 * 1000)) {
61
- res.status(429).json({
62
- error: "Too many requests",
63
- message: "Rate limit exceeded. Please try again later.",
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
- query_string: {
99
+ simple_query_string: {
126
100
  query: query,
127
- default_operator: "OR",
128
- analyze_wildcard: true,
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: true,
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 clientId = getClientIdentifier(req);
162
- if (!checkRateLimit(clientId, 30, 60 * 1000)) {
163
- res.status(429).json({
164
- error: "Too many requests",
165
- message: "Rate limit exceeded. Please try again later.",
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"}
@@ -0,0 +1,89 @@
1
+ const rateLimitMap = new Map();
2
+ const MAX_TRACKED_CLIENTS = 20000;
3
+ function headerString(value) {
4
+ if (Array.isArray(value)) {
5
+ return value[0] ?? "";
6
+ }
7
+ return value ?? "";
8
+ }
9
+ function hostOf(urlOrOrigin) {
10
+ try {
11
+ return new URL(urlOrOrigin).host;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function isTrustProxyEnabled() {
18
+ return String(process.env.VOICE_NAV_TRUST_PROXY || "").toLowerCase() === "true";
19
+ }
20
+ export function parseAllowedOrigins() {
21
+ return String(process.env.VOICE_NAV_ALLOWED_ORIGINS || "")
22
+ .split(",")
23
+ .map((entry) => entry.trim())
24
+ .filter(Boolean);
25
+ }
26
+ export function getClientId(req) {
27
+ if (isTrustProxyEnabled()) {
28
+ const forwardedFor = headerString(req.headers?.["x-forwarded-for"]);
29
+ if (forwardedFor) {
30
+ return forwardedFor.split(",")[0].trim();
31
+ }
32
+ const realIp = headerString(req.headers?.["x-real-ip"]);
33
+ if (realIp) {
34
+ return realIp;
35
+ }
36
+ }
37
+ return req.socket?.remoteAddress || "unknown";
38
+ }
39
+ export function isOriginAllowed(req) {
40
+ const origin = headerString(req.headers?.origin);
41
+ const referer = headerString(req.headers?.referer);
42
+ const host = headerString(req.headers?.host);
43
+ const candidateHost = origin
44
+ ? hostOf(origin)
45
+ : referer
46
+ ? hostOf(referer)
47
+ : null;
48
+ if (!candidateHost) {
49
+ return false;
50
+ }
51
+ const allowed = parseAllowedOrigins();
52
+ if (allowed.length > 0) {
53
+ return allowed.some((entry) => (hostOf(entry) || entry) === candidateHost);
54
+ }
55
+ return host ? candidateHost === host : false;
56
+ }
57
+ export function checkRateLimit(identifier, maxRequests, windowMs) {
58
+ const now = Date.now();
59
+ if (rateLimitMap.size > MAX_TRACKED_CLIENTS) {
60
+ for (const [key, record] of rateLimitMap) {
61
+ if (now > record.resetTime) {
62
+ rateLimitMap.delete(key);
63
+ }
64
+ }
65
+ if (rateLimitMap.size > MAX_TRACKED_CLIENTS) {
66
+ rateLimitMap.clear();
67
+ }
68
+ }
69
+ const record = rateLimitMap.get(identifier);
70
+ if (!record || now > record.resetTime) {
71
+ rateLimitMap.set(identifier, { count: 1, resetTime: now + windowMs });
72
+ return true;
73
+ }
74
+ if (record.count >= maxRequests) {
75
+ return false;
76
+ }
77
+ record.count++;
78
+ return true;
79
+ }
80
+ export function evaluateGate(req, options) {
81
+ if (!isOriginAllowed(req)) {
82
+ return { ok: false, status: 403, error: "Origin not allowed" };
83
+ }
84
+ const key = `${options.bucket ?? "default"}:${getClientId(req)}`;
85
+ if (!checkRateLimit(key, options.maxRequests, options.windowMs)) {
86
+ return { ok: false, status: 429, error: "Too many requests" };
87
+ }
88
+ return { ok: true };
89
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"bedrock.d.ts","sourceRoot":"","sources":["../../src/services/bedrock.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,aAAa;IAE5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAqDD,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,QAAQ,CAAkB;gBAEtB,MAAM,EAAE,aAAa;IA6CjC,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAO7C,OAAO,CAAC,iBAAiB;IA+BnB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;YAWtD,qBAAqB;YA2ErB,mBAAmB;IAyEjC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc;IAO9C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAWlC,iBAAiB;YAkFjB,eAAe;IAwFvB,uBAAuB,CAC3B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC;YA0BJ,wBAAwB;YAwExB,sBAAsB;CA+DrC"}
1
+ {"version":3,"file":"bedrock.d.ts","sourceRoot":"","sources":["../../src/services/bedrock.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,aAAa;IAE5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAiED,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,QAAQ,CAAkB;gBAEtB,MAAM,EAAE,aAAa;IA6CjC,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAO7C,OAAO,CAAC,iBAAiB;IA+BnB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;YAWtD,qBAAqB;YA2ErB,mBAAmB;IAyEjC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc;IAO9C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAWlC,iBAAiB;YAkFjB,eAAe;IAwFvB,uBAAuB,CAC3B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC;YA2BJ,wBAAwB;YAwExB,sBAAsB;CA+DrC"}
@@ -11,12 +11,24 @@ CRITICAL RULES:
11
11
  6. FOR PAGE NAVIGATION, USE THE EXACT "navigate_<id>" ACTION PROVIDED FOR THAT PAGE
12
12
  7. IF UNCERTAIN, RESPOND WITH {"action": "unknown"}
13
13
 
14
+ MULTILINGUAL INPUT:
15
+ - The user may speak in ANY supported Indian language (e.g. Hindi, Tamil, Telugu, Bengali, Marathi, Gujarati, Kannada, Malayalam, Punjabi, Odia, Assamese, Urdu, and others), or mix languages.
16
+ - Understand the MEANING of the command regardless of the input language and map it to the correct action.
17
+ - The "action" value, action IDs, and "navigate_<id>" page IDs are ALWAYS in English/ASCII and MUST NOT be translated or transliterated.
18
+ - For "query" (search) and "text" (type) fields, preserve the user's own words as spoken.
19
+
14
20
  RESPONSE FORMAT:
15
21
  - Core actions: {"action": "action_name"}
16
22
  - Search: {"action": "search_content", "query": "keywords"}
17
23
  - Type text: {"action": "type_text", "text": "text to type"}
18
24
  - Page navigation: {"action": "navigate_<page_id>"}
19
25
 
26
+ MULTILINGUAL EXAMPLES:
27
+ - User says "नीचे स्क्रॉल करो" (Hindi: scroll down) → {"action": "scroll_down"}
28
+ - User says "डैशबोर्ड खोलो" (Hindi: open dashboard) → {"action": "navigate_dashboard"}
29
+ - User says "மேலே செல்" (Tamil: go up) → {"action": "scroll_up"}
30
+ - User says "వెనక్కి వెళ్ళు" (Telugu: go back) → {"action": "go_back"}
31
+
20
32
  EXAMPLES:
21
33
  - User says "go to dashboard" → {"action": "navigate_dashboard"}
22
34
  - User says "show me the team page" → {"action": "navigate_team"}
@@ -362,6 +374,7 @@ export class BedrockService {
362
374
  Generate brief, natural, conversational feedback messages (maximum 10 words).
363
375
  Be specific and clear about what action was performed.
364
376
  Use the language: ${language}
377
+ Never output internal action identifiers, underscores, technical codes, or JSON — describe the action only in plain, everyday words a non-technical listener would understand.
365
378
  Respond with ONLY the feedback message text, no JSON, no explanations.`;
366
379
  const userPrompt = prompt;
367
380
  console.log("[Bedrock] Using proxy mode:", this.useProxy);
@@ -0,0 +1,47 @@
1
+ export interface SarvamSpeechConfig {
2
+ ttsUrl?: string;
3
+ sttUrl?: string;
4
+ sttModel?: "saaras:v3" | "saarika:v2.5";
5
+ ttsModel?: "bulbul:v3" | "bulbul:v2";
6
+ speaker?: string;
7
+ sttSampleRate?: 8000 | 16000;
8
+ ttsSampleRate?: 8000 | 16000 | 22050 | 24000 | 32000 | 44100 | 48000;
9
+ }
10
+ export interface SarvamSttHandlers {
11
+ onPartial?: (text: string) => void;
12
+ onFinal?: (text: string) => void;
13
+ onSpeechStart?: () => void;
14
+ onSpeechEnd?: () => void;
15
+ onError?: (error: Error) => void;
16
+ shouldSendAudio?: () => boolean;
17
+ }
18
+ export declare class SarvamSpeechService {
19
+ private readonly ttsUrl;
20
+ private readonly sttUrl;
21
+ private readonly sttModel;
22
+ private readonly ttsModel;
23
+ private readonly speaker;
24
+ private readonly sttSampleRate;
25
+ private readonly ttsSampleRate;
26
+ private sttSocket;
27
+ private sttContext;
28
+ private sttMediaStream;
29
+ private sttSourceNode;
30
+ private sttProcessorNode;
31
+ private audioElement;
32
+ private currentBlobUrl;
33
+ private speechGeneration;
34
+ private listeningGeneration;
35
+ constructor(config?: SarvamSpeechConfig);
36
+ startListening(language: string, handlers: SarvamSttHandlers): Promise<void>;
37
+ private sendFlush;
38
+ stopListening(): Promise<void>;
39
+ speak(text: string, opts?: {
40
+ language?: string;
41
+ onAudioStart?: () => void;
42
+ }): Promise<void>;
43
+ stopSpeaking(): void;
44
+ dispose(): Promise<void>;
45
+ private cleanupAudioElement;
46
+ }
47
+ //# sourceMappingURL=sarvam-speech.d.ts.map