Skip to content

Shopee Seller connector, query-signed HMAC and Shopee token refresh - #1147

Merged
helpcodeai merged 4 commits into
mainfrom
helpcodeai/shopee-signing
Oct 19, 2026
Merged

helpcodeai merged 4 commits into
mainfrom
helpcodeai/shopee-signing

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Adds a Shopee Seller connector. Shopee signs every call with a query-string HMAC and authorizes shops with its own flow (not OAuth 2), so the engine needed two additions. Both are opt-in: nothing changes for existing connectors.

Engine changes and why they are opt-in

HMAC signing (rest.engine.ts, applyHmacSignature) takes new optional fields in authConfig.signature:

  • placement: "query" and paramName: put the signature in a query parameter.
  • timestampParam: send the timestamp as a query parameter.
  • queryParams: static signed parameters, such as the partner id.
  • Two template variables: ${pathname} (the path without the query) and ${query.NAME}.

The new path only runs when one of these fields is set. Otherwise the original code runs line for line.

The first commit pins Kaufland's exact headers, URL and query at a fixed clock. It was written and passing on main before any engine change, and it still passes unchanged. A further test checks that kaufland and shopee are the only HMAC adapters, and that Kaufland uses none of the new fields and never calls the token service.

Shopee tokens (ShopeeTokenService). It is only reached when the connector has authType: HMAC and authConfig.tokenProvider: "shopee". It follows the OAuth2 and bearer-renewal patterns:

  • Tokens are stored encrypted in the connector's authConfig.
  • The token is renewed 5 minutes before expiry.
  • Renewal is single-flight per connector, both within a process and across processes (Postgres advisory lock). The stored refresh token is re-read inside the lock.
  • Shopee rotates the refresh token on every renewal. The new one is stored before the lock is released, and stored again outside the lock if the transaction fails.
  • When Shopee refuses the refresh token (error_shop_refresh_token, refresh_token_expired, shop_access_expired, shop_no_linked, or the older error_auth), ConnectorAuthStatusService marks the connector needs_reauth with one owner email, and calls fail fast until the shop is authorized again.
  • When a shop call answers invalid_acceess_token, the token is renewed, the request is re-signed and sent once more.

Other small opt-in branches, all keyed on Shopee:

  • A Shopee connector without tokens reports needs_authorization, so MCP does not list it.
  • The import probe is skipped for it.
  • PendingOAuthFlow gets an optional provider field.

How a user authorizes a shop

  1. Install the connector with SHOPEE_HOST, SHOPEE_PARTNER_ID and SHOPEE_PARTNER_KEY.
  2. Click Authorize with Provider on the connector page. The setup page also starts this right after saving.
  3. POST /api/shopee/connectors/:id/authorize returns Shopee's signed /api/v2/shop/auth_partner link. The redirect is <SERVER_URL>/api/shopee/callback/<state>, with the state in the path because Shopee returns no state on this link.
  4. The callback only checks the state and forwards code and shop_id to the dashboard.
  5. POST /api/shopee/complete exchanges the code, but only for the user who started the attempt. This is the same two-step model as the OAuth callback.

The generic OAuth callback and the existing providers are untouched. I chose this over a branch in /api/mcp-oauth: Shopee's link is signed, returns shop_id, and its token calls are signed JSON. Fitting all of that in would have meant changing the shared callback, and keeping it separate costs about 200 lines.

Tests

  • New suites:
    • rest.engine.hmac-pin.spec.ts: the Kaufland pin.
    • rest.engine.shopee.spec.ts: signing through the engine, the retry, and the opt-in guarantees.
    • shopee-token.service.spec.ts: code exchange, rotation across renewals, single flight (8 calls, 1 request), adopting a token another process stored, needs_reauth with a fail-fast afterwards, refusal on HTTP 200, a 5xx that does not mark the connector, and the re-store after a failed transaction.
    • shopee-auth.controller.spec.ts: permissions, state, other-user refusal, and a generic flow being refused.
    • shopee.live.spec.ts: all 22 tools through the real engine, plus a live block that is skipped by default.
  • The signatures use the exact base strings from Shopee's developer guide, hashed independently with Python's hmac. The guide shows a sample signature without the key that produced it, so a test key was used.
  • Full backend suite, before and after:
Suites Failed suites Tests Passed Failed Pending
Baseline (main) 679 1 21,889 20,392 1 1,496
After 684 0 21,988 20,491 0 1,497

The baseline failure is the timing test in curl.parser.spec.ts (639 ms against a 250 ms limit under load). It passed in the second run.

  • Backend tsc and eslint are clean. Frontend tsc and eslint are clean on the touched files. validate-adapters, adapter-count --check (487) and regenerate-catalog all pass; the regenerated catalog is byte-identical. No new dependency, no migration.
  • I built the backend and started it: dependency injection resolves and the three /api/shopee routes are mapped. It stopped at the missing database, as expected.

Not tested live. I had no Shopee partner account and did not create one. Everything was checked against the official API reference (open.shopee.com, v2) and how the widely used SDKs behave, not against a real shop.

Left out

  • Shipping or arranging orders, cancellations, buyer chat, returns handling, anything that moves money, and listing creation or deletion.
  • Main/merchant-account authorization: one shop per connector.
  • The newer unsigned open.shopee.com/auth link. The signed auth_partner link works on every API host.
  • Open points the docs leave unclear:
    • returns/get_return_list page_no: the text says 0, but the samples and SDKs use 1. The default is 1.
    • item_id_list is sent as repeated keys, like the SDKs do.
  • A refresh token lasts 30 days. A connector unused for longer needs a new authorization, and it says so.

Captured on main before any signing change: the exact headers, URL and
query Kaufland and the generic header template put on the wire.
- HMAC signing takes optional query placement (placement, paramName,
  timestampParam, queryParams) and two template variables (${pathname},
  ${query.NAME}). Without those fields the original code path runs
  unchanged; the header-placed signatures pinned on main stay identical.
- ShopeeTokenService: code exchange, renewal before expiry with the
  rotated refresh token stored encrypted on the connector, one renewal
  per connector (in process and under an advisory lock), needs_reauth
  and one email when Shopee refuses the refresh token. Only connectors
  with authType HMAC and authConfig.tokenProvider "shopee" reach it.
- Shopee's own authorize, callback and complete endpoints under
  /api/shopee, on the same encrypted single-use attempt store as OAuth;
  the generic OAuth callback is untouched.
- A Shopee connector without a shop authorization reports
  needs_authorization and skips the import probe.
- Dashboard: "Authorize with Provider" card for Shopee connectors, the
  completion page and the setup page route Shopee to its endpoints.
22 tools on the Shopee Open Platform API v2: shop info and profile,
listings (list, search, details, performance, variations), orders and
order details, orders to ship, shipping parameters, tracking number and
history, logistics channels, returns, order income, discounts, Ads
balance and daily performance. Stock and price updates install switched
off. Host, partner id and partner key are variables; the shop is
authorized from the connector page.

Logo, catalog and quoted adapter counts (487).
Comment thread packages/backend/src/connectors/engines/shopee-token.service.ts Dismissed
Another signed-in user holding the state could burn the owner's pending
authorization. Peek first, refuse a different user, then take it once.
@helpcodeai
helpcodeai merged commit 8b05876 into main Oct 19, 2026
14 checks passed
@helpcodeai
helpcodeai deleted the helpcodeai/shopee-signing branch October 19, 2026 11:14
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 19, 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.

2 participants