Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions packages/cli-kit/src/public/node/base-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import Command from './base-command.js'
import {Environments} from './environments.js'
import {encodeToml as encodeTOML} from './toml/codec.js'
import {globalFlags, jsonFlag, requiredIfNonInteractive} from './cli.js'
import {emitCommandEvent} from './command-events.js'
import {emitCommandEvent, runWithCommandEvents} from './command-events.js'
import {inTemporaryDirectory, mkdir, writeFile} from './fs.js'
import {joinPath, resolvePath, cwd} from './path.js'
import {mockAndCaptureOutput} from './testing/output.js'
import {unstyled} from './output.js'
import {defineJsonOutputSchema} from './json-output-schema.js'
import {zod} from './schema.js'
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'
import {Flags} from '@oclif/core'
import {Config, Flags} from '@oclif/core'
import {Ajv} from 'ajv'

let originalStdinIsTTY: boolean | undefined
Expand Down Expand Up @@ -54,6 +54,7 @@ class MockCommand extends Command {
default: 'default stringy',
}),
password: Flags.string({}),
'store-password': Flags.string({}),
environment: Flags.string({
multiple: true,
default: [],
Expand Down Expand Up @@ -220,6 +221,11 @@ const environmentWithPassword = {
password: 'password',
}

const environmentWithCredentials = {
password: 'admin-password',
'store-password': 'storefront-secret',
}

const allEnvironments: Environments = {
environments: {
validEnvironment,
Expand All @@ -232,6 +238,7 @@ const allEnvironments: Environments = {
environmentMatchingDefault,
environmentWithDefaultOverride,
environmentWithPassword,
environmentWithCredentials,
},
}

Expand Down Expand Up @@ -754,6 +761,31 @@ describe('applying environments', async () => {
`)
})

runTestInTmpDir('reports environment settings as JSON diagnostics with masked passwords', async (tmpDir) => {
const sink = vi.fn()
const output = mockAndCaptureOutput()
output.clear()
await runWithCommandEvents({outputMode: 'json', sink}, async () => {
const config = new Config({root: __dirname})
await config.load()
const command = new MockCommand(['--path', tmpDir, '--environment', 'environmentWithCredentials'], config)
await command.run()
})

expect(sink).toHaveBeenCalledWith(
expect.objectContaining({
type: 'diagnostic',
level: 'info',
message:
'Using applicable flags from environmentWithCredentials environment:\npassword: ********word\nstore-password: ********cret',
}),
)
for (const credential of Object.values(environmentWithCredentials)) {
expect(JSON.stringify(sink.mock.calls)).not.toContain(credential)
}
expect(output.info()).toBe('')
})

runTestInTmpDir('reports environment settings with masked passwords', async (tmpDir: string) => {
// Given
const outputMock = mockAndCaptureOutput()
Expand Down
18 changes: 15 additions & 3 deletions packages/cli-kit/src/public/node/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {isDevelopment} from './context/local.js'
import {addPublicMetadata} from './metadata.js'
import {AbortError} from './error.js'
import {runWithCommandEventsForCommand} from './command-events.js'
import {commandEventOutputMode, emitCommandEvent, runWithCommandEventsForCommand} from './command-events.js'
import {outputContent, outputResult, outputToken} from './output.js'
import {setCurrentSessionAlias} from './session.js'
import {terminalSupportsPrompting} from './system.js'
Expand Down Expand Up @@ -145,7 +145,7 @@ abstract class BaseCommand extends Command {
result = await this.resultWithEnvironment<TFlags, TGlobalFlags, TArgs>(result, options, argv)
await setCurrentSessionAlias(result.flags['auth-alias'])
await addFromParsedFlags(result.flags)
this.failMissingNonTTYFlagRequirements(result.flags, this.applicableNonTTYFlagRequirements(result.flags))
this.validateNonTTYFlags(result.flags)
return {...result, ...{argv: result.argv as string[]}}
}

Expand All @@ -154,6 +154,10 @@ abstract class BaseCommand extends Command {
return undefined
}

protected validateNonTTYFlags(flags: FlagOutput): void {
this.failMissingNonTTYFlagRequirements(flags, this.applicableNonTTYFlagRequirements(flags))
}

protected failMissingNonTTYFlags(flags: FlagOutput, requiredFlags: string[]): void {
this.failMissingNonTTYFlagRequirements(
flags,
Expand Down Expand Up @@ -322,13 +326,21 @@ function reportEnvironmentApplication<
const userSpecifiedThisFlag = Object.prototype.hasOwnProperty.call(noDefaultsFlags, name)
const environmentContainsFlag = Object.prototype.hasOwnProperty.call(environment, name)
if (!userSpecifiedThisFlag && environmentContainsFlag) {
const valueToReport = name === 'password' ? `********${value.substr(-4)}` : value
const valueToReport = name === 'password' || name === 'store-password' ? `********${value.substr(-4)}` : value
changes[name] = valueToReport
}
}
if (Object.keys(changes).length === 0) return

const items = Object.entries(changes).map(([name, value]) => `${name}: ${value}`)
if (commandEventOutputMode() === 'json') {
emitCommandEvent({
type: 'diagnostic',
level: 'info',
message: `Using applicable flags from ${environmentName} environment:\n${items.join('\n')}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: let's redact store-password before emitting these diagnostics too. The reporter only masks the exact password key, so an environment's storefront password is included in full in the JSON diagnostic message. theme profile --environment preview --json supports this field. The text banner already had this redaction gap, but this branch now sends the secret through the structured event channel as well. Redact both credential fields before building items, and extend the diagnostic test to check that the full storefront password never appears in emitted events.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both credential fields are now masked

})
return
}
// eslint-disable-next-line no-void
void import('./ui.js').then(({renderInfo}) => {
renderInfo({
Expand Down
100 changes: 100 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8022,6 +8022,106 @@ FLAGS
DESCRIPTION
Displays information about your theme environment, including your current store. Can also retrieve information about a
specific theme.

Use `--json-schema` to print the result, error, and event schemas.

Output from `--json` conforms to the `ThemeInfoResult` schema.

```json
{
"anyOf": [
{
"$ref": "#/definitions/ThemeInfoThemeResult"
},
{
"$ref": "#/definitions/ThemeEnvironmentInfo"
}
],
"title": "ThemeInfoResult",
"definitions": {
"ThemeInfoTheme": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"role": {
"type": "string"
},
"shop": {
"type": "string"
},
"preview_url": {
"type": "string"
},
"editor_url": {
"type": "string"
}
},
"required": [
"id",
"name",
"role",
"shop",
"preview_url",
"editor_url"
],
"additionalProperties": false
},
"ThemeInfoThemeResult": {
"type": "object",
"properties": {
"theme": {
"$ref": "#/definitions/ThemeInfoTheme"
}
},
"required": [
"theme"
],
"additionalProperties": false
},
"ThemeEnvironmentInfo": {
"type": "object",
"properties": {
"store": {
"type": "string"
},
"development_theme_id": {
"type": [
"number",
"null"
]
},
"cli_version": {
"type": "string"
},
"os": {
"type": "string"
},
"shell": {
"type": "string"
},
"node_version": {
"type": "string"
}
},
"required": [
"store",
"development_theme_id",
"cli_version",
"os",
"shell",
"node_version"
],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
```
```

## `shopify theme init [name] [flags]`
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10124,7 +10124,8 @@
"args": {
},
"customPluginName": "@shopify/theme",
"description": "Displays information about your theme environment, including your current store. Can also retrieve information about a specific theme.",
"description": "Displays information about your theme environment, including your current store. Can also retrieve information about a specific theme.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeInfoResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeInfoThemeResult\"\n },\n {\n \"$ref\": \"#/definitions/ThemeEnvironmentInfo\"\n }\n ],\n \"title\": \"ThemeInfoResult\",\n \"definitions\": {\n \"ThemeInfoTheme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"shop\": {\n \"type\": \"string\"\n },\n \"preview_url\": {\n \"type\": \"string\"\n },\n \"editor_url\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"shop\",\n \"preview_url\",\n \"editor_url\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeInfoThemeResult\": {\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"$ref\": \"#/definitions/ThemeInfoTheme\"\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironmentInfo\": {\n \"type\": \"object\",\n \"properties\": {\n \"store\": {\n \"type\": \"string\"\n },\n \"development_theme_id\": {\n \"type\": [\n \"number\",\n \"null\"\n ]\n },\n \"cli_version\": {\n \"type\": \"string\"\n },\n \"os\": {\n \"type\": \"string\"\n },\n \"shell\": {\n \"type\": \"string\"\n },\n \"node_version\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"store\",\n \"development_theme_id\",\n \"cli_version\",\n \"os\",\n \"shell\",\n \"node_version\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
"descriptionWithMarkdown": "Displays information about your theme environment, including your current store. Can also retrieve information about a specific theme.",
"enableJsonFlag": false,
"flags": {
"auth-alias": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const commandExceptions = [
'packages/theme/src/cli/commands/theme/check.ts',
'packages/theme/src/cli/commands/theme/delete.ts',
'packages/theme/src/cli/commands/theme/duplicate.ts',
'packages/theme/src/cli/commands/theme/info.ts',
'packages/theme/src/cli/commands/theme/init.ts',
'packages/theme/src/cli/commands/theme/list.ts',
'packages/theme/src/cli/commands/theme/metafields/pull.ts',
Expand Down
Loading
Loading