@databricks/appkit 0.5.0 → 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 (117) hide show
  1. package/dist/app/index.d.ts +11 -2
  2. package/dist/app/index.d.ts.map +1 -1
  3. package/dist/app/index.js +51 -37
  4. package/dist/app/index.js.map +1 -1
  5. package/dist/appkit/package.js +1 -1
  6. package/dist/plugin/dev-reader.d.ts +1 -0
  7. package/dist/plugin/dev-reader.d.ts.map +1 -1
  8. package/dist/plugin/dev-reader.js +38 -0
  9. package/dist/plugin/dev-reader.js.map +1 -1
  10. package/dist/server/remote-tunnel/remote-tunnel-manager.js +22 -1
  11. package/dist/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  12. package/docs/docs/api/appkit/Class.AppKitError/index.html +2 -2
  13. package/docs/docs/api/appkit/Class.AuthenticationError/index.html +2 -2
  14. package/docs/docs/api/appkit/Class.ConfigurationError/index.html +2 -2
  15. package/docs/docs/api/appkit/Class.ConnectionError/index.html +2 -2
  16. package/docs/docs/api/appkit/Class.ExecutionError/index.html +2 -2
  17. package/docs/docs/api/appkit/Class.InitializationError/index.html +2 -2
  18. package/docs/docs/api/appkit/Class.Plugin/index.html +2 -2
  19. package/docs/docs/api/appkit/Class.ServerError/index.html +2 -2
  20. package/docs/docs/api/appkit/Class.TunnelError/index.html +2 -2
  21. package/docs/docs/api/appkit/Class.ValidationError/index.html +2 -2
  22. package/docs/docs/api/appkit/Function.appKitTypesPlugin/index.html +2 -2
  23. package/docs/docs/api/appkit/Function.createApp/index.html +2 -2
  24. package/docs/docs/api/appkit/Function.getExecutionContext/index.html +2 -2
  25. package/docs/docs/api/appkit/Function.isSQLTypeMarker/index.html +2 -2
  26. package/docs/docs/api/appkit/Interface.BasePluginConfig/index.html +2 -2
  27. package/docs/docs/api/appkit/Interface.CacheConfig/index.html +2 -2
  28. package/docs/docs/api/appkit/Interface.ITelemetry/index.html +2 -2
  29. package/docs/docs/api/appkit/Interface.StreamExecutionSettings/index.html +2 -2
  30. package/docs/docs/api/appkit/Interface.TelemetryConfig/index.html +2 -2
  31. package/docs/docs/api/appkit/TypeAlias.IAppRouter/index.html +2 -2
  32. package/docs/docs/api/appkit/Variable.sql/index.html +2 -2
  33. package/docs/docs/api/appkit/index.html +2 -2
  34. package/docs/docs/api/appkit-ui/data/AreaChart/index.html +2 -2
  35. package/docs/docs/api/appkit-ui/data/BarChart/index.html +2 -2
  36. package/docs/docs/api/appkit-ui/data/DataTable/index.html +2 -2
  37. package/docs/docs/api/appkit-ui/data/DonutChart/index.html +2 -2
  38. package/docs/docs/api/appkit-ui/data/HeatmapChart/index.html +2 -2
  39. package/docs/docs/api/appkit-ui/data/LineChart/index.html +2 -2
  40. package/docs/docs/api/appkit-ui/data/PieChart/index.html +2 -2
  41. package/docs/docs/api/appkit-ui/data/RadarChart/index.html +2 -2
  42. package/docs/docs/api/appkit-ui/data/ScatterChart/index.html +2 -2
  43. package/docs/docs/api/appkit-ui/index.html +2 -2
  44. package/docs/docs/api/appkit-ui/styling/index.html +2 -2
  45. package/docs/docs/api/appkit-ui/ui/Accordion/index.html +2 -2
  46. package/docs/docs/api/appkit-ui/ui/Alert/index.html +2 -2
  47. package/docs/docs/api/appkit-ui/ui/AlertDialog/index.html +2 -2
  48. package/docs/docs/api/appkit-ui/ui/AspectRatio/index.html +2 -2
  49. package/docs/docs/api/appkit-ui/ui/Avatar/index.html +2 -2
  50. package/docs/docs/api/appkit-ui/ui/Badge/index.html +2 -2
  51. package/docs/docs/api/appkit-ui/ui/Breadcrumb/index.html +2 -2
  52. package/docs/docs/api/appkit-ui/ui/Button/index.html +2 -2
  53. package/docs/docs/api/appkit-ui/ui/ButtonGroup/index.html +2 -2
  54. package/docs/docs/api/appkit-ui/ui/Calendar/index.html +2 -2
  55. package/docs/docs/api/appkit-ui/ui/Card/index.html +2 -2
  56. package/docs/docs/api/appkit-ui/ui/Carousel/index.html +2 -2
  57. package/docs/docs/api/appkit-ui/ui/ChartContainer/index.html +2 -2
  58. package/docs/docs/api/appkit-ui/ui/Checkbox/index.html +2 -2
  59. package/docs/docs/api/appkit-ui/ui/Collapsible/index.html +2 -2
  60. package/docs/docs/api/appkit-ui/ui/Command/index.html +2 -2
  61. package/docs/docs/api/appkit-ui/ui/ContextMenu/index.html +2 -2
  62. package/docs/docs/api/appkit-ui/ui/Dialog/index.html +2 -2
  63. package/docs/docs/api/appkit-ui/ui/Drawer/index.html +2 -2
  64. package/docs/docs/api/appkit-ui/ui/DropdownMenu/index.html +2 -2
  65. package/docs/docs/api/appkit-ui/ui/Empty/index.html +2 -2
  66. package/docs/docs/api/appkit-ui/ui/Field/index.html +2 -2
  67. package/docs/docs/api/appkit-ui/ui/FormControl/index.html +2 -2
  68. package/docs/docs/api/appkit-ui/ui/HoverCard/index.html +2 -2
  69. package/docs/docs/api/appkit-ui/ui/Input/index.html +2 -2
  70. package/docs/docs/api/appkit-ui/ui/InputGroup/index.html +2 -2
  71. package/docs/docs/api/appkit-ui/ui/InputOTP/index.html +2 -2
  72. package/docs/docs/api/appkit-ui/ui/Item/index.html +2 -2
  73. package/docs/docs/api/appkit-ui/ui/Kbd/index.html +2 -2
  74. package/docs/docs/api/appkit-ui/ui/Label/index.html +2 -2
  75. package/docs/docs/api/appkit-ui/ui/Menubar/index.html +2 -2
  76. package/docs/docs/api/appkit-ui/ui/NavigationMenu/index.html +2 -2
  77. package/docs/docs/api/appkit-ui/ui/Pagination/index.html +2 -2
  78. package/docs/docs/api/appkit-ui/ui/Popover/index.html +2 -2
  79. package/docs/docs/api/appkit-ui/ui/Progress/index.html +2 -2
  80. package/docs/docs/api/appkit-ui/ui/RadioGroup/index.html +2 -2
  81. package/docs/docs/api/appkit-ui/ui/ResizableHandle/index.html +2 -2
  82. package/docs/docs/api/appkit-ui/ui/ScrollArea/index.html +2 -2
  83. package/docs/docs/api/appkit-ui/ui/Select/index.html +2 -2
  84. package/docs/docs/api/appkit-ui/ui/Separator/index.html +2 -2
  85. package/docs/docs/api/appkit-ui/ui/Sheet/index.html +2 -2
  86. package/docs/docs/api/appkit-ui/ui/Sidebar/index.html +2 -2
  87. package/docs/docs/api/appkit-ui/ui/Skeleton/index.html +2 -2
  88. package/docs/docs/api/appkit-ui/ui/Slider/index.html +2 -2
  89. package/docs/docs/api/appkit-ui/ui/Spinner/index.html +2 -2
  90. package/docs/docs/api/appkit-ui/ui/Switch/index.html +2 -2
  91. package/docs/docs/api/appkit-ui/ui/Table/index.html +2 -2
  92. package/docs/docs/api/appkit-ui/ui/Tabs/index.html +2 -2
  93. package/docs/docs/api/appkit-ui/ui/Textarea/index.html +2 -2
  94. package/docs/docs/api/appkit-ui/ui/Toaster/index.html +2 -2
  95. package/docs/docs/api/appkit-ui/ui/Toggle/index.html +2 -2
  96. package/docs/docs/api/appkit-ui/ui/ToggleGroup/index.html +2 -2
  97. package/docs/docs/api/appkit-ui/ui/Tooltip/index.html +2 -2
  98. package/docs/docs/api/index.html +2 -2
  99. package/docs/docs/app-management/index.html +4 -4
  100. package/docs/docs/app-management.md +2 -2
  101. package/docs/docs/architecture/index.html +2 -2
  102. package/docs/docs/category/development/index.html +2 -2
  103. package/docs/docs/configuration/index.html +2 -2
  104. package/docs/docs/core-principles/index.html +2 -2
  105. package/docs/docs/development/index.html +4 -4
  106. package/docs/docs/development/llm-guide/index.html +2 -2
  107. package/docs/docs/development/local-development/index.html +4 -4
  108. package/docs/docs/development/local-development.md +2 -2
  109. package/docs/docs/development/project-setup/index.html +2 -2
  110. package/docs/docs/development/remote-bridge/index.html +4 -4
  111. package/docs/docs/development/remote-bridge.md +2 -2
  112. package/docs/docs/development/type-generation/index.html +2 -2
  113. package/docs/docs/development.md +2 -2
  114. package/docs/docs/index.html +4 -4
  115. package/docs/docs/plugins/index.html +2 -2
  116. package/docs/docs.md +2 -2
  117. package/package.json +1 -1
@@ -5,20 +5,29 @@ interface RequestLike {
5
5
  }
6
6
  interface DevFileReader {
7
7
  readFile(filePath: string, req: RequestLike): Promise<string>;
8
+ readdir(dirPath: string, req: RequestLike): Promise<string[]>;
8
9
  }
9
10
  interface QueryResult {
10
11
  query: string;
11
12
  isAsUser: boolean;
12
13
  }
13
14
  declare class AppManager {
15
+ private readonly queriesDir;
16
+ /**
17
+ * Validates that a file path is within the queries directory
18
+ */
19
+ private validatePath;
20
+ /**
21
+ * Creates a filesystem adapter based on dev mode or production mode
22
+ */
23
+ private createFsAdapter;
14
24
  /**
15
25
  * Retrieves a query file by key from the queries directory
16
26
  * In dev mode with a request context, reads from local filesystem via WebSocket
17
27
  * @param queryKey - The query file name (without extension)
18
28
  * @param req - Optional request object to detect dev mode
19
29
  * @param devFileReader - Optional DevFileReader instance to read files from local filesystem
20
- * @returns The query content as a string
21
- * @throws Error if query key is invalid or file not found
30
+ * @returns The query content and execution mode (as user or as service principal)
22
31
  */
23
32
  getAppQuery(queryKey: string, req?: RequestLike, devFileReader?: DevFileReader): Promise<QueryResult | null>;
24
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":[],"mappings":";UAMU,WAAA;EAAA,KAAA,CAAA,EACA,MADW,CAAA,MAAA,EAAA,GAAA,CAAA;EAAA,OAAA,EAEV,MAFU,CAAA,MAAA,EAAA,MAAA,GAAA,MAAA,EAAA,GAAA,SAAA,CAAA;;UAKX,aAAA,CAHC;EAAM,QAAA,CAAA,QAAA,EAAA,MAAA,EAAA,GAAA,EAIiB,WAJjB,CAAA,EAI+B,OAJ/B,CAAA,MAAA,CAAA;AAAA;UAOP,WAAA,CAJa;OACW,EAAA,MAAA;UAAc,EAAA,OAAA;;AAGtC,cAKG,UAAA,CALQ;EAKR;;;;;;;;;sCAYH,6BACU,gBACf,QAAQ"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":[],"mappings":";UAMU,WAAA;EAAA,KAAA,CAAA,EACA,MADW,CAAA,MAAA,EAAA,GAAA,CAAA;EAAA,OAAA,EAEV,MAFU,CAAA,MAAA,EAAA,MAAA,GAAA,MAAA,EAAA,GAAA,SAAA,CAAA;;UAKX,aAAA,CAHC;EAAM,QAAA,CAAA,QAAA,EAAA,MAAA,EAAA,GAAA,EAIiB,WAJjB,CAAA,EAI+B,OAJ/B,CAAA,MAAA,CAAA;EAGP,OAAA,CAAA,OAAA,EAAa,MAAA,EAAA,GAAA,EAES,WAFT,CAAA,EAEuB,OAFvB,CAAA,MAAA,EAAA,CAAA;;UAKb,WAAA,CAJwB;OAAc,EAAA,MAAA;UAChB,EAAA,OAAA;;AAAqB,cAgBxC,UAAA,CAhBwC;EAG3C,iBAAW,UAAA;EAaR;;;UA4DO,YAAA;;;;;;;;;;;;;sCADV,6BACU,gBACf,QAAQ"}
package/dist/app/index.js CHANGED
@@ -5,71 +5,85 @@ import path from "node:path";
5
5
  //#region src/app/index.ts
6
6
  const logger = createLogger("app");
7
7
  var AppManager = class {
8
+ queriesDir = path.resolve(process.cwd(), "config/queries");
9
+ /**
10
+ * Validates that a file path is within the queries directory
11
+ */
12
+ validatePath(fileName) {
13
+ const queryFilePath = path.join(this.queriesDir, fileName);
14
+ const resolvedPath = path.resolve(queryFilePath);
15
+ const resolvedQueriesDir = path.resolve(this.queriesDir);
16
+ if (!resolvedPath.startsWith(resolvedQueriesDir)) {
17
+ logger.error("Invalid query path: path traversal detected");
18
+ return null;
19
+ }
20
+ return resolvedPath;
21
+ }
22
+ /**
23
+ * Creates a filesystem adapter based on dev mode or production mode
24
+ */
25
+ createFsAdapter(req, devFileReader) {
26
+ if (req?.query?.dev !== void 0 && devFileReader && req) return {
27
+ readdir: async (dirPath) => {
28
+ const relativePath = path.relative(process.cwd(), dirPath);
29
+ return devFileReader.readdir(relativePath, req);
30
+ },
31
+ readFile: async (filePath) => {
32
+ const relativePath = path.relative(process.cwd(), filePath);
33
+ return devFileReader.readFile(relativePath, req);
34
+ }
35
+ };
36
+ return {
37
+ readdir: (dirPath) => fs.readdir(dirPath),
38
+ readFile: (filePath) => fs.readFile(filePath, "utf8")
39
+ };
40
+ }
8
41
  /**
9
42
  * Retrieves a query file by key from the queries directory
10
43
  * In dev mode with a request context, reads from local filesystem via WebSocket
11
44
  * @param queryKey - The query file name (without extension)
12
45
  * @param req - Optional request object to detect dev mode
13
46
  * @param devFileReader - Optional DevFileReader instance to read files from local filesystem
14
- * @returns The query content as a string
15
- * @throws Error if query key is invalid or file not found
47
+ * @returns The query content and execution mode (as user or as service principal)
16
48
  */
17
49
  async getAppQuery(queryKey, req, devFileReader) {
18
50
  if (!queryKey || !/^[a-zA-Z0-9_-]+$/.test(queryKey)) {
19
51
  logger.error("Invalid query key format: %s. Only alphanumeric characters, underscores, and hyphens are allowed.", queryKey);
20
52
  return null;
21
53
  }
22
- const queriesDir = path.resolve(process.cwd(), "config/queries");
54
+ const fsAdapter = this.createFsAdapter(req, devFileReader);
23
55
  const oboFileName = `${queryKey}.obo.sql`;
24
56
  const defaultFileName = `${queryKey}.sql`;
25
- let queryFileName = null;
26
- let isAsUser = false;
57
+ let files;
27
58
  try {
28
- const files = await fs.readdir(queriesDir);
29
- if (files.includes(oboFileName)) {
30
- queryFileName = oboFileName;
31
- isAsUser = true;
32
- if (files.includes(defaultFileName)) logger.warn(`Both ${oboFileName} and ${defaultFileName} found for query ${queryKey}. Using ${oboFileName}.`);
33
- } else if (files.includes(defaultFileName)) {
34
- queryFileName = defaultFileName;
35
- isAsUser = false;
36
- }
59
+ files = await fsAdapter.readdir(this.queriesDir);
37
60
  } catch (error) {
38
61
  logger.error(`Failed to read queries directory: ${error.message}`);
39
62
  return null;
40
63
  }
64
+ let queryFileName = null;
65
+ let isAsUser = false;
66
+ if (files.includes(oboFileName)) {
67
+ queryFileName = oboFileName;
68
+ isAsUser = true;
69
+ if (files.includes(defaultFileName)) logger.warn(`Both ${oboFileName} and ${defaultFileName} found for query ${queryKey}. Using ${oboFileName}.`);
70
+ } else if (files.includes(defaultFileName)) {
71
+ queryFileName = defaultFileName;
72
+ isAsUser = false;
73
+ }
41
74
  if (!queryFileName) {
42
75
  logger.error(`Query file not found: ${queryKey}`);
43
76
  return null;
44
77
  }
45
- const queryFilePath = path.join(queriesDir, queryFileName);
46
- const resolvedPath = path.resolve(queryFilePath);
47
- const resolvedQueriesDir = path.resolve(queriesDir);
48
- if (!resolvedPath.startsWith(resolvedQueriesDir)) {
49
- logger.error(`Invalid query path: path traversal detected`);
50
- return null;
51
- }
52
- if (req?.query?.dev !== void 0 && devFileReader && req) try {
53
- const relativePath = path.relative(process.cwd(), resolvedPath);
54
- return {
55
- query: await devFileReader.readFile(relativePath, req),
56
- isAsUser
57
- };
58
- } catch (error) {
59
- logger.error(`Failed to read query from dev tunnel: ${error.message}`);
60
- return null;
61
- }
78
+ const resolvedPath = this.validatePath(queryFileName);
79
+ if (!resolvedPath) return null;
62
80
  try {
63
81
  return {
64
- query: await fs.readFile(resolvedPath, "utf8"),
82
+ query: await fsAdapter.readFile(resolvedPath),
65
83
  isAsUser
66
84
  };
67
85
  } catch (error) {
68
- if (error.code === "ENOENT") {
69
- logger.error(`Failed to read query from server filesystem: ${error.message}`);
70
- return null;
71
- }
72
- logger.error(`Failed to read query from server filesystem: ${error.message}`);
86
+ logger.error(`Failed to read query file: ${error.message}`);
73
87
  return null;
74
88
  }
75
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"app\");\n\ninterface RequestLike {\n query?: Record<string, any>;\n headers: Record<string, string | string[] | undefined>;\n}\n\ninterface DevFileReader {\n readFile(filePath: string, req: RequestLike): Promise<string>;\n}\n\ninterface QueryResult {\n query: string;\n isAsUser: boolean;\n}\n\nexport class AppManager {\n /**\n * Retrieves a query file by key from the queries directory\n * In dev mode with a request context, reads from local filesystem via WebSocket\n * @param queryKey - The query file name (without extension)\n * @param req - Optional request object to detect dev mode\n * @param devFileReader - Optional DevFileReader instance to read files from local filesystem\n * @returns The query content as a string\n * @throws Error if query key is invalid or file not found\n */\n async getAppQuery(\n queryKey: string,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n ): Promise<QueryResult | null> {\n // Security: Sanitize query key to prevent path traversal\n if (!queryKey || !/^[a-zA-Z0-9_-]+$/.test(queryKey)) {\n logger.error(\n \"Invalid query key format: %s. Only alphanumeric characters, underscores, and hyphens are allowed.\",\n queryKey,\n );\n return null;\n }\n\n const queriesDir = path.resolve(process.cwd(), \"config/queries\");\n\n // priority order: .obo.sql first (asUser), then .sql (default)\n const oboFileName = `${queryKey}.obo.sql`;\n const defaultFileName = `${queryKey}.sql`;\n\n let queryFileName: string | null = null;\n let isAsUser: boolean = false;\n\n try {\n const files = await fs.readdir(queriesDir);\n\n // check for OBO query first\n if (files.includes(oboFileName)) {\n queryFileName = oboFileName;\n isAsUser = true;\n\n // check for both files and warn if both are present\n if (files.includes(defaultFileName)) {\n logger.warn(\n `Both ${oboFileName} and ${defaultFileName} found for query ${queryKey}. Using ${oboFileName}.`,\n );\n }\n // check for default query if OBO query is not present\n } else if (files.includes(defaultFileName)) {\n queryFileName = defaultFileName;\n isAsUser = false;\n }\n } catch (error) {\n logger.error(\n `Failed to read queries directory: ${(error as Error).message}`,\n );\n return null;\n }\n\n if (!queryFileName) {\n logger.error(`Query file not found: ${queryKey}`);\n return null;\n }\n\n const queryFilePath = path.join(queriesDir, queryFileName);\n\n // security: validate resolved path is within queries directory\n const resolvedPath = path.resolve(queryFilePath);\n const resolvedQueriesDir = path.resolve(queriesDir);\n\n if (!resolvedPath.startsWith(resolvedQueriesDir)) {\n logger.error(`Invalid query path: path traversal detected`);\n return null;\n }\n\n // check if we're in dev mode and should use WebSocket\n const isDevMode = req?.query?.dev !== undefined;\n if (isDevMode && devFileReader && req) {\n try {\n const relativePath = path.relative(process.cwd(), resolvedPath);\n return {\n query: await devFileReader.readFile(relativePath, req),\n isAsUser,\n };\n } catch (error) {\n logger.error(\n `Failed to read query from dev tunnel: ${(error as Error).message}`,\n );\n return null;\n }\n }\n\n // production mode: read from server filesystem\n try {\n const query = await fs.readFile(resolvedPath, \"utf8\");\n return { query, isAsUser };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n logger.error(\n `Failed to read query from server filesystem: ${(error as Error).message}`,\n );\n return null;\n }\n\n logger.error(\n `Failed to read query from server filesystem: ${(error as Error).message}`,\n );\n return null;\n }\n }\n}\n\nexport type { DevFileReader, QueryResult, RequestLike };\n"],"mappings":";;;;;AAIA,MAAM,SAAS,aAAa,MAAM;AAgBlC,IAAa,aAAb,MAAwB;;;;;;;;;;CAUtB,MAAM,YACJ,UACA,KACA,eAC6B;AAE7B,MAAI,CAAC,YAAY,CAAC,mBAAmB,KAAK,SAAS,EAAE;AACnD,UAAO,MACL,qGACA,SACD;AACD,UAAO;;EAGT,MAAM,aAAa,KAAK,QAAQ,QAAQ,KAAK,EAAE,iBAAiB;EAGhE,MAAM,cAAc,GAAG,SAAS;EAChC,MAAM,kBAAkB,GAAG,SAAS;EAEpC,IAAI,gBAA+B;EACnC,IAAI,WAAoB;AAExB,MAAI;GACF,MAAM,QAAQ,MAAM,GAAG,QAAQ,WAAW;AAG1C,OAAI,MAAM,SAAS,YAAY,EAAE;AAC/B,oBAAgB;AAChB,eAAW;AAGX,QAAI,MAAM,SAAS,gBAAgB,CACjC,QAAO,KACL,QAAQ,YAAY,OAAO,gBAAgB,mBAAmB,SAAS,UAAU,YAAY,GAC9F;cAGM,MAAM,SAAS,gBAAgB,EAAE;AAC1C,oBAAgB;AAChB,eAAW;;WAEN,OAAO;AACd,UAAO,MACL,qCAAsC,MAAgB,UACvD;AACD,UAAO;;AAGT,MAAI,CAAC,eAAe;AAClB,UAAO,MAAM,yBAAyB,WAAW;AACjD,UAAO;;EAGT,MAAM,gBAAgB,KAAK,KAAK,YAAY,cAAc;EAG1D,MAAM,eAAe,KAAK,QAAQ,cAAc;EAChD,MAAM,qBAAqB,KAAK,QAAQ,WAAW;AAEnD,MAAI,CAAC,aAAa,WAAW,mBAAmB,EAAE;AAChD,UAAO,MAAM,8CAA8C;AAC3D,UAAO;;AAKT,MADkB,KAAK,OAAO,QAAQ,UACrB,iBAAiB,IAChC,KAAI;GACF,MAAM,eAAe,KAAK,SAAS,QAAQ,KAAK,EAAE,aAAa;AAC/D,UAAO;IACL,OAAO,MAAM,cAAc,SAAS,cAAc,IAAI;IACtD;IACD;WACM,OAAO;AACd,UAAO,MACL,yCAA0C,MAAgB,UAC3D;AACD,UAAO;;AAKX,MAAI;AAEF,UAAO;IAAE,OADK,MAAM,GAAG,SAAS,cAAc,OAAO;IACrC;IAAU;WACnB,OAAO;AACd,OAAK,MAAgC,SAAS,UAAU;AACtD,WAAO,MACL,gDAAiD,MAAgB,UAClE;AACD,WAAO;;AAGT,UAAO,MACL,gDAAiD,MAAgB,UAClE;AACD,UAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"app\");\n\ninterface RequestLike {\n query?: Record<string, any>;\n headers: Record<string, string | string[] | undefined>;\n}\n\ninterface DevFileReader {\n readFile(filePath: string, req: RequestLike): Promise<string>;\n readdir(dirPath: string, req: RequestLike): Promise<string[]>;\n}\n\ninterface QueryResult {\n query: string;\n isAsUser: boolean;\n}\n\n/**\n * Abstraction for filesystem operations that works in both dev and production modes\n */\ninterface FileSystemAdapter {\n readdir(dirPath: string): Promise<string[]>;\n readFile(filePath: string): Promise<string>;\n}\n\nexport class AppManager {\n private readonly queriesDir = path.resolve(process.cwd(), \"config/queries\");\n\n /**\n * Validates that a file path is within the queries directory\n */\n private validatePath(fileName: string): string | null {\n const queryFilePath = path.join(this.queriesDir, fileName);\n const resolvedPath = path.resolve(queryFilePath);\n const resolvedQueriesDir = path.resolve(this.queriesDir);\n\n if (!resolvedPath.startsWith(resolvedQueriesDir)) {\n logger.error(\"Invalid query path: path traversal detected\");\n return null;\n }\n\n return resolvedPath;\n }\n\n /**\n * Creates a filesystem adapter based on dev mode or production mode\n */\n private createFsAdapter(\n req?: RequestLike,\n devFileReader?: DevFileReader,\n ): FileSystemAdapter {\n const isDevMode = req?.query?.dev !== undefined;\n\n if (isDevMode && devFileReader && req) {\n // Dev mode: use WebSocket tunnel to read from local filesystem\n return {\n readdir: async (dirPath: string) => {\n const relativePath = path.relative(process.cwd(), dirPath);\n return devFileReader.readdir(relativePath, req);\n },\n readFile: async (filePath: string) => {\n const relativePath = path.relative(process.cwd(), filePath);\n return devFileReader.readFile(relativePath, req);\n },\n };\n }\n\n // Production mode: use server filesystem\n return {\n readdir: (dirPath: string) => fs.readdir(dirPath),\n readFile: (filePath: string) => fs.readFile(filePath, \"utf8\"),\n };\n }\n\n /**\n * Retrieves a query file by key from the queries directory\n * In dev mode with a request context, reads from local filesystem via WebSocket\n * @param queryKey - The query file name (without extension)\n * @param req - Optional request object to detect dev mode\n * @param devFileReader - Optional DevFileReader instance to read files from local filesystem\n * @returns The query content and execution mode (as user or as service principal)\n */\n async getAppQuery(\n queryKey: string,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n ): Promise<QueryResult | null> {\n // Security: Sanitize query key to prevent path traversal\n if (!queryKey || !/^[a-zA-Z0-9_-]+$/.test(queryKey)) {\n logger.error(\n \"Invalid query key format: %s. Only alphanumeric characters, underscores, and hyphens are allowed.\",\n queryKey,\n );\n return null;\n }\n\n // Create filesystem adapter for dev or production mode\n const fsAdapter = this.createFsAdapter(req, devFileReader);\n\n // Priority order: .obo.sql first (as user), then .sql (as service principal)\n const oboFileName = `${queryKey}.obo.sql`;\n const defaultFileName = `${queryKey}.sql`;\n\n // List directory to find which query file exists\n let files: string[];\n try {\n files = await fsAdapter.readdir(this.queriesDir);\n } catch (error) {\n logger.error(\n `Failed to read queries directory: ${(error as Error).message}`,\n );\n return null;\n }\n\n // Determine which query file to use\n let queryFileName: string | null = null;\n let isAsUser = false;\n\n if (files.includes(oboFileName)) {\n queryFileName = oboFileName;\n isAsUser = true;\n\n // Warn if both variants exist\n if (files.includes(defaultFileName)) {\n logger.warn(\n `Both ${oboFileName} and ${defaultFileName} found for query ${queryKey}. Using ${oboFileName}.`,\n );\n }\n } else if (files.includes(defaultFileName)) {\n queryFileName = defaultFileName;\n isAsUser = false;\n }\n\n if (!queryFileName) {\n logger.error(`Query file not found: ${queryKey}`);\n return null;\n }\n\n // Validate and resolve the file path\n const resolvedPath = this.validatePath(queryFileName);\n if (!resolvedPath) {\n return null;\n }\n\n // Read the query file\n try {\n const query = await fsAdapter.readFile(resolvedPath);\n return { query, isAsUser };\n } catch (error) {\n logger.error(`Failed to read query file: ${(error as Error).message}`);\n return null;\n }\n }\n}\n\nexport type { DevFileReader, QueryResult, RequestLike };\n"],"mappings":";;;;;AAIA,MAAM,SAAS,aAAa,MAAM;AAyBlC,IAAa,aAAb,MAAwB;CACtB,AAAiB,aAAa,KAAK,QAAQ,QAAQ,KAAK,EAAE,iBAAiB;;;;CAK3E,AAAQ,aAAa,UAAiC;EACpD,MAAM,gBAAgB,KAAK,KAAK,KAAK,YAAY,SAAS;EAC1D,MAAM,eAAe,KAAK,QAAQ,cAAc;EAChD,MAAM,qBAAqB,KAAK,QAAQ,KAAK,WAAW;AAExD,MAAI,CAAC,aAAa,WAAW,mBAAmB,EAAE;AAChD,UAAO,MAAM,8CAA8C;AAC3D,UAAO;;AAGT,SAAO;;;;;CAMT,AAAQ,gBACN,KACA,eACmB;AAGnB,MAFkB,KAAK,OAAO,QAAQ,UAErB,iBAAiB,IAEhC,QAAO;GACL,SAAS,OAAO,YAAoB;IAClC,MAAM,eAAe,KAAK,SAAS,QAAQ,KAAK,EAAE,QAAQ;AAC1D,WAAO,cAAc,QAAQ,cAAc,IAAI;;GAEjD,UAAU,OAAO,aAAqB;IACpC,MAAM,eAAe,KAAK,SAAS,QAAQ,KAAK,EAAE,SAAS;AAC3D,WAAO,cAAc,SAAS,cAAc,IAAI;;GAEnD;AAIH,SAAO;GACL,UAAU,YAAoB,GAAG,QAAQ,QAAQ;GACjD,WAAW,aAAqB,GAAG,SAAS,UAAU,OAAO;GAC9D;;;;;;;;;;CAWH,MAAM,YACJ,UACA,KACA,eAC6B;AAE7B,MAAI,CAAC,YAAY,CAAC,mBAAmB,KAAK,SAAS,EAAE;AACnD,UAAO,MACL,qGACA,SACD;AACD,UAAO;;EAIT,MAAM,YAAY,KAAK,gBAAgB,KAAK,cAAc;EAG1D,MAAM,cAAc,GAAG,SAAS;EAChC,MAAM,kBAAkB,GAAG,SAAS;EAGpC,IAAI;AACJ,MAAI;AACF,WAAQ,MAAM,UAAU,QAAQ,KAAK,WAAW;WACzC,OAAO;AACd,UAAO,MACL,qCAAsC,MAAgB,UACvD;AACD,UAAO;;EAIT,IAAI,gBAA+B;EACnC,IAAI,WAAW;AAEf,MAAI,MAAM,SAAS,YAAY,EAAE;AAC/B,mBAAgB;AAChB,cAAW;AAGX,OAAI,MAAM,SAAS,gBAAgB,CACjC,QAAO,KACL,QAAQ,YAAY,OAAO,gBAAgB,mBAAmB,SAAS,UAAU,YAAY,GAC9F;aAEM,MAAM,SAAS,gBAAgB,EAAE;AAC1C,mBAAgB;AAChB,cAAW;;AAGb,MAAI,CAAC,eAAe;AAClB,UAAO,MAAM,yBAAyB,WAAW;AACjD,UAAO;;EAIT,MAAM,eAAe,KAAK,aAAa,cAAc;AACrD,MAAI,CAAC,aACH,QAAO;AAIT,MAAI;AAEF,UAAO;IAAE,OADK,MAAM,UAAU,SAAS,aAAa;IACpC;IAAU;WACnB,OAAO;AACd,UAAO,MAAM,8BAA+B,MAAgB,UAAU;AACtE,UAAO"}
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.5.0";
3
+ var version = "0.5.1";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -14,6 +14,7 @@ declare class DevFileReader {
14
14
  static getInstance(): DevFileReader;
15
15
  registerTunnelGetter(getter: TunnelConnectionGetter): void;
16
16
  readFile(filePath: string, req: express0.Request): Promise<string>;
17
+ readdir(dirPath: string, req: express0.Request): Promise<string[]>;
17
18
  }
18
19
  //#endregion
19
20
  export { DevFileReader };
@@ -1 +1 @@
1
- {"version":3,"file":"dev-reader.d.ts","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":[],"mappings":";;;;KAQK,sBAAA,SAAsB,QAAA,CACF,YACpB;;;AAT0C;;AAOpB,cAQd,aAAA,CAPY;iBACpB,QAAA;EAAgB,QAAA,mBAAA;EAMR,QAAA,WAAa,CAAA;EAAA,OAAA,WAAA,CAAA,CAAA,EAMF,aANE;sBAMF,CAAA,MAAA,EA+BO,sBA/BP,CAAA,EAAA,IAAA;UA+BO,CAAA,QAAA,EAAA,MAAA,EAAA,GAAA,EAAsB,QAAA,CAM1B,OANI,CAAA,EAO1B,OAP0B,CAAA,MAAA,CAAA"}
1
+ {"version":3,"file":"dev-reader.d.ts","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":[],"mappings":";;;;KAQK,sBAAA,SAAsB,QAAA,CACF,YACpB;;;AAT0C;;AAOpB,cAQd,aAAA,CAPY;iBACpB,QAAA;EAAgB,QAAA,mBAAA;EAMR,QAAA,WAAa,CAAA;EAAA,OAAA,WAAA,CAAA,CAAA,EAMF,aANE;sBAMF,CAAA,MAAA,EA+BO,sBA/BP,CAAA,EAAA,IAAA;UA+BO,CAAA,QAAA,EAAA,MAAA,EAAA,GAAA,EAAsB,QAAA,CAM1B,OANI,CAAA,EAO1B,OAP0B,CAAA,MAAA,CAAA;SAAsB,CAAA,OAM1B,EAAA,MAAA,EAAA,GAAA,EACf,QAAA,CAiCe,OAlCA,CAAA,EAmCtB,OAnCsB,CAAA,MAAA,EAAA,CAAA"}
@@ -58,6 +58,44 @@ var DevFileReader = class DevFileReader {
58
58
  }));
59
59
  });
60
60
  }
61
+ async readdir(dirPath, req) {
62
+ if (!this.getTunnelForRequest) throw TunnelError.getterNotRegistered();
63
+ const tunnel = this.getTunnelForRequest(req);
64
+ if (!tunnel) throw TunnelError.noConnection();
65
+ const { ws, pendingFileReads } = tunnel;
66
+ const requestId = randomUUID();
67
+ return new Promise((resolve, reject) => {
68
+ const timeout = setTimeout(() => {
69
+ pendingFileReads.delete(requestId);
70
+ reject(/* @__PURE__ */ new Error(`Directory read timeout: ${dirPath}`));
71
+ }, 1e4);
72
+ pendingFileReads.set(requestId, {
73
+ resolve: (data) => {
74
+ try {
75
+ const files = JSON.parse(data);
76
+ if (!Array.isArray(files)) {
77
+ reject(/* @__PURE__ */ new Error("Invalid directory listing format: expected array, got " + typeof files));
78
+ return;
79
+ }
80
+ if (!files.every((f) => typeof f === "string")) {
81
+ reject(/* @__PURE__ */ new Error("Invalid directory listing format: expected array of strings"));
82
+ return;
83
+ }
84
+ resolve(files);
85
+ } catch (error) {
86
+ reject(/* @__PURE__ */ new Error(`Failed to parse directory listing: ${error.message}`));
87
+ }
88
+ },
89
+ reject,
90
+ timeout
91
+ });
92
+ ws.send(JSON.stringify({
93
+ type: "dir:list",
94
+ requestId,
95
+ path: dirPath
96
+ }));
97
+ });
98
+ }
61
99
  };
62
100
 
63
101
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"dev-reader.js","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { TunnelConnection } from \"shared\";\nimport { isRemoteTunnelAllowedByEnv } from \"@/server/remote-tunnel/gate\";\nimport { TunnelError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"plugin:dev-reader\");\n\ntype TunnelConnectionGetter = (\n req: import(\"express\").Request,\n) => TunnelConnection | null;\n\n/**\n * This class is used to read files from the local filesystem in dev mode\n * through the WebSocket tunnel.\n */\nexport class DevFileReader {\n private static instance: DevFileReader | null = null;\n private getTunnelForRequest: TunnelConnectionGetter | null = null;\n\n private constructor() {}\n\n static getInstance(): DevFileReader {\n if (!DevFileReader.instance) {\n DevFileReader.instance = new Proxy(new DevFileReader(), {\n /**\n * We proxy the reader to return a noop function if the remote server is disabled.\n */\n get(target, prop, receiver) {\n if (isRemoteTunnelAllowedByEnv()) {\n return Reflect.get(target, prop, receiver);\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value === \"function\") {\n return function noop() {\n logger.debug(\"Noop: %s (remote server disabled)\", String(prop));\n return Promise.resolve(\"\");\n };\n }\n\n return value;\n },\n set(target, prop, value, receiver) {\n return Reflect.set(target, prop, value, receiver);\n },\n });\n }\n\n return DevFileReader.instance;\n }\n\n registerTunnelGetter(getter: TunnelConnectionGetter) {\n this.getTunnelForRequest = getter;\n }\n\n async readFile(\n filePath: string,\n req: import(\"express\").Request,\n ): Promise<string> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`File read timeout: ${filePath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, { resolve, reject, timeout });\n\n ws.send(\n JSON.stringify({\n type: \"file:read\",\n requestId,\n path: filePath,\n }),\n );\n });\n }\n}\n"],"mappings":";;;;;;;aAGwC;AAGxC,MAAM,SAAS,aAAa,oBAAoB;;;;;AAUhD,IAAa,gBAAb,MAAa,cAAc;CACzB,OAAe,WAAiC;CAChD,AAAQ,sBAAqD;CAE7D,AAAQ,cAAc;CAEtB,OAAO,cAA6B;AAClC,MAAI,CAAC,cAAc,SACjB,eAAc,WAAW,IAAI,MAAM,IAAI,eAAe,EAAE;GAItD,IAAI,QAAQ,MAAM,UAAU;AAC1B,QAAI,4BAA4B,CAC9B,QAAO,QAAQ,IAAI,QAAQ,MAAM,SAAS;IAG5C,MAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,SAAS;AAEjD,QAAI,OAAO,UAAU,WACnB,QAAO,SAAS,OAAO;AACrB,YAAO,MAAM,qCAAqC,OAAO,KAAK,CAAC;AAC/D,YAAO,QAAQ,QAAQ,GAAG;;AAI9B,WAAO;;GAET,IAAI,QAAQ,MAAM,OAAO,UAAU;AACjC,WAAO,QAAQ,IAAI,QAAQ,MAAM,OAAO,SAAS;;GAEpD,CAAC;AAGJ,SAAO,cAAc;;CAGvB,qBAAqB,QAAgC;AACnD,OAAK,sBAAsB;;CAG7B,MAAM,SACJ,UACA,KACiB;AACjB,MAAI,CAAC,KAAK,oBACR,OAAM,YAAY,qBAAqB;EAEzC,MAAM,SAAS,KAAK,oBAAoB,IAAI;AAE5C,MAAI,CAAC,OACH,OAAM,YAAY,cAAc;EAGlC,MAAM,EAAE,IAAI,qBAAqB;EACjC,MAAM,YAAY,YAAY;AAE9B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,UAAU,iBAAiB;AAC/B,qBAAiB,OAAO,UAAU;AAClC,2BAAO,IAAI,MAAM,sBAAsB,WAAW,CAAC;MAClD,IAAM;AAET,oBAAiB,IAAI,WAAW;IAAE;IAAS;IAAQ;IAAS,CAAC;AAE7D,MAAG,KACD,KAAK,UAAU;IACb,MAAM;IACN;IACA,MAAM;IACP,CAAC,CACH;IACD"}
1
+ {"version":3,"file":"dev-reader.js","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { TunnelConnection } from \"shared\";\nimport { isRemoteTunnelAllowedByEnv } from \"@/server/remote-tunnel/gate\";\nimport { TunnelError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"plugin:dev-reader\");\n\ntype TunnelConnectionGetter = (\n req: import(\"express\").Request,\n) => TunnelConnection | null;\n\n/**\n * This class is used to read files from the local filesystem in dev mode\n * through the WebSocket tunnel.\n */\nexport class DevFileReader {\n private static instance: DevFileReader | null = null;\n private getTunnelForRequest: TunnelConnectionGetter | null = null;\n\n private constructor() {}\n\n static getInstance(): DevFileReader {\n if (!DevFileReader.instance) {\n DevFileReader.instance = new Proxy(new DevFileReader(), {\n /**\n * We proxy the reader to return a noop function if the remote server is disabled.\n */\n get(target, prop, receiver) {\n if (isRemoteTunnelAllowedByEnv()) {\n return Reflect.get(target, prop, receiver);\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value === \"function\") {\n return function noop() {\n logger.debug(\"Noop: %s (remote server disabled)\", String(prop));\n return Promise.resolve(\"\");\n };\n }\n\n return value;\n },\n set(target, prop, value, receiver) {\n return Reflect.set(target, prop, value, receiver);\n },\n });\n }\n\n return DevFileReader.instance;\n }\n\n registerTunnelGetter(getter: TunnelConnectionGetter) {\n this.getTunnelForRequest = getter;\n }\n\n async readFile(\n filePath: string,\n req: import(\"express\").Request,\n ): Promise<string> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`File read timeout: ${filePath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, { resolve, reject, timeout });\n\n ws.send(\n JSON.stringify({\n type: \"file:read\",\n requestId,\n path: filePath,\n }),\n );\n });\n }\n\n async readdir(\n dirPath: string,\n req: import(\"express\").Request,\n ): Promise<string[]> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`Directory read timeout: ${dirPath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, {\n resolve: (data: string) => {\n try {\n const files = JSON.parse(data);\n // Validate it's an array of strings\n if (!Array.isArray(files)) {\n reject(\n new Error(\n \"Invalid directory listing format: expected array, got \" +\n typeof files,\n ),\n );\n return;\n }\n if (!files.every((f) => typeof f === \"string\")) {\n reject(\n new Error(\n \"Invalid directory listing format: expected array of strings\",\n ),\n );\n return;\n }\n resolve(files);\n } catch (error) {\n reject(\n new Error(\n `Failed to parse directory listing: ${(error as Error).message}`,\n ),\n );\n }\n },\n reject,\n timeout,\n });\n\n ws.send(\n JSON.stringify({\n type: \"dir:list\",\n requestId,\n path: dirPath,\n }),\n );\n });\n }\n}\n"],"mappings":";;;;;;;aAGwC;AAGxC,MAAM,SAAS,aAAa,oBAAoB;;;;;AAUhD,IAAa,gBAAb,MAAa,cAAc;CACzB,OAAe,WAAiC;CAChD,AAAQ,sBAAqD;CAE7D,AAAQ,cAAc;CAEtB,OAAO,cAA6B;AAClC,MAAI,CAAC,cAAc,SACjB,eAAc,WAAW,IAAI,MAAM,IAAI,eAAe,EAAE;GAItD,IAAI,QAAQ,MAAM,UAAU;AAC1B,QAAI,4BAA4B,CAC9B,QAAO,QAAQ,IAAI,QAAQ,MAAM,SAAS;IAG5C,MAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,SAAS;AAEjD,QAAI,OAAO,UAAU,WACnB,QAAO,SAAS,OAAO;AACrB,YAAO,MAAM,qCAAqC,OAAO,KAAK,CAAC;AAC/D,YAAO,QAAQ,QAAQ,GAAG;;AAI9B,WAAO;;GAET,IAAI,QAAQ,MAAM,OAAO,UAAU;AACjC,WAAO,QAAQ,IAAI,QAAQ,MAAM,OAAO,SAAS;;GAEpD,CAAC;AAGJ,SAAO,cAAc;;CAGvB,qBAAqB,QAAgC;AACnD,OAAK,sBAAsB;;CAG7B,MAAM,SACJ,UACA,KACiB;AACjB,MAAI,CAAC,KAAK,oBACR,OAAM,YAAY,qBAAqB;EAEzC,MAAM,SAAS,KAAK,oBAAoB,IAAI;AAE5C,MAAI,CAAC,OACH,OAAM,YAAY,cAAc;EAGlC,MAAM,EAAE,IAAI,qBAAqB;EACjC,MAAM,YAAY,YAAY;AAE9B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,UAAU,iBAAiB;AAC/B,qBAAiB,OAAO,UAAU;AAClC,2BAAO,IAAI,MAAM,sBAAsB,WAAW,CAAC;MAClD,IAAM;AAET,oBAAiB,IAAI,WAAW;IAAE;IAAS;IAAQ;IAAS,CAAC;AAE7D,MAAG,KACD,KAAK,UAAU;IACb,MAAM;IACN;IACA,MAAM;IACP,CAAC,CACH;IACD;;CAGJ,MAAM,QACJ,SACA,KACmB;AACnB,MAAI,CAAC,KAAK,oBACR,OAAM,YAAY,qBAAqB;EAEzC,MAAM,SAAS,KAAK,oBAAoB,IAAI;AAE5C,MAAI,CAAC,OACH,OAAM,YAAY,cAAc;EAGlC,MAAM,EAAE,IAAI,qBAAqB;EACjC,MAAM,YAAY,YAAY;AAE9B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,UAAU,iBAAiB;AAC/B,qBAAiB,OAAO,UAAU;AAClC,2BAAO,IAAI,MAAM,2BAA2B,UAAU,CAAC;MACtD,IAAM;AAET,oBAAiB,IAAI,WAAW;IAC9B,UAAU,SAAiB;AACzB,SAAI;MACF,MAAM,QAAQ,KAAK,MAAM,KAAK;AAE9B,UAAI,CAAC,MAAM,QAAQ,MAAM,EAAE;AACzB,8BACE,IAAI,MACF,2DACE,OAAO,MACV,CACF;AACD;;AAEF,UAAI,CAAC,MAAM,OAAO,MAAM,OAAO,MAAM,SAAS,EAAE;AAC9C,8BACE,IAAI,MACF,8DACD,CACF;AACD;;AAEF,cAAQ,MAAM;cACP,OAAO;AACd,6BACE,IAAI,MACF,sCAAuC,MAAgB,UACxD,CACF;;;IAGL;IACA;IACD,CAAC;AAEF,MAAG,KACD,KAAK,UAAU;IACb,MAAM;IACN;IACA,MAAM;IACP,CAAC,CACH;IACD"}
@@ -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.
@@ -193,6 +200,10 @@ var RemoteTunnelManager = class {
193
200
  }
194
201
  try {
195
202
  const data = JSON.parse(msg.toString());
203
+ if (!isWebSocketMessage(data)) {
204
+ logger.error("Invalid WebSocket message format: %O", data);
205
+ return;
206
+ }
196
207
  if (data.type === "connection:response") {
197
208
  if (tunnel && data.viewer) {
198
209
  tunnel.pendingRequests.delete(data.viewer);
@@ -227,7 +238,17 @@ var RemoteTunnelManager = class {
227
238
  clearTimeout(pending.timeout);
228
239
  tunnel.pendingFileReads.delete(data.requestId);
229
240
  if (data.error) pending.reject(new Error(data.error));
230
- 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"));
231
252
  }
232
253
  }
233
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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"}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="generator" content="Docusaurus v3.9.2">
6
6
  <title data-rh="true">Abstract Class: AppKitError | AppKit</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://databricks.github.io/appkit/docs/api/appkit/Class.AppKitError"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="keywords" content="Databricks Apps, Node.js, React.js, SDK, TypeScript, SQL, Databricks, AI, full-stack, development"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Abstract Class: AppKitError | AppKit"><meta data-rh="true" name="description" content="Base error class for all AppKit errors."><meta data-rh="true" property="og:description" content="Base error class for all AppKit errors."><link data-rh="true" rel="icon" href="/appkit/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://databricks.github.io/appkit/docs/api/appkit/Class.AppKitError"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.AppKitError" hreflang="en"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.AppKitError" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"API reference","item":"https://databricks.github.io/appkit/docs/api/"},{"@type":"ListItem","position":2,"name":"@databricks/appkit","item":"https://databricks.github.io/appkit/docs/api/appkit/"},{"@type":"ListItem","position":3,"name":"AppKitError","item":"https://databricks.github.io/appkit/docs/api/appkit/Class.AppKitError"}]}</script><link rel="stylesheet" href="/appkit/assets/css/styles.0ec7398f.css">
7
- <script src="/appkit/assets/js/runtime~main.46b4a147.js" defer="defer"></script>
8
- <script src="/appkit/assets/js/main.109beba6.js" defer="defer"></script>
7
+ <script src="/appkit/assets/js/runtime~main.11a6f15e.js" defer="defer"></script>
8
+ <script src="/appkit/assets/js/main.d41b6412.js" defer="defer"></script>
9
9
  </head>
10
10
  <body class="navigation-with-keyboard">
11
11
  <svg style="display: none;"><defs>
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="generator" content="Docusaurus v3.9.2">
6
6
  <title data-rh="true">Class: AuthenticationError | AppKit</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://databricks.github.io/appkit/docs/api/appkit/Class.AuthenticationError"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="keywords" content="Databricks Apps, Node.js, React.js, SDK, TypeScript, SQL, Databricks, AI, full-stack, development"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Class: AuthenticationError | AppKit"><meta data-rh="true" name="description" content="Error thrown when authentication fails."><meta data-rh="true" property="og:description" content="Error thrown when authentication fails."><link data-rh="true" rel="icon" href="/appkit/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://databricks.github.io/appkit/docs/api/appkit/Class.AuthenticationError"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.AuthenticationError" hreflang="en"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.AuthenticationError" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"API reference","item":"https://databricks.github.io/appkit/docs/api/"},{"@type":"ListItem","position":2,"name":"@databricks/appkit","item":"https://databricks.github.io/appkit/docs/api/appkit/"},{"@type":"ListItem","position":3,"name":"AuthenticationError","item":"https://databricks.github.io/appkit/docs/api/appkit/Class.AuthenticationError"}]}</script><link rel="stylesheet" href="/appkit/assets/css/styles.0ec7398f.css">
7
- <script src="/appkit/assets/js/runtime~main.46b4a147.js" defer="defer"></script>
8
- <script src="/appkit/assets/js/main.109beba6.js" defer="defer"></script>
7
+ <script src="/appkit/assets/js/runtime~main.11a6f15e.js" defer="defer"></script>
8
+ <script src="/appkit/assets/js/main.d41b6412.js" defer="defer"></script>
9
9
  </head>
10
10
  <body class="navigation-with-keyboard">
11
11
  <svg style="display: none;"><defs>
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="generator" content="Docusaurus v3.9.2">
6
6
  <title data-rh="true">Class: ConfigurationError | AppKit</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://databricks.github.io/appkit/docs/api/appkit/Class.ConfigurationError"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="keywords" content="Databricks Apps, Node.js, React.js, SDK, TypeScript, SQL, Databricks, AI, full-stack, development"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Class: ConfigurationError | AppKit"><meta data-rh="true" name="description" content="Error thrown when configuration is missing or invalid."><meta data-rh="true" property="og:description" content="Error thrown when configuration is missing or invalid."><link data-rh="true" rel="icon" href="/appkit/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ConfigurationError"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ConfigurationError" hreflang="en"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ConfigurationError" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"API reference","item":"https://databricks.github.io/appkit/docs/api/"},{"@type":"ListItem","position":2,"name":"@databricks/appkit","item":"https://databricks.github.io/appkit/docs/api/appkit/"},{"@type":"ListItem","position":3,"name":"ConfigurationError","item":"https://databricks.github.io/appkit/docs/api/appkit/Class.ConfigurationError"}]}</script><link rel="stylesheet" href="/appkit/assets/css/styles.0ec7398f.css">
7
- <script src="/appkit/assets/js/runtime~main.46b4a147.js" defer="defer"></script>
8
- <script src="/appkit/assets/js/main.109beba6.js" defer="defer"></script>
7
+ <script src="/appkit/assets/js/runtime~main.11a6f15e.js" defer="defer"></script>
8
+ <script src="/appkit/assets/js/main.d41b6412.js" defer="defer"></script>
9
9
  </head>
10
10
  <body class="navigation-with-keyboard">
11
11
  <svg style="display: none;"><defs>
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="generator" content="Docusaurus v3.9.2">
6
6
  <title data-rh="true">Class: ConnectionError | AppKit</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://databricks.github.io/appkit/docs/api/appkit/Class.ConnectionError"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="keywords" content="Databricks Apps, Node.js, React.js, SDK, TypeScript, SQL, Databricks, AI, full-stack, development"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Class: ConnectionError | AppKit"><meta data-rh="true" name="description" content="Error thrown when a connection or network operation fails."><meta data-rh="true" property="og:description" content="Error thrown when a connection or network operation fails."><link data-rh="true" rel="icon" href="/appkit/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ConnectionError"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ConnectionError" hreflang="en"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ConnectionError" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"API reference","item":"https://databricks.github.io/appkit/docs/api/"},{"@type":"ListItem","position":2,"name":"@databricks/appkit","item":"https://databricks.github.io/appkit/docs/api/appkit/"},{"@type":"ListItem","position":3,"name":"ConnectionError","item":"https://databricks.github.io/appkit/docs/api/appkit/Class.ConnectionError"}]}</script><link rel="stylesheet" href="/appkit/assets/css/styles.0ec7398f.css">
7
- <script src="/appkit/assets/js/runtime~main.46b4a147.js" defer="defer"></script>
8
- <script src="/appkit/assets/js/main.109beba6.js" defer="defer"></script>
7
+ <script src="/appkit/assets/js/runtime~main.11a6f15e.js" defer="defer"></script>
8
+ <script src="/appkit/assets/js/main.d41b6412.js" defer="defer"></script>
9
9
  </head>
10
10
  <body class="navigation-with-keyboard">
11
11
  <svg style="display: none;"><defs>
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="generator" content="Docusaurus v3.9.2">
6
6
  <title data-rh="true">Class: ExecutionError | AppKit</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://databricks.github.io/appkit/docs/api/appkit/Class.ExecutionError"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="keywords" content="Databricks Apps, Node.js, React.js, SDK, TypeScript, SQL, Databricks, AI, full-stack, development"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Class: ExecutionError | AppKit"><meta data-rh="true" name="description" content="Error thrown when an operation execution fails."><meta data-rh="true" property="og:description" content="Error thrown when an operation execution fails."><link data-rh="true" rel="icon" href="/appkit/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ExecutionError"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ExecutionError" hreflang="en"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.ExecutionError" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"API reference","item":"https://databricks.github.io/appkit/docs/api/"},{"@type":"ListItem","position":2,"name":"@databricks/appkit","item":"https://databricks.github.io/appkit/docs/api/appkit/"},{"@type":"ListItem","position":3,"name":"ExecutionError","item":"https://databricks.github.io/appkit/docs/api/appkit/Class.ExecutionError"}]}</script><link rel="stylesheet" href="/appkit/assets/css/styles.0ec7398f.css">
7
- <script src="/appkit/assets/js/runtime~main.46b4a147.js" defer="defer"></script>
8
- <script src="/appkit/assets/js/main.109beba6.js" defer="defer"></script>
7
+ <script src="/appkit/assets/js/runtime~main.11a6f15e.js" defer="defer"></script>
8
+ <script src="/appkit/assets/js/main.d41b6412.js" defer="defer"></script>
9
9
  </head>
10
10
  <body class="navigation-with-keyboard">
11
11
  <svg style="display: none;"><defs>
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="generator" content="Docusaurus v3.9.2">
6
6
  <title data-rh="true">Class: InitializationError | AppKit</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://databricks.github.io/appkit/docs/api/appkit/Class.InitializationError"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="keywords" content="Databricks Apps, Node.js, React.js, SDK, TypeScript, SQL, Databricks, AI, full-stack, development"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Class: InitializationError | AppKit"><meta data-rh="true" name="description" content="Error thrown when a service or component is not properly initialized."><meta data-rh="true" property="og:description" content="Error thrown when a service or component is not properly initialized."><link data-rh="true" rel="icon" href="/appkit/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://databricks.github.io/appkit/docs/api/appkit/Class.InitializationError"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.InitializationError" hreflang="en"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.InitializationError" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"API reference","item":"https://databricks.github.io/appkit/docs/api/"},{"@type":"ListItem","position":2,"name":"@databricks/appkit","item":"https://databricks.github.io/appkit/docs/api/appkit/"},{"@type":"ListItem","position":3,"name":"InitializationError","item":"https://databricks.github.io/appkit/docs/api/appkit/Class.InitializationError"}]}</script><link rel="stylesheet" href="/appkit/assets/css/styles.0ec7398f.css">
7
- <script src="/appkit/assets/js/runtime~main.46b4a147.js" defer="defer"></script>
8
- <script src="/appkit/assets/js/main.109beba6.js" defer="defer"></script>
7
+ <script src="/appkit/assets/js/runtime~main.11a6f15e.js" defer="defer"></script>
8
+ <script src="/appkit/assets/js/main.d41b6412.js" defer="defer"></script>
9
9
  </head>
10
10
  <body class="navigation-with-keyboard">
11
11
  <svg style="display: none;"><defs>
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="generator" content="Docusaurus v3.9.2">
6
6
  <title data-rh="true">Abstract Class: Plugin\&lt;TConfig\&gt; | AppKit</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://databricks.github.io/appkit/docs/api/appkit/Class.Plugin"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="keywords" content="Databricks Apps, Node.js, React.js, SDK, TypeScript, SQL, Databricks, AI, full-stack, development"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Abstract Class: Plugin\&lt;TConfig\&gt; | AppKit"><meta data-rh="true" name="description" content="Base abstract class for creating AppKit plugins"><meta data-rh="true" property="og:description" content="Base abstract class for creating AppKit plugins"><link data-rh="true" rel="icon" href="/appkit/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://databricks.github.io/appkit/docs/api/appkit/Class.Plugin"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.Plugin" hreflang="en"><link data-rh="true" rel="alternate" href="https://databricks.github.io/appkit/docs/api/appkit/Class.Plugin" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"API reference","item":"https://databricks.github.io/appkit/docs/api/"},{"@type":"ListItem","position":2,"name":"@databricks/appkit","item":"https://databricks.github.io/appkit/docs/api/appkit/"},{"@type":"ListItem","position":3,"name":"Plugin","item":"https://databricks.github.io/appkit/docs/api/appkit/Class.Plugin"}]}</script><link rel="stylesheet" href="/appkit/assets/css/styles.0ec7398f.css">
7
- <script src="/appkit/assets/js/runtime~main.46b4a147.js" defer="defer"></script>
8
- <script src="/appkit/assets/js/main.109beba6.js" defer="defer"></script>
7
+ <script src="/appkit/assets/js/runtime~main.11a6f15e.js" defer="defer"></script>
8
+ <script src="/appkit/assets/js/main.d41b6412.js" defer="defer"></script>
9
9
  </head>
10
10
  <body class="navigation-with-keyboard">
11
11
  <svg style="display: none;"><defs>