@databricks/appkit 0.4.1 → 0.5.1

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 (212) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/analytics/analytics.d.ts +11 -1
  3. package/dist/analytics/analytics.d.ts.map +1 -1
  4. package/dist/analytics/analytics.js +12 -5
  5. package/dist/analytics/analytics.js.map +1 -1
  6. package/dist/app/index.d.ts +11 -2
  7. package/dist/app/index.d.ts.map +1 -1
  8. package/dist/app/index.js +51 -37
  9. package/dist/app/index.js.map +1 -1
  10. package/dist/appkit/package.js +1 -1
  11. package/dist/cache/index.js +11 -10
  12. package/dist/cache/index.js.map +1 -1
  13. package/dist/cache/storage/memory.js +4 -2
  14. package/dist/cache/storage/memory.js.map +1 -1
  15. package/dist/cache/storage/persistent.js +7 -0
  16. package/dist/cache/storage/persistent.js.map +1 -1
  17. package/dist/connectors/lakebase/client.js +8 -4
  18. package/dist/connectors/lakebase/client.js.map +1 -1
  19. package/dist/connectors/sql-warehouse/client.js +5 -2
  20. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  21. package/dist/context/execution-context.d.ts +17 -0
  22. package/dist/context/execution-context.d.ts.map +1 -0
  23. package/dist/context/service-context.d.ts +21 -0
  24. package/dist/context/service-context.d.ts.map +1 -0
  25. package/dist/context/service-context.js +2 -6
  26. package/dist/context/service-context.js.map +1 -1
  27. package/dist/context/user-context.d.ts +29 -0
  28. package/dist/context/user-context.d.ts.map +1 -0
  29. package/dist/core/appkit.d.ts.map +1 -1
  30. package/dist/core/appkit.js +35 -12
  31. package/dist/core/appkit.js.map +1 -1
  32. package/dist/errors/authentication.js +3 -6
  33. package/dist/errors/authentication.js.map +1 -1
  34. package/dist/errors/base.js +4 -0
  35. package/dist/errors/base.js.map +1 -1
  36. package/dist/errors/configuration.js +3 -6
  37. package/dist/errors/configuration.js.map +1 -1
  38. package/dist/errors/connection.js +3 -6
  39. package/dist/errors/connection.js.map +1 -1
  40. package/dist/errors/execution.js +3 -6
  41. package/dist/errors/execution.js.map +1 -1
  42. package/dist/errors/initialization.js +3 -6
  43. package/dist/errors/initialization.js.map +1 -1
  44. package/dist/errors/server.js +3 -6
  45. package/dist/errors/server.js.map +1 -1
  46. package/dist/errors/tunnel.js +3 -6
  47. package/dist/errors/tunnel.js.map +1 -1
  48. package/dist/errors/validation.js +3 -6
  49. package/dist/errors/validation.js.map +1 -1
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +4 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/logging/wide-event-emitter.js +1 -3
  54. package/dist/logging/wide-event-emitter.js.map +1 -1
  55. package/dist/logging/wide-event.js +2 -0
  56. package/dist/logging/wide-event.js.map +1 -1
  57. package/dist/plugin/dev-reader.d.ts +1 -0
  58. package/dist/plugin/dev-reader.d.ts.map +1 -1
  59. package/dist/plugin/dev-reader.js +41 -6
  60. package/dist/plugin/dev-reader.js.map +1 -1
  61. package/dist/plugin/interceptors/retry.js +3 -0
  62. package/dist/plugin/interceptors/retry.js.map +1 -1
  63. package/dist/plugin/plugin.d.ts +10 -24
  64. package/dist/plugin/plugin.d.ts.map +1 -1
  65. package/dist/plugin/plugin.js +23 -30
  66. package/dist/plugin/plugin.js.map +1 -1
  67. package/dist/server/base-server.js +2 -0
  68. package/dist/server/base-server.js.map +1 -1
  69. package/dist/server/index.d.ts +23 -1
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +29 -13
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/server/remote-tunnel/remote-tunnel-controller.js +30 -15
  74. package/dist/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
  75. package/dist/server/remote-tunnel/remote-tunnel-manager.js +27 -2
  76. package/dist/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  77. package/dist/server/static-server.js +1 -0
  78. package/dist/server/static-server.js.map +1 -1
  79. package/dist/server/vite-dev-server.js +1 -0
  80. package/dist/server/vite-dev-server.js.map +1 -1
  81. package/dist/shared/src/plugin.d.ts +25 -1
  82. package/dist/shared/src/plugin.d.ts.map +1 -1
  83. package/dist/stream/arrow-stream-processor.js +5 -10
  84. package/dist/stream/arrow-stream-processor.js.map +1 -1
  85. package/dist/stream/buffers.js +7 -0
  86. package/dist/stream/buffers.js.map +1 -1
  87. package/dist/stream/stream-manager.js +5 -0
  88. package/dist/stream/stream-manager.js.map +1 -1
  89. package/dist/stream/stream-registry.js +1 -0
  90. package/dist/stream/stream-registry.js.map +1 -1
  91. package/dist/stream/validator.js +2 -6
  92. package/dist/stream/validator.js.map +1 -1
  93. package/dist/telemetry/noop.js +1 -0
  94. package/dist/telemetry/noop.js.map +1 -1
  95. package/dist/telemetry/telemetry-manager.js +4 -6
  96. package/dist/telemetry/telemetry-manager.js.map +1 -1
  97. package/dist/telemetry/telemetry-provider.js +3 -0
  98. package/dist/telemetry/telemetry-provider.js.map +1 -1
  99. package/dist/type-generator/spinner.js +9 -11
  100. package/dist/type-generator/spinner.js.map +1 -1
  101. package/docs/docs/api/appkit/Class.AppKitError/index.html +5 -3
  102. package/docs/docs/api/appkit/Class.AppKitError.md +7 -0
  103. package/docs/docs/api/appkit/Class.AuthenticationError/index.html +3 -3
  104. package/docs/docs/api/appkit/Class.ConfigurationError/index.html +3 -3
  105. package/docs/docs/api/appkit/Class.ConnectionError/index.html +3 -3
  106. package/docs/docs/api/appkit/Class.ExecutionError/index.html +3 -3
  107. package/docs/docs/api/appkit/Class.InitializationError/index.html +3 -3
  108. package/docs/docs/api/appkit/Class.Plugin/index.html +28 -21
  109. package/docs/docs/api/appkit/Class.Plugin.md +34 -34
  110. package/docs/docs/api/appkit/Class.ServerError/index.html +3 -3
  111. package/docs/docs/api/appkit/Class.TunnelError/index.html +3 -3
  112. package/docs/docs/api/appkit/Class.ValidationError/index.html +3 -3
  113. package/docs/docs/api/appkit/Function.appKitTypesPlugin/index.html +3 -3
  114. package/docs/docs/api/appkit/Function.createApp/index.html +4 -4
  115. package/docs/docs/api/appkit/Function.getExecutionContext/index.html +26 -0
  116. package/docs/docs/api/appkit/Function.getExecutionContext.md +19 -0
  117. package/docs/docs/api/appkit/Function.isSQLTypeMarker/index.html +4 -4
  118. package/docs/docs/api/appkit/Interface.BasePluginConfig/index.html +3 -3
  119. package/docs/docs/api/appkit/Interface.CacheConfig/index.html +3 -3
  120. package/docs/docs/api/appkit/Interface.ITelemetry/index.html +3 -3
  121. package/docs/docs/api/appkit/Interface.StreamExecutionSettings/index.html +3 -3
  122. package/docs/docs/api/appkit/Interface.TelemetryConfig/index.html +3 -3
  123. package/docs/docs/api/appkit/TypeAlias.IAppRouter/index.html +3 -3
  124. package/docs/docs/api/appkit/Variable.sql/index.html +3 -3
  125. package/docs/docs/api/appkit/index.html +4 -4
  126. package/docs/docs/api/appkit-ui/data/AreaChart/index.html +2 -2
  127. package/docs/docs/api/appkit-ui/data/BarChart/index.html +2 -2
  128. package/docs/docs/api/appkit-ui/data/DataTable/index.html +2 -2
  129. package/docs/docs/api/appkit-ui/data/DonutChart/index.html +2 -2
  130. package/docs/docs/api/appkit-ui/data/HeatmapChart/index.html +2 -2
  131. package/docs/docs/api/appkit-ui/data/LineChart/index.html +2 -2
  132. package/docs/docs/api/appkit-ui/data/PieChart/index.html +2 -2
  133. package/docs/docs/api/appkit-ui/data/RadarChart/index.html +2 -2
  134. package/docs/docs/api/appkit-ui/data/ScatterChart/index.html +2 -2
  135. package/docs/docs/api/appkit-ui/index.html +2 -2
  136. package/docs/docs/api/appkit-ui/styling/index.html +2 -2
  137. package/docs/docs/api/appkit-ui/ui/Accordion/index.html +2 -2
  138. package/docs/docs/api/appkit-ui/ui/Alert/index.html +2 -2
  139. package/docs/docs/api/appkit-ui/ui/AlertDialog/index.html +2 -2
  140. package/docs/docs/api/appkit-ui/ui/AspectRatio/index.html +2 -2
  141. package/docs/docs/api/appkit-ui/ui/Avatar/index.html +2 -2
  142. package/docs/docs/api/appkit-ui/ui/Badge/index.html +2 -2
  143. package/docs/docs/api/appkit-ui/ui/Breadcrumb/index.html +2 -2
  144. package/docs/docs/api/appkit-ui/ui/Button/index.html +2 -2
  145. package/docs/docs/api/appkit-ui/ui/ButtonGroup/index.html +2 -2
  146. package/docs/docs/api/appkit-ui/ui/Calendar/index.html +2 -2
  147. package/docs/docs/api/appkit-ui/ui/Card/index.html +2 -2
  148. package/docs/docs/api/appkit-ui/ui/Carousel/index.html +2 -2
  149. package/docs/docs/api/appkit-ui/ui/ChartContainer/index.html +2 -2
  150. package/docs/docs/api/appkit-ui/ui/Checkbox/index.html +2 -2
  151. package/docs/docs/api/appkit-ui/ui/Collapsible/index.html +2 -2
  152. package/docs/docs/api/appkit-ui/ui/Command/index.html +2 -2
  153. package/docs/docs/api/appkit-ui/ui/ContextMenu/index.html +2 -2
  154. package/docs/docs/api/appkit-ui/ui/Dialog/index.html +2 -2
  155. package/docs/docs/api/appkit-ui/ui/Drawer/index.html +2 -2
  156. package/docs/docs/api/appkit-ui/ui/DropdownMenu/index.html +2 -2
  157. package/docs/docs/api/appkit-ui/ui/Empty/index.html +2 -2
  158. package/docs/docs/api/appkit-ui/ui/Field/index.html +2 -2
  159. package/docs/docs/api/appkit-ui/ui/FormControl/index.html +2 -2
  160. package/docs/docs/api/appkit-ui/ui/HoverCard/index.html +2 -2
  161. package/docs/docs/api/appkit-ui/ui/Input/index.html +2 -2
  162. package/docs/docs/api/appkit-ui/ui/InputGroup/index.html +2 -2
  163. package/docs/docs/api/appkit-ui/ui/InputOTP/index.html +2 -2
  164. package/docs/docs/api/appkit-ui/ui/Item/index.html +2 -2
  165. package/docs/docs/api/appkit-ui/ui/Kbd/index.html +2 -2
  166. package/docs/docs/api/appkit-ui/ui/Label/index.html +2 -2
  167. package/docs/docs/api/appkit-ui/ui/Menubar/index.html +2 -2
  168. package/docs/docs/api/appkit-ui/ui/NavigationMenu/index.html +2 -2
  169. package/docs/docs/api/appkit-ui/ui/Pagination/index.html +2 -2
  170. package/docs/docs/api/appkit-ui/ui/Popover/index.html +2 -2
  171. package/docs/docs/api/appkit-ui/ui/Progress/index.html +2 -2
  172. package/docs/docs/api/appkit-ui/ui/RadioGroup/index.html +2 -2
  173. package/docs/docs/api/appkit-ui/ui/ResizableHandle/index.html +2 -2
  174. package/docs/docs/api/appkit-ui/ui/ScrollArea/index.html +2 -2
  175. package/docs/docs/api/appkit-ui/ui/Select/index.html +2 -2
  176. package/docs/docs/api/appkit-ui/ui/Separator/index.html +2 -2
  177. package/docs/docs/api/appkit-ui/ui/Sheet/index.html +2 -2
  178. package/docs/docs/api/appkit-ui/ui/Sidebar/index.html +2 -2
  179. package/docs/docs/api/appkit-ui/ui/Skeleton/index.html +2 -2
  180. package/docs/docs/api/appkit-ui/ui/Slider/index.html +2 -2
  181. package/docs/docs/api/appkit-ui/ui/Spinner/index.html +2 -2
  182. package/docs/docs/api/appkit-ui/ui/Switch/index.html +2 -2
  183. package/docs/docs/api/appkit-ui/ui/Table/index.html +2 -2
  184. package/docs/docs/api/appkit-ui/ui/Tabs/index.html +2 -2
  185. package/docs/docs/api/appkit-ui/ui/Textarea/index.html +2 -2
  186. package/docs/docs/api/appkit-ui/ui/Toaster/index.html +2 -2
  187. package/docs/docs/api/appkit-ui/ui/Toggle/index.html +2 -2
  188. package/docs/docs/api/appkit-ui/ui/ToggleGroup/index.html +2 -2
  189. package/docs/docs/api/appkit-ui/ui/Tooltip/index.html +2 -2
  190. package/docs/docs/api/appkit.md +6 -5
  191. package/docs/docs/api/index.html +2 -2
  192. package/docs/docs/app-management/index.html +4 -4
  193. package/docs/docs/app-management.md +2 -2
  194. package/docs/docs/architecture/index.html +2 -2
  195. package/docs/docs/category/development/index.html +2 -2
  196. package/docs/docs/configuration/index.html +2 -2
  197. package/docs/docs/core-principles/index.html +2 -2
  198. package/docs/docs/development/index.html +4 -4
  199. package/docs/docs/development/llm-guide/index.html +2 -2
  200. package/docs/docs/development/local-development/index.html +4 -4
  201. package/docs/docs/development/local-development.md +2 -2
  202. package/docs/docs/development/project-setup/index.html +2 -2
  203. package/docs/docs/development/remote-bridge/index.html +4 -4
  204. package/docs/docs/development/remote-bridge.md +2 -2
  205. package/docs/docs/development/type-generation/index.html +2 -2
  206. package/docs/docs/development.md +2 -2
  207. package/docs/docs/index.html +4 -4
  208. package/docs/docs/plugins/index.html +7 -3
  209. package/docs/docs/plugins.md +36 -11
  210. package/docs/docs.md +2 -2
  211. package/llms.txt +1 -0
  212. package/package.json +1 -1
@@ -40,13 +40,18 @@ const EXCLUDED_FROM_PROXY = new Set([
40
40
  ]);
41
41
  /** Base abstract class for creating AppKit plugins */
42
42
  var Plugin = class {
43
- static {
44
- this.phase = "normal";
45
- }
43
+ isReady = false;
44
+ cache;
45
+ app;
46
+ devFileReader;
47
+ streamManager;
48
+ telemetry;
49
+ /** Registered endpoints for this plugin */
50
+ registeredEndpoints = {};
51
+ static phase = "normal";
52
+ name;
46
53
  constructor(config) {
47
54
  this.config = config;
48
- this.isReady = false;
49
- this.registeredEndpoints = {};
50
55
  this.name = config.name ?? "plugin";
51
56
  this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);
52
57
  this.streamManager = new StreamManager();
@@ -67,33 +72,21 @@ var Plugin = class {
67
72
  this.streamManager.abortAll();
68
73
  }
69
74
  /**
75
+ * Returns the public exports for this plugin.
76
+ * Override this to define a custom public API.
77
+ * By default, returns an empty object.
78
+ */
79
+ exports() {
80
+ return {};
81
+ }
82
+ /**
70
83
  * Execute operations using the user's identity from the request.
71
- *
72
- * Returns a scoped instance of this plugin where all method calls
73
- * will execute with the user's Databricks credentials instead of
74
- * the service principal.
84
+ * Returns a proxy of this plugin where all method calls execute
85
+ * with the user's Databricks credentials instead of the service principal.
75
86
  *
76
87
  * @param req - The Express request containing the user token in headers
77
- * @returns A scoped plugin instance that executes as the user
88
+ * @returns A proxied plugin instance that executes as the user
78
89
  * @throws Error if user token is not available in request headers
79
- *
80
- * @example
81
- * ```typescript
82
- * // In route handler - execute query as the requesting user
83
- * router.post('/users/me/query/:key', async (req, res) => {
84
- * const result = await this.asUser(req).query(req.params.key)
85
- * res.json(result)
86
- * })
87
- *
88
- * // Mixed execution in same handler
89
- * router.post('/dashboard', async (req, res) => {
90
- * const [systemData, userData] = await Promise.all([
91
- * this.getSystemStats(), // Service principal
92
- * this.asUser(req).getUserPreferences(), // User context
93
- * ])
94
- * res.json({ systemData, userData })
95
- * })
96
- * ```
97
90
  */
98
91
  asUser(req) {
99
92
  const token = req.headers["x-forwarded-access-token"];
@@ -107,14 +100,14 @@ var Plugin = class {
107
100
  if (!userId && !isDev) throw AuthenticationError.missingUserId();
108
101
  const effectiveUserId = userId || "dev-user";
109
102
  const userContext = ServiceContext.createUserContext(token, effectiveUserId);
110
- return this.createUserContextProxy(userContext);
103
+ return this._createUserContextProxy(userContext);
111
104
  }
112
105
  /**
113
106
  * Creates a proxy that wraps method calls in a user context.
114
107
  * This allows all plugin methods to automatically use the user's
115
108
  * Databricks credentials.
116
109
  */
117
- createUserContextProxy(userContext) {
110
+ _createUserContextProxy(userContext) {
118
111
  return new Proxy(this, { get: (target, prop, receiver) => {
119
112
  const value = Reflect.get(target, prop, receiver);
120
113
  if (typeof value !== "function") return value;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n getCurrentUserId,\n runInUserContext,\n ServiceContext,\n type UserContext,\n} from \"../context\";\nimport { AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge, validateEnv } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"validateEnv\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"abortActiveOperations\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/** Base abstract class for creating AppKit plugins */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n protected abstract envVars: string[];\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n static phase: PluginPhase = \"normal\";\n name: string;\n\n constructor(protected config: TConfig) {\n this.name = config.name ?? \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n validateEnv() {\n validateEnv(this.envVars);\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n *\n * Returns a scoped instance of this plugin where all method calls\n * will execute with the user's Databricks credentials instead of\n * the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A scoped plugin instance that executes as the user\n * @throws Error if user token is not available in request headers\n *\n * @example\n * ```typescript\n * // In route handler - execute query as the requesting user\n * router.post('/users/me/query/:key', async (req, res) => {\n * const result = await this.asUser(req).query(req.params.key)\n * res.json(result)\n * })\n *\n * // Mixed execution in same handler\n * router.post('/dashboard', async (req, res) => {\n * const [systemData, userData] = await Promise.all([\n * this.getSystemStats(), // Service principal\n * this.asUser(req).getUserPreferences(), // User context\n * ])\n * res.json({ systemData, userData })\n * })\n * ```\n */\n asUser(req: express.Request): this {\n const token = req.headers[\"x-forwarded-access-token\"] as string;\n const userId = req.headers[\"x-forwarded-user\"] as string;\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, fall back to service principal\n // since there's no user token available\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Using service principal.\",\n );\n\n return this;\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n );\n\n // Return a proxy that wraps method calls in user context\n return this.createUserContextProxy(userContext);\n }\n\n /**\n * Creates a proxy that wraps method calls in a user context.\n * This allows all plugin methods to automatically use the user's\n * Databricks credentials.\n */\n private createUserContextProxy(userContext: UserContext): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop)) {\n return value;\n }\n\n return (...args: unknown[]) => {\n return runInUserContext(userContext, () => value.apply(target, args));\n };\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n // single sync execution with interceptors\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (_error) {\n // production-safe, don't crash sdk\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n this.registerEndpoint(name, `/api/${this.name}${path}`);\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n getCurrentUserId,\n runInUserContext,\n ServiceContext,\n type UserContext,\n} from \"../context\";\nimport { AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge, validateEnv } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"validateEnv\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"abortActiveOperations\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/** Base abstract class for creating AppKit plugins */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n protected abstract envVars: string[];\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n static phase: PluginPhase = \"normal\";\n name: string;\n\n constructor(protected config: TConfig) {\n this.name = config.name ?? \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n validateEnv() {\n validateEnv(this.envVars);\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws Error if user token is not available in request headers\n */\n asUser(req: express.Request): this {\n const token = req.headers[\"x-forwarded-access-token\"] as string;\n const userId = req.headers[\"x-forwarded-user\"] as string;\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, fall back to service principal\n // since there's no user token available\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Using service principal.\",\n );\n\n return this;\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n );\n\n // Return a proxy that wraps method calls in user context\n return this._createUserContextProxy(userContext);\n }\n\n /**\n * Creates a proxy that wraps method calls in a user context.\n * This allows all plugin methods to automatically use the user's\n * Databricks credentials.\n */\n private _createUserContextProxy(userContext: UserContext): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop)) {\n return value;\n }\n\n return (...args: unknown[]) => {\n return runInUserContext(userContext, () => value.apply(target, args));\n };\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n // single sync execution with interceptors\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (_error) {\n // production-safe, don't crash sdk\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n this.registerEndpoint(name, `/api/${this.name}${path}`);\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
@@ -9,6 +9,8 @@ import { getConfigScript } from "./utils.js";
9
9
  * (Vite dev server, static file server, etc.).
10
10
  */
11
11
  var BaseServer = class {
12
+ app;
13
+ endpoints;
12
14
  constructor(app, endpoints = {}) {
13
15
  this.app = app;
14
16
  this.endpoints = endpoints;
@@ -1 +1 @@
1
- {"version":3,"file":"base-server.js","names":[],"sources":["../../src/server/base-server.ts"],"sourcesContent":["import type express from \"express\";\nimport { type PluginEndpoints, getConfigScript } from \"./utils\";\n\n/**\n * Base server for the AppKit.\n *\n * Abstract base class that provides common functionality for serving\n * frontend applications. Subclasses implement specific serving strategies\n * (Vite dev server, static file server, etc.).\n */\nexport abstract class BaseServer {\n protected app: express.Application;\n protected endpoints: PluginEndpoints;\n\n constructor(app: express.Application, endpoints: PluginEndpoints = {}) {\n this.app = app;\n this.endpoints = endpoints;\n }\n\n abstract setup(): void | Promise<void>;\n\n async close(): Promise<void> {}\n\n protected getConfigScript(): string {\n return getConfigScript(this.endpoints);\n }\n}\n"],"mappings":";;;;;;;;;;AAUA,IAAsB,aAAtB,MAAiC;CAI/B,YAAY,KAA0B,YAA6B,EAAE,EAAE;AACrE,OAAK,MAAM;AACX,OAAK,YAAY;;CAKnB,MAAM,QAAuB;CAE7B,AAAU,kBAA0B;AAClC,SAAO,gBAAgB,KAAK,UAAU"}
1
+ {"version":3,"file":"base-server.js","names":[],"sources":["../../src/server/base-server.ts"],"sourcesContent":["import type express from \"express\";\nimport { type PluginEndpoints, getConfigScript } from \"./utils\";\n\n/**\n * Base server for the AppKit.\n *\n * Abstract base class that provides common functionality for serving\n * frontend applications. Subclasses implement specific serving strategies\n * (Vite dev server, static file server, etc.).\n */\nexport abstract class BaseServer {\n protected app: express.Application;\n protected endpoints: PluginEndpoints;\n\n constructor(app: express.Application, endpoints: PluginEndpoints = {}) {\n this.app = app;\n this.endpoints = endpoints;\n }\n\n abstract setup(): void | Promise<void>;\n\n async close(): Promise<void> {}\n\n protected getConfigScript(): string {\n return getConfigScript(this.endpoints);\n }\n}\n"],"mappings":";;;;;;;;;;AAUA,IAAsB,aAAtB,MAAiC;CAC/B,AAAU;CACV,AAAU;CAEV,YAAY,KAA0B,YAA6B,EAAE,EAAE;AACrE,OAAK,MAAM;AACX,OAAK,YAAY;;CAKnB,MAAM,QAAuB;CAE7B,AAAU,kBAA0B;AAClC,SAAO,gBAAgB,KAAK,UAAU"}
@@ -27,7 +27,7 @@ declare class ServerPlugin extends Plugin {
27
27
  port: number;
28
28
  };
29
29
  name: "server";
30
- envVars: string[];
30
+ protected envVars: string[];
31
31
  private serverApplication;
32
32
  private server;
33
33
  private viteDevServer?;
@@ -93,6 +93,28 @@ declare class ServerPlugin extends Plugin {
93
93
  private static findStaticPath;
94
94
  private logStartupInfo;
95
95
  private _gracefulShutdown;
96
+ /**
97
+ * Returns the public exports for the server plugin.
98
+ * Exposes server management methods.
99
+ */
100
+ exports(): {
101
+ /** Start the server */
102
+ start: () => Promise<express.Application>;
103
+ /** Extend the server with custom routes or middleware */
104
+ extend(fn: (app: express.Application) => void): /*elided*/any;
105
+ /** Get the underlying HTTP server instance */
106
+ getServer: () => Server;
107
+ /** Get the server configuration */
108
+ getConfig: () => {
109
+ [key: string]: unknown;
110
+ port?: number;
111
+ staticPath?: string;
112
+ autoStart?: boolean;
113
+ host?: string;
114
+ name?: string;
115
+ telemetry?: TelemetryOptions;
116
+ };
117
+ };
96
118
  }
97
119
  /**
98
120
  * @internal
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAkCA;;;;;;;;;;;;AAAwC,cAA3B,YAAA,SAAqB,MAAA,CAAM;EA0S3B,OAAA,cAGZ,EAAA;IAAA,SAAA,EAAA,OAAA;IAHkB,IAAA,EAAA,MAAA;IAAA,IAAA,EAAA,MAAA;;EAAA,IAAA,EAAA,QAAA;;;;;;oBA7RS;;gBAEZ;sBAEM;;WAaT;;;;;;;;;gBAAA;;;;;;;;;;;;WA0BI,QAAQ,OAAA,CAAQ;;;;;;;;;eA8ClB;;;;;;;;mBAmBI,OAAA,CAAQ;;;;;;;;;;;;;;;;;;;;;;cAiLd,QAAM,gBAAA,cAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAkCA;;;;;;;;;;;;AAwDiB,cAxDJ,YAAA,SAAqB,MAAA,CAwDjB;SAsPM,cAAQ,EAAA;IAxMhB,SAAA,EAAA,OAAA;IAAU,IAAA,EAAA,MAAA;IAtGS,IAAA,EAAA,MAAA;EAAM,CAAA;EA+T3B,IAAA,EAAA,QAGZ;EAAA,UAAA,OAAA,EAAA,MAAA,EAAA;UAHkB,iBAAA;UAAA,MAAA;UAAA,aAAA;EAAA,QAAA,sBAAA;oBAlTS;;gBAEZ;sBAEM;;WAaT;;;;;;;;;gBAAA;;;;;;;;;;;;WA0BI,QAAQ,OAAA,CAAQ;;;;;;;;;eA8ClB;;;;;;;;mBAmBI,OAAA,CAAQ;;;;;;;;;;;;;;;;;;;;;;;;iBAjEV,QAAQ,OAAA,CAAQ;;qBAsPV,OAAA,CAAQ;;qBAxMhB;;;;;;;;;kBAAU;;;;;;;cAyNZ,QAAM,gBAAA,cAAA"}
@@ -34,21 +34,21 @@ const logger = createLogger("server");
34
34
  *
35
35
  */
36
36
  var ServerPlugin = class ServerPlugin extends Plugin {
37
- static {
38
- this.DEFAULT_CONFIG = {
39
- autoStart: true,
40
- host: process.env.FLASK_RUN_HOST || "0.0.0.0",
41
- port: Number(process.env.DATABRICKS_APP_PORT) || 8e3
42
- };
43
- }
44
- static {
45
- this.phase = "deferred";
46
- }
37
+ static DEFAULT_CONFIG = {
38
+ autoStart: true,
39
+ host: process.env.FLASK_RUN_HOST || "0.0.0.0",
40
+ port: Number(process.env.DATABRICKS_APP_PORT) || 8e3
41
+ };
42
+ name = "server";
43
+ envVars = [];
44
+ serverApplication;
45
+ server;
46
+ viteDevServer;
47
+ remoteTunnelController;
48
+ serverExtensions = [];
49
+ static phase = "deferred";
47
50
  constructor(config) {
48
51
  super(config);
49
- this.name = "server";
50
- this.envVars = [];
51
- this.serverExtensions = [];
52
52
  this.config = config;
53
53
  this.serverApplication = express();
54
54
  this.server = null;
@@ -216,6 +216,22 @@ var ServerPlugin = class ServerPlugin extends Plugin {
216
216
  }, 15e3);
217
217
  } else process.exit(0);
218
218
  }
219
+ /**
220
+ * Returns the public exports for the server plugin.
221
+ * Exposes server management methods.
222
+ */
223
+ exports() {
224
+ const self = this;
225
+ return {
226
+ start: this.start,
227
+ extend(fn) {
228
+ self.extend(fn);
229
+ return this;
230
+ },
231
+ getServer: this.getServer,
232
+ getConfig: this.getConfig
233
+ };
234
+ }
219
235
  };
220
236
  const EXCLUDED_PLUGINS = [ServerPlugin.name];
221
237
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport type { PluginPhase } from \"shared\";\nimport { ServerError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { Plugin, toPlugin } from \"../plugin\";\nimport { instrumentations } from \"../telemetry\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\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 * @example\n * ```ts\n * createApp({\n * plugins: [server(), telemetryExamples(), analytics({})],\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n autoStart: true,\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n public name = \"server\" as const;\n public envVars: string[] = [];\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n }\n\n /** Setup the server plugin. */\n async setup() {\n if (this.shouldAutoStart()) {\n await this.start();\n }\n }\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /** Check if the server should auto start. */\n shouldAutoStart() {\n return this.config.autoStart;\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(express.json());\n\n const endpoints = 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);\n\n const server = this.serverApplication.listen(\n this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port,\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 process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n console.dir(allRoutes, { depth: null });\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 is not started or autoStart is true.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"get server\");\n }\n\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 * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n * @throws {Error} If autoStart is true.\n */\n extend(fn: (app: express.Application) => void) {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"extend server\");\n }\n\n this.serverExtensions.push(fn);\n return this;\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 */\n private async extendRoutes(): Promise<PluginEndpoints> {\n const endpoints: PluginEndpoints = {};\n\n if (!this.config.plugins) return endpoints;\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 Object.values(this.config.plugins)) {\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/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n // Collect named endpoints from the plugin\n endpoints[plugin.name] = plugin.getEndpoints();\n }\n }\n\n return endpoints;\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(endpoints: PluginEndpoints) {\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 );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints);\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 );\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 private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port = this.config.port ?? 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 private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n // 1. abort active operations from plugins\n if (this.config.plugins) {\n for (const plugin of Object.values(this.config.plugins)) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n}\n\nconst EXCLUDED_PLUGINS = [ServerPlugin.name];\n\n/**\n * @internal\n */\nexport const server = toPlugin<typeof ServerPlugin, ServerConfig, \"server\">(\n ServerPlugin,\n \"server\",\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport type { PluginPhase } from \"shared\";\nimport { ServerError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { Plugin, toPlugin } from \"../plugin\";\nimport { instrumentations } from \"../telemetry\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\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 * @example\n * ```ts\n * createApp({\n * plugins: [server(), telemetryExamples(), analytics({})],\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n autoStart: true,\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n public name = \"server\" as const;\n protected envVars: string[] = [];\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n }\n\n /** Setup the server plugin. */\n async setup() {\n if (this.shouldAutoStart()) {\n await this.start();\n }\n }\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /** Check if the server should auto start. */\n shouldAutoStart() {\n return this.config.autoStart;\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(express.json());\n\n const endpoints = 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);\n\n const server = this.serverApplication.listen(\n this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port,\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 process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n console.dir(allRoutes, { depth: null });\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 is not started or autoStart is true.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"get server\");\n }\n\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 * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n * @throws {Error} If autoStart is true.\n */\n extend(fn: (app: express.Application) => void) {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"extend server\");\n }\n\n this.serverExtensions.push(fn);\n return this;\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 */\n private async extendRoutes(): Promise<PluginEndpoints> {\n const endpoints: PluginEndpoints = {};\n\n if (!this.config.plugins) return endpoints;\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 Object.values(this.config.plugins)) {\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/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n // Collect named endpoints from the plugin\n endpoints[plugin.name] = plugin.getEndpoints();\n }\n }\n\n return endpoints;\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(endpoints: PluginEndpoints) {\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 );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints);\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 );\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 private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port = this.config.port ?? 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 private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n // 1. abort active operations from plugins\n if (this.config.plugins) {\n for (const plugin of Object.values(this.config.plugins)) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\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 /** Start the server */\n start: this.start,\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 };\n }\n}\n\nconst EXCLUDED_PLUGINS = [ServerPlugin.name];\n\n/**\n * @internal\n */\nexport const server = toPlugin<typeof ServerPlugin, ServerConfig, \"server\">(\n ServerPlugin,\n \"server\",\n);\n"],"mappings":"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"}
@@ -12,22 +12,12 @@ const logger = createLogger("server:remote-tunnel:controller");
12
12
  * - Cleans up the remote tunnel
13
13
  */
14
14
  var RemoteTunnelController = class {
15
+ devFileReader;
16
+ server;
17
+ manager;
18
+ initPromise;
19
+ wsReady;
15
20
  constructor(devFileReader) {
16
- this.middleware = async (req, res, next) => {
17
- if (isLocalDev()) return next();
18
- if (!this.isAllowedByEnv()) return next();
19
- const wantsDev = hasDevQuery(req);
20
- const wantsRemoteAssets = isRemoteTunnelAssetRequest(req);
21
- if (!wantsDev && !wantsRemoteAssets) return next();
22
- const manager = await this.getOrInitManager();
23
- if (!manager) return next();
24
- if (wantsDev) return manager.devModeMiddleware()(req, res, next);
25
- try {
26
- await manager.assetMiddleware()(req, res);
27
- } catch (error) {
28
- next(error);
29
- }
30
- };
31
21
  this.devFileReader = devFileReader;
32
22
  this.manager = null;
33
23
  this.initPromise = null;
@@ -49,6 +39,31 @@ var RemoteTunnelController = class {
49
39
  isAllowedByEnv() {
50
40
  return isRemoteTunnelAllowedByEnv();
51
41
  }
42
+ /**
43
+ * Middleware for the remote tunnel
44
+ * - Hard blocks in local dev
45
+ * - Blocks when not allowed by env
46
+ * - Handles dev query and asset requests
47
+ * @param req - the request
48
+ * @param res - the response
49
+ * @param next - the next function
50
+ * @returns the next function
51
+ */
52
+ middleware = async (req, res, next) => {
53
+ if (isLocalDev()) return next();
54
+ if (!this.isAllowedByEnv()) return next();
55
+ const wantsDev = hasDevQuery(req);
56
+ const wantsRemoteAssets = isRemoteTunnelAssetRequest(req);
57
+ if (!wantsDev && !wantsRemoteAssets) return next();
58
+ const manager = await this.getOrInitManager();
59
+ if (!manager) return next();
60
+ if (wantsDev) return manager.devModeMiddleware()(req, res, next);
61
+ try {
62
+ await manager.assetMiddleware()(req, res);
63
+ } catch (error) {
64
+ next(error);
65
+ }
66
+ };
52
67
  /** Cleanup the remote tunnel */
53
68
  cleanup() {
54
69
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-tunnel-controller.js","names":[],"sources":["../../../src/server/remote-tunnel/remote-tunnel-controller.ts"],"sourcesContent":["import type { Server as HTTPServer } from \"node:http\";\nimport type express from \"express\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { DevFileReader } from \"../../plugin/dev-reader\";\nimport {\n hasDevQuery,\n isLocalDev,\n isRemoteTunnelAllowedByEnv,\n isRemoteTunnelAssetRequest,\n} from \"./gate\";\nimport type { RemoteTunnelManager } from \"./remote-tunnel-manager\";\n\nconst logger = createLogger(\"server:remote-tunnel:controller\");\n\n/**\n * Controller for the remote tunnel\n *\n * - Reads files from the dev file reader\n * - Manages the remote tunnel manager\n * - Sets up the web socket\n * - Cleans up the remote tunnel\n */\nexport class RemoteTunnelController {\n private devFileReader: DevFileReader;\n private server?: HTTPServer;\n private manager: RemoteTunnelManager | null;\n private initPromise: Promise<RemoteTunnelManager | null> | null;\n private wsReady: boolean;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n\n /**\n * Set the server instance\n * @param server\n */\n setServer(server: HTTPServer) {\n this.server = server;\n this.maybeSetupWebSocket();\n }\n\n /** Check if the remote tunnel is active */\n isActive(): boolean {\n return this.manager != null;\n }\n\n /** Check if the remote tunnel is allowed by the environment */\n isAllowedByEnv(): boolean {\n return isRemoteTunnelAllowedByEnv();\n }\n\n /**\n * Middleware for the remote tunnel\n * - Hard blocks in local dev\n * - Blocks when not allowed by env\n * - Handles dev query and asset requests\n * @param req - the request\n * @param res - the response\n * @param next - the next function\n * @returns the next function\n */\n middleware: express.RequestHandler = async (req, res, next) => {\n // hard blocker in local dev\n if (isLocalDev()) return next();\n\n // if not allowed by env, block\n if (!this.isAllowedByEnv()) return next();\n\n const wantsDev = hasDevQuery(req);\n const wantsRemoteAssets = isRemoteTunnelAssetRequest(req);\n\n // if not wants dev and not wants remote assets, skip\n if (!wantsDev && !wantsRemoteAssets) return next();\n\n const manager = await this.getOrInitManager();\n // if no manager, skip\n if (!manager) return next();\n\n // dev query present, let manager handle it\n if (wantsDev) {\n return manager.devModeMiddleware()(req, res, next);\n }\n\n // otherwise, handle vite asset fetch paths through the tunnel\n try {\n await manager.assetMiddleware()(req, res);\n } catch (error) {\n next(error);\n }\n };\n\n /** Cleanup the remote tunnel */\n cleanup() {\n try {\n this.manager?.cleanup();\n } finally {\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n }\n\n /**\n * Get or initialize the remote tunnel manager\n * - If the manager is already initialized, return it\n * - If the manager is not initialized, initialize it\n * - If the manager is not allowed by the environment, return null\n * @returns the remote tunnel manager\n */\n private async getOrInitManager(): Promise<RemoteTunnelManager | null> {\n if (this.manager) return this.manager;\n if (this.initPromise) return await this.initPromise;\n\n this.initPromise = (async () => {\n // double check gate\n if (isLocalDev() || !isRemoteTunnelAllowedByEnv()) return null;\n const mod = await import(\"./remote-tunnel-manager\");\n const remoteTunnelManager = new mod.RemoteTunnelManager(\n this.devFileReader,\n );\n this.manager = remoteTunnelManager;\n\n // attach server + ws setup\n this.maybeSetupWebSocket();\n\n logger.debug(\"RemoteTunnel: initialized (on-demand)\");\n return remoteTunnelManager;\n })();\n\n return this.initPromise;\n }\n\n /**\n * Setup the web socket\n * - If the server is not set, return\n * - If the manager is not set, return\n * - If the web socket is already setup, return\n * - Setup the web socket\n */\n private maybeSetupWebSocket() {\n if (!this.server) return;\n if (!this.manager) return;\n if (this.wsReady) return;\n\n this.manager.setServer(this.server);\n this.manager.setupWebSocket();\n this.wsReady = true;\n\n logger.debug(\"RemoteTunnel: web socket setup complete\");\n }\n}\n"],"mappings":";;;;AAYA,MAAM,SAAS,aAAa,kCAAkC;;;;;;;;;AAU9D,IAAa,yBAAb,MAAoC;CAOlC,YAAY,eAA8B;oBAoCL,OAAO,KAAK,KAAK,SAAS;AAE7D,OAAI,YAAY,CAAE,QAAO,MAAM;AAG/B,OAAI,CAAC,KAAK,gBAAgB,CAAE,QAAO,MAAM;GAEzC,MAAM,WAAW,YAAY,IAAI;GACjC,MAAM,oBAAoB,2BAA2B,IAAI;AAGzD,OAAI,CAAC,YAAY,CAAC,kBAAmB,QAAO,MAAM;GAElD,MAAM,UAAU,MAAM,KAAK,kBAAkB;AAE7C,OAAI,CAAC,QAAS,QAAO,MAAM;AAG3B,OAAI,SACF,QAAO,QAAQ,mBAAmB,CAAC,KAAK,KAAK,KAAK;AAIpD,OAAI;AACF,UAAM,QAAQ,iBAAiB,CAAC,KAAK,IAAI;YAClC,OAAO;AACd,SAAK,MAAM;;;AA7Db,OAAK,gBAAgB;AACrB,OAAK,UAAU;AACf,OAAK,cAAc;AACnB,OAAK,UAAU;;;;;;CAOjB,UAAU,QAAoB;AAC5B,OAAK,SAAS;AACd,OAAK,qBAAqB;;;CAI5B,WAAoB;AAClB,SAAO,KAAK,WAAW;;;CAIzB,iBAA0B;AACxB,SAAO,4BAA4B;;;CA4CrC,UAAU;AACR,MAAI;AACF,QAAK,SAAS,SAAS;YACf;AACR,QAAK,UAAU;AACf,QAAK,cAAc;AACnB,QAAK,UAAU;;;;;;;;;;CAWnB,MAAc,mBAAwD;AACpE,MAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,MAAI,KAAK,YAAa,QAAO,MAAM,KAAK;AAExC,OAAK,eAAe,YAAY;AAE9B,OAAI,YAAY,IAAI,CAAC,4BAA4B,CAAE,QAAO;GAE1D,MAAM,sBAAsB,KADhB,OAAM,OAAO,gCACW,oBAClC,KAAK,cACN;AACD,QAAK,UAAU;AAGf,QAAK,qBAAqB;AAE1B,UAAO,MAAM,wCAAwC;AACrD,UAAO;MACL;AAEJ,SAAO,KAAK;;;;;;;;;CAUd,AAAQ,sBAAsB;AAC5B,MAAI,CAAC,KAAK,OAAQ;AAClB,MAAI,CAAC,KAAK,QAAS;AACnB,MAAI,KAAK,QAAS;AAElB,OAAK,QAAQ,UAAU,KAAK,OAAO;AACnC,OAAK,QAAQ,gBAAgB;AAC7B,OAAK,UAAU;AAEf,SAAO,MAAM,0CAA0C"}
1
+ {"version":3,"file":"remote-tunnel-controller.js","names":[],"sources":["../../../src/server/remote-tunnel/remote-tunnel-controller.ts"],"sourcesContent":["import type { Server as HTTPServer } from \"node:http\";\nimport type express from \"express\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { DevFileReader } from \"../../plugin/dev-reader\";\nimport {\n hasDevQuery,\n isLocalDev,\n isRemoteTunnelAllowedByEnv,\n isRemoteTunnelAssetRequest,\n} from \"./gate\";\nimport type { RemoteTunnelManager } from \"./remote-tunnel-manager\";\n\nconst logger = createLogger(\"server:remote-tunnel:controller\");\n\n/**\n * Controller for the remote tunnel\n *\n * - Reads files from the dev file reader\n * - Manages the remote tunnel manager\n * - Sets up the web socket\n * - Cleans up the remote tunnel\n */\nexport class RemoteTunnelController {\n private devFileReader: DevFileReader;\n private server?: HTTPServer;\n private manager: RemoteTunnelManager | null;\n private initPromise: Promise<RemoteTunnelManager | null> | null;\n private wsReady: boolean;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n\n /**\n * Set the server instance\n * @param server\n */\n setServer(server: HTTPServer) {\n this.server = server;\n this.maybeSetupWebSocket();\n }\n\n /** Check if the remote tunnel is active */\n isActive(): boolean {\n return this.manager != null;\n }\n\n /** Check if the remote tunnel is allowed by the environment */\n isAllowedByEnv(): boolean {\n return isRemoteTunnelAllowedByEnv();\n }\n\n /**\n * Middleware for the remote tunnel\n * - Hard blocks in local dev\n * - Blocks when not allowed by env\n * - Handles dev query and asset requests\n * @param req - the request\n * @param res - the response\n * @param next - the next function\n * @returns the next function\n */\n middleware: express.RequestHandler = async (req, res, next) => {\n // hard blocker in local dev\n if (isLocalDev()) return next();\n\n // if not allowed by env, block\n if (!this.isAllowedByEnv()) return next();\n\n const wantsDev = hasDevQuery(req);\n const wantsRemoteAssets = isRemoteTunnelAssetRequest(req);\n\n // if not wants dev and not wants remote assets, skip\n if (!wantsDev && !wantsRemoteAssets) return next();\n\n const manager = await this.getOrInitManager();\n // if no manager, skip\n if (!manager) return next();\n\n // dev query present, let manager handle it\n if (wantsDev) {\n return manager.devModeMiddleware()(req, res, next);\n }\n\n // otherwise, handle vite asset fetch paths through the tunnel\n try {\n await manager.assetMiddleware()(req, res);\n } catch (error) {\n next(error);\n }\n };\n\n /** Cleanup the remote tunnel */\n cleanup() {\n try {\n this.manager?.cleanup();\n } finally {\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n }\n\n /**\n * Get or initialize the remote tunnel manager\n * - If the manager is already initialized, return it\n * - If the manager is not initialized, initialize it\n * - If the manager is not allowed by the environment, return null\n * @returns the remote tunnel manager\n */\n private async getOrInitManager(): Promise<RemoteTunnelManager | null> {\n if (this.manager) return this.manager;\n if (this.initPromise) return await this.initPromise;\n\n this.initPromise = (async () => {\n // double check gate\n if (isLocalDev() || !isRemoteTunnelAllowedByEnv()) return null;\n const mod = await import(\"./remote-tunnel-manager\");\n const remoteTunnelManager = new mod.RemoteTunnelManager(\n this.devFileReader,\n );\n this.manager = remoteTunnelManager;\n\n // attach server + ws setup\n this.maybeSetupWebSocket();\n\n logger.debug(\"RemoteTunnel: initialized (on-demand)\");\n return remoteTunnelManager;\n })();\n\n return this.initPromise;\n }\n\n /**\n * Setup the web socket\n * - If the server is not set, return\n * - If the manager is not set, return\n * - If the web socket is already setup, return\n * - Setup the web socket\n */\n private maybeSetupWebSocket() {\n if (!this.server) return;\n if (!this.manager) return;\n if (this.wsReady) return;\n\n this.manager.setServer(this.server);\n this.manager.setupWebSocket();\n this.wsReady = true;\n\n logger.debug(\"RemoteTunnel: web socket setup complete\");\n }\n}\n"],"mappings":";;;;AAYA,MAAM,SAAS,aAAa,kCAAkC;;;;;;;;;AAU9D,IAAa,yBAAb,MAAoC;CAClC,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,YAAY,eAA8B;AACxC,OAAK,gBAAgB;AACrB,OAAK,UAAU;AACf,OAAK,cAAc;AACnB,OAAK,UAAU;;;;;;CAOjB,UAAU,QAAoB;AAC5B,OAAK,SAAS;AACd,OAAK,qBAAqB;;;CAI5B,WAAoB;AAClB,SAAO,KAAK,WAAW;;;CAIzB,iBAA0B;AACxB,SAAO,4BAA4B;;;;;;;;;;;;CAarC,aAAqC,OAAO,KAAK,KAAK,SAAS;AAE7D,MAAI,YAAY,CAAE,QAAO,MAAM;AAG/B,MAAI,CAAC,KAAK,gBAAgB,CAAE,QAAO,MAAM;EAEzC,MAAM,WAAW,YAAY,IAAI;EACjC,MAAM,oBAAoB,2BAA2B,IAAI;AAGzD,MAAI,CAAC,YAAY,CAAC,kBAAmB,QAAO,MAAM;EAElD,MAAM,UAAU,MAAM,KAAK,kBAAkB;AAE7C,MAAI,CAAC,QAAS,QAAO,MAAM;AAG3B,MAAI,SACF,QAAO,QAAQ,mBAAmB,CAAC,KAAK,KAAK,KAAK;AAIpD,MAAI;AACF,SAAM,QAAQ,iBAAiB,CAAC,KAAK,IAAI;WAClC,OAAO;AACd,QAAK,MAAM;;;;CAKf,UAAU;AACR,MAAI;AACF,QAAK,SAAS,SAAS;YACf;AACR,QAAK,UAAU;AACf,QAAK,cAAc;AACnB,QAAK,UAAU;;;;;;;;;;CAWnB,MAAc,mBAAwD;AACpE,MAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,MAAI,KAAK,YAAa,QAAO,MAAM,KAAK;AAExC,OAAK,eAAe,YAAY;AAE9B,OAAI,YAAY,IAAI,CAAC,4BAA4B,CAAE,QAAO;GAE1D,MAAM,sBAAsB,KADhB,OAAM,OAAO,gCACW,oBAClC,KAAK,cACN;AACD,QAAK,UAAU;AAGf,QAAK,qBAAqB;AAE1B,UAAO,MAAM,wCAAwC;AACrD,UAAO;MACL;AAEJ,SAAO,KAAK;;;;;;;;;CAUd,AAAQ,sBAAsB;AAC5B,MAAI,CAAC,KAAK,OAAQ;AAClB,MAAI,CAAC,KAAK,QAAS;AACnB,MAAI,KAAK,QAAS;AAElB,OAAK,QAAQ,UAAU,KAAK,OAAO;AACnC,OAAK,QAAQ,gBAAgB;AAC7B,OAAK,UAAU;AAEf,SAAO,MAAM,0CAA0C"}
@@ -13,6 +13,13 @@ const __dirname = path$1.dirname(__filename);
13
13
  const MAX_ASSET_FETCH_TIMEOUT = 6e4;
14
14
  const logger = createLogger("server:remote-tunnel");
15
15
  /**
16
+ * Type guard to validate WebSocket message structure
17
+ */
18
+ function isWebSocketMessage(data) {
19
+ if (!data || typeof data !== "object") return false;
20
+ return typeof data.type === "string";
21
+ }
22
+ /**
16
23
  * Remote tunnel manager for the AppKit.
17
24
  *
18
25
  * This class is responsible for managing the remote tunnels for the development server.
@@ -25,8 +32,12 @@ const logger = createLogger("server:remote-tunnel");
25
32
  * ```
26
33
  */
27
34
  var RemoteTunnelManager = class {
35
+ tunnels = /* @__PURE__ */ new Map();
36
+ wss;
37
+ hmrWss;
38
+ server;
39
+ devFileReader;
28
40
  constructor(devFileReader) {
29
- this.tunnels = /* @__PURE__ */ new Map();
30
41
  this.devFileReader = devFileReader;
31
42
  this.wss = new WebSocketServer({
32
43
  noServer: true,
@@ -189,6 +200,10 @@ var RemoteTunnelManager = class {
189
200
  }
190
201
  try {
191
202
  const data = JSON.parse(msg.toString());
203
+ if (!isWebSocketMessage(data)) {
204
+ logger.error("Invalid WebSocket message format: %O", data);
205
+ return;
206
+ }
192
207
  if (data.type === "connection:response") {
193
208
  if (tunnel && data.viewer) {
194
209
  tunnel.pendingRequests.delete(data.viewer);
@@ -223,7 +238,17 @@ var RemoteTunnelManager = class {
223
238
  clearTimeout(pending.timeout);
224
239
  tunnel.pendingFileReads.delete(data.requestId);
225
240
  if (data.error) pending.reject(new Error(data.error));
226
- else pending.resolve(data.content);
241
+ else if (data.content !== void 0) pending.resolve(data.content);
242
+ else pending.reject(/* @__PURE__ */ new Error("Missing content in file:read:response"));
243
+ }
244
+ } else if (data.type === "dir:list:response") {
245
+ const pending = tunnel.pendingFileReads.get(data.requestId);
246
+ if (pending) {
247
+ clearTimeout(pending.timeout);
248
+ tunnel.pendingFileReads.delete(data.requestId);
249
+ if (data.error) pending.reject(new Error(data.error));
250
+ else if (data.content !== void 0) pending.resolve(data.content);
251
+ else pending.reject(/* @__PURE__ */ new Error("Missing content in dir:list:response"));
227
252
  }
228
253
  }
229
254
  } catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-tunnel-manager.js","names":["path"],"sources":["../../../src/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\";\nimport type express from \"express\";\nimport type { TunnelConnection } from \"shared\";\nimport { WebSocketServer } from \"ws\";\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 * 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: false,\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 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 {\n pending.resolve(data.content);\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":["path"],"sources":["../../../src/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\";\nimport type express from \"express\";\nimport type { TunnelConnection } from \"shared\";\nimport { WebSocketServer } from \"ws\";\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: false,\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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"}
@@ -17,6 +17,7 @@ import express from "express";
17
17
  * ```
18
18
  */
19
19
  var StaticServer = class extends BaseServer {
20
+ staticPath;
20
21
  constructor(app, staticPath, endpoints = {}) {
21
22
  super(app, endpoints);
22
23
  this.staticPath = staticPath;