Skip to content
Draft
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
41 changes: 41 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3744,6 +3744,31 @@ DESCRIPTION

Re-run this command if the stored token is missing, expires, or no longer has the scopes you need.

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

Use `--json-schema` to print the schema directly:

```ts
interface StoreAuthResult {
store: string
userId: string
scopes: string[]
acquiredAt: string
expiresAt?: string
refreshTokenExpiresAt?: string
hasRefreshToken: boolean
associatedUser?: StoreAuthAssociatedUser
}

interface StoreAuthAssociatedUser {
id: number
email?: string
firstName?: string
lastName?: string
accountOwner?: boolean
}
```

EXAMPLES
$ shopify store auth --store shop.myshopify.com --scopes read_products,write_products

Expand Down Expand Up @@ -3783,6 +3808,22 @@ DESCRIPTION
Use this command to find stores that can be used with store-authenticated commands such as `shopify store execute`.
To list stores in a Shopify organization, run `shopify store list`.

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

Use `--json-schema` to print the schema directly:

```ts
interface StoreAuthListResult {
sessions: StoreAuthListSession[]
message?: string
}

interface StoreAuthListSession {
subdomain: string
connected: string
}
```

EXAMPLES
$ shopify store auth list

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6919,7 +6919,7 @@
"args": {
},
"customPluginName": "@shopify/store",
"description": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.",
"description": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.\n\nOutput from `--json` conforms to the `StoreAuthResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface StoreAuthResult {\n store: string\n userId: string\n scopes: string[]\n acquiredAt: string\n expiresAt?: string\n refreshTokenExpiresAt?: string\n hasRefreshToken: boolean\n associatedUser?: StoreAuthAssociatedUser\n}\n\ninterface StoreAuthAssociatedUser {\n id: number\n email?: string\n firstName?: string\n lastName?: string\n accountOwner?: boolean\n}\n```",
"descriptionWithMarkdown": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.",
"examples": [
"<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products",
Expand Down Expand Up @@ -6994,7 +6994,7 @@
"args": {
},
"customPluginName": "@shopify/store",
"description": "Lists stores authenticated directly on this machine with `shopify store auth`.\n\nUse this command to find stores that can be used with store-authenticated commands such as `shopify store execute`.\nTo list stores in a Shopify organization, run `shopify store list`.",
"description": "Lists stores authenticated directly on this machine with `shopify store auth`.\n\nUse this command to find stores that can be used with store-authenticated commands such as `shopify store execute`.\nTo list stores in a Shopify organization, run `shopify store list`.\n\nOutput from `--json` conforms to the `StoreAuthListResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface StoreAuthListResult {\n sessions: StoreAuthListSession[]\n message?: string\n}\n\ninterface StoreAuthListSession {\n subdomain: string\n connected: string\n}\n```",
"descriptionWithMarkdown": "Lists stores authenticated directly on this machine with `shopify store auth`.\n\nUse this command to find stores that can be used with store-authenticated commands such as `shopify store execute`.\nTo list stores in a Shopify organization, run `shopify store list`.",
"enableJsonFlag": false,
"examples": [
Expand Down
45 changes: 24 additions & 21 deletions packages/store/src/cli/commands/store/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
import StoreAuth from './auth.js'
import {authenticateStoreWithApp} from '../../services/store/auth/index.js'
import {createStoreAuthPresenter} from '../../services/store/auth/result.js'
import {presentStoreAuthResult} from '../../services/store/auth/result.js'
import {storeAuthJsonOutputSchema} from '../../services/store/auth/types.js'
import {describe, expect, test, vi} from 'vitest'

vi.mock('../../services/store/auth/index.js')
vi.mock('../../services/store/attribution.js')
vi.mock('../../services/store/auth/result.js', () => ({
createStoreAuthPresenter: vi.fn((format: 'text' | 'json') => ({format})),
}))
vi.mock('../../services/store/auth/result.js')

const authResult = {
store: 'shop.myshopify.com',
userId: '42',
scopes: ['read_products'],
acquiredAt: '2026-04-02T00:00:00.000Z',
hasRefreshToken: true,
}

describe('store auth command', () => {
test('passes parsed flags through to the auth service', async () => {
vi.mocked(authenticateStoreWithApp).mockResolvedValue(authResult)
await StoreAuth.run(['--store', 'shop.myshopify.com', '--scopes', 'read_products,write_products'])

expect(createStoreAuthPresenter).toHaveBeenCalledWith('text')
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
{
store: 'shop.myshopify.com',
scopes: 'read_products,write_products',
},
{presenter: {format: 'text'}},
)
expect(authenticateStoreWithApp).toHaveBeenCalledWith({
store: 'shop.myshopify.com',
scopes: 'read_products,write_products',
})
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'text')
})

test('passes a json presenter when --json is provided', async () => {
test('presents JSON when --json is provided', async () => {
vi.mocked(authenticateStoreWithApp).mockResolvedValue(authResult)
await StoreAuth.run(['--store', 'shop.myshopify.com', '--scopes', 'read_products', '--json'])

expect(createStoreAuthPresenter).toHaveBeenCalledWith('json')
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
{
store: 'shop.myshopify.com',
scopes: 'read_products',
},
{presenter: {format: 'json'}},
)
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'json')
})

test('defines the expected flags', () => {
Expand All @@ -44,4 +43,8 @@ describe('store auth command', () => {
expect('port' in StoreAuth.flags).toBe(false)
expect('client-secret-file' in StoreAuth.flags).toBe(false)
})

test('exposes the JSON output schema', () => {
expect(StoreAuth.jsonOutputSchema).toBe(storeAuthJsonOutputSchema)
})
})
21 changes: 11 additions & 10 deletions packages/store/src/cli/commands/store/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {authenticateStoreWithApp} from '../../services/store/auth/index.js'
import {createStoreAuthPresenter} from '../../services/store/auth/result.js'
import {presentStoreAuthResult} from '../../services/store/auth/result.js'
import {storeAuthJsonOutputSchema} from '../../services/store/auth/types.js'
import StoreCommand from '../../utilities/store-command.js'
import {storeFlags} from '../../flags.js'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
Expand Down Expand Up @@ -30,17 +31,17 @@ Re-run this command if the stored token is missing, expires, or no longer has th
}),
}

static get jsonOutputSchema() {
return storeAuthJsonOutputSchema
}

public async run(): Promise<void> {
const {flags} = await this.parse(StoreAuth)

await authenticateStoreWithApp(
{
store: flags.store,
scopes: flags.scopes,
},
{
presenter: createStoreAuthPresenter(flags.json ? 'json' : 'text'),
},
)
const result = await authenticateStoreWithApp({
store: flags.store,
scopes: flags.scopes,
})
presentStoreAuthResult(result, flags.json ? 'json' : 'text')
}
}
13 changes: 9 additions & 4 deletions packages/store/src/cli/commands/store/auth/list.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
import StoreAuthList from './list.js'
import {listStoreAuthSessions} from '../../../services/store/auth/list.js'
import {writeStoreAuthListResult} from '../../../services/store/auth/list-result.js'
import {storeAuthListJsonOutputSchema} from '../../../services/store/auth/list-types.js'
import {describe, expect, test, vi} from 'vitest'

vi.mock('../../../services/store/auth/list.js')
vi.mock('../../../services/store/auth/list-result.js')

describe('store auth list command', () => {
test('lists direct store-auth sessions and writes text output by default', async () => {
vi.mocked(listStoreAuthSessions).mockReturnValue({sessions: []})
vi.mocked(listStoreAuthSessions).mockReturnValue({sessions: [], message: 'No stores.'})

await StoreAuthList.run([])

expect(listStoreAuthSessions).toHaveBeenCalledWith()
expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: []}, 'text')
expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: [], message: 'No stores.'}, 'text')
})

test('writes json output when requested', async () => {
vi.mocked(listStoreAuthSessions).mockReturnValue({sessions: []})
vi.mocked(listStoreAuthSessions).mockReturnValue({sessions: [], message: 'No stores.'})

await StoreAuthList.run(['--json'])

expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: []}, 'json')
expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: [], message: 'No stores.'}, 'json')
})

test('does not expose organization or source-selection flags', () => {
expect(StoreAuthList.flags.json).toBeDefined()
expect(StoreAuthList.flags).not.toHaveProperty('organization-id')
expect(StoreAuthList.flags).not.toHaveProperty('from')
})

test('exposes the JSON output schema', () => {
expect(StoreAuthList.jsonOutputSchema).toBe(storeAuthListJsonOutputSchema)
})
})
5 changes: 5 additions & 0 deletions packages/store/src/cli/commands/store/auth/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {listStoreAuthSessions} from '../../../services/store/auth/list.js'
import {writeStoreAuthListResult} from '../../../services/store/auth/list-result.js'
import {storeAuthListJsonOutputSchema} from '../../../services/store/auth/list-types.js'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'

Expand All @@ -20,6 +21,10 @@ To list stores in a Shopify organization, run \`shopify store list\`.`
...jsonFlag,
}

static get jsonOutputSchema() {
return storeAuthListJsonOutputSchema
}

async run(): Promise<void> {
const {flags} = await this.parse(StoreAuthList)
const result = listStoreAuthSessions()
Expand Down
43 changes: 20 additions & 23 deletions packages/store/src/cli/commands/store/stripe-auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import StoreStripeAuth from './stripe-auth.js'
import {authenticateStoreWithApp} from '../../services/store/auth/index.js'
import {createStoreAuthPresenter} from '../../services/store/auth/result.js'
import {presentStoreAuthResult} from '../../services/store/auth/result.js'
import {describe, expect, test, vi} from 'vitest'

vi.mock('../../services/store/auth/index.js')
vi.mock('../../services/store/attribution.js')
vi.mock('../../services/store/auth/result.js', () => ({
createStoreAuthPresenter: vi.fn((format: 'text' | 'json') => ({format})),
}))
vi.mock('../../services/store/auth/result.js')

const authResult = {
store: 'shop.myshopify.com',
userId: '42',
scopes: ['read_products'],
acquiredAt: '2026-04-02T00:00:00.000Z',
hasRefreshToken: true,
}

describe('store stripe-auth command', () => {
test('passes signup JWT through to the auth service', async () => {
vi.mocked(authenticateStoreWithApp).mockResolvedValue(authResult)
await StoreStripeAuth.run([
'--store',
'shop.myshopify.com',
Expand All @@ -20,18 +27,16 @@ describe('store stripe-auth command', () => {
'signed.signup.jwt',
])

expect(createStoreAuthPresenter).toHaveBeenCalledWith('text')
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
{
store: 'shop.myshopify.com',
scopes: 'read_products',
signup: 'signed.signup.jwt',
},
{presenter: {format: 'text'}},
)
expect(authenticateStoreWithApp).toHaveBeenCalledWith({
store: 'shop.myshopify.com',
scopes: 'read_products',
signup: 'signed.signup.jwt',
})
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'text')
})

test('passes a json presenter when --json is provided', async () => {
test('presents JSON when --json is provided', async () => {
vi.mocked(authenticateStoreWithApp).mockResolvedValue(authResult)
await StoreStripeAuth.run([
'--store',
'shop.myshopify.com',
Expand All @@ -42,15 +47,7 @@ describe('store stripe-auth command', () => {
'--json',
])

expect(createStoreAuthPresenter).toHaveBeenCalledWith('json')
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
{
store: 'shop.myshopify.com',
scopes: 'read_products',
signup: 'signed.signup.jwt',
},
{presenter: {format: 'json'}},
)
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'json')
})

test('defines the expected flags', () => {
Expand Down
18 changes: 7 additions & 11 deletions packages/store/src/cli/commands/store/stripe-auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {authenticateStoreWithApp} from '../../services/store/auth/index.js'
import {createStoreAuthPresenter} from '../../services/store/auth/result.js'
import {presentStoreAuthResult} from '../../services/store/auth/result.js'
import StoreCommand from '../../utilities/store-command.js'
import {storeFlags} from '../../flags.js'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
Expand Down Expand Up @@ -38,15 +38,11 @@ export default class StoreStripeAuth extends StoreCommand {
public async run(): Promise<void> {
const {flags} = await this.parse(StoreStripeAuth)

await authenticateStoreWithApp(
{
store: flags.store,
scopes: flags.scopes,
signup: flags.signup,
},
{
presenter: createStoreAuthPresenter(flags.json ? 'json' : 'text'),
},
)
const result = await authenticateStoreWithApp({
store: flags.store,
scopes: flags.scopes,
signup: flags.signup,
})
presentStoreAuthResult(result, flags.json ? 'json' : 'text')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import {STORE_AUTH_APP_CLIENT_ID} from './config.js'
import {resolveExistingStoreAuthScopes} from './existing-scopes.js'
import {loadStoredStoreSession} from './session-lifecycle.js'
import {getCurrentStoredStoreAppSession} from '@shopify/cli-kit/node/store-auth-session'
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {adminUrl} from '@shopify/cli-kit/node/api/admin'
import {graphqlRequest} from '@shopify/cli-kit/node/api/graphql'
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'

vi.mock('@shopify/cli-kit/node/store-auth-session')
vi.mock('./session-lifecycle.js', () => ({loadStoredStoreSession: vi.fn()}))
Expand All @@ -25,10 +25,6 @@ describe('resolveExistingStoreAuthScopes', () => {
vi.mocked(adminUrl).mockReturnValue('https://shop.myshopify.com/admin/api/unstable/graphql.json')
})

afterEach(() => {
mockAndCaptureOutput().clear()
})

test('returns no scopes when no stored auth exists', async () => {
vi.mocked(getCurrentStoredStoreAppSession).mockReturnValue(undefined)

Expand Down
Loading
Loading