Skip to content

Add WooCommerce one-click key behind WOOCOMMERCE_ONE_CLICK (off by default) - #1633

Merged
helpcodeai merged 5 commits into
mainfrom
feat/woo-one-click-key
Nov 12, 2026
Merged

helpcodeai merged 5 commits into
mainfrom
feat/woo-one-click-key

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

Adds "Connect with one click" to the WooCommerce setup (item L1 of the WooCommerce plan, section 6.2 (A) and 6.3). The merchant approves a REST API key on their own shop through WooCommerce's /wc-auth/v1/authorize instead of creating a key and pasting it. Most WooCommerce setup failures today are key-paste mistakes: the wrong pair, a secret that was not copied, or a key that belongs to a customer-role user.

This ships behind a flag that is OFF by default everywhere. WOOCOMMERCE_ONE_CLICK=true turns it on. With the flag unset, all three new routes answer 404, the setup page shows no button, and the paste flow works as before. The new endpoint is unauthenticated and receives secrets, and we could not test it end to end on a real store here. Before the flag is turned on for Cloud, it needs an end-to-end test on a real store (procedure below). Matteo approved the authentication change / new credential path on 2026-11-11.

How it works

  1. POST /api/woocommerce/connectors/:id/authorize requires a JWT, the connector's organization, and the connector's owner or an admin (never a VIEWER). It answers 404 unless the flag is on and SERVER_URL is a public https address. The connector must be a WooCommerce connector whose shop address is saved: the setup page saves a draft first. The route stores a pending flow in the existing ConnectorOAuthAttempt store (hashed state, encrypted payload, single use, 15-minute TTL). The flow holds the provider woocommerce, connectorId, userId, organizationId, the normalised shop URL and the scope (read, the default, or read_write). The route returns <shop>/wc-auth/v1/authorize?app_name=AnythingMCP&scope=…&user_id=<state>&return_url=<FRONTEND_URL>/connectors/setup/woocommerce?connector=<id>&wc=done&callback_url=<SERVER_URL>/api/woocommerce/callback.
  2. POST /api/woocommerce/callback is unauthenticated, throttled to 30 requests a minute, has its own 4 KB body parser mounted before the global one, and answers 404 when the flag is off. It takes the pending flow by user_id (single use) and checks the ck_/cs_ patterns. It then records a "working" outcome and answers 200 immediately: WooCommerce deletes the key if this POST fails, and it waits for the answer before it sends the merchant back. Bad body, unknown/expired/used state or a malformed key → 400/410, so WooCommerce deletes the key it just made.
  3. In the background, the server tries the key on the shop URL stored with the flow: GET /wp-json/wc/v3/orders?per_page=1&_fields=id, sent through assertSafeOutboundUrl + outboundRequest, following no redirects. Only when the shop accepts the key does the server write WOOCOMMERCE_CONSUMER_KEY/SECRET, through the same code as PUT /api/connectors/:id/env-vars: normalise, field checks, derived settings, rebuild the catalog credentials, reload tools. The write goes to the flow's connector, only if that connector is still in the flow's organization, is still a WooCommerce connector, and still has the same shop URL. Nothing else from the body is used.
  4. The browser returns to the setup page, which polls GET /api/woocommerce/connectors/:id/one-click-status?state=<user_id> (JWT, same organization, same user) every 2 seconds for up to 60 seconds. The result is pending, working, ready (success screen) or failed with the reason, for example "its WordPress user may not see orders", a firewall, a 404 or permalinks set to Plain. success=0 means the merchant did not approve, and the page says so.

The callback never logs the body, the key or the secret. Log lines name only the connector id and a failure kind. Errors are logged by name, not message, and verification messages never quote the key.

Changes

  • connectors/woocommerce-auth.controller.ts (new): the three routes plus GET /api/woocommerce/one-click, which tells the frontend whether to show the button (same pattern as GET /api/tiktok-shop/platform). Also the helpers it needs: the flag/reachability gate, shop URL normalisation, key verification, the callback body parser, and DTOs. The callback body is validated with class-validator by hand, without forbidNonWhitelisted, so a field WooCommerce adds later cannot make it delete the key.
  • connectors/connector-env-write.util.ts (new): the body of PUT /api/connectors/:id/env-vars and its helpers, moved here unchanged so the callback writes through exactly the same path. ConnectorsController now delegates to it, and its behaviour is unchanged (all 107 connectors spec suites pass).
  • connectors/oauth1-consumer-key.util.ts (new): looksLikeEmail / assertConsumerKeyIsNotEmail moved out of the controller to avoid a circular import. The controller still re-exports them.
  • connectors/mcp-oauth.service.ts: the provider union gains woocommerce / woocommerce-outcome, plus an optional woocommerce payload field. No Prisma schema change.
  • main.ts: the 4 KB parser for /api/woocommerce/callback. connectors.module.ts: registers the controller.
  • Frontend: the WooCommerceOneClick panel offers Read only (recommended) or Read and write, with the paste fields kept below as the fallback. It shows on the key step of the guide and above the key fields in the one-page form, only when the backend reports the feature available. The page also handles the return and polls for the outcome. Strings are in connectorSetup.wooOneClick (en, de, it), following the setup page's next-intl pattern.
  • Docs: .env.example, docs/deployment.md (env table), and docker-compose.yml / docker-compose.cloud.yml pass WOOCOMMERCE_ONE_CLICK through, defaulting to empty, which means off.
  • Nothing under ee/. adapters/intl/woocommerce.json is not touched (Auto-apply catalog updates that only add optional query parameters; WooCommerce sync filters #1631 and the guided-setup branch edit it).

Cloud vs self-hosted

The behaviour is the same in both modes and keyed only on the flag and SERVER_URL:

  • Flag unset (the default everywhere): no button, routes 404, paste flow as today.
  • Flag true and SERVER_URL a public https address (Cloud): button shown.
  • Flag true and SERVER_URL http, localhost, a private IP or a single-label host (a typical self-hosted LAN install): button hidden and routes 404, because WooCommerce's wp_safe_remote_post cannot reach such a callback.

Type

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance improvement
  • Other (describe below)

Testing

  • Added/updated unit tests
  • Tested manually (describe steps below)
  • No tests needed (explain why)

packages/backend/src/connectors/woocommerce-auth.controller.spec.ts has 50 tests. They cover:

  • flag off → 404 on authorize, callback and status, and any value other than true stays off
  • a non-public SERVER_URL hides the button and gives 404
  • authorize needs a JWT guard (the callback has none), the same organization, and the owner or an admin; VIEWER is refused
  • the authorize link, the stored flow and the scope
  • callback: single use; an expired state is refused; an unknown state or a body that is not a callback is refused; unknown fields are ignored; wrong key/secret patterns are refused without a request or a write
  • a verification failure writes nothing and reports the reason; an SSRF refusal writes nothing
  • a connector moved to another organization, or a changed shop URL, writes nothing
  • success writes through the env-var path to the flow's connector only, rebuilding authConfig and reloading tools
  • the 4 KB body limit (a real express server answers 413)
  • no key or secret appears in any log line, including when upstream and storage errors echo them
  • shop URL normalisation and the error mapping of verifyWooCommerceKey

Commands run locally:

  • packages/backend: npm run lint → 0 errors (11 existing warnings, none in changed files); the diff after --fix was reviewed and has no unintended changes.
  • packages/backend: npx tsc --noEmit -p tsconfig.json → no errors in changed files. The only errors are the existing local ones from modules not installed in this checkout (@aws-sdk/client-s3, picomatch types, ssh2, basic-ftp).
  • packages/backend: npx jest src/connectors src/adapters --maxWorkers=4, with a local jest config that only stubs those missing modules → 845 suites passed, 3 skipped; 28,197 tests passed, 1,878 skipped; 0 failures (the new spec: 50/50).
  • packages/frontend: npm run lint → 0 errors (54 existing warnings, none in changed files); npx tsc --noEmit -p tsconfig.json → clean.

Manual end-to-end test on a real store (required before enabling on Cloud)

Prerequisites: a WordPress + WooCommerce store on https with pretty permalinks, an administrator login, and an AnythingMCP backend reachable from the internet over https (SERVER_URL=https://…, for example a staging droplet) with WOOCOMMERCE_ONE_CLICK=true.

  1. GET /api/woocommerce/one-click while signed in → {"available": true}.
  2. Open /connectors/setup/woocommerce, enter the shop address, and go to "Create a REST API key". The "Connect with one click" panel shows. Keep Read only, then click it.
  3. The browser goes to <shop>/wc-auth/v1/authorize?.... Sign in to WordPress if asked and click Approve.
  4. You come back to /connectors/setup/woocommerce?connector=…&wc=done&success=1&user_id=…. The page shows "Checking the new key with your shop…", then the success screen.
  5. Check: the connector page shows the key and secret set (masked); woocommerce_list_orders {per_page:1} works; in WP admin, WooCommerce → Settings → Advanced → REST API lists "AnythingMCP - API (Read)" with Read permission. Repeat with Read and write and check that a write tool works.
  6. Backend logs: one line WooCommerce one-click key (read) stored for connector <id>, and no key or secret anywhere (grep ck_ / cs_ on the logs).
  7. Negative cases:
    (a) Click Deny → back with success=0 and the "not approved" message.
    (b) Approve while signed in as a WordPress user without manage_woocommerce → "failed" with the shop manager hint, and nothing written.
    (c) Replay the same callback with curl → 410.
    (d) Send a callback body over 4 KB → 413.
    (e) Wait more than 15 minutes on the Approve screen, then approve → WooCommerce shows its error, and the key is deleted on the shop side.
    (f) With Wordfence on default rules and with Cloudflare in front: check whether /wc-auth/ and the callback get through.
    (g) Plain permalinks: the authorize link 404s (expected; the paste flow remains).
    (h) Set WOOCOMMERCE_ONE_CLICK unset → button gone, all routes 404.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable)
  • All existing tests pass (npm test): run in CI; locally the connectors and adapters suites
  • This PR has a descriptive title

Related Issues

WooCommerce plan, item L1 (sections 6.2 (A), 6.3, 8.4). Auth change approved by Matteo on 2026-11-11.

…fault)

The merchant approves a REST API key on their shop (/wc-auth/v1/authorize)
instead of pasting one. The unauthenticated callback takes the single-use
pending flow, checks the ck_/cs_ shape, answers 200 at once, tries the key
on the shop URL stored with the flow through the SSRF guard, and only then
writes it through the PUT env-vars path, now shared in
connector-env-write.util.ts. Offered only when the flag is true and
SERVER_URL is public https; all routes 404 otherwise.
The new controller's WooCommerce tag was missing from the OpenAPI tag
list, which failed openapi.spec in CI. A saved shop address that is
not public (localhost, private network) now gets its own message
instead of "save the address first", and a one-click error (denied,
failed, timeout, refused) scrolls into view: it is shown below the key
fields and looked as if the click did nothing (#1633).
@helpcodeai

Copy link
Copy Markdown
Contributor Author

Merged main, fixed CI, tested end to end on a local WooCommerce

Branch updates

Checks (in the worktree after the merge)

  • Backend:
    • npx tsc --noEmit is clean.
    • The connectors suites and openapi.spec pass: 116 suites, 2,091 tests.
    • The full npx jest --maxWorkers=4 run passes, apart from 14 tests in jwt-or-management-key.guard.spec.ts. That spec needs a migrated database, and it passed 14/14 once the database was migrated.
  • Frontend: tsc is clean, and eslint on the changed files reports 0 errors.

Manual end-to-end test

Setup:

  • WordPress 7.1.4 + WooCommerce 11.2.2 in Docker, served over https by Caddy (internal CA) as shop.localtest.me:8443, which resolves to 127.0.0.1.
  • This branch running locally with WOOCOMMERCE_ONE_CLICK=true, SSRF_ALLOW_LOCALHOST/PRIVATE=true and SERVER_URL set to a temporary cloudflared https tunnel, because WooCommerce requires an https callback.
  • Browser: Playwright. A test-only mu-plugin in the throwaway store signs the browser in on /wc-auth/.

Results, step numbers from the PR description:

  1. GET /api/woocommerce/one-click → {"available":true} ✅
    2–4. Read only → Approve → returned with success=1 → "Checking…" → success screen ✅
  2. Connector setupStatus: ready with key, secret and URL set. WP lists the key with read permission. woocommerce_search_products returns the product. A write tool is refused with "The API key provided does not have write permissions" ✅ The same with Read and write: the key has read_write and create_product_category works ✅
  3. Log: one line WooCommerce one-click key (read) stored for connector …, and 0 ck_/cs_ matches in the backend log ✅
  4. Negative cases:
    • (a) Deny → success=0, the "not approved" message, now in view ✅
    • (b) A WP user without manage_woocommerce gets WooCommerce's own "You do not have permission" page. Nothing is posted and nothing is written ✅
    • (c) Replaying a consumed callback → 410. An unknown state → 410. A malformed key → 410 ✅
    • (d) A callback body over 4 KB → 413 ✅
    • An unused state with a fake key → 200, then verification fails (auth_failed 401) and nothing is written ✅
    • (h) Flag unset → available:false, authorize, callback and status all 404, no panel, paste fields shown ✅
    • Not tested: (e) the 15-minute expiry, (f) Wordfence or Cloudflare in front of the shop, (g) plain permalinks.
  • Regression check on a non-Woo connector (Todoist): PUT /api/connectors/:id/env-vars and the setup verify behave as on main. The PUT still replaces the whole set, as before.

The flag stays off by default. Turning it on for Cloud is a separate decision.

@helpcodeai
helpcodeai merged commit 92153d7 into main Nov 12, 2026
16 checks passed
@helpcodeai
helpcodeai deleted the feat/woo-one-click-key branch November 12, 2026 08:23
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant