@sanity/cli 8.10.0 → 8.12.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/README.md +69 -26
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js +21 -0
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js.map +1 -0
- package/dist/actions/assets/withUrlIngestProgress.js +45 -0
- package/dist/actions/assets/withUrlIngestProgress.js.map +1 -0
- package/dist/actions/auth/authServer.js +5 -8
- package/dist/actions/auth/authServer.js.map +1 -1
- package/dist/actions/auth/login/getProvider.js +10 -14
- package/dist/actions/auth/login/getProvider.js.map +1 -1
- package/dist/actions/auth/login/login.js +0 -1
- package/dist/actions/auth/login/login.js.map +1 -1
- package/dist/actions/deploy/deployApp.js +5 -2
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployChecks.js +3 -2
- package/dist/actions/deploy/deployChecks.js.map +1 -1
- package/dist/actions/deploy/findUserApplication.js +3 -2
- package/dist/actions/deploy/findUserApplication.js.map +1 -1
- package/dist/actions/deploy/resolveDeployTarget.js +6 -1
- package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
- package/dist/actions/documents/validateDocuments.worker.js +1 -4
- package/dist/actions/documents/validateDocuments.worker.js.map +1 -1
- package/dist/actions/media/importMedia.js.map +1 -1
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js +19 -0
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js.map +1 -0
- package/dist/commands/assets/upload.js +78 -52
- package/dist/commands/assets/upload.js.map +1 -1
- package/dist/commands/deploy.js +33 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/media/import.js +149 -13
- package/dist/commands/media/import.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/index.js +1 -4
- package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +26 -6
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js +112 -0
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js.map +1 -0
- package/dist/exports/invokeSanityCli/help.js +8 -1
- package/dist/exports/invokeSanityCli/help.js.map +1 -1
- package/dist/exports/invokeSanityCli/index.d.ts +1 -2
- package/dist/exports/invokeSanityCli/index.js +37 -15
- package/dist/exports/invokeSanityCli/index.js.map +1 -1
- package/dist/generated/apiRoutes.js +2 -0
- package/dist/generated/apiRoutes.js.map +1 -1
- package/dist/services/assets.js +46 -1
- package/dist/services/assets.js.map +1 -1
- package/dist/services/mediaLibraries.js +39 -1
- package/dist/services/mediaLibraries.js.map +1 -1
- package/dist/util/assetSourceValidation.js +56 -0
- package/dist/util/assetSourceValidation.js.map +1 -0
- package/dist/util/assetUploadErrors.js +64 -0
- package/dist/util/assetUploadErrors.js.map +1 -0
- package/dist/util/isRemoteAssetSource.js +19 -0
- package/dist/util/isRemoteAssetSource.js.map +1 -0
- package/dist/util/parseAspectFlags.js +34 -0
- package/dist/util/parseAspectFlags.js.map +1 -0
- package/oclif.config.js +0 -1
- package/oclif.manifest.json +2134 -2055
- package/package.json +18 -18
- package/templates/app-quickstart/AGENTS.md +18 -0
- package/templates/app-sanity-ui/AGENTS.md +18 -0
- package/templates/shopify/.gitignore +26 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js +0 -46
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +0 -1
|
@@ -6,14 +6,41 @@ import { boxen, spinner } from '@sanity/cli-core/ux';
|
|
|
6
6
|
import { pipe, scan, tap } from 'rxjs';
|
|
7
7
|
import { importer } from '../../actions/media/importMedia.js';
|
|
8
8
|
import { importMediaDebug } from '../../actions/media/importMediaDebug.js';
|
|
9
|
+
import { ingestMediaAssetFromUrlWithProgress } from '../../actions/media/ingestMediaAssetFromUrlWithProgress.js';
|
|
9
10
|
import { promptForMediaLibrary } from '../../prompts/promptForMediaLibrary.js';
|
|
10
11
|
import { promptForProject } from '../../prompts/promptForProject.js';
|
|
11
12
|
import { getMediaLibraries } from '../../services/mediaLibraries.js';
|
|
13
|
+
import { getAssetFilenameError, getIngestUrlError } from '../../util/assetSourceValidation.js';
|
|
14
|
+
import { getAssetUploadErrorMessage } from '../../util/assetUploadErrors.js';
|
|
15
|
+
import { isRemoteAssetSource } from '../../util/isRemoteAssetSource.js';
|
|
16
|
+
import { parseAspectFlags } from '../../util/parseAspectFlags.js';
|
|
12
17
|
import { getProjectIdFlag } from '../../util/sharedFlags.js';
|
|
18
|
+
import { defineCommandTelemetry } from '../../util/telemetry/commandTelemetry.js';
|
|
19
|
+
const flags = {
|
|
20
|
+
...getProjectIdFlag({
|
|
21
|
+
description: 'Project ID to import media to',
|
|
22
|
+
semantics: 'override'
|
|
23
|
+
}),
|
|
24
|
+
aspect: Flags.string({
|
|
25
|
+
description: 'Aspect value to set on the imported asset, as key=value. Repeatable. Only applies when the source is a URL - directory and archive imports read aspects from their data.ndjson',
|
|
26
|
+
helpValue: '<key=value>',
|
|
27
|
+
multiple: true
|
|
28
|
+
}),
|
|
29
|
+
filename: Flags.string({
|
|
30
|
+
description: 'Original filename to store on the asset. Only applies when the source is a URL. Defaults to a name derived from the URL',
|
|
31
|
+
helpValue: '<filename>'
|
|
32
|
+
}),
|
|
33
|
+
'media-library-id': requiredWhenUnattended(Flags.string({
|
|
34
|
+
description: 'The id of the target media library'
|
|
35
|
+
})),
|
|
36
|
+
'replace-aspects': Flags.boolean({
|
|
37
|
+
description: 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data). Only applies to directory and archive imports'
|
|
38
|
+
})
|
|
39
|
+
};
|
|
13
40
|
export class MediaImportCommand extends SanityCommand {
|
|
14
41
|
static args = {
|
|
15
42
|
source: Args.string({
|
|
16
|
-
description: '
|
|
43
|
+
description: 'Directory, archive, or asset URL to import from. An https URL imports one asset that Sanity fetches itself',
|
|
17
44
|
required: true
|
|
18
45
|
})
|
|
19
46
|
};
|
|
@@ -30,24 +57,54 @@ export class MediaImportCommand extends SanityCommand {
|
|
|
30
57
|
{
|
|
31
58
|
command: '<%= config.bin %> <%= command.id %> products --replace-aspects',
|
|
32
59
|
description: 'Import all assets from the "products" directory and replace aspects'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
command: '<%= config.bin %> <%= command.id %> https://example.com/hero.png',
|
|
63
|
+
description: 'Have Sanity fetch a single asset from a public URL'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
command: '<%= config.bin %> <%= command.id %> https://example.com/hero.png --aspect department=Brand',
|
|
67
|
+
description: 'Fetch an asset from a URL and set aspect data on it'
|
|
33
68
|
}
|
|
34
69
|
];
|
|
35
|
-
static flags =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})),
|
|
43
|
-
'replace-aspects': Flags.boolean({
|
|
44
|
-
description: 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data)'
|
|
45
|
-
})
|
|
46
|
-
};
|
|
70
|
+
static flags = flags;
|
|
71
|
+
static telemetry = defineCommandTelemetry(flags, {
|
|
72
|
+
redact: [
|
|
73
|
+
'aspect',
|
|
74
|
+
'filename'
|
|
75
|
+
]
|
|
76
|
+
});
|
|
47
77
|
async run() {
|
|
48
78
|
const { args, flags } = await this.parse(MediaImportCommand);
|
|
49
79
|
const { source } = args;
|
|
50
80
|
const replaceAspects = flags['replace-aspects'];
|
|
81
|
+
const isUrlSource = isRemoteAssetSource(source);
|
|
82
|
+
const aspects = this.resolveAspects({
|
|
83
|
+
aspectFlags: flags.aspect,
|
|
84
|
+
isUrlSource
|
|
85
|
+
});
|
|
86
|
+
if (!isUrlSource && flags.filename !== undefined) {
|
|
87
|
+
this.error('The --filename flag only applies when importing from a URL. Directory and archive imports keep each asset’s own filename.', {
|
|
88
|
+
exit: exitCodes.USAGE_ERROR
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (isUrlSource && replaceAspects) {
|
|
92
|
+
this.error('The --replace-aspects flag only applies to directory and archive imports. Aspects passed with --aspect are set when the asset is created.', {
|
|
93
|
+
exit: exitCodes.USAGE_ERROR
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const filenameError = flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename);
|
|
97
|
+
if (filenameError) {
|
|
98
|
+
this.error(filenameError, {
|
|
99
|
+
exit: exitCodes.USAGE_ERROR
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const sourceUrlError = isUrlSource ? getIngestUrlError(source) : undefined;
|
|
103
|
+
if (sourceUrlError) {
|
|
104
|
+
this.error(sourceUrlError, {
|
|
105
|
+
exit: exitCodes.USAGE_ERROR
|
|
106
|
+
});
|
|
107
|
+
}
|
|
51
108
|
const projectId = await this.getProjectId({
|
|
52
109
|
fallback: ()=>promptForProject({})
|
|
53
110
|
});
|
|
@@ -85,6 +142,19 @@ export class MediaImportCommand extends SanityCommand {
|
|
|
85
142
|
exit: exitCodes.RUNTIME_ERROR
|
|
86
143
|
});
|
|
87
144
|
}
|
|
145
|
+
if (isUrlSource) {
|
|
146
|
+
await this.importFromUrl({
|
|
147
|
+
...aspects ? {
|
|
148
|
+
aspects
|
|
149
|
+
} : {},
|
|
150
|
+
...flags.filename ? {
|
|
151
|
+
filename: flags.filename
|
|
152
|
+
} : {},
|
|
153
|
+
mediaLibraryId,
|
|
154
|
+
url: source
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
88
158
|
const projectClient = await getProjectCliClient({
|
|
89
159
|
apiVersion: 'v2025-02-19',
|
|
90
160
|
dataset,
|
|
@@ -142,6 +212,53 @@ export class MediaImportCommand extends SanityCommand {
|
|
|
142
212
|
});
|
|
143
213
|
});
|
|
144
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Import one asset that Sanity fetches from a URL itself.
|
|
217
|
+
*
|
|
218
|
+
* Nothing but the resulting documents goes to stdout, so the output stays
|
|
219
|
+
* consumable as JSON; the wait is reported on stderr by the spinner.
|
|
220
|
+
*/ async importFromUrl(options) {
|
|
221
|
+
const { aspects, filename, mediaLibraryId, url } = options;
|
|
222
|
+
let result;
|
|
223
|
+
try {
|
|
224
|
+
result = await ingestMediaAssetFromUrlWithProgress({
|
|
225
|
+
...aspects ? {
|
|
226
|
+
aspects
|
|
227
|
+
} : {},
|
|
228
|
+
...filename ? {
|
|
229
|
+
filename
|
|
230
|
+
} : {},
|
|
231
|
+
isInteractive: this.resolveIsInteractive(),
|
|
232
|
+
logToStderr: (message)=>this.logToStderr(message),
|
|
233
|
+
mediaLibraryId,
|
|
234
|
+
url
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error instanceof Error && error.message === 'SIGINT') throw error;
|
|
238
|
+
importMediaDebug('Error importing asset from URL', error);
|
|
239
|
+
this.error(getAssetUploadErrorMessage(error, {
|
|
240
|
+
fromUrl: true,
|
|
241
|
+
target: 'media-library'
|
|
242
|
+
}), {
|
|
243
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
this.log(JSON.stringify({
|
|
247
|
+
asset: {
|
|
248
|
+
_id: result.asset._id,
|
|
249
|
+
_type: result.asset._type,
|
|
250
|
+
assetType: result.asset.assetType
|
|
251
|
+
},
|
|
252
|
+
assetInstance: {
|
|
253
|
+
_id: result.assetInstance._id,
|
|
254
|
+
extension: result.assetInstance.extension,
|
|
255
|
+
mimeType: result.assetInstance.mimeType,
|
|
256
|
+
originalFilename: result.assetInstance.originalFilename,
|
|
257
|
+
size: result.assetInstance.size,
|
|
258
|
+
url: result.assetInstance.url
|
|
259
|
+
}
|
|
260
|
+
}, null, 2));
|
|
261
|
+
}
|
|
145
262
|
reportResult(spin) {
|
|
146
263
|
let previousState;
|
|
147
264
|
return pipe(scan((processedAssetsCount, state)=>[
|
|
@@ -158,6 +275,25 @@ export class MediaImportCommand extends SanityCommand {
|
|
|
158
275
|
}
|
|
159
276
|
}));
|
|
160
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Validate `--aspect` against the source kind and shape it for the request.
|
|
280
|
+
* Runs before any network call so a malformed flag fails immediately.
|
|
281
|
+
*/ resolveAspects(options) {
|
|
282
|
+
const { aspectFlags, isUrlSource } = options;
|
|
283
|
+
if (!aspectFlags?.length) return undefined;
|
|
284
|
+
if (!isUrlSource) {
|
|
285
|
+
this.error('The --aspect flag only applies when importing from a URL. Directory and archive imports read aspect data from their data.ndjson file.', {
|
|
286
|
+
exit: exitCodes.USAGE_ERROR
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const parsed = parseAspectFlags(aspectFlags);
|
|
290
|
+
if ('error' in parsed) {
|
|
291
|
+
this.error(parsed.error, {
|
|
292
|
+
exit: exitCodes.USAGE_ERROR
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return parsed.aspects;
|
|
296
|
+
}
|
|
161
297
|
}
|
|
162
298
|
|
|
163
299
|
//# sourceMappingURL=import.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/media/import.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, getProjectCliClient, SanityCommand} from '@sanity/cli-core'\nimport {requiredWhenUnattended} from '@sanity/cli-core/flags'\nimport {boxen, spinner} from '@sanity/cli-core/ux'\nimport {SanityClient} from '@sanity/client'\nimport {type OperatorFunction, pipe, scan, tap} from 'rxjs'\n\nimport {importer, type State} from '../../actions/media/importMedia.js'\nimport {importMediaDebug} from '../../actions/media/importMediaDebug.js'\nimport {promptForMediaLibrary} from '../../prompts/promptForMediaLibrary.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {getMediaLibraries} from '../../services/mediaLibraries.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nexport class MediaImportCommand extends SanityCommand<typeof MediaImportCommand> {\n static override args = {\n source: Args.string({\n description: 'Image file or folder to import from',\n required: true,\n }),\n }\n\n static override description = 'Import a set of assets to the target media library.'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> products',\n description: 'Import all assets from the \"products\" directory',\n },\n {\n command: '<%= config.bin %> <%= command.id %> gallery.tar.gz',\n description: 'Import all assets from \"gallery\" archive',\n },\n {\n command: '<%= config.bin %> <%= command.id %> products --replace-aspects',\n description: 'Import all assets from the \"products\" directory and replace aspects',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to import media to',\n semantics: 'override',\n }),\n 'media-library-id': requiredWhenUnattended(\n Flags.string({\n description: 'The id of the target media library',\n }),\n ),\n 'replace-aspects': Flags.boolean({\n description:\n 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data)',\n }),\n }\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(MediaImportCommand)\n const {source} = args\n const replaceAspects = flags['replace-aspects']\n\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n\n const cliConfig = await this.tryGetCliConfig()\n const dataset = cliConfig.api?.dataset\n\n let mediaLibraries\n try {\n mediaLibraries = await getMediaLibraries(projectId)\n } catch (error) {\n importMediaDebug('Error listing media libraries', error)\n this.error(\n `Failed to list media libraries:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n\n if (mediaLibraries.length === 0) {\n this.error('No active media libraries found in this project', {exit: exitCodes.RUNTIME_ERROR})\n }\n\n let mediaLibraryId = flags['media-library-id']\n if (!mediaLibraryId) {\n try {\n mediaLibraryId = await promptForMediaLibrary({mediaLibraries})\n } catch (error) {\n importMediaDebug('Error selecting media library', error)\n this.error(\n `Failed to select media library:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n }\n\n if (!mediaLibraries.some((library) => library.id === mediaLibraryId)) {\n this.error(`Media library with id \"${mediaLibraryId}\" not found`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n const projectClient = await getProjectCliClient({\n apiVersion: 'v2025-02-19',\n dataset,\n perspective: 'drafts',\n projectId,\n requestTagPrefix: 'sanity.mediaLibraryCli.import',\n requireUser: true,\n '~experimental_resource': {\n id: mediaLibraryId,\n type: 'media-library',\n },\n })\n\n this.log(\n boxen(\n `\n Importing to media library: ${mediaLibraryId.padEnd(37)}\n Importing from path: ${source}\n `,\n {\n borderColor: 'yellow',\n borderStyle: 'round',\n },\n ),\n )\n\n const spin = spinner('Beginning import…').start()\n\n await this.importAssets({projectClient, replaceAspects, source, spin})\n }\n\n private async importAssets(options: {\n projectClient: SanityClient\n replaceAspects: boolean\n source: string\n spin: ReturnType<typeof spinner>\n }): Promise<void> {\n const {projectClient, replaceAspects, source, spin} = options\n\n return new Promise<void>((resolve, reject) => {\n const subscription = importer({\n client: projectClient,\n replaceAspects,\n sourcePath: source,\n spinner: spin,\n })\n .pipe(this.reportResult(spin))\n .subscribe({\n complete: () => {\n resolve()\n },\n error: (error: unknown) => {\n const message = error instanceof Error ? error.message : String(error)\n spin.stop()\n reject(new Error(message))\n },\n })\n\n // Cleanup on Ctrl+C\n process.once('SIGINT', () => {\n subscription.unsubscribe()\n spin.fail('Import interrupted.')\n process.exit(exitCodes.SIGINT)\n })\n }).catch((error) => {\n this.error(styleText('red', error.message), {exit: exitCodes.RUNTIME_ERROR})\n })\n }\n\n private reportResult(\n spin: ReturnType<typeof spinner>,\n ): OperatorFunction<State, [number, State | undefined]> {\n let previousState: State | undefined\n\n return pipe(\n scan<State, [number, State | undefined]>(\n (processedAssetsCount, state) => [processedAssetsCount[0] + 1, state],\n [0, undefined],\n ),\n tap({\n complete: () => spin.succeed(`Imported ${previousState?.fileCount} assets`),\n next: ([processedAssetsCount, state]) => {\n previousState = state\n spin.text = `${processedAssetsCount} of ${state?.fileCount} assets imported ${styleText('dim', state?.asset.originalFilename ?? '')}`\n },\n }),\n )\n }\n}\n"],"names":["styleText","Args","Flags","exitCodes","getProjectCliClient","SanityCommand","requiredWhenUnattended","boxen","spinner","pipe","scan","tap","importer","importMediaDebug","promptForMediaLibrary","promptForProject","getMediaLibraries","getProjectIdFlag","MediaImportCommand","args","source","string","description","required","examples","command","flags","semantics","boolean","run","parse","replaceAspects","projectId","getProjectId","fallback","cliConfig","tryGetCliConfig","dataset","api","mediaLibraries","error","Error","message","exit","RUNTIME_ERROR","length","mediaLibraryId","some","library","id","projectClient","apiVersion","perspective","requestTagPrefix","requireUser","type","log","padEnd","borderColor","borderStyle","spin","start","importAssets","options","Promise","resolve","reject","subscription","client","sourcePath","reportResult","subscribe","complete","String","stop","process","once","unsubscribe","fail","SIGINT","catch","previousState","processedAssetsCount","state","undefined","succeed","fileCount","next","text","asset","originalFilename"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/media/import.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, getProjectCliClient, SanityCommand} from '@sanity/cli-core'\nimport {requiredWhenUnattended} from '@sanity/cli-core/flags'\nimport {boxen, spinner} from '@sanity/cli-core/ux'\nimport {SanityClient} from '@sanity/client'\nimport {type OperatorFunction, pipe, scan, tap} from 'rxjs'\n\nimport {importer, type State} from '../../actions/media/importMedia.js'\nimport {importMediaDebug} from '../../actions/media/importMediaDebug.js'\nimport {ingestMediaAssetFromUrlWithProgress} from '../../actions/media/ingestMediaAssetFromUrlWithProgress.js'\nimport {promptForMediaLibrary} from '../../prompts/promptForMediaLibrary.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {getMediaLibraries} from '../../services/mediaLibraries.js'\nimport {getAssetFilenameError, getIngestUrlError} from '../../util/assetSourceValidation.js'\nimport {getAssetUploadErrorMessage} from '../../util/assetUploadErrors.js'\nimport {isRemoteAssetSource} from '../../util/isRemoteAssetSource.js'\nimport {parseAspectFlags} from '../../util/parseAspectFlags.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst flags = {\n ...getProjectIdFlag({\n description: 'Project ID to import media to',\n semantics: 'override',\n }),\n aspect: Flags.string({\n description:\n 'Aspect value to set on the imported asset, as key=value. Repeatable. Only applies when the source is a URL - directory and archive imports read aspects from their data.ndjson',\n helpValue: '<key=value>',\n multiple: true,\n }),\n filename: Flags.string({\n description:\n 'Original filename to store on the asset. Only applies when the source is a URL. Defaults to a name derived from the URL',\n helpValue: '<filename>',\n }),\n 'media-library-id': requiredWhenUnattended(\n Flags.string({\n description: 'The id of the target media library',\n }),\n ),\n 'replace-aspects': Flags.boolean({\n description:\n 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data). Only applies to directory and archive imports',\n }),\n} satisfies FlagInput\n\nexport class MediaImportCommand extends SanityCommand<typeof MediaImportCommand> {\n static override args = {\n source: Args.string({\n description:\n 'Directory, archive, or asset URL to import from. An https URL imports one asset that Sanity fetches itself',\n required: true,\n }),\n }\n\n static override description = 'Import a set of assets to the target media library.'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> products',\n description: 'Import all assets from the \"products\" directory',\n },\n {\n command: '<%= config.bin %> <%= command.id %> gallery.tar.gz',\n description: 'Import all assets from \"gallery\" archive',\n },\n {\n command: '<%= config.bin %> <%= command.id %> products --replace-aspects',\n description: 'Import all assets from the \"products\" directory and replace aspects',\n },\n {\n command: '<%= config.bin %> <%= command.id %> https://example.com/hero.png',\n description: 'Have Sanity fetch a single asset from a public URL',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> https://example.com/hero.png --aspect department=Brand',\n description: 'Fetch an asset from a URL and set aspect data on it',\n },\n ]\n\n static override flags = flags\n\n static telemetry = defineCommandTelemetry(flags, {\n redact: ['aspect', 'filename'],\n })\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(MediaImportCommand)\n const {source} = args\n const replaceAspects = flags['replace-aspects']\n const isUrlSource = isRemoteAssetSource(source)\n\n const aspects = this.resolveAspects({aspectFlags: flags.aspect, isUrlSource})\n\n if (!isUrlSource && flags.filename !== undefined) {\n this.error(\n 'The --filename flag only applies when importing from a URL. Directory and archive imports keep each asset’s own filename.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n if (isUrlSource && replaceAspects) {\n this.error(\n 'The --replace-aspects flag only applies to directory and archive imports. Aspects passed with --aspect are set when the asset is created.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const filenameError =\n flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename)\n if (filenameError) {\n this.error(filenameError, {exit: exitCodes.USAGE_ERROR})\n }\n\n const sourceUrlError = isUrlSource ? getIngestUrlError(source) : undefined\n if (sourceUrlError) {\n this.error(sourceUrlError, {exit: exitCodes.USAGE_ERROR})\n }\n\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n\n const cliConfig = await this.tryGetCliConfig()\n const dataset = cliConfig.api?.dataset\n\n let mediaLibraries\n try {\n mediaLibraries = await getMediaLibraries(projectId)\n } catch (error) {\n importMediaDebug('Error listing media libraries', error)\n this.error(\n `Failed to list media libraries:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n\n if (mediaLibraries.length === 0) {\n this.error('No active media libraries found in this project', {exit: exitCodes.RUNTIME_ERROR})\n }\n\n let mediaLibraryId = flags['media-library-id']\n if (!mediaLibraryId) {\n try {\n mediaLibraryId = await promptForMediaLibrary({mediaLibraries})\n } catch (error) {\n importMediaDebug('Error selecting media library', error)\n this.error(\n `Failed to select media library:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n }\n\n if (!mediaLibraries.some((library) => library.id === mediaLibraryId)) {\n this.error(`Media library with id \"${mediaLibraryId}\" not found`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n if (isUrlSource) {\n await this.importFromUrl({\n ...(aspects ? {aspects} : {}),\n ...(flags.filename ? {filename: flags.filename} : {}),\n mediaLibraryId,\n url: source,\n })\n return\n }\n\n const projectClient = await getProjectCliClient({\n apiVersion: 'v2025-02-19',\n dataset,\n perspective: 'drafts',\n projectId,\n requestTagPrefix: 'sanity.mediaLibraryCli.import',\n requireUser: true,\n '~experimental_resource': {\n id: mediaLibraryId,\n type: 'media-library',\n },\n })\n\n this.log(\n boxen(\n `\n Importing to media library: ${mediaLibraryId.padEnd(37)}\n Importing from path: ${source}\n `,\n {\n borderColor: 'yellow',\n borderStyle: 'round',\n },\n ),\n )\n\n const spin = spinner('Beginning import…').start()\n\n await this.importAssets({projectClient, replaceAspects, source, spin})\n }\n\n private async importAssets(options: {\n projectClient: SanityClient\n replaceAspects: boolean\n source: string\n spin: ReturnType<typeof spinner>\n }): Promise<void> {\n const {projectClient, replaceAspects, source, spin} = options\n\n return new Promise<void>((resolve, reject) => {\n const subscription = importer({\n client: projectClient,\n replaceAspects,\n sourcePath: source,\n spinner: spin,\n })\n .pipe(this.reportResult(spin))\n .subscribe({\n complete: () => {\n resolve()\n },\n error: (error: unknown) => {\n const message = error instanceof Error ? error.message : String(error)\n spin.stop()\n reject(new Error(message))\n },\n })\n\n // Cleanup on Ctrl+C\n process.once('SIGINT', () => {\n subscription.unsubscribe()\n spin.fail('Import interrupted.')\n process.exit(exitCodes.SIGINT)\n })\n }).catch((error) => {\n this.error(styleText('red', error.message), {exit: exitCodes.RUNTIME_ERROR})\n })\n }\n\n /**\n * Import one asset that Sanity fetches from a URL itself.\n *\n * Nothing but the resulting documents goes to stdout, so the output stays\n * consumable as JSON; the wait is reported on stderr by the spinner.\n */\n private async importFromUrl(options: {\n aspects?: Record<string, string>\n filename?: string\n\n mediaLibraryId: string\n url: string\n }): Promise<void> {\n const {aspects, filename, mediaLibraryId, url} = options\n\n let result\n try {\n result = await ingestMediaAssetFromUrlWithProgress({\n ...(aspects ? {aspects} : {}),\n ...(filename ? {filename} : {}),\n isInteractive: this.resolveIsInteractive(),\n logToStderr: (message) => this.logToStderr(message),\n mediaLibraryId,\n url,\n })\n } catch (error) {\n if (error instanceof Error && error.message === 'SIGINT') throw error\n importMediaDebug('Error importing asset from URL', error)\n this.error(getAssetUploadErrorMessage(error, {fromUrl: true, target: 'media-library'}), {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n this.log(\n JSON.stringify(\n {\n asset: {\n _id: result.asset._id,\n _type: result.asset._type,\n assetType: result.asset.assetType,\n },\n assetInstance: {\n _id: result.assetInstance._id,\n extension: result.assetInstance.extension,\n mimeType: result.assetInstance.mimeType,\n originalFilename: result.assetInstance.originalFilename,\n size: result.assetInstance.size,\n url: result.assetInstance.url,\n },\n },\n null,\n 2,\n ),\n )\n }\n\n private reportResult(\n spin: ReturnType<typeof spinner>,\n ): OperatorFunction<State, [number, State | undefined]> {\n let previousState: State | undefined\n\n return pipe(\n scan<State, [number, State | undefined]>(\n (processedAssetsCount, state) => [processedAssetsCount[0] + 1, state],\n [0, undefined],\n ),\n tap({\n complete: () => spin.succeed(`Imported ${previousState?.fileCount} assets`),\n next: ([processedAssetsCount, state]) => {\n previousState = state\n spin.text = `${processedAssetsCount} of ${state?.fileCount} assets imported ${styleText('dim', state?.asset.originalFilename ?? '')}`\n },\n }),\n )\n }\n\n /**\n * Validate `--aspect` against the source kind and shape it for the request.\n * Runs before any network call so a malformed flag fails immediately.\n */\n private resolveAspects(options: {\n aspectFlags?: string[]\n\n isUrlSource: boolean\n }): Record<string, string> | undefined {\n const {aspectFlags, isUrlSource} = options\n if (!aspectFlags?.length) return undefined\n\n if (!isUrlSource) {\n this.error(\n 'The --aspect flag only applies when importing from a URL. Directory and archive imports read aspect data from their data.ndjson file.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const parsed = parseAspectFlags(aspectFlags)\n if ('error' in parsed) {\n this.error(parsed.error, {exit: exitCodes.USAGE_ERROR})\n }\n\n return parsed.aspects\n }\n}\n"],"names":["styleText","Args","Flags","exitCodes","getProjectCliClient","SanityCommand","requiredWhenUnattended","boxen","spinner","pipe","scan","tap","importer","importMediaDebug","ingestMediaAssetFromUrlWithProgress","promptForMediaLibrary","promptForProject","getMediaLibraries","getAssetFilenameError","getIngestUrlError","getAssetUploadErrorMessage","isRemoteAssetSource","parseAspectFlags","getProjectIdFlag","defineCommandTelemetry","flags","description","semantics","aspect","string","helpValue","multiple","filename","boolean","MediaImportCommand","args","source","required","examples","command","telemetry","redact","run","parse","replaceAspects","isUrlSource","aspects","resolveAspects","aspectFlags","undefined","error","exit","USAGE_ERROR","filenameError","sourceUrlError","projectId","getProjectId","fallback","cliConfig","tryGetCliConfig","dataset","api","mediaLibraries","Error","message","RUNTIME_ERROR","length","mediaLibraryId","some","library","id","importFromUrl","url","projectClient","apiVersion","perspective","requestTagPrefix","requireUser","type","log","padEnd","borderColor","borderStyle","spin","start","importAssets","options","Promise","resolve","reject","subscription","client","sourcePath","reportResult","subscribe","complete","String","stop","process","once","unsubscribe","fail","SIGINT","catch","result","isInteractive","resolveIsInteractive","logToStderr","fromUrl","target","JSON","stringify","asset","_id","_type","assetType","assetInstance","extension","mimeType","originalFilename","size","previousState","processedAssetsCount","state","succeed","fileCount","next","text","parsed"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/index.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/index.ts"],"sourcesContent":["export {commandPolicies, resolveCommandPolicies} from './resolve.js'\n"],"names":["commandPolicies","resolveCommandPolicies"],"mappings":"AAAA,SAAQA,eAAe,EAAEC,sBAAsB,QAAO,eAAc"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { allow, conditionalDenyFlags, conditionalPolicy, deny } from '
|
|
1
|
+
import { allow, conditionalDenyFlags, conditionalPolicy, deny } from '@sanity/cli-core/commandPolicy';
|
|
2
|
+
import { isRemoteAssetSource } from '../../../util/isRemoteAssetSource.js';
|
|
2
3
|
function apiValidator({ args, flags }) {
|
|
3
4
|
const fields = Array.isArray(flags.field) ? flags.field : [];
|
|
4
5
|
const fieldReadsFromHost = fields.some((field)=>{
|
|
@@ -26,10 +27,14 @@ function apiValidator({ args, flags }) {
|
|
|
26
27
|
* a usage error, but cannot cause local filesystem access. Destructive remote
|
|
27
28
|
* operations are allowed and do not by themselves make a command unsafe.
|
|
28
29
|
*
|
|
29
|
-
* Every
|
|
30
|
+
* Every command this package contributes must have exactly one policy here:
|
|
30
31
|
* - allow: every valid invocation is safe
|
|
31
32
|
* - conditional: safety depends on parsed arguments or flags
|
|
32
33
|
* - deny: no invocation is safe
|
|
34
|
+
*
|
|
35
|
+
* Commands contributed by plugins are not listed here. Each plugin declares
|
|
36
|
+
* policies for its own commands (see `PluginInvocationPolicies` in
|
|
37
|
+
* `@sanity/cli-core/commandPolicy`).
|
|
33
38
|
*/ export const mcpPolicy = {
|
|
34
39
|
// Special exception, this can be very dangerous but is also super useful
|
|
35
40
|
// to expose. Refuse authentication overrides and host input channels:
|
|
@@ -44,8 +49,14 @@ function apiValidator({ args, flags }) {
|
|
|
44
49
|
],
|
|
45
50
|
validate: apiValidator
|
|
46
51
|
}),
|
|
47
|
-
//
|
|
48
|
-
'assets:upload':
|
|
52
|
+
// --file reads from the machine running the command and must only run locally.
|
|
53
|
+
'assets:upload': conditionalPolicy({
|
|
54
|
+
deniedFlags: [
|
|
55
|
+
'content-type',
|
|
56
|
+
'file'
|
|
57
|
+
],
|
|
58
|
+
validate: ({ flags })=>typeof flags['from-url'] === 'string' && flags['from-url'].length > 0
|
|
59
|
+
}),
|
|
49
60
|
'backups:disable': allow,
|
|
50
61
|
// Writes a downloaded backup to the local filesystem.
|
|
51
62
|
'backups:download': deny,
|
|
@@ -148,8 +159,17 @@ function apiValidator({ args, flags }) {
|
|
|
148
159
|
'media:deploy-aspect': deny,
|
|
149
160
|
// Writes media assets to the local filesystem.
|
|
150
161
|
'media:export': deny,
|
|
151
|
-
//
|
|
152
|
-
|
|
162
|
+
// A directory or archive source reads media assets from the local
|
|
163
|
+
// filesystem. A URL source does not: Sanity fetches the asset itself, so the
|
|
164
|
+
// only local input is the URL. `--replace-aspects` is meaningless for a URL
|
|
165
|
+
// source (the command rejects the combination), so it is hidden here rather
|
|
166
|
+
// than advertised as usable surface.
|
|
167
|
+
'media:import': conditionalPolicy({
|
|
168
|
+
deniedFlags: [
|
|
169
|
+
'replace-aspects'
|
|
170
|
+
],
|
|
171
|
+
validate: ({ args })=>typeof args.source === 'string' && isRemoteAssetSource(args.source)
|
|
172
|
+
}),
|
|
153
173
|
// Creates migration source files in the local project.
|
|
154
174
|
'migrations:create': deny,
|
|
155
175
|
// Reads and loads migration definitions from the local project.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/mcpPolicy.ts"],"sourcesContent":["import {\n allow,\n type CommandPolicySet,\n conditionalDenyFlags,\n conditionalPolicy,\n deny,\n} from './policy.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every manifest command must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n // Reads a file from the machine running the command and must only run locally.\n 'assets:upload': deny,\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n // --watch polls a job with no deadline; a stuck job would hang the MCP tool\n // call indefinitely. Non-watch invocations are single remote requests.\n 'context:build': conditionalDenyFlags('watch'),\n 'context:create': allow,\n 'context:delete': allow,\n 'context:get': allow,\n // --file reads the import payload from the local filesystem; text, URL and\n // dataset imports are remote-only.\n 'context:imports:create': conditionalDenyFlags('file'),\n 'context:imports:delete': allow,\n 'context:imports:download': allow,\n 'context:imports:get': allow,\n 'context:imports:list': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:jobs:get': conditionalDenyFlags('watch'),\n 'context:list': allow,\n 'context:refresh': allow,\n 'context:update': allow,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // Reads media assets from the local filesystem.\n 'media:import': deny,\n\n // Creates migration source files in the local project.\n 'migrations:create': deny,\n // Reads and loads migration definitions from the local project.\n 'migrations:list': deny,\n // Executes local migration code that may perform arbitrary document mutations.\n 'migrations:run': deny,\n\n // Writes project setup values and scaffolds local applications.\n new: deny,\n\n // --web opens a browser on the machine running the MCP server.\n 'openapi:get': conditionalDenyFlags('web'),\n 'openapi:list': conditionalDenyFlags('web'),\n\n 'organizations:create': allow,\n 'organizations:delete': allow,\n 'organizations:get': allow,\n 'organizations:list': allow,\n 'organizations:update': allow,\n\n // Serves a local production build.\n preview: deny,\n\n 'projects:create': allow,\n 'projects:list': allow,\n // Reads locally stored claim URLs and robot tokens.\n 'projects:unclaimed': deny,\n\n // Loads the local Studio configuration to resolve the datasets containing each schema.\n 'schemas:delete': deny,\n // Loads local schema files before deploying them.\n 'schemas:deploy': deny,\n // Loads local Studio configuration and writes an extracted schema to disk.\n 'schemas:extract': deny,\n // Requires a local project and loads its schema configuration.\n 'schemas:list': deny,\n // Loads and executes a local Studio schema.\n 'schemas:validate': deny,\n\n // Installs skills into local editor configuration directories.\n 'skills:install': deny,\n\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:disable': deny,\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:enable': deny,\n 'telemetry:status': allow,\n\n // Creates authentication credentials.\n 'tokens:create': deny,\n // Deletes authentication credentials.\n 'tokens:delete': deny,\n // Exposes authentication credential metadata.\n 'tokens:list': deny,\n // Replaces authentication credentials and exposes the new secret.\n 'tokens:rotate': deny,\n\n // Reads the local schema and source files and writes generated types to the project.\n 'typegen:generate': deny,\n\n // Loads local CLI and workbench configuration to identify the deployed Studio or application.\n undeploy: deny,\n\n // Grants a user access to a project.\n 'users:invite': deny,\n 'users:list': allow,\n\n // Reads the local project and installed package tree.\n versions: deny,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","new","preview","undeploy","versions"],"mappings":"AAAA,SACEA,KAAK,EAELC,oBAAoB,EACpBC,iBAAiB,EACjBC,IAAI,QACC,cAAa;AAEpB,SAASC,aAAa,EACpBC,IAAI,EACJC,KAAK,EAIN;IACC,MAAMC,SAAoBC,MAAMC,OAAO,CAACH,MAAMI,KAAK,IAAIJ,MAAMI,KAAK,GAAG,EAAE;IAEvE,MAAMC,qBAAqBJ,OAAOK,IAAI,CAAC,CAACF;QACtC,IAAI,OAAOA,UAAU,UAAU,OAAO;QACtC,MAAMG,iBAAiBH,MAAMI,OAAO,CAAC;QACrC,OAAOD,iBAAiB,KAAKH,KAAK,CAACG,iBAAiB,EAAE,KAAK;IAC7D;IACA,IAAIF,oBAAoB,OAAO;IAE/B,MAAMI,UAAqBP,MAAMC,OAAO,CAACH,MAAMU,MAAM,IAAIV,MAAMU,MAAM,GAAG,EAAE;IAE1E,MAAMC,gCAAgCF,QAAQH,IAAI,CAAC,CAACI;QAClD,IAAI,OAAOA,WAAW,UAAU,OAAO;QACvC,MAAMH,iBAAiBG,OAAOF,OAAO,CAAC;QACtC,IAAID,kBAAkB,GAAG,OAAO;QAEhC,MAAMK,OAAOF,OAAOG,KAAK,CAAC,GAAGN,gBAAgBO,IAAI,GAAGC,WAAW;QAC/D,OAAOH,SAAS,mBAAmBA,SAAS;IAC9C;IAEA,IAAID,+BAA+B,OAAO;IAE1C,MAAMK,WAAW,OAAOjB,KAAKiB,QAAQ,KAAK,WAAWC,IAAIC,KAAK,CAACnB,KAAKiB,QAAQ,IAAI;IAChF,MAAMG,uBACJH,aAAa,QAASA,CAAAA,SAASI,QAAQ,KAAK,MAAMJ,SAASK,QAAQ,KAAK,EAAC;IAE3E,OAAO,CAACF;AACV;AAEA;;;;;;;;;;CAUC,GACD,OAAO,MAAMG,YAA8B;IACzC,yEAAyE;IACzE,sEAAsE;IACtE,sFAAsF;IACtF,iEAAiE;IACjE,4EAA4E;IAC5E,2DAA2D;IAC3DC,KAAK3B,kBAAkB;QACrB4B,aAAa;YAAC;YAAS;SAAQ;QAC/BC,UAAU3B;IACZ;IAEA,+EAA+E;IAC/E,iBAAiBD;IAEjB,mBAAmBH;IACnB,sDAAsD;IACtD,oBAAoBG;IACpB,kBAAkBH;IAClB,gBAAgBA;IAEhB,mEAAmE;IACnEgC,OAAO7B;IAEP,wCAAwC;IACxC8B,SAAS9B;IAET,4EAA4E;IAC5E,uEAAuE;IACvE,iBAAiBF,qBAAqB;IACtC,kBAAkBD;IAClB,kBAAkBA;IAClB,eAAeA;IACf,2EAA2E;IAC3E,mCAAmC;IACnC,0BAA0BC,qBAAqB;IAC/C,0BAA0BD;IAC1B,4BAA4BA;IAC5B,uBAAuBA;IACvB,wBAAwBA;IACxB,gFAAgF;IAChF,oBAAoBC,qBAAqB;IACzC,gBAAgBD;IAChB,mBAAmBA;IACnB,kBAAkBA;IAElB,YAAYA;IACZ,eAAeA;IACf,aAAaA;IAEb,yBAAyBA;IACzB,yBAAyBA;IACzB,uBAAuBA;IACvB,yBAAyBA;IACzB,iBAAiBA;IACjB,mBAAmBA;IACnB,mBAAmBA;IACnB,+BAA+BA;IAC/B,8BAA8BA;IAC9B,8BAA8BA;IAC9B,8DAA8D;IAC9D,mBAAmBG;IACnB,yEAAyE;IACzE,mBAAmBA;IACnB,iBAAiBH;IACjB,2BAA2BA;IAC3B,2BAA2BA;IAE3B,qEAAqE;IACrEkC,OAAO/B;IAEP,qDAAqD;IACrDgC,QAAQhC;IAER,gEAAgE;IAChEiC,KAAKjC;IAEL,yDAAyD;IACzD,eAAeA;IACf,+DAA+D;IAC/D,aAAaF,qBAAqB;IAClC,eAAeD;IAEf,kEAAkE;IAClEqC,QAAQlC;IAER,6DAA6D;IAC7D,oBAAoBA;IACpB,oBAAoBH;IACpB,iBAAiBA;IACjB,mBAAmBA;IACnB,qDAAqD;IACrD,sBAAsBG;IAEtB,uDAAuD;IACvDmC,MAAMnC;IAEN,kDAAkD;IAClD,kBAAkBA;IAClB,gBAAgBH;IAChB,mGAAmG;IACnG,oBAAoBC,qBAAqB;IAEzC,iBAAiBD;IACjB,gBAAgBA;IAChB,gBAAgBA;IAChB,cAAcA;IACd,cAAcA;IAEd,oEAAoE;IACpEuC,MAAMpC;IAEN,4CAA4C;IAC5CqC,SAASrC;IAET,yDAAyD;IACzDsC,OAAOtC;IAEP,mCAAmC;IACnCuC,OAAOvC;IAEP,qEAAqE;IACrEwC,QAAQxC;IAER,yDAAyD;IACzDyC,QAAQzC;IAER,8DAA8D;IAC9D,oBAAoBA;IAEpB,mDAAmD;IACnD,iBAAiBA;IAEjB,uDAAuD;IACvD,uBAAuBA;IACvB,uBAAuBH;IACvB,uDAAuD;IACvD,uBAAuBG;IACvB,+CAA+C;IAC/C,gBAAgBA;IAChB,gDAAgD;IAChD,gBAAgBA;IAEhB,uDAAuD;IACvD,qBAAqBA;IACrB,gEAAgE;IAChE,mBAAmBA;IACnB,+EAA+E;IAC/E,kBAAkBA;IAElB,gEAAgE;IAChE0C,KAAK1C;IAEL,+DAA+D;IAC/D,eAAeF,qBAAqB;IACpC,gBAAgBA,qBAAqB;IAErC,wBAAwBD;IACxB,wBAAwBA;IACxB,qBAAqBA;IACrB,sBAAsBA;IACtB,wBAAwBA;IAExB,mCAAmC;IACnC8C,SAAS3C;IAET,mBAAmBH;IACnB,iBAAiBA;IACjB,oDAAoD;IACpD,sBAAsBG;IAEtB,uFAAuF;IACvF,kBAAkBA;IAClB,kDAAkD;IAClD,kBAAkBA;IAClB,2EAA2E;IAC3E,mBAAmBA;IACnB,+DAA+D;IAC/D,gBAAgBA;IAChB,4CAA4C;IAC5C,oBAAoBA;IAEpB,+DAA+D;IAC/D,kBAAkBA;IAElB,gFAAgF;IAChF,qBAAqBA;IACrB,gFAAgF;IAChF,oBAAoBA;IACpB,oBAAoBH;IAEpB,sCAAsC;IACtC,iBAAiBG;IACjB,sCAAsC;IACtC,iBAAiBA;IACjB,8CAA8C;IAC9C,eAAeA;IACf,kEAAkE;IAClE,iBAAiBA;IAEjB,qFAAqF;IACrF,oBAAoBA;IAEpB,8FAA8F;IAC9F4C,UAAU5C;IAEV,qCAAqC;IACrC,gBAAgBA;IAChB,cAAcH;IAEd,sDAAsD;IACtDgD,UAAU7C;AACZ,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/mcpPolicy.ts"],"sourcesContent":["import {\n allow,\n type CommandPolicySet,\n conditionalDenyFlags,\n conditionalPolicy,\n deny,\n} from '@sanity/cli-core/commandPolicy'\n\nimport {isRemoteAssetSource} from '../../../util/isRemoteAssetSource.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every command this package contributes must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n *\n * Commands contributed by plugins are not listed here. Each plugin declares\n * policies for its own commands (see `PluginInvocationPolicies` in\n * `@sanity/cli-core/commandPolicy`).\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n // --file reads from the machine running the command and must only run locally.\n 'assets:upload': conditionalPolicy({\n deniedFlags: ['content-type', 'file'],\n validate: ({flags}) => typeof flags['from-url'] === 'string' && flags['from-url'].length > 0,\n }),\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n // --watch polls a job with no deadline; a stuck job would hang the MCP tool\n // call indefinitely. Non-watch invocations are single remote requests.\n 'context:build': conditionalDenyFlags('watch'),\n 'context:create': allow,\n 'context:delete': allow,\n 'context:get': allow,\n // --file reads the import payload from the local filesystem; text, URL and\n // dataset imports are remote-only.\n 'context:imports:create': conditionalDenyFlags('file'),\n 'context:imports:delete': allow,\n 'context:imports:download': allow,\n 'context:imports:get': allow,\n 'context:imports:list': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:jobs:get': conditionalDenyFlags('watch'),\n 'context:list': allow,\n 'context:refresh': allow,\n 'context:update': allow,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // A directory or archive source reads media assets from the local\n // filesystem. A URL source does not: Sanity fetches the asset itself, so the\n // only local input is the URL. `--replace-aspects` is meaningless for a URL\n // source (the command rejects the combination), so it is hidden here rather\n // than advertised as usable surface.\n 'media:import': conditionalPolicy({\n deniedFlags: ['replace-aspects'],\n validate: ({args}) => typeof args.source === 'string' && isRemoteAssetSource(args.source),\n }),\n\n // Creates migration source files in the local project.\n 'migrations:create': deny,\n // Reads and loads migration definitions from the local project.\n 'migrations:list': deny,\n // Executes local migration code that may perform arbitrary document mutations.\n 'migrations:run': deny,\n\n // Writes project setup values and scaffolds local applications.\n new: deny,\n\n // --web opens a browser on the machine running the MCP server.\n 'openapi:get': conditionalDenyFlags('web'),\n 'openapi:list': conditionalDenyFlags('web'),\n\n 'organizations:create': allow,\n 'organizations:delete': allow,\n 'organizations:get': allow,\n 'organizations:list': allow,\n 'organizations:update': allow,\n\n // Serves a local production build.\n preview: deny,\n\n 'projects:create': allow,\n 'projects:list': allow,\n // Reads locally stored claim URLs and robot tokens.\n 'projects:unclaimed': deny,\n\n // Loads the local Studio configuration to resolve the datasets containing each schema.\n 'schemas:delete': deny,\n // Loads local schema files before deploying them.\n 'schemas:deploy': deny,\n // Loads local Studio configuration and writes an extracted schema to disk.\n 'schemas:extract': deny,\n // Requires a local project and loads its schema configuration.\n 'schemas:list': deny,\n // Loads and executes a local Studio schema.\n 'schemas:validate': deny,\n\n // Installs skills into local editor configuration directories.\n 'skills:install': deny,\n\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:disable': deny,\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:enable': deny,\n 'telemetry:status': allow,\n\n // Creates authentication credentials.\n 'tokens:create': deny,\n // Deletes authentication credentials.\n 'tokens:delete': deny,\n // Exposes authentication credential metadata.\n 'tokens:list': deny,\n // Replaces authentication credentials and exposes the new secret.\n 'tokens:rotate': deny,\n\n // Reads the local schema and source files and writes generated types to the project.\n 'typegen:generate': deny,\n\n // Loads local CLI and workbench configuration to identify the deployed Studio or application.\n undeploy: deny,\n\n // Grants a user access to a project.\n 'users:invite': deny,\n 'users:list': allow,\n\n // Reads the local project and installed package tree.\n versions: deny,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","isRemoteAssetSource","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","length","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","source","new","preview","undeploy","versions"],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the effective policy table for one invocation source, combining what
|
|
3
|
+
* this CLI declares for its own commands with what each plugin declares for
|
|
4
|
+
* the commands it contributes.
|
|
5
|
+
*
|
|
6
|
+
* The table is deny-by-default in both directions: a command with no entry is
|
|
7
|
+
* denied by the caller (`policySet[id] ?? deny`), and every step here that
|
|
8
|
+
* cannot establish permission simply omits an entry rather than falling back
|
|
9
|
+
* to something permissive.
|
|
10
|
+
*/ import { resolve } from 'node:path';
|
|
11
|
+
import { isCommandPolicySet } from '@sanity/cli-core/commandPolicy';
|
|
12
|
+
import { subdebug } from '@sanity/cli-core/debug';
|
|
13
|
+
import { doImport } from '@sanity/cli-core/util';
|
|
14
|
+
import { mcpPolicy } from './mcpPolicy.js';
|
|
15
|
+
const debug = subdebug('commandPolicies');
|
|
16
|
+
/** Policies this CLI declares for its own commands, by invocation source. */ export const commandPolicies = {
|
|
17
|
+
mcp: mcpPolicy
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolution reads plugin package.json files and imports their policy
|
|
21
|
+
* modules, so it is async and depends on the loaded oclif config. Memoized
|
|
22
|
+
* per config so that work happens once per process rather than per invocation.
|
|
23
|
+
*/ const cache = new WeakMap();
|
|
24
|
+
export function resolveCommandPolicies(config, source) {
|
|
25
|
+
let bySource = cache.get(config);
|
|
26
|
+
if (!bySource) {
|
|
27
|
+
bySource = new Map();
|
|
28
|
+
cache.set(config, bySource);
|
|
29
|
+
}
|
|
30
|
+
let resolved = bySource.get(source);
|
|
31
|
+
if (!resolved) {
|
|
32
|
+
resolved = buildCommandPolicies(config, source);
|
|
33
|
+
bySource.set(source, resolved);
|
|
34
|
+
}
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
async function buildCommandPolicies(config, source) {
|
|
38
|
+
const hostName = config.pjson.name;
|
|
39
|
+
const entries = {};
|
|
40
|
+
for (const [id, policy] of Object.entries(commandPolicies[source])){
|
|
41
|
+
entries[id] = {
|
|
42
|
+
...policy,
|
|
43
|
+
declaredBy: hostName
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
for (const plugin of config.plugins.values()){
|
|
47
|
+
if (plugin.name === hostName) continue;
|
|
48
|
+
// Only plugins this CLI declares itself should contribute commands.
|
|
49
|
+
// oclif loads plugins recursively, so a dependency of a plugin also lands here.
|
|
50
|
+
if (plugin.parent) {
|
|
51
|
+
debug('ignoring %s, contributed by %s rather than declared here', plugin.name, plugin.parent.name);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const declared = await loadPluginPolicies(plugin, source);
|
|
55
|
+
if (!declared) continue;
|
|
56
|
+
// Plugin policy can only be set for commands they actually contributed.
|
|
57
|
+
const contributed = new Set(plugin.commands.map((command)=>command.id));
|
|
58
|
+
for (const [id, policy] of Object.entries(declared)){
|
|
59
|
+
if (!contributed.has(id)) {
|
|
60
|
+
debug('%s declared a policy for %s, which it does not contribute', plugin.name, id);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Ids already claimed keep their existing declaration, mirroring
|
|
64
|
+
// oclif's own first-one-wins command resolution.
|
|
65
|
+
if (entries[id]) continue;
|
|
66
|
+
entries[id] = {
|
|
67
|
+
...policy,
|
|
68
|
+
declaredBy: plugin.name
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load the policy table a plugin declares for one invocation source, or
|
|
76
|
+
* `undefined` when it declares none, points at a module that cannot be
|
|
77
|
+
* loaded, or exports something that is not a policy table.
|
|
78
|
+
*
|
|
79
|
+
* Every failure is silent and non-fatal by design. A plugin's policy module
|
|
80
|
+
* is third-party code resolved at runtime: it must not be able to take down
|
|
81
|
+
* the whole invocation surface by throwing, and it must not be able to reach
|
|
82
|
+
* a permissive outcome by being malformed. Returning `undefined` leaves the
|
|
83
|
+
* plugin's commands uncategorized, which the caller treats as denied.
|
|
84
|
+
*/ async function loadPluginPolicies(plugin, source) {
|
|
85
|
+
const declaredPath = readInvocationPoliciesPath(plugin);
|
|
86
|
+
if (!declaredPath) return undefined;
|
|
87
|
+
try {
|
|
88
|
+
const loaded = await doImport(resolve(plugin.root, declaredPath));
|
|
89
|
+
const policies = loaded.invocationPolicies;
|
|
90
|
+
if (typeof policies !== 'object' || policies === null) {
|
|
91
|
+
debug('%s policy module has no usable `invocationPolicies` export', plugin.name);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const forSource = policies[source];
|
|
95
|
+
if (forSource === undefined) return undefined;
|
|
96
|
+
if (!isCommandPolicySet(forSource)) {
|
|
97
|
+
debug('%s declared a malformed policy table for %s', plugin.name, source);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return forSource;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
debug('failed to load policy module for %s: %s', plugin.name, err);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function readInvocationPoliciesPath(plugin) {
|
|
107
|
+
const { sanity } = plugin.pjson;
|
|
108
|
+
const declared = sanity?.invocationPolicies;
|
|
109
|
+
return typeof declared === 'string' && declared !== '' ? declared : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/resolve.ts"],"sourcesContent":["/**\n * Builds the effective policy table for one invocation source, combining what\n * this CLI declares for its own commands with what each plugin declares for\n * the commands it contributes.\n *\n * The table is deny-by-default in both directions: a command with no entry is\n * denied by the caller (`policySet[id] ?? deny`), and every step here that\n * cannot establish permission simply omits an entry rather than falling back\n * to something permissive.\n */\nimport {resolve} from 'node:path'\n\nimport {type Config, type Interfaces} from '@oclif/core'\nimport {\n type CommandPolicy,\n type CommandPolicySet,\n type InvocationSource,\n isCommandPolicySet,\n} from '@sanity/cli-core/commandPolicy'\nimport {subdebug} from '@sanity/cli-core/debug'\nimport {doImport} from '@sanity/cli-core/util'\n\nimport {mcpPolicy} from './mcpPolicy.js'\n\nconst debug = subdebug('commandPolicies')\n\n/** Policies this CLI declares for its own commands, by invocation source. */\nexport const commandPolicies: Record<InvocationSource, CommandPolicySet> = {\n mcp: mcpPolicy,\n}\n\n/**\n * Policy entries annotated with the package that declared each one.\n */\nexport type ResolvedCommandPolicySet = Readonly<\n Record<string, CommandPolicy & {declaredBy: string}>\n>\n\ntype ResolvedCommandPolicy = ResolvedCommandPolicySet[string]\n\n/**\n * Resolution reads plugin package.json files and imports their policy\n * modules, so it is async and depends on the loaded oclif config. Memoized\n * per config so that work happens once per process rather than per invocation.\n */\nconst cache = new WeakMap<Config, Map<InvocationSource, Promise<ResolvedCommandPolicySet>>>()\n\nexport function resolveCommandPolicies(\n config: Config,\n source: InvocationSource,\n): Promise<ResolvedCommandPolicySet> {\n let bySource = cache.get(config)\n if (!bySource) {\n bySource = new Map()\n cache.set(config, bySource)\n }\n\n let resolved = bySource.get(source)\n if (!resolved) {\n resolved = buildCommandPolicies(config, source)\n bySource.set(source, resolved)\n }\n return resolved\n}\n\nasync function buildCommandPolicies(\n config: Config,\n source: InvocationSource,\n): Promise<ResolvedCommandPolicySet> {\n const hostName = config.pjson.name\n const entries: Record<string, ResolvedCommandPolicy> = {}\n\n for (const [id, policy] of Object.entries(commandPolicies[source])) {\n entries[id] = {...policy, declaredBy: hostName}\n }\n\n for (const plugin of config.plugins.values()) {\n if (plugin.name === hostName) continue\n\n // Only plugins this CLI declares itself should contribute commands.\n // oclif loads plugins recursively, so a dependency of a plugin also lands here.\n if (plugin.parent) {\n debug(\n 'ignoring %s, contributed by %s rather than declared here',\n plugin.name,\n plugin.parent.name,\n )\n continue\n }\n\n const declared = await loadPluginPolicies(plugin, source)\n if (!declared) continue\n\n // Plugin policy can only be set for commands they actually contributed.\n const contributed = new Set(plugin.commands.map((command) => command.id))\n\n for (const [id, policy] of Object.entries(declared)) {\n if (!contributed.has(id)) {\n debug('%s declared a policy for %s, which it does not contribute', plugin.name, id)\n continue\n }\n // Ids already claimed keep their existing declaration, mirroring\n // oclif's own first-one-wins command resolution.\n if (entries[id]) continue\n\n entries[id] = {...policy, declaredBy: plugin.name}\n }\n }\n\n return entries\n}\n\n/**\n * Load the policy table a plugin declares for one invocation source, or\n * `undefined` when it declares none, points at a module that cannot be\n * loaded, or exports something that is not a policy table.\n *\n * Every failure is silent and non-fatal by design. A plugin's policy module\n * is third-party code resolved at runtime: it must not be able to take down\n * the whole invocation surface by throwing, and it must not be able to reach\n * a permissive outcome by being malformed. Returning `undefined` leaves the\n * plugin's commands uncategorized, which the caller treats as denied.\n */\nasync function loadPluginPolicies(\n plugin: Interfaces.Plugin,\n source: InvocationSource,\n): Promise<CommandPolicySet | undefined> {\n const declaredPath = readInvocationPoliciesPath(plugin)\n if (!declaredPath) return undefined\n\n try {\n const loaded: unknown = await doImport(resolve(plugin.root, declaredPath))\n const policies = (loaded as {invocationPolicies?: unknown}).invocationPolicies\n\n if (typeof policies !== 'object' || policies === null) {\n debug('%s policy module has no usable `invocationPolicies` export', plugin.name)\n return undefined\n }\n\n const forSource = (policies as Record<string, unknown>)[source]\n if (forSource === undefined) return undefined\n\n if (!isCommandPolicySet(forSource)) {\n debug('%s declared a malformed policy table for %s', plugin.name, source)\n return undefined\n }\n\n return forSource\n } catch (err) {\n debug('failed to load policy module for %s: %s', plugin.name, err)\n return undefined\n }\n}\n\nfunction readInvocationPoliciesPath(plugin: Interfaces.Plugin): string | undefined {\n const {sanity} = plugin.pjson as {sanity?: {invocationPolicies?: unknown}}\n const declared = sanity?.invocationPolicies\n return typeof declared === 'string' && declared !== '' ? declared : undefined\n}\n"],"names":["resolve","isCommandPolicySet","subdebug","doImport","mcpPolicy","debug","commandPolicies","mcp","cache","WeakMap","resolveCommandPolicies","config","source","bySource","get","Map","set","resolved","buildCommandPolicies","hostName","pjson","name","entries","id","policy","Object","declaredBy","plugin","plugins","values","parent","declared","loadPluginPolicies","contributed","Set","commands","map","command","has","declaredPath","readInvocationPoliciesPath","undefined","loaded","root","policies","invocationPolicies","forSource","err","sanity"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Help } from '@oclif/core';
|
|
2
2
|
import { getHelpFlagAdditions } from '@oclif/core/help';
|
|
3
|
+
import { isConditionalInvocationPolicy } from '@sanity/cli-core/commandPolicy';
|
|
3
4
|
import { resolveTopicAliasInArgv } from '../../topicAliases.js';
|
|
4
|
-
import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
5
5
|
/** Command ids a policy exposes: entries that are not denied. */ function visibleCommandIds(policySet) {
|
|
6
6
|
return new Set(Object.entries(policySet).filter(([, policy])=>policy.kind !== 'deny').map(([id])=>id));
|
|
7
7
|
}
|
|
@@ -74,6 +74,13 @@ import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
|
74
74
|
this.commandIds = visibleCommandIds(policySet);
|
|
75
75
|
this.topicNames = visibleTopicNames(this.commandIds);
|
|
76
76
|
}
|
|
77
|
+
// Both listings start from oclif's, which already drops `hidden` entries, so
|
|
78
|
+
// help advertises the same commands a CLI user sees, minus the ones the
|
|
79
|
+
// policy denies. Hiding and denying answer different questions — "should this
|
|
80
|
+
// be advertised?" and "may this be invoked?" — and a command that is hidden
|
|
81
|
+
// but allowed stays invocable by id, exactly as it is for a CLI user who
|
|
82
|
+
// knows the name.
|
|
83
|
+
//
|
|
77
84
|
// The copies below are load-bearing: oclif's formatters rewrite ids/names
|
|
78
85
|
// in place (`cors:list` → `cors list`). Without copies those writes corrupt
|
|
79
86
|
// the shared (cached) config, breaking the policy checks on subsequent
|