@databricks/appkit 0.36.0 → 0.38.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 (160) hide show
  1. package/CLAUDE.md +3 -2
  2. package/NOTICE.md +1 -0
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/cache/index.js +1 -2
  5. package/dist/cache/index.js.map +1 -1
  6. package/dist/cache/storage/persistent.js +1 -2
  7. package/dist/cache/storage/persistent.js.map +1 -1
  8. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  9. package/dist/cli/commands/plugin/create/resource-defaults.js +2 -1
  10. package/dist/cli/commands/plugin/create/resource-defaults.js.map +1 -1
  11. package/dist/cli/commands/plugin/schema-resources.js +34 -51
  12. package/dist/cli/commands/plugin/schema-resources.js.map +1 -1
  13. package/dist/cli/commands/plugin/sync/sync.js +20 -6
  14. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  15. package/dist/cli/commands/plugin/validate/validate-manifest.js +71 -157
  16. package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
  17. package/dist/cli/commands/plugin/validate/validate.js +2 -2
  18. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  19. package/dist/cli/commands/setup.js +2 -2
  20. package/dist/cli/commands/setup.js.map +1 -1
  21. package/dist/connectors/index.js +0 -1
  22. package/dist/connectors/jobs/client.js +1 -2
  23. package/dist/connectors/jobs/client.js.map +1 -1
  24. package/dist/connectors/lakebase/routing-pool.js +1 -2
  25. package/dist/connectors/lakebase/routing-pool.js.map +1 -1
  26. package/dist/connectors/sql-warehouse/client.js +1 -2
  27. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  28. package/dist/context/execution-context.js +9 -13
  29. package/dist/context/execution-context.js.map +1 -1
  30. package/dist/context/index.js +3 -13
  31. package/dist/context/service-context.js +137 -136
  32. package/dist/context/service-context.js.map +1 -1
  33. package/dist/context/user-context.js +1 -5
  34. package/dist/context/user-context.js.map +1 -1
  35. package/dist/core/agent/create-agent.js +1 -2
  36. package/dist/core/agent/create-agent.js.map +1 -1
  37. package/dist/core/appkit.d.ts.map +1 -1
  38. package/dist/core/appkit.js +9 -37
  39. package/dist/core/appkit.js.map +1 -1
  40. package/dist/errors/authentication.js +44 -40
  41. package/dist/errors/authentication.js.map +1 -1
  42. package/dist/errors/base.js +86 -70
  43. package/dist/errors/base.js.map +1 -1
  44. package/dist/errors/configuration.js +70 -66
  45. package/dist/errors/configuration.js.map +1 -1
  46. package/dist/errors/connection.js +50 -46
  47. package/dist/errors/connection.js.map +1 -1
  48. package/dist/errors/execution.js +47 -43
  49. package/dist/errors/execution.js.map +1 -1
  50. package/dist/errors/index.js +10 -27
  51. package/dist/errors/initialization.js +38 -34
  52. package/dist/errors/initialization.js.map +1 -1
  53. package/dist/errors/server.js +34 -31
  54. package/dist/errors/server.js.map +1 -1
  55. package/dist/errors/tunnel.js +47 -43
  56. package/dist/errors/tunnel.js.map +1 -1
  57. package/dist/errors/validation.js +41 -37
  58. package/dist/errors/validation.js.map +1 -1
  59. package/dist/index.d.ts +1 -1
  60. package/dist/index.js +3 -9
  61. package/dist/plugin/dev-reader.js +1 -2
  62. package/dist/plugin/dev-reader.js.map +1 -1
  63. package/dist/plugin/interceptors/retry.js +1 -2
  64. package/dist/plugin/interceptors/retry.js.map +1 -1
  65. package/dist/plugin/interceptors/telemetry.js +1 -2
  66. package/dist/plugin/interceptors/telemetry.js.map +1 -1
  67. package/dist/plugin/plugin.d.ts +12 -4
  68. package/dist/plugin/plugin.d.ts.map +1 -1
  69. package/dist/plugin/plugin.js +59 -28
  70. package/dist/plugin/plugin.js.map +1 -1
  71. package/dist/plugins/agents/agents.d.ts +56 -3
  72. package/dist/plugins/agents/agents.d.ts.map +1 -1
  73. package/dist/plugins/agents/agents.js +160 -8
  74. package/dist/plugins/agents/agents.js.map +1 -1
  75. package/dist/plugins/agents/schemas.js +0 -1
  76. package/dist/plugins/agents/schemas.js.map +1 -1
  77. package/dist/plugins/analytics/analytics.js +1 -2
  78. package/dist/plugins/analytics/analytics.js.map +1 -1
  79. package/dist/plugins/analytics/manifest.js +9 -1
  80. package/dist/plugins/analytics/query.js +2 -4
  81. package/dist/plugins/analytics/query.js.map +1 -1
  82. package/dist/plugins/files/manifest.js +7 -1
  83. package/dist/plugins/files/plugin.js +3 -6
  84. package/dist/plugins/files/plugin.js.map +1 -1
  85. package/dist/plugins/genie/genie.js +1 -2
  86. package/dist/plugins/genie/genie.js.map +1 -1
  87. package/dist/plugins/genie/manifest.js +7 -1
  88. package/dist/plugins/jobs/plugin.js +2 -4
  89. package/dist/plugins/jobs/plugin.js.map +1 -1
  90. package/dist/plugins/lakebase/lakebase.js +1 -2
  91. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  92. package/dist/plugins/lakebase/manifest.js +26 -4
  93. package/dist/plugins/server/index.js +1 -2
  94. package/dist/plugins/server/index.js.map +1 -1
  95. package/dist/plugins/server/vite-dev-server.js +1 -2
  96. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  97. package/dist/plugins/serving/serving.js +1 -2
  98. package/dist/plugins/serving/serving.js.map +1 -1
  99. package/dist/registry/index.d.ts +1 -1
  100. package/dist/registry/manifest-loader.d.ts +4 -4
  101. package/dist/registry/manifest-loader.d.ts.map +1 -1
  102. package/dist/registry/manifest-loader.js +1 -2
  103. package/dist/registry/manifest-loader.js.map +1 -1
  104. package/dist/registry/resource-registry.js +1 -2
  105. package/dist/registry/resource-registry.js.map +1 -1
  106. package/dist/registry/types.d.ts +21 -7
  107. package/dist/registry/types.d.ts.map +1 -1
  108. package/dist/registry/types.generated.d.ts +1 -1
  109. package/dist/registry/types.generated.js +1 -1
  110. package/dist/registry/types.generated.js.map +1 -1
  111. package/dist/registry/types.js +3 -3
  112. package/dist/registry/types.js.map +1 -1
  113. package/dist/schemas/manifest.d.ts +1139 -0
  114. package/dist/schemas/manifest.d.ts.map +1 -0
  115. package/dist/schemas/manifest.js +524 -0
  116. package/dist/schemas/manifest.js.map +1 -0
  117. package/dist/shared/src/index.d.ts +1 -1
  118. package/dist/shared/src/plugin.d.ts +38 -7
  119. package/dist/shared/src/plugin.d.ts.map +1 -1
  120. package/dist/shared/src/schemas/manifest.d.ts +1110 -0
  121. package/dist/shared/src/schemas/manifest.d.ts.map +1 -0
  122. package/dist/stream/arrow-stream-processor.js +1 -2
  123. package/dist/stream/arrow-stream-processor.js.map +1 -1
  124. package/dist/stream/buffers.js +1 -2
  125. package/dist/stream/buffers.js.map +1 -1
  126. package/docs/api/appkit/Enumeration.ResourceType.md +1 -1
  127. package/docs/api/appkit/Interface.PluginManifest.md +57 -3
  128. package/docs/api/appkit/Interface.ResourceEntry.md +6 -4
  129. package/docs/api/appkit/Interface.ResourceRequirement.md +7 -61
  130. package/docs/api/appkit/TypeAlias.ResourceFieldEntry.md +6 -0
  131. package/docs/api/appkit/Variable.agents.md +1 -1
  132. package/docs/api/appkit.md +12 -12
  133. package/docs/app-management.md +1 -1
  134. package/docs/development/ai-assisted-development.md +2 -2
  135. package/docs/development/local-development.md +1 -1
  136. package/docs/development/remote-bridge.md +1 -1
  137. package/docs/development/templates.md +118 -12
  138. package/docs/development.md +1 -1
  139. package/docs/plugins/agents.md +8 -4
  140. package/docs/plugins/custom-plugins.md +33 -23
  141. package/docs/plugins/lakebase.md +1 -1
  142. package/docs/plugins/manifest.md +293 -0
  143. package/docs.md +2 -2
  144. package/llms.txt +3 -2
  145. package/package.json +3 -3
  146. package/sbom.cdx.json +1 -1
  147. package/dist/_virtual/_rolldown/runtime.js +0 -7
  148. package/dist/connectors/lakebase-v1/client.js +0 -15
  149. package/dist/connectors/lakebase-v1/client.js.map +0 -1
  150. package/dist/connectors/lakebase-v1/index.js +0 -3
  151. package/dist/context/index.js.map +0 -1
  152. package/dist/errors/index.js.map +0 -1
  153. package/dist/index.js.map +0 -1
  154. package/dist/schemas/plugin-manifest.generated.d.ts +0 -182
  155. package/dist/schemas/plugin-manifest.generated.d.ts.map +0 -1
  156. package/dist/schemas/plugin-manifest.schema.json +0 -489
  157. package/dist/schemas/template-plugins.schema.json +0 -113
  158. package/dist/shared/src/schemas/plugin-manifest.generated.d.ts +0 -182
  159. package/dist/shared/src/schemas/plugin-manifest.generated.d.ts.map +0 -1
  160. package/docs/api/appkit/Interface.ResourceFieldEntry.md +0 -82
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":["manifest"],"sources":["../../../src/plugins/jobs/plugin.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport type { jobs as jobsTypes } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRequest,\n IAppRouter,\n PluginExecutionSettings,\n StreamExecutionSettings,\n} from \"shared\";\nimport { toJSONSchema } from \"zod\";\nimport { JobsConnector } from \"../../connectors/jobs\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { ExecutionError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { ExecutionResult } from \"../../plugin\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport {\n JOBS_READ_DEFAULTS,\n JOBS_STREAM_DEFAULTS,\n JOBS_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { mapParams } from \"./params\";\nimport type {\n IJobsConfig,\n JobAPI,\n JobConfig,\n JobHandle,\n JobRunStatus,\n JobsExport,\n} from \"./types\";\n\nconst logger = createLogger(\"jobs\");\n\nconst DEFAULT_WAIT_TIMEOUT = 600_000;\nconst DEFAULT_POLL_INTERVAL = 5_000;\n/** Cap on param-key count when a job has no Zod schema. Jobs that need more keys must define a schema. */\nconst MAX_UNVALIDATED_PARAM_KEYS = 50;\n\n/** Replace upstream error messages with generic descriptions keyed by HTTP status. */\nfunction errorResult(status: number): ExecutionResult<never> {\n return {\n ok: false,\n status,\n message: STATUS_CODES[status] ?? \"Request failed\",\n };\n}\n\nfunction isTerminalRunState(state: string | undefined): boolean {\n return (\n state === \"TERMINATED\" || state === \"SKIPPED\" || state === \"INTERNAL_ERROR\"\n );\n}\n\n/** Exponential backoff (1.5x) with +/- 20% jitter, capped at `max`. */\nfunction nextPollDelay(\n current: number,\n max: number,\n): { delay: number; next: number } {\n const jitter = 1 + (Math.random() * 0.4 - 0.2);\n return {\n delay: Math.min(current * jitter, max),\n next: Math.min(current * 1.5, max),\n };\n}\n\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve) => {\n if (signal?.aborted) {\n resolve();\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nclass JobsPlugin extends Plugin {\n static manifest = manifest as PluginManifest;\n\n protected declare config: IJobsConfig;\n private connector: JobsConnector;\n private jobIds: Record<string, number> = {};\n private jobConfigs: Record<string, JobConfig> = {};\n private jobKeys: string[] = [];\n\n /**\n * Scans process.env for DATABRICKS_JOB_* keys and merges with explicit config.\n * Explicit config wins for per-job overrides; auto-discovered jobs get default `{}` config.\n */\n static discoverJobs(config: IJobsConfig): Record<string, JobConfig> {\n const explicit = config.jobs ?? {};\n const discovered: Record<string, JobConfig> = {};\n\n const prefix = \"DATABRICKS_JOB_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n if (key === \"DATABRICKS_JOB_ID\") continue;\n const suffix = key.slice(prefix.length);\n if (!suffix || !process.env[key]) continue;\n const jobKey = suffix.toLowerCase();\n if (!(jobKey in explicit)) {\n discovered[jobKey] = {};\n }\n }\n\n // Single-job shorthand: DATABRICKS_JOB_ID maps to \"default\" key\n if (\n process.env.DATABRICKS_JOB_ID &&\n Object.keys(explicit).length === 0 &&\n Object.keys(discovered).length === 0\n ) {\n discovered.default = {};\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured jobs.\n * Each job key maps to a `DATABRICKS_JOB_{KEY_UPPERCASE}` env var (or `DATABRICKS_JOB_ID` for \"default\").\n */\n static getResourceRequirements(config: IJobsConfig): ResourceRequirement[] {\n const jobs = JobsPlugin.discoverJobs(config);\n return Object.keys(jobs).map((key) => ({\n type: ResourceType.JOB,\n alias: `job-${key}`,\n resourceKey: `job-${key}`,\n description: `Databricks Job \"${key}\"`,\n permission: \"CAN_MANAGE_RUN\" as const,\n fields: {\n id: {\n env:\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`,\n description: `Job ID for \"${key}\"`,\n },\n },\n required: true,\n }));\n }\n\n constructor(config: IJobsConfig) {\n super(config);\n this.config = config;\n this.connector = new JobsConnector({\n telemetry: config.telemetry,\n });\n\n const jobs = JobsPlugin.discoverJobs(config);\n this.jobKeys = Object.keys(jobs);\n this.jobConfigs = jobs;\n\n for (const key of this.jobKeys) {\n const envVar =\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`;\n const jobIdStr = process.env[envVar];\n if (jobIdStr) {\n const parsed = Number.parseInt(jobIdStr, 10);\n if (!Number.isNaN(parsed)) {\n this.jobIds[key] = parsed;\n }\n }\n }\n }\n\n async setup() {\n logger.info(\n `Jobs plugin initialized with ${this.jobKeys.length} job(s): ${this.jobKeys.join(\", \")}`,\n );\n }\n\n private get client() {\n return getWorkspaceClient();\n }\n\n private getJobId(jobKey: string): number {\n const id = this.jobIds[jobKey];\n if (!id) {\n const envVar =\n jobKey === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${jobKey.toUpperCase()}`;\n throw new Error(\n `Job \"${jobKey}\" has no configured job ID. Set ${envVar} env var.`,\n );\n }\n return id;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_READ_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n cache: { ...JOBS_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n private _writeSettings(): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_WRITE_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n },\n };\n }\n\n /**\n * Validates params against the job's Zod schema (if any) and maps them\n * to SDK request fields based on the task type. Shared by runNow and runAndWait.\n */\n private _validateAndMap(\n jobKey: string,\n params?: Record<string, unknown>,\n ): Record<string, unknown> {\n const jobConfig = this.jobConfigs[jobKey];\n let validated = params;\n\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(params ?? {});\n if (!result.success) {\n throw new ValidationError(\n `Parameter validation failed for job \"${jobKey}\": ${result.error.message}`,\n );\n }\n validated = result.data as Record<string, unknown>;\n }\n\n return jobConfig?.taskType && validated\n ? mapParams(jobConfig.taskType, validated)\n : (validated ?? {});\n }\n\n /**\n * Creates a JobAPI for a specific configured job key.\n * Each method is scoped to the job's configured ID.\n */\n protected createJobAPI(jobKey: string): JobAPI {\n const jobId = this.getJobId(jobKey);\n const jobConfig = this.jobConfigs[jobKey];\n // Capture `this` for use in the async generator\n const self = this;\n // Eagerly capture the client and userId so that when createJobAPI is\n // called inside an asUser() proxy (which runs in user context), the\n // closures below use the user-scoped client instead of falling back\n // to the service principal when the ALS context has already exited.\n const client = this.client;\n const userKey = getCurrentUserId();\n\n /**\n * Verify that `runId` belongs to this job's configured `jobId`. Returns\n * null if the run is in scope; otherwise returns a 404 `ExecutionResult`.\n * Prevents cross-job access via the `/:jobKey/runs/:runId` HTTP surface.\n */\n const verifyRunScope = async (\n runId: number,\n ): Promise<ExecutionResult<never> | null> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return null;\n };\n\n return {\n runNow: async (\n params?: Record<string, unknown>,\n ): Promise<ExecutionResult<jobsTypes.RunNowResponse>> => {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const result = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n async *runAndWait(\n params?: Record<string, unknown>,\n signal?: AbortSignal,\n ): AsyncGenerator<JobRunStatus, void, unknown> {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const runResult = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n\n if (!runResult.ok) {\n throw new ExecutionError(\"Failed to trigger job run\");\n }\n const runId = runResult.data.run_id;\n if (!runId) {\n throw new Error(\"runNow did not return a run_id\");\n }\n\n const basePollInterval =\n self.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;\n const maxPollInterval = basePollInterval * 6;\n const timeout = jobConfig?.waitTimeout ?? DEFAULT_WAIT_TIMEOUT;\n const startTime = Date.now();\n let currentInterval = basePollInterval;\n\n while (!signal?.aborted) {\n if (Date.now() - startTime > timeout) {\n throw new Error(\n `Job run ${runId} polling timeout after ${timeout}ms`,\n );\n }\n\n const runStatusResult = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n {\n default: {\n ...JOBS_READ_DEFAULTS,\n cache: { enabled: false },\n },\n },\n userKey,\n );\n if (!runStatusResult.ok) {\n throw new ExecutionError(\n `Failed to poll run status for run ${runId}`,\n );\n }\n const run = runStatusResult.data;\n const state = run.state?.life_cycle_state;\n\n yield { status: state, timestamp: Date.now(), run };\n\n if (isTerminalRunState(state)) return;\n\n const { delay, next } = nextPollDelay(\n currentInterval,\n maxPollInterval,\n );\n currentInterval = next;\n await abortableSleep(delay, signal);\n }\n },\n\n lastRun: async (): Promise<\n ExecutionResult<jobsTypes.BaseRun | undefined>\n > => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: 1 },\n signal,\n ),\n self._readSettings([\"jobs:lastRun\", jobKey]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n return { ok: true, data: result.data[0] };\n },\n\n listRuns: async (options?: {\n limit?: number;\n }): Promise<ExecutionResult<jobsTypes.BaseRun[]>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: options?.limit },\n signal,\n ),\n self._readSettings([\n \"jobs:listRuns\",\n jobKey,\n options?.limit ?? \"default\",\n ]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getRun: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.Run>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return result;\n },\n\n getRunOutput: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.RunOutput>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.getRunOutput(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRunOutput\", jobKey, runId]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n cancelRun: async (runId: number): Promise<ExecutionResult<void>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.cancelRun(client, { run_id: runId }, signal),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getJob: async (): Promise<ExecutionResult<jobsTypes.Job>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getJob(client, { job_id: jobId }, signal),\n self._readSettings([\"jobs:getJob\", jobKey]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n };\n }\n\n /**\n * Resolve `:jobKey` from the request. Returns the key and ID,\n * or sends a 404 and returns `{ jobKey: undefined, jobId: undefined }`.\n */\n private _resolveJob(\n req: express.Request,\n res: express.Response,\n ):\n | { jobKey: string; jobId: number }\n | { jobKey: undefined; jobId: undefined } {\n const jobKey = req.params.jobKey;\n if (!this.jobKeys.includes(jobKey)) {\n const safeKey = jobKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown job \"${safeKey}\"`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n const jobId = this.jobIds[jobKey];\n if (!jobId) {\n res.status(404).json({\n error: `Job \"${jobKey}\" has no configured job ID`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n return { jobKey, jobId };\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 /**\n * Validate params from an HTTP request body. Eager validation lets streaming\n * requests get a clean 400 instead of a generic SSE error event. Throws\n * ValidationError so handlers can map to a 400 response via their catch block.\n */\n private _parseRunParams(\n jobKey: string,\n rawParams: unknown,\n ): Record<string, unknown> | undefined {\n if (\n rawParams !== undefined &&\n (typeof rawParams !== \"object\" ||\n rawParams === null ||\n Array.isArray(rawParams))\n ) {\n throw new ValidationError(\"params must be a plain object\");\n }\n\n const jobConfig = this.jobConfigs[jobKey];\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(rawParams ?? {});\n if (!result.success) {\n throw new ValidationError(\"Invalid job parameters\");\n }\n // Pass rawParams — not result.data — to avoid double-transforming\n // when _validateAndMap calls safeParse again downstream.\n return rawParams as Record<string, unknown>;\n }\n // No schema. Either reject (no taskType) or enforce a key cap so that\n // untrusted clients can't spread arbitrarily many fields into the SDK.\n if (rawParams !== undefined) {\n if (!jobConfig?.taskType) {\n throw new ValidationError(\"This job does not accept parameters\");\n }\n const keyCount = Object.keys(rawParams as Record<string, unknown>).length;\n if (keyCount > MAX_UNVALIDATED_PARAM_KEYS) {\n throw new ValidationError(\n `Too many parameters (${keyCount}). Define a Zod schema to accept more than ${MAX_UNVALIDATED_PARAM_KEYS}.`,\n );\n }\n }\n return rawParams as Record<string, unknown> | undefined;\n }\n\n private async _handleRun(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const stream = req.query.stream === \"true\";\n\n try {\n const params = this._parseRunParams(jobKey, req.body?.params);\n const api = this.createJobAPI(jobKey);\n\n if (stream) {\n const streamSettings: StreamExecutionSettings = {\n default: JOBS_STREAM_DEFAULTS,\n };\n await this.executeStream<JobRunStatus>(\n res,\n (signal) => api.runAndWait(params, signal),\n streamSettings,\n );\n } else {\n const result = await api.runNow(params);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runId: result.data.run_id });\n }\n } catch (error) {\n if (error instanceof ValidationError) {\n if (!res.headersSent) {\n res.status(400).json({ error: error.message, plugin: this.name });\n }\n return;\n }\n logger.error(\"Run failed for job %s: %O\", jobKey, error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Run failed\", plugin: this.name });\n }\n }\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"run\",\n method: \"post\",\n path: \"/:jobKey/run\",\n handler: (req, res) => this._handleRun(req, res),\n });\n\n // GET /:jobKey/runs\n this.route(router, {\n name: \"runs\",\n method: \"get\",\n path: \"/:jobKey/runs\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const limit = Math.max(\n 1,\n Math.min(Number.parseInt(req.query.limit as string, 10) || 20, 100),\n );\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.listRuns({ limit });\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runs: result.data });\n } catch (error) {\n logger.error(\"List runs failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"List runs failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/runs/:runId\n this.route(router, {\n name: \"run-detail\",\n method: \"get\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.getRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\n } catch (error) {\n logger.error(\n \"Get run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res.status(500).json({ error: \"Get run failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/status\n this.route(router, {\n name: \"status\",\n method: \"get\",\n path: \"/:jobKey/status\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.lastRun();\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({\n status: result.data?.state?.life_cycle_state ?? null,\n run: result.data ?? null,\n });\n } catch (error) {\n logger.error(\"Status check failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"Status check failed\", plugin: this.name });\n }\n },\n });\n\n // DELETE /:jobKey/runs/:runId\n this.route(router, {\n name: \"cancel-run\",\n method: \"delete\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.cancelRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.status(204).end();\n } catch (error) {\n logger.error(\n \"Cancel run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res\n .status(500)\n .json({ error: \"Cancel run failed\", plugin: this.name });\n }\n },\n });\n }\n\n exports(): JobsExport {\n const resolveJob = (jobKey: string): JobHandle => {\n if (!this.jobKeys.includes(jobKey)) {\n throw new Error(\n `Unknown job \"${jobKey}\". Available jobs: ${this.jobKeys.join(\", \")}`,\n );\n }\n\n const spApi = this.createJobAPI(jobKey);\n\n return {\n ...spApi,\n asUser: (req: IAppRequest) => {\n const userPlugin = this.asUser(req) as JobsPlugin;\n return userPlugin.createJobAPI(jobKey);\n },\n };\n };\n\n return resolveJob as JobsExport;\n }\n\n clientConfig(): Record<string, unknown> {\n const jobs: Record<string, { params: unknown; taskType: string | null }> =\n {};\n for (const key of this.jobKeys) {\n const config = this.jobConfigs[key];\n jobs[key] = {\n params: config?.params ? toJSONSchema(config.params) : null,\n taskType: config?.taskType ?? null,\n };\n }\n return { jobs };\n }\n}\n\n/**\n * @internal\n */\nexport const jobs = toPlugin(JobsPlugin);\n\n/**\n * @internal\n */\nexport { JobsPlugin };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;cAWqE;aACN;AAsB/D,MAAM,SAAS,aAAa,OAAO;AAEnC,MAAM,uBAAuB;AAC7B,MAAM,wBAAwB;;AAE9B,MAAM,6BAA6B;;AAGnC,SAAS,YAAY,QAAwC;AAC3D,QAAO;EACL,IAAI;EACJ;EACA,SAAS,aAAa,WAAW;EAClC;;AAGH,SAAS,mBAAmB,OAAoC;AAC9D,QACE,UAAU,gBAAgB,UAAU,aAAa,UAAU;;;AAK/D,SAAS,cACP,SACA,KACiC;CACjC,MAAM,SAAS,KAAK,KAAK,QAAQ,GAAG,KAAM;AAC1C,QAAO;EACL,OAAO,KAAK,IAAI,UAAU,QAAQ,IAAI;EACtC,MAAM,KAAK,IAAI,UAAU,KAAK,IAAI;EACnC;;AAGH,SAAS,eAAe,IAAY,QAAqC;AACvE,QAAO,IAAI,SAAe,YAAY;AACpC,MAAI,QAAQ,SAAS;AACnB,YAAS;AACT;;EAEF,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;AAGJ,IAAM,aAAN,MAAM,mBAAmB,OAAO;CAC9B,OAAO,WAAWA;CAGlB,AAAQ;CACR,AAAQ,SAAiC,EAAE;CAC3C,AAAQ,aAAwC,EAAE;CAClD,AAAQ,UAAoB,EAAE;;;;;CAM9B,OAAO,aAAa,QAAgD;EAClE,MAAM,WAAW,OAAO,QAAQ,EAAE;EAClC,MAAM,aAAwC,EAAE;EAEhD,MAAM,SAAS;AACf,OAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,IAAI,EAAE;AAC1C,OAAI,CAAC,IAAI,WAAW,OAAO,CAAE;AAC7B,OAAI,QAAQ,oBAAqB;GACjC,MAAM,SAAS,IAAI,MAAM,GAAc;AACvC,OAAI,CAAC,UAAU,CAAC,QAAQ,IAAI,KAAM;GAClC,MAAM,SAAS,OAAO,aAAa;AACnC,OAAI,EAAE,UAAU,UACd,YAAW,UAAU,EAAE;;AAK3B,MACE,QAAQ,IAAI,qBACZ,OAAO,KAAK,SAAS,CAAC,WAAW,KACjC,OAAO,KAAK,WAAW,CAAC,WAAW,EAEnC,YAAW,UAAU,EAAE;AAGzB,SAAO;GAAE,GAAG;GAAY,GAAG;GAAU;;;;;;CAOvC,OAAO,wBAAwB,QAA4C;EACzE,MAAM,OAAO,WAAW,aAAa,OAAO;AAC5C,SAAO,OAAO,KAAK,KAAK,CAAC,KAAK,SAAS;GACrC,MAAM,aAAa;GACnB,OAAO,OAAO;GACd,aAAa,OAAO;GACpB,aAAa,mBAAmB,IAAI;GACpC,YAAY;GACZ,QAAQ,EACN,IAAI;IACF,KACE,QAAQ,YACJ,sBACA,kBAAkB,IAAI,aAAa;IACzC,aAAa,eAAe,IAAI;IACjC,EACF;GACD,UAAU;GACX,EAAE;;CAGL,YAAY,QAAqB;AAC/B,QAAM,OAAO;AACb,OAAK,SAAS;AACd,OAAK,YAAY,IAAI,cAAc,EACjC,WAAW,OAAO,WACnB,CAAC;EAEF,MAAM,OAAO,WAAW,aAAa,OAAO;AAC5C,OAAK,UAAU,OAAO,KAAK,KAAK;AAChC,OAAK,aAAa;AAElB,OAAK,MAAM,OAAO,KAAK,SAAS;GAC9B,MAAM,SACJ,QAAQ,YACJ,sBACA,kBAAkB,IAAI,aAAa;GACzC,MAAM,WAAW,QAAQ,IAAI;AAC7B,OAAI,UAAU;IACZ,MAAM,SAAS,OAAO,SAAS,UAAU,GAAG;AAC5C,QAAI,CAAC,OAAO,MAAM,OAAO,CACvB,MAAK,OAAO,OAAO;;;;CAM3B,MAAM,QAAQ;AACZ,SAAO,KACL,gCAAgC,KAAK,QAAQ,OAAO,WAAW,KAAK,QAAQ,KAAK,KAAK,GACvF;;CAGH,IAAY,SAAS;AACnB,SAAO,oBAAoB;;CAG7B,AAAQ,SAAS,QAAwB;EACvC,MAAM,KAAK,KAAK,OAAO;AACvB,MAAI,CAAC,IAAI;GACP,MAAM,SACJ,WAAW,YACP,sBACA,kBAAkB,OAAO,aAAa;AAC5C,SAAM,IAAI,MACR,QAAQ,OAAO,kCAAkC,OAAO,WACzD;;AAEH,SAAO;;CAGT,AAAQ,cACN,UACyB;AACzB,SAAO,EACL,SAAS;GACP,GAAG;GACH,GAAI,KAAK,OAAO,WAAW,QAAQ,EAAE,SAAS,KAAK,OAAO,SAAS;GACnE,OAAO;IAAE,GAAG,mBAAmB;IAAO;IAAU;GACjD,EACF;;CAGH,AAAQ,iBAA0C;AAChD,SAAO,EACL,SAAS;GACP,GAAG;GACH,GAAI,KAAK,OAAO,WAAW,QAAQ,EAAE,SAAS,KAAK,OAAO,SAAS;GACpE,EACF;;;;;;CAOH,AAAQ,gBACN,QACA,QACyB;EACzB,MAAM,YAAY,KAAK,WAAW;EAClC,IAAI,YAAY;AAEhB,MAAI,WAAW,QAAQ;GACrB,MAAM,SAAS,UAAU,OAAO,UAAU,UAAU,EAAE,CAAC;AACvD,OAAI,CAAC,OAAO,QACV,OAAM,IAAI,gBACR,wCAAwC,OAAO,KAAK,OAAO,MAAM,UAClE;AAEH,eAAY,OAAO;;AAGrB,SAAO,WAAW,YAAY,YAC1B,UAAU,UAAU,UAAU,UAAU,GACvC,aAAa,EAAE;;;;;;CAOtB,AAAU,aAAa,QAAwB;EAC7C,MAAM,QAAQ,KAAK,SAAS,OAAO;EACnC,MAAM,YAAY,KAAK,WAAW;EAElC,MAAM,OAAO;EAKb,MAAM,SAAS,KAAK;EACpB,MAAM,UAAU,kBAAkB;;;;;;EAOlC,MAAM,iBAAiB,OACrB,UAC2C;GAC3C,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,OAAO,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,EAC1D,KAAK,cAAc;IAAC;IAAe;IAAQ;IAAM,CAAC,EAClD,QACD;AACD,OAAI,CAAC,OAAO,GAAI,QAAO,YAAY,OAAO,OAAO;AACjD,OAAI,OAAO,KAAK,WAAW,MAAO,QAAO,YAAY,IAAI;AACzD,UAAO;;AAGT,SAAO;GACL,QAAQ,OACN,WACuD;IACvD,MAAM,YAAY,KAAK,gBAAgB,QAAQ,OAAO;IAEtD,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,OACb,QACA;KAAE,GAAG;KAAW,QAAQ;KAAO,EAC/B,OACD,EACH,KAAK,gBAAgB,EACrB,QACD;AACD,WAAO,OAAO,KAAK,SAAS,YAAY,OAAO,OAAO;;GAGxD,OAAO,WACL,QACA,QAC6C;IAC7C,MAAM,YAAY,KAAK,gBAAgB,QAAQ,OAAO;IAEtD,MAAM,YAAY,MAAM,KAAK,QAC3B,OAAO,WACL,KAAK,UAAU,OACb,QACA;KAAE,GAAG;KAAW,QAAQ;KAAO,EAC/B,OACD,EACH,KAAK,gBAAgB,EACrB,QACD;AAED,QAAI,CAAC,UAAU,GACb,OAAM,IAAI,eAAe,4BAA4B;IAEvD,MAAM,QAAQ,UAAU,KAAK;AAC7B,QAAI,CAAC,MACH,OAAM,IAAI,MAAM,iCAAiC;IAGnD,MAAM,mBACJ,KAAK,OAAO,kBAAkB;IAChC,MAAM,kBAAkB,mBAAmB;IAC3C,MAAM,UAAU,WAAW,eAAe;IAC1C,MAAM,YAAY,KAAK,KAAK;IAC5B,IAAI,kBAAkB;AAEtB,WAAO,CAAC,QAAQ,SAAS;AACvB,SAAI,KAAK,KAAK,GAAG,YAAY,QAC3B,OAAM,IAAI,MACR,WAAW,MAAM,yBAAyB,QAAQ,IACnD;KAGH,MAAM,kBAAkB,MAAM,KAAK,QACjC,OAAO,WACL,KAAK,UAAU,OAAO,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,EAC1D,EACE,SAAS;MACP,GAAG;MACH,OAAO,EAAE,SAAS,OAAO;MAC1B,EACF,EACD,QACD;AACD,SAAI,CAAC,gBAAgB,GACnB,OAAM,IAAI,eACR,qCAAqC,QACtC;KAEH,MAAM,MAAM,gBAAgB;KAC5B,MAAM,QAAQ,IAAI,OAAO;AAEzB,WAAM;MAAE,QAAQ;MAAO,WAAW,KAAK,KAAK;MAAE;MAAK;AAEnD,SAAI,mBAAmB,MAAM,CAAE;KAE/B,MAAM,EAAE,OAAO,SAAS,cACtB,iBACA,gBACD;AACD,uBAAkB;AAClB,WAAM,eAAe,OAAO,OAAO;;;GAIvC,SAAS,YAEJ;IACH,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,SACb,QACA;KAAE,QAAQ;KAAO,OAAO;KAAG,EAC3B,OACD,EACH,KAAK,cAAc,CAAC,gBAAgB,OAAO,CAAC,EAC5C,QACD;AACD,QAAI,CAAC,OAAO,GAAI,QAAO,YAAY,OAAO,OAAO;AACjD,WAAO;KAAE,IAAI;KAAM,MAAM,OAAO,KAAK;KAAI;;GAG3C,UAAU,OAAO,YAEoC;IACnD,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,SACb,QACA;KAAE,QAAQ;KAAO,OAAO,SAAS;KAAO,EACxC,OACD,EACH,KAAK,cAAc;KACjB;KACA;KACA,SAAS,SAAS;KACnB,CAAC,EACF,QACD;AACD,WAAO,OAAO,KAAK,SAAS,YAAY,OAAO,OAAO;;GAGxD,QAAQ,OACN,UAC4C;IAC5C,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,OAAO,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,EAC1D,KAAK,cAAc;KAAC;KAAe;KAAQ;KAAM,CAAC,EAClD,QACD;AACD,QAAI,CAAC,OAAO,GAAI,QAAO,YAAY,OAAO,OAAO;AACjD,QAAI,OAAO,KAAK,WAAW,MAAO,QAAO,YAAY,IAAI;AACzD,WAAO;;GAGT,cAAc,OACZ,UACkD;IAClD,MAAM,aAAa,MAAM,eAAe,MAAM;AAC9C,QAAI,WAAY,QAAO;IACvB,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,aAAa,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,EAChE,KAAK,cAAc;KAAC;KAAqB;KAAQ;KAAM,CAAC,EACxD,QACD;AACD,WAAO,OAAO,KAAK,SAAS,YAAY,OAAO,OAAO;;GAGxD,WAAW,OAAO,UAAkD;IAClE,MAAM,aAAa,MAAM,eAAe,MAAM;AAC9C,QAAI,WAAY,QAAO;IACvB,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,UAAU,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,EAC7D,KAAK,gBAAgB,EACrB,QACD;AACD,WAAO,OAAO,KAAK,SAAS,YAAY,OAAO,OAAO;;GAGxD,QAAQ,YAAqD;IAC3D,MAAM,SAAS,MAAM,KAAK,QACxB,OAAO,WACL,KAAK,UAAU,OAAO,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,EAC1D,KAAK,cAAc,CAAC,eAAe,OAAO,CAAC,EAC3C,QACD;AACD,WAAO,OAAO,KAAK,SAAS,YAAY,OAAO,OAAO;;GAEzD;;;;;;CAOH,AAAQ,YACN,KACA,KAG0C;EAC1C,MAAM,SAAS,IAAI,OAAO;AAC1B,MAAI,CAAC,KAAK,QAAQ,SAAS,OAAO,EAAE;GAClC,MAAM,UAAU,OAAO,QAAQ,mBAAmB,GAAG;AACrD,OAAI,OAAO,IAAI,CAAC,KAAK;IACnB,OAAO,gBAAgB,QAAQ;IAC/B,QAAQ,KAAK;IACd,CAAC;AACF,UAAO;IAAE,QAAQ;IAAW,OAAO;IAAW;;EAEhD,MAAM,QAAQ,KAAK,OAAO;AAC1B,MAAI,CAAC,OAAO;AACV,OAAI,OAAO,IAAI,CAAC,KAAK;IACnB,OAAO,QAAQ,OAAO;IACtB,QAAQ,KAAK;IACd,CAAC;AACF,UAAO;IAAE,QAAQ;IAAW,OAAO;IAAW;;AAEhD,SAAO;GAAE;GAAQ;GAAO;;CAG1B,AAAQ,iBAAiB,KAAuB,QAAsB;AACpE,MAAI,OAAO,OAAO,CAAC,KAAK;GACtB,OAAO,aAAa,WAAW;GAC/B,QAAQ,KAAK;GACd,CAAC;;;;;;;CAQJ,AAAQ,gBACN,QACA,WACqC;AACrC,MACE,cAAc,WACb,OAAO,cAAc,YACpB,cAAc,QACd,MAAM,QAAQ,UAAU,EAE1B,OAAM,IAAI,gBAAgB,gCAAgC;EAG5D,MAAM,YAAY,KAAK,WAAW;AAClC,MAAI,WAAW,QAAQ;AAErB,OAAI,CADW,UAAU,OAAO,UAAU,aAAa,EAAE,CAAC,CAC9C,QACV,OAAM,IAAI,gBAAgB,yBAAyB;AAIrD,UAAO;;AAIT,MAAI,cAAc,QAAW;AAC3B,OAAI,CAAC,WAAW,SACd,OAAM,IAAI,gBAAgB,sCAAsC;GAElE,MAAM,WAAW,OAAO,KAAK,UAAqC,CAAC;AACnE,OAAI,WAAW,2BACb,OAAM,IAAI,gBACR,wBAAwB,SAAS,6CAA6C,2BAA2B,GAC1G;;AAGL,SAAO;;CAGT,MAAc,WACZ,KACA,KACe;EACf,MAAM,EAAE,WAAW,KAAK,YAAY,KAAK,IAAI;AAC7C,MAAI,CAAC,OAAQ;EAEb,MAAM,SAAS,IAAI,MAAM,WAAW;AAEpC,MAAI;GACF,MAAM,SAAS,KAAK,gBAAgB,QAAQ,IAAI,MAAM,OAAO;GAC7D,MAAM,MAAM,KAAK,aAAa,OAAO;AAErC,OAAI,QAAQ;IACV,MAAM,iBAA0C,EAC9C,SAAS,sBACV;AACD,UAAM,KAAK,cACT,MACC,WAAW,IAAI,WAAW,QAAQ,OAAO,EAC1C,eACD;UACI;IACL,MAAM,SAAS,MAAM,IAAI,OAAO,OAAO;AACvC,QAAI,CAAC,OAAO,IAAI;AACd,UAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,QAAI,KAAK,EAAE,OAAO,OAAO,KAAK,QAAQ,CAAC;;WAElC,OAAO;AACd,OAAI,iBAAiB,iBAAiB;AACpC,QAAI,CAAC,IAAI,YACP,KAAI,OAAO,IAAI,CAAC,KAAK;KAAE,OAAO,MAAM;KAAS,QAAQ,KAAK;KAAM,CAAC;AAEnE;;AAEF,UAAO,MAAM,6BAA6B,QAAQ,MAAM;AACxD,OAAI,CAAC,IAAI,YACP,KAAI,OAAO,IAAI,CAAC,KAAK;IAAE,OAAO;IAAc,QAAQ,KAAK;IAAM,CAAC;;;CAKtE,aAAa,QAAoB;AAC/B,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,UAAU,KAAK,QAAQ,KAAK,WAAW,KAAK,IAAI;GACjD,CAAC;AAGF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,KAAK,YAAY,KAAK,IAAI;AAC7C,QAAI,CAAC,OAAQ;IAEb,MAAM,QAAQ,KAAK,IACjB,GACA,KAAK,IAAI,OAAO,SAAS,IAAI,MAAM,OAAiB,GAAG,IAAI,IAAI,IAAI,CACpE;AAED,QAAI;KAEF,MAAM,SAAS,MADH,KAAK,aAAa,OAAO,CACZ,SAAS,EAAE,OAAO,CAAC;AAC5C,SAAI,CAAC,OAAO,IAAI;AACd,WAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,SAAI,KAAK,EAAE,MAAM,OAAO,MAAM,CAAC;aACxB,OAAO;AACd,YAAO,MAAM,mCAAmC,QAAQ,MAAM;AAC9D,SACG,OAAO,IAAI,CACX,KAAK;MAAE,OAAO;MAAoB,QAAQ,KAAK;MAAM,CAAC;;;GAG9D,CAAC;AAGF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,KAAK,YAAY,KAAK,IAAI;AAC7C,QAAI,CAAC,OAAQ;IAEb,MAAM,QAAQ,OAAO,SAAS,IAAI,OAAO,OAAO,GAAG;AACnD,QAAI,OAAO,MAAM,MAAM,IAAI,SAAS,GAAG;AACrC,SAAI,OAAO,IAAI,CAAC,KAAK;MAAE,OAAO;MAAiB,QAAQ,KAAK;MAAM,CAAC;AACnE;;AAGF,QAAI;KAEF,MAAM,SAAS,MADH,KAAK,aAAa,OAAO,CACZ,OAAO,MAAM;AACtC,SAAI,CAAC,OAAO,IAAI;AACd,WAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,SAAI,KAAK,OAAO,KAAK;aACd,OAAO;AACd,YAAO,MACL,wCACA,QACA,OACA,MACD;AACD,SAAI,OAAO,IAAI,CAAC,KAAK;MAAE,OAAO;MAAkB,QAAQ,KAAK;MAAM,CAAC;;;GAGzE,CAAC;AAGF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,KAAK,YAAY,KAAK,IAAI;AAC7C,QAAI,CAAC,OAAQ;AAEb,QAAI;KAEF,MAAM,SAAS,MADH,KAAK,aAAa,OAAO,CACZ,SAAS;AAClC,SAAI,CAAC,OAAO,IAAI;AACd,WAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,SAAI,KAAK;MACP,QAAQ,OAAO,MAAM,OAAO,oBAAoB;MAChD,KAAK,OAAO,QAAQ;MACrB,CAAC;aACK,OAAO;AACd,YAAO,MAAM,sCAAsC,QAAQ,MAAM;AACjE,SACG,OAAO,IAAI,CACX,KAAK;MAAE,OAAO;MAAuB,QAAQ,KAAK;MAAM,CAAC;;;GAGjE,CAAC;AAGF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;IAC9D,MAAM,EAAE,WAAW,KAAK,YAAY,KAAK,IAAI;AAC7C,QAAI,CAAC,OAAQ;IAEb,MAAM,QAAQ,OAAO,SAAS,IAAI,OAAO,OAAO,GAAG;AACnD,QAAI,OAAO,MAAM,MAAM,IAAI,SAAS,GAAG;AACrC,SAAI,OAAO,IAAI,CAAC,KAAK;MAAE,OAAO;MAAiB,QAAQ,KAAK;MAAM,CAAC;AACnE;;AAGF,QAAI;KAEF,MAAM,SAAS,MADH,KAAK,aAAa,OAAO,CACZ,UAAU,MAAM;AACzC,SAAI,CAAC,OAAO,IAAI;AACd,WAAK,iBAAiB,KAAK,OAAO,OAAO;AACzC;;AAEF,SAAI,OAAO,IAAI,CAAC,KAAK;aACd,OAAO;AACd,YAAO,MACL,2CACA,QACA,OACA,MACD;AACD,SACG,OAAO,IAAI,CACX,KAAK;MAAE,OAAO;MAAqB,QAAQ,KAAK;MAAM,CAAC;;;GAG/D,CAAC;;CAGJ,UAAsB;EACpB,MAAM,cAAc,WAA8B;AAChD,OAAI,CAAC,KAAK,QAAQ,SAAS,OAAO,CAChC,OAAM,IAAI,MACR,gBAAgB,OAAO,qBAAqB,KAAK,QAAQ,KAAK,KAAK,GACpE;AAKH,UAAO;IACL,GAHY,KAAK,aAAa,OAAO;IAIrC,SAAS,QAAqB;AAE5B,YADmB,KAAK,OAAO,IAAI,CACjB,aAAa,OAAO;;IAEzC;;AAGH,SAAO;;CAGT,eAAwC;EACtC,MAAM,OACJ,EAAE;AACJ,OAAK,MAAM,OAAO,KAAK,SAAS;GAC9B,MAAM,SAAS,KAAK,WAAW;AAC/B,QAAK,OAAO;IACV,QAAQ,QAAQ,SAAS,aAAa,OAAO,OAAO,GAAG;IACvD,UAAU,QAAQ,YAAY;IAC/B;;AAEH,SAAO,EAAE,MAAM;;;;;;AAOnB,MAAa,OAAO,SAAS,WAAW"}
1
+ {"version":3,"file":"plugin.js","names":["manifest"],"sources":["../../../src/plugins/jobs/plugin.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport type { jobs as jobsTypes } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRequest,\n IAppRouter,\n PluginExecutionSettings,\n StreamExecutionSettings,\n} from \"shared\";\nimport { toJSONSchema } from \"zod\";\nimport { JobsConnector } from \"../../connectors/jobs\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { ExecutionError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { ExecutionResult } from \"../../plugin\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport {\n JOBS_READ_DEFAULTS,\n JOBS_STREAM_DEFAULTS,\n JOBS_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { mapParams } from \"./params\";\nimport type {\n IJobsConfig,\n JobAPI,\n JobConfig,\n JobHandle,\n JobRunStatus,\n JobsExport,\n} from \"./types\";\n\nconst logger = createLogger(\"jobs\");\n\nconst DEFAULT_WAIT_TIMEOUT = 600_000;\nconst DEFAULT_POLL_INTERVAL = 5_000;\n/** Cap on param-key count when a job has no Zod schema. Jobs that need more keys must define a schema. */\nconst MAX_UNVALIDATED_PARAM_KEYS = 50;\n\n/** Replace upstream error messages with generic descriptions keyed by HTTP status. */\nfunction errorResult(status: number): ExecutionResult<never> {\n return {\n ok: false,\n status,\n message: STATUS_CODES[status] ?? \"Request failed\",\n };\n}\n\nfunction isTerminalRunState(state: string | undefined): boolean {\n return (\n state === \"TERMINATED\" || state === \"SKIPPED\" || state === \"INTERNAL_ERROR\"\n );\n}\n\n/** Exponential backoff (1.5x) with +/- 20% jitter, capped at `max`. */\nfunction nextPollDelay(\n current: number,\n max: number,\n): { delay: number; next: number } {\n const jitter = 1 + (Math.random() * 0.4 - 0.2);\n return {\n delay: Math.min(current * jitter, max),\n next: Math.min(current * 1.5, max),\n };\n}\n\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve) => {\n if (signal?.aborted) {\n resolve();\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nclass JobsPlugin extends Plugin {\n static manifest = manifest as PluginManifest;\n\n protected declare config: IJobsConfig;\n private connector: JobsConnector;\n private jobIds: Record<string, number> = {};\n private jobConfigs: Record<string, JobConfig> = {};\n private jobKeys: string[] = [];\n\n /**\n * Scans process.env for DATABRICKS_JOB_* keys and merges with explicit config.\n * Explicit config wins for per-job overrides; auto-discovered jobs get default `{}` config.\n */\n static discoverJobs(config: IJobsConfig): Record<string, JobConfig> {\n const explicit = config.jobs ?? {};\n const discovered: Record<string, JobConfig> = {};\n\n const prefix = \"DATABRICKS_JOB_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n if (key === \"DATABRICKS_JOB_ID\") continue;\n const suffix = key.slice(prefix.length);\n if (!suffix || !process.env[key]) continue;\n const jobKey = suffix.toLowerCase();\n if (!(jobKey in explicit)) {\n discovered[jobKey] = {};\n }\n }\n\n // Single-job shorthand: DATABRICKS_JOB_ID maps to \"default\" key\n if (\n process.env.DATABRICKS_JOB_ID &&\n Object.keys(explicit).length === 0 &&\n Object.keys(discovered).length === 0\n ) {\n discovered.default = {};\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured jobs.\n * Each job key maps to a `DATABRICKS_JOB_{KEY_UPPERCASE}` env var (or `DATABRICKS_JOB_ID` for \"default\").\n */\n static getResourceRequirements(config: IJobsConfig): ResourceRequirement[] {\n const jobs = JobsPlugin.discoverJobs(config);\n return Object.keys(jobs).map((key) => ({\n type: ResourceType.JOB,\n alias: `job-${key}`,\n resourceKey: `job-${key}`,\n description: `Databricks Job \"${key}\"`,\n permission: \"CAN_MANAGE_RUN\" as const,\n fields: {\n id: {\n env:\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`,\n description: `Job ID for \"${key}\"`,\n },\n },\n required: true,\n }));\n }\n\n constructor(config: IJobsConfig) {\n super(config);\n this.config = config;\n this.connector = new JobsConnector({\n telemetry: config.telemetry,\n });\n\n const jobs = JobsPlugin.discoverJobs(config);\n this.jobKeys = Object.keys(jobs);\n this.jobConfigs = jobs;\n\n for (const key of this.jobKeys) {\n const envVar =\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`;\n const jobIdStr = process.env[envVar];\n if (jobIdStr) {\n const parsed = Number.parseInt(jobIdStr, 10);\n if (!Number.isNaN(parsed)) {\n this.jobIds[key] = parsed;\n }\n }\n }\n }\n\n async setup() {\n logger.info(\n `Jobs plugin initialized with ${this.jobKeys.length} job(s): ${this.jobKeys.join(\", \")}`,\n );\n }\n\n private get client() {\n return getWorkspaceClient();\n }\n\n private getJobId(jobKey: string): number {\n const id = this.jobIds[jobKey];\n if (!id) {\n const envVar =\n jobKey === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${jobKey.toUpperCase()}`;\n throw new Error(\n `Job \"${jobKey}\" has no configured job ID. Set ${envVar} env var.`,\n );\n }\n return id;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_READ_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n cache: { ...JOBS_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n private _writeSettings(): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_WRITE_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n },\n };\n }\n\n /**\n * Validates params against the job's Zod schema (if any) and maps them\n * to SDK request fields based on the task type. Shared by runNow and runAndWait.\n */\n private _validateAndMap(\n jobKey: string,\n params?: Record<string, unknown>,\n ): Record<string, unknown> {\n const jobConfig = this.jobConfigs[jobKey];\n let validated = params;\n\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(params ?? {});\n if (!result.success) {\n throw new ValidationError(\n `Parameter validation failed for job \"${jobKey}\": ${result.error.message}`,\n );\n }\n validated = result.data as Record<string, unknown>;\n }\n\n return jobConfig?.taskType && validated\n ? mapParams(jobConfig.taskType, validated)\n : (validated ?? {});\n }\n\n /**\n * Creates a JobAPI for a specific configured job key.\n * Each method is scoped to the job's configured ID.\n */\n protected createJobAPI(jobKey: string): JobAPI {\n const jobId = this.getJobId(jobKey);\n const jobConfig = this.jobConfigs[jobKey];\n // Capture `this` for use in the async generator\n const self = this;\n // Eagerly capture the client and userId so that when createJobAPI is\n // called inside an asUser() proxy (which runs in user context), the\n // closures below use the user-scoped client instead of falling back\n // to the service principal when the ALS context has already exited.\n const client = this.client;\n const userKey = getCurrentUserId();\n\n /**\n * Verify that `runId` belongs to this job's configured `jobId`. Returns\n * null if the run is in scope; otherwise returns a 404 `ExecutionResult`.\n * Prevents cross-job access via the `/:jobKey/runs/:runId` HTTP surface.\n */\n const verifyRunScope = async (\n runId: number,\n ): Promise<ExecutionResult<never> | null> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return null;\n };\n\n return {\n runNow: async (\n params?: Record<string, unknown>,\n ): Promise<ExecutionResult<jobsTypes.RunNowResponse>> => {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const result = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n async *runAndWait(\n params?: Record<string, unknown>,\n signal?: AbortSignal,\n ): AsyncGenerator<JobRunStatus, void, unknown> {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const runResult = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n\n if (!runResult.ok) {\n throw new ExecutionError(\"Failed to trigger job run\");\n }\n const runId = runResult.data.run_id;\n if (!runId) {\n throw new Error(\"runNow did not return a run_id\");\n }\n\n const basePollInterval =\n self.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;\n const maxPollInterval = basePollInterval * 6;\n const timeout = jobConfig?.waitTimeout ?? DEFAULT_WAIT_TIMEOUT;\n const startTime = Date.now();\n let currentInterval = basePollInterval;\n\n while (!signal?.aborted) {\n if (Date.now() - startTime > timeout) {\n throw new Error(\n `Job run ${runId} polling timeout after ${timeout}ms`,\n );\n }\n\n const runStatusResult = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n {\n default: {\n ...JOBS_READ_DEFAULTS,\n cache: { enabled: false },\n },\n },\n userKey,\n );\n if (!runStatusResult.ok) {\n throw new ExecutionError(\n `Failed to poll run status for run ${runId}`,\n );\n }\n const run = runStatusResult.data;\n const state = run.state?.life_cycle_state;\n\n yield { status: state, timestamp: Date.now(), run };\n\n if (isTerminalRunState(state)) return;\n\n const { delay, next } = nextPollDelay(\n currentInterval,\n maxPollInterval,\n );\n currentInterval = next;\n await abortableSleep(delay, signal);\n }\n },\n\n lastRun: async (): Promise<\n ExecutionResult<jobsTypes.BaseRun | undefined>\n > => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: 1 },\n signal,\n ),\n self._readSettings([\"jobs:lastRun\", jobKey]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n return { ok: true, data: result.data[0] };\n },\n\n listRuns: async (options?: {\n limit?: number;\n }): Promise<ExecutionResult<jobsTypes.BaseRun[]>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: options?.limit },\n signal,\n ),\n self._readSettings([\n \"jobs:listRuns\",\n jobKey,\n options?.limit ?? \"default\",\n ]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getRun: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.Run>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return result;\n },\n\n getRunOutput: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.RunOutput>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.getRunOutput(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRunOutput\", jobKey, runId]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n cancelRun: async (runId: number): Promise<ExecutionResult<void>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.cancelRun(client, { run_id: runId }, signal),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getJob: async (): Promise<ExecutionResult<jobsTypes.Job>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getJob(client, { job_id: jobId }, signal),\n self._readSettings([\"jobs:getJob\", jobKey]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n };\n }\n\n /**\n * Resolve `:jobKey` from the request. Returns the key and ID,\n * or sends a 404 and returns `{ jobKey: undefined, jobId: undefined }`.\n */\n private _resolveJob(\n req: express.Request,\n res: express.Response,\n ):\n | { jobKey: string; jobId: number }\n | { jobKey: undefined; jobId: undefined } {\n const jobKey = req.params.jobKey;\n if (!this.jobKeys.includes(jobKey)) {\n const safeKey = jobKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown job \"${safeKey}\"`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n const jobId = this.jobIds[jobKey];\n if (!jobId) {\n res.status(404).json({\n error: `Job \"${jobKey}\" has no configured job ID`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n return { jobKey, jobId };\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 /**\n * Validate params from an HTTP request body. Eager validation lets streaming\n * requests get a clean 400 instead of a generic SSE error event. Throws\n * ValidationError so handlers can map to a 400 response via their catch block.\n */\n private _parseRunParams(\n jobKey: string,\n rawParams: unknown,\n ): Record<string, unknown> | undefined {\n if (\n rawParams !== undefined &&\n (typeof rawParams !== \"object\" ||\n rawParams === null ||\n Array.isArray(rawParams))\n ) {\n throw new ValidationError(\"params must be a plain object\");\n }\n\n const jobConfig = this.jobConfigs[jobKey];\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(rawParams ?? {});\n if (!result.success) {\n throw new ValidationError(\"Invalid job parameters\");\n }\n // Pass rawParams — not result.data — to avoid double-transforming\n // when _validateAndMap calls safeParse again downstream.\n return rawParams as Record<string, unknown>;\n }\n // No schema. Either reject (no taskType) or enforce a key cap so that\n // untrusted clients can't spread arbitrarily many fields into the SDK.\n if (rawParams !== undefined) {\n if (!jobConfig?.taskType) {\n throw new ValidationError(\"This job does not accept parameters\");\n }\n const keyCount = Object.keys(rawParams as Record<string, unknown>).length;\n if (keyCount > MAX_UNVALIDATED_PARAM_KEYS) {\n throw new ValidationError(\n `Too many parameters (${keyCount}). Define a Zod schema to accept more than ${MAX_UNVALIDATED_PARAM_KEYS}.`,\n );\n }\n }\n return rawParams as Record<string, unknown> | undefined;\n }\n\n private async _handleRun(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const stream = req.query.stream === \"true\";\n\n try {\n const params = this._parseRunParams(jobKey, req.body?.params);\n const api = this.createJobAPI(jobKey);\n\n if (stream) {\n const streamSettings: StreamExecutionSettings = {\n default: JOBS_STREAM_DEFAULTS,\n };\n await this.executeStream<JobRunStatus>(\n res,\n (signal) => api.runAndWait(params, signal),\n streamSettings,\n );\n } else {\n const result = await api.runNow(params);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runId: result.data.run_id });\n }\n } catch (error) {\n if (error instanceof ValidationError) {\n if (!res.headersSent) {\n res.status(400).json({ error: error.message, plugin: this.name });\n }\n return;\n }\n logger.error(\"Run failed for job %s: %O\", jobKey, error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Run failed\", plugin: this.name });\n }\n }\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"run\",\n method: \"post\",\n path: \"/:jobKey/run\",\n handler: (req, res) => this._handleRun(req, res),\n });\n\n // GET /:jobKey/runs\n this.route(router, {\n name: \"runs\",\n method: \"get\",\n path: \"/:jobKey/runs\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const limit = Math.max(\n 1,\n Math.min(Number.parseInt(req.query.limit as string, 10) || 20, 100),\n );\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.listRuns({ limit });\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runs: result.data });\n } catch (error) {\n logger.error(\"List runs failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"List runs failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/runs/:runId\n this.route(router, {\n name: \"run-detail\",\n method: \"get\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.getRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\n } catch (error) {\n logger.error(\n \"Get run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res.status(500).json({ error: \"Get run failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/status\n this.route(router, {\n name: \"status\",\n method: \"get\",\n path: \"/:jobKey/status\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.lastRun();\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({\n status: result.data?.state?.life_cycle_state ?? null,\n run: result.data ?? null,\n });\n } catch (error) {\n logger.error(\"Status check failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"Status check failed\", plugin: this.name });\n }\n },\n });\n\n // DELETE /:jobKey/runs/:runId\n this.route(router, {\n name: \"cancel-run\",\n method: \"delete\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.cancelRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.status(204).end();\n } catch (error) {\n logger.error(\n \"Cancel run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res\n .status(500)\n .json({ error: \"Cancel run failed\", plugin: this.name });\n }\n },\n });\n }\n\n exports(): JobsExport {\n const resolveJob = (jobKey: string): JobHandle => {\n if (!this.jobKeys.includes(jobKey)) {\n throw new Error(\n `Unknown job \"${jobKey}\". Available jobs: ${this.jobKeys.join(\", \")}`,\n );\n }\n\n const spApi = this.createJobAPI(jobKey);\n\n return {\n ...spApi,\n asUser: (req: IAppRequest) => {\n const userPlugin = this.asUser(req) as JobsPlugin;\n return userPlugin.createJobAPI(jobKey);\n },\n };\n };\n\n return resolveJob as JobsExport;\n }\n\n clientConfig(): Record<string, unknown> {\n const jobs: Record<string, { params: unknown; taskType: string | null }> =\n {};\n for (const key of this.jobKeys) {\n const config = this.jobConfigs[key];\n jobs[key] = {\n params: config?.params ? toJSONSchema(config.params) : null,\n taskType: config?.taskType ?? null,\n };\n }\n return { jobs };\n }\n}\n\n/**\n * @internal\n */\nexport const jobs = toPlugin(JobsPlugin);\n\n/**\n * @internal\n */\nexport { JobsPlugin };\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
2
  import { createLakebasePoolManager } from "../../connectors/lakebase/pool-manager.js";
3
- import { getUserContext, init_execution_context } from "../../context/execution-context.js";
3
+ import { getUserContext } from "../../context/execution-context.js";
4
4
  import { RoutingPool } from "../../connectors/lakebase/routing-pool.js";
5
5
  import { createLakebasePool, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup } from "../../connectors/lakebase/index.js";
6
6
  import { Plugin } from "../../plugin/plugin.js";
@@ -13,7 +13,6 @@ import manifest_default from "./manifest.js";
13
13
  import { z } from "zod";
14
14
 
15
15
  //#region src/plugins/lakebase/lakebase.ts
16
- init_execution_context();
17
16
  const logger = createLogger("lakebase");
18
17
  /** Default pool settings for per-user OBO pools. */
19
18
  const OBO_POOL_DEFAULTS = {
@@ -1 +1 @@
1
- {"version":3,"file":"lakebase.js","names":["manifest"],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"sourcesContent":["import type { QueryResult, QueryResultRow } from \"pg\";\nimport type { AgentToolDefinition, ToolProvider } from \"shared\";\nimport { z } from \"zod\";\nimport {\n createLakebasePool,\n createLakebasePoolManager,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n type LakebasePool,\n type LakebasePoolManager,\n RoutingPool,\n} from \"../../connectors/lakebase\";\nimport { getUserContext } from \"../../context/execution-context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport manifest from \"./manifest.json\";\nimport type { ILakebaseConfig } from \"./types\";\n\nconst logger = createLogger(\"lakebase\");\n\n/** Default pool settings for per-user OBO pools. */\nconst OBO_POOL_DEFAULTS = {\n max: 3,\n allowExitOnIdle: true,\n idleTimeoutMillis: 30_000,\n};\n\n/**\n * AppKit plugin for Databricks Lakebase Autoscaling.\n *\n * Wraps `@databricks/lakebase` to provide a standard `pg.Pool` with automatic\n * OAuth token refresh, integrated with AppKit's logger and OpenTelemetry setup.\n *\n * Supports On-Behalf-Of (OBO) via `asUser(req)` — each user gets a separate\n * `pg.Pool` authenticated with their Databricks identity, enabling features\n * like Row-Level Security (RLS). Routing is handled transparently by\n * {@link RoutingPool}, which reads the execution context set by the base\n * class `asUser()`.\n *\n * @example\n * ```ts\n * import { createApp, lakebase, server } from \"@databricks/appkit\";\n *\n * const AppKit = await createApp({\n * plugins: [server(), lakebase()],\n * });\n *\n * // Service principal query\n * const result = await AppKit.lakebase.query(\"SELECT * FROM users WHERE id = $1\", [userId]);\n *\n * // User-scoped query (per-user pool, RLS enforced)\n * const mine = await AppKit.lakebase.asUser(req).query(\"SELECT * FROM my_data\");\n * ```\n */\nexport class LakebasePlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"lakebase\">;\n\n protected declare config: ILakebaseConfig;\n private pool: RoutingPool | null = null;\n private oboPoolManager: LakebasePoolManager | null = null;\n\n /**\n * Initializes the Lakebase connection pool and OBO pool manager.\n * Called automatically by AppKit during the plugin setup phase.\n *\n * Creates a {@link RoutingPool} that automatically routes queries to either\n * the service-principal pool or a per-user pool based on the execution\n * context (set by `Plugin.asUser(req)` via AsyncLocalStorage).\n */\n async setup() {\n const poolConfig = this.config.pool;\n const user = await getUsernameWithApiLookup(poolConfig);\n\n const spPool = createLakebasePool({ ...poolConfig, user });\n logger.info(\"Lakebase SP pool initialized\");\n\n this.oboPoolManager = createLakebasePoolManager({\n ...poolConfig,\n ...OBO_POOL_DEFAULTS,\n });\n logger.info(\"Lakebase OBO pool manager initialized\");\n\n const oboManager = this.oboPoolManager;\n this.pool = new RoutingPool(spPool, (ctx) => {\n if (!oboManager) throw new Error(\"OBO pool manager not initialized\");\n // Lakebase OAuth roles use email as the postgres role when available\n const userKey = ctx.userEmail ?? ctx.userId;\n const isNew = !oboManager.hasPool(userKey);\n const pool = oboManager.getPool(\n userKey,\n { workspaceClient: ctx.client, user: userKey },\n ctx.tokenFingerprint,\n );\n if (isNew) {\n logger.debug(\"Created OBO pool for user (total: %d)\", oboManager.size);\n }\n return pool;\n });\n }\n\n /**\n * Executes a parameterized SQL query against the Lakebase pool.\n *\n * When called inside `asUser(req)`, the query automatically routes to\n * the per-user pool via {@link RoutingPool}.\n *\n * @param text - SQL query string, using `$1`, `$2`, ... placeholders\n * @param values - Parameter values corresponding to placeholders\n * @returns Query result with typed rows\n *\n * @example\n * ```ts\n * const result = await AppKit.lakebase.query<{ id: number; name: string }>(\n * \"SELECT id, name FROM users WHERE active = $1\",\n * [true],\n * );\n * ```\n */\n async query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n return this.pool!.query<T>(text, values);\n }\n\n /**\n * Execute a single statement inside a `BEGIN READ ONLY … ROLLBACK`\n * transaction on a dedicated client.\n *\n * The three commands MUST share a connection — a naive\n * `pool.query(\"BEGIN READ ONLY; <stmt>; ROLLBACK\")` batch cannot accept\n * parameter values (PostgreSQL's Extended Query protocol rejects multi-\n * statement prepared queries), which would silently break every\n * parameterized query the agent tool issues.\n *\n * Returns the raw `rows` array for the user's statement. Side effects the\n * statement may attempt (writes, writable-function side effects) are\n * rejected by PostgreSQL under the read-only transaction posture.\n */\n private async runReadOnlyStatement(\n text: string,\n values?: unknown[],\n ): Promise<unknown[]> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup()\n const client = await this.pool!.connect();\n try {\n await client.query(\"BEGIN READ ONLY\");\n const result = await client.query(text, values);\n return result.rows;\n } finally {\n try {\n await client.query(\"ROLLBACK\");\n } finally {\n client.release();\n }\n }\n }\n\n /**\n * Gracefully drains and closes all connection pools (SP + OBO).\n * Called automatically by AppKit during shutdown.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n if (this.pool) {\n logger.info(\"Closing Lakebase SP pool\");\n this.pool.end().catch((err) => {\n logger.error(\"Error closing Lakebase SP pool: %O\", err);\n });\n this.pool = null;\n }\n if (this.oboPoolManager) {\n logger.info(\n \"Closing all Lakebase OBO pools (%d)\",\n this.oboPoolManager.size,\n );\n this.oboPoolManager.closeAll().catch((err) => {\n logger.error(\"Error closing Lakebase OBO pools: %O\", err);\n });\n this.oboPoolManager = null;\n }\n }\n\n /**\n * Agent tool registry. Empty by default — the Lakebase plugin does NOT\n * expose its SQL connection to LLM agents unless the developer explicitly\n * opts in via `config.exposeAsAgentTool`. See {@link buildQueryTool}.\n */\n private tools: Record<string, ReturnType<typeof this.buildQueryTool>> = {};\n\n constructor(config: ILakebaseConfig) {\n super(config);\n this.config = config;\n if (config.exposeAsAgentTool) {\n this.tools = { query: this.buildQueryTool(config.exposeAsAgentTool) };\n logger.warn(\n \"Lakebase agent tool is enabled (readOnly=%s). Every agent with access to this plugin can execute SQL against the Lakebase database as the requesting user's identity.\",\n config.exposeAsAgentTool.readOnly !== false,\n );\n }\n }\n\n private buildQueryTool(\n opt: NonNullable<ILakebaseConfig[\"exposeAsAgentTool\"]>,\n ) {\n const readOnly = opt.readOnly !== false;\n return defineTool({\n description: readOnly\n ? \"Execute a read-only SQL query against the Lakebase PostgreSQL database. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted. Use $1, $2, etc. as placeholders and pass values separately.\"\n : \"Execute a parameterized SQL statement against the Lakebase PostgreSQL database. Use $1, $2, etc. as placeholders and pass values separately. This tool can modify data; every invocation requires explicit human approval.\",\n schema: z.object({\n text: z\n .string()\n .describe(\n \"SQL statement with $1, $2, ... placeholders for parameters\",\n ),\n values: z\n .array(z.unknown())\n .optional()\n .describe(\"Parameter values corresponding to placeholders\"),\n }),\n annotations: {\n effect: readOnly ? \"read\" : \"destructive\",\n idempotent: false,\n requiresUserContext: true,\n },\n execute: async (args, signal) => {\n // Matches the files plugin pattern: the pg connection API\n // doesn't accept AbortSignal in its current shape, so deeper\n // mid-call cancellation needs a separate plumbing pass on the\n // connector. This entry check still catches the common case —\n // a tool dispatched after the user already cancelled the\n // stream — and unwinds cleanly instead of running to\n // completion against the SQL warehouse.\n signal?.throwIfAborted();\n if (readOnly) {\n assertReadOnlySql(args.text);\n return this.runReadOnlyStatement(args.text, args.values);\n }\n const result = await this.query(args.text, args.values);\n return result.rows;\n },\n });\n }\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the pool config for the current execution context.\n * Inside `asUser(req)`, returns user-scoped config; otherwise SP config.\n */\n private activePoolConfig() {\n const ctx = getUserContext();\n if (ctx) {\n const user = ctx.userEmail ?? ctx.userId;\n return { ...this.config.pool, workspaceClient: ctx.client, user };\n }\n return this.config.pool;\n }\n\n /**\n * Returns the plugin's public API, accessible via `AppKit.lakebase`.\n *\n * - `pool` — The connection pool (routes to per-user pool when inside `asUser(req)`)\n * - `query` — Convenience method for executing parameterized SQL queries\n * - `getOrmConfig()` — Returns a config object compatible with Drizzle, TypeORM, Sequelize, etc.\n * Inside `asUser(req)`, returns user-scoped config.\n * - `getPgConfig()` — Returns a `pg.PoolConfig` object for manual pool construction.\n * Inside `asUser(req)`, returns user-scoped config.\n *\n * Use `AppKit.lakebase.asUser(req)` to get the same API backed by a per-user pool.\n */\n exports() {\n return {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n pool: this.pool! as LakebasePool,\n query: this.query.bind(this),\n getOrmConfig: () => getLakebaseOrmConfig(this.activePoolConfig()),\n getPgConfig: () => getLakebasePgConfig(this.activePoolConfig()),\n };\n }\n}\n\n/**\n * @internal\n */\nexport const lakebase = toPlugin(LakebasePlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"lakebase.js","names":["manifest"],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"sourcesContent":["import type { QueryResult, QueryResultRow } from \"pg\";\nimport type { AgentToolDefinition, ToolProvider } from \"shared\";\nimport { z } from \"zod\";\nimport {\n createLakebasePool,\n createLakebasePoolManager,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n type LakebasePool,\n type LakebasePoolManager,\n RoutingPool,\n} from \"../../connectors/lakebase\";\nimport { getUserContext } from \"../../context/execution-context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport manifest from \"./manifest.json\";\nimport type { ILakebaseConfig } from \"./types\";\n\nconst logger = createLogger(\"lakebase\");\n\n/** Default pool settings for per-user OBO pools. */\nconst OBO_POOL_DEFAULTS = {\n max: 3,\n allowExitOnIdle: true,\n idleTimeoutMillis: 30_000,\n};\n\n/**\n * AppKit plugin for Databricks Lakebase Autoscaling.\n *\n * Wraps `@databricks/lakebase` to provide a standard `pg.Pool` with automatic\n * OAuth token refresh, integrated with AppKit's logger and OpenTelemetry setup.\n *\n * Supports On-Behalf-Of (OBO) via `asUser(req)` — each user gets a separate\n * `pg.Pool` authenticated with their Databricks identity, enabling features\n * like Row-Level Security (RLS). Routing is handled transparently by\n * {@link RoutingPool}, which reads the execution context set by the base\n * class `asUser()`.\n *\n * @example\n * ```ts\n * import { createApp, lakebase, server } from \"@databricks/appkit\";\n *\n * const AppKit = await createApp({\n * plugins: [server(), lakebase()],\n * });\n *\n * // Service principal query\n * const result = await AppKit.lakebase.query(\"SELECT * FROM users WHERE id = $1\", [userId]);\n *\n * // User-scoped query (per-user pool, RLS enforced)\n * const mine = await AppKit.lakebase.asUser(req).query(\"SELECT * FROM my_data\");\n * ```\n */\nexport class LakebasePlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"lakebase\">;\n\n protected declare config: ILakebaseConfig;\n private pool: RoutingPool | null = null;\n private oboPoolManager: LakebasePoolManager | null = null;\n\n /**\n * Initializes the Lakebase connection pool and OBO pool manager.\n * Called automatically by AppKit during the plugin setup phase.\n *\n * Creates a {@link RoutingPool} that automatically routes queries to either\n * the service-principal pool or a per-user pool based on the execution\n * context (set by `Plugin.asUser(req)` via AsyncLocalStorage).\n */\n async setup() {\n const poolConfig = this.config.pool;\n const user = await getUsernameWithApiLookup(poolConfig);\n\n const spPool = createLakebasePool({ ...poolConfig, user });\n logger.info(\"Lakebase SP pool initialized\");\n\n this.oboPoolManager = createLakebasePoolManager({\n ...poolConfig,\n ...OBO_POOL_DEFAULTS,\n });\n logger.info(\"Lakebase OBO pool manager initialized\");\n\n const oboManager = this.oboPoolManager;\n this.pool = new RoutingPool(spPool, (ctx) => {\n if (!oboManager) throw new Error(\"OBO pool manager not initialized\");\n // Lakebase OAuth roles use email as the postgres role when available\n const userKey = ctx.userEmail ?? ctx.userId;\n const isNew = !oboManager.hasPool(userKey);\n const pool = oboManager.getPool(\n userKey,\n { workspaceClient: ctx.client, user: userKey },\n ctx.tokenFingerprint,\n );\n if (isNew) {\n logger.debug(\"Created OBO pool for user (total: %d)\", oboManager.size);\n }\n return pool;\n });\n }\n\n /**\n * Executes a parameterized SQL query against the Lakebase pool.\n *\n * When called inside `asUser(req)`, the query automatically routes to\n * the per-user pool via {@link RoutingPool}.\n *\n * @param text - SQL query string, using `$1`, `$2`, ... placeholders\n * @param values - Parameter values corresponding to placeholders\n * @returns Query result with typed rows\n *\n * @example\n * ```ts\n * const result = await AppKit.lakebase.query<{ id: number; name: string }>(\n * \"SELECT id, name FROM users WHERE active = $1\",\n * [true],\n * );\n * ```\n */\n async query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n return this.pool!.query<T>(text, values);\n }\n\n /**\n * Execute a single statement inside a `BEGIN READ ONLY … ROLLBACK`\n * transaction on a dedicated client.\n *\n * The three commands MUST share a connection — a naive\n * `pool.query(\"BEGIN READ ONLY; <stmt>; ROLLBACK\")` batch cannot accept\n * parameter values (PostgreSQL's Extended Query protocol rejects multi-\n * statement prepared queries), which would silently break every\n * parameterized query the agent tool issues.\n *\n * Returns the raw `rows` array for the user's statement. Side effects the\n * statement may attempt (writes, writable-function side effects) are\n * rejected by PostgreSQL under the read-only transaction posture.\n */\n private async runReadOnlyStatement(\n text: string,\n values?: unknown[],\n ): Promise<unknown[]> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup()\n const client = await this.pool!.connect();\n try {\n await client.query(\"BEGIN READ ONLY\");\n const result = await client.query(text, values);\n return result.rows;\n } finally {\n try {\n await client.query(\"ROLLBACK\");\n } finally {\n client.release();\n }\n }\n }\n\n /**\n * Gracefully drains and closes all connection pools (SP + OBO).\n * Called automatically by AppKit during shutdown.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n if (this.pool) {\n logger.info(\"Closing Lakebase SP pool\");\n this.pool.end().catch((err) => {\n logger.error(\"Error closing Lakebase SP pool: %O\", err);\n });\n this.pool = null;\n }\n if (this.oboPoolManager) {\n logger.info(\n \"Closing all Lakebase OBO pools (%d)\",\n this.oboPoolManager.size,\n );\n this.oboPoolManager.closeAll().catch((err) => {\n logger.error(\"Error closing Lakebase OBO pools: %O\", err);\n });\n this.oboPoolManager = null;\n }\n }\n\n /**\n * Agent tool registry. Empty by default — the Lakebase plugin does NOT\n * expose its SQL connection to LLM agents unless the developer explicitly\n * opts in via `config.exposeAsAgentTool`. See {@link buildQueryTool}.\n */\n private tools: Record<string, ReturnType<typeof this.buildQueryTool>> = {};\n\n constructor(config: ILakebaseConfig) {\n super(config);\n this.config = config;\n if (config.exposeAsAgentTool) {\n this.tools = { query: this.buildQueryTool(config.exposeAsAgentTool) };\n logger.warn(\n \"Lakebase agent tool is enabled (readOnly=%s). Every agent with access to this plugin can execute SQL against the Lakebase database as the requesting user's identity.\",\n config.exposeAsAgentTool.readOnly !== false,\n );\n }\n }\n\n private buildQueryTool(\n opt: NonNullable<ILakebaseConfig[\"exposeAsAgentTool\"]>,\n ) {\n const readOnly = opt.readOnly !== false;\n return defineTool({\n description: readOnly\n ? \"Execute a read-only SQL query against the Lakebase PostgreSQL database. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted. Use $1, $2, etc. as placeholders and pass values separately.\"\n : \"Execute a parameterized SQL statement against the Lakebase PostgreSQL database. Use $1, $2, etc. as placeholders and pass values separately. This tool can modify data; every invocation requires explicit human approval.\",\n schema: z.object({\n text: z\n .string()\n .describe(\n \"SQL statement with $1, $2, ... placeholders for parameters\",\n ),\n values: z\n .array(z.unknown())\n .optional()\n .describe(\"Parameter values corresponding to placeholders\"),\n }),\n annotations: {\n effect: readOnly ? \"read\" : \"destructive\",\n idempotent: false,\n requiresUserContext: true,\n },\n execute: async (args, signal) => {\n // Matches the files plugin pattern: the pg connection API\n // doesn't accept AbortSignal in its current shape, so deeper\n // mid-call cancellation needs a separate plumbing pass on the\n // connector. This entry check still catches the common case —\n // a tool dispatched after the user already cancelled the\n // stream — and unwinds cleanly instead of running to\n // completion against the SQL warehouse.\n signal?.throwIfAborted();\n if (readOnly) {\n assertReadOnlySql(args.text);\n return this.runReadOnlyStatement(args.text, args.values);\n }\n const result = await this.query(args.text, args.values);\n return result.rows;\n },\n });\n }\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the pool config for the current execution context.\n * Inside `asUser(req)`, returns user-scoped config; otherwise SP config.\n */\n private activePoolConfig() {\n const ctx = getUserContext();\n if (ctx) {\n const user = ctx.userEmail ?? ctx.userId;\n return { ...this.config.pool, workspaceClient: ctx.client, user };\n }\n return this.config.pool;\n }\n\n /**\n * Returns the plugin's public API, accessible via `AppKit.lakebase`.\n *\n * - `pool` — The connection pool (routes to per-user pool when inside `asUser(req)`)\n * - `query` — Convenience method for executing parameterized SQL queries\n * - `getOrmConfig()` — Returns a config object compatible with Drizzle, TypeORM, Sequelize, etc.\n * Inside `asUser(req)`, returns user-scoped config.\n * - `getPgConfig()` — Returns a `pg.PoolConfig` object for manual pool construction.\n * Inside `asUser(req)`, returns user-scoped config.\n *\n * Use `AppKit.lakebase.asUser(req)` to get the same API backed by a per-user pool.\n */\n exports() {\n return {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n pool: this.pool! as LakebasePool,\n query: this.query.bind(this),\n getOrmConfig: () => getLakebaseOrmConfig(this.activePoolConfig()),\n getPgConfig: () => getLakebasePgConfig(this.activePoolConfig()),\n };\n }\n}\n\n/**\n * @internal\n */\nexport const lakebase = toPlugin(LakebasePlugin);\n"],"mappings":"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"}
@@ -13,13 +13,34 @@ var manifest_default = {
13
13
  "description": "Lakebase Postgres database for persistent storage",
14
14
  "permission": "CAN_CONNECT_AND_CREATE",
15
15
  "fields": {
16
+ "project": {
17
+ "description": "Full Lakebase Postgres project resource name. Obtain by running `databricks postgres list-projects`, select the desired item from the output array and use its .name value.",
18
+ "examples": ["projects/{project-id}"],
19
+ "discovery": {
20
+ "type": "kind",
21
+ "resourceKind": "postgres_project",
22
+ "select": "name"
23
+ }
24
+ },
16
25
  "branch": {
17
- "description": "Full Lakebase Postgres branch resource name. Obtain by running `databricks postgres list-branches projects/{project-id}`, select the desired item from the output array and use its .name value.",
18
- "examples": ["projects/{project-id}/branches/{branch-id}"]
26
+ "description": "Full Lakebase Postgres branch resource name. Obtain by running `databricks postgres list-branches {project-name}`, select the desired item from the output array and use its .name value. Requires the project resource name.",
27
+ "examples": ["projects/{project-id}/branches/{branch-id}"],
28
+ "discovery": {
29
+ "type": "kind",
30
+ "resourceKind": "postgres_branch",
31
+ "select": "name",
32
+ "dependsOn": "project"
33
+ }
19
34
  },
20
35
  "database": {
21
36
  "description": "Full Lakebase Postgres database resource name. Obtain by running `databricks postgres list-databases {branch-name}`, select the desired item from the output array and use its .name value. Requires the branch resource name.",
22
- "examples": ["projects/{project-id}/branches/{branch-id}/databases/{database-id}"]
37
+ "examples": ["projects/{project-id}/branches/{branch-id}/databases/{database-id}"],
38
+ "discovery": {
39
+ "type": "kind",
40
+ "resourceKind": "postgres_database",
41
+ "select": "name",
42
+ "dependsOn": "branch"
43
+ }
23
44
  },
24
45
  "host": {
25
46
  "env": "PGHOST",
@@ -55,7 +76,8 @@ var manifest_default = {
55
76
  }
56
77
  }],
57
78
  "optional": []
58
- }
79
+ },
80
+ scaffolding: { "rules": { "should": ["After init, run any database migrations for your chosen ORM before first request", "After init, verify Lakebase connectivity with 'psql $PGHOST -c \"select 1\"'"] } }
59
81
  };
60
82
 
61
83
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
2
  import { ServerError } from "../../errors/server.js";
3
- import { init_errors } from "../../errors/index.js";
3
+ import "../../errors/index.js";
4
4
  import { instrumentations } from "../../telemetry/instrumentations.js";
5
5
  import "../../telemetry/index.js";
6
6
  import { TelemetryReporter } from "../../internal-telemetry/reporter.js";
@@ -21,7 +21,6 @@ import express from "express";
21
21
  import getPort, { portNumbers } from "get-port";
22
22
 
23
23
  //#region src/plugins/server/index.ts
24
- init_errors();
25
24
  dotenv.config({ path: path.resolve(process.cwd(), "./.env") });
26
25
  const logger = createLogger("server");
27
26
  /** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of plugins.values()) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n TelemetryReporter.getInstance()?.stop();\n\n // 1. abort active operations from plugins\n const shutdownPlugins = this.context?.getPlugins();\n if (shutdownPlugins) {\n for (const plugin of shutdownPlugins.values()) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of plugins.values()) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n TelemetryReporter.getInstance()?.stop();\n\n // 1. abort active operations from plugins\n const shutdownPlugins = this.context?.getPlugins();\n if (shutdownPlugins) {\n for (const plugin of shutdownPlugins.values()) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
2
  import { ServerError } from "../../errors/server.js";
3
- import { init_errors } from "../../errors/index.js";
3
+ import "../../errors/index.js";
4
4
  import { mergeConfigDedup } from "../../utils/vite-config-merge.js";
5
5
  import { BaseServer } from "./base-server.js";
6
6
  import { appKitServingTypesPlugin } from "../../type-generator/serving/vite-plugin.js";
@@ -10,7 +10,6 @@ import path from "node:path";
10
10
  import fs from "node:fs";
11
11
 
12
12
  //#region src/plugins/server/vite-dev-server.ts
13
- init_errors();
14
13
  const logger = createLogger("server:vite");
15
14
  /**
16
15
  * Vite dev server for the AppKit.
@@ -1 +1 @@
1
- {"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../../src/plugins/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { appKitServingTypesPlugin } from \"../../type-generator/serving/vite-plugin\";\nimport { appKitTypesPlugin } from \"../../type-generator/vite-plugin\";\nimport { mergeConfigDedup } from \"../../utils\";\nimport { BaseServer } from \"./base-server\";\nimport { reactSourceLocPlugin } from \"./react-source-loc-vite-plugin\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n const projectRoot = process.cwd();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const viteClientPort = process.env.VITE_CLIENT_PORT;\n const serverHmr = viteClientPort\n ? { hmr: { clientPort: viteClientPort } }\n : {};\n\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n ...serverHmr,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [\n react.default(),\n reactSourceLocPlugin({ projectRoot }),\n appKitTypesPlugin(),\n appKitServingTypesPlugin(),\n ],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;aAI2C;AAS3C,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAC5C,AAAQ;CAER,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;EACxC,MAAM,cAAc,QAAQ,KAAK;EAWjC,MAAM,cATe,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE;EAC7C,MAAM,iBAAiB,QAAQ,IAAI;AA0BnC,OAAK,OAAO,MAAM,iBADI,iBAAiB,YApBpB;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,GATc,iBACd,EAAE,KAAK,EAAE,YAAY,gBAAgB,EAAE,GACvC,EAAE;IAQF,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS;IACP,MAAM,SAAS;IACf,qBAAqB,EAAE,aAAa,CAAC;IACrC,mBAAmB;IACnB,0BAA0B;IAC3B;GACD,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
1
+ {"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../../src/plugins/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { appKitServingTypesPlugin } from \"../../type-generator/serving/vite-plugin\";\nimport { appKitTypesPlugin } from \"../../type-generator/vite-plugin\";\nimport { mergeConfigDedup } from \"../../utils\";\nimport { BaseServer } from \"./base-server\";\nimport { reactSourceLocPlugin } from \"./react-source-loc-vite-plugin\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n const projectRoot = process.cwd();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const viteClientPort = process.env.VITE_CLIENT_PORT;\n const serverHmr = viteClientPort\n ? { hmr: { clientPort: viteClientPort } }\n : {};\n\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n ...serverHmr,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [\n react.default(),\n reactSourceLocPlugin({ projectRoot }),\n appKitTypesPlugin(),\n appKitServingTypesPlugin(),\n ],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAaA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAC5C,AAAQ;CAER,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;EACxC,MAAM,cAAc,QAAQ,KAAK;EAWjC,MAAM,cATe,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE;EAC7C,MAAM,iBAAiB,QAAQ,IAAI;AA0BnC,OAAK,OAAO,MAAM,iBADI,iBAAiB,YApBpB;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,GATc,iBACd,EAAE,KAAK,EAAE,YAAY,gBAAgB,EAAE,GACvC,EAAE;IAQF,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS;IACP,MAAM,SAAS;IACf,qBAAqB,EAAE,aAAa,CAAC;IACrC,mBAAmB;IACnB,0BAA0B;IAC3B;GACD,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
2
  import { getWorkspaceClient } from "../../context/execution-context.js";
3
- import { init_context } from "../../context/index.js";
3
+ import "../../context/index.js";
4
4
  import { Plugin } from "../../plugin/plugin.js";
5
5
  import { ResourceType } from "../../registry/types.generated.js";
6
6
  import "../../registry/index.js";
@@ -16,7 +16,6 @@ import { Readable } from "node:stream";
16
16
  import { pipeline } from "node:stream/promises";
17
17
 
18
18
  //#region src/plugins/serving/serving.ts
19
- init_context();
20
19
  const logger = createLogger("serving");
21
20
  var EndpointNotFoundError = class extends Error {
22
21
  constructor(alias) {
@@ -1 +1 @@
1
- {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"serving\">;\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n protected declare config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\nexport class ServingPlugin extends Plugin {\n static manifest = manifest as PluginManifest<\"serving\">;\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n protected declare config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { ResourceFieldEntry } from "../shared/src/schemas/plugin-manifest.generated.js";
1
+ import { ResourceFieldEntry } from "../shared/src/schemas/manifest.js";
2
2
  import { ResourcePermission, ResourceType } from "./types.generated.js";
3
3
  import { ConfigSchema, PluginManifest, ResourceEntry, ResourceRequirement, ValidationResult } from "./types.js";
4
4
  import { getPluginManifest, getResourceRequirements } from "./manifest-loader.js";