@databricks/appkit 0.61.1 → 0.63.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 (194) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/agents/databricks.d.ts.map +1 -1
  3. package/dist/agents/databricks.js.map +1 -1
  4. package/dist/agents/supervisor-api.d.ts.map +1 -1
  5. package/dist/agents/supervisor-api.js.map +1 -1
  6. package/dist/app/index.d.ts.map +1 -1
  7. package/dist/app/index.js.map +1 -1
  8. package/dist/appkit/package.js +1 -1
  9. package/dist/cache/index.d.ts.map +1 -1
  10. package/dist/cache/index.js.map +1 -1
  11. package/dist/cache/storage/memory.js.map +1 -1
  12. package/dist/cache/storage/persistent.js.map +1 -1
  13. package/dist/cli/commands/codemod/index.js.map +1 -1
  14. package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
  15. package/dist/cli/commands/docs.js.map +1 -1
  16. package/dist/cli/commands/doctor/bundle.js.map +1 -1
  17. package/dist/cli/commands/doctor/index.js.map +1 -1
  18. package/dist/cli/commands/doctor/report.js.map +1 -1
  19. package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
  20. package/dist/cli/commands/doctor/run.js.map +1 -1
  21. package/dist/cli/commands/generate-types.js.map +1 -1
  22. package/dist/cli/commands/lint.js.map +1 -1
  23. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  24. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  25. package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
  26. package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
  27. package/dist/cli/commands/plugin/index.js.map +1 -1
  28. package/dist/cli/commands/plugin/list/list.js.map +1 -1
  29. package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
  30. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  31. package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
  32. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  33. package/dist/cli/commands/registry/add.js.map +1 -1
  34. package/dist/cli/commands/registry/client.js.map +1 -1
  35. package/dist/cli/commands/registry/config-writer.js.map +1 -1
  36. package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
  37. package/dist/cli/commands/registry/env-writer.js.map +1 -1
  38. package/dist/cli/commands/registry/index.js.map +1 -1
  39. package/dist/cli/commands/registry/info.js.map +1 -1
  40. package/dist/cli/commands/registry/list.js.map +1 -1
  41. package/dist/cli/commands/registry/requirements.js.map +1 -1
  42. package/dist/cli/commands/registry/server-register.js.map +1 -1
  43. package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
  44. package/dist/cli/commands/setup.js.map +1 -1
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/connectors/files/client.js.map +1 -1
  47. package/dist/connectors/lakebase/index.d.ts.map +1 -1
  48. package/dist/connectors/lakebase/index.js.map +1 -1
  49. package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -1
  50. package/dist/connectors/lakebase/pool-manager.js.map +1 -1
  51. package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
  52. package/dist/connectors/lakebase/routing-pool.js.map +1 -1
  53. package/dist/connectors/mcp/client.d.ts.map +1 -1
  54. package/dist/connectors/mcp/client.js.map +1 -1
  55. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  56. package/dist/context/client-options.js.map +1 -1
  57. package/dist/context/execution-context.d.ts.map +1 -1
  58. package/dist/context/execution-context.js.map +1 -1
  59. package/dist/context/index.d.ts +1 -1
  60. package/dist/context/service-context.d.ts +55 -3
  61. package/dist/context/service-context.d.ts.map +1 -1
  62. package/dist/context/service-context.js.map +1 -1
  63. package/dist/core/agent/build-toolkit.js.map +1 -1
  64. package/dist/core/agent/load-agents.d.ts.map +1 -1
  65. package/dist/core/agent/load-agents.js.map +1 -1
  66. package/dist/core/agent/run-agent.d.ts.map +1 -1
  67. package/dist/core/agent/run-agent.js.map +1 -1
  68. package/dist/core/agent/toolkit-resolver.js.map +1 -1
  69. package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
  70. package/dist/core/agent/tools/define-tool.js.map +1 -1
  71. package/dist/core/agent/tools/tool.d.ts.map +1 -1
  72. package/dist/core/agent/tools/tool.js.map +1 -1
  73. package/dist/core/agent/types.d.ts.map +1 -1
  74. package/dist/core/agent/types.js.map +1 -1
  75. package/dist/core/appkit.d.ts.map +1 -1
  76. package/dist/core/appkit.js.map +1 -1
  77. package/dist/core/lifecycle-manager.js.map +1 -1
  78. package/dist/core/plugin-context.d.ts +13 -0
  79. package/dist/core/plugin-context.d.ts.map +1 -1
  80. package/dist/core/plugin-context.js +12 -1
  81. package/dist/core/plugin-context.js.map +1 -1
  82. package/dist/errors/configuration.d.ts.map +1 -1
  83. package/dist/errors/configuration.js.map +1 -1
  84. package/dist/logging/logger.js.map +1 -1
  85. package/dist/logging/wide-event-emitter.js.map +1 -1
  86. package/dist/plugin/dev-reader.d.ts.map +1 -1
  87. package/dist/plugin/dev-reader.js.map +1 -1
  88. package/dist/plugin/interceptors/cache.js.map +1 -1
  89. package/dist/plugin/interceptors/retry.js.map +1 -1
  90. package/dist/plugin/interceptors/telemetry.js.map +1 -1
  91. package/dist/plugin/plugin.d.ts +2 -2
  92. package/dist/plugin/plugin.d.ts.map +1 -1
  93. package/dist/plugin/plugin.js.map +1 -1
  94. package/dist/plugins/agents/agents.d.ts.map +1 -1
  95. package/dist/plugins/agents/agents.js.map +1 -1
  96. package/dist/plugins/agents/event-translator.js.map +1 -1
  97. package/dist/plugins/agents/thread-store.js.map +1 -1
  98. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  99. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  100. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  101. package/dist/plugins/analytics/analytics.js.map +1 -1
  102. package/dist/plugins/analytics/mv/cache.js.map +1 -1
  103. package/dist/plugins/analytics/mv/constants.js.map +1 -1
  104. package/dist/plugins/analytics/mv/formatters.js.map +1 -1
  105. package/dist/plugins/analytics/mv/metadata.js.map +1 -1
  106. package/dist/plugins/analytics/mv/registry.js.map +1 -1
  107. package/dist/plugins/analytics/mv/schemas.js.map +1 -1
  108. package/dist/plugins/analytics/query.js.map +1 -1
  109. package/dist/plugins/analytics/result-delivery.js.map +1 -1
  110. package/dist/plugins/files/helpers.js.map +1 -1
  111. package/dist/plugins/files/plugin.d.ts.map +1 -1
  112. package/dist/plugins/files/plugin.js.map +1 -1
  113. package/dist/plugins/files/types.d.ts.map +1 -1
  114. package/dist/plugins/genie/genie.d.ts.map +1 -1
  115. package/dist/plugins/genie/genie.js.map +1 -1
  116. package/dist/plugins/jobs/plugin.d.ts.map +1 -1
  117. package/dist/plugins/jobs/plugin.js.map +1 -1
  118. package/dist/plugins/jobs/types.d.ts.map +1 -1
  119. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  120. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  121. package/dist/plugins/lakebase/types.d.ts.map +1 -1
  122. package/dist/plugins/server/base-server.js.map +1 -1
  123. package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
  124. package/dist/plugins/server/index.d.ts.map +1 -1
  125. package/dist/plugins/server/index.js.map +1 -1
  126. package/dist/plugins/server/react-source-loc-vite-plugin.js.map +1 -1
  127. package/dist/plugins/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
  128. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  129. package/dist/plugins/server/static-server.js.map +1 -1
  130. package/dist/plugins/server/utils.js.map +1 -1
  131. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  132. package/dist/plugins/serving/schema-filter.js.map +1 -1
  133. package/dist/plugins/serving/serving.d.ts.map +1 -1
  134. package/dist/plugins/serving/serving.js.map +1 -1
  135. package/dist/plugins/ui-variants/index.js.map +1 -1
  136. package/dist/registry/manifest-loader.d.ts.map +1 -1
  137. package/dist/registry/manifest-loader.js.map +1 -1
  138. package/dist/registry/resource-registry.d.ts.map +1 -1
  139. package/dist/registry/resource-registry.js.map +1 -1
  140. package/dist/registry/types.d.ts.map +1 -1
  141. package/dist/registry/types.js.map +1 -1
  142. package/dist/schemas/manifest.d.ts.map +1 -1
  143. package/dist/schemas/manifest.js.map +1 -1
  144. package/dist/schemas/metric-fqn.js.map +1 -1
  145. package/dist/shared/src/plugin.d.ts.map +1 -1
  146. package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
  147. package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
  148. package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -1
  149. package/dist/shared/src/schemas/metric-source.js.map +1 -1
  150. package/dist/shared/src/sse/analytics.js.map +1 -1
  151. package/dist/shared/src/workspace-client/legacy.js.map +1 -1
  152. package/dist/shared/src/workspace-client/types.d.ts.map +1 -1
  153. package/dist/stream/sse-writer.js.map +1 -1
  154. package/dist/stream/stream-manager.d.ts.map +1 -1
  155. package/dist/stream/stream-manager.js.map +1 -1
  156. package/dist/stream/types.js.map +1 -1
  157. package/dist/telemetry/instrumentations.js.map +1 -1
  158. package/dist/telemetry/telemetry-manager.js.map +1 -1
  159. package/dist/telemetry/telemetry-provider.js.map +1 -1
  160. package/dist/telemetry/trace-sampler.js.map +1 -1
  161. package/dist/testing/expect-stream.d.ts +111 -0
  162. package/dist/testing/expect-stream.d.ts.map +1 -0
  163. package/dist/testing/expect-stream.js +154 -0
  164. package/dist/testing/expect-stream.js.map +1 -0
  165. package/dist/testing/fixtures.d.ts +257 -0
  166. package/dist/testing/fixtures.d.ts.map +1 -0
  167. package/dist/testing/fixtures.js +385 -0
  168. package/dist/testing/fixtures.js.map +1 -0
  169. package/dist/testing/index.d.ts +5 -0
  170. package/dist/testing/index.js +5 -0
  171. package/dist/testing/test-plugin-context.d.ts +158 -0
  172. package/dist/testing/test-plugin-context.d.ts.map +1 -0
  173. package/dist/testing/test-plugin-context.js +139 -0
  174. package/dist/testing/test-plugin-context.js.map +1 -0
  175. package/dist/type-generator/cache.js.map +1 -1
  176. package/dist/type-generator/index.js.map +1 -1
  177. package/dist/type-generator/migration.js.map +1 -1
  178. package/dist/type-generator/mv-registry/config.js.map +1 -1
  179. package/dist/type-generator/query-registry.js.map +1 -1
  180. package/dist/type-generator/serving/cache.js.map +1 -1
  181. package/dist/type-generator/serving/generator.js.map +1 -1
  182. package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -1
  183. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  184. package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -1
  185. package/dist/type-generator/serving/vite-plugin.js.map +1 -1
  186. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  187. package/dist/type-generator/vite-plugin.js.map +1 -1
  188. package/dist/workspace-client/legacy.js.map +1 -1
  189. package/dist/workspace-client/types.d.ts.map +1 -1
  190. package/docs/plugins/testing.md +245 -0
  191. package/llms.txt +1 -0
  192. package/package.json +15 -2
  193. package/sbom.cdx.json +1 -1
  194. package/skills/appkit-ui-variants/SKILL.md +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"remote-tunnel-manager.js","names":[],"sources":["../../../../src/plugins/server/remote-tunnel/remote-tunnel-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\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: true,\n secure: true,\n sameSite: \"lax\",\n });\n\n const indexPath = path.join(__dirname, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${getConfigScript()}`);\n\n res.send(html);\n };\n }\n\n /** Setup the dev mode middleware. */\n setup(app: express.Application) {\n app.use(this.devModeMiddleware());\n app.use(REMOTE_TUNNEL_ASSET_PREFIXES, this.assetMiddleware());\n }\n\n static isRemoteServerEnabled() {\n return (\n process.env.NODE_ENV !== \"production\" &&\n process.env.DISABLE_REMOTE_SERVING !== \"true\" &&\n // DATABRICKS_CLIENT_SECRET is set in the .env file for deployed environments\n Boolean(process.env.DATABRICKS_CLIENT_SECRET)\n );\n }\n\n private loadHtmlTemplate(\n filename: string,\n replacements: Record<string, string>,\n ): string {\n const filePath = path.join(__dirname, filename);\n let content = fs.readFileSync(filePath, \"utf-8\");\n\n for (const [key, value] of Object.entries(replacements)) {\n content = content.replaceAll(`{{${key}}}`, value);\n }\n\n return content;\n }\n\n private handleViewerApproval(\n tunnelId: string,\n viewerEmail: string,\n retry: boolean,\n res: express.Response,\n ): express.Response | null {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) {\n return res.status(404).send(\"Tunnel not found\");\n }\n\n if (viewerEmail === tunnel.owner) {\n return null;\n }\n\n if (retry) {\n tunnel.rejectedViewers.delete(viewerEmail);\n }\n\n if (tunnel.rejectedViewers.has(viewerEmail)) {\n const html = this.loadHtmlTemplate(\"denied.html\", { tunnelId });\n return res.status(403).send(html);\n }\n\n if (tunnel.approvedViewers.has(viewerEmail)) {\n return null;\n }\n\n if (!tunnel.pendingRequests.has(viewerEmail)) {\n const requestId = randomUUID();\n tunnel.pendingRequests.add(viewerEmail);\n tunnel.ws.send(\n JSON.stringify({\n type: \"connection:request\",\n requestId,\n viewer: viewerEmail,\n }),\n );\n }\n\n const html = this.loadHtmlTemplate(\"wait.html\", { tunnelId });\n return res.status(200).send(html);\n }\n\n setupWebSocket() {\n this.wss.on(\"connection\", (ws, req) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId = generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return ws.close();\n\n this.tunnels.set(tunnelId, {\n ws,\n owner: email,\n approvedViewers: new Set([email]),\n pendingRequests: new Set(),\n rejectedViewers: new Set(),\n pendingFetches: new Map(),\n pendingFileReads: new Map(),\n waitingForBinaryBody: null,\n });\n\n ws.on(\"message\", (msg, isBinary) => {\n const tunnel = this.tunnels.get(tunnelId);\n if (!tunnel) return;\n\n if (isBinary) {\n if (!tunnel.waitingForBinaryBody) {\n logger.debug(\n \"Received binary message but no requestId is waiting for body\",\n );\n return;\n }\n\n const requestId = tunnel.waitingForBinaryBody;\n const pending = tunnel.pendingFetches.get(requestId);\n\n if (!pending || !pending.metadata) {\n logger.debug(\"Received binary message but pending fetch not found\");\n tunnel.waitingForBinaryBody = null;\n return;\n }\n\n tunnel.waitingForBinaryBody = null;\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(requestId);\n\n pending.resolve({\n status: pending.metadata.status,\n headers: pending.metadata.headers,\n body: msg as Buffer,\n });\n return;\n }\n\n try {\n const data = JSON.parse(msg.toString());\n\n // Validate message structure\n if (!isWebSocketMessage(data)) {\n logger.error(\"Invalid WebSocket message format: %O\", data);\n return;\n }\n\n if (data.type === \"connection:response\") {\n if (tunnel && data.viewer) {\n tunnel.pendingRequests.delete(data.viewer);\n\n if (data.approved) {\n tunnel.approvedViewers.add(data.viewer);\n logger.debug(\n \"✅ Approved %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n } else {\n tunnel.rejectedViewers.add(data.viewer);\n logger.debug(\n \"❌ Denied %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n }\n }\n } else if (data.type === \"fetch:response:meta\") {\n const pending = tunnel.pendingFetches.get(data.requestId);\n if (pending) {\n pending.metadata = {\n status: data.status,\n headers: data.headers,\n };\n if (\n data.status === 304 ||\n data.status === 204 ||\n (data.status >= 300 && data.status < 400)\n ) {\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(data.requestId);\n pending.resolve({\n status: data.status,\n headers: data.headers,\n body: Buffer.from(\"\"),\n });\n } else {\n tunnel.waitingForBinaryBody = data.requestId;\n }\n }\n } else if (data.type === \"file:read:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in file:read:response\"),\n );\n }\n }\n } else if (data.type === \"dir:list:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in dir:list:response\"),\n );\n }\n }\n }\n } catch (e) {\n logger.error(\"Failed to parse WebSocket message: %O\", e);\n }\n });\n\n ws.send(JSON.stringify({ type: \"tunnel:ready\", tunnelId }));\n\n ws.on(\"close\", () => {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (tunnel) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Tunnel closed\"));\n }\n tunnel.pendingFetches.clear();\n }\n\n this.tunnels.delete(tunnelId);\n });\n });\n\n this.hmrWss.on(\"connection\", (browserWs, req) => {\n const cookies = parseCookies(req);\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId =\n cookies[\"dev-tunnel-id\"] || generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return browserWs.close();\n\n const cliTunnel = this.tunnels.get(tunnelId);\n\n if (!cliTunnel) return browserWs.close();\n\n const { ws: cliWs, approvedViewers } = cliTunnel;\n\n if (!approvedViewers.has(email)) {\n return browserWs.close(1008, \"Not approved\");\n }\n // Browser → CLI\n browserWs.on(\"message\", (msg) => {\n const hmrStart = Date.now();\n logger.debug(\"browser -> cli browserWS message: %s\", msg.toString());\n cliWs.send(\n JSON.stringify({\n type: \"hmr:message\",\n body: msg.toString(),\n timestamp: hmrStart,\n }),\n );\n });\n\n // // CLI → Browser\n const cliHandler = (msg: Buffer | string, isBinary: boolean) => {\n // Ignore binary messages (they're for fetch responses, not HMR)\n if (isBinary) return;\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"hmr:message\") {\n browserWs.send(data.body);\n }\n } catch {\n logger.error(\n \"Failed to parse CLI message for HMR: %s\",\n msg.toString().substring(0, 100),\n );\n }\n };\n cliWs.on(\"message\", cliHandler);\n\n browserWs.on(\"close\", () => {\n cliWs.off(\"message\", cliHandler);\n });\n });\n\n // // Browser HMR connection\n this.server?.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n\n if (url.startsWith(\"/dev-tunnel\")) {\n this.wss.handleUpgrade(req, socket, head, (ws) => {\n this.wss.emit(\"connection\", ws, req);\n });\n } else if (url.startsWith(\"/dev-hmr\")) {\n this.hmrWss.handleUpgrade(req, socket, head, (browserWs) => {\n this.hmrWss.emit(\"connection\", browserWs, req);\n });\n }\n });\n }\n\n registerTunnelGetter() {\n this.devFileReader.registerTunnelGetter(\n this.getTunnelForRequest.bind(this),\n );\n }\n\n getTunnelForRequest(req: express.Request) {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const cookieHeader = req.headers.cookie;\n\n let tunnelId: string | undefined;\n\n if (cookieHeader) {\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n return tunnelId ? this.tunnels.get(tunnelId) || null : null;\n }\n\n cleanup() {\n for (const [, tunnel] of this.tunnels) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Server shutting down\"));\n }\n tunnel.pendingFetches.clear();\n tunnel.ws.close();\n }\n this.tunnels.clear();\n\n if (this.wss) {\n this.wss.close();\n }\n if (this.hmrWss) {\n this.hmrWss.close();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"remote-tunnel-manager.js","names":[],"sources":["../../../../src/plugins/server/remote-tunnel/remote-tunnel-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type express from \"express\";\nimport type { TunnelConnection } from \"shared\";\nimport { WebSocketServer } from \"ws\";\n\nimport { createLogger } from \"../../../logging/logger\";\nimport {\n generateTunnelIdFromEmail,\n getConfigScript,\n parseCookies,\n} from \"../utils\";\nimport { REMOTE_TUNNEL_ASSET_PREFIXES } from \"./gate\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst MAX_ASSET_FETCH_TIMEOUT = 60_000;\n\nconst logger = createLogger(\"server:remote-tunnel\");\n\ninterface DevFileReader {\n registerTunnelGetter(\n getter: (req: express.Request) => TunnelConnection | null,\n ): void;\n}\n\n/**\n * WebSocket message types for CLI <-> Server communication\n */\ntype WebSocketMessage =\n | {\n type: \"connection:response\";\n viewer: string;\n approved: boolean;\n }\n | {\n type: \"fetch:response:meta\";\n requestId: string;\n status: number;\n headers: Record<string, string>;\n }\n | {\n type: \"file:read:response\";\n requestId: string;\n content?: string;\n error?: string;\n }\n | {\n type: \"dir:list:response\";\n requestId: string;\n content?: string;\n error?: string;\n }\n | {\n type: \"hmr:message\";\n body: string;\n };\n\n/**\n * Type guard to validate WebSocket message structure\n */\nfunction isWebSocketMessage(data: unknown): data is WebSocketMessage {\n if (!data || typeof data !== \"object\") {\n return false;\n }\n\n const msg = data as Record<string, unknown>;\n return typeof msg.type === \"string\";\n}\n\n/**\n * Remote tunnel manager for the AppKit.\n *\n * This class is responsible for managing the remote tunnels for the development server.\n * It also handles the asset fetching and the HMR for the development server.\n *\n * @example\n * ```ts\n * const remoteTunnelManager = new RemoteTunnelManager(devFileReader);\n * remoteTunnelManager.setup(app);\n * ```\n */\nexport class RemoteTunnelManager {\n private tunnels = new Map<string, TunnelConnection>();\n private wss: WebSocketServer;\n private hmrWss: WebSocketServer;\n private server?: HTTPServer;\n private devFileReader: DevFileReader;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.wss = new WebSocketServer({ noServer: true, path: \"/dev-tunnel\" });\n this.hmrWss = new WebSocketServer({ noServer: true, path: \"/dev-hmr\" });\n\n this.registerTunnelGetter();\n }\n\n setServer(server: HTTPServer) {\n this.server = server;\n }\n\n /** Asset middleware for the development server. */\n assetMiddleware() {\n return async (req: express.Request, res: express.Response) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n\n // Try cookie first, then generate from email\n let tunnelId: string | undefined;\n const cookieHeader = req.headers.cookie;\n\n if (cookieHeader) {\n // Fast path: extract dev-tunnel-id from cookie without full parse\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n if (!tunnelId) return res.status(404).send(\"Tunnel not ready\");\n\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) return res.status(404).send(\"Tunnel not found\");\n\n const { ws, approvedViewers, pendingFetches } = tunnel;\n\n if (!approvedViewers.has(email)) {\n return res.status(403).send(\"Not approved for this tunnel\");\n }\n\n const path = req.originalUrl;\n const requestId = randomUUID();\n\n const request = { type: \"fetch\", path, method: req.method, requestId };\n\n const response = await new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFetches.delete(requestId);\n reject(new Error(\"Asset fetch timeout\"));\n }, MAX_ASSET_FETCH_TIMEOUT);\n\n pendingFetches.set(requestId, { resolve, reject, timeout });\n\n ws.send(JSON.stringify(request));\n }).catch((err) => {\n logger.error(\"Failed to fetch %s: %s\", path, err.message);\n return { status: 504, body: Buffer.from(\"\"), headers: {} };\n });\n\n const r = response as any;\n\n res\n .status(r.status)\n .set(r.headers)\n .send(r.body || Buffer.from(\"\"));\n };\n }\n\n /** Dev mode middleware for the development server. */\n devModeMiddleware() {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n const dev = req.query?.dev;\n\n if (dev === undefined) {\n return next();\n }\n\n if (\n req.path.startsWith(\"/api\") ||\n req.path.startsWith(\"/query\") ||\n req.path.match(/\\.(js|css|png|jpg|jpeg|svg|ico|json|woff|woff2|ttf)$/)\n ) {\n return next();\n }\n\n const viewerEmail = req.headers[\"x-forwarded-email\"] as string;\n const isOwnerMode = dev === \"\" || dev === \"true\";\n\n const tunnelId = isOwnerMode\n ? generateTunnelIdFromEmail(viewerEmail)\n : dev.toString();\n\n if (!tunnelId) {\n return res.status(400).send(\"Invalid tunnel ID\");\n }\n\n if (!isOwnerMode) {\n const approvalResponse = this.handleViewerApproval(\n tunnelId,\n viewerEmail,\n req.query.retry === \"true\",\n res,\n );\n\n if (approvalResponse) {\n return approvalResponse;\n }\n }\n\n res.cookie(\"dev-tunnel-id\", tunnelId, {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n });\n\n const indexPath = path.join(__dirname, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${getConfigScript()}`);\n\n res.send(html);\n };\n }\n\n /** Setup the dev mode middleware. */\n setup(app: express.Application) {\n app.use(this.devModeMiddleware());\n app.use(REMOTE_TUNNEL_ASSET_PREFIXES, this.assetMiddleware());\n }\n\n static isRemoteServerEnabled() {\n return (\n process.env.NODE_ENV !== \"production\" &&\n process.env.DISABLE_REMOTE_SERVING !== \"true\" &&\n // DATABRICKS_CLIENT_SECRET is set in the .env file for deployed environments\n Boolean(process.env.DATABRICKS_CLIENT_SECRET)\n );\n }\n\n private loadHtmlTemplate(\n filename: string,\n replacements: Record<string, string>,\n ): string {\n const filePath = path.join(__dirname, filename);\n let content = fs.readFileSync(filePath, \"utf-8\");\n\n for (const [key, value] of Object.entries(replacements)) {\n content = content.replaceAll(`{{${key}}}`, value);\n }\n\n return content;\n }\n\n private handleViewerApproval(\n tunnelId: string,\n viewerEmail: string,\n retry: boolean,\n res: express.Response,\n ): express.Response | null {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) {\n return res.status(404).send(\"Tunnel not found\");\n }\n\n if (viewerEmail === tunnel.owner) {\n return null;\n }\n\n if (retry) {\n tunnel.rejectedViewers.delete(viewerEmail);\n }\n\n if (tunnel.rejectedViewers.has(viewerEmail)) {\n const html = this.loadHtmlTemplate(\"denied.html\", { tunnelId });\n return res.status(403).send(html);\n }\n\n if (tunnel.approvedViewers.has(viewerEmail)) {\n return null;\n }\n\n if (!tunnel.pendingRequests.has(viewerEmail)) {\n const requestId = randomUUID();\n tunnel.pendingRequests.add(viewerEmail);\n tunnel.ws.send(\n JSON.stringify({\n type: \"connection:request\",\n requestId,\n viewer: viewerEmail,\n }),\n );\n }\n\n const html = this.loadHtmlTemplate(\"wait.html\", { tunnelId });\n return res.status(200).send(html);\n }\n\n setupWebSocket() {\n this.wss.on(\"connection\", (ws, req) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId = generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return ws.close();\n\n this.tunnels.set(tunnelId, {\n ws,\n owner: email,\n approvedViewers: new Set([email]),\n pendingRequests: new Set(),\n rejectedViewers: new Set(),\n pendingFetches: new Map(),\n pendingFileReads: new Map(),\n waitingForBinaryBody: null,\n });\n\n ws.on(\"message\", (msg, isBinary) => {\n const tunnel = this.tunnels.get(tunnelId);\n if (!tunnel) return;\n\n if (isBinary) {\n if (!tunnel.waitingForBinaryBody) {\n logger.debug(\n \"Received binary message but no requestId is waiting for body\",\n );\n return;\n }\n\n const requestId = tunnel.waitingForBinaryBody;\n const pending = tunnel.pendingFetches.get(requestId);\n\n if (!pending || !pending.metadata) {\n logger.debug(\"Received binary message but pending fetch not found\");\n tunnel.waitingForBinaryBody = null;\n return;\n }\n\n tunnel.waitingForBinaryBody = null;\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(requestId);\n\n pending.resolve({\n status: pending.metadata.status,\n headers: pending.metadata.headers,\n body: msg as Buffer,\n });\n return;\n }\n\n try {\n const data = JSON.parse(msg.toString());\n\n // Validate message structure\n if (!isWebSocketMessage(data)) {\n logger.error(\"Invalid WebSocket message format: %O\", data);\n return;\n }\n\n if (data.type === \"connection:response\") {\n if (tunnel && data.viewer) {\n tunnel.pendingRequests.delete(data.viewer);\n\n if (data.approved) {\n tunnel.approvedViewers.add(data.viewer);\n logger.debug(\n \"✅ Approved %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n } else {\n tunnel.rejectedViewers.add(data.viewer);\n logger.debug(\n \"❌ Denied %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n }\n }\n } else if (data.type === \"fetch:response:meta\") {\n const pending = tunnel.pendingFetches.get(data.requestId);\n if (pending) {\n pending.metadata = {\n status: data.status,\n headers: data.headers,\n };\n if (\n data.status === 304 ||\n data.status === 204 ||\n (data.status >= 300 && data.status < 400)\n ) {\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(data.requestId);\n pending.resolve({\n status: data.status,\n headers: data.headers,\n body: Buffer.from(\"\"),\n });\n } else {\n tunnel.waitingForBinaryBody = data.requestId;\n }\n }\n } else if (data.type === \"file:read:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in file:read:response\"),\n );\n }\n }\n } else if (data.type === \"dir:list:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else if (data.content !== undefined) {\n pending.resolve(data.content);\n } else {\n pending.reject(\n new Error(\"Missing content in dir:list:response\"),\n );\n }\n }\n }\n } catch (e) {\n logger.error(\"Failed to parse WebSocket message: %O\", e);\n }\n });\n\n ws.send(JSON.stringify({ type: \"tunnel:ready\", tunnelId }));\n\n ws.on(\"close\", () => {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (tunnel) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Tunnel closed\"));\n }\n tunnel.pendingFetches.clear();\n }\n\n this.tunnels.delete(tunnelId);\n });\n });\n\n this.hmrWss.on(\"connection\", (browserWs, req) => {\n const cookies = parseCookies(req);\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId =\n cookies[\"dev-tunnel-id\"] || generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return browserWs.close();\n\n const cliTunnel = this.tunnels.get(tunnelId);\n\n if (!cliTunnel) return browserWs.close();\n\n const { ws: cliWs, approvedViewers } = cliTunnel;\n\n if (!approvedViewers.has(email)) {\n return browserWs.close(1008, \"Not approved\");\n }\n // Browser → CLI\n browserWs.on(\"message\", (msg) => {\n const hmrStart = Date.now();\n logger.debug(\"browser -> cli browserWS message: %s\", msg.toString());\n cliWs.send(\n JSON.stringify({\n type: \"hmr:message\",\n body: msg.toString(),\n timestamp: hmrStart,\n }),\n );\n });\n\n // // CLI → Browser\n const cliHandler = (msg: Buffer | string, isBinary: boolean) => {\n // Ignore binary messages (they're for fetch responses, not HMR)\n if (isBinary) return;\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"hmr:message\") {\n browserWs.send(data.body);\n }\n } catch {\n logger.error(\n \"Failed to parse CLI message for HMR: %s\",\n msg.toString().substring(0, 100),\n );\n }\n };\n cliWs.on(\"message\", cliHandler);\n\n browserWs.on(\"close\", () => {\n cliWs.off(\"message\", cliHandler);\n });\n });\n\n // // Browser HMR connection\n this.server?.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n\n if (url.startsWith(\"/dev-tunnel\")) {\n this.wss.handleUpgrade(req, socket, head, (ws) => {\n this.wss.emit(\"connection\", ws, req);\n });\n } else if (url.startsWith(\"/dev-hmr\")) {\n this.hmrWss.handleUpgrade(req, socket, head, (browserWs) => {\n this.hmrWss.emit(\"connection\", browserWs, req);\n });\n }\n });\n }\n\n registerTunnelGetter() {\n this.devFileReader.registerTunnelGetter(\n this.getTunnelForRequest.bind(this),\n );\n }\n\n getTunnelForRequest(req: express.Request) {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const cookieHeader = req.headers.cookie;\n\n let tunnelId: string | undefined;\n\n if (cookieHeader) {\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n return tunnelId ? this.tunnels.get(tunnelId) || null : null;\n }\n\n cleanup() {\n for (const [, tunnel] of this.tunnels) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Server shutting down\"));\n }\n tunnel.pendingFetches.clear();\n tunnel.ws.close();\n }\n this.tunnels.clear();\n\n if (this.wss) {\n this.wss.close();\n }\n if (this.hmrWss) {\n this.hmrWss.close();\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"static-server.js","names":["expressStatic"],"sources":["../../../src/plugins/server/static-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport expressStatic from \"express\";\nimport { BaseServer } from \"./base-server\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\n/**\n * Static server for the AppKit.\n *\n * Serves pre-built static files in production mode. Handles SPA routing\n * by serving index.html for non-API routes and injects runtime configuration.\n *\n * @example\n * ```ts\n * const staticServer = new StaticServer(app, staticPath, endpoints);\n * staticServer.setup();\n * ```\n */\nexport class StaticServer extends BaseServer {\n private staticPath: string;\n\n constructor(\n app: express.Application,\n staticPath: string,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.staticPath = staticPath;\n }\n\n /** Setup the static server. */\n setup() {\n this.app.use(\n expressStatic.static(this.staticPath, {\n index: false,\n }),\n );\n\n this.app.get(\"*\", (req, res, next) => {\n if (req.path.startsWith(\"/api\") || req.path.startsWith(\"/query\")) {\n return next();\n }\n this.serveIndex(res);\n });\n }\n\n /** Serve the index.html file. */\n private serveIndex(res: express.Response) {\n const indexPath = path.join(this.staticPath, \"index.html\");\n\n if (!fs.existsSync(indexPath)) {\n res.status(404).send(\"index.html not found\");\n return;\n }\n\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n res.send(html);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,IAAa,eAAb,cAAkC,WAAW;CAC3C,AAAQ;CAER,YACE,KACA,YACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,aAAa;;;CAIpB,QAAQ;AACN,OAAK,IAAI,IACPA,QAAc,OAAO,KAAK,YAAY,EACpC,OAAO,OACR,CAAC,CACH;AAED,OAAK,IAAI,IAAI,MAAM,KAAK,KAAK,SAAS;AACpC,OAAI,IAAI,KAAK,WAAW,OAAO,IAAI,IAAI,KAAK,WAAW,SAAS,CAC9D,QAAO,MAAM;AAEf,QAAK,WAAW,IAAI;IACpB;;;CAIJ,AAAQ,WAAW,KAAuB;EACxC,MAAM,YAAY,KAAK,KAAK,KAAK,YAAY,aAAa;AAE1D,MAAI,CAAC,GAAG,WAAW,UAAU,EAAE;AAC7B,OAAI,OAAO,IAAI,CAAC,KAAK,uBAAuB;AAC5C;;EAGF,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,SAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,MAAI,KAAK,KAAK"}
1
+ {"version":3,"file":"static-server.js","names":["expressStatic"],"sources":["../../../src/plugins/server/static-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type express from \"express\";\nimport expressStatic from \"express\";\n\nimport { BaseServer } from \"./base-server\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\n/**\n * Static server for the AppKit.\n *\n * Serves pre-built static files in production mode. Handles SPA routing\n * by serving index.html for non-API routes and injects runtime configuration.\n *\n * @example\n * ```ts\n * const staticServer = new StaticServer(app, staticPath, endpoints);\n * staticServer.setup();\n * ```\n */\nexport class StaticServer extends BaseServer {\n private staticPath: string;\n\n constructor(\n app: express.Application,\n staticPath: string,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.staticPath = staticPath;\n }\n\n /** Setup the static server. */\n setup() {\n this.app.use(\n expressStatic.static(this.staticPath, {\n index: false,\n }),\n );\n\n this.app.get(\"*\", (req, res, next) => {\n if (req.path.startsWith(\"/api\") || req.path.startsWith(\"/query\")) {\n return next();\n }\n this.serveIndex(res);\n });\n }\n\n /** Serve the index.html file. */\n private serveIndex(res: express.Response) {\n const indexPath = path.join(this.staticPath, \"index.html\");\n\n if (!fs.existsSync(indexPath)) {\n res.status(404).send(\"index.html not found\");\n return;\n }\n\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n res.send(html);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,IAAa,eAAb,cAAkC,WAAW;CAC3C,AAAQ;CAER,YACE,KACA,YACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,aAAa;;;CAIpB,QAAQ;AACN,OAAK,IAAI,IACPA,QAAc,OAAO,KAAK,YAAY,EACpC,OAAO,OACR,CAAC,CACH;AAED,OAAK,IAAI,IAAI,MAAM,KAAK,KAAK,SAAS;AACpC,OAAI,IAAI,KAAK,WAAW,OAAO,IAAI,IAAI,KAAK,WAAW,SAAS,CAC9D,QAAO,MAAM;AAEf,QAAK,WAAW,IAAI;IACpB;;;CAIJ,AAAQ,WAAW,KAAuB;EACxC,MAAM,YAAY,KAAK,KAAK,KAAK,YAAY,aAAa;AAE1D,MAAI,CAAC,GAAG,WAAW,UAAU,EAAE;AAC7B,OAAI,OAAO,IAAI,CAAC,KAAK,uBAAuB;AAC5C;;EAGF,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,SAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,MAAI,KAAK,KAAK"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../../../src/plugins/server/utils.ts"],"sourcesContent":["import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type http from \"node:http\";\nimport path from \"node:path\";\nimport pc from \"picocolors\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"shared\";\n\nexport function parseCookies(\n req: http.IncomingMessage,\n): Record<string, string> {\n const cookieHeader = req.headers.cookie;\n if (!cookieHeader) return {};\n\n // Fast path: if there's no semicolon, there's only one cookie\n const semicolonIndex = cookieHeader.indexOf(\";\");\n if (semicolonIndex === -1) {\n const eqIndex = cookieHeader.indexOf(\"=\");\n if (eqIndex === -1) return {};\n return {\n [cookieHeader.slice(0, eqIndex).trim()]: cookieHeader.slice(eqIndex + 1),\n };\n }\n\n // Multiple cookies: parse them all\n const cookies: Record<string, string> = {};\n const parts = cookieHeader.split(\";\");\n for (let i = 0; i < parts.length; i++) {\n const eqIndex = parts[i].indexOf(\"=\");\n if (eqIndex !== -1) {\n const key = parts[i].slice(0, eqIndex).trim();\n const value = parts[i].slice(eqIndex + 1);\n cookies[key] = value;\n }\n }\n return cookies;\n}\n\nexport function generateTunnelIdFromEmail(email?: string): string | undefined {\n if (!email) return undefined;\n\n const tunnelId = crypto\n .createHash(\"sha256\")\n .update(email)\n .digest(\"base64url\")\n .slice(0, 8);\n\n return tunnelId;\n}\n\nexport function getRoutes(stack: unknown[], basePath = \"\") {\n const routes: Array<{ path: string; methods: string[] }> = [];\n\n stack.forEach((layer: any) => {\n if (layer.route) {\n // normal route\n const path = basePath + layer.route.path;\n const methods = Object.keys(layer.route.methods).map((m) =>\n m.toUpperCase(),\n );\n routes.push({ path, methods });\n } else if (layer.name === \"router\" && layer.handle.stack) {\n // nested router\n const nestedBase =\n basePath +\n layer.regexp.source\n .replace(\"^\\\\\", \"\")\n .replace(\"\\\\/?(?=\\\\/|$)\", \"\")\n .replace(/\\\\\\//g, \"/\") // convert escaped slashes\n .replace(/\\$$/, \"\") || \"\";\n routes.push(...getRoutes(layer.handle.stack, nestedBase));\n }\n });\n\n return routes;\n}\n\nconst METHOD_COLORS: Record<string, (s: string) => string> = {\n GET: pc.green,\n POST: pc.blue,\n PUT: pc.yellow,\n PATCH: pc.yellow,\n DELETE: pc.red,\n HEAD: pc.magenta,\n OPTIONS: pc.magenta,\n};\n\nexport function printRoutes(\n routes: Array<{ path: string; methods: string[] }>,\n) {\n if (routes.length === 0) return;\n\n const rows = routes\n .flatMap((r) => r.methods.map((m) => ({ method: m, path: r.path })))\n .sort(\n (a, b) =>\n a.method.localeCompare(b.method) || a.path.localeCompare(b.path),\n );\n\n const maxMethodLen = Math.max(...rows.map((r) => r.method.length));\n const separator = pc.dim(\"─\".repeat(50));\n\n const colorizeParams = (p: string) =>\n p.replace(/(:[a-zA-Z_]\\w*)/g, (match) => pc.cyan(match));\n\n console.log(\"\");\n console.log(\n ` ${pc.bold(\"Registered Routes\")} ${pc.dim(`(${rows.length})`)}`,\n );\n console.log(` ${separator}`);\n\n for (const { method, path } of rows) {\n const colorize = METHOD_COLORS[method] || pc.white;\n const methodStr = colorize(pc.bold(method.padEnd(maxMethodLen)));\n console.log(` ${methodStr} ${colorizeParams(path)}`);\n }\n\n console.log(` ${separator}`);\n console.log(\"\");\n}\n\nexport function getQueries(configFolder: string) {\n const queriesFolder = path.join(configFolder, \"queries\");\n\n if (!fs.existsSync(queriesFolder)) {\n return {};\n }\n\n return Object.fromEntries(\n fs\n .readdirSync(queriesFolder)\n .filter((f) => path.extname(f) === \".sql\")\n .map((f) => [path.basename(f, \".sql\"), path.basename(f, \".sql\")]),\n );\n}\n\nexport type { PluginClientConfigs, PluginEndpoints };\n\ninterface RuntimeConfig {\n appName: string;\n queries: Record<string, string>;\n endpoints: PluginEndpoints;\n plugins: PluginClientConfigs;\n}\n\nconst APPKIT_CONFIG_SCRIPT_ID = \"__appkit__\";\nconst EMPTY_RUNTIME_CONFIG: RuntimeConfig = {\n appName: \"\",\n queries: {},\n endpoints: {},\n plugins: {},\n};\nconst EMPTY_RUNTIME_CONFIG_JSON = JSON.stringify(EMPTY_RUNTIME_CONFIG);\nconst JSON_SCRIPT_ESCAPE_MAP: Record<string, string> = {\n \"<\": \"\\\\u003c\",\n \">\": \"\\\\u003e\",\n \"&\": \"\\\\u0026\",\n \"\\u2028\": \"\\\\u2028\",\n \"\\u2029\": \"\\\\u2029\",\n};\n\nexport function getRuntimeConfig(\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n): RuntimeConfig {\n const configFolder = path.join(process.cwd(), \"config\");\n\n return {\n appName: process.env.DATABRICKS_APP_NAME || \"\",\n queries: getQueries(configFolder),\n endpoints,\n plugins: pluginConfigs,\n };\n}\n\nexport function getConfigScript(\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n): string {\n const config = getRuntimeConfig(endpoints, pluginConfigs);\n\n return `\n <script id=\"${APPKIT_CONFIG_SCRIPT_ID}\" type=\"application/json\">\n ${serializeRuntimeConfig(config)}\n </script>\n <script>\n window.__appkit__ = JSON.parse(\n document.getElementById(\"${APPKIT_CONFIG_SCRIPT_ID}\")?.textContent ||\n '${EMPTY_RUNTIME_CONFIG_JSON}',\n );\n </script>\n `;\n}\n\nfunction serializeRuntimeConfig(config: RuntimeConfig): string {\n return JSON.stringify(config).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => JSON_SCRIPT_ESCAPE_MAP[char] ?? char,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../../../src/plugins/server/utils.ts"],"sourcesContent":["import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type http from \"node:http\";\nimport path from \"node:path\";\n\nimport pc from \"picocolors\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"shared\";\n\nexport function parseCookies(\n req: http.IncomingMessage,\n): Record<string, string> {\n const cookieHeader = req.headers.cookie;\n if (!cookieHeader) return {};\n\n // Fast path: if there's no semicolon, there's only one cookie\n const semicolonIndex = cookieHeader.indexOf(\";\");\n if (semicolonIndex === -1) {\n const eqIndex = cookieHeader.indexOf(\"=\");\n if (eqIndex === -1) return {};\n return {\n [cookieHeader.slice(0, eqIndex).trim()]: cookieHeader.slice(eqIndex + 1),\n };\n }\n\n // Multiple cookies: parse them all\n const cookies: Record<string, string> = {};\n const parts = cookieHeader.split(\";\");\n for (let i = 0; i < parts.length; i++) {\n const eqIndex = parts[i].indexOf(\"=\");\n if (eqIndex !== -1) {\n const key = parts[i].slice(0, eqIndex).trim();\n const value = parts[i].slice(eqIndex + 1);\n cookies[key] = value;\n }\n }\n return cookies;\n}\n\nexport function generateTunnelIdFromEmail(email?: string): string | undefined {\n if (!email) return undefined;\n\n const tunnelId = crypto\n .createHash(\"sha256\")\n .update(email)\n .digest(\"base64url\")\n .slice(0, 8);\n\n return tunnelId;\n}\n\nexport function getRoutes(stack: unknown[], basePath = \"\") {\n const routes: Array<{ path: string; methods: string[] }> = [];\n\n stack.forEach((layer: any) => {\n if (layer.route) {\n // normal route\n const path = basePath + layer.route.path;\n const methods = Object.keys(layer.route.methods).map((m) =>\n m.toUpperCase(),\n );\n routes.push({ path, methods });\n } else if (layer.name === \"router\" && layer.handle.stack) {\n // nested router\n const nestedBase =\n basePath +\n layer.regexp.source\n .replace(\"^\\\\\", \"\")\n .replace(\"\\\\/?(?=\\\\/|$)\", \"\")\n .replace(/\\\\\\//g, \"/\") // convert escaped slashes\n .replace(/\\$$/, \"\") || \"\";\n routes.push(...getRoutes(layer.handle.stack, nestedBase));\n }\n });\n\n return routes;\n}\n\nconst METHOD_COLORS: Record<string, (s: string) => string> = {\n GET: pc.green,\n POST: pc.blue,\n PUT: pc.yellow,\n PATCH: pc.yellow,\n DELETE: pc.red,\n HEAD: pc.magenta,\n OPTIONS: pc.magenta,\n};\n\nexport function printRoutes(\n routes: Array<{ path: string; methods: string[] }>,\n) {\n if (routes.length === 0) return;\n\n const rows = routes\n .flatMap((r) => r.methods.map((m) => ({ method: m, path: r.path })))\n .sort(\n (a, b) =>\n a.method.localeCompare(b.method) || a.path.localeCompare(b.path),\n );\n\n const maxMethodLen = Math.max(...rows.map((r) => r.method.length));\n const separator = pc.dim(\"─\".repeat(50));\n\n const colorizeParams = (p: string) =>\n p.replace(/(:[a-zA-Z_]\\w*)/g, (match) => pc.cyan(match));\n\n console.log(\"\");\n console.log(\n ` ${pc.bold(\"Registered Routes\")} ${pc.dim(`(${rows.length})`)}`,\n );\n console.log(` ${separator}`);\n\n for (const { method, path } of rows) {\n const colorize = METHOD_COLORS[method] || pc.white;\n const methodStr = colorize(pc.bold(method.padEnd(maxMethodLen)));\n console.log(` ${methodStr} ${colorizeParams(path)}`);\n }\n\n console.log(` ${separator}`);\n console.log(\"\");\n}\n\nexport function getQueries(configFolder: string) {\n const queriesFolder = path.join(configFolder, \"queries\");\n\n if (!fs.existsSync(queriesFolder)) {\n return {};\n }\n\n return Object.fromEntries(\n fs\n .readdirSync(queriesFolder)\n .filter((f) => path.extname(f) === \".sql\")\n .map((f) => [path.basename(f, \".sql\"), path.basename(f, \".sql\")]),\n );\n}\n\nexport type { PluginClientConfigs, PluginEndpoints };\n\ninterface RuntimeConfig {\n appName: string;\n queries: Record<string, string>;\n endpoints: PluginEndpoints;\n plugins: PluginClientConfigs;\n}\n\nconst APPKIT_CONFIG_SCRIPT_ID = \"__appkit__\";\nconst EMPTY_RUNTIME_CONFIG: RuntimeConfig = {\n appName: \"\",\n queries: {},\n endpoints: {},\n plugins: {},\n};\nconst EMPTY_RUNTIME_CONFIG_JSON = JSON.stringify(EMPTY_RUNTIME_CONFIG);\nconst JSON_SCRIPT_ESCAPE_MAP: Record<string, string> = {\n \"<\": \"\\\\u003c\",\n \">\": \"\\\\u003e\",\n \"&\": \"\\\\u0026\",\n \"\\u2028\": \"\\\\u2028\",\n \"\\u2029\": \"\\\\u2029\",\n};\n\nexport function getRuntimeConfig(\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n): RuntimeConfig {\n const configFolder = path.join(process.cwd(), \"config\");\n\n return {\n appName: process.env.DATABRICKS_APP_NAME || \"\",\n queries: getQueries(configFolder),\n endpoints,\n plugins: pluginConfigs,\n };\n}\n\nexport function getConfigScript(\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n): string {\n const config = getRuntimeConfig(endpoints, pluginConfigs);\n\n return `\n <script id=\"${APPKIT_CONFIG_SCRIPT_ID}\" type=\"application/json\">\n ${serializeRuntimeConfig(config)}\n </script>\n <script>\n window.__appkit__ = JSON.parse(\n document.getElementById(\"${APPKIT_CONFIG_SCRIPT_ID}\")?.textContent ||\n '${EMPTY_RUNTIME_CONFIG_JSON}',\n );\n </script>\n `;\n}\n\nfunction serializeRuntimeConfig(config: RuntimeConfig): string {\n return JSON.stringify(config).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => JSON_SCRIPT_ESCAPE_MAP[char] ?? char,\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../../src/plugins/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { appKitServingTypesPlugin } from \"../../type-generator/serving/vite-plugin\";\nimport { appKitTypesPlugin } from \"../../type-generator/vite-plugin\";\nimport { mergeConfigDedup } from \"../../utils\";\nimport { BaseServer } from \"./base-server\";\nimport { reactSourceLocPlugin } from \"./react-source-loc-vite-plugin\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n const projectRoot = process.cwd();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const viteClientPort = process.env.VITE_CLIENT_PORT;\n const serverHmr = viteClientPort\n ? { hmr: { clientPort: viteClientPort } }\n : {};\n\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n ...serverHmr,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [\n react.default(),\n reactSourceLocPlugin({ projectRoot }),\n appKitTypesPlugin(),\n appKitServingTypesPlugin(),\n ],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAaA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAC5C,AAAQ;CAER,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;EACxC,MAAM,cAAc,QAAQ,KAAK;EAWjC,MAAM,cATe,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE;EAC7C,MAAM,iBAAiB,QAAQ,IAAI;AA0BnC,OAAK,OAAO,MAAM,iBADI,iBAAiB,YApBpB;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,GATc,iBACd,EAAE,KAAK,EAAE,YAAY,gBAAgB,EAAE,GACvC,EAAE;IAQF,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS;IACP,MAAM,SAAS;IACf,qBAAqB,EAAE,aAAa,CAAC;IACrC,mBAAmB;IACnB,0BAA0B;IAC3B;GACD,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
1
+ {"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../../src/plugins/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\n\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { appKitServingTypesPlugin } from \"../../type-generator/serving/vite-plugin\";\nimport { appKitTypesPlugin } from \"../../type-generator/vite-plugin\";\nimport { mergeConfigDedup } from \"../../utils\";\nimport { BaseServer } from \"./base-server\";\nimport { reactSourceLocPlugin } from \"./react-source-loc-vite-plugin\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n const projectRoot = process.cwd();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const viteClientPort = process.env.VITE_CLIENT_PORT;\n const serverHmr = viteClientPort\n ? { hmr: { clientPort: viteClientPort } }\n : {};\n\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n ...serverHmr,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [\n react.default(),\n reactSourceLocPlugin({ projectRoot }),\n appKitTypesPlugin(),\n appKitServingTypesPlugin(),\n ],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAeA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAC5C,AAAQ;CAER,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;EACxC,MAAM,cAAc,QAAQ,KAAK;EAWjC,MAAM,cATe,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE;EAC7C,MAAM,iBAAiB,QAAQ,IAAI;AA0BnC,OAAK,OAAO,MAAM,iBADI,iBAAiB,YApBpB;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,GATc,iBACd,EAAE,KAAK,EAAE,YAAY,gBAAgB,EAAE,GACvC,EAAE;IAQF,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS;IACP,MAAM,SAAS;IACf,qBAAqB,EAAE,aAAa,CAAC;IACrC,mBAAmB;IACnB,0BAA0B;IAC3B;GACD,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"schema-filter.js","names":[],"sources":["../../../src/plugins/serving/schema-filter.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n CACHE_VERSION,\n type ServingCache,\n} from \"../../type-generator/serving/cache\";\n\nconst logger = createLogger(\"serving:schema-filter\");\n\nfunction isValidCache(data: unknown): data is ServingCache {\n return (\n typeof data === \"object\" &&\n data !== null &&\n \"version\" in data &&\n (data as ServingCache).version === CACHE_VERSION &&\n \"endpoints\" in data &&\n typeof (data as ServingCache).endpoints === \"object\"\n );\n}\n\n/**\n * Loads endpoint schemas from the type generation cache file.\n * Returns a map of alias → allowed parameter keys.\n */\nexport async function loadEndpointSchemas(\n cacheFile: string,\n): Promise<Map<string, Set<string>>> {\n const allowlists = new Map<string, Set<string>>();\n\n try {\n const raw = await fs.readFile(cacheFile, \"utf8\");\n const parsed: unknown = JSON.parse(raw);\n if (!isValidCache(parsed)) {\n logger.warn(\"Serving types cache has invalid structure, skipping\");\n return allowlists;\n }\n const cache = parsed;\n\n for (const [alias, entry] of Object.entries(cache.endpoints)) {\n if (entry.requestKeys && entry.requestKeys.length > 0) {\n allowlists.set(alias, new Set(entry.requestKeys));\n }\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n logger.warn(\n \"Failed to load serving types cache: %s\",\n (err as Error).message,\n );\n }\n // No cache → no filtering, passthrough mode\n }\n\n return allowlists;\n}\n\n/**\n * Filters a request body against the allowed keys for an endpoint alias.\n * Returns the filtered body and logs a warning for stripped params.\n *\n * If no allowlist exists for the alias, returns the body unchanged (passthrough).\n */\nexport function filterRequestBody(\n body: Record<string, unknown>,\n allowlists: Map<string, Set<string>>,\n alias: string,\n filterMode: \"strip\" | \"reject\" = \"strip\",\n): Record<string, unknown> {\n const allowed = allowlists.get(alias);\n if (!allowed) return body;\n\n const stripped: string[] = [];\n const filtered: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(body)) {\n if (allowed.has(key)) {\n filtered[key] = value;\n } else {\n stripped.push(key);\n }\n }\n\n if (stripped.length > 0) {\n if (filterMode === \"reject\") {\n throw new Error(`Unknown request parameters: ${stripped.join(\", \")}`);\n }\n logger.warn(\n \"Stripped unknown params from '%s': %s\",\n alias,\n stripped.join(\", \"),\n );\n }\n\n return filtered;\n}\n"],"mappings":";;;;;AAOA,MAAM,SAAS,aAAa,wBAAwB;AAEpD,SAAS,aAAa,MAAqC;AACzD,QACE,OAAO,SAAS,YAChB,SAAS,QACT,aAAa,QACZ,KAAsB,YAAY,iBACnC,eAAe,QACf,OAAQ,KAAsB,cAAc;;;;;;AAQhD,eAAsB,oBACpB,WACmC;CACnC,MAAM,6BAAa,IAAI,KAA0B;AAEjD,KAAI;EACF,MAAM,MAAM,MAAM,GAAG,SAAS,WAAW,OAAO;EAChD,MAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,MAAI,CAAC,aAAa,OAAO,EAAE;AACzB,UAAO,KAAK,sDAAsD;AAClE,UAAO;;EAET,MAAM,QAAQ;AAEd,OAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,MAAM,UAAU,CAC1D,KAAI,MAAM,eAAe,MAAM,YAAY,SAAS,EAClD,YAAW,IAAI,OAAO,IAAI,IAAI,MAAM,YAAY,CAAC;UAG9C,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO,KACL,0CACC,IAAc,QAChB;;AAKL,QAAO;;;;;;;;AAST,SAAgB,kBACd,MACA,YACA,OACA,aAAiC,SACR;CACzB,MAAM,UAAU,WAAW,IAAI,MAAM;AACrC,KAAI,CAAC,QAAS,QAAO;CAErB,MAAM,WAAqB,EAAE;CAC7B,MAAM,WAAoC,EAAE;AAE5C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,CAC7C,KAAI,QAAQ,IAAI,IAAI,CAClB,UAAS,OAAO;KAEhB,UAAS,KAAK,IAAI;AAItB,KAAI,SAAS,SAAS,GAAG;AACvB,MAAI,eAAe,SACjB,OAAM,IAAI,MAAM,+BAA+B,SAAS,KAAK,KAAK,GAAG;AAEvE,SAAO,KACL,yCACA,OACA,SAAS,KAAK,KAAK,CACpB;;AAGH,QAAO"}
1
+ {"version":3,"file":"schema-filter.js","names":[],"sources":["../../../src/plugins/serving/schema-filter.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport {\n CACHE_VERSION,\n type ServingCache,\n} from \"../../type-generator/serving/cache\";\n\nconst logger = createLogger(\"serving:schema-filter\");\n\nfunction isValidCache(data: unknown): data is ServingCache {\n return (\n typeof data === \"object\" &&\n data !== null &&\n \"version\" in data &&\n (data as ServingCache).version === CACHE_VERSION &&\n \"endpoints\" in data &&\n typeof (data as ServingCache).endpoints === \"object\"\n );\n}\n\n/**\n * Loads endpoint schemas from the type generation cache file.\n * Returns a map of alias → allowed parameter keys.\n */\nexport async function loadEndpointSchemas(\n cacheFile: string,\n): Promise<Map<string, Set<string>>> {\n const allowlists = new Map<string, Set<string>>();\n\n try {\n const raw = await fs.readFile(cacheFile, \"utf8\");\n const parsed: unknown = JSON.parse(raw);\n if (!isValidCache(parsed)) {\n logger.warn(\"Serving types cache has invalid structure, skipping\");\n return allowlists;\n }\n const cache = parsed;\n\n for (const [alias, entry] of Object.entries(cache.endpoints)) {\n if (entry.requestKeys && entry.requestKeys.length > 0) {\n allowlists.set(alias, new Set(entry.requestKeys));\n }\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n logger.warn(\n \"Failed to load serving types cache: %s\",\n (err as Error).message,\n );\n }\n // No cache → no filtering, passthrough mode\n }\n\n return allowlists;\n}\n\n/**\n * Filters a request body against the allowed keys for an endpoint alias.\n * Returns the filtered body and logs a warning for stripped params.\n *\n * If no allowlist exists for the alias, returns the body unchanged (passthrough).\n */\nexport function filterRequestBody(\n body: Record<string, unknown>,\n allowlists: Map<string, Set<string>>,\n alias: string,\n filterMode: \"strip\" | \"reject\" = \"strip\",\n): Record<string, unknown> {\n const allowed = allowlists.get(alias);\n if (!allowed) return body;\n\n const stripped: string[] = [];\n const filtered: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(body)) {\n if (allowed.has(key)) {\n filtered[key] = value;\n } else {\n stripped.push(key);\n }\n }\n\n if (stripped.length > 0) {\n if (filterMode === \"reject\") {\n throw new Error(`Unknown request parameters: ${stripped.join(\", \")}`);\n }\n logger.warn(\n \"Stripped unknown params from '%s': %s\",\n alias,\n stripped.join(\", \"),\n );\n }\n\n return filtered;\n}\n"],"mappings":";;;;;AAQA,MAAM,SAAS,aAAa,wBAAwB;AAEpD,SAAS,aAAa,MAAqC;AACzD,QACE,OAAO,SAAS,YAChB,SAAS,QACT,aAAa,QACZ,KAAsB,YAAY,iBACnC,eAAe,QACf,OAAQ,KAAsB,cAAc;;;;;;AAQhD,eAAsB,oBACpB,WACmC;CACnC,MAAM,6BAAa,IAAI,KAA0B;AAEjD,KAAI;EACF,MAAM,MAAM,MAAM,GAAG,SAAS,WAAW,OAAO;EAChD,MAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,MAAI,CAAC,aAAa,OAAO,EAAE;AACzB,UAAO,KAAK,sDAAsD;AAClE,UAAO;;EAET,MAAM,QAAQ;AAEd,OAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,MAAM,UAAU,CAC1D,KAAI,MAAM,eAAe,MAAM,YAAY,SAAS,EAClD,YAAW,IAAI,OAAO,IAAI,IAAI,MAAM,YAAY,CAAC;UAG9C,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO,KACL,0CACC,IAAc,QAChB;;AAKL,QAAO;;;;;;;;AAST,SAAgB,kBACd,MACA,YACA,OACA,aAAiC,SACR;CACzB,MAAM,UAAU,WAAW,IAAI,MAAM;AACrC,KAAI,CAAC,QAAS,QAAO;CAErB,MAAM,WAAqB,EAAE;CAC7B,MAAM,WAAoC,EAAE;AAE5C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,CAC7C,KAAI,QAAQ,IAAI,IAAI,CAClB,UAAS,OAAO;KAEhB,UAAS,KAAK,IAAI;AAItB,KAAI,SAAS,SAAS,GAAG;AACvB,MAAI,eAAe,SACjB,OAAM,IAAI,MAAM,+BAA+B,SAAS,KAAK,KAAK,GAAG;AAEvE,SAAO,KACL,yCACA,OACA,SAAS,KAAK,KAAK,CACpB;;AAGH,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"serving.d.ts","names":[],"sources":["../../../src/plugins/serving/serving.ts"],"mappings":";;;;;;;;;;;cAyCa,aAAA,SAAsB,MAAA;EAAA,OAC1B,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,cAAA;EAAA,iBAET,SAAA;EAAA,iBACA,WAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,cAAA;EAed,KAAA,CAAA,GAAS,OAAA;EAAA,OAiBR,uBAAA,CACL,MAAA,EAAQ,cAAA,GACP,mBAAA;EAAA,QAqBK,gBAAA;EA0BR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAgEf,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA0BG,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2DG,MAAA,CACJ,KAAA,UACA,IAAA,EAAM,MAAA,oBACL,OAAA,CAAQ,eAAA;EAiBX,YAAA,CAAA,GAAgB,MAAA;EAOV,QAAA,CAAA,GAAY,OAAA;EAAA,UAIR,iBAAA,CAAkB,KAAA,WAAgB,sBAAA;EAM5C,OAAA,CAAA,GAAW,cAAA;AAAA;;;;cAmBA,OAAA,EAAO,QAAA,QAAA,aAAA,EAAA,cAAA"}
1
+ {"version":3,"file":"serving.d.ts","names":[],"sources":["../../../src/plugins/serving/serving.ts"],"mappings":";;;;;;;;;;;cA2Ca,aAAA,SAAsB,MAAA;EAAA,OAC1B,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,cAAA;EAAA,iBAET,SAAA;EAAA,iBACA,WAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,cAAA;EAed,KAAA,CAAA,GAAS,OAAA;EAAA,OAiBR,uBAAA,CACL,MAAA,EAAQ,cAAA,GACP,mBAAA;EAAA,QAqBK,gBAAA;EA0BR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAgEf,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA0BG,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2DG,MAAA,CACJ,KAAA,UACA,IAAA,EAAM,MAAA,oBACL,OAAA,CAAQ,eAAA;EAiBX,YAAA,CAAA,GAAgB,MAAA;EAOV,QAAA,CAAA,GAAY,OAAA;EAAA,UAIR,iBAAA,CAAkB,KAAA,WAAgB,sBAAA;EAM5C,OAAA,CAAA,GAAW,cAAA;AAAA;;;;cAmBA,OAAA,EAAO,QAAA,QAAA,aAAA,EAAA,cAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"serving\">;\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n protected declare config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\n\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"serving\">;\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n declare protected config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/ui-variants/index.ts"],"sourcesContent":["import type { BasePluginConfig, IAppRouter } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { FileChoiceStore, type UiChoiceRecord } from \"./choice-sink\";\nimport manifest from \"./manifest.json\";\n\nconst logger = createLogger(\"ui-variants\");\n\n/** Untrusted body of `POST /api/ui-variants/confirm`; validated in the handler. */\ninterface ConfirmRequestBody {\n blockId?: unknown;\n chosenIndex?: unknown;\n label?: unknown;\n note?: unknown;\n}\n\n/**\n * Dev-only recorder backing the `<Variants>` UI picker.\n *\n * When a developer confirms a variant in the browser, the `<Variants>`\n * component POSTs to `/api/ui-variants/confirm`. This plugin records the choice\n * into a keyed JSONL file, which a coding agent reads to finalize the chosen\n * variant into the component source. Recording is a keyed upsert — one entry\n * per `<Variants>` id — and the plugin only records; it never edits source.\n */\nclass UiVariantsPlugin extends Plugin<BasePluginConfig> {\n static manifest = manifest as PluginManifest<\"uiVariants\">;\n\n protected static description =\n \"Dev-only recorder for the <Variants> UI picker\";\n\n private readonly store: FileChoiceStore;\n\n constructor(config: BasePluginConfig = {} as BasePluginConfig) {\n super(config);\n this.store = new FileChoiceStore();\n }\n\n injectRoutes(router: IAppRouter): void {\n this.route(router, {\n name: \"confirm\",\n method: \"post\",\n path: \"/confirm\",\n handler: async (req, res) => {\n // Dev-only: the recorder exists solely to drive the local edit loop,\n // so it must never run in a deployed app.\n if (process.env.NODE_ENV !== \"development\") {\n res.status(403).json({\n ok: false,\n message: \"ui-variants confirm is only available in development\",\n });\n return;\n }\n\n const body = (req.body ?? {}) as ConfirmRequestBody;\n const { blockId, chosenIndex } = body;\n\n if (typeof blockId !== \"string\" || blockId.length === 0) {\n res.status(400).json({\n ok: false,\n message: \"`blockId` (non-empty string) is required\",\n });\n return;\n }\n if (typeof chosenIndex !== \"number\" || !Number.isInteger(chosenIndex)) {\n res.status(400).json({\n ok: false,\n message: \"`chosenIndex` (integer) is required\",\n });\n return;\n }\n\n const record: UiChoiceRecord = {\n ts: new Date().toISOString(),\n blockId,\n chosenIndex,\n ...(typeof body.label === \"string\" ? { label: body.label } : {}),\n ...(typeof body.note === \"string\" ? { note: body.note } : {}),\n };\n\n try {\n await this.store.record(record);\n } catch (error) {\n logger.error(\"Failed to record UI variant choice\", {\n error,\n blockId,\n });\n res\n .status(500)\n .json({ ok: false, message: \"Failed to record variant choice\" });\n return;\n }\n\n logger.info(\"Recorded UI variant choice\", {\n blockId,\n chosenIndex,\n label: record.label,\n });\n res.json({ ok: true, blockId, chosenIndex });\n },\n });\n }\n}\n\nexport const uiVariants = toPlugin(UiVariantsPlugin);\n"],"mappings":";;;;;;;;AAOA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;AAmB1C,IAAM,mBAAN,cAA+B,OAAyB;CACtD,OAAO,WAAWA;CAElB,OAAiB,cACf;CAEF,AAAiB;CAEjB,YAAY,SAA2B,EAAE,EAAsB;AAC7D,QAAM,OAAO;AACb,OAAK,QAAQ,IAAI,iBAAiB;;CAGpC,aAAa,QAA0B;AACrC,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAK,QAAQ;AAG3B,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,OAAQ,IAAI,QAAQ,EAAE;IAC5B,MAAM,EAAE,SAAS,gBAAgB;AAEjC,QAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,GAAG;AACvD,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;AAEF,QAAI,OAAO,gBAAgB,YAAY,CAAC,OAAO,UAAU,YAAY,EAAE;AACrE,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,SAAyB;KAC7B,qBAAI,IAAI,MAAM,EAAC,aAAa;KAC5B;KACA;KACA,GAAI,OAAO,KAAK,UAAU,WAAW,EAAE,OAAO,KAAK,OAAO,GAAG,EAAE;KAC/D,GAAI,OAAO,KAAK,SAAS,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,EAAE;KAC7D;AAED,QAAI;AACF,WAAM,KAAK,MAAM,OAAO,OAAO;aACxB,OAAO;AACd,YAAO,MAAM,sCAAsC;MACjD;MACA;MACD,CAAC;AACF,SACG,OAAO,IAAI,CACX,KAAK;MAAE,IAAI;MAAO,SAAS;MAAmC,CAAC;AAClE;;AAGF,WAAO,KAAK,8BAA8B;KACxC;KACA;KACA,OAAO,OAAO;KACf,CAAC;AACF,QAAI,KAAK;KAAE,IAAI;KAAM;KAAS;KAAa,CAAC;;GAE/C,CAAC;;;AAIN,MAAa,aAAa,SAAS,iBAAiB"}
1
+ {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/ui-variants/index.ts"],"sourcesContent":["import type { BasePluginConfig, IAppRouter } from \"shared\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { FileChoiceStore, type UiChoiceRecord } from \"./choice-sink\";\nimport manifest from \"./manifest.json\";\n\nconst logger = createLogger(\"ui-variants\");\n\n/** Untrusted body of `POST /api/ui-variants/confirm`; validated in the handler. */\ninterface ConfirmRequestBody {\n blockId?: unknown;\n chosenIndex?: unknown;\n label?: unknown;\n note?: unknown;\n}\n\n/**\n * Dev-only recorder backing the `<Variants>` UI picker.\n *\n * When a developer confirms a variant in the browser, the `<Variants>`\n * component POSTs to `/api/ui-variants/confirm`. This plugin records the choice\n * into a keyed JSONL file, which a coding agent reads to finalize the chosen\n * variant into the component source. Recording is a keyed upsert — one entry\n * per `<Variants>` id — and the plugin only records; it never edits source.\n */\nclass UiVariantsPlugin extends Plugin<BasePluginConfig> {\n static manifest = manifest as PluginManifest<\"uiVariants\">;\n\n protected static description =\n \"Dev-only recorder for the <Variants> UI picker\";\n\n private readonly store: FileChoiceStore;\n\n constructor(config: BasePluginConfig = {} as BasePluginConfig) {\n super(config);\n this.store = new FileChoiceStore();\n }\n\n injectRoutes(router: IAppRouter): void {\n this.route(router, {\n name: \"confirm\",\n method: \"post\",\n path: \"/confirm\",\n handler: async (req, res) => {\n // Dev-only: the recorder exists solely to drive the local edit loop,\n // so it must never run in a deployed app.\n if (process.env.NODE_ENV !== \"development\") {\n res.status(403).json({\n ok: false,\n message: \"ui-variants confirm is only available in development\",\n });\n return;\n }\n\n const body = (req.body ?? {}) as ConfirmRequestBody;\n const { blockId, chosenIndex } = body;\n\n if (typeof blockId !== \"string\" || blockId.length === 0) {\n res.status(400).json({\n ok: false,\n message: \"`blockId` (non-empty string) is required\",\n });\n return;\n }\n if (typeof chosenIndex !== \"number\" || !Number.isInteger(chosenIndex)) {\n res.status(400).json({\n ok: false,\n message: \"`chosenIndex` (integer) is required\",\n });\n return;\n }\n\n const record: UiChoiceRecord = {\n ts: new Date().toISOString(),\n blockId,\n chosenIndex,\n ...(typeof body.label === \"string\" ? { label: body.label } : {}),\n ...(typeof body.note === \"string\" ? { note: body.note } : {}),\n };\n\n try {\n await this.store.record(record);\n } catch (error) {\n logger.error(\"Failed to record UI variant choice\", {\n error,\n blockId,\n });\n res\n .status(500)\n .json({ ok: false, message: \"Failed to record variant choice\" });\n return;\n }\n\n logger.info(\"Recorded UI variant choice\", {\n blockId,\n chosenIndex,\n label: record.label,\n });\n res.json({ ok: true, blockId, chosenIndex });\n },\n });\n }\n}\n\nexport const uiVariants = toPlugin(UiVariantsPlugin);\n"],"mappings":";;;;;;;;AAQA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;AAmB1C,IAAM,mBAAN,cAA+B,OAAyB;CACtD,OAAO,WAAWA;CAElB,OAAiB,cACf;CAEF,AAAiB;CAEjB,YAAY,SAA2B,EAAE,EAAsB;AAC7D,QAAM,OAAO;AACb,OAAK,QAAQ,IAAI,iBAAiB;;CAGpC,aAAa,QAA0B;AACrC,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAK,QAAQ;AAG3B,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,OAAQ,IAAI,QAAQ,EAAE;IAC5B,MAAM,EAAE,SAAS,gBAAgB;AAEjC,QAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,GAAG;AACvD,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;AAEF,QAAI,OAAO,gBAAgB,YAAY,CAAC,OAAO,UAAU,YAAY,EAAE;AACrE,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,SAAyB;KAC7B,qBAAI,IAAI,MAAM,EAAC,aAAa;KAC5B;KACA;KACA,GAAI,OAAO,KAAK,UAAU,WAAW,EAAE,OAAO,KAAK,OAAO,GAAG,EAAE;KAC/D,GAAI,OAAO,KAAK,SAAS,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,EAAE;KAC7D;AAED,QAAI;AACF,WAAM,KAAK,MAAM,OAAO,OAAO;aACxB,OAAO;AACd,YAAO,MAAM,sCAAsC;MACjD;MACA;MACD,CAAC;AACF,SACG,OAAO,IAAI,CACX,KAAK;MAAE,IAAI;MAAO,SAAS;MAAmC,CAAC;AAClE;;AAGF,WAAO,KAAK,8BAA8B;KACxC;KACA;KACA,OAAO,OAAO;KACf,CAAC;AACF,QAAI,KAAK;KAAE,IAAI;KAAM;KAAS;KAAa,CAAC;;GAE/C,CAAC;;;AAIN,MAAa,aAAa,SAAS,iBAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","names":[],"sources":["../../src/registry/manifest-loader.ts"],"mappings":";;;;;;;;;;;AA4DA;;;;iBAAgB,iBAAA,CAAkB,MAAA,EAAQ,iBAAA,GAAoB,cAAA;;;;;AA4F9D;;;;;;;;;;;;;;iBAAgB,uBAAA,CAAwB,MAAA,EAAQ,iBAAA"}
1
+ {"version":3,"file":"manifest-loader.d.ts","names":[],"sources":["../../src/registry/manifest-loader.ts"],"mappings":";;;;;;;;;;;AA6DA;;;;iBAAgB,iBAAA,CAAkB,MAAA,EAAQ,iBAAA,GAAoB,cAAA;;;;;AA4F9D;;;;;;;;;;;;;;iBAAgB,uBAAA,CAAwB,MAAA,EAAQ,iBAAA"}
@@ -1 +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.manifest?.name || 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":";;;;;;;AAUA,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,UAAU,QAAQ,OAAO,QAAQ;AAE3D,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"}
1
+ {"version":3,"file":"manifest-loader.js","names":[],"sources":["../../src/registry/manifest-loader.ts"],"sourcesContent":["import type { PluginConstructor } from \"shared\";\n\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.manifest?.name || 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":";;;;;;;AAWA,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,UAAU,QAAQ,OAAO,QAAQ;AAE3D,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"resource-registry.d.ts","names":[],"sources":["../../src/registry/resource-registry.ts"],"mappings":";;;;;;;;;cAsDa,gBAAA;EAAA,QACH,SAAA;EAoZsC;;;;;;;;;;;;EAtYvC,QAAA,CAAS,MAAA,UAAgB,QAAA,EAAU,mBAAA;EAiKnC;;;;;;;EAtIA,gBAAA,CACL,UAAA,EAAY,UAAA,CAAW,iBAAA;EAyKlB;;;;EAAA,QAjIC,cAAA;EAqJD;;;;;;EAxDA,MAAA,CAAA,GAAU,aAAA;EAgNH;;;;;;;EArMP,GAAA,CAAI,IAAA,UAAc,WAAA,WAAsB,aAAA;;;;;EAQxC,KAAA,CAAA;;;;EAOA,IAAA,CAAA;;;;;;;EAUA,WAAA,CAAY,UAAA,WAAqB,aAAA;;;;;;EAWjC,WAAA,CAAA,GAAe,aAAA;;;;;;EASf,WAAA,CAAA,GAAe,aAAA;;;;;;;;;;;;;;;;;;;;;;EAyBf,QAAA,CAAA,GAAY,gBAAA;;;;;;;;;;;;EA+DZ,iBAAA,CAAA,GAAqB,gBAAA;;;;;;;;iBA8Cb,YAAA;;;;;;;SAkBD,sBAAA,CAAuB,OAAA,EAAS,aAAA;;;;;;;;SAqBhC,sBAAA,CAAuB,OAAA,EAAS,aAAA;AAAA"}
1
+ {"version":3,"file":"resource-registry.d.ts","names":[],"sources":["../../src/registry/resource-registry.ts"],"mappings":";;;;;;;;;cAuDa,gBAAA;EAAA,QACH,SAAA;EAoZsC;;;;;;;;;;;;EAtYvC,QAAA,CAAS,MAAA,UAAgB,QAAA,EAAU,mBAAA;EAiKnC;;;;;;;EAtIA,gBAAA,CACL,UAAA,EAAY,UAAA,CAAW,iBAAA;EAyKlB;;;;EAAA,QAjIC,cAAA;EAqJD;;;;;;EAxDA,MAAA,CAAA,GAAU,aAAA;EAgNH;;;;;;;EArMP,GAAA,CAAI,IAAA,UAAc,WAAA,WAAsB,aAAA;;;;;EAQxC,KAAA,CAAA;;;;EAOA,IAAA,CAAA;;;;;;;EAUA,WAAA,CAAY,UAAA,WAAqB,aAAA;;;;;;EAWjC,WAAA,CAAA,GAAe,aAAA;;;;;;EASf,WAAA,CAAA,GAAe,aAAA;;;;;;;;;;;;;;;;;;;;;;EAyBf,QAAA,CAAA,GAAY,gBAAA;;;;;;;;;;;;EA+DZ,iBAAA,CAAA,GAAqB,gBAAA;;;;;;;;iBA8Cb,YAAA;;;;;;;SAkBD,sBAAA,CAAuB,OAAA,EAAS,aAAA;;;;;;;;SAqBhC,sBAAA,CAAuB,OAAA,EAAS,aAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"resource-registry.js","names":[],"sources":["../../src/registry/resource-registry.ts"],"sourcesContent":["/**\n * Resource Registry\n *\n * Central registry that tracks all resource requirements across all plugins.\n * Provides visibility into Databricks resources needed by the application\n * and handles deduplication when multiple plugins require the same resource\n * (dedup key: type + resourceKey).\n *\n * Use `new ResourceRegistry()` for instance-scoped usage (e.g. createApp).\n * getInstance() / resetInstance() remain for backward compatibility in tests.\n */\n\nimport type { BasePluginConfig, PluginConstructor, PluginData } from \"shared\";\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { formatWarningBanner } from \"../utils/banner\";\nimport { getPluginManifest } from \"./manifest-loader\";\nimport type {\n ResourceEntry,\n ResourcePermission,\n ResourceRequirement,\n ValidationResult,\n} from \"./types\";\nimport { PERMISSION_HIERARCHY_BY_TYPE, type ResourceType } from \"./types\";\n\nconst logger = createLogger(\"resource-registry\");\n\n/**\n * Dedup key for registry: type + resourceKey (machine-stable).\n * alias is for UI/display only.\n */\nfunction getDedupKey(type: string, resourceKey: string): string {\n return `${type}:${resourceKey}`;\n}\n\n/**\n * Returns the most permissive permission for a given resource type.\n * Uses per-type hierarchy; unknown permissions are treated as least permissive.\n */\nfunction getMostPermissivePermission(\n resourceType: ResourceType,\n p1: ResourcePermission,\n p2: ResourcePermission,\n): ResourcePermission {\n const hierarchy = PERMISSION_HIERARCHY_BY_TYPE[resourceType as ResourceType];\n const index1 = hierarchy?.indexOf(p1) ?? -1;\n const index2 = hierarchy?.indexOf(p2) ?? -1;\n return index1 > index2 ? p1 : p2;\n}\n\n/**\n * Central registry for tracking plugin resource requirements.\n * Deduplication uses type + resourceKey (machine-stable); alias is for display only.\n */\nexport class ResourceRegistry {\n private resources: Map<string, ResourceEntry> = new Map();\n\n /**\n * Registers a resource requirement for a plugin.\n * If a resource with the same type+resourceKey already exists, merges them:\n * - Combines plugin names (comma-separated)\n * - Uses the most permissive permission (per-type hierarchy)\n * - Marks as required if any plugin requires it\n * - Combines descriptions if they differ\n * - Merges fields; warns when same field name uses different env vars\n *\n * @param plugin - Name of the plugin registering the resource\n * @param resource - Resource requirement specification\n */\n public register(plugin: string, resource: ResourceRequirement): void {\n const key = getDedupKey(resource.type, resource.resourceKey);\n const existing = this.resources.get(key);\n\n if (existing) {\n // Merge with existing resource\n const merged = this.mergeResources(existing, plugin, resource);\n this.resources.set(key, merged);\n } else {\n // Create new resource entry with permission source tracking\n const entry: ResourceEntry = {\n ...resource,\n plugin,\n resolved: false,\n permissionSources: { [plugin]: resource.permission },\n };\n this.resources.set(key, entry);\n }\n }\n\n /**\n * Collects and registers resource requirements from an array of plugins.\n * For each plugin, loads its manifest (required) and runtime resource requirements.\n *\n * @param rawPlugins - Array of plugin data entries from createApp configuration\n * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid\n */\n public collectResources(\n rawPlugins: PluginData<PluginConstructor, unknown, string>[],\n ): void {\n for (const pluginData of rawPlugins) {\n if (!pluginData?.plugin) continue;\n\n const pluginName = pluginData.name;\n const manifest = getPluginManifest(pluginData.plugin);\n\n // Register required resources\n for (const resource of manifest.resources.required) {\n this.register(pluginName, { ...resource, required: true });\n }\n\n // Register optional resources\n for (const resource of manifest.resources.optional || []) {\n this.register(pluginName, { ...resource, required: false });\n }\n\n // Check for runtime resource requirements\n if (typeof pluginData.plugin.getResourceRequirements === \"function\") {\n const runtimeResources = pluginData.plugin.getResourceRequirements(\n pluginData.config as BasePluginConfig,\n );\n for (const resource of runtimeResources) {\n this.register(pluginName, resource as ResourceRequirement);\n }\n }\n\n logger.debug(\n \"Collected resources from plugin %s: %d total\",\n pluginName,\n this.getByPlugin(pluginName).length,\n );\n }\n }\n\n /**\n * Merges a new resource requirement with an existing entry.\n * Applies intelligent merging logic for conflicting properties.\n */\n private mergeResources(\n existing: ResourceEntry,\n newPlugin: string,\n newResource: ResourceRequirement,\n ): ResourceEntry {\n // Combine plugin names if not already included\n const plugins = existing.plugin.split(\", \");\n if (!plugins.includes(newPlugin)) {\n plugins.push(newPlugin);\n }\n\n // Track per-plugin permission sources\n const permissionSources: Record<string, ResourcePermission> = {\n ...(existing.permissionSources ?? {}),\n [newPlugin]: newResource.permission,\n };\n\n // Use the most permissive permission for this resource type; warn when escalating\n const permission = getMostPermissivePermission(\n existing.type as ResourceType,\n existing.permission,\n newResource.permission,\n );\n\n if (permission !== existing.permission) {\n logger.warn(\n 'Resource %s:%s permission escalated from \"%s\" to \"%s\" due to plugin \"%s\" ' +\n \"(previously requested by: %s). Review plugin permissions to ensure least-privilege.\",\n existing.type,\n existing.resourceKey,\n existing.permission,\n permission,\n newPlugin,\n existing.plugin,\n );\n }\n\n // Mark as required if any plugin requires it\n const required = existing.required || newResource.required;\n\n // Combine descriptions if they differ\n let description = existing.description;\n if (\n newResource.description &&\n newResource.description !== existing.description\n ) {\n if (!existing.description.includes(newResource.description)) {\n description = `${existing.description}; ${newResource.description}`;\n }\n }\n\n // Merge fields: union of field names; warn when same field name uses different env\n const fields = { ...(existing.fields ?? {}) };\n for (const [fieldName, newField] of Object.entries(\n newResource.fields ?? {},\n )) {\n const existingField = fields[fieldName];\n if (existingField) {\n if (existingField.env !== newField.env) {\n logger.warn(\n 'Resource %s:%s field \"%s\": conflicting env vars \"%s\" (from %s) vs \"%s\" (from %s). Using first.',\n existing.type,\n existing.resourceKey,\n fieldName,\n existingField.env,\n existing.plugin,\n newField.env,\n newPlugin,\n );\n }\n // keep existing\n } else {\n fields[fieldName] = newField;\n }\n }\n\n return {\n ...existing,\n plugin: plugins.join(\", \"),\n permission,\n permissionSources,\n required,\n description,\n fields,\n };\n }\n\n /**\n * Retrieves all registered resources.\n * Returns a copy of the array to prevent external mutations.\n *\n * @returns Array of all registered resource entries\n */\n public getAll(): ResourceEntry[] {\n return Array.from(this.resources.values());\n }\n\n /**\n * Gets a specific resource by type and resourceKey (dedup key).\n *\n * @param type - Resource type\n * @param resourceKey - Stable machine key (not alias; alias is for display only)\n * @returns The resource entry if found, undefined otherwise\n */\n public get(type: string, resourceKey: string): ResourceEntry | undefined {\n return this.resources.get(getDedupKey(type, resourceKey));\n }\n\n /**\n * Clears all registered resources.\n * Useful for testing or when rebuilding the registry.\n */\n public clear(): void {\n this.resources.clear();\n }\n\n /**\n * Returns the number of registered resources.\n */\n public size(): number {\n return this.resources.size;\n }\n\n /**\n * Gets all resources required by a specific plugin.\n *\n * @param pluginName - Name of the plugin\n * @returns Array of resources where the plugin is listed as a requester\n */\n public getByPlugin(pluginName: string): ResourceEntry[] {\n return this.getAll().filter((entry) =>\n entry.plugin.split(\", \").includes(pluginName),\n );\n }\n\n /**\n * Gets all required resources (where required=true).\n *\n * @returns Array of required resource entries\n */\n public getRequired(): ResourceEntry[] {\n return this.getAll().filter((entry) => entry.required);\n }\n\n /**\n * Gets all optional resources (where required=false).\n *\n * @returns Array of optional resource entries\n */\n public getOptional(): ResourceEntry[] {\n return this.getAll().filter((entry) => !entry.required);\n }\n\n /**\n * Validates all registered resources against the environment.\n *\n * Checks each resource's field environment variables to determine if it's resolved.\n * Updates the `resolved` and `values` fields on each resource entry.\n *\n * Only required resources affect the `valid` status - optional resources\n * are checked but don't cause validation failure.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n *\n * @example\n * ```typescript\n * const registry = ResourceRegistry.getInstance();\n * const result = registry.validate();\n *\n * if (!result.valid) {\n * console.error(ResourceRegistry.formatMissingResources(result.missing));\n * }\n * ```\n */\n public validate(): ValidationResult {\n const missing: ResourceEntry[] = [];\n\n for (const entry of this.resources.values()) {\n const values: Record<string, string> = {};\n let allSet = true;\n for (const [fieldName, fieldDef] of Object.entries(entry.fields)) {\n if (!fieldDef.env) continue;\n const val = process.env[fieldDef.env];\n if (val !== undefined && val !== \"\") {\n values[fieldName] = val;\n } else {\n allSet = false;\n }\n }\n if (allSet) {\n entry.resolved = true;\n entry.values = values;\n logger.debug(\n \"Resource %s:%s resolved from fields\",\n entry.type,\n entry.alias,\n );\n } else {\n entry.resolved = false;\n entry.values = Object.keys(values).length > 0 ? values : undefined;\n if (entry.required) {\n missing.push(entry);\n logger.debug(\n \"Required resource %s:%s missing (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n } else {\n logger.debug(\n \"Optional resource %s:%s not configured (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n }\n }\n }\n\n return {\n valid: missing.length === 0,\n missing,\n all: this.getAll(),\n };\n }\n\n /**\n * Validates all registered resources and enforces the result.\n *\n * - In production: throws a {@link ConfigurationError} if any required resources are missing.\n * - In development (`NODE_ENV=development`): logs a warning but continues, unless\n * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.\n * - When all resources are valid: logs a debug message with the count.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true\n */\n public enforceValidation(): ValidationResult {\n const validation = this.validate();\n const isDevelopment = process.env.NODE_ENV === \"development\";\n const strictValidation =\n process.env.APPKIT_STRICT_VALIDATION === \"true\" ||\n process.env.APPKIT_STRICT_VALIDATION === \"1\";\n\n if (!validation.valid) {\n const errorMessage = ResourceRegistry.formatMissingResources(\n validation.missing,\n );\n\n const shouldThrow = !isDevelopment || strictValidation;\n\n if (shouldThrow) {\n throw new ConfigurationError(errorMessage, {\n context: {\n missingResources: validation.missing.map((r) => ({\n type: r.type,\n alias: r.alias,\n plugin: r.plugin,\n envVars: ResourceRegistry.unsetEnvVars(r),\n })),\n },\n });\n }\n\n // Dev mode without strict: use a visually prominent box so the warning can't be missed\n const banner = ResourceRegistry.formatDevWarningBanner(\n validation.missing,\n );\n logger.warn(\"\\n%s\", banner);\n } else if (this.size() > 0) {\n logger.debug(\"All %d resources validated successfully\", this.size());\n }\n\n return validation;\n }\n\n /**\n * Env var names for an entry's fields that are actually unset (undefined or\n * empty), skipping fields that declare no env var (e.g. discovery-only\n * fields resolved at deploy time). Mirrors the resolution check in\n * {@link validate} so error output lists exactly what the caller must set —\n * not every field on the resource.\n */\n private static unsetEnvVars(entry: ResourceEntry): string[] {\n const unset: string[] = [];\n for (const field of Object.values(entry.fields)) {\n if (!field.env) continue;\n const val = process.env[field.env];\n if (val === undefined || val === \"\") {\n unset.push(field.env);\n }\n }\n return unset;\n }\n\n /**\n * Formats missing resources into a human-readable error message.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted error message string\n */\n public static formatMissingResources(missing: ResourceEntry[]): string {\n if (missing.length === 0) {\n return \"No missing resources\";\n }\n\n const lines = missing.map((entry) => {\n const envVars = ResourceRegistry.unsetEnvVars(entry);\n const envHint = envVars.length > 0 ? ` (set ${envVars.join(\", \")})` : \"\";\n return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;\n });\n\n return `Missing required resources:\\n${lines.join(\"\\n\")}`;\n }\n\n /**\n * Formats a highly visible warning banner for dev-mode missing resources.\n * Uses box drawing to ensure the message is impossible to miss in scrolling logs.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted banner string\n */\n public static formatDevWarningBanner(missing: ResourceEntry[]): string {\n const contentLines: string[] = [\n \"MISSING REQUIRED RESOURCES (dev mode — would fail in production)\",\n \"\",\n ];\n\n for (const entry of missing) {\n const envVars = ResourceRegistry.unsetEnvVars(entry);\n contentLines.push(\n ` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`,\n );\n if (envVars.length > 0) {\n contentLines.push(` Set: ${envVars.join(\", \")}`);\n }\n }\n\n contentLines.push(\"\");\n contentLines.push(\n \"Add these to your .env file or environment to suppress this warning.\",\n );\n\n return formatWarningBanner(contentLines);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resource-registry.js","names":[],"sources":["../../src/registry/resource-registry.ts"],"sourcesContent":["/**\n * Resource Registry\n *\n * Central registry that tracks all resource requirements across all plugins.\n * Provides visibility into Databricks resources needed by the application\n * and handles deduplication when multiple plugins require the same resource\n * (dedup key: type + resourceKey).\n *\n * Use `new ResourceRegistry()` for instance-scoped usage (e.g. createApp).\n * getInstance() / resetInstance() remain for backward compatibility in tests.\n */\n\nimport type { BasePluginConfig, PluginConstructor, PluginData } from \"shared\";\n\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { formatWarningBanner } from \"../utils/banner\";\nimport { getPluginManifest } from \"./manifest-loader\";\nimport type {\n ResourceEntry,\n ResourcePermission,\n ResourceRequirement,\n ValidationResult,\n} from \"./types\";\nimport { PERMISSION_HIERARCHY_BY_TYPE, type ResourceType } from \"./types\";\n\nconst logger = createLogger(\"resource-registry\");\n\n/**\n * Dedup key for registry: type + resourceKey (machine-stable).\n * alias is for UI/display only.\n */\nfunction getDedupKey(type: string, resourceKey: string): string {\n return `${type}:${resourceKey}`;\n}\n\n/**\n * Returns the most permissive permission for a given resource type.\n * Uses per-type hierarchy; unknown permissions are treated as least permissive.\n */\nfunction getMostPermissivePermission(\n resourceType: ResourceType,\n p1: ResourcePermission,\n p2: ResourcePermission,\n): ResourcePermission {\n const hierarchy = PERMISSION_HIERARCHY_BY_TYPE[resourceType as ResourceType];\n const index1 = hierarchy?.indexOf(p1) ?? -1;\n const index2 = hierarchy?.indexOf(p2) ?? -1;\n return index1 > index2 ? p1 : p2;\n}\n\n/**\n * Central registry for tracking plugin resource requirements.\n * Deduplication uses type + resourceKey (machine-stable); alias is for display only.\n */\nexport class ResourceRegistry {\n private resources: Map<string, ResourceEntry> = new Map();\n\n /**\n * Registers a resource requirement for a plugin.\n * If a resource with the same type+resourceKey already exists, merges them:\n * - Combines plugin names (comma-separated)\n * - Uses the most permissive permission (per-type hierarchy)\n * - Marks as required if any plugin requires it\n * - Combines descriptions if they differ\n * - Merges fields; warns when same field name uses different env vars\n *\n * @param plugin - Name of the plugin registering the resource\n * @param resource - Resource requirement specification\n */\n public register(plugin: string, resource: ResourceRequirement): void {\n const key = getDedupKey(resource.type, resource.resourceKey);\n const existing = this.resources.get(key);\n\n if (existing) {\n // Merge with existing resource\n const merged = this.mergeResources(existing, plugin, resource);\n this.resources.set(key, merged);\n } else {\n // Create new resource entry with permission source tracking\n const entry: ResourceEntry = {\n ...resource,\n plugin,\n resolved: false,\n permissionSources: { [plugin]: resource.permission },\n };\n this.resources.set(key, entry);\n }\n }\n\n /**\n * Collects and registers resource requirements from an array of plugins.\n * For each plugin, loads its manifest (required) and runtime resource requirements.\n *\n * @param rawPlugins - Array of plugin data entries from createApp configuration\n * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid\n */\n public collectResources(\n rawPlugins: PluginData<PluginConstructor, unknown, string>[],\n ): void {\n for (const pluginData of rawPlugins) {\n if (!pluginData?.plugin) continue;\n\n const pluginName = pluginData.name;\n const manifest = getPluginManifest(pluginData.plugin);\n\n // Register required resources\n for (const resource of manifest.resources.required) {\n this.register(pluginName, { ...resource, required: true });\n }\n\n // Register optional resources\n for (const resource of manifest.resources.optional || []) {\n this.register(pluginName, { ...resource, required: false });\n }\n\n // Check for runtime resource requirements\n if (typeof pluginData.plugin.getResourceRequirements === \"function\") {\n const runtimeResources = pluginData.plugin.getResourceRequirements(\n pluginData.config as BasePluginConfig,\n );\n for (const resource of runtimeResources) {\n this.register(pluginName, resource as ResourceRequirement);\n }\n }\n\n logger.debug(\n \"Collected resources from plugin %s: %d total\",\n pluginName,\n this.getByPlugin(pluginName).length,\n );\n }\n }\n\n /**\n * Merges a new resource requirement with an existing entry.\n * Applies intelligent merging logic for conflicting properties.\n */\n private mergeResources(\n existing: ResourceEntry,\n newPlugin: string,\n newResource: ResourceRequirement,\n ): ResourceEntry {\n // Combine plugin names if not already included\n const plugins = existing.plugin.split(\", \");\n if (!plugins.includes(newPlugin)) {\n plugins.push(newPlugin);\n }\n\n // Track per-plugin permission sources\n const permissionSources: Record<string, ResourcePermission> = {\n ...(existing.permissionSources ?? {}),\n [newPlugin]: newResource.permission,\n };\n\n // Use the most permissive permission for this resource type; warn when escalating\n const permission = getMostPermissivePermission(\n existing.type as ResourceType,\n existing.permission,\n newResource.permission,\n );\n\n if (permission !== existing.permission) {\n logger.warn(\n 'Resource %s:%s permission escalated from \"%s\" to \"%s\" due to plugin \"%s\" ' +\n \"(previously requested by: %s). Review plugin permissions to ensure least-privilege.\",\n existing.type,\n existing.resourceKey,\n existing.permission,\n permission,\n newPlugin,\n existing.plugin,\n );\n }\n\n // Mark as required if any plugin requires it\n const required = existing.required || newResource.required;\n\n // Combine descriptions if they differ\n let description = existing.description;\n if (\n newResource.description &&\n newResource.description !== existing.description\n ) {\n if (!existing.description.includes(newResource.description)) {\n description = `${existing.description}; ${newResource.description}`;\n }\n }\n\n // Merge fields: union of field names; warn when same field name uses different env\n const fields = { ...(existing.fields ?? {}) };\n for (const [fieldName, newField] of Object.entries(\n newResource.fields ?? {},\n )) {\n const existingField = fields[fieldName];\n if (existingField) {\n if (existingField.env !== newField.env) {\n logger.warn(\n 'Resource %s:%s field \"%s\": conflicting env vars \"%s\" (from %s) vs \"%s\" (from %s). Using first.',\n existing.type,\n existing.resourceKey,\n fieldName,\n existingField.env,\n existing.plugin,\n newField.env,\n newPlugin,\n );\n }\n // keep existing\n } else {\n fields[fieldName] = newField;\n }\n }\n\n return {\n ...existing,\n plugin: plugins.join(\", \"),\n permission,\n permissionSources,\n required,\n description,\n fields,\n };\n }\n\n /**\n * Retrieves all registered resources.\n * Returns a copy of the array to prevent external mutations.\n *\n * @returns Array of all registered resource entries\n */\n public getAll(): ResourceEntry[] {\n return Array.from(this.resources.values());\n }\n\n /**\n * Gets a specific resource by type and resourceKey (dedup key).\n *\n * @param type - Resource type\n * @param resourceKey - Stable machine key (not alias; alias is for display only)\n * @returns The resource entry if found, undefined otherwise\n */\n public get(type: string, resourceKey: string): ResourceEntry | undefined {\n return this.resources.get(getDedupKey(type, resourceKey));\n }\n\n /**\n * Clears all registered resources.\n * Useful for testing or when rebuilding the registry.\n */\n public clear(): void {\n this.resources.clear();\n }\n\n /**\n * Returns the number of registered resources.\n */\n public size(): number {\n return this.resources.size;\n }\n\n /**\n * Gets all resources required by a specific plugin.\n *\n * @param pluginName - Name of the plugin\n * @returns Array of resources where the plugin is listed as a requester\n */\n public getByPlugin(pluginName: string): ResourceEntry[] {\n return this.getAll().filter((entry) =>\n entry.plugin.split(\", \").includes(pluginName),\n );\n }\n\n /**\n * Gets all required resources (where required=true).\n *\n * @returns Array of required resource entries\n */\n public getRequired(): ResourceEntry[] {\n return this.getAll().filter((entry) => entry.required);\n }\n\n /**\n * Gets all optional resources (where required=false).\n *\n * @returns Array of optional resource entries\n */\n public getOptional(): ResourceEntry[] {\n return this.getAll().filter((entry) => !entry.required);\n }\n\n /**\n * Validates all registered resources against the environment.\n *\n * Checks each resource's field environment variables to determine if it's resolved.\n * Updates the `resolved` and `values` fields on each resource entry.\n *\n * Only required resources affect the `valid` status - optional resources\n * are checked but don't cause validation failure.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n *\n * @example\n * ```typescript\n * const registry = ResourceRegistry.getInstance();\n * const result = registry.validate();\n *\n * if (!result.valid) {\n * console.error(ResourceRegistry.formatMissingResources(result.missing));\n * }\n * ```\n */\n public validate(): ValidationResult {\n const missing: ResourceEntry[] = [];\n\n for (const entry of this.resources.values()) {\n const values: Record<string, string> = {};\n let allSet = true;\n for (const [fieldName, fieldDef] of Object.entries(entry.fields)) {\n if (!fieldDef.env) continue;\n const val = process.env[fieldDef.env];\n if (val !== undefined && val !== \"\") {\n values[fieldName] = val;\n } else {\n allSet = false;\n }\n }\n if (allSet) {\n entry.resolved = true;\n entry.values = values;\n logger.debug(\n \"Resource %s:%s resolved from fields\",\n entry.type,\n entry.alias,\n );\n } else {\n entry.resolved = false;\n entry.values = Object.keys(values).length > 0 ? values : undefined;\n if (entry.required) {\n missing.push(entry);\n logger.debug(\n \"Required resource %s:%s missing (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n } else {\n logger.debug(\n \"Optional resource %s:%s not configured (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n }\n }\n }\n\n return {\n valid: missing.length === 0,\n missing,\n all: this.getAll(),\n };\n }\n\n /**\n * Validates all registered resources and enforces the result.\n *\n * - In production: throws a {@link ConfigurationError} if any required resources are missing.\n * - In development (`NODE_ENV=development`): logs a warning but continues, unless\n * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.\n * - When all resources are valid: logs a debug message with the count.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true\n */\n public enforceValidation(): ValidationResult {\n const validation = this.validate();\n const isDevelopment = process.env.NODE_ENV === \"development\";\n const strictValidation =\n process.env.APPKIT_STRICT_VALIDATION === \"true\" ||\n process.env.APPKIT_STRICT_VALIDATION === \"1\";\n\n if (!validation.valid) {\n const errorMessage = ResourceRegistry.formatMissingResources(\n validation.missing,\n );\n\n const shouldThrow = !isDevelopment || strictValidation;\n\n if (shouldThrow) {\n throw new ConfigurationError(errorMessage, {\n context: {\n missingResources: validation.missing.map((r) => ({\n type: r.type,\n alias: r.alias,\n plugin: r.plugin,\n envVars: ResourceRegistry.unsetEnvVars(r),\n })),\n },\n });\n }\n\n // Dev mode without strict: use a visually prominent box so the warning can't be missed\n const banner = ResourceRegistry.formatDevWarningBanner(\n validation.missing,\n );\n logger.warn(\"\\n%s\", banner);\n } else if (this.size() > 0) {\n logger.debug(\"All %d resources validated successfully\", this.size());\n }\n\n return validation;\n }\n\n /**\n * Env var names for an entry's fields that are actually unset (undefined or\n * empty), skipping fields that declare no env var (e.g. discovery-only\n * fields resolved at deploy time). Mirrors the resolution check in\n * {@link validate} so error output lists exactly what the caller must set —\n * not every field on the resource.\n */\n private static unsetEnvVars(entry: ResourceEntry): string[] {\n const unset: string[] = [];\n for (const field of Object.values(entry.fields)) {\n if (!field.env) continue;\n const val = process.env[field.env];\n if (val === undefined || val === \"\") {\n unset.push(field.env);\n }\n }\n return unset;\n }\n\n /**\n * Formats missing resources into a human-readable error message.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted error message string\n */\n public static formatMissingResources(missing: ResourceEntry[]): string {\n if (missing.length === 0) {\n return \"No missing resources\";\n }\n\n const lines = missing.map((entry) => {\n const envVars = ResourceRegistry.unsetEnvVars(entry);\n const envHint = envVars.length > 0 ? ` (set ${envVars.join(\", \")})` : \"\";\n return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;\n });\n\n return `Missing required resources:\\n${lines.join(\"\\n\")}`;\n }\n\n /**\n * Formats a highly visible warning banner for dev-mode missing resources.\n * Uses box drawing to ensure the message is impossible to miss in scrolling logs.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted banner string\n */\n public static formatDevWarningBanner(missing: ResourceEntry[]): string {\n const contentLines: string[] = [\n \"MISSING REQUIRED RESOURCES (dev mode — would fail in production)\",\n \"\",\n ];\n\n for (const entry of missing) {\n const envVars = ResourceRegistry.unsetEnvVars(entry);\n contentLines.push(\n ` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`,\n );\n if (envVars.length > 0) {\n contentLines.push(` Set: ${envVars.join(\", \")}`);\n }\n }\n\n contentLines.push(\"\");\n contentLines.push(\n \"Add these to your .env file or environment to suppress this warning.\",\n );\n\n return formatWarningBanner(contentLines);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/registry/types.ts"],"mappings":";;;;;;;;;;;;;;;;;AA6EA;;UArBiB,mBAAA;EA6BN;EA3BT,IAAA,EAAM,YAAA;EAkCc;EAhCpB,UAAA,EAAY,kBAAA;EAiB4C;EAfxD,KAAA;EAeqC;EAbrC,WAAA;EAkBA;EAhBA,WAAA;EAmBS;EAjBT,MAAA,EAAQ,MAAA,SAAe,kBAAA;EAwBH;EAtBpB,QAAA;AAAA;;AA4BF;;;UArBiB,aAAA,SAAsB,mBAAA;EAuBrC;EArBA,MAAA;EAwBS;EArBT,QAAA;EAwBK;EArBL,MAAA,GAAS,MAAA;EAqBS;AAWpB;;;;EAzBE,iBAAA,GAAoB,MAAA,SAAe,kBAAA;AAAA;;;;UAMpB,gBAAA;EAuCE;EArCjB,KAAA;EAwCiB;EArCjB,OAAA,EAAS,aAAA;EA6CC;EA1CV,GAAA,EAAK,aAAA;AAAA;;;;;;;KAWK,YAAA,GAAe,WAAA;;;;;;;;;UAUV,cAAA,wCACP,IAAA,CAAK,gBAAA;EAoBS;EAlBtB,IAAA,EAAM,KAAA;;;;EAKN,SAAA;sEAEE,QAAA,EAAU,IAAA,CAAK,mBAAA;IAGf,QAAA,EAAU,IAAA,CAAK,mBAAA;EAAA;;;;;EAOjB,MAAA;IACE,MAAA,EAAQ,YAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/registry/types.ts"],"mappings":";;;;;;;;;;;;;;;;;AA6EA;;UArBiB,mBAAA;EA6BN;EA3BT,IAAA,EAAM,YAAA;EAkCc;EAhCpB,UAAA,EAAY,kBAAA;EAiB4C;EAfxD,KAAA;EAeqC;EAbrC,WAAA;EAkBA;EAhBA,WAAA;EAmBS;EAjBT,MAAA,EAAQ,MAAA,SAAe,kBAAA;EAwBH;EAtBpB,QAAA;AAAA;;AA4BF;;;UArBiB,aAAA,SAAsB,mBAAA;EAuBrC;EArBA,MAAA;EAwBS;EArBT,QAAA;EAwBK;EArBL,MAAA,GAAS,MAAA;EAqBS;AAWpB;;;;EAzBE,iBAAA,GAAoB,MAAA,SAAe,kBAAA;AAAA;;;;UAMpB,gBAAA;EAyCE;EAvCjB,KAAA;EA0CiB;EAvCjB,OAAA,EAAS,aAAA;EA+CC;EA5CV,GAAA,EAAK,aAAA;AAAA;;;;;;;KAWK,YAAA,GAAe,WAAA;;;;;;;;;UAUV,cAAA,wCAAsD,IAAA,CACrE,gBAAA;EAsBsB;EAlBtB,IAAA,EAAM,KAAA;;;;EAKN,SAAA;sEAEE,QAAA,EAAU,IAAA,CAAK,mBAAA;IAGf,QAAA,EAAU,IAAA,CAAK,mBAAA;EAAA;;;;;EAOjB,MAAA;IACE,MAAA,EAAQ,YAAA;EAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/registry/types.ts"],"sourcesContent":["/**\n * Resource Registry Type System\n *\n * This module defines the type system for the AppKit Resource Registry,\n * which enables plugins to declare their Databricks resource requirements\n * in a machine-readable format.\n *\n * Base interfaces (ResourceFieldEntry, ResourceRequirement, PluginManifest)\n * are inferred from the Zod schemas in `packages/shared/src/schemas/manifest.ts`\n * and imported via shared. This module re-exports them with strict narrowing\n * (ResourceType enum, ResourcePermission union) for appkit-internal use.\n *\n * Resource types, permissions, and hierarchy constants are generated by\n * tools/generate-registry-types.ts into types.generated.ts.\n */\n\n// Re-export generated registry types (enum + const must be value exports for runtime)\nimport {\n PERMISSION_HIERARCHY_BY_TYPE,\n PERMISSIONS_BY_TYPE,\n type ResourcePermission,\n ResourceType,\n} from \"./types.generated\";\n\nexport {\n PERMISSION_HIERARCHY_BY_TYPE,\n PERMISSIONS_BY_TYPE,\n ResourceType,\n type ResourcePermission,\n};\n\n// Re-export schema-derived base type from shared.\nexport type { ResourceFieldEntry } from \"shared\";\n\n// Import shared base types for strict extension below.\nimport type {\n ResourceFieldEntry,\n PluginManifest as SharedPluginManifest,\n} from \"shared\";\n\n// ============================================================================\n// Strict appkit wrappers — narrow shared base types for registry use\n// ============================================================================\n\n/**\n * Declares a resource requirement for a plugin.\n * Can be defined statically in a manifest or dynamically via getResourceRequirements().\n *\n * Defined as a flat structural shape rather than narrowing the shared\n * discriminated-union `ResourceRequirement`. The registry needs to construct\n * and pass these values around at runtime where `type` and `permission` are\n * looked up dynamically from a string; preserving per-variant tightness here\n * would force every constructor to thread variant types end-to-end. The\n * runtime guarantee (permission valid for type) is enforced by the manifest\n * schema's parse, not by the registry's static types.\n */\nexport interface ResourceRequirement {\n /** Type of Databricks resource required (narrowed to enum) */\n type: ResourceType;\n /** Required permission level for the resource (narrowed to union) */\n permission: ResourcePermission;\n /** Human-readable label for UI/display only. */\n alias: string;\n /** Stable key for machine use: deduplication, env naming, composite keys, app.yaml. */\n resourceKey: string;\n /** Human-readable description of why this resource is needed. */\n description: string;\n /** Map of field name to field entry. Required at runtime. */\n fields: Record<string, ResourceFieldEntry>;\n /** Whether the resource is mandatory at runtime. */\n required: boolean;\n}\n\n/**\n * Internal representation of a resource in the registry.\n * Extends ResourceRequirement with resolution state and plugin ownership.\n */\nexport interface ResourceEntry extends ResourceRequirement {\n /** Plugin(s) that require this resource (comma-separated if multiple) */\n plugin: string;\n\n /** Whether the resource has been resolved (all field env vars set) */\n resolved: boolean;\n\n /** Resolved value per field name. Populated by validate() when all field env vars are set. */\n values?: Record<string, string>;\n\n /**\n * Per-plugin permission tracking.\n * Maps plugin name to the permission it originally requested.\n * Populated when multiple plugins share the same resource.\n */\n permissionSources?: Record<string, ResourcePermission>;\n}\n\n/**\n * Result of validating all registered resources against the environment.\n */\nexport interface ValidationResult {\n /** Whether all required resources are available */\n valid: boolean;\n\n /** List of missing required resources */\n missing: ResourceEntry[];\n\n /** Complete list of all registered resources (required and optional) */\n all: ResourceEntry[];\n}\n\nimport type { JSONSchema7 } from \"json-schema\";\n\n/**\n * Configuration schema definition for plugin config.\n * Re-exported from the standard JSON Schema Draft 7 types.\n *\n * @see {@link https://json-schema.org/draft-07/json-schema-release-notes | JSON Schema Draft 7}\n */\nexport type ConfigSchema = JSONSchema7;\n\n/**\n * Plugin manifest that declares metadata and resource requirements.\n * Attached to plugin classes as a static property.\n * Extends the shared PluginManifest with strict resource types.\n *\n * @see `packages/shared/src/schemas/manifest.ts` `pluginManifestSchema` — Zod source of truth\n * @see {@link SharedPluginManifest} — shared re-export with JSONSchema7 config\n */\nexport interface PluginManifest<TName extends string = string>\n extends Omit<SharedPluginManifest, \"resources\" | \"config\"> {\n /** Plugin identifier — the single source of truth for the plugin's name */\n name: TName;\n\n /**\n * Resource requirements declaration (with strict ResourceRequirement types)\n */\n resources: {\n /** Resources that must be available for the plugin to function */\n required: Omit<ResourceRequirement, \"required\">[];\n\n /** Resources that enhance functionality but are not mandatory */\n optional: Omit<ResourceRequirement, \"required\">[];\n };\n\n /**\n * Configuration schema for the plugin.\n * Uses JSONSchema7 instead of the generated ConfigSchema (which is too restrictive).\n */\n config?: {\n schema: ConfigSchema;\n };\n}\n"],"mappings":""}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/registry/types.ts"],"sourcesContent":["/**\n * Resource Registry Type System\n *\n * This module defines the type system for the AppKit Resource Registry,\n * which enables plugins to declare their Databricks resource requirements\n * in a machine-readable format.\n *\n * Base interfaces (ResourceFieldEntry, ResourceRequirement, PluginManifest)\n * are inferred from the Zod schemas in `packages/shared/src/schemas/manifest.ts`\n * and imported via shared. This module re-exports them with strict narrowing\n * (ResourceType enum, ResourcePermission union) for appkit-internal use.\n *\n * Resource types, permissions, and hierarchy constants are generated by\n * tools/generate-registry-types.ts into types.generated.ts.\n */\n\n// Re-export generated registry types (enum + const must be value exports for runtime)\nimport {\n PERMISSION_HIERARCHY_BY_TYPE,\n PERMISSIONS_BY_TYPE,\n type ResourcePermission,\n ResourceType,\n} from \"./types.generated\";\n\nexport {\n PERMISSION_HIERARCHY_BY_TYPE,\n PERMISSIONS_BY_TYPE,\n ResourceType,\n type ResourcePermission,\n};\n\n// Re-export schema-derived base type from shared.\nexport type { ResourceFieldEntry } from \"shared\";\n\n// Import shared base types for strict extension below.\nimport type {\n ResourceFieldEntry,\n PluginManifest as SharedPluginManifest,\n} from \"shared\";\n\n// ============================================================================\n// Strict appkit wrappers — narrow shared base types for registry use\n// ============================================================================\n\n/**\n * Declares a resource requirement for a plugin.\n * Can be defined statically in a manifest or dynamically via getResourceRequirements().\n *\n * Defined as a flat structural shape rather than narrowing the shared\n * discriminated-union `ResourceRequirement`. The registry needs to construct\n * and pass these values around at runtime where `type` and `permission` are\n * looked up dynamically from a string; preserving per-variant tightness here\n * would force every constructor to thread variant types end-to-end. The\n * runtime guarantee (permission valid for type) is enforced by the manifest\n * schema's parse, not by the registry's static types.\n */\nexport interface ResourceRequirement {\n /** Type of Databricks resource required (narrowed to enum) */\n type: ResourceType;\n /** Required permission level for the resource (narrowed to union) */\n permission: ResourcePermission;\n /** Human-readable label for UI/display only. */\n alias: string;\n /** Stable key for machine use: deduplication, env naming, composite keys, app.yaml. */\n resourceKey: string;\n /** Human-readable description of why this resource is needed. */\n description: string;\n /** Map of field name to field entry. Required at runtime. */\n fields: Record<string, ResourceFieldEntry>;\n /** Whether the resource is mandatory at runtime. */\n required: boolean;\n}\n\n/**\n * Internal representation of a resource in the registry.\n * Extends ResourceRequirement with resolution state and plugin ownership.\n */\nexport interface ResourceEntry extends ResourceRequirement {\n /** Plugin(s) that require this resource (comma-separated if multiple) */\n plugin: string;\n\n /** Whether the resource has been resolved (all field env vars set) */\n resolved: boolean;\n\n /** Resolved value per field name. Populated by validate() when all field env vars are set. */\n values?: Record<string, string>;\n\n /**\n * Per-plugin permission tracking.\n * Maps plugin name to the permission it originally requested.\n * Populated when multiple plugins share the same resource.\n */\n permissionSources?: Record<string, ResourcePermission>;\n}\n\n/**\n * Result of validating all registered resources against the environment.\n */\nexport interface ValidationResult {\n /** Whether all required resources are available */\n valid: boolean;\n\n /** List of missing required resources */\n missing: ResourceEntry[];\n\n /** Complete list of all registered resources (required and optional) */\n all: ResourceEntry[];\n}\n\nimport type { JSONSchema7 } from \"json-schema\";\n\n/**\n * Configuration schema definition for plugin config.\n * Re-exported from the standard JSON Schema Draft 7 types.\n *\n * @see {@link https://json-schema.org/draft-07/json-schema-release-notes | JSON Schema Draft 7}\n */\nexport type ConfigSchema = JSONSchema7;\n\n/**\n * Plugin manifest that declares metadata and resource requirements.\n * Attached to plugin classes as a static property.\n * Extends the shared PluginManifest with strict resource types.\n *\n * @see `packages/shared/src/schemas/manifest.ts` `pluginManifestSchema` — Zod source of truth\n * @see {@link SharedPluginManifest} — shared re-export with JSONSchema7 config\n */\nexport interface PluginManifest<TName extends string = string> extends Omit<\n SharedPluginManifest,\n \"resources\" | \"config\"\n> {\n /** Plugin identifier — the single source of truth for the plugin's name */\n name: TName;\n\n /**\n * Resource requirements declaration (with strict ResourceRequirement types)\n */\n resources: {\n /** Resources that must be available for the plugin to function */\n required: Omit<ResourceRequirement, \"required\">[];\n\n /** Resources that enhance functionality but are not mandatory */\n optional: Omit<ResourceRequirement, \"required\">[];\n };\n\n /**\n * Configuration schema for the plugin.\n * Uses JSONSchema7 instead of the generated ConfigSchema (which is too restrictive).\n */\n config?: {\n schema: ConfigSchema;\n };\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","names":[],"sources":["../../src/schemas/manifest.ts"],"mappings":";;;cAuPa,yBAAA,EAAyB,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;cA8FzB,wBAAA,EAAwB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkQxB,4BAAA,EAA4B,CAAA,CAAA,SAAA;;;;;cAoE5B,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqgBrB,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;AAAA,KACrC,kBAAA,GAAqB,CAAA,CAAE,KAAA,QAAa,wBAAA;AAAA,KAIpC,sBAAA,GAAyB,CAAA,CAAE,KAAA,QAC9B,4BAAA;AAAA,KAEG,cAAA,GAAiB,CAAA,CAAE,KAAA,QAAa,oBAAA"}
1
+ {"version":3,"file":"manifest.d.ts","names":[],"sources":["../../src/schemas/manifest.ts"],"mappings":";;;cAwPa,yBAAA,EAAyB,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;cA8FzB,wBAAA,EAAwB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkQxB,4BAAA,EAA4B,CAAA,CAAA,SAAA;;;;;cAoE5B,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqgBrB,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;AAAA,KACrC,kBAAA,GAAqB,CAAA,CAAE,KAAA,QAAa,wBAAA;AAAA,KAIpC,sBAAA,GAAyB,CAAA,CAAE,KAAA,QAC9B,4BAAA;AAAA,KAEG,cAAA,GAAiB,CAAA,CAAE,KAAA,QAAa,oBAAA"}