@databricks/appkit 0.76.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/NOTICE.md +10 -12
  2. package/README.md +40 -20
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/cli/commands/codemod/on-plugins-ready.js +2 -1
  5. package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
  6. package/dist/cli/commands/lint.js +2 -1
  7. package/dist/cli/commands/lint.js.map +1 -1
  8. package/dist/cli/commands/plugin/add-resource/add-resource.js +1 -1
  9. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  10. package/dist/cli/commands/plugin/create/create.js +1 -1
  11. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  12. package/dist/cli/commands/plugin/create/prompt-resource.js +1 -1
  13. package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
  14. package/dist/cli/commands/plugin/list/list.js +22 -9
  15. package/dist/cli/commands/plugin/list/list.js.map +1 -1
  16. package/dist/cli/commands/plugin/sync/sync.js +12 -3
  17. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  18. package/dist/cli/commands/registry/env-writer.js +4 -1
  19. package/dist/cli/commands/registry/env-writer.js.map +1 -1
  20. package/dist/evals/judge.d.ts.map +1 -1
  21. package/dist/evals/judge.js +8 -2
  22. package/dist/evals/judge.js.map +1 -1
  23. package/dist/plugins/agents/mlflow.js +6 -1
  24. package/dist/plugins/agents/mlflow.js.map +1 -1
  25. package/dist/plugins/server/index.js +2 -2
  26. package/dist/plugins/server/index.js.map +1 -1
  27. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
  28. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  29. package/dist/plugins/server/static-server.js +3 -3
  30. package/dist/plugins/server/static-server.js.map +1 -1
  31. package/dist/plugins/server/utils.js +3 -3
  32. package/dist/plugins/server/utils.js.map +1 -1
  33. package/dist/plugins/server/vite-dev-server.js +4 -4
  34. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  35. package/dist/plugins/serving/manifest.js +3 -2
  36. package/dist/plugins/serving/serving.d.ts +6 -0
  37. package/dist/plugins/serving/serving.d.ts.map +1 -1
  38. package/dist/plugins/serving/serving.js +7 -0
  39. package/dist/plugins/serving/serving.js.map +1 -1
  40. package/dist/schemas/manifest.d.ts +1 -0
  41. package/dist/schemas/manifest.d.ts.map +1 -1
  42. package/dist/schemas/manifest.js +2 -0
  43. package/dist/schemas/manifest.js.map +1 -1
  44. package/dist/shared/src/plugin.d.ts +3 -1
  45. package/dist/shared/src/plugin.d.ts.map +1 -1
  46. package/dist/shared/src/schemas/manifest.d.ts +34 -33
  47. package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
  48. package/dist/shared/src/schemas/manifest.js +2 -0
  49. package/dist/shared/src/schemas/manifest.js.map +1 -1
  50. package/dist/type-generator/database/generate.js +3 -3
  51. package/dist/type-generator/database/generate.js.map +1 -1
  52. package/dist/type-generator/migration.js +2 -2
  53. package/dist/type-generator/migration.js.map +1 -1
  54. package/dist/type-generator/serving/server-file-extractor.js +3 -3
  55. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  56. package/docs/api/appkit/Interface.PluginManifest.md +29 -11
  57. package/docs/plugins/agents.md +3 -1
  58. package/package.json +12 -4
  59. package/sbom.cdx.json +1 -1
@@ -1,6 +1,7 @@
1
1
  //#region src/evals/judge.ts
2
2
  let mod;
3
3
  let enabled = false;
4
+ let notInstalled = false;
4
5
  let configured = false;
5
6
  let prevBaseUrl;
6
7
  let prevApiKey;
@@ -19,8 +20,9 @@ async function configureJudge(config) {
19
20
  configured = true;
20
21
  mod.init({ defaultModel: config.model });
21
22
  enabled = true;
22
- } catch {
23
+ } catch (err) {
23
24
  enabled = false;
25
+ notInstalled = isModuleNotFound(err);
24
26
  }
25
27
  }
26
28
  function isJudgeConfigured() {
@@ -44,6 +46,10 @@ function restoreEnv(key, prev) {
44
46
  if (prev === void 0) delete process.env[key];
45
47
  else process.env[key] = prev;
46
48
  }
49
+ /** True when a dynamic `import()` failed because the package isn't installed. */
50
+ function isModuleNotFound(err) {
51
+ return !!err && typeof err === "object" && "code" in err && (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND");
52
+ }
47
53
  /** Normalize an autoevals `Score` into a `JudgeScore`. */
48
54
  function toJudgeScore(s) {
49
55
  const rationale = s.metadata?.rationale;
@@ -53,7 +59,7 @@ function toJudgeScore(s) {
53
59
  };
54
60
  }
55
61
  function ensure() {
56
- if (!enabled || !mod) throw new Error("LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*");
62
+ if (!enabled || !mod) throw new Error(notInstalled ? "LLM judge requires the optional `autoevals` package. Install it to use t.judge.*: npm i autoevals" : "LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*");
57
63
  return mod;
58
64
  }
59
65
  /** Factuality of `output` vs an `expected` reference. */
@@ -1 +1 @@
1
- {"version":3,"file":"judge.js","names":[],"sources":["../../src/evals/judge.ts"],"sourcesContent":["import type { MlflowClient } from \"../connectors/mlflow\";\n\n/**\n * LLM-as-judge scoring via the `autoevals` library (the same scorers eve uses),\n * pointed at a Databricks serving endpoint. autoevals talks to an\n * OpenAI-compatible API; Databricks Model Serving exposes one at\n * `<host>/serving-endpoints`, so we set `OPENAI_BASE_URL`/`OPENAI_API_KEY` and\n * use the judge endpoint name as the model.\n *\n * There is no public REST to call Databricks' built-in judges directly (they're\n * Python/SDK-only and the rubric prompts live in the mlflow package), so we run\n * autoevals' equivalent scorers against a Databricks judge model.\n */\ntype AutoEvals = typeof import(\"autoevals\");\n\nlet mod: AutoEvals | undefined;\nlet enabled = false;\n// Whether configureJudge overwrote the OPENAI_* env vars and they still need\n// restoring, plus the values to restore them to.\nlet configured = false;\nlet prevBaseUrl: string | undefined;\nlet prevApiKey: string | undefined;\n\nexport interface JudgeConfig {\n /** Client for the workspace hosting the judge serving endpoint. */\n client: MlflowClient;\n /** Bearer token for the serving endpoint. */\n token: string;\n /** Serving endpoint name used as the judge model. */\n model: string;\n}\n\n/** A normalized judge result. `score` is 0..1. */\nexport interface JudgeScore {\n score: number;\n rationale?: string;\n}\n\n/**\n * Configure the judge once. Sets the OpenAI-compatible client env autoevals\n * reads and the default judge model. No-op-safe: on failure, judging stays\n * disabled and {@link isJudgeConfigured} returns false.\n */\nexport async function configureJudge(config: JudgeConfig): Promise<void> {\n try {\n mod = await import(\"autoevals\");\n prevBaseUrl = process.env.OPENAI_BASE_URL;\n prevApiKey = process.env.OPENAI_API_KEY;\n process.env.OPENAI_BASE_URL = config.client.servingEndpointsUrl();\n process.env.OPENAI_API_KEY = config.token;\n configured = true;\n mod.init({ defaultModel: config.model });\n enabled = true;\n } catch {\n enabled = false;\n }\n}\n\nexport function isJudgeConfigured(): boolean {\n return enabled;\n}\n\n/**\n * Restore the `OPENAI_*` env vars {@link configureJudge} set, so the judge\n * bearer doesn't linger in `process.env` (readable by any imported eval code)\n * after the run. Call once the run is done; safe when the judge was never\n * configured. Also disables judging so a late `t.judge.*` call fails cleanly\n * rather than hitting a torn-down client.\n */\nexport function teardownJudge(): void {\n if (!configured) return;\n restoreEnv(\"OPENAI_BASE_URL\", prevBaseUrl);\n restoreEnv(\"OPENAI_API_KEY\", prevApiKey);\n configured = false;\n enabled = false;\n}\n\nfunction restoreEnv(key: string, prev: string | undefined): void {\n if (prev === undefined) delete process.env[key];\n else process.env[key] = prev;\n}\n\n/** Normalize an autoevals `Score` into a `JudgeScore`. */\nexport function toJudgeScore(s: {\n score?: number | null;\n metadata?: Record<string, unknown>;\n}): JudgeScore {\n const rationale = s.metadata?.rationale;\n return {\n score: typeof s.score === \"number\" ? s.score : 0,\n rationale: typeof rationale === \"string\" ? rationale : undefined,\n };\n}\n\nfunction ensure(): AutoEvals {\n if (!enabled || !mod) {\n throw new Error(\n \"LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*\",\n );\n }\n return mod;\n}\n\n/** Factuality of `output` vs an `expected` reference. */\nexport async function judgeFactuality(args: {\n input: string;\n output: string;\n expected: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().Factuality(args));\n}\n\n/** Whether `output` answers the question in `input`, optionally constrained by `criteria`. */\nexport async function judgeClosedQA(args: {\n input: string;\n output: string;\n criteria: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().ClosedQA(args));\n}\n\n/**\n * A custom LLM judge defined by a prompt template + choice→score map — the\n * TypeScript analog of MLflow's custom `@scorer`.\n */\nexport async function judgeCustom(\n spec: {\n name: string;\n promptTemplate: string;\n choiceScores: Record<string, number>;\n },\n args: { input: string; output: string },\n): Promise<JudgeScore> {\n const scorer = ensure().LLMClassifierFromTemplate(spec);\n return toJudgeScore(await scorer(args));\n}\n"],"mappings":";AAeA,IAAI;AACJ,IAAI,UAAU;AAGd,IAAI,aAAa;AACjB,IAAI;AACJ,IAAI;;;;;;AAsBJ,eAAsB,eAAe,QAAoC;AACvE,KAAI;AACF,QAAM,MAAM,OAAO;AACnB,gBAAc,QAAQ,IAAI;AAC1B,eAAa,QAAQ,IAAI;AACzB,UAAQ,IAAI,kBAAkB,OAAO,OAAO,qBAAqB;AACjE,UAAQ,IAAI,iBAAiB,OAAO;AACpC,eAAa;AACb,MAAI,KAAK,EAAE,cAAc,OAAO,OAAO,CAAC;AACxC,YAAU;SACJ;AACN,YAAU;;;AAId,SAAgB,oBAA6B;AAC3C,QAAO;;;;;;;;;AAUT,SAAgB,gBAAsB;AACpC,KAAI,CAAC,WAAY;AACjB,YAAW,mBAAmB,YAAY;AAC1C,YAAW,kBAAkB,WAAW;AACxC,cAAa;AACb,WAAU;;AAGZ,SAAS,WAAW,KAAa,MAAgC;AAC/D,KAAI,SAAS,OAAW,QAAO,QAAQ,IAAI;KACtC,SAAQ,IAAI,OAAO;;;AAI1B,SAAgB,aAAa,GAGd;CACb,MAAM,YAAY,EAAE,UAAU;AAC9B,QAAO;EACL,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;EAC/C,WAAW,OAAO,cAAc,WAAW,YAAY;EACxD;;AAGH,SAAS,SAAoB;AAC3B,KAAI,CAAC,WAAW,CAAC,IACf,OAAM,IAAI,MACR,wIACD;AAEH,QAAO;;;AAIT,eAAsB,gBAAgB,MAId;AACtB,QAAO,aAAa,MAAM,QAAQ,CAAC,WAAW,KAAK,CAAC;;;AAItD,eAAsB,cAAc,MAIZ;AACtB,QAAO,aAAa,MAAM,QAAQ,CAAC,SAAS,KAAK,CAAC;;;;;;AAOpD,eAAsB,YACpB,MAKA,MACqB;AAErB,QAAO,aAAa,MADL,QAAQ,CAAC,0BAA0B,KAAK,CACtB,KAAK,CAAC"}
1
+ {"version":3,"file":"judge.js","names":[],"sources":["../../src/evals/judge.ts"],"sourcesContent":["import type { MlflowClient } from \"../connectors/mlflow\";\n\n/**\n * LLM-as-judge scoring via the `autoevals` library (the same scorers eve uses),\n * pointed at a Databricks serving endpoint. autoevals talks to an\n * OpenAI-compatible API; Databricks Model Serving exposes one at\n * `<host>/serving-endpoints`, so we set `OPENAI_BASE_URL`/`OPENAI_API_KEY` and\n * use the judge endpoint name as the model.\n *\n * There is no public REST to call Databricks' built-in judges directly (they're\n * Python/SDK-only and the rubric prompts live in the mlflow package), so we run\n * autoevals' equivalent scorers against a Databricks judge model.\n */\ntype AutoEvals = typeof import(\"autoevals\");\n\nlet mod: AutoEvals | undefined;\nlet enabled = false;\n// Set when the dynamic import failed because `autoevals` (an optional peer) is\n// not installed, so `ensure()` can point at the fix instead of the auth path.\nlet notInstalled = false;\n// Whether configureJudge overwrote the OPENAI_* env vars and they still need\n// restoring, plus the values to restore them to.\nlet configured = false;\nlet prevBaseUrl: string | undefined;\nlet prevApiKey: string | undefined;\n\nexport interface JudgeConfig {\n /** Client for the workspace hosting the judge serving endpoint. */\n client: MlflowClient;\n /** Bearer token for the serving endpoint. */\n token: string;\n /** Serving endpoint name used as the judge model. */\n model: string;\n}\n\n/** A normalized judge result. `score` is 0..1. */\nexport interface JudgeScore {\n score: number;\n rationale?: string;\n}\n\n/**\n * Configure the judge once. Sets the OpenAI-compatible client env autoevals\n * reads and the default judge model. No-op-safe: on failure, judging stays\n * disabled and {@link isJudgeConfigured} returns false.\n */\nexport async function configureJudge(config: JudgeConfig): Promise<void> {\n try {\n mod = await import(\"autoevals\");\n prevBaseUrl = process.env.OPENAI_BASE_URL;\n prevApiKey = process.env.OPENAI_API_KEY;\n process.env.OPENAI_BASE_URL = config.client.servingEndpointsUrl();\n process.env.OPENAI_API_KEY = config.token;\n configured = true;\n mod.init({ defaultModel: config.model });\n enabled = true;\n } catch (err) {\n enabled = false;\n notInstalled = isModuleNotFound(err);\n }\n}\n\nexport function isJudgeConfigured(): boolean {\n return enabled;\n}\n\n/**\n * Restore the `OPENAI_*` env vars {@link configureJudge} set, so the judge\n * bearer doesn't linger in `process.env` (readable by any imported eval code)\n * after the run. Call once the run is done; safe when the judge was never\n * configured. Also disables judging so a late `t.judge.*` call fails cleanly\n * rather than hitting a torn-down client.\n */\nexport function teardownJudge(): void {\n if (!configured) return;\n restoreEnv(\"OPENAI_BASE_URL\", prevBaseUrl);\n restoreEnv(\"OPENAI_API_KEY\", prevApiKey);\n configured = false;\n enabled = false;\n}\n\nfunction restoreEnv(key: string, prev: string | undefined): void {\n if (prev === undefined) delete process.env[key];\n else process.env[key] = prev;\n}\n\n/** True when a dynamic `import()` failed because the package isn't installed. */\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n !!err &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err.code === \"ERR_MODULE_NOT_FOUND\" || err.code === \"MODULE_NOT_FOUND\")\n );\n}\n\n/** Normalize an autoevals `Score` into a `JudgeScore`. */\nexport function toJudgeScore(s: {\n score?: number | null;\n metadata?: Record<string, unknown>;\n}): JudgeScore {\n const rationale = s.metadata?.rationale;\n return {\n score: typeof s.score === \"number\" ? s.score : 0,\n rationale: typeof rationale === \"string\" ? rationale : undefined,\n };\n}\n\nfunction ensure(): AutoEvals {\n if (!enabled || !mod) {\n throw new Error(\n notInstalled\n ? \"LLM judge requires the optional `autoevals` package. Install it to use t.judge.*: npm i autoevals\"\n : \"LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*\",\n );\n }\n return mod;\n}\n\n/** Factuality of `output` vs an `expected` reference. */\nexport async function judgeFactuality(args: {\n input: string;\n output: string;\n expected: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().Factuality(args));\n}\n\n/** Whether `output` answers the question in `input`, optionally constrained by `criteria`. */\nexport async function judgeClosedQA(args: {\n input: string;\n output: string;\n criteria: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().ClosedQA(args));\n}\n\n/**\n * A custom LLM judge defined by a prompt template + choice→score map — the\n * TypeScript analog of MLflow's custom `@scorer`.\n */\nexport async function judgeCustom(\n spec: {\n name: string;\n promptTemplate: string;\n choiceScores: Record<string, number>;\n },\n args: { input: string; output: string },\n): Promise<JudgeScore> {\n const scorer = ensure().LLMClassifierFromTemplate(spec);\n return toJudgeScore(await scorer(args));\n}\n"],"mappings":";AAeA,IAAI;AACJ,IAAI,UAAU;AAGd,IAAI,eAAe;AAGnB,IAAI,aAAa;AACjB,IAAI;AACJ,IAAI;;;;;;AAsBJ,eAAsB,eAAe,QAAoC;AACvE,KAAI;AACF,QAAM,MAAM,OAAO;AACnB,gBAAc,QAAQ,IAAI;AAC1B,eAAa,QAAQ,IAAI;AACzB,UAAQ,IAAI,kBAAkB,OAAO,OAAO,qBAAqB;AACjE,UAAQ,IAAI,iBAAiB,OAAO;AACpC,eAAa;AACb,MAAI,KAAK,EAAE,cAAc,OAAO,OAAO,CAAC;AACxC,YAAU;UACH,KAAK;AACZ,YAAU;AACV,iBAAe,iBAAiB,IAAI;;;AAIxC,SAAgB,oBAA6B;AAC3C,QAAO;;;;;;;;;AAUT,SAAgB,gBAAsB;AACpC,KAAI,CAAC,WAAY;AACjB,YAAW,mBAAmB,YAAY;AAC1C,YAAW,kBAAkB,WAAW;AACxC,cAAa;AACb,WAAU;;AAGZ,SAAS,WAAW,KAAa,MAAgC;AAC/D,KAAI,SAAS,OAAW,QAAO,QAAQ,IAAI;KACtC,SAAQ,IAAI,OAAO;;;AAI1B,SAAS,iBAAiB,KAAuB;AAC/C,QACE,CAAC,CAAC,OACF,OAAO,QAAQ,YACf,UAAU,QACT,IAAI,SAAS,0BAA0B,IAAI,SAAS;;;AAKzD,SAAgB,aAAa,GAGd;CACb,MAAM,YAAY,EAAE,UAAU;AAC9B,QAAO;EACL,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;EAC/C,WAAW,OAAO,cAAc,WAAW,YAAY;EACxD;;AAGH,SAAS,SAAoB;AAC3B,KAAI,CAAC,WAAW,CAAC,IACf,OAAM,IAAI,MACR,eACI,sGACA,wIACL;AAEH,QAAO;;;AAIT,eAAsB,gBAAgB,MAId;AACtB,QAAO,aAAa,MAAM,QAAQ,CAAC,WAAW,KAAK,CAAC;;;AAItD,eAAsB,cAAc,MAIZ;AACtB,QAAO,aAAa,MAAM,QAAQ,CAAC,SAAS,KAAK,CAAC;;;;;;AAOpD,eAAsB,YACpB,MAKA,MACqB;AAErB,QAAO,aAAa,MADL,QAAQ,CAAC,0BAA0B,KAAK,CACtB,KAAK,CAAC"}
@@ -260,9 +260,14 @@ async function initAgentTracing() {
260
260
  if (ucLocation) logger.info("MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)", id, ucLocation.catalogName, ucLocation.schemaName, ucLocation.tablePrefix);
261
261
  else logger.info("MLflow agent tracing enabled (experiment %s)", id);
262
262
  } catch (err) {
263
- logger.warn("MLflow agent tracing disabled: %O", err);
263
+ if (isModuleNotFound(err)) logger.warn("MLflow agent tracing requires the optional `@mlflow/core` package. Install it to enable tracing: npm i @mlflow/core");
264
+ else logger.warn("MLflow agent tracing disabled: %O", err);
264
265
  }
265
266
  }
267
+ /** True when a dynamic `import()` failed because the package isn't installed. */
268
+ function isModuleNotFound(err) {
269
+ return !!err && typeof err === "object" && "code" in err && (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND");
270
+ }
266
271
  const noopRecorder = { setOutputs() {} };
267
272
  /**
268
273
  * Seed the classic processor's global config once, AFTER
@@ -1 +1 @@
1
- {"version":3,"file":"mlflow.js","names":["#inner","#popTrace","#spanTypeKey","#agentSpanTypes","#maxTracked","#flushTimeoutMs","#ready","#forwarded","#agentTraceIds","#boundedFlush","trace"],"sources":["../../../src/plugins/agents/mlflow.ts"],"sourcesContent":["import type { UnityCatalogLocation } from \"@mlflow/core\";\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { TelemetryManager } from \"../../telemetry\";\n\nconst logger = createLogger(\"agents\");\n\ntype MlflowModule = typeof import(\"@mlflow/core\");\ntype MlflowClientInstance = InstanceType<MlflowModule[\"MlflowClient\"]>;\n\ninterface MlflowInitConfig {\n trackingUri: string;\n experimentId: string;\n host?: string;\n}\n\nlet mlflow: MlflowModule | undefined;\nlet enabled = false;\nlet initStarted = false;\nlet configured = false;\nlet initConfig: MlflowInitConfig | undefined;\n// The resolved UC trace location, or undefined for classic experiment storage.\nlet ucLocation: UnityCatalogLocation | undefined;\nlet gatedProcessor: GatedMlflowSpanProcessor | undefined;\n\n/**\n * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs:\n *\n * 1. Stay inert until ready. The classic processor's `onStart` calls its own\n * `getConfig()`, which THROWS before `init()` runs — and that throw\n * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit\n * spans (HTTP, analytics) created between `TelemetryManager.start()` and the\n * first agent turn. We contribute this to AppKit's single tracer provider\n * during `setup()`, but only start forwarding once `ready()` is called by\n * {@link ensureConfigured}. (The UC processor reads no global config and\n * can't throw here, but forwarding is gated uniformly either way.)\n *\n * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY\n * parentless span, and AppKit's single provider carries every HTTP/DB span —\n * so unscoped, every request would become an MLflow trace. mlflow stamps\n * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so\n * presence alone can't tell an agent turn from a plain request — we key on\n * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow\n * exports the trace) only if some span in the trace carried an AGENT/TOOL\n * type; otherwise `popTrace` to discard the trace mlflow built in memory. The\n * real span type is set after `onStart` (the constructor stamps UNKNOWN\n * first), and children end before their root, so the flag is set by the time\n * the root decides.\n *\n * It also drops the exporters' own outbound spans at `onStart` (parentless\n * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP\n * shipping a trace). Forwarding those would loop: each upload is an HTTP call\n * that auto-instrumentation turns into a new span to trace and upload.\n *\n * ponytail: non-agent requests still build (then discard) an in-memory trace\n * tree — allocation-only, no network (export happens only when we forward the\n * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn\n * matter, root MLflow at a detached agent span instead (costs the HTTP envelope\n * on the trace and splits the OTLP trace).\n */\nexport class GatedMlflowSpanProcessor implements SpanProcessor {\n #inner: SpanProcessor;\n #ready = false;\n // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never\n // sees an end without a matching start.\n #forwarded = new WeakSet<object>();\n // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the\n // root's `onEnd` exports rather than discards. Cleared as each root ends.\n #agentTraceIds = new Set<string>();\n #popTrace: (otelTraceId: string) => void;\n #spanTypeKey: string;\n // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace\n // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which\n // mlflow stamps on all non-agent spans) is treated as non-agent.\n #agentSpanTypes: ReadonlySet<string>;\n // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd.\n #maxTracked: number;\n // Cap on how long forceFlush/shutdown wait for a stuck export.\n #flushTimeoutMs: number;\n\n constructor(\n inner: SpanProcessor,\n deps: {\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n maxTracked?: number;\n flushTimeoutMs?: number;\n },\n ) {\n this.#inner = inner;\n this.#popTrace = deps.popTrace;\n this.#spanTypeKey = deps.spanTypeKey;\n this.#agentSpanTypes = deps.agentSpanTypes;\n this.#maxTracked = deps.maxTracked ?? 1024;\n this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000;\n }\n\n ready(): void {\n this.#ready = true;\n }\n\n onStart(\n span: Parameters<SpanProcessor[\"onStart\"]>[0],\n parentContext: Parameters<SpanProcessor[\"onStart\"]>[1],\n ): void {\n if (!this.#ready) return;\n // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is\n // an outgoing request made outside any agent turn — e.g. mlflow or OTLP\n // shipping a trace. Forwarding those would loop: each upload is itself an\n // HTTP call that auto-instrumentation turns into a new span to trace and\n // upload.\n if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) {\n return;\n }\n this.#forwarded.add(span);\n this.#inner.onStart(span, parentContext);\n }\n\n onEnd(span: Parameters<SpanProcessor[\"onEnd\"]>[0]): void {\n if (!this.#forwarded.has(span)) return;\n const traceId = span.spanContext().traceId;\n // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps\n // `mlflow.spanType` on every span (UNKNOWN by default), so match the value,\n // not mere presence; the real type is set after `onStart`, so `onEnd` is the\n // earliest we can read it.\n if (\n this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) &&\n !this.#agentTraceIds.has(traceId)\n ) {\n // Normally an entry lives only until its root's onEnd deletes it. But a\n // root that ends BEFORE its agent child (streaming client-disconnect) or\n // never ends (crash) orphans the entry, and #agentTraceIds — unlike the\n // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean.\n // FIFO-evict at the cap so an abandoned-trace pattern can't grow it\n // unboundedly over process uptime.\n // ponytail: evicting a still-live trace only mis-discards it, and that\n // needs >#maxTracked concurrent agent turns — far above real load.\n if (this.#agentTraceIds.size >= this.#maxTracked) {\n const oldest = this.#agentTraceIds.values().next().value;\n if (oldest !== undefined) this.#agentTraceIds.delete(oldest);\n }\n this.#agentTraceIds.add(traceId);\n }\n if (span.parentSpanContext?.spanId) {\n // Non-root: mlflow's own `onEnd` early-returns, but forward for balance.\n this.#inner.onEnd(span);\n return;\n }\n // Root span: export only agent traces; discard everything else so plain HTTP\n // requests never become MLflow traces. `delete` reports whether it was agent.\n if (this.#agentTraceIds.delete(traceId)) {\n this.#inner.onEnd(span);\n } else {\n this.#popTrace(traceId);\n }\n }\n\n forceFlush(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.forceFlush());\n }\n\n shutdown(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.shutdown());\n }\n\n // Bound the inner flush/shutdown wait: both exporters export fire-and-forget,\n // so a stuck export only wedges here (graceful shutdown), never a turn.\n // Resolves — not rejects — on timeout: the caller is tearing down.\n async #boundedFlush(op: () => Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n op().catch((err) => {\n logger.warn(\"MLflow trace flush error: %O\", err);\n }),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n logger.warn(\n \"MLflow trace flush exceeded %dms; continuing (export may still be in flight)\",\n this.#flushTimeoutMs,\n );\n resolve();\n }, this.#flushTimeoutMs);\n timer.unref(); // don't keep the event loop alive on the timeout alone\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n\n/**\n * Resolve the Unity Catalog trace location for the bound experiment, or\n * `undefined` for classic experiment-backed storage. Any failure falls back to\n * classic — a tracing misconfiguration must never break the agent.\n *\n * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` +\n * `MLFLOW_UC_TABLE_PREFIX`, all three required.\n * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since\n * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with\n * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the\n * backend-populated spans/logs table names for custom-provisioned locations.\n * 3. Otherwise classic.\n *\n * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so\n * it's deep-imported like the exporter classes and covered by the tripwire test.\n */\nasync function resolveUcLocation(\n experimentId: string,\n client: MlflowClientInstance,\n): Promise<UnityCatalogLocation | undefined> {\n const catalogName = process.env.MLFLOW_UC_CATALOG?.trim();\n const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim();\n const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim();\n if (catalogName && schemaName && tablePrefix) {\n return { catalogName, schemaName, tablePrefix };\n }\n\n if (!/^\\d+$/.test(experimentId)) return undefined;\n\n try {\n const experiment = await client.getExperiment(experimentId);\n if (!experiment) return undefined;\n const { ucLocationFromExperimentTags } =\n await import(\"@mlflow/core/dist/core/destination\");\n return ucLocationFromExperimentTags(experiment.tags) ?? undefined;\n } catch (err) {\n logger.warn(\n \"MLflow UC trace-location auto-detect failed; using classic experiment storage: %O\",\n err,\n );\n return undefined;\n }\n}\n\n/**\n * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()`\n * build and globally register its own tracer provider (its own `NodeSDK`). This\n * lets AppKit own the single global provider (OTLP + this processor), so agent\n * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the\n * global slot.\n *\n * When a UC trace location is bound, builds the Unity Catalog processor +\n * exporter (V4 trace ids, spans uploaded to the experiment's UC table);\n * otherwise the classic experiment-backed processor. `createAuthProvider`,\n * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public\n * in `@mlflow/core`, so only the exporter/processor classes are deep-imported —\n * pinned to the exact version in package.json and guarded by a test that fails\n * loudly if a version bump renames them.\n *\n * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope\n * forwarding to agent traces: `popTrace` (to discard non-agent traces), the\n * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values\n * that mark a trace as an agent turn (mlflow stamps every span, defaulting to\n * UNKNOWN, so the gate must match the value, not presence).\n */\nasync function buildMlflowSpanProcessor(\n m: MlflowModule,\n client: MlflowClientInstance,\n ucLoc: UnityCatalogLocation | undefined,\n): Promise<{\n processor: SpanProcessor;\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n}> {\n let processor: SpanProcessor;\n if (ucLoc) {\n const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/uc_table\");\n processor = new DatabricksUCTableSpanProcessor(\n new DatabricksUCTableSpanExporter(client),\n ucLoc,\n );\n } else {\n const { MlflowSpanExporter, MlflowSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/mlflow\");\n processor = new MlflowSpanProcessor(new MlflowSpanExporter(client));\n }\n return {\n processor,\n popTrace: (otelTraceId) =>\n m.InMemoryTraceManager.getInstance().popTrace(otelTraceId),\n spanTypeKey: m.SpanAttributeKey.SPAN_TYPE,\n // mlflow JSON-stringifies attribute values, so the stored values are\n // `\"AGENT\"`/`\"TOOL\"` (quoted). Match that exact form.\n agentSpanTypes: new Set([\n JSON.stringify(m.SpanType.AGENT),\n JSON.stringify(m.SpanType.TOOL),\n ]),\n };\n}\n\n/** The bound MLflow experiment id, from the optional `experiment` resource. */\nfunction experimentId(): string | undefined {\n const id = process.env.MLFLOW_EXPERIMENT_ID?.trim();\n return id || undefined;\n}\n\n/**\n * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST`\n * verbatim to build request URLs and doesn't add `https://`, so a bare host\n * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit\n * normalized host when the env var is set; when it isn't (profile-based auth),\n * return undefined and let the SDK read the host from `~/.databrickscfg`.\n */\nfunction normalizedDatabricksHost(): string | undefined {\n const raw = process.env.DATABRICKS_HOST?.trim();\n if (!raw) return undefined;\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}`;\n}\n\n/**\n * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the\n * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`).\n * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`.\n *\n * Rather than let `@mlflow/core`'s `init()` stand up and globally register its\n * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the\n * span processor ourselves and contribute it to AppKit's single provider via\n * {@link TelemetryManager.registerSpanProcessor}. For the classic\n * experiment-backed processor, mlflow's global config is seeded by\n * {@link startAgentTracing} on the `\"setup:complete\"` lifecycle event (after\n * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the\n * UC processor needs no seeded config, so that path never calls `init()`.\n *\n * The trace store is resolved here: a UC table prefix (env-configured or\n * auto-detected from the experiment's Databricks tags) vs. the classic\n * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks\n * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg`\n * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP\n * headers are wired by hand. A failure (missing creds, bad experiment) logs and\n * leaves tracing disabled rather than breaking the agent.\n *\n * Safe to call repeatedly; only the first call does work.\n */\nexport async function initAgentTracing(): Promise<void> {\n if (initStarted) return;\n initStarted = true;\n\n const id = experimentId();\n if (!id) return;\n\n try {\n mlflow = await import(\"@mlflow/core\");\n const host = normalizedDatabricksHost();\n initConfig = {\n trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || \"databricks\",\n experimentId: id,\n ...(host ? { host } : {}),\n };\n // One auth resolution + client, reused for UC auto-detect and the exporter.\n const authProvider = mlflow.createAuthProvider({\n trackingUri: initConfig.trackingUri,\n ...(host ? { host } : {}),\n });\n const client = new mlflow.MlflowClient({\n trackingUri: initConfig.trackingUri,\n authProvider,\n });\n ucLocation = await resolveUcLocation(id, client);\n const { processor, popTrace, spanTypeKey, agentSpanTypes } =\n await buildMlflowSpanProcessor(mlflow, client, ucLocation);\n gatedProcessor = new GatedMlflowSpanProcessor(processor, {\n popTrace,\n spanTypeKey,\n agentSpanTypes,\n });\n TelemetryManager.registerSpanProcessor(gatedProcessor);\n enabled = true;\n if (ucLocation) {\n logger.info(\n \"MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)\",\n id,\n ucLocation.catalogName,\n ucLocation.schemaName,\n ucLocation.tablePrefix,\n );\n } else {\n logger.info(\"MLflow agent tracing enabled (experiment %s)\", id);\n }\n } catch (err) {\n logger.warn(\"MLflow agent tracing disabled: %O\", err);\n }\n}\n\n/**\n * Records a span's outputs. Callers get one from `traceAgent`/`traceTool`;\n * it's a no-op when tracing is disabled, so call sites never branch on it.\n */\nexport interface SpanRecorder {\n setOutputs(outputs: unknown): void;\n}\n\nconst noopRecorder: SpanRecorder = { setOutputs() {} };\n\n/**\n * Seed the classic processor's global config once, AFTER\n * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly\n * by {@link startAgentTracing} on `\"setup:complete\"`, or lazily by {@link trace}\n * as a fallback — then enable forwarding on the gated processor. Returns whether\n * tracing is usable.\n *\n * The classic experiment-backed `MlflowSpanProcessor` reads\n * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's\n * global config. `init()` also stands up its own `NodeSDK` whose provider loses\n * the global slot to AppKit's already-registered one (non-fatal); we call it\n * only for that config side-effect. The UC processor carries its location and\n * reads no global config, so the UC path skips `init()` entirely — no second\n * `NodeSDK`, no competing global registration by `@mlflow/core`.\n */\nfunction ensureConfigured(): boolean {\n if (configured) return enabled;\n configured = true;\n // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw,\n // `mlflow` and `initConfig` are still set but there is no gate. Calling\n // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if\n // AppKit registered none (no OTLP, no processor) it wins the global slot,\n // routing every span into mlflow un-gated: the exact over-tracing + exporter\n // loop this file exists to prevent.\n if (!mlflow || !initConfig || !gatedProcessor) return false;\n try {\n if (!ucLocation) mlflow.init(initConfig);\n gatedProcessor.ready();\n return true;\n } catch (err) {\n enabled = false;\n logger.warn(\"MLflow agent tracing disabled (init failed): %O\", err);\n return false;\n }\n}\n\n/**\n * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the\n * agents plugin wires this to the `\"setup:complete\"` lifecycle event, before the\n * server serves any request. Doing it here means the request's own root span is\n * already forwarded when the first turn runs, so that turn assembles into a\n * trace instead of being dropped (mlflow roots a trace only at the top-level\n * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a\n * turn whose agent span is itself the trace root; an HTTP-wrapped first turn\n * seeded lazily loses its root span (already started — and dropped — before\n * `ready()`), so this eager path is the reliable one.\n */\nexport function startAgentTracing(): void {\n ensureConfigured();\n}\n\n/**\n * Run `fn` inside an MLflow span of `spanType` when tracing is enabled,\n * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active\n * context, so a TOOL span opened inside an AGENT span's callback becomes its\n * child. The callback's resolved value is recorded as the span's outputs unless\n * it called `setOutputs` first; return `undefined` (or set outputs explicitly)\n * when the return value isn't the output you want traced.\n */\nasync function trace<T>(\n spanType: \"AGENT\" | \"TOOL\",\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n if (!enabled || !mlflow) return fn(noopRecorder);\n const m = mlflow;\n if (!ensureConfigured()) return fn(noopRecorder);\n const type = spanType === \"AGENT\" ? m.SpanType.AGENT : m.SpanType.TOOL;\n return await m.withSpan<T>(\n async (span) => {\n if (inputs !== undefined) span.setInputs(inputs);\n let outputsSet = false;\n const result = await fn({\n setOutputs(outputs) {\n outputsSet = true;\n span.setOutputs(outputs);\n },\n });\n if (!outputsSet && result !== undefined) span.setOutputs(result);\n return result;\n },\n { name, spanType: type },\n );\n}\n\n/** Trace a turn's root AGENT span. See {@link trace}. */\nexport function traceAgent<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"AGENT\", name, inputs, fn);\n}\n\n/** Trace a TOOL span, nested under the active AGENT span. See {@link trace}. */\nexport function traceTool<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"TOOL\", name, inputs, fn);\n}\n\n/**\n * The MLflow trace id for the active turn, when tracing is enabled. Must be\n * read inside an agent span so eval runs can correlate the turn to its trace\n * and attach assessments. Returns undefined when tracing is off.\n *\n * Reads the context-active span rather than `getLastActiveTraceId()`: the\n * latter is only populated when a root span *ends* (on export), so mid-turn it\n * returns the previous turn's id — or, under concurrent turns, another turn's.\n */\nexport function currentTraceId(): string | undefined {\n if (!enabled || !mlflow) return undefined;\n try {\n return mlflow.getCurrentActiveSpan()?.traceId;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Link the active turn's trace to an MLflow run by id, via the `mlflow.sourceRun`\n * trace metadata. Used by eval runs so each case's trace shows under the run.\n * Must be called while a trace is active (inside an agent span). No-op when\n * tracing is disabled.\n */\nexport function linkTraceToRun(runId: string): void {\n if (!enabled || !mlflow) return;\n try {\n mlflow.updateCurrentTrace({ metadata: { \"mlflow.sourceRun\": runId } });\n } catch (err) {\n logger.warn(\"Failed to link trace to run %s: %O\", runId, err);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mlflow.js","names":["#inner","#popTrace","#spanTypeKey","#agentSpanTypes","#maxTracked","#flushTimeoutMs","#ready","#forwarded","#agentTraceIds","#boundedFlush","trace"],"sources":["../../../src/plugins/agents/mlflow.ts"],"sourcesContent":["import type { UnityCatalogLocation } from \"@mlflow/core\";\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { TelemetryManager } from \"../../telemetry\";\n\nconst logger = createLogger(\"agents\");\n\ntype MlflowModule = typeof import(\"@mlflow/core\");\ntype MlflowClientInstance = InstanceType<MlflowModule[\"MlflowClient\"]>;\n\ninterface MlflowInitConfig {\n trackingUri: string;\n experimentId: string;\n host?: string;\n}\n\nlet mlflow: MlflowModule | undefined;\nlet enabled = false;\nlet initStarted = false;\nlet configured = false;\nlet initConfig: MlflowInitConfig | undefined;\n// The resolved UC trace location, or undefined for classic experiment storage.\nlet ucLocation: UnityCatalogLocation | undefined;\nlet gatedProcessor: GatedMlflowSpanProcessor | undefined;\n\n/**\n * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs:\n *\n * 1. Stay inert until ready. The classic processor's `onStart` calls its own\n * `getConfig()`, which THROWS before `init()` runs — and that throw\n * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit\n * spans (HTTP, analytics) created between `TelemetryManager.start()` and the\n * first agent turn. We contribute this to AppKit's single tracer provider\n * during `setup()`, but only start forwarding once `ready()` is called by\n * {@link ensureConfigured}. (The UC processor reads no global config and\n * can't throw here, but forwarding is gated uniformly either way.)\n *\n * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY\n * parentless span, and AppKit's single provider carries every HTTP/DB span —\n * so unscoped, every request would become an MLflow trace. mlflow stamps\n * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so\n * presence alone can't tell an agent turn from a plain request — we key on\n * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow\n * exports the trace) only if some span in the trace carried an AGENT/TOOL\n * type; otherwise `popTrace` to discard the trace mlflow built in memory. The\n * real span type is set after `onStart` (the constructor stamps UNKNOWN\n * first), and children end before their root, so the flag is set by the time\n * the root decides.\n *\n * It also drops the exporters' own outbound spans at `onStart` (parentless\n * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP\n * shipping a trace). Forwarding those would loop: each upload is an HTTP call\n * that auto-instrumentation turns into a new span to trace and upload.\n *\n * ponytail: non-agent requests still build (then discard) an in-memory trace\n * tree — allocation-only, no network (export happens only when we forward the\n * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn\n * matter, root MLflow at a detached agent span instead (costs the HTTP envelope\n * on the trace and splits the OTLP trace).\n */\nexport class GatedMlflowSpanProcessor implements SpanProcessor {\n #inner: SpanProcessor;\n #ready = false;\n // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never\n // sees an end without a matching start.\n #forwarded = new WeakSet<object>();\n // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the\n // root's `onEnd` exports rather than discards. Cleared as each root ends.\n #agentTraceIds = new Set<string>();\n #popTrace: (otelTraceId: string) => void;\n #spanTypeKey: string;\n // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace\n // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which\n // mlflow stamps on all non-agent spans) is treated as non-agent.\n #agentSpanTypes: ReadonlySet<string>;\n // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd.\n #maxTracked: number;\n // Cap on how long forceFlush/shutdown wait for a stuck export.\n #flushTimeoutMs: number;\n\n constructor(\n inner: SpanProcessor,\n deps: {\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n maxTracked?: number;\n flushTimeoutMs?: number;\n },\n ) {\n this.#inner = inner;\n this.#popTrace = deps.popTrace;\n this.#spanTypeKey = deps.spanTypeKey;\n this.#agentSpanTypes = deps.agentSpanTypes;\n this.#maxTracked = deps.maxTracked ?? 1024;\n this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000;\n }\n\n ready(): void {\n this.#ready = true;\n }\n\n onStart(\n span: Parameters<SpanProcessor[\"onStart\"]>[0],\n parentContext: Parameters<SpanProcessor[\"onStart\"]>[1],\n ): void {\n if (!this.#ready) return;\n // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is\n // an outgoing request made outside any agent turn — e.g. mlflow or OTLP\n // shipping a trace. Forwarding those would loop: each upload is itself an\n // HTTP call that auto-instrumentation turns into a new span to trace and\n // upload.\n if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) {\n return;\n }\n this.#forwarded.add(span);\n this.#inner.onStart(span, parentContext);\n }\n\n onEnd(span: Parameters<SpanProcessor[\"onEnd\"]>[0]): void {\n if (!this.#forwarded.has(span)) return;\n const traceId = span.spanContext().traceId;\n // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps\n // `mlflow.spanType` on every span (UNKNOWN by default), so match the value,\n // not mere presence; the real type is set after `onStart`, so `onEnd` is the\n // earliest we can read it.\n if (\n this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) &&\n !this.#agentTraceIds.has(traceId)\n ) {\n // Normally an entry lives only until its root's onEnd deletes it. But a\n // root that ends BEFORE its agent child (streaming client-disconnect) or\n // never ends (crash) orphans the entry, and #agentTraceIds — unlike the\n // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean.\n // FIFO-evict at the cap so an abandoned-trace pattern can't grow it\n // unboundedly over process uptime.\n // ponytail: evicting a still-live trace only mis-discards it, and that\n // needs >#maxTracked concurrent agent turns — far above real load.\n if (this.#agentTraceIds.size >= this.#maxTracked) {\n const oldest = this.#agentTraceIds.values().next().value;\n if (oldest !== undefined) this.#agentTraceIds.delete(oldest);\n }\n this.#agentTraceIds.add(traceId);\n }\n if (span.parentSpanContext?.spanId) {\n // Non-root: mlflow's own `onEnd` early-returns, but forward for balance.\n this.#inner.onEnd(span);\n return;\n }\n // Root span: export only agent traces; discard everything else so plain HTTP\n // requests never become MLflow traces. `delete` reports whether it was agent.\n if (this.#agentTraceIds.delete(traceId)) {\n this.#inner.onEnd(span);\n } else {\n this.#popTrace(traceId);\n }\n }\n\n forceFlush(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.forceFlush());\n }\n\n shutdown(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.shutdown());\n }\n\n // Bound the inner flush/shutdown wait: both exporters export fire-and-forget,\n // so a stuck export only wedges here (graceful shutdown), never a turn.\n // Resolves — not rejects — on timeout: the caller is tearing down.\n async #boundedFlush(op: () => Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n op().catch((err) => {\n logger.warn(\"MLflow trace flush error: %O\", err);\n }),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n logger.warn(\n \"MLflow trace flush exceeded %dms; continuing (export may still be in flight)\",\n this.#flushTimeoutMs,\n );\n resolve();\n }, this.#flushTimeoutMs);\n timer.unref(); // don't keep the event loop alive on the timeout alone\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n\n/**\n * Resolve the Unity Catalog trace location for the bound experiment, or\n * `undefined` for classic experiment-backed storage. Any failure falls back to\n * classic — a tracing misconfiguration must never break the agent.\n *\n * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` +\n * `MLFLOW_UC_TABLE_PREFIX`, all three required.\n * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since\n * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with\n * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the\n * backend-populated spans/logs table names for custom-provisioned locations.\n * 3. Otherwise classic.\n *\n * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so\n * it's deep-imported like the exporter classes and covered by the tripwire test.\n */\nasync function resolveUcLocation(\n experimentId: string,\n client: MlflowClientInstance,\n): Promise<UnityCatalogLocation | undefined> {\n const catalogName = process.env.MLFLOW_UC_CATALOG?.trim();\n const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim();\n const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim();\n if (catalogName && schemaName && tablePrefix) {\n return { catalogName, schemaName, tablePrefix };\n }\n\n if (!/^\\d+$/.test(experimentId)) return undefined;\n\n try {\n const experiment = await client.getExperiment(experimentId);\n if (!experiment) return undefined;\n const { ucLocationFromExperimentTags } =\n await import(\"@mlflow/core/dist/core/destination\");\n return ucLocationFromExperimentTags(experiment.tags) ?? undefined;\n } catch (err) {\n logger.warn(\n \"MLflow UC trace-location auto-detect failed; using classic experiment storage: %O\",\n err,\n );\n return undefined;\n }\n}\n\n/**\n * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()`\n * build and globally register its own tracer provider (its own `NodeSDK`). This\n * lets AppKit own the single global provider (OTLP + this processor), so agent\n * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the\n * global slot.\n *\n * When a UC trace location is bound, builds the Unity Catalog processor +\n * exporter (V4 trace ids, spans uploaded to the experiment's UC table);\n * otherwise the classic experiment-backed processor. `createAuthProvider`,\n * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public\n * in `@mlflow/core`, so only the exporter/processor classes are deep-imported —\n * pinned to the exact version in package.json and guarded by a test that fails\n * loudly if a version bump renames them.\n *\n * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope\n * forwarding to agent traces: `popTrace` (to discard non-agent traces), the\n * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values\n * that mark a trace as an agent turn (mlflow stamps every span, defaulting to\n * UNKNOWN, so the gate must match the value, not presence).\n */\nasync function buildMlflowSpanProcessor(\n m: MlflowModule,\n client: MlflowClientInstance,\n ucLoc: UnityCatalogLocation | undefined,\n): Promise<{\n processor: SpanProcessor;\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n}> {\n let processor: SpanProcessor;\n if (ucLoc) {\n const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/uc_table\");\n processor = new DatabricksUCTableSpanProcessor(\n new DatabricksUCTableSpanExporter(client),\n ucLoc,\n );\n } else {\n const { MlflowSpanExporter, MlflowSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/mlflow\");\n processor = new MlflowSpanProcessor(new MlflowSpanExporter(client));\n }\n return {\n processor,\n popTrace: (otelTraceId) =>\n m.InMemoryTraceManager.getInstance().popTrace(otelTraceId),\n spanTypeKey: m.SpanAttributeKey.SPAN_TYPE,\n // mlflow JSON-stringifies attribute values, so the stored values are\n // `\"AGENT\"`/`\"TOOL\"` (quoted). Match that exact form.\n agentSpanTypes: new Set([\n JSON.stringify(m.SpanType.AGENT),\n JSON.stringify(m.SpanType.TOOL),\n ]),\n };\n}\n\n/** The bound MLflow experiment id, from the optional `experiment` resource. */\nfunction experimentId(): string | undefined {\n const id = process.env.MLFLOW_EXPERIMENT_ID?.trim();\n return id || undefined;\n}\n\n/**\n * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST`\n * verbatim to build request URLs and doesn't add `https://`, so a bare host\n * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit\n * normalized host when the env var is set; when it isn't (profile-based auth),\n * return undefined and let the SDK read the host from `~/.databrickscfg`.\n */\nfunction normalizedDatabricksHost(): string | undefined {\n const raw = process.env.DATABRICKS_HOST?.trim();\n if (!raw) return undefined;\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}`;\n}\n\n/**\n * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the\n * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`).\n * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`.\n *\n * Rather than let `@mlflow/core`'s `init()` stand up and globally register its\n * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the\n * span processor ourselves and contribute it to AppKit's single provider via\n * {@link TelemetryManager.registerSpanProcessor}. For the classic\n * experiment-backed processor, mlflow's global config is seeded by\n * {@link startAgentTracing} on the `\"setup:complete\"` lifecycle event (after\n * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the\n * UC processor needs no seeded config, so that path never calls `init()`.\n *\n * The trace store is resolved here: a UC table prefix (env-configured or\n * auto-detected from the experiment's Databricks tags) vs. the classic\n * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks\n * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg`\n * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP\n * headers are wired by hand. A failure (missing creds, bad experiment) logs and\n * leaves tracing disabled rather than breaking the agent.\n *\n * Safe to call repeatedly; only the first call does work.\n */\nexport async function initAgentTracing(): Promise<void> {\n if (initStarted) return;\n initStarted = true;\n\n const id = experimentId();\n if (!id) return;\n\n try {\n mlflow = await import(\"@mlflow/core\");\n const host = normalizedDatabricksHost();\n initConfig = {\n trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || \"databricks\",\n experimentId: id,\n ...(host ? { host } : {}),\n };\n // One auth resolution + client, reused for UC auto-detect and the exporter.\n const authProvider = mlflow.createAuthProvider({\n trackingUri: initConfig.trackingUri,\n ...(host ? { host } : {}),\n });\n const client = new mlflow.MlflowClient({\n trackingUri: initConfig.trackingUri,\n authProvider,\n });\n ucLocation = await resolveUcLocation(id, client);\n const { processor, popTrace, spanTypeKey, agentSpanTypes } =\n await buildMlflowSpanProcessor(mlflow, client, ucLocation);\n gatedProcessor = new GatedMlflowSpanProcessor(processor, {\n popTrace,\n spanTypeKey,\n agentSpanTypes,\n });\n TelemetryManager.registerSpanProcessor(gatedProcessor);\n enabled = true;\n if (ucLocation) {\n logger.info(\n \"MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)\",\n id,\n ucLocation.catalogName,\n ucLocation.schemaName,\n ucLocation.tablePrefix,\n );\n } else {\n logger.info(\"MLflow agent tracing enabled (experiment %s)\", id);\n }\n } catch (err) {\n if (isModuleNotFound(err)) {\n logger.warn(\n \"MLflow agent tracing requires the optional `@mlflow/core` package. \" +\n \"Install it to enable tracing: npm i @mlflow/core\",\n );\n } else {\n logger.warn(\"MLflow agent tracing disabled: %O\", err);\n }\n }\n}\n\n/** True when a dynamic `import()` failed because the package isn't installed. */\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n !!err &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err.code === \"ERR_MODULE_NOT_FOUND\" || err.code === \"MODULE_NOT_FOUND\")\n );\n}\n\n/**\n * Records a span's outputs. Callers get one from `traceAgent`/`traceTool`;\n * it's a no-op when tracing is disabled, so call sites never branch on it.\n */\nexport interface SpanRecorder {\n setOutputs(outputs: unknown): void;\n}\n\nconst noopRecorder: SpanRecorder = { setOutputs() {} };\n\n/**\n * Seed the classic processor's global config once, AFTER\n * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly\n * by {@link startAgentTracing} on `\"setup:complete\"`, or lazily by {@link trace}\n * as a fallback — then enable forwarding on the gated processor. Returns whether\n * tracing is usable.\n *\n * The classic experiment-backed `MlflowSpanProcessor` reads\n * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's\n * global config. `init()` also stands up its own `NodeSDK` whose provider loses\n * the global slot to AppKit's already-registered one (non-fatal); we call it\n * only for that config side-effect. The UC processor carries its location and\n * reads no global config, so the UC path skips `init()` entirely — no second\n * `NodeSDK`, no competing global registration by `@mlflow/core`.\n */\nfunction ensureConfigured(): boolean {\n if (configured) return enabled;\n configured = true;\n // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw,\n // `mlflow` and `initConfig` are still set but there is no gate. Calling\n // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if\n // AppKit registered none (no OTLP, no processor) it wins the global slot,\n // routing every span into mlflow un-gated: the exact over-tracing + exporter\n // loop this file exists to prevent.\n if (!mlflow || !initConfig || !gatedProcessor) return false;\n try {\n if (!ucLocation) mlflow.init(initConfig);\n gatedProcessor.ready();\n return true;\n } catch (err) {\n enabled = false;\n logger.warn(\"MLflow agent tracing disabled (init failed): %O\", err);\n return false;\n }\n}\n\n/**\n * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the\n * agents plugin wires this to the `\"setup:complete\"` lifecycle event, before the\n * server serves any request. Doing it here means the request's own root span is\n * already forwarded when the first turn runs, so that turn assembles into a\n * trace instead of being dropped (mlflow roots a trace only at the top-level\n * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a\n * turn whose agent span is itself the trace root; an HTTP-wrapped first turn\n * seeded lazily loses its root span (already started — and dropped — before\n * `ready()`), so this eager path is the reliable one.\n */\nexport function startAgentTracing(): void {\n ensureConfigured();\n}\n\n/**\n * Run `fn` inside an MLflow span of `spanType` when tracing is enabled,\n * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active\n * context, so a TOOL span opened inside an AGENT span's callback becomes its\n * child. The callback's resolved value is recorded as the span's outputs unless\n * it called `setOutputs` first; return `undefined` (or set outputs explicitly)\n * when the return value isn't the output you want traced.\n */\nasync function trace<T>(\n spanType: \"AGENT\" | \"TOOL\",\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n if (!enabled || !mlflow) return fn(noopRecorder);\n const m = mlflow;\n if (!ensureConfigured()) return fn(noopRecorder);\n const type = spanType === \"AGENT\" ? m.SpanType.AGENT : m.SpanType.TOOL;\n return await m.withSpan<T>(\n async (span) => {\n if (inputs !== undefined) span.setInputs(inputs);\n let outputsSet = false;\n const result = await fn({\n setOutputs(outputs) {\n outputsSet = true;\n span.setOutputs(outputs);\n },\n });\n if (!outputsSet && result !== undefined) span.setOutputs(result);\n return result;\n },\n { name, spanType: type },\n );\n}\n\n/** Trace a turn's root AGENT span. See {@link trace}. */\nexport function traceAgent<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"AGENT\", name, inputs, fn);\n}\n\n/** Trace a TOOL span, nested under the active AGENT span. See {@link trace}. */\nexport function traceTool<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"TOOL\", name, inputs, fn);\n}\n\n/**\n * The MLflow trace id for the active turn, when tracing is enabled. Must be\n * read inside an agent span so eval runs can correlate the turn to its trace\n * and attach assessments. Returns undefined when tracing is off.\n *\n * Reads the context-active span rather than `getLastActiveTraceId()`: the\n * latter is only populated when a root span *ends* (on export), so mid-turn it\n * returns the previous turn's id — or, under concurrent turns, another turn's.\n */\nexport function currentTraceId(): string | undefined {\n if (!enabled || !mlflow) return undefined;\n try {\n return mlflow.getCurrentActiveSpan()?.traceId;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Link the active turn's trace to an MLflow run by id, via the `mlflow.sourceRun`\n * trace metadata. Used by eval runs so each case's trace shows under the run.\n * Must be called while a trace is active (inside an agent span). No-op when\n * tracing is disabled.\n */\nexport function linkTraceToRun(runId: string): void {\n if (!enabled || !mlflow) return;\n try {\n mlflow.updateCurrentTrace({ metadata: { \"mlflow.sourceRun\": runId } });\n } catch (err) {\n logger.warn(\"Failed to link trace to run %s: %O\", runId, err);\n }\n}\n"],"mappings":"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"}
@@ -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 fs from "node:fs";
22
+ import fsSync 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 (fs.existsSync(path.resolve(fullPath, "index.html"))) {
249
+ if (fsSync.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"],"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","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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"}
@@ -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 fs from "node:fs";
6
+ import fsSync 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 = fs.readFileSync(indexPath, "utf-8");
121
+ let html = fsSync.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 = fs.readFileSync(filePath, "utf-8");
136
+ let content = fsSync.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":[],"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":["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,6 +1,6 @@
1
1
  import { BaseServer } from "./base-server.js";
2
2
  import path from "node:path";
3
- import fs from "node:fs";
3
+ import fsSync 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 (!fs.existsSync(indexPath)) {
36
+ if (!fsSync.existsSync(indexPath)) {
37
37
  res.status(404).send("index.html not found");
38
38
  return;
39
39
  }
40
- let html = fs.readFileSync(indexPath, "utf-8");
40
+ let html = fsSync.readFileSync(indexPath, "utf-8");
41
41
  html = html.replace("<body>", `<body>${this.getConfigScript()}`);
42
42
  res.send(html);
43
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"static-server.js","names":["expressStatic"],"sources":["../../../src/plugins/server/static-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type express from \"express\";\nimport expressStatic from \"express\";\n\nimport { BaseServer } from \"./base-server\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\n/**\n * Static server for the AppKit.\n *\n * Serves pre-built static files in production mode. Handles SPA routing\n * by serving index.html for non-API routes and injects runtime configuration.\n *\n * @example\n * ```ts\n * const staticServer = new StaticServer(app, staticPath, endpoints);\n * staticServer.setup();\n * ```\n */\nexport class StaticServer extends BaseServer {\n private staticPath: string;\n\n constructor(\n app: express.Application,\n staticPath: string,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.staticPath = staticPath;\n }\n\n /** Setup the static server. */\n setup() {\n this.app.use(\n expressStatic.static(this.staticPath, {\n index: false,\n }),\n );\n\n this.app.get(\"*\", (req, res, next) => {\n if (req.path.startsWith(\"/api\") || req.path.startsWith(\"/query\")) {\n return next();\n }\n this.serveIndex(res);\n });\n }\n\n /** Serve the index.html file. */\n private serveIndex(res: express.Response) {\n const indexPath = path.join(this.staticPath, \"index.html\");\n\n if (!fs.existsSync(indexPath)) {\n res.status(404).send(\"index.html not found\");\n return;\n }\n\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n res.send(html);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,IAAa,eAAb,cAAkC,WAAW;CAC3C,AAAQ;CAER,YACE,KACA,YACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,aAAa;;;CAIpB,QAAQ;AACN,OAAK,IAAI,IACPA,QAAc,OAAO,KAAK,YAAY,EACpC,OAAO,OACR,CAAC,CACH;AAED,OAAK,IAAI,IAAI,MAAM,KAAK,KAAK,SAAS;AACpC,OAAI,IAAI,KAAK,WAAW,OAAO,IAAI,IAAI,KAAK,WAAW,SAAS,CAC9D,QAAO,MAAM;AAEf,QAAK,WAAW,IAAI;IACpB;;;CAIJ,AAAQ,WAAW,KAAuB;EACxC,MAAM,YAAY,KAAK,KAAK,KAAK,YAAY,aAAa;AAE1D,MAAI,CAAC,GAAG,WAAW,UAAU,EAAE;AAC7B,OAAI,OAAO,IAAI,CAAC,KAAK,uBAAuB;AAC5C;;EAGF,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,SAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,MAAI,KAAK,KAAK"}
1
+ {"version":3,"file":"static-server.js","names":["expressStatic","fs"],"sources":["../../../src/plugins/server/static-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type express from \"express\";\nimport expressStatic from \"express\";\n\nimport { BaseServer } from \"./base-server\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\n/**\n * Static server for the AppKit.\n *\n * Serves pre-built static files in production mode. Handles SPA routing\n * by serving index.html for non-API routes and injects runtime configuration.\n *\n * @example\n * ```ts\n * const staticServer = new StaticServer(app, staticPath, endpoints);\n * staticServer.setup();\n * ```\n */\nexport class StaticServer extends BaseServer {\n private staticPath: string;\n\n constructor(\n app: express.Application,\n staticPath: string,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.staticPath = staticPath;\n }\n\n /** Setup the static server. */\n setup() {\n this.app.use(\n expressStatic.static(this.staticPath, {\n index: false,\n }),\n );\n\n this.app.get(\"*\", (req, res, next) => {\n if (req.path.startsWith(\"/api\") || req.path.startsWith(\"/query\")) {\n return next();\n }\n this.serveIndex(res);\n });\n }\n\n /** Serve the index.html file. */\n private serveIndex(res: express.Response) {\n const indexPath = path.join(this.staticPath, \"index.html\");\n\n if (!fs.existsSync(indexPath)) {\n res.status(404).send(\"index.html not found\");\n return;\n }\n\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n res.send(html);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,IAAa,eAAb,cAAkC,WAAW;CAC3C,AAAQ;CAER,YACE,KACA,YACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,aAAa;;;CAIpB,QAAQ;AACN,OAAK,IAAI,IACPA,QAAc,OAAO,KAAK,YAAY,EACpC,OAAO,OACR,CAAC,CACH;AAED,OAAK,IAAI,IAAI,MAAM,KAAK,KAAK,SAAS;AACpC,OAAI,IAAI,KAAK,WAAW,OAAO,IAAI,IAAI,KAAK,WAAW,SAAS,CAC9D,QAAO,MAAM;AAEf,QAAK,WAAW,IAAI;IACpB;;;CAIJ,AAAQ,WAAW,KAAuB;EACxC,MAAM,YAAY,KAAK,KAAK,KAAK,YAAY,aAAa;AAE1D,MAAI,CAACC,OAAG,WAAW,UAAU,EAAE;AAC7B,OAAI,OAAO,IAAI,CAAC,KAAK,uBAAuB;AAC5C;;EAGF,IAAI,OAAOA,OAAG,aAAa,WAAW,QAAQ;AAC9C,SAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,MAAI,KAAK,KAAK"}
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import pc from "picocolors";
3
3
  import path from "node:path";
4
- import fs from "node:fs";
4
+ import fsSync from "node:fs";
5
5
 
6
6
  //#region src/plugins/server/utils.ts
7
7
  function parseCookies(req) {
@@ -74,8 +74,8 @@ function printRoutes(routes) {
74
74
  }
75
75
  function getQueries(configFolder) {
76
76
  const queriesFolder = path.join(configFolder, "queries");
77
- if (!fs.existsSync(queriesFolder)) return {};
78
- return Object.fromEntries(fs.readdirSync(queriesFolder).filter((f) => path.extname(f) === ".sql").map((f) => [path.basename(f, ".sql"), path.basename(f, ".sql")]));
77
+ if (!fsSync.existsSync(queriesFolder)) return {};
78
+ return Object.fromEntries(fsSync.readdirSync(queriesFolder).filter((f) => path.extname(f) === ".sql").map((f) => [path.basename(f, ".sql"), path.basename(f, ".sql")]));
79
79
  }
80
80
  const APPKIT_CONFIG_SCRIPT_ID = "__appkit__";
81
81
  const EMPTY_RUNTIME_CONFIG_JSON = JSON.stringify({