@databricks/appkit 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CLAUDE.md +8 -1
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/context/execution-context.js +1 -7
  4. package/dist/context/execution-context.js.map +1 -1
  5. package/dist/context/index.js +1 -1
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/plugins/files/plugin.d.ts +46 -15
  11. package/dist/plugins/files/plugin.d.ts.map +1 -1
  12. package/dist/plugins/files/plugin.js +182 -103
  13. package/dist/plugins/files/plugin.js.map +1 -1
  14. package/dist/plugins/files/policy.d.ts +45 -0
  15. package/dist/plugins/files/policy.d.ts.map +1 -0
  16. package/dist/plugins/files/policy.js +63 -0
  17. package/dist/plugins/files/policy.js.map +1 -0
  18. package/dist/plugins/files/types.d.ts +16 -8
  19. package/dist/plugins/files/types.d.ts.map +1 -1
  20. package/docs/api/appkit/Class.PolicyDeniedError.md +52 -0
  21. package/docs/api/appkit/Interface.FilePolicyUser.md +23 -0
  22. package/docs/api/appkit/Interface.FileResource.md +36 -0
  23. package/docs/api/appkit/TypeAlias.FileAction.md +18 -0
  24. package/docs/api/appkit/TypeAlias.FilePolicy.md +20 -0
  25. package/docs/api/appkit/Variable.READ_ACTIONS.md +8 -0
  26. package/docs/api/appkit/Variable.WRITE_ACTIONS.md +8 -0
  27. package/docs/api/appkit.md +19 -12
  28. package/docs/faq.md +8 -8
  29. package/docs/plugins/execution-context.md +0 -1
  30. package/docs/plugins/files.md +150 -2
  31. package/docs/plugins/{serving.md → model-serving.md} +1 -1
  32. package/llms.txt +8 -1
  33. package/package.json +1 -1
  34. package/sbom.cdx.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
2
  import { AuthenticationError } from "../../errors/authentication.js";
3
3
  import { init_errors } from "../../errors/index.js";
4
- import { getWorkspaceClient, isInUserContext } from "../../context/execution-context.js";
4
+ import { getCurrentUserId, getWorkspaceClient } from "../../context/execution-context.js";
5
5
  import { init_context } from "../../context/index.js";
6
6
  import { ResourceType } from "../../registry/types.generated.js";
7
7
  import "../../registry/index.js";
@@ -14,6 +14,7 @@ import "../../connectors/files/index.js";
14
14
  import { FILES_DOWNLOAD_DEFAULTS, FILES_MAX_UPLOAD_SIZE, FILES_READ_DEFAULTS, FILES_WRITE_DEFAULTS } from "./defaults.js";
15
15
  import { parentDirectory, sanitizeFilename } from "./helpers.js";
16
16
  import manifest_default from "./manifest.js";
17
+ import { PolicyDeniedError, policy } from "./policy.js";
17
18
  import { ApiError } from "@databricks/sdk-experimental";
18
19
  import { STATUS_CODES } from "node:http";
19
20
  import { Readable } from "node:stream";
@@ -72,18 +73,72 @@ var FilesPlugin = class FilesPlugin extends Plugin {
72
73
  }));
73
74
  }
74
75
  /**
75
- * Warns when a method is called without a user context (i.e. as service principal).
76
- * OBO access via `asUser(req)` is strongly recommended.
76
+ * Extract user identity from the request.
77
+ * Falls back to `getCurrentUserId()` in development mode.
77
78
  */
78
- warnIfNoUserContext(volumeKey, method) {
79
- if (!isInUserContext()) logger.warn(`app.files("${volumeKey}").${method}() called without user context (service principal). Please use OBO instead: app.files("${volumeKey}").asUser(req).${method}()`);
79
+ _extractUser(req) {
80
+ const userId = req.header("x-forwarded-user")?.trim();
81
+ if (userId) return { id: userId };
82
+ if (process.env.NODE_ENV === "development") {
83
+ logger.warn("No x-forwarded-user header — falling back to service principal identity for policy checks. Ensure your proxy forwards user headers to test per-user policies.");
84
+ return { id: getCurrentUserId() };
85
+ }
86
+ throw AuthenticationError.missingToken("Missing x-forwarded-user header. Cannot resolve user ID.");
87
+ }
88
+ /**
89
+ * Check the policy for a volume. No-op if no policy is configured.
90
+ * Throws `PolicyDeniedError` if denied.
91
+ */
92
+ async _checkPolicy(volumeKey, action, path, user, resourceOverrides) {
93
+ const policyFn = this.volumeConfigs[volumeKey]?.policy;
94
+ if (typeof policyFn !== "function") return;
95
+ if (!await policyFn(action, {
96
+ path,
97
+ volume: volumeKey,
98
+ ...resourceOverrides
99
+ }, user)) {
100
+ const userId = user.isServicePrincipal ? "<service-principal>" : user.id;
101
+ logger.warn("Policy denied \"%s\" on volume \"%s\" for user \"%s\"", action, volumeKey, userId);
102
+ throw new PolicyDeniedError(action, volumeKey);
103
+ }
80
104
  }
81
105
  /**
82
- * Throws when a method is called without a user context (i.e. as service principal).
83
- * OBO access via `asUser(req)` is enforced for now.
106
+ * HTTP-level wrapper around `_checkPolicy`.
107
+ * Extracts user (401 on failure), runs policy (403 on denial).
108
+ * Returns `true` if the request may proceed, `false` if a response was sent.
84
109
  */
85
- throwIfNoUserContext(volumeKey, method) {
86
- if (!isInUserContext()) throw new Error(`app.files("${volumeKey}").${method}() called without user context (service principal). Use OBO instead: app.files("${volumeKey}").asUser(req).${method}()`);
110
+ async _enforcePolicy(req, res, volumeKey, action, path, resourceOverrides) {
111
+ let user;
112
+ try {
113
+ user = this._extractUser(req);
114
+ } catch (error) {
115
+ if (error instanceof AuthenticationError) {
116
+ res.status(401).json({
117
+ error: error.message,
118
+ plugin: this.name
119
+ });
120
+ return false;
121
+ }
122
+ throw error;
123
+ }
124
+ try {
125
+ await this._checkPolicy(volumeKey, action, path, user, resourceOverrides);
126
+ } catch (error) {
127
+ if (error instanceof PolicyDeniedError) {
128
+ res.status(403).json({
129
+ error: error.message,
130
+ plugin: this.name
131
+ });
132
+ return false;
133
+ }
134
+ logger.error("Policy function threw on volume %s: %O", volumeKey, error);
135
+ res.status(500).json({
136
+ error: "Policy evaluation failed",
137
+ plugin: this.name
138
+ });
139
+ return false;
140
+ }
141
+ return true;
87
142
  }
88
143
  constructor(config) {
89
144
  super(config);
@@ -97,7 +152,8 @@ var FilesPlugin = class FilesPlugin extends Plugin {
97
152
  const volumePath = process.env[envVar];
98
153
  const mergedConfig = {
99
154
  maxUploadSize: volumeCfg.maxUploadSize ?? config.maxUploadSize,
100
- customContentTypes: volumeCfg.customContentTypes ?? config.customContentTypes
155
+ customContentTypes: volumeCfg.customContentTypes ?? config.customContentTypes,
156
+ policy: volumeCfg.policy ?? policy.publicRead()
101
157
  };
102
158
  this.volumeConfigs[key] = mergedConfig;
103
159
  this.volumeConnectors[key] = new FilesConnector({
@@ -107,51 +163,7 @@ var FilesPlugin = class FilesPlugin extends Plugin {
107
163
  customContentTypes: mergedConfig.customContentTypes
108
164
  });
109
165
  }
110
- }
111
- /**
112
- * Creates a VolumeAPI for a specific volume key.
113
- * Each method warns if called outside a user context (service principal).
114
- */
115
- createVolumeAPI(volumeKey) {
116
- const connector = this.volumeConnectors[volumeKey];
117
- return {
118
- list: (directoryPath) => {
119
- this.throwIfNoUserContext(volumeKey, `list`);
120
- return connector.list(getWorkspaceClient(), directoryPath);
121
- },
122
- read: (filePath, options) => {
123
- this.throwIfNoUserContext(volumeKey, `read`);
124
- return connector.read(getWorkspaceClient(), filePath, options);
125
- },
126
- download: (filePath) => {
127
- this.throwIfNoUserContext(volumeKey, `download`);
128
- return connector.download(getWorkspaceClient(), filePath);
129
- },
130
- exists: (filePath) => {
131
- this.throwIfNoUserContext(volumeKey, `exists`);
132
- return connector.exists(getWorkspaceClient(), filePath);
133
- },
134
- metadata: (filePath) => {
135
- this.throwIfNoUserContext(volumeKey, `metadata`);
136
- return connector.metadata(getWorkspaceClient(), filePath);
137
- },
138
- upload: (filePath, contents, options) => {
139
- this.throwIfNoUserContext(volumeKey, `upload`);
140
- return connector.upload(getWorkspaceClient(), filePath, contents, options);
141
- },
142
- createDirectory: (directoryPath) => {
143
- this.throwIfNoUserContext(volumeKey, `createDirectory`);
144
- return connector.createDirectory(getWorkspaceClient(), directoryPath);
145
- },
146
- delete: (filePath) => {
147
- this.throwIfNoUserContext(volumeKey, `delete`);
148
- return connector.delete(getWorkspaceClient(), filePath);
149
- },
150
- preview: (filePath) => {
151
- this.throwIfNoUserContext(volumeKey, `preview`);
152
- return connector.preview(getWorkspaceClient(), filePath);
153
- }
154
- };
166
+ for (const key of this.volumeKeys) if (!volumes[key].policy) logger.warn("Volume \"%s\" has no explicit policy — defaulting to publicRead(). Set a policy in files({ volumes: { %s: { policy: ... } } }) to silence this warning.", key, key);
155
167
  }
156
168
  injectRoutes(router) {
157
169
  this.route(router, {
@@ -310,14 +322,25 @@ var FilesPlugin = class FilesPlugin extends Plugin {
310
322
  * Invalidate cached list entries for a directory after a write operation.
311
323
  * Uses the same cache-key format as `_handleList`: resolved path for
312
324
  * subdirectories, `"__root__"` for the volume root.
325
+ *
326
+ * Cache keys include `getCurrentUserId()` — must match the identity used
327
+ * by `this.execute()` in `_handleList`. Both run in service-principal
328
+ * context; wrapping either in `runInUserContext` would break invalidation.
313
329
  */
314
- _invalidateListCache(volumeKey, parentPath, userId, connector) {
330
+ _invalidateListCache(volumeKey, parentPath, connector) {
315
331
  const parent = parentDirectory(parentPath);
316
332
  const cachePathSegment = parent ? connector.resolvePath(parent) : "__root__";
317
- const listKey = this.cache.generateKey([`files:${volumeKey}:list`, cachePathSegment], userId);
333
+ const listKey = this.cache.generateKey([`files:${volumeKey}:list`, cachePathSegment], getCurrentUserId());
318
334
  this.cache.delete(listKey);
319
335
  }
320
336
  _handleApiError(res, error, fallbackMessage) {
337
+ if (error instanceof PolicyDeniedError) {
338
+ res.status(403).json({
339
+ error: error.message,
340
+ plugin: this.name
341
+ });
342
+ return;
343
+ }
321
344
  if (error instanceof AuthenticationError) {
322
345
  res.status(401).json({
323
346
  error: error.message,
@@ -356,11 +379,9 @@ var FilesPlugin = class FilesPlugin extends Plugin {
356
379
  }
357
380
  async _handleList(req, res, connector, volumeKey) {
358
381
  const path = req.query.path;
382
+ if (!await this._enforcePolicy(req, res, volumeKey, "list", path ?? "/")) return;
359
383
  try {
360
- const result = await this.asUser(req).execute(async () => {
361
- this.warnIfNoUserContext(volumeKey, `list`);
362
- return connector.list(getWorkspaceClient(), path);
363
- }, this._readSettings([`files:${volumeKey}:list`, path ? connector.resolvePath(path) : "__root__"]));
384
+ const result = await this.execute(async () => connector.list(getWorkspaceClient(), path), this._readSettings([`files:${volumeKey}:list`, path ? connector.resolvePath(path) : "__root__"]));
364
385
  if (!result.ok) {
365
386
  this._sendStatusError(res, result.status);
366
387
  return;
@@ -380,11 +401,9 @@ var FilesPlugin = class FilesPlugin extends Plugin {
380
401
  });
381
402
  return;
382
403
  }
404
+ if (!await this._enforcePolicy(req, res, volumeKey, "read", path)) return;
383
405
  try {
384
- const result = await this.asUser(req).execute(async () => {
385
- this.warnIfNoUserContext(volumeKey, `read`);
386
- return connector.read(getWorkspaceClient(), path);
387
- }, this._readSettings([`files:${volumeKey}:read`, connector.resolvePath(path)]));
406
+ const result = await this.execute(async () => connector.read(getWorkspaceClient(), path), this._readSettings([`files:${volumeKey}:read`, connector.resolvePath(path)]));
388
407
  if (!result.ok) {
389
408
  this._sendStatusError(res, result.status);
390
409
  return;
@@ -415,15 +434,12 @@ var FilesPlugin = class FilesPlugin extends Plugin {
415
434
  });
416
435
  return;
417
436
  }
437
+ if (!await this._enforcePolicy(req, res, volumeKey, opts.mode, path)) return;
418
438
  const label = opts.mode === "download" ? "Download" : "Raw fetch";
419
439
  const volumeCfg = this.volumeConfigs[volumeKey];
420
440
  try {
421
- const userPlugin = this.asUser(req);
422
441
  const settings = { default: FILES_DOWNLOAD_DEFAULTS };
423
- const response = await userPlugin.execute(async () => {
424
- this.warnIfNoUserContext(volumeKey, `download`);
425
- return connector.download(getWorkspaceClient(), path);
426
- }, settings);
442
+ const response = await this.execute(async () => connector.download(getWorkspaceClient(), path), settings);
427
443
  if (!response.ok) {
428
444
  this._sendStatusError(res, response.status);
429
445
  return;
@@ -459,11 +475,9 @@ var FilesPlugin = class FilesPlugin extends Plugin {
459
475
  });
460
476
  return;
461
477
  }
478
+ if (!await this._enforcePolicy(req, res, volumeKey, "exists", path)) return;
462
479
  try {
463
- const result = await this.asUser(req).execute(async () => {
464
- this.warnIfNoUserContext(volumeKey, `exists`);
465
- return connector.exists(getWorkspaceClient(), path);
466
- }, this._readSettings([`files:${volumeKey}:exists`, connector.resolvePath(path)]));
480
+ const result = await this.execute(async () => connector.exists(getWorkspaceClient(), path), this._readSettings([`files:${volumeKey}:exists`, connector.resolvePath(path)]));
467
481
  if (!result.ok) {
468
482
  this._sendStatusError(res, result.status);
469
483
  return;
@@ -483,11 +497,9 @@ var FilesPlugin = class FilesPlugin extends Plugin {
483
497
  });
484
498
  return;
485
499
  }
500
+ if (!await this._enforcePolicy(req, res, volumeKey, "metadata", path)) return;
486
501
  try {
487
- const result = await this.asUser(req).execute(async () => {
488
- this.warnIfNoUserContext(volumeKey, `metadata`);
489
- return connector.metadata(getWorkspaceClient(), path);
490
- }, this._readSettings([`files:${volumeKey}:metadata`, connector.resolvePath(path)]));
502
+ const result = await this.execute(async () => connector.metadata(getWorkspaceClient(), path), this._readSettings([`files:${volumeKey}:metadata`, connector.resolvePath(path)]));
491
503
  if (!result.ok) {
492
504
  this._sendStatusError(res, result.status);
493
505
  return;
@@ -507,11 +519,9 @@ var FilesPlugin = class FilesPlugin extends Plugin {
507
519
  });
508
520
  return;
509
521
  }
522
+ if (!await this._enforcePolicy(req, res, volumeKey, "preview", path)) return;
510
523
  try {
511
- const result = await this.asUser(req).execute(async () => {
512
- this.warnIfNoUserContext(volumeKey, `preview`);
513
- return connector.preview(getWorkspaceClient(), path);
514
- }, this._readSettings([`files:${volumeKey}:preview`, connector.resolvePath(path)]));
524
+ const result = await this.execute(async () => connector.preview(getWorkspaceClient(), path), this._readSettings([`files:${volumeKey}:preview`, connector.resolvePath(path)]));
515
525
  if (!result.ok) {
516
526
  this._sendStatusError(res, result.status);
517
527
  return;
@@ -533,8 +543,19 @@ var FilesPlugin = class FilesPlugin extends Plugin {
533
543
  }
534
544
  const maxSize = this.volumeConfigs[volumeKey].maxUploadSize ?? FILES_MAX_UPLOAD_SIZE;
535
545
  const rawContentLength = req.headers["content-length"];
536
- const contentLength = rawContentLength ? parseInt(rawContentLength, 10) : void 0;
537
- if (contentLength !== void 0 && !Number.isNaN(contentLength) && contentLength > maxSize) {
546
+ let contentLength;
547
+ if (typeof rawContentLength === "string" && rawContentLength.length > 0) {
548
+ if (!/^\d+$/.test(rawContentLength)) {
549
+ res.status(400).json({
550
+ error: "Invalid Content-Length header.",
551
+ plugin: this.name
552
+ });
553
+ return;
554
+ }
555
+ contentLength = Number(rawContentLength);
556
+ }
557
+ if (!await this._enforcePolicy(req, res, volumeKey, "upload", path, { size: contentLength })) return;
558
+ if (contentLength !== void 0 && contentLength > maxSize) {
538
559
  res.status(413).json({
539
560
  error: `File size (${contentLength} bytes) exceeds maximum allowed size (${maxSize} bytes).`,
540
561
  plugin: this.name
@@ -554,14 +575,12 @@ var FilesPlugin = class FilesPlugin extends Plugin {
554
575
  controller.enqueue(chunk);
555
576
  } }));
556
577
  logger.debug(req, "Upload body received: volume=%s path=%s, size=%d bytes", volumeKey, path, contentLength ?? 0);
557
- const userPlugin = this.asUser(req);
558
578
  const settings = { default: FILES_WRITE_DEFAULTS };
559
- const result = await this.trackWrite(() => userPlugin.execute(async () => {
560
- this.warnIfNoUserContext(volumeKey, `upload`);
579
+ const result = await this.trackWrite(() => this.execute(async () => {
561
580
  await connector.upload(getWorkspaceClient(), path, webStream);
562
581
  return { success: true };
563
582
  }, settings));
564
- this._invalidateListCache(volumeKey, path, this.resolveUserId(req), connector);
583
+ this._invalidateListCache(volumeKey, path, connector);
565
584
  if (!result.ok) {
566
585
  logger.error(req, "Upload failed: volume=%s path=%s, size=%d bytes", volumeKey, path, contentLength ?? 0);
567
586
  this._sendStatusError(res, result.status);
@@ -590,15 +609,14 @@ var FilesPlugin = class FilesPlugin extends Plugin {
590
609
  });
591
610
  return;
592
611
  }
612
+ if (!await this._enforcePolicy(req, res, volumeKey, "mkdir", dirPath)) return;
593
613
  try {
594
- const userPlugin = this.asUser(req);
595
614
  const settings = { default: FILES_WRITE_DEFAULTS };
596
- const result = await this.trackWrite(() => userPlugin.execute(async () => {
597
- this.warnIfNoUserContext(volumeKey, `createDirectory`);
615
+ const result = await this.trackWrite(() => this.execute(async () => {
598
616
  await connector.createDirectory(getWorkspaceClient(), dirPath);
599
617
  return { success: true };
600
618
  }, settings));
601
- this._invalidateListCache(volumeKey, dirPath, this.resolveUserId(req), connector);
619
+ this._invalidateListCache(volumeKey, dirPath, connector);
602
620
  if (!result.ok) {
603
621
  this._sendStatusError(res, result.status);
604
622
  return;
@@ -619,15 +637,14 @@ var FilesPlugin = class FilesPlugin extends Plugin {
619
637
  return;
620
638
  }
621
639
  const path = rawPath;
640
+ if (!await this._enforcePolicy(req, res, volumeKey, "delete", path)) return;
622
641
  try {
623
- const userPlugin = this.asUser(req);
624
642
  const settings = { default: FILES_WRITE_DEFAULTS };
625
- const result = await this.trackWrite(() => userPlugin.execute(async () => {
626
- this.warnIfNoUserContext(volumeKey, `delete`);
643
+ const result = await this.trackWrite(() => this.execute(async () => {
627
644
  await connector.delete(getWorkspaceClient(), path);
628
645
  return { success: true };
629
646
  }, settings));
630
- this._invalidateListCache(volumeKey, path, this.resolveUserId(req), connector);
647
+ this._invalidateListCache(volumeKey, path, connector);
631
648
  if (!result.ok) {
632
649
  this._sendStatusError(res, result.status);
633
650
  return;
@@ -637,6 +654,59 @@ var FilesPlugin = class FilesPlugin extends Plugin {
637
654
  this._handleApiError(res, error, "Delete failed");
638
655
  }
639
656
  }
657
+ /**
658
+ * Creates a VolumeAPI for a specific volume key.
659
+ *
660
+ * By default, enforces the volume's policy before each operation.
661
+ * Pass `bypassPolicy: true` to skip policy checks — useful for
662
+ * background jobs or migrations that should bypass user-facing policies.
663
+ *
664
+ * @security When `bypassPolicy` is `true`, no policy enforcement runs.
665
+ * Do not expose bypassed APIs to HTTP routes or end-user code paths.
666
+ */
667
+ createVolumeAPI(volumeKey, user, options) {
668
+ const connector = this.volumeConnectors[volumeKey];
669
+ const noop = () => Promise.resolve();
670
+ const check = options?.bypassPolicy ? noop : (action, path, overrides) => this._checkPolicy(volumeKey, action, path, user, overrides);
671
+ return {
672
+ list: async (directoryPath) => {
673
+ await check("list", directoryPath ?? "/");
674
+ return connector.list(getWorkspaceClient(), directoryPath);
675
+ },
676
+ read: async (filePath, opts) => {
677
+ await check("read", filePath);
678
+ return connector.read(getWorkspaceClient(), filePath, opts);
679
+ },
680
+ download: async (filePath) => {
681
+ await check("download", filePath);
682
+ return connector.download(getWorkspaceClient(), filePath);
683
+ },
684
+ exists: async (filePath) => {
685
+ await check("exists", filePath);
686
+ return connector.exists(getWorkspaceClient(), filePath);
687
+ },
688
+ metadata: async (filePath) => {
689
+ await check("metadata", filePath);
690
+ return connector.metadata(getWorkspaceClient(), filePath);
691
+ },
692
+ upload: async (filePath, contents, opts) => {
693
+ await check("upload", filePath);
694
+ return connector.upload(getWorkspaceClient(), filePath, contents, opts);
695
+ },
696
+ createDirectory: async (directoryPath) => {
697
+ await check("mkdir", directoryPath);
698
+ return connector.createDirectory(getWorkspaceClient(), directoryPath);
699
+ },
700
+ delete: async (filePath) => {
701
+ await check("delete", filePath);
702
+ return connector.delete(getWorkspaceClient(), filePath);
703
+ },
704
+ preview: async (filePath) => {
705
+ await check("preview", filePath);
706
+ return connector.preview(getWorkspaceClient(), filePath);
707
+ }
708
+ };
709
+ }
640
710
  inflightWrites = 0;
641
711
  trackWrite(fn) {
642
712
  this.inflightWrites++;
@@ -657,22 +727,31 @@ var FilesPlugin = class FilesPlugin extends Plugin {
657
727
  * Returns the programmatic API for the Files plugin.
658
728
  * Callable with a volume key to get a volume-scoped handle.
659
729
  *
730
+ * All operations execute as the service principal.
731
+ * Use policies to control per-user access.
732
+ *
660
733
  * @example
661
734
  * ```ts
662
- * // OBO access (recommended)
663
- * appKit.files("uploads").asUser(req).list()
664
- *
665
- * // Service principal access (logs a warning)
735
+ * // Service principal access
666
736
  * appKit.files("uploads").list()
737
+ *
738
+ * // With policy: pass user identity for access control
739
+ * appKit.files("uploads").asUser(req).list()
667
740
  * ```
668
741
  */
669
742
  exports() {
670
743
  const resolveVolume = (volumeKey) => {
671
744
  if (!this.volumeKeys.includes(volumeKey)) throw new Error(`Unknown volume "${volumeKey}". Available volumes: ${this.volumeKeys.join(", ")}`);
672
745
  return {
673
- ...this.createVolumeAPI(volumeKey),
746
+ ...this.createVolumeAPI(volumeKey, {
747
+ get id() {
748
+ return getCurrentUserId();
749
+ },
750
+ isServicePrincipal: true
751
+ }),
674
752
  asUser: (req) => {
675
- return this.asUser(req).createVolumeAPI(volumeKey);
753
+ const user = this._extractUser(req);
754
+ return this.createVolumeAPI(volumeKey, user);
676
755
  }
677
756
  };
678
757
  };
@@ -687,7 +766,7 @@ var FilesPlugin = class FilesPlugin extends Plugin {
687
766
  /**
688
767
  * @internal
689
768
  */
690
- const files$1 = toPlugin(FilesPlugin);
769
+ const files$1 = Object.assign(toPlugin(FilesPlugin), { policy });
691
770
 
692
771
  //#endregion
693
772
  export { FilesPlugin, files$1 as files };