@sanity/cli 7.13.0 → 7.15.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 +24 -14
- package/dist/SanityHelp.js +10 -0
- package/dist/SanityHelp.js.map +1 -1
- package/dist/actions/auth/getProviderName.js +13 -0
- package/dist/actions/auth/getProviderName.js.map +1 -1
- package/dist/actions/debug/types.js.map +1 -1
- package/dist/actions/deploy/deployApp.js +1 -0
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployChecks.js +25 -4
- package/dist/actions/deploy/deployChecks.js.map +1 -1
- package/dist/actions/deploy/resolveDeployTarget.js +23 -4
- package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
- package/dist/actions/init/initAction.js +3 -3
- package/dist/actions/init/initAction.js.map +1 -1
- package/dist/commands/cors/add.js +13 -7
- package/dist/commands/cors/add.js.map +1 -1
- package/dist/commands/datasets/copy.js +6 -3
- package/dist/commands/datasets/copy.js.map +1 -1
- package/dist/commands/debug.js +1 -1
- package/dist/commands/debug.js.map +1 -1
- package/dist/commands/login.js +12 -11
- package/dist/commands/login.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/index.js +6 -0
- package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +174 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js +46 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +1 -0
- package/dist/exports/invokeSanityCli/help.js +153 -0
- package/dist/exports/invokeSanityCli/help.js.map +1 -0
- package/dist/exports/invokeSanityCli/index.d.ts +73 -0
- package/dist/exports/invokeSanityCli/index.js +186 -0
- package/dist/exports/invokeSanityCli/index.js.map +1 -0
- package/dist/exports/invokeSanityCli/prettyPrintError.js +39 -0
- package/dist/exports/invokeSanityCli/prettyPrintError.js.map +1 -0
- package/dist/server/devServer.js +16 -2
- package/dist/server/devServer.js.map +1 -1
- package/dist/util/getProjectDefaults.js +4 -1
- package/dist/util/getProjectDefaults.js.map +1 -1
- package/dist/util/getSanityEnv.js +6 -1
- package/dist/util/getSanityEnv.js.map +1 -1
- package/dist/util/packageManager/preferredPm.js +45 -6
- package/dist/util/packageManager/preferredPm.js.map +1 -1
- package/dist/util/warnAboutMissingAppId.js +2 -1
- package/dist/util/warnAboutMissingAppId.js.map +1 -1
- package/oclif.manifest.json +54 -54
- package/package.json +17 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/commands/login.ts"],"sourcesContent":["import {text} from 'node:stream/consumers'\n\nimport {Command, Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\n\nimport {login} from '../actions/auth/login/login.js'\nimport {LOGIN_PROVIDER_IDS} from '../actions/auth/login/loginInstructions.js'\n\nexport class LoginCommand extends SanityCommand<typeof LoginCommand> {\n static override description = 'Log in to your Sanity account'\n static override examples: Array<Command.Example> = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Log in using default settings',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --provider github --no-open',\n description: 'Login with GitHub provider, but do not open a browser window automatically',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --sso my-organization',\n description: 'Log in using Single Sign-On with the \"my-organization\" slug',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> --sso my-organization --sso-provider \"Okta SSO\"',\n description: 'Log in using a specific SSO provider within an organization',\n },\n
|
|
1
|
+
{"version":3,"sources":["../../src/commands/login.ts"],"sourcesContent":["import {text} from 'node:stream/consumers'\n\nimport {Command, Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\n\nimport {login} from '../actions/auth/login/login.js'\nimport {LOGIN_PROVIDER_IDS} from '../actions/auth/login/loginInstructions.js'\n\nexport class LoginCommand extends SanityCommand<typeof LoginCommand> {\n static override description = 'Log in to your Sanity account'\n static override examples: Array<Command.Example> = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Log in using default settings',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --with-token < token.txt',\n description: 'Log in using a token from standard input',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --provider github --no-open',\n description: 'Login with GitHub provider, but do not open a browser window automatically',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --sso my-organization',\n description: 'Log in using Single Sign-On with the \"my-organization\" slug',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> --sso my-organization --sso-provider \"Okta SSO\"',\n description: 'Log in using a specific SSO provider within an organization',\n },\n ]\n static override flags = {\n 'with-token': Flags.boolean({\n description: 'Read token from standard input',\n exclusive: ['provider', 'sso'],\n }),\n // eslint-disable-next-line perfectionist/sort-objects -- Keep token login first in generated usage.\n experimental: Flags.boolean({\n default: false,\n hidden: true,\n }),\n open: Flags.boolean({\n allowNo: true,\n default: true,\n description: 'Open a browser window to log in (`--no-open` only prints URL)',\n }),\n provider: Flags.string({\n description: `Log in using a provider ID (${LOGIN_PROVIDER_IDS.join(', ')})`,\n exclusive: ['sso', 'with-token'],\n helpValue: '<providerId>',\n }),\n sso: Flags.string({\n description: 'Log in using Single Sign-On, using the given organization slug',\n exclusive: ['provider', 'with-token'],\n helpValue: '<slug>',\n }),\n 'sso-provider': Flags.string({\n dependsOn: ['sso'],\n description: 'Select a specific SSO provider by name (use with --sso)',\n helpValue: '<name>',\n }),\n } satisfies FlagInput\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(LoginCommand)\n const {'sso-provider': ssoProvider, 'with-token': withToken, ...loginFlags} = flags\n\n try {\n const token = withToken ? await readTokenFromStdin() : undefined\n\n await login({\n ...loginFlags,\n output: this.output,\n ssoProvider,\n telemetry: this.telemetry,\n token,\n })\n this.log('Login successful')\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n this.error(`Login failed: ${message}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n}\n\nasync function readTokenFromStdin(): Promise<string> {\n if (process.stdin.isTTY) {\n throw new Error(\n 'Token is required on standard input. Run `sanity login --with-token < token.txt`.',\n )\n }\n\n return text(process.stdin)\n}\n"],"names":["text","Flags","exitCodes","SanityCommand","login","LOGIN_PROVIDER_IDS","LoginCommand","description","examples","command","flags","boolean","exclusive","experimental","default","hidden","open","allowNo","provider","string","join","helpValue","sso","dependsOn","run","parse","ssoProvider","withToken","loginFlags","token","readTokenFromStdin","undefined","output","telemetry","log","error","message","Error","String","exit","RUNTIME_ERROR","process","stdin","isTTY"],"mappings":"AAAA,SAAQA,IAAI,QAAO,wBAAuB;AAE1C,SAAiBC,KAAK,QAAO,cAAa;AAE1C,SAAQC,SAAS,EAAEC,aAAa,QAAO,mBAAkB;AAEzD,SAAQC,KAAK,QAAO,iCAAgC;AACpD,SAAQC,kBAAkB,QAAO,6CAA4C;AAE7E,OAAO,MAAMC,qBAAqBH;IAChC,OAAgBI,cAAc,gCAA+B;IAC7D,OAAgBC,WAAmC;QACjD;YACEC,SAAS;YACTF,aAAa;QACf;QACA;YACEE,SAAS;YACTF,aAAa;QACf;QACA;YACEE,SAAS;YACTF,aAAa;QACf;QACA;YACEE,SAAS;YACTF,aAAa;QACf;QACA;YACEE,SACE;YACFF,aAAa;QACf;KACD,CAAA;IACD,OAAgBG,QAAQ;QACtB,cAAcT,MAAMU,OAAO,CAAC;YAC1BJ,aAAa;YACbK,WAAW;gBAAC;gBAAY;aAAM;QAChC;QACA,oGAAoG;QACpGC,cAAcZ,MAAMU,OAAO,CAAC;YAC1BG,SAAS;YACTC,QAAQ;QACV;QACAC,MAAMf,MAAMU,OAAO,CAAC;YAClBM,SAAS;YACTH,SAAS;YACTP,aAAa;QACf;QACAW,UAAUjB,MAAMkB,MAAM,CAAC;YACrBZ,aAAa,CAAC,4BAA4B,EAAEF,mBAAmBe,IAAI,CAAC,MAAM,CAAC,CAAC;YAC5ER,WAAW;gBAAC;gBAAO;aAAa;YAChCS,WAAW;QACb;QACAC,KAAKrB,MAAMkB,MAAM,CAAC;YAChBZ,aAAa;YACbK,WAAW;gBAAC;gBAAY;aAAa;YACrCS,WAAW;QACb;QACA,gBAAgBpB,MAAMkB,MAAM,CAAC;YAC3BI,WAAW;gBAAC;aAAM;YAClBhB,aAAa;YACbc,WAAW;QACb;IACF,EAAqB;IAErB,MAAaG,MAAqB;QAChC,MAAM,EAACd,KAAK,EAAC,GAAG,MAAM,IAAI,CAACe,KAAK,CAACnB;QACjC,MAAM,EAAC,gBAAgBoB,WAAW,EAAE,cAAcC,SAAS,EAAE,GAAGC,YAAW,GAAGlB;QAE9E,IAAI;YACF,MAAMmB,QAAQF,YAAY,MAAMG,uBAAuBC;YAEvD,MAAM3B,MAAM;gBACV,GAAGwB,UAAU;gBACbI,QAAQ,IAAI,CAACA,MAAM;gBACnBN;gBACAO,WAAW,IAAI,CAACA,SAAS;gBACzBJ;YACF;YACA,IAAI,CAACK,GAAG,CAAC;QACX,EAAE,OAAOC,OAAO;YACd,MAAMC,UAAUD,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH;YAChE,IAAI,CAACA,KAAK,CAAC,CAAC,cAAc,EAAEC,SAAS,EAAE;gBAACG,MAAMrC,UAAUsC,aAAa;YAAA;QACvE;IACF;AACF;AAEA,eAAeV;IACb,IAAIW,QAAQC,KAAK,CAACC,KAAK,EAAE;QACvB,MAAM,IAAIN,MACR;IAEJ;IAEA,OAAOrC,KAAKyC,QAAQC,KAAK;AAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/index.ts"],"sourcesContent":["import {mcpPolicy} from './mcpPolicy.js'\nimport {type CommandPolicySet, type InvocationSource} from './policy.js'\n\nexport const commandPolicies: Record<InvocationSource, CommandPolicySet> = {\n mcp: mcpPolicy,\n}\n"],"names":["mcpPolicy","commandPolicies","mcp"],"mappings":"AAAA,SAAQA,SAAS,QAAO,iBAAgB;AAGxC,OAAO,MAAMC,kBAA8D;IACzEC,KAAKF;AACP,EAAC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { allow, conditionalDenyFlags, conditionalPolicy, deny } from './policy.js';
|
|
2
|
+
/**
|
|
3
|
+
* A typed `-F/--field` value of `@<file>` or `@-` makes the `api` command
|
|
4
|
+
* read the host's filesystem or stdin (raw `-f` fields are always verbatim).
|
|
5
|
+
*/ function fieldReadsFromHost(field) {
|
|
6
|
+
if (typeof field !== 'string') return false;
|
|
7
|
+
const separatorIndex = field.indexOf('=');
|
|
8
|
+
return separatorIndex > 0 && field[separatorIndex + 1] === '@';
|
|
9
|
+
}
|
|
10
|
+
/** A `-H/--header` value that replaces the invocation's authentication. */ function headerOverridesAuthentication(header) {
|
|
11
|
+
if (typeof header !== 'string') return false;
|
|
12
|
+
const separatorIndex = header.indexOf(':');
|
|
13
|
+
return separatorIndex > 0 && header.slice(0, separatorIndex).trim().toLowerCase() === 'authorization';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* MCP programmatic mode disables local project/config discovery (see the CLI
|
|
17
|
+
* execution context). Missing project or dataset values may therefore produce
|
|
18
|
+
* a usage error, but cannot cause local filesystem access. Destructive remote
|
|
19
|
+
* operations are allowed and do not by themselves make a command unsafe.
|
|
20
|
+
*
|
|
21
|
+
* Every manifest command must have exactly one policy here:
|
|
22
|
+
* - allow: every valid invocation is safe
|
|
23
|
+
* - conditional: safety depends on parsed arguments or flags
|
|
24
|
+
* - deny: no invocation is safe
|
|
25
|
+
*/ export const mcpPolicy = {
|
|
26
|
+
// Special exception, this can be very dangerous but is also super useful
|
|
27
|
+
// to expose. Refuse authentication overrides and host input channels:
|
|
28
|
+
// `--token` and an Authorization header replace the MCP user's token,
|
|
29
|
+
// `--input` reads the request body from the host's filesystem or stdin, and
|
|
30
|
+
// `-F key=@<file>` / `-F key=@-` field values do the same.
|
|
31
|
+
api: conditionalPolicy({
|
|
32
|
+
deniedFlags: [
|
|
33
|
+
'input',
|
|
34
|
+
'token'
|
|
35
|
+
],
|
|
36
|
+
validate: ({ flags })=>(!Array.isArray(flags.field) || !flags.field.some((field)=>fieldReadsFromHost(field))) && (!Array.isArray(flags.header) || !flags.header.some((header)=>headerOverridesAuthentication(header)))
|
|
37
|
+
}),
|
|
38
|
+
'backups:disable': allow,
|
|
39
|
+
// Writes a downloaded backup to the local filesystem.
|
|
40
|
+
'backups:download': deny,
|
|
41
|
+
'backups:enable': allow,
|
|
42
|
+
'backups:list': allow,
|
|
43
|
+
// Requires a local Studio project and writes build output to disk.
|
|
44
|
+
build: deny,
|
|
45
|
+
// Reads and rewrites local source code.
|
|
46
|
+
codemod: deny,
|
|
47
|
+
'cors:add': allow,
|
|
48
|
+
'cors:delete': allow,
|
|
49
|
+
'cors:list': allow,
|
|
50
|
+
'datasets:alias:create': allow,
|
|
51
|
+
'datasets:alias:delete': allow,
|
|
52
|
+
'datasets:alias:link': allow,
|
|
53
|
+
'datasets:alias:unlink': allow,
|
|
54
|
+
'datasets:copy': allow,
|
|
55
|
+
'datasets:create': allow,
|
|
56
|
+
'datasets:delete': allow,
|
|
57
|
+
'datasets:embeddings:disable': allow,
|
|
58
|
+
'datasets:embeddings:enable': allow,
|
|
59
|
+
'datasets:embeddings:status': allow,
|
|
60
|
+
// Writes dataset contents and assets to the local filesystem.
|
|
61
|
+
'datasets:export': deny,
|
|
62
|
+
// Reads import data from the local filesystem and may replace documents.
|
|
63
|
+
'datasets:import': deny,
|
|
64
|
+
'datasets:list': allow,
|
|
65
|
+
'datasets:visibility:get': allow,
|
|
66
|
+
'datasets:visibility:set': allow,
|
|
67
|
+
// Inspects local project files and can print authentication secrets.
|
|
68
|
+
debug: deny,
|
|
69
|
+
// Reads, builds, and deploys a local Studio project.
|
|
70
|
+
deploy: deny,
|
|
71
|
+
// Loads a local Studio project and starts a development server.
|
|
72
|
+
dev: deny,
|
|
73
|
+
// Opens a browser on the machine running the MCP server.
|
|
74
|
+
'docs:browse': deny,
|
|
75
|
+
// --web opens a browser on the machine running the MCP server.
|
|
76
|
+
'docs:read': conditionalDenyFlags('web'),
|
|
77
|
+
'docs:search': allow,
|
|
78
|
+
// Reads and executes local project configuration for diagnostics.
|
|
79
|
+
doctor: deny,
|
|
80
|
+
// Reads document input from disk or launches a local editor.
|
|
81
|
+
'documents:create': deny,
|
|
82
|
+
'documents:delete': allow,
|
|
83
|
+
'documents:get': allow,
|
|
84
|
+
'documents:query': allow,
|
|
85
|
+
// Loads a local Studio schema to validate documents.
|
|
86
|
+
'documents:validate': deny,
|
|
87
|
+
// Executes arbitrary code in the local Studio context.
|
|
88
|
+
exec: deny,
|
|
89
|
+
// Loads a local schema and deploys a GraphQL API.
|
|
90
|
+
'graphql:deploy': deny,
|
|
91
|
+
'graphql:list': allow,
|
|
92
|
+
// --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.
|
|
93
|
+
'graphql:undeploy': conditionalDenyFlags('api'),
|
|
94
|
+
'hooks:attempt': allow,
|
|
95
|
+
'hooks:create': allow,
|
|
96
|
+
'hooks:delete': allow,
|
|
97
|
+
'hooks:list': allow,
|
|
98
|
+
'hooks:logs': allow,
|
|
99
|
+
// Creates or modifies a local project and may install dependencies.
|
|
100
|
+
init: deny,
|
|
101
|
+
// Installs packages into the local project.
|
|
102
|
+
install: deny,
|
|
103
|
+
// Opens a browser on the machine running the MCP server.
|
|
104
|
+
learn: deny,
|
|
105
|
+
// Performs an authentication flow.
|
|
106
|
+
login: deny,
|
|
107
|
+
// Performs an authentication operation and clears local credentials.
|
|
108
|
+
logout: deny,
|
|
109
|
+
// Reads local project configuration and opens a browser.
|
|
110
|
+
manage: deny,
|
|
111
|
+
// Loads local Studio configuration and writes manifest files.
|
|
112
|
+
'manifest:extract': deny,
|
|
113
|
+
// Reads and writes local MCP client configuration.
|
|
114
|
+
'mcp:configure': deny,
|
|
115
|
+
// Writes an aspect definition to the local filesystem.
|
|
116
|
+
'media:create-aspect': deny,
|
|
117
|
+
'media:delete-aspect': allow,
|
|
118
|
+
// Reads a local aspect definition before deploying it.
|
|
119
|
+
'media:deploy-aspect': deny,
|
|
120
|
+
// Writes media assets to the local filesystem.
|
|
121
|
+
'media:export': deny,
|
|
122
|
+
// Reads media assets from the local filesystem.
|
|
123
|
+
'media:import': deny,
|
|
124
|
+
// Creates migration source files in the local project.
|
|
125
|
+
'migrations:create': deny,
|
|
126
|
+
// Reads and loads migration definitions from the local project.
|
|
127
|
+
'migrations:list': deny,
|
|
128
|
+
// Executes local migration code that may perform arbitrary document mutations.
|
|
129
|
+
'migrations:run': deny,
|
|
130
|
+
// --web opens a browser on the machine running the MCP server.
|
|
131
|
+
'openapi:get': conditionalDenyFlags('web'),
|
|
132
|
+
'openapi:list': conditionalDenyFlags('web'),
|
|
133
|
+
'organizations:create': allow,
|
|
134
|
+
'organizations:delete': allow,
|
|
135
|
+
'organizations:get': allow,
|
|
136
|
+
'organizations:list': allow,
|
|
137
|
+
'organizations:update': allow,
|
|
138
|
+
// Serves a local production build.
|
|
139
|
+
preview: deny,
|
|
140
|
+
'projects:create': allow,
|
|
141
|
+
'projects:list': allow,
|
|
142
|
+
// Loads the local Studio configuration to resolve the datasets containing each schema.
|
|
143
|
+
'schemas:delete': deny,
|
|
144
|
+
// Loads local schema files before deploying them.
|
|
145
|
+
'schemas:deploy': deny,
|
|
146
|
+
// Loads local Studio configuration and writes an extracted schema to disk.
|
|
147
|
+
'schemas:extract': deny,
|
|
148
|
+
// Requires a local project and loads its schema configuration.
|
|
149
|
+
'schemas:list': deny,
|
|
150
|
+
// Loads and executes a local Studio schema.
|
|
151
|
+
'schemas:validate': deny,
|
|
152
|
+
// Installs skills into local editor configuration directories.
|
|
153
|
+
'skills:install': deny,
|
|
154
|
+
// Changes account telemetry preferences and mutates local cached configuration.
|
|
155
|
+
'telemetry:disable': deny,
|
|
156
|
+
// Changes account telemetry preferences and mutates local cached configuration.
|
|
157
|
+
'telemetry:enable': deny,
|
|
158
|
+
'telemetry:status': allow,
|
|
159
|
+
// Creates authentication credentials.
|
|
160
|
+
'tokens:create': deny,
|
|
161
|
+
// Deletes authentication credentials.
|
|
162
|
+
'tokens:delete': deny,
|
|
163
|
+
// Exposes authentication credential metadata.
|
|
164
|
+
'tokens:list': deny,
|
|
165
|
+
// Loads local CLI and workbench configuration to identify the deployed Studio or application.
|
|
166
|
+
undeploy: deny,
|
|
167
|
+
// Grants a user access to a project.
|
|
168
|
+
'users:invite': deny,
|
|
169
|
+
'users:list': allow,
|
|
170
|
+
// Reads the local project and installed package tree.
|
|
171
|
+
versions: deny
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=mcpPolicy.js.map
|
|
@@ -0,0 +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\n/**\n * A typed `-F/--field` value of `@<file>` or `@-` makes the `api` command\n * read the host's filesystem or stdin (raw `-f` fields are always verbatim).\n */\nfunction fieldReadsFromHost(field: unknown): boolean {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n}\n\n/** A `-H/--header` value that replaces the invocation's authentication. */\nfunction headerOverridesAuthentication(header: unknown): boolean {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n return (\n separatorIndex > 0 && header.slice(0, separatorIndex).trim().toLowerCase() === 'authorization'\n )\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` and an Authorization header replace the MCP user's token,\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: ({flags}) =>\n (!Array.isArray(flags.field) || !flags.field.some((field) => fieldReadsFromHost(field))) &&\n (!Array.isArray(flags.header) ||\n !flags.header.some((header) => headerOverridesAuthentication(header))),\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 '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 // --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\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\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","fieldReadsFromHost","field","separatorIndex","indexOf","headerOverridesAuthentication","header","slice","trim","toLowerCase","mcpPolicy","api","deniedFlags","validate","flags","Array","isArray","some","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","preview","undeploy","versions"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-source command policies for programmatic CLI invocation.
|
|
3
|
+
*
|
|
4
|
+
* A policy decides, for one invocation source (e.g. the remote MCP server),
|
|
5
|
+
* which commands may be invoked and — for conditional entries — which parsed
|
|
6
|
+
* invocations of those commands are acceptable. Policies are exhaustive:
|
|
7
|
+
* every command in this package's oclif manifest has exactly one entry, so a
|
|
8
|
+
* newly added CLI command cannot slip into (or ambiguously stay out of) the
|
|
9
|
+
* invokable surface. A unit test enforces this; at runtime an uncategorized
|
|
10
|
+
* command id fails closed (treated as deny).
|
|
11
|
+
*/ /** The parsed command invocation a conditional policy is evaluated against. */ /** Every valid invocation of the command is safe. */ export const allow = {
|
|
12
|
+
kind: 'allow',
|
|
13
|
+
validate: ()=>true
|
|
14
|
+
};
|
|
15
|
+
/** No invocation of the command is safe. Behaves like an unknown command. */ export const deny = {
|
|
16
|
+
kind: 'deny',
|
|
17
|
+
validate: ()=>false
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Conditional policies to deny flags. The flags are also hidden
|
|
21
|
+
* from rendered help, so hosts are never told about surface they cannot use.
|
|
22
|
+
*/ export function conditionalDenyFlags(...names) {
|
|
23
|
+
return {
|
|
24
|
+
deniedFlags: names,
|
|
25
|
+
kind: 'conditional',
|
|
26
|
+
validate: ({ flags })=>names.every((name)=>flags[name] === undefined || flags[name] === false)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Conditional policy combining {@link conditionalDenyFlags} with an extra
|
|
31
|
+
* predicate on the parsed invocation, for conditions a flag name alone
|
|
32
|
+
* cannot express — e.g. flag values that would read from the host machine.
|
|
33
|
+
* `deniedFlags` are hidden from rendered help; the predicate is not
|
|
34
|
+
* expressible there, so flags it constrains stay advertised.
|
|
35
|
+
*/ export function conditionalPolicy(options) {
|
|
36
|
+
const denyFlags = conditionalDenyFlags(...options.deniedFlags ?? []);
|
|
37
|
+
return {
|
|
38
|
+
...denyFlags,
|
|
39
|
+
validate: (invocation)=>denyFlags.validate(invocation) && options.validate(invocation)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function isConditionalInvocationPolicy(policy) {
|
|
43
|
+
return policy.kind === 'conditional';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/policy.ts"],"sourcesContent":["/**\n * Per-source command policies for programmatic CLI invocation.\n *\n * A policy decides, for one invocation source (e.g. the remote MCP server),\n * which commands may be invoked and — for conditional entries — which parsed\n * invocations of those commands are acceptable. Policies are exhaustive:\n * every command in this package's oclif manifest has exactly one entry, so a\n * newly added CLI command cannot slip into (or ambiguously stay out of) the\n * invokable surface. A unit test enforces this; at runtime an uncategorized\n * command id fails closed (treated as deny).\n */\n\n/** The parsed command invocation a conditional policy is evaluated against. */\ninterface Invocation {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}\n\ntype InvocationPolicy = (invocation: Invocation) => boolean\n\ntype ConditionalInvocationPolicy = CommandPolicy & {\n /**\n * Flag names this policy refuses. Declarative so the help renderer can\n * omit them (and examples using them) from the advertised command surface.\n */\n deniedFlags: readonly string[]\n\n kind: 'conditional'\n}\n\nexport interface CommandPolicy {\n kind: 'allow' | 'conditional' | 'deny'\n validate: InvocationPolicy\n}\n\n/** Every valid invocation of the command is safe. */\nexport const allow: CommandPolicy = {kind: 'allow', validate: () => true}\n\n/** No invocation of the command is safe. Behaves like an unknown command. */\nexport const deny: CommandPolicy = {kind: 'deny', validate: () => false}\n\n/**\n * Conditional policies to deny flags. The flags are also hidden\n * from rendered help, so hosts are never told about surface they cannot use.\n */\nexport function conditionalDenyFlags(...names: string[]): ConditionalInvocationPolicy {\n return {\n deniedFlags: names,\n kind: 'conditional',\n validate: ({flags}) =>\n names.every((name) => flags[name] === undefined || flags[name] === false),\n }\n}\n\n/**\n * Conditional policy combining {@link conditionalDenyFlags} with an extra\n * predicate on the parsed invocation, for conditions a flag name alone\n * cannot express — e.g. flag values that would read from the host machine.\n * `deniedFlags` are hidden from rendered help; the predicate is not\n * expressible there, so flags it constrains stay advertised.\n */\nexport function conditionalPolicy(options: {\n deniedFlags?: string[]\n validate: InvocationPolicy\n}): ConditionalInvocationPolicy {\n const denyFlags = conditionalDenyFlags(...(options.deniedFlags ?? []))\n return {\n ...denyFlags,\n validate: (invocation) => denyFlags.validate(invocation) && options.validate(invocation),\n }\n}\n\nexport function isConditionalInvocationPolicy(\n policy: CommandPolicy,\n): policy is ConditionalInvocationPolicy {\n return policy.kind === 'conditional'\n}\n\n/** A complete policy table, keyed by oclif command id. */\nexport type CommandPolicySet = Readonly<Record<string, CommandPolicy>>\n\n/** Where an invocation originates; selects the policy to enforce. */\nexport type InvocationSource = 'mcp'\n"],"names":["allow","kind","validate","deny","conditionalDenyFlags","names","deniedFlags","flags","every","name","undefined","conditionalPolicy","options","denyFlags","invocation","isConditionalInvocationPolicy","policy"],"mappings":"AAAA;;;;;;;;;;CAUC,GAED,6EAA6E,GAuB7E,mDAAmD,GACnD,OAAO,MAAMA,QAAuB;IAACC,MAAM;IAASC,UAAU,IAAM;AAAI,EAAC;AAEzE,2EAA2E,GAC3E,OAAO,MAAMC,OAAsB;IAACF,MAAM;IAAQC,UAAU,IAAM;AAAK,EAAC;AAExE;;;CAGC,GACD,OAAO,SAASE,qBAAqB,GAAGC,KAAe;IACrD,OAAO;QACLC,aAAaD;QACbJ,MAAM;QACNC,UAAU,CAAC,EAACK,KAAK,EAAC,GAChBF,MAAMG,KAAK,CAAC,CAACC,OAASF,KAAK,CAACE,KAAK,KAAKC,aAAaH,KAAK,CAACE,KAAK,KAAK;IACvE;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,kBAAkBC,OAGjC;IACC,MAAMC,YAAYT,wBAAyBQ,QAAQN,WAAW,IAAI,EAAE;IACpE,OAAO;QACL,GAAGO,SAAS;QACZX,UAAU,CAACY,aAAeD,UAAUX,QAAQ,CAACY,eAAeF,QAAQV,QAAQ,CAACY;IAC/E;AACF;AAEA,OAAO,SAASC,8BACdC,MAAqB;IAErB,OAAOA,OAAOf,IAAI,KAAK;AACzB"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Help } from '@oclif/core';
|
|
2
|
+
import { getHelpFlagAdditions } from '@oclif/core/help';
|
|
3
|
+
import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
4
|
+
/** Command ids a policy exposes: entries that are not denied. */ function visibleCommandIds(policySet) {
|
|
5
|
+
return new Set(Object.entries(policySet).filter(([, policy])=>policy.kind !== 'deny').map(([id])=>id));
|
|
6
|
+
}
|
|
7
|
+
/** Topic names covering the visible commands: every prefix of every id. */ function visibleTopicNames(commandIds) {
|
|
8
|
+
const names = new Set();
|
|
9
|
+
for (const id of commandIds){
|
|
10
|
+
const parts = id.split(':');
|
|
11
|
+
for(let length = 1; length < parts.length; length++){
|
|
12
|
+
names.add(parts.slice(0, length).join(':'));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return names;
|
|
16
|
+
}
|
|
17
|
+
/** Thrown when help is requested for a subject outside the policy surface. */ class NotInvokableError extends Error {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A copy of `command` without the policy's denied flags, and without examples
|
|
21
|
+
* that use them (under any spelling: `--web`, `--w`, `-w`), so rendered help
|
|
22
|
+
* only advertises invocations the policy accepts.
|
|
23
|
+
*/ function withoutDeniedFlags(command, policy) {
|
|
24
|
+
if (!policy || !isConditionalInvocationPolicy(policy)) return {
|
|
25
|
+
...command
|
|
26
|
+
};
|
|
27
|
+
const denied = policy.deniedFlags;
|
|
28
|
+
if (denied.length === 0) return {
|
|
29
|
+
...command
|
|
30
|
+
};
|
|
31
|
+
const flags = {
|
|
32
|
+
...command.flags
|
|
33
|
+
};
|
|
34
|
+
const spellings = [];
|
|
35
|
+
for (const name of denied){
|
|
36
|
+
const definition = flags[name];
|
|
37
|
+
if (!definition) continue;
|
|
38
|
+
spellings.push(`--${name}`);
|
|
39
|
+
if (definition.char) spellings.push(`-${definition.char}`);
|
|
40
|
+
for (const alias of definition.aliases ?? [])spellings.push(`--${alias}`, `-${alias}`);
|
|
41
|
+
delete flags[name];
|
|
42
|
+
}
|
|
43
|
+
const usesDeniedFlag = (example)=>{
|
|
44
|
+
const text = typeof example === 'string' ? example : example.command;
|
|
45
|
+
return spellings.some((spelling)=>new RegExp(String.raw`(^|\s)${spelling}(=|\s|$)`).test(text ?? ''));
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
...command,
|
|
49
|
+
examples: command.examples?.filter((example)=>!usesDeniedFlag(example)),
|
|
50
|
+
flags
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* oclif help renderer scoped to a policy's command surface. Subject
|
|
55
|
+
* resolution (root vs topic vs command help) is oclif's own; the policy is
|
|
56
|
+
* enforced at the rendering entry points so the rest of the CLI stays
|
|
57
|
+
* invisible to embedding hosts, and listings are filtered to non-denied
|
|
58
|
+
* commands and their topics only.
|
|
59
|
+
*
|
|
60
|
+
* Output is collected in {@link InvokableHelp.lines} instead of being written
|
|
61
|
+
* to the process streams.
|
|
62
|
+
*/ class InvokableHelp extends Help {
|
|
63
|
+
commandId;
|
|
64
|
+
lines = [];
|
|
65
|
+
commandIds;
|
|
66
|
+
policySet;
|
|
67
|
+
topicNames;
|
|
68
|
+
constructor(config, policySet){
|
|
69
|
+
super(config, {
|
|
70
|
+
stripAnsi: true
|
|
71
|
+
});
|
|
72
|
+
this.policySet = policySet;
|
|
73
|
+
this.commandIds = visibleCommandIds(policySet);
|
|
74
|
+
this.topicNames = visibleTopicNames(this.commandIds);
|
|
75
|
+
}
|
|
76
|
+
// The copies below are load-bearing: oclif's formatters rewrite ids/names
|
|
77
|
+
// in place (`cors:list` → `cors list`). Without copies those writes corrupt
|
|
78
|
+
// the shared (cached) config, breaking the policy checks on subsequent
|
|
79
|
+
// invocations.
|
|
80
|
+
get sortedCommands() {
|
|
81
|
+
return super.sortedCommands.filter((command)=>this.commandIds.has(command.id)).map((command)=>({
|
|
82
|
+
...command
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
get sortedTopics() {
|
|
86
|
+
// Topic names and descriptions come from the oclif config (oclif.config.js)
|
|
87
|
+
return super.sortedTopics.filter((topic)=>this.topicNames.has(topic.name)).map((topic)=>({
|
|
88
|
+
...topic
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
log(...args) {
|
|
92
|
+
this.lines.push(...args);
|
|
93
|
+
}
|
|
94
|
+
async showCommandHelp(command) {
|
|
95
|
+
if (!this.commandIds.has(command.id)) throw new NotInvokableError(command.id);
|
|
96
|
+
this.commandId = command.id;
|
|
97
|
+
return super.showCommandHelp(withoutDeniedFlags(command, this.policySet[command.id]));
|
|
98
|
+
}
|
|
99
|
+
async showTopicHelp(topic) {
|
|
100
|
+
if (!this.topicNames.has(topic.name)) throw new NotInvokableError(topic.name);
|
|
101
|
+
return super.showTopicHelp({
|
|
102
|
+
...topic
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Render help text for a policy's command surface: root help for a bare help
|
|
108
|
+
* request, or topic/command help when `argv` names a subject (e.g.
|
|
109
|
+
* `['cors', '--help']`), exactly as oclif would resolve it. ANSI styling is
|
|
110
|
+
* stripped so programmatic callers get plain text.
|
|
111
|
+
*
|
|
112
|
+
* Returns `undefined` when the subject is unknown or denied — callers should
|
|
113
|
+
* respond the same way as for an unknown command, so hosts cannot probe the
|
|
114
|
+
* full CLI surface through help.
|
|
115
|
+
*
|
|
116
|
+
* @internal
|
|
117
|
+
*/ export async function renderInvokableHelp(config, argv, policySet) {
|
|
118
|
+
const help = new InvokableHelp(config, policySet);
|
|
119
|
+
try {
|
|
120
|
+
await help.showHelp(argv);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
// Subjects outside the policy surface (NotInvokableError) and subjects
|
|
123
|
+
// oclif itself cannot resolve (CLIError, marked with an `oclif` property)
|
|
124
|
+
// both yield no help output.
|
|
125
|
+
if (err instanceof NotInvokableError || err.oclif) return undefined;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
...help.commandId && {
|
|
130
|
+
commandId: help.commandId
|
|
131
|
+
},
|
|
132
|
+
output: help.lines.join('\n').trimEnd()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether `argv` asks for help rather than a command invocation: a leading
|
|
137
|
+
* `help` (oclif's help command), or a recognized help flag before any `--`
|
|
138
|
+
* terminator. The flags come from {@link getHelpFlagAdditions} — the same
|
|
139
|
+
* source oclif's own dispatch consults — so this surface recognizes exactly
|
|
140
|
+
* what the regular CLI recognizes.
|
|
141
|
+
*
|
|
142
|
+
* @internal
|
|
143
|
+
*/ export function isHelpRequest(argv, config) {
|
|
144
|
+
if (argv[0] === 'help') return true;
|
|
145
|
+
const helpFlags = getHelpFlagAdditions(config);
|
|
146
|
+
for (const token of argv){
|
|
147
|
+
if (token === '--') return false;
|
|
148
|
+
if (helpFlags.includes(token)) return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//# sourceMappingURL=help.js.map
|
|
@@ -0,0 +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 {\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 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","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","showTopicHelp","renderInvokableHelp","argv","help","showHelp","err","oclif","undefined","output","trimEnd","isHelpRequest","helpFlags","token","includes"],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Config } from "@oclif/core";
|
|
2
|
+
import { SanityEnvironment } from "@sanity/cli-core/executionContext";
|
|
3
|
+
|
|
4
|
+
/** Where an invocation originates; selects the policy to enforce. */
|
|
5
|
+
declare type InvocationSource = "mcp";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run a policy-permitted CLI command in-process and capture its result.
|
|
9
|
+
*
|
|
10
|
+
* Command-level failures (unknown command, bad flags, API errors) are
|
|
11
|
+
* reported through `exitCode`/`output` rather than thrown, so callers can
|
|
12
|
+
* relay them verbatim.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export declare function invokeSanityCli({
|
|
17
|
+
args,
|
|
18
|
+
config,
|
|
19
|
+
sanityEnv,
|
|
20
|
+
source,
|
|
21
|
+
token,
|
|
22
|
+
}: InvokeSanityCliOptions): Promise<InvokeSanityCliResult>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export declare interface InvokeSanityCliOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Arguments after `sanity` (a leading `sanity` token is tolerated), either
|
|
30
|
+
* as a single string — shell-style quoting is supported, but no shell is
|
|
31
|
+
* ever executed — or as a pre-split argv array.
|
|
32
|
+
*/
|
|
33
|
+
args: string | string[];
|
|
34
|
+
/**
|
|
35
|
+
* Where this invocation originates. Selects the command policy to enforce:
|
|
36
|
+
* which commands are invokable and which invocations of them are permitted.
|
|
37
|
+
*/
|
|
38
|
+
source: InvocationSource;
|
|
39
|
+
/**
|
|
40
|
+
* Auth token for this invocation. Scoped to this call via the CLI execution
|
|
41
|
+
* context: it never touches process env or the process-wide token cache, so
|
|
42
|
+
* concurrent invocations with different tokens are fully isolated.
|
|
43
|
+
*/
|
|
44
|
+
token: string;
|
|
45
|
+
/**
|
|
46
|
+
* Optional oclif config override (mainly for tests). Defaults to this
|
|
47
|
+
* package's config, loaded once and cached across invocations.
|
|
48
|
+
*/
|
|
49
|
+
config?: Config;
|
|
50
|
+
/**
|
|
51
|
+
* Sanity deployment environment for this invocation. Scoped to this call
|
|
52
|
+
* via the CLI execution context and defaults to `SANITY_INTERNAL_ENV` (or
|
|
53
|
+
* production when it is unset).
|
|
54
|
+
*/
|
|
55
|
+
sanityEnv?: SanityEnvironment;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export declare interface InvokeSanityCliResult {
|
|
62
|
+
/** `0` on success, the command's exit code otherwise. */
|
|
63
|
+
exitCode: number;
|
|
64
|
+
/** Combined stdout and stderr output, in emission order. */
|
|
65
|
+
output: string;
|
|
66
|
+
/**
|
|
67
|
+
* Canonical oclif command id when the invocation resolved to a command
|
|
68
|
+
* exposed by the selected policy (for example, `datasets:create`).
|
|
69
|
+
*/
|
|
70
|
+
commandId?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export {};
|