@123toto/ai-app-assistant-server 0.1.0

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.
@@ -0,0 +1,136 @@
1
+ // src/http.ts
2
+ import {
3
+ askDocumentationRequestSchema
4
+ } from "@123toto/ai-app-assistant-contracts";
5
+ import { ZodError } from "zod";
6
+ var AiDocsRequestError = class extends Error {
7
+ constructor(status, code, message) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = "AiDocsRequestError";
12
+ }
13
+ status;
14
+ code;
15
+ };
16
+ function createAiDocsFetchHandlers(options) {
17
+ const prepare = async (request) => {
18
+ assertPost(request);
19
+ const raw = await readJson(request, options.maxBodyBytes ?? 86e5);
20
+ const validated = askDocumentationRequestSchema.parse(raw);
21
+ if (!options.resolveContext && !options.authorize && !options.allowAnonymous) {
22
+ throw new AiDocsRequestError(401, "unauthorized", "Authentication is required");
23
+ }
24
+ const context = options.resolveContext ? await options.resolveContext(request) : void 0;
25
+ await options.authorize?.(context, request);
26
+ const input = options.transformRequest ? await options.transformRequest(validated, context) : validated;
27
+ return { input: askDocumentationRequestSchema.parse(input), context };
28
+ };
29
+ const present = async (response, context) => options.transformResponse ? options.transformResponse(response, context) : response;
30
+ const ask = async (request) => {
31
+ try {
32
+ const { input, context } = await prepare(request);
33
+ return jsonResponse(await present(await options.assistant.answer(input, {
34
+ signal: request.signal
35
+ }), context));
36
+ } catch (error) {
37
+ return mapError(error, request, options.onError);
38
+ }
39
+ };
40
+ const stream = async (request) => {
41
+ try {
42
+ const { input, context } = await prepare(request);
43
+ const encoder = new TextEncoder();
44
+ const body = new ReadableStream({
45
+ async start(controller) {
46
+ try {
47
+ const generation = options.assistant.stream(input, { signal: request.signal });
48
+ while (true) {
49
+ const next = await generation.next();
50
+ if (next.done) break;
51
+ let event = next.value.type === "complete" ? { ...next.value, response: await present(next.value.response, context) } : next.value;
52
+ if (options.transformStreamEvent) {
53
+ event = await options.transformStreamEvent(event, context);
54
+ }
55
+ controller.enqueue(encoder.encode(`${JSON.stringify(event)}
56
+ `));
57
+ }
58
+ } catch {
59
+ controller.enqueue(encoder.encode(`${JSON.stringify({
60
+ type: "error",
61
+ message: "The assistant response could not be generated.",
62
+ retryable: false
63
+ })}
64
+ `));
65
+ } finally {
66
+ controller.close();
67
+ }
68
+ }
69
+ });
70
+ return new Response(body, {
71
+ status: 200,
72
+ headers: {
73
+ "cache-control": "no-store",
74
+ "content-type": "application/x-ndjson; charset=utf-8",
75
+ "x-content-type-options": "nosniff"
76
+ }
77
+ });
78
+ } catch (error) {
79
+ return mapError(error, request, options.onError);
80
+ }
81
+ };
82
+ return {
83
+ ask,
84
+ stream,
85
+ handle: (request) => new URL(request.url).pathname.endsWith("/stream") ? stream(request) : ask(request)
86
+ };
87
+ }
88
+ function assertPost(request) {
89
+ if (request.method !== "POST") {
90
+ throw new AiDocsRequestError(405, "method_not_allowed", "Only POST is supported");
91
+ }
92
+ }
93
+ async function readJson(request, maxBodyBytes) {
94
+ const contentLength = Number(request.headers.get("content-length"));
95
+ if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
96
+ throw new AiDocsRequestError(413, "request_too_large", "Request body is too large");
97
+ }
98
+ const text = await request.text();
99
+ if (new TextEncoder().encode(text).byteLength > maxBodyBytes) {
100
+ throw new AiDocsRequestError(413, "request_too_large", "Request body is too large");
101
+ }
102
+ try {
103
+ return JSON.parse(text);
104
+ } catch {
105
+ throw new AiDocsRequestError(400, "invalid_json", "Request body must be valid JSON");
106
+ }
107
+ }
108
+ async function mapError(error, request, customMapper) {
109
+ if (customMapper) return customMapper(error, request);
110
+ if (error instanceof AiDocsRequestError) {
111
+ return jsonResponse({ error: error.code, message: error.message }, error.status);
112
+ }
113
+ if (error instanceof ZodError) {
114
+ return jsonResponse({ error: "invalid_request", message: "The assistant request is invalid." }, 400);
115
+ }
116
+ return jsonResponse({
117
+ error: "assistant_error",
118
+ message: "The assistant response could not be generated."
119
+ }, 500);
120
+ }
121
+ function jsonResponse(value, status = 200) {
122
+ return new Response(JSON.stringify(value), {
123
+ status,
124
+ headers: {
125
+ "cache-control": "no-store",
126
+ "content-type": "application/json; charset=utf-8",
127
+ "x-content-type-options": "nosniff"
128
+ }
129
+ });
130
+ }
131
+
132
+ export {
133
+ AiDocsRequestError,
134
+ createAiDocsFetchHandlers
135
+ };
136
+ //# sourceMappingURL=chunk-OA7OXUK7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http.ts"],"sourcesContent":["import {\n askDocumentationRequestSchema,\n type AskDocumentationRequest,\n type AskDocumentationResponse\n} from \"@123toto/ai-app-assistant-contracts\";\nimport { ZodError } from \"zod\";\nimport type { DocsAssistant, DocsAssistantStreamEvent } from \"./assistant.js\";\n\nexport interface AiDocsFetchHandlerOptions<TContext = undefined> {\n assistant: DocsAssistant;\n /** Resolves framework-specific authentication into an application context. */\n resolveContext?: (request: Request) => Promise<TContext> | TContext;\n /** Optional application authorization executed before assistant work. */\n authorize?: (context: TContext, request: Request) => Promise<void> | void;\n /** Explicit opt-in for public prototypes without authentication hooks. */\n allowAnonymous?: boolean;\n /** Optional privacy/input policy applied after protocol validation. */\n transformRequest?: (\n input: AskDocumentationRequest,\n context: TContext\n ) => Promise<AskDocumentationRequest> | AskDocumentationRequest;\n /** Optional privacy/output policy applied before serialization. */\n transformResponse?: (\n response: AskDocumentationResponse,\n context: TContext\n ) => Promise<AskDocumentationResponse> | AskDocumentationResponse;\n /** Applies privacy rules to progressive text before it leaves the backend. */\n transformStreamEvent?: (\n event: DocsAssistantStreamEvent,\n context: TContext\n ) => Promise<DocsAssistantStreamEvent> | DocsAssistantStreamEvent;\n /** Maximum HTTP request size. Defaults slightly above the protocol limit. */\n maxBodyBytes?: number;\n /** Maps application errors without coupling the library to one framework. */\n onError?: (error: unknown, request: Request) => Promise<Response> | Response;\n}\n\nexport interface AiDocsFetchHandlers {\n ask(request: Request): Promise<Response>;\n stream(request: Request): Promise<Response>;\n /** Dispatches to `stream` when the pathname ends in `/stream`, otherwise to `ask`. */\n handle(request: Request): Promise<Response>;\n}\n\n/** Explicit safe HTTP error that host authorization hooks can throw. */\nexport class AiDocsRequestError extends Error {\n public constructor(\n readonly status: number,\n readonly code: string,\n message: string\n ) {\n super(message);\n this.name = \"AiDocsRequestError\";\n }\n}\n\n/**\n * Creates Fetch API handlers usable by standards-based runtimes and thin\n * Express, Fastify, Nest, Next.js, Hono, Bun or serverless adapters.\n */\nexport function createAiDocsFetchHandlers<TContext = undefined>(\n options: AiDocsFetchHandlerOptions<TContext>\n): AiDocsFetchHandlers {\n const prepare = async (request: Request): Promise<{\n input: AskDocumentationRequest;\n context: TContext;\n }> => {\n assertPost(request);\n const raw = await readJson(request, options.maxBodyBytes ?? 8_600_000);\n const validated = askDocumentationRequestSchema.parse(raw);\n if (!options.resolveContext && !options.authorize && !options.allowAnonymous) {\n throw new AiDocsRequestError(401, \"unauthorized\", \"Authentication is required\");\n }\n const context = options.resolveContext\n ? await options.resolveContext(request)\n : undefined as TContext;\n await options.authorize?.(context, request);\n const input = options.transformRequest\n ? await options.transformRequest(validated, context)\n : validated;\n return { input: askDocumentationRequestSchema.parse(input), context };\n };\n\n const present = async (\n response: AskDocumentationResponse,\n context: TContext\n ): Promise<AskDocumentationResponse> => options.transformResponse\n ? options.transformResponse(response, context)\n : response;\n\n const ask = async (request: Request): Promise<Response> => {\n try {\n const { input, context } = await prepare(request);\n return jsonResponse(await present(await options.assistant.answer(input, {\n signal: request.signal\n }), context));\n } catch (error) {\n return mapError(error, request, options.onError);\n }\n };\n\n const stream = async (request: Request): Promise<Response> => {\n try {\n const { input, context } = await prepare(request);\n const encoder = new TextEncoder();\n const body = new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n const generation = options.assistant.stream(input, { signal: request.signal });\n while (true) {\n const next = await generation.next();\n if (next.done) break;\n let event = next.value.type === \"complete\"\n ? { ...next.value, response: await present(next.value.response, context) }\n : next.value;\n if (options.transformStreamEvent) {\n event = await options.transformStreamEvent(event, context);\n }\n controller.enqueue(encoder.encode(`${JSON.stringify(event)}\\n`));\n }\n } catch {\n controller.enqueue(encoder.encode(`${JSON.stringify({\n type: \"error\",\n message: \"The assistant response could not be generated.\",\n retryable: false\n })}\\n`));\n } finally {\n controller.close();\n }\n }\n });\n return new Response(body, {\n status: 200,\n headers: {\n \"cache-control\": \"no-store\",\n \"content-type\": \"application/x-ndjson; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\"\n }\n });\n } catch (error) {\n return mapError(error, request, options.onError);\n }\n };\n\n return {\n ask,\n stream,\n handle: (request) => new URL(request.url).pathname.endsWith(\"/stream\")\n ? stream(request)\n : ask(request)\n };\n}\n\nfunction assertPost(request: Request): void {\n if (request.method !== \"POST\") {\n throw new AiDocsRequestError(405, \"method_not_allowed\", \"Only POST is supported\");\n }\n}\n\nasync function readJson(request: Request, maxBodyBytes: number): Promise<unknown> {\n const contentLength = Number(request.headers.get(\"content-length\"));\n if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {\n throw new AiDocsRequestError(413, \"request_too_large\", \"Request body is too large\");\n }\n const text = await request.text();\n if (new TextEncoder().encode(text).byteLength > maxBodyBytes) {\n throw new AiDocsRequestError(413, \"request_too_large\", \"Request body is too large\");\n }\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw new AiDocsRequestError(400, \"invalid_json\", \"Request body must be valid JSON\");\n }\n}\n\nasync function mapError(\n error: unknown,\n request: Request,\n customMapper?: AiDocsFetchHandlerOptions<unknown>[\"onError\"]\n): Promise<Response> {\n if (customMapper) return customMapper(error, request);\n if (error instanceof AiDocsRequestError) {\n return jsonResponse({ error: error.code, message: error.message }, error.status);\n }\n if (error instanceof ZodError) {\n return jsonResponse({ error: \"invalid_request\", message: \"The assistant request is invalid.\" }, 400);\n }\n return jsonResponse({\n error: \"assistant_error\",\n message: \"The assistant response could not be generated.\"\n }, 500);\n}\n\nfunction jsonResponse(value: unknown, status = 200): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: {\n \"cache-control\": \"no-store\",\n \"content-type\": \"application/json; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\"\n }\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/express.ts
21
+ var express_exports = {};
22
+ __export(express_exports, {
23
+ createManagedAiDocsExpressHandler: () => createManagedAiDocsExpressHandler
24
+ });
25
+ module.exports = __toCommonJS(express_exports);
26
+
27
+ // src/http.ts
28
+ var import_ai_app_assistant_contracts = require("@123toto/ai-app-assistant-contracts");
29
+ var import_zod = require("zod");
30
+ var AiDocsRequestError = class extends Error {
31
+ constructor(status, code, message) {
32
+ super(message);
33
+ this.status = status;
34
+ this.code = code;
35
+ this.name = "AiDocsRequestError";
36
+ }
37
+ status;
38
+ code;
39
+ };
40
+
41
+ // src/express.ts
42
+ function createManagedAiDocsExpressHandler(server, options = {}) {
43
+ return async (request, response, next) => {
44
+ const abortController = new AbortController();
45
+ const abort = () => abortController.abort(new DOMException("Client disconnected", "AbortError"));
46
+ request.on?.("close", abort);
47
+ try {
48
+ const webRequest = await toFetchRequest(request, abortController.signal, options);
49
+ const result = await server.fetch.handle(webRequest, request);
50
+ if (options.fallthrough && result.status === 404 && next) {
51
+ next();
52
+ return;
53
+ }
54
+ await writeFetchResponse(response, result);
55
+ } catch (error) {
56
+ if (next) {
57
+ next(error);
58
+ return;
59
+ }
60
+ writeConnectorError(response, error);
61
+ } finally {
62
+ request.off?.("close", abort);
63
+ }
64
+ };
65
+ }
66
+ async function toFetchRequest(request, signal, options) {
67
+ const headers = new Headers();
68
+ for (const [name, value] of Object.entries(request.headers)) {
69
+ if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));
70
+ else if (value !== void 0) headers.set(name, value);
71
+ }
72
+ const method = request.method?.toUpperCase() || "GET";
73
+ const host = headers.get("host");
74
+ const protocol = request.protocol || headers.get("x-forwarded-proto") || "http";
75
+ const origin = options.origin ?? `${protocol}://${host || "localhost"}`;
76
+ const url = new URL(request.originalUrl || request.url || "/", origin);
77
+ const body = method === "GET" || method === "HEAD" ? void 0 : request.body !== void 0 ? serializeBody(request.body, headers) : await readBody(request, options.maxBodyBytes ?? 86e5);
78
+ return new Request(url, {
79
+ method,
80
+ headers,
81
+ signal,
82
+ ...body !== void 0 ? { body } : {}
83
+ });
84
+ }
85
+ function serializeBody(body, headers) {
86
+ if (typeof body === "string") return body;
87
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
88
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
89
+ return JSON.stringify(body) ?? "null";
90
+ }
91
+ async function readBody(request, maxBodyBytes) {
92
+ if (!(Symbol.asyncIterator in request)) return void 0;
93
+ const chunks = [];
94
+ let size = 0;
95
+ for await (const chunk of request) {
96
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : new Uint8Array(chunk);
97
+ size += bytes.byteLength;
98
+ if (size > maxBodyBytes) {
99
+ throw new AiDocsRequestError(413, "request_too_large", "Request body is too large");
100
+ }
101
+ chunks.push(bytes);
102
+ }
103
+ return chunks.length ? Buffer.concat(chunks).toString("utf8") : void 0;
104
+ }
105
+ async function writeFetchResponse(response, result) {
106
+ response.statusCode = result.status;
107
+ result.headers.forEach((value, name) => response.setHeader(name, value));
108
+ if (!result.body) {
109
+ response.end();
110
+ return;
111
+ }
112
+ const reader = result.body.getReader();
113
+ try {
114
+ while (true) {
115
+ const { done, value } = await reader.read();
116
+ if (done) break;
117
+ if (!response.write(value) && response.once) {
118
+ await new Promise((resolve) => response.once("drain", resolve));
119
+ }
120
+ }
121
+ if (!response.destroyed) response.end();
122
+ } finally {
123
+ reader.releaseLock();
124
+ }
125
+ }
126
+ function writeConnectorError(response, error) {
127
+ const status = error instanceof AiDocsRequestError ? error.status : 500;
128
+ const code = error instanceof AiDocsRequestError ? error.code : "assistant_error";
129
+ response.statusCode = status;
130
+ response.setHeader("content-type", "application/json; charset=utf-8");
131
+ response.end(JSON.stringify({ error: code }));
132
+ }
133
+ // Annotate the CommonJS export names for ESM import in node:
134
+ 0 && (module.exports = {
135
+ createManagedAiDocsExpressHandler
136
+ });
137
+ //# sourceMappingURL=express.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/express.ts","../src/http.ts"],"sourcesContent":["import { AiDocsRequestError } from \"./http.js\";\nimport type { ManagedAiDocsServer } from \"./managed-server.js\";\n\n/** Minimal Express request shape; importing Express types is intentionally unnecessary. */\nexport interface AiDocsExpressRequest {\n method?: string;\n originalUrl?: string;\n url?: string;\n protocol?: string;\n headers: Record<string, string | string[] | undefined>;\n body?: unknown;\n on?(event: string, listener: () => void): unknown;\n off?(event: string, listener: () => void): unknown;\n [key: string]: unknown;\n}\n\n/** Minimal Express response shape used by the connector. */\nexport interface AiDocsExpressResponse {\n statusCode: number;\n setHeader(name: string, value: string): unknown;\n write(chunk: Uint8Array): boolean;\n end(chunk?: string): unknown;\n once?(event: string, listener: () => void): unknown;\n destroyed?: boolean;\n}\n\nexport type AiDocsExpressNext = (error?: unknown) => void;\n\nexport type AiDocsExpressManagedServer<\n TRequest extends AiDocsExpressRequest = AiDocsExpressRequest\n> = Pick<ManagedAiDocsServer<{ id: string; label: string }, TRequest>, \"fetch\">;\n\nexport interface AiDocsExpressHandlerOptions {\n /** Origin used only when Express does not expose a Host header. */\n origin?: string;\n /** Maximum size accepted when no JSON body parser populated request.body. */\n maxBodyBytes?: number;\n /** Lets another Express handler process unknown routes below the mounted prefix. */\n fallthrough?: boolean;\n}\n\nexport type AiDocsExpressHandler<TRequest extends AiDocsExpressRequest> = (\n request: TRequest,\n response: AiDocsExpressResponse,\n next?: AiDocsExpressNext\n) => Promise<void>;\n\n/**\n * Creates an Express-compatible handler for the complete managed API.\n *\n * @example\n * `app.use(\"/api/ai-docs\", createManagedAiDocsExpressHandler(aiDocs));`\n */\nexport function createManagedAiDocsExpressHandler<\n TRequest extends AiDocsExpressRequest = AiDocsExpressRequest\n>(\n server: AiDocsExpressManagedServer<TRequest>,\n options: AiDocsExpressHandlerOptions = {}\n): AiDocsExpressHandler<TRequest> {\n return async (request, response, next) => {\n const abortController = new AbortController();\n const abort = (): void => abortController.abort(new DOMException(\"Client disconnected\", \"AbortError\"));\n request.on?.(\"close\", abort);\n try {\n const webRequest = await toFetchRequest(request, abortController.signal, options);\n const result = await server.fetch.handle(webRequest, request);\n if (options.fallthrough && result.status === 404 && next) {\n next();\n return;\n }\n await writeFetchResponse(response, result);\n } catch (error) {\n if (next) {\n next(error);\n return;\n }\n writeConnectorError(response, error);\n } finally {\n request.off?.(\"close\", abort);\n }\n };\n}\n\nasync function toFetchRequest(\n request: AiDocsExpressRequest,\n signal: AbortSignal,\n options: AiDocsExpressHandlerOptions\n): Promise<Request> {\n const headers = new Headers();\n for (const [name, value] of Object.entries(request.headers)) {\n if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));\n else if (value !== undefined) headers.set(name, value);\n }\n const method = request.method?.toUpperCase() || \"GET\";\n const host = headers.get(\"host\");\n const protocol = request.protocol || headers.get(\"x-forwarded-proto\") || \"http\";\n const origin = options.origin ?? `${protocol}://${host || \"localhost\"}`;\n // originalUrl preserves the mount prefix stripped from request.url by Express.\n const url = new URL(request.originalUrl || request.url || \"/\", origin);\n const body = method === \"GET\" || method === \"HEAD\"\n ? undefined\n : request.body !== undefined\n ? serializeBody(request.body, headers)\n : await readBody(request, options.maxBodyBytes ?? 8_600_000);\n return new Request(url, {\n method,\n headers,\n signal,\n ...(body !== undefined ? { body } : {})\n });\n}\n\nfunction serializeBody(body: unknown, headers: Headers): string {\n if (typeof body === \"string\") return body;\n if (body instanceof Uint8Array) return new TextDecoder().decode(body);\n if (!headers.has(\"content-type\")) headers.set(\"content-type\", \"application/json\");\n return JSON.stringify(body) ?? \"null\";\n}\n\nasync function readBody(request: AiDocsExpressRequest, maxBodyBytes: number): Promise<string | undefined> {\n if (!(Symbol.asyncIterator in request)) return undefined;\n const chunks: Uint8Array[] = [];\n let size = 0;\n for await (const chunk of request as AsyncIterable<string | Uint8Array>) {\n const bytes = typeof chunk === \"string\" ? Buffer.from(chunk) : new Uint8Array(chunk);\n size += bytes.byteLength;\n if (size > maxBodyBytes) {\n throw new AiDocsRequestError(413, \"request_too_large\", \"Request body is too large\");\n }\n chunks.push(bytes);\n }\n return chunks.length ? Buffer.concat(chunks).toString(\"utf8\") : undefined;\n}\n\nasync function writeFetchResponse(response: AiDocsExpressResponse, result: Response): Promise<void> {\n response.statusCode = result.status;\n result.headers.forEach((value, name) => response.setHeader(name, value));\n if (!result.body) {\n response.end();\n return;\n }\n const reader = result.body.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!response.write(value) && response.once) {\n await new Promise<void>((resolve) => response.once!(\"drain\", resolve));\n }\n }\n if (!response.destroyed) response.end();\n } finally {\n reader.releaseLock();\n }\n}\n\nfunction writeConnectorError(response: AiDocsExpressResponse, error: unknown): void {\n const status = error instanceof AiDocsRequestError ? error.status : 500;\n const code = error instanceof AiDocsRequestError ? error.code : \"assistant_error\";\n response.statusCode = status;\n response.setHeader(\"content-type\", \"application/json; charset=utf-8\");\n response.end(JSON.stringify({ error: code }));\n}\n","import {\n askDocumentationRequestSchema,\n type AskDocumentationRequest,\n type AskDocumentationResponse\n} from \"@123toto/ai-app-assistant-contracts\";\nimport { ZodError } from \"zod\";\nimport type { DocsAssistant, DocsAssistantStreamEvent } from \"./assistant.js\";\n\nexport interface AiDocsFetchHandlerOptions<TContext = undefined> {\n assistant: DocsAssistant;\n /** Resolves framework-specific authentication into an application context. */\n resolveContext?: (request: Request) => Promise<TContext> | TContext;\n /** Optional application authorization executed before assistant work. */\n authorize?: (context: TContext, request: Request) => Promise<void> | void;\n /** Explicit opt-in for public prototypes without authentication hooks. */\n allowAnonymous?: boolean;\n /** Optional privacy/input policy applied after protocol validation. */\n transformRequest?: (\n input: AskDocumentationRequest,\n context: TContext\n ) => Promise<AskDocumentationRequest> | AskDocumentationRequest;\n /** Optional privacy/output policy applied before serialization. */\n transformResponse?: (\n response: AskDocumentationResponse,\n context: TContext\n ) => Promise<AskDocumentationResponse> | AskDocumentationResponse;\n /** Applies privacy rules to progressive text before it leaves the backend. */\n transformStreamEvent?: (\n event: DocsAssistantStreamEvent,\n context: TContext\n ) => Promise<DocsAssistantStreamEvent> | DocsAssistantStreamEvent;\n /** Maximum HTTP request size. Defaults slightly above the protocol limit. */\n maxBodyBytes?: number;\n /** Maps application errors without coupling the library to one framework. */\n onError?: (error: unknown, request: Request) => Promise<Response> | Response;\n}\n\nexport interface AiDocsFetchHandlers {\n ask(request: Request): Promise<Response>;\n stream(request: Request): Promise<Response>;\n /** Dispatches to `stream` when the pathname ends in `/stream`, otherwise to `ask`. */\n handle(request: Request): Promise<Response>;\n}\n\n/** Explicit safe HTTP error that host authorization hooks can throw. */\nexport class AiDocsRequestError extends Error {\n public constructor(\n readonly status: number,\n readonly code: string,\n message: string\n ) {\n super(message);\n this.name = \"AiDocsRequestError\";\n }\n}\n\n/**\n * Creates Fetch API handlers usable by standards-based runtimes and thin\n * Express, Fastify, Nest, Next.js, Hono, Bun or serverless adapters.\n */\nexport function createAiDocsFetchHandlers<TContext = undefined>(\n options: AiDocsFetchHandlerOptions<TContext>\n): AiDocsFetchHandlers {\n const prepare = async (request: Request): Promise<{\n input: AskDocumentationRequest;\n context: TContext;\n }> => {\n assertPost(request);\n const raw = await readJson(request, options.maxBodyBytes ?? 8_600_000);\n const validated = askDocumentationRequestSchema.parse(raw);\n if (!options.resolveContext && !options.authorize && !options.allowAnonymous) {\n throw new AiDocsRequestError(401, \"unauthorized\", \"Authentication is required\");\n }\n const context = options.resolveContext\n ? await options.resolveContext(request)\n : undefined as TContext;\n await options.authorize?.(context, request);\n const input = options.transformRequest\n ? await options.transformRequest(validated, context)\n : validated;\n return { input: askDocumentationRequestSchema.parse(input), context };\n };\n\n const present = async (\n response: AskDocumentationResponse,\n context: TContext\n ): Promise<AskDocumentationResponse> => options.transformResponse\n ? options.transformResponse(response, context)\n : response;\n\n const ask = async (request: Request): Promise<Response> => {\n try {\n const { input, context } = await prepare(request);\n return jsonResponse(await present(await options.assistant.answer(input, {\n signal: request.signal\n }), context));\n } catch (error) {\n return mapError(error, request, options.onError);\n }\n };\n\n const stream = async (request: Request): Promise<Response> => {\n try {\n const { input, context } = await prepare(request);\n const encoder = new TextEncoder();\n const body = new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n const generation = options.assistant.stream(input, { signal: request.signal });\n while (true) {\n const next = await generation.next();\n if (next.done) break;\n let event = next.value.type === \"complete\"\n ? { ...next.value, response: await present(next.value.response, context) }\n : next.value;\n if (options.transformStreamEvent) {\n event = await options.transformStreamEvent(event, context);\n }\n controller.enqueue(encoder.encode(`${JSON.stringify(event)}\\n`));\n }\n } catch {\n controller.enqueue(encoder.encode(`${JSON.stringify({\n type: \"error\",\n message: \"The assistant response could not be generated.\",\n retryable: false\n })}\\n`));\n } finally {\n controller.close();\n }\n }\n });\n return new Response(body, {\n status: 200,\n headers: {\n \"cache-control\": \"no-store\",\n \"content-type\": \"application/x-ndjson; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\"\n }\n });\n } catch (error) {\n return mapError(error, request, options.onError);\n }\n };\n\n return {\n ask,\n stream,\n handle: (request) => new URL(request.url).pathname.endsWith(\"/stream\")\n ? stream(request)\n : ask(request)\n };\n}\n\nfunction assertPost(request: Request): void {\n if (request.method !== \"POST\") {\n throw new AiDocsRequestError(405, \"method_not_allowed\", \"Only POST is supported\");\n }\n}\n\nasync function readJson(request: Request, maxBodyBytes: number): Promise<unknown> {\n const contentLength = Number(request.headers.get(\"content-length\"));\n if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {\n throw new AiDocsRequestError(413, \"request_too_large\", \"Request body is too large\");\n }\n const text = await request.text();\n if (new TextEncoder().encode(text).byteLength > maxBodyBytes) {\n throw new AiDocsRequestError(413, \"request_too_large\", \"Request body is too large\");\n }\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw new AiDocsRequestError(400, \"invalid_json\", \"Request body must be valid JSON\");\n }\n}\n\nasync function mapError(\n error: unknown,\n request: Request,\n customMapper?: AiDocsFetchHandlerOptions<unknown>[\"onError\"]\n): Promise<Response> {\n if (customMapper) return customMapper(error, request);\n if (error instanceof AiDocsRequestError) {\n return jsonResponse({ error: error.code, message: error.message }, error.status);\n }\n if (error instanceof ZodError) {\n return jsonResponse({ error: \"invalid_request\", message: \"The assistant request is invalid.\" }, 400);\n }\n return jsonResponse({\n error: \"assistant_error\",\n message: \"The assistant response could not be generated.\"\n }, 500);\n}\n\nfunction jsonResponse(value: unknown, status = 200): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: {\n \"cache-control\": \"no-store\",\n \"content-type\": \"application/json; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\"\n }\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,49 @@
1
+ import { M as ManagedAiDocsServer } from './managed-server-7iurKxF1.cjs';
2
+ import './ai-sdk-QImICd56.cjs';
3
+ import 'ai';
4
+ import '@123toto/ai-app-assistant-contracts';
5
+
6
+ /** Minimal Express request shape; importing Express types is intentionally unnecessary. */
7
+ interface AiDocsExpressRequest {
8
+ method?: string;
9
+ originalUrl?: string;
10
+ url?: string;
11
+ protocol?: string;
12
+ headers: Record<string, string | string[] | undefined>;
13
+ body?: unknown;
14
+ on?(event: string, listener: () => void): unknown;
15
+ off?(event: string, listener: () => void): unknown;
16
+ [key: string]: unknown;
17
+ }
18
+ /** Minimal Express response shape used by the connector. */
19
+ interface AiDocsExpressResponse {
20
+ statusCode: number;
21
+ setHeader(name: string, value: string): unknown;
22
+ write(chunk: Uint8Array): boolean;
23
+ end(chunk?: string): unknown;
24
+ once?(event: string, listener: () => void): unknown;
25
+ destroyed?: boolean;
26
+ }
27
+ type AiDocsExpressNext = (error?: unknown) => void;
28
+ type AiDocsExpressManagedServer<TRequest extends AiDocsExpressRequest = AiDocsExpressRequest> = Pick<ManagedAiDocsServer<{
29
+ id: string;
30
+ label: string;
31
+ }, TRequest>, "fetch">;
32
+ interface AiDocsExpressHandlerOptions {
33
+ /** Origin used only when Express does not expose a Host header. */
34
+ origin?: string;
35
+ /** Maximum size accepted when no JSON body parser populated request.body. */
36
+ maxBodyBytes?: number;
37
+ /** Lets another Express handler process unknown routes below the mounted prefix. */
38
+ fallthrough?: boolean;
39
+ }
40
+ type AiDocsExpressHandler<TRequest extends AiDocsExpressRequest> = (request: TRequest, response: AiDocsExpressResponse, next?: AiDocsExpressNext) => Promise<void>;
41
+ /**
42
+ * Creates an Express-compatible handler for the complete managed API.
43
+ *
44
+ * @example
45
+ * `app.use("/api/ai-docs", createManagedAiDocsExpressHandler(aiDocs));`
46
+ */
47
+ declare function createManagedAiDocsExpressHandler<TRequest extends AiDocsExpressRequest = AiDocsExpressRequest>(server: AiDocsExpressManagedServer<TRequest>, options?: AiDocsExpressHandlerOptions): AiDocsExpressHandler<TRequest>;
48
+
49
+ export { type AiDocsExpressHandler, type AiDocsExpressHandlerOptions, type AiDocsExpressManagedServer, type AiDocsExpressNext, type AiDocsExpressRequest, type AiDocsExpressResponse, createManagedAiDocsExpressHandler };
@@ -0,0 +1,49 @@
1
+ import { M as ManagedAiDocsServer } from './managed-server-CrZumvVU.js';
2
+ import './ai-sdk-QImICd56.js';
3
+ import 'ai';
4
+ import '@123toto/ai-app-assistant-contracts';
5
+
6
+ /** Minimal Express request shape; importing Express types is intentionally unnecessary. */
7
+ interface AiDocsExpressRequest {
8
+ method?: string;
9
+ originalUrl?: string;
10
+ url?: string;
11
+ protocol?: string;
12
+ headers: Record<string, string | string[] | undefined>;
13
+ body?: unknown;
14
+ on?(event: string, listener: () => void): unknown;
15
+ off?(event: string, listener: () => void): unknown;
16
+ [key: string]: unknown;
17
+ }
18
+ /** Minimal Express response shape used by the connector. */
19
+ interface AiDocsExpressResponse {
20
+ statusCode: number;
21
+ setHeader(name: string, value: string): unknown;
22
+ write(chunk: Uint8Array): boolean;
23
+ end(chunk?: string): unknown;
24
+ once?(event: string, listener: () => void): unknown;
25
+ destroyed?: boolean;
26
+ }
27
+ type AiDocsExpressNext = (error?: unknown) => void;
28
+ type AiDocsExpressManagedServer<TRequest extends AiDocsExpressRequest = AiDocsExpressRequest> = Pick<ManagedAiDocsServer<{
29
+ id: string;
30
+ label: string;
31
+ }, TRequest>, "fetch">;
32
+ interface AiDocsExpressHandlerOptions {
33
+ /** Origin used only when Express does not expose a Host header. */
34
+ origin?: string;
35
+ /** Maximum size accepted when no JSON body parser populated request.body. */
36
+ maxBodyBytes?: number;
37
+ /** Lets another Express handler process unknown routes below the mounted prefix. */
38
+ fallthrough?: boolean;
39
+ }
40
+ type AiDocsExpressHandler<TRequest extends AiDocsExpressRequest> = (request: TRequest, response: AiDocsExpressResponse, next?: AiDocsExpressNext) => Promise<void>;
41
+ /**
42
+ * Creates an Express-compatible handler for the complete managed API.
43
+ *
44
+ * @example
45
+ * `app.use("/api/ai-docs", createManagedAiDocsExpressHandler(aiDocs));`
46
+ */
47
+ declare function createManagedAiDocsExpressHandler<TRequest extends AiDocsExpressRequest = AiDocsExpressRequest>(server: AiDocsExpressManagedServer<TRequest>, options?: AiDocsExpressHandlerOptions): AiDocsExpressHandler<TRequest>;
48
+
49
+ export { type AiDocsExpressHandler, type AiDocsExpressHandlerOptions, type AiDocsExpressManagedServer, type AiDocsExpressNext, type AiDocsExpressRequest, type AiDocsExpressResponse, createManagedAiDocsExpressHandler };
@@ -0,0 +1,100 @@
1
+ import {
2
+ AiDocsRequestError
3
+ } from "./chunk-OA7OXUK7.js";
4
+
5
+ // src/express.ts
6
+ function createManagedAiDocsExpressHandler(server, options = {}) {
7
+ return async (request, response, next) => {
8
+ const abortController = new AbortController();
9
+ const abort = () => abortController.abort(new DOMException("Client disconnected", "AbortError"));
10
+ request.on?.("close", abort);
11
+ try {
12
+ const webRequest = await toFetchRequest(request, abortController.signal, options);
13
+ const result = await server.fetch.handle(webRequest, request);
14
+ if (options.fallthrough && result.status === 404 && next) {
15
+ next();
16
+ return;
17
+ }
18
+ await writeFetchResponse(response, result);
19
+ } catch (error) {
20
+ if (next) {
21
+ next(error);
22
+ return;
23
+ }
24
+ writeConnectorError(response, error);
25
+ } finally {
26
+ request.off?.("close", abort);
27
+ }
28
+ };
29
+ }
30
+ async function toFetchRequest(request, signal, options) {
31
+ const headers = new Headers();
32
+ for (const [name, value] of Object.entries(request.headers)) {
33
+ if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));
34
+ else if (value !== void 0) headers.set(name, value);
35
+ }
36
+ const method = request.method?.toUpperCase() || "GET";
37
+ const host = headers.get("host");
38
+ const protocol = request.protocol || headers.get("x-forwarded-proto") || "http";
39
+ const origin = options.origin ?? `${protocol}://${host || "localhost"}`;
40
+ const url = new URL(request.originalUrl || request.url || "/", origin);
41
+ const body = method === "GET" || method === "HEAD" ? void 0 : request.body !== void 0 ? serializeBody(request.body, headers) : await readBody(request, options.maxBodyBytes ?? 86e5);
42
+ return new Request(url, {
43
+ method,
44
+ headers,
45
+ signal,
46
+ ...body !== void 0 ? { body } : {}
47
+ });
48
+ }
49
+ function serializeBody(body, headers) {
50
+ if (typeof body === "string") return body;
51
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
52
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
53
+ return JSON.stringify(body) ?? "null";
54
+ }
55
+ async function readBody(request, maxBodyBytes) {
56
+ if (!(Symbol.asyncIterator in request)) return void 0;
57
+ const chunks = [];
58
+ let size = 0;
59
+ for await (const chunk of request) {
60
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : new Uint8Array(chunk);
61
+ size += bytes.byteLength;
62
+ if (size > maxBodyBytes) {
63
+ throw new AiDocsRequestError(413, "request_too_large", "Request body is too large");
64
+ }
65
+ chunks.push(bytes);
66
+ }
67
+ return chunks.length ? Buffer.concat(chunks).toString("utf8") : void 0;
68
+ }
69
+ async function writeFetchResponse(response, result) {
70
+ response.statusCode = result.status;
71
+ result.headers.forEach((value, name) => response.setHeader(name, value));
72
+ if (!result.body) {
73
+ response.end();
74
+ return;
75
+ }
76
+ const reader = result.body.getReader();
77
+ try {
78
+ while (true) {
79
+ const { done, value } = await reader.read();
80
+ if (done) break;
81
+ if (!response.write(value) && response.once) {
82
+ await new Promise((resolve) => response.once("drain", resolve));
83
+ }
84
+ }
85
+ if (!response.destroyed) response.end();
86
+ } finally {
87
+ reader.releaseLock();
88
+ }
89
+ }
90
+ function writeConnectorError(response, error) {
91
+ const status = error instanceof AiDocsRequestError ? error.status : 500;
92
+ const code = error instanceof AiDocsRequestError ? error.code : "assistant_error";
93
+ response.statusCode = status;
94
+ response.setHeader("content-type", "application/json; charset=utf-8");
95
+ response.end(JSON.stringify({ error: code }));
96
+ }
97
+ export {
98
+ createManagedAiDocsExpressHandler
99
+ };
100
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/express.ts"],"sourcesContent":["import { AiDocsRequestError } from \"./http.js\";\nimport type { ManagedAiDocsServer } from \"./managed-server.js\";\n\n/** Minimal Express request shape; importing Express types is intentionally unnecessary. */\nexport interface AiDocsExpressRequest {\n method?: string;\n originalUrl?: string;\n url?: string;\n protocol?: string;\n headers: Record<string, string | string[] | undefined>;\n body?: unknown;\n on?(event: string, listener: () => void): unknown;\n off?(event: string, listener: () => void): unknown;\n [key: string]: unknown;\n}\n\n/** Minimal Express response shape used by the connector. */\nexport interface AiDocsExpressResponse {\n statusCode: number;\n setHeader(name: string, value: string): unknown;\n write(chunk: Uint8Array): boolean;\n end(chunk?: string): unknown;\n once?(event: string, listener: () => void): unknown;\n destroyed?: boolean;\n}\n\nexport type AiDocsExpressNext = (error?: unknown) => void;\n\nexport type AiDocsExpressManagedServer<\n TRequest extends AiDocsExpressRequest = AiDocsExpressRequest\n> = Pick<ManagedAiDocsServer<{ id: string; label: string }, TRequest>, \"fetch\">;\n\nexport interface AiDocsExpressHandlerOptions {\n /** Origin used only when Express does not expose a Host header. */\n origin?: string;\n /** Maximum size accepted when no JSON body parser populated request.body. */\n maxBodyBytes?: number;\n /** Lets another Express handler process unknown routes below the mounted prefix. */\n fallthrough?: boolean;\n}\n\nexport type AiDocsExpressHandler<TRequest extends AiDocsExpressRequest> = (\n request: TRequest,\n response: AiDocsExpressResponse,\n next?: AiDocsExpressNext\n) => Promise<void>;\n\n/**\n * Creates an Express-compatible handler for the complete managed API.\n *\n * @example\n * `app.use(\"/api/ai-docs\", createManagedAiDocsExpressHandler(aiDocs));`\n */\nexport function createManagedAiDocsExpressHandler<\n TRequest extends AiDocsExpressRequest = AiDocsExpressRequest\n>(\n server: AiDocsExpressManagedServer<TRequest>,\n options: AiDocsExpressHandlerOptions = {}\n): AiDocsExpressHandler<TRequest> {\n return async (request, response, next) => {\n const abortController = new AbortController();\n const abort = (): void => abortController.abort(new DOMException(\"Client disconnected\", \"AbortError\"));\n request.on?.(\"close\", abort);\n try {\n const webRequest = await toFetchRequest(request, abortController.signal, options);\n const result = await server.fetch.handle(webRequest, request);\n if (options.fallthrough && result.status === 404 && next) {\n next();\n return;\n }\n await writeFetchResponse(response, result);\n } catch (error) {\n if (next) {\n next(error);\n return;\n }\n writeConnectorError(response, error);\n } finally {\n request.off?.(\"close\", abort);\n }\n };\n}\n\nasync function toFetchRequest(\n request: AiDocsExpressRequest,\n signal: AbortSignal,\n options: AiDocsExpressHandlerOptions\n): Promise<Request> {\n const headers = new Headers();\n for (const [name, value] of Object.entries(request.headers)) {\n if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));\n else if (value !== undefined) headers.set(name, value);\n }\n const method = request.method?.toUpperCase() || \"GET\";\n const host = headers.get(\"host\");\n const protocol = request.protocol || headers.get(\"x-forwarded-proto\") || \"http\";\n const origin = options.origin ?? `${protocol}://${host || \"localhost\"}`;\n // originalUrl preserves the mount prefix stripped from request.url by Express.\n const url = new URL(request.originalUrl || request.url || \"/\", origin);\n const body = method === \"GET\" || method === \"HEAD\"\n ? undefined\n : request.body !== undefined\n ? serializeBody(request.body, headers)\n : await readBody(request, options.maxBodyBytes ?? 8_600_000);\n return new Request(url, {\n method,\n headers,\n signal,\n ...(body !== undefined ? { body } : {})\n });\n}\n\nfunction serializeBody(body: unknown, headers: Headers): string {\n if (typeof body === \"string\") return body;\n if (body instanceof Uint8Array) return new TextDecoder().decode(body);\n if (!headers.has(\"content-type\")) headers.set(\"content-type\", \"application/json\");\n return JSON.stringify(body) ?? \"null\";\n}\n\nasync function readBody(request: AiDocsExpressRequest, maxBodyBytes: number): Promise<string | undefined> {\n if (!(Symbol.asyncIterator in request)) return undefined;\n const chunks: Uint8Array[] = [];\n let size = 0;\n for await (const chunk of request as AsyncIterable<string | Uint8Array>) {\n const bytes = typeof chunk === \"string\" ? Buffer.from(chunk) : new Uint8Array(chunk);\n size += bytes.byteLength;\n if (size > maxBodyBytes) {\n throw new AiDocsRequestError(413, \"request_too_large\", \"Request body is too large\");\n }\n chunks.push(bytes);\n }\n return chunks.length ? Buffer.concat(chunks).toString(\"utf8\") : undefined;\n}\n\nasync function writeFetchResponse(response: AiDocsExpressResponse, result: Response): Promise<void> {\n response.statusCode = result.status;\n result.headers.forEach((value, name) => response.setHeader(name, value));\n if (!result.body) {\n response.end();\n return;\n }\n const reader = result.body.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!response.write(value) && response.once) {\n await new Promise<void>((resolve) => response.once!(\"drain\", resolve));\n }\n }\n if (!response.destroyed) response.end();\n } finally {\n reader.releaseLock();\n }\n}\n\nfunction writeConnectorError(response: AiDocsExpressResponse, error: unknown): void {\n const status = error instanceof AiDocsRequestError ? error.status : 500;\n const code = error instanceof AiDocsRequestError ? error.code : \"assistant_error\";\n response.statusCode = status;\n response.setHeader(\"content-type\", \"application/json; charset=utf-8\");\n response.end(JSON.stringify({ error: code }));\n}\n"],"mappings":"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","names":[]}