@bintangtimurlangit/shopee-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +25 -0
- package/CHANGELOG.md +22 -0
- package/CODE_OF_CONDUCT.md +12 -0
- package/CONTRIBUTING.md +48 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/SECURITY.md +29 -0
- package/build/api/client.d.ts +27 -0
- package/build/api/client.d.ts.map +1 -0
- package/build/api/client.js +57 -0
- package/build/api/client.js.map +1 -0
- package/build/api/types.d.ts +92 -0
- package/build/api/types.d.ts.map +1 -0
- package/build/api/types.js +4 -0
- package/build/api/types.js.map +1 -0
- package/build/browser/session.d.ts +30 -0
- package/build/browser/session.d.ts.map +1 -0
- package/build/browser/session.js +109 -0
- package/build/browser/session.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +33 -0
- package/build/index.js.map +1 -0
- package/build/login.d.ts +12 -0
- package/build/login.d.ts.map +1 -0
- package/build/login.js +53 -0
- package/build/login.js.map +1 -0
- package/build/tools/product.d.ts +3 -0
- package/build/tools/product.d.ts.map +1 -0
- package/build/tools/product.js +129 -0
- package/build/tools/product.js.map +1 -0
- package/build/tools/search.d.ts +3 -0
- package/build/tools/search.d.ts.map +1 -0
- package/build/tools/search.js +98 -0
- package/build/tools/search.js.map +1 -0
- package/build/utils/cache.d.ts +13 -0
- package/build/utils/cache.d.ts.map +1 -0
- package/build/utils/cache.js +33 -0
- package/build/utils/cache.js.map +1 -0
- package/build/utils/errors.d.ts +18 -0
- package/build/utils/errors.d.ts.map +1 -0
- package/build/utils/errors.js +44 -0
- package/build/utils/errors.js.map +1 -0
- package/docs/CONFIGURATION.md +73 -0
- package/docs/DEVELOPMENT.md +52 -0
- package/docs/README.md +14 -0
- package/docs/RELEASES.md +46 -0
- package/package.json +86 -0
package/.env.example
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# Shopee MCP — Environment Variables
|
|
3
|
+
#
|
|
4
|
+
# Unlike a plain scraper, this server drives a real Chromium profile so it can
|
|
5
|
+
# clear Shopee's anti-bot gate. You log in ONCE with `npm run login`; the session
|
|
6
|
+
# is saved to the profile dir below and reused. Everything here is optional.
|
|
7
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
# Shopee regional domain (default: shopee.co.id)
|
|
10
|
+
SHOPEE_DOMAIN=shopee.co.id
|
|
11
|
+
|
|
12
|
+
# Where the persistent Chromium profile (your saved login) lives.
|
|
13
|
+
# Default: ~/.shopee-mcp/chrome-profile
|
|
14
|
+
# SHOPEE_PROFILE_DIR=/home/you/.shopee-mcp/chrome-profile
|
|
15
|
+
|
|
16
|
+
# The browser runs HEADED by default because Shopee detects headless — keep it
|
|
17
|
+
# that way. On a server, provide a virtual display via `xvfb-run`. Setting this
|
|
18
|
+
# to "true" is for experiments only and will almost certainly be blocked.
|
|
19
|
+
SHOPEE_HEADLESS=false
|
|
20
|
+
|
|
21
|
+
# Cache lifetime in milliseconds (default: 30000 = 30s)
|
|
22
|
+
CACHE_TTL_MS=30000
|
|
23
|
+
|
|
24
|
+
# Set to "true" to log startup/debug info to stderr
|
|
25
|
+
DEBUG=false
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0/). For **how** we version, tag, and publish, see [docs/RELEASES.md](./docs/RELEASES.md).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Standardized project scaffolding: `LICENSE` (MIT), `.editorconfig`, ESLint + Prettier, Conventional Commits (commitlint), Husky pre-commit hooks, CI + release workflows, issue/PR templates, and a `docs/` guide set.
|
|
12
|
+
|
|
13
|
+
## [0.1.0] - 2026-07-12
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Initial release: MCP server for **exploring Shopee** over stdio, driving a logged-in CloakBrowser session to clear Shopee's anti-bot gate.
|
|
18
|
+
- **2 tools:** `search_products` (keyword search with sorting & pagination) and `get_product_detail` (price, discount, brand, condition, rating, review/sold counts, stock, location, description).
|
|
19
|
+
- In-memory read cache and a persistent browser profile under `~/.shopee-mcp/`.
|
|
20
|
+
|
|
21
|
+
[Unreleased]: https://github.com/bintangtimurlangit/shopee-mcp/compare/v0.1.0...HEAD
|
|
22
|
+
[0.1.0]: https://github.com/bintangtimurlangit/shopee-mcp/releases/tag/v0.1.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Code of conduct
|
|
2
|
+
|
|
3
|
+
We want a respectful, harassment-free environment for everyone.
|
|
4
|
+
|
|
5
|
+
This project adopts the **[Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)** version **2.1**. See the linked page for the full text.
|
|
6
|
+
|
|
7
|
+
## Reporting
|
|
8
|
+
|
|
9
|
+
- For **security-sensitive** issues, follow [SECURITY.md](./SECURITY.md).
|
|
10
|
+
- For **conduct** concerns, contact the maintainers via GitHub (e.g. a direct message if available) or open a discussion/issue as appropriate for the situation.
|
|
11
|
+
|
|
12
|
+
Unacceptable behavior may result in temporary or permanent exclusion from project spaces.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Contributing to shopee-mcp
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve this MCP server.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Node.js](https://nodejs.org/) **18+** (see `engines` in `package.json`)
|
|
8
|
+
- Git
|
|
9
|
+
- A display (or `xvfb`) — the server drives a **headed** browser.
|
|
10
|
+
|
|
11
|
+
## Getting started
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/bintangtimurlangit/shopee-mcp.git
|
|
15
|
+
cd shopee-mcp
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
npm run login # one-time: log into Shopee in the CloakBrowser window
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
More detail: [docs/DEVELOPMENT.md](./docs/DEVELOPMENT.md).
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
1. **Fork** the repository and create a **branch** from `main` (`feat/...`, `fix/...`, etc.).
|
|
26
|
+
2. Make focused changes; match existing **style**, **types**, and **patterns** in the codebase.
|
|
27
|
+
3. **Run checks** before opening a PR:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm run lint
|
|
31
|
+
npm run format:check
|
|
32
|
+
npm run typecheck
|
|
33
|
+
npm run build
|
|
34
|
+
npm test # live smoke test (needs a login + a display)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
4. **Commit messages** follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat(tools): ...`, `fix(browser): ...`, `docs: ...`). A `commit-msg` hook enforces this.
|
|
38
|
+
5. Open a **pull request** with a clear description of what changed and why.
|
|
39
|
+
|
|
40
|
+
## What to contribute
|
|
41
|
+
|
|
42
|
+
- Bug fixes with steps to reproduce when possible
|
|
43
|
+
- Documentation improvements (`README.md`, `docs/`)
|
|
44
|
+
- Features that fit the project's scope: **public** Shopee discovery via MCP — product search and detail. This is a read-only, discovery-only tool; seller/account features are intentionally out of scope.
|
|
45
|
+
|
|
46
|
+
## AI-assisted contributions
|
|
47
|
+
|
|
48
|
+
If a change was produced or heavily guided by an **AI coding agent or assistant**, disclose that in the PR description and **name the model** (e.g. _Claude Opus 4.8_, _GPT-5_).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bintang Timurlangit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# shopee-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@bintangtimurlangit/shopee-mcp)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://github.com/bintangtimurlangit/shopee-mcp/actions)
|
|
6
|
+
[](https://github.com/bintangtimurlangit/shopee-mcp)
|
|
7
|
+
|
|
8
|
+
An MCP server for **exploring Shopee** — product search and prices — from any MCP client (Claude Desktop, Claude Code, etc.). Discovery only: no seller features.
|
|
9
|
+
|
|
10
|
+
> **Login required, read-only.** Shopee blocks anonymous requests, so this **unofficial** server reads public data through your own logged-in browser session (see [Why a browser?](#why-a-browser)). It performs no seller or account actions.
|
|
11
|
+
|
|
12
|
+
**Full reference:** [Documentation](./docs/README.md) · **Changelog:** [CHANGELOG.md](./CHANGELOG.md) · **Versioning & releases:** [docs/RELEASES.md](./docs/RELEASES.md)
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
| Tool | What it returns |
|
|
17
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `search_products` | Keyword search with sorting & pagination — names, prices, sold counts, ratings, seller location, product IDs, URLs. |
|
|
19
|
+
| `get_product_detail` | One product — price & discount, brand, condition, category, rating, **review count**, **sold count**, stock, location, description. |
|
|
20
|
+
|
|
21
|
+
### Tool annotations
|
|
22
|
+
|
|
23
|
+
Per the [MCP annotations spec](https://modelcontextprotocol.io/) — both tools are read-only, with no side effects.
|
|
24
|
+
|
|
25
|
+
| Tool | Read-only | Idempotent | Destructive |
|
|
26
|
+
| -------------------- | :-------: | :--------: | :---------: |
|
|
27
|
+
| `search_products` | ✓ | ✓ | – |
|
|
28
|
+
| `get_product_detail` | ✓ | ✓ | – |
|
|
29
|
+
|
|
30
|
+
## Why a browser?
|
|
31
|
+
|
|
32
|
+
Shopee does **not** expose an open API or server-rendered product HTML. Its `/api/v4/*` endpoints are guarded by an anti-fraud gate (`error 90309999`) that requires per-request signature headers (`af-ac-enc-dat`, `x-sap-sec`, …) minted by Shopee's own obfuscated SDK. Plain `fetch`, headless Chromium, and even a hand-rolled fetch from inside the page all get rejected.
|
|
33
|
+
|
|
34
|
+
So this server:
|
|
35
|
+
|
|
36
|
+
1. Drives **[CloakBrowser](https://github.com/CloakHQ/cloakbrowser)** — a Chromium with binary-level fingerprint patches — against a **persistent profile you log into once**.
|
|
37
|
+
2. **Navigates to the real Shopee page and intercepts the response** its own app fetches, so the request carries valid signatures.
|
|
38
|
+
|
|
39
|
+
The browser must run **headed** (Shopee detects headless); on a server use a virtual display (`xvfb`).
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install # also downloads the CloakBrowser binary (~200 MB, cached)
|
|
45
|
+
npm run build
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 1. Log in once
|
|
49
|
+
|
|
50
|
+
Shopee blocks anonymous requests, so you sign in one time. This saves a session to `~/.shopee-mcp/chrome-profile`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run login # opens a CloakBrowser window — log in, then press Enter
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- On a desktop / WSLg, the window appears normally.
|
|
57
|
+
- Re-run only when the session expires.
|
|
58
|
+
|
|
59
|
+
### 2. Register with your MCP client
|
|
60
|
+
|
|
61
|
+
The server launches a **headed** browser, so it needs a display. On a headless machine, wrap it with `xvfb-run`.
|
|
62
|
+
|
|
63
|
+
Claude Desktop / Claude Code `mcpServers` entry:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"shopee": {
|
|
69
|
+
"command": "xvfb-run",
|
|
70
|
+
"args": ["-a", "node", "/absolute/path/to/shopee-mcp/build/index.js"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
On a machine with a real display you can drop `xvfb-run` and use `"command": "node"`, `"args": ["…/build/index.js"]` (a browser window will pop up per session).
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
All optional — see `.env.example`. Copy to `.env` to override.
|
|
81
|
+
|
|
82
|
+
| Variable | Default | Purpose |
|
|
83
|
+
| -------------------- | ------------------------------ | ------------------------------------ |
|
|
84
|
+
| `SHOPEE_DOMAIN` | `shopee.co.id` | Regional Shopee domain. |
|
|
85
|
+
| `SHOPEE_PROFILE_DIR` | `~/.shopee-mcp/chrome-profile` | Where the saved login lives. |
|
|
86
|
+
| `SHOPEE_HEADLESS` | `false` | Keep `false` — headless is detected. |
|
|
87
|
+
| `CACHE_TTL_MS` | `30000` | In-memory cache lifetime. |
|
|
88
|
+
| `DEBUG` | `false` | Log startup/debug info to stderr. |
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm run lint # eslint
|
|
94
|
+
npm run format # prettier --write (format:check to verify)
|
|
95
|
+
npm run typecheck
|
|
96
|
+
npm test # live smoke test (needs a display; use xvfb-run on servers)
|
|
97
|
+
npm run dev # tsx watch
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
More detail: **[docs/DEVELOPMENT.md](./docs/DEVELOPMENT.md)**.
|
|
101
|
+
|
|
102
|
+
## Troubleshooting
|
|
103
|
+
|
|
104
|
+
| Symptom | Likely cause / fix |
|
|
105
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `🔒 Not signed in` / anonymous-request errors | No/expired session → run `npm run login`. |
|
|
107
|
+
| `error 90309999` or empty results | Shopee's anti-bot gate rejected the request. Ensure you're logged in and running **headed** (or via `xvfb-run`); retry. |
|
|
108
|
+
| A read returns empty for a valid product | Shopee lazy-loads; retry, and run with `DEBUG=true` to inspect. |
|
|
109
|
+
| Empty or stale results right after a change | In-memory cache — lower `CACHE_TTL_MS` or wait for the TTL to expire. |
|
|
110
|
+
| Headless / server has no display | Wrap the command in `xvfb-run -a …`. |
|
|
111
|
+
|
|
112
|
+
## Caveats
|
|
113
|
+
|
|
114
|
+
- **Login required.** No session → tools return a friendly "run `npm run login`" prompt.
|
|
115
|
+
- **Anti-bot is a moving target.** The free CloakBrowser binary can go stale as Shopee updates detection; CloakBrowser Pro ships newer patches.
|
|
116
|
+
- Respect Shopee's Terms of Service. This is for personal market exploration, not scraping at scale.
|
|
117
|
+
|
|
118
|
+
## Contributing & security
|
|
119
|
+
|
|
120
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md) · [SECURITY.md](./SECURITY.md) · [Code of Conduct](./CODE_OF_CONDUCT.md)
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
[MIT](./LICENSE)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Disclaimer
|
|
129
|
+
|
|
130
|
+
This is an **unofficial** project. It is **not affiliated with, authorized, maintained, sponsored, or endorsed by Shopee or Sea Limited**.
|
|
131
|
+
|
|
132
|
+
It works by driving a real logged-in browser session against Shopee's web app, which can change without notice — a tool may break when Shopee updates its site or anti-bot behavior. It reads only publicly available product data and performs no account actions.
|
|
133
|
+
|
|
134
|
+
You are responsible for using this software in compliance with [Shopee's Terms of Service](https://shopee.co.id/docs/terms) and applicable law. Use reasonable request volumes. All product names, logos, and brands are property of their respective owners.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are applied to the **latest release** on the default branch when practical.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please **do not** open a public GitHub issue for undisclosed security problems.
|
|
10
|
+
|
|
11
|
+
1. Use [GitHub private vulnerability reporting](https://github.com/bintangtimurlangit/shopee-mcp/security/advisories/new) if it is enabled for this repository, **or**
|
|
12
|
+
2. Contact the maintainers via a private channel (e.g. email on your GitHub profile).
|
|
13
|
+
|
|
14
|
+
Include:
|
|
15
|
+
|
|
16
|
+
- A short description of the issue and its impact
|
|
17
|
+
- Steps to reproduce (or a proof-of-concept), if safe to share
|
|
18
|
+
- Affected versions or dependency versions, if known
|
|
19
|
+
|
|
20
|
+
We aim to acknowledge reports within a few days and coordinate disclosure after a fix is available.
|
|
21
|
+
|
|
22
|
+
## Scope and credential handling
|
|
23
|
+
|
|
24
|
+
This is a **local MCP server** that reads **public** Shopee product data through a **logged-in browser session** (Shopee blocks anonymous requests). Be aware:
|
|
25
|
+
|
|
26
|
+
- Your **session lives on your machine** under `~/.shopee-mcp/chrome-profile` (configurable via `SHOPEE_PROFILE_DIR`). Treat that directory like a password. It is never transmitted anywhere by this server, and the repo **gitignores** local profile/state.
|
|
27
|
+
- The server is **read-only** — it performs product search and detail lookups only; there are no seller or account actions.
|
|
28
|
+
|
|
29
|
+
Issues in **Shopee's services**, **CloakBrowser**, or **upstream** dependencies (e.g. `@modelcontextprotocol/sdk`, `playwright`) should be reported to those projects when appropriate.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Shopee's anti-bot/anti-fraud rejection — almost always means "not logged in / detected". */
|
|
2
|
+
export declare const SHOPEE_ANTIBOT_ERROR = 90309999;
|
|
3
|
+
export declare class ShopeeAPIError extends Error {
|
|
4
|
+
readonly statusCode?: number | undefined;
|
|
5
|
+
readonly endpoint?: string | undefined;
|
|
6
|
+
readonly shopeeError?: number | undefined;
|
|
7
|
+
constructor(message: string, statusCode?: number | undefined, endpoint?: string | undefined, shopeeError?: number | undefined);
|
|
8
|
+
}
|
|
9
|
+
/** Thrown specifically when the anti-bot gate blocks us (needs login / a fresher binary). */
|
|
10
|
+
export declare class ShopeeAuthRequiredError extends ShopeeAPIError {
|
|
11
|
+
constructor(endpoint?: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Load a Shopee page and capture the JSON that its own app fetches from
|
|
15
|
+
* `/api/v4/*` — the only way to obtain data past the per-request anti-fraud
|
|
16
|
+
* signature (a hand-rolled fetch lacks the af-ac-enc-dat / x-sap-sec headers).
|
|
17
|
+
*
|
|
18
|
+
* @param pageUrl the Shopee page to load (its app fires the API call)
|
|
19
|
+
* @param apiMatch substring identifying the target /api/v4 response
|
|
20
|
+
*/
|
|
21
|
+
export declare function shopeeCapture<T extends {
|
|
22
|
+
error?: number;
|
|
23
|
+
error_msg?: string;
|
|
24
|
+
}>(pageUrl: string, apiMatch: string, timeoutMs?: number): Promise<T>;
|
|
25
|
+
/** Build an absolute Shopee URL from a path. */
|
|
26
|
+
export declare function shopeeUrl(pathAndQuery: string): string;
|
|
27
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C,qBAAa,cAAe,SAAQ,KAAK;aAGrB,UAAU,CAAC,EAAE,MAAM;aACnB,QAAQ,CAAC,EAAE,MAAM;aACjB,WAAW,CAAC,EAAE,MAAM;gBAHpC,OAAO,EAAE,MAAM,EACC,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,QAAQ,CAAC,EAAE,MAAM,YAAA,EACjB,WAAW,CAAC,EAAE,MAAM,YAAA;CAKvC;AAED,6FAA6F;AAC7F,qBAAa,uBAAwB,SAAQ,cAAc;gBAC7C,QAAQ,CAAC,EAAE,MAAM;CAU9B;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAClF,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAyBZ;AAED,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { captureJson, BASE_URL } from '../browser/session.js';
|
|
2
|
+
/** Shopee's anti-bot/anti-fraud rejection — almost always means "not logged in / detected". */
|
|
3
|
+
export const SHOPEE_ANTIBOT_ERROR = 90309999;
|
|
4
|
+
export class ShopeeAPIError extends Error {
|
|
5
|
+
statusCode;
|
|
6
|
+
endpoint;
|
|
7
|
+
shopeeError;
|
|
8
|
+
constructor(message, statusCode, endpoint, shopeeError) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
this.endpoint = endpoint;
|
|
12
|
+
this.shopeeError = shopeeError;
|
|
13
|
+
this.name = 'ShopeeAPIError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Thrown specifically when the anti-bot gate blocks us (needs login / a fresher binary). */
|
|
17
|
+
export class ShopeeAuthRequiredError extends ShopeeAPIError {
|
|
18
|
+
constructor(endpoint) {
|
|
19
|
+
super('Shopee blocked this request with its anti-bot gate. Run `npm run login` (or ' +
|
|
20
|
+
'`shopee-mcp-login`) once to sign in, then retry.', 200, endpoint, SHOPEE_ANTIBOT_ERROR);
|
|
21
|
+
this.name = 'ShopeeAuthRequiredError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load a Shopee page and capture the JSON that its own app fetches from
|
|
26
|
+
* `/api/v4/*` — the only way to obtain data past the per-request anti-fraud
|
|
27
|
+
* signature (a hand-rolled fetch lacks the af-ac-enc-dat / x-sap-sec headers).
|
|
28
|
+
*
|
|
29
|
+
* @param pageUrl the Shopee page to load (its app fires the API call)
|
|
30
|
+
* @param apiMatch substring identifying the target /api/v4 response
|
|
31
|
+
*/
|
|
32
|
+
export async function shopeeCapture(pageUrl, apiMatch, timeoutMs) {
|
|
33
|
+
let json;
|
|
34
|
+
try {
|
|
35
|
+
json = await captureJson(pageUrl, { apiMatch, timeoutMs });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
39
|
+
if (/timeout/i.test(msg)) {
|
|
40
|
+
// No matching response arrived — usually the gate silently dropped it.
|
|
41
|
+
throw new ShopeeAuthRequiredError(apiMatch);
|
|
42
|
+
}
|
|
43
|
+
throw new ShopeeAPIError(`Browser error loading ${apiMatch}: ${msg}`, undefined, apiMatch);
|
|
44
|
+
}
|
|
45
|
+
if (json.error === SHOPEE_ANTIBOT_ERROR) {
|
|
46
|
+
throw new ShopeeAuthRequiredError(apiMatch);
|
|
47
|
+
}
|
|
48
|
+
if (json.error !== undefined && json.error !== null && json.error !== 0) {
|
|
49
|
+
throw new ShopeeAPIError(`Shopee API error ${json.error}${json.error_msg ? `: ${json.error_msg}` : ''} for ${apiMatch}`, 200, apiMatch, json.error);
|
|
50
|
+
}
|
|
51
|
+
return json;
|
|
52
|
+
}
|
|
53
|
+
/** Build an absolute Shopee URL from a path. */
|
|
54
|
+
export function shopeeUrl(pathAndQuery) {
|
|
55
|
+
return `${BASE_URL}${pathAndQuery.startsWith('/') ? '' : '/'}${pathAndQuery}`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAE9D,+FAA+F;AAC/F,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAE7C,MAAM,OAAO,cAAe,SAAQ,KAAK;IAGrB;IACA;IACA;IAJlB,YACE,OAAe,EACC,UAAmB,EACnB,QAAiB,EACjB,WAAoB;QAEpC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,eAAU,GAAV,UAAU,CAAS;QACnB,aAAQ,GAAR,QAAQ,CAAS;QACjB,gBAAW,GAAX,WAAW,CAAS;QAGpC,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,6FAA6F;AAC7F,MAAM,OAAO,uBAAwB,SAAQ,cAAc;IACzD,YAAY,QAAiB;QAC3B,KAAK,CACH,8EAA8E;YAC5E,kDAAkD,EACpD,GAAG,EACH,QAAQ,EACR,oBAAoB,CACrB,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAe,EACf,QAAgB,EAChB,SAAkB;IAElB,IAAI,IAAO,CAAC;IACZ,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,WAAW,CAAI,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,uEAAuE;YACvE,MAAM,IAAI,uBAAuB,CAAC,QAAQ,CAAC,CAAC;QAC9C,CAAC;QACD,MAAM,IAAI,cAAc,CAAC,yBAAyB,QAAQ,KAAK,GAAG,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7F,CAAC;IAED,IAAI,IAAI,CAAC,KAAK,KAAK,oBAAoB,EAAE,CAAC;QACxC,MAAM,IAAI,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,cAAc,CACtB,oBAAoB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,QAAQ,EAAE,EAC9F,GAAG,EACH,QAAQ,EACR,IAAI,CAAC,KAAK,CACX,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,SAAS,CAAC,YAAoB;IAC5C,OAAO,GAAG,QAAQ,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,YAAY,EAAE,CAAC;AAChF,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface ItemRating {
|
|
2
|
+
rating_star: number;
|
|
3
|
+
rating_count: number[];
|
|
4
|
+
}
|
|
5
|
+
export interface ItemBasic {
|
|
6
|
+
itemid: number;
|
|
7
|
+
shopid: number;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Price in the currency's smallest unit × 100000 (divide by 100000 for IDR). */
|
|
10
|
+
price: number;
|
|
11
|
+
price_min: number;
|
|
12
|
+
price_max: number;
|
|
13
|
+
price_before_discount: number;
|
|
14
|
+
currency: string;
|
|
15
|
+
stock: number;
|
|
16
|
+
sold: number;
|
|
17
|
+
historical_sold: number;
|
|
18
|
+
liked_count: number;
|
|
19
|
+
discount?: string;
|
|
20
|
+
item_rating: ItemRating;
|
|
21
|
+
shop_location: string;
|
|
22
|
+
is_official_shop: boolean;
|
|
23
|
+
shopee_verified: boolean;
|
|
24
|
+
image: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SearchItem {
|
|
27
|
+
itemid: number;
|
|
28
|
+
shopid: number;
|
|
29
|
+
item_basic: ItemBasic;
|
|
30
|
+
}
|
|
31
|
+
export interface SearchItemsResponse {
|
|
32
|
+
error?: number;
|
|
33
|
+
total_count: number;
|
|
34
|
+
nomore: boolean;
|
|
35
|
+
items: SearchItem[] | null;
|
|
36
|
+
}
|
|
37
|
+
export interface PdpCategory {
|
|
38
|
+
display_name: string;
|
|
39
|
+
}
|
|
40
|
+
export interface PdpItem {
|
|
41
|
+
item_id: number;
|
|
42
|
+
shop_id: number;
|
|
43
|
+
title: string;
|
|
44
|
+
brand?: string;
|
|
45
|
+
/** 1 = new, otherwise used. */
|
|
46
|
+
condition?: number;
|
|
47
|
+
currency?: string;
|
|
48
|
+
item_rating?: {
|
|
49
|
+
rating_star: number;
|
|
50
|
+
rating_count: number[];
|
|
51
|
+
};
|
|
52
|
+
shop_location?: string;
|
|
53
|
+
categories?: PdpCategory[];
|
|
54
|
+
description?: string;
|
|
55
|
+
image?: string;
|
|
56
|
+
stock?: number | null;
|
|
57
|
+
normal_stock?: number | null;
|
|
58
|
+
historical_sold?: number;
|
|
59
|
+
global_sold_count?: number;
|
|
60
|
+
is_free_shipping?: boolean;
|
|
61
|
+
is_official_shop?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** A price value: prices are the real amount × 100000. range_* are -1 when single. */
|
|
64
|
+
export interface PdpPriceValue {
|
|
65
|
+
single_value: number;
|
|
66
|
+
range_min: number;
|
|
67
|
+
range_max: number;
|
|
68
|
+
}
|
|
69
|
+
export interface PdpProductPrice {
|
|
70
|
+
discount?: number;
|
|
71
|
+
price: PdpPriceValue;
|
|
72
|
+
price_before_discount?: PdpPriceValue;
|
|
73
|
+
}
|
|
74
|
+
/** /api/v4/pdp/get_pc → data.product_review — where the sold/review counts live. */
|
|
75
|
+
export interface PdpProductReview {
|
|
76
|
+
total_rating_count?: number;
|
|
77
|
+
cmt_count?: number;
|
|
78
|
+
/** Human-formatted sold counts, e.g. "355", "1,2rb". */
|
|
79
|
+
sold_count_display?: string;
|
|
80
|
+
historical_sold_display?: string;
|
|
81
|
+
global_sold_display?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface PdpResponse {
|
|
84
|
+
error?: number;
|
|
85
|
+
error_msg?: string;
|
|
86
|
+
data?: {
|
|
87
|
+
item: PdpItem;
|
|
88
|
+
product_price: PdpProductPrice;
|
|
89
|
+
product_review?: PdpProductReview;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,UAAU,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;CAC5B;AAKD,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,aAAa,CAAC;IACrB,qBAAqB,CAAC,EAAE,aAAa,CAAC;CACvC;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,OAAO,CAAC;QACd,aAAa,EAAE,eAAe,CAAC;QAC/B,cAAc,CAAC,EAAE,gBAAgB,CAAC;KACnC,CAAC;CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,0EAA0E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import type { BrowserContext } from 'playwright';
|
|
3
|
+
export declare const DOMAIN: string;
|
|
4
|
+
export declare const BASE_URL: string;
|
|
5
|
+
export declare const PROFILE_DIR: string;
|
|
6
|
+
/**
|
|
7
|
+
* Get the shared browser context, launching it on first use.
|
|
8
|
+
* `headless` overrides the env default (the login flow forces a visible window).
|
|
9
|
+
*/
|
|
10
|
+
export declare function getContext(headless?: boolean): Promise<BrowserContext>;
|
|
11
|
+
export interface CaptureOptions {
|
|
12
|
+
/** Substring the target /api/v4 response URL must contain. */
|
|
13
|
+
apiMatch: string;
|
|
14
|
+
/** Max time to wait for the matching response (ms). */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Navigate to `pageUrl` and return the JSON body of the first `/api/v4/*`
|
|
19
|
+
* response whose URL contains `apiMatch` — i.e. the request Shopee's own app
|
|
20
|
+
* fires (carrying the valid anti-fraud signature). Returns the raw parsed JSON;
|
|
21
|
+
* callers inspect its `error` field.
|
|
22
|
+
*/
|
|
23
|
+
export declare function captureJson<T>(pageUrl: string, opts: CaptureOptions): Promise<T>;
|
|
24
|
+
/** Warm the session once (loads Shopee so the anti-fraud SDK initialises). */
|
|
25
|
+
export declare function warm(): Promise<void>;
|
|
26
|
+
/** Best-effort check that the saved profile is logged in. */
|
|
27
|
+
export declare function isLoggedIn(): Promise<boolean>;
|
|
28
|
+
/** Cleanly close the browser (used on shutdown / after login). */
|
|
29
|
+
export declare function closeContext(): Promise<void>;
|
|
30
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/browser/session.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,YAAY,CAAC;AAIjE,eAAO,MAAM,MAAM,QAA8C,CAAC;AAClE,eAAO,MAAM,QAAQ,QAAsB,CAAC;AAE5C,eAAO,MAAM,WAAW,QACoE,CAAC;AAwC7F;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,GAAE,OAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAGtF;AAqBD,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CAgBtF;AAED,8EAA8E;AAC9E,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAS1C;AAED,6DAA6D;AAC7D,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAKnD;AAED,kEAAkE;AAClE,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAMlD"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { launchPersistentContext } from 'cloakbrowser';
|
|
5
|
+
// ─── Configuration ────────────────────────────────────────────────────────────
|
|
6
|
+
export const DOMAIN = process.env.SHOPEE_DOMAIN || 'shopee.co.id';
|
|
7
|
+
export const BASE_URL = `https://${DOMAIN}`;
|
|
8
|
+
export const PROFILE_DIR = process.env.SHOPEE_PROFILE_DIR || path.join(os.homedir(), '.shopee-mcp', 'chrome-profile');
|
|
9
|
+
// Shopee detects headless even with fingerprint patches, so we run HEADED by
|
|
10
|
+
// default (needs a display: WSLg, a desktop X server, or xvfb for servers).
|
|
11
|
+
// Set SHOPEE_HEADLESS=true only to experiment.
|
|
12
|
+
const HEADLESS = process.env.SHOPEE_HEADLESS === 'true';
|
|
13
|
+
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
14
|
+
function debug(msg) {
|
|
15
|
+
if (process.env.DEBUG === 'true')
|
|
16
|
+
process.stderr.write(`[shopee-mcp] ${msg}\n`);
|
|
17
|
+
}
|
|
18
|
+
// ─── Context singleton ────────────────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// Shopee gates product data behind per-request anti-fraud signatures that only
|
|
21
|
+
// its own SDK, running in a non-detected browser, can mint. So we drive
|
|
22
|
+
// CloakBrowser (a fingerprint-patched Chromium) against a persistent profile the
|
|
23
|
+
// user logs into once (npm run login). We never hand-craft the signed request —
|
|
24
|
+
// instead we navigate to the relevant page and intercept the response Shopee's
|
|
25
|
+
// app fires (see captureJson).
|
|
26
|
+
let contextPromise = null;
|
|
27
|
+
async function createContext(headless) {
|
|
28
|
+
debug(`Launching CloakBrowser (headless=${headless}) with profile: ${PROFILE_DIR}`);
|
|
29
|
+
const ctx = (await launchPersistentContext({
|
|
30
|
+
userDataDir: PROFILE_DIR,
|
|
31
|
+
headless,
|
|
32
|
+
userAgent: USER_AGENT,
|
|
33
|
+
locale: 'id-ID',
|
|
34
|
+
timezone: 'Asia/Jakarta',
|
|
35
|
+
viewport: { width: 1366, height: 768 },
|
|
36
|
+
humanize: true,
|
|
37
|
+
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
38
|
+
}));
|
|
39
|
+
return ctx;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the shared browser context, launching it on first use.
|
|
43
|
+
* `headless` overrides the env default (the login flow forces a visible window).
|
|
44
|
+
*/
|
|
45
|
+
export async function getContext(headless = HEADLESS) {
|
|
46
|
+
if (!contextPromise)
|
|
47
|
+
contextPromise = createContext(headless);
|
|
48
|
+
return contextPromise;
|
|
49
|
+
}
|
|
50
|
+
/** The single reused page. */
|
|
51
|
+
async function getPage() {
|
|
52
|
+
const ctx = await getContext();
|
|
53
|
+
const existing = ctx.pages().find((p) => !p.isClosed());
|
|
54
|
+
return existing ?? (await ctx.newPage());
|
|
55
|
+
}
|
|
56
|
+
// ─── Serialized navigation + interception ──────────────────────────────────────
|
|
57
|
+
//
|
|
58
|
+
// A single page is shared across tool calls; serialize access so overlapping
|
|
59
|
+
// calls don't clobber each other's navigation.
|
|
60
|
+
let lock = Promise.resolve();
|
|
61
|
+
function withLock(fn) {
|
|
62
|
+
const run = lock.then(fn, fn);
|
|
63
|
+
lock = run.catch(() => { });
|
|
64
|
+
return run;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Navigate to `pageUrl` and return the JSON body of the first `/api/v4/*`
|
|
68
|
+
* response whose URL contains `apiMatch` — i.e. the request Shopee's own app
|
|
69
|
+
* fires (carrying the valid anti-fraud signature). Returns the raw parsed JSON;
|
|
70
|
+
* callers inspect its `error` field.
|
|
71
|
+
*/
|
|
72
|
+
export async function captureJson(pageUrl, opts) {
|
|
73
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
74
|
+
return withLock(async () => {
|
|
75
|
+
const page = await getPage();
|
|
76
|
+
const matched = page.waitForResponse((r) => r.url().includes('/api/v4/') && r.url().includes(opts.apiMatch), { timeout: timeoutMs });
|
|
77
|
+
await page.goto(pageUrl, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
|
|
78
|
+
const resp = await matched;
|
|
79
|
+
const json = (await resp.json());
|
|
80
|
+
return json;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Warm the session once (loads Shopee so the anti-fraud SDK initialises). */
|
|
84
|
+
export async function warm() {
|
|
85
|
+
await withLock(async () => {
|
|
86
|
+
const page = await getPage();
|
|
87
|
+
if (!page.url().includes(DOMAIN)) {
|
|
88
|
+
debug('Warming session on Shopee homepage…');
|
|
89
|
+
await page.goto(`${BASE_URL}/`, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
90
|
+
await page.waitForTimeout(3000);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Best-effort check that the saved profile is logged in. */
|
|
95
|
+
export async function isLoggedIn() {
|
|
96
|
+
const ctx = await getContext();
|
|
97
|
+
const cookies = await ctx.cookies(BASE_URL);
|
|
98
|
+
// Shopee sets SPC_U (user id) and SPC_EC (encrypted session) once authenticated.
|
|
99
|
+
return cookies.some((c) => (c.name === 'SPC_U' || c.name === 'SPC_EC') && c.value.length > 4);
|
|
100
|
+
}
|
|
101
|
+
/** Cleanly close the browser (used on shutdown / after login). */
|
|
102
|
+
export async function closeContext() {
|
|
103
|
+
if (contextPromise) {
|
|
104
|
+
const ctx = await contextPromise;
|
|
105
|
+
await ctx.close();
|
|
106
|
+
contextPromise = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=session.js.map
|