@databricks/appkit 0.21.0 → 0.22.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 (57) hide show
  1. package/CLAUDE.md +1 -0
  2. package/README.md +3 -20
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/cli/commands/setup.js +2 -2
  5. package/dist/cli/commands/setup.js.map +1 -1
  6. package/dist/connectors/genie/client.js +50 -0
  7. package/dist/connectors/genie/client.js.map +1 -1
  8. package/dist/plugin/plugin.d.ts +47 -1
  9. package/dist/plugin/plugin.d.ts.map +1 -1
  10. package/dist/plugin/plugin.js +51 -2
  11. package/dist/plugin/plugin.js.map +1 -1
  12. package/dist/plugins/files/plugin.d.ts +1 -0
  13. package/dist/plugins/files/plugin.d.ts.map +1 -1
  14. package/dist/plugins/files/plugin.js +3 -0
  15. package/dist/plugins/files/plugin.js.map +1 -1
  16. package/dist/plugins/genie/genie.d.ts +1 -0
  17. package/dist/plugins/genie/genie.d.ts.map +1 -1
  18. package/dist/plugins/genie/genie.js +42 -3
  19. package/dist/plugins/genie/genie.js.map +1 -1
  20. package/dist/plugins/server/base-server.js +4 -2
  21. package/dist/plugins/server/base-server.js.map +1 -1
  22. package/dist/plugins/server/client-config-sanitizer.js +184 -0
  23. package/dist/plugins/server/client-config-sanitizer.js.map +1 -0
  24. package/dist/plugins/server/index.d.ts +2 -1
  25. package/dist/plugins/server/index.d.ts.map +1 -1
  26. package/dist/plugins/server/index.js +27 -9
  27. package/dist/plugins/server/index.js.map +1 -1
  28. package/dist/plugins/server/remote-tunnel/denied.html +68 -0
  29. package/dist/plugins/server/remote-tunnel/index.html +165 -0
  30. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +2 -1
  31. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  32. package/dist/plugins/server/remote-tunnel/wait.html +158 -0
  33. package/dist/plugins/server/static-server.js +2 -2
  34. package/dist/plugins/server/static-server.js.map +1 -1
  35. package/dist/plugins/server/utils.js +28 -5
  36. package/dist/plugins/server/utils.js.map +1 -1
  37. package/dist/plugins/server/vite-dev-server.js +2 -2
  38. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  39. package/dist/shared/src/plugin.d.ts +1 -0
  40. package/dist/shared/src/plugin.d.ts.map +1 -1
  41. package/dist/type-generator/index.js +10 -0
  42. package/dist/type-generator/index.js.map +1 -1
  43. package/docs/api/appkit/Class.Plugin.md +75 -17
  44. package/docs/app-management.md +1 -1
  45. package/docs/architecture.md +1 -1
  46. package/docs/development/ai-assisted-development.md +2 -2
  47. package/docs/development/local-development.md +1 -1
  48. package/docs/development/remote-bridge.md +1 -1
  49. package/docs/development/templates.md +93 -0
  50. package/docs/development.md +1 -1
  51. package/docs/plugins/caching.md +3 -1
  52. package/docs/plugins/execution-context.md +1 -1
  53. package/docs/plugins/lakebase.md +1 -1
  54. package/docs.md +2 -2
  55. package/llms.txt +1 -0
  56. package/package.json +37 -36
  57. package/sbom.cdx.json +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":["manifest","files"],"sources":["../../../src/plugins/files/plugin.ts"],"sourcesContent":["import { Readable } from \"node:stream\";\nimport { ApiError } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type { IAppRouter, PluginExecutionSettings } from \"shared\";\nimport {\n contentTypeFromPath,\n FilesConnector,\n isSafeInlineContentType,\n validateCustomContentTypes,\n} from \"../../connectors/files\";\nimport { getWorkspaceClient, isInUserContext } from \"../../context\";\nimport { AuthenticationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport {\n FILES_DOWNLOAD_DEFAULTS,\n FILES_MAX_UPLOAD_SIZE,\n FILES_READ_DEFAULTS,\n FILES_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport { parentDirectory, sanitizeFilename } from \"./helpers\";\nimport manifest from \"./manifest.json\";\nimport type {\n DownloadResponse,\n FilesExport,\n IFilesConfig,\n VolumeAPI,\n VolumeConfig,\n VolumeHandle,\n} from \"./types\";\n\nconst logger = createLogger(\"files\");\n\nexport class FilesPlugin extends Plugin {\n name = \"files\";\n\n /** Plugin manifest declaring metadata and resource requirements. */\n static manifest = manifest as PluginManifest;\n protected static description = \"Files plugin for Databricks file operations\";\n protected declare config: IFilesConfig;\n\n private volumeConnectors: Record<string, FilesConnector> = {};\n private volumeConfigs: Record<string, VolumeConfig> = {};\n private volumeKeys: string[] = [];\n\n /**\n * Scans `process.env` for `DATABRICKS_VOLUME_*` keys and merges them with\n * any explicitly configured volumes. Explicit config wins for per-volume\n * overrides; auto-discovered volumes get default `{}` config.\n */\n static discoverVolumes(config: IFilesConfig): Record<string, VolumeConfig> {\n const explicit = config.volumes ?? {};\n const discovered: Record<string, VolumeConfig> = {};\n\n const prefix = \"DATABRICKS_VOLUME_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n const suffix = key.slice(prefix.length);\n if (!suffix) continue;\n if (!process.env[key]) continue;\n const volumeKey = suffix.toLowerCase();\n if (!(volumeKey in explicit)) {\n discovered[volumeKey] = {};\n }\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured volumes.\n * Each volume key maps to a `DATABRICKS_VOLUME_{KEY_UPPERCASE}` env var.\n */\n static getResourceRequirements(config: IFilesConfig): ResourceRequirement[] {\n const volumes = FilesPlugin.discoverVolumes(config);\n return Object.keys(volumes).map((key) => ({\n type: ResourceType.VOLUME,\n alias: `volume-${key}`,\n resourceKey: `volume-${key}`,\n description: `Unity Catalog Volume for \"${key}\" file storage`,\n permission: \"WRITE_VOLUME\",\n fields: {\n path: {\n env: `DATABRICKS_VOLUME_${key.toUpperCase()}`,\n description: `Volume path for \"${key}\" (e.g. /Volumes/catalog/schema/volume_name)`,\n },\n },\n required: true,\n }));\n }\n\n /**\n * Warns when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is strongly recommended.\n */\n private warnIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n logger.warn(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). ` +\n `Please use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n /**\n * Throws when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is enforced for now.\n */\n private throwIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n throw new Error(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). Use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n constructor(config: IFilesConfig) {\n super(config);\n this.config = config;\n\n if (config.customContentTypes) {\n validateCustomContentTypes(config.customContentTypes);\n }\n\n const volumes = FilesPlugin.discoverVolumes(config);\n this.volumeKeys = Object.keys(volumes);\n\n for (const key of this.volumeKeys) {\n const volumeCfg = volumes[key];\n const envVar = `DATABRICKS_VOLUME_${key.toUpperCase()}`;\n const volumePath = process.env[envVar];\n\n // Merge per-volume config with plugin-level defaults\n const mergedConfig: VolumeConfig = {\n maxUploadSize: volumeCfg.maxUploadSize ?? config.maxUploadSize,\n customContentTypes:\n volumeCfg.customContentTypes ?? config.customContentTypes,\n };\n this.volumeConfigs[key] = mergedConfig;\n\n this.volumeConnectors[key] = new FilesConnector({\n defaultVolume: volumePath,\n timeout: config.timeout,\n telemetry: config.telemetry,\n customContentTypes: mergedConfig.customContentTypes,\n });\n }\n }\n\n /**\n * Creates a VolumeAPI for a specific volume key.\n * Each method warns if called outside a user context (service principal).\n */\n protected createVolumeAPI(volumeKey: string): VolumeAPI {\n const connector = this.volumeConnectors[volumeKey];\n return {\n list: (directoryPath?: string) => {\n this.throwIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), directoryPath);\n },\n read: (filePath: string, options?: { maxSize?: number }) => {\n this.throwIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), filePath, options);\n },\n download: (filePath: string): Promise<DownloadResponse> => {\n this.throwIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), filePath);\n },\n exists: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), filePath);\n },\n metadata: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), filePath);\n },\n upload: (\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ) => {\n this.throwIfNoUserContext(volumeKey, `upload`);\n return connector.upload(\n getWorkspaceClient(),\n filePath,\n contents,\n options,\n );\n },\n createDirectory: (directoryPath: string) => {\n this.throwIfNoUserContext(volumeKey, `createDirectory`);\n return connector.createDirectory(getWorkspaceClient(), directoryPath);\n },\n delete: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `delete`);\n return connector.delete(getWorkspaceClient(), filePath);\n },\n preview: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), filePath);\n },\n };\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"volumes\",\n method: \"get\",\n path: \"/volumes\",\n handler: async (_req: express.Request, res: express.Response) => {\n res.json({ volumes: this.volumeKeys });\n },\n });\n\n this.route(router, {\n name: \"list\",\n method: \"get\",\n path: \"/:volumeKey/list\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleList(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"read\",\n method: \"get\",\n path: \"/:volumeKey/read\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRead(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"download\",\n method: \"get\",\n path: \"/:volumeKey/download\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDownload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"raw\",\n method: \"get\",\n path: \"/:volumeKey/raw\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRaw(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"exists\",\n method: \"get\",\n path: \"/:volumeKey/exists\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleExists(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"metadata\",\n method: \"get\",\n path: \"/:volumeKey/metadata\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMetadata(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"preview\",\n method: \"get\",\n path: \"/:volumeKey/preview\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handlePreview(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"upload\",\n method: \"post\",\n path: \"/:volumeKey/upload\",\n skipBodyParsing: true,\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleUpload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"mkdir\",\n method: \"post\",\n path: \"/:volumeKey/mkdir\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMkdir(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"delete\",\n method: \"delete\",\n path: \"/:volumeKey\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDelete(req, res, connector, volumeKey);\n },\n });\n }\n\n /**\n * Resolve `:volumeKey` from the request. Returns the connector and key,\n * or sends a 404 and returns `{ connector: undefined }`.\n */\n private _resolveVolume(\n req: express.Request,\n res: express.Response,\n ):\n | { connector: FilesConnector; volumeKey: string }\n | { connector: undefined; volumeKey: undefined } {\n const volumeKey = req.params.volumeKey;\n const connector = this.volumeConnectors[volumeKey];\n if (!connector) {\n const safeKey = volumeKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown volume \"${safeKey}\"`,\n plugin: this.name,\n });\n return { connector: undefined, volumeKey: undefined };\n }\n return { connector, volumeKey };\n }\n\n /**\n * Validate a file/directory path from user input.\n * Returns `true` if valid, or an error message string if invalid.\n */\n private _isValidPath(path: string | undefined): true | string {\n if (!path) return \"path is required\";\n if (path.length > 4096)\n return `path exceeds maximum length of 4096 characters (got ${path.length})`;\n if (path.includes(\"\\0\")) return \"path must not contain null bytes\";\n return true;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...FILES_READ_DEFAULTS,\n cache: { ...FILES_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n /**\n * Invalidate cached list entries for a directory after a write operation.\n * Uses the same cache-key format as `_handleList`: resolved path for\n * subdirectories, `\"__root__\"` for the volume root.\n */\n private _invalidateListCache(\n volumeKey: string,\n parentPath: string,\n userId: string,\n connector: FilesConnector,\n ): void {\n const parent = parentDirectory(parentPath);\n const cachePathSegment = parent\n ? connector.resolvePath(parent)\n : \"__root__\";\n const listKey = this.cache.generateKey(\n [`files:${volumeKey}:list`, cachePathSegment],\n userId,\n );\n this.cache.delete(listKey);\n }\n\n private _handleApiError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n if (error instanceof AuthenticationError) {\n res.status(401).json({\n error: error.message,\n plugin: this.name,\n });\n return;\n }\n if (error instanceof ApiError) {\n const status = error.statusCode ?? 500;\n if (status >= 400 && status < 500) {\n res.status(status).json({\n error: error.message,\n statusCode: status,\n plugin: this.name,\n });\n return;\n }\n logger.error(\"Upstream server error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n return;\n }\n logger.error(\"Unhandled error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n }\n\n private async _handleList(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string | undefined;\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:list`,\n path ? connector.resolvePath(path) : \"__root__\",\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"List failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"List failed\");\n }\n }\n\n private async _handleRead(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:read`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Read failed\", plugin: this.name });\n return;\n }\n res.type(\"text/plain\").send(result);\n } catch (error) {\n this._handleApiError(res, error, \"Read failed\");\n }\n }\n\n private async _handleDownload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"download\",\n });\n }\n\n private async _handleRaw(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"raw\",\n });\n }\n\n /**\n * Shared handler for `/download` and `/raw` endpoints.\n * - `download`: always forces `Content-Disposition: attachment`.\n * - `raw`: adds CSP sandbox; forces attachment only for unsafe content types.\n */\n private async _serveFile(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n opts: { mode: \"download\" | \"raw\" },\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const label = opts.mode === \"download\" ? \"Download\" : \"Raw fetch\";\n const volumeCfg = this.volumeConfigs[volumeKey];\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_DOWNLOAD_DEFAULTS,\n };\n const response = await userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), path);\n }, settings);\n\n if (response === undefined) {\n res.status(500).json({ error: `${label} failed`, plugin: this.name });\n return;\n }\n\n const resolvedType = contentTypeFromPath(\n path,\n undefined,\n volumeCfg.customContentTypes,\n );\n const fileName = sanitizeFilename(path.split(\"/\").pop() ?? \"download\");\n\n res.setHeader(\"Content-Type\", resolvedType);\n res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n\n if (opts.mode === \"raw\") {\n res.setHeader(\"Content-Security-Policy\", \"sandbox\");\n if (!isSafeInlineContentType(resolvedType)) {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n } else {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n\n if (response.contents) {\n const nodeStream = Readable.fromWeb(\n response.contents as import(\"node:stream/web\").ReadableStream,\n );\n nodeStream.on(\"error\", (err) => {\n logger.error(\"Stream error during %s: %O\", opts.mode, err);\n if (!res.headersSent) {\n res\n .status(500)\n .json({ error: `${label} failed`, plugin: this.name });\n } else {\n res.destroy();\n }\n });\n nodeStream.pipe(res);\n } else {\n res.end();\n }\n } catch (error) {\n this._handleApiError(res, error, `${label} failed`);\n }\n }\n\n private async _handleExists(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:exists`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Exists check failed\", plugin: this.name });\n return;\n }\n res.json({ exists: result });\n } catch (error) {\n this._handleApiError(res, error, \"Exists check failed\");\n }\n }\n\n private async _handleMetadata(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:metadata`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Metadata fetch failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Metadata fetch failed\");\n }\n }\n\n private async _handlePreview(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:preview`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Preview failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Preview failed\");\n }\n }\n\n private async _handleUpload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const volumeCfg = this.volumeConfigs[volumeKey];\n const maxSize = volumeCfg.maxUploadSize ?? FILES_MAX_UPLOAD_SIZE;\n const rawContentLength = req.headers[\"content-length\"];\n const contentLength = rawContentLength\n ? parseInt(rawContentLength, 10)\n : undefined;\n\n if (\n contentLength !== undefined &&\n !Number.isNaN(contentLength) &&\n contentLength > maxSize\n ) {\n res.status(413).json({\n error: `File size (${contentLength} bytes) exceeds maximum allowed size (${maxSize} bytes).`,\n plugin: this.name,\n });\n return;\n }\n\n logger.debug(req, \"Upload started: volume=%s path=%s\", volumeKey, path);\n\n try {\n const rawStream: ReadableStream<Uint8Array> = Readable.toWeb(req);\n\n let bytesReceived = 0;\n const webStream = rawStream.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesReceived += chunk.byteLength;\n if (bytesReceived > maxSize) {\n controller.error(\n new Error(\n `Upload stream exceeds maximum allowed size (${maxSize} bytes)`,\n ),\n );\n return;\n }\n controller.enqueue(chunk);\n },\n }),\n );\n\n logger.debug(\n req,\n \"Upload body received: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `upload`);\n await connector.upload(getWorkspaceClient(), path, webStream);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n logger.error(\n req,\n \"Upload failed: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n res.status(500).json({ error: \"Upload failed\", plugin: this.name });\n return;\n }\n\n logger.debug(req, \"Upload complete: volume=%s path=%s\", volumeKey, path);\n res.json(result);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"exceeds maximum allowed size\")\n ) {\n res.status(413).json({ error: error.message, plugin: this.name });\n return;\n }\n this._handleApiError(res, error, \"Upload failed\");\n }\n }\n\n private async _handleMkdir(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const dirPath =\n typeof req.body?.path === \"string\" ? req.body.path : undefined;\n const valid = this._isValidPath(dirPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `createDirectory`);\n await connector.createDirectory(getWorkspaceClient(), dirPath);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n dirPath,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Create directory failed\", plugin: this.name });\n return;\n }\n\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Create directory failed\");\n }\n }\n\n private async _handleDelete(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const rawPath = req.query.path as string | undefined;\n const valid = this._isValidPath(rawPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n const path = rawPath as string;\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `delete`);\n await connector.delete(getWorkspaceClient(), path);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Delete failed\", plugin: this.name });\n return;\n }\n\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Delete failed\");\n }\n }\n\n private inflightWrites = 0;\n\n private trackWrite<T>(fn: () => Promise<T>): Promise<T> {\n this.inflightWrites++;\n return fn().finally(() => {\n this.inflightWrites--;\n });\n }\n\n async shutdown(): Promise<void> {\n // Wait up to 10 seconds for in-flight write operations to finish\n const deadline = Date.now() + 10_000;\n while (this.inflightWrites > 0 && Date.now() < deadline) {\n logger.info(\n \"Waiting for %d in-flight write(s) to complete before shutdown…\",\n this.inflightWrites,\n );\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (this.inflightWrites > 0) {\n logger.warn(\n \"Shutdown deadline reached with %d in-flight write(s) still pending.\",\n this.inflightWrites,\n );\n }\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the programmatic API for the Files plugin.\n * Callable with a volume key to get a volume-scoped handle.\n *\n * @example\n * ```ts\n * // OBO access (recommended)\n * appKit.files(\"uploads\").asUser(req).list()\n *\n * // Service principal access (logs a warning)\n * appKit.files(\"uploads\").list()\n * ```\n */\n exports(): FilesExport {\n const resolveVolume = (volumeKey: string): VolumeHandle => {\n if (!this.volumeKeys.includes(volumeKey)) {\n throw new Error(\n `Unknown volume \"${volumeKey}\". Available volumes: ${this.volumeKeys.join(\", \")}`,\n );\n }\n\n // Service principal API — each method logs a warning recommending OBO\n const spApi = this.createVolumeAPI(volumeKey);\n\n return {\n ...spApi,\n asUser: (req: import(\"express\").Request) => {\n const userPlugin = this.asUser(req) as FilesPlugin;\n return userPlugin.createVolumeAPI(volumeKey);\n },\n };\n };\n\n const filesExport = ((volumeKey: string) =>\n resolveVolume(volumeKey)) as FilesExport;\n filesExport.volume = resolveVolume;\n\n return filesExport;\n }\n}\n\n/**\n * @internal\n */\nexport const files = toPlugin(FilesPlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","names":["manifest","files"],"sources":["../../../src/plugins/files/plugin.ts"],"sourcesContent":["import { Readable } from \"node:stream\";\nimport { ApiError } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type { IAppRouter, PluginExecutionSettings } from \"shared\";\nimport {\n contentTypeFromPath,\n FilesConnector,\n isSafeInlineContentType,\n validateCustomContentTypes,\n} from \"../../connectors/files\";\nimport { getWorkspaceClient, isInUserContext } from \"../../context\";\nimport { AuthenticationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport {\n FILES_DOWNLOAD_DEFAULTS,\n FILES_MAX_UPLOAD_SIZE,\n FILES_READ_DEFAULTS,\n FILES_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport { parentDirectory, sanitizeFilename } from \"./helpers\";\nimport manifest from \"./manifest.json\";\nimport type {\n DownloadResponse,\n FilesExport,\n IFilesConfig,\n VolumeAPI,\n VolumeConfig,\n VolumeHandle,\n} from \"./types\";\n\nconst logger = createLogger(\"files\");\n\nexport class FilesPlugin extends Plugin {\n name = \"files\";\n\n /** Plugin manifest declaring metadata and resource requirements. */\n static manifest = manifest as PluginManifest;\n protected static description = \"Files plugin for Databricks file operations\";\n protected declare config: IFilesConfig;\n\n private volumeConnectors: Record<string, FilesConnector> = {};\n private volumeConfigs: Record<string, VolumeConfig> = {};\n private volumeKeys: string[] = [];\n\n /**\n * Scans `process.env` for `DATABRICKS_VOLUME_*` keys and merges them with\n * any explicitly configured volumes. Explicit config wins for per-volume\n * overrides; auto-discovered volumes get default `{}` config.\n */\n static discoverVolumes(config: IFilesConfig): Record<string, VolumeConfig> {\n const explicit = config.volumes ?? {};\n const discovered: Record<string, VolumeConfig> = {};\n\n const prefix = \"DATABRICKS_VOLUME_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n const suffix = key.slice(prefix.length);\n if (!suffix) continue;\n if (!process.env[key]) continue;\n const volumeKey = suffix.toLowerCase();\n if (!(volumeKey in explicit)) {\n discovered[volumeKey] = {};\n }\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured volumes.\n * Each volume key maps to a `DATABRICKS_VOLUME_{KEY_UPPERCASE}` env var.\n */\n static getResourceRequirements(config: IFilesConfig): ResourceRequirement[] {\n const volumes = FilesPlugin.discoverVolumes(config);\n return Object.keys(volumes).map((key) => ({\n type: ResourceType.VOLUME,\n alias: `volume-${key}`,\n resourceKey: `volume-${key}`,\n description: `Unity Catalog Volume for \"${key}\" file storage`,\n permission: \"WRITE_VOLUME\",\n fields: {\n path: {\n env: `DATABRICKS_VOLUME_${key.toUpperCase()}`,\n description: `Volume path for \"${key}\" (e.g. /Volumes/catalog/schema/volume_name)`,\n },\n },\n required: true,\n }));\n }\n\n /**\n * Warns when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is strongly recommended.\n */\n private warnIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n logger.warn(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). ` +\n `Please use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n /**\n * Throws when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is enforced for now.\n */\n private throwIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n throw new Error(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). Use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n constructor(config: IFilesConfig) {\n super(config);\n this.config = config;\n\n if (config.customContentTypes) {\n validateCustomContentTypes(config.customContentTypes);\n }\n\n const volumes = FilesPlugin.discoverVolumes(config);\n this.volumeKeys = Object.keys(volumes);\n\n for (const key of this.volumeKeys) {\n const volumeCfg = volumes[key];\n const envVar = `DATABRICKS_VOLUME_${key.toUpperCase()}`;\n const volumePath = process.env[envVar];\n\n // Merge per-volume config with plugin-level defaults\n const mergedConfig: VolumeConfig = {\n maxUploadSize: volumeCfg.maxUploadSize ?? config.maxUploadSize,\n customContentTypes:\n volumeCfg.customContentTypes ?? config.customContentTypes,\n };\n this.volumeConfigs[key] = mergedConfig;\n\n this.volumeConnectors[key] = new FilesConnector({\n defaultVolume: volumePath,\n timeout: config.timeout,\n telemetry: config.telemetry,\n customContentTypes: mergedConfig.customContentTypes,\n });\n }\n }\n\n /**\n * Creates a VolumeAPI for a specific volume key.\n * Each method warns if called outside a user context (service principal).\n */\n protected createVolumeAPI(volumeKey: string): VolumeAPI {\n const connector = this.volumeConnectors[volumeKey];\n return {\n list: (directoryPath?: string) => {\n this.throwIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), directoryPath);\n },\n read: (filePath: string, options?: { maxSize?: number }) => {\n this.throwIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), filePath, options);\n },\n download: (filePath: string): Promise<DownloadResponse> => {\n this.throwIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), filePath);\n },\n exists: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), filePath);\n },\n metadata: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), filePath);\n },\n upload: (\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ) => {\n this.throwIfNoUserContext(volumeKey, `upload`);\n return connector.upload(\n getWorkspaceClient(),\n filePath,\n contents,\n options,\n );\n },\n createDirectory: (directoryPath: string) => {\n this.throwIfNoUserContext(volumeKey, `createDirectory`);\n return connector.createDirectory(getWorkspaceClient(), directoryPath);\n },\n delete: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `delete`);\n return connector.delete(getWorkspaceClient(), filePath);\n },\n preview: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), filePath);\n },\n };\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"volumes\",\n method: \"get\",\n path: \"/volumes\",\n handler: async (_req: express.Request, res: express.Response) => {\n res.json({ volumes: this.volumeKeys });\n },\n });\n\n this.route(router, {\n name: \"list\",\n method: \"get\",\n path: \"/:volumeKey/list\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleList(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"read\",\n method: \"get\",\n path: \"/:volumeKey/read\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRead(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"download\",\n method: \"get\",\n path: \"/:volumeKey/download\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDownload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"raw\",\n method: \"get\",\n path: \"/:volumeKey/raw\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRaw(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"exists\",\n method: \"get\",\n path: \"/:volumeKey/exists\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleExists(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"metadata\",\n method: \"get\",\n path: \"/:volumeKey/metadata\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMetadata(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"preview\",\n method: \"get\",\n path: \"/:volumeKey/preview\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handlePreview(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"upload\",\n method: \"post\",\n path: \"/:volumeKey/upload\",\n skipBodyParsing: true,\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleUpload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"mkdir\",\n method: \"post\",\n path: \"/:volumeKey/mkdir\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMkdir(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"delete\",\n method: \"delete\",\n path: \"/:volumeKey\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDelete(req, res, connector, volumeKey);\n },\n });\n }\n\n /**\n * Resolve `:volumeKey` from the request. Returns the connector and key,\n * or sends a 404 and returns `{ connector: undefined }`.\n */\n private _resolveVolume(\n req: express.Request,\n res: express.Response,\n ):\n | { connector: FilesConnector; volumeKey: string }\n | { connector: undefined; volumeKey: undefined } {\n const volumeKey = req.params.volumeKey;\n const connector = this.volumeConnectors[volumeKey];\n if (!connector) {\n const safeKey = volumeKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown volume \"${safeKey}\"`,\n plugin: this.name,\n });\n return { connector: undefined, volumeKey: undefined };\n }\n return { connector, volumeKey };\n }\n\n /**\n * Validate a file/directory path from user input.\n * Returns `true` if valid, or an error message string if invalid.\n */\n private _isValidPath(path: string | undefined): true | string {\n if (!path) return \"path is required\";\n if (path.length > 4096)\n return `path exceeds maximum length of 4096 characters (got ${path.length})`;\n if (path.includes(\"\\0\")) return \"path must not contain null bytes\";\n return true;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...FILES_READ_DEFAULTS,\n cache: { ...FILES_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n /**\n * Invalidate cached list entries for a directory after a write operation.\n * Uses the same cache-key format as `_handleList`: resolved path for\n * subdirectories, `\"__root__\"` for the volume root.\n */\n private _invalidateListCache(\n volumeKey: string,\n parentPath: string,\n userId: string,\n connector: FilesConnector,\n ): void {\n const parent = parentDirectory(parentPath);\n const cachePathSegment = parent\n ? connector.resolvePath(parent)\n : \"__root__\";\n const listKey = this.cache.generateKey(\n [`files:${volumeKey}:list`, cachePathSegment],\n userId,\n );\n this.cache.delete(listKey);\n }\n\n private _handleApiError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n if (error instanceof AuthenticationError) {\n res.status(401).json({\n error: error.message,\n plugin: this.name,\n });\n return;\n }\n if (error instanceof ApiError) {\n const status = error.statusCode ?? 500;\n if (status >= 400 && status < 500) {\n res.status(status).json({\n error: error.message,\n statusCode: status,\n plugin: this.name,\n });\n return;\n }\n logger.error(\"Upstream server error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n return;\n }\n logger.error(\"Unhandled error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n }\n\n private async _handleList(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string | undefined;\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:list`,\n path ? connector.resolvePath(path) : \"__root__\",\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"List failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"List failed\");\n }\n }\n\n private async _handleRead(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:read`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Read failed\", plugin: this.name });\n return;\n }\n res.type(\"text/plain\").send(result);\n } catch (error) {\n this._handleApiError(res, error, \"Read failed\");\n }\n }\n\n private async _handleDownload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"download\",\n });\n }\n\n private async _handleRaw(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"raw\",\n });\n }\n\n /**\n * Shared handler for `/download` and `/raw` endpoints.\n * - `download`: always forces `Content-Disposition: attachment`.\n * - `raw`: adds CSP sandbox; forces attachment only for unsafe content types.\n */\n private async _serveFile(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n opts: { mode: \"download\" | \"raw\" },\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const label = opts.mode === \"download\" ? \"Download\" : \"Raw fetch\";\n const volumeCfg = this.volumeConfigs[volumeKey];\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_DOWNLOAD_DEFAULTS,\n };\n const response = await userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), path);\n }, settings);\n\n if (response === undefined) {\n res.status(500).json({ error: `${label} failed`, plugin: this.name });\n return;\n }\n\n const resolvedType = contentTypeFromPath(\n path,\n undefined,\n volumeCfg.customContentTypes,\n );\n const fileName = sanitizeFilename(path.split(\"/\").pop() ?? \"download\");\n\n res.setHeader(\"Content-Type\", resolvedType);\n res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n\n if (opts.mode === \"raw\") {\n res.setHeader(\"Content-Security-Policy\", \"sandbox\");\n if (!isSafeInlineContentType(resolvedType)) {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n } else {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n\n if (response.contents) {\n const nodeStream = Readable.fromWeb(\n response.contents as import(\"node:stream/web\").ReadableStream,\n );\n nodeStream.on(\"error\", (err) => {\n logger.error(\"Stream error during %s: %O\", opts.mode, err);\n if (!res.headersSent) {\n res\n .status(500)\n .json({ error: `${label} failed`, plugin: this.name });\n } else {\n res.destroy();\n }\n });\n nodeStream.pipe(res);\n } else {\n res.end();\n }\n } catch (error) {\n this._handleApiError(res, error, `${label} failed`);\n }\n }\n\n private async _handleExists(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:exists`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Exists check failed\", plugin: this.name });\n return;\n }\n res.json({ exists: result });\n } catch (error) {\n this._handleApiError(res, error, \"Exists check failed\");\n }\n }\n\n private async _handleMetadata(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:metadata`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Metadata fetch failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Metadata fetch failed\");\n }\n }\n\n private async _handlePreview(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:preview`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Preview failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Preview failed\");\n }\n }\n\n private async _handleUpload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const volumeCfg = this.volumeConfigs[volumeKey];\n const maxSize = volumeCfg.maxUploadSize ?? FILES_MAX_UPLOAD_SIZE;\n const rawContentLength = req.headers[\"content-length\"];\n const contentLength = rawContentLength\n ? parseInt(rawContentLength, 10)\n : undefined;\n\n if (\n contentLength !== undefined &&\n !Number.isNaN(contentLength) &&\n contentLength > maxSize\n ) {\n res.status(413).json({\n error: `File size (${contentLength} bytes) exceeds maximum allowed size (${maxSize} bytes).`,\n plugin: this.name,\n });\n return;\n }\n\n logger.debug(req, \"Upload started: volume=%s path=%s\", volumeKey, path);\n\n try {\n const rawStream: ReadableStream<Uint8Array> = Readable.toWeb(req);\n\n let bytesReceived = 0;\n const webStream = rawStream.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesReceived += chunk.byteLength;\n if (bytesReceived > maxSize) {\n controller.error(\n new Error(\n `Upload stream exceeds maximum allowed size (${maxSize} bytes)`,\n ),\n );\n return;\n }\n controller.enqueue(chunk);\n },\n }),\n );\n\n logger.debug(\n req,\n \"Upload body received: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `upload`);\n await connector.upload(getWorkspaceClient(), path, webStream);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n logger.error(\n req,\n \"Upload failed: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n res.status(500).json({ error: \"Upload failed\", plugin: this.name });\n return;\n }\n\n logger.debug(req, \"Upload complete: volume=%s path=%s\", volumeKey, path);\n res.json(result);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"exceeds maximum allowed size\")\n ) {\n res.status(413).json({ error: error.message, plugin: this.name });\n return;\n }\n this._handleApiError(res, error, \"Upload failed\");\n }\n }\n\n private async _handleMkdir(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const dirPath =\n typeof req.body?.path === \"string\" ? req.body.path : undefined;\n const valid = this._isValidPath(dirPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `createDirectory`);\n await connector.createDirectory(getWorkspaceClient(), dirPath);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n dirPath,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Create directory failed\", plugin: this.name });\n return;\n }\n\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Create directory failed\");\n }\n }\n\n private async _handleDelete(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const rawPath = req.query.path as string | undefined;\n const valid = this._isValidPath(rawPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n const path = rawPath as string;\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `delete`);\n await connector.delete(getWorkspaceClient(), path);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Delete failed\", plugin: this.name });\n return;\n }\n\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Delete failed\");\n }\n }\n\n private inflightWrites = 0;\n\n private trackWrite<T>(fn: () => Promise<T>): Promise<T> {\n this.inflightWrites++;\n return fn().finally(() => {\n this.inflightWrites--;\n });\n }\n\n async shutdown(): Promise<void> {\n // Wait up to 10 seconds for in-flight write operations to finish\n const deadline = Date.now() + 10_000;\n while (this.inflightWrites > 0 && Date.now() < deadline) {\n logger.info(\n \"Waiting for %d in-flight write(s) to complete before shutdown…\",\n this.inflightWrites,\n );\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (this.inflightWrites > 0) {\n logger.warn(\n \"Shutdown deadline reached with %d in-flight write(s) still pending.\",\n this.inflightWrites,\n );\n }\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the programmatic API for the Files plugin.\n * Callable with a volume key to get a volume-scoped handle.\n *\n * @example\n * ```ts\n * // OBO access (recommended)\n * appKit.files(\"uploads\").asUser(req).list()\n *\n * // Service principal access (logs a warning)\n * appKit.files(\"uploads\").list()\n * ```\n */\n exports(): FilesExport {\n const resolveVolume = (volumeKey: string): VolumeHandle => {\n if (!this.volumeKeys.includes(volumeKey)) {\n throw new Error(\n `Unknown volume \"${volumeKey}\". Available volumes: ${this.volumeKeys.join(\", \")}`,\n );\n }\n\n // Service principal API — each method logs a warning recommending OBO\n const spApi = this.createVolumeAPI(volumeKey);\n\n return {\n ...spApi,\n asUser: (req: import(\"express\").Request) => {\n const userPlugin = this.asUser(req) as FilesPlugin;\n return userPlugin.createVolumeAPI(volumeKey);\n },\n };\n };\n\n const filesExport = ((volumeKey: string) =>\n resolveVolume(volumeKey)) as FilesExport;\n filesExport.volume = resolveVolume;\n\n return filesExport;\n }\n\n clientConfig(): Record<string, unknown> {\n return { volumes: this.volumeKeys };\n }\n}\n\n/**\n * @internal\n */\nexport const files = toPlugin(FilesPlugin);\n"],"mappings":"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"}
@@ -21,6 +21,7 @@ declare class GeniePlugin extends Plugin {
21
21
  injectRoutes(router: IAppRouter): void;
22
22
  _handleSendMessage(req: express.Request, res: express.Response): Promise<void>;
23
23
  _handleGetConversation(req: express.Request, res: express.Response): Promise<void>;
24
+ _handleGetMessage(req: express.Request, res: express.Response): Promise<void>;
24
25
  getConversation(alias: string, conversationId: string): Promise<GenieConversationHistoryResponse>;
25
26
  /**
26
27
  * Send a message and consume events as a stream (message_start, status,
@@ -1 +1 @@
1
- {"version":3,"file":"genie.d.ts","names":[],"sources":["../../../src/plugins/genie/genie.ts"],"mappings":";;;;;;;;;;;;cAmBa,WAAA,SAAoB,MAAA;EAAA,OACxB,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,YAAA;EAAA,iBAET,cAAA;cAEL,MAAA,EAAQ,YAAA;EAAA,QAYZ,aAAA;EAAA,QAKA,cAAA;EAIR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAoBf,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAwDG,sBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAgDG,eAAA,CACJ,KAAA,UACA,cAAA,WACC,OAAA,CAAQ,gCAAA;EA1JS;;;;EA8Kb,WAAA,CACL,KAAA,UACA,OAAA,UACA,cAAA,WACA,OAAA;IAAY,OAAA;EAAA,IACX,cAAA,CAAe,gBAAA;EAgBZ,QAAA,CAAA,GAAY,OAAA;EAIlB,OAAA,CAAA;iCAxBe,OAAA,UACE,cAAA,WACQ,OAAA;MACX,OAAA;IAAA,MACX,cAAA,CAAe,gBAAA;qCA3BH,cAAA,aAEZ,OAAA,CAAQ,gCAAA;EAAA;AAAA;;;;cAwDA,KAAA,EAAK,QAAA,QAAA,WAAA,EAAA,YAAA"}
1
+ {"version":3,"file":"genie.d.ts","names":[],"sources":["../../../src/plugins/genie/genie.ts"],"mappings":";;;;;;;;;;;;cAmBa,WAAA,SAAoB,MAAA;EAAA,OACxB,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,YAAA;EAAA,iBAET,cAAA;cAEL,MAAA,EAAQ,YAAA;EAAA,QAYZ,aAAA;EAAA,QAKA,cAAA;EAIR,YAAA,CAAa,MAAA,EAAQ,UAAA;EA6Bf,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAwDG,sBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAgDG,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAkDG,eAAA,CACJ,KAAA,UACA,cAAA,WACC,OAAA,CAAQ,gCAAA;EAnMU;;;;EAuNd,WAAA,CACL,KAAA,UACA,OAAA,UACA,cAAA,WACA,OAAA;IAAY,OAAA;EAAA,IACX,cAAA,CAAe,gBAAA;EAgBZ,QAAA,CAAA,GAAY,OAAA;EAIlB,OAAA,CAAA;iCAxBe,OAAA,UACE,cAAA,WACQ,OAAA;MACX,OAAA;IAAA,MACX,cAAA,CAAe,gBAAA;qCA3BH,cAAA,aAEZ,OAAA,CAAQ,gCAAA;EAAA;AAAA;;;;cAwDA,KAAA,EAAK,QAAA,QAAA,WAAA,EAAA,YAAA"}
@@ -53,6 +53,14 @@ var GeniePlugin = class extends Plugin {
53
53
  await this.asUser(req)._handleGetConversation(req, res);
54
54
  }
55
55
  });
56
+ this.route(router, {
57
+ name: "getMessage",
58
+ method: "get",
59
+ path: "/:alias/conversations/:conversationId/messages/:messageId",
60
+ handler: async (req, res) => {
61
+ await this.asUser(req)._handleGetMessage(req, res);
62
+ }
63
+ });
56
64
  }
57
65
  async _handleSendMessage(req, res) {
58
66
  const { alias } = req.params;
@@ -81,7 +89,10 @@ var GeniePlugin = class extends Plugin {
81
89
  }
82
90
  };
83
91
  const workspaceClient = getWorkspaceClient();
84
- await this.executeStream(res, () => this.genieConnector.streamSendMessage(workspaceClient, spaceId, content, conversationId, { timeout }), streamSettings);
92
+ await this.executeStream(res, (signal) => this.genieConnector.streamSendMessage(workspaceClient, spaceId, content, conversationId, {
93
+ timeout,
94
+ signal
95
+ }), streamSettings);
85
96
  }
86
97
  async _handleGetConversation(req, res) {
87
98
  const { alias, conversationId } = req.params;
@@ -102,9 +113,37 @@ var GeniePlugin = class extends Plugin {
102
113
  }
103
114
  };
104
115
  const workspaceClient = getWorkspaceClient();
105
- await this.executeStream(res, () => this.genieConnector.streamConversation(workspaceClient, spaceId, conversationId, {
116
+ await this.executeStream(res, (signal) => this.genieConnector.streamConversation(workspaceClient, spaceId, conversationId, {
106
117
  includeQueryResults,
107
- pageToken
118
+ pageToken,
119
+ signal
120
+ }), streamSettings);
121
+ }
122
+ async _handleGetMessage(req, res) {
123
+ const { alias, conversationId, messageId } = req.params;
124
+ const spaceId = this.resolveSpaceId(alias);
125
+ if (!spaceId) {
126
+ res.status(404).json({ error: `Unknown space alias: ${alias}` });
127
+ return;
128
+ }
129
+ const requestId = typeof req.query.requestId === "string" && req.query.requestId || randomUUID();
130
+ logger.debug("Polling message %s in conversation %s from space %s (alias=%s)", messageId, conversationId, spaceId, alias);
131
+ const timeout = this.config.timeout ?? 12e4;
132
+ const streamSettings = {
133
+ ...genieStreamDefaults,
134
+ default: {
135
+ ...genieStreamDefaults.default,
136
+ timeout
137
+ },
138
+ stream: {
139
+ ...genieStreamDefaults.stream,
140
+ streamId: requestId
141
+ }
142
+ };
143
+ const workspaceClient = getWorkspaceClient();
144
+ await this.executeStream(res, (signal) => this.genieConnector.streamGetMessage(workspaceClient, spaceId, conversationId, messageId, {
145
+ timeout,
146
+ signal
108
147
  }), streamSettings);
109
148
  }
110
149
  async getConversation(alias, conversationId) {
@@ -1 +1 @@
1
- {"version":3,"file":"genie.js","names":["manifest"],"sources":["../../../src/plugins/genie/genie.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type express from \"express\";\nimport type { IAppRouter, StreamExecutionSettings } from \"shared\";\nimport { GenieConnector } from \"../../connectors\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { genieStreamDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n GenieConversationHistoryResponse,\n GenieSendMessageRequest,\n GenieStreamEvent,\n IGenieConfig,\n} from \"./types\";\n\nconst logger = createLogger(\"genie\");\n\nexport class GeniePlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"genie\">;\n\n protected static description =\n \"AI/BI Genie space integration for natural language data queries\";\n protected declare config: IGenieConfig;\n\n private readonly genieConnector: GenieConnector;\n\n constructor(config: IGenieConfig) {\n super(config);\n this.config = {\n ...config,\n spaces: config.spaces ?? this.defaultSpaces(),\n };\n this.genieConnector = new GenieConnector({\n timeout: this.config.timeout,\n maxMessages: 200,\n });\n }\n\n private defaultSpaces(): Record<string, string> {\n const spaceId = process.env.DATABRICKS_GENIE_SPACE_ID;\n return spaceId ? { default: spaceId } : {};\n }\n\n private resolveSpaceId(alias: string): string | null {\n return this.config.spaces?.[alias] ?? null;\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"sendMessage\",\n method: \"post\",\n path: \"/:alias/messages\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleSendMessage(req, res);\n },\n });\n\n this.route(router, {\n name: \"getConversation\",\n method: \"get\",\n path: \"/:alias/conversations/:conversationId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleGetConversation(req, res);\n },\n });\n }\n\n async _handleSendMessage(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const spaceId = this.resolveSpaceId(alias);\n\n if (!spaceId) {\n res.status(404).json({ error: `Unknown space alias: ${alias}` });\n return;\n }\n\n const { content, conversationId } = req.body as GenieSendMessageRequest;\n\n if (!content) {\n res.status(400).json({ error: \"content is required\" });\n return;\n }\n\n logger.debug(\n \"Sending message to space %s (alias=%s, conversationId=%s)\",\n spaceId,\n alias,\n conversationId ?? \"new\",\n );\n\n const timeout = this.config.timeout ?? 120_000;\n const requestId =\n (typeof req.query.requestId === \"string\" && req.query.requestId) ||\n randomUUID();\n\n const streamSettings: StreamExecutionSettings = {\n ...genieStreamDefaults,\n default: {\n ...genieStreamDefaults.default,\n timeout,\n },\n stream: {\n ...genieStreamDefaults.stream,\n streamId: requestId,\n },\n };\n\n const workspaceClient = getWorkspaceClient();\n\n await this.executeStream<GenieStreamEvent>(\n res,\n () =>\n this.genieConnector.streamSendMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n { timeout },\n ),\n streamSettings,\n );\n }\n\n async _handleGetConversation(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias, conversationId } = req.params;\n const spaceId = this.resolveSpaceId(alias);\n\n if (!spaceId) {\n res.status(404).json({ error: `Unknown space alias: ${alias}` });\n return;\n }\n\n const includeQueryResults = req.query.includeQueryResults !== \"false\";\n const pageToken =\n typeof req.query.pageToken === \"string\" ? req.query.pageToken : undefined;\n const requestId =\n (typeof req.query.requestId === \"string\" && req.query.requestId) ||\n randomUUID();\n\n logger.debug(\n \"Fetching conversation %s from space %s (alias=%s, includeQueryResults=%s, pageToken=%s)\",\n conversationId,\n spaceId,\n alias,\n includeQueryResults,\n pageToken ?? \"none\",\n );\n\n const streamSettings: StreamExecutionSettings = {\n ...genieStreamDefaults,\n stream: {\n ...genieStreamDefaults.stream,\n streamId: requestId,\n },\n };\n\n const workspaceClient = getWorkspaceClient();\n\n await this.executeStream<GenieStreamEvent>(\n res,\n () =>\n this.genieConnector.streamConversation(\n workspaceClient,\n spaceId,\n conversationId,\n { includeQueryResults, pageToken },\n ),\n streamSettings,\n );\n }\n\n async getConversation(\n alias: string,\n conversationId: string,\n ): Promise<GenieConversationHistoryResponse> {\n const spaceId = this.resolveSpaceId(alias);\n\n if (!spaceId) {\n throw new Error(`Unknown space alias: ${alias}`);\n }\n\n const workspaceClient = getWorkspaceClient();\n\n return this.genieConnector.getConversation(\n workspaceClient,\n spaceId,\n conversationId,\n );\n }\n\n /**\n * Send a message and consume events as a stream (message_start, status,\n * message_result, query_result, error).\n */\n async *sendMessage(\n alias: string,\n content: string,\n conversationId?: string,\n options?: { timeout?: number },\n ): AsyncGenerator<GenieStreamEvent> {\n const spaceId = this.resolveSpaceId(alias);\n if (!spaceId) {\n throw new Error(`Unknown space alias: ${alias}`);\n }\n const workspaceClient = getWorkspaceClient();\n const timeout = options?.timeout ?? this.config.timeout ?? 120_000;\n yield* this.genieConnector.streamSendMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n { timeout },\n );\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n exports() {\n return {\n sendMessage: this.sendMessage,\n getConversation: this.getConversation,\n };\n }\n}\n\n/**\n * @internal\n */\nexport const genie = toPlugin(GeniePlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"genie.js","names":["manifest"],"sources":["../../../src/plugins/genie/genie.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type express from \"express\";\nimport type { IAppRouter, StreamExecutionSettings } from \"shared\";\nimport { GenieConnector } from \"../../connectors\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { genieStreamDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n GenieConversationHistoryResponse,\n GenieSendMessageRequest,\n GenieStreamEvent,\n IGenieConfig,\n} from \"./types\";\n\nconst logger = createLogger(\"genie\");\n\nexport class GeniePlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"genie\">;\n\n protected static description =\n \"AI/BI Genie space integration for natural language data queries\";\n protected declare config: IGenieConfig;\n\n private readonly genieConnector: GenieConnector;\n\n constructor(config: IGenieConfig) {\n super(config);\n this.config = {\n ...config,\n spaces: config.spaces ?? this.defaultSpaces(),\n };\n this.genieConnector = new GenieConnector({\n timeout: this.config.timeout,\n maxMessages: 200,\n });\n }\n\n private defaultSpaces(): Record<string, string> {\n const spaceId = process.env.DATABRICKS_GENIE_SPACE_ID;\n return spaceId ? { default: spaceId } : {};\n }\n\n private resolveSpaceId(alias: string): string | null {\n return this.config.spaces?.[alias] ?? null;\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"sendMessage\",\n method: \"post\",\n path: \"/:alias/messages\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleSendMessage(req, res);\n },\n });\n\n this.route(router, {\n name: \"getConversation\",\n method: \"get\",\n path: \"/:alias/conversations/:conversationId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleGetConversation(req, res);\n },\n });\n\n this.route(router, {\n name: \"getMessage\",\n method: \"get\",\n path: \"/:alias/conversations/:conversationId/messages/:messageId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleGetMessage(req, res);\n },\n });\n }\n\n async _handleSendMessage(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const spaceId = this.resolveSpaceId(alias);\n\n if (!spaceId) {\n res.status(404).json({ error: `Unknown space alias: ${alias}` });\n return;\n }\n\n const { content, conversationId } = req.body as GenieSendMessageRequest;\n\n if (!content) {\n res.status(400).json({ error: \"content is required\" });\n return;\n }\n\n logger.debug(\n \"Sending message to space %s (alias=%s, conversationId=%s)\",\n spaceId,\n alias,\n conversationId ?? \"new\",\n );\n\n const timeout = this.config.timeout ?? 120_000;\n const requestId =\n (typeof req.query.requestId === \"string\" && req.query.requestId) ||\n randomUUID();\n\n const streamSettings: StreamExecutionSettings = {\n ...genieStreamDefaults,\n default: {\n ...genieStreamDefaults.default,\n timeout,\n },\n stream: {\n ...genieStreamDefaults.stream,\n streamId: requestId,\n },\n };\n\n const workspaceClient = getWorkspaceClient();\n\n await this.executeStream<GenieStreamEvent>(\n res,\n (signal) =>\n this.genieConnector.streamSendMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n { timeout, signal },\n ),\n streamSettings,\n );\n }\n\n async _handleGetConversation(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias, conversationId } = req.params;\n const spaceId = this.resolveSpaceId(alias);\n\n if (!spaceId) {\n res.status(404).json({ error: `Unknown space alias: ${alias}` });\n return;\n }\n\n const includeQueryResults = req.query.includeQueryResults !== \"false\";\n const pageToken =\n typeof req.query.pageToken === \"string\" ? req.query.pageToken : undefined;\n const requestId =\n (typeof req.query.requestId === \"string\" && req.query.requestId) ||\n randomUUID();\n\n logger.debug(\n \"Fetching conversation %s from space %s (alias=%s, includeQueryResults=%s, pageToken=%s)\",\n conversationId,\n spaceId,\n alias,\n includeQueryResults,\n pageToken ?? \"none\",\n );\n\n const streamSettings: StreamExecutionSettings = {\n ...genieStreamDefaults,\n stream: {\n ...genieStreamDefaults.stream,\n streamId: requestId,\n },\n };\n\n const workspaceClient = getWorkspaceClient();\n\n await this.executeStream<GenieStreamEvent>(\n res,\n (signal) =>\n this.genieConnector.streamConversation(\n workspaceClient,\n spaceId,\n conversationId,\n { includeQueryResults, pageToken, signal },\n ),\n streamSettings,\n );\n }\n\n async _handleGetMessage(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias, conversationId, messageId } = req.params;\n const spaceId = this.resolveSpaceId(alias);\n\n if (!spaceId) {\n res.status(404).json({ error: `Unknown space alias: ${alias}` });\n return;\n }\n\n const requestId =\n (typeof req.query.requestId === \"string\" && req.query.requestId) ||\n randomUUID();\n\n logger.debug(\n \"Polling message %s in conversation %s from space %s (alias=%s)\",\n messageId,\n conversationId,\n spaceId,\n alias,\n );\n\n const timeout = this.config.timeout ?? 120_000;\n const streamSettings: StreamExecutionSettings = {\n ...genieStreamDefaults,\n default: {\n ...genieStreamDefaults.default,\n timeout,\n },\n stream: {\n ...genieStreamDefaults.stream,\n streamId: requestId,\n },\n };\n\n const workspaceClient = getWorkspaceClient();\n\n await this.executeStream<GenieStreamEvent>(\n res,\n (signal) =>\n this.genieConnector.streamGetMessage(\n workspaceClient,\n spaceId,\n conversationId,\n messageId,\n { timeout, signal },\n ),\n streamSettings,\n );\n }\n\n async getConversation(\n alias: string,\n conversationId: string,\n ): Promise<GenieConversationHistoryResponse> {\n const spaceId = this.resolveSpaceId(alias);\n\n if (!spaceId) {\n throw new Error(`Unknown space alias: ${alias}`);\n }\n\n const workspaceClient = getWorkspaceClient();\n\n return this.genieConnector.getConversation(\n workspaceClient,\n spaceId,\n conversationId,\n );\n }\n\n /**\n * Send a message and consume events as a stream (message_start, status,\n * message_result, query_result, error).\n */\n async *sendMessage(\n alias: string,\n content: string,\n conversationId?: string,\n options?: { timeout?: number },\n ): AsyncGenerator<GenieStreamEvent> {\n const spaceId = this.resolveSpaceId(alias);\n if (!spaceId) {\n throw new Error(`Unknown space alias: ${alias}`);\n }\n const workspaceClient = getWorkspaceClient();\n const timeout = options?.timeout ?? this.config.timeout ?? 120_000;\n yield* this.genieConnector.streamSendMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n { timeout },\n );\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n exports() {\n return {\n sendMessage: this.sendMessage,\n getConversation: this.getConversation,\n };\n }\n}\n\n/**\n * @internal\n */\nexport const genie = toPlugin(GeniePlugin);\n"],"mappings":"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"}
@@ -11,13 +11,15 @@ import { getConfigScript } from "./utils.js";
11
11
  var BaseServer = class {
12
12
  app;
13
13
  endpoints;
14
- constructor(app, endpoints = {}) {
14
+ pluginConfigs;
15
+ constructor(app, endpoints = {}, pluginConfigs = {}) {
15
16
  this.app = app;
16
17
  this.endpoints = endpoints;
18
+ this.pluginConfigs = pluginConfigs;
17
19
  }
18
20
  async close() {}
19
21
  getConfigScript() {
20
- return getConfigScript(this.endpoints);
22
+ return getConfigScript(this.endpoints, this.pluginConfigs);
21
23
  }
22
24
  };
23
25
 
@@ -1 +1 @@
1
- {"version":3,"file":"base-server.js","names":[],"sources":["../../../src/plugins/server/base-server.ts"],"sourcesContent":["import type express from \"express\";\nimport { getConfigScript, type PluginEndpoints } from \"./utils\";\n\n/**\n * Base server for the AppKit.\n *\n * Abstract base class that provides common functionality for serving\n * frontend applications. Subclasses implement specific serving strategies\n * (Vite dev server, static file server, etc.).\n */\nexport abstract class BaseServer {\n protected app: express.Application;\n protected endpoints: PluginEndpoints;\n\n constructor(app: express.Application, endpoints: PluginEndpoints = {}) {\n this.app = app;\n this.endpoints = endpoints;\n }\n\n abstract setup(): void | Promise<void>;\n\n async close(): Promise<void> {}\n\n protected getConfigScript(): string {\n return getConfigScript(this.endpoints);\n }\n}\n"],"mappings":";;;;;;;;;;AAUA,IAAsB,aAAtB,MAAiC;CAC/B,AAAU;CACV,AAAU;CAEV,YAAY,KAA0B,YAA6B,EAAE,EAAE;AACrE,OAAK,MAAM;AACX,OAAK,YAAY;;CAKnB,MAAM,QAAuB;CAE7B,AAAU,kBAA0B;AAClC,SAAO,gBAAgB,KAAK,UAAU"}
1
+ {"version":3,"file":"base-server.js","names":[],"sources":["../../../src/plugins/server/base-server.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n getConfigScript,\n type PluginClientConfigs,\n type PluginEndpoints,\n} from \"./utils\";\n\n/**\n * Base server for the AppKit.\n *\n * Abstract base class that provides common functionality for serving\n * frontend applications. Subclasses implement specific serving strategies\n * (Vite dev server, static file server, etc.).\n */\nexport abstract class BaseServer {\n protected app: express.Application;\n protected endpoints: PluginEndpoints;\n protected pluginConfigs: PluginClientConfigs;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n this.app = app;\n this.endpoints = endpoints;\n this.pluginConfigs = pluginConfigs;\n }\n\n abstract setup(): void | Promise<void>;\n\n async close(): Promise<void> {}\n\n protected getConfigScript(): string {\n return getConfigScript(this.endpoints, this.pluginConfigs);\n }\n}\n"],"mappings":";;;;;;;;;;AAcA,IAAsB,aAAtB,MAAiC;CAC/B,AAAU;CACV,AAAU;CACV,AAAU;CAEV,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,OAAK,MAAM;AACX,OAAK,YAAY;AACjB,OAAK,gBAAgB;;CAKvB,MAAM,QAAuB;CAE7B,AAAU,kBAA0B;AAClC,SAAO,gBAAgB,KAAK,WAAW,KAAK,cAAc"}
@@ -0,0 +1,184 @@
1
+ import { createLogger } from "../../logging/logger.js";
2
+ import pc from "picocolors";
3
+
4
+ //#region src/plugins/server/client-config-sanitizer.ts
5
+ const logger = createLogger("server:config");
6
+ const REDACTED_CLIENT_CONFIG_VALUE = "[redacted by appkit]";
7
+ const MIN_SUBSTRING_LENGTH = 3;
8
+ const DISALLOWED_CLIENT_CONFIG_KEYS = new Set([
9
+ "__proto__",
10
+ "constructor",
11
+ "prototype"
12
+ ]);
13
+ /**
14
+ * Builds a Map of non-public env var values (value -> key names)
15
+ * and a Set of public env var values for overlap resolution.
16
+ */
17
+ function getEnvValueSets() {
18
+ const nonPublic = /* @__PURE__ */ new Map();
19
+ const publicValues = /* @__PURE__ */ new Set();
20
+ for (const [key, value] of Object.entries(process.env)) {
21
+ if (!value) continue;
22
+ if (key.startsWith("PUBLIC_APPKIT_")) publicValues.add(value);
23
+ else {
24
+ const existing = nonPublic.get(value);
25
+ if (existing) existing.push(key);
26
+ else nonPublic.set(value, [key]);
27
+ }
28
+ }
29
+ return {
30
+ nonPublic,
31
+ publicValues
32
+ };
33
+ }
34
+ function getMatchRanges(haystack, needle) {
35
+ const ranges = [];
36
+ let startIndex = 0;
37
+ while (startIndex < haystack.length) {
38
+ const matchIndex = haystack.indexOf(needle, startIndex);
39
+ if (matchIndex === -1) break;
40
+ ranges.push([matchIndex, matchIndex + needle.length]);
41
+ startIndex = matchIndex + 1;
42
+ }
43
+ return ranges;
44
+ }
45
+ function isSecretCoveredByPublicValue(value, envValue, publicValues) {
46
+ const publicRanges = [...publicValues].filter((publicValue) => publicValue.includes(envValue)).flatMap((publicValue) => getMatchRanges(value, publicValue));
47
+ if (publicRanges.length === 0) return false;
48
+ return getMatchRanges(value, envValue).every(([secretStart, secretEnd]) => publicRanges.some(([publicStart, publicEnd]) => publicStart <= secretStart && publicEnd >= secretEnd));
49
+ }
50
+ function isPlainObject(value) {
51
+ const proto = Object.getPrototypeOf(value);
52
+ return proto === Object.prototype || proto === null;
53
+ }
54
+ function invalidClientConfig(pluginName, path, message) {
55
+ return /* @__PURE__ */ new Error(`Plugin '${pluginName}' clientConfig() ${message} at ${path}. Only JSON-serializable plain data is supported.`);
56
+ }
57
+ function assertSafeClientConfigKey(pluginName, key, path) {
58
+ if (DISALLOWED_CLIENT_CONFIG_KEYS.has(key)) throw invalidClientConfig(pluginName, `${path}.${key}`, "contains a reserved key");
59
+ }
60
+ function validateClientConfigValue(pluginName, value, path, stack) {
61
+ if (value === null) return null;
62
+ switch (typeof value) {
63
+ case "string":
64
+ case "boolean": return value;
65
+ case "number":
66
+ if (!Number.isFinite(value)) throw invalidClientConfig(pluginName, path, "contains a non-finite number");
67
+ return value;
68
+ case "bigint": throw invalidClientConfig(pluginName, path, "contains a BigInt");
69
+ case "undefined": return;
70
+ case "function": throw invalidClientConfig(pluginName, path, "contains a function");
71
+ case "symbol": throw invalidClientConfig(pluginName, path, "contains a symbol");
72
+ }
73
+ if (Array.isArray(value)) {
74
+ if (stack.has(value)) throw invalidClientConfig(pluginName, path, "contains a circular reference");
75
+ stack.add(value);
76
+ const result = value.map((item, index) => validateClientConfigValue(pluginName, item, `${path}[${index}]`, stack) ?? null);
77
+ stack.delete(value);
78
+ return result;
79
+ }
80
+ if (typeof value === "object") {
81
+ if (!isPlainObject(value)) throw invalidClientConfig(pluginName, path, "contains a non-plain object");
82
+ if (stack.has(value)) throw invalidClientConfig(pluginName, path, "contains a circular reference");
83
+ stack.add(value);
84
+ const result = {};
85
+ for (const [key, nestedValue] of Object.entries(value)) {
86
+ assertSafeClientConfigKey(pluginName, key, path);
87
+ const normalizedValue = validateClientConfigValue(pluginName, nestedValue, `${path}.${key}`, stack);
88
+ if (normalizedValue !== void 0) result[key] = normalizedValue;
89
+ }
90
+ stack.delete(value);
91
+ return result;
92
+ }
93
+ throw invalidClientConfig(pluginName, path, "contains an unsupported value");
94
+ }
95
+ function validateClientConfig(pluginName, config) {
96
+ if (config === null || typeof config !== "object" || Array.isArray(config)) throw new Error(`Plugin '${pluginName}' clientConfig() must return a plain object.`);
97
+ return validateClientConfigValue(pluginName, config, "clientConfig()", /* @__PURE__ */ new WeakSet());
98
+ }
99
+ /**
100
+ * Redacts a string when it contains a non-public env var value. Exact matches
101
+ * are caught regardless of length; substring containment requires the env value
102
+ * to be at least MIN_SUBSTRING_LENGTH chars to avoid false positives from very
103
+ * short values.
104
+ */
105
+ function redactLeakedString(value, nonPublicValues, publicValues, leakedVars) {
106
+ for (const [envValue, envKeys] of nonPublicValues) {
107
+ if (value === envValue && !publicValues.has(envValue)) {
108
+ for (const k of envKeys) leakedVars.add(k);
109
+ return REDACTED_CLIENT_CONFIG_VALUE;
110
+ }
111
+ if (envValue.length >= MIN_SUBSTRING_LENGTH && value.includes(envValue) && !isSecretCoveredByPublicValue(value, envValue, publicValues)) {
112
+ for (const k of envKeys) leakedVars.add(k);
113
+ return REDACTED_CLIENT_CONFIG_VALUE;
114
+ }
115
+ }
116
+ return value;
117
+ }
118
+ function redactLeakedValues(obj, nonPublicValues, publicValues, leakedVars) {
119
+ if (typeof obj === "string") return redactLeakedString(obj, nonPublicValues, publicValues, leakedVars);
120
+ if (Array.isArray(obj)) return obj.map((item) => redactLeakedValues(item, nonPublicValues, publicValues, leakedVars));
121
+ if (obj !== null && typeof obj === "object") {
122
+ const result = {};
123
+ for (const [key, value] of Object.entries(obj)) {
124
+ const uniqueKey = getUniqueObjectKey(redactLeakedString(key, nonPublicValues, publicValues, leakedVars), result);
125
+ result[uniqueKey] = redactLeakedValues(value, nonPublicValues, publicValues, leakedVars);
126
+ }
127
+ return result;
128
+ }
129
+ return obj;
130
+ }
131
+ function getUniqueObjectKey(key, result) {
132
+ if (!Object.hasOwn(result, key)) return key;
133
+ let suffix = 2;
134
+ let candidate = `${key} (${suffix})`;
135
+ while (Object.hasOwn(result, candidate)) {
136
+ suffix += 1;
137
+ candidate = `${key} (${suffix})`;
138
+ }
139
+ return candidate;
140
+ }
141
+ /**
142
+ * Scans a plugin's clientConfig return value for string values that
143
+ * match or contain non-public environment variable values. Matches are
144
+ * replaced with "[redacted by appkit]" and a warning is logged.
145
+ *
146
+ * Only env vars prefixed with `PUBLIC_APPKIT_` are allowed through;
147
+ * all other process.env values are treated as sensitive.
148
+ */
149
+ function sanitizeClientConfig(pluginName, config) {
150
+ const validated = validateClientConfig(pluginName, config);
151
+ const { nonPublic, publicValues } = getEnvValueSets();
152
+ if (nonPublic.size === 0) return validated;
153
+ const leakedVars = /* @__PURE__ */ new Set();
154
+ const sanitized = redactLeakedValues(validated, nonPublic, publicValues, leakedVars);
155
+ if (leakedVars.size > 0) {
156
+ const banner = formatLeakedVarsBanner(pluginName, leakedVars);
157
+ logger.warn("\n\n%s\n", banner);
158
+ }
159
+ return sanitized;
160
+ }
161
+ function formatLeakedVarsBanner(pluginName, leakedVars) {
162
+ const s = leakedVars.size === 1 ? "" : "s";
163
+ const contentLines = [
164
+ `${pc.bold(pluginName)}.clientConfig() contained ${pc.bold(String(leakedVars.size))} env var value${s}`,
165
+ `that would have been sent to the browser. AppKit ${pc.green("redacted")} them automatically.`,
166
+ "",
167
+ ...Array.from(leakedVars, (v) => ` ${pc.red("-")} ${pc.yellow(v)}`),
168
+ "",
169
+ `To intentionally expose a value, set a matching ${pc.green("PUBLIC_APPKIT_")} variable.`,
170
+ `Example: ${pc.dim("PUBLIC_APPKIT_MY_VAR=\"safe-value\"")}`
171
+ ];
172
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, "");
173
+ const maxLen = Math.max(...contentLines.map((l) => stripAnsi(l).length));
174
+ const border = pc.yellow("=".repeat(maxLen + 4));
175
+ return [
176
+ border,
177
+ ...contentLines.map((line) => `${pc.yellow("|")} ${line}${" ".repeat(maxLen - stripAnsi(line).length)} ${pc.yellow("|")}`),
178
+ border
179
+ ].join("\n");
180
+ }
181
+
182
+ //#endregion
183
+ export { sanitizeClientConfig };
184
+ //# sourceMappingURL=client-config-sanitizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-config-sanitizer.js","names":[],"sources":["../../../src/plugins/server/client-config-sanitizer.ts"],"sourcesContent":["import pc from \"picocolors\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"server:config\");\n\nconst REDACTED_CLIENT_CONFIG_VALUE = \"[redacted by appkit]\";\nconst MIN_SUBSTRING_LENGTH = 3;\nconst DISALLOWED_CLIENT_CONFIG_KEYS = new Set([\n \"__proto__\",\n \"constructor\",\n \"prototype\",\n]);\n\n/**\n * Builds a Map of non-public env var values (value -> key names)\n * and a Set of public env var values for overlap resolution.\n */\nfunction getEnvValueSets(): {\n nonPublic: Map<string, string[]>;\n publicValues: Set<string>;\n} {\n const nonPublic = new Map<string, string[]>();\n const publicValues = new Set<string>();\n for (const [key, value] of Object.entries(process.env)) {\n if (!value) continue;\n if (key.startsWith(\"PUBLIC_APPKIT_\")) {\n publicValues.add(value);\n } else {\n const existing = nonPublic.get(value);\n if (existing) {\n existing.push(key);\n } else {\n nonPublic.set(value, [key]);\n }\n }\n }\n return { nonPublic, publicValues };\n}\n\nfunction getMatchRanges(\n haystack: string,\n needle: string,\n): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n let startIndex = 0;\n\n while (startIndex < haystack.length) {\n const matchIndex = haystack.indexOf(needle, startIndex);\n if (matchIndex === -1) {\n break;\n }\n ranges.push([matchIndex, matchIndex + needle.length]);\n startIndex = matchIndex + 1;\n }\n\n return ranges;\n}\n\nfunction isSecretCoveredByPublicValue(\n value: string,\n envValue: string,\n publicValues: Set<string>,\n): boolean {\n const publicRanges = [...publicValues]\n .filter((publicValue) => publicValue.includes(envValue))\n .flatMap((publicValue) => getMatchRanges(value, publicValue));\n\n if (publicRanges.length === 0) {\n return false;\n }\n\n return getMatchRanges(value, envValue).every(([secretStart, secretEnd]) =>\n publicRanges.some(\n ([publicStart, publicEnd]) =>\n publicStart <= secretStart && publicEnd >= secretEnd,\n ),\n );\n}\n\nfunction isPlainObject(value: object): boolean {\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction invalidClientConfig(\n pluginName: string,\n path: string,\n message: string,\n): Error {\n return new Error(\n `Plugin '${pluginName}' clientConfig() ${message} at ${path}. Only JSON-serializable plain data is supported.`,\n );\n}\n\nfunction assertSafeClientConfigKey(\n pluginName: string,\n key: string,\n path: string,\n): void {\n if (DISALLOWED_CLIENT_CONFIG_KEYS.has(key)) {\n throw invalidClientConfig(\n pluginName,\n `${path}.${key}`,\n \"contains a reserved key\",\n );\n }\n}\n\nfunction validateClientConfigValue(\n pluginName: string,\n value: unknown,\n path: string,\n stack: WeakSet<object>,\n): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case \"string\":\n case \"boolean\":\n return value;\n case \"number\":\n if (!Number.isFinite(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-finite number\",\n );\n }\n return value;\n case \"bigint\":\n throw invalidClientConfig(pluginName, path, \"contains a BigInt\");\n case \"undefined\":\n return undefined;\n case \"function\":\n throw invalidClientConfig(pluginName, path, \"contains a function\");\n case \"symbol\":\n throw invalidClientConfig(pluginName, path, \"contains a symbol\");\n }\n\n if (Array.isArray(value)) {\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result = value.map(\n (item, index) =>\n validateClientConfigValue(\n pluginName,\n item,\n `${path}[${index}]`,\n stack,\n ) ?? null,\n );\n stack.delete(value);\n return result;\n }\n\n if (typeof value === \"object\") {\n if (!isPlainObject(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-plain object\",\n );\n }\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result: Record<string, unknown> = {};\n for (const [key, nestedValue] of Object.entries(value)) {\n assertSafeClientConfigKey(pluginName, key, path);\n const normalizedValue = validateClientConfigValue(\n pluginName,\n nestedValue,\n `${path}.${key}`,\n stack,\n );\n if (normalizedValue !== undefined) {\n result[key] = normalizedValue;\n }\n }\n stack.delete(value);\n return result;\n }\n\n throw invalidClientConfig(pluginName, path, \"contains an unsupported value\");\n}\n\nfunction validateClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n if (config === null || typeof config !== \"object\" || Array.isArray(config)) {\n throw new Error(\n `Plugin '${pluginName}' clientConfig() must return a plain object.`,\n );\n }\n\n return validateClientConfigValue(\n pluginName,\n config,\n \"clientConfig()\",\n new WeakSet(),\n ) as Record<string, unknown>;\n}\n\n/**\n * Redacts a string when it contains a non-public env var value. Exact matches\n * are caught regardless of length; substring containment requires the env value\n * to be at least MIN_SUBSTRING_LENGTH chars to avoid false positives from very\n * short values.\n */\nfunction redactLeakedString(\n value: string,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): string {\n for (const [envValue, envKeys] of nonPublicValues) {\n if (value === envValue && !publicValues.has(envValue)) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n if (\n envValue.length >= MIN_SUBSTRING_LENGTH &&\n value.includes(envValue) &&\n !isSecretCoveredByPublicValue(value, envValue, publicValues)\n ) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n }\n\n return value;\n}\n\nfunction redactLeakedValues(\n obj: unknown,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): unknown {\n if (typeof obj === \"string\") {\n return redactLeakedString(obj, nonPublicValues, publicValues, leakedVars);\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) =>\n redactLeakedValues(item, nonPublicValues, publicValues, leakedVars),\n );\n }\n\n if (obj !== null && typeof obj === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const redactedKey = redactLeakedString(\n key,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n const uniqueKey = getUniqueObjectKey(redactedKey, result);\n result[uniqueKey] = redactLeakedValues(\n value,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n }\n return result;\n }\n\n return obj;\n}\n\nfunction getUniqueObjectKey(\n key: string,\n result: Record<string, unknown>,\n): string {\n if (!Object.hasOwn(result, key)) {\n return key;\n }\n\n let suffix = 2;\n let candidate = `${key} (${suffix})`;\n while (Object.hasOwn(result, candidate)) {\n suffix += 1;\n candidate = `${key} (${suffix})`;\n }\n\n return candidate;\n}\n\n/**\n * Scans a plugin's clientConfig return value for string values that\n * match or contain non-public environment variable values. Matches are\n * replaced with \"[redacted by appkit]\" and a warning is logged.\n *\n * Only env vars prefixed with `PUBLIC_APPKIT_` are allowed through;\n * all other process.env values are treated as sensitive.\n */\nexport function sanitizeClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n const validated = validateClientConfig(pluginName, config);\n const { nonPublic, publicValues } = getEnvValueSets();\n if (nonPublic.size === 0) return validated;\n\n const leakedVars = new Set<string>();\n const sanitized = redactLeakedValues(\n validated,\n nonPublic,\n publicValues,\n leakedVars,\n ) as Record<string, unknown>;\n\n if (leakedVars.size > 0) {\n const banner = formatLeakedVarsBanner(pluginName, leakedVars);\n logger.warn(\"\\n\\n%s\\n\", banner);\n }\n\n return sanitized;\n}\n\nfunction formatLeakedVarsBanner(\n pluginName: string,\n leakedVars: Set<string>,\n): string {\n const s = leakedVars.size === 1 ? \"\" : \"s\";\n const contentLines: string[] = [\n `${pc.bold(pluginName)}.clientConfig() contained ${pc.bold(String(leakedVars.size))} env var value${s}`,\n `that would have been sent to the browser. AppKit ${pc.green(\"redacted\")} them automatically.`,\n \"\",\n ...Array.from(leakedVars, (v) => ` ${pc.red(\"-\")} ${pc.yellow(v)}`),\n \"\",\n `To intentionally expose a value, set a matching ${pc.green(\"PUBLIC_APPKIT_\")} variable.`,\n `Example: ${pc.dim('PUBLIC_APPKIT_MY_VAR=\"safe-value\"')}`,\n ];\n\n // biome-ignore lint: stripping ANSI escape sequences requires matching the ESC control character\n const stripAnsi = (str: string) => str.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n const maxLen = Math.max(...contentLines.map((l) => stripAnsi(l).length));\n const border = pc.yellow(\"=\".repeat(maxLen + 4));\n const boxed = contentLines.map(\n (line) =>\n `${pc.yellow(\"|\")} ${line}${\" \".repeat(maxLen - stripAnsi(line).length)} ${pc.yellow(\"|\")}`,\n );\n\n return [border, ...boxed, border].join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -84,7 +84,8 @@ declare class ServerPlugin extends Plugin {
84
84
  * Setup the routes with the plugins.
85
85
  *
86
86
  * This method goes through all the plugins and injects the routes into the server application.
87
- * Returns a map of plugin names to their registered named endpoints.
87
+ * Returns a map of plugin names to their registered named endpoints,
88
+ * and a map of plugin names to their client-exposed configs.
88
89
  */
89
90
  private extendRoutes;
90
91
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;AAoCA;;;;;;;;;cAAa,YAAA,SAAqB,MAAA;EAAA,OAClB,cAAA;;;;;EAqUO;EAAA,OA9Td,QAAA,EAAuB,cAAA;EAAA,QACtB,iBAAA;EAAA,QACA,MAAA;EAAA,QACA,aAAA;EAAA,QACA,sBAAA;EAAA,UACU,MAAA,EAAQ,YAAA;EAAA,QAClB,gBAAA;EAAA,QACA,YAAA;EAAA,OACD,KAAA,EAAO,WAAA;cAEF,MAAA,EAAQ,YAAA;EAVb;EAuBD,KAAA,CAAA,GAAK,OAAA;EAtBH;EA6BR,SAAA,CAAA;IAAA;;;;;;gBAPW,gBAAA;EAAA;;EAcX,eAAA,CAAA;EA3BY;;;;;;;;EAuCN,KAAA,CAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,WAAA;;;;;;;;;EA2D/B,SAAA,CAAA,GAAa,MAAA;EAAA;;;;;;;EAmBb,MAAA,CAAO,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;EA+FV;;;;;;EAAA,QAhFD,YAAA;EA7FiB;;;;;;EAAA,QA0IjB,aAAA;EAAA,eAmCC,cAAA;EAAA,QAaP,cAAA;EAAA,QA4BM,iBAAA;;;;;EAiDd,OAAA,CAAA;wCAvQe,OAAA,CAAQ,OAAA,CAAQ,WAAA;gBA6Qf,GAAA,EAAK,OAAA,CAAQ,WAAA,2BAiBlB;qBAnOE,MAAA,EAmO6B;;;;;;;;kBAnOnB,gBAAA;IAAA;EAAA;AAAA;;;;cAmOZ,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,YAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;AAqCA;;;;;;;;;cAAa,YAAA,SAAqB,MAAA;EAAA,OAClB,cAAA;;;;;EAoWO;EAAA,OA7Vd,QAAA,EAAuB,cAAA;EAAA,QACtB,iBAAA;EAAA,QACA,MAAA;EAAA,QACA,aAAA;EAAA,QACA,sBAAA;EAAA,UACU,MAAA,EAAQ,YAAA;EAAA,QAClB,gBAAA;EAAA,QACA,YAAA;EAAA,OACD,KAAA,EAAO,WAAA;cAEF,MAAA,EAAQ,YAAA;EAVb;EAuBD,KAAA,CAAA,GAAK,OAAA;EAtBH;EA6BR,SAAA,CAAA;IAAA;;;;;;gBAPW,gBAAA;EAAA;;EAcX,eAAA,CAAA;EA3BY;;;;;;;;EAuCN,KAAA,CAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,WAAA;;;;;;;;;EA2D/B,SAAA,CAAA,GAAa,MAAA;EAAA;;;;;;;EAmBb,MAAA,CAAO,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;EA8HV;;;;;;;EAAA,QA9GD,YAAA;;;;;;;UAkEA,aAAA;EAAA,eA4CC,cAAA;EAAA,QAaP,cAAA;EAAA,QA4BM,iBAAA;;;;;EAiDd,OAAA,CAAA;wCAtSe,OAAA,CAAQ,OAAA,CAAQ,WAAA;gBA4Sf,GAAA,EAAK,OAAA,CAAQ,WAAA,YAiBa,eAAA;qBAlQ7B,MAAA,EAkQI;;;;;;;;kBAlQM,gBAAA;IAAA;EAAA;AAAA;;;;cAkQZ,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,YAAA"}