Skip to content

Commit 76fc2e8

Browse files
Add typed JSON output to store authentication
1 parent e737aa3 commit 76fc2e8

20 files changed

Lines changed: 367 additions & 454 deletions

‎packages/cli/README.md‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3764,6 +3764,81 @@ DESCRIPTION
37643764
37653765
Re-run this command if the stored token is missing, expires, or no longer has the scopes you need.
37663766
3767+
Output from `--json` conforms to the `StoreAuthResult` schema.
3768+
3769+
Use `--json-schema` to print the result, error, and event schemas.
3770+
3771+
```json
3772+
{
3773+
"type": "object",
3774+
"properties": {
3775+
"store": {
3776+
"type": "string"
3777+
},
3778+
"userId": {
3779+
"type": "string"
3780+
},
3781+
"scopes": {
3782+
"type": "array",
3783+
"items": {
3784+
"type": "string"
3785+
}
3786+
},
3787+
"acquiredAt": {
3788+
"type": "string"
3789+
},
3790+
"expiresAt": {
3791+
"type": "string"
3792+
},
3793+
"refreshTokenExpiresAt": {
3794+
"type": "string"
3795+
},
3796+
"hasRefreshToken": {
3797+
"type": "boolean"
3798+
},
3799+
"associatedUser": {
3800+
"$ref": "#/definitions/StoreAuthAssociatedUser"
3801+
}
3802+
},
3803+
"required": [
3804+
"store",
3805+
"userId",
3806+
"scopes",
3807+
"acquiredAt",
3808+
"hasRefreshToken"
3809+
],
3810+
"additionalProperties": false,
3811+
"title": "StoreAuthResult",
3812+
"definitions": {
3813+
"StoreAuthAssociatedUser": {
3814+
"type": "object",
3815+
"properties": {
3816+
"id": {
3817+
"type": "number"
3818+
},
3819+
"email": {
3820+
"type": "string"
3821+
},
3822+
"firstName": {
3823+
"type": "string"
3824+
},
3825+
"lastName": {
3826+
"type": "string"
3827+
},
3828+
"accountOwner": {
3829+
"type": "boolean"
3830+
}
3831+
},
3832+
"required": [
3833+
"id"
3834+
],
3835+
"additionalProperties": false
3836+
}
3837+
},
3838+
"$schema": "http://json-schema.org/draft-07/schema#"
3839+
}
3840+
```
3841+
37673842
EXAMPLES
37683843
$ shopify store auth --store shop.myshopify.com --scopes read_products,write_products
37693844

@@ -3803,6 +3878,51 @@ DESCRIPTION
38033878
Use this command to find stores that can be used with store-authenticated commands such as `shopify store execute`.
38043879
To list stores in a Shopify organization, run `shopify store list`.
38053880

3881+
Output from `--json` conforms to the `StoreAuthListResult` schema.
3882+
3883+
Use `--json-schema` to print the result, error, and event schemas.
3884+
3885+
```json
3886+
{
3887+
"type": "object",
3888+
"properties": {
3889+
"sessions": {
3890+
"type": "array",
3891+
"items": {
3892+
"$ref": "#/definitions/StoreAuthListSession"
3893+
}
3894+
},
3895+
"message": {
3896+
"type": "string"
3897+
}
3898+
},
3899+
"required": [
3900+
"sessions"
3901+
],
3902+
"additionalProperties": false,
3903+
"title": "StoreAuthListResult",
3904+
"definitions": {
3905+
"StoreAuthListSession": {
3906+
"type": "object",
3907+
"properties": {
3908+
"subdomain": {
3909+
"type": "string"
3910+
},
3911+
"connected": {
3912+
"type": "string"
3913+
}
3914+
},
3915+
"required": [
3916+
"subdomain",
3917+
"connected"
3918+
],
3919+
"additionalProperties": false
3920+
}
3921+
},
3922+
"$schema": "http://json-schema.org/draft-07/schema#"
3923+
}
3924+
```
3925+
38063926
EXAMPLES
38073927
$ shopify store auth list
38083928

‎packages/cli/oclif.manifest.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7877,7 +7877,7 @@
78777877
"args": {
78787878
},
78797879
"customPluginName": "@shopify/store",
7880-
"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.",
7880+
"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 result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"store\": {\n \"type\": \"string\"\n },\n \"userId\": {\n \"type\": \"string\"\n },\n \"scopes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"acquiredAt\": {\n \"type\": \"string\"\n },\n \"expiresAt\": {\n \"type\": \"string\"\n },\n \"refreshTokenExpiresAt\": {\n \"type\": \"string\"\n },\n \"hasRefreshToken\": {\n \"type\": \"boolean\"\n },\n \"associatedUser\": {\n \"$ref\": \"#/definitions/StoreAuthAssociatedUser\"\n }\n },\n \"required\": [\n \"store\",\n \"userId\",\n \"scopes\",\n \"acquiredAt\",\n \"hasRefreshToken\"\n ],\n \"additionalProperties\": false,\n \"title\": \"StoreAuthResult\",\n \"definitions\": {\n \"StoreAuthAssociatedUser\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"email\": {\n \"type\": \"string\"\n },\n \"firstName\": {\n \"type\": \"string\"\n },\n \"lastName\": {\n \"type\": \"string\"\n },\n \"accountOwner\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"id\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
78817881
"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.",
78827882
"examples": [
78837883
"<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products",
@@ -7952,7 +7952,7 @@
79527952
"args": {
79537953
},
79547954
"customPluginName": "@shopify/store",
7955-
"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`.",
7955+
"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 result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"sessions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/StoreAuthListSession\"\n }\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"sessions\"\n ],\n \"additionalProperties\": false,\n \"title\": \"StoreAuthListResult\",\n \"definitions\": {\n \"StoreAuthListSession\": {\n \"type\": \"object\",\n \"properties\": {\n \"subdomain\": {\n \"type\": \"string\"\n },\n \"connected\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"subdomain\",\n \"connected\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
79567956
"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`.",
79577957
"enableJsonFlag": false,
79587958
"examples": [
Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,38 @@
11
import StoreAuth from './auth.js'
22
import {authenticateStoreWithApp} from '../../services/store/auth/index.js'
3-
import {createStoreAuthPresenter} from '../../services/store/auth/result.js'
3+
import {presentStoreAuthResult} from '../../services/store/auth/result.js'
4+
import {storeAuthJsonOutputSchema} from '../../services/store/auth/types.js'
45
import {describe, expect, test, vi} from 'vitest'
56

67
vi.mock('../../services/store/auth/index.js')
78
vi.mock('../../services/store/attribution.js')
8-
vi.mock('../../services/store/auth/result.js', () => ({
9-
createStoreAuthPresenter: vi.fn((format: 'text' | 'json') => ({format})),
10-
}))
9+
vi.mock('../../services/store/auth/result.js')
10+
11+
const authResult = {
12+
store: 'shop.myshopify.com',
13+
userId: '42',
14+
scopes: ['read_products'],
15+
acquiredAt: '2026-04-02T00:00:00.000Z',
16+
hasRefreshToken: true,
17+
}
1118

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

16-
expect(createStoreAuthPresenter).toHaveBeenCalledWith('text')
17-
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
18-
{
19-
store: 'shop.myshopify.com',
20-
scopes: 'read_products,write_products',
21-
},
22-
{presenter: {format: 'text'}},
23-
)
24+
expect(authenticateStoreWithApp).toHaveBeenCalledWith({
25+
store: 'shop.myshopify.com',
26+
scopes: 'read_products,write_products',
27+
})
28+
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'text')
2429
})
2530

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

29-
expect(createStoreAuthPresenter).toHaveBeenCalledWith('json')
30-
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
31-
{
32-
store: 'shop.myshopify.com',
33-
scopes: 'read_products',
34-
},
35-
{presenter: {format: 'json'}},
36-
)
35+
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'json')
3736
})
3837

3938
test('defines the expected flags', () => {
@@ -44,4 +43,8 @@ describe('store auth command', () => {
4443
expect('port' in StoreAuth.flags).toBe(false)
4544
expect('client-secret-file' in StoreAuth.flags).toBe(false)
4645
})
46+
47+
test('exposes the JSON output schema', () => {
48+
expect(StoreAuth.jsonOutputSchema).toBe(storeAuthJsonOutputSchema)
49+
})
4750
})

‎packages/store/src/cli/commands/store/auth.ts‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {authenticateStoreWithApp} from '../../services/store/auth/index.js'
2-
import {createStoreAuthPresenter} from '../../services/store/auth/result.js'
2+
import {presentStoreAuthResult} from '../../services/store/auth/result.js'
3+
import {storeAuthJsonOutputSchema} from '../../services/store/auth/types.js'
34
import StoreCommand from '../../utilities/store-command.js'
45
import {storeFlags} from '../../flags.js'
56
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
@@ -30,17 +31,17 @@ Re-run this command if the stored token is missing, expires, or no longer has th
3031
}),
3132
}
3233

34+
static get jsonOutputSchema() {
35+
return storeAuthJsonOutputSchema
36+
}
37+
3338
public async run(): Promise<void> {
3439
const {flags} = await this.parse(StoreAuth)
3540

36-
await authenticateStoreWithApp(
37-
{
38-
store: flags.store,
39-
scopes: flags.scopes,
40-
},
41-
{
42-
presenter: createStoreAuthPresenter(flags.json ? 'json' : 'text'),
43-
},
44-
)
41+
const result = await authenticateStoreWithApp({
42+
store: flags.store,
43+
scopes: flags.scopes,
44+
})
45+
presentStoreAuthResult(result, flags.json ? 'json' : 'text')
4546
}
4647
}
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
import StoreAuthList from './list.js'
22
import {listStoreAuthSessions} from '../../../services/store/auth/list.js'
33
import {writeStoreAuthListResult} from '../../../services/store/auth/list-result.js'
4+
import {storeAuthListJsonOutputSchema} from '../../../services/store/auth/list-types.js'
45
import {describe, expect, test, vi} from 'vitest'
56

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

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

1314
await StoreAuthList.run([])
1415

1516
expect(listStoreAuthSessions).toHaveBeenCalledWith()
16-
expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: []}, 'text')
17+
expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: [], message: 'No stores.'}, 'text')
1718
})
1819

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

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

24-
expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: []}, 'json')
25+
expect(writeStoreAuthListResult).toHaveBeenCalledWith({sessions: [], message: 'No stores.'}, 'json')
2526
})
2627

2728
test('does not expose organization or source-selection flags', () => {
2829
expect(StoreAuthList.flags.json).toBeDefined()
2930
expect(StoreAuthList.flags).not.toHaveProperty('organization-id')
3031
expect(StoreAuthList.flags).not.toHaveProperty('from')
3132
})
33+
34+
test('exposes the JSON output schema', () => {
35+
expect(StoreAuthList.jsonOutputSchema).toBe(storeAuthListJsonOutputSchema)
36+
})
3237
})

‎packages/store/src/cli/commands/store/auth/list.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {listStoreAuthSessions} from '../../../services/store/auth/list.js'
22
import {writeStoreAuthListResult} from '../../../services/store/auth/list-result.js'
3+
import {storeAuthListJsonOutputSchema} from '../../../services/store/auth/list-types.js'
34
import Command from '@shopify/cli-kit/node/base-command'
45
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
56

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

24+
static get jsonOutputSchema() {
25+
return storeAuthListJsonOutputSchema
26+
}
27+
2328
async run(): Promise<void> {
2429
const {flags} = await this.parse(StoreAuthList)
2530
const result = listStoreAuthSessions()

‎packages/store/src/cli/commands/store/stripe-auth.test.ts‎

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import StoreStripeAuth, {readSignupJwtFromStdin} from './stripe-auth.js'
22
import {authenticateStoreWithApp} from '../../services/store/auth/index.js'
3-
import {createStoreAuthPresenter} from '../../services/store/auth/result.js'
3+
import {presentStoreAuthResult} from '../../services/store/auth/result.js'
44
import {describe, expect, test, vi} from 'vitest'
55
import {Readable} from 'stream'
66

77
vi.mock('../../services/store/auth/index.js')
88
vi.mock('../../services/store/attribution.js')
9-
vi.mock('../../services/store/auth/result.js', () => ({
10-
createStoreAuthPresenter: vi.fn((format: 'text' | 'json') => ({format})),
11-
}))
9+
vi.mock('../../services/store/auth/result.js')
10+
11+
const authResult = {
12+
store: 'shop.myshopify.com',
13+
userId: '42',
14+
scopes: ['read_products'],
15+
acquiredAt: '2026-04-02T00:00:00.000Z',
16+
hasRefreshToken: true,
17+
}
1218

1319
describe('store stripe-auth command', () => {
1420
test('passes signup JWT through to the auth service', async () => {
21+
vi.mocked(authenticateStoreWithApp).mockResolvedValue(authResult)
1522
await StoreStripeAuth.run([
1623
'--store',
1724
'shop.myshopify.com',
@@ -21,18 +28,16 @@ describe('store stripe-auth command', () => {
2128
'signed.signup.jwt',
2229
])
2330

24-
expect(createStoreAuthPresenter).toHaveBeenCalledWith('text')
25-
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
26-
{
27-
store: 'shop.myshopify.com',
28-
scopes: 'read_products',
29-
signup: 'signed.signup.jwt',
30-
},
31-
{presenter: {format: 'text'}},
32-
)
31+
expect(authenticateStoreWithApp).toHaveBeenCalledWith({
32+
store: 'shop.myshopify.com',
33+
scopes: 'read_products',
34+
signup: 'signed.signup.jwt',
35+
})
36+
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'text')
3337
})
3438

35-
test('passes a json presenter when --json is provided', async () => {
39+
test('presents JSON when --json is provided', async () => {
40+
vi.mocked(authenticateStoreWithApp).mockResolvedValue(authResult)
3641
await StoreStripeAuth.run([
3742
'--store',
3843
'shop.myshopify.com',
@@ -43,15 +48,7 @@ describe('store stripe-auth command', () => {
4348
'--json',
4449
])
4550

46-
expect(createStoreAuthPresenter).toHaveBeenCalledWith('json')
47-
expect(authenticateStoreWithApp).toHaveBeenCalledWith(
48-
{
49-
store: 'shop.myshopify.com',
50-
scopes: 'read_products',
51-
signup: 'signed.signup.jwt',
52-
},
53-
{presenter: {format: 'json'}},
54-
)
51+
expect(presentStoreAuthResult).toHaveBeenCalledWith(authResult, 'json')
5552
})
5653

5754
test('defines the expected flags', () => {

0 commit comments

Comments
 (0)