Skip to content

Let theme app extensions declare the network origins they contact - #9003

Open
jplhomer wants to merge 3 commits into
mainfrom
jplhomer/tae-network-access-allowed-sources
Open

jplhomer wants to merge 3 commits into
mainfrom
jplhomer/tae-network-access-allowed-sources

Conversation

@jplhomer

@jplhomer jplhomer commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Important

Needs server support before release. The platform rejects unknown keys in a theme app extension's deploy config today, so deploying an extension that declares [extensions.capabilities.network_access] fails until the server accepts capabilities.network_access.allowed_sources. Extensions that don't declare it send the same payload as before and aren't affected.

WHY are these changes introduced?

Theme app extensions load scripts and make requests on storefronts, but there's no way for them to say which origins they contact. Declaring them lets the platform build a storefront Content Security Policy for each extension, starting in report-only mode. Undeclared origins may be reported now and blocked later.

UI extensions (checkout, customer accounts) already use a boolean capabilities.network_access. They run sandboxed, so they keep the boolean. Theme app extensions use the same key as a table.

WHAT is this pull request doing?

Theme app extensions can declare the origins they contact in shopify.extension.toml:

[[extensions]]
type = "theme"
name = "My theme app extension"
handle = "my-theme-app-extension"

[extensions.capabilities.network_access]
allowed_sources = ["https://api.example.com", "https://*.cdn.example.com"]
  • Theme-only schema. The theme specification gets its own capabilities schema in which network_access is a table with allowed_sources. The shared CapabilitiesSchema still accepts only a boolean, so UI extensions don't change. Only the TypeScript type of the shared field is widened so the theme config still fits BaseConfigType. The dev payload now reads network_access === true, which is the same for booleans.
  • Deploy config. The theme deployConfig adds capabilities: {network_access: {allowed_sources: [...]}} next to theme_extension only when the table is declared. Otherwise the payload stays {theme_extension: {files: {}}}, byte for byte.

Validation rules (each entry in allowed_sources):

  • https:// only, with a host and an optional port. No path, query, fragment or credentials, so http://, scheme-less values, CSP keywords like 'self', and * are rejected.
  • The host needs at least two labels made of letters, digits and hyphens (localhost is rejected). Labels can't start or end with a hyphen, and the host is at most 253 characters. A numeric last label is rejected, which rules out IP addresses.
  • Only a leading *. wildcard is allowed, and it must cover at least two labels (https://*.cdn.example.com is fine, but https://*.com and https://api.*.example.com aren't). CSP host sources support this form, and extensions often load from CDN subdomains.
  • Ports must be 1–65535. The default :443 is dropped, and other ports are kept.
  • Entries are trimmed and lowercased (hosts are case-insensitive), a trailing / is dropped, and duplicates are removed after normalization. The normalized list is what gets deployed.
  • Up to 20 origins (counted after duplicates are removed). This keeps the generated policy header small. It's a constant, so raising it later is cheap.
  • One error lists every invalid entry, with an example of a valid one.

Omitted vs. empty:

TOML Meaning Sent on deploy
No [extensions.capabilities.network_access] table Not declared. The extension's requests may be reported as undeclared. Nothing (payload unchanged)
allowed_sources = [] Declared: the extension contacts no other origins allowed_sources: []
Table without allowed_sources Error: allowed_sources is required. Use an empty list if your extension contacts no other origins. n/a
network_access = true / false (the UI extension boolean) Never had an effect on theme app extensions, and still doesn't. It's ignored, so existing configs keep loading. Nothing

app dev needs no separate change: dev sessions build the app manifest from the same deployConfig, so declared sources reach the server there too, and are subject to the same server-support caveat.

There's no generated JSON schema or docs for extension TOML in this repo. The field should be documented in the theme app extension configuration docs once the server supports it.

Tests (packages/app):

  • pnpm vitest run src/cli/models/extensions/specifications/theme.test.ts: 36 tests pass. They cover valid origins and normalization, the empty list, omitted, the ignored boolean, missing allowed_sources, wrong types, http, no scheme, path, query, fragment, credentials, wildcard placement, single-label hosts, empty labels, trailing dots, newlines, IPs, ports, CSP keywords and injection, the 20-origin limit, deploy config with and without the table, and UI extensions keeping the boolean (and rejecting the table).
  • loader.test.ts: new test that loads a theme extension TOML with the table and checks the deploy config.
  • Related suites: src/cli/models/extensions/specifications/, extension-instance, dev/extension/payload, app/loader, load-specifications and services/context/: 34 files, 605 tests pass.
  • pnpm tsc --noEmit, pnpm eslint on changed files and prettier --check: clean.

How to manually test your changes?

  1. Add the TOML above to a theme app extension and run shopify app build or shopify app deploy. The config loads, and the deploy payload includes the normalized allowed_sources. Deploy succeeds only once the server supports the field.
  2. Change an entry to http://api.example.com or https://api.example.com/path. Loading fails with the invalid-entry error.
  3. Remove the table. The deploy payload is {"theme_extension":{"files":{}}} as before.

Post-release steps

Document [extensions.capabilities.network_access] allowed_sources for theme app extensions once the server supports it.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've 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

Theme app extensions can now list the origins they contact in
shopify.extension.toml:

    [extensions.capabilities.network_access]
    allowed_sources = ["https://api.example.com", "https://*.example-cdn.com"]

Each entry must be an HTTPS origin with no path, query or fragment.
Entries are normalized and deduplicated, and a list can have up to 20
origins. The theme deploy config includes capabilities only when the
table is declared, so existing payloads don't change. UI extensions keep
the boolean network_access.
@jplhomer
jplhomer requested a review from a team as a code owner October 23, 2026 21:36
Copilot AI balanced review requested due to automatic review settings October 23, 2026 21:36

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions github-actions Bot added the Area: @shopify/app @shopify/app package issues label Oct 23, 2026
Use a wildcard example under example.com in the error hint, and test empty
labels, trailing dots, newlines and single-label wildcards.
Knip flags it as unused outside the module.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/app @shopify/app package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants