@databricks/appkit 0.22.0 → 0.24.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.
- package/CLAUDE.md +11 -0
- package/NOTICE.md +1 -0
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/docs.js +7 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/generate-types.js +27 -15
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js +3 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js +73 -8
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js +164 -20
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/resource-defaults.js +5 -1
- package/dist/cli/commands/plugin/create/resource-defaults.js.map +1 -1
- package/dist/cli/commands/plugin/index.js +7 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js +7 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +27 -14
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js +39 -9
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/setup.js +6 -5
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/connectors/index.js +1 -0
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/connectors/lakebase-v1/client.js.map +1 -1
- package/dist/connectors/serving/client.js +47 -0
- package/dist/connectors/serving/client.js.map +1 -0
- package/dist/connectors/vector-search/client.js +9 -0
- package/dist/connectors/vector-search/client.js.map +1 -0
- package/dist/connectors/vector-search/index.js +3 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/execution-result.d.ts +26 -0
- package/dist/plugin/execution-result.d.ts.map +1 -0
- package/dist/plugin/index.d.ts +1 -0
- package/dist/plugin/interceptors/retry.js +1 -1
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +7 -4
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +36 -5
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +2 -3
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/files/plugin.d.ts +1 -0
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +36 -59
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -1
- package/dist/plugins/index.js +2 -0
- package/dist/plugins/server/index.d.ts +1 -1
- package/dist/plugins/server/vite-dev-server.js +6 -1
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/plugins/serving/defaults.js +10 -0
- package/dist/plugins/serving/defaults.js.map +1 -0
- package/dist/plugins/serving/index.d.ts +2 -0
- package/dist/plugins/serving/index.js +3 -0
- package/dist/plugins/serving/manifest.js +53 -0
- package/dist/plugins/serving/manifest.js.map +1 -0
- package/dist/plugins/serving/schema-filter.js +52 -0
- package/dist/plugins/serving/schema-filter.js.map +1 -0
- package/dist/plugins/serving/serving.d.ts +38 -0
- package/dist/plugins/serving/serving.d.ts.map +1 -0
- package/dist/plugins/serving/serving.js +227 -0
- package/dist/plugins/serving/serving.js.map +1 -0
- package/dist/plugins/serving/types.d.ts +59 -0
- package/dist/plugins/serving/types.d.ts.map +1 -0
- package/dist/shared/src/execute.d.ts +1 -1
- package/dist/stream/stream-manager.js +1 -0
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/types.js +2 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/type-generator/cache.js +1 -1
- package/dist/type-generator/cache.js.map +1 -1
- package/dist/type-generator/index.js +15 -1
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/migration.js +155 -0
- package/dist/type-generator/migration.js.map +1 -0
- package/dist/type-generator/query-registry.js +77 -4
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/cache.js +38 -0
- package/dist/type-generator/serving/cache.js.map +1 -0
- package/dist/type-generator/serving/converter.js +108 -0
- package/dist/type-generator/serving/converter.js.map +1 -0
- package/dist/type-generator/serving/fetcher.js +54 -0
- package/dist/type-generator/serving/fetcher.js.map +1 -0
- package/dist/type-generator/serving/generator.js +206 -0
- package/dist/type-generator/serving/generator.js.map +1 -0
- package/dist/type-generator/serving/server-file-extractor.d.ts +22 -0
- package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -0
- package/dist/type-generator/serving/server-file-extractor.js +131 -0
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -0
- package/dist/type-generator/serving/vite-plugin.d.ts +24 -0
- package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -0
- package/dist/type-generator/serving/vite-plugin.js +60 -0
- package/dist/type-generator/serving/vite-plugin.js.map +1 -0
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +3 -4
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/docs/api/appkit/Class.Plugin.md +8 -3
- package/docs/api/appkit/Function.appKitServingTypesPlugin.md +24 -0
- package/docs/api/appkit/Function.extractServingEndpoints.md +22 -0
- package/docs/api/appkit/Function.findServerFile.md +20 -0
- package/docs/api/appkit/Interface.EndpointConfig.md +23 -0
- package/docs/api/appkit/Interface.ServingEndpointEntry.md +30 -0
- package/docs/api/appkit/Interface.ServingEndpointRegistry.md +3 -0
- package/docs/api/appkit/TypeAlias.ExecutionResult.md +36 -0
- package/docs/api/appkit/TypeAlias.ServingFactory.md +19 -0
- package/docs/api/appkit.md +39 -31
- package/docs/development/type-generation.md +6 -5
- package/docs/faq.md +66 -0
- package/docs/plugins/analytics.md +1 -1
- package/docs/plugins/custom-plugins.md +4 -0
- package/docs/plugins/plugin-management.md +22 -6
- package/docs/plugins/serving.md +223 -0
- package/docs/plugins/vector-search.md +247 -0
- package/llms.txt +11 -0
- package/package.json +2 -2
- package/sbom.cdx.json +1 -1
|
@@ -15,6 +15,7 @@ import { FILES_DOWNLOAD_DEFAULTS, FILES_MAX_UPLOAD_SIZE, FILES_READ_DEFAULTS, FI
|
|
|
15
15
|
import { parentDirectory, sanitizeFilename } from "./helpers.js";
|
|
16
16
|
import manifest_default from "./manifest.js";
|
|
17
17
|
import { ApiError } from "@databricks/sdk-experimental";
|
|
18
|
+
import { STATUS_CODES } from "node:http";
|
|
18
19
|
import { Readable } from "node:stream";
|
|
19
20
|
|
|
20
21
|
//#region src/plugins/files/plugin.ts
|
|
@@ -347,6 +348,12 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
347
348
|
plugin: this.name
|
|
348
349
|
});
|
|
349
350
|
}
|
|
351
|
+
_sendStatusError(res, status) {
|
|
352
|
+
res.status(status).json({
|
|
353
|
+
error: STATUS_CODES[status] ?? "Unknown Error",
|
|
354
|
+
plugin: this.name
|
|
355
|
+
});
|
|
356
|
+
}
|
|
350
357
|
async _handleList(req, res, connector, volumeKey) {
|
|
351
358
|
const path = req.query.path;
|
|
352
359
|
try {
|
|
@@ -354,14 +361,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
354
361
|
this.warnIfNoUserContext(volumeKey, `list`);
|
|
355
362
|
return connector.list(getWorkspaceClient(), path);
|
|
356
363
|
}, this._readSettings([`files:${volumeKey}:list`, path ? connector.resolvePath(path) : "__root__"]));
|
|
357
|
-
if (result
|
|
358
|
-
res.status
|
|
359
|
-
error: "List failed",
|
|
360
|
-
plugin: this.name
|
|
361
|
-
});
|
|
364
|
+
if (!result.ok) {
|
|
365
|
+
this._sendStatusError(res, result.status);
|
|
362
366
|
return;
|
|
363
367
|
}
|
|
364
|
-
res.json(result);
|
|
368
|
+
res.json(result.data);
|
|
365
369
|
} catch (error) {
|
|
366
370
|
this._handleApiError(res, error, "List failed");
|
|
367
371
|
}
|
|
@@ -381,14 +385,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
381
385
|
this.warnIfNoUserContext(volumeKey, `read`);
|
|
382
386
|
return connector.read(getWorkspaceClient(), path);
|
|
383
387
|
}, this._readSettings([`files:${volumeKey}:read`, connector.resolvePath(path)]));
|
|
384
|
-
if (result
|
|
385
|
-
res.status
|
|
386
|
-
error: "Read failed",
|
|
387
|
-
plugin: this.name
|
|
388
|
-
});
|
|
388
|
+
if (!result.ok) {
|
|
389
|
+
this._sendStatusError(res, result.status);
|
|
389
390
|
return;
|
|
390
391
|
}
|
|
391
|
-
res.type("text/plain").send(result);
|
|
392
|
+
res.type("text/plain").send(result.data);
|
|
392
393
|
} catch (error) {
|
|
393
394
|
this._handleApiError(res, error, "Read failed");
|
|
394
395
|
}
|
|
@@ -423,11 +424,8 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
423
424
|
this.warnIfNoUserContext(volumeKey, `download`);
|
|
424
425
|
return connector.download(getWorkspaceClient(), path);
|
|
425
426
|
}, settings);
|
|
426
|
-
if (response
|
|
427
|
-
res.status
|
|
428
|
-
error: `${label} failed`,
|
|
429
|
-
plugin: this.name
|
|
430
|
-
});
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
this._sendStatusError(res, response.status);
|
|
431
429
|
return;
|
|
432
430
|
}
|
|
433
431
|
const resolvedType = contentTypeFromPath(path, void 0, volumeCfg.customContentTypes);
|
|
@@ -438,14 +436,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
438
436
|
res.setHeader("Content-Security-Policy", "sandbox");
|
|
439
437
|
if (!isSafeInlineContentType(resolvedType)) res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
|
440
438
|
} else res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
|
441
|
-
if (response.contents) {
|
|
442
|
-
const nodeStream = Readable.fromWeb(response.contents);
|
|
439
|
+
if (response.data.contents) {
|
|
440
|
+
const nodeStream = Readable.fromWeb(response.data.contents);
|
|
443
441
|
nodeStream.on("error", (err) => {
|
|
444
442
|
logger.error("Stream error during %s: %O", opts.mode, err);
|
|
445
|
-
if (!res.headersSent)
|
|
446
|
-
error: `${label} failed`,
|
|
447
|
-
plugin: this.name
|
|
448
|
-
});
|
|
443
|
+
if (!res.headersSent) this._sendStatusError(res, 500);
|
|
449
444
|
else res.destroy();
|
|
450
445
|
});
|
|
451
446
|
nodeStream.pipe(res);
|
|
@@ -469,14 +464,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
469
464
|
this.warnIfNoUserContext(volumeKey, `exists`);
|
|
470
465
|
return connector.exists(getWorkspaceClient(), path);
|
|
471
466
|
}, this._readSettings([`files:${volumeKey}:exists`, connector.resolvePath(path)]));
|
|
472
|
-
if (result
|
|
473
|
-
res.status
|
|
474
|
-
error: "Exists check failed",
|
|
475
|
-
plugin: this.name
|
|
476
|
-
});
|
|
467
|
+
if (!result.ok) {
|
|
468
|
+
this._sendStatusError(res, result.status);
|
|
477
469
|
return;
|
|
478
470
|
}
|
|
479
|
-
res.json({ exists: result });
|
|
471
|
+
res.json({ exists: result.data });
|
|
480
472
|
} catch (error) {
|
|
481
473
|
this._handleApiError(res, error, "Exists check failed");
|
|
482
474
|
}
|
|
@@ -496,14 +488,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
496
488
|
this.warnIfNoUserContext(volumeKey, `metadata`);
|
|
497
489
|
return connector.metadata(getWorkspaceClient(), path);
|
|
498
490
|
}, this._readSettings([`files:${volumeKey}:metadata`, connector.resolvePath(path)]));
|
|
499
|
-
if (result
|
|
500
|
-
res.status
|
|
501
|
-
error: "Metadata fetch failed",
|
|
502
|
-
plugin: this.name
|
|
503
|
-
});
|
|
491
|
+
if (!result.ok) {
|
|
492
|
+
this._sendStatusError(res, result.status);
|
|
504
493
|
return;
|
|
505
494
|
}
|
|
506
|
-
res.json(result);
|
|
495
|
+
res.json(result.data);
|
|
507
496
|
} catch (error) {
|
|
508
497
|
this._handleApiError(res, error, "Metadata fetch failed");
|
|
509
498
|
}
|
|
@@ -523,14 +512,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
523
512
|
this.warnIfNoUserContext(volumeKey, `preview`);
|
|
524
513
|
return connector.preview(getWorkspaceClient(), path);
|
|
525
514
|
}, this._readSettings([`files:${volumeKey}:preview`, connector.resolvePath(path)]));
|
|
526
|
-
if (result
|
|
527
|
-
res.status
|
|
528
|
-
error: "Preview failed",
|
|
529
|
-
plugin: this.name
|
|
530
|
-
});
|
|
515
|
+
if (!result.ok) {
|
|
516
|
+
this._sendStatusError(res, result.status);
|
|
531
517
|
return;
|
|
532
518
|
}
|
|
533
|
-
res.json(result);
|
|
519
|
+
res.json(result.data);
|
|
534
520
|
} catch (error) {
|
|
535
521
|
this._handleApiError(res, error, "Preview failed");
|
|
536
522
|
}
|
|
@@ -576,16 +562,13 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
576
562
|
return { success: true };
|
|
577
563
|
}, settings));
|
|
578
564
|
this._invalidateListCache(volumeKey, path, this.resolveUserId(req), connector);
|
|
579
|
-
if (result
|
|
565
|
+
if (!result.ok) {
|
|
580
566
|
logger.error(req, "Upload failed: volume=%s path=%s, size=%d bytes", volumeKey, path, contentLength ?? 0);
|
|
581
|
-
res.status
|
|
582
|
-
error: "Upload failed",
|
|
583
|
-
plugin: this.name
|
|
584
|
-
});
|
|
567
|
+
this._sendStatusError(res, result.status);
|
|
585
568
|
return;
|
|
586
569
|
}
|
|
587
570
|
logger.debug(req, "Upload complete: volume=%s path=%s", volumeKey, path);
|
|
588
|
-
res.json(result);
|
|
571
|
+
res.json(result.data);
|
|
589
572
|
} catch (error) {
|
|
590
573
|
if (error instanceof Error && error.message.includes("exceeds maximum allowed size")) {
|
|
591
574
|
res.status(413).json({
|
|
@@ -616,14 +599,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
616
599
|
return { success: true };
|
|
617
600
|
}, settings));
|
|
618
601
|
this._invalidateListCache(volumeKey, dirPath, this.resolveUserId(req), connector);
|
|
619
|
-
if (result
|
|
620
|
-
res.status
|
|
621
|
-
error: "Create directory failed",
|
|
622
|
-
plugin: this.name
|
|
623
|
-
});
|
|
602
|
+
if (!result.ok) {
|
|
603
|
+
this._sendStatusError(res, result.status);
|
|
624
604
|
return;
|
|
625
605
|
}
|
|
626
|
-
res.json(result);
|
|
606
|
+
res.json(result.data);
|
|
627
607
|
} catch (error) {
|
|
628
608
|
this._handleApiError(res, error, "Create directory failed");
|
|
629
609
|
}
|
|
@@ -648,14 +628,11 @@ var FilesPlugin = class FilesPlugin extends Plugin {
|
|
|
648
628
|
return { success: true };
|
|
649
629
|
}, settings));
|
|
650
630
|
this._invalidateListCache(volumeKey, path, this.resolveUserId(req), connector);
|
|
651
|
-
if (result
|
|
652
|
-
res.status
|
|
653
|
-
error: "Delete failed",
|
|
654
|
-
plugin: this.name
|
|
655
|
-
});
|
|
631
|
+
if (!result.ok) {
|
|
632
|
+
this._sendStatusError(res, result.status);
|
|
656
633
|
return;
|
|
657
634
|
}
|
|
658
|
-
res.json(result);
|
|
635
|
+
res.json(result.data);
|
|
659
636
|
} catch (error) {
|
|
660
637
|
this._handleApiError(res, error, "Delete failed");
|
|
661
638
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["manifest","files"],"sources":["../../../src/plugins/files/plugin.ts"],"sourcesContent":["import { Readable } from \"node:stream\";\nimport { ApiError } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type { IAppRouter, PluginExecutionSettings } from \"shared\";\nimport {\n contentTypeFromPath,\n FilesConnector,\n isSafeInlineContentType,\n validateCustomContentTypes,\n} from \"../../connectors/files\";\nimport { getWorkspaceClient, isInUserContext } from \"../../context\";\nimport { AuthenticationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport {\n FILES_DOWNLOAD_DEFAULTS,\n FILES_MAX_UPLOAD_SIZE,\n FILES_READ_DEFAULTS,\n FILES_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport { parentDirectory, sanitizeFilename } from \"./helpers\";\nimport manifest from \"./manifest.json\";\nimport type {\n DownloadResponse,\n FilesExport,\n IFilesConfig,\n VolumeAPI,\n VolumeConfig,\n VolumeHandle,\n} from \"./types\";\n\nconst logger = createLogger(\"files\");\n\nexport class FilesPlugin extends Plugin {\n name = \"files\";\n\n /** Plugin manifest declaring metadata and resource requirements. */\n static manifest = manifest as PluginManifest;\n protected static description = \"Files plugin for Databricks file operations\";\n protected declare config: IFilesConfig;\n\n private volumeConnectors: Record<string, FilesConnector> = {};\n private volumeConfigs: Record<string, VolumeConfig> = {};\n private volumeKeys: string[] = [];\n\n /**\n * Scans `process.env` for `DATABRICKS_VOLUME_*` keys and merges them with\n * any explicitly configured volumes. Explicit config wins for per-volume\n * overrides; auto-discovered volumes get default `{}` config.\n */\n static discoverVolumes(config: IFilesConfig): Record<string, VolumeConfig> {\n const explicit = config.volumes ?? {};\n const discovered: Record<string, VolumeConfig> = {};\n\n const prefix = \"DATABRICKS_VOLUME_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n const suffix = key.slice(prefix.length);\n if (!suffix) continue;\n if (!process.env[key]) continue;\n const volumeKey = suffix.toLowerCase();\n if (!(volumeKey in explicit)) {\n discovered[volumeKey] = {};\n }\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured volumes.\n * Each volume key maps to a `DATABRICKS_VOLUME_{KEY_UPPERCASE}` env var.\n */\n static getResourceRequirements(config: IFilesConfig): ResourceRequirement[] {\n const volumes = FilesPlugin.discoverVolumes(config);\n return Object.keys(volumes).map((key) => ({\n type: ResourceType.VOLUME,\n alias: `volume-${key}`,\n resourceKey: `volume-${key}`,\n description: `Unity Catalog Volume for \"${key}\" file storage`,\n permission: \"WRITE_VOLUME\",\n fields: {\n path: {\n env: `DATABRICKS_VOLUME_${key.toUpperCase()}`,\n description: `Volume path for \"${key}\" (e.g. /Volumes/catalog/schema/volume_name)`,\n },\n },\n required: true,\n }));\n }\n\n /**\n * Warns when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is strongly recommended.\n */\n private warnIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n logger.warn(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). ` +\n `Please use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n /**\n * Throws when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is enforced for now.\n */\n private throwIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n throw new Error(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). Use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n constructor(config: IFilesConfig) {\n super(config);\n this.config = config;\n\n if (config.customContentTypes) {\n validateCustomContentTypes(config.customContentTypes);\n }\n\n const volumes = FilesPlugin.discoverVolumes(config);\n this.volumeKeys = Object.keys(volumes);\n\n for (const key of this.volumeKeys) {\n const volumeCfg = volumes[key];\n const envVar = `DATABRICKS_VOLUME_${key.toUpperCase()}`;\n const volumePath = process.env[envVar];\n\n // Merge per-volume config with plugin-level defaults\n const mergedConfig: VolumeConfig = {\n maxUploadSize: volumeCfg.maxUploadSize ?? config.maxUploadSize,\n customContentTypes:\n volumeCfg.customContentTypes ?? config.customContentTypes,\n };\n this.volumeConfigs[key] = mergedConfig;\n\n this.volumeConnectors[key] = new FilesConnector({\n defaultVolume: volumePath,\n timeout: config.timeout,\n telemetry: config.telemetry,\n customContentTypes: mergedConfig.customContentTypes,\n });\n }\n }\n\n /**\n * Creates a VolumeAPI for a specific volume key.\n * Each method warns if called outside a user context (service principal).\n */\n protected createVolumeAPI(volumeKey: string): VolumeAPI {\n const connector = this.volumeConnectors[volumeKey];\n return {\n list: (directoryPath?: string) => {\n this.throwIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), directoryPath);\n },\n read: (filePath: string, options?: { maxSize?: number }) => {\n this.throwIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), filePath, options);\n },\n download: (filePath: string): Promise<DownloadResponse> => {\n this.throwIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), filePath);\n },\n exists: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), filePath);\n },\n metadata: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), filePath);\n },\n upload: (\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ) => {\n this.throwIfNoUserContext(volumeKey, `upload`);\n return connector.upload(\n getWorkspaceClient(),\n filePath,\n contents,\n options,\n );\n },\n createDirectory: (directoryPath: string) => {\n this.throwIfNoUserContext(volumeKey, `createDirectory`);\n return connector.createDirectory(getWorkspaceClient(), directoryPath);\n },\n delete: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `delete`);\n return connector.delete(getWorkspaceClient(), filePath);\n },\n preview: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), filePath);\n },\n };\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"volumes\",\n method: \"get\",\n path: \"/volumes\",\n handler: async (_req: express.Request, res: express.Response) => {\n res.json({ volumes: this.volumeKeys });\n },\n });\n\n this.route(router, {\n name: \"list\",\n method: \"get\",\n path: \"/:volumeKey/list\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleList(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"read\",\n method: \"get\",\n path: \"/:volumeKey/read\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRead(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"download\",\n method: \"get\",\n path: \"/:volumeKey/download\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDownload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"raw\",\n method: \"get\",\n path: \"/:volumeKey/raw\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRaw(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"exists\",\n method: \"get\",\n path: \"/:volumeKey/exists\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleExists(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"metadata\",\n method: \"get\",\n path: \"/:volumeKey/metadata\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMetadata(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"preview\",\n method: \"get\",\n path: \"/:volumeKey/preview\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handlePreview(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"upload\",\n method: \"post\",\n path: \"/:volumeKey/upload\",\n skipBodyParsing: true,\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleUpload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"mkdir\",\n method: \"post\",\n path: \"/:volumeKey/mkdir\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMkdir(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"delete\",\n method: \"delete\",\n path: \"/:volumeKey\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDelete(req, res, connector, volumeKey);\n },\n });\n }\n\n /**\n * Resolve `:volumeKey` from the request. Returns the connector and key,\n * or sends a 404 and returns `{ connector: undefined }`.\n */\n private _resolveVolume(\n req: express.Request,\n res: express.Response,\n ):\n | { connector: FilesConnector; volumeKey: string }\n | { connector: undefined; volumeKey: undefined } {\n const volumeKey = req.params.volumeKey;\n const connector = this.volumeConnectors[volumeKey];\n if (!connector) {\n const safeKey = volumeKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown volume \"${safeKey}\"`,\n plugin: this.name,\n });\n return { connector: undefined, volumeKey: undefined };\n }\n return { connector, volumeKey };\n }\n\n /**\n * Validate a file/directory path from user input.\n * Returns `true` if valid, or an error message string if invalid.\n */\n private _isValidPath(path: string | undefined): true | string {\n if (!path) return \"path is required\";\n if (path.length > 4096)\n return `path exceeds maximum length of 4096 characters (got ${path.length})`;\n if (path.includes(\"\\0\")) return \"path must not contain null bytes\";\n return true;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...FILES_READ_DEFAULTS,\n cache: { ...FILES_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n /**\n * Invalidate cached list entries for a directory after a write operation.\n * Uses the same cache-key format as `_handleList`: resolved path for\n * subdirectories, `\"__root__\"` for the volume root.\n */\n private _invalidateListCache(\n volumeKey: string,\n parentPath: string,\n userId: string,\n connector: FilesConnector,\n ): void {\n const parent = parentDirectory(parentPath);\n const cachePathSegment = parent\n ? connector.resolvePath(parent)\n : \"__root__\";\n const listKey = this.cache.generateKey(\n [`files:${volumeKey}:list`, cachePathSegment],\n userId,\n );\n this.cache.delete(listKey);\n }\n\n private _handleApiError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n if (error instanceof AuthenticationError) {\n res.status(401).json({\n error: error.message,\n plugin: this.name,\n });\n return;\n }\n if (error instanceof ApiError) {\n const status = error.statusCode ?? 500;\n if (status >= 400 && status < 500) {\n res.status(status).json({\n error: error.message,\n statusCode: status,\n plugin: this.name,\n });\n return;\n }\n logger.error(\"Upstream server error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n return;\n }\n logger.error(\"Unhandled error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n }\n\n private async _handleList(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string | undefined;\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:list`,\n path ? connector.resolvePath(path) : \"__root__\",\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"List failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"List failed\");\n }\n }\n\n private async _handleRead(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:read`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Read failed\", plugin: this.name });\n return;\n }\n res.type(\"text/plain\").send(result);\n } catch (error) {\n this._handleApiError(res, error, \"Read failed\");\n }\n }\n\n private async _handleDownload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"download\",\n });\n }\n\n private async _handleRaw(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"raw\",\n });\n }\n\n /**\n * Shared handler for `/download` and `/raw` endpoints.\n * - `download`: always forces `Content-Disposition: attachment`.\n * - `raw`: adds CSP sandbox; forces attachment only for unsafe content types.\n */\n private async _serveFile(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n opts: { mode: \"download\" | \"raw\" },\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const label = opts.mode === \"download\" ? \"Download\" : \"Raw fetch\";\n const volumeCfg = this.volumeConfigs[volumeKey];\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_DOWNLOAD_DEFAULTS,\n };\n const response = await userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), path);\n }, settings);\n\n if (response === undefined) {\n res.status(500).json({ error: `${label} failed`, plugin: this.name });\n return;\n }\n\n const resolvedType = contentTypeFromPath(\n path,\n undefined,\n volumeCfg.customContentTypes,\n );\n const fileName = sanitizeFilename(path.split(\"/\").pop() ?? \"download\");\n\n res.setHeader(\"Content-Type\", resolvedType);\n res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n\n if (opts.mode === \"raw\") {\n res.setHeader(\"Content-Security-Policy\", \"sandbox\");\n if (!isSafeInlineContentType(resolvedType)) {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n } else {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n\n if (response.contents) {\n const nodeStream = Readable.fromWeb(\n response.contents as import(\"node:stream/web\").ReadableStream,\n );\n nodeStream.on(\"error\", (err) => {\n logger.error(\"Stream error during %s: %O\", opts.mode, err);\n if (!res.headersSent) {\n res\n .status(500)\n .json({ error: `${label} failed`, plugin: this.name });\n } else {\n res.destroy();\n }\n });\n nodeStream.pipe(res);\n } else {\n res.end();\n }\n } catch (error) {\n this._handleApiError(res, error, `${label} failed`);\n }\n }\n\n private async _handleExists(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:exists`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Exists check failed\", plugin: this.name });\n return;\n }\n res.json({ exists: result });\n } catch (error) {\n this._handleApiError(res, error, \"Exists check failed\");\n }\n }\n\n private async _handleMetadata(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:metadata`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Metadata fetch failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Metadata fetch failed\");\n }\n }\n\n private async _handlePreview(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:preview`,\n connector.resolvePath(path),\n ]),\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Preview failed\", plugin: this.name });\n return;\n }\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Preview failed\");\n }\n }\n\n private async _handleUpload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const volumeCfg = this.volumeConfigs[volumeKey];\n const maxSize = volumeCfg.maxUploadSize ?? FILES_MAX_UPLOAD_SIZE;\n const rawContentLength = req.headers[\"content-length\"];\n const contentLength = rawContentLength\n ? parseInt(rawContentLength, 10)\n : undefined;\n\n if (\n contentLength !== undefined &&\n !Number.isNaN(contentLength) &&\n contentLength > maxSize\n ) {\n res.status(413).json({\n error: `File size (${contentLength} bytes) exceeds maximum allowed size (${maxSize} bytes).`,\n plugin: this.name,\n });\n return;\n }\n\n logger.debug(req, \"Upload started: volume=%s path=%s\", volumeKey, path);\n\n try {\n const rawStream: ReadableStream<Uint8Array> = Readable.toWeb(req);\n\n let bytesReceived = 0;\n const webStream = rawStream.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesReceived += chunk.byteLength;\n if (bytesReceived > maxSize) {\n controller.error(\n new Error(\n `Upload stream exceeds maximum allowed size (${maxSize} bytes)`,\n ),\n );\n return;\n }\n controller.enqueue(chunk);\n },\n }),\n );\n\n logger.debug(\n req,\n \"Upload body received: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `upload`);\n await connector.upload(getWorkspaceClient(), path, webStream);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n logger.error(\n req,\n \"Upload failed: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n res.status(500).json({ error: \"Upload failed\", plugin: this.name });\n return;\n }\n\n logger.debug(req, \"Upload complete: volume=%s path=%s\", volumeKey, path);\n res.json(result);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"exceeds maximum allowed size\")\n ) {\n res.status(413).json({ error: error.message, plugin: this.name });\n return;\n }\n this._handleApiError(res, error, \"Upload failed\");\n }\n }\n\n private async _handleMkdir(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const dirPath =\n typeof req.body?.path === \"string\" ? req.body.path : undefined;\n const valid = this._isValidPath(dirPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `createDirectory`);\n await connector.createDirectory(getWorkspaceClient(), dirPath);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n dirPath,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n res\n .status(500)\n .json({ error: \"Create directory failed\", plugin: this.name });\n return;\n }\n\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Create directory failed\");\n }\n }\n\n private async _handleDelete(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const rawPath = req.query.path as string | undefined;\n const valid = this._isValidPath(rawPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n const path = rawPath as string;\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `delete`);\n await connector.delete(getWorkspaceClient(), path);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (result === undefined) {\n res.status(500).json({ error: \"Delete failed\", plugin: this.name });\n return;\n }\n\n res.json(result);\n } catch (error) {\n this._handleApiError(res, error, \"Delete failed\");\n }\n }\n\n private inflightWrites = 0;\n\n private trackWrite<T>(fn: () => Promise<T>): Promise<T> {\n this.inflightWrites++;\n return fn().finally(() => {\n this.inflightWrites--;\n });\n }\n\n async shutdown(): Promise<void> {\n // Wait up to 10 seconds for in-flight write operations to finish\n const deadline = Date.now() + 10_000;\n while (this.inflightWrites > 0 && Date.now() < deadline) {\n logger.info(\n \"Waiting for %d in-flight write(s) to complete before shutdown…\",\n this.inflightWrites,\n );\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (this.inflightWrites > 0) {\n logger.warn(\n \"Shutdown deadline reached with %d in-flight write(s) still pending.\",\n this.inflightWrites,\n );\n }\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the programmatic API for the Files plugin.\n * Callable with a volume key to get a volume-scoped handle.\n *\n * @example\n * ```ts\n * // OBO access (recommended)\n * appKit.files(\"uploads\").asUser(req).list()\n *\n * // Service principal access (logs a warning)\n * appKit.files(\"uploads\").list()\n * ```\n */\n exports(): FilesExport {\n const resolveVolume = (volumeKey: string): VolumeHandle => {\n if (!this.volumeKeys.includes(volumeKey)) {\n throw new Error(\n `Unknown volume \"${volumeKey}\". Available volumes: ${this.volumeKeys.join(\", \")}`,\n );\n }\n\n // Service principal API — each method logs a warning recommending OBO\n const spApi = this.createVolumeAPI(volumeKey);\n\n return {\n ...spApi,\n asUser: (req: import(\"express\").Request) => {\n const userPlugin = this.asUser(req) as FilesPlugin;\n return userPlugin.createVolumeAPI(volumeKey);\n },\n };\n };\n\n const filesExport = ((volumeKey: string) =>\n resolveVolume(volumeKey)) as FilesExport;\n filesExport.volume = resolveVolume;\n\n return filesExport;\n }\n\n clientConfig(): Record<string, unknown> {\n return { volumes: this.volumeKeys };\n }\n}\n\n/**\n * @internal\n */\nexport const files = toPlugin(FilesPlugin);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["manifest","files"],"sources":["../../../src/plugins/files/plugin.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport { Readable } from \"node:stream\";\nimport { ApiError } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type { IAppRouter, PluginExecutionSettings } from \"shared\";\nimport {\n contentTypeFromPath,\n FilesConnector,\n isSafeInlineContentType,\n validateCustomContentTypes,\n} from \"../../connectors/files\";\nimport { getWorkspaceClient, isInUserContext } from \"../../context\";\nimport { AuthenticationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport {\n FILES_DOWNLOAD_DEFAULTS,\n FILES_MAX_UPLOAD_SIZE,\n FILES_READ_DEFAULTS,\n FILES_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport { parentDirectory, sanitizeFilename } from \"./helpers\";\nimport manifest from \"./manifest.json\";\nimport type {\n DownloadResponse,\n FilesExport,\n IFilesConfig,\n VolumeAPI,\n VolumeConfig,\n VolumeHandle,\n} from \"./types\";\n\nconst logger = createLogger(\"files\");\n\nexport class FilesPlugin extends Plugin {\n name = \"files\";\n\n /** Plugin manifest declaring metadata and resource requirements. */\n static manifest = manifest as PluginManifest;\n protected static description = \"Files plugin for Databricks file operations\";\n protected declare config: IFilesConfig;\n\n private volumeConnectors: Record<string, FilesConnector> = {};\n private volumeConfigs: Record<string, VolumeConfig> = {};\n private volumeKeys: string[] = [];\n\n /**\n * Scans `process.env` for `DATABRICKS_VOLUME_*` keys and merges them with\n * any explicitly configured volumes. Explicit config wins for per-volume\n * overrides; auto-discovered volumes get default `{}` config.\n */\n static discoverVolumes(config: IFilesConfig): Record<string, VolumeConfig> {\n const explicit = config.volumes ?? {};\n const discovered: Record<string, VolumeConfig> = {};\n\n const prefix = \"DATABRICKS_VOLUME_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n const suffix = key.slice(prefix.length);\n if (!suffix) continue;\n if (!process.env[key]) continue;\n const volumeKey = suffix.toLowerCase();\n if (!(volumeKey in explicit)) {\n discovered[volumeKey] = {};\n }\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured volumes.\n * Each volume key maps to a `DATABRICKS_VOLUME_{KEY_UPPERCASE}` env var.\n */\n static getResourceRequirements(config: IFilesConfig): ResourceRequirement[] {\n const volumes = FilesPlugin.discoverVolumes(config);\n return Object.keys(volumes).map((key) => ({\n type: ResourceType.VOLUME,\n alias: `volume-${key}`,\n resourceKey: `volume-${key}`,\n description: `Unity Catalog Volume for \"${key}\" file storage`,\n permission: \"WRITE_VOLUME\",\n fields: {\n path: {\n env: `DATABRICKS_VOLUME_${key.toUpperCase()}`,\n description: `Volume path for \"${key}\" (e.g. /Volumes/catalog/schema/volume_name)`,\n },\n },\n required: true,\n }));\n }\n\n /**\n * Warns when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is strongly recommended.\n */\n private warnIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n logger.warn(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). ` +\n `Please use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n /**\n * Throws when a method is called without a user context (i.e. as service principal).\n * OBO access via `asUser(req)` is enforced for now.\n */\n private throwIfNoUserContext(volumeKey: string, method: string): void {\n if (!isInUserContext()) {\n throw new Error(\n `app.files(\"${volumeKey}\").${method}() called without user context (service principal). Use OBO instead: app.files(\"${volumeKey}\").asUser(req).${method}()`,\n );\n }\n }\n\n constructor(config: IFilesConfig) {\n super(config);\n this.config = config;\n\n if (config.customContentTypes) {\n validateCustomContentTypes(config.customContentTypes);\n }\n\n const volumes = FilesPlugin.discoverVolumes(config);\n this.volumeKeys = Object.keys(volumes);\n\n for (const key of this.volumeKeys) {\n const volumeCfg = volumes[key];\n const envVar = `DATABRICKS_VOLUME_${key.toUpperCase()}`;\n const volumePath = process.env[envVar];\n\n // Merge per-volume config with plugin-level defaults\n const mergedConfig: VolumeConfig = {\n maxUploadSize: volumeCfg.maxUploadSize ?? config.maxUploadSize,\n customContentTypes:\n volumeCfg.customContentTypes ?? config.customContentTypes,\n };\n this.volumeConfigs[key] = mergedConfig;\n\n this.volumeConnectors[key] = new FilesConnector({\n defaultVolume: volumePath,\n timeout: config.timeout,\n telemetry: config.telemetry,\n customContentTypes: mergedConfig.customContentTypes,\n });\n }\n }\n\n /**\n * Creates a VolumeAPI for a specific volume key.\n * Each method warns if called outside a user context (service principal).\n */\n protected createVolumeAPI(volumeKey: string): VolumeAPI {\n const connector = this.volumeConnectors[volumeKey];\n return {\n list: (directoryPath?: string) => {\n this.throwIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), directoryPath);\n },\n read: (filePath: string, options?: { maxSize?: number }) => {\n this.throwIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), filePath, options);\n },\n download: (filePath: string): Promise<DownloadResponse> => {\n this.throwIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), filePath);\n },\n exists: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), filePath);\n },\n metadata: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), filePath);\n },\n upload: (\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ) => {\n this.throwIfNoUserContext(volumeKey, `upload`);\n return connector.upload(\n getWorkspaceClient(),\n filePath,\n contents,\n options,\n );\n },\n createDirectory: (directoryPath: string) => {\n this.throwIfNoUserContext(volumeKey, `createDirectory`);\n return connector.createDirectory(getWorkspaceClient(), directoryPath);\n },\n delete: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `delete`);\n return connector.delete(getWorkspaceClient(), filePath);\n },\n preview: (filePath: string) => {\n this.throwIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), filePath);\n },\n };\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"volumes\",\n method: \"get\",\n path: \"/volumes\",\n handler: async (_req: express.Request, res: express.Response) => {\n res.json({ volumes: this.volumeKeys });\n },\n });\n\n this.route(router, {\n name: \"list\",\n method: \"get\",\n path: \"/:volumeKey/list\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleList(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"read\",\n method: \"get\",\n path: \"/:volumeKey/read\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRead(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"download\",\n method: \"get\",\n path: \"/:volumeKey/download\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDownload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"raw\",\n method: \"get\",\n path: \"/:volumeKey/raw\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleRaw(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"exists\",\n method: \"get\",\n path: \"/:volumeKey/exists\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleExists(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"metadata\",\n method: \"get\",\n path: \"/:volumeKey/metadata\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMetadata(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"preview\",\n method: \"get\",\n path: \"/:volumeKey/preview\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handlePreview(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"upload\",\n method: \"post\",\n path: \"/:volumeKey/upload\",\n skipBodyParsing: true,\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleUpload(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"mkdir\",\n method: \"post\",\n path: \"/:volumeKey/mkdir\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleMkdir(req, res, connector, volumeKey);\n },\n });\n\n this.route(router, {\n name: \"delete\",\n method: \"delete\",\n path: \"/:volumeKey\",\n handler: async (req: express.Request, res: express.Response) => {\n const { connector, volumeKey } = this._resolveVolume(req, res);\n if (!connector) return;\n await this._handleDelete(req, res, connector, volumeKey);\n },\n });\n }\n\n /**\n * Resolve `:volumeKey` from the request. Returns the connector and key,\n * or sends a 404 and returns `{ connector: undefined }`.\n */\n private _resolveVolume(\n req: express.Request,\n res: express.Response,\n ):\n | { connector: FilesConnector; volumeKey: string }\n | { connector: undefined; volumeKey: undefined } {\n const volumeKey = req.params.volumeKey;\n const connector = this.volumeConnectors[volumeKey];\n if (!connector) {\n const safeKey = volumeKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown volume \"${safeKey}\"`,\n plugin: this.name,\n });\n return { connector: undefined, volumeKey: undefined };\n }\n return { connector, volumeKey };\n }\n\n /**\n * Validate a file/directory path from user input.\n * Returns `true` if valid, or an error message string if invalid.\n */\n private _isValidPath(path: string | undefined): true | string {\n if (!path) return \"path is required\";\n if (path.length > 4096)\n return `path exceeds maximum length of 4096 characters (got ${path.length})`;\n if (path.includes(\"\\0\")) return \"path must not contain null bytes\";\n return true;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...FILES_READ_DEFAULTS,\n cache: { ...FILES_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n /**\n * Invalidate cached list entries for a directory after a write operation.\n * Uses the same cache-key format as `_handleList`: resolved path for\n * subdirectories, `\"__root__\"` for the volume root.\n */\n private _invalidateListCache(\n volumeKey: string,\n parentPath: string,\n userId: string,\n connector: FilesConnector,\n ): void {\n const parent = parentDirectory(parentPath);\n const cachePathSegment = parent\n ? connector.resolvePath(parent)\n : \"__root__\";\n const listKey = this.cache.generateKey(\n [`files:${volumeKey}:list`, cachePathSegment],\n userId,\n );\n this.cache.delete(listKey);\n }\n\n private _handleApiError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n if (error instanceof AuthenticationError) {\n res.status(401).json({\n error: error.message,\n plugin: this.name,\n });\n return;\n }\n if (error instanceof ApiError) {\n const status = error.statusCode ?? 500;\n if (status >= 400 && status < 500) {\n res.status(status).json({\n error: error.message,\n statusCode: status,\n plugin: this.name,\n });\n return;\n }\n logger.error(\"Upstream server error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n return;\n }\n logger.error(\"Unhandled error in %s: %O\", this.name, error);\n res.status(500).json({ error: fallbackMessage, plugin: this.name });\n }\n\n private _sendStatusError(res: express.Response, status: number): void {\n res.status(status).json({\n error: STATUS_CODES[status] ?? \"Unknown Error\",\n plugin: this.name,\n });\n }\n\n private async _handleList(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string | undefined;\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `list`);\n return connector.list(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:list`,\n path ? connector.resolvePath(path) : \"__root__\",\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\n } catch (error) {\n this._handleApiError(res, error, \"List failed\");\n }\n }\n\n private async _handleRead(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `read`);\n return connector.read(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:read`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.type(\"text/plain\").send(result.data);\n } catch (error) {\n this._handleApiError(res, error, \"Read failed\");\n }\n }\n\n private async _handleDownload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"download\",\n });\n }\n\n private async _handleRaw(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n return this._serveFile(req, res, connector, volumeKey, {\n mode: \"raw\",\n });\n }\n\n /**\n * Shared handler for `/download` and `/raw` endpoints.\n * - `download`: always forces `Content-Disposition: attachment`.\n * - `raw`: adds CSP sandbox; forces attachment only for unsafe content types.\n */\n private async _serveFile(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n opts: { mode: \"download\" | \"raw\" },\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const label = opts.mode === \"download\" ? \"Download\" : \"Raw fetch\";\n const volumeCfg = this.volumeConfigs[volumeKey];\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_DOWNLOAD_DEFAULTS,\n };\n const response = await userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `download`);\n return connector.download(getWorkspaceClient(), path);\n }, settings);\n\n if (!response.ok) {\n this._sendStatusError(res, response.status);\n return;\n }\n\n const resolvedType = contentTypeFromPath(\n path,\n undefined,\n volumeCfg.customContentTypes,\n );\n const fileName = sanitizeFilename(path.split(\"/\").pop() ?? \"download\");\n\n res.setHeader(\"Content-Type\", resolvedType);\n res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n\n if (opts.mode === \"raw\") {\n res.setHeader(\"Content-Security-Policy\", \"sandbox\");\n if (!isSafeInlineContentType(resolvedType)) {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n } else {\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${fileName}\"`,\n );\n }\n\n if (response.data.contents) {\n const nodeStream = Readable.fromWeb(\n response.data.contents as import(\"node:stream/web\").ReadableStream,\n );\n nodeStream.on(\"error\", (err) => {\n logger.error(\"Stream error during %s: %O\", opts.mode, err);\n if (!res.headersSent) {\n this._sendStatusError(res, 500);\n } else {\n res.destroy();\n }\n });\n nodeStream.pipe(res);\n } else {\n res.end();\n }\n } catch (error) {\n this._handleApiError(res, error, `${label} failed`);\n }\n }\n\n private async _handleExists(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `exists`);\n return connector.exists(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:exists`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ exists: result.data });\n } catch (error) {\n this._handleApiError(res, error, \"Exists check failed\");\n }\n }\n\n private async _handleMetadata(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `metadata`);\n return connector.metadata(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:metadata`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\n } catch (error) {\n this._handleApiError(res, error, \"Metadata fetch failed\");\n }\n }\n\n private async _handlePreview(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const result = await userPlugin.execute(\n async () => {\n this.warnIfNoUserContext(volumeKey, `preview`);\n return connector.preview(getWorkspaceClient(), path);\n },\n this._readSettings([\n `files:${volumeKey}:preview`,\n connector.resolvePath(path),\n ]),\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\n } catch (error) {\n this._handleApiError(res, error, \"Preview failed\");\n }\n }\n\n private async _handleUpload(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const path = req.query.path as string;\n const valid = this._isValidPath(path);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n const volumeCfg = this.volumeConfigs[volumeKey];\n const maxSize = volumeCfg.maxUploadSize ?? FILES_MAX_UPLOAD_SIZE;\n const rawContentLength = req.headers[\"content-length\"];\n const contentLength = rawContentLength\n ? parseInt(rawContentLength, 10)\n : undefined;\n\n if (\n contentLength !== undefined &&\n !Number.isNaN(contentLength) &&\n contentLength > maxSize\n ) {\n res.status(413).json({\n error: `File size (${contentLength} bytes) exceeds maximum allowed size (${maxSize} bytes).`,\n plugin: this.name,\n });\n return;\n }\n\n logger.debug(req, \"Upload started: volume=%s path=%s\", volumeKey, path);\n\n try {\n const rawStream: ReadableStream<Uint8Array> = Readable.toWeb(req);\n\n let bytesReceived = 0;\n const webStream = rawStream.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytesReceived += chunk.byteLength;\n if (bytesReceived > maxSize) {\n controller.error(\n new Error(\n `Upload stream exceeds maximum allowed size (${maxSize} bytes)`,\n ),\n );\n return;\n }\n controller.enqueue(chunk);\n },\n }),\n );\n\n logger.debug(\n req,\n \"Upload body received: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `upload`);\n await connector.upload(getWorkspaceClient(), path, webStream);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (!result.ok) {\n logger.error(\n req,\n \"Upload failed: volume=%s path=%s, size=%d bytes\",\n volumeKey,\n path,\n contentLength ?? 0,\n );\n this._sendStatusError(res, result.status);\n return;\n }\n\n logger.debug(req, \"Upload complete: volume=%s path=%s\", volumeKey, path);\n res.json(result.data);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"exceeds maximum allowed size\")\n ) {\n res.status(413).json({ error: error.message, plugin: this.name });\n return;\n }\n this._handleApiError(res, error, \"Upload failed\");\n }\n }\n\n private async _handleMkdir(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const dirPath =\n typeof req.body?.path === \"string\" ? req.body.path : undefined;\n const valid = this._isValidPath(dirPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `createDirectory`);\n await connector.createDirectory(getWorkspaceClient(), dirPath);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n dirPath,\n this.resolveUserId(req),\n connector,\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n\n res.json(result.data);\n } catch (error) {\n this._handleApiError(res, error, \"Create directory failed\");\n }\n }\n\n private async _handleDelete(\n req: express.Request,\n res: express.Response,\n connector: FilesConnector,\n volumeKey: string,\n ): Promise<void> {\n const rawPath = req.query.path as string | undefined;\n const valid = this._isValidPath(rawPath);\n if (valid !== true) {\n res.status(400).json({ error: valid, plugin: this.name });\n return;\n }\n const path = rawPath as string;\n\n try {\n const userPlugin = this.asUser(req);\n const settings: PluginExecutionSettings = {\n default: FILES_WRITE_DEFAULTS,\n };\n const result = await this.trackWrite(() =>\n userPlugin.execute(async () => {\n this.warnIfNoUserContext(volumeKey, `delete`);\n await connector.delete(getWorkspaceClient(), path);\n return { success: true as const };\n }, settings),\n );\n\n this._invalidateListCache(\n volumeKey,\n path,\n this.resolveUserId(req),\n connector,\n );\n\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n\n res.json(result.data);\n } catch (error) {\n this._handleApiError(res, error, \"Delete failed\");\n }\n }\n\n private inflightWrites = 0;\n\n private trackWrite<T>(fn: () => Promise<T>): Promise<T> {\n this.inflightWrites++;\n return fn().finally(() => {\n this.inflightWrites--;\n });\n }\n\n async shutdown(): Promise<void> {\n // Wait up to 10 seconds for in-flight write operations to finish\n const deadline = Date.now() + 10_000;\n while (this.inflightWrites > 0 && Date.now() < deadline) {\n logger.info(\n \"Waiting for %d in-flight write(s) to complete before shutdown…\",\n this.inflightWrites,\n );\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (this.inflightWrites > 0) {\n logger.warn(\n \"Shutdown deadline reached with %d in-flight write(s) still pending.\",\n this.inflightWrites,\n );\n }\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the programmatic API for the Files plugin.\n * Callable with a volume key to get a volume-scoped handle.\n *\n * @example\n * ```ts\n * // OBO access (recommended)\n * appKit.files(\"uploads\").asUser(req).list()\n *\n * // Service principal access (logs a warning)\n * appKit.files(\"uploads\").list()\n * ```\n */\n exports(): FilesExport {\n const resolveVolume = (volumeKey: string): VolumeHandle => {\n if (!this.volumeKeys.includes(volumeKey)) {\n throw new Error(\n `Unknown volume \"${volumeKey}\". Available volumes: ${this.volumeKeys.join(\", \")}`,\n );\n }\n\n // Service principal API — each method logs a warning recommending OBO\n const spApi = this.createVolumeAPI(volumeKey);\n\n return {\n ...spApi,\n asUser: (req: import(\"express\").Request) => {\n const userPlugin = this.asUser(req) as FilesPlugin;\n return userPlugin.createVolumeAPI(volumeKey);\n },\n };\n };\n\n const filesExport = ((volumeKey: string) =>\n resolveVolume(volumeKey)) as FilesExport;\n filesExport.volume = resolveVolume;\n\n return filesExport;\n }\n\n clientConfig(): Record<string, unknown> {\n return { volumes: this.volumeKeys };\n }\n}\n\n/**\n * @internal\n */\nexport const files = toPlugin(FilesPlugin);\n"],"mappings":"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"}
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -12,4 +12,7 @@ import "./genie/index.js";
|
|
|
12
12
|
import { ILakebaseConfig } from "./lakebase/types.js";
|
|
13
13
|
import { lakebase } from "./lakebase/lakebase.js";
|
|
14
14
|
import "./lakebase/index.js";
|
|
15
|
-
import { ServerPlugin, server } from "./server/index.js";
|
|
15
|
+
import { ServerPlugin, server } from "./server/index.js";
|
|
16
|
+
import { EndpointConfig, IServingConfig, ServingEndpointEntry, ServingEndpointHandle, ServingEndpointMethods, ServingEndpointRegistry, ServingFactory } from "./serving/types.js";
|
|
17
|
+
import { ServingPlugin, serving } from "./serving/serving.js";
|
|
18
|
+
import "./serving/index.js";
|
package/dist/plugins/index.js
CHANGED
|
@@ -8,5 +8,7 @@ import "./genie/index.js";
|
|
|
8
8
|
import { lakebase } from "./lakebase/lakebase.js";
|
|
9
9
|
import "./lakebase/index.js";
|
|
10
10
|
import { ServerPlugin, server } from "./server/index.js";
|
|
11
|
+
import { ServingPlugin, serving } from "./serving/serving.js";
|
|
12
|
+
import "./serving/index.js";
|
|
11
13
|
|
|
12
14
|
export { };
|
|
@@ -5,8 +5,8 @@ import "../../plugin/index.js";
|
|
|
5
5
|
import { PluginManifest } from "../../registry/types.js";
|
|
6
6
|
import "../../registry/index.js";
|
|
7
7
|
import { ServerConfig } from "./types.js";
|
|
8
|
-
import express from "express";
|
|
9
8
|
import { Server } from "node:http";
|
|
9
|
+
import express from "express";
|
|
10
10
|
|
|
11
11
|
//#region src/plugins/server/index.d.ts
|
|
12
12
|
/**
|
|
@@ -3,6 +3,7 @@ import { ServerError } from "../../errors/server.js";
|
|
|
3
3
|
import { init_errors } from "../../errors/index.js";
|
|
4
4
|
import { mergeConfigDedup } from "../../utils/vite-config-merge.js";
|
|
5
5
|
import { BaseServer } from "./base-server.js";
|
|
6
|
+
import { appKitServingTypesPlugin } from "../../type-generator/serving/vite-plugin.js";
|
|
6
7
|
import { appKitTypesPlugin } from "../../type-generator/vite-plugin.js";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import fs from "node:fs";
|
|
@@ -55,7 +56,11 @@ var ViteDevServer = class extends BaseServer {
|
|
|
55
56
|
ignored: ["**/node_modules/**", "!**/node_modules/@databricks/**"]
|
|
56
57
|
}
|
|
57
58
|
},
|
|
58
|
-
plugins: [
|
|
59
|
+
plugins: [
|
|
60
|
+
react.default(),
|
|
61
|
+
appKitTypesPlugin(),
|
|
62
|
+
appKitServingTypesPlugin()
|
|
63
|
+
],
|
|
59
64
|
appType: "custom"
|
|
60
65
|
}, mergeConfig));
|
|
61
66
|
this.app.use(this.vite.middlewares);
|
|
@@ -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 {
|
|
1
|
+
{"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../../src/plugins/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { appKitServingTypesPlugin } from \"../../type-generator/serving/vite-plugin\";\nimport { appKitTypesPlugin } from \"../../type-generator/vite-plugin\";\nimport { mergeConfigDedup } from \"../../utils\";\nimport { BaseServer } from \"./base-server\";\nimport type { PluginClientConfigs, PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(\n app: express.Application,\n endpoints: PluginEndpoints = {},\n pluginConfigs: PluginClientConfigs = {},\n ) {\n super(app, endpoints, pluginConfigs);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const viteClientPort = process.env.VITE_CLIENT_PORT;\n const serverHmr = viteClientPort\n ? { hmr: { clientPort: viteClientPort } }\n : {};\n\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n ...serverHmr,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [\n react.default(),\n appKitTypesPlugin(),\n appKitServingTypesPlugin(),\n ],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;aAI2C;AAQ3C,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAC5C,AAAQ;CAER,YACE,KACA,YAA6B,EAAE,EAC/B,gBAAqC,EAAE,EACvC;AACA,QAAM,KAAK,WAAW,cAAc;AACpC,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;EAWxC,MAAM,cATe,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE;EAC7C,MAAM,iBAAiB,QAAQ,IAAI;AAyBnC,OAAK,OAAO,MAAM,iBADI,iBAAiB,YAnBpB;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,GATc,iBACd,EAAE,KAAK,EAAE,YAAY,gBAAgB,EAAE,GACvC,EAAE;IAQF,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS;IACP,MAAM,SAAS;IACf,mBAAmB;IACnB,0BAA0B;IAC3B;GACD,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/serving/defaults.ts"],"sourcesContent":["export const servingInvokeDefaults = {\n cache: {\n enabled: false,\n },\n retry: {\n enabled: false,\n },\n timeout: 120_000,\n};\n"],"mappings":";AAAA,MAAa,wBAAwB;CACnC,OAAO,EACL,SAAS,OACV;CACD,OAAO,EACL,SAAS,OACV;CACD,SAAS;CACV"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/plugins/serving/manifest.json
|
|
2
|
+
var manifest_default = {
|
|
3
|
+
$schema: "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
|
|
4
|
+
name: "serving",
|
|
5
|
+
displayName: "Model Serving Plugin",
|
|
6
|
+
description: "Authenticated proxy to Databricks Model Serving endpoints",
|
|
7
|
+
resources: {
|
|
8
|
+
"required": [{
|
|
9
|
+
"type": "serving_endpoint",
|
|
10
|
+
"alias": "Serving Endpoint",
|
|
11
|
+
"resourceKey": "serving-endpoint",
|
|
12
|
+
"description": "Model Serving endpoint for inference",
|
|
13
|
+
"permission": "CAN_QUERY",
|
|
14
|
+
"fields": { "name": {
|
|
15
|
+
"env": "DATABRICKS_SERVING_ENDPOINT_NAME",
|
|
16
|
+
"description": "Serving endpoint name"
|
|
17
|
+
} }
|
|
18
|
+
}],
|
|
19
|
+
"optional": []
|
|
20
|
+
},
|
|
21
|
+
config: { "schema": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"endpoints": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "Map of alias names to endpoint configurations",
|
|
27
|
+
"additionalProperties": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"env": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Environment variable holding the endpoint name"
|
|
33
|
+
},
|
|
34
|
+
"servedModel": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Target a specific served model (bypasses traffic routing)"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": ["env"]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"timeout": {
|
|
43
|
+
"type": "number",
|
|
44
|
+
"default": 12e4,
|
|
45
|
+
"description": "Request timeout in ms. Default: 120000 (2 min)"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} }
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { manifest_default as default };
|
|
53
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","names":[],"sources":["../../../src/plugins/serving/manifest.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
import { CACHE_VERSION } from "../../type-generator/serving/cache.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
//#region src/plugins/serving/schema-filter.ts
|
|
6
|
+
const logger = createLogger("serving:schema-filter");
|
|
7
|
+
function isValidCache(data) {
|
|
8
|
+
return typeof data === "object" && data !== null && "version" in data && data.version === CACHE_VERSION && "endpoints" in data && typeof data.endpoints === "object";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Loads endpoint schemas from the type generation cache file.
|
|
12
|
+
* Returns a map of alias → allowed parameter keys.
|
|
13
|
+
*/
|
|
14
|
+
async function loadEndpointSchemas(cacheFile) {
|
|
15
|
+
const allowlists = /* @__PURE__ */ new Map();
|
|
16
|
+
try {
|
|
17
|
+
const raw = await fs.readFile(cacheFile, "utf8");
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (!isValidCache(parsed)) {
|
|
20
|
+
logger.warn("Serving types cache has invalid structure, skipping");
|
|
21
|
+
return allowlists;
|
|
22
|
+
}
|
|
23
|
+
const cache = parsed;
|
|
24
|
+
for (const [alias, entry] of Object.entries(cache.endpoints)) if (entry.requestKeys && entry.requestKeys.length > 0) allowlists.set(alias, new Set(entry.requestKeys));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err.code !== "ENOENT") logger.warn("Failed to load serving types cache: %s", err.message);
|
|
27
|
+
}
|
|
28
|
+
return allowlists;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Filters a request body against the allowed keys for an endpoint alias.
|
|
32
|
+
* Returns the filtered body and logs a warning for stripped params.
|
|
33
|
+
*
|
|
34
|
+
* If no allowlist exists for the alias, returns the body unchanged (passthrough).
|
|
35
|
+
*/
|
|
36
|
+
function filterRequestBody(body, allowlists, alias, filterMode = "strip") {
|
|
37
|
+
const allowed = allowlists.get(alias);
|
|
38
|
+
if (!allowed) return body;
|
|
39
|
+
const stripped = [];
|
|
40
|
+
const filtered = {};
|
|
41
|
+
for (const [key, value] of Object.entries(body)) if (allowed.has(key)) filtered[key] = value;
|
|
42
|
+
else stripped.push(key);
|
|
43
|
+
if (stripped.length > 0) {
|
|
44
|
+
if (filterMode === "reject") throw new Error(`Unknown request parameters: ${stripped.join(", ")}`);
|
|
45
|
+
logger.warn("Stripped unknown params from '%s': %s", alias, stripped.join(", "));
|
|
46
|
+
}
|
|
47
|
+
return filtered;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { filterRequestBody, loadEndpointSchemas };
|
|
52
|
+
//# sourceMappingURL=schema-filter.js.map
|