@databricks/appkit 0.5.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CLAUDE.md +23 -1
  2. package/NOTICE.md +3 -1
  3. package/bin/appkit.js +0 -0
  4. package/dist/_virtual/_rolldown/runtime.js +1 -33
  5. package/dist/appkit/package.js +1 -1
  6. package/dist/cache/index.d.ts.map +1 -1
  7. package/dist/cache/index.js +7 -7
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/cache/storage/persistent.js +23 -21
  10. package/dist/cache/storage/persistent.js.map +1 -1
  11. package/dist/cli/commands/plugins-sync.js +369 -0
  12. package/dist/cli/commands/plugins-sync.js.map +1 -0
  13. package/dist/cli/commands/plugins.js +19 -0
  14. package/dist/cli/commands/plugins.js.map +1 -0
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/connectors/index.js +2 -2
  18. package/dist/connectors/lakebase/index.d.ts +16 -0
  19. package/dist/connectors/lakebase/index.d.ts.map +1 -0
  20. package/dist/connectors/lakebase/index.js +21 -2
  21. package/dist/connectors/lakebase/index.js.map +1 -0
  22. package/dist/connectors/lakebase-v1/client.js +13 -0
  23. package/dist/connectors/lakebase-v1/client.js.map +1 -0
  24. package/dist/connectors/lakebase-v1/index.js +3 -0
  25. package/dist/connectors/sql-warehouse/client.js +2 -2
  26. package/dist/context/execution-context.js +1 -7
  27. package/dist/context/execution-context.js.map +1 -1
  28. package/dist/context/index.js +4 -16
  29. package/dist/context/index.js.map +1 -1
  30. package/dist/core/appkit.d.ts.map +1 -1
  31. package/dist/core/appkit.js +6 -2
  32. package/dist/core/appkit.js.map +1 -1
  33. package/dist/index.d.ts +5 -1
  34. package/dist/index.js +8 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/logging/logger.js +1 -1
  37. package/dist/plugin/plugin.d.ts +95 -3
  38. package/dist/plugin/plugin.d.ts.map +1 -1
  39. package/dist/plugin/plugin.js +98 -10
  40. package/dist/plugin/plugin.js.map +1 -1
  41. package/dist/plugins/analytics/analytics.d.ts +3 -1
  42. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  43. package/dist/plugins/analytics/analytics.js +5 -3
  44. package/dist/plugins/analytics/analytics.js.map +1 -1
  45. package/dist/plugins/analytics/index.js +1 -0
  46. package/dist/plugins/analytics/manifest.js +21 -0
  47. package/dist/plugins/analytics/manifest.js.map +1 -0
  48. package/dist/plugins/analytics/manifest.json +36 -0
  49. package/dist/plugins/index.js +2 -0
  50. package/dist/plugins/server/index.d.ts +3 -1
  51. package/dist/plugins/server/index.d.ts.map +1 -1
  52. package/dist/plugins/server/index.js +5 -3
  53. package/dist/plugins/server/index.js.map +1 -1
  54. package/dist/plugins/server/manifest.js +21 -0
  55. package/dist/plugins/server/manifest.js.map +1 -0
  56. package/dist/plugins/server/manifest.json +36 -0
  57. package/dist/registry/index.js +5 -0
  58. package/dist/registry/manifest-loader.d.ts +44 -0
  59. package/dist/registry/manifest-loader.d.ts.map +1 -0
  60. package/dist/registry/manifest-loader.js +97 -0
  61. package/dist/registry/manifest-loader.js.map +1 -0
  62. package/dist/registry/resource-registry.d.ts +133 -0
  63. package/dist/registry/resource-registry.d.ts.map +1 -0
  64. package/dist/registry/resource-registry.js +297 -0
  65. package/dist/registry/resource-registry.js.map +1 -0
  66. package/dist/registry/types.d.ts +181 -0
  67. package/dist/registry/types.d.ts.map +1 -0
  68. package/dist/registry/types.js +89 -0
  69. package/dist/registry/types.js.map +1 -0
  70. package/dist/shared/src/plugin.d.ts +66 -1
  71. package/dist/shared/src/plugin.d.ts.map +1 -1
  72. package/docs/docs/api/appkit/Class.AppKitError/index.html +5 -5
  73. package/docs/docs/api/appkit/Class.AuthenticationError/index.html +4 -4
  74. package/docs/docs/api/appkit/Class.ConfigurationError/index.html +4 -4
  75. package/docs/docs/api/appkit/Class.ConnectionError/index.html +4 -4
  76. package/docs/docs/api/appkit/Class.ExecutionError/index.html +4 -4
  77. package/docs/docs/api/appkit/Class.InitializationError/index.html +4 -4
  78. package/docs/docs/api/appkit/Class.Plugin/index.html +28 -16
  79. package/docs/docs/api/appkit/Class.Plugin.md +90 -30
  80. package/docs/docs/api/appkit/Class.ResourceRegistry/index.html +150 -0
  81. package/docs/docs/api/appkit/Class.ResourceRegistry.md +301 -0
  82. package/docs/docs/api/appkit/Class.ServerError/index.html +5 -5
  83. package/docs/docs/api/appkit/Class.TunnelError/index.html +4 -4
  84. package/docs/docs/api/appkit/Class.ValidationError/index.html +4 -4
  85. package/docs/docs/api/appkit/Enumeration.RequestedClaimsPermissionSet/index.html +21 -0
  86. package/docs/docs/api/appkit/Enumeration.RequestedClaimsPermissionSet.md +14 -0
  87. package/docs/docs/api/appkit/Enumeration.ResourceType/index.html +66 -0
  88. package/docs/docs/api/appkit/Enumeration.ResourceType.md +135 -0
  89. package/docs/docs/api/appkit/Function.appKitTypesPlugin/index.html +4 -4
  90. package/docs/docs/api/appkit/Function.createApp/index.html +5 -5
  91. package/docs/docs/api/appkit/Function.createLakebasePool/index.html +24 -0
  92. package/docs/docs/api/appkit/Function.createLakebasePool.md +20 -0
  93. package/docs/docs/api/appkit/Function.generateDatabaseCredential/index.html +30 -0
  94. package/docs/docs/api/appkit/Function.generateDatabaseCredential.md +57 -0
  95. package/docs/docs/api/appkit/Function.getExecutionContext/index.html +5 -5
  96. package/docs/docs/api/appkit/Function.getLakebaseOrmConfig/index.html +39 -0
  97. package/docs/docs/api/appkit/Function.getLakebaseOrmConfig.md +90 -0
  98. package/docs/docs/api/appkit/Function.getLakebasePgConfig/index.html +27 -0
  99. package/docs/docs/api/appkit/Function.getLakebasePgConfig.md +31 -0
  100. package/docs/docs/api/appkit/Function.getPluginManifest/index.html +26 -0
  101. package/docs/docs/api/appkit/Function.getPluginManifest.md +24 -0
  102. package/docs/docs/api/appkit/Function.getResourceRequirements/index.html +28 -0
  103. package/docs/docs/api/appkit/Function.getResourceRequirements.md +42 -0
  104. package/docs/docs/api/appkit/Function.getWorkspaceClient/index.html +22 -0
  105. package/docs/docs/api/appkit/Function.getWorkspaceClient.md +18 -0
  106. package/docs/docs/api/appkit/Function.isSQLTypeMarker/index.html +5 -5
  107. package/docs/docs/api/appkit/Interface.BasePluginConfig/index.html +4 -4
  108. package/docs/docs/api/appkit/Interface.CacheConfig/index.html +5 -5
  109. package/docs/docs/api/appkit/Interface.DatabaseCredential/index.html +28 -0
  110. package/docs/docs/api/appkit/Interface.DatabaseCredential.md +32 -0
  111. package/docs/docs/api/appkit/Interface.GenerateDatabaseCredentialRequest/index.html +38 -0
  112. package/docs/docs/api/appkit/Interface.GenerateDatabaseCredentialRequest.md +54 -0
  113. package/docs/docs/api/appkit/Interface.ITelemetry/index.html +5 -5
  114. package/docs/docs/api/appkit/Interface.LakebasePoolConfig/index.html +79 -0
  115. package/docs/docs/api/appkit/Interface.LakebasePoolConfig.md +126 -0
  116. package/docs/docs/api/appkit/Interface.PluginManifest/index.html +63 -0
  117. package/docs/docs/api/appkit/Interface.PluginManifest.md +135 -0
  118. package/docs/docs/api/appkit/Interface.RequestedClaims/index.html +26 -0
  119. package/docs/docs/api/appkit/Interface.RequestedClaims.md +25 -0
  120. package/docs/docs/api/appkit/Interface.RequestedResource/index.html +27 -0
  121. package/docs/docs/api/appkit/Interface.RequestedResource.md +32 -0
  122. package/docs/docs/api/appkit/Interface.ResourceEntry/index.html +83 -0
  123. package/docs/docs/api/appkit/Interface.ResourceEntry.md +156 -0
  124. package/docs/docs/api/appkit/Interface.ResourceFieldEntry/index.html +26 -0
  125. package/docs/docs/api/appkit/Interface.ResourceFieldEntry.md +25 -0
  126. package/docs/docs/api/appkit/Interface.ResourceRequirement/index.html +51 -0
  127. package/docs/docs/api/appkit/Interface.ResourceRequirement.md +84 -0
  128. package/docs/docs/api/appkit/Interface.StreamExecutionSettings/index.html +5 -5
  129. package/docs/docs/api/appkit/Interface.TelemetryConfig/index.html +5 -5
  130. package/docs/docs/api/appkit/Interface.ValidationResult/index.html +29 -0
  131. package/docs/docs/api/appkit/Interface.ValidationResult.md +36 -0
  132. package/docs/docs/api/appkit/TypeAlias.ConfigSchema/index.html +21 -0
  133. package/docs/docs/api/appkit/TypeAlias.ConfigSchema.md +12 -0
  134. package/docs/docs/api/appkit/TypeAlias.IAppRouter/index.html +5 -5
  135. package/docs/docs/api/appkit/TypeAlias.ResourcePermission/index.html +18 -0
  136. package/docs/docs/api/appkit/TypeAlias.ResourcePermission.md +20 -0
  137. package/docs/docs/api/appkit/Variable.sql/index.html +5 -5
  138. package/docs/docs/api/appkit/index.html +10 -8
  139. package/docs/docs/api/appkit-ui/data/AreaChart/index.html +3 -3
  140. package/docs/docs/api/appkit-ui/data/BarChart/index.html +3 -3
  141. package/docs/docs/api/appkit-ui/data/DataTable/index.html +3 -3
  142. package/docs/docs/api/appkit-ui/data/DonutChart/index.html +3 -3
  143. package/docs/docs/api/appkit-ui/data/HeatmapChart/index.html +3 -3
  144. package/docs/docs/api/appkit-ui/data/LineChart/index.html +3 -3
  145. package/docs/docs/api/appkit-ui/data/PieChart/index.html +3 -3
  146. package/docs/docs/api/appkit-ui/data/RadarChart/index.html +3 -3
  147. package/docs/docs/api/appkit-ui/data/ScatterChart/index.html +3 -3
  148. package/docs/docs/api/appkit-ui/index.html +3 -3
  149. package/docs/docs/api/appkit-ui/styling/index.html +3 -3
  150. package/docs/docs/api/appkit-ui/ui/Accordion/index.html +3 -3
  151. package/docs/docs/api/appkit-ui/ui/Alert/index.html +3 -3
  152. package/docs/docs/api/appkit-ui/ui/AlertDialog/index.html +3 -3
  153. package/docs/docs/api/appkit-ui/ui/AspectRatio/index.html +3 -3
  154. package/docs/docs/api/appkit-ui/ui/Avatar/index.html +3 -3
  155. package/docs/docs/api/appkit-ui/ui/Badge/index.html +3 -3
  156. package/docs/docs/api/appkit-ui/ui/Breadcrumb/index.html +3 -3
  157. package/docs/docs/api/appkit-ui/ui/Button/index.html +3 -3
  158. package/docs/docs/api/appkit-ui/ui/ButtonGroup/index.html +3 -3
  159. package/docs/docs/api/appkit-ui/ui/Calendar/index.html +3 -3
  160. package/docs/docs/api/appkit-ui/ui/Card/index.html +3 -3
  161. package/docs/docs/api/appkit-ui/ui/Carousel/index.html +3 -3
  162. package/docs/docs/api/appkit-ui/ui/ChartContainer/index.html +3 -3
  163. package/docs/docs/api/appkit-ui/ui/Checkbox/index.html +3 -3
  164. package/docs/docs/api/appkit-ui/ui/Collapsible/index.html +3 -3
  165. package/docs/docs/api/appkit-ui/ui/Command/index.html +3 -3
  166. package/docs/docs/api/appkit-ui/ui/ContextMenu/index.html +3 -3
  167. package/docs/docs/api/appkit-ui/ui/Dialog/index.html +3 -3
  168. package/docs/docs/api/appkit-ui/ui/Drawer/index.html +3 -3
  169. package/docs/docs/api/appkit-ui/ui/DropdownMenu/index.html +3 -3
  170. package/docs/docs/api/appkit-ui/ui/Empty/index.html +3 -3
  171. package/docs/docs/api/appkit-ui/ui/Field/index.html +3 -3
  172. package/docs/docs/api/appkit-ui/ui/FormControl/index.html +3 -3
  173. package/docs/docs/api/appkit-ui/ui/HoverCard/index.html +3 -3
  174. package/docs/docs/api/appkit-ui/ui/Input/index.html +3 -3
  175. package/docs/docs/api/appkit-ui/ui/InputGroup/index.html +3 -3
  176. package/docs/docs/api/appkit-ui/ui/InputOTP/index.html +3 -3
  177. package/docs/docs/api/appkit-ui/ui/Item/index.html +3 -3
  178. package/docs/docs/api/appkit-ui/ui/Kbd/index.html +3 -3
  179. package/docs/docs/api/appkit-ui/ui/Label/index.html +3 -3
  180. package/docs/docs/api/appkit-ui/ui/Menubar/index.html +3 -3
  181. package/docs/docs/api/appkit-ui/ui/NavigationMenu/index.html +3 -3
  182. package/docs/docs/api/appkit-ui/ui/Pagination/index.html +3 -3
  183. package/docs/docs/api/appkit-ui/ui/Popover/index.html +3 -3
  184. package/docs/docs/api/appkit-ui/ui/Progress/index.html +3 -3
  185. package/docs/docs/api/appkit-ui/ui/RadioGroup/index.html +3 -3
  186. package/docs/docs/api/appkit-ui/ui/ResizableHandle/index.html +3 -3
  187. package/docs/docs/api/appkit-ui/ui/ScrollArea/index.html +3 -3
  188. package/docs/docs/api/appkit-ui/ui/Select/index.html +3 -3
  189. package/docs/docs/api/appkit-ui/ui/Separator/index.html +3 -3
  190. package/docs/docs/api/appkit-ui/ui/Sheet/index.html +3 -3
  191. package/docs/docs/api/appkit-ui/ui/Sidebar/index.html +3 -3
  192. package/docs/docs/api/appkit-ui/ui/Skeleton/index.html +3 -3
  193. package/docs/docs/api/appkit-ui/ui/Slider/index.html +3 -3
  194. package/docs/docs/api/appkit-ui/ui/Spinner/index.html +3 -3
  195. package/docs/docs/api/appkit-ui/ui/Switch/index.html +3 -3
  196. package/docs/docs/api/appkit-ui/ui/Table/index.html +3 -3
  197. package/docs/docs/api/appkit-ui/ui/Tabs/index.html +3 -3
  198. package/docs/docs/api/appkit-ui/ui/Textarea/index.html +3 -3
  199. package/docs/docs/api/appkit-ui/ui/Toaster/index.html +3 -3
  200. package/docs/docs/api/appkit-ui/ui/Toggle/index.html +3 -3
  201. package/docs/docs/api/appkit-ui/ui/ToggleGroup/index.html +3 -3
  202. package/docs/docs/api/appkit-ui/ui/Tooltip/index.html +3 -3
  203. package/docs/docs/api/appkit.md +55 -28
  204. package/docs/docs/api/index.html +3 -3
  205. package/docs/docs/app-management/index.html +3 -3
  206. package/docs/docs/architecture/index.html +4 -4
  207. package/docs/docs/architecture.md +1 -1
  208. package/docs/docs/category/development/index.html +3 -3
  209. package/docs/docs/configuration/index.html +3 -3
  210. package/docs/docs/core-principles/index.html +3 -3
  211. package/docs/docs/development/ai-assisted-development/index.html +3 -3
  212. package/docs/docs/development/index.html +3 -3
  213. package/docs/docs/development/llm-guide/index.html +3 -3
  214. package/docs/docs/development/local-development/index.html +3 -3
  215. package/docs/docs/development/project-setup/index.html +3 -3
  216. package/docs/docs/development/remote-bridge/index.html +3 -3
  217. package/docs/docs/development/type-generation/index.html +3 -3
  218. package/docs/docs/index.html +3 -3
  219. package/docs/docs/plugins/index.html +18 -8
  220. package/docs/docs/plugins.md +82 -4
  221. package/llms.txt +23 -1
  222. package/package.json +8 -4
  223. package/dist/connectors/lakebase/client.js +0 -362
  224. package/dist/connectors/lakebase/client.js.map +0 -1
  225. package/dist/connectors/lakebase/defaults.js +0 -13
  226. package/dist/connectors/lakebase/defaults.js.map +0 -1
  227. package/dist/utils/env-validator.js +0 -14
  228. package/dist/utils/env-validator.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRouter,\n PluginExecuteConfig,\n SQLTypeMarker,\n StreamExecutionSettings,\n} from \"shared\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n getCurrentUserId,\n getWarehouseId,\n getWorkspaceClient,\n} from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { queryDefaults } from \"./defaults\";\nimport { QueryProcessor } from \"./query\";\nimport type {\n AnalyticsQueryResponse,\n IAnalyticsConfig,\n IAnalyticsQueryRequest,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\nexport class AnalyticsPlugin extends Plugin {\n name = \"analytics\";\n protected envVars: string[] = [];\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n // Service principal endpoints\n this.route(router, {\n name: \"arrow\",\n method: \"get\",\n path: \"/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n }\n\n /**\n * Handle Arrow data download requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleArrowRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n try {\n const { jobId } = req.params;\n const workspaceClient = getWorkspaceClient();\n\n logger.debug(\"Processing Arrow job request for jobId=%s\", jobId);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"getArrowData\").setContext(\"analytics\", {\n job_id: jobId,\n plugin: this.name,\n });\n\n const result = await this.getArrowData(workspaceClient, jobId);\n\n res.setHeader(\"Content-Type\", \"application/octet-stream\");\n res.setHeader(\"Content-Length\", result.data.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n logger.debug(\n \"Sending Arrow buffer: %d bytes for job %s\",\n result.data.length,\n jobId,\n );\n res.send(Buffer.from(result.data));\n } catch (error) {\n logger.error(\"Arrow job error: %O\", error);\n res.status(404).json({\n error: error instanceof Error ? error.message : \"Arrow job not found\",\n plugin: this.name,\n });\n }\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format = \"JSON\" } = req.body as IAnalyticsQueryRequest;\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const userKey = getCurrentUserId();\n const executorKey = isAsUser ? userKey : \"global\";\n\n const queryParameters =\n format === \"ARROW\"\n ? {\n formatParameters: {\n disposition: \"EXTERNAL_LINKS\",\n format: \"ARROW_STREAM\",\n },\n type: \"arrow\",\n }\n : {\n type: \"result\",\n };\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const defaultConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n JSON.stringify(format),\n hashedQuery,\n executorKey,\n ],\n },\n };\n\n const streamExecutionSettings: StreamExecutionSettings = {\n default: defaultConfig,\n };\n\n await executor.executeStream(\n res,\n async (signal) => {\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const result = await executor.query(\n query,\n processedParams,\n queryParameters.formatParameters,\n signal,\n );\n\n return { type: queryParameters.type, ...result };\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n /**\n * Get Arrow-formatted data for a completed query job.\n */\n protected async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.SQLClient.getArrowData>> {\n return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * @internal\n */\nexport const analytics = toPlugin<\n typeof AnalyticsPlugin,\n IAnalyticsConfig,\n \"analytics\"\n>(AnalyticsPlugin, \"analytics\");\n"],"mappings":"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"}
1
+ {"version":3,"file":"analytics.js","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRouter,\n PluginExecuteConfig,\n SQLTypeMarker,\n StreamExecutionSettings,\n} from \"shared\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n getCurrentUserId,\n getWarehouseId,\n getWorkspaceClient,\n} from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { queryDefaults } from \"./defaults\";\nimport { analyticsManifest } from \"./manifest\";\nimport { QueryProcessor } from \"./query\";\nimport type {\n AnalyticsQueryResponse,\n IAnalyticsConfig,\n IAnalyticsQueryRequest,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\nexport class AnalyticsPlugin extends Plugin {\n name = \"analytics\";\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = analyticsManifest;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n // Service principal endpoints\n this.route(router, {\n name: \"arrow\",\n method: \"get\",\n path: \"/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n }\n\n /**\n * Handle Arrow data download requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleArrowRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n try {\n const { jobId } = req.params;\n const workspaceClient = getWorkspaceClient();\n\n logger.debug(\"Processing Arrow job request for jobId=%s\", jobId);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"getArrowData\").setContext(\"analytics\", {\n job_id: jobId,\n plugin: this.name,\n });\n\n const result = await this.getArrowData(workspaceClient, jobId);\n\n res.setHeader(\"Content-Type\", \"application/octet-stream\");\n res.setHeader(\"Content-Length\", result.data.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n logger.debug(\n \"Sending Arrow buffer: %d bytes for job %s\",\n result.data.length,\n jobId,\n );\n res.send(Buffer.from(result.data));\n } catch (error) {\n logger.error(\"Arrow job error: %O\", error);\n res.status(404).json({\n error: error instanceof Error ? error.message : \"Arrow job not found\",\n plugin: this.name,\n });\n }\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format = \"JSON\" } = req.body as IAnalyticsQueryRequest;\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const userKey = getCurrentUserId();\n const executorKey = isAsUser ? userKey : \"global\";\n\n const queryParameters =\n format === \"ARROW\"\n ? {\n formatParameters: {\n disposition: \"EXTERNAL_LINKS\",\n format: \"ARROW_STREAM\",\n },\n type: \"arrow\",\n }\n : {\n type: \"result\",\n };\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const defaultConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n JSON.stringify(format),\n hashedQuery,\n executorKey,\n ],\n },\n };\n\n const streamExecutionSettings: StreamExecutionSettings = {\n default: defaultConfig,\n };\n\n await executor.executeStream(\n res,\n async (signal) => {\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const result = await executor.query(\n query,\n processedParams,\n queryParameters.formatParameters,\n signal,\n );\n\n return { type: queryParameters.type, ...result };\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n /**\n * Get Arrow-formatted data for a completed query job.\n */\n protected async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.SQLClient.getArrowData>> {\n return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * @internal\n */\nexport const analytics = toPlugin<\n typeof AnalyticsPlugin,\n IAnalyticsConfig,\n \"analytics\"\n>(AnalyticsPlugin, \"analytics\");\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { analyticsManifest } from "./manifest.js";
1
2
  import { AnalyticsPlugin, analytics } from "./analytics.js";
2
3
 
3
4
  export { };
@@ -0,0 +1,21 @@
1
+ import { dirname, join } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ //#region src/plugins/analytics/manifest.ts
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ /**
8
+ * Analytics plugin manifest.
9
+ *
10
+ * The analytics plugin requires a SQL Warehouse for executing queries
11
+ * against Databricks data sources.
12
+ *
13
+ * @remarks
14
+ * The source of truth for this manifest is `manifest.json` in the same directory.
15
+ * This file loads the JSON and exports it with proper TypeScript typing.
16
+ */
17
+ const analyticsManifest = JSON.parse(readFileSync(join(__dirname, "manifest.json"), "utf-8"));
18
+
19
+ //#endregion
20
+ export { analyticsManifest };
21
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","names":[],"sources":["../../../src/plugins/analytics/manifest.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { PluginManifest } from \"../../registry\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Analytics plugin manifest.\n *\n * The analytics plugin requires a SQL Warehouse for executing queries\n * against Databricks data sources.\n *\n * @remarks\n * The source of truth for this manifest is `manifest.json` in the same directory.\n * This file loads the JSON and exports it with proper TypeScript typing.\n */\nexport const analyticsManifest: PluginManifest = JSON.parse(\n readFileSync(join(__dirname, \"manifest.json\"), \"utf-8\"),\n) as PluginManifest;\n"],"mappings":";;;;;AAKA,MAAM,YAAY,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;;;;AAYzD,MAAa,oBAAoC,KAAK,MACpD,aAAa,KAAK,WAAW,gBAAgB,EAAE,QAAQ,CACxD"}
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
3
+ "name": "analytics",
4
+ "displayName": "Analytics Plugin",
5
+ "description": "SQL query execution against Databricks SQL Warehouses",
6
+ "resources": {
7
+ "required": [
8
+ {
9
+ "type": "sql_warehouse",
10
+ "alias": "SQL Warehouse",
11
+ "resourceKey": "sql-warehouse",
12
+ "description": "SQL Warehouse for executing analytics queries",
13
+ "permission": "CAN_USE",
14
+ "fields": {
15
+ "id": {
16
+ "env": "DATABRICKS_WAREHOUSE_ID",
17
+ "description": "SQL Warehouse ID"
18
+ }
19
+ }
20
+ }
21
+ ],
22
+ "optional": []
23
+ },
24
+ "config": {
25
+ "schema": {
26
+ "type": "object",
27
+ "properties": {
28
+ "timeout": {
29
+ "type": "number",
30
+ "default": 30000,
31
+ "description": "Query execution timeout in milliseconds"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
@@ -1,5 +1,7 @@
1
+ import { analyticsManifest } from "./analytics/manifest.js";
1
2
  import { AnalyticsPlugin, analytics } from "./analytics/analytics.js";
2
3
  import "./analytics/index.js";
4
+ import { serverManifest } from "./server/manifest.js";
3
5
  import { ServerPlugin, server } from "./server/index.js";
4
6
 
5
7
  export { };
@@ -1,5 +1,6 @@
1
1
  import { PluginPhase, TelemetryOptions, ToPlugin } from "../../shared/src/plugin.js";
2
2
  import { Plugin } from "../../plugin/plugin.js";
3
+ import { PluginManifest } from "../../registry/types.js";
3
4
  import { ServerConfig } from "./types.js";
4
5
  import express from "express";
5
6
  import { Server } from "node:http";
@@ -26,8 +27,9 @@ declare class ServerPlugin extends Plugin {
26
27
  host: string;
27
28
  port: number;
28
29
  };
30
+ /** Plugin manifest declaring metadata and resource requirements */
31
+ static manifest: PluginManifest;
29
32
  name: "server";
30
- protected envVars: string[];
31
33
  private serverApplication;
32
34
  private server;
33
35
  private viteDevServer?;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;AAmCA;;;;;;;;;;;AA2HmB,cA3HN,YAAA,SAAqB,MAAA,CA2HP;SAjEF,cAAQ,EAAA;IAAhB,SAAA,EAAA,OAAA;IAsPM,IAAA,EAAA,MAAQ;IAxMhB,IAAA,EAAA,MAAA;;;EAxGyB,OAAA,QAAA,EAAd,cAAc;EAiU3B,IAAA,EAAA,QAGZ;EAAA,QAAA,iBAAA;UAHkB,MAAA;UAAA,aAAA;UAAA,sBAAA;EAAA,UAAA,MAAA,EAlTS,YAkTT;;gBAhTH;sBAEM;;WAaT;;;;;;;;;gBAAA;;;;;;;;;;;;WA0BI,QAAQ,OAAA,CAAQ;;;;;;;;;eA8ClB;;;;;;;;mBAmBI,OAAA,CAAQ;;;;;;;;;;;;;;;;;;;;;;;;iBAjEV,QAAQ,OAAA,CAAQ;;qBAsPV,OAAA,CAAQ;;qBAxMhB;;;;;;;;;kBAAU;;;;;;;cAyNZ,QAAM,gBAAA,cAAA"}
@@ -1,11 +1,12 @@
1
- import { instrumentations } from "../../telemetry/instrumentations.js";
2
1
  import { createLogger } from "../../logging/logger.js";
3
- import "../../telemetry/index.js";
4
2
  import { ServerError } from "../../errors/server.js";
5
3
  import { init_errors } from "../../errors/index.js";
4
+ import { instrumentations } from "../../telemetry/instrumentations.js";
5
+ import "../../telemetry/index.js";
6
6
  import { Plugin } from "../../plugin/plugin.js";
7
7
  import { toPlugin } from "../../plugin/to-plugin.js";
8
8
  import "../../plugin/index.js";
9
+ import { serverManifest } from "./manifest.js";
9
10
  import { RemoteTunnelController } from "./remote-tunnel/remote-tunnel-controller.js";
10
11
  import { getRoutes } from "./utils.js";
11
12
  import { StaticServer } from "./static-server.js";
@@ -39,8 +40,9 @@ var ServerPlugin = class ServerPlugin extends Plugin {
39
40
  host: process.env.FLASK_RUN_HOST || "0.0.0.0",
40
41
  port: Number(process.env.DATABRICKS_APP_PORT) || 8e3
41
42
  };
43
+ /** Plugin manifest declaring metadata and resource requirements */
44
+ static manifest = serverManifest;
42
45
  name = "server";
43
- envVars = [];
44
46
  serverApplication;
45
47
  server;
46
48
  viteDevServer;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\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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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\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 { serverManifest } from \"./manifest\";\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 /** Plugin manifest declaring metadata and resource requirements */\n static manifest = serverManifest;\n\n public name = \"server\" as const;\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\n// Export manifest and types\nexport { serverManifest } from \"./manifest\";\nexport type { ServerConfig } from \"./types\";\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { dirname, join } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ //#region src/plugins/server/manifest.ts
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ /**
8
+ * Server plugin manifest.
9
+ *
10
+ * The server plugin doesn't require any Databricks resources - it only
11
+ * provides HTTP server functionality and static file serving.
12
+ *
13
+ * @remarks
14
+ * The source of truth for this manifest is `manifest.json` in the same directory.
15
+ * This file loads the JSON and exports it with proper TypeScript typing.
16
+ */
17
+ const serverManifest = JSON.parse(readFileSync(join(__dirname, "manifest.json"), "utf-8"));
18
+
19
+ //#endregion
20
+ export { serverManifest };
21
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","names":[],"sources":["../../../src/plugins/server/manifest.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { PluginManifest } from \"../../registry\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Server plugin manifest.\n *\n * The server plugin doesn't require any Databricks resources - it only\n * provides HTTP server functionality and static file serving.\n *\n * @remarks\n * The source of truth for this manifest is `manifest.json` in the same directory.\n * This file loads the JSON and exports it with proper TypeScript typing.\n */\nexport const serverManifest: PluginManifest = JSON.parse(\n readFileSync(join(__dirname, \"manifest.json\"), \"utf-8\"),\n) as PluginManifest;\n"],"mappings":";;;;;AAKA,MAAM,YAAY,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;;;;AAYzD,MAAa,iBAAiC,KAAK,MACjD,aAAa,KAAK,WAAW,gBAAgB,EAAE,QAAQ,CACxD"}
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
3
+ "name": "server",
4
+ "displayName": "Server Plugin",
5
+ "description": "HTTP server with Express, static file serving, and Vite dev mode support",
6
+ "resources": {
7
+ "required": [],
8
+ "optional": []
9
+ },
10
+ "config": {
11
+ "schema": {
12
+ "type": "object",
13
+ "properties": {
14
+ "autoStart": {
15
+ "type": "boolean",
16
+ "default": true,
17
+ "description": "Automatically start the server on plugin setup"
18
+ },
19
+ "host": {
20
+ "type": "string",
21
+ "default": "0.0.0.0",
22
+ "description": "Host address to bind the server to"
23
+ },
24
+ "port": {
25
+ "type": "number",
26
+ "default": 8000,
27
+ "description": "Port number for the server"
28
+ },
29
+ "staticPath": {
30
+ "type": "string",
31
+ "description": "Path to static files directory (auto-detected if not provided)"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,5 @@
1
+ import { PERMISSIONS_BY_TYPE, PERMISSION_HIERARCHY_BY_TYPE, ResourceType } from "./types.js";
2
+ import { getPluginManifest, getResourceRequirements } from "./manifest-loader.js";
3
+ import { ResourceRegistry } from "./resource-registry.js";
4
+
5
+ export { };
@@ -0,0 +1,44 @@
1
+ import { PluginConstructor } from "../shared/src/plugin.js";
2
+ import { PluginManifest, ResourceFieldEntry, ResourcePermission, ResourceType } from "./types.js";
3
+
4
+ //#region src/registry/manifest-loader.d.ts
5
+
6
+ /**
7
+ * Loads and validates the manifest from a plugin constructor.
8
+ * Normalizes string type/permission to strict ResourceType/ResourcePermission.
9
+ *
10
+ * @param plugin - The plugin constructor class
11
+ * @returns The validated, normalized plugin manifest
12
+ * @throws {ConfigurationError} If the manifest is missing, invalid, or has invalid resource type/permission
13
+ */
14
+ declare function getPluginManifest(plugin: PluginConstructor): PluginManifest;
15
+ /**
16
+ * Gets the resource requirements from a plugin's manifest.
17
+ *
18
+ * Combines required and optional resources into a single array with the
19
+ * `required` flag set appropriately.
20
+ *
21
+ * @param plugin - The plugin constructor class
22
+ * @returns Combined array of required and optional resources
23
+ * @throws {ConfigurationError} If the plugin manifest is missing or invalid
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const resources = getResourceRequirements(AnalyticsPlugin);
28
+ * for (const resource of resources) {
29
+ * console.log(`${resource.type}: ${resource.description} (required: ${resource.required})`);
30
+ * }
31
+ * ```
32
+ */
33
+ declare function getResourceRequirements(plugin: PluginConstructor): {
34
+ required: boolean;
35
+ type: ResourceType;
36
+ alias: string;
37
+ resourceKey: string;
38
+ description: string;
39
+ permission: ResourcePermission;
40
+ fields: Record<string, ResourceFieldEntry>;
41
+ }[];
42
+ //#endregion
43
+ export { getPluginManifest, getResourceRequirements };
44
+ //# sourceMappingURL=manifest-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-loader.d.ts","names":[],"sources":["../../src/registry/manifest-loader.ts"],"sourcesContent":[],"mappings":";;;;;;AA4DA;;;;;AA4FA;;AAAgD,iBA5FhC,iBAAA,CA4FgC,MAAA,EA5FN,iBA4FM,CAAA,EA5Fc,cA4Fd;;;;;;;;;;;;;;;;;;;iBAAhC,uBAAA,SAAgC"}
@@ -0,0 +1,97 @@
1
+ import { createLogger } from "../logging/logger.js";
2
+ import { ConfigurationError } from "../errors/configuration.js";
3
+ import { init_errors } from "../errors/index.js";
4
+ import { PERMISSIONS_BY_TYPE, ResourceType } from "./types.js";
5
+
6
+ //#region src/registry/manifest-loader.ts
7
+ init_errors();
8
+ const logger = createLogger("manifest-loader");
9
+ function normalizeType(s) {
10
+ const v = Object.values(ResourceType).find((x) => x === s);
11
+ if (v !== void 0) return v;
12
+ throw new ConfigurationError(`Invalid resource type: "${s}". Valid: ${Object.values(ResourceType).join(", ")}`);
13
+ }
14
+ function normalizePermission(type, s) {
15
+ const allowed = PERMISSIONS_BY_TYPE[type];
16
+ if (allowed.includes(s)) return s;
17
+ throw new ConfigurationError(`Invalid permission "${s}" for type ${type}. Valid: ${allowed.join(", ")}`);
18
+ }
19
+ function normalizeResource(r) {
20
+ const type = normalizeType(r.type);
21
+ const permission = normalizePermission(type, r.permission);
22
+ return {
23
+ ...r,
24
+ type,
25
+ permission,
26
+ required: false
27
+ };
28
+ }
29
+ /**
30
+ * Loads and validates the manifest from a plugin constructor.
31
+ * Normalizes string type/permission to strict ResourceType/ResourcePermission.
32
+ *
33
+ * @param plugin - The plugin constructor class
34
+ * @returns The validated, normalized plugin manifest
35
+ * @throws {ConfigurationError} If the manifest is missing, invalid, or has invalid resource type/permission
36
+ */
37
+ function getPluginManifest(plugin) {
38
+ const pluginName = plugin.name || "unknown";
39
+ if (!plugin.manifest) throw new ConfigurationError(`Plugin ${pluginName} is missing a manifest. All plugins must declare a static manifest property.`);
40
+ const raw = plugin.manifest;
41
+ if (!raw.name || typeof raw.name !== "string") throw new ConfigurationError(`Plugin ${pluginName} manifest has missing or invalid 'name' field`);
42
+ if (!raw.displayName || typeof raw.displayName !== "string") throw new ConfigurationError(`Plugin ${raw.name} manifest has missing or invalid 'displayName' field`);
43
+ if (!raw.description || typeof raw.description !== "string") throw new ConfigurationError(`Plugin ${raw.name} manifest has missing or invalid 'description' field`);
44
+ if (!raw.resources) throw new ConfigurationError(`Plugin ${raw.name} manifest is missing 'resources' field`);
45
+ if (!Array.isArray(raw.resources.required)) throw new ConfigurationError(`Plugin ${raw.name} manifest has invalid 'resources.required' field (expected array)`);
46
+ if (raw.resources.optional !== void 0 && !Array.isArray(raw.resources.optional)) throw new ConfigurationError(`Plugin ${raw.name} manifest has invalid 'resources.optional' field (expected array)`);
47
+ const required = raw.resources.required.map((r) => {
48
+ const { required: _, ...rest } = normalizeResource(r);
49
+ return rest;
50
+ });
51
+ const optional = (raw.resources.optional || []).map((r) => {
52
+ const { required: _, ...rest } = normalizeResource(r);
53
+ return rest;
54
+ });
55
+ logger.debug("Loaded manifest for plugin %s: %d required resources, %d optional resources", raw.name, required.length, optional.length);
56
+ return {
57
+ ...raw,
58
+ resources: {
59
+ required,
60
+ optional
61
+ }
62
+ };
63
+ }
64
+ /**
65
+ * Gets the resource requirements from a plugin's manifest.
66
+ *
67
+ * Combines required and optional resources into a single array with the
68
+ * `required` flag set appropriately.
69
+ *
70
+ * @param plugin - The plugin constructor class
71
+ * @returns Combined array of required and optional resources
72
+ * @throws {ConfigurationError} If the plugin manifest is missing or invalid
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const resources = getResourceRequirements(AnalyticsPlugin);
77
+ * for (const resource of resources) {
78
+ * console.log(`${resource.type}: ${resource.description} (required: ${resource.required})`);
79
+ * }
80
+ * ```
81
+ */
82
+ function getResourceRequirements(plugin) {
83
+ const manifest = getPluginManifest(plugin);
84
+ const required = manifest.resources.required.map((r) => ({
85
+ ...r,
86
+ required: true
87
+ }));
88
+ const optional = (manifest.resources.optional || []).map((r) => ({
89
+ ...r,
90
+ required: false
91
+ }));
92
+ return [...required, ...optional];
93
+ }
94
+
95
+ //#endregion
96
+ export { getPluginManifest, getResourceRequirements };
97
+ //# sourceMappingURL=manifest-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-loader.js","names":[],"sources":["../../src/registry/manifest-loader.ts"],"sourcesContent":["import type { PluginConstructor } from \"shared\";\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type {\n PluginManifest,\n ResourcePermission,\n ResourceRequirement,\n} from \"./types\";\nimport { PERMISSIONS_BY_TYPE, ResourceType } from \"./types\";\n\nconst logger = createLogger(\"manifest-loader\");\n\n/** Loose resource from shared/manifest (string type and permission). */\ninterface LooseResource {\n type: string;\n alias: string;\n resourceKey: string;\n description: string;\n permission: string;\n fields: Record<string, { env: string; description?: string }>;\n}\n\nfunction normalizeType(s: string): ResourceType {\n const v = Object.values(ResourceType).find((x) => x === s);\n if (v !== undefined) return v;\n throw new ConfigurationError(\n `Invalid resource type: \"${s}\". Valid: ${Object.values(ResourceType).join(\", \")}`,\n );\n}\n\nfunction normalizePermission(\n type: ResourceType,\n s: string,\n): ResourcePermission {\n const allowed = PERMISSIONS_BY_TYPE[type];\n if (allowed.includes(s as ResourcePermission)) return s as ResourcePermission;\n throw new ConfigurationError(\n `Invalid permission \"${s}\" for type ${type}. Valid: ${allowed.join(\", \")}`,\n );\n}\n\nfunction normalizeResource(r: LooseResource): ResourceRequirement {\n const type = normalizeType(r.type);\n const permission = normalizePermission(type, r.permission);\n return {\n ...r,\n type,\n permission,\n required: false,\n };\n}\n\n/**\n * Loads and validates the manifest from a plugin constructor.\n * Normalizes string type/permission to strict ResourceType/ResourcePermission.\n *\n * @param plugin - The plugin constructor class\n * @returns The validated, normalized plugin manifest\n * @throws {ConfigurationError} If the manifest is missing, invalid, or has invalid resource type/permission\n */\nexport function getPluginManifest(plugin: PluginConstructor): PluginManifest {\n const pluginName = plugin.name || \"unknown\";\n\n if (!plugin.manifest) {\n throw new ConfigurationError(\n `Plugin ${pluginName} is missing a manifest. All plugins must declare a static manifest property.`,\n );\n }\n\n const raw = plugin.manifest;\n\n if (!raw.name || typeof raw.name !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${pluginName} manifest has missing or invalid 'name' field`,\n );\n }\n\n if (!raw.displayName || typeof raw.displayName !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'displayName' field`,\n );\n }\n\n if (!raw.description || typeof raw.description !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'description' field`,\n );\n }\n\n if (!raw.resources) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest is missing 'resources' field`,\n );\n }\n\n if (!Array.isArray(raw.resources.required)) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.required' field (expected array)`,\n );\n }\n\n if (\n raw.resources.optional !== undefined &&\n !Array.isArray(raw.resources.optional)\n ) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.optional' field (expected array)`,\n );\n }\n\n const required = raw.resources.required.map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n const optional = (raw.resources.optional || []).map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n\n logger.debug(\n \"Loaded manifest for plugin %s: %d required resources, %d optional resources\",\n raw.name,\n required.length,\n optional.length,\n );\n\n return {\n ...raw,\n resources: { required, optional },\n };\n}\n\n/**\n * Gets the resource requirements from a plugin's manifest.\n *\n * Combines required and optional resources into a single array with the\n * `required` flag set appropriately.\n *\n * @param plugin - The plugin constructor class\n * @returns Combined array of required and optional resources\n * @throws {ConfigurationError} If the plugin manifest is missing or invalid\n *\n * @example\n * ```typescript\n * const resources = getResourceRequirements(AnalyticsPlugin);\n * for (const resource of resources) {\n * console.log(`${resource.type}: ${resource.description} (required: ${resource.required})`);\n * }\n * ```\n */\nexport function getResourceRequirements(plugin: PluginConstructor) {\n const manifest = getPluginManifest(plugin);\n\n const required = manifest.resources.required.map((r) => ({\n ...r,\n required: true,\n }));\n const optional = (manifest.resources.optional || []).map((r) => ({\n ...r,\n required: false,\n }));\n\n return [...required, ...optional];\n}\n\n/**\n * Validates a manifest object structure.\n *\n * @param manifest - The manifest object to validate\n * @returns true if the manifest is valid, false otherwise\n *\n * @internal\n */\nexport function isValidManifest(manifest: unknown): manifest is PluginManifest {\n if (!manifest || typeof manifest !== \"object\") {\n return false;\n }\n\n const m = manifest as Record<string, unknown>;\n\n // Check required fields\n if (typeof m.name !== \"string\") return false;\n if (typeof m.displayName !== \"string\") return false;\n if (typeof m.description !== \"string\") return false;\n\n // Check resources structure\n if (!m.resources || typeof m.resources !== \"object\") return false;\n\n const resources = m.resources as Record<string, unknown>;\n if (!Array.isArray(resources.required)) return false;\n\n // Optional field can be missing or must be an array\n if (resources.optional !== undefined && !Array.isArray(resources.optional)) {\n return false;\n }\n\n return true;\n}\n"],"mappings":";;;;;;aAC+C;AAS/C,MAAM,SAAS,aAAa,kBAAkB;AAY9C,SAAS,cAAc,GAAyB;CAC9C,MAAM,IAAI,OAAO,OAAO,aAAa,CAAC,MAAM,MAAM,MAAM,EAAE;AAC1D,KAAI,MAAM,OAAW,QAAO;AAC5B,OAAM,IAAI,mBACR,2BAA2B,EAAE,YAAY,OAAO,OAAO,aAAa,CAAC,KAAK,KAAK,GAChF;;AAGH,SAAS,oBACP,MACA,GACoB;CACpB,MAAM,UAAU,oBAAoB;AACpC,KAAI,QAAQ,SAAS,EAAwB,CAAE,QAAO;AACtD,OAAM,IAAI,mBACR,uBAAuB,EAAE,aAAa,KAAK,WAAW,QAAQ,KAAK,KAAK,GACzE;;AAGH,SAAS,kBAAkB,GAAuC;CAChE,MAAM,OAAO,cAAc,EAAE,KAAK;CAClC,MAAM,aAAa,oBAAoB,MAAM,EAAE,WAAW;AAC1D,QAAO;EACL,GAAG;EACH;EACA;EACA,UAAU;EACX;;;;;;;;;;AAWH,SAAgB,kBAAkB,QAA2C;CAC3E,MAAM,aAAa,OAAO,QAAQ;AAElC,KAAI,CAAC,OAAO,SACV,OAAM,IAAI,mBACR,UAAU,WAAW,8EACtB;CAGH,MAAM,MAAM,OAAO;AAEnB,KAAI,CAAC,IAAI,QAAQ,OAAO,IAAI,SAAS,SACnC,OAAM,IAAI,mBACR,UAAU,WAAW,+CACtB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,UACP,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,wCACpB;AAGH,KAAI,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CACxC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;AAGH,KACE,IAAI,UAAU,aAAa,UAC3B,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CAEtC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;CAGH,MAAM,WAAW,IAAI,UAAU,SAAS,KAAK,MAAM;EAEjD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;CACF,MAAM,YAAY,IAAI,UAAU,YAAY,EAAE,EAAE,KAAK,MAAM;EAEzD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;AAEF,QAAO,MACL,+EACA,IAAI,MACJ,SAAS,QACT,SAAS,OACV;AAED,QAAO;EACL,GAAG;EACH,WAAW;GAAE;GAAU;GAAU;EAClC;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,wBAAwB,QAA2B;CACjE,MAAM,WAAW,kBAAkB,OAAO;CAE1C,MAAM,WAAW,SAAS,UAAU,SAAS,KAAK,OAAO;EACvD,GAAG;EACH,UAAU;EACX,EAAE;CACH,MAAM,YAAY,SAAS,UAAU,YAAY,EAAE,EAAE,KAAK,OAAO;EAC/D,GAAG;EACH,UAAU;EACX,EAAE;AAEH,QAAO,CAAC,GAAG,UAAU,GAAG,SAAS"}
@@ -0,0 +1,133 @@
1
+ import { PluginConstructor, PluginData } from "../shared/src/plugin.js";
2
+ import { ResourceEntry, ResourceRequirement, ValidationResult } from "./types.js";
3
+
4
+ //#region src/registry/resource-registry.d.ts
5
+
6
+ /**
7
+ * Central registry for tracking plugin resource requirements.
8
+ * Deduplication uses type + resourceKey (machine-stable); alias is for display only.
9
+ */
10
+ declare class ResourceRegistry {
11
+ private resources;
12
+ /**
13
+ * Registers a resource requirement for a plugin.
14
+ * If a resource with the same type+resourceKey already exists, merges them:
15
+ * - Combines plugin names (comma-separated)
16
+ * - Uses the most permissive permission (per-type hierarchy)
17
+ * - Marks as required if any plugin requires it
18
+ * - Combines descriptions if they differ
19
+ * - Merges fields; warns when same field name uses different env vars
20
+ *
21
+ * @param plugin - Name of the plugin registering the resource
22
+ * @param resource - Resource requirement specification
23
+ */
24
+ register(plugin: string, resource: ResourceRequirement): void;
25
+ /**
26
+ * Collects and registers resource requirements from an array of plugins.
27
+ * For each plugin, loads its manifest (required) and runtime resource requirements.
28
+ *
29
+ * @param rawPlugins - Array of plugin data entries from createApp configuration
30
+ * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid
31
+ */
32
+ collectResources(rawPlugins: PluginData<PluginConstructor, unknown, string>[]): void;
33
+ /**
34
+ * Merges a new resource requirement with an existing entry.
35
+ * Applies intelligent merging logic for conflicting properties.
36
+ */
37
+ private mergeResources;
38
+ /**
39
+ * Retrieves all registered resources.
40
+ * Returns a copy of the array to prevent external mutations.
41
+ *
42
+ * @returns Array of all registered resource entries
43
+ */
44
+ getAll(): ResourceEntry[];
45
+ /**
46
+ * Gets a specific resource by type and resourceKey (dedup key).
47
+ *
48
+ * @param type - Resource type
49
+ * @param resourceKey - Stable machine key (not alias; alias is for display only)
50
+ * @returns The resource entry if found, undefined otherwise
51
+ */
52
+ get(type: string, resourceKey: string): ResourceEntry | undefined;
53
+ /**
54
+ * Clears all registered resources.
55
+ * Useful for testing or when rebuilding the registry.
56
+ */
57
+ clear(): void;
58
+ /**
59
+ * Returns the number of registered resources.
60
+ */
61
+ size(): number;
62
+ /**
63
+ * Gets all resources required by a specific plugin.
64
+ *
65
+ * @param pluginName - Name of the plugin
66
+ * @returns Array of resources where the plugin is listed as a requester
67
+ */
68
+ getByPlugin(pluginName: string): ResourceEntry[];
69
+ /**
70
+ * Gets all required resources (where required=true).
71
+ *
72
+ * @returns Array of required resource entries
73
+ */
74
+ getRequired(): ResourceEntry[];
75
+ /**
76
+ * Gets all optional resources (where required=false).
77
+ *
78
+ * @returns Array of optional resource entries
79
+ */
80
+ getOptional(): ResourceEntry[];
81
+ /**
82
+ * Validates all registered resources against the environment.
83
+ *
84
+ * Checks each resource's field environment variables to determine if it's resolved.
85
+ * Updates the `resolved` and `values` fields on each resource entry.
86
+ *
87
+ * Only required resources affect the `valid` status - optional resources
88
+ * are checked but don't cause validation failure.
89
+ *
90
+ * @returns ValidationResult with validity status, missing resources, and all resources
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const registry = ResourceRegistry.getInstance();
95
+ * const result = registry.validate();
96
+ *
97
+ * if (!result.valid) {
98
+ * console.error("Missing resources:", result.missing.map(r => Object.values(r.fields).map(f => f.env)));
99
+ * }
100
+ * ```
101
+ */
102
+ validate(): ValidationResult;
103
+ /**
104
+ * Validates all registered resources and enforces the result.
105
+ *
106
+ * - In production: throws a {@link ConfigurationError} if any required resources are missing.
107
+ * - In development (`NODE_ENV=development`): logs a warning but continues, unless
108
+ * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.
109
+ * - When all resources are valid: logs a debug message with the count.
110
+ *
111
+ * @returns ValidationResult with validity status, missing resources, and all resources
112
+ * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true
113
+ */
114
+ enforceValidation(): ValidationResult;
115
+ /**
116
+ * Formats missing resources into a human-readable error message.
117
+ *
118
+ * @param missing - Array of missing resource entries
119
+ * @returns Formatted error message string
120
+ */
121
+ static formatMissingResources(missing: ResourceEntry[]): string;
122
+ /**
123
+ * Formats a highly visible warning banner for dev-mode missing resources.
124
+ * Uses box drawing to ensure the message is impossible to miss in scrolling logs.
125
+ *
126
+ * @param missing - Array of missing resource entries
127
+ * @returns Formatted banner string
128
+ */
129
+ static formatDevWarningBanner(missing: ResourceEntry[]): string;
130
+ }
131
+ //#endregion
132
+ export { ResourceRegistry };
133
+ //# sourceMappingURL=resource-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-registry.d.ts","names":[],"sources":["../../src/registry/resource-registry.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAsbgD,cAjYnC,gBAAA,CAiYmC;EAAa,QAAA,SAAA;;;;;;;;;;;;;qCAlXjB;;;;;;;;+BA4B5B,WAAW;;;;;;;;;;;;YAqIR;;;;;;;;0CAW8B;;;;;;;;;;;;;;;;mCAyBP;;;;;;iBAWlB;;;;;;iBASA;;;;;;;;;;;;;;;;;;;;;;cAyBH;;;;;;;;;;;;uBA8DS;;;;;;;yCA6CkB;;;;;;;;yCAqBA"}