@sanity/cli 8.11.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/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 +3071 -2992
- 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
|
@@ -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":"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"}
|
|
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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/exports/invokeSanityCli/help.ts"],"sourcesContent":["import {type Command, type Config, Help, type Interfaces} from '@oclif/core'\nimport {getHelpFlagAdditions} from '@oclif/core/help'\n\nimport {resolveTopicAliasInArgv} from '../../topicAliases.js'\nimport {\n type CommandPolicy,\n type CommandPolicySet,\n isConditionalInvocationPolicy,\n} from './commandPolicies/policy.js'\n\n/** Command ids a policy exposes: entries that are not denied. */\nfunction visibleCommandIds(policySet: CommandPolicySet): Set<string> {\n return new Set(\n Object.entries(policySet)\n .filter(([, policy]) => policy.kind !== 'deny')\n .map(([id]) => id),\n )\n}\n\n/** Topic names covering the visible commands: every prefix of every id. */\nfunction visibleTopicNames(commandIds: Set<string>): Set<string> {\n const names = new Set<string>()\n for (const id of commandIds) {\n const parts = id.split(':')\n for (let length = 1; length < parts.length; length++) {\n names.add(parts.slice(0, length).join(':'))\n }\n }\n return names\n}\n\n/** Thrown when help is requested for a subject outside the policy surface. */\nclass NotInvokableError extends Error {}\n\n/**\n * A copy of `command` without the policy's denied flags, and without examples\n * that use them (under any spelling: `--web`, `--w`, `-w`), so rendered help\n * only advertises invocations the policy accepts.\n */\nfunction withoutDeniedFlags(command: Command.Loadable, policy?: CommandPolicy): Command.Loadable {\n if (!policy || !isConditionalInvocationPolicy(policy)) return {...command}\n\n const denied = policy.deniedFlags\n if (denied.length === 0) return {...command}\n\n const flags = {...command.flags}\n const spellings: string[] = []\n for (const name of denied) {\n const definition = flags[name]\n if (!definition) continue\n spellings.push(`--${name}`)\n if (definition.char) spellings.push(`-${definition.char}`)\n for (const alias of definition.aliases ?? []) spellings.push(`--${alias}`, `-${alias}`)\n delete flags[name]\n }\n\n const usesDeniedFlag = (example: Command.Example) => {\n const text = typeof example === 'string' ? example : example.command\n return spellings.some((spelling) =>\n new RegExp(String.raw`(^|\\s)${spelling}(=|\\s|$)`).test(text ?? ''),\n )\n }\n\n return {\n ...command,\n examples: command.examples?.filter((example) => !usesDeniedFlag(example)),\n flags,\n }\n}\n\n/**\n * oclif help renderer scoped to a policy's command surface. Subject\n * resolution (root vs topic vs command help) is oclif's own; the policy is\n * enforced at the rendering entry points so the rest of the CLI stays\n * invisible to embedding hosts, and listings are filtered to non-denied\n * commands and their topics only.\n *\n * Output is collected in {@link InvokableHelp.lines} instead of being written\n * to the process streams.\n */\nclass InvokableHelp extends Help {\n public commandId: string | undefined\n public readonly lines: string[] = []\n\n private readonly commandIds: Set<string>\n private readonly policySet: CommandPolicySet\n private readonly topicNames: Set<string>\n\n constructor(config: Config, policySet: CommandPolicySet) {\n super(config, {stripAnsi: true})\n this.policySet = policySet\n this.commandIds = visibleCommandIds(policySet)\n this.topicNames = visibleTopicNames(this.commandIds)\n }\n\n // The copies below are load-bearing: oclif's formatters rewrite ids/names\n // in place (`cors:list` → `cors list`). Without copies those writes corrupt\n // the shared (cached) config, breaking the policy checks on subsequent\n // invocations.\n\n protected override get sortedCommands(): Command.Loadable[] {\n return super.sortedCommands\n .filter((command) => this.commandIds.has(command.id))\n .map((command) => ({...command}))\n }\n\n protected override get sortedTopics(): Interfaces.Topic[] {\n // Topic names and descriptions come from the oclif config (oclif.config.js)\n return super.sortedTopics\n .filter((topic) => this.topicNames.has(topic.name))\n .map((topic) => ({...topic}))\n }\n\n protected override log(...args: string[]): void {\n this.lines.push(...args)\n }\n\n public override async showCommandHelp(command: Command.Loadable): Promise<void> {\n if (!this.commandIds.has(command.id)) throw new NotInvokableError(command.id)\n this.commandId = command.id\n return super.showCommandHelp(withoutDeniedFlags(command, this.policySet[command.id]))\n }\n\n public override async showHelp(argv: string[]): Promise<void> {\n return super.showHelp(resolveTopicAliasInArgv(argv))\n }\n\n protected override async showTopicHelp(topic: Interfaces.Topic): Promise<void> {\n if (!this.topicNames.has(topic.name)) throw new NotInvokableError(topic.name)\n return super.showTopicHelp({...topic})\n }\n}\n\n/**\n * Render help text for a policy's command surface: root help for a bare help\n * request, or topic/command help when `argv` names a subject (e.g.\n * `['cors', '--help']`), exactly as oclif would resolve it. ANSI styling is\n * stripped so programmatic callers get plain text.\n *\n * Returns `undefined` when the subject is unknown or denied — callers should\n * respond the same way as for an unknown command, so hosts cannot probe the\n * full CLI surface through help.\n *\n * @internal\n */\nexport async function renderInvokableHelp(\n config: Config,\n argv: string[],\n policySet: CommandPolicySet,\n): Promise<{commandId?: string; output: string} | undefined> {\n const help = new InvokableHelp(config, policySet)\n try {\n await help.showHelp(argv)\n } catch (err) {\n // Subjects outside the policy surface (NotInvokableError) and subjects\n // oclif itself cannot resolve (CLIError, marked with an `oclif` property)\n // both yield no help output.\n if (err instanceof NotInvokableError || err.oclif) return undefined\n throw err\n }\n return {\n ...(help.commandId && {commandId: help.commandId}),\n output: help.lines.join('\\n').trimEnd(),\n }\n}\n\n/**\n * Whether `argv` asks for help rather than a command invocation: a leading\n * `help` (oclif's help command), or a recognized help flag before any `--`\n * terminator. The flags come from {@link getHelpFlagAdditions} — the same\n * source oclif's own dispatch consults — so this surface recognizes exactly\n * what the regular CLI recognizes.\n *\n * @internal\n */\nexport function isHelpRequest(argv: string[], config: Config): boolean {\n if (argv[0] === 'help') return true\n const helpFlags = getHelpFlagAdditions(config)\n for (const token of argv) {\n if (token === '--') return false\n if (helpFlags.includes(token)) return true\n }\n return false\n}\n"],"names":["Help","getHelpFlagAdditions","resolveTopicAliasInArgv","isConditionalInvocationPolicy","visibleCommandIds","policySet","Set","Object","entries","filter","policy","kind","map","id","visibleTopicNames","commandIds","names","parts","split","length","add","slice","join","NotInvokableError","Error","withoutDeniedFlags","command","denied","deniedFlags","flags","spellings","name","definition","push","char","alias","aliases","usesDeniedFlag","example","text","some","spelling","RegExp","String","raw","test","examples","InvokableHelp","commandId","lines","topicNames","config","stripAnsi","sortedCommands","has","sortedTopics","topic","log","args","showCommandHelp","showHelp","argv","showTopicHelp","renderInvokableHelp","help","err","oclif","undefined","output","trimEnd","isHelpRequest","helpFlags","token","includes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/exports/invokeSanityCli/help.ts"],"sourcesContent":["import {type Command, type Config, Help, type Interfaces} from '@oclif/core'\nimport {getHelpFlagAdditions} from '@oclif/core/help'\nimport {\n type CommandPolicy,\n type CommandPolicySet,\n isConditionalInvocationPolicy,\n} from '@sanity/cli-core/commandPolicy'\n\nimport {resolveTopicAliasInArgv} from '../../topicAliases.js'\n\n/** Command ids a policy exposes: entries that are not denied. */\nfunction visibleCommandIds(policySet: CommandPolicySet): Set<string> {\n return new Set(\n Object.entries(policySet)\n .filter(([, policy]) => policy.kind !== 'deny')\n .map(([id]) => id),\n )\n}\n\n/** Topic names covering the visible commands: every prefix of every id. */\nfunction visibleTopicNames(commandIds: Set<string>): Set<string> {\n const names = new Set<string>()\n for (const id of commandIds) {\n const parts = id.split(':')\n for (let length = 1; length < parts.length; length++) {\n names.add(parts.slice(0, length).join(':'))\n }\n }\n return names\n}\n\n/** Thrown when help is requested for a subject outside the policy surface. */\nclass NotInvokableError extends Error {}\n\n/**\n * A copy of `command` without the policy's denied flags, and without examples\n * that use them (under any spelling: `--web`, `--w`, `-w`), so rendered help\n * only advertises invocations the policy accepts.\n */\nfunction withoutDeniedFlags(command: Command.Loadable, policy?: CommandPolicy): Command.Loadable {\n if (!policy || !isConditionalInvocationPolicy(policy)) return {...command}\n\n const denied = policy.deniedFlags\n if (denied.length === 0) return {...command}\n\n const flags = {...command.flags}\n const spellings: string[] = []\n for (const name of denied) {\n const definition = flags[name]\n if (!definition) continue\n spellings.push(`--${name}`)\n if (definition.char) spellings.push(`-${definition.char}`)\n for (const alias of definition.aliases ?? []) spellings.push(`--${alias}`, `-${alias}`)\n delete flags[name]\n }\n\n const usesDeniedFlag = (example: Command.Example) => {\n const text = typeof example === 'string' ? example : example.command\n return spellings.some((spelling) =>\n new RegExp(String.raw`(^|\\s)${spelling}(=|\\s|$)`).test(text ?? ''),\n )\n }\n\n return {\n ...command,\n examples: command.examples?.filter((example) => !usesDeniedFlag(example)),\n flags,\n }\n}\n\n/**\n * oclif help renderer scoped to a policy's command surface. Subject\n * resolution (root vs topic vs command help) is oclif's own; the policy is\n * enforced at the rendering entry points so the rest of the CLI stays\n * invisible to embedding hosts, and listings are filtered to non-denied\n * commands and their topics only.\n *\n * Output is collected in {@link InvokableHelp.lines} instead of being written\n * to the process streams.\n */\nclass InvokableHelp extends Help {\n public commandId: string | undefined\n public readonly lines: string[] = []\n\n private readonly commandIds: Set<string>\n private readonly policySet: CommandPolicySet\n private readonly topicNames: Set<string>\n\n constructor(config: Config, policySet: CommandPolicySet) {\n super(config, {stripAnsi: true})\n this.policySet = policySet\n this.commandIds = visibleCommandIds(policySet)\n this.topicNames = visibleTopicNames(this.commandIds)\n }\n\n // Both listings start from oclif's, which already drops `hidden` entries, so\n // help advertises the same commands a CLI user sees, minus the ones the\n // policy denies. Hiding and denying answer different questions — \"should this\n // be advertised?\" and \"may this be invoked?\" — and a command that is hidden\n // but allowed stays invocable by id, exactly as it is for a CLI user who\n // knows the name.\n //\n // The copies below are load-bearing: oclif's formatters rewrite ids/names\n // in place (`cors:list` → `cors list`). Without copies those writes corrupt\n // the shared (cached) config, breaking the policy checks on subsequent\n // invocations.\n\n protected override get sortedCommands(): Command.Loadable[] {\n return super.sortedCommands\n .filter((command) => this.commandIds.has(command.id))\n .map((command) => ({...command}))\n }\n\n protected override get sortedTopics(): Interfaces.Topic[] {\n // Topic names and descriptions come from the oclif config (oclif.config.js)\n return super.sortedTopics\n .filter((topic) => this.topicNames.has(topic.name))\n .map((topic) => ({...topic}))\n }\n\n protected override log(...args: string[]): void {\n this.lines.push(...args)\n }\n\n public override async showCommandHelp(command: Command.Loadable): Promise<void> {\n if (!this.commandIds.has(command.id)) throw new NotInvokableError(command.id)\n this.commandId = command.id\n return super.showCommandHelp(withoutDeniedFlags(command, this.policySet[command.id]))\n }\n\n public override async showHelp(argv: string[]): Promise<void> {\n return super.showHelp(resolveTopicAliasInArgv(argv))\n }\n\n protected override async showTopicHelp(topic: Interfaces.Topic): Promise<void> {\n if (!this.topicNames.has(topic.name)) throw new NotInvokableError(topic.name)\n return super.showTopicHelp({...topic})\n }\n}\n\n/**\n * Render help text for a policy's command surface: root help for a bare help\n * request, or topic/command help when `argv` names a subject (e.g.\n * `['cors', '--help']`), exactly as oclif would resolve it. ANSI styling is\n * stripped so programmatic callers get plain text.\n *\n * Returns `undefined` when the subject is unknown or denied — callers should\n * respond the same way as for an unknown command, so hosts cannot probe the\n * full CLI surface through help.\n *\n * @internal\n */\nexport async function renderInvokableHelp(\n config: Config,\n argv: string[],\n policySet: CommandPolicySet,\n): Promise<{commandId?: string; output: string} | undefined> {\n const help = new InvokableHelp(config, policySet)\n try {\n await help.showHelp(argv)\n } catch (err) {\n // Subjects outside the policy surface (NotInvokableError) and subjects\n // oclif itself cannot resolve (CLIError, marked with an `oclif` property)\n // both yield no help output.\n if (err instanceof NotInvokableError || err.oclif) return undefined\n throw err\n }\n return {\n ...(help.commandId && {commandId: help.commandId}),\n output: help.lines.join('\\n').trimEnd(),\n }\n}\n\n/**\n * Whether `argv` asks for help rather than a command invocation: a leading\n * `help` (oclif's help command), or a recognized help flag before any `--`\n * terminator. The flags come from {@link getHelpFlagAdditions} — the same\n * source oclif's own dispatch consults — so this surface recognizes exactly\n * what the regular CLI recognizes.\n *\n * @internal\n */\nexport function isHelpRequest(argv: string[], config: Config): boolean {\n if (argv[0] === 'help') return true\n const helpFlags = getHelpFlagAdditions(config)\n for (const token of argv) {\n if (token === '--') return false\n if (helpFlags.includes(token)) return true\n }\n return false\n}\n"],"names":["Help","getHelpFlagAdditions","isConditionalInvocationPolicy","resolveTopicAliasInArgv","visibleCommandIds","policySet","Set","Object","entries","filter","policy","kind","map","id","visibleTopicNames","commandIds","names","parts","split","length","add","slice","join","NotInvokableError","Error","withoutDeniedFlags","command","denied","deniedFlags","flags","spellings","name","definition","push","char","alias","aliases","usesDeniedFlag","example","text","some","spelling","RegExp","String","raw","test","examples","InvokableHelp","commandId","lines","topicNames","config","stripAnsi","sortedCommands","has","sortedTopics","topic","log","args","showCommandHelp","showHelp","argv","showTopicHelp","renderInvokableHelp","help","err","oclif","undefined","output","trimEnd","isHelpRequest","helpFlags","token","includes"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Config } from "@oclif/core";
|
|
2
|
+
import { InvocationSource } from "@sanity/cli-core/commandPolicy";
|
|
2
3
|
import { SanityEnvironment } from "@sanity/cli-core/executionContext";
|
|
3
4
|
/**
|
|
4
5
|
* Valid body types for fetch requests.
|
|
@@ -89,8 +90,6 @@ interface FetchResponse {
|
|
|
89
90
|
* @public
|
|
90
91
|
*/
|
|
91
92
|
type FetchFunction = (input: string, init?: FetchInit) => Promise<FetchResponse>;
|
|
92
|
-
/** Where an invocation originates; selects the policy to enforce. */
|
|
93
|
-
type InvocationSource = 'mcp';
|
|
94
93
|
/**
|
|
95
94
|
* @internal
|
|
96
95
|
*/
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Programmatic (in-process) invocation of CLI commands, e.g. from an MCP
|
|
3
3
|
* server. The invokable surface is governed by a per-source command policy
|
|
4
4
|
* (see ./commandPolicies): every CLI command is explicitly allowed, denied,
|
|
5
|
-
* or allowed conditionally on the parsed invocation.
|
|
5
|
+
* or allowed conditionally on the parsed invocation. Commands contributed by
|
|
6
|
+
* plugins are covered by the policies those plugins declare, and are denied
|
|
7
|
+
* unless they declare one.
|
|
6
8
|
*
|
|
7
9
|
* {@link invokeSanityCli} handles arg parsing, policy enforcement, command
|
|
8
10
|
* dispatch, per-invocation auth, and output capture.
|
|
@@ -19,34 +21,42 @@
|
|
|
19
21
|
* Help works like the regular CLI, scoped to the source's policy: `--help`
|
|
20
22
|
* (or `help`) renders root help listing the invokable topics, and a subject
|
|
21
23
|
* (`cors --help`, `cors list --help`) renders topic or command help.
|
|
22
|
-
*/ import {
|
|
24
|
+
*/ import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { Config, Parser, settings } from '@oclif/core';
|
|
23
26
|
import { getHelpFlagAdditions, normalizeArgv } from '@oclif/core/help';
|
|
24
27
|
import { exitCodes } from '@sanity/cli-core';
|
|
28
|
+
import { deny, isConditionalInvocationPolicy } from '@sanity/cli-core/commandPolicy';
|
|
25
29
|
import { runWithCliExecutionContext } from '@sanity/cli-core/executionContext';
|
|
26
30
|
import { parseArgsStringToArgv } from 'string-argv';
|
|
27
31
|
import { resolveTopicAliasInArgv } from '../../topicAliases.js';
|
|
28
|
-
import {
|
|
29
|
-
import { deny, isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
32
|
+
import { resolveCommandPolicies } from './commandPolicies/index.js';
|
|
30
33
|
import { isHelpRequest, renderInvokableHelp } from './help.js';
|
|
31
34
|
import { prettyPrintError } from './prettyPrintError.js';
|
|
35
|
+
/**
|
|
36
|
+
* Whether a command can run in isolation, which in practice means it extends
|
|
37
|
+
* `SanityCommand`: only that base class routes output, token resolution,
|
|
38
|
+
* interactivity, and project discovery through the CLI execution context.
|
|
39
|
+
*
|
|
40
|
+
* This is a capability check, not a policy check. A plugin can declare a
|
|
41
|
+
* policy for a command that still extends oclif's `Command` directly, and
|
|
42
|
+
* such a command must not run here — the policy says the invocation is safe,
|
|
43
|
+
* but nothing would hold it to the isolation guarantees that assessment
|
|
44
|
+
* assumes.
|
|
45
|
+
*/ function supportsIsolatedExecution(CommandClass) {
|
|
46
|
+
const prototype = CommandClass.prototype;
|
|
47
|
+
return typeof prototype?.runInExecutionContext === 'function';
|
|
48
|
+
}
|
|
32
49
|
/**
|
|
33
50
|
* Instantiate a policy-approved command for isolated execution.
|
|
34
51
|
*
|
|
35
|
-
* Every invokable command extends `SanityCommand`, whose
|
|
36
52
|
* `runInExecutionContext()` runs the instance without oclif's static
|
|
37
53
|
* `Command.run()` — the static runner re-enters `Config.load()` (filesystem
|
|
38
54
|
* reads and a process-global config cache write) on every call. The manifest
|
|
39
55
|
* types command classes as the abstract oclif `Command`, hence the
|
|
40
|
-
* constructor cast.
|
|
41
|
-
* `instanceof`, because command modules may load from the compiled build
|
|
42
|
-
* while this module runs from source, giving `SanityCommand` two identities.
|
|
56
|
+
* constructor cast.
|
|
43
57
|
*/ function instantiateCommand(CommandClass, argv, config) {
|
|
44
58
|
const ConcreteCommand = CommandClass;
|
|
45
|
-
|
|
46
|
-
if (typeof command.runInExecutionContext !== 'function') {
|
|
47
|
-
throw new TypeError(`Command "${CommandClass.id}" does not support isolated execution`);
|
|
48
|
-
}
|
|
49
|
-
return command;
|
|
59
|
+
return new ConcreteCommand(argv, config);
|
|
50
60
|
}
|
|
51
61
|
/**
|
|
52
62
|
* Load the oclif `Config` for this package, needed to resolve, load, and run
|
|
@@ -54,8 +64,14 @@ import { prettyPrintError } from './prettyPrintError.js';
|
|
|
54
64
|
* re-reading the command manifest per call. It only reads this package's own
|
|
55
65
|
* installed files — process-lifetime initialization, not per-invocation host
|
|
56
66
|
* state — so it happens outside any execution context.
|
|
67
|
+
*
|
|
68
|
+
* `userPlugins: false` keeps this surface to the plugins this package ships
|
|
69
|
+
* with. oclif otherwise loads whatever `<dataDir>/package.json` lists.
|
|
57
70
|
*/ function loadCliCommandConfig() {
|
|
58
|
-
return Config.load(
|
|
71
|
+
return Config.load({
|
|
72
|
+
root: fileURLToPath(import.meta.url),
|
|
73
|
+
userPlugins: false
|
|
74
|
+
});
|
|
59
75
|
}
|
|
60
76
|
let cachedConfig;
|
|
61
77
|
function unknownCommandResult(argv, policySet) {
|
|
@@ -106,7 +122,9 @@ function unknownCommandResult(argv, policySet) {
|
|
|
106
122
|
}, ()=>invokeSanityCliInContext(options, resolvedConfig, output));
|
|
107
123
|
}
|
|
108
124
|
async function invokeSanityCliInContext({ args, source }, resolvedConfig, output) {
|
|
109
|
-
|
|
125
|
+
// Combines this package's own policies with those each plugin declares for
|
|
126
|
+
// the commands it contributes. Resolved once per config, not per call.
|
|
127
|
+
const policySet = await resolveCommandPolicies(resolvedConfig, source);
|
|
110
128
|
// Pre-split argv arrays are taken verbatim; only string input goes through
|
|
111
129
|
// shell-style tokenization and quote normalization.
|
|
112
130
|
let argv = typeof args === 'string' ? parseArgsStringToArgv(args).map((t)=>stripFlagQuotes(t)) : [
|
|
@@ -148,6 +166,10 @@ async function invokeSanityCliInContext({ args, source }, resolvedConfig, output
|
|
|
148
166
|
const commandDefinition = policy.kind === 'deny' ? undefined : resolvedConfig.findCommand(commandId);
|
|
149
167
|
if (!commandDefinition) return unknownCommandResult(argv, policySet);
|
|
150
168
|
const CommandClass = await commandDefinition.load();
|
|
169
|
+
// A policy can only speak to whether an invocation is safe; it cannot make
|
|
170
|
+
// a command isolatable. One that isn't is treated as if it did not exist,
|
|
171
|
+
// like every other fail-closed path here.
|
|
172
|
+
if (!supportsIsolatedExecution(CommandClass)) return unknownCommandResult(argv, policySet);
|
|
151
173
|
// Parse with the command's real definitions (without executing anything) so
|
|
152
174
|
// conditional policies are evaluated against typed args/flags, not tokens.
|
|
153
175
|
let invocation;
|