@databricks/appkit 0.69.0 → 0.69.2
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/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +31 -8
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/plugins/server/index.js +2 -2
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js +3 -3
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js +3 -3
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js +4 -4
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/stream/defaults.js +1 -1
- package/dist/stream/defaults.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +6 -3
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/type-generator/database/generate.js +3 -3
- package/dist/type-generator/database/generate.js.map +1 -1
- package/dist/type-generator/migration.js +2 -2
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js +3 -3
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"databricks.d.ts","names":[],"sources":["../../src/agents/databricks.ts"],"mappings":";;;;;;;;;;AAwCA;;UAAiB,gBAAA;EAAgB;EAE/B,WAAA;EAEA;EAAA,KAAA;EAIA;EAFA,IAAA;EAIgB;EAFhB,iBAAA;
|
|
1
|
+
{"version":3,"file":"databricks.d.ts","names":[],"sources":["../../src/agents/databricks.ts"],"mappings":";;;;;;;;;;AAwCA;;UAAiB,gBAAA;EAAgB;EAE/B,WAAA;EAEA;EAAA,KAAA;EAIA;EAFA,IAAA;EAIgB;EAFhB,iBAAA;EA6EQ;EA3ER,gBAAA;AAAA;;;;;;UA2EQ,sBAAA;EACR,WAAA;EACA,YAAA,QAAoB,OAAA,CAAQ,MAAA;EAC5B,QAAA;EACA,SAAA;EAAA;EAEA,gBAAA,GAAmB,gBAAA;EAAA;EAEnB,eAAA;EAEA;EAAA,kBAAA;EAEqB;EAArB,qBAAA;AAAA;;;;;;;UASQ,wBAAA;EACR,UAAA,EAAY,UAAA;EACZ,QAAA;EACA,SAAA;EACA,gBAAA,GAAmB,gBAAA;EACnB,eAAA;EACA,kBAAA;EACA,qBAAA;AAAA;AAAA,KAGG,wBAAA,GACD,sBAAA,GACA,wBAAA;;;;AAAwB;;;;UAelB,mBAAA;EACR,SAAA;IACE,OAAA,CAAQ,OAAA,EAAS,MAAA,oBAA0B,OAAA;EAAA;AAAA;AAAA,UAIrC,sBAAA;EACR,eAAA,EAAiB,mBAAA;EACjB,YAAA;EACA,QAAA;EACA,SAAA;EACA,gBAAA,GAAmB,gBAAA;EACnB,eAAA;EACA,kBAAA;EACA,qBAAA;AAAA;AAAA,UAGQ,mBAAA;EACR,QAAA;EACA,SAAA;EACA,gBAAA,GAAmB,gBAAA;EACnB,eAAA,GAAkB,mBAAA;EAClB,eAAA;EACA,kBAAA;EACA,qBAAA;AAAA;;;;;;;;;;;;;;;AAuFF;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,iBAAA,YAA6B,YAAA;EAAA,QAChC,UAAA;EAAA,QACA,QAAA;EAAA,QACA,SAAA;EAAA,QACA,gBAAA;EAAA,QACA,eAAA;EAAA,QACA,kBAAA;EAAA,QACA,qBAAA;cAEI,OAAA,EAAS,wBAAA;EAuGnB;;;;;;;;EAAA,OAtDW,mBAAA,CACX,OAAA,EAAS,sBAAA,GACR,OAAA,CAAQ,iBAAA;EA4GA;;;;;;;;;;;;;;;AAoZb;;;;;EApZa,OAzDE,gBAAA,CACX,YAAA,WACA,OAAA,GAAU,mBAAA,GACT,OAAA,CAAQ,iBAAA;EA4cF;;;;;;;;;;;;;;;;;;;;;EAAA,OAxZI,iBAAA,CACX,OAAA,EArDQ,2BAAA,GAsDP,OAAA,CAAQ,YAAA;EAKJ,GAAA,CACL,KAAA,EAAO,UAAA,EACP,OAAA,EAAS,eAAA,GACR,cAAA,CAAe,UAAA;;UAuDH,iBAAA;EAAA,QA8CA,gBAAA;EAAA,QAyLA,gBAAA;;;;;;UAoCP,aAAA;EAAA,QAgDA,UAAA;AAAA;;;;;;;;iBA0BM,kBAAA,CACd,IAAA,WACC,KAAA;EAAQ,IAAA;EAAc,IAAA;AAAA"}
|
|
@@ -53,6 +53,15 @@ function isStreamingDeltaToolCall(value) {
|
|
|
53
53
|
function throwIfExceedsStreamLimit(label, currentLength, chunk, max) {
|
|
54
54
|
if (currentLength + chunk.length > max) throw new Error(`DatabricksAdapter: ${label} exceeds configured limit (${max} UTF-16 code units)`);
|
|
55
55
|
}
|
|
56
|
+
/** Text of a harmony `reasoning` part: `summary[].text` plus an optional `text`. */
|
|
57
|
+
function reasoningPartText(part) {
|
|
58
|
+
let text = "";
|
|
59
|
+
if (Array.isArray(part.summary)) {
|
|
60
|
+
for (const s of part.summary) if (isRecord(s) && typeof s.text === "string") text += s.text;
|
|
61
|
+
}
|
|
62
|
+
if (typeof part.text === "string") text += part.text;
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
56
65
|
function isStreamBodyOptions(o) {
|
|
57
66
|
return "streamBody" in o;
|
|
58
67
|
}
|
|
@@ -347,14 +356,28 @@ var DatabricksAdapter = class DatabricksAdapter {
|
|
|
347
356
|
}
|
|
348
357
|
const deltaUnknown = openAiChoicesDelta(parsed);
|
|
349
358
|
if (!isRecord(deltaUnknown)) continue;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
359
|
+
const contentUnknown = deltaUnknown.content;
|
|
360
|
+
const parts = typeof contentUnknown === "string" ? [{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: contentUnknown
|
|
363
|
+
}] : Array.isArray(contentUnknown) ? contentUnknown : [];
|
|
364
|
+
for (const part of parts) {
|
|
365
|
+
if (!isRecord(part)) continue;
|
|
366
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
367
|
+
const content = part.text;
|
|
368
|
+
throwIfExceedsStreamLimit("streamed assistant text", fullText.length, content, this.maxStreamTextChars);
|
|
369
|
+
fullText += content;
|
|
370
|
+
yield {
|
|
371
|
+
type: "message_delta",
|
|
372
|
+
content
|
|
373
|
+
};
|
|
374
|
+
} else if (part.type === "reasoning") {
|
|
375
|
+
const content = reasoningPartText(part);
|
|
376
|
+
if (content) yield {
|
|
377
|
+
type: "thinking",
|
|
378
|
+
content
|
|
379
|
+
};
|
|
380
|
+
}
|
|
358
381
|
}
|
|
359
382
|
const toolCallsRaw = deltaUnknown.tool_calls;
|
|
360
383
|
if (!Array.isArray(toolCallsRaw)) continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"databricks.js","names":["servingStream"],"sources":["../../src/agents/databricks.ts"],"sourcesContent":["import type {\n AgentAdapter,\n AgentEvent,\n AgentInput,\n AgentRunContext,\n AgentToolDefinition,\n} from \"shared\";\n\nimport {\n type StreamBody,\n stream as servingStream,\n} from \"../connectors/serving/client\";\nimport { APPKIT_USER_AGENT, getClientOptions } from \"../context/client-options\";\nimport { createWorkspaceClient } from \"../workspace-client\";\n\n/** Default cap for a single incomplete SSE line tail (DoS guard). */\nconst DEFAULT_MAX_SSE_LINE_CHARS = 1024 * 1024;\n\n/** Default cap for accumulated assistant text from `delta.content`. */\nconst DEFAULT_MAX_STREAM_TEXT_CHARS = 4 * 1024 * 1024;\n\n/** Default cap for accumulated JSON arguments per streamed tool call index. */\nconst DEFAULT_MAX_TOOL_ARGUMENT_CHARS = 2 * 1024 * 1024;\n\n/** Cap text length before running Python-style tool-call regex (ReDoS guard). */\nconst PYTHON_STYLE_TOOL_PARSE_MAX_INPUT = 64 * 1024;\n\n/** Fallback HTTP timeout when the raw fetch adapter path receives no AbortSignal from the runner. */\nconst RAW_FETCH_DEFAULT_TIMEOUT_MS = 120_000;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Optional generation parameters forwarded to the OpenAI-compatible serving\n * request body. Names match the serving API wire keys. Only keys that are set\n * are sent — undefined values are omitted so the endpoint applies its own\n * defaults. Ranges are not validated here; the serving endpoint validates.\n */\nexport interface GenerationParams {\n /** Sampling temperature. */\n temperature?: number;\n /** Nucleus sampling probability mass (`top_p`). */\n top_p?: number;\n /** Stop sequence(s) that end generation. */\n stop?: string | string[];\n /** Penalize tokens by frequency. */\n frequency_penalty?: number;\n /** Penalize tokens by prior presence. */\n presence_penalty?: number;\n}\n\nconst GENERATION_PARAM_KEYS = [\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"frequency_penalty\",\n \"presence_penalty\",\n] as const satisfies readonly (keyof GenerationParams)[];\n\n/** Copy only the set generation params onto the request body. */\nfunction applyGenerationParams(\n body: Record<string, unknown>,\n params: GenerationParams,\n): void {\n for (const key of GENERATION_PARAM_KEYS) {\n const value = params[key];\n if (value !== undefined) body[key] = value;\n }\n}\n\nfunction extractLlamaToolJsonSlice(text: string): string | undefined {\n const start = text.indexOf(\"[{\");\n if (start < 0) return undefined;\n const endBracket = text.lastIndexOf(\"}]\");\n if (endBracket < start) return undefined;\n return text.slice(start, endBracket + 2);\n}\n\n/** OpenAI SSE payload: `{ choices: [{ delta }] }`. */\nfunction openAiChoicesDelta(parsed: unknown): unknown {\n if (!isRecord(parsed)) return undefined;\n const choices = parsed.choices;\n if (!Array.isArray(choices) || choices.length < 1) return undefined;\n const first = choices[0];\n if (!isRecord(first)) return undefined;\n return first.delta;\n}\n\nfunction isStreamingDeltaToolCall(value: unknown): value is DeltaToolCall {\n if (!isRecord(value)) return false;\n return typeof value.index === \"number\";\n}\n\nfunction throwIfExceedsStreamLimit(\n label: string,\n currentLength: number,\n chunk: string,\n max: number,\n): void {\n if (currentLength + chunk.length > max) {\n throw new Error(\n `DatabricksAdapter: ${label} exceeds configured limit (${max} UTF-16 code units)`,\n );\n }\n}\n\n/**\n * Escape-hatch options: provide an `endpointUrl` + `authenticate()` and the\n * adapter uses a bare `fetch()` to call it. Useful for tests and for pointing\n * the adapter at non-workspace endpoints (reverse proxies, mocks).\n */\ninterface RawFetchAdapterOptions {\n endpointUrl: string;\n authenticate: () => Promise<Record<string, string>>;\n maxSteps?: number;\n maxTokens?: number;\n /** Optional generation params forwarded to the serving request body. */\n generationParams?: GenerationParams;\n /** Max length of one SSE line (including an incomplete tail in the buffer). */\n maxSseLineChars?: number;\n /** Max total length of assistant `delta.content` across the stream. */\n maxStreamTextChars?: number;\n /** Max length of streamed `function.arguments` per tool call index. */\n maxToolArgumentsChars?: number;\n}\n\n/**\n * Preferred options: caller provides the transport function directly.\n * The `fromServingEndpoint` / `fromModelServing` factories use this to route\n * through `connectors/serving/stream`, which centralises URL encoding, auth\n * via the SDK's `apiClient.request`, and any future retries/telemetry.\n */\ninterface StreamBodyAdapterOptions {\n streamBody: StreamBody;\n maxSteps?: number;\n maxTokens?: number;\n generationParams?: GenerationParams;\n maxSseLineChars?: number;\n maxStreamTextChars?: number;\n maxToolArgumentsChars?: number;\n}\n\ntype DatabricksAdapterOptions =\n | RawFetchAdapterOptions\n | StreamBodyAdapterOptions;\n\nfunction isStreamBodyOptions(\n o: DatabricksAdapterOptions,\n): o is StreamBodyAdapterOptions {\n return \"streamBody\" in o;\n}\n\n/**\n * Duck-typed subset of the Databricks SDK `WorkspaceClient`. Callers of\n * `fromServingEndpoint` and `fromModelServing` pass a real `WorkspaceClient`,\n * but we only need the `apiClient.request` surface — so we declare the minimal\n * interface rather than importing the SDK type directly. This keeps the adapter\n * free of a hard compile-time dependency on `@databricks/sdk-experimental`.\n */\ninterface WorkspaceClientLike {\n apiClient: {\n request(options: Record<string, unknown>): Promise<unknown>;\n };\n}\n\ninterface ServingEndpointOptions {\n workspaceClient: WorkspaceClientLike;\n endpointName: string;\n maxSteps?: number;\n maxTokens?: number;\n generationParams?: GenerationParams;\n maxSseLineChars?: number;\n maxStreamTextChars?: number;\n maxToolArgumentsChars?: number;\n}\n\ninterface ModelServingOptions {\n maxSteps?: number;\n maxTokens?: number;\n generationParams?: GenerationParams;\n workspaceClient?: WorkspaceClientLike;\n maxSseLineChars?: number;\n maxStreamTextChars?: number;\n maxToolArgumentsChars?: number;\n}\n\ninterface OpenAIMessage {\n role: \"system\" | \"user\" | \"assistant\" | \"tool\";\n content: string | null;\n tool_calls?: OpenAIToolCall[];\n tool_call_id?: string;\n}\n\ninterface OpenAIToolCall {\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n /**\n * Opaque Vertex/Gemini \"thought signature\" blob the request must echo\n * back verbatim on the next turn. Vertex's OpenAI-compat proxy emits\n * this as `thoughtSignature` (camelCase) at the top level of the\n * tool_call delta (verified against `gemini-3.1-flash-lite-preview`),\n * and accepts the same spelling back on outbound. Non-Gemini endpoints\n * (Claude on Databricks, external OpenAI-compat models, Llama, etc.)\n * leave this undefined and the serializer omits the key.\n * See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thought-signatures\n */\n thoughtSignature?: string;\n}\n\ninterface OpenAITool {\n type: \"function\";\n function: {\n name: string;\n description: string;\n parameters: unknown;\n };\n}\n\ninterface DeltaToolCall {\n index: number;\n id?: string;\n type?: string;\n function?: { name?: string; arguments?: string };\n /** See {@link OpenAIToolCall.thoughtSignature}. */\n thoughtSignature?: string;\n}\n\n/**\n * Adapter that talks directly to Databricks Model Serving `/invocations` endpoint.\n *\n * No dependency on the Vercel AI SDK or LangChain. Uses raw `fetch()` to POST\n * OpenAI-compatible payloads and parses the SSE stream itself. Calls\n * `authenticate()` per-request so tokens are always fresh.\n *\n * Handles both structured `tool_calls` responses and text-based tool call\n * fallback parsing for models that output tool calls as text.\n *\n * @example Using the factory (recommended)\n * ```ts\n * import { createApp, createAgent, agents, createWorkspaceClient } from \"@databricks/appkit\";\n * import { DatabricksAdapter } from \"@databricks/appkit/beta\";\n *\n * const adapter = DatabricksAdapter.fromServingEndpoint({\n * workspaceClient: createWorkspaceClient(),\n * endpointName: \"my-endpoint\",\n * });\n *\n * await createApp({\n * plugins: [\n * agents({\n * agents: {\n * assistant: createAgent({\n * instructions: \"You are a helpful assistant.\",\n * model: adapter,\n * }),\n * },\n * }),\n * ],\n * });\n * ```\n *\n * @example Using the raw constructor\n * ```ts\n * const adapter = new DatabricksAdapter({\n * endpointUrl: \"https://host/serving-endpoints/my-endpoint/invocations\",\n * authenticate: async () => ({ Authorization: `Bearer ${token}` }),\n * });\n * ```\n */\nexport class DatabricksAdapter implements AgentAdapter {\n private streamBody: StreamBody;\n private maxSteps: number;\n private maxTokens: number;\n private generationParams: GenerationParams;\n private maxSseLineChars: number;\n private maxStreamTextChars: number;\n private maxToolArgumentsChars: number;\n\n constructor(options: DatabricksAdapterOptions) {\n this.maxSteps = options.maxSteps ?? 10;\n this.maxTokens = options.maxTokens ?? 4096;\n this.generationParams = options.generationParams ?? {};\n this.maxSseLineChars =\n options.maxSseLineChars ?? DEFAULT_MAX_SSE_LINE_CHARS;\n this.maxStreamTextChars =\n options.maxStreamTextChars ?? DEFAULT_MAX_STREAM_TEXT_CHARS;\n this.maxToolArgumentsChars =\n options.maxToolArgumentsChars ?? DEFAULT_MAX_TOOL_ARGUMENT_CHARS;\n\n if (isStreamBodyOptions(options)) {\n this.streamBody = options.streamBody;\n } else {\n const { endpointUrl, authenticate } = options;\n this.streamBody = async (body, signal) => {\n const fetchSignal =\n signal ?? AbortSignal.timeout(RAW_FETCH_DEFAULT_TIMEOUT_MS);\n const authHeaders = await authenticate();\n const response = await fetch(endpointUrl, {\n method: \"POST\",\n headers: {\n \"User-Agent\": APPKIT_USER_AGENT,\n \"Content-Type\": \"application/json\",\n ...authHeaders,\n },\n body: JSON.stringify(body),\n signal: fetchSignal,\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new Error(\n `Databricks API error (${response.status}): ${errorText}`,\n );\n }\n if (!response.body) throw new Error(\"No response body\");\n return response.body;\n };\n }\n }\n\n /**\n * Creates a DatabricksAdapter for a Databricks Model Serving endpoint.\n *\n * Routes through the shared `connectors/serving/stream` helper, which\n * delegates to the SDK's `apiClient.request({ raw: true })`. That gives the\n * adapter centralised URL encoding + authentication with the rest of the\n * serving surface — no bespoke `fetch()` + `authenticate()` plumbing.\n */\n static async fromServingEndpoint(\n options: ServingEndpointOptions,\n ): Promise<DatabricksAdapter> {\n const {\n workspaceClient,\n endpointName,\n maxSteps,\n maxTokens,\n generationParams,\n maxSseLineChars,\n maxStreamTextChars,\n maxToolArgumentsChars,\n } = options;\n return new DatabricksAdapter({\n streamBody: (body, signal) =>\n // Cast through the structural shape: the connector types\n // `workspaceClient` as the SDK's concrete `WorkspaceClient`, but we\n // only need `apiClient.request`.\n servingStream(\n workspaceClient as unknown as Parameters<typeof servingStream>[0],\n endpointName,\n body,\n signal,\n ),\n maxSteps,\n maxTokens,\n generationParams,\n maxSseLineChars,\n maxStreamTextChars,\n maxToolArgumentsChars,\n });\n }\n\n /**\n * Creates a DatabricksAdapter from a Model Serving endpoint name.\n * Auto-creates a WorkspaceClient internally. Reads the endpoint name\n * from the argument or the `DATABRICKS_SERVING_ENDPOINT_NAME` env var.\n *\n * @example\n * ```ts\n * // Reads endpoint from DATABRICKS_SERVING_ENDPOINT_NAME env var\n * const adapter = await DatabricksAdapter.fromModelServing();\n *\n * // Explicit endpoint\n * const adapter = await DatabricksAdapter.fromModelServing(\"my-endpoint\");\n *\n * // With options\n * const adapter = await DatabricksAdapter.fromModelServing(\"my-endpoint\", {\n * maxSteps: 5,\n * maxTokens: 2048,\n * });\n * ```\n */\n static async fromModelServing(\n endpointName?: string,\n options?: ModelServingOptions,\n ): Promise<DatabricksAdapter> {\n const resolvedEndpoint =\n endpointName ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;\n\n if (!resolvedEndpoint) {\n throw new Error(\n \"No endpoint name provided and DATABRICKS_SERVING_ENDPOINT_NAME env var is not set. \" +\n \"Pass an endpoint name or set DATABRICKS_SERVING_ENDPOINT_NAME.\",\n );\n }\n\n let workspaceClient: WorkspaceClientLike | undefined =\n options?.workspaceClient;\n if (!workspaceClient) {\n workspaceClient = createWorkspaceClient({\n clientOptions: getClientOptions(),\n }) as unknown as WorkspaceClientLike;\n }\n\n return DatabricksAdapter.fromServingEndpoint({\n workspaceClient,\n endpointName: resolvedEndpoint,\n maxSteps: options?.maxSteps,\n maxTokens: options?.maxTokens,\n generationParams: options?.generationParams,\n maxSseLineChars: options?.maxSseLineChars,\n maxStreamTextChars: options?.maxStreamTextChars,\n maxToolArgumentsChars: options?.maxToolArgumentsChars,\n });\n }\n\n /**\n * Discoverability shim for the Supervisor API adapter. Returns an\n * {@link AgentAdapter} (a `SupervisorApiAdapter` at runtime), NOT a\n * {@link DatabricksAdapter} — the two are separate classes (different\n * wire formats, different lifecycle). The return type is the\n * {@link AgentAdapter} interface so callers aren't bound to the concrete\n * class. Surfaced here so application developers see a single\n * `DatabricksAdapter.from*` autocomplete root.\n *\n * Dynamic-imports `./supervisor-api` to avoid forming a load-time cycle:\n * both files share `connectors/serving/client.ts`.\n *\n * @example\n * ```ts\n * import { DatabricksAdapter } from \"@databricks/appkit/beta\";\n *\n * const model = await DatabricksAdapter.fromSupervisorApi({\n * model: \"databricks-claude-sonnet-4-5\",\n * });\n * ```\n */\n static async fromSupervisorApi(\n options: import(\"./supervisor-api\").SupervisorApiAdapterOptions,\n ): Promise<AgentAdapter> {\n const { fromSupervisorApi } = await import(\"./supervisor-api\");\n return fromSupervisorApi(options);\n }\n\n async *run(\n input: AgentInput,\n context: AgentRunContext,\n ): AsyncGenerator<AgentEvent, void, unknown> {\n // Databricks API requires tool names to match [a-zA-Z0-9_-].\n // Our tool names use dots (e.g. \"analytics.query\"), so we swap dots\n // for double-underscores in the wire format and map back on receipt.\n const nameToWire = new Map<string, string>();\n const wireToName = new Map<string, string>();\n for (const tool of input.tools) {\n const wire = tool.name.replace(/\\./g, \"__\");\n if (wireToName.has(wire) && wireToName.get(wire) !== tool.name) {\n throw new Error(\n `Tool name collision: '${tool.name}' and '${wireToName.get(wire)}' both map to wire name '${wire}'`,\n );\n }\n nameToWire.set(tool.name, wire);\n wireToName.set(wire, tool.name);\n }\n\n const tools = this.buildTools(input.tools, nameToWire);\n const messages = this.buildMessages(input.messages, nameToWire);\n\n yield { type: \"status\", status: \"running\" };\n\n for (let step = 0; step < this.maxSteps; step++) {\n if (context.signal?.aborted) break;\n\n const { text, toolCalls } = yield* this.streamCompletion(\n messages,\n tools,\n context,\n );\n\n if (toolCalls.length === 0) {\n const parsed = parseTextToolCalls(text);\n if (parsed.length > 0) {\n yield* this.executeToolCalls(parsed, messages, context, nameToWire);\n continue;\n }\n break;\n }\n\n messages.push({\n role: \"assistant\",\n content: text || null,\n tool_calls: toolCalls,\n });\n\n for (const tc of toolCalls) {\n const wireName = tc.function.name;\n const originalName = wireToName.get(wireName) ?? wireName;\n yield* this.executeSingleTool(tc, originalName, messages, context);\n }\n }\n }\n\n /** Parse wire arguments, emit tool_call / tool_result, append tool messages. */\n private async *executeSingleTool(\n tc: OpenAIToolCall,\n originalName: string,\n messages: OpenAIMessage[],\n context: AgentRunContext,\n ): AsyncGenerator<AgentEvent, void, unknown> {\n let args: unknown;\n try {\n args = JSON.parse(tc.function.arguments);\n } catch {\n args = {};\n }\n\n yield { type: \"tool_call\", callId: tc.id, name: originalName, args };\n\n try {\n const result = await context.executeTool(originalName, args);\n const resultStr =\n typeof result === \"string\" ? result : JSON.stringify(result);\n\n yield { type: \"tool_result\", callId: tc.id, result };\n\n messages.push({\n role: \"tool\",\n content: resultStr,\n tool_call_id: tc.id,\n });\n } catch (error) {\n const errMsg =\n error instanceof Error ? error.message : \"Tool execution failed\";\n\n yield {\n type: \"tool_result\",\n callId: tc.id,\n result: null,\n error: errMsg,\n };\n\n messages.push({\n role: \"tool\",\n content: JSON.stringify({ error: errMsg }),\n tool_call_id: tc.id,\n });\n }\n }\n\n private async *streamCompletion(\n messages: OpenAIMessage[],\n tools: OpenAITool[],\n context: AgentRunContext,\n ): AsyncGenerator<\n AgentEvent,\n { text: string; toolCalls: OpenAIToolCall[] },\n unknown\n > {\n const body: Record<string, unknown> = {\n messages,\n stream: true,\n max_tokens: this.maxTokens,\n };\n\n applyGenerationParams(body, this.generationParams);\n\n if (tools.length > 0) {\n body.tools = tools;\n }\n\n let responseBody: ReadableStream<Uint8Array>;\n try {\n responseBody = await this.streamBody(body, context.signal);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Stream request failed\";\n yield { type: \"status\", status: \"error\", error: msg };\n throw err;\n }\n\n const reader = responseBody.getReader();\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n let fullText = \"\";\n const toolCallAccumulator = new Map<\n number,\n {\n id: string;\n name: string;\n arguments: string;\n thoughtSignature?: string;\n }\n >();\n\n try {\n while (true) {\n if (context.signal?.aborted) break;\n\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n if (buffer.length > this.maxSseLineChars) {\n throw new Error(\n `DatabricksAdapter: SSE line buffer exceeds configured limit (${this.maxSseLineChars} UTF-16 code units)`,\n );\n }\n\n for (const line of lines) {\n if (line.length > this.maxSseLineChars) {\n throw new Error(\n `DatabricksAdapter: SSE line exceeds configured limit (${this.maxSseLineChars} UTF-16 code units)`,\n );\n }\n\n const trimmed = line.trim();\n if (!trimmed.startsWith(\"data: \")) continue;\n const data = trimmed.slice(6);\n if (data === \"[DONE]\") continue;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n } catch (parseErr) {\n console.debug(\n \"[DatabricksAdapter] malformed SSE data line JSON\",\n { line: `${data.slice(0, 256)}${data.length > 256 ? \"…\" : \"\"}` },\n parseErr,\n );\n continue;\n }\n\n const deltaUnknown = openAiChoicesDelta(parsed);\n if (!isRecord(deltaUnknown)) continue;\n\n if (typeof deltaUnknown.content === \"string\") {\n const content = deltaUnknown.content;\n throwIfExceedsStreamLimit(\n \"streamed assistant text\",\n fullText.length,\n content,\n this.maxStreamTextChars,\n );\n fullText += content;\n yield { type: \"message_delta\" as const, content };\n }\n\n const toolCallsRaw = deltaUnknown.tool_calls;\n if (!Array.isArray(toolCallsRaw)) continue;\n\n for (const tc of toolCallsRaw) {\n if (!isStreamingDeltaToolCall(tc)) continue;\n const sig = tc.thoughtSignature;\n const existing = toolCallAccumulator.get(tc.index);\n if (existing) {\n if (tc.function?.arguments) {\n throwIfExceedsStreamLimit(\n \"tool call arguments\",\n existing.arguments.length,\n tc.function.arguments,\n this.maxToolArgumentsChars,\n );\n existing.arguments += tc.function.arguments;\n }\n if (sig && !existing.thoughtSignature) {\n existing.thoughtSignature = sig;\n }\n } else {\n const initial = tc.function?.arguments ?? \"\";\n if (initial.length > this.maxToolArgumentsChars) {\n throw new Error(\n `DatabricksAdapter: tool call arguments exceed configured limit (${this.maxToolArgumentsChars} UTF-16 code units)`,\n );\n }\n toolCallAccumulator.set(tc.index, {\n id: tc.id ?? `call_${tc.index}`,\n name: tc.function?.name ?? \"\",\n arguments: initial,\n ...(sig ? { thoughtSignature: sig } : {}),\n });\n }\n }\n }\n }\n } finally {\n try {\n await reader.cancel();\n } catch (cancelErr) {\n console.debug(\n \"[DatabricksAdapter] reader.cancel() failed during teardown\",\n cancelErr,\n );\n }\n try {\n reader.releaseLock();\n } catch (unlockErr) {\n console.debug(\n \"[DatabricksAdapter] reader.releaseLock() failed during teardown\",\n unlockErr,\n );\n }\n }\n\n const toolCalls: OpenAIToolCall[] = Array.from(\n toolCallAccumulator.values(),\n ).map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments || \"{}\" },\n ...(tc.thoughtSignature ? { thoughtSignature: tc.thoughtSignature } : {}),\n }));\n\n return { text: fullText, toolCalls };\n }\n\n private async *executeToolCalls(\n calls: Array<{ name: string; args: unknown }>,\n messages: OpenAIMessage[],\n context: AgentRunContext,\n nameToWire: Map<string, string>,\n ): AsyncGenerator<AgentEvent, void, unknown> {\n const wireToolName = (name: string) =>\n nameToWire.get(name) ?? name.replace(/\\./g, \"__\");\n\n const toolCallObjs: OpenAIToolCall[] = calls.map((c, i) => ({\n id: `text_call_${i}`,\n type: \"function\" as const,\n function: {\n name: wireToolName(c.name),\n arguments: JSON.stringify(c.args),\n },\n }));\n\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: toolCallObjs,\n });\n\n for (let i = 0; i < toolCallObjs.length; i++) {\n const tc = toolCallObjs[i];\n const originalName = calls[i]?.name ?? tc.function.name;\n yield* this.executeSingleTool(tc, originalName, messages, context);\n }\n }\n\n /**\n * Maps AppKit {@link AgentInput} messages into OpenAI-compatible wire messages.\n * Preserves multi-turn tool state (`toolCalls` → `tool_calls`, `toolCallId` →\n * `tool_call_id`) so resumed threads and hydrated history reach the model.\n */\n private buildMessages(\n messages: AgentInput[\"messages\"],\n nameToWire: Map<string, string>,\n ): OpenAIMessage[] {\n const wireToolName = (name: string) =>\n nameToWire.get(name) ?? name.replace(/\\./g, \"__\");\n\n return messages.map((m) => {\n let content: string | null = m.content;\n if (\n m.role === \"assistant\" &&\n m.toolCalls &&\n m.toolCalls.length > 0 &&\n (!m.content || m.content.trim() === \"\")\n ) {\n content = null;\n }\n\n const out: OpenAIMessage = {\n role: m.role as OpenAIMessage[\"role\"],\n content,\n };\n\n if (m.toolCallId) {\n out.tool_call_id = m.toolCallId;\n }\n\n if (m.toolCalls && m.toolCalls.length > 0) {\n out.tool_calls = m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: wireToolName(tc.name),\n arguments:\n typeof tc.args === \"string\"\n ? tc.args\n : JSON.stringify(tc.args ?? {}),\n },\n ...(tc.thoughtSignature\n ? { thoughtSignature: tc.thoughtSignature }\n : {}),\n }));\n }\n\n return out;\n });\n }\n\n private buildTools(\n definitions: AgentToolDefinition[],\n nameToWire: Map<string, string>,\n ): OpenAITool[] {\n return definitions.map((def) => ({\n type: \"function\" as const,\n function: {\n name: nameToWire.get(def.name) ?? def.name,\n description: def.description,\n parameters: def.parameters,\n },\n }));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Text-based tool call parsing (fallback)\n// ---------------------------------------------------------------------------\n\n/**\n * Parses text-based tool calls from model output.\n *\n * Handles two formats:\n * 1. Llama native: `[{\"name\": \"tool_name\", \"parameters\": {\"arg\": \"val\"}}]`\n * 2. Python-style: `[tool_name(arg1='val1', arg2='val2')]`\n */\nexport function parseTextToolCalls(\n text: string,\n): Array<{ name: string; args: unknown }> {\n const trimmed = text.trim();\n\n const jsonResult = tryParseLlamaJsonToolCalls(trimmed);\n if (jsonResult.length > 0) return jsonResult;\n\n const pyResult = tryParsePythonStyleToolCalls(trimmed);\n if (pyResult.length > 0) return pyResult;\n\n return [];\n}\n\nfunction isLlamaToolJsonItem(value: unknown): value is Record<\n string,\n unknown\n> & {\n name: string;\n} {\n if (!isRecord(value)) return false;\n return typeof value.name === \"string\";\n}\n\nfunction tryParseLlamaJsonToolCalls(\n text: string,\n): Array<{ name: string; args: unknown }> {\n const slice = extractLlamaToolJsonSlice(text);\n if (!slice) return [];\n\n try {\n const parsed: unknown = JSON.parse(slice);\n if (!Array.isArray(parsed)) return [];\n\n return parsed.filter(isLlamaToolJsonItem).map((item) => ({\n name: item.name,\n args: item.parameters ?? item.arguments ?? item.args ?? {},\n }));\n } catch {\n return [];\n }\n}\n\nfunction tryParsePythonStyleToolCalls(\n text: string,\n): Array<{ name: string; args: unknown }> {\n if (text.length > PYTHON_STYLE_TOOL_PARSE_MAX_INPUT) {\n return [];\n }\n\n const pattern = /\\[?([a-zA-Z_][\\w.]*)\\(([^)]*)\\)\\]?/g;\n const results: Array<{ name: string; args: unknown }> = [];\n\n for (const match of text.matchAll(pattern)) {\n const name = match[1];\n const argsStr = match[2];\n\n const args: Record<string, unknown> = {};\n const argPattern = /(\\w+)\\s*=\\s*(?:'([^']*)'|\"([^\"]*)\"|(\\S+))/g;\n for (const argMatch of argsStr.matchAll(argPattern)) {\n const key = argMatch[1];\n const value = argMatch[2] ?? argMatch[3] ?? argMatch[4];\n args[key] = value;\n }\n\n results.push({ name, args });\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"databricks.js","names":["servingStream"],"sources":["../../src/agents/databricks.ts"],"sourcesContent":["import type {\n AgentAdapter,\n AgentEvent,\n AgentInput,\n AgentRunContext,\n AgentToolDefinition,\n} from \"shared\";\n\nimport {\n type StreamBody,\n stream as servingStream,\n} from \"../connectors/serving/client\";\nimport { APPKIT_USER_AGENT, getClientOptions } from \"../context/client-options\";\nimport { createWorkspaceClient } from \"../workspace-client\";\n\n/** Default cap for a single incomplete SSE line tail (DoS guard). */\nconst DEFAULT_MAX_SSE_LINE_CHARS = 1024 * 1024;\n\n/** Default cap for accumulated assistant text from `delta.content`. */\nconst DEFAULT_MAX_STREAM_TEXT_CHARS = 4 * 1024 * 1024;\n\n/** Default cap for accumulated JSON arguments per streamed tool call index. */\nconst DEFAULT_MAX_TOOL_ARGUMENT_CHARS = 2 * 1024 * 1024;\n\n/** Cap text length before running Python-style tool-call regex (ReDoS guard). */\nconst PYTHON_STYLE_TOOL_PARSE_MAX_INPUT = 64 * 1024;\n\n/** Fallback HTTP timeout when the raw fetch adapter path receives no AbortSignal from the runner. */\nconst RAW_FETCH_DEFAULT_TIMEOUT_MS = 120_000;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Optional generation parameters forwarded to the OpenAI-compatible serving\n * request body. Names match the serving API wire keys. Only keys that are set\n * are sent — undefined values are omitted so the endpoint applies its own\n * defaults. Ranges are not validated here; the serving endpoint validates.\n */\nexport interface GenerationParams {\n /** Sampling temperature. */\n temperature?: number;\n /** Nucleus sampling probability mass (`top_p`). */\n top_p?: number;\n /** Stop sequence(s) that end generation. */\n stop?: string | string[];\n /** Penalize tokens by frequency. */\n frequency_penalty?: number;\n /** Penalize tokens by prior presence. */\n presence_penalty?: number;\n}\n\nconst GENERATION_PARAM_KEYS = [\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"frequency_penalty\",\n \"presence_penalty\",\n] as const satisfies readonly (keyof GenerationParams)[];\n\n/** Copy only the set generation params onto the request body. */\nfunction applyGenerationParams(\n body: Record<string, unknown>,\n params: GenerationParams,\n): void {\n for (const key of GENERATION_PARAM_KEYS) {\n const value = params[key];\n if (value !== undefined) body[key] = value;\n }\n}\n\nfunction extractLlamaToolJsonSlice(text: string): string | undefined {\n const start = text.indexOf(\"[{\");\n if (start < 0) return undefined;\n const endBracket = text.lastIndexOf(\"}]\");\n if (endBracket < start) return undefined;\n return text.slice(start, endBracket + 2);\n}\n\n/** OpenAI SSE payload: `{ choices: [{ delta }] }`. */\nfunction openAiChoicesDelta(parsed: unknown): unknown {\n if (!isRecord(parsed)) return undefined;\n const choices = parsed.choices;\n if (!Array.isArray(choices) || choices.length < 1) return undefined;\n const first = choices[0];\n if (!isRecord(first)) return undefined;\n return first.delta;\n}\n\nfunction isStreamingDeltaToolCall(value: unknown): value is DeltaToolCall {\n if (!isRecord(value)) return false;\n return typeof value.index === \"number\";\n}\n\nfunction throwIfExceedsStreamLimit(\n label: string,\n currentLength: number,\n chunk: string,\n max: number,\n): void {\n if (currentLength + chunk.length > max) {\n throw new Error(\n `DatabricksAdapter: ${label} exceeds configured limit (${max} UTF-16 code units)`,\n );\n }\n}\n\n/** Text of a harmony `reasoning` part: `summary[].text` plus an optional `text`. */\nfunction reasoningPartText(part: Record<string, unknown>): string {\n let text = \"\";\n if (Array.isArray(part.summary)) {\n for (const s of part.summary) {\n if (isRecord(s) && typeof s.text === \"string\") text += s.text;\n }\n }\n if (typeof part.text === \"string\") text += part.text;\n return text;\n}\n\n/**\n * Escape-hatch options: provide an `endpointUrl` + `authenticate()` and the\n * adapter uses a bare `fetch()` to call it. Useful for tests and for pointing\n * the adapter at non-workspace endpoints (reverse proxies, mocks).\n */\ninterface RawFetchAdapterOptions {\n endpointUrl: string;\n authenticate: () => Promise<Record<string, string>>;\n maxSteps?: number;\n maxTokens?: number;\n /** Optional generation params forwarded to the serving request body. */\n generationParams?: GenerationParams;\n /** Max length of one SSE line (including an incomplete tail in the buffer). */\n maxSseLineChars?: number;\n /** Max total length of assistant `delta.content` across the stream. */\n maxStreamTextChars?: number;\n /** Max length of streamed `function.arguments` per tool call index. */\n maxToolArgumentsChars?: number;\n}\n\n/**\n * Preferred options: caller provides the transport function directly.\n * The `fromServingEndpoint` / `fromModelServing` factories use this to route\n * through `connectors/serving/stream`, which centralises URL encoding, auth\n * via the SDK's `apiClient.request`, and any future retries/telemetry.\n */\ninterface StreamBodyAdapterOptions {\n streamBody: StreamBody;\n maxSteps?: number;\n maxTokens?: number;\n generationParams?: GenerationParams;\n maxSseLineChars?: number;\n maxStreamTextChars?: number;\n maxToolArgumentsChars?: number;\n}\n\ntype DatabricksAdapterOptions =\n | RawFetchAdapterOptions\n | StreamBodyAdapterOptions;\n\nfunction isStreamBodyOptions(\n o: DatabricksAdapterOptions,\n): o is StreamBodyAdapterOptions {\n return \"streamBody\" in o;\n}\n\n/**\n * Duck-typed subset of the Databricks SDK `WorkspaceClient`. Callers of\n * `fromServingEndpoint` and `fromModelServing` pass a real `WorkspaceClient`,\n * but we only need the `apiClient.request` surface — so we declare the minimal\n * interface rather than importing the SDK type directly. This keeps the adapter\n * free of a hard compile-time dependency on `@databricks/sdk-experimental`.\n */\ninterface WorkspaceClientLike {\n apiClient: {\n request(options: Record<string, unknown>): Promise<unknown>;\n };\n}\n\ninterface ServingEndpointOptions {\n workspaceClient: WorkspaceClientLike;\n endpointName: string;\n maxSteps?: number;\n maxTokens?: number;\n generationParams?: GenerationParams;\n maxSseLineChars?: number;\n maxStreamTextChars?: number;\n maxToolArgumentsChars?: number;\n}\n\ninterface ModelServingOptions {\n maxSteps?: number;\n maxTokens?: number;\n generationParams?: GenerationParams;\n workspaceClient?: WorkspaceClientLike;\n maxSseLineChars?: number;\n maxStreamTextChars?: number;\n maxToolArgumentsChars?: number;\n}\n\ninterface OpenAIMessage {\n role: \"system\" | \"user\" | \"assistant\" | \"tool\";\n content: string | null;\n tool_calls?: OpenAIToolCall[];\n tool_call_id?: string;\n}\n\ninterface OpenAIToolCall {\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n /**\n * Opaque Vertex/Gemini \"thought signature\" blob the request must echo\n * back verbatim on the next turn. Vertex's OpenAI-compat proxy emits\n * this as `thoughtSignature` (camelCase) at the top level of the\n * tool_call delta (verified against `gemini-3.1-flash-lite-preview`),\n * and accepts the same spelling back on outbound. Non-Gemini endpoints\n * (Claude on Databricks, external OpenAI-compat models, Llama, etc.)\n * leave this undefined and the serializer omits the key.\n * See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thought-signatures\n */\n thoughtSignature?: string;\n}\n\ninterface OpenAITool {\n type: \"function\";\n function: {\n name: string;\n description: string;\n parameters: unknown;\n };\n}\n\ninterface DeltaToolCall {\n index: number;\n id?: string;\n type?: string;\n function?: { name?: string; arguments?: string };\n /** See {@link OpenAIToolCall.thoughtSignature}. */\n thoughtSignature?: string;\n}\n\n/**\n * Adapter that talks directly to Databricks Model Serving `/invocations` endpoint.\n *\n * No dependency on the Vercel AI SDK or LangChain. Uses raw `fetch()` to POST\n * OpenAI-compatible payloads and parses the SSE stream itself. Calls\n * `authenticate()` per-request so tokens are always fresh.\n *\n * Handles both structured `tool_calls` responses and text-based tool call\n * fallback parsing for models that output tool calls as text.\n *\n * @example Using the factory (recommended)\n * ```ts\n * import { createApp, createAgent, agents, createWorkspaceClient } from \"@databricks/appkit\";\n * import { DatabricksAdapter } from \"@databricks/appkit/beta\";\n *\n * const adapter = DatabricksAdapter.fromServingEndpoint({\n * workspaceClient: createWorkspaceClient(),\n * endpointName: \"my-endpoint\",\n * });\n *\n * await createApp({\n * plugins: [\n * agents({\n * agents: {\n * assistant: createAgent({\n * instructions: \"You are a helpful assistant.\",\n * model: adapter,\n * }),\n * },\n * }),\n * ],\n * });\n * ```\n *\n * @example Using the raw constructor\n * ```ts\n * const adapter = new DatabricksAdapter({\n * endpointUrl: \"https://host/serving-endpoints/my-endpoint/invocations\",\n * authenticate: async () => ({ Authorization: `Bearer ${token}` }),\n * });\n * ```\n */\nexport class DatabricksAdapter implements AgentAdapter {\n private streamBody: StreamBody;\n private maxSteps: number;\n private maxTokens: number;\n private generationParams: GenerationParams;\n private maxSseLineChars: number;\n private maxStreamTextChars: number;\n private maxToolArgumentsChars: number;\n\n constructor(options: DatabricksAdapterOptions) {\n this.maxSteps = options.maxSteps ?? 10;\n this.maxTokens = options.maxTokens ?? 4096;\n this.generationParams = options.generationParams ?? {};\n this.maxSseLineChars =\n options.maxSseLineChars ?? DEFAULT_MAX_SSE_LINE_CHARS;\n this.maxStreamTextChars =\n options.maxStreamTextChars ?? DEFAULT_MAX_STREAM_TEXT_CHARS;\n this.maxToolArgumentsChars =\n options.maxToolArgumentsChars ?? DEFAULT_MAX_TOOL_ARGUMENT_CHARS;\n\n if (isStreamBodyOptions(options)) {\n this.streamBody = options.streamBody;\n } else {\n const { endpointUrl, authenticate } = options;\n this.streamBody = async (body, signal) => {\n const fetchSignal =\n signal ?? AbortSignal.timeout(RAW_FETCH_DEFAULT_TIMEOUT_MS);\n const authHeaders = await authenticate();\n const response = await fetch(endpointUrl, {\n method: \"POST\",\n headers: {\n \"User-Agent\": APPKIT_USER_AGENT,\n \"Content-Type\": \"application/json\",\n ...authHeaders,\n },\n body: JSON.stringify(body),\n signal: fetchSignal,\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new Error(\n `Databricks API error (${response.status}): ${errorText}`,\n );\n }\n if (!response.body) throw new Error(\"No response body\");\n return response.body;\n };\n }\n }\n\n /**\n * Creates a DatabricksAdapter for a Databricks Model Serving endpoint.\n *\n * Routes through the shared `connectors/serving/stream` helper, which\n * delegates to the SDK's `apiClient.request({ raw: true })`. That gives the\n * adapter centralised URL encoding + authentication with the rest of the\n * serving surface — no bespoke `fetch()` + `authenticate()` plumbing.\n */\n static async fromServingEndpoint(\n options: ServingEndpointOptions,\n ): Promise<DatabricksAdapter> {\n const {\n workspaceClient,\n endpointName,\n maxSteps,\n maxTokens,\n generationParams,\n maxSseLineChars,\n maxStreamTextChars,\n maxToolArgumentsChars,\n } = options;\n return new DatabricksAdapter({\n streamBody: (body, signal) =>\n // Cast through the structural shape: the connector types\n // `workspaceClient` as the SDK's concrete `WorkspaceClient`, but we\n // only need `apiClient.request`.\n servingStream(\n workspaceClient as unknown as Parameters<typeof servingStream>[0],\n endpointName,\n body,\n signal,\n ),\n maxSteps,\n maxTokens,\n generationParams,\n maxSseLineChars,\n maxStreamTextChars,\n maxToolArgumentsChars,\n });\n }\n\n /**\n * Creates a DatabricksAdapter from a Model Serving endpoint name.\n * Auto-creates a WorkspaceClient internally. Reads the endpoint name\n * from the argument or the `DATABRICKS_SERVING_ENDPOINT_NAME` env var.\n *\n * @example\n * ```ts\n * // Reads endpoint from DATABRICKS_SERVING_ENDPOINT_NAME env var\n * const adapter = await DatabricksAdapter.fromModelServing();\n *\n * // Explicit endpoint\n * const adapter = await DatabricksAdapter.fromModelServing(\"my-endpoint\");\n *\n * // With options\n * const adapter = await DatabricksAdapter.fromModelServing(\"my-endpoint\", {\n * maxSteps: 5,\n * maxTokens: 2048,\n * });\n * ```\n */\n static async fromModelServing(\n endpointName?: string,\n options?: ModelServingOptions,\n ): Promise<DatabricksAdapter> {\n const resolvedEndpoint =\n endpointName ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;\n\n if (!resolvedEndpoint) {\n throw new Error(\n \"No endpoint name provided and DATABRICKS_SERVING_ENDPOINT_NAME env var is not set. \" +\n \"Pass an endpoint name or set DATABRICKS_SERVING_ENDPOINT_NAME.\",\n );\n }\n\n let workspaceClient: WorkspaceClientLike | undefined =\n options?.workspaceClient;\n if (!workspaceClient) {\n workspaceClient = createWorkspaceClient({\n clientOptions: getClientOptions(),\n }) as unknown as WorkspaceClientLike;\n }\n\n return DatabricksAdapter.fromServingEndpoint({\n workspaceClient,\n endpointName: resolvedEndpoint,\n maxSteps: options?.maxSteps,\n maxTokens: options?.maxTokens,\n generationParams: options?.generationParams,\n maxSseLineChars: options?.maxSseLineChars,\n maxStreamTextChars: options?.maxStreamTextChars,\n maxToolArgumentsChars: options?.maxToolArgumentsChars,\n });\n }\n\n /**\n * Discoverability shim for the Supervisor API adapter. Returns an\n * {@link AgentAdapter} (a `SupervisorApiAdapter` at runtime), NOT a\n * {@link DatabricksAdapter} — the two are separate classes (different\n * wire formats, different lifecycle). The return type is the\n * {@link AgentAdapter} interface so callers aren't bound to the concrete\n * class. Surfaced here so application developers see a single\n * `DatabricksAdapter.from*` autocomplete root.\n *\n * Dynamic-imports `./supervisor-api` to avoid forming a load-time cycle:\n * both files share `connectors/serving/client.ts`.\n *\n * @example\n * ```ts\n * import { DatabricksAdapter } from \"@databricks/appkit/beta\";\n *\n * const model = await DatabricksAdapter.fromSupervisorApi({\n * model: \"databricks-claude-sonnet-4-5\",\n * });\n * ```\n */\n static async fromSupervisorApi(\n options: import(\"./supervisor-api\").SupervisorApiAdapterOptions,\n ): Promise<AgentAdapter> {\n const { fromSupervisorApi } = await import(\"./supervisor-api\");\n return fromSupervisorApi(options);\n }\n\n async *run(\n input: AgentInput,\n context: AgentRunContext,\n ): AsyncGenerator<AgentEvent, void, unknown> {\n // Databricks API requires tool names to match [a-zA-Z0-9_-].\n // Our tool names use dots (e.g. \"analytics.query\"), so we swap dots\n // for double-underscores in the wire format and map back on receipt.\n const nameToWire = new Map<string, string>();\n const wireToName = new Map<string, string>();\n for (const tool of input.tools) {\n const wire = tool.name.replace(/\\./g, \"__\");\n if (wireToName.has(wire) && wireToName.get(wire) !== tool.name) {\n throw new Error(\n `Tool name collision: '${tool.name}' and '${wireToName.get(wire)}' both map to wire name '${wire}'`,\n );\n }\n nameToWire.set(tool.name, wire);\n wireToName.set(wire, tool.name);\n }\n\n const tools = this.buildTools(input.tools, nameToWire);\n const messages = this.buildMessages(input.messages, nameToWire);\n\n yield { type: \"status\", status: \"running\" };\n\n for (let step = 0; step < this.maxSteps; step++) {\n if (context.signal?.aborted) break;\n\n const { text, toolCalls } = yield* this.streamCompletion(\n messages,\n tools,\n context,\n );\n\n if (toolCalls.length === 0) {\n const parsed = parseTextToolCalls(text);\n if (parsed.length > 0) {\n yield* this.executeToolCalls(parsed, messages, context, nameToWire);\n continue;\n }\n break;\n }\n\n messages.push({\n role: \"assistant\",\n content: text || null,\n tool_calls: toolCalls,\n });\n\n for (const tc of toolCalls) {\n const wireName = tc.function.name;\n const originalName = wireToName.get(wireName) ?? wireName;\n yield* this.executeSingleTool(tc, originalName, messages, context);\n }\n }\n }\n\n /** Parse wire arguments, emit tool_call / tool_result, append tool messages. */\n private async *executeSingleTool(\n tc: OpenAIToolCall,\n originalName: string,\n messages: OpenAIMessage[],\n context: AgentRunContext,\n ): AsyncGenerator<AgentEvent, void, unknown> {\n let args: unknown;\n try {\n args = JSON.parse(tc.function.arguments);\n } catch {\n args = {};\n }\n\n yield { type: \"tool_call\", callId: tc.id, name: originalName, args };\n\n try {\n const result = await context.executeTool(originalName, args);\n const resultStr =\n typeof result === \"string\" ? result : JSON.stringify(result);\n\n yield { type: \"tool_result\", callId: tc.id, result };\n\n messages.push({\n role: \"tool\",\n content: resultStr,\n tool_call_id: tc.id,\n });\n } catch (error) {\n const errMsg =\n error instanceof Error ? error.message : \"Tool execution failed\";\n\n yield {\n type: \"tool_result\",\n callId: tc.id,\n result: null,\n error: errMsg,\n };\n\n messages.push({\n role: \"tool\",\n content: JSON.stringify({ error: errMsg }),\n tool_call_id: tc.id,\n });\n }\n }\n\n private async *streamCompletion(\n messages: OpenAIMessage[],\n tools: OpenAITool[],\n context: AgentRunContext,\n ): AsyncGenerator<\n AgentEvent,\n { text: string; toolCalls: OpenAIToolCall[] },\n unknown\n > {\n const body: Record<string, unknown> = {\n messages,\n stream: true,\n max_tokens: this.maxTokens,\n };\n\n applyGenerationParams(body, this.generationParams);\n\n if (tools.length > 0) {\n body.tools = tools;\n }\n\n let responseBody: ReadableStream<Uint8Array>;\n try {\n responseBody = await this.streamBody(body, context.signal);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Stream request failed\";\n yield { type: \"status\", status: \"error\", error: msg };\n throw err;\n }\n\n const reader = responseBody.getReader();\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n let fullText = \"\";\n const toolCallAccumulator = new Map<\n number,\n {\n id: string;\n name: string;\n arguments: string;\n thoughtSignature?: string;\n }\n >();\n\n try {\n while (true) {\n if (context.signal?.aborted) break;\n\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n if (buffer.length > this.maxSseLineChars) {\n throw new Error(\n `DatabricksAdapter: SSE line buffer exceeds configured limit (${this.maxSseLineChars} UTF-16 code units)`,\n );\n }\n\n for (const line of lines) {\n if (line.length > this.maxSseLineChars) {\n throw new Error(\n `DatabricksAdapter: SSE line exceeds configured limit (${this.maxSseLineChars} UTF-16 code units)`,\n );\n }\n\n const trimmed = line.trim();\n if (!trimmed.startsWith(\"data: \")) continue;\n const data = trimmed.slice(6);\n if (data === \"[DONE]\") continue;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n } catch (parseErr) {\n console.debug(\n \"[DatabricksAdapter] malformed SSE data line JSON\",\n { line: `${data.slice(0, 256)}${data.length > 256 ? \"…\" : \"\"}` },\n parseErr,\n );\n continue;\n }\n\n const deltaUnknown = openAiChoicesDelta(parsed);\n if (!isRecord(deltaUnknown)) continue;\n\n // gpt-oss (harmony) sends delta.content as an array of parts\n // (text = answer, reasoning = chain-of-thought), not a string.\n const contentUnknown = deltaUnknown.content;\n const parts =\n typeof contentUnknown === \"string\"\n ? [{ type: \"text\", text: contentUnknown }]\n : Array.isArray(contentUnknown)\n ? contentUnknown\n : [];\n\n for (const part of parts) {\n if (!isRecord(part)) continue;\n if (part.type === \"text\" && typeof part.text === \"string\") {\n const content = part.text;\n throwIfExceedsStreamLimit(\n \"streamed assistant text\",\n fullText.length,\n content,\n this.maxStreamTextChars,\n );\n fullText += content;\n yield { type: \"message_delta\" as const, content };\n } else if (part.type === \"reasoning\") {\n const content = reasoningPartText(part);\n if (content) yield { type: \"thinking\" as const, content };\n }\n }\n\n const toolCallsRaw = deltaUnknown.tool_calls;\n if (!Array.isArray(toolCallsRaw)) continue;\n\n for (const tc of toolCallsRaw) {\n if (!isStreamingDeltaToolCall(tc)) continue;\n const sig = tc.thoughtSignature;\n const existing = toolCallAccumulator.get(tc.index);\n if (existing) {\n if (tc.function?.arguments) {\n throwIfExceedsStreamLimit(\n \"tool call arguments\",\n existing.arguments.length,\n tc.function.arguments,\n this.maxToolArgumentsChars,\n );\n existing.arguments += tc.function.arguments;\n }\n if (sig && !existing.thoughtSignature) {\n existing.thoughtSignature = sig;\n }\n } else {\n const initial = tc.function?.arguments ?? \"\";\n if (initial.length > this.maxToolArgumentsChars) {\n throw new Error(\n `DatabricksAdapter: tool call arguments exceed configured limit (${this.maxToolArgumentsChars} UTF-16 code units)`,\n );\n }\n toolCallAccumulator.set(tc.index, {\n id: tc.id ?? `call_${tc.index}`,\n name: tc.function?.name ?? \"\",\n arguments: initial,\n ...(sig ? { thoughtSignature: sig } : {}),\n });\n }\n }\n }\n }\n } finally {\n try {\n await reader.cancel();\n } catch (cancelErr) {\n console.debug(\n \"[DatabricksAdapter] reader.cancel() failed during teardown\",\n cancelErr,\n );\n }\n try {\n reader.releaseLock();\n } catch (unlockErr) {\n console.debug(\n \"[DatabricksAdapter] reader.releaseLock() failed during teardown\",\n unlockErr,\n );\n }\n }\n\n const toolCalls: OpenAIToolCall[] = Array.from(\n toolCallAccumulator.values(),\n ).map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments || \"{}\" },\n ...(tc.thoughtSignature ? { thoughtSignature: tc.thoughtSignature } : {}),\n }));\n\n return { text: fullText, toolCalls };\n }\n\n private async *executeToolCalls(\n calls: Array<{ name: string; args: unknown }>,\n messages: OpenAIMessage[],\n context: AgentRunContext,\n nameToWire: Map<string, string>,\n ): AsyncGenerator<AgentEvent, void, unknown> {\n const wireToolName = (name: string) =>\n nameToWire.get(name) ?? name.replace(/\\./g, \"__\");\n\n const toolCallObjs: OpenAIToolCall[] = calls.map((c, i) => ({\n id: `text_call_${i}`,\n type: \"function\" as const,\n function: {\n name: wireToolName(c.name),\n arguments: JSON.stringify(c.args),\n },\n }));\n\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: toolCallObjs,\n });\n\n for (let i = 0; i < toolCallObjs.length; i++) {\n const tc = toolCallObjs[i];\n const originalName = calls[i]?.name ?? tc.function.name;\n yield* this.executeSingleTool(tc, originalName, messages, context);\n }\n }\n\n /**\n * Maps AppKit {@link AgentInput} messages into OpenAI-compatible wire messages.\n * Preserves multi-turn tool state (`toolCalls` → `tool_calls`, `toolCallId` →\n * `tool_call_id`) so resumed threads and hydrated history reach the model.\n */\n private buildMessages(\n messages: AgentInput[\"messages\"],\n nameToWire: Map<string, string>,\n ): OpenAIMessage[] {\n const wireToolName = (name: string) =>\n nameToWire.get(name) ?? name.replace(/\\./g, \"__\");\n\n return messages.map((m) => {\n let content: string | null = m.content;\n if (\n m.role === \"assistant\" &&\n m.toolCalls &&\n m.toolCalls.length > 0 &&\n (!m.content || m.content.trim() === \"\")\n ) {\n content = null;\n }\n\n const out: OpenAIMessage = {\n role: m.role as OpenAIMessage[\"role\"],\n content,\n };\n\n if (m.toolCallId) {\n out.tool_call_id = m.toolCallId;\n }\n\n if (m.toolCalls && m.toolCalls.length > 0) {\n out.tool_calls = m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: wireToolName(tc.name),\n arguments:\n typeof tc.args === \"string\"\n ? tc.args\n : JSON.stringify(tc.args ?? {}),\n },\n ...(tc.thoughtSignature\n ? { thoughtSignature: tc.thoughtSignature }\n : {}),\n }));\n }\n\n return out;\n });\n }\n\n private buildTools(\n definitions: AgentToolDefinition[],\n nameToWire: Map<string, string>,\n ): OpenAITool[] {\n return definitions.map((def) => ({\n type: \"function\" as const,\n function: {\n name: nameToWire.get(def.name) ?? def.name,\n description: def.description,\n parameters: def.parameters,\n },\n }));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Text-based tool call parsing (fallback)\n// ---------------------------------------------------------------------------\n\n/**\n * Parses text-based tool calls from model output.\n *\n * Handles two formats:\n * 1. Llama native: `[{\"name\": \"tool_name\", \"parameters\": {\"arg\": \"val\"}}]`\n * 2. Python-style: `[tool_name(arg1='val1', arg2='val2')]`\n */\nexport function parseTextToolCalls(\n text: string,\n): Array<{ name: string; args: unknown }> {\n const trimmed = text.trim();\n\n const jsonResult = tryParseLlamaJsonToolCalls(trimmed);\n if (jsonResult.length > 0) return jsonResult;\n\n const pyResult = tryParsePythonStyleToolCalls(trimmed);\n if (pyResult.length > 0) return pyResult;\n\n return [];\n}\n\nfunction isLlamaToolJsonItem(value: unknown): value is Record<\n string,\n unknown\n> & {\n name: string;\n} {\n if (!isRecord(value)) return false;\n return typeof value.name === \"string\";\n}\n\nfunction tryParseLlamaJsonToolCalls(\n text: string,\n): Array<{ name: string; args: unknown }> {\n const slice = extractLlamaToolJsonSlice(text);\n if (!slice) return [];\n\n try {\n const parsed: unknown = JSON.parse(slice);\n if (!Array.isArray(parsed)) return [];\n\n return parsed.filter(isLlamaToolJsonItem).map((item) => ({\n name: item.name,\n args: item.parameters ?? item.arguments ?? item.args ?? {},\n }));\n } catch {\n return [];\n }\n}\n\nfunction tryParsePythonStyleToolCalls(\n text: string,\n): Array<{ name: string; args: unknown }> {\n if (text.length > PYTHON_STYLE_TOOL_PARSE_MAX_INPUT) {\n return [];\n }\n\n const pattern = /\\[?([a-zA-Z_][\\w.]*)\\(([^)]*)\\)\\]?/g;\n const results: Array<{ name: string; args: unknown }> = [];\n\n for (const match of text.matchAll(pattern)) {\n const name = match[1];\n const argsStr = match[2];\n\n const args: Record<string, unknown> = {};\n const argPattern = /(\\w+)\\s*=\\s*(?:'([^']*)'|\"([^\"]*)\"|(\\S+))/g;\n for (const argMatch of argsStr.matchAll(argPattern)) {\n const key = argMatch[1];\n const value = argMatch[2] ?? argMatch[3] ?? argMatch[4];\n args[key] = value;\n }\n\n results.push({ name, args });\n }\n\n return results;\n}\n"],"mappings":"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"}
|
package/dist/appkit/package.js
CHANGED
|
@@ -19,7 +19,7 @@ import { getRoutes, printRoutes } from "./utils.js";
|
|
|
19
19
|
import { StaticServer } from "./static-server.js";
|
|
20
20
|
import { ViteDevServer } from "./vite-dev-server.js";
|
|
21
21
|
import path from "node:path";
|
|
22
|
-
import
|
|
22
|
+
import fs from "node:fs";
|
|
23
23
|
import dotenv from "dotenv";
|
|
24
24
|
import express from "express";
|
|
25
25
|
import getPort, { portNumbers } from "get-port";
|
|
@@ -246,7 +246,7 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
246
246
|
const cwd = process.cwd();
|
|
247
247
|
for (const p of staticPaths) {
|
|
248
248
|
const fullPath = path.resolve(cwd, p);
|
|
249
|
-
if (
|
|
249
|
+
if (fs.existsSync(path.resolve(fullPath, "index.html"))) {
|
|
250
250
|
logger.debug("Static files: serving from %s", fullPath);
|
|
251
251
|
return fullPath;
|
|
252
252
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["manifest","fs"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\n\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppKitError, ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /**\n * Budget for awaiting `server.close()` during shutdown. Bounded because\n * `closeAllConnections()` runs immediately before the await, so the close\n * is expected to resolve promptly; the core lifecycle manager's overall\n * force-exit timer is the ultimate backstop if it does not.\n */\n private static readonly SERVER_CLOSE_TIMEOUT_MS = 2_000;\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = defineManifest<\"server\">(manifest);\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n declare protected config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n /**\n * Resolves when `server.close()` completes. Created in\n * {@link abortActiveOperations} (which initiates the close) and awaited in\n * {@link closeRemainingConnections} after other plugins have drained.\n */\n private serverClosed?: Promise<void>;\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n // Terminal error handler — registered after all routes, extensions, and\n // frontend middleware so that errors forwarded via next(err) from any of\n // them (e.g. async handler rejections forwarded by Plugin.route()) are\n // turned into JSON error responses instead of hanging the request.\n this.serverApplication.use(errorHandlerMiddleware);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n // Graceful shutdown is orchestrated by the core LifecycleManager, which\n // owns the process signal handlers. This plugin only contributes its\n // HTTP concerns: it aborts/initiates the socket close in\n // abortActiveOperations(), drains dev servers in shutdown(), and force-\n // closes remaining sockets in the \"shutdown\" lifecycle hook below (which\n // fires after every plugin's shutdown() has run).\n this.context?.onLifecycle(\"shutdown\", () =>\n this.closeRemainingConnections(),\n );\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * Note: async handlers registered directly on the app must handle their own\n * rejections — Express 4 does not forward rejected promises to the error\n * middleware. Errors passed explicitly to `next(err)` are formatted by the\n * terminal error middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of plugins.values()) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${camelToKebab(plugin.name)}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n /**\n * Cancel in-flight work and begin closing the HTTP server.\n *\n * Runs in the first phase of the core LifecycleManager's graceful shutdown,\n * before any plugin `shutdown()` hook. In addition to the base behavior\n * (aborting SSE streams), it stops the server accepting new connections and\n * drops idle keep-alive sockets — without `closeIdleConnections()`, a\n * connected browser would pin `server.close()` open until the force-exit\n * timer fires. The close is only *initiated* here; the returned promise is\n * awaited later in {@link closeRemainingConnections} so peer plugins can\n * still drain in-flight requests through the server first.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n\n if (this.server) {\n const server = this.server;\n this.serverClosed = new Promise((resolve) => {\n server.close(() => resolve());\n });\n server.closeIdleConnections();\n }\n }\n\n /**\n * Drain the dev-only servers as this plugin's `shutdown()` hook.\n *\n * Runs concurrently with the other plugins' hooks during graceful shutdown.\n * These are no-ops in production (neither is created), so this hook is\n * effectively dev-only.\n */\n async shutdown(): Promise<void> {\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n }\n\n /**\n * Force-close whatever sockets remain and await the server close.\n *\n * Registered on the `\"shutdown\"` lifecycle event in {@link start}, so it\n * fires after every plugin's `shutdown()` hook has run — meaning any\n * in-flight request a peer plugin was draining has already completed.\n * `closeAllConnections()` destroys the leftover sockets (aborted SSE\n * responses, keep-alive connections) synchronously so the pending\n * `server.close()` can complete; the await is bounded because the close is\n * expected to resolve promptly once the sockets are gone.\n */\n private async closeRemainingConnections(): Promise<void> {\n if (this.server) {\n this.server.closeAllConnections();\n }\n if (!this.serverClosed) return;\n\n await Promise.race([\n this.serverClosed,\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ServerPlugin.SERVER_CLOSE_TIMEOUT_MS);\n timer.unref();\n }),\n ]);\n logger.debug(\"Server closed gracefully\");\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property in the HTTP error range (e.g. `ApiError` from\n * `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n if (!(error instanceof Error) || !(\"statusCode\" in error)) return false;\n const statusCode = error.statusCode;\n return (\n typeof statusCode === \"number\" &&\n Number.isInteger(statusCode) &&\n statusCode >= 400 &&\n statusCode <= 599\n );\n}\n\n/**\n * Terminal Express error-handling middleware.\n *\n * Converts errors forwarded via `next(err)` (including async handler\n * rejections forwarded by `Plugin.route()`) into JSON error responses,\n * following the same status/message conventions as `Plugin.execute()`:\n * - errors carrying an HTTP `statusCode` (including `AppKitError`) → that\n * status; 5xx messages are masked in production to avoid leaking internals\n * - anything else → 500 with a generic message in production\n *\n * 4xx errors are expected client errors (e.g. missing auth headers) and are\n * logged at `warn` with just the message; 5xx/unknown errors are logged at\n * `error` with the full error.\n *\n * @internal Exported for unit tests.\n */\nexport function errorHandlerMiddleware(\n err: unknown,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const httpError =\n err instanceof AppKitError || hasHttpStatusCode(err) ? err : null;\n const statusCode = httpError ? httpError.statusCode : 500;\n const isClientError = statusCode < 500;\n\n if (isClientError) {\n logger.warn(\n \"Request failed for %s %s with status %d: %s\",\n req.method,\n req.originalUrl,\n statusCode,\n err instanceof Error ? err.message : String(err),\n );\n } else {\n logger.error(\n \"Unhandled error for %s %s: %O\",\n req.method,\n req.originalUrl,\n // toJSON() redacts sensitive context fields; %O on a raw AppKitError\n // would enumerate them unredacted.\n err instanceof AppKitError ? err.toJSON() : err,\n );\n }\n\n if (res.headersSent) {\n next(err);\n return;\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n const message = err instanceof Error ? err.message : \"Server error\";\n\n res.status(statusCode).json({\n error: isDev || isClientError ? message : \"Server error\",\n });\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\n\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppKitError, ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /**\n * Budget for awaiting `server.close()` during shutdown. Bounded because\n * `closeAllConnections()` runs immediately before the await, so the close\n * is expected to resolve promptly; the core lifecycle manager's overall\n * force-exit timer is the ultimate backstop if it does not.\n */\n private static readonly SERVER_CLOSE_TIMEOUT_MS = 2_000;\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = defineManifest<\"server\">(manifest);\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n declare protected config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n /**\n * Resolves when `server.close()` completes. Created in\n * {@link abortActiveOperations} (which initiates the close) and awaited in\n * {@link closeRemainingConnections} after other plugins have drained.\n */\n private serverClosed?: Promise<void>;\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n // Terminal error handler — registered after all routes, extensions, and\n // frontend middleware so that errors forwarded via next(err) from any of\n // them (e.g. async handler rejections forwarded by Plugin.route()) are\n // turned into JSON error responses instead of hanging the request.\n this.serverApplication.use(errorHandlerMiddleware);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n // Graceful shutdown is orchestrated by the core LifecycleManager, which\n // owns the process signal handlers. This plugin only contributes its\n // HTTP concerns: it aborts/initiates the socket close in\n // abortActiveOperations(), drains dev servers in shutdown(), and force-\n // closes remaining sockets in the \"shutdown\" lifecycle hook below (which\n // fires after every plugin's shutdown() has run).\n this.context?.onLifecycle(\"shutdown\", () =>\n this.closeRemainingConnections(),\n );\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * Note: async handlers registered directly on the app must handle their own\n * rejections — Express 4 does not forward rejected promises to the error\n * middleware. Errors passed explicitly to `next(err)` are formatted by the\n * terminal error middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of plugins.values()) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${camelToKebab(plugin.name)}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n /**\n * Cancel in-flight work and begin closing the HTTP server.\n *\n * Runs in the first phase of the core LifecycleManager's graceful shutdown,\n * before any plugin `shutdown()` hook. In addition to the base behavior\n * (aborting SSE streams), it stops the server accepting new connections and\n * drops idle keep-alive sockets — without `closeIdleConnections()`, a\n * connected browser would pin `server.close()` open until the force-exit\n * timer fires. The close is only *initiated* here; the returned promise is\n * awaited later in {@link closeRemainingConnections} so peer plugins can\n * still drain in-flight requests through the server first.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n\n if (this.server) {\n const server = this.server;\n this.serverClosed = new Promise((resolve) => {\n server.close(() => resolve());\n });\n server.closeIdleConnections();\n }\n }\n\n /**\n * Drain the dev-only servers as this plugin's `shutdown()` hook.\n *\n * Runs concurrently with the other plugins' hooks during graceful shutdown.\n * These are no-ops in production (neither is created), so this hook is\n * effectively dev-only.\n */\n async shutdown(): Promise<void> {\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n }\n\n /**\n * Force-close whatever sockets remain and await the server close.\n *\n * Registered on the `\"shutdown\"` lifecycle event in {@link start}, so it\n * fires after every plugin's `shutdown()` hook has run — meaning any\n * in-flight request a peer plugin was draining has already completed.\n * `closeAllConnections()` destroys the leftover sockets (aborted SSE\n * responses, keep-alive connections) synchronously so the pending\n * `server.close()` can complete; the await is bounded because the close is\n * expected to resolve promptly once the sockets are gone.\n */\n private async closeRemainingConnections(): Promise<void> {\n if (this.server) {\n this.server.closeAllConnections();\n }\n if (!this.serverClosed) return;\n\n await Promise.race([\n this.serverClosed,\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ServerPlugin.SERVER_CLOSE_TIMEOUT_MS);\n timer.unref();\n }),\n ]);\n logger.debug(\"Server closed gracefully\");\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property in the HTTP error range (e.g. `ApiError` from\n * `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n if (!(error instanceof Error) || !(\"statusCode\" in error)) return false;\n const statusCode = error.statusCode;\n return (\n typeof statusCode === \"number\" &&\n Number.isInteger(statusCode) &&\n statusCode >= 400 &&\n statusCode <= 599\n );\n}\n\n/**\n * Terminal Express error-handling middleware.\n *\n * Converts errors forwarded via `next(err)` (including async handler\n * rejections forwarded by `Plugin.route()`) into JSON error responses,\n * following the same status/message conventions as `Plugin.execute()`:\n * - errors carrying an HTTP `statusCode` (including `AppKitError`) → that\n * status; 5xx messages are masked in production to avoid leaking internals\n * - anything else → 500 with a generic message in production\n *\n * 4xx errors are expected client errors (e.g. missing auth headers) and are\n * logged at `warn` with just the message; 5xx/unknown errors are logged at\n * `error` with the full error.\n *\n * @internal Exported for unit tests.\n */\nexport function errorHandlerMiddleware(\n err: unknown,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const httpError =\n err instanceof AppKitError || hasHttpStatusCode(err) ? err : null;\n const statusCode = httpError ? httpError.statusCode : 500;\n const isClientError = statusCode < 500;\n\n if (isClientError) {\n logger.warn(\n \"Request failed for %s %s with status %d: %s\",\n req.method,\n req.originalUrl,\n statusCode,\n err instanceof Error ? err.message : String(err),\n );\n } else {\n logger.error(\n \"Unhandled error for %s %s: %O\",\n req.method,\n req.originalUrl,\n // toJSON() redacts sensitive context fields; %O on a raw AppKitError\n // would enumerate them unredacted.\n err instanceof AppKitError ? err.toJSON() : err,\n );\n }\n\n if (res.headersSent) {\n next(err);\n return;\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n const message = err instanceof Error ? err.message : \"Server error\";\n\n res.status(statusCode).json({\n error: isDev || isClientError ? message : \"Server error\",\n });\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { REMOTE_TUNNEL_ASSET_PREFIXES } from "./gate.js";
|
|
|
3
3
|
import { generateTunnelIdFromEmail, getConfigScript, parseCookies } from "../utils.js";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import fs from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { WebSocketServer } from "ws";
|
|
9
9
|
|
|
@@ -118,7 +118,7 @@ var RemoteTunnelManager = class {
|
|
|
118
118
|
sameSite: "lax"
|
|
119
119
|
});
|
|
120
120
|
const indexPath = path.join(__dirname, "index.html");
|
|
121
|
-
let html =
|
|
121
|
+
let html = fs.readFileSync(indexPath, "utf-8");
|
|
122
122
|
html = html.replace("<body>", `<body>${getConfigScript()}`);
|
|
123
123
|
res.send(html);
|
|
124
124
|
};
|
|
@@ -133,7 +133,7 @@ var RemoteTunnelManager = class {
|
|
|
133
133
|
}
|
|
134
134
|
loadHtmlTemplate(filename, replacements) {
|
|
135
135
|
const filePath = path.join(__dirname, filename);
|
|
136
|
-
let content =
|
|
136
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
137
137
|
for (const [key, value] of Object.entries(replacements)) content = content.replaceAll(`{{${key}}}`, value);
|
|
138
138
|
return content;
|
|
139
139
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-tunnel-manager.js","names":["fs"],"sources":["../../../../src/plugins/server/remote-tunnel/remote-tunnel-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type express from \"express\";\nimport type { TunnelConnection } from \"shared\";\nimport { WebSocketServer } from \"ws\";\n\nimport { createLogger } from \"../../../logging/logger\";\nimport {\n generateTunnelIdFromEmail,\n getConfigScript,\n parseCookies,\n} from \"../utils\";\nimport { REMOTE_TUNNEL_ASSET_PREFIXES } from \"./gate\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst MAX_ASSET_FETCH_TIMEOUT = 60_000;\n\nconst logger = createLogger(\"server:remote-tunnel\");\n\ninterface DevFileReader {\n registerTunnelGetter(\n getter: (req: express.Request) => TunnelConnection | null,\n ): void;\n}\n\n/**\n * WebSocket message types for CLI <-> Server communication\n */\ntype WebSocketMessage =\n | {\n type: \"connection:response\";\n viewer: string;\n approved: boolean;\n }\n | {\n type: \"fetch:response:meta\";\n requestId: string;\n status: number;\n headers: Record<string, string>;\n }\n | {\n type: \"file:read:response\";\n requestId: string;\n content?: string;\n error?: string;\n }\n | {\n type: \"dir:list:response\";\n requestId: string;\n content?: string;\n error?: string;\n }\n | {\n type: \"hmr:message\";\n body: string;\n };\n\n/**\n * Type guard to validate WebSocket message structure\n */\nfunction isWebSocketMessage(data: unknown): data is WebSocketMessage {\n if (!data || typeof data !== \"object\") {\n return false;\n }\n\n const msg = data as Record<string, unknown>;\n return typeof msg.type === \"string\";\n}\n\n/**\n * Remote tunnel manager for the AppKit.\n *\n * This class is responsible for managing the remote tunnels for the development server.\n * It also handles the asset fetching and the HMR for the development server.\n *\n * @example\n * ```ts\n * const remoteTunnelManager = new RemoteTunnelManager(devFileReader);\n * remoteTunnelManager.setup(app);\n * ```\n */\nexport class RemoteTunnelManager {\n private tunnels = new Map<string, TunnelConnection>();\n private wss: WebSocketServer;\n private hmrWss: WebSocketServer;\n private server?: HTTPServer;\n private devFileReader: DevFileReader;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.wss = new WebSocketServer({ noServer: true, path: \"/dev-tunnel\" });\n this.hmrWss = new WebSocketServer({ noServer: true, path: \"/dev-hmr\" });\n\n this.registerTunnelGetter();\n }\n\n setServer(server: HTTPServer) {\n this.server = server;\n }\n\n /** Asset middleware for the development server. */\n assetMiddleware() {\n return async (req: express.Request, res: express.Response) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n\n // Try cookie first, then generate from email\n let tunnelId: string | undefined;\n const cookieHeader = req.headers.cookie;\n\n if (cookieHeader) {\n // Fast path: extract dev-tunnel-id from cookie without full parse\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n if (!tunnelId) return res.status(404).send(\"Tunnel not ready\");\n\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) return res.status(404).send(\"Tunnel not found\");\n\n const { ws, approvedViewers, pendingFetches } = tunnel;\n\n if (!approvedViewers.has(email)) {\n return res.status(403).send(\"Not approved for this tunnel\");\n }\n\n const path = req.originalUrl;\n const requestId = randomUUID();\n\n const request = { type: \"fetch\", path, method: req.method, requestId };\n\n const response = await new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFetches.delete(requestId);\n reject(new Error(\"Asset fetch timeout\"));\n }, MAX_ASSET_FETCH_TIMEOUT);\n\n pendingFetches.set(requestId, { resolve, reject, timeout });\n\n ws.send(JSON.stringify(request));\n }).catch((err) => {\n logger.error(\"Failed to fetch %s: %s\", path, err.message);\n return { status: 504, body: Buffer.from(\"\"), headers: {} };\n });\n\n const r = response as any;\n\n res\n .status(r.status)\n .set(r.headers)\n .send(r.body || Buffer.from(\"\"));\n };\n }\n\n /** Dev mode middleware for the development server. */\n devModeMiddleware() {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n const dev = req.query?.dev;\n\n if (dev === undefined) {\n return next();\n }\n\n if (\n req.path.startsWith(\"/api\") ||\n req.path.startsWith(\"/query\") ||\n req.path.match(/\\.(js|css|png|jpg|jpeg|svg|ico|json|woff|woff2|ttf)$/)\n ) {\n return next();\n }\n\n const viewerEmail = req.headers[\"x-forwarded-email\"] as string;\n const isOwnerMode = dev === \"\" || dev === \"true\";\n\n const tunnelId = isOwnerMode\n ? generateTunnelIdFromEmail(viewerEmail)\n : dev.toString();\n\n if (!tunnelId) {\n return res.status(400).send(\"Invalid tunnel ID\");\n }\n\n if (!isOwnerMode) {\n const approvalResponse = this.handleViewerApproval(\n tunnelId,\n viewerEmail,\n req.query.retry === \"true\",\n res,\n );\n\n if (approvalResponse) {\n return approvalResponse;\n }\n }\n\n res.cookie(\"dev-tunnel-id\", tunnelId, {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n });\n\n const indexPath = path.join(__dirname, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${getConfigScript()}`);\n\n res.send(html);\n };\n }\n\n /** Setup the dev mode middleware. */\n setup(app: express.Application) {\n app.use(this.devModeMiddleware());\n app.use(REMOTE_TUNNEL_ASSET_PREFIXES, this.assetMiddleware());\n }\n\n static isRemoteServerEnabled() {\n return (\n process.env.NODE_ENV !== \"production\" &&\n process.env.DISABLE_REMOTE_SERVING !== \"true\" &&\n // DATABRICKS_CLIENT_SECRET is set in the .env file for deployed environments\n Boolean(process.env.DATABRICKS_CLIENT_SECRET)\n );\n }\n\n private loadHtmlTemplate(\n filename: string,\n replacements: Record<string, string>,\n ): string {\n const filePath = path.join(__dirname, filename);\n let content = fs.readFileSync(filePath, \"utf-8\");\n\n for (const [key, value] of Object.entries(replacements)) {\n content = content.replaceAll(`{{${key}}}`, value);\n }\n\n return content;\n }\n\n private handleViewerApproval(\n tunnelId: string,\n viewerEmail: string,\n retry: boolean,\n res: express.Response,\n ): express.Response | null {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) {\n return res.status(404).send(\"Tunnel not found\");\n }\n\n if (viewerEmail === tunnel.owner) {\n return null;\n }\n\n if (retry) {\n tunnel.rejectedViewers.delete(viewerEmail);\n }\n\n if (tunnel.rejectedViewers.has(viewerEmail)) {\n const html = this.loadHtmlTemplate(\"denied.html\", { tunnelId });\n return res.status(403).send(html);\n }\n\n if (tunnel.approvedViewers.has(viewerEmail)) {\n return null;\n }\n\n if (!tunnel.pendingRequests.has(viewerEmail)) {\n const requestId = randomUUID();\n tunnel.pendingRequests.add(viewerEmail);\n tunnel.ws.send(\n JSON.stringify({\n type: \"connection:request\",\n requestId,\n viewer: viewerEmail,\n }),\n );\n }\n\n const html = this.loadHtmlTemplate(\"wait.html\", { tunnelId });\n return res.status(200).send(html);\n }\n\n setupWebSocket() {\n this.wss.on(\"connection\", (ws, req) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId = generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return ws.close();\n\n this.tunnels.set(tunnelId, {\n ws,\n owner: email,\n approvedViewers: new Set([email]),\n pendingRequests: new Set(),\n rejectedViewers: new Set(),\n pendingFetches: new Map(),\n pendingFileReads: new Map(),\n waitingForBinaryBody: null,\n });\n\n ws.on(\"message\", (msg, isBinary) => {\n const tunnel = this.tunnels.get(tunnelId);\n if (!tunnel) return;\n\n if (isBinary) {\n if (!tunnel.waitingForBinaryBody) {\n logger.debug(\n \"Received binary message but no requestId is waiting for body\",\n );\n return;\n }\n\n const requestId = tunnel.waitingForBinaryBody;\n const pending = tunnel.pendingFetches.get(requestId);\n\n if (!pending || !pending.metadata) {\n logger.debug(\"Received binary message but pending fetch not found\");\n tunnel.waitingForBinaryBody = null;\n return;\n }\n\n tunnel.waitingForBinaryBody = null;\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(requestId);\n\n pending.resolve({\n status: pending.metadata.status,\n headers: pending.metadata.headers,\n body: msg as Buffer,\n });\n return;\n }\n\n try {\n const data = JSON.parse(msg.toString());\n\n // Validate message structure\n if (!isWebSocketMessage(data)) {\n logger.error(\"Invalid WebSocket message format: %O\", data);\n return;\n }\n\n if (data.type === \"connection:response\") {\n if (tunnel && data.viewer) {\n tunnel.pendingRequests.delete(data.viewer);\n\n if (data.approved) {\n tunnel.approvedViewers.add(data.viewer);\n logger.debug(\n \"✅ Approved %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n } else {\n tunnel.rejectedViewers.add(data.viewer);\n logger.debug(\n \"❌ Denied %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n }\n }\n } else if (data.type === \"fetch:response:meta\") {\n const pending = tunnel.pendingFetches.get(data.requestId);\n if (pending) {\n pending.metadata = {\n status: data.status,\n headers: data.headers,\n };\n if (\n data.status === 304 ||\n data.status === 204 ||\n (data.status >= 300 && data.status < 400)\n ) {\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(data.requestId);\n pending.resolve({\n status: data.status,\n headers: data.headers,\n body: Buffer.from(\"\"),\n });\n } else {\n tunnel.waitingForBinaryBody = data.requestId;\n }\n }\n } else if (data.type === \"file:read:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in file:read:response\"),\n );\n }\n }\n } else if (data.type === \"dir:list:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in dir:list:response\"),\n );\n }\n }\n }\n } catch (e) {\n logger.error(\"Failed to parse WebSocket message: %O\", e);\n }\n });\n\n ws.send(JSON.stringify({ type: \"tunnel:ready\", tunnelId }));\n\n ws.on(\"close\", () => {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (tunnel) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Tunnel closed\"));\n }\n tunnel.pendingFetches.clear();\n }\n\n this.tunnels.delete(tunnelId);\n });\n });\n\n this.hmrWss.on(\"connection\", (browserWs, req) => {\n const cookies = parseCookies(req);\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId =\n cookies[\"dev-tunnel-id\"] || generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return browserWs.close();\n\n const cliTunnel = this.tunnels.get(tunnelId);\n\n if (!cliTunnel) return browserWs.close();\n\n const { ws: cliWs, approvedViewers } = cliTunnel;\n\n if (!approvedViewers.has(email)) {\n return browserWs.close(1008, \"Not approved\");\n }\n // Browser → CLI\n browserWs.on(\"message\", (msg) => {\n const hmrStart = Date.now();\n logger.debug(\"browser -> cli browserWS message: %s\", msg.toString());\n cliWs.send(\n JSON.stringify({\n type: \"hmr:message\",\n body: msg.toString(),\n timestamp: hmrStart,\n }),\n );\n });\n\n // // CLI → Browser\n const cliHandler = (msg: Buffer | string, isBinary: boolean) => {\n // Ignore binary messages (they're for fetch responses, not HMR)\n if (isBinary) return;\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"hmr:message\") {\n browserWs.send(data.body);\n }\n } catch {\n logger.error(\n \"Failed to parse CLI message for HMR: %s\",\n msg.toString().substring(0, 100),\n );\n }\n };\n cliWs.on(\"message\", cliHandler);\n\n browserWs.on(\"close\", () => {\n cliWs.off(\"message\", cliHandler);\n });\n });\n\n // // Browser HMR connection\n this.server?.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n\n if (url.startsWith(\"/dev-tunnel\")) {\n this.wss.handleUpgrade(req, socket, head, (ws) => {\n this.wss.emit(\"connection\", ws, req);\n });\n } else if (url.startsWith(\"/dev-hmr\")) {\n this.hmrWss.handleUpgrade(req, socket, head, (browserWs) => {\n this.hmrWss.emit(\"connection\", browserWs, req);\n });\n }\n });\n }\n\n registerTunnelGetter() {\n this.devFileReader.registerTunnelGetter(\n this.getTunnelForRequest.bind(this),\n );\n }\n\n getTunnelForRequest(req: express.Request) {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const cookieHeader = req.headers.cookie;\n\n let tunnelId: string | undefined;\n\n if (cookieHeader) {\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n return tunnelId ? this.tunnels.get(tunnelId) || null : null;\n }\n\n cleanup() {\n for (const [, tunnel] of this.tunnels) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Server shutting down\"));\n }\n tunnel.pendingFetches.clear();\n tunnel.ws.close();\n }\n this.tunnels.clear();\n\n if (this.wss) {\n this.wss.close();\n }\n if (this.hmrWss) {\n this.hmrWss.close();\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"remote-tunnel-manager.js","names":[],"sources":["../../../../src/plugins/server/remote-tunnel/remote-tunnel-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type express from \"express\";\nimport type { TunnelConnection } from \"shared\";\nimport { WebSocketServer } from \"ws\";\n\nimport { createLogger } from \"../../../logging/logger\";\nimport {\n generateTunnelIdFromEmail,\n getConfigScript,\n parseCookies,\n} from \"../utils\";\nimport { REMOTE_TUNNEL_ASSET_PREFIXES } from \"./gate\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst MAX_ASSET_FETCH_TIMEOUT = 60_000;\n\nconst logger = createLogger(\"server:remote-tunnel\");\n\ninterface DevFileReader {\n registerTunnelGetter(\n getter: (req: express.Request) => TunnelConnection | null,\n ): void;\n}\n\n/**\n * WebSocket message types for CLI <-> Server communication\n */\ntype WebSocketMessage =\n | {\n type: \"connection:response\";\n viewer: string;\n approved: boolean;\n }\n | {\n type: \"fetch:response:meta\";\n requestId: string;\n status: number;\n headers: Record<string, string>;\n }\n | {\n type: \"file:read:response\";\n requestId: string;\n content?: string;\n error?: string;\n }\n | {\n type: \"dir:list:response\";\n requestId: string;\n content?: string;\n error?: string;\n }\n | {\n type: \"hmr:message\";\n body: string;\n };\n\n/**\n * Type guard to validate WebSocket message structure\n */\nfunction isWebSocketMessage(data: unknown): data is WebSocketMessage {\n if (!data || typeof data !== \"object\") {\n return false;\n }\n\n const msg = data as Record<string, unknown>;\n return typeof msg.type === \"string\";\n}\n\n/**\n * Remote tunnel manager for the AppKit.\n *\n * This class is responsible for managing the remote tunnels for the development server.\n * It also handles the asset fetching and the HMR for the development server.\n *\n * @example\n * ```ts\n * const remoteTunnelManager = new RemoteTunnelManager(devFileReader);\n * remoteTunnelManager.setup(app);\n * ```\n */\nexport class RemoteTunnelManager {\n private tunnels = new Map<string, TunnelConnection>();\n private wss: WebSocketServer;\n private hmrWss: WebSocketServer;\n private server?: HTTPServer;\n private devFileReader: DevFileReader;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.wss = new WebSocketServer({ noServer: true, path: \"/dev-tunnel\" });\n this.hmrWss = new WebSocketServer({ noServer: true, path: \"/dev-hmr\" });\n\n this.registerTunnelGetter();\n }\n\n setServer(server: HTTPServer) {\n this.server = server;\n }\n\n /** Asset middleware for the development server. */\n assetMiddleware() {\n return async (req: express.Request, res: express.Response) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n\n // Try cookie first, then generate from email\n let tunnelId: string | undefined;\n const cookieHeader = req.headers.cookie;\n\n if (cookieHeader) {\n // Fast path: extract dev-tunnel-id from cookie without full parse\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n if (!tunnelId) return res.status(404).send(\"Tunnel not ready\");\n\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) return res.status(404).send(\"Tunnel not found\");\n\n const { ws, approvedViewers, pendingFetches } = tunnel;\n\n if (!approvedViewers.has(email)) {\n return res.status(403).send(\"Not approved for this tunnel\");\n }\n\n const path = req.originalUrl;\n const requestId = randomUUID();\n\n const request = { type: \"fetch\", path, method: req.method, requestId };\n\n const response = await new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFetches.delete(requestId);\n reject(new Error(\"Asset fetch timeout\"));\n }, MAX_ASSET_FETCH_TIMEOUT);\n\n pendingFetches.set(requestId, { resolve, reject, timeout });\n\n ws.send(JSON.stringify(request));\n }).catch((err) => {\n logger.error(\"Failed to fetch %s: %s\", path, err.message);\n return { status: 504, body: Buffer.from(\"\"), headers: {} };\n });\n\n const r = response as any;\n\n res\n .status(r.status)\n .set(r.headers)\n .send(r.body || Buffer.from(\"\"));\n };\n }\n\n /** Dev mode middleware for the development server. */\n devModeMiddleware() {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n const dev = req.query?.dev;\n\n if (dev === undefined) {\n return next();\n }\n\n if (\n req.path.startsWith(\"/api\") ||\n req.path.startsWith(\"/query\") ||\n req.path.match(/\\.(js|css|png|jpg|jpeg|svg|ico|json|woff|woff2|ttf)$/)\n ) {\n return next();\n }\n\n const viewerEmail = req.headers[\"x-forwarded-email\"] as string;\n const isOwnerMode = dev === \"\" || dev === \"true\";\n\n const tunnelId = isOwnerMode\n ? generateTunnelIdFromEmail(viewerEmail)\n : dev.toString();\n\n if (!tunnelId) {\n return res.status(400).send(\"Invalid tunnel ID\");\n }\n\n if (!isOwnerMode) {\n const approvalResponse = this.handleViewerApproval(\n tunnelId,\n viewerEmail,\n req.query.retry === \"true\",\n res,\n );\n\n if (approvalResponse) {\n return approvalResponse;\n }\n }\n\n res.cookie(\"dev-tunnel-id\", tunnelId, {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n });\n\n const indexPath = path.join(__dirname, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${getConfigScript()}`);\n\n res.send(html);\n };\n }\n\n /** Setup the dev mode middleware. */\n setup(app: express.Application) {\n app.use(this.devModeMiddleware());\n app.use(REMOTE_TUNNEL_ASSET_PREFIXES, this.assetMiddleware());\n }\n\n static isRemoteServerEnabled() {\n return (\n process.env.NODE_ENV !== \"production\" &&\n process.env.DISABLE_REMOTE_SERVING !== \"true\" &&\n // DATABRICKS_CLIENT_SECRET is set in the .env file for deployed environments\n Boolean(process.env.DATABRICKS_CLIENT_SECRET)\n );\n }\n\n private loadHtmlTemplate(\n filename: string,\n replacements: Record<string, string>,\n ): string {\n const filePath = path.join(__dirname, filename);\n let content = fs.readFileSync(filePath, \"utf-8\");\n\n for (const [key, value] of Object.entries(replacements)) {\n content = content.replaceAll(`{{${key}}}`, value);\n }\n\n return content;\n }\n\n private handleViewerApproval(\n tunnelId: string,\n viewerEmail: string,\n retry: boolean,\n res: express.Response,\n ): express.Response | null {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) {\n return res.status(404).send(\"Tunnel not found\");\n }\n\n if (viewerEmail === tunnel.owner) {\n return null;\n }\n\n if (retry) {\n tunnel.rejectedViewers.delete(viewerEmail);\n }\n\n if (tunnel.rejectedViewers.has(viewerEmail)) {\n const html = this.loadHtmlTemplate(\"denied.html\", { tunnelId });\n return res.status(403).send(html);\n }\n\n if (tunnel.approvedViewers.has(viewerEmail)) {\n return null;\n }\n\n if (!tunnel.pendingRequests.has(viewerEmail)) {\n const requestId = randomUUID();\n tunnel.pendingRequests.add(viewerEmail);\n tunnel.ws.send(\n JSON.stringify({\n type: \"connection:request\",\n requestId,\n viewer: viewerEmail,\n }),\n );\n }\n\n const html = this.loadHtmlTemplate(\"wait.html\", { tunnelId });\n return res.status(200).send(html);\n }\n\n setupWebSocket() {\n this.wss.on(\"connection\", (ws, req) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId = generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return ws.close();\n\n this.tunnels.set(tunnelId, {\n ws,\n owner: email,\n approvedViewers: new Set([email]),\n pendingRequests: new Set(),\n rejectedViewers: new Set(),\n pendingFetches: new Map(),\n pendingFileReads: new Map(),\n waitingForBinaryBody: null,\n });\n\n ws.on(\"message\", (msg, isBinary) => {\n const tunnel = this.tunnels.get(tunnelId);\n if (!tunnel) return;\n\n if (isBinary) {\n if (!tunnel.waitingForBinaryBody) {\n logger.debug(\n \"Received binary message but no requestId is waiting for body\",\n );\n return;\n }\n\n const requestId = tunnel.waitingForBinaryBody;\n const pending = tunnel.pendingFetches.get(requestId);\n\n if (!pending || !pending.metadata) {\n logger.debug(\"Received binary message but pending fetch not found\");\n tunnel.waitingForBinaryBody = null;\n return;\n }\n\n tunnel.waitingForBinaryBody = null;\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(requestId);\n\n pending.resolve({\n status: pending.metadata.status,\n headers: pending.metadata.headers,\n body: msg as Buffer,\n });\n return;\n }\n\n try {\n const data = JSON.parse(msg.toString());\n\n // Validate message structure\n if (!isWebSocketMessage(data)) {\n logger.error(\"Invalid WebSocket message format: %O\", data);\n return;\n }\n\n if (data.type === \"connection:response\") {\n if (tunnel && data.viewer) {\n tunnel.pendingRequests.delete(data.viewer);\n\n if (data.approved) {\n tunnel.approvedViewers.add(data.viewer);\n logger.debug(\n \"✅ Approved %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n } else {\n tunnel.rejectedViewers.add(data.viewer);\n logger.debug(\n \"❌ Denied %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n }\n }\n } else if (data.type === \"fetch:response:meta\") {\n const pending = tunnel.pendingFetches.get(data.requestId);\n if (pending) {\n pending.metadata = {\n status: data.status,\n headers: data.headers,\n };\n if (\n data.status === 304 ||\n data.status === 204 ||\n (data.status >= 300 && data.status < 400)\n ) {\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(data.requestId);\n pending.resolve({\n status: data.status,\n headers: data.headers,\n body: Buffer.from(\"\"),\n });\n } else {\n tunnel.waitingForBinaryBody = data.requestId;\n }\n }\n } else if (data.type === \"file:read:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in file:read:response\"),\n );\n }\n }\n } else if (data.type === \"dir:list:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in dir:list:response\"),\n );\n }\n }\n }\n } catch (e) {\n logger.error(\"Failed to parse WebSocket message: %O\", e);\n }\n });\n\n ws.send(JSON.stringify({ type: \"tunnel:ready\", tunnelId }));\n\n ws.on(\"close\", () => {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (tunnel) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Tunnel closed\"));\n }\n tunnel.pendingFetches.clear();\n }\n\n this.tunnels.delete(tunnelId);\n });\n });\n\n this.hmrWss.on(\"connection\", (browserWs, req) => {\n const cookies = parseCookies(req);\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId =\n cookies[\"dev-tunnel-id\"] || generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return browserWs.close();\n\n const cliTunnel = this.tunnels.get(tunnelId);\n\n if (!cliTunnel) return browserWs.close();\n\n const { ws: cliWs, approvedViewers } = cliTunnel;\n\n if (!approvedViewers.has(email)) {\n return browserWs.close(1008, \"Not approved\");\n }\n // Browser → CLI\n browserWs.on(\"message\", (msg) => {\n const hmrStart = Date.now();\n logger.debug(\"browser -> cli browserWS message: %s\", msg.toString());\n cliWs.send(\n JSON.stringify({\n type: \"hmr:message\",\n body: msg.toString(),\n timestamp: hmrStart,\n }),\n );\n });\n\n // // CLI → Browser\n const cliHandler = (msg: Buffer | string, isBinary: boolean) => {\n // Ignore binary messages (they're for fetch responses, not HMR)\n if (isBinary) return;\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"hmr:message\") {\n browserWs.send(data.body);\n }\n } catch {\n logger.error(\n \"Failed to parse CLI message for HMR: %s\",\n msg.toString().substring(0, 100),\n );\n }\n };\n cliWs.on(\"message\", cliHandler);\n\n browserWs.on(\"close\", () => {\n cliWs.off(\"message\", cliHandler);\n });\n });\n\n // // Browser HMR connection\n this.server?.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n\n if (url.startsWith(\"/dev-tunnel\")) {\n this.wss.handleUpgrade(req, socket, head, (ws) => {\n this.wss.emit(\"connection\", ws, req);\n });\n } else if (url.startsWith(\"/dev-hmr\")) {\n this.hmrWss.handleUpgrade(req, socket, head, (browserWs) => {\n this.hmrWss.emit(\"connection\", browserWs, req);\n });\n }\n });\n }\n\n registerTunnelGetter() {\n this.devFileReader.registerTunnelGetter(\n this.getTunnelForRequest.bind(this),\n );\n }\n\n getTunnelForRequest(req: express.Request) {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const cookieHeader = req.headers.cookie;\n\n let tunnelId: string | undefined;\n\n if (cookieHeader) {\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n return tunnelId ? this.tunnels.get(tunnelId) || null : null;\n }\n\n cleanup() {\n for (const [, tunnel] of this.tunnels) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Server shutting down\"));\n }\n tunnel.pendingFetches.clear();\n tunnel.ws.close();\n }\n this.tunnels.clear();\n\n if (this.wss) {\n this.wss.close();\n }\n if (this.hmrWss) {\n this.hmrWss.close();\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseServer } from "./base-server.js";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import fs from "node:fs";
|
|
4
4
|
import express from "express";
|
|
5
5
|
|
|
6
6
|
//#region src/plugins/server/static-server.ts
|
|
@@ -33,11 +33,11 @@ var StaticServer = class extends BaseServer {
|
|
|
33
33
|
/** Serve the index.html file. */
|
|
34
34
|
serveIndex(res) {
|
|
35
35
|
const indexPath = path.join(this.staticPath, "index.html");
|
|
36
|
-
if (!
|
|
36
|
+
if (!fs.existsSync(indexPath)) {
|
|
37
37
|
res.status(404).send("index.html not found");
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
let html =
|
|
40
|
+
let html = fs.readFileSync(indexPath, "utf-8");
|
|
41
41
|
html = html.replace("<body>", `<body>${this.getConfigScript()}`);
|
|
42
42
|
res.send(html);
|
|
43
43
|
}
|