@strapi/upload 5.52.3 → 5.54.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/dist/admin/future/components/Drawer.js +6 -3
- package/dist/admin/future/components/Drawer.js.map +1 -1
- package/dist/admin/future/components/Drawer.mjs +6 -3
- package/dist/admin/future/components/Drawer.mjs.map +1 -1
- package/dist/admin/future/components/UploadProgressDialog.js +9 -8
- package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
- package/dist/admin/future/components/UploadProgressDialog.mjs +9 -8
- package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/AssetsPage.js +16 -4
- package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
- package/dist/admin/future/pages/Assets/AssetsPage.mjs +16 -4
- package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js +5 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs +5 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +19 -4
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs +19 -4
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +35 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +35 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js +10 -0
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +10 -0
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.js +9 -0
- package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +10 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js +132 -88
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs +135 -91
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +7 -0
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +7 -0
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js +4 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs +4 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.js +181 -0
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.mjs +179 -0
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/constants.js +22 -0
- package/dist/admin/future/pages/Assets/constants.js.map +1 -1
- package/dist/admin/future/pages/Assets/constants.mjs +19 -1
- package/dist/admin/future/pages/Assets/constants.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +65 -10
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +66 -11
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.js +111 -0
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.js.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.mjs +109 -0
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.mjs.map +1 -0
- package/dist/admin/future/services/api.js +17 -0
- package/dist/admin/future/services/api.js.map +1 -1
- package/dist/admin/future/services/api.mjs +18 -1
- package/dist/admin/future/services/api.mjs.map +1 -1
- package/dist/admin/future/store/uploadProgress.js +31 -22
- package/dist/admin/future/store/uploadProgress.js.map +1 -1
- package/dist/admin/future/store/uploadProgress.mjs +31 -22
- package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
- package/dist/admin/index.js +11 -10
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +11 -10
- package/dist/admin/index.mjs.map +1 -1
- package/dist/admin/src/future/components/Drawer.d.ts +10 -0
- package/dist/admin/src/future/pages/Assets/components/MainAreaContextMenu.d.ts +28 -0
- package/dist/admin/src/future/pages/Assets/constants.d.ts +20 -0
- package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +4 -0
- package/dist/admin/src/future/pages/Assets/hooks/useListScrollRestoration.d.ts +15 -0
- package/dist/admin/src/future/store/uploadProgress.d.ts +14 -20
- package/dist/admin/translations/en.json.js +1 -3
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +1 -3
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/server/bootstrap.js +11 -0
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/server/bootstrap.mjs +11 -0
- package/dist/server/bootstrap.mjs.map +1 -1
- package/dist/server/controllers/admin-upload.js +31 -1
- package/dist/server/controllers/admin-upload.js.map +1 -1
- package/dist/server/controllers/admin-upload.mjs +31 -1
- package/dist/server/controllers/admin-upload.mjs.map +1 -1
- package/dist/server/controllers/utils/find-entity-and-check-permissions.js +15 -4
- package/dist/server/controllers/utils/find-entity-and-check-permissions.js.map +1 -1
- package/dist/server/controllers/utils/find-entity-and-check-permissions.mjs +15 -4
- package/dist/server/controllers/utils/find-entity-and-check-permissions.mjs.map +1 -1
- package/dist/server/controllers/utils/folders.js +1 -1
- package/dist/server/controllers/utils/folders.js.map +1 -1
- package/dist/server/controllers/utils/folders.mjs +1 -1
- package/dist/server/controllers/utils/folders.mjs.map +1 -1
- package/dist/server/mcp/ambient-instance.js +46 -0
- package/dist/server/mcp/ambient-instance.js.map +1 -0
- package/dist/server/mcp/ambient-instance.mjs +43 -0
- package/dist/server/mcp/ambient-instance.mjs.map +1 -0
- package/dist/server/mcp/handlers/constants.js +60 -0
- package/dist/server/mcp/handlers/constants.js.map +1 -0
- package/dist/server/mcp/handlers/constants.mjs +45 -0
- package/dist/server/mcp/handlers/constants.mjs.map +1 -0
- package/dist/server/mcp/handlers/folder-handlers.js +308 -0
- package/dist/server/mcp/handlers/folder-handlers.js.map +1 -0
- package/dist/server/mcp/handlers/folder-handlers.mjs +303 -0
- package/dist/server/mcp/handlers/folder-handlers.mjs.map +1 -0
- package/dist/server/mcp/handlers/read-handlers.js +134 -0
- package/dist/server/mcp/handlers/read-handlers.js.map +1 -0
- package/dist/server/mcp/handlers/read-handlers.mjs +130 -0
- package/dist/server/mcp/handlers/read-handlers.mjs.map +1 -0
- package/dist/server/mcp/handlers/write-handlers.js +315 -0
- package/dist/server/mcp/handlers/write-handlers.js.map +1 -0
- package/dist/server/mcp/handlers/write-handlers.mjs +311 -0
- package/dist/server/mcp/handlers/write-handlers.mjs.map +1 -0
- package/dist/server/mcp/permissions.js +36 -0
- package/dist/server/mcp/permissions.js.map +1 -0
- package/dist/server/mcp/permissions.mjs +33 -0
- package/dist/server/mcp/permissions.mjs.map +1 -0
- package/dist/server/mcp/register-upload-mcp-tools.js +235 -0
- package/dist/server/mcp/register-upload-mcp-tools.js.map +1 -0
- package/dist/server/mcp/register-upload-mcp-tools.mjs +232 -0
- package/dist/server/mcp/register-upload-mcp-tools.mjs.map +1 -0
- package/dist/server/mcp/sanitizers/sanitize-media.js +97 -0
- package/dist/server/mcp/sanitizers/sanitize-media.js.map +1 -0
- package/dist/server/mcp/sanitizers/sanitize-media.mjs +93 -0
- package/dist/server/mcp/sanitizers/sanitize-media.mjs.map +1 -0
- package/dist/server/mcp/schemas/input-schemas.js +206 -0
- package/dist/server/mcp/schemas/input-schemas.js.map +1 -0
- package/dist/server/mcp/schemas/input-schemas.mjs +190 -0
- package/dist/server/mcp/schemas/input-schemas.mjs.map +1 -0
- package/dist/server/mcp/schemas/output-schemas.js +158 -0
- package/dist/server/mcp/schemas/output-schemas.js.map +1 -0
- package/dist/server/mcp/schemas/output-schemas.mjs +142 -0
- package/dist/server/mcp/schemas/output-schemas.mjs.map +1 -0
- package/dist/server/mcp/utils.js +14 -0
- package/dist/server/mcp/utils.js.map +1 -0
- package/dist/server/mcp/utils.mjs +12 -0
- package/dist/server/mcp/utils.mjs.map +1 -0
- package/dist/server/media-library-default-notice.js +42 -0
- package/dist/server/media-library-default-notice.js.map +1 -0
- package/dist/server/media-library-default-notice.mjs +40 -0
- package/dist/server/media-library-default-notice.mjs.map +1 -0
- package/dist/server/register.js +5 -0
- package/dist/server/register.js.map +1 -1
- package/dist/server/register.mjs +5 -0
- package/dist/server/register.mjs.map +1 -1
- package/dist/server/services/folder.js.map +1 -1
- package/dist/server/services/folder.mjs.map +1 -1
- package/dist/server/services/upload.js +34 -10
- package/dist/server/services/upload.js.map +1 -1
- package/dist/server/services/upload.mjs +34 -10
- package/dist/server/services/upload.mjs.map +1 -1
- package/dist/server/src/bootstrap.d.ts.map +1 -1
- package/dist/server/src/controllers/admin-upload.d.ts +1 -0
- package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
- package/dist/server/src/controllers/utils/find-entity-and-check-permissions.d.ts +14 -1
- package/dist/server/src/controllers/utils/find-entity-and-check-permissions.d.ts.map +1 -1
- package/dist/server/src/controllers/utils/folders.d.ts +2 -1
- package/dist/server/src/controllers/utils/folders.d.ts.map +1 -1
- package/dist/server/src/index.d.ts +10 -2
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/mcp/ambient-instance.d.ts +65 -0
- package/dist/server/src/mcp/ambient-instance.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/constants.d.ts +48 -0
- package/dist/server/src/mcp/handlers/constants.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/folder-handlers.d.ts +49 -0
- package/dist/server/src/mcp/handlers/folder-handlers.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/index.d.ts +5 -0
- package/dist/server/src/mcp/handlers/index.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/read-handlers.d.ts +35 -0
- package/dist/server/src/mcp/handlers/read-handlers.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/write-handlers.d.ts +66 -0
- package/dist/server/src/mcp/handlers/write-handlers.d.ts.map +1 -0
- package/dist/server/src/mcp/index.d.ts +3 -0
- package/dist/server/src/mcp/index.d.ts.map +1 -0
- package/dist/server/src/mcp/permissions.d.ts +22 -0
- package/dist/server/src/mcp/permissions.d.ts.map +1 -0
- package/dist/server/src/mcp/register-upload-mcp-tools.d.ts +29 -0
- package/dist/server/src/mcp/register-upload-mcp-tools.d.ts.map +1 -0
- package/dist/server/src/mcp/sanitizers/sanitize-media.d.ts +63 -0
- package/dist/server/src/mcp/sanitizers/sanitize-media.d.ts.map +1 -0
- package/dist/server/src/mcp/schemas/index.d.ts +3 -0
- package/dist/server/src/mcp/schemas/index.d.ts.map +1 -0
- package/dist/server/src/mcp/schemas/input-schemas.d.ts +148 -0
- package/dist/server/src/mcp/schemas/input-schemas.d.ts.map +1 -0
- package/dist/server/src/mcp/schemas/output-schemas.d.ts +276 -0
- package/dist/server/src/mcp/schemas/output-schemas.d.ts.map +1 -0
- package/dist/server/src/mcp/types.d.ts +19 -0
- package/dist/server/src/mcp/types.d.ts.map +1 -0
- package/dist/server/src/mcp/utils.d.ts +4 -0
- package/dist/server/src/mcp/utils.d.ts.map +1 -0
- package/dist/server/src/media-library-default-notice.d.ts +19 -0
- package/dist/server/src/media-library-default-notice.d.ts.map +1 -0
- package/dist/server/src/register.d.ts.map +1 -1
- package/dist/server/src/services/folder.d.ts +6 -2
- package/dist/server/src/services/folder.d.ts.map +1 -1
- package/dist/server/src/services/index.d.ts +10 -2
- package/dist/server/src/services/index.d.ts.map +1 -1
- package/dist/server/src/services/upload.d.ts +10 -1
- package/dist/server/src/services/upload.d.ts.map +1 -1
- package/dist/server/src/types.d.ts +2 -2
- package/dist/server/src/types.d.ts.map +1 -1
- package/dist/server/src/utils/index.d.ts +2 -1
- package/dist/server/src/utils/index.d.ts.map +1 -1
- package/dist/server/utils/index.js +2 -2
- package/dist/server/utils/index.js.map +1 -1
- package/dist/server/utils/index.mjs +2 -2
- package/dist/server/utils/index.mjs.map +1 -1
- package/dist/shared/contracts/files.d.ts +22 -0
- package/dist/shared/contracts/files.d.ts.map +1 -1
- package/package.json +7 -6
- package/dist/admin/future/components/BetaNotice.js +0 -72
- package/dist/admin/future/components/BetaNotice.js.map +0 -1
- package/dist/admin/future/components/BetaNotice.mjs +0 -70
- package/dist/admin/future/components/BetaNotice.mjs.map +0 -1
- package/dist/admin/src/future/components/BetaNotice.d.ts +0 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-handlers.mjs","sources":["../../../../server/src/mcp/handlers/read-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { assertAmbientInstance, getFolderService } from '../ambient-instance';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { assertMediaPermission } from '../permissions';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { sanitizeMediaAsset, sanitizeMediaFolderTree } from '../sanitizers/sanitize-media';\nimport { MCP_NOT_FOUND_ASSET } from './constants';\nimport { ok } from '../utils';\n\nconst DEFAULT_SORT = 'createdAt:DESC';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's Zod input\n// schema before the handler runs, so handlers accept the erased `Record<string, unknown>` the\n// registry hands them and narrow it here.\ntype ListMediaArgs = {\n folderId?: number | null;\n mime?: string;\n name?: string;\n page?: number;\n pageSize?: number;\n sort?: string;\n};\n\ntype GetMediaArgs = {\n id: number;\n};\n\n/**\n * Builds the `filters` clause for `media_list_assets`.\n *\n * `folderId: null` is meaningfully different from an omitted `folderId`: null means \"assets at\n * the media library root\" (no folder relation), while omitting it means \"any folder\".\n *\n * `mime` accepts both a full type and a bare prefix. An exact match on \"image\" would never hit,\n * so a value without a slash is matched as a prefix (\"image\" → every \"image/*\").\n */\nconst buildAssetFilters = (args: ListMediaArgs): Record<string, unknown> => {\n const filters: Record<string, unknown> = {};\n\n if (args.folderId === null) {\n filters.folder = null;\n } else if (args.folderId !== undefined) {\n filters.folder = { id: args.folderId };\n }\n\n if (args.mime !== undefined) {\n filters.mime = args.mime.includes('/')\n ? { $eqi: args.mime }\n : { $startsWithi: `${args.mime}/` };\n }\n\n if (args.name !== undefined) {\n filters.name = { $containsi: args.name };\n }\n\n return filters;\n};\n\n/**\n * `media_list_assets` — paginated, filtered listing of media files.\n *\n * Permission conditions are applied through the permissions manager\n * (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —\n * sees the same subset it would through the admin API.\n */\nexport const createMediaListAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { folderId, mime, name, page, pageSize, sort } = args as ListMediaArgs;\n const pm = assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n const query = await pm.addPermissionsQueryTo({\n filters: buildAssetFilters({ folderId, mime, name }),\n sort: sort ?? DEFAULT_SORT,\n page: page ?? 1,\n pageSize: pageSize ?? 25,\n populate: { folder: { fields: ['id', 'name'] } },\n });\n\n const { results, pagination } = await getService('upload', strapi).findPage(query);\n\n return ok({\n results: results.map(sanitizeMediaAsset),\n pagination,\n });\n };\n};\n\n/**\n * `media_get_asset` — a single asset by numeric id.\n *\n * Row-level permission *conditions* are enforced by `findEntityAndCheckPermissions`, the same\n * helper the admin `findOne` controller uses: it populates `createdBy` and the creator's roles\n * before building the CASL subject, which is what the admin conditions read. Checking a subject\n * without those fields would deny a conditioned grant its own assets, and would disagree with\n * `media_list_assets`, where the condition is pushed into the query instead.\n */\nexport const createMediaGetAssetHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id } = args as GetMediaArgs;\n\n // Model-level gate first, so a token without upload read at all is refused before a lookup.\n assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n let asset;\n try {\n ({ file: asset } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.read,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n // The helper is written for HTTP, where a bare NotFoundError is enough. MCP answers an\n // agent, so restate it with the message the other media tools use.\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n throw error;\n }\n\n return ok({ data: sanitizeMediaAsset(asset) });\n };\n};\n\n/**\n * `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.\n *\n * `getStructure()` returns the whole tree in one query and has no permission-condition\n * filtering, so this is gated on the model-level read permission only — matching\n * `GET /upload/folder-structure` in the admin API.\n *\n * The factory asserts the handler's instance is the ambient `global.strapi` the folder service\n * actually queries — see `assertAmbientInstance` for why.\n */\nexport const createMediaListFoldersHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async (): Promise<Modules.MCP.McpToolHandlerReturn> => {\n assertMediaPermission(strapi, context, ACTIONS.read, FOLDER_MODEL_UID);\n\n const structure = await getFolderService(strapi).getStructure();\n\n return ok({ data: sanitizeMediaFolderTree(structure) });\n };\n};\n"],"names":["DEFAULT_SORT","buildAssetFilters","args","filters","folderId","folder","undefined","id","mime","includes","$eqi","$startsWithi","name","$containsi","createMediaListAssetsHandler","strapi","context","assertAmbientInstance","page","pageSize","sort","pm","assertMediaPermission","ACTIONS","read","FILE_MODEL_UID","query","addPermissionsQueryTo","populate","fields","results","pagination","getService","findPage","ok","map","sanitizeMediaAsset","createMediaGetAssetHandler","asset","file","findEntityAndCheckPermissions","userAbility","error","errors","NotFoundError","MCP_NOT_FOUND_ASSET","data","createMediaListFoldersHandler","FOLDER_MODEL_UID","structure","getFolderService","getStructure","sanitizeMediaFolderTree"],"mappings":"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"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils = require('@strapi/utils');
|
|
4
|
+
var index = require('../../utils/index.js');
|
|
5
|
+
var constants$1 = require('../../constants.js');
|
|
6
|
+
var findEntityAndCheckPermissions = require('../../controllers/utils/find-entity-and-check-permissions.js');
|
|
7
|
+
var ambientInstance = require('../ambient-instance.js');
|
|
8
|
+
var permissions = require('../permissions.js');
|
|
9
|
+
var sanitizeMedia = require('../sanitizers/sanitize-media.js');
|
|
10
|
+
var constants = require('./constants.js');
|
|
11
|
+
var utils$1 = require('../utils.js');
|
|
12
|
+
|
|
13
|
+
/** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */ const WRITABLE_FIELDS = [
|
|
14
|
+
'name',
|
|
15
|
+
'alternativeText',
|
|
16
|
+
'caption'
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Picks the metadata the caller actually sent.
|
|
20
|
+
*
|
|
21
|
+
* `updateFileInfo` treats nil as "keep the stored value" (`_.isNil`), so an explicit
|
|
22
|
+
* `alternativeText: null` cannot be forwarded as null — it would be read as "unchanged"
|
|
23
|
+
* instead of "clear it". Clearing is expressed as an empty string, which is what the admin
|
|
24
|
+
* panel writes when the field is emptied.
|
|
25
|
+
*/ const buildFileInfo = (args)=>{
|
|
26
|
+
const fileInfo = {};
|
|
27
|
+
for (const field of WRITABLE_FIELDS){
|
|
28
|
+
const value = args[field];
|
|
29
|
+
if (value !== undefined) {
|
|
30
|
+
fileInfo[field] = value === null ? '' : value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return fileInfo;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `media_update_asset` — edits the writable metadata of one asset.
|
|
37
|
+
*
|
|
38
|
+
* Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same
|
|
39
|
+
* `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and
|
|
40
|
+
* the same `updatedBy` attribution from the session user.
|
|
41
|
+
*
|
|
42
|
+
* The response carries the updated asset through the read sanitizer, so a client can confirm
|
|
43
|
+
* the write without a second `media_get_asset` round-trip — and so provider fields stay
|
|
44
|
+
* invisible on the write path too.
|
|
45
|
+
*/ const createMediaUpdateAssetHandler = (strapi, context)=>{
|
|
46
|
+
ambientInstance.assertAmbientInstance(strapi);
|
|
47
|
+
return async ({ args })=>{
|
|
48
|
+
const { id, ...metadata } = args;
|
|
49
|
+
const fileInfo = buildFileInfo(metadata);
|
|
50
|
+
// A patch with no writable field is a caller error, not a no-op success: the schema cannot
|
|
51
|
+
// express "at least one of" without becoming a ZodEffects the registry can't publish.
|
|
52
|
+
if (Object.keys(fileInfo).length === 0) {
|
|
53
|
+
throw new utils.errors.ValidationError(constants.MCP_UPDATE_ASSET_NO_FIELDS);
|
|
54
|
+
}
|
|
55
|
+
// Model-level gate first, so a token without the action is refused before any DB read.
|
|
56
|
+
// `findEntityAndCheckPermissions` only covers the row-level check.
|
|
57
|
+
permissions.assertMediaPermission(strapi, context, constants$1.ACTIONS.update, constants$1.FILE_MODEL_UID);
|
|
58
|
+
// Row-level check, shared with the admin controller: resolves the creator's roles so an
|
|
59
|
+
// owner-scoped permission condition is evaluated against the same subject the REST API
|
|
60
|
+
// would build, and throws Forbidden when a condition excludes this asset.
|
|
61
|
+
//
|
|
62
|
+
// Its NotFoundError carries no message, which would reach the agent as a bare "Not Found";
|
|
63
|
+
// rethrowing adds the same wording the read tools use.
|
|
64
|
+
try {
|
|
65
|
+
await findEntityAndCheckPermissions.findEntityAndCheckPermissions(context.userAbility, constants$1.ACTIONS.update, constants$1.FILE_MODEL_UID, id, strapi);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof utils.errors.NotFoundError) {
|
|
68
|
+
throw new utils.errors.NotFoundError(constants.MCP_NOT_FOUND_ASSET);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const written = await index.getService('upload', strapi).updateFileInfo(id, fileInfo, {
|
|
73
|
+
user: context.user
|
|
74
|
+
});
|
|
75
|
+
const updated = await index.getService('upload', strapi).findOne(id, [
|
|
76
|
+
'folder'
|
|
77
|
+
]);
|
|
78
|
+
return utils$1.ok({
|
|
79
|
+
data: sanitizeMedia.sanitizeMediaAsset(updated ?? written)
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.
|
|
85
|
+
*
|
|
86
|
+
* `null` is the media library root and always valid. A named folder is looked up so a typo — or a
|
|
87
|
+
* folder id passed where the agent meant an asset — is refused before anything is moved, matching
|
|
88
|
+
* the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.
|
|
89
|
+
*
|
|
90
|
+
* A bad destination is the one failure that rejects the whole call rather than being reported per
|
|
91
|
+
* id: it is a property of the request, not of any single asset, so every id would fail for the
|
|
92
|
+
* same reason and a per-id report would say nothing the agent could act on.
|
|
93
|
+
*/ const resolveDestinationFolder = async (strapi, folder)=>{
|
|
94
|
+
if (folder === null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const destination = await strapi.db.query(constants$1.FOLDER_MODEL_UID).findOne({
|
|
98
|
+
select: [
|
|
99
|
+
'id',
|
|
100
|
+
'name'
|
|
101
|
+
],
|
|
102
|
+
where: {
|
|
103
|
+
id: folder
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (destination === null || destination === undefined) {
|
|
107
|
+
throw new utils.errors.ValidationError(constants.MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
id: Number(destination.id),
|
|
111
|
+
name: String(destination.name ?? '')
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* `media_move_assets` — moves assets between folders in bulk.
|
|
116
|
+
*
|
|
117
|
+
* Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves
|
|
118
|
+
* many assets at once, and one bad id among good ones must not discard the valid moves. Each
|
|
119
|
+
* asset is moved on its own, so a failure is confined to its own id and the response says exactly
|
|
120
|
+
* which ids to retry.
|
|
121
|
+
*
|
|
122
|
+
* That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single
|
|
123
|
+
* transaction is all-or-nothing by construction (and which also handles folders, out of scope
|
|
124
|
+
* here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and
|
|
125
|
+
* `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private
|
|
126
|
+
* `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the
|
|
127
|
+
* controller's raw join-table writes would skip all four.
|
|
128
|
+
*
|
|
129
|
+
* The moves are sequential on purpose. Each one writes the join table and `folderPath` for its
|
|
130
|
+
* asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the
|
|
131
|
+
* connection-pool contention of firing them in parallel.
|
|
132
|
+
*/ const createMediaMoveAssetsHandler = (strapi, context)=>{
|
|
133
|
+
ambientInstance.assertAmbientInstance(strapi);
|
|
134
|
+
return async ({ args })=>{
|
|
135
|
+
const { ids, folder } = args;
|
|
136
|
+
// Model-level gate first, so a token without the action is refused before any DB read.
|
|
137
|
+
permissions.assertMediaPermission(strapi, context, constants$1.ACTIONS.update, constants$1.FILE_MODEL_UID);
|
|
138
|
+
const destinationFolder = await resolveDestinationFolder(strapi, folder);
|
|
139
|
+
const moved = [];
|
|
140
|
+
const failed = [];
|
|
141
|
+
// `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than
|
|
142
|
+
// reporting the same asset twice for a request that moved it once.
|
|
143
|
+
for (const id of new Set(ids)){
|
|
144
|
+
try {
|
|
145
|
+
// Row-level check, shared with the admin controller: an owner-scoped permission
|
|
146
|
+
// condition is evaluated against the same subject the REST API would build.
|
|
147
|
+
await findEntityAndCheckPermissions.findEntityAndCheckPermissions(context.userAbility, constants$1.ACTIONS.update, constants$1.FILE_MODEL_UID, id, strapi);
|
|
148
|
+
const updated = await index.getService('upload', strapi).updateFileInfo(id, // `updateFileInfo` reads `undefined` as "keep the stored folder" and null as the root,
|
|
149
|
+
// so the destination is forwarded as-is.
|
|
150
|
+
{
|
|
151
|
+
folder
|
|
152
|
+
}, {
|
|
153
|
+
user: context.user
|
|
154
|
+
});
|
|
155
|
+
// `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so
|
|
156
|
+
// the destination is attached here instead of costing a read-back per asset.
|
|
157
|
+
moved.push(sanitizeMedia.sanitizeMediaAsset({
|
|
158
|
+
...updated,
|
|
159
|
+
folder: destinationFolder
|
|
160
|
+
}));
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof utils.errors.NotFoundError) {
|
|
163
|
+
failed.push({
|
|
164
|
+
id,
|
|
165
|
+
reason: constants.MCP_MOVE_ASSETS_ID_NOT_FOUND
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (error instanceof utils.errors.ForbiddenError) {
|
|
170
|
+
failed.push({
|
|
171
|
+
id,
|
|
172
|
+
reason: constants.MCP_MOVE_ASSETS_ID_FORBIDDEN
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Any other failure — a DB error, a provider fault — is reported against this id too,
|
|
178
|
+
* rather than thrown.
|
|
179
|
+
*
|
|
180
|
+
* Throwing here would be the one way to lose committed work silently: a tool error
|
|
181
|
+
* reaches the client as `{ content: [text], isError: true }` with NO
|
|
182
|
+
* `structuredContent` (see `tool-registry.ts`), so an error on the third id would
|
|
183
|
+
* discard the report saying the first two had already moved. The agent would be left
|
|
184
|
+
* unable to tell which ids to retry — exactly the recoverability this tool exists to
|
|
185
|
+
* provide.
|
|
186
|
+
*
|
|
187
|
+
* The message is carried through verbatim so the real fault is still legible, and the
|
|
188
|
+
* loop continues: one broken asset must not strand the rest of a reorganisation.
|
|
189
|
+
*/ failed.push({
|
|
190
|
+
id,
|
|
191
|
+
reason: constants.MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error))
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A request where nothing moved still returns the per-id report.
|
|
197
|
+
*
|
|
198
|
+
* `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing
|
|
199
|
+
* moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool
|
|
200
|
+
* error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a
|
|
201
|
+
* machine-readable entry, for the same class of mistake.
|
|
202
|
+
*
|
|
203
|
+
* The only failure that still rejects the whole call is an invalid destination folder, which
|
|
204
|
+
* is checked before the loop: nothing has moved, so there is no report to preserve.
|
|
205
|
+
*/ return utils$1.ok({
|
|
206
|
+
destinationFolder,
|
|
207
|
+
moved,
|
|
208
|
+
failed
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.
|
|
214
|
+
*
|
|
215
|
+
* Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the
|
|
216
|
+
* deletion must agree on what would be destroyed, and splitting them across tools would both let
|
|
217
|
+
* the two drift apart and let an agent reach the destructive one without ever seeing a preview.
|
|
218
|
+
* `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting
|
|
219
|
+
* takes an explicit `dryRun: false`.
|
|
220
|
+
*
|
|
221
|
+
* Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete
|
|
222
|
+
* cascades, so a mixed id list there is refused outright rather than half-applied over an unknown
|
|
223
|
+
* amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and
|
|
224
|
+
* the requirement is explicit: a bad id among good ones must not discard the valid deletions.
|
|
225
|
+
* The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*
|
|
226
|
+
* anything is destroyed rather than after.
|
|
227
|
+
*
|
|
228
|
+
* This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper
|
|
229
|
+
* fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report
|
|
230
|
+
* of everything already deleted — unrecoverable for an operation with no undo. The removals are
|
|
231
|
+
* driven one at a time here instead, through the same `upload.remove` service, which deletes the
|
|
232
|
+
* provider file and every generated format, emits `media.delete`, and then deletes the row.
|
|
233
|
+
*
|
|
234
|
+
* The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch
|
|
235
|
+
* (100 ids max, per the input schema) is not worth firing at a provider in parallel.
|
|
236
|
+
*/ const createMediaDeleteAssetsHandler = (strapi, context)=>{
|
|
237
|
+
ambientInstance.assertAmbientInstance(strapi);
|
|
238
|
+
return async ({ args })=>{
|
|
239
|
+
const { ids, dryRun = true } = args;
|
|
240
|
+
// Model-level gate first, so a token without the action is refused before any DB read.
|
|
241
|
+
// The preview takes the same gate: it reveals which assets exist and what they are.
|
|
242
|
+
permissions.assertMediaPermission(strapi, context, constants$1.ACTIONS.update, constants$1.FILE_MODEL_UID);
|
|
243
|
+
const deleted = [];
|
|
244
|
+
const failed = [];
|
|
245
|
+
// `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the
|
|
246
|
+
// second occurrence of an already-deleted asset from being reported as a missing one.
|
|
247
|
+
for (const id of new Set(ids)){
|
|
248
|
+
let file;
|
|
249
|
+
try {
|
|
250
|
+
// Row-level check, shared with the admin controller: an owner-scoped permission
|
|
251
|
+
// condition is evaluated against the same subject the REST API would build. Run on the
|
|
252
|
+
// dry run too — a preview must not list an asset the executing call would refuse.
|
|
253
|
+
({ file } = await findEntityAndCheckPermissions.findEntityAndCheckPermissions(context.userAbility, constants$1.ACTIONS.update, constants$1.FILE_MODEL_UID, id, strapi));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof utils.errors.NotFoundError) {
|
|
256
|
+
failed.push({
|
|
257
|
+
id,
|
|
258
|
+
reason: constants.MCP_DELETE_ASSETS_ID_NOT_FOUND
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (error instanceof utils.errors.ForbiddenError) {
|
|
263
|
+
failed.push({
|
|
264
|
+
id,
|
|
265
|
+
reason: constants.MCP_DELETE_ASSETS_ID_FORBIDDEN
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
failed.push({
|
|
270
|
+
id,
|
|
271
|
+
reason: constants.MCP_DELETE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error))
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// The preview stops here: the asset resolved and this token may delete it, which is
|
|
276
|
+
// everything the agent needs to confirm — reported in the same shape the real run uses.
|
|
277
|
+
if (dryRun) {
|
|
278
|
+
deleted.push(sanitizeMedia.sanitizeMediaAsset(file));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
await index.getService('upload', strapi).remove(file);
|
|
283
|
+
// Reported from the row read before the delete: the asset no longer exists, so this is
|
|
284
|
+
// the only description of it the agent will ever get.
|
|
285
|
+
deleted.push(sanitizeMedia.sanitizeMediaAsset(file));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
/**
|
|
288
|
+
* A failed removal is reported against its own id rather than thrown, for the reason
|
|
289
|
+
* spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing
|
|
290
|
+
* on the third id would discard the report saying the first two are gone. Here that
|
|
291
|
+
* matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.
|
|
292
|
+
*/ failed.push({
|
|
293
|
+
id,
|
|
294
|
+
reason: constants.MCP_DELETE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error))
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* A request where nothing was deleted still returns the per-id report, on both branches:
|
|
300
|
+
* `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.
|
|
301
|
+
* Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose
|
|
302
|
+
* where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.
|
|
303
|
+
*/ return utils$1.ok({
|
|
304
|
+
dryRun,
|
|
305
|
+
deleted,
|
|
306
|
+
failed,
|
|
307
|
+
totalFileNumber: deleted.length
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
exports.createMediaDeleteAssetsHandler = createMediaDeleteAssetsHandler;
|
|
313
|
+
exports.createMediaMoveAssetsHandler = createMediaMoveAssetsHandler;
|
|
314
|
+
exports.createMediaUpdateAssetHandler = createMediaUpdateAssetHandler;
|
|
315
|
+
//# sourceMappingURL=write-handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-handlers.js","sources":["../../../../server/src/mcp/handlers/write-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { assertAmbientInstance } from '../ambient-instance';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaAsset } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_ASSET,\n MCP_UPDATE_ASSET_NO_FIELDS,\n MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_FORBIDDEN,\n MCP_MOVE_ASSETS_ID_FAILED,\n MCP_DELETE_ASSETS_ID_NOT_FOUND,\n MCP_DELETE_ASSETS_ID_FORBIDDEN,\n MCP_DELETE_ASSETS_ID_FAILED,\n} from './constants';\nimport { ok } from '../utils';\n\n// Type-level only: the MCP SDK validates `args` against the tool's strict Zod input schema\n// before the handler runs, so unknown keys never reach here.\ntype MediaUpdateAssetArgs = {\n id: number;\n name?: string;\n alternativeText?: string | null;\n caption?: string | null;\n};\n\ntype MediaMoveAssetsArgs = {\n ids: number[];\n folder: number | null;\n};\n\ntype MediaDeleteAssetsArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */\nconst WRITABLE_FIELDS = ['name', 'alternativeText', 'caption'] as const;\n\n/**\n * Picks the metadata the caller actually sent.\n *\n * `updateFileInfo` treats nil as \"keep the stored value\" (`_.isNil`), so an explicit\n * `alternativeText: null` cannot be forwarded as null — it would be read as \"unchanged\"\n * instead of \"clear it\". Clearing is expressed as an empty string, which is what the admin\n * panel writes when the field is emptied.\n */\nconst buildFileInfo = (args: MediaUpdateAssetArgs): Record<string, string> => {\n const fileInfo: Record<string, string> = {};\n\n for (const field of WRITABLE_FIELDS) {\n const value = args[field];\n\n if (value !== undefined) {\n fileInfo[field] = value === null ? '' : value;\n }\n }\n\n return fileInfo;\n};\n\n/**\n * `media_update_asset` — edits the writable metadata of one asset.\n *\n * Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same\n * `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and\n * the same `updatedBy` attribution from the session user.\n *\n * The response carries the updated asset through the read sanitizer, so a client can confirm\n * the write without a second `media_get_asset` round-trip — and so provider fields stay\n * invisible on the write path too.\n */\nexport const createMediaUpdateAssetHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, ...metadata } = args as MediaUpdateAssetArgs;\n const fileInfo = buildFileInfo(metadata as MediaUpdateAssetArgs);\n\n // A patch with no writable field is a caller error, not a no-op success: the schema cannot\n // express \"at least one of\" without becoming a ZodEffects the registry can't publish.\n if (Object.keys(fileInfo).length === 0) {\n throw new errors.ValidationError(MCP_UPDATE_ASSET_NO_FIELDS);\n }\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // `findEntityAndCheckPermissions` only covers the row-level check.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n // Row-level check, shared with the admin controller: resolves the creator's roles so an\n // owner-scoped permission condition is evaluated against the same subject the REST API\n // would build, and throws Forbidden when a condition excludes this asset.\n //\n // Its NotFoundError carries no message, which would reach the agent as a bare \"Not Found\";\n // rethrowing adds the same wording the read tools use.\n try {\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n\n throw error;\n }\n\n const written = await getService('upload', strapi).updateFileInfo(id, fileInfo, {\n user: context.user,\n });\n\n const updated = await getService('upload', strapi).findOne(id, ['folder']);\n\n return ok({ data: sanitizeMediaAsset(updated ?? written) });\n };\n};\n\n/**\n * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.\n *\n * `null` is the media library root and always valid. A named folder is looked up so a typo — or a\n * folder id passed where the agent meant an asset — is refused before anything is moved, matching\n * the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.\n *\n * A bad destination is the one failure that rejects the whole call rather than being reported per\n * id: it is a property of the request, not of any single asset, so every id would fail for the\n * same reason and a per-id report would say nothing the agent could act on.\n */\nconst resolveDestinationFolder = async (strapi: Core.Strapi, folder: number | null) => {\n if (folder === null) {\n return null;\n }\n\n const destination = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name'],\n where: { id: folder },\n });\n\n if (destination === null || destination === undefined) {\n throw new errors.ValidationError(MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);\n }\n\n return { id: Number(destination.id), name: String(destination.name ?? '') };\n};\n\n/**\n * `media_move_assets` — moves assets between folders in bulk.\n *\n * Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves\n * many assets at once, and one bad id among good ones must not discard the valid moves. Each\n * asset is moved on its own, so a failure is confined to its own id and the response says exactly\n * which ids to retry.\n *\n * That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single\n * transaction is all-or-nothing by construction (and which also handles folders, out of scope\n * here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and\n * `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private\n * `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the\n * controller's raw join-table writes would skip all four.\n *\n * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its\n * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the\n * connection-pool contention of firing them in parallel.\n */\nexport const createMediaMoveAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, folder } = args as MediaMoveAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const destinationFolder = await resolveDestinationFolder(strapi, folder);\n\n const moved: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than\n // reporting the same asset twice for a request that moved it once.\n for (const id of new Set(ids)) {\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build.\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n const updated = await getService('upload', strapi).updateFileInfo(\n id,\n // `updateFileInfo` reads `undefined` as \"keep the stored folder\" and null as the root,\n // so the destination is forwarded as-is.\n { folder },\n { user: context.user }\n );\n\n // `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so\n // the destination is attached here instead of costing a read-back per asset.\n moved.push(sanitizeMediaAsset({ ...updated, folder: destinationFolder }));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n /**\n * Any other failure — a DB error, a provider fault — is reported against this id too,\n * rather than thrown.\n *\n * Throwing here would be the one way to lose committed work silently: a tool error\n * reaches the client as `{ content: [text], isError: true }` with NO\n * `structuredContent` (see `tool-registry.ts`), so an error on the third id would\n * discard the report saying the first two had already moved. The agent would be left\n * unable to tell which ids to retry — exactly the recoverability this tool exists to\n * provide.\n *\n * The message is carried through verbatim so the real fault is still legible, and the\n * loop continues: one broken asset must not strand the rest of a reorganisation.\n */\n failed.push({\n id,\n reason: MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error)),\n });\n }\n }\n\n /**\n * A request where nothing moved still returns the per-id report.\n *\n * `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing\n * moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool\n * error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a\n * machine-readable entry, for the same class of mistake.\n *\n * The only failure that still rejects the whole call is an invalid destination folder, which\n * is checked before the loop: nothing has moved, so there is no report to preserve.\n */\n return ok({ destinationFolder, moved, failed });\n };\n};\n\n/**\n * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.\n *\n * Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the\n * deletion must agree on what would be destroyed, and splitting them across tools would both let\n * the two drift apart and let an agent reach the destructive one without ever seeing a preview.\n * `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting\n * takes an explicit `dryRun: false`.\n *\n * Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete\n * cascades, so a mixed id list there is refused outright rather than half-applied over an unknown\n * amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and\n * the requirement is explicit: a bad id among good ones must not discard the valid deletions.\n * The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*\n * anything is destroyed rather than after.\n *\n * This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper\n * fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report\n * of everything already deleted — unrecoverable for an operation with no undo. The removals are\n * driven one at a time here instead, through the same `upload.remove` service, which deletes the\n * provider file and every generated format, emits `media.delete`, and then deletes the row.\n *\n * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch\n * (100 ids max, per the input schema) is not worth firing at a provider in parallel.\n */\nexport const createMediaDeleteAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, dryRun = true } = args as MediaDeleteAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // The preview takes the same gate: it reveals which assets exist and what they are.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const deleted: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the\n // second occurrence of an already-deleted asset from being reported as a missing one.\n for (const id of new Set(ids)) {\n let file;\n\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build. Run on the\n // dry run too — a preview must not list an asset the executing call would refuse.\n ({ file } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n continue;\n }\n\n // The preview stops here: the asset resolved and this token may delete it, which is\n // everything the agent needs to confirm — reported in the same shape the real run uses.\n if (dryRun) {\n deleted.push(sanitizeMediaAsset(file));\n continue;\n }\n\n try {\n await getService('upload', strapi).remove(file);\n\n // Reported from the row read before the delete: the asset no longer exists, so this is\n // the only description of it the agent will ever get.\n deleted.push(sanitizeMediaAsset(file));\n } catch (error) {\n /**\n * A failed removal is reported against its own id rather than thrown, for the reason\n * spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing\n * on the third id would discard the report saying the first two are gone. Here that\n * matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.\n */\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n }\n }\n\n /**\n * A request where nothing was deleted still returns the per-id report, on both branches:\n * `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.\n * Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose\n * where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.\n */\n return ok({ dryRun, deleted, failed, totalFileNumber: deleted.length });\n };\n};\n"],"names":["WRITABLE_FIELDS","buildFileInfo","args","fileInfo","field","value","undefined","createMediaUpdateAssetHandler","strapi","context","assertAmbientInstance","id","metadata","Object","keys","length","errors","ValidationError","MCP_UPDATE_ASSET_NO_FIELDS","assertMediaPermission","ACTIONS","update","FILE_MODEL_UID","findEntityAndCheckPermissions","userAbility","error","NotFoundError","MCP_NOT_FOUND_ASSET","written","getService","updateFileInfo","user","updated","findOne","ok","data","sanitizeMediaAsset","resolveDestinationFolder","folder","destination","db","query","FOLDER_MODEL_UID","select","where","MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND","Number","name","String","createMediaMoveAssetsHandler","ids","destinationFolder","moved","failed","Set","push","reason","MCP_MOVE_ASSETS_ID_NOT_FOUND","ForbiddenError","MCP_MOVE_ASSETS_ID_FORBIDDEN","MCP_MOVE_ASSETS_ID_FAILED","Error","message","createMediaDeleteAssetsHandler","dryRun","deleted","file","MCP_DELETE_ASSETS_ID_NOT_FOUND","MCP_DELETE_ASSETS_ID_FORBIDDEN","MCP_DELETE_ASSETS_ID_FAILED","remove","totalFileNumber"],"mappings":";;;;;;;;;;;;AAyCA,oGACA,MAAMA,eAAAA,GAAkB;AAAC,IAAA,MAAA;AAAQ,IAAA,iBAAA;AAAmB,IAAA;AAAU,CAAA;AAE9D;;;;;;;IAQA,MAAMC,gBAAgB,CAACC,IAAAA,GAAAA;AACrB,IAAA,MAAMC,WAAmC,EAAC;IAE1C,KAAK,MAAMC,SAASJ,eAAAA,CAAiB;QACnC,MAAMK,KAAAA,GAAQH,IAAI,CAACE,KAAAA,CAAM;AAEzB,QAAA,IAAIC,UAAUC,SAAAA,EAAW;AACvBH,YAAAA,QAAQ,CAACC,KAAAA,CAAM,GAAGC,KAAAA,KAAU,OAAO,EAAA,GAAKA,KAAAA;AAC1C,QAAA;AACF,IAAA;IAEA,OAAOF,QAAAA;AACT,CAAA;AAEA;;;;;;;;;;AAUC,IACM,MAAMI,6BAAAA,GAAgC,CAC3CC,MAAAA,EACAC,OAAAA,GAAAA;IAEAC,qCAAAA,CAAsBF,MAAAA,CAAAA;IAEtB,OAAO,OAAO,EACZN,IAAI,EAGL,GAAA;AACC,QAAA,MAAM,EAAES,EAAE,EAAE,GAAGC,UAAU,GAAGV,IAAAA;AAC5B,QAAA,MAAMC,WAAWF,aAAAA,CAAcW,QAAAA,CAAAA;;;AAI/B,QAAA,IAAIC,OAAOC,IAAI,CAACX,QAAAA,CAAAA,CAAUY,MAAM,KAAK,CAAA,EAAG;YACtC,MAAM,IAAIC,YAAAA,CAAOC,eAAe,CAACC,oCAAAA,CAAAA;AACnC,QAAA;;;AAIAC,QAAAA,iCAAAA,CAAsBX,MAAAA,EAAQC,OAAAA,EAASW,mBAAAA,CAAQC,MAAM,EAAEC,0BAAAA,CAAAA;;;;;;;QAQvD,IAAI;YACF,MAAMC,2DAAAA,CACJd,QAAQe,WAAW,EACnBJ,oBAAQC,MAAM,EACdC,4BACAX,EAAAA,EACAH,MAAAA,CAAAA;AAEJ,QAAA,CAAA,CAAE,OAAOiB,KAAAA,EAAO;YACd,IAAIA,KAAAA,YAAiBT,YAAAA,CAAOU,aAAa,EAAE;gBACzC,MAAM,IAAIV,YAAAA,CAAOU,aAAa,CAACC,6BAAAA,CAAAA;AACjC,YAAA;YAEA,MAAMF,KAAAA;AACR,QAAA;QAEA,MAAMG,OAAAA,GAAU,MAAMC,gBAAAA,CAAW,QAAA,EAAUrB,QAAQsB,cAAc,CAACnB,IAAIR,QAAAA,EAAU;AAC9E4B,YAAAA,IAAAA,EAAMtB,QAAQsB;AAChB,SAAA,CAAA;AAEA,QAAA,MAAMC,UAAU,MAAMH,gBAAAA,CAAW,UAAUrB,MAAAA,CAAAA,CAAQyB,OAAO,CAACtB,EAAAA,EAAI;AAAC,YAAA;AAAS,SAAA,CAAA;AAEzE,QAAA,OAAOuB,UAAAA,CAAG;AAAEC,YAAAA,IAAAA,EAAMC,iCAAmBJ,OAAAA,IAAWJ,OAAAA;AAAS,SAAA,CAAA;AAC3D,IAAA,CAAA;AACF;AAEA;;;;;;;;;;IAWA,MAAMS,wBAAAA,GAA2B,OAAO7B,MAAAA,EAAqB8B,MAAAA,GAAAA;AAC3D,IAAA,IAAIA,WAAW,IAAA,EAAM;QACnB,OAAO,IAAA;AACT,IAAA;IAEA,MAAMC,WAAAA,GAAc,MAAM/B,MAAAA,CAAOgC,EAAE,CAACC,KAAK,CAACC,4BAAAA,CAAAA,CAAkBT,OAAO,CAAC;QAClEU,MAAAA,EAAQ;AAAC,YAAA,IAAA;AAAM,YAAA;AAAO,SAAA;QACtBC,KAAAA,EAAO;YAAEjC,EAAAA,EAAI2B;AAAO;AACtB,KAAA,CAAA;IAEA,IAAIC,WAAAA,KAAgB,IAAA,IAAQA,WAAAA,KAAgBjC,SAAAA,EAAW;QACrD,MAAM,IAAIU,YAAAA,CAAOC,eAAe,CAAC4B,+CAAAA,CAAAA;AACnC,IAAA;IAEA,OAAO;QAAElC,EAAAA,EAAImC,MAAAA,CAAOP,YAAY5B,EAAE,CAAA;QAAGoC,IAAAA,EAAMC,MAAAA,CAAOT,WAAAA,CAAYQ,IAAI,IAAI,EAAA;AAAI,KAAA;AAC5E,CAAA;AAEA;;;;;;;;;;;;;;;;;;AAkBC,IACM,MAAME,4BAAAA,GAA+B,CAC1CzC,MAAAA,EACAC,OAAAA,GAAAA;IAEAC,qCAAAA,CAAsBF,MAAAA,CAAAA;IAEtB,OAAO,OAAO,EACZN,IAAI,EAGL,GAAA;AACC,QAAA,MAAM,EAAEgD,GAAG,EAAEZ,MAAM,EAAE,GAAGpC,IAAAA;;AAGxBiB,QAAAA,iCAAAA,CAAsBX,MAAAA,EAAQC,OAAAA,EAASW,mBAAAA,CAAQC,MAAM,EAAEC,0BAAAA,CAAAA;QAEvD,MAAM6B,iBAAAA,GAAoB,MAAMd,wBAAAA,CAAyB7B,MAAAA,EAAQ8B,MAAAA,CAAAA;AAEjE,QAAA,MAAMc,QAAiD,EAAE;AACzD,QAAA,MAAMC,SAA2C,EAAE;;;AAInD,QAAA,KAAK,MAAM1C,EAAAA,IAAM,IAAI2C,GAAAA,CAAIJ,GAAAA,CAAAA,CAAM;YAC7B,IAAI;;;gBAGF,MAAM3B,2DAAAA,CACJd,QAAQe,WAAW,EACnBJ,oBAAQC,MAAM,EACdC,4BACAX,EAAAA,EACAH,MAAAA,CAAAA;gBAEF,MAAMwB,OAAAA,GAAU,MAAMH,gBAAAA,CAAW,QAAA,EAAUrB,QAAQsB,cAAc,CAC/DnB;;AAGA,gBAAA;AAAE2B,oBAAAA;iBAAO,EACT;AAAEP,oBAAAA,IAAAA,EAAMtB,QAAQsB;AAAK,iBAAA,CAAA;;;gBAKvBqB,KAAAA,CAAMG,IAAI,CAACnB,gCAAAA,CAAmB;AAAE,oBAAA,GAAGJ,OAAO;oBAAEM,MAAAA,EAAQa;AAAkB,iBAAA,CAAA,CAAA;AACxE,YAAA,CAAA,CAAE,OAAO1B,KAAAA,EAAO;gBACd,IAAIA,KAAAA,YAAiBT,YAAAA,CAAOU,aAAa,EAAE;AACzC2B,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;AAAE5C,wBAAAA,EAAAA;wBAAI6C,MAAAA,EAAQC;AAA6B,qBAAA,CAAA;AACvD,oBAAA;AACF,gBAAA;gBAEA,IAAIhC,KAAAA,YAAiBT,YAAAA,CAAO0C,cAAc,EAAE;AAC1CL,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;AAAE5C,wBAAAA,EAAAA;wBAAI6C,MAAAA,EAAQG;AAA6B,qBAAA,CAAA;AACvD,oBAAA;AACF,gBAAA;AAEA;;;;;;;;;;;;;YAcAN,MAAAA,CAAOE,IAAI,CAAC;AACV5C,oBAAAA,EAAAA;AACA6C,oBAAAA,MAAAA,EAAQI,oCAA0BnC,KAAAA,YAAiBoC,KAAAA,GAAQpC,KAAAA,CAAMqC,OAAO,GAAGd,MAAAA,CAAOvB,KAAAA,CAAAA;AACpF,iBAAA,CAAA;AACF,YAAA;AACF,QAAA;AAEA;;;;;;;;;;AAUC,QACD,OAAOS,UAAAA,CAAG;AAAEiB,YAAAA,iBAAAA;AAAmBC,YAAAA,KAAAA;AAAOC,YAAAA;AAAO,SAAA,CAAA;AAC/C,IAAA,CAAA;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;AAwBC,IACM,MAAMU,8BAAAA,GAAiC,CAC5CvD,MAAAA,EACAC,OAAAA,GAAAA;IAEAC,qCAAAA,CAAsBF,MAAAA,CAAAA;IAEtB,OAAO,OAAO,EACZN,IAAI,EAGL,GAAA;AACC,QAAA,MAAM,EAAEgD,GAAG,EAAEc,MAAAA,GAAS,IAAI,EAAE,GAAG9D,IAAAA;;;AAI/BiB,QAAAA,iCAAAA,CAAsBX,MAAAA,EAAQC,OAAAA,EAASW,mBAAAA,CAAQC,MAAM,EAAEC,0BAAAA,CAAAA;AAEvD,QAAA,MAAM2C,UAAmD,EAAE;AAC3D,QAAA,MAAMZ,SAA2C,EAAE;;;AAInD,QAAA,KAAK,MAAM1C,EAAAA,IAAM,IAAI2C,GAAAA,CAAIJ,GAAAA,CAAAA,CAAM;YAC7B,IAAIgB,IAAAA;YAEJ,IAAI;;;;AAID,gBAAA,CAAA,EAAEA,IAAI,EAAE,GAAG,MAAM3C,2DAAAA,CAChBd,OAAAA,CAAQe,WAAW,EACnBJ,mBAAAA,CAAQC,MAAM,EACdC,0BAAAA,EACAX,IACAH,MAAAA,CACF;AACF,YAAA,CAAA,CAAE,OAAOiB,KAAAA,EAAO;gBACd,IAAIA,KAAAA,YAAiBT,YAAAA,CAAOU,aAAa,EAAE;AACzC2B,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;AAAE5C,wBAAAA,EAAAA;wBAAI6C,MAAAA,EAAQW;AAA+B,qBAAA,CAAA;AACzD,oBAAA;AACF,gBAAA;gBAEA,IAAI1C,KAAAA,YAAiBT,YAAAA,CAAO0C,cAAc,EAAE;AAC1CL,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;AAAE5C,wBAAAA,EAAAA;wBAAI6C,MAAAA,EAAQY;AAA+B,qBAAA,CAAA;AACzD,oBAAA;AACF,gBAAA;AAEAf,gBAAAA,MAAAA,CAAOE,IAAI,CAAC;AACV5C,oBAAAA,EAAAA;AACA6C,oBAAAA,MAAAA,EAAQa,sCACN5C,KAAAA,YAAiBoC,KAAAA,GAAQpC,KAAAA,CAAMqC,OAAO,GAAGd,MAAAA,CAAOvB,KAAAA,CAAAA;AAEpD,iBAAA,CAAA;AACA,gBAAA;AACF,YAAA;;;AAIA,YAAA,IAAIuC,MAAAA,EAAQ;gBACVC,OAAAA,CAAQV,IAAI,CAACnB,gCAAAA,CAAmB8B,IAAAA,CAAAA,CAAAA;AAChC,gBAAA;AACF,YAAA;YAEA,IAAI;AACF,gBAAA,MAAMrC,gBAAAA,CAAW,QAAA,EAAUrB,MAAAA,CAAAA,CAAQ8D,MAAM,CAACJ,IAAAA,CAAAA;;;gBAI1CD,OAAAA,CAAQV,IAAI,CAACnB,gCAAAA,CAAmB8B,IAAAA,CAAAA,CAAAA;AAClC,YAAA,CAAA,CAAE,OAAOzC,KAAAA,EAAO;AACd;;;;;YAMA4B,MAAAA,CAAOE,IAAI,CAAC;AACV5C,oBAAAA,EAAAA;AACA6C,oBAAAA,MAAAA,EAAQa,sCACN5C,KAAAA,YAAiBoC,KAAAA,GAAQpC,KAAAA,CAAMqC,OAAO,GAAGd,MAAAA,CAAOvB,KAAAA,CAAAA;AAEpD,iBAAA,CAAA;AACF,YAAA;AACF,QAAA;AAEA;;;;;AAKC,QACD,OAAOS,UAAAA,CAAG;AAAE8B,YAAAA,MAAAA;AAAQC,YAAAA,OAAAA;AAASZ,YAAAA,MAAAA;AAAQkB,YAAAA,eAAAA,EAAiBN,QAAQlD;AAAO,SAAA,CAAA;AACvE,IAAA,CAAA;AACF;;;;;;"}
|