@h-ai/serv 0.1.0-alpha.34 → 0.1.0-alpha.36
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 +25 -0
- package/dist/features/ai.d.ts +1 -1
- package/dist/features/iam.d.ts +1 -1
- package/dist/features/storage.d.ts +1 -1
- package/dist/index.d.ts +87 -3
- package/dist/index.js +412 -50
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-CwewduFi.d.ts → serv-context-CatIx8r-.d.ts} +1 -1
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { WILDCARD_PERMISSION, requirePermission } from './chunk-YQV2OMQK.js';
|
|
2
2
|
import { servM, mapHaiError, requireAuth, normalizeServLocale, buildHaiErrorBody } from './chunk-4QBF4PUG.js';
|
|
3
3
|
import { implement } from '@orpc/server';
|
|
4
|
-
import
|
|
4
|
+
import process2 from 'process';
|
|
5
5
|
import { serve } from '@hono/node-server';
|
|
6
|
-
import { core, err, HaiCommonError
|
|
6
|
+
import { core, ok, err, HaiCommonError } from '@h-ai/core';
|
|
7
|
+
import { createNodeWebSocket } from '@hono/node-ws';
|
|
7
8
|
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
8
9
|
import { RPCHandler } from '@orpc/server/fetch';
|
|
9
10
|
import { Hono } from 'hono';
|
|
11
|
+
import { Buffer } from 'buffer';
|
|
10
12
|
import { z } from 'zod';
|
|
11
13
|
import { IAM_AUTH_ROUTES } from '@h-ai/api-contract';
|
|
12
14
|
import { getCookie } from 'hono/cookie';
|
|
@@ -23,55 +25,343 @@ function toFetch(app) {
|
|
|
23
25
|
};
|
|
24
26
|
return handler;
|
|
25
27
|
}
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
var logger = core.logger.child({ module: "serv", scope: "audio-ws" });
|
|
29
|
+
var DEFAULT_MAX_MESSAGE_BYTES = 1048576;
|
|
30
|
+
var DEFAULT_MAX_BUFFERED_BYTES = 10 * 1024 * 1024;
|
|
31
|
+
var DEFAULT_MAX_TEXT_BYTES = 1 * 1024 * 1024;
|
|
32
|
+
var DEFAULT_MAX_SESSION_MS = 5 * 60 * 1e3;
|
|
33
|
+
function registerAudioWsRoute(app, path, upgradeWebSocket, deps) {
|
|
34
|
+
const maxMessageBytes = deps.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
|
|
35
|
+
const maxBufferedBytes = deps.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
|
|
36
|
+
const maxTextBytes = deps.maxTextBytes ?? DEFAULT_MAX_TEXT_BYTES;
|
|
37
|
+
const maxSessionMs = deps.maxSessionMs ?? DEFAULT_MAX_SESSION_MS;
|
|
38
|
+
app.get(path, upgradeWebSocket((c) => {
|
|
39
|
+
const ticket = c.req.query("ticket");
|
|
40
|
+
const conn = new AudioConnection(deps, ticket, maxMessageBytes, maxBufferedBytes, maxTextBytes);
|
|
41
|
+
let sessionTimer;
|
|
42
|
+
return {
|
|
43
|
+
onOpen(_evt, ws) {
|
|
44
|
+
sessionTimer = setTimeout(() => {
|
|
45
|
+
conn.fail(ws, "hai:ai:057", "audio session timeout");
|
|
46
|
+
}, maxSessionMs);
|
|
47
|
+
},
|
|
48
|
+
onMessage(evt, ws) {
|
|
49
|
+
conn.enqueue(ws, evt.data);
|
|
50
|
+
},
|
|
51
|
+
onClose() {
|
|
52
|
+
if (sessionTimer)
|
|
53
|
+
clearTimeout(sessionTimer);
|
|
54
|
+
conn.dispose();
|
|
55
|
+
},
|
|
56
|
+
onError() {
|
|
57
|
+
if (sessionTimer)
|
|
58
|
+
clearTimeout(sessionTimer);
|
|
59
|
+
conn.dispose();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
var AsyncQueue = class {
|
|
65
|
+
items = [];
|
|
66
|
+
waiting = null;
|
|
67
|
+
ended = false;
|
|
68
|
+
push(item) {
|
|
69
|
+
if (this.ended)
|
|
70
|
+
return;
|
|
71
|
+
if (this.waiting) {
|
|
72
|
+
const w = this.waiting;
|
|
73
|
+
this.waiting = null;
|
|
74
|
+
w({ value: item, done: false });
|
|
75
|
+
} else {
|
|
76
|
+
this.items.push(item);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
end() {
|
|
80
|
+
if (this.ended)
|
|
81
|
+
return;
|
|
82
|
+
this.ended = true;
|
|
83
|
+
if (this.waiting) {
|
|
84
|
+
const w = this.waiting;
|
|
85
|
+
this.waiting = null;
|
|
86
|
+
w({ value: void 0, done: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async *iterate() {
|
|
90
|
+
while (true) {
|
|
91
|
+
if (this.items.length > 0) {
|
|
92
|
+
yield this.items.shift();
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (this.ended)
|
|
96
|
+
return;
|
|
97
|
+
const next = await new Promise((res) => {
|
|
98
|
+
this.waiting = res;
|
|
43
99
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
100
|
+
if (next.done)
|
|
101
|
+
return;
|
|
102
|
+
yield next.value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var AudioConnection = class {
|
|
107
|
+
constructor(deps, ticket, maxMessageBytes, maxBufferedBytes, maxTextBytes) {
|
|
108
|
+
this.deps = deps;
|
|
109
|
+
this.ticket = ticket;
|
|
110
|
+
this.maxMessageBytes = maxMessageBytes;
|
|
111
|
+
this.maxBufferedBytes = maxBufferedBytes;
|
|
112
|
+
this.maxTextBytes = maxTextBytes;
|
|
113
|
+
}
|
|
114
|
+
started = false;
|
|
115
|
+
start;
|
|
116
|
+
session;
|
|
117
|
+
authorizedRequest;
|
|
118
|
+
audioQueue = new AsyncQueue();
|
|
119
|
+
textQueue = new AsyncQueue();
|
|
120
|
+
audioBuffer = [];
|
|
121
|
+
receivedBytes = 0;
|
|
122
|
+
receivedTextBytes = 0;
|
|
123
|
+
controller = new AbortController();
|
|
124
|
+
closed = false;
|
|
125
|
+
/** 消息串行处理链:确保同一连接的消息按到达顺序处理,鉴权完成前不处理业务帧。 */
|
|
126
|
+
queue = Promise.resolve();
|
|
127
|
+
/** 将消息追加到串行处理链,保证按序处理(start 的鉴权完成前不会处理后续业务帧)。 */
|
|
128
|
+
enqueue(ws, data) {
|
|
129
|
+
this.queue = this.queue.then(() => this.handleMessage(ws, data)).catch((error) => {
|
|
130
|
+
logger.debug("audio ws message handling failed", { error: error instanceof Error ? error.message : String(error) });
|
|
131
|
+
if (!this.closed)
|
|
132
|
+
this.failFromError(ws, error);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async handleMessage(ws, data) {
|
|
136
|
+
if (this.closed)
|
|
137
|
+
return;
|
|
138
|
+
const bytes = toBytes(data);
|
|
139
|
+
if (bytes) {
|
|
140
|
+
if (!this.started) {
|
|
141
|
+
this.fail(ws, "hai:ai:050", "audio frame before start");
|
|
51
142
|
return;
|
|
52
|
-
shuttingDown = true;
|
|
53
|
-
try {
|
|
54
|
-
await handle.close();
|
|
55
|
-
} catch {
|
|
56
143
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
144
|
+
if (bytes.length > this.maxMessageBytes) {
|
|
145
|
+
this.fail(ws, "hai:ai:058", "audio frame too large");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.receivedBytes += bytes.length;
|
|
149
|
+
if (this.receivedBytes > this.maxBufferedBytes) {
|
|
150
|
+
this.fail(ws, "hai:ai:058", "buffered audio exceeds limit");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (this.start?.stream)
|
|
154
|
+
this.audioQueue.push(bytes);
|
|
155
|
+
else
|
|
156
|
+
this.audioBuffer.push(bytes);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (typeof data !== "string")
|
|
160
|
+
return;
|
|
161
|
+
if (data.length > this.maxMessageBytes) {
|
|
162
|
+
this.fail(ws, "hai:ai:058", "message too large");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let message;
|
|
166
|
+
try {
|
|
167
|
+
message = JSON.parse(data);
|
|
168
|
+
} catch {
|
|
169
|
+
this.fail(ws, "hai:ai:050", "invalid json message");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (message.type === "start") {
|
|
173
|
+
await this.onStart(ws, message);
|
|
174
|
+
} else if (!this.started) {
|
|
175
|
+
this.fail(ws, "hai:ai:050", "message before start");
|
|
176
|
+
} else if (message.type === "text") {
|
|
177
|
+
this.receivedTextBytes += Buffer.byteLength(message.text, "utf8");
|
|
178
|
+
if (this.receivedTextBytes > this.maxTextBytes) {
|
|
179
|
+
this.fail(ws, "hai:ai:058", "buffered text exceeds limit");
|
|
180
|
+
return;
|
|
60
181
|
}
|
|
182
|
+
this.textQueue.push({ id: message.segmentId, text: message.text });
|
|
183
|
+
} else if (message.type === "done") {
|
|
184
|
+
this.audioQueue.end();
|
|
185
|
+
this.textQueue.end();
|
|
186
|
+
if (this.start && !this.start.stream && this.start.operation === "transcribe")
|
|
187
|
+
await this.runBufferedTranscribe(ws);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async onStart(ws, message) {
|
|
191
|
+
if (this.started) {
|
|
192
|
+
this.fail(ws, "hai:ai:050", "session already started");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!this.ticket) {
|
|
196
|
+
this.fail(ws, "hai:ai:054", "missing audio ticket");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const verified = await this.deps.verifyTicket(this.ticket);
|
|
200
|
+
if (!verified.success) {
|
|
201
|
+
this.fail(ws, String(verified.error.code), verified.error.message);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.session = verified.data;
|
|
205
|
+
const authorized = this.deps.authorize ? await this.deps.authorize(verified.data, message) : { success: true, data: { operation: message.operation, format: message.format } };
|
|
206
|
+
if (!authorized.success) {
|
|
207
|
+
this.fail(ws, String(authorized.error.code), authorized.error.message);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const confirmed = {
|
|
211
|
+
...message,
|
|
212
|
+
operation: authorized.data.operation,
|
|
213
|
+
model: authorized.data.model,
|
|
214
|
+
voice: authorized.data.voice,
|
|
215
|
+
instruction: authorized.data.instruction,
|
|
216
|
+
format: authorized.data.format,
|
|
217
|
+
sampleRate: authorized.data.sampleRate
|
|
61
218
|
};
|
|
62
|
-
|
|
63
|
-
|
|
219
|
+
this.started = true;
|
|
220
|
+
this.start = confirmed;
|
|
221
|
+
this.authorizedRequest = authorized.data;
|
|
222
|
+
if (confirmed.operation === "synthesize")
|
|
223
|
+
void this.runSynthesize(ws, confirmed);
|
|
224
|
+
else if (confirmed.operation === "transcribe" && confirmed.stream)
|
|
225
|
+
void this.runStreamTranscribe(ws, confirmed);
|
|
64
226
|
}
|
|
65
|
-
|
|
227
|
+
/** 流式识别:桥接持续音频输入,流式返回临时/最终结果与语音起止事件 */
|
|
228
|
+
async runStreamTranscribe(ws, start) {
|
|
229
|
+
try {
|
|
230
|
+
const stream = this.deps.ai.audio.transcribeStream({
|
|
231
|
+
audio: {
|
|
232
|
+
chunks: this.audioQueue.iterate(),
|
|
233
|
+
format: start.format ?? "pcm16",
|
|
234
|
+
sampleRate: start.sampleRate ?? 16e3,
|
|
235
|
+
channels: start.channels
|
|
236
|
+
},
|
|
237
|
+
language: start.language,
|
|
238
|
+
contextHints: start.contextHints,
|
|
239
|
+
model: start.model,
|
|
240
|
+
signal: this.controller.signal
|
|
241
|
+
});
|
|
242
|
+
for await (const event of stream) {
|
|
243
|
+
if (event.type === "transcript")
|
|
244
|
+
this.send(ws, { type: "transcript", text: event.text, final: event.final });
|
|
245
|
+
else if (event.type === "speech_started")
|
|
246
|
+
this.send(ws, { type: "speech_started" });
|
|
247
|
+
else if (event.type === "speech_stopped")
|
|
248
|
+
this.send(ws, { type: "speech_stopped" });
|
|
249
|
+
}
|
|
250
|
+
this.end(ws);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.failFromError(ws, error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** 非流式识别:缓冲完整音频后返回单条最终结果 */
|
|
256
|
+
async runBufferedTranscribe(ws) {
|
|
257
|
+
const start = this.start;
|
|
258
|
+
if (!start)
|
|
259
|
+
return;
|
|
260
|
+
try {
|
|
261
|
+
const result = await this.deps.ai.audio.transcribe({
|
|
262
|
+
audio: {
|
|
263
|
+
data: concat(this.audioBuffer),
|
|
264
|
+
format: start.format ?? "wav",
|
|
265
|
+
sampleRate: start.sampleRate,
|
|
266
|
+
channels: start.channels
|
|
267
|
+
},
|
|
268
|
+
language: start.language,
|
|
269
|
+
contextHints: start.contextHints,
|
|
270
|
+
model: start.model,
|
|
271
|
+
signal: this.controller.signal
|
|
272
|
+
});
|
|
273
|
+
if (!result.success) {
|
|
274
|
+
this.fail(ws, String(result.error.code), result.error.message);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.send(ws, { type: "transcript", text: result.data.text, final: true });
|
|
278
|
+
this.end(ws);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
this.failFromError(ws, error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** 语音合成:桥接文本输入,流式返回音频二进制帧 */
|
|
284
|
+
async runSynthesize(ws, start) {
|
|
285
|
+
try {
|
|
286
|
+
const stream = this.deps.ai.audio.synthesizeStream({
|
|
287
|
+
text: this.textQueue.iterate(),
|
|
288
|
+
voice: start.voice,
|
|
289
|
+
instruction: start.instruction,
|
|
290
|
+
format: start.format,
|
|
291
|
+
sampleRate: start.sampleRate,
|
|
292
|
+
model: start.model,
|
|
293
|
+
signal: this.controller.signal
|
|
294
|
+
});
|
|
295
|
+
for await (const event of stream) {
|
|
296
|
+
if (event.type === "audio")
|
|
297
|
+
ws.send(toArrayBuffer(event.data));
|
|
298
|
+
else
|
|
299
|
+
this.send(ws, event);
|
|
300
|
+
}
|
|
301
|
+
this.end(ws);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
this.failFromError(ws, error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
fail(ws, code, message) {
|
|
307
|
+
this.send(ws, { type: "error", code, message });
|
|
308
|
+
this.dispose();
|
|
309
|
+
ws.close();
|
|
310
|
+
}
|
|
311
|
+
failFromError(ws, error) {
|
|
312
|
+
const hai = error;
|
|
313
|
+
const code = typeof hai?.code === "string" ? hai.code : "hai:ai:054";
|
|
314
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
315
|
+
this.fail(ws, code, message);
|
|
316
|
+
}
|
|
317
|
+
end(ws) {
|
|
318
|
+
this.send(ws, { type: "end" });
|
|
319
|
+
this.dispose();
|
|
320
|
+
ws.close();
|
|
321
|
+
}
|
|
322
|
+
send(ws, message) {
|
|
323
|
+
if (!this.closed)
|
|
324
|
+
ws.send(JSON.stringify(message));
|
|
325
|
+
}
|
|
326
|
+
dispose() {
|
|
327
|
+
if (this.closed)
|
|
328
|
+
return;
|
|
329
|
+
this.closed = true;
|
|
330
|
+
this.audioQueue.end();
|
|
331
|
+
this.textQueue.end();
|
|
332
|
+
this.controller.abort();
|
|
333
|
+
if (this.session && this.authorizedRequest && this.deps.onSessionEnd) {
|
|
334
|
+
void Promise.resolve(this.deps.onSessionEnd(this.session, this.authorizedRequest)).catch((error) => {
|
|
335
|
+
logger.warn("audio ws session end hook failed", { error: error instanceof Error ? error.message : String(error) });
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
function toBytes(data) {
|
|
341
|
+
if (typeof data === "string")
|
|
342
|
+
return void 0;
|
|
343
|
+
if (data instanceof ArrayBuffer)
|
|
344
|
+
return new Uint8Array(data);
|
|
345
|
+
if (ArrayBuffer.isView(data))
|
|
346
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
347
|
+
if (Buffer.isBuffer(data))
|
|
348
|
+
return new Uint8Array(data);
|
|
349
|
+
return void 0;
|
|
66
350
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
351
|
+
function concat(chunks) {
|
|
352
|
+
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
353
|
+
const out = new Uint8Array(total);
|
|
354
|
+
let offset = 0;
|
|
355
|
+
for (const chunk of chunks) {
|
|
356
|
+
out.set(chunk, offset);
|
|
357
|
+
offset += chunk.length;
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
function toArrayBuffer(data) {
|
|
362
|
+
const buffer = new ArrayBuffer(data.length);
|
|
363
|
+
new Uint8Array(buffer).set(data);
|
|
364
|
+
return buffer;
|
|
75
365
|
}
|
|
76
366
|
var DEFAULT_LOCALE = "zh-CN";
|
|
77
367
|
function resolveRequestLocale(headers) {
|
|
@@ -293,7 +583,7 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
|
|
|
293
583
|
return {
|
|
294
584
|
cookieName: config.cookieName ?? "hai_refresh_token",
|
|
295
585
|
maxAge: config.maxAge ?? 30 * 24 * 3600,
|
|
296
|
-
isSecure: config.secure ??
|
|
586
|
+
isSecure: config.secure ?? process2.env.NODE_ENV === "production",
|
|
297
587
|
refreshCookiePath: `${apiPrefix}${IAM_AUTH_ROUTES.refresh}`,
|
|
298
588
|
onRefresh
|
|
299
589
|
};
|
|
@@ -479,7 +769,7 @@ function readScalarBrowserEntry(packageInfo) {
|
|
|
479
769
|
const browser = packageInfo.browser;
|
|
480
770
|
return typeof browser === "string" ? browser : void 0;
|
|
481
771
|
}
|
|
482
|
-
var
|
|
772
|
+
var logger2 = core.logger.child({ module: "serv", scope: "transport" });
|
|
483
773
|
var MAX_ENCRYPTED_BODY = 1048576;
|
|
484
774
|
var ENCRYPTABLE_RESPONSE_CONTENT_TYPES = ["application/json"];
|
|
485
775
|
function createTransportMiddleware(manager, keyExchangePath, excludePaths = []) {
|
|
@@ -520,7 +810,7 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [])
|
|
|
520
810
|
try {
|
|
521
811
|
bodyText = await response.clone().text();
|
|
522
812
|
} catch (cause) {
|
|
523
|
-
|
|
813
|
+
logger2.warn("Failed to read response body for encryption", { error: cause });
|
|
524
814
|
c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
|
|
525
815
|
return;
|
|
526
816
|
}
|
|
@@ -532,7 +822,7 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [])
|
|
|
532
822
|
}
|
|
533
823
|
const encResult = await manager.encryptResponse(clientId, bodyText);
|
|
534
824
|
if (!encResult.success) {
|
|
535
|
-
|
|
825
|
+
logger2.warn("Failed to encrypt response", { error: encResult.error });
|
|
536
826
|
c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
|
|
537
827
|
return;
|
|
538
828
|
}
|
|
@@ -564,7 +854,7 @@ async function handleKeyExchange(request, manager) {
|
|
|
564
854
|
headers: { "Content-Type": "application/json; charset=utf-8" }
|
|
565
855
|
});
|
|
566
856
|
} catch (cause) {
|
|
567
|
-
|
|
857
|
+
logger2.error("Key exchange failed", { error: cause });
|
|
568
858
|
return jsonError(servM("serv_transportKeyExchangeFailed"), 500);
|
|
569
859
|
}
|
|
570
860
|
}
|
|
@@ -582,7 +872,7 @@ async function decryptRequestInPlace(c, manager) {
|
|
|
582
872
|
return jsonError(servM("serv_transportInvalidPayload"), 400);
|
|
583
873
|
const decResult = manager.decryptRequest(payload);
|
|
584
874
|
if (!decResult.success) {
|
|
585
|
-
|
|
875
|
+
logger2.warn("Failed to decrypt request", { error: decResult.error });
|
|
586
876
|
return jsonError(servM("serv_transportDecryptFailed"), 400);
|
|
587
877
|
}
|
|
588
878
|
const original = c.req.raw;
|
|
@@ -638,7 +928,9 @@ function isEncryptedPayloadShape(value) {
|
|
|
638
928
|
}
|
|
639
929
|
|
|
640
930
|
// src/serv-app.ts
|
|
931
|
+
var DEFAULT_AUDIO_WS_PATH = "/ai/audio";
|
|
641
932
|
var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
933
|
+
var audioWsInjectors = /* @__PURE__ */ new WeakMap();
|
|
642
934
|
function createApp(options) {
|
|
643
935
|
const http = resolveServHttpConfig(options.http);
|
|
644
936
|
const app = new Hono();
|
|
@@ -672,6 +964,21 @@ function createApp(options) {
|
|
|
672
964
|
mountHealthRoutes(app, http.health);
|
|
673
965
|
if (options.refreshCookie)
|
|
674
966
|
mountRefreshCookieRoutes(app, http.apiPrefix, options.refreshCookie, options.iam);
|
|
967
|
+
if (options.audio) {
|
|
968
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
969
|
+
const audioPath = `${http.apiPrefix}${options.audio.path ?? DEFAULT_AUDIO_WS_PATH}`;
|
|
970
|
+
registerAudioWsRoute(app, audioPath, upgradeWebSocket, {
|
|
971
|
+
ai: options.audio.ai,
|
|
972
|
+
verifyTicket: options.audio.verifyTicket,
|
|
973
|
+
authorize: options.audio.authorize,
|
|
974
|
+
onSessionEnd: options.audio.onSessionEnd,
|
|
975
|
+
maxMessageBytes: options.audio.maxMessageBytes,
|
|
976
|
+
maxBufferedBytes: options.audio.maxBufferedBytes,
|
|
977
|
+
maxTextBytes: options.audio.maxTextBytes,
|
|
978
|
+
maxSessionMs: options.audio.maxSessionMs
|
|
979
|
+
});
|
|
980
|
+
audioWsInjectors.set(app, injectWebSocket);
|
|
981
|
+
}
|
|
675
982
|
mountOpenAPIRoutes(app, options.procedures, http.apiPrefix, createContext);
|
|
676
983
|
if (http.rpc !== false)
|
|
677
984
|
mountRPCRoutes(app, options.procedures, http.rpc, createContext);
|
|
@@ -779,6 +1086,61 @@ function isOrpcValidationErrorPayload(value) {
|
|
|
779
1086
|
return Array.isArray(issues);
|
|
780
1087
|
}
|
|
781
1088
|
|
|
1089
|
+
// src/adapters/serv-adapter-node.ts
|
|
1090
|
+
var DEFAULT_SERV_HOST = "127.0.0.1";
|
|
1091
|
+
var DEFAULT_SERV_PORT = 3e3;
|
|
1092
|
+
function listen(app, options = {}) {
|
|
1093
|
+
const host = options.host ?? process2.env.HOST ?? DEFAULT_SERV_HOST;
|
|
1094
|
+
const port = options.port ?? (process2.env.PORT ? Number(process2.env.PORT) : DEFAULT_SERV_PORT);
|
|
1095
|
+
const server = serve(
|
|
1096
|
+
{ fetch: app.fetch, hostname: host, port },
|
|
1097
|
+
options.onListening
|
|
1098
|
+
);
|
|
1099
|
+
const injectWebSocket = audioWsInjectors.get(app);
|
|
1100
|
+
if (injectWebSocket)
|
|
1101
|
+
injectWebSocket(server);
|
|
1102
|
+
const handle = {
|
|
1103
|
+
server,
|
|
1104
|
+
address: { host, port },
|
|
1105
|
+
close: () => new Promise((resolve, reject) => {
|
|
1106
|
+
server.close((error) => {
|
|
1107
|
+
if (error)
|
|
1108
|
+
reject(error);
|
|
1109
|
+
else resolve();
|
|
1110
|
+
});
|
|
1111
|
+
})
|
|
1112
|
+
};
|
|
1113
|
+
if (options.onClose) {
|
|
1114
|
+
const onClose = options.onClose;
|
|
1115
|
+
let shuttingDown = false;
|
|
1116
|
+
const shutdown = async () => {
|
|
1117
|
+
if (shuttingDown)
|
|
1118
|
+
return;
|
|
1119
|
+
shuttingDown = true;
|
|
1120
|
+
try {
|
|
1121
|
+
await handle.close();
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
await onClose();
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
process2.once("SIGINT", () => void shutdown());
|
|
1130
|
+
process2.once("SIGTERM", () => void shutdown());
|
|
1131
|
+
}
|
|
1132
|
+
return handle;
|
|
1133
|
+
}
|
|
1134
|
+
var WILDCARD_ROLE = "*";
|
|
1135
|
+
function requireRole(role, handler) {
|
|
1136
|
+
return requireAuth(async (options) => {
|
|
1137
|
+
const roles = options.context.session?.roles ?? [];
|
|
1138
|
+
if (!roles.includes(role) && !roles.includes(WILDCARD_ROLE))
|
|
1139
|
+
return err(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale: options.context.locale }));
|
|
1140
|
+
return handler(options);
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
782
1144
|
// src/serv-cors.ts
|
|
783
1145
|
var DEFAULT_CORS_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"];
|
|
784
1146
|
var DEFAULT_CORS_ALLOWED_HEADERS = ["Authorization", "Content-Type", "X-Requested-With"];
|