@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.
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 +9 -2
  12. package/dist/microphone-handler.d.ts.map +1 -1
  13. package/dist/microphone-handler.js +104 -6
  14. package/dist/navigation-controller.d.ts +5 -0
  15. package/dist/navigation-controller.d.ts.map +1 -1
  16. package/dist/navigation-controller.js +149 -35
  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,5 @@
1
- type NextApiRequest = {
1
+ import { type GateRequestLike } from "./security.js";
2
+ type NextApiRequest = GateRequestLike & {
2
3
  method?: string;
3
4
  body?: {
4
5
  text?: string | string[];
@@ -1 +1 @@
1
- {"version":3,"file":"bedrock-embedding-handler.d.ts","sourceRoot":"","sources":["../../src/server/bedrock-embedding-handler.ts"],"names":[],"mappings":"AA4BA,KAAK,cAAc,GAAG;IACpB,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;AA0MF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAMD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,QAAQ,CAAC,CAkCnB"}
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 { text, inputType = "search_query" } = req.body || {};
145
- const result = await generateBedrockEmbedding(text, inputType);
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.status(result.statusCode).json(result.error);
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 = "search_query" } = body || {};
157
- const result = await generateBedrockEmbedding(text, inputType);
158
- const headers = {
159
- "Content-Type": "application/json",
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
- else {
168
- return new Response(JSON.stringify(result.error), {
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 (error) {
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: { "Content-Type": "application/json" },
234
+ headers,
181
235
  });
182
236
  }
183
237
  }
@@ -1,4 +1,5 @@
1
- type NextApiRequest = {
1
+ import { type GateRequestLike } from "./security.js";
2
+ type NextApiRequest = GateRequestLike & {
2
3
  method?: string;
3
4
  body?: {
4
5
  body?: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"bedrock-handler.d.ts","sourceRoot":"","sources":["../../src/server/bedrock-handler.ts"],"names":[],"mappings":"AA4BA,KAAK,cAAc,GAAG;IACpB,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;AAqLF,wBAAsB,cAAc,CAClC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CAkBf;AAMD,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkCrE"}
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 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"}