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
6 changes: 6 additions & 0 deletions .changeset/theme-account-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/cli-kit': patch
'@shopify/theme': patch
---

Allow theme commands to authenticate with a Shopify account alias without changing the current global CLI session.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themeconsole {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themedelete {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* Delete your development theme.
* @environment SHOPIFY_FLAG_DEVELOPMENT
Expand Down
6 changes: 6 additions & 0 deletions docs-shopify.dev/commands/interfaces/theme-dev.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themedev {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* Allow development on a live theme.
* @environment SHOPIFY_FLAG_ALLOW_LIVE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themeduplicate {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themeinfo {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* Retrieve info from your development theme.
* @environment SHOPIFY_FLAG_DEVELOPMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themelist {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface thememetafieldspull {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themeopen {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* Open your development theme.
* @environment SHOPIFY_FLAG_DEVELOPMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themepreview {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themeprofile {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themepublish {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themepull {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* Pull theme files from your remote development theme.
* @environment SHOPIFY_FLAG_DEVELOPMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themepush {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* Allow push to a live theme.
* @environment SHOPIFY_FLAG_ALLOW_LIVE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themerename {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* Rename your development theme.
* @environment SHOPIFY_FLAG_DEVELOPMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* @publicDocs
*/
export interface themeshare {
/**
* Alias of the Shopify account to use for authentication.
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
165 changes: 150 additions & 15 deletions docs-shopify.dev/generated/generated_docs_data_v2.json

Large diffs are not rendered by default.

54 changes: 53 additions & 1 deletion packages/cli-kit/src/private/node/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {ApplicationToken, IdentityToken, Sessions} from './session/schema.js'
import {validateSession} from './session/validate.js'
import {applicationId} from './session/identity.js'
import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js'
import {getCurrentSessionId} from './conf-store.js'
import {getCurrentSessionId, setCurrentSessionId} from './conf-store.js'
import * as fqdnModule from '../../public/node/context/fqdn.js'
import {themeToken} from '../../public/node/context/local.js'
import {partnersRequest} from '../../public/node/api/partners.js'
Expand Down Expand Up @@ -313,6 +313,33 @@ describe('when existing session is valid', () => {
expect(fetchSessions).toHaveBeenCalledOnce()
})

test('uses an explicitly selected session without reading the current session ID', async () => {
// Given
const selectedUserId = 'selected-user-id'
const sessions: Sessions = {
[fqdn]: {
[userId]: {
identity: validIdentityToken,
applications: {},
},
[selectedUserId]: {
identity: {...validIdentityToken, userId: selectedUserId},
applications: appTokens,
},
},
}
vi.mocked(validateSession).mockResolvedValueOnce('ok')
vi.mocked(fetchSessions).mockResolvedValue(sessions)

// When
const got = await ensureAuthenticated(defaultApplications, process.env, {sessionId: selectedUserId})

// Then
expect(getCurrentSessionId).not.toHaveBeenCalled()
expect(validateSession).toHaveBeenCalledWith(expect.any(Array), expect.any(Object), sessions[fqdn]![selectedUserId])
expect(got).toEqual({...validTokens, userId: selectedUserId})
})

test('overwrites partners token if provided with a custom CLI token', async () => {
// Given
vi.mocked(validateSession).mockResolvedValueOnce('ok')
Expand Down Expand Up @@ -350,6 +377,31 @@ describe('when existing session is valid', () => {
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
expect(fetchSessions).toHaveBeenCalledOnce()
})

test('refreshes an explicitly selected session without changing the current session ID', async () => {
// Given
const selectedUserId = 'selected-user-id'
const sessions: Sessions = {
[fqdn]: {
[selectedUserId]: {
identity: {...validIdentityToken, userId: selectedUserId},
applications: appTokens,
},
},
}
vi.mocked(validateSession).mockResolvedValueOnce('needs_refresh')
vi.mocked(fetchSessions).mockResolvedValue(sessions)
vi.mocked(refreshAccessToken).mockResolvedValueOnce({...validIdentityToken, userId: selectedUserId})

// When
const got = await ensureAuthenticated(defaultApplications, process.env, {sessionId: selectedUserId})

// Then
expect(refreshAccessToken).toHaveBeenCalled()
expect(storeSessions).toHaveBeenCalledWith(sessions)
expect(setCurrentSessionId).not.toHaveBeenCalled()
expect(got).toEqual({...validTokens, userId: selectedUserId})
})
})

describe('when existing session is expired', () => {
Expand Down
14 changes: 10 additions & 4 deletions packages/cli-kit/src/private/node/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export interface EnsureAuthenticatedAdditionalOptions {
noPrompt?: boolean
forceRefresh?: boolean
forceNewSession?: boolean
sessionId?: string
}

/**
Expand All @@ -196,7 +197,12 @@ export interface EnsureAuthenticatedAdditionalOptions {
export async function ensureAuthenticated(
applications: OAuthApplications,
_env?: NodeJS.ProcessEnv,
{forceRefresh = false, noPrompt = false, forceNewSession = false}: EnsureAuthenticatedAdditionalOptions = {},
{
forceRefresh = false,
noPrompt = false,
forceNewSession = false,
sessionId,
}: EnsureAuthenticatedAdditionalOptions = {},
): Promise<OAuthSession> {
const fqdn = await identityFqdn()

Expand All @@ -210,8 +216,8 @@ export async function ensureAuthenticated(

const sessions = (await sessionStore.fetch()) ?? {}

let currentSessionId = getCurrentSessionId()
if (!currentSessionId) {
let currentSessionId = forceNewSession ? undefined : (sessionId ?? getCurrentSessionId())
if (!currentSessionId && !sessionId) {
const userIds = Object.keys(sessions[fqdn] ?? {})
if (userIds.length > 0) currentSessionId = userIds[0]
}
Expand Down Expand Up @@ -260,7 +266,7 @@ ${outputToken.json(applications)}
// Save the new session info if it has changed
if (!isEmpty(newSession)) {
await sessionStore.store(updatedSessions)
setCurrentSessionId(newSessionId)
if (!sessionId) setCurrentSessionId(newSessionId)
}

const tokens = await tokensFor(applications, completeSession)
Expand Down
34 changes: 34 additions & 0 deletions packages/cli-kit/src/public/node/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
ensureAuthenticatedPartners,
ensureAuthenticatedStorefront,
ensureAuthenticatedThemes,
findSessionIdByAlias,
setLastSeenUserId,
} from './session.js'

import {getAppAutomationToken} from './environment.js'
import {shopifyFetch} from './http.js'
import {ensureAuthenticated, setLastSeenAuthMethod, setLastSeenUserIdAfterAuth} from '../../private/node/session.js'
import * as sessionStore from '../../private/node/session/store.js'
import {ApplicationToken} from '../../private/node/session/schema.js'
import {
exchangeCustomPartnerToken,
Expand All @@ -31,6 +33,7 @@ const partnersToken: ApplicationToken = {

vi.mock('../../private/node/session.js')
vi.mock('../../private/node/session/exchange.js')
vi.mock('../../private/node/session/store.js')
vi.mock('./environment.js')
vi.mock('./http.js')

Expand All @@ -42,6 +45,20 @@ describe('store command analytics session helpers', () => {
})
})

describe('findSessionIdByAlias', () => {
test('returns the matching session ID without selecting it', async () => {
// Given
vi.mocked(sessionStore.findSessionByAlias).mockResolvedValueOnce('user-id')

// When
const got = await findSessionIdByAlias('work')

// Then
expect(got).toEqual('user-id')
expect(sessionStore.findSessionByAlias).toHaveBeenCalledWith('work')
})
})

describe('ensureAuthenticatedStorefront', () => {
test('returns only storefront token if success', async () => {
// Given
Expand Down Expand Up @@ -172,6 +189,23 @@ describe('ensureAuthenticatedTheme', () => {
expect(setLastSeenUserIdAfterAuth).not.toBeCalled()
})

test('passes additional auth options through to the shared authenticator', async () => {
// Given
vi.mocked(ensureAuthenticated).mockResolvedValueOnce({
admin: {token: 'admin_token', storeFqdn: 'mystore.myshopify.com'},
userId: '1234-5678',
})

// When
const got = await ensureAuthenticatedThemes('mystore', undefined, [], {sessionId: 'user-id'})

// Then
expect(got).toEqual({token: 'admin_token', storeFqdn: 'mystore.myshopify.com', sessionId: '1234-5678'})
expect(ensureAuthenticated).toHaveBeenCalledWith({adminApi: {scopes: [], storeFqdn: 'mystore'}}, process.env, {
sessionId: 'user-id',
})
})

test('throws error if there is no token when no password is provided', async () => {
// Given
vi.mocked(ensureAuthenticated).mockResolvedValueOnce({userId: ''})
Expand Down
Loading
Loading