@databricks/appkit 0.35.1 → 0.35.2

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.
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.35.1";
3
+ var version = "0.35.2";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -3,9 +3,34 @@ import { TelemetryManager } from "../../telemetry/telemetry-manager.js";
3
3
  import { SpanKind, SpanStatusCode } from "../../telemetry/index.js";
4
4
  import { FILES_MAX_READ_SIZE, contentTypeFromPath, isTextContentType } from "./defaults.js";
5
5
  import { ApiError } from "@databricks/sdk-experimental";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
6
7
 
7
8
  //#region src/connectors/files/client.ts
8
9
  const logger = createLogger("connectors:files");
10
+ /**
11
+ * Ambient span-attribute propagation for `FilesConnector.traced()`.
12
+ *
13
+ * Callers (e.g. the plugin's `_withAuthModeAttributes` wrapper) set extra
14
+ * span attributes here via `runWithFilesSpanAttributes(attrs, fn)`. The
15
+ * connector's `traced()` decorator reads them and merges them into the
16
+ * span it creates around the SDK call. This lets the plugin tag spans with
17
+ * `files.auth_mode` without opening a duplicate `files.<op>` span.
18
+ *
19
+ * AsyncLocalStorage is used so concurrent requests don't see each other's
20
+ * attributes. Outside an active scope, `getStore()` returns `undefined` and
21
+ * the connector falls back to the static attribute set.
22
+ */
23
+ const filesSpanAttributesStorage = new AsyncLocalStorage();
24
+ /**
25
+ * Run `fn` with the supplied attributes attached to whatever span the
26
+ * `FilesConnector` opens for its SDK call. Used to propagate request-scoped
27
+ * attributes (e.g. `files.auth_mode`) onto the connector's span without
28
+ * opening a parent span — avoids the 2x span allocation that
29
+ * `startActiveSpan` parented otherwise.
30
+ */
31
+ function runWithFilesSpanAttributes(attributes, fn) {
32
+ return filesSpanAttributesStorage.run(attributes, fn);
33
+ }
9
34
  var FilesConnector = class {
10
35
  name = "files";
11
36
  defaultVolume;
@@ -41,10 +66,12 @@ var FilesConnector = class {
41
66
  async traced(operation, attributes, fn) {
42
67
  const startTime = Date.now();
43
68
  let success = false;
69
+ const ambient = filesSpanAttributesStorage.getStore();
44
70
  return this.telemetry.startActiveSpan(`files.${operation}`, {
45
71
  kind: SpanKind.CLIENT,
46
72
  attributes: {
47
73
  "files.operation": operation,
74
+ ...ambient ?? {},
48
75
  ...attributes
49
76
  }
50
77
  }, async (span) => {
@@ -219,5 +246,5 @@ var FilesConnector = class {
219
246
  };
220
247
 
221
248
  //#endregion
222
- export { FilesConnector };
249
+ export { FilesConnector, runWithFilesSpanAttributes };
223
250
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/files/client.ts"],"sourcesContent":["import { ApiError, type WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { TelemetryOptions } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport type {\n DirectoryEntry,\n DownloadResponse,\n FileMetadata,\n FilePreview,\n} from \"../../plugins/files/types\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport {\n contentTypeFromPath,\n FILES_MAX_READ_SIZE,\n isTextContentType,\n} from \"./defaults\";\n\nconst logger = createLogger(\"connectors:files\");\n\ninterface FilesConnectorConfig {\n defaultVolume?: string;\n timeout?: number;\n telemetry?: TelemetryOptions;\n customContentTypes?: Record<string, string>;\n}\n\nexport class FilesConnector {\n private readonly name = \"files\";\n private defaultVolume: string | undefined;\n private readonly customContentTypes: Record<string, string> | undefined;\n\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n operationCount: Counter;\n operationDuration: Histogram;\n };\n\n constructor(config: FilesConnectorConfig) {\n this.defaultVolume = config.defaultVolume;\n this.customContentTypes = config.customContentTypes;\n\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.telemetryMetrics = {\n operationCount: this.telemetry\n .getMeter()\n .createCounter(\"files.operation.count\", {\n description: \"Total number of file operations\",\n unit: \"1\",\n }),\n operationDuration: this.telemetry\n .getMeter()\n .createHistogram(\"files.operation.duration\", {\n description: \"Duration of file operations\",\n unit: \"ms\",\n }),\n };\n }\n\n resolvePath(filePath: string): string {\n if (filePath.length > 4096) {\n throw new Error(\n `Path exceeds maximum length of 4096 characters (got ${filePath.length}).`,\n );\n }\n if (filePath.includes(\"\\0\")) {\n throw new Error(\"Path must not contain null bytes.\");\n }\n\n const segments = filePath.split(\"/\");\n if (segments.some((s) => s === \"..\")) {\n throw new Error('Path traversal (\"../\") is not allowed.');\n }\n if (filePath.startsWith(\"/\")) {\n if (!filePath.startsWith(\"/Volumes/\")) {\n throw new Error(\n 'Absolute paths must start with \"/Volumes/\". ' +\n \"Unity Catalog volume paths follow the format: /Volumes/<catalog>/<schema>/<volume>/\",\n );\n }\n return filePath;\n }\n if (!this.defaultVolume) {\n throw new Error(\n \"Cannot resolve relative path: no default volume set. Use an absolute path or set a default volume.\",\n );\n }\n return `${this.defaultVolume}/${filePath}`;\n }\n\n private async traced<T>(\n operation: string,\n attributes: Record<string, string>,\n fn: (span: Span) => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n return this.telemetry.startActiveSpan(\n `files.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"files.operation\": operation,\n ...attributes,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn(span);\n success = true;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n } finally {\n span.end();\n const duration = Date.now() - startTime;\n const metricAttrs = {\n \"files.operation\": operation,\n success: String(success),\n };\n this.telemetryMetrics.operationCount.add(1, metricAttrs);\n this.telemetryMetrics.operationDuration.record(duration, metricAttrs);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n async list(\n client: WorkspaceClient,\n directoryPath?: string,\n ): Promise<DirectoryEntry[]> {\n const resolvedPath = directoryPath\n ? this.resolvePath(directoryPath)\n : this.defaultVolume;\n if (!resolvedPath) {\n throw new Error(\"No directory path provided and no default volume set.\");\n }\n\n return this.traced(\"list\", { \"files.path\": resolvedPath }, async () => {\n const entries: DirectoryEntry[] = [];\n for await (const entry of client.files.listDirectoryContents({\n directory_path: resolvedPath,\n })) {\n entries.push(entry);\n }\n return entries;\n });\n }\n\n async read(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxSize?: number },\n ): Promise<string> {\n const resolvedPath = this.resolvePath(filePath);\n const maxSize = options?.maxSize ?? FILES_MAX_READ_SIZE;\n return this.traced(\"read\", { \"files.path\": resolvedPath }, async () => {\n const response = await this.download(client, filePath);\n if (!response.contents) {\n return \"\";\n }\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let result = \"\";\n let bytesRead = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > maxSize) {\n await reader.cancel();\n throw new Error(\n `File exceeds maximum read size (${maxSize} bytes). Use download() for large files.`,\n );\n }\n result += decoder.decode(value, { stream: true });\n }\n result += decoder.decode();\n return result;\n });\n }\n\n async download(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<DownloadResponse> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"download\", { \"files.path\": resolvedPath }, async () => {\n return client.files.download({\n file_path: resolvedPath,\n });\n });\n }\n\n async exists(client: WorkspaceClient, filePath: string): Promise<boolean> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"exists\", { \"files.path\": resolvedPath }, async () => {\n try {\n await this.metadata(client, filePath);\n return true;\n } catch (error) {\n if (error instanceof ApiError && error.statusCode === 404) {\n return false;\n }\n throw error;\n }\n });\n }\n\n async metadata(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<FileMetadata> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"metadata\", { \"files.path\": resolvedPath }, async () => {\n const response = await client.files.getMetadata({\n file_path: resolvedPath,\n });\n return {\n contentLength: response[\"content-length\"],\n contentType: contentTypeFromPath(\n filePath,\n response[\"content-type\"],\n this.customContentTypes,\n ),\n lastModified: response[\"last-modified\"],\n };\n });\n }\n\n async upload(\n client: WorkspaceClient,\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n\n return this.traced(\"upload\", { \"files.path\": resolvedPath }, async () => {\n const body = contents;\n const overwrite = options?.overwrite ?? true;\n\n // Workaround: The SDK's files.upload() has two bugs:\n // 1. It ignores the `contents` field (sets body to undefined)\n // 2. apiClient.request() checks `instanceof` against its own ReadableStream\n // subclass, so standard ReadableStream instances get JSON.stringified to \"{}\"\n // Bypass both by calling the REST API directly with SDK-provided auth.\n const hostValue = client.config.host;\n if (!hostValue) {\n throw new Error(\n \"Databricks host is not configured. Set DATABRICKS_HOST or configure client.config.host.\",\n );\n }\n const host = hostValue.startsWith(\"http\")\n ? hostValue\n : `https://${hostValue}`;\n const url = new URL(`/api/2.0/fs/files${resolvedPath}`, host);\n url.searchParams.set(\"overwrite\", String(overwrite));\n\n const headers = new Headers({\n \"Content-Type\": \"application/octet-stream\",\n });\n const fetchOptions: RequestInit = { method: \"PUT\", headers, body };\n\n if (body instanceof ReadableStream) {\n fetchOptions.duplex = \"half\";\n } else if (body instanceof Buffer) {\n headers.set(\"Content-Length\", String(body.length));\n } else if (typeof body === \"string\") {\n headers.set(\"Content-Length\", String(Buffer.byteLength(body)));\n }\n\n await client.config.authenticate(headers);\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text();\n logger.error(`Upload failed (${res.status}): ${text}`);\n const safeMessage = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n throw new ApiError(\n `Upload failed: ${safeMessage}`,\n \"UPLOAD_FAILED\",\n res.status,\n undefined,\n [],\n );\n }\n });\n }\n\n async createDirectory(\n client: WorkspaceClient,\n directoryPath: string,\n ): Promise<void> {\n const resolvedPath = this.resolvePath(directoryPath);\n return this.traced(\n \"createDirectory\",\n { \"files.path\": resolvedPath },\n async () => {\n await client.files.createDirectory({\n directory_path: resolvedPath,\n });\n },\n );\n }\n\n async delete(client: WorkspaceClient, filePath: string): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"delete\", { \"files.path\": resolvedPath }, async () => {\n await client.files.delete({\n file_path: resolvedPath,\n });\n });\n }\n\n async preview(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxChars?: number },\n ): Promise<FilePreview> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"preview\", { \"files.path\": resolvedPath }, async () => {\n const meta = await this.metadata(client, filePath);\n const isText = isTextContentType(meta.contentType);\n const isImage = meta.contentType?.startsWith(\"image/\") || false;\n\n if (!isText) {\n return { ...meta, textPreview: null, isText: false, isImage };\n }\n\n const response = await client.files.download({\n file_path: resolvedPath,\n });\n if (!response.contents) {\n return { ...meta, textPreview: \"\", isText: true, isImage: false };\n }\n\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let preview = \"\";\n const maxChars = options?.maxChars ?? 1024;\n\n while (preview.length < maxChars) {\n const { done, value } = await reader.read();\n if (done) break;\n preview += decoder.decode(value, { stream: true });\n }\n preview += decoder.decode();\n await reader.cancel();\n\n if (preview.length > maxChars) {\n preview = preview.slice(0, maxChars);\n }\n\n return { ...meta, textPreview: preview, isText: true, isImage: false };\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/files/client.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { ApiError, type WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { TelemetryOptions } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport type {\n DirectoryEntry,\n DownloadResponse,\n FileMetadata,\n FilePreview,\n} from \"../../plugins/files/types\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport {\n contentTypeFromPath,\n FILES_MAX_READ_SIZE,\n isTextContentType,\n} from \"./defaults\";\n\nconst logger = createLogger(\"connectors:files\");\n\n/**\n * Ambient span-attribute propagation for `FilesConnector.traced()`.\n *\n * Callers (e.g. the plugin's `_withAuthModeAttributes` wrapper) set extra\n * span attributes here via `runWithFilesSpanAttributes(attrs, fn)`. The\n * connector's `traced()` decorator reads them and merges them into the\n * span it creates around the SDK call. This lets the plugin tag spans with\n * `files.auth_mode` without opening a duplicate `files.<op>` span.\n *\n * AsyncLocalStorage is used so concurrent requests don't see each other's\n * attributes. Outside an active scope, `getStore()` returns `undefined` and\n * the connector falls back to the static attribute set.\n */\nconst filesSpanAttributesStorage = new AsyncLocalStorage<\n Record<string, string>\n>();\n\n/**\n * Run `fn` with the supplied attributes attached to whatever span the\n * `FilesConnector` opens for its SDK call. Used to propagate request-scoped\n * attributes (e.g. `files.auth_mode`) onto the connector's span without\n * opening a parent span — avoids the 2x span allocation that\n * `startActiveSpan` parented otherwise.\n */\nexport function runWithFilesSpanAttributes<T>(\n attributes: Record<string, string>,\n fn: () => Promise<T>,\n): Promise<T> {\n return filesSpanAttributesStorage.run(attributes, fn);\n}\n\ninterface FilesConnectorConfig {\n defaultVolume?: string;\n timeout?: number;\n telemetry?: TelemetryOptions;\n customContentTypes?: Record<string, string>;\n}\n\nexport class FilesConnector {\n private readonly name = \"files\";\n private defaultVolume: string | undefined;\n private readonly customContentTypes: Record<string, string> | undefined;\n\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n operationCount: Counter;\n operationDuration: Histogram;\n };\n\n constructor(config: FilesConnectorConfig) {\n this.defaultVolume = config.defaultVolume;\n this.customContentTypes = config.customContentTypes;\n\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.telemetryMetrics = {\n operationCount: this.telemetry\n .getMeter()\n .createCounter(\"files.operation.count\", {\n description: \"Total number of file operations\",\n unit: \"1\",\n }),\n operationDuration: this.telemetry\n .getMeter()\n .createHistogram(\"files.operation.duration\", {\n description: \"Duration of file operations\",\n unit: \"ms\",\n }),\n };\n }\n\n resolvePath(filePath: string): string {\n if (filePath.length > 4096) {\n throw new Error(\n `Path exceeds maximum length of 4096 characters (got ${filePath.length}).`,\n );\n }\n if (filePath.includes(\"\\0\")) {\n throw new Error(\"Path must not contain null bytes.\");\n }\n\n const segments = filePath.split(\"/\");\n if (segments.some((s) => s === \"..\")) {\n throw new Error('Path traversal (\"../\") is not allowed.');\n }\n if (filePath.startsWith(\"/\")) {\n if (!filePath.startsWith(\"/Volumes/\")) {\n throw new Error(\n 'Absolute paths must start with \"/Volumes/\". ' +\n \"Unity Catalog volume paths follow the format: /Volumes/<catalog>/<schema>/<volume>/\",\n );\n }\n return filePath;\n }\n if (!this.defaultVolume) {\n throw new Error(\n \"Cannot resolve relative path: no default volume set. Use an absolute path or set a default volume.\",\n );\n }\n return `${this.defaultVolume}/${filePath}`;\n }\n\n private async traced<T>(\n operation: string,\n attributes: Record<string, string>,\n fn: (span: Span) => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n // Pull any ambient attributes set by `runWithFilesSpanAttributes` (e.g.\n // `files.auth_mode` from the plugin layer). Static `attributes` win on\n // collision so callers can override per-call.\n const ambient = filesSpanAttributesStorage.getStore();\n\n return this.telemetry.startActiveSpan(\n `files.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"files.operation\": operation,\n ...(ambient ?? {}),\n ...attributes,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn(span);\n success = true;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n } finally {\n span.end();\n const duration = Date.now() - startTime;\n const metricAttrs = {\n \"files.operation\": operation,\n success: String(success),\n };\n this.telemetryMetrics.operationCount.add(1, metricAttrs);\n this.telemetryMetrics.operationDuration.record(duration, metricAttrs);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n async list(\n client: WorkspaceClient,\n directoryPath?: string,\n ): Promise<DirectoryEntry[]> {\n const resolvedPath = directoryPath\n ? this.resolvePath(directoryPath)\n : this.defaultVolume;\n if (!resolvedPath) {\n throw new Error(\"No directory path provided and no default volume set.\");\n }\n\n return this.traced(\"list\", { \"files.path\": resolvedPath }, async () => {\n const entries: DirectoryEntry[] = [];\n for await (const entry of client.files.listDirectoryContents({\n directory_path: resolvedPath,\n })) {\n entries.push(entry);\n }\n return entries;\n });\n }\n\n async read(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxSize?: number },\n ): Promise<string> {\n const resolvedPath = this.resolvePath(filePath);\n const maxSize = options?.maxSize ?? FILES_MAX_READ_SIZE;\n return this.traced(\"read\", { \"files.path\": resolvedPath }, async () => {\n const response = await this.download(client, filePath);\n if (!response.contents) {\n return \"\";\n }\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let result = \"\";\n let bytesRead = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > maxSize) {\n await reader.cancel();\n throw new Error(\n `File exceeds maximum read size (${maxSize} bytes). Use download() for large files.`,\n );\n }\n result += decoder.decode(value, { stream: true });\n }\n result += decoder.decode();\n return result;\n });\n }\n\n async download(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<DownloadResponse> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"download\", { \"files.path\": resolvedPath }, async () => {\n return client.files.download({\n file_path: resolvedPath,\n });\n });\n }\n\n async exists(client: WorkspaceClient, filePath: string): Promise<boolean> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"exists\", { \"files.path\": resolvedPath }, async () => {\n try {\n await this.metadata(client, filePath);\n return true;\n } catch (error) {\n if (error instanceof ApiError && error.statusCode === 404) {\n return false;\n }\n throw error;\n }\n });\n }\n\n async metadata(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<FileMetadata> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"metadata\", { \"files.path\": resolvedPath }, async () => {\n const response = await client.files.getMetadata({\n file_path: resolvedPath,\n });\n return {\n contentLength: response[\"content-length\"],\n contentType: contentTypeFromPath(\n filePath,\n response[\"content-type\"],\n this.customContentTypes,\n ),\n lastModified: response[\"last-modified\"],\n };\n });\n }\n\n async upload(\n client: WorkspaceClient,\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n\n return this.traced(\"upload\", { \"files.path\": resolvedPath }, async () => {\n const body = contents;\n const overwrite = options?.overwrite ?? true;\n\n // Workaround: The SDK's files.upload() has two bugs:\n // 1. It ignores the `contents` field (sets body to undefined)\n // 2. apiClient.request() checks `instanceof` against its own ReadableStream\n // subclass, so standard ReadableStream instances get JSON.stringified to \"{}\"\n // Bypass both by calling the REST API directly with SDK-provided auth.\n const hostValue = client.config.host;\n if (!hostValue) {\n throw new Error(\n \"Databricks host is not configured. Set DATABRICKS_HOST or configure client.config.host.\",\n );\n }\n const host = hostValue.startsWith(\"http\")\n ? hostValue\n : `https://${hostValue}`;\n const url = new URL(`/api/2.0/fs/files${resolvedPath}`, host);\n url.searchParams.set(\"overwrite\", String(overwrite));\n\n const headers = new Headers({\n \"Content-Type\": \"application/octet-stream\",\n });\n const fetchOptions: RequestInit = { method: \"PUT\", headers, body };\n\n if (body instanceof ReadableStream) {\n fetchOptions.duplex = \"half\";\n } else if (body instanceof Buffer) {\n headers.set(\"Content-Length\", String(body.length));\n } else if (typeof body === \"string\") {\n headers.set(\"Content-Length\", String(Buffer.byteLength(body)));\n }\n\n await client.config.authenticate(headers);\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text();\n logger.error(`Upload failed (${res.status}): ${text}`);\n const safeMessage = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n throw new ApiError(\n `Upload failed: ${safeMessage}`,\n \"UPLOAD_FAILED\",\n res.status,\n undefined,\n [],\n );\n }\n });\n }\n\n async createDirectory(\n client: WorkspaceClient,\n directoryPath: string,\n ): Promise<void> {\n const resolvedPath = this.resolvePath(directoryPath);\n return this.traced(\n \"createDirectory\",\n { \"files.path\": resolvedPath },\n async () => {\n await client.files.createDirectory({\n directory_path: resolvedPath,\n });\n },\n );\n }\n\n async delete(client: WorkspaceClient, filePath: string): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"delete\", { \"files.path\": resolvedPath }, async () => {\n await client.files.delete({\n file_path: resolvedPath,\n });\n });\n }\n\n async preview(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxChars?: number },\n ): Promise<FilePreview> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"preview\", { \"files.path\": resolvedPath }, async () => {\n const meta = await this.metadata(client, filePath);\n const isText = isTextContentType(meta.contentType);\n const isImage = meta.contentType?.startsWith(\"image/\") || false;\n\n if (!isText) {\n return { ...meta, textPreview: null, isText: false, isImage };\n }\n\n const response = await client.files.download({\n file_path: resolvedPath,\n });\n if (!response.contents) {\n return { ...meta, textPreview: \"\", isText: true, isImage: false };\n }\n\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let preview = \"\";\n const maxChars = options?.maxChars ?? 1024;\n\n while (preview.length < maxChars) {\n const { done, value } = await reader.read();\n if (done) break;\n preview += decoder.decode(value, { stream: true });\n }\n preview += decoder.decode();\n await reader.cancel();\n\n if (preview.length > maxChars) {\n preview = preview.slice(0, maxChars);\n }\n\n return { ...meta, textPreview: preview, isText: true, isImage: false };\n });\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { FILES_MAX_READ_SIZE, SAFE_INLINE_CONTENT_TYPES, contentTypeFromPath, isSafeInlineContentType, isTextContentType, validateCustomContentTypes } from "./defaults.js";
2
- import { FilesConnector } from "./client.js";
2
+ import { FilesConnector, runWithFilesSpanAttributes } from "./client.js";
3
3
 
4
4
  export { };
@@ -2,7 +2,7 @@ import { createLakebasePoolManager } from "./lakebase/pool-manager.js";
2
2
  import { RoutingPool } from "./lakebase/routing-pool.js";
3
3
  import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "./lakebase/index.js";
4
4
  import { FILES_MAX_READ_SIZE, SAFE_INLINE_CONTENT_TYPES, contentTypeFromPath, isSafeInlineContentType, isTextContentType, validateCustomContentTypes } from "./files/defaults.js";
5
- import { FilesConnector } from "./files/client.js";
5
+ import { FilesConnector, runWithFilesSpanAttributes } from "./files/client.js";
6
6
  import "./files/index.js";
7
7
  import { GenieConnector } from "./genie/client.js";
8
8
  import "./genie/index.js";
@@ -30,13 +30,59 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
30
30
  /**
31
31
  * Generates resource requirements dynamically from discovered + configured volumes.
32
32
  * Each volume key maps to a `DATABRICKS_VOLUME_{KEY_UPPERCASE}` env var.
33
+ *
34
+ * ## Per-volume permission scope (SP vs OBO)
35
+ *
36
+ * The returned manifest entries describe a single permission grant per
37
+ * volume, but the *grantee* depends on the volume's `auth` setting at
38
+ * runtime — and that distinction is **not** expressed in the manifest
39
+ * today:
40
+ *
41
+ * - **Service-principal volumes** (the default, `auth: "service-principal"`):
42
+ * the app's service principal needs `WRITE_VOLUME` (or read-equivalent)
43
+ * on the UC volume. This matches the manifest entry as written.
44
+ * - **On-behalf-of-user volumes** (`auth: "on-behalf-of-user"`): SDK calls
45
+ * execute as the **end user**, so the *user* — not the SP — must hold
46
+ * `WRITE_VOLUME` (or read-equivalent) on the UC volume. The SP only
47
+ * needs to be allowed to mint user-token requests; it does not need
48
+ * direct volume permissions.
49
+ *
50
+ * The static manifest cannot currently express this per-volume split, so
51
+ * callers configuring OBO volumes must communicate the per-user permission
52
+ * requirement out-of-band (docs, runbooks, deployment scripts) until the
53
+ * manifest schema gains a per-volume auth scope field.
33
54
  */
34
55
  static getResourceRequirements(config: IFilesConfig): ResourceRequirement[];
35
56
  /**
36
- * Extract user identity from the request.
37
- * Falls back to `getCurrentUserId()` in development mode.
57
+ * Extraction for `VolumeHandle.asUser(req)`. In production we require BOTH
58
+ * `x-forwarded-user` and `x-forwarded-access-token`, and throw
59
+ * `AuthenticationError.missingToken` if either is missing — otherwise a
60
+ * request with only `x-forwarded-user: alice` would let policies see Alice
61
+ * as a "real user" (`isServicePrincipal: false`) while the SDK call below
62
+ * falls through to the SP client because `_buildUserContextOrNull` returns
63
+ * `null`. Net effect: policy approves the user, SDK runs as SP, privilege
64
+ * confusion (CWE-639/863).
65
+ *
66
+ * In development (`NODE_ENV === "development"`) we keep a local-loop
67
+ * convenience: if either header is missing we emit a single warning and
68
+ * return a policy user explicitly marked `isServicePrincipal: true`, so
69
+ * even in dev a `usersOnly`-style policy that gates on
70
+ * `!user.isServicePrincipal` cannot be tricked. The matching SDK execution
71
+ * path also falls through to the SP client (no `runInUserContext` wrap),
72
+ * so the policy user and the SDK identity stay aligned.
38
73
  */
39
74
  private _extractUser;
75
+ /**
76
+ * Extraction for OBO (on-behalf-of-user) volumes on the HTTP path. Both the
77
+ * `x-forwarded-access-token` and `x-forwarded-user` headers must be present
78
+ * for a valid end-user identity. When the token is missing:
79
+ * - In production we throw `AuthenticationError.missingToken` so the route
80
+ * responds with 401 (no SDK call is made).
81
+ * - In development (`NODE_ENV === "development"`) we emit a single warning
82
+ * and fall back to the service principal identity so local testing
83
+ * without a reverse proxy continues to work.
84
+ */
85
+ private _extractOboUser;
40
86
  /**
41
87
  * Check the policy for a volume. No-op if no policy is configured.
42
88
  * Throws `PolicyDeniedError` if denied.
@@ -44,8 +90,22 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
44
90
  private _checkPolicy;
45
91
  /**
46
92
  * HTTP-level wrapper around `_checkPolicy`.
47
- * Extracts user (401 on failure), runs policy (403 on denial).
93
+ * Selects the policy user based on the volume's auth mode (resolved via
94
+ * `_resolveAuth`):
95
+ * - `"service-principal"` (default): use the `x-forwarded-user` header when
96
+ * present, otherwise fall back to the SP identity (legacy behavior).
97
+ * - `"on-behalf-of-user"`: require `x-forwarded-access-token` (and the
98
+ * matching `x-forwarded-user`); 401 in production when missing,
99
+ * dev-fallback to SP identity in development.
100
+ * Then runs the volume policy (403 on denial, 500 on unexpected error).
48
101
  * Returns `true` if the request may proceed, `false` if a response was sent.
102
+ *
103
+ * NOTE: This method only selects which identity the *policy* sees. The
104
+ * matching SDK execution identity is selected separately by
105
+ * `_resolveAuthForRequest` and applied via `_runWithAuth` /
106
+ * `runInUserContext` in each handler. The two selections are designed to
107
+ * converge on the same identity per the policy-user matrix in the docs —
108
+ * see `docs/docs/plugins/files.md#policy-user-matrix`.
49
109
  */
50
110
  private _enforcePolicy;
51
111
  constructor(config: IFilesConfig);
@@ -55,20 +115,65 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
55
115
  * or sends a 404 and returns `{ connector: undefined }`.
56
116
  */
57
117
  private _resolveVolume;
118
+ /**
119
+ * Extract `req.query.path` as a single string when present.
120
+ *
121
+ * Express coerces repeated query parameters (`?path=a&path=b`) to a string
122
+ * array and dotted/nested params (`?path[k]=v`) to an object. Reject those
123
+ * with `400` instead of letting non-string values reach `_isValidPath` /
124
+ * `connector.resolvePath`, which would misbehave on arrays or objects.
125
+ *
126
+ * Returns `{ path }` (with `path` either a string or `undefined` when the
127
+ * query parameter was absent) on success. Returns `undefined` and writes a
128
+ * `400` response when the value is not a single string — callers must
129
+ * check the return for `undefined` before continuing.
130
+ */
131
+ private _readPathQuery;
58
132
  /**
59
133
  * Validate a file/directory path from user input.
60
134
  * Returns `true` if valid, or an error message string if invalid.
61
135
  */
62
136
  private _isValidPath;
63
137
  private _readSettings;
138
+ private _writeSettings;
139
+ private _downloadSettings;
64
140
  /**
65
141
  * Invalidate cached list entries for a directory after a write operation.
66
- * Uses the same cache-key format as `_handleList`: resolved path for
67
- * subdirectories, `"__root__"` for the volume root.
142
+ * Must produce the SAME cache-key shape that `_handleList` stored under.
143
+ * `_handleList` builds its key from `req.query.path`: when `path` is
144
+ * provided it uses `connector.resolvePath(path)`, otherwise it uses the
145
+ * sentinel `"__root__"`. The invalidation here must derive the matching
146
+ * directory from the FILE path being written:
147
+ *
148
+ * - `"/Volumes/c/s/v/foo/bar.txt"` → `parentDirectory` returns
149
+ * `"/Volumes/c/s/v/foo"` → resolved path key.
150
+ * - `"/bar.txt"` and `"bar.txt"` → root-level files: matching list cache
151
+ * was a rootless `list()` call → `"__root__"` sentinel.
152
+ * - `"/Volumes/c/s/v/bar.txt"` → `parentDirectory` returns the UC
153
+ * volume path (`"/Volumes/c/s/v"`). That's also root-level — a
154
+ * rootless `list()` would have cached under `"__root__"`, while
155
+ * `list("/Volumes/c/s/v")` and `list("/Volumes/c/s/v/")` would have
156
+ * cached under the volume path with and without trailing slash. All
157
+ * three are invalidated.
158
+ *
159
+ * On OBO volumes the read cache is disabled (see `_readSettings`), so
160
+ * invalidation is a no-op here for `mode === "on-behalf-of-user"`. The
161
+ * cache layer is keyed by `getCurrentUserId()`, so user A's writes can
162
+ * only invalidate user A's cache entry — user B would otherwise see stale
163
+ * data for the same volume/path until TTL. Disabling the cache on OBO
164
+ * trades read performance for correctness; the alternative is a
165
+ * per-(volume, path) generation counter folded into the cache key on
166
+ * writes (a future enhancement).
68
167
  *
69
- * Cache keys include `getCurrentUserId()` must match the identity used
70
- * by `this.execute()` in `_handleList`. Both run in service-principal
71
- * context; wrapping either in `runInUserContext` would break invalidation.
168
+ * Best-effort: a thrown `connector.resolvePath` (e.g. on malformed input)
169
+ * is swallowed here. Invalidation is purely an optimization signal — a
170
+ * missed delete only costs read freshness, not correctness, and
171
+ * propagating the error would convert a successful write into an HTTP
172
+ * 500.
173
+ *
174
+ * Returns a `Promise<void>`; callers MUST `await` this before sending the
175
+ * HTTP success response so a follow-up `GET /list` issued in the same tick
176
+ * cannot race the underlying `cache.delete()` and observe stale data.
72
177
  */
73
178
  private _invalidateListCache;
74
179
  private _handleApiError;
@@ -89,19 +194,103 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
89
194
  private _handleUpload;
90
195
  private _handleMkdir;
91
196
  private _handleDelete;
197
+ private _resolveAuth;
92
198
  /**
93
- * Creates a VolumeAPI for a specific volume key.
199
+ * Build a `UserContext` from request headers when both
200
+ * `x-forwarded-access-token` and `x-forwarded-user` are present, otherwise
201
+ * return `null`. Used by OBO route handlers to wrap SDK calls in the
202
+ * end-user's identity. A `null` result means "fall back to the service
203
+ * principal client" — for OBO volumes in production, `_enforcePolicy` will
204
+ * already have responded 401 before we get here, so `null` is reachable
205
+ * only on the dev-fallback path.
206
+ */
207
+ private _buildUserContextOrNull;
208
+ /**
209
+ * Build the telemetry attribute hash for the `files.auth_mode` span
210
+ * attribute. The value reflects what operationally happened — i.e.
211
+ * whether `runInUserContext` actually wrapped the SDK call:
212
+ * - HTTP route on OBO volume + valid token → `"on-behalf-of-user"`.
213
+ * - HTTP route on OBO volume + dev-fallback (no token) →
214
+ * `"service-principal"` (the route falls through to the SP client).
215
+ * - HTTP route on SP volume → `"service-principal"`.
216
+ * - `asUser(req)` programmatic calls with a real user context →
217
+ * `"on-behalf-of-user"`.
218
+ * - Any unwrapped path → `"service-principal"`.
219
+ */
220
+ private _authModeAttributes;
221
+ /**
222
+ * One-shot resolver for HTTP route handlers. Builds the request's
223
+ * `UserContext` AT MOST ONCE (when the volume is OBO and the headers are
224
+ * present) and returns both the operationally-effective auth mode and the
225
+ * pre-built `UserContext`.
226
+ *
227
+ * Handlers thread the `userCtx` into `_runWithAuth(userCtx, fn)` to avoid
228
+ * a second `ServiceContext.createUserContext()` allocation — that call
229
+ * builds a fresh `WorkspaceClient` per invocation, so doing it twice per
230
+ * request was pure throwaway overhead.
231
+ */
232
+ private _resolveAuthForRequest;
233
+ /**
234
+ * Run `fn` under the correct execution context.
235
+ * - `userCtx` is `null`: invokes `fn` directly so the service-principal
236
+ * `WorkspaceClient` and `getCurrentUserId()` are used — identical
237
+ * behavior to pre-OBO releases. This covers both SP volumes and the
238
+ * OBO dev-fallback path (where headers were missing).
239
+ * - `userCtx` is a `UserContext`: wraps `fn` in `runInUserContext(userCtx)`,
240
+ * so SDK calls execute as the end user and `getCurrentUserId()` (and
241
+ * therefore cache keys) resolve to the user's ID.
94
242
  *
95
- * By default, enforces the volume's policy before each operation.
96
- * Pass `bypassPolicy: true` to skip policy checks — useful for
97
- * background jobs or migrations that should bypass user-facing policies.
243
+ * The caller is responsible for building `userCtx` exactly once per
244
+ * request via `_resolveAuthForRequest`; this signature deliberately does
245
+ * NOT take a `req` so it cannot accidentally re-build the context.
246
+ */
247
+ private _runWithAuth;
248
+ /**
249
+ * Tag the span that `FilesConnector.<operation>` opens with
250
+ * `files.auth_mode`. Programmatic VolumeAPI methods bypass
251
+ * `this.execute(...)` (and therefore the `TelemetryInterceptor`), so the
252
+ * connector's own `files.<operation>` span is the natural place to land
253
+ * this attribute. Rather than opening a parent `files.<operation>` span
254
+ * (which would duplicate the connector's span — same name, doubled
255
+ * allocation/export), we propagate the attribute via AsyncLocalStorage
256
+ * and let the connector merge it into its existing span at creation
257
+ * time.
258
+ *
259
+ * The `operation` parameter is unused by the propagation mechanism (the
260
+ * connector knows its own operation), but kept in the signature for API
261
+ * stability with the previous span-creation form.
262
+ */
263
+ private _withAuthModeAttributes;
264
+ /**
265
+ * Wrap each `VolumeAPI` method so the `FilesConnector` span it produces is
266
+ * tagged with `files.auth_mode = "service-principal"`. Used for
267
+ * programmatic calls that don't go through `asUser(req)`.
268
+ *
269
+ * The attribute is attached to the connector's existing span via
270
+ * AsyncLocalStorage propagation (see `_withAuthModeAttributes`); no
271
+ * additional parent span is opened, so each call produces exactly one
272
+ * `files.<operation>` span instead of two.
273
+ */
274
+ private _wrapVolumeAPIWithSPSpan;
275
+ /**
276
+ * Wrap each `VolumeAPI` method so its execution runs inside
277
+ * `runInUserContext(userCtx, ...)`. Used by `VolumeHandle.asUser(req)` to
278
+ * force the SDK identity to the end user regardless of the volume's
279
+ * `auth` setting. The policy check baked into each method (via
280
+ * `createVolumeAPI`) runs inside the same scope, so `getCurrentUserId()`
281
+ * and any cache `userKey` derived from it also resolve to the user.
282
+ *
283
+ * Each wrapped invocation tags the connector's span with
284
+ * `files.auth_mode = "on-behalf-of-user"` via AsyncLocalStorage
285
+ * propagation — no additional parent span is opened.
286
+ */
287
+ private _wrapVolumeAPIInUserContext;
288
+ /**
289
+ * Creates a VolumeAPI for a specific volume key.
98
290
  *
99
- * @security When `bypassPolicy` is `true`, no policy enforcement runs.
100
- * Do not expose bypassed APIs to HTTP routes or end-user code paths.
291
+ * Enforces the volume's policy before each operation.
101
292
  */
102
- protected createVolumeAPI(volumeKey: string, user: FilePolicyUser, options?: {
103
- bypassPolicy?: boolean;
104
- }): VolumeAPI;
293
+ protected createVolumeAPI(volumeKey: string, user: FilePolicyUser): VolumeAPI;
105
294
  /**
106
295
  * Builds the agent-tool registry entries for a single volume. One set of
107
296
  * tools per configured volume, keyed by `${volumeKey}.${method}`.
@@ -123,15 +312,22 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
123
312
  * Returns the programmatic API for the Files plugin.
124
313
  * Callable with a volume key to get a volume-scoped handle.
125
314
  *
126
- * All operations execute as the service principal.
127
- * Use policies to control per-user access.
315
+ * SP volumes (`auth: "service-principal"`, the default) execute as the
316
+ * service principal. OBO volumes (`auth: "on-behalf-of-user"`) executed
317
+ * through the HTTP routes run as the end user; for programmatic calls
318
+ * outside a route, use `asUser(req)` to opt into per-user execution.
319
+ * `asUser(req)` is a hard override at the SDK level: it forces every
320
+ * subsequent call to execute as the end user inside `runInUserContext`,
321
+ * regardless of the volume's `auth` setting. Policies control per-user
322
+ * access in either mode.
128
323
  *
129
324
  * @example
130
325
  * ```ts
131
326
  * // Service principal access
132
327
  * appKit.files("uploads").list()
133
328
  *
134
- * // With policy: pass user identity for access control
329
+ * // With policy: pass user identity for access control. The SDK call
330
+ * // also executes as the user (not the service principal).
135
331
  * appKit.files("uploads").asUser(req).list()
136
332
  * ```
137
333
  */
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../src/plugins/files/plugin.ts"],"mappings":";;;;;;;;;;;;;cA4Da,WAAA,SAAoB,MAAA,YAAkB,YAAA;EACjD,IAAA;;SAGO,QAAA,EAAuB,cAAA;EAAA,iBACb,WAAA;EAAA,UACC,MAAA,EAAQ,YAAA;EAAA,QAElB,gBAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;EAAA,QACA,KAAA;;;;;;SAOD,eAAA,CAAgB,MAAA,EAAQ,YAAA,GAAe,MAAA,SAAe,YAAA;EAuBtB;;;;EAAA,OAAhC,uBAAA,CAAwB,MAAA,EAAQ,YAAA,GAAe,mBAAA;EAs3BnD;;;;EAAA,QAh2BK,YAAA;EA8hCE;;;;EAAA,QA3gCI,YAAA;EAlFiB;;;;;EAAA,QAmHjB,cAAA;cAsCF,MAAA,EAAQ,YAAA;EA8CpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EAnMS;;;;EAAA,QAiUtB,cAAA;EA5TA;;;;EAAA,QAmVA,YAAA;EAAA,QAQA,aAAA;EAlVsC;;;;;;;;;EAAA,QAsWtC,oBAAA;EAAA,QAgBA,eAAA;EAAA,QAqCA,gBAAA;EAAA,QAOM,WAAA;EAAA,QA8BA,WAAA;EAAA,QAmCA,eAAA;EAAA,QAWA,UAAA;EApKN;;;;;EAAA,QAoLM,UAAA;EAAA,QAiFA,aAAA;EAAA,QAoCA,eAAA;EAAA,QAoCA,cAAA;EAAA,QAoCA,aAAA;EAAA,QAgHA,YAAA;EAAA,QA0CA,aAAA;EA9LA;;;;;;;;;;EAAA,UAkPJ,eAAA,CACR,SAAA,UACA,IAAA,EAAM,cAAA,EACN,OAAA;IAAY,YAAA;EAAA,IACX,SAAA;EA4JK;;;;;;;;;;EAAA,QA9FA,kBAAA;EAAA,QA4FA,cAAA;EAAA,QAEA,UAAA;EAOF,QAAA,CAAA,GAAY,OAAA;EAmBlB,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EAIH,OAAA,CAAQ,IAAA,GAJE,cAAA,GAIoD,MAAA,SAAA,YAAA;EAoBnD;;;;;AA0Cb;;;;;;;;;;;EA1CE,OAAA,CAAA,GAAW,WAAA;EAkCX,YAAA,CAAA,GAAgB,MAAA;AAAA;;;;cAQL,KAAA,EAAK,QAAA,QAAA,WAAA,EAAA,YAAA;;gCAAA,UAAA"}
1
+ {"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../src/plugins/files/plugin.ts"],"mappings":";;;;;;;;;;;;;cAiEa,WAAA,SAAoB,MAAA,YAAkB,YAAA;EACjD,IAAA;;SAGO,QAAA,EAAuB,cAAA;EAAA,iBACb,WAAA;EAAA,UACC,MAAA,EAAQ,YAAA;EAAA,QAElB,gBAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;EAAA,QACA,KAAA;;;;;;SAOD,eAAA,CAAgB,MAAA,EAAQ,YAAA,GAAe,MAAA,SAAe,YAAA;EA4CtB;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,OAAhC,uBAAA,CAAwB,MAAA,EAAQ,YAAA,GAAe,mBAAA;EAxD5B;;;;;;;;;;;;;;;;;;EAAA,QA+FlB,YAAA;EA+JY;;;;;;;;;;EAAA,QA/HZ,eAAA;EAkcM;;;;EAAA,QA1aA,YAAA;EAmqBA;;;;;;;;;;;;;;;;;;;EAAA,QApnBA,cAAA;cAwDF,MAAA,EAAQ,YAAA;EAiDpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA8uCb;;;;EAAA,QAhnCA,cAAA;EAwuCR;;;;;;;;;;;;;EAAA,QAxsCQ,cAAA;EA+uCG;;;;EAAA,QA5tCH,YAAA;EAAA,QAQA,aAAA;EAAA,QAyBA,cAAA;EAAA,QAaA,iBAAA;EAuuCQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,QAprCF,oBAAA;EAAA,QAiFN,eAAA;EAAA,QAuCA,gBAAA;EAAA,QAOM,WAAA;EAAA,QAsCA,WAAA;EAAA,QAoFA,eAAA;EAAA,QAWA,UAAA;;;;;;UAgBA,UAAA;EAAA,QAqFA,aAAA;EAAA,QA0CA,eAAA;EAAA,QA0CA,cAAA;EAAA,QA0CA,aAAA;EAAA,QAkJA,YAAA;EAAA,QA8CA,aAAA;EAAA,QAgDN,YAAA;;;;;;;;;;UAmBA,uBAAA;;;;;;;;;;;;;UAmBA,mBAAA;;;;;;;;;;;;UAiBA,sBAAA;;;;;;;;;;;;;;;UA8BM,YAAA;;;;;;;;;;;;;;;;UAyBN,uBAAA;;;;;;;;;;;UAkBA,wBAAA;;;;;;;;;;;;;UAoCA,2BAAA;;;;;;YAgCE,eAAA,CACR,SAAA,UACA,IAAA,EAAM,cAAA,GACL,SAAA;;;;;;;;;;;UA8DK,kBAAA;EAAA,QA4FA,cAAA;EAAA,QAEA,UAAA;EAOF,QAAA,CAAA,GAAY,OAAA;EAmBlB,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EAIH,OAAA,CAAQ,IAAA,GAJE,cAAA,GAIoD,MAAA,SAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;EA2B9D,OAAA,CAAA,GAAW,WAAA;EAiDX,YAAA,CAAA,GAAgB,MAAA;AAAA;;;;cAQL,KAAA,EAAK,QAAA,QAAA,WAAA,EAAA,YAAA;;gCAAA,UAAA"}