From 078677972689ed5ccee246e6232fa9e5a9e6781a Mon Sep 17 00:00:00 2001 From: River Date: Wed, 14 Oct 2026 20:10:05 +0000 Subject: [PATCH 1/2] Report service_account_token as the auth method for the service account token CLI analytics report `env_auth_method: partners_token` for every automation token, so commands run with SHOPIFY_SERVICE_ACCOUNT_TOKEN can't be told apart from app and Partner tokens. Since 4.9 the token variable names are no longer sent in `env_shopify_variables`, so this field is the only place the token type can show up. While SHOPIFY_SERVICE_ACCOUNT_TOKEN is the selected variable, report `service_account_token`, both after a token exchange and in the fallback that reads the environment. SHOPIFY_APP_AUTOMATION_TOKEN and SHOPIFY_CLI_PARTNERS_TOKEN keep reporting `partners_token`, so existing dashboards don't change. The value comes from `automationTokenAuthMethod()`, next to the variable selection in automation-token.ts. Co-authored-by: Eddie Gao --- .../cli-kit/src/private/node/session.test.ts | 19 ++++++++++++++++- packages/cli-kit/src/private/node/session.ts | 15 +++++++++---- .../node/session/automation-token.test.ts | 21 ++++++++++++++++++- .../private/node/session/automation-token.ts | 14 +++++++++++++ .../src/private/node/session/exchange.test.ts | 20 +++++++++++++++++- .../src/private/node/session/exchange.ts | 3 ++- 6 files changed, 84 insertions(+), 8 deletions(-) diff --git a/packages/cli-kit/src/private/node/session.test.ts b/packages/cli-kit/src/private/node/session.test.ts index b18a6a85452..cf0d9fc6445 100644 --- a/packages/cli-kit/src/private/node/session.test.ts +++ b/packages/cli-kit/src/private/node/session.test.ts @@ -20,7 +20,11 @@ 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 {automationTokenVariable, automationTokenVariablesProblem} from './session/automation-token.js' +import { + automationTokenAuthMethod, + automationTokenVariable, + automationTokenVariablesProblem, +} from './session/automation-token.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' @@ -660,6 +664,7 @@ describe('getLastSeenAuthMethod', () => { test('returns partners_token if there is a partners token in the environment', async () => { // Given vi.mocked(getAppAutomationToken).mockReturnValue('partners-token-456') + vi.mocked(automationTokenAuthMethod).mockReturnValue('partners_token') // When const method = await getLastSeenAuthMethod() @@ -669,6 +674,18 @@ describe('getLastSeenAuthMethod', () => { expect(getCurrentSessionId).toHaveBeenCalledOnce() }) + test('returns service_account_token if the service account token is in the environment', async () => { + // Given + vi.mocked(getAppAutomationToken).mockReturnValue('service-account-token-789') + vi.mocked(automationTokenAuthMethod).mockReturnValue('service_account_token') + + // When + const method = await getLastSeenAuthMethod() + + // Then + expect(method).toBe('service_account_token') + }) + test('returns custom_app_token if there is a theme token in the environment and doesnt start with shptka_', async () => { // Given vi.mocked(themeToken).mockReturnValue('theme-token-123') diff --git a/packages/cli-kit/src/private/node/session.ts b/packages/cli-kit/src/private/node/session.ts index aec2781a0c1..d8562e6c17f 100644 --- a/packages/cli-kit/src/private/node/session.ts +++ b/packages/cli-kit/src/private/node/session.ts @@ -12,7 +12,7 @@ import { import {IdentityToken, Session, Sessions} from './session/schema.js' import * as sessionStore from './session/store.js' import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js' -import {automationTokenVariablesProblem} from './session/automation-token.js' +import {automationTokenAuthMethod, automationTokenVariablesProblem} from './session/automation-token.js' import {isThemeAccessSession} from './api/rest.js' import {getCurrentSessionId, setCurrentSessionId} from './conf-store.js' import {UserEmailQueryString, UserEmailQuery} from './api/graphql/business-platform-destinations/user-email.js' @@ -115,7 +115,13 @@ export interface OAuthSession { userId: string } -type AuthMethod = 'partners_token' | 'device_auth' | 'theme_access_token' | 'custom_app_token' | 'none' +type AuthMethod = + | 'partners_token' + | 'service_account_token' + | 'device_auth' + | 'theme_access_token' + | 'custom_app_token' + | 'none' let userId: undefined | string let authMethod: AuthMethod = 'none' @@ -155,7 +161,8 @@ export function setLastSeenUserIdAfterAuth(id: string) { * This function checks for the authentication method in the following order: * 1. Returns the cached auth method if it's not 'none'. * 2. Checks for a cached session, which implies 'device_auth' was used. - * 3. Checks for a partners token in the environment. + * 3. Checks for an automation token in the environment: `service_account_token` for SHOPIFY_SERVICE_ACCOUNT_TOKEN, + * `partners_token` for the app and Partner variables. * 4. Checks for a theme password in the environment. * 5. If none of the above are true, returns 'none'. * @@ -167,7 +174,7 @@ export async function getLastSeenAuthMethod(): Promise { if (getCurrentSessionId()) return 'device_auth' const appAutomationToken = getAppAutomationToken() - if (appAutomationToken) return 'partners_token' + if (appAutomationToken) return automationTokenAuthMethod() const themePassword = themeToken() if (themePassword) { diff --git a/packages/cli-kit/src/private/node/session/automation-token.test.ts b/packages/cli-kit/src/private/node/session/automation-token.test.ts index 5e630ec565f..3769e50fba1 100644 --- a/packages/cli-kit/src/private/node/session/automation-token.test.ts +++ b/packages/cli-kit/src/private/node/session/automation-token.test.ts @@ -1,4 +1,8 @@ -import {automationTokenVariable, automationTokenVariablesProblem} from './automation-token.js' +import { + automationTokenAuthMethod, + automationTokenVariable, + automationTokenVariablesProblem, +} from './automation-token.js' import {environmentVariables} from '../constants.js' import {describe, expect, test} from 'vitest' @@ -28,6 +32,21 @@ describe('automationTokenVariable', () => { }) }) +describe('automationTokenAuthMethod', () => { + test.each([ + {name: 'the service account variable', env: {[SERVICE_ACCOUNT]: 's'}, expected: 'service_account_token'}, + {name: 'the app variable', env: {[APP]: 'a'}, expected: 'partners_token'}, + {name: 'the Partners variable', env: {[PARTNERS]: 'p'}, expected: 'partners_token'}, + { + name: 'the service account variable over another one', + env: {[SERVICE_ACCOUNT]: 's', [APP]: 'a'}, + expected: 'service_account_token', + }, + ])('reports $expected for $name', ({env, expected}) => { + expect(automationTokenAuthMethod(env)).toBe(expected) + }) +}) + describe('automationTokenVariablesProblem', () => { test.each([ {name: 'no automation token variable', env: {}}, diff --git a/packages/cli-kit/src/private/node/session/automation-token.ts b/packages/cli-kit/src/private/node/session/automation-token.ts index 63825990423..e6ebd58a53d 100644 --- a/packages/cli-kit/src/private/node/session/automation-token.ts +++ b/packages/cli-kit/src/private/node/session/automation-token.ts @@ -22,6 +22,20 @@ export function automationTokenVariable(env: NodeJS.ProcessEnv = process.env): s return [SERVICE_ACCOUNT_TOKEN, APP_TOKEN, PARTNERS_TOKEN].find((name) => env[name] !== undefined) } +/** + * Returns the `env_auth_method` analytics value for the automation token variable the CLI authenticates with: + * `service_account_token` for SHOPIFY_SERVICE_ACCOUNT_TOKEN, and `partners_token` for SHOPIFY_APP_AUTOMATION_TOKEN and + * SHOPIFY_CLI_PARTNERS_TOKEN, which existing dashboards already expect. + * + * @param env - Environment variables to read. + * @returns The auth method to report. + */ +export function automationTokenAuthMethod( + env: NodeJS.ProcessEnv = process.env, +): 'service_account_token' | 'partners_token' { + return automationTokenVariable(env) === SERVICE_ACCOUNT_TOKEN ? 'service_account_token' : 'partners_token' +} + /** * Explains why the automation token variables can't be used, so a misconfigured environment fails instead of * falling back to the logged-in user. diff --git a/packages/cli-kit/src/private/node/session/exchange.test.ts b/packages/cli-kit/src/private/node/session/exchange.test.ts index 3114970836b..24e09cdaa68 100644 --- a/packages/cli-kit/src/private/node/session/exchange.test.ts +++ b/packages/cli-kit/src/private/node/session/exchange.test.ts @@ -18,7 +18,7 @@ import {AbortError} from '../../../public/node/error.js' import {outputDebug} from '../../../public/node/output.js' import {err, ok} from '../../../public/node/result.js' -import {describe, test, expect, vi, afterAll, beforeEach} from 'vitest' +import {describe, test, expect, vi, afterAll, afterEach, beforeEach} from 'vitest' import {Response} from 'node-fetch' const currentDate = new Date(2022, 1, 1, 10) @@ -275,6 +275,10 @@ describe.each(tokenExchangeMethods)( const grantType = 'urn:ietf:params:oauth:grant-type:token-exchange' const accessTokenType = 'urn:ietf:params:oauth:token-type:access_token' + afterEach(() => { + vi.unstubAllEnvs() + }) + test(`Executing ${tokenExchangeMethod.name} returns access token and user ID for a valid CLI token`, async () => { // Given let capturedUrl = '' @@ -325,6 +329,20 @@ describe.each(tokenExchangeMethods)( expect(params.get('subject_token')).toBe(automationToken) }) + test(`Executing ${tokenExchangeMethod.name} reports service_account_token when SHOPIFY_SERVICE_ACCOUNT_TOKEN is used`, async () => { + // Given + vi.stubEnv('SHOPIFY_SERVICE_ACCOUNT_TOKEN', automationToken) + vi.mocked(shopifyFetch).mockResolvedValue( + new Response(JSON.stringify({access_token: 'expected_access_token', expires_in: 300, scope: 'scope'})), + ) + + // When + await tokenExchangeMethod(automationToken) + + // Then + await expect(getLastSeenAuthMethod()).resolves.toBe('service_account_token') + }) + test(`Executing ${tokenExchangeMethod.name} throws AbortError and logs the caught error if an error is caught`, async () => { const expectedErrorMessage = `The custom token provided can't be used for the ${expectedErrorName} API.` vi.mocked(shopifyFetch).mockImplementation(async () => { diff --git a/packages/cli-kit/src/private/node/session/exchange.ts b/packages/cli-kit/src/private/node/session/exchange.ts index a0c75fd1ffa..8bf24e513d5 100644 --- a/packages/cli-kit/src/private/node/session/exchange.ts +++ b/packages/cli-kit/src/private/node/session/exchange.ts @@ -1,6 +1,7 @@ import {ApplicationToken, IdentityToken} from './schema.js' import {applicationId, clientId as getIdentityClientId} from './identity.js' import {tokenExchangeScopes} from './scopes.js' +import {automationTokenAuthMethod} from './automation-token.js' import {API} from '../api.js' import {identityFqdn} from '../../../public/node/context/fqdn.js' import {shopifyFetch} from '../../../public/node/http.js' @@ -89,7 +90,7 @@ async function exchangeAppAutomationTokenForAccessToken( const accessToken = newToken[appId]!.accessToken const userId = nonRandomUUID(token) setLastSeenUserIdAfterAuth(userId) - setLastSeenAuthMethod('partners_token') + setLastSeenAuthMethod(automationTokenAuthMethod()) return {accessToken, userId} } catch (error) { const prettyName = apiName.replace(/-/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) From d7cafe58187c45b9f331a7dcf5edcd9071625be7 Mon Sep 17 00:00:00 2001 From: River Date: Thu, 15 Oct 2026 13:25:27 +0000 Subject: [PATCH 2/2] Say that the auth method reflects the variable, not the kind of token The CLI can't tell token kinds apart, and any token works in any of the automation variables, so `service_account_token` means the token came from SHOPIFY_SERVICE_ACCOUNT_TOKEN. Say so in automationTokenAuthMethod's doc comment. Co-authored-by: Eddie Gao --- packages/cli-kit/src/private/node/session/automation-token.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli-kit/src/private/node/session/automation-token.ts b/packages/cli-kit/src/private/node/session/automation-token.ts index e6ebd58a53d..53b4ee4f86a 100644 --- a/packages/cli-kit/src/private/node/session/automation-token.ts +++ b/packages/cli-kit/src/private/node/session/automation-token.ts @@ -27,6 +27,9 @@ export function automationTokenVariable(env: NodeJS.ProcessEnv = process.env): s * `service_account_token` for SHOPIFY_SERVICE_ACCOUNT_TOKEN, and `partners_token` for SHOPIFY_APP_AUTOMATION_TOKEN and * SHOPIFY_CLI_PARTNERS_TOKEN, which existing dashboards already expect. * + * The value tells which variable held the token, not which kind of token it is: the CLI can't tell tokens apart, and + * any of them works in any of the variables. It shows how much usage has moved to SHOPIFY_SERVICE_ACCOUNT_TOKEN. + * * @param env - Environment variables to read. * @returns The auth method to report. */