@databricks/appkit 0.45.0 → 0.47.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 (113) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/agents/databricks.d.ts +25 -1
  3. package/dist/agents/databricks.d.ts.map +1 -1
  4. package/dist/agents/databricks.js +20 -1
  5. package/dist/agents/databricks.js.map +1 -1
  6. package/dist/app/index.d.ts +49 -2
  7. package/dist/app/index.d.ts.map +1 -1
  8. package/dist/app/index.js +87 -10
  9. package/dist/app/index.js.map +1 -1
  10. package/dist/appkit/package.js +1 -1
  11. package/dist/beta.d.ts +2 -2
  12. package/dist/cli/commands/generate-types.js +9 -4
  13. package/dist/cli/commands/generate-types.js.map +1 -1
  14. package/dist/core/agent/load-agents.d.ts.map +1 -1
  15. package/dist/core/agent/load-agents.js +52 -0
  16. package/dist/core/agent/load-agents.js.map +1 -1
  17. package/dist/core/agent/types.d.ts +11 -0
  18. package/dist/core/agent/types.d.ts.map +1 -1
  19. package/dist/core/agent/types.js.map +1 -1
  20. package/dist/core/appkit.d.ts.map +1 -1
  21. package/dist/core/appkit.js +2 -0
  22. package/dist/core/appkit.js.map +1 -1
  23. package/dist/core/lifecycle-manager.js +183 -0
  24. package/dist/core/lifecycle-manager.js.map +1 -0
  25. package/dist/core/plugin-context.d.ts +19 -1
  26. package/dist/core/plugin-context.d.ts.map +1 -1
  27. package/dist/core/plugin-context.js +9 -3
  28. package/dist/core/plugin-context.js.map +1 -1
  29. package/dist/plugin/plugin.d.ts.map +1 -1
  30. package/dist/plugin/plugin.js +2 -1
  31. package/dist/plugin/plugin.js.map +1 -1
  32. package/dist/plugins/agents/agents.d.ts.map +1 -1
  33. package/dist/plugins/agents/agents.js +3 -3
  34. package/dist/plugins/agents/agents.js.map +1 -1
  35. package/dist/plugins/analytics/analytics.d.ts +16 -0
  36. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  37. package/dist/plugins/analytics/analytics.js +162 -1
  38. package/dist/plugins/analytics/analytics.js.map +1 -1
  39. package/dist/plugins/analytics/metric.js +7 -0
  40. package/dist/plugins/analytics/mv/cache.js +51 -0
  41. package/dist/plugins/analytics/mv/cache.js.map +1 -0
  42. package/dist/plugins/analytics/mv/constants.js +72 -0
  43. package/dist/plugins/analytics/mv/constants.js.map +1 -0
  44. package/dist/plugins/analytics/mv/formatters.js +151 -0
  45. package/dist/plugins/analytics/mv/formatters.js.map +1 -0
  46. package/dist/plugins/analytics/mv/index.js +6 -0
  47. package/dist/plugins/analytics/mv/registry.js +55 -0
  48. package/dist/plugins/analytics/mv/registry.js.map +1 -0
  49. package/dist/plugins/analytics/mv/schemas.js +178 -0
  50. package/dist/plugins/analytics/mv/schemas.js.map +1 -0
  51. package/dist/plugins/analytics/types.js.map +1 -1
  52. package/dist/plugins/lakebase/lakebase.d.ts +10 -2
  53. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  54. package/dist/plugins/lakebase/lakebase.js +18 -7
  55. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  56. package/dist/plugins/server/index.d.ts +51 -1
  57. package/dist/plugins/server/index.d.ts.map +1 -1
  58. package/dist/plugins/server/index.js +110 -23
  59. package/dist/plugins/server/index.js.map +1 -1
  60. package/dist/registry/resource-registry.d.ts +9 -1
  61. package/dist/registry/resource-registry.d.ts.map +1 -1
  62. package/dist/registry/resource-registry.js +22 -5
  63. package/dist/registry/resource-registry.js.map +1 -1
  64. package/dist/schemas/metric-fqn.js +14 -0
  65. package/dist/schemas/metric-fqn.js.map +1 -0
  66. package/dist/shared/src/execute.d.ts +1 -3
  67. package/dist/shared/src/execute.d.ts.map +1 -1
  68. package/dist/shared/src/plugin.d.ts +16 -0
  69. package/dist/shared/src/plugin.d.ts.map +1 -1
  70. package/dist/shared/src/schemas/metric-fqn.js +78 -46
  71. package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
  72. package/dist/shared/src/schemas/metric-source.js +90 -0
  73. package/dist/shared/src/schemas/metric-source.js.map +1 -0
  74. package/dist/stream/buffers.js +1 -1
  75. package/dist/stream/buffers.js.map +1 -1
  76. package/dist/stream/defaults.js +0 -2
  77. package/dist/stream/defaults.js.map +1 -1
  78. package/dist/stream/stream-manager.d.ts +2 -2
  79. package/dist/stream/stream-manager.d.ts.map +1 -1
  80. package/dist/stream/stream-manager.js +26 -24
  81. package/dist/stream/stream-manager.js.map +1 -1
  82. package/dist/stream/stream-registry.js +30 -23
  83. package/dist/stream/stream-registry.js.map +1 -1
  84. package/dist/stream/timers.js +17 -0
  85. package/dist/stream/timers.js.map +1 -0
  86. package/dist/stream/types.js.map +1 -1
  87. package/dist/telemetry/telemetry-manager.js +19 -13
  88. package/dist/telemetry/telemetry-manager.js.map +1 -1
  89. package/dist/type-generator/index.js +14 -7
  90. package/dist/type-generator/index.js.map +1 -1
  91. package/dist/type-generator/mv-registry/config.js +13 -31
  92. package/dist/type-generator/mv-registry/config.js.map +1 -1
  93. package/dist/type-generator/mv-registry/describe.js +1 -31
  94. package/dist/type-generator/mv-registry/describe.js.map +1 -1
  95. package/dist/type-generator/mv-registry/sync.js +1 -1
  96. package/dist/type-generator/mv-registry/sync.js.map +1 -1
  97. package/dist/type-generator/vite-plugin.d.ts +5 -1
  98. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  99. package/dist/type-generator/vite-plugin.js +21 -4
  100. package/dist/type-generator/vite-plugin.js.map +1 -1
  101. package/dist/utils/safe-handler.js +28 -0
  102. package/dist/utils/safe-handler.js.map +1 -0
  103. package/docs/api/appkit/Class.Plugin.md +2 -0
  104. package/docs/api/appkit/Class.ResourceRegistry.md +1 -1
  105. package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
  106. package/docs/api/appkit/Interface.GenerationParams.md +58 -0
  107. package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
  108. package/docs/api/appkit.md +1 -0
  109. package/docs/development/type-generation.md +3 -3
  110. package/docs/plugins/analytics.md +172 -23
  111. package/llms.txt +1 -0
  112. package/package.json +1 -1
  113. package/sbom.cdx.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
+ import { AppKitError } from "../../errors/base.js";
2
3
  import { ServerError } from "../../errors/server.js";
3
4
  import "../../errors/index.js";
4
5
  import { instrumentations } from "../../telemetry/instrumentations.js";
@@ -52,6 +53,13 @@ var ServerPlugin = class ServerPlugin extends Plugin {
52
53
  host: process.env.FLASK_RUN_HOST || "0.0.0.0",
53
54
  port: Number(process.env.DATABRICKS_APP_PORT) || 8e3
54
55
  };
56
+ /**
57
+ * Budget for awaiting `server.close()` during shutdown. Bounded because
58
+ * `closeAllConnections()` runs immediately before the await, so the close
59
+ * is expected to resolve promptly; the core lifecycle manager's overall
60
+ * force-exit timer is the ultimate backstop if it does not.
61
+ */
62
+ static SERVER_CLOSE_TIMEOUT_MS = 2e3;
55
63
  /** Plugin manifest declaring metadata and resource requirements */
56
64
  static manifest = manifest_default;
57
65
  serverApplication;
@@ -62,6 +70,12 @@ var ServerPlugin = class ServerPlugin extends Plugin {
62
70
  resolvedListenPort;
63
71
  serverExtensions = [];
64
72
  rawBodyPaths = /* @__PURE__ */ new Set();
73
+ /**
74
+ * Resolves when `server.close()` completes. Created in
75
+ * {@link abortActiveOperations} (which initiates the close) and awaited in
76
+ * {@link closeRemainingConnections} after other plugins have drained.
77
+ */
78
+ serverClosed;
65
79
  static phase = "deferred";
66
80
  constructor(config) {
67
81
  super(config);
@@ -106,12 +120,12 @@ var ServerPlugin = class ServerPlugin extends Plugin {
106
120
  this.remoteTunnelController = new RemoteTunnelController(this.devFileReader);
107
121
  this.serverApplication.use(this.remoteTunnelController.middleware);
108
122
  await this.setupFrontend(endpoints, pluginConfigs);
123
+ this.serverApplication.use(errorHandlerMiddleware);
109
124
  const listenPort = await this.resolveListenPort();
110
125
  const server = this.serverApplication.listen(listenPort, this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host, () => this.logStartupInfo());
111
126
  this.server = server;
112
127
  this.remoteTunnelController.setServer(server);
113
- process.on("SIGTERM", () => this._gracefulShutdown());
114
- process.on("SIGINT", () => this._gracefulShutdown());
128
+ this.context?.onLifecycle("shutdown", () => this.closeRemainingConnections());
115
129
  if (process.env.NODE_ENV === "development") printRoutes(getRoutes(this.serverApplication._router.stack));
116
130
  return this.serverApplication;
117
131
  }
@@ -133,6 +147,11 @@ var ServerPlugin = class ServerPlugin extends Plugin {
133
147
  * Call this inside the `onPluginsReady` callback of `createApp` to register
134
148
  * custom Express routes or middleware before the server starts listening.
135
149
  *
150
+ * Note: async handlers registered directly on the app must handle their own
151
+ * rejections — Express 4 does not forward rejected promises to the error
152
+ * middleware. Errors passed explicitly to `next(err)` are formatted by the
153
+ * terminal error middleware.
154
+ *
136
155
  * @param fn - A function that receives the express application.
137
156
  * @returns The server plugin instance for chaining.
138
157
  */
@@ -262,29 +281,57 @@ var ServerPlugin = class ServerPlugin extends Plugin {
262
281
  if (!remoteServerController) logger.debug("Remote tunnel: disabled (controller not initialized)");
263
282
  else logger.debug("Remote tunnel: %s; %s", remoteServerController.isAllowedByEnv() ? "allowed" : "blocked", remoteServerController.isActive() ? "active" : "inactive");
264
283
  }
265
- async _gracefulShutdown() {
266
- logger.info("Starting graceful shutdown...");
267
- if (this.viteDevServer) await this.viteDevServer.close();
268
- if (this.remoteTunnelController) this.remoteTunnelController.cleanup();
269
- TelemetryReporter.getInstance()?.stop();
270
- const shutdownPlugins = this.context?.getPlugins();
271
- if (shutdownPlugins) {
272
- for (const plugin of shutdownPlugins.values()) if (plugin.abortActiveOperations) try {
273
- plugin.abortActiveOperations();
274
- } catch (err) {
275
- logger.error("Error aborting operations for plugin %s: %O", plugin.name, err);
276
- }
277
- }
284
+ /**
285
+ * Cancel in-flight work and begin closing the HTTP server.
286
+ *
287
+ * Runs in the first phase of the core LifecycleManager's graceful shutdown,
288
+ * before any plugin `shutdown()` hook. In addition to the base behavior
289
+ * (aborting SSE streams), it stops the server accepting new connections and
290
+ * drops idle keep-alive sockets — without `closeIdleConnections()`, a
291
+ * connected browser would pin `server.close()` open until the force-exit
292
+ * timer fires. The close is only *initiated* here; the returned promise is
293
+ * awaited later in {@link closeRemainingConnections} so peer plugins can
294
+ * still drain in-flight requests through the server first.
295
+ */
296
+ abortActiveOperations() {
297
+ super.abortActiveOperations();
278
298
  if (this.server) {
279
- this.server.close(() => {
280
- logger.debug("Server closed gracefully");
281
- process.exit(0);
299
+ const server = this.server;
300
+ this.serverClosed = new Promise((resolve) => {
301
+ server.close(() => resolve());
282
302
  });
283
- setTimeout(() => {
284
- logger.debug("Force shutdown after timeout");
285
- process.exit(1);
286
- }, 15e3);
287
- } else process.exit(0);
303
+ server.closeIdleConnections();
304
+ }
305
+ }
306
+ /**
307
+ * Drain the dev-only servers as this plugin's `shutdown()` hook.
308
+ *
309
+ * Runs concurrently with the other plugins' hooks during graceful shutdown.
310
+ * These are no-ops in production (neither is created), so this hook is
311
+ * effectively dev-only.
312
+ */
313
+ async shutdown() {
314
+ if (this.viteDevServer) await this.viteDevServer.close();
315
+ if (this.remoteTunnelController) this.remoteTunnelController.cleanup();
316
+ }
317
+ /**
318
+ * Force-close whatever sockets remain and await the server close.
319
+ *
320
+ * Registered on the `"shutdown"` lifecycle event in {@link start}, so it
321
+ * fires after every plugin's `shutdown()` hook has run — meaning any
322
+ * in-flight request a peer plugin was draining has already completed.
323
+ * `closeAllConnections()` destroys the leftover sockets (aborted SSE
324
+ * responses, keep-alive connections) synchronously so the pending
325
+ * `server.close()` can complete; the await is bounded because the close is
326
+ * expected to resolve promptly once the sockets are gone.
327
+ */
328
+ async closeRemainingConnections() {
329
+ if (this.server) this.server.closeAllConnections();
330
+ if (!this.serverClosed) return;
331
+ await Promise.race([this.serverClosed, new Promise((resolve) => {
332
+ setTimeout(resolve, ServerPlugin.SERVER_CLOSE_TIMEOUT_MS).unref();
333
+ })]);
334
+ logger.debug("Server closed gracefully");
288
335
  }
289
336
  /**
290
337
  * Returns the public exports for the server plugin.
@@ -320,6 +367,46 @@ function requestMetricsMiddleware(req, res, next) {
320
367
  next();
321
368
  }
322
369
  /**
370
+ * Narrow an unknown thrown value to an Error that carries a numeric
371
+ * `statusCode` property in the HTTP error range (e.g. `ApiError` from
372
+ * `@databricks/sdk-experimental`).
373
+ */
374
+ function hasHttpStatusCode(error) {
375
+ if (!(error instanceof Error) || !("statusCode" in error)) return false;
376
+ const statusCode = error.statusCode;
377
+ return typeof statusCode === "number" && Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599;
378
+ }
379
+ /**
380
+ * Terminal Express error-handling middleware.
381
+ *
382
+ * Converts errors forwarded via `next(err)` (including async handler
383
+ * rejections forwarded by `Plugin.route()`) into JSON error responses,
384
+ * following the same status/message conventions as `Plugin.execute()`:
385
+ * - errors carrying an HTTP `statusCode` (including `AppKitError`) → that
386
+ * status; 5xx messages are masked in production to avoid leaking internals
387
+ * - anything else → 500 with a generic message in production
388
+ *
389
+ * 4xx errors are expected client errors (e.g. missing auth headers) and are
390
+ * logged at `warn` with just the message; 5xx/unknown errors are logged at
391
+ * `error` with the full error.
392
+ *
393
+ * @internal Exported for unit tests.
394
+ */
395
+ function errorHandlerMiddleware(err, req, res, next) {
396
+ const httpError = err instanceof AppKitError || hasHttpStatusCode(err) ? err : null;
397
+ const statusCode = httpError ? httpError.statusCode : 500;
398
+ const isClientError = statusCode < 500;
399
+ if (isClientError) logger.warn("Request failed for %s %s with status %d: %s", req.method, req.originalUrl, statusCode, err instanceof Error ? err.message : String(err));
400
+ else logger.error("Unhandled error for %s %s: %O", req.method, req.originalUrl, err instanceof AppKitError ? err.toJSON() : err);
401
+ if (res.headersSent) {
402
+ next(err);
403
+ return;
404
+ }
405
+ const isDev = process.env.NODE_ENV !== "production";
406
+ const message = err instanceof Error ? err.message : "Server error";
407
+ res.status(statusCode).json({ error: isDev || isClientError ? message : "Server error" });
408
+ }
409
+ /**
323
410
  * @internal
324
411
  */
325
412
  const server = toPlugin(ServerPlugin);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of plugins.values()) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n TelemetryReporter.getInstance()?.stop();\n\n // 1. abort active operations from plugins\n const shutdownPlugins = this.context?.getPlugins();\n if (shutdownPlugins) {\n for (const plugin of shutdownPlugins.values()) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { AppKitError, ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /**\n * Budget for awaiting `server.close()` during shutdown. Bounded because\n * `closeAllConnections()` runs immediately before the await, so the close\n * is expected to resolve promptly; the core lifecycle manager's overall\n * force-exit timer is the ultimate backstop if it does not.\n */\n private static readonly SERVER_CLOSE_TIMEOUT_MS = 2_000;\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n /**\n * Resolves when `server.close()` completes. Created in\n * {@link abortActiveOperations} (which initiates the close) and awaited in\n * {@link closeRemainingConnections} after other plugins have drained.\n */\n private serverClosed?: Promise<void>;\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n // Terminal error handler — registered after all routes, extensions, and\n // frontend middleware so that errors forwarded via next(err) from any of\n // them (e.g. async handler rejections forwarded by Plugin.route()) are\n // turned into JSON error responses instead of hanging the request.\n this.serverApplication.use(errorHandlerMiddleware);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n // Graceful shutdown is orchestrated by the core LifecycleManager, which\n // owns the process signal handlers. This plugin only contributes its\n // HTTP concerns: it aborts/initiates the socket close in\n // abortActiveOperations(), drains dev servers in shutdown(), and force-\n // closes remaining sockets in the \"shutdown\" lifecycle hook below (which\n // fires after every plugin's shutdown() has run).\n this.context?.onLifecycle(\"shutdown\", () =>\n this.closeRemainingConnections(),\n );\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * Note: async handlers registered directly on the app must handle their own\n * rejections — Express 4 does not forward rejected promises to the error\n * middleware. Errors passed explicitly to `next(err)` are formatted by the\n * terminal error middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of plugins.values()) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n /**\n * Cancel in-flight work and begin closing the HTTP server.\n *\n * Runs in the first phase of the core LifecycleManager's graceful shutdown,\n * before any plugin `shutdown()` hook. In addition to the base behavior\n * (aborting SSE streams), it stops the server accepting new connections and\n * drops idle keep-alive sockets — without `closeIdleConnections()`, a\n * connected browser would pin `server.close()` open until the force-exit\n * timer fires. The close is only *initiated* here; the returned promise is\n * awaited later in {@link closeRemainingConnections} so peer plugins can\n * still drain in-flight requests through the server first.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n\n if (this.server) {\n const server = this.server;\n this.serverClosed = new Promise((resolve) => {\n server.close(() => resolve());\n });\n server.closeIdleConnections();\n }\n }\n\n /**\n * Drain the dev-only servers as this plugin's `shutdown()` hook.\n *\n * Runs concurrently with the other plugins' hooks during graceful shutdown.\n * These are no-ops in production (neither is created), so this hook is\n * effectively dev-only.\n */\n async shutdown(): Promise<void> {\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n }\n\n /**\n * Force-close whatever sockets remain and await the server close.\n *\n * Registered on the `\"shutdown\"` lifecycle event in {@link start}, so it\n * fires after every plugin's `shutdown()` hook has run — meaning any\n * in-flight request a peer plugin was draining has already completed.\n * `closeAllConnections()` destroys the leftover sockets (aborted SSE\n * responses, keep-alive connections) synchronously so the pending\n * `server.close()` can complete; the await is bounded because the close is\n * expected to resolve promptly once the sockets are gone.\n */\n private async closeRemainingConnections(): Promise<void> {\n if (this.server) {\n this.server.closeAllConnections();\n }\n if (!this.serverClosed) return;\n\n await Promise.race([\n this.serverClosed,\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ServerPlugin.SERVER_CLOSE_TIMEOUT_MS);\n timer.unref();\n }),\n ]);\n logger.debug(\"Server closed gracefully\");\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property in the HTTP error range (e.g. `ApiError` from\n * `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n if (!(error instanceof Error) || !(\"statusCode\" in error)) return false;\n const statusCode = error.statusCode;\n return (\n typeof statusCode === \"number\" &&\n Number.isInteger(statusCode) &&\n statusCode >= 400 &&\n statusCode <= 599\n );\n}\n\n/**\n * Terminal Express error-handling middleware.\n *\n * Converts errors forwarded via `next(err)` (including async handler\n * rejections forwarded by `Plugin.route()`) into JSON error responses,\n * following the same status/message conventions as `Plugin.execute()`:\n * - errors carrying an HTTP `statusCode` (including `AppKitError`) → that\n * status; 5xx messages are masked in production to avoid leaking internals\n * - anything else → 500 with a generic message in production\n *\n * 4xx errors are expected client errors (e.g. missing auth headers) and are\n * logged at `warn` with just the message; 5xx/unknown errors are logged at\n * `error` with the full error.\n *\n * @internal Exported for unit tests.\n */\nexport function errorHandlerMiddleware(\n err: unknown,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const httpError =\n err instanceof AppKitError || hasHttpStatusCode(err) ? err : null;\n const statusCode = httpError ? httpError.statusCode : 500;\n const isClientError = statusCode < 500;\n\n if (isClientError) {\n logger.warn(\n \"Request failed for %s %s with status %d: %s\",\n req.method,\n req.originalUrl,\n statusCode,\n err instanceof Error ? err.message : String(err),\n );\n } else {\n logger.error(\n \"Unhandled error for %s %s: %O\",\n req.method,\n req.originalUrl,\n // toJSON() redacts sensitive context fields; %O on a raw AppKitError\n // would enumerate them unredacted.\n err instanceof AppKitError ? err.toJSON() : err,\n );\n }\n\n if (res.headersSent) {\n next(err);\n return;\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n const message = err instanceof Error ? err.message : \"Server error\";\n\n res.status(statusCode).json({\n error: isDev || isClientError ? message : \"Server error\",\n });\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
@@ -95,7 +95,7 @@ declare class ResourceRegistry {
95
95
  * const result = registry.validate();
96
96
  *
97
97
  * if (!result.valid) {
98
- * console.error("Missing resources:", result.missing.map(r => Object.values(r.fields).map(f => f.env)));
98
+ * console.error(ResourceRegistry.formatMissingResources(result.missing));
99
99
  * }
100
100
  * ```
101
101
  */
@@ -112,6 +112,14 @@ declare class ResourceRegistry {
112
112
  * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true
113
113
  */
114
114
  enforceValidation(): ValidationResult;
115
+ /**
116
+ * Env var names for an entry's fields that are actually unset (undefined or
117
+ * empty), skipping fields that declare no env var (e.g. discovery-only
118
+ * fields resolved at deploy time). Mirrors the resolution check in
119
+ * {@link validate} so error output lists exactly what the caller must set —
120
+ * not every field on the resource.
121
+ */
122
+ private static unsetEnvVars;
115
123
  /**
116
124
  * Formats missing resources into a human-readable error message.
117
125
  *
@@ -1 +1 @@
1
- {"version":3,"file":"resource-registry.d.ts","names":[],"sources":["../../src/registry/resource-registry.ts"],"mappings":";;;;;;;;;cAqDa,gBAAA;EAAA,QACH,SAAA;EAiYsC;;;;;;;;;;;;EAnXvC,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;EA6L6B;;;;;;;EAlLvC,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;;;;;;;SA6Cd,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":";;;;;;;;;cAqDa,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"}
@@ -192,7 +192,7 @@ var ResourceRegistry = class ResourceRegistry {
192
192
  * const result = registry.validate();
193
193
  *
194
194
  * if (!result.valid) {
195
- * console.error("Missing resources:", result.missing.map(r => Object.values(r.fields).map(f => f.env)));
195
+ * console.error(ResourceRegistry.formatMissingResources(result.missing));
196
196
  * }
197
197
  * ```
198
198
  */
@@ -247,7 +247,7 @@ var ResourceRegistry = class ResourceRegistry {
247
247
  type: r.type,
248
248
  alias: r.alias,
249
249
  plugin: r.plugin,
250
- envVars: Object.values(r.fields).map((f) => f.env)
250
+ envVars: ResourceRegistry.unsetEnvVars(r)
251
251
  })) } });
252
252
  const banner = ResourceRegistry.formatDevWarningBanner(validation.missing);
253
253
  logger.warn("\n%s", banner);
@@ -255,6 +255,22 @@ var ResourceRegistry = class ResourceRegistry {
255
255
  return validation;
256
256
  }
257
257
  /**
258
+ * Env var names for an entry's fields that are actually unset (undefined or
259
+ * empty), skipping fields that declare no env var (e.g. discovery-only
260
+ * fields resolved at deploy time). Mirrors the resolution check in
261
+ * {@link validate} so error output lists exactly what the caller must set —
262
+ * not every field on the resource.
263
+ */
264
+ static unsetEnvVars(entry) {
265
+ const unset = [];
266
+ for (const field of Object.values(entry.fields)) {
267
+ if (!field.env) continue;
268
+ const val = process.env[field.env];
269
+ if (val === void 0 || val === "") unset.push(field.env);
270
+ }
271
+ return unset;
272
+ }
273
+ /**
258
274
  * Formats missing resources into a human-readable error message.
259
275
  *
260
276
  * @param missing - Array of missing resource entries
@@ -263,7 +279,8 @@ var ResourceRegistry = class ResourceRegistry {
263
279
  static formatMissingResources(missing) {
264
280
  if (missing.length === 0) return "No missing resources";
265
281
  return `Missing required resources:\n${missing.map((entry) => {
266
- const envHint = ` (set ${Object.values(entry.fields).map((f) => f.env).join(", ")})`;
282
+ const envVars = ResourceRegistry.unsetEnvVars(entry);
283
+ const envHint = envVars.length > 0 ? ` (set ${envVars.join(", ")})` : "";
267
284
  return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;
268
285
  }).join("\n")}`;
269
286
  }
@@ -277,9 +294,9 @@ var ResourceRegistry = class ResourceRegistry {
277
294
  static formatDevWarningBanner(missing) {
278
295
  const contentLines = ["MISSING REQUIRED RESOURCES (dev mode — would fail in production)", ""];
279
296
  for (const entry of missing) {
280
- const envVars = Object.values(entry.fields).map((f) => f.env);
297
+ const envVars = ResourceRegistry.unsetEnvVars(entry);
281
298
  contentLines.push(` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`);
282
- contentLines.push(` Set: ${envVars.join(", ")}`);
299
+ if (envVars.length > 0) contentLines.push(` Set: ${envVars.join(", ")}`);
283
300
  }
284
301
  contentLines.push("");
285
302
  contentLines.push("Add these to your .env file or environment to suppress this warning.");
@@ -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 { 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(\"Missing resources:\", result.missing.map(r => Object.values(r.fields).map(f => f.env)));\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: Object.values(r.fields).map((f) => f.env),\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 * 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 = Object.values(entry.fields).map((f) => f.env);\n const envHint = ` (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 = Object.values(entry.fields).map((f) => f.env);\n contentLines.push(\n ` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`,\n );\n contentLines.push(` Set: ${envVars.join(\", \")}`);\n }\n\n contentLines.push(\"\");\n contentLines.push(\n \"Add these to your .env file or environment to suppress this warning.\",\n );\n\n const maxLen = Math.max(...contentLines.map((l) => l.length));\n const border = \"=\".repeat(maxLen + 4);\n\n const boxed = contentLines.map((line) => `| ${line.padEnd(maxLen)} |`);\n\n return [border, ...boxed, border].join(\"\\n\");\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\";\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\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 const maxLen = Math.max(...contentLines.map((l) => l.length));\n const border = \"=\".repeat(maxLen + 4);\n\n const boxed = contentLines.map((line) => `| ${line.padEnd(maxLen)} |`);\n\n return [border, ...boxed, border].join(\"\\n\");\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ //#region src/schemas/metric-fqn.ts
2
+ /**
3
+ * Basename of the metric-view declarations file, resolved inside a
4
+ * `config/metric-views/` folder. Single source of truth shared by the analytics
5
+ * runtime (`plugins/analytics/mv`), the type-generator, its Vite watcher, and
6
+ * the `generate-types` CLI. It lives in this zod-free module (rather than the
7
+ * canonical `metric-source.ts` schema) so the type-generator can import it
8
+ * without pulling zod into its locked dependency graph.
9
+ */
10
+ const METRIC_CONFIG_FILE = "definitions.json";
11
+
12
+ //#endregion
13
+ export { METRIC_CONFIG_FILE };
14
+ //# sourceMappingURL=metric-fqn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metric-fqn.js","names":[],"sources":["../../src/schemas/metric-fqn.ts"],"sourcesContent":["/**\n * Unity Catalog object-name grammar\n * the single source of truth for metric view FQN (Fully Qualified Name).\n *\n * A metric view's `source` FQN is validated in two places that must agree:\n *\n * 1. The canonical Zod schema (`./metric-source.ts`), which composes the\n * three-part FQN regex from {@link UC_FQN_PATTERN} for IDE/CI and the\n * generated JSON schema (`docs/static/schemas/metric-source.schema.json`).\n * 2. The type-generator runtime (`packages/appkit/src/type-generator/mv-registry/config.ts`),\n * which imports {@link UC_FQN_PATTERN} as a plain value to validate each\n * dot-split segment.\n *\n *\n * A Unity Catalog object name:\n * - cannot exceed 255 characters ({@link MAX_UC_OBJECT_NAME_LENGTH}); and\n * - cannot contain any of these characters:\n * - period (`.`)\n * - space (U+0020)\n * - forward slash (`/`)\n * - all ASCII control characters (U+0000-U+001F)\n * - the DELETE character (U+007F)\n *\n * Every other character is permitted in a quoted name, including non-ASCII\n * letters (the docs demonstrate Chinese/Russian/Portuguese names) and hyphens.\n *\n * @note the regex is the per-segment character set; structure (3 parts) and length are intentionally left to the callers.\n */\n\nexport const MAX_UC_OBJECT_NAME_LENGTH = 255;\n\n/**\n * Basename of the metric-view declarations file, resolved inside a\n * `config/metric-views/` folder. Single source of truth shared by the analytics\n * runtime (`plugins/analytics/mv`), the type-generator, its Vite watcher, and\n * the `generate-types` CLI. It lives in this zod-free module (rather than the\n * canonical `metric-source.ts` schema) so the type-generator can import it\n * without pulling zod into its locked dependency graph.\n */\nexport const METRIC_CONFIG_FILE = \"definitions.json\";\n\n/**\n * Matches a single, non-empty Unity Catalog object name as it may appear in a backtick-quoted (delimited) identifier.\n *\n * @example\n * UC_FQN_PATTERN.test(\"revenue_metrics\"); // true\n * UC_FQN_PATTERN.test(\"prod-data\"); // true (hyphens are UC-legal)\n * UC_FQN_PATTERN.test(\"cafe\\u0301\"); // true (non-ASCII is UC-legal)\n * UC_FQN_PATTERN.test(\"bad name\"); // false (space is prohibited)\n * UC_FQN_PATTERN.test(\"a/b\"); // false (slash is prohibited)\n */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: UC explicitly prohibits ASCII control characters in object names; this negated class encodes that rule.\nexport const UC_FQN_PATTERN = /^[^\\x00-\\x20\\x7f./]+$/;\n\n/** A metric view FQN is exactly three segments: catalog.schema.metric_view. */\nconst FQN_SEGMENT_COUNT = 3;\n\n/**\n * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?\n * Well-formed = exactly three non-empty, dot-separated segments, each a valid Unity Catalog object name per {@link UC_FQN_PATTERN}.\n * @example\n * isValidFqn(\"main.analytics.revenue\"); // true\n * isValidFqn(\"prod-data.analytics.rev\"); // true (hyphens are UC-legal)\n * isValidFqn(\"main.analytics\"); // false (only two segments)\n */\nexport function isValidFqn(fqn: string): boolean {\n const segments = fqn.split(\".\");\n if (segments.length !== FQN_SEGMENT_COUNT) {\n return false;\n }\n return segments.every((segment) => UC_FQN_PATTERN.test(segment));\n}\n\n/**\n * Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL\n * statement.\n *\n * Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The\n * one character that can break out of a backtick-quoted identifier is the\n * backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick\n * inside a segment is doubled before the segment is wrapped. Control characters\n * and newlines have no valid escape inside a quoted identifier, so a segment\n * containing one is rejected outright.\n *\n * This is a pure, standalone escaper: it is intentionally independent of FQN\n * naming validation ({@link isValidFqn}). Naming validation decides whether an\n * FQN is an acceptable metric source; this function only guarantees that\n * whatever it is handed cannot break out of the quoted identifier it produces.\n * Grammar and quoting live together here so a metric source is validated and\n * escaped against one shared source of truth.\n *\n * An ordinary identifier is unchanged apart from the wrapping backticks:\n * `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.\n *\n * @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).\n * @returns The backtick-quoted, escaped identifier ready for interpolation.\n * @throws If any segment contains a control character or newline.\n */\nexport function quoteFqnForSql(fqn: string): string {\n return fqn.split(\".\").map(quoteIdentifier).join(\".\");\n}\n\n/**\n * The Unicode \"control\" category (`\\p{Cc}`): C0 (incl. `\\n`, `\\r`, `\\t`), DEL,\n * and C1 — every control character/newline. These have no valid escape inside\n * a backtick-quoted identifier, so a name containing one cannot be safely\n * quoted and is rejected.\n */\nconst CONTROL_OR_NEWLINE = /\\p{Cc}/u;\n\nexport function isValidColumnName(name: string): boolean {\n return name.length > 0 && !CONTROL_OR_NEWLINE.test(name);\n}\n\n/**\n * Quote a single identifier (one column/measure/dimension name, or one FQN\n * segment) as a backtick-delimited identifier for safe SQL interpolation.\n *\n * @throws If `name` contains a control character or newline.\n */\nexport function quoteIdentifier(name: string): string {\n if (CONTROL_OR_NEWLINE.test(name)) {\n throw new Error(\n `Cannot quote identifier \"${name}\" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`,\n );\n }\n // Double every backtick, then wrap in backticks.\n return `\\`${name.replace(/`/g, \"``\")}\\``;\n}\n"],"mappings":";;;;;;;;;AAuCA,MAAa,qBAAqB"}
@@ -1,15 +1,13 @@
1
1
  import { CacheConfig } from "./cache.js";
2
2
 
3
3
  //#region ../shared/src/execute.d.ts
4
- /** SSE stream configuration for `executeStream()`. Controls buffer sizes, heartbeat interval, and cleanup behavior. */
4
+ /** SSE stream configuration for `executeStream()`. Controls buffer sizes, buffer retention, and heartbeat interval. */
5
5
  interface StreamConfig {
6
6
  userSignal?: AbortSignal;
7
7
  streamId?: string;
8
8
  bufferSize?: number;
9
9
  maxEventSize?: number;
10
10
  bufferTTL?: number;
11
- cleanupInterval?: number;
12
- maxPersistentBuffers?: number;
13
11
  heartbeatInterval?: number;
14
12
  maxActiveStreams?: number;
15
13
  disconnectGraceMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"execute.d.ts","names":[],"sources":["../../../../shared/src/execute.ts"],"mappings":";;;;UAGiB,YAAA;EACf,UAAA,GAAa,WAAA;EACb,QAAA;EACA,UAAA;EACA,YAAA;EACA,SAAA;EACA,eAAA;EACA,oBAAA;EACA,iBAAA;EACA,gBAAA;EAGA,iBAAA;AAAA;;UAIe,WAAA;EACf,OAAA;EACA,QAAA;EACA,YAAA;EACA,QAAA;AAAA;;UAIe,eAAA;EACf,OAAA;EACA,QAAA;EACA,UAAA,GAAa,MAAA;AAAA;;UAIE,mBAAA;EACf,KAAA,GAAQ,WAAA;EACR,KAAA,GAAQ,WAAA;EAER,oBAAA,GAAuB,eAAA;EACvB,KAAA,GAAQ,WAAA;EACR,OAAA;EAAA,CACC,GAAA;AAAA;;UAIc,uBAAA;EACf,OAAA,EAAS,mBAAA;EACT,IAAA,GAAO,mBAAA;AAAA;;KAIG,oBAAA,QACN,MAAA,GAAS,WAAA,KAAgB,OAAA,CAAQ,CAAA,OACjC,MAAA,GAAS,WAAA,KAAgB,cAAA,CAAe,CAAA;;UAG7B,uBAAA;EACf,OAAA,EAAS,mBAAA;EACT,IAAA,GAAO,mBAAA;EACP,MAAA,GAAS,YAAA;AAAA"}
1
+ {"version":3,"file":"execute.d.ts","names":[],"sources":["../../../../shared/src/execute.ts"],"mappings":";;;;UAGiB,YAAA;EACf,UAAA,GAAa,WAAA;EACb,QAAA;EACA,UAAA;EACA,YAAA;EACA,SAAA;EACA,iBAAA;EACA,gBAAA;EAGA,iBAAA;AAAA;;UAIe,WAAA;EACf,OAAA;EACA,QAAA;EACA,YAAA;EACA,QAAA;AAAA;;UAIe,eAAA;EACf,OAAA;EACA,QAAA;EACA,UAAA,GAAa,MAAA;AAAA;;UAIE,mBAAA;EACf,KAAA,GAAQ,WAAA;EACR,KAAA,GAAQ,WAAA;EAER,oBAAA,GAAuB,eAAA;EACvB,KAAA,GAAQ,WAAA;EACR,OAAA;EAAA,CACC,GAAA;AAAA;;UAIc,uBAAA;EACf,OAAA,EAAS,mBAAA;EACT,IAAA,GAAO,mBAAA;AAAA;;KAIG,oBAAA,QACN,MAAA,GAAS,WAAA,KAAgB,OAAA,CAAQ,CAAA,OACjC,MAAA,GAAS,WAAA,KAAgB,cAAA,CAAe,CAAA;;UAG7B,uBAAA;EACf,OAAA,EAAS,mBAAA;EACT,IAAA,GAAO,mBAAA;EACP,MAAA,GAAS,YAAA;AAAA"}
@@ -6,7 +6,23 @@ import { JSONSchema7 } from "json-schema";
6
6
  /** Base plugin interface. */
7
7
  interface BasePlugin {
8
8
  name: string;
9
+ /**
10
+ * Cancel in-flight work (abort signals, SSE streams). Runs in the first
11
+ * phase of graceful shutdown, BEFORE any plugin's `shutdown()` hook —
12
+ * so it must not tear down shared resources (e.g. connection pools)
13
+ * that other plugins' hooks may still need. Put teardown in `shutdown()`.
14
+ */
9
15
  abortActiveOperations?(): void;
16
+ /**
17
+ * Optional graceful-shutdown hook.
18
+ *
19
+ * Invoked by AppKit's core lifecycle manager during graceful shutdown
20
+ * (SIGTERM/SIGINT), after `abortActiveOperations()` has run. Use it to
21
+ * drain in-flight work or release resources. Each plugin's hook is bounded
22
+ * by a per-plugin timeout and runs concurrently with other plugins' hooks;
23
+ * errors are logged and never abort the shutdown.
24
+ */
25
+ shutdown?(): Promise<void> | void;
10
26
  setup(): Promise<void>;
11
27
  injectRoutes(router: express.Router): void;
12
28
  getEndpoints(): PluginEndpointMap;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../../shared/src/plugin.ts"],"mappings":";;;;;;UAeiB,UAAA;EACf,IAAA;EAEA,qBAAA;EAEA,KAAA,IAAS,OAAA;EAET,YAAA,CAAa,MAAA,EAAQ,OAAA,CAAQ,MAAA;EAE7B,YAAA,IAAgB,iBAAA;EAEhB,uBAAA,KAA4B,WAAA;EAE5B,OAAA;EAEA,YAAA,KAAiB,MAAA;EAQmB;;;;EAFpC,aAAA,EAAe,IAAA;IACb,OAAA;IACA,eAAA,GAAkB,gBAAA;EAAA;AAAA;;UAKL,gBAAA;EACf,IAAA;EACA,IAAA;EAAA,CAEC,GAAA;EAMD,SAAA,GAAY,gBAAA;AAAA;AAAA,KAGF,gBAAA;EAGN,MAAA;EACA,OAAA;EACA,IAAA;AAAA;AAAA,KAQM,WAAA;;;;;KAMA,iBAAA,KACN,gBAAA,YACM,UAAA,GAAa,UAAA,UAEvB,MAAA,EAAQ,CAAA,KACL,CAAA;EACH,cAAA,GAAiB,MAAA;EACjB,KAAA,GAAQ,WAAA;EA7BoB;;AAG9B;;EA+BE,QAAA,EAAU,cAAA;EA/BgB;;;;EAoC1B,uBAAA,EAAyB,MAAA,EAAQ,CAAA,GAAI,mBAAA;AAAA;AAvBvC;;;;;AAMA;;;;AANA,UAmCiB,cAAA,wCACP,IAAA,CACN,gBAAA;EAgBF,IAAA,EAAM,KAAA;EACN,SAAA;IACE,QAAA,EAAU,IAAA,CAAK,mBAAA;IACf,QAAA,EAAU,IAAA,CAAK,mBAAA;EAAA;EAEjB,MAAA;IACE,MAAA,EAAQ,WAAA;EAAA;EApC8C;EAuCxD,WAAA;EAvDA;EAyDA,WAAA;EAxDA;EA0DA,MAAA;EA1DuB;EA4DvB,OAAA;EA1DA;EA4DA,UAAA;EA1DA;EA4DA,QAAA;EA3DA;EA6DA,OAAA;EAxDA;EA0DA,cAAA;EArDA;EAuDA,MAAA;EAvDyB;EAyDzB,SAAA;AAAA;;AA7CF;;;;;;;;;;;;;UA8DiB,mBAAA;EACf,IAAA;EACA,KAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;EACA,MAAA,EAAQ,MAAA,SAAe,kBAAA;EACvB,QAAA;AAAA;;AAPF;;;;KA2CY,aAAA,WAAwB,UAAA,IAClC,CAAA,sCAAqC,CAAA,GAAI,MAAA;;;;;;;;KAS/B,UAAA,QAAkB,GAAA,cAAgB,IAAA,mBAC1C,GAAA,GACA,GAAA;EAhDM;;AAoCV;;;EAkBM,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,GAAA;AAAA;;;;;;;;;KAWxB,SAAA,oBACS,UAAA,CAAW,iBAAA,gCAExB,CAAA,YAAa,CAAA,WAAY,UAAA,CAC7B,aAAA,CAAc,YAAA,CAAa,CAAA;AAvB/B;AAAA,KA4BY,UAAA;EAAwB,MAAA,EAAQ,CAAA;EAAG,MAAA,EAAQ,CAAA;EAAG,IAAA,EAAM,CAAA;AAAA;;KAEpD,QAAA,4BACV,MAAA,GAAS,CAAA,KACN,UAAA,CAAW,CAAA,EAAG,CAAA,EAAG,CAAA;;KAGV,UAAA,GAAa,OAAA,CAAQ,MAAA;AAAA,KACrB,YAAA,GAAe,OAAA,CAAQ,QAAA;AAAA,KACvB,WAAA,GAAc,OAAA,CAAQ,OAAA;AAAA,KAEtB,UAAA;AAAA,KAEA,WAAA;EAvCR,+DAyCF,IAAA;EACA,MAAA,EAAQ,UAAA;EACR,IAAA;EACA,OAAA,GAAU,GAAA,EAAK,WAAA,EAAa,GAAA,EAAK,YAAA,KAAiB,OAAA,QAtCb;EAwCrC,eAAA;AAAA;;KAIU,iBAAA,GAAoB,MAAA"}
1
+ {"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../../shared/src/plugin.ts"],"mappings":";;;;;;UAeiB,UAAA;EACf,IAAA;;;;;;;EAQA,qBAAA;EA+BoB;;;;;;;;;EApBpB,QAAA,KAAa,OAAA;EAEb,KAAA,IAAS,OAAA;EAET,YAAA,CAAa,MAAA,EAAQ,OAAA,CAAQ,MAAA;EAE7B,YAAA,IAAgB,iBAAA;EAEhB,uBAAA,KAA4B,WAAA;EAE5B,OAAA;EAEA,YAAA,KAAiB,MAAA;EAJW;;;;EAU5B,aAAA,EAAe,IAAA;IACb,OAAA;IACA,eAAA,GAAkB,gBAAA;EAAA;AAAA;;UAKL,gBAAA;EACf,IAAA;EACA,IAAA;EAAA,CAEC,GAAA;EAMD,SAAA,GAAY,gBAAA;AAAA;AAAA,KAGF,gBAAA;EAGN,MAAA;EACA,OAAA;EACA,IAAA;AAAA;AAAA,KAQM,WAAA;;;;;KAMA,iBAAA,KACN,gBAAA,YACM,UAAA,GAAa,UAAA,UAEvB,MAAA,EAAQ,CAAA,KACL,CAAA;EACH,cAAA,GAAiB,MAAA;EACjB,KAAA,GAAQ,WAAA;;;;AAPV;EAYE,QAAA,EAAU,cAAA;EAZiB;;;;EAiB3B,uBAAA,EAAyB,MAAA,EAAQ,CAAA,GAAI,mBAAA;AAAA;;;;;;;;;;UAYtB,cAAA,wCACP,IAAA,CACN,gBAAA;EAgBF,IAAA,EAAM,KAAA;EACN,SAAA;IACE,QAAA,EAAU,IAAA,CAAK,mBAAA;IACf,QAAA,EAAU,IAAA,CAAK,mBAAA;EAAA;EAEjB,MAAA;IACE,MAAA,EAAQ,WAAA;EAAA;EA9CF;EAiDR,WAAA;EA5CU;EA8CV,WAAA;EAzCiC;EA2CjC,MAAA;EA3CqC;EA6CrC,OAAA;EA7CwD;EA+CxD,UAAA;EAnC6B;EAqC7B,QAAA;EAnCE;EAqCF,OAAA;EAnBiB;EAqBjB,cAAA;EApBiB;EAsBjB,MAAA;EAnBU;EAqBV,SAAA;AAAA;;;;;;;;;;;;;;;UAiBe,mBAAA;EACf,IAAA;EACA,KAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;EACA,MAAA,EAAQ,MAAA,SAAe,kBAAA;EACvB,QAAA;AAAA;;;;AAoCF;;KAAY,aAAA,WAAwB,UAAA,IAClC,CAAA,sCAAqC,CAAA,GAAI,MAAA;;;;;;;;KAS/B,UAAA,QAAkB,GAAA,cAAgB,IAAA,mBAC1C,GAAA,GACA,GAAA;EAX+B;;;;;EAiB7B,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,GAAA;AAAA;;;;;;;;;KAWxB,SAAA,oBACS,UAAA,CAAW,iBAAA,gCAExB,CAAA,YAAa,CAAA,WAAY,UAAA,CAC7B,aAAA,CAAc,YAAA,CAAa,CAAA;;KAKnB,UAAA;EAAwB,MAAA,EAAQ,CAAA;EAAG,MAAA,EAAQ,CAAA;EAAG,IAAA,EAAM,CAAA;AAAA;;KAEpD,QAAA,4BACV,MAAA,GAAS,CAAA,KACN,UAAA,CAAW,CAAA,EAAG,CAAA,EAAG,CAAA;AAbtB;AAAA,KAgBY,UAAA,GAAa,OAAA,CAAQ,MAAA;AAAA,KACrB,YAAA,GAAe,OAAA,CAAQ,QAAA;AAAA,KACvB,WAAA,GAAc,OAAA,CAAQ,OAAA;AAAA,KAEtB,UAAA;AAAA,KAEA,WAAA;EAnBS,+DAqBnB,IAAA;EACA,MAAA,EAAQ,UAAA;EACR,IAAA;EACA,OAAA,GAAU,GAAA,EAAK,WAAA,EAAa,GAAA,EAAK,YAAA,KAAiB,OAAA,QAxBT;EA0BzC,eAAA;AAAA;;KAIU,iBAAA,GAAoB,MAAA"}