Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
ba01d1d to
3b51cc5
Compare
|
We detected some changes at Caution DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release. |
There was a problem hiding this comment.
Pull request overview
This PR changes shopify store auth to behave additively by preserving existing app-install scopes when requesting additional scopes, preferring remote (store-reported) scopes when available and falling back to locally cached scopes when not.
Changes:
- Added a shared stored-session loader/refresh module and reused it across store services.
- Implemented scope reconciliation for
store authby merging newly requested scopes with existing scopes (remote-first, local fallback). - Added tests covering remote-scope resolution, fallback behavior, and additive scope requests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
packages/cli/src/cli/services/store/stored-session.ts |
New shared helper to load/refresh a stored store app session. |
packages/cli/src/cli/services/store/auth.ts |
Resolves existing scopes (remote-first), merges requested scopes additively, and adjusts validation/persistence behavior. |
packages/cli/src/cli/services/store/auth.test.ts |
Adds unit tests for scope resolution and additive auth behavior. |
packages/cli/src/cli/services/store/admin-graphql-context.ts |
Refactors to use the shared stored-session loader instead of owning refresh logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // write_* -> read_* permissions as satisfied, so callers should not assume | ||
| // session.scopes is an expanded/effective permission set. | ||
| scopes: resolveGrantedScopes(tokenResponse, scopes), | ||
| scopes: resolveGrantedScopes(tokenResponse, validationScopes), |
There was a problem hiding this comment.
When tokenResponse.scope is missing, resolveGrantedScopes falls back to its requestedScopes argument. In the non-authoritative fallback path, you pass validationScopes (which excludes cached existing scopes), so a successful auth with a scope-less token response would persist only the newly requested scopes and unintentionally drop previously stored scopes. Consider validating against validationScopes when tokenResponse.scope is present, but falling back to the actual OAuth-requested scopes when it isn’t, so additive behavior is preserved even without a scope field.
| scopes: resolveGrantedScopes(tokenResponse, validationScopes), | |
| scopes: resolveGrantedScopes(tokenResponse, tokenResponse.scope ? validationScopes : scopes), |
| const body = await response.text() | ||
| if (!response.ok) { | ||
| throw new Error(`HTTP ${response.status}: ${body || response.statusText}`) | ||
| } |
There was a problem hiding this comment.
On non-OK responses, the thrown error includes the full response body (HTTP ${status}: ${body || statusText}), which is later surfaced in debug logs during the fallback path. This can produce extremely large debug output (HTML error pages, etc.). Consider truncating/sanitizing the body similarly to the token-refresh path (e.g., first N chars) while still retaining status and a small snippet for troubleshooting.
| } from './session.js' | ||
| import type {StoredStoreAppSession} from './session.js' | ||
|
|
||
| async function refreshStoreToken(session: StoredStoreAppSession): Promise<StoredStoreAppSession> { |
There was a problem hiding this comment.
it'd be nice to split out the data fetching concern and the storage get/set concerns, wdyt?
There was a problem hiding this comment.
I agree, I'm gonna pursue more cleanup upstack. Extracting StoredSession in this PR felt like the right size for this particular move.
3b51cc5 to
aa2f837
Compare
aa2f837 to
eefef3a
Compare

What
Make
shopify store authpreserve existing app-install scopes when adding new access.Before starting OAuth, CLI now resolves the current granted scope set from the store when it can, falls back to locally stored scopes when it can't, and merges the new requested scopes onto that set.
Why
store authcurrently behaves like a replace operation.That means an agent can request a narrower scope set for one task and accidentally remove scopes another agent, tab, or user was already relying on. Using local state alone also leaves us vulnerable to stale scope data when the install has already changed remotely.
How
/admin/oauth/access_scopes.jsonstore authdoes not depend on the Admin GraphQL context module for generic session lifecycle behaviorConsidered
admin-graphql-context.ts: rejected. That made an Admin GraphQL module the accidental owner of generic stored-session lifecycle behavior.Testing
Measuring impact
How do we know this change was effective? Please choose one:
Checklist