@databricks/appkit 0.23.0 → 0.25.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 (88) hide show
  1. package/CLAUDE.md +9 -1
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/cache/index.js.map +1 -1
  4. package/dist/cli/commands/docs.js +7 -1
  5. package/dist/cli/commands/docs.js.map +1 -1
  6. package/dist/cli/commands/generate-types.js +20 -10
  7. package/dist/cli/commands/generate-types.js.map +1 -1
  8. package/dist/cli/commands/lint.js +3 -1
  9. package/dist/cli/commands/lint.js.map +1 -1
  10. package/dist/cli/commands/plugin/add-resource/add-resource.js +73 -8
  11. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  12. package/dist/cli/commands/plugin/create/create.js +164 -20
  13. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  14. package/dist/cli/commands/plugin/create/resource-defaults.js +5 -1
  15. package/dist/cli/commands/plugin/create/resource-defaults.js.map +1 -1
  16. package/dist/cli/commands/plugin/index.js +7 -1
  17. package/dist/cli/commands/plugin/index.js.map +1 -1
  18. package/dist/cli/commands/plugin/list/list.js +7 -1
  19. package/dist/cli/commands/plugin/list/list.js.map +1 -1
  20. package/dist/cli/commands/plugin/sync/sync.js +27 -14
  21. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  22. package/dist/cli/commands/plugin/validate/validate.js +39 -9
  23. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  24. package/dist/cli/commands/setup.js +6 -5
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/connectors/index.js +1 -0
  27. package/dist/connectors/lakebase/index.js.map +1 -1
  28. package/dist/connectors/lakebase-v1/client.js.map +1 -1
  29. package/dist/connectors/vector-search/client.js +9 -0
  30. package/dist/connectors/vector-search/client.js.map +1 -0
  31. package/dist/connectors/vector-search/index.js +3 -0
  32. package/dist/context/execution-context.js +1 -7
  33. package/dist/context/execution-context.js.map +1 -1
  34. package/dist/context/index.js +1 -1
  35. package/dist/context/index.js.map +1 -1
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/plugin/dev-reader.js.map +1 -1
  40. package/dist/plugins/files/plugin.d.ts +46 -15
  41. package/dist/plugins/files/plugin.d.ts.map +1 -1
  42. package/dist/plugins/files/plugin.js +182 -103
  43. package/dist/plugins/files/plugin.js.map +1 -1
  44. package/dist/plugins/files/policy.d.ts +45 -0
  45. package/dist/plugins/files/policy.d.ts.map +1 -0
  46. package/dist/plugins/files/policy.js +63 -0
  47. package/dist/plugins/files/policy.js.map +1 -0
  48. package/dist/plugins/files/types.d.ts +16 -8
  49. package/dist/plugins/files/types.d.ts.map +1 -1
  50. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  51. package/dist/plugins/serving/serving.d.ts.map +1 -1
  52. package/dist/plugins/serving/serving.js +22 -8
  53. package/dist/plugins/serving/serving.js.map +1 -1
  54. package/dist/plugins/serving/types.d.ts +11 -10
  55. package/dist/plugins/serving/types.d.ts.map +1 -1
  56. package/dist/type-generator/index.js +13 -1
  57. package/dist/type-generator/index.js.map +1 -1
  58. package/dist/type-generator/migration.js +155 -0
  59. package/dist/type-generator/migration.js.map +1 -0
  60. package/dist/type-generator/serving/generator.js +22 -1
  61. package/dist/type-generator/serving/generator.js.map +1 -1
  62. package/dist/type-generator/serving/vite-plugin.d.ts +1 -1
  63. package/dist/type-generator/serving/vite-plugin.js +2 -2
  64. package/dist/type-generator/serving/vite-plugin.js.map +1 -1
  65. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  66. package/dist/type-generator/vite-plugin.js +3 -4
  67. package/dist/type-generator/vite-plugin.js.map +1 -1
  68. package/docs/api/appkit/Class.PolicyDeniedError.md +52 -0
  69. package/docs/api/appkit/Interface.FilePolicyUser.md +23 -0
  70. package/docs/api/appkit/Interface.FileResource.md +36 -0
  71. package/docs/api/appkit/TypeAlias.FileAction.md +18 -0
  72. package/docs/api/appkit/TypeAlias.FilePolicy.md +20 -0
  73. package/docs/api/appkit/TypeAlias.ServingFactory.md +9 -5
  74. package/docs/api/appkit/Variable.READ_ACTIONS.md +8 -0
  75. package/docs/api/appkit/Variable.WRITE_ACTIONS.md +8 -0
  76. package/docs/api/appkit.md +19 -12
  77. package/docs/development/type-generation.md +6 -5
  78. package/docs/faq.md +8 -8
  79. package/docs/plugins/analytics.md +1 -1
  80. package/docs/plugins/custom-plugins.md +4 -0
  81. package/docs/plugins/execution-context.md +0 -1
  82. package/docs/plugins/files.md +150 -2
  83. package/docs/plugins/{serving.md → model-serving.md} +1 -1
  84. package/docs/plugins/plugin-management.md +22 -6
  85. package/docs/plugins/vector-search.md +247 -0
  86. package/llms.txt +9 -1
  87. package/package.json +1 -1
  88. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":["manifest","files"],"sources":["../../../src/plugins/files/plugin.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport { 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 _sendStatusError(res: express.Response, status: number): void {\n res.status(status).json({\n error: STATUS_CODES[status] ?? \"Unknown Error\",\n plugin: this.name,\n });\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.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\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.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.type(\"text/plain\").send(result.data);\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.ok) {\n this._sendStatusError(res, response.status);\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.data.contents) {\n const nodeStream = Readable.fromWeb(\n response.data.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 this._sendStatusError(res, 500);\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.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ exists: result.data });\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.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\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.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\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.ok) {\n logger.error(\n req,\n \"Upload failed: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n this._sendStatusError(res, result.status);\n return;\n }\n\n logger.debug(req, \"Upload complete: volume=%s path=%s\", volumeKey, path);\n res.json(result.data);\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.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n\n res.json(result.data);\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.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n\n res.json(result.data);\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":";;;;;;;;;;;;;;;;;;;;;cAWoE;aACjB;AAsBnD,MAAM,SAAS,aAAa,QAAQ;AAEpC,IAAa,cAAb,MAAa,oBAAoB,OAAO;CACtC,OAAO;;CAGP,OAAO,WAAWA;CAClB,OAAiB,cAAc;CAG/B,AAAQ,mBAAmD,EAAE;CAC7D,AAAQ,gBAA8C,EAAE;CACxD,AAAQ,aAAuB,EAAE;;;;;;CAOjC,OAAO,gBAAgB,QAAoD;EACzE,MAAM,WAAW,OAAO,WAAW,EAAE;EACrC,MAAM,aAA2C,EAAE;EAEnD,MAAM,SAAS;AACf,OAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,IAAI,EAAE;AAC1C,OAAI,CAAC,IAAI,WAAW,OAAO,CAAE;GAC7B,MAAM,SAAS,IAAI,MAAM,GAAc;AACvC,OAAI,CAAC,OAAQ;AACb,OAAI,CAAC,QAAQ,IAAI,KAAM;GACvB,MAAM,YAAY,OAAO,aAAa;AACtC,OAAI,EAAE,aAAa,UACjB,YAAW,aAAa,EAAE;;AAI9B,SAAO;GAAE,GAAG;GAAY,GAAG;GAAU;;;;;;CAOvC,OAAO,wBAAwB,QAA6C;EAC1E,MAAM,UAAU,YAAY,gBAAgB,OAAO;AACnD,SAAO,OAAO,KAAK,QAAQ,CAAC,KAAK,SAAS;GACxC,MAAM,aAAa;GACnB,OAAO,UAAU;GACjB,aAAa,UAAU;GACvB,aAAa,6BAA6B,IAAI;GAC9C,YAAY;GACZ,QAAQ,EACN,MAAM;IACJ,KAAK,qBAAqB,IAAI,aAAa;IAC3C,aAAa,oBAAoB,IAAI;IACtC,EACF;GACD,UAAU;GACX,EAAE;;;;;;CAOL,AAAQ,oBAAoB,WAAmB,QAAsB;AACnE,MAAI,CAAC,iBAAiB,CACpB,QAAO,KACL,cAAc,UAAU,KAAK,OAAO,yFACI,UAAU,iBAAiB,OAAO,IAC3E;;;;;;CAQL,AAAQ,qBAAqB,WAAmB,QAAsB;AACpE,MAAI,CAAC,iBAAiB,CACpB,OAAM,IAAI,MACR,cAAc,UAAU,KAAK,OAAO,kFAAkF,UAAU,iBAAiB,OAAO,IACzJ;;CAIL,YAAY,QAAsB;AAChC,QAAM,OAAO;AACb,OAAK,SAAS;AAEd,MAAI,OAAO,mBACT,4BAA2B,OAAO,mBAAmB;EAGvD,MAAM,UAAU,YAAY,gBAAgB,OAAO;AACnD,OAAK,aAAa,OAAO,KAAK,QAAQ;AAEtC,OAAK,MAAM,OAAO,KAAK,YAAY;GACjC,MAAM,YAAY,QAAQ;GAC1B,MAAM,SAAS,qBAAqB,IAAI,aAAa;GACrD,MAAM,aAAa,QAAQ,IAAI;GAG/B,MAAM,eAA6B;IACjC,eAAe,UAAU,iBAAiB,OAAO;IACjD,oBACE,UAAU,sBAAsB,OAAO;IAC1C;AACD,QAAK,cAAc,OAAO;AAE1B,QAAK,iBAAiB,OAAO,IAAI,eAAe;IAC9C,eAAe;IACf,SAAS,OAAO;IAChB,WAAW,OAAO;IAClB,oBAAoB,aAAa;IAClC,CAAC;;;;;;;CAQN,AAAU,gBAAgB,WAA8B;EACtD,MAAM,YAAY,KAAK,iBAAiB;AACxC,SAAO;GACL,OAAO,kBAA2B;AAChC,SAAK,qBAAqB,WAAW,OAAO;AAC5C,WAAO,UAAU,KAAK,oBAAoB,EAAE,cAAc;;GAE5D,OAAO,UAAkB,YAAmC;AAC1D,SAAK,qBAAqB,WAAW,OAAO;AAC5C,WAAO,UAAU,KAAK,oBAAoB,EAAE,UAAU,QAAQ;;GAEhE,WAAW,aAAgD;AACzD,SAAK,qBAAqB,WAAW,WAAW;AAChD,WAAO,UAAU,SAAS,oBAAoB,EAAE,SAAS;;GAE3D,SAAS,aAAqB;AAC5B,SAAK,qBAAqB,WAAW,SAAS;AAC9C,WAAO,UAAU,OAAO,oBAAoB,EAAE,SAAS;;GAEzD,WAAW,aAAqB;AAC9B,SAAK,qBAAqB,WAAW,WAAW;AAChD,WAAO,UAAU,SAAS,oBAAoB,EAAE,SAAS;;GAE3D,SACE,UACA,UACA,YACG;AACH,SAAK,qBAAqB,WAAW,SAAS;AAC9C,WAAO,UAAU,OACf,oBAAoB,EACpB,UACA,UACA,QACD;;GAEH,kBAAkB,kBAA0B;AAC1C,SAAK,qBAAqB,WAAW,kBAAkB;AACvD,WAAO,UAAU,gBAAgB,oBAAoB,EAAE,cAAc;;GAEvE,SAAS,aAAqB;AAC5B,SAAK,qBAAqB,WAAW,SAAS;AAC9C,WAAO,UAAU,OAAO,oBAAoB,EAAE,SAAS;;GAEzD,UAAU,aAAqB;AAC7B,SAAK,qBAAqB,WAAW,UAAU;AAC/C,WAAO,UAAU,QAAQ,oBAAoB,EAAE,SAAS;;GAE3D;;CAGH,aAAa,QAAoB;AAC/B,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,MAAuB,QAA0B;AAC/D,QAAI,KAAK,EAAE,SAAS,KAAK,YAAY,CAAC;;GAEzC,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,YAAY,KAAK,KAAK,WAAW,UAAU;;GAEzD,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,YAAY,KAAK,KAAK,WAAW,UAAU;;GAEzD,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,gBAAgB,KAAK,KAAK,WAAW,UAAU;;GAE7D,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,WAAW,KAAK,KAAK,WAAW,UAAU;;GAExD,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,cAAc,KAAK,KAAK,WAAW,UAAU;;GAE3D,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,gBAAgB,KAAK,KAAK,WAAW,UAAU;;GAE7D,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,eAAe,KAAK,KAAK,WAAW,UAAU;;GAE5D,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,iBAAiB;GACjB,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,cAAc,KAAK,KAAK,WAAW,UAAU;;GAE3D,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,aAAa,KAAK,KAAK,WAAW,UAAU;;GAE1D,CAAC;AAEF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,cAAc,KAAK,eAAe,KAAK,IAAI;AAC9D,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,cAAc,KAAK,KAAK,WAAW,UAAU;;GAE3D,CAAC;;;;;;CAOJ,AAAQ,eACN,KACA,KAGiD;EACjD,MAAM,YAAY,IAAI,OAAO;EAC7B,MAAM,YAAY,KAAK,iBAAiB;AACxC,MAAI,CAAC,WAAW;GACd,MAAM,UAAU,UAAU,QAAQ,mBAAmB,GAAG;AACxD,OAAI,OAAO,IAAI,CAAC,KAAK;IACnB,OAAO,mBAAmB,QAAQ;IAClC,QAAQ,KAAK;IACd,CAAC;AACF,UAAO;IAAE,WAAW;IAAW,WAAW;IAAW;;AAEvD,SAAO;GAAE;GAAW;GAAW;;;;;;CAOjC,AAAQ,aAAa,MAAyC;AAC5D,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,KAChB,QAAO,uDAAuD,KAAK,OAAO;AAC5E,MAAI,KAAK,SAAS,KAAK,CAAE,QAAO;AAChC,SAAO;;CAGT,AAAQ,cACN,UACyB;AACzB,SAAO,EACL,SAAS;GACP,GAAG;GACH,OAAO;IAAE,GAAG,oBAAoB;IAAO;IAAU;GAClD,EACF;;;;;;;CAQH,AAAQ,qBACN,WACA,YACA,QACA,WACM;EACN,MAAM,SAAS,gBAAgB,WAAW;EAC1C,MAAM,mBAAmB,SACrB,UAAU,YAAY,OAAO,GAC7B;EACJ,MAAM,UAAU,KAAK,MAAM,YACzB,CAAC,SAAS,UAAU,QAAQ,iBAAiB,EAC7C,OACD;AACD,OAAK,MAAM,OAAO,QAAQ;;CAG5B,AAAQ,gBACN,KACA,OACA,iBACM;AACN,MAAI,iBAAiB,qBAAqB;AACxC,OAAI,OAAO,IAAI,CAAC,KAAK;IACnB,OAAO,MAAM;IACb,QAAQ,KAAK;IACd,CAAC;AACF;;AAEF,MAAI,iBAAiB,UAAU;GAC7B,MAAM,SAAS,MAAM,cAAc;AACnC,OAAI,UAAU,OAAO,SAAS,KAAK;AACjC,QAAI,OAAO,OAAO,CAAC,KAAK;KACtB,OAAO,MAAM;KACb,YAAY;KACZ,QAAQ,KAAK;KACd,CAAC;AACF;;AAEF,UAAO,MAAM,mCAAmC,KAAK,MAAM,MAAM;AACjE,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAiB,QAAQ,KAAK;IAAM,CAAC;AACnE;;AAEF,SAAO,MAAM,6BAA6B,KAAK,MAAM,MAAM;AAC3D,MAAI,OAAO,IAAI,CAAC,KAAK;GAAE,OAAO;GAAiB,QAAQ,KAAK;GAAM,CAAC;;CAGrE,AAAQ,iBAAiB,KAAuB,QAAsB;AACpE,MAAI,OAAO,OAAO,CAAC,KAAK;GACtB,OAAO,aAAa,WAAW;GAC/B,QAAQ,KAAK;GACd,CAAC;;CAGJ,MAAc,YACZ,KACA,KACA,WACA,WACe;EACf,MAAM,OAAO,IAAI,MAAM;AAEvB,MAAI;GAEF,MAAM,SAAS,MADI,KAAK,OAAO,IAAI,CACH,QAC9B,YAAY;AACV,SAAK,oBAAoB,WAAW,OAAO;AAC3C,WAAO,UAAU,KAAK,oBAAoB,EAAE,KAAK;MAEnD,KAAK,cAAc,CACjB,SAAS,UAAU,QACnB,OAAO,UAAU,YAAY,KAAK,GAAG,WACtC,CAAC,CACH;AAED,OAAI,CAAC,OAAO,IAAI;AACd,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,OAAI,KAAK,OAAO,KAAK;WACd,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,cAAc;;;CAInD,MAAc,YACZ,KACA,KACA,WACA,WACe;EACf,MAAM,OAAO,IAAI,MAAM;EACvB,MAAM,QAAQ,KAAK,aAAa,KAAK;AACrC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;AAGF,MAAI;GAEF,MAAM,SAAS,MADI,KAAK,OAAO,IAAI,CACH,QAC9B,YAAY;AACV,SAAK,oBAAoB,WAAW,OAAO;AAC3C,WAAO,UAAU,KAAK,oBAAoB,EAAE,KAAK;MAEnD,KAAK,cAAc,CACjB,SAAS,UAAU,QACnB,UAAU,YAAY,KAAK,CAC5B,CAAC,CACH;AAED,OAAI,CAAC,OAAO,IAAI;AACd,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,OAAI,KAAK,aAAa,CAAC,KAAK,OAAO,KAAK;WACjC,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,cAAc;;;CAInD,MAAc,gBACZ,KACA,KACA,WACA,WACe;AACf,SAAO,KAAK,WAAW,KAAK,KAAK,WAAW,WAAW,EACrD,MAAM,YACP,CAAC;;CAGJ,MAAc,WACZ,KACA,KACA,WACA,WACe;AACf,SAAO,KAAK,WAAW,KAAK,KAAK,WAAW,WAAW,EACrD,MAAM,OACP,CAAC;;;;;;;CAQJ,MAAc,WACZ,KACA,KACA,WACA,WACA,MACe;EACf,MAAM,OAAO,IAAI,MAAM;EACvB,MAAM,QAAQ,KAAK,aAAa,KAAK;AACrC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;EAGF,MAAM,QAAQ,KAAK,SAAS,aAAa,aAAa;EACtD,MAAM,YAAY,KAAK,cAAc;AAErC,MAAI;GACF,MAAM,aAAa,KAAK,OAAO,IAAI;GACnC,MAAM,WAAoC,EACxC,SAAS,yBACV;GACD,MAAM,WAAW,MAAM,WAAW,QAAQ,YAAY;AACpD,SAAK,oBAAoB,WAAW,WAAW;AAC/C,WAAO,UAAU,SAAS,oBAAoB,EAAE,KAAK;MACpD,SAAS;AAEZ,OAAI,CAAC,SAAS,IAAI;AAChB,SAAK,iBAAiB,KAAK,SAAS,OAAO;AAC3C;;GAGF,MAAM,eAAe,oBACnB,MACA,QACA,UAAU,mBACX;GACD,MAAM,WAAW,iBAAiB,KAAK,MAAM,IAAI,CAAC,KAAK,IAAI,WAAW;AAEtE,OAAI,UAAU,gBAAgB,aAAa;AAC3C,OAAI,UAAU,0BAA0B,UAAU;AAElD,OAAI,KAAK,SAAS,OAAO;AACvB,QAAI,UAAU,2BAA2B,UAAU;AACnD,QAAI,CAAC,wBAAwB,aAAa,CACxC,KAAI,UACF,uBACA,yBAAyB,SAAS,GACnC;SAGH,KAAI,UACF,uBACA,yBAAyB,SAAS,GACnC;AAGH,OAAI,SAAS,KAAK,UAAU;IAC1B,MAAM,aAAa,SAAS,QAC1B,SAAS,KAAK,SACf;AACD,eAAW,GAAG,UAAU,QAAQ;AAC9B,YAAO,MAAM,8BAA8B,KAAK,MAAM,IAAI;AAC1D,SAAI,CAAC,IAAI,YACP,MAAK,iBAAiB,KAAK,IAAI;SAE/B,KAAI,SAAS;MAEf;AACF,eAAW,KAAK,IAAI;SAEpB,KAAI,KAAK;WAEJ,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,GAAG,MAAM,SAAS;;;CAIvD,MAAc,cACZ,KACA,KACA,WACA,WACe;EACf,MAAM,OAAO,IAAI,MAAM;EACvB,MAAM,QAAQ,KAAK,aAAa,KAAK;AACrC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;AAGF,MAAI;GAEF,MAAM,SAAS,MADI,KAAK,OAAO,IAAI,CACH,QAC9B,YAAY;AACV,SAAK,oBAAoB,WAAW,SAAS;AAC7C,WAAO,UAAU,OAAO,oBAAoB,EAAE,KAAK;MAErD,KAAK,cAAc,CACjB,SAAS,UAAU,UACnB,UAAU,YAAY,KAAK,CAC5B,CAAC,CACH;AAED,OAAI,CAAC,OAAO,IAAI;AACd,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,OAAI,KAAK,EAAE,QAAQ,OAAO,MAAM,CAAC;WAC1B,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,sBAAsB;;;CAI3D,MAAc,gBACZ,KACA,KACA,WACA,WACe;EACf,MAAM,OAAO,IAAI,MAAM;EACvB,MAAM,QAAQ,KAAK,aAAa,KAAK;AACrC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;AAGF,MAAI;GAEF,MAAM,SAAS,MADI,KAAK,OAAO,IAAI,CACH,QAC9B,YAAY;AACV,SAAK,oBAAoB,WAAW,WAAW;AAC/C,WAAO,UAAU,SAAS,oBAAoB,EAAE,KAAK;MAEvD,KAAK,cAAc,CACjB,SAAS,UAAU,YACnB,UAAU,YAAY,KAAK,CAC5B,CAAC,CACH;AAED,OAAI,CAAC,OAAO,IAAI;AACd,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,OAAI,KAAK,OAAO,KAAK;WACd,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,wBAAwB;;;CAI7D,MAAc,eACZ,KACA,KACA,WACA,WACe;EACf,MAAM,OAAO,IAAI,MAAM;EACvB,MAAM,QAAQ,KAAK,aAAa,KAAK;AACrC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;AAGF,MAAI;GAEF,MAAM,SAAS,MADI,KAAK,OAAO,IAAI,CACH,QAC9B,YAAY;AACV,SAAK,oBAAoB,WAAW,UAAU;AAC9C,WAAO,UAAU,QAAQ,oBAAoB,EAAE,KAAK;MAEtD,KAAK,cAAc,CACjB,SAAS,UAAU,WACnB,UAAU,YAAY,KAAK,CAC5B,CAAC,CACH;AAED,OAAI,CAAC,OAAO,IAAI;AACd,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,OAAI,KAAK,OAAO,KAAK;WACd,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,iBAAiB;;;CAItD,MAAc,cACZ,KACA,KACA,WACA,WACe;EACf,MAAM,OAAO,IAAI,MAAM;EACvB,MAAM,QAAQ,KAAK,aAAa,KAAK;AACrC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;EAIF,MAAM,UADY,KAAK,cAAc,WACX,iBAAiB;EAC3C,MAAM,mBAAmB,IAAI,QAAQ;EACrC,MAAM,gBAAgB,mBAClB,SAAS,kBAAkB,GAAG,GAC9B;AAEJ,MACE,kBAAkB,UAClB,CAAC,OAAO,MAAM,cAAc,IAC5B,gBAAgB,SAChB;AACA,OAAI,OAAO,IAAI,CAAC,KAAK;IACnB,OAAO,cAAc,cAAc,wCAAwC,QAAQ;IACnF,QAAQ,KAAK;IACd,CAAC;AACF;;AAGF,SAAO,MAAM,KAAK,qCAAqC,WAAW,KAAK;AAEvE,MAAI;GACF,MAAM,YAAwC,SAAS,MAAM,IAAI;GAEjE,IAAI,gBAAgB;GACpB,MAAM,YAAY,UAAU,YAC1B,IAAI,gBAAwC,EAC1C,UAAU,OAAO,YAAY;AAC3B,qBAAiB,MAAM;AACvB,QAAI,gBAAgB,SAAS;AAC3B,gBAAW,sBACT,IAAI,MACF,+CAA+C,QAAQ,SACxD,CACF;AACD;;AAEF,eAAW,QAAQ,MAAM;MAE5B,CAAC,CACH;AAED,UAAO,MACL,KACA,0DACA,WACA,MACA,iBAAiB,EAClB;GACD,MAAM,aAAa,KAAK,OAAO,IAAI;GACnC,MAAM,WAAoC,EACxC,SAAS,sBACV;GACD,MAAM,SAAS,MAAM,KAAK,iBACxB,WAAW,QAAQ,YAAY;AAC7B,SAAK,oBAAoB,WAAW,SAAS;AAC7C,UAAM,UAAU,OAAO,oBAAoB,EAAE,MAAM,UAAU;AAC7D,WAAO,EAAE,SAAS,MAAe;MAChC,SAAS,CACb;AAED,QAAK,qBACH,WACA,MACA,KAAK,cAAc,IAAI,EACvB,UACD;AAED,OAAI,CAAC,OAAO,IAAI;AACd,WAAO,MACL,KACA,mDACA,WACA,MACA,iBAAiB,EAClB;AACD,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAGF,UAAO,MAAM,KAAK,sCAAsC,WAAW,KAAK;AACxE,OAAI,KAAK,OAAO,KAAK;WACd,OAAO;AACd,OACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,+BAA+B,EACtD;AACA,QAAI,OAAO,IAAI,CAAC,KAAK;KAAE,OAAO,MAAM;KAAS,QAAQ,KAAK;KAAM,CAAC;AACjE;;AAEF,QAAK,gBAAgB,KAAK,OAAO,gBAAgB;;;CAIrD,MAAc,aACZ,KACA,KACA,WACA,WACe;EACf,MAAM,UACJ,OAAO,IAAI,MAAM,SAAS,WAAW,IAAI,KAAK,OAAO;EACvD,MAAM,QAAQ,KAAK,aAAa,QAAQ;AACxC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;AAGF,MAAI;GACF,MAAM,aAAa,KAAK,OAAO,IAAI;GACnC,MAAM,WAAoC,EACxC,SAAS,sBACV;GACD,MAAM,SAAS,MAAM,KAAK,iBACxB,WAAW,QAAQ,YAAY;AAC7B,SAAK,oBAAoB,WAAW,kBAAkB;AACtD,UAAM,UAAU,gBAAgB,oBAAoB,EAAE,QAAQ;AAC9D,WAAO,EAAE,SAAS,MAAe;MAChC,SAAS,CACb;AAED,QAAK,qBACH,WACA,SACA,KAAK,cAAc,IAAI,EACvB,UACD;AAED,OAAI,CAAC,OAAO,IAAI;AACd,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAGF,OAAI,KAAK,OAAO,KAAK;WACd,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,0BAA0B;;;CAI/D,MAAc,cACZ,KACA,KACA,WACA,WACe;EACf,MAAM,UAAU,IAAI,MAAM;EAC1B,MAAM,QAAQ,KAAK,aAAa,QAAQ;AACxC,MAAI,UAAU,MAAM;AAClB,OAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAO,QAAQ,KAAK;IAAM,CAAC;AACzD;;EAEF,MAAM,OAAO;AAEb,MAAI;GACF,MAAM,aAAa,KAAK,OAAO,IAAI;GACnC,MAAM,WAAoC,EACxC,SAAS,sBACV;GACD,MAAM,SAAS,MAAM,KAAK,iBACxB,WAAW,QAAQ,YAAY;AAC7B,SAAK,oBAAoB,WAAW,SAAS;AAC7C,UAAM,UAAU,OAAO,oBAAoB,EAAE,KAAK;AAClD,WAAO,EAAE,SAAS,MAAe;MAChC,SAAS,CACb;AAED,QAAK,qBACH,WACA,MACA,KAAK,cAAc,IAAI,EACvB,UACD;AAED,OAAI,CAAC,OAAO,IAAI;AACd,SAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAGF,OAAI,KAAK,OAAO,KAAK;WACd,OAAO;AACd,QAAK,gBAAgB,KAAK,OAAO,gBAAgB;;;CAIrD,AAAQ,iBAAiB;CAEzB,AAAQ,WAAc,IAAkC;AACtD,OAAK;AACL,SAAO,IAAI,CAAC,cAAc;AACxB,QAAK;IACL;;CAGJ,MAAM,WAA0B;EAE9B,MAAM,WAAW,KAAK,KAAK,GAAG;AAC9B,SAAO,KAAK,iBAAiB,KAAK,KAAK,KAAK,GAAG,UAAU;AACvD,UAAO,KACL,kEACA,KAAK,eACN;AACD,SAAM,IAAI,SAAS,YAAY,WAAW,SAAS,IAAI,CAAC;;AAE1D,MAAI,KAAK,iBAAiB,EACxB,QAAO,KACL,uEACA,KAAK,eACN;AAEH,OAAK,cAAc,UAAU;;;;;;;;;;;;;;;CAgB/B,UAAuB;EACrB,MAAM,iBAAiB,cAAoC;AACzD,OAAI,CAAC,KAAK,WAAW,SAAS,UAAU,CACtC,OAAM,IAAI,MACR,mBAAmB,UAAU,wBAAwB,KAAK,WAAW,KAAK,KAAK,GAChF;AAMH,UAAO;IACL,GAHY,KAAK,gBAAgB,UAAU;IAI3C,SAAS,QAAmC;AAE1C,YADmB,KAAK,OAAO,IAAI,CACjB,gBAAgB,UAAU;;IAE/C;;EAGH,MAAM,gBAAgB,cACpB,cAAc,UAAU;AAC1B,cAAY,SAAS;AAErB,SAAO;;CAGT,eAAwC;AACtC,SAAO,EAAE,SAAS,KAAK,YAAY;;;;;;AAOvC,MAAaC,UAAQ,SAAS,YAAY"}
1
+ {"version":3,"file":"plugin.js","names":["manifest","files"],"sources":["../../../src/plugins/files/plugin.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport { 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 { getCurrentUserId, getWorkspaceClient } 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 {\n type FileAction,\n type FilePolicyUser,\n type FileResource,\n PolicyDeniedError,\n policy,\n} from \"./policy\";\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 * Extract user identity from the request.\n * Falls back to `getCurrentUserId()` in development mode.\n */\n private _extractUser(req: express.Request): FilePolicyUser {\n const userId = req.header(\"x-forwarded-user\")?.trim();\n if (userId) return { id: userId };\n if (process.env.NODE_ENV === \"development\") {\n logger.warn(\n \"No x-forwarded-user header — falling back to service principal identity for policy checks. \" +\n \"Ensure your proxy forwards user headers to test per-user policies.\",\n );\n return { id: getCurrentUserId() };\n }\n throw AuthenticationError.missingToken(\n \"Missing x-forwarded-user header. Cannot resolve user ID.\",\n );\n }\n\n /**\n * Check the policy for a volume. No-op if no policy is configured.\n * Throws `PolicyDeniedError` if denied.\n */\n private async _checkPolicy(\n volumeKey: string,\n action: FileAction,\n path: string,\n user: FilePolicyUser,\n resourceOverrides?: Partial<FileResource>,\n ): Promise<void> {\n const policyFn = this.volumeConfigs[volumeKey]?.policy;\n if (typeof policyFn !== \"function\") return;\n\n const resource: FileResource = {\n path,\n volume: volumeKey,\n ...resourceOverrides,\n };\n const allowed = await policyFn(action, resource, user);\n if (!allowed) {\n const userId = user.isServicePrincipal ? \"<service-principal>\" : user.id;\n logger.warn(\n 'Policy denied \"%s\" on volume \"%s\" for user \"%s\"',\n action,\n volumeKey,\n userId,\n );\n throw new PolicyDeniedError(action, volumeKey);\n }\n }\n\n /**\n * HTTP-level wrapper around `_checkPolicy`.\n * Extracts user (401 on failure), runs policy (403 on denial).\n * Returns `true` if the request may proceed, `false` if a response was sent.\n */\n private async _enforcePolicy(\n req: express.Request,\n res: express.Response,\n volumeKey: string,\n action: FileAction,\n path: string,\n resourceOverrides?: Partial<FileResource>,\n ): Promise<boolean> {\n let user: FilePolicyUser;\n try {\n user = this._extractUser(req);\n } catch (error) {\n if (error instanceof AuthenticationError) {\n res.status(401).json({ error: error.message, plugin: this.name });\n return false;\n }\n throw error;\n }\n\n try {\n await this._checkPolicy(volumeKey, action, path, user, resourceOverrides);\n } catch (error) {\n if (error instanceof PolicyDeniedError) {\n res.status(403).json({ error: error.message, plugin: this.name });\n return false;\n }\n // A crashing policy is treated as a server error (fail closed).\n logger.error(\"Policy function threw on volume %s: %O\", volumeKey, error);\n res.status(500).json({\n error: \"Policy evaluation failed\",\n plugin: this.name,\n });\n return false;\n }\n\n return true;\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 policy: volumeCfg.policy ?? policy.publicRead(),\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 // Warn at startup for volumes without an explicit policy\n for (const key of this.volumeKeys) {\n if (!volumes[key].policy) {\n logger.warn(\n 'Volume \"%s\" has no explicit policy — defaulting to publicRead(). ' +\n \"Set a policy in files({ volumes: { %s: { policy: ... } } }) to silence this warning.\",\n key,\n key,\n );\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 * Cache keys include `getCurrentUserId()` — must match the identity used\n * by `this.execute()` in `_handleList`. Both run in service-principal\n * context; wrapping either in `runInUserContext` would break invalidation.\n */\n private _invalidateListCache(\n volumeKey: string,\n parentPath: 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 getCurrentUserId(),\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 PolicyDeniedError) {\n res.status(403).json({\n error: error.message,\n plugin: this.name,\n });\n return;\n }\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 _sendStatusError(res: express.Response, status: number): void {\n res.status(status).json({\n error: STATUS_CODES[status] ?? \"Unknown Error\",\n plugin: this.name,\n });\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 if (!(await this._enforcePolicy(req, res, volumeKey, \"list\", path ?? \"/\")))\n return;\n\n try {\n const result = await this.execute(\n async () => connector.list(getWorkspaceClient(), path),\n this._readSettings([\n `files:${volumeKey}:list`,\n path ? connector.resolvePath(path) : \"__root__\",\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\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\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 if (!(await this._enforcePolicy(req, res, volumeKey, \"read\", path))) return;\n\n try {\n const result = await this.execute(\n async () => connector.read(getWorkspaceClient(), path),\n this._readSettings([\n `files:${volumeKey}:read`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.type(\"text/plain\").send(result.data);\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\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 if (!(await this._enforcePolicy(req, res, volumeKey, opts.mode, path)))\n return;\n\n const label = opts.mode === \"download\" ? \"Download\" : \"Raw fetch\";\n const volumeCfg = this.volumeConfigs[volumeKey];\n\n try {\n const settings: PluginExecutionSettings = {\n default: FILES_DOWNLOAD_DEFAULTS,\n };\n const response = await this.execute(\n async () => connector.download(getWorkspaceClient(), path),\n settings,\n );\n\n if (!response.ok) {\n this._sendStatusError(res, response.status);\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.data.contents) {\n const nodeStream = Readable.fromWeb(\n response.data.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 this._sendStatusError(res, 500);\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\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 if (!(await this._enforcePolicy(req, res, volumeKey, \"exists\", path)))\n return;\n\n try {\n const result = await this.execute(\n async () => connector.exists(getWorkspaceClient(), path),\n this._readSettings([\n `files:${volumeKey}:exists`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ exists: result.data });\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\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 if (!(await this._enforcePolicy(req, res, volumeKey, \"metadata\", path)))\n return;\n\n try {\n const result = await this.execute(\n async () => connector.metadata(getWorkspaceClient(), path),\n this._readSettings([\n `files:${volumeKey}:metadata`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\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\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 if (!(await this._enforcePolicy(req, res, volumeKey, \"preview\", path)))\n return;\n\n try {\n const result = await this.execute(\n async () => connector.preview(getWorkspaceClient(), path),\n this._readSettings([\n `files:${volumeKey}:preview`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\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 let contentLength: number | undefined;\n\n if (typeof rawContentLength === \"string\" && rawContentLength.length > 0) {\n if (!/^\\d+$/.test(rawContentLength)) {\n res.status(400).json({\n error: \"Invalid Content-Length header.\",\n plugin: this.name,\n });\n return;\n }\n contentLength = Number(rawContentLength);\n }\n\n if (\n !(await this._enforcePolicy(req, res, volumeKey, \"upload\", path, {\n size: contentLength,\n }))\n )\n return;\n\n if (contentLength !== undefined && contentLength > maxSize) {\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 settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n this.execute(async () => {\n await connector.upload(getWorkspaceClient(), path, webStream);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(volumeKey, path, connector);\n\n if (!result.ok) {\n logger.error(\n req,\n \"Upload failed: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n this._sendStatusError(res, result.status);\n return;\n }\n\n logger.debug(req, \"Upload complete: volume=%s path=%s\", volumeKey, path);\n res.json(result.data);\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\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 if (!(await this._enforcePolicy(req, res, volumeKey, \"mkdir\", dirPath)))\n return;\n\n try {\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n this.execute(async () => {\n await connector.createDirectory(getWorkspaceClient(), dirPath);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(volumeKey, dirPath, connector);\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n\n res.json(result.data);\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\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 if (!(await this._enforcePolicy(req, res, volumeKey, \"delete\", path)))\n return;\n\n try {\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n this.execute(async () => {\n await connector.delete(getWorkspaceClient(), path);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(volumeKey, path, connector);\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n\n res.json(result.data);\n } catch (error) {\n this._handleApiError(res, error, \"Delete failed\");\n }\n }\n\n /**\n * Creates a VolumeAPI for a specific volume key.\n *\n * By default, enforces the volume's policy before each operation.\n * Pass `bypassPolicy: true` to skip policy checks — useful for\n * background jobs or migrations that should bypass user-facing policies.\n *\n * @security When `bypassPolicy` is `true`, no policy enforcement runs.\n * Do not expose bypassed APIs to HTTP routes or end-user code paths.\n */\n protected createVolumeAPI(\n volumeKey: string,\n user: FilePolicyUser,\n options?: { bypassPolicy?: boolean },\n ): VolumeAPI {\n const connector = this.volumeConnectors[volumeKey];\n const noop = () => Promise.resolve();\n const check = options?.bypassPolicy\n ? noop\n : (action: FileAction, path: string, overrides?: Partial<FileResource>) =>\n this._checkPolicy(volumeKey, action, path, user, overrides);\n\n return {\n list: async (directoryPath?: string) => {\n await check(\"list\", directoryPath ?? \"/\");\n return connector.list(getWorkspaceClient(), directoryPath);\n },\n read: async (filePath: string, opts?: { maxSize?: number }) => {\n await check(\"read\", filePath);\n return connector.read(getWorkspaceClient(), filePath, opts);\n },\n download: async (filePath: string) => {\n await check(\"download\", filePath);\n return connector.download(getWorkspaceClient(), filePath);\n },\n exists: async (filePath: string) => {\n await check(\"exists\", filePath);\n return connector.exists(getWorkspaceClient(), filePath);\n },\n metadata: async (filePath: string) => {\n await check(\"metadata\", filePath);\n return connector.metadata(getWorkspaceClient(), filePath);\n },\n upload: async (\n filePath: string,\n contents: ReadableStream | Buffer | string,\n opts?: { overwrite?: boolean },\n ) => {\n await check(\"upload\", filePath);\n return connector.upload(getWorkspaceClient(), filePath, contents, opts);\n },\n createDirectory: async (directoryPath: string) => {\n await check(\"mkdir\", directoryPath);\n return connector.createDirectory(getWorkspaceClient(), directoryPath);\n },\n delete: async (filePath: string) => {\n await check(\"delete\", filePath);\n return connector.delete(getWorkspaceClient(), filePath);\n },\n preview: async (filePath: string) => {\n await check(\"preview\", filePath);\n return connector.preview(getWorkspaceClient(), filePath);\n },\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 * All operations execute as the service principal.\n * Use policies to control per-user access.\n *\n * @example\n * ```ts\n * // Service principal access\n * appKit.files(\"uploads\").list()\n *\n * // With policy: pass user identity for access control\n * appKit.files(\"uploads\").asUser(req).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 // Lazy user resolution: getCurrentUserId() is called when a method\n // is invoked (policy check), not when exports() is called.\n const spUser: FilePolicyUser = {\n get id() {\n return getCurrentUserId();\n },\n isServicePrincipal: true,\n };\n const spApi = this.createVolumeAPI(volumeKey, spUser);\n\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const user = this._extractUser(req);\n return this.createVolumeAPI(volumeKey, user);\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 = Object.assign(toPlugin(FilesPlugin), { policy });\n"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ //#region src/plugins/files/policy.d.ts
2
+ /**
3
+ * Per-volume file access policies.
4
+ *
5
+ * A `FilePolicy` is a function that decides whether a given action on a
6
+ * resource is allowed for a specific user. When a policy is attached to a
7
+ * volume, the policy controls whether the action is allowed for the requesting user.
8
+ */
9
+ /** Every action the files plugin can perform. */
10
+ type FileAction = "list" | "read" | "download" | "raw" | "exists" | "metadata" | "preview" | "upload" | "mkdir" | "delete";
11
+ /** Actions that only read data. */
12
+ declare const READ_ACTIONS: ReadonlySet<FileAction>;
13
+ /** Actions that mutate data. */
14
+ declare const WRITE_ACTIONS: ReadonlySet<FileAction>;
15
+ /** Describes the file or directory being acted upon. */
16
+ interface FileResource {
17
+ /** Relative path within the volume. */
18
+ path: string;
19
+ /** The volume key (e.g. `"uploads"`). */
20
+ volume: string;
21
+ /** Content length in bytes — only present for uploads. */
22
+ size?: number;
23
+ }
24
+ /** Minimal user identity passed to the policy function. */
25
+ interface FilePolicyUser {
26
+ id: string;
27
+ /** `true` when the caller is the service principal (direct SDK call, not `asUser`). */
28
+ isServicePrincipal?: boolean;
29
+ }
30
+ /**
31
+ * A policy function that decides whether `user` may perform `action` on
32
+ * `resource`. Return `true` to allow, `false` to deny.
33
+ */
34
+ type FilePolicy = (action: FileAction, resource: FileResource, user: FilePolicyUser) => boolean | Promise<boolean>;
35
+ /**
36
+ * Thrown when a policy denies an action.
37
+ */
38
+ declare class PolicyDeniedError extends Error {
39
+ readonly action: FileAction;
40
+ readonly volumeKey: string;
41
+ constructor(action: FileAction, volumeKey: string);
42
+ }
43
+ //#endregion
44
+ export { FileAction, FilePolicy, FilePolicyUser, FileResource, PolicyDeniedError, READ_ACTIONS, WRITE_ACTIONS };
45
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","names":[],"sources":["../../../src/plugins/files/policy.ts"],"mappings":";;AAaA;;;;;AAaA;;KAbY,UAAA;;cAaC,YAAA,EAAc,WAAA,CAAY,UAAA;AAWvC;AAAA,cAAa,aAAA,EAAe,WAAA,CAAY,UAAA;;UAWvB,YAAA;EAXiC;EAahD,IAAA;EAF2B;EAI3B,MAAA;EAJ2B;EAM3B,IAAA;AAAA;;UAIe,cAAA;EACf,EAAA;EADe;EAGf,kBAAA;AAAA;;;AAWF;;KAAY,UAAA,IACV,MAAA,EAAQ,UAAA,EACR,QAAA,EAAU,YAAA,EACV,IAAA,EAAM,cAAA,eACO,OAAA;;;;cASF,iBAAA,SAA0B,KAAA;EAAA,SAC5B,MAAA,EAAQ,UAAA;EAAA,SACR,SAAA;cAEG,MAAA,EAAQ,UAAA,EAAY,SAAA;AAAA"}
@@ -0,0 +1,63 @@
1
+ //#region src/plugins/files/policy.ts
2
+ /** Actions that only read data. */
3
+ const READ_ACTIONS = new Set([
4
+ "list",
5
+ "read",
6
+ "download",
7
+ "raw",
8
+ "exists",
9
+ "metadata",
10
+ "preview"
11
+ ]);
12
+ /** Actions that mutate data. */
13
+ const WRITE_ACTIONS = new Set([
14
+ "upload",
15
+ "mkdir",
16
+ "delete"
17
+ ]);
18
+ /**
19
+ * Thrown when a policy denies an action.
20
+ */
21
+ var PolicyDeniedError = class extends Error {
22
+ action;
23
+ volumeKey;
24
+ constructor(action, volumeKey) {
25
+ super(`Policy denied "${action}" on volume "${volumeKey}"`);
26
+ this.name = "PolicyDeniedError";
27
+ this.action = action;
28
+ this.volumeKey = volumeKey;
29
+ }
30
+ };
31
+ /** Utility namespace with common policy combinators. */
32
+ const policy = {
33
+ all(...policies) {
34
+ if (policies.length === 0) throw new Error("policy.all() requires at least one policy");
35
+ return async (action, resource, user) => {
36
+ for (const p of policies) if (!await p(action, resource, user)) return false;
37
+ return true;
38
+ };
39
+ },
40
+ any(...policies) {
41
+ if (policies.length === 0) throw new Error("policy.any() requires at least one policy");
42
+ return async (action, resource, user) => {
43
+ for (const p of policies) if (await p(action, resource, user)) return true;
44
+ return false;
45
+ };
46
+ },
47
+ not(p) {
48
+ return async (action, resource, user) => !await p(action, resource, user);
49
+ },
50
+ publicRead() {
51
+ return (action) => READ_ACTIONS.has(action);
52
+ },
53
+ denyAll() {
54
+ return () => false;
55
+ },
56
+ allowAll() {
57
+ return () => true;
58
+ }
59
+ };
60
+
61
+ //#endregion
62
+ export { PolicyDeniedError, READ_ACTIONS, WRITE_ACTIONS, policy };
63
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","names":[],"sources":["../../../src/plugins/files/policy.ts"],"sourcesContent":["/**\n * Per-volume file access policies.\n *\n * A `FilePolicy` is a function that decides whether a given action on a\n * resource is allowed for a specific user. When a policy is attached to a\n * volume, the policy controls whether the action is allowed for the requesting user.\n */\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\n/** Every action the files plugin can perform. */\nexport type FileAction =\n | \"list\"\n | \"read\"\n | \"download\"\n | \"raw\"\n | \"exists\"\n | \"metadata\"\n | \"preview\"\n | \"upload\"\n | \"mkdir\"\n | \"delete\";\n\n/** Actions that only read data. */\nexport const READ_ACTIONS: ReadonlySet<FileAction> = new Set<FileAction>([\n \"list\",\n \"read\",\n \"download\",\n \"raw\",\n \"exists\",\n \"metadata\",\n \"preview\",\n]);\n\n/** Actions that mutate data. */\nexport const WRITE_ACTIONS: ReadonlySet<FileAction> = new Set<FileAction>([\n \"upload\",\n \"mkdir\",\n \"delete\",\n]);\n\n// ---------------------------------------------------------------------------\n// Resource & User\n// ---------------------------------------------------------------------------\n\n/** Describes the file or directory being acted upon. */\nexport interface FileResource {\n /** Relative path within the volume. */\n path: string;\n /** The volume key (e.g. `\"uploads\"`). */\n volume: string;\n /** Content length in bytes — only present for uploads. */\n size?: number;\n}\n\n/** Minimal user identity passed to the policy function. */\nexport interface FilePolicyUser {\n id: string;\n /** `true` when the caller is the service principal (direct SDK call, not `asUser`). */\n isServicePrincipal?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Policy function type\n// ---------------------------------------------------------------------------\n\n/**\n * A policy function that decides whether `user` may perform `action` on\n * `resource`. Return `true` to allow, `false` to deny.\n */\nexport type FilePolicy = (\n action: FileAction,\n resource: FileResource,\n user: FilePolicyUser,\n) => boolean | Promise<boolean>;\n\n// ---------------------------------------------------------------------------\n// PolicyDeniedError\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when a policy denies an action.\n */\nexport class PolicyDeniedError extends Error {\n readonly action: FileAction;\n readonly volumeKey: string;\n\n constructor(action: FileAction, volumeKey: string) {\n super(`Policy denied \"${action}\" on volume \"${volumeKey}\"`);\n this.name = \"PolicyDeniedError\";\n this.action = action;\n this.volumeKey = volumeKey;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Combinators\n// ---------------------------------------------------------------------------\n\n/** Utility namespace with common policy combinators. */\nexport const policy = {\n /**\n * AND — all policies must allow. Short-circuits on first denial.\n */\n all(...policies: FilePolicy[]): FilePolicy {\n if (policies.length === 0) {\n throw new Error(\"policy.all() requires at least one policy\");\n }\n return async (action, resource, user) => {\n for (const p of policies) {\n if (!(await p(action, resource, user))) return false;\n }\n return true;\n };\n },\n\n /**\n * OR — at least one policy must allow. Short-circuits on first allow.\n */\n any(...policies: FilePolicy[]): FilePolicy {\n if (policies.length === 0) {\n throw new Error(\"policy.any() requires at least one policy\");\n }\n return async (action, resource, user) => {\n for (const p of policies) {\n if (await p(action, resource, user)) return true;\n }\n return false;\n };\n },\n\n /** Negates a policy. */\n not(p: FilePolicy): FilePolicy {\n return async (action, resource, user) => !(await p(action, resource, user));\n },\n\n /** Allow all read actions (list, read, download, raw, exists, metadata, preview). */\n publicRead(): FilePolicy {\n return (action) => READ_ACTIONS.has(action);\n },\n\n /** Deny every action. */\n denyAll(): FilePolicy {\n return () => false;\n },\n\n /** Allow every action. */\n allowAll(): FilePolicy {\n return () => true;\n },\n} as const;\n"],"mappings":";;AA0BA,MAAa,eAAwC,IAAI,IAAgB;CACvE;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,MAAa,gBAAyC,IAAI,IAAgB;CACxE;CACA;CACA;CACD,CAAC;;;;AA4CF,IAAa,oBAAb,cAAuC,MAAM;CAC3C,AAAS;CACT,AAAS;CAET,YAAY,QAAoB,WAAmB;AACjD,QAAM,kBAAkB,OAAO,eAAe,UAAU,GAAG;AAC3D,OAAK,OAAO;AACZ,OAAK,SAAS;AACd,OAAK,YAAY;;;;AASrB,MAAa,SAAS;CAIpB,IAAI,GAAG,UAAoC;AACzC,MAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MAAM,4CAA4C;AAE9D,SAAO,OAAO,QAAQ,UAAU,SAAS;AACvC,QAAK,MAAM,KAAK,SACd,KAAI,CAAE,MAAM,EAAE,QAAQ,UAAU,KAAK,CAAG,QAAO;AAEjD,UAAO;;;CAOX,IAAI,GAAG,UAAoC;AACzC,MAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MAAM,4CAA4C;AAE9D,SAAO,OAAO,QAAQ,UAAU,SAAS;AACvC,QAAK,MAAM,KAAK,SACd,KAAI,MAAM,EAAE,QAAQ,UAAU,KAAK,CAAE,QAAO;AAE9C,UAAO;;;CAKX,IAAI,GAA2B;AAC7B,SAAO,OAAO,QAAQ,UAAU,SAAS,CAAE,MAAM,EAAE,QAAQ,UAAU,KAAK;;CAI5E,aAAyB;AACvB,UAAQ,WAAW,aAAa,IAAI,OAAO;;CAI7C,UAAsB;AACpB,eAAa;;CAIf,WAAuB;AACrB,eAAa;;CAEhB"}
@@ -1,5 +1,6 @@
1
1
  import { BasePluginConfig, IAppRequest } from "../../shared/src/plugin.js";
2
2
  import "../../shared/src/index.js";
3
+ import { FilePolicy } from "./policy.js";
3
4
  import { files } from "@databricks/sdk-experimental";
4
5
 
5
6
  //#region src/plugins/files/types.d.ts
@@ -11,10 +12,16 @@ interface VolumeConfig {
11
12
  maxUploadSize?: number;
12
13
  /** Map of file extensions to MIME types for this volume. Inherits from plugin-level `customContentTypes` if not set. */
13
14
  customContentTypes?: Record<string, string>;
15
+ /**
16
+ * Access-control policy for this volume. When set, operations execute as the
17
+ * service principal and the policy decides whether the action is allowed.
18
+ */
19
+ policy?: FilePolicy;
14
20
  }
15
21
  /**
16
22
  * User-facing API for a single volume.
17
- * Prefer OBO access via `app.files("volumeKey").asUser(req).list()`.
23
+ * All operations execute as the service principal. When a policy is
24
+ * configured on the volume, every call is checked against that policy.
18
25
  */
19
26
  interface VolumeAPI {
20
27
  list(directoryPath?: string): Promise<DirectoryEntry[]>;
@@ -73,8 +80,9 @@ interface FilePreview extends FileMetadata {
73
80
  /**
74
81
  * Volume handle returned by `app.files("volumeKey")`.
75
82
  *
76
- * - `asUser(req)` executes on behalf of the user (recommended).
77
- * - Direct methods (e.g. `.list()`) execute as the service principal (logs a warning encouraging OBO).
83
+ * All methods execute as the service principal and enforce the volume's
84
+ * policy (if configured) with `{ isServicePrincipal: true }`.
85
+ * `asUser(req)` re-wraps with the real user identity for per-user policy checks.
78
86
  */
79
87
  type VolumeHandle = VolumeAPI & {
80
88
  asUser: (req: IAppRequest) => VolumeAPI;
@@ -85,15 +93,15 @@ type VolumeHandle = VolumeAPI & {
85
93
  *
86
94
  * @example
87
95
  * ```ts
88
- * // OBO access (recommended)
89
- * appKit.files("uploads").asUser(req).list()
90
- *
91
- * // Service principal access (logs a warning)
96
+ * // Service principal access
92
97
  * appKit.files("uploads").list()
93
98
  *
99
+ * // With policy: pass user identity for access control
100
+ * appKit.files("uploads").asUser(req).list()
101
+ *
94
102
  * // Named accessor
95
103
  * const vol = appKit.files.volume("uploads")
96
- * await vol.asUser(req).list()
104
+ * await vol.list()
97
105
  * ```
98
106
  */
99
107
  interface FilesExport {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/files/types.ts"],"mappings":";;;;;;;;UAMiB,YAAA;EAAY;EAE3B,aAAA;EAE2B;EAA3B,kBAAA,GAAqB,MAAA;AAAA;;;;AAOvB;UAAiB,SAAA;EACf,IAAA,CAAK,aAAA,YAAyB,OAAA,CAAQ,cAAA;EACtC,IAAA,CAAK,QAAA,UAAkB,OAAA;IAAY,OAAA;EAAA,IAAqB,OAAA;EACxD,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,gBAAA;EACpC,MAAA,CAAO,QAAA,WAAmB,OAAA;EAC1B,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,YAAA;EACpC,MAAA,CACE,QAAA,UACA,QAAA,EAAU,cAAA,GAAiB,MAAA,WAC3B,OAAA;IAAY,SAAA;EAAA,IACX,OAAA;EACH,eAAA,CAAgB,aAAA,WAAwB,OAAA;EACxC,MAAA,CAAO,QAAA,WAAmB,OAAA;EAC1B,OAAA,CAAQ,QAAA,WAAmB,OAAA,CAAQ,WAAA;AAAA;;;;UAMpB,YAAA,SAAqB,gBAAA;EAlBpC;EAoBA,OAAA;EApB8B;EAsB9B,OAAA,GAAU,MAAA,SAAe,YAAA;EArBzB;EAuBA,kBAAA,GAAqB,MAAA;EAvBc;EAyBnC,aAAA;AAAA;;KAIU,cAAA,GAAiB,KAAA,CAAM,cAAA;;KAGvB,gBAAA,GAAmB,KAAA,CAAM,gBAAA;;;;UAKpB,YAAA;EAlCN;EAoCT,aAAA;EApCoC;EAsCpC,WAAA;EApCE;EAsCF,YAAA;AAAA;;;;UAMe,WAAA,SAAoB,YAAA;EAxCnC;EA0CA,WAAA;EA1CwC;EA4CxC,MAAA;EA3CO;EA6CP,OAAA;AAAA;;;;;;AAtCF;KA+CY,YAAA,GAAe,SAAA;EACzB,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,SAAA;AAAA;;;;;;;;;;;;;;;;AApChC;;UAwDiB,WAAA;EAAA,CACd,SAAA,WAAoB,YAAA;EACrB,MAAA,GAAS,SAAA,aAAsB,YAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/files/types.ts"],"mappings":";;;;;;;;;UAOiB,YAAA;EAAY;EAE3B,aAAA;EAOmB;EALnB,kBAAA,GAAqB,MAAA;EAArB;;;;EAKA,MAAA,GAAS,UAAA;AAAA;AAQX;;;;;AAAA,UAAiB,SAAA;EACf,IAAA,CAAK,aAAA,YAAyB,OAAA,CAAQ,cAAA;EACtC,IAAA,CAAK,QAAA,UAAkB,OAAA;IAAY,OAAA;EAAA,IAAqB,OAAA;EACxD,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,gBAAA;EACpC,MAAA,CAAO,QAAA,WAAmB,OAAA;EAC1B,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,YAAA;EACpC,MAAA,CACE,QAAA,UACA,QAAA,EAAU,cAAA,GAAiB,MAAA,WAC3B,OAAA;IAAY,SAAA;EAAA,IACX,OAAA;EACH,eAAA,CAAgB,aAAA,WAAwB,OAAA;EACxC,MAAA,CAAO,QAAA,WAAmB,OAAA;EAC1B,OAAA,CAAQ,QAAA,WAAmB,OAAA,CAAQ,WAAA;AAAA;;;;UAMpB,YAAA,SAAqB,gBAAA;EAjBpC;EAmBA,OAAA;EAnBmC;EAqBnC,OAAA,GAAU,MAAA,SAAe,YAAA;EArB+B;EAuBxD,kBAAA,GAAqB,MAAA;EAtBZ;EAwBT,aAAA;AAAA;;KAIU,cAAA,GAAiB,KAAA,CAAM,cAAA;;KAGvB,gBAAA,GAAmB,KAAA,CAAM,gBAAA;;;;UAKpB,YAAA;EAhCb;EAkCF,aAAA;EAjC6B;EAmC7B,WAAA;EAlCc;EAoCd,YAAA;AAAA;;;;UAMe,WAAA,SAAoB,YAAA;EAvC5B;EAyCP,WAAA;EAxCA;EA0CA,MAAA;EA1C2B;EA4C3B,OAAA;AAAA;;AAtCF;;;;;;KAgDY,YAAA,GAAe,SAAA;EACzB,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,SAAA;AAAA;;;;;;;;;;;AArChC;;;;;AAGA;;UAsDiB,WAAA;EAAA,CACd,SAAA,WAAoB,YAAA;EACrB,MAAA,GAAS,SAAA,aAAsB,YAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../../src/plugins/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { mergeConfigDedup } from \"@/utils\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { appKitServingTypesPlugin } from \"../../type-generator/serving/vite-plugin\";\nimport { appKitTypesPlugin } from \"../../type-generator/vite-plugin\";\nimport { BaseServer } from \"./base-server\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const viteClientPort = process.env.VITE_CLIENT_PORT;\n const serverHmr = viteClientPort\n ? { hmr: { clientPort: viteClientPort } }\n : {};\n\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n ...serverHmr,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [\n react.default(),\n appKitTypesPlugin(),\n appKitServingTypesPlugin(),\n ],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;aAK2C;AAO3C,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAC5C,AAAQ;CAER,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;EAWxC,MAAM,cATe,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE;EAC7C,MAAM,iBAAiB,QAAQ,IAAI;AAyBnC,OAAK,OAAO,MAAM,iBADI,iBAAiB,YAnBpB;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,GATc,iBACd,EAAE,KAAK,EAAE,YAAY,gBAAgB,EAAE,GACvC,EAAE;IAQF,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS;IACP,MAAM,SAAS;IACf,mBAAmB;IACnB,0BAA0B;IAC3B;GACD,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
1
+ {"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../../src/plugins/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { appKitServingTypesPlugin } from \"../../type-generator/serving/vite-plugin\";\nimport { appKitTypesPlugin } from \"../../type-generator/vite-plugin\";\nimport { mergeConfigDedup } from \"../../utils\";\nimport { BaseServer } from \"./base-server\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const viteClientPort = process.env.VITE_CLIENT_PORT;\n const serverHmr = viteClientPort\n ? { hmr: { clientPort: viteClientPort } }\n : {};\n\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n ...serverHmr,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [\n react.default(),\n appKitTypesPlugin(),\n appKitServingTypesPlugin(),\n ],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;aAI2C;AAQ3C,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAC5C,AAAQ;CAER,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;EAWxC,MAAM,cATe,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE;EAC7C,MAAM,iBAAiB,QAAQ,IAAI;AAyBnC,OAAK,OAAO,MAAM,iBADI,iBAAiB,YAnBpB;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,GATc,iBACd,EAAE,KAAK,EAAE,YAAY,gBAAgB,EAAE,GACvC,EAAE;IAQF,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS;IACP,MAAM,SAAS;IACf,mBAAmB;IACnB,0BAA0B;IAC3B;GACD,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"serving.d.ts","names":[],"sources":["../../../src/plugins/serving/serving.ts"],"mappings":";;;;;;;;;;;cAyCa,aAAA,SAAsB,MAAA;EAAA,OAC1B,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,cAAA;EAAA,iBAET,SAAA;EAAA,iBACA,WAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,cAAA;EAed,KAAA,CAAA,GAAS,OAAA;EAAA,OAiBR,uBAAA,CACL,MAAA,EAAQ,cAAA,GACP,mBAAA;EAAA,QAqBK,gBAAA;EA0BR,YAAA,CAAa,MAAA,EAAQ,UAAA;EA0Cf,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA0BG,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2DG,MAAA,CACJ,KAAA,UACA,IAAA,EAAM,MAAA,oBACL,OAAA,CAAQ,eAAA;EAiBX,YAAA,CAAA,GAAgB,MAAA;EAOV,QAAA,CAAA,GAAY,OAAA;EAAA,UAIR,iBAAA,CAAkB,KAAA,WAAgB,sBAAA;EAM5C,OAAA,CAAA,GAAW,cAAA;AAAA;;;;cAmBA,OAAA,EAAO,QAAA,QAAA,aAAA,EAAA,cAAA"}
1
+ {"version":3,"file":"serving.d.ts","names":[],"sources":["../../../src/plugins/serving/serving.ts"],"mappings":";;;;;;;;;;;cAyCa,aAAA,SAAsB,MAAA;EAAA,OAC1B,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,cAAA;EAAA,iBAET,SAAA;EAAA,iBACA,WAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,cAAA;EAed,KAAA,CAAA,GAAS,OAAA;EAAA,OAiBR,uBAAA,CACL,MAAA,EAAQ,cAAA,GACP,mBAAA;EAAA,QAqBK,gBAAA;EA0BR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAgEf,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA0BG,aAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2DG,MAAA,CACJ,KAAA,UACA,IAAA,EAAM,MAAA,oBACL,OAAA,CAAQ,eAAA;EAiBX,YAAA,CAAA,GAAgB,MAAA;EAOV,QAAA,CAAA,GAAY,OAAA;EAAA,UAIR,iBAAA,CAAkB,KAAA,WAAgB,sBAAA;EAM5C,OAAA,CAAA,GAAW,cAAA;AAAA;;;;cAmBA,OAAA,EAAO,QAAA,QAAA,aAAA,EAAA,cAAA"}
@@ -93,23 +93,37 @@ var ServingPlugin = class extends Plugin {
93
93
  }
94
94
  });
95
95
  } else {
96
+ const invokeHandler = async (req, res) => {
97
+ req.params.alias ??= "default";
98
+ await this.asUser(req)._handleInvoke(req, res);
99
+ };
100
+ const streamHandler = async (req, res) => {
101
+ req.params.alias ??= "default";
102
+ await this.asUser(req)._handleStream(req, res);
103
+ };
96
104
  this.route(router, {
97
105
  name: "invoke",
98
106
  method: "post",
99
107
  path: "/invoke",
100
- handler: async (req, res) => {
101
- req.params.alias = "default";
102
- await this.asUser(req)._handleInvoke(req, res);
103
- }
108
+ handler: invokeHandler
109
+ });
110
+ this.route(router, {
111
+ name: "invoke-named",
112
+ method: "post",
113
+ path: "/:alias/invoke",
114
+ handler: invokeHandler
104
115
  });
105
116
  this.route(router, {
106
117
  name: "stream",
107
118
  method: "post",
108
119
  path: "/stream",
109
- handler: async (req, res) => {
110
- req.params.alias = "default";
111
- await this.asUser(req)._handleStream(req, res);
112
- }
120
+ handler: streamHandler
121
+ });
122
+ this.route(router, {
123
+ name: "stream-named",
124
+ method: "post",
125
+ path: "/:alias/stream",
126
+ handler: streamHandler
113
127
  });
114
128
  }
115
129
  }
@@ -1 +1 @@
1
- {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"serving\">;\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n protected declare config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n req.params.alias = \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n req.params.alias = \"default\";\n await this.asUser(req)._handleStream(req, res);\n },\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Content-Encoding\", \"none\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"serving\">;\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n protected declare config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Content-Encoding\", \"none\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}