Skip to content

Make GraphiQL server auth-agnostic#7380

Merged
gonzaloriestra merged 1 commit into
mainfrom
graphiql-store/auth-agnostic-server
Jun 12, 2026
Merged

Make GraphiQL server auth-agnostic#7380
gonzaloriestra merged 1 commit into
mainfrom
graphiql-store/auth-agnostic-server

Conversation

@gonzaloriestra

@gonzaloriestra gonzaloriestra commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Store GraphiQL needs the shared server to work with tokens that do not come from app dev client credentials.

WHAT is this pull request doing?

Makes the cli-kit GraphiQL server accept a token provider, adds shared Admin API GraphQL helpers, keeps app dev wired to its existing auth flow, and extracts the app dev client-credentials token provider into a focused module with tests.

How to test your changes?

Open GraphiQL from shopify app dev

Checklist

  • I have considered possible cross-platform impacts (Mac, Linux, Windows)
  • I have considered possible documentation changes
  • I have considered analytics changes to measure impact
  • The change is user-facing — I have identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

gonzaloriestra commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@gonzaloriestra gonzaloriestra changed the title refactor(cli-kit): make GraphiQL server auth-agnostic Make GraphiQL server auth-agnostic Apr 23, 2026
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from de745ff to 6a55501 Compare April 23, 2026 12:23
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from 2cf7228 to 9fd0733 Compare April 23, 2026 12:23
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from 6a55501 to d2d4ab7 Compare April 24, 2026 07:57
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch 2 times, most recently from 31897c8 to 99ac270 Compare April 24, 2026 08:00
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from d2d4ab7 to a4bce79 Compare April 24, 2026 08:00
@github-actions

Copy link
Copy Markdown
Contributor

This PR seems inactive. If it's still relevant, please add a comment saying so. Otherwise, take no action.
→ If there's no activity within a week, then a bot will automatically close this.
Thanks for helping to improve Shopify's dev tooling and experience.

@github-actions github-actions Bot closed this Jun 1, 2026
@gonzaloriestra gonzaloriestra reopened this Jun 1, 2026
@gonzaloriestra gonzaloriestra added stale-exempt If added, the PR/issue won't be closed by stale-bot and removed no-pr-activity labels Jun 1, 2026
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from a4bce79 to ece2311 Compare June 1, 2026 10:38
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from 99ac270 to 3b24a0d Compare June 1, 2026 10:38
@github-actions github-actions Bot added the no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. label Jun 1, 2026
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from 3b24a0d to ba3912c Compare June 3, 2026 11:35
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch 2 times, most recently from 69dc468 to a8b2437 Compare June 3, 2026 13:41
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from ba3912c to 0a39296 Compare June 3, 2026 13:41
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from a8b2437 to 34afd2f Compare June 4, 2026 08:59
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from 0a39296 to 5664802 Compare June 4, 2026 08:59
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch 2 times, most recently from 79b5bfe to 97a7efa Compare June 4, 2026 11:03
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from 5664802 to 5ff0757 Compare June 4, 2026 11:03
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from 97a7efa to c48fc34 Compare June 4, 2026 11:14
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from c48fc34 to 87ea94f Compare June 4, 2026 12:12
@gonzaloriestra gonzaloriestra marked this pull request as ready for review June 4, 2026 14:07
@gonzaloriestra gonzaloriestra requested a review from a team as a code owner June 4, 2026 14:07

@isaacroldan isaacroldan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Review assisted by pair-review

Comment thread packages/app/src/cli/services/dev/processes/graphiql-token-provider.ts Outdated
Comment thread packages/cli-kit/src/public/node/graphiql/server.ts Outdated
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from 87ea94f to bb8a029 Compare June 5, 2026 08:47
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from 319a914 to ef412d3 Compare June 12, 2026 12:26
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from bb8a029 to 1d74b3b Compare June 12, 2026 12:26
Decouple the GraphiQL HTTP proxy from any specific token strategy or app
context so it can be reused outside of `shopify app dev`.

- New `TokenProvider` interface with `getToken` and optional `refreshToken`.
  The proxy delegates auth to the provider, removing the hard-coded
  `client_credentials` flow.
- App-specific concerns (app name/url, app secret) move behind an optional
  `appContext` option. Without it, the template hides the App pill, swaps
  the unauthorized badge label, and shows a stored-auth scopes note.
- New `protectMutations` option rejects mutation operations server-side
  with HTTP 400 before forwarding. This lets interactive sessions mirror
  the safe-by-default semantics of `shopify store execute` without
  --allow-mutations.
- Key resolution: explicit key wins; otherwise derive deterministically
  from `appContext.apiSecret` + `storeFqdn` (preserving today's behavior
  for app dev) or fall back to a random per-process key.
- Extract `containsMutation(query, operationName?)` to
  `@shopify/cli-kit/node/graphql` for use by the proxy.
- Update the app dev wrapper to provide an in-memory
  client_credentials `TokenProvider`. Behavior unchanged for `app dev`.

Tests cover protectMutations behavior, key handling, token-provider
plumbing, and containsMutation across queries, mutations, fragments,
named operations, and invalid input.
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/extract-to-cli-kit branch from ef412d3 to 2e14951 Compare June 12, 2026 13:51
@gonzaloriestra gonzaloriestra force-pushed the graphiql-store/auth-agnostic-server branch from 1d74b3b to 573f91b Compare June 12, 2026 13:51
@github-actions

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/public/node/graphql.d.ts
/**
 * Returns true if the GraphQL document contains a mutation operation that
 * would actually be executed for the given (optional) operation name.
 *
 * - When `operationName` is provided, only the matching operation is checked.
 * - When `operationName` is omitted and the document has a single operation,
 * that operation is checked.
 * - When the document has multiple operations and no operation name is given,
 * any mutation in the document is treated as a mutation request (the GraphQL
 * server would reject the ambiguous request anyway).
 *
 * Returns false for queries, subscriptions, fragment-only documents, and any
 * input that fails to parse as GraphQL.
 *
 * @param query - The GraphQL document to inspect.
 * @param operationName - Optional name of the operation to check; when set, only that operation is considered.
 * @returns True if the relevant operation is a mutation; false otherwise.
 */
export declare function containsMutation(query: string, operationName?: string): boolean;
packages/cli-kit/dist/public/node/graphiql/server.d.ts
import { Server } from 'http';
import { Writable } from 'stream';
/**
 * Derives a deterministic GraphiQL authentication key from the app's API secret and store FQDN.
 * The key is stable across dev server restarts (so browser tabs survive restarts)
 * but is not guessable without the app secret.
 *
 * @param apiSecret - The Partners app's client secret used as the HMAC key.
 * @param storeFqdn - The myshopify.com domain the GraphiQL session targets.
 * @returns A 64-character hex string suitable for use as the `?key=` query param.
 */
export declare function deriveGraphiQLKey(apiSecret: string, storeFqdn: string): string;
/**
 * Resolves the GraphiQL authentication key. Uses the explicitly provided key
 * if non-empty, otherwise derives one deterministically from the app secret.
 *
 * @param providedKey - An explicit key supplied by the caller; takes precedence when non-empty.
 * @param apiSecret - The Partners app's client secret, used to derive a stable key as a fallback.
 * @param storeFqdn - The myshopify.com domain the GraphiQL session targets.
 * @returns The resolved key.
 */
export declare function resolveGraphiQLKey(providedKey: string | undefined, apiSecret: string, storeFqdn: string): string;
/**
 * Pluggable strategy for obtaining and refreshing the Admin API access token
 * that the GraphiQL proxy injects into every request.
 *
 * - `getToken` may return a cached token; the proxy calls it for every request.
 * - `refreshToken` (optional) is invoked when the upstream Admin API returns 401.
 * When omitted, the proxy falls back to calling `getToken` again on 401.
 *
 * Implementations must throw `TokenRefreshError` (or any thrown error) when the
 * token cannot be obtained; the proxy renders the unauthorized template in that case.
 */
export interface TokenProvider {
    getToken: () => Promise<string>;
    refreshToken?: () => Promise<string>;
}
/**
 * Optional app-specific context, used to render the app pill and scopes note in the
 * GraphiQL header and to drive the deterministic key derivation. Pass when the GraphiQL
 * server is hosted as part of `shopify app dev`; omit for app-less use cases such as
 * `shopify store execute`.
 */
export interface GraphiQLAppContext {
    appName: string;
    appUrl: string;
    apiSecret: string;
}
export interface SetupGraphiQLServerOptions {
    stdout: Writable;
    port: number;
    storeFqdn: string;
    tokenProvider: TokenProvider;
    /**
     * Authentication key required as a `?key=` query string on every request. When omitted:
     * - if `appContext` is provided, derived deterministically from `apiSecret` + `storeFqdn`
     * so browser tabs survive dev server restarts.
     * - otherwise, generated randomly per process.
     */
    key?: string;
    appContext?: GraphiQLAppContext;
    /**
     * When true, the proxy rejects mutation operations with HTTP 400 before forwarding
     * them to the Admin API. Use this to mirror non-interactive safety guarantees in the
     * interactive UI.
     */
    protectMutations?: boolean;
}
export declare const MUTATIONS_BLOCKED_MESSAGE = "Mutations are disabled. Re-run with --allow-mutations to enable mutations.";
/**
 * Starts a local HTTP server that hosts the GraphiQL UI and proxies requests to the
 * Admin API for the configured store. Authentication is delegated to the supplied
 * `tokenProvider`, so the same server can serve both `shopify app dev` and stored-session
 * use cases.
 *
 * @param options - Configuration for the server, including the target store, the
 * pluggable token provider, and the local port to bind to.
 * @returns The underlying Node `http.Server` instance, already listening on `options.port`.
 */
export declare function setupGraphiQLServer(options: SetupGraphiQLServerOptions): Server;
packages/cli-kit/dist/public/node/graphiql/utilities.d.ts
/**
 * Filters request headers to extract only custom headers that are safe to forward.
 * Blocked headers and non-string values are excluded.
 *
 * @param headers - The raw incoming request headers.
 * @returns The subset of headers that are safe to forward to the Admin API.
 */
export declare function filterCustomHeaders(headers: {
    [key: string]: string | string[] | undefined;
}): {
    [key: string]: string;
};
packages/cli-kit/dist/public/node/graphiql/templates/graphiql.d.ts
export declare const defaultQuery: string;
interface GraphiQLTemplateOptions {
    apiVersion: string;
    apiVersions: string[];
    appName?: string;
    appUrl?: string;
    key: string;
    storeFqdn: string;
    protectMutations?: boolean;
}
export declare function graphiqlTemplate({ apiVersion, apiVersions, appName, appUrl, key, storeFqdn, protectMutations, }: GraphiQLTemplateOptions): string;
export {};
packages/cli-kit/dist/public/node/graphiql/templates/unauthorized.d.ts
interface UnauthorizedTemplateOptions {
    hasAppContext: boolean;
}
export declare function unauthorizedTemplate({ hasAppContext }: UnauthorizedTemplateOptions): string;
export {};

Existing type declarations

We found no diffs with existing type declarations

Base automatically changed from graphiql-store/extract-to-cli-kit to main June 12, 2026 14:04
@gonzaloriestra gonzaloriestra added this pull request to the merge queue Jun 12, 2026
Merged via the queue into main with commit 0237573 Jun 12, 2026
27 of 51 checks passed
@gonzaloriestra gonzaloriestra deleted the graphiql-store/auth-agnostic-server branch June 12, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. stale-exempt If added, the PR/issue won't be closed by stale-bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants