semrush-cli 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +165 -2
- package/THIRD-PARTY-NOTICES.md +19 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +1 -0
- package/dist/auth-BcUPx0Nk.js +82 -0
- package/dist/auth-BcUPx0Nk.js.map +1 -0
- package/dist/browser-layer-D6mEyv9Y.js +269 -0
- package/dist/browser-layer-D6mEyv9Y.js.map +1 -0
- package/dist/browser-service-BYYE-vSm.js +9 -0
- package/dist/browser-service-BYYE-vSm.js.map +1 -0
- package/dist/browser-service-CL8YPaIT.js +3 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon/galleton-darwin-arm64 +0 -0
- package/dist/daemon/galleton-darwin-x64 +0 -0
- package/dist/daemon/galleton-linux-arm64 +0 -0
- package/dist/daemon/galleton-linux-x64 +0 -0
- package/dist/daemon/galleton-win32-x64.exe +0 -0
- package/dist/errors-BhvanypE.js +2124 -0
- package/dist/errors-BhvanypE.js.map +1 -0
- package/dist/mcp-Cuzenjra.js +95 -0
- package/dist/mcp-Cuzenjra.js.map +1 -0
- package/dist/session-layer-BUU0ZG9M.js +239 -0
- package/dist/session-layer-BUU0ZG9M.js.map +1 -0
- package/dist/validation-BH5mq69L.js +4 -0
- package/dist/validation-C_lcBSzX.js +30 -0
- package/dist/validation-C_lcBSzX.js.map +1 -0
- package/docs/architecture.md +21 -0
- package/package.json +55 -4
- package/src/vendor/GALLETON-LICENSE +21 -0
- package/src/vendor/README.md +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Microck
|
|
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
CHANGED
|
@@ -1,3 +1,166 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><img src="assets/logo.svg" width="100" alt="semrush-cli"></p>
|
|
2
|
+
<h1 align="center">semrush-cli</h1>
|
|
3
|
+
<p align="center">Semrush research from your terminal and MCP clients.</p>
|
|
4
|
+
<p align="center"><a href="https://semrush.micr.dev">Documentation</a> · <a href="https://www.npmjs.com/package/semrush-cli">npm</a> · <a href="https://github.com/Microck/semrush-cli/issues">Issues</a></p>
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
The executable is `semrush-cli`. It can coexist with other tools named `semrush`.
|
|
7
|
+
|
|
8
|
+
48 read-only web reports for keyword research, organic rankings, competitors,
|
|
9
|
+
keyword and backlink gaps, backlinks, historical/mobile research and AI visibility.
|
|
10
|
+
Written in TypeScript with Effect. Sessions live in an encrypted Galleton vault.
|
|
11
|
+
No Semrush developer API key, Python runtime or CAPTCHA service required.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Requires Node.js 22+. Includes Galleton binaries for Linux x64/arm64,
|
|
16
|
+
macOS x64/arm64 and Windows x64.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install -g semrush-cli
|
|
20
|
+
semrush-cli setup
|
|
21
|
+
semrush-cli auth login
|
|
22
|
+
semrush-cli domain-overview --domain openai.com --json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`setup` installs Chromium and starts a private Galleton daemon. `auth login`
|
|
26
|
+
opens an isolated browser for sign-in, verifies the session, and stores it in
|
|
27
|
+
Galleton. It requires a graphical desktop. On Linux, missing Chromium system
|
|
28
|
+
libraries can be installed with `npx patchright install-deps chromium`.
|
|
29
|
+
|
|
30
|
+
To import an existing, authorized Chromium session on a headless machine:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
semrush-cli auth import --cdp-url http://127.0.0.1:9234
|
|
34
|
+
semrush-cli doctor --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The browser must already expose CDP on a literal loopback HTTP address. This
|
|
38
|
+
supports the browser managed by [semrush-web-mcp](https://github.com/Microck/semrush-web-mcp).
|
|
39
|
+
Import does not stop or change that browser. Keep CDP private; forward it over
|
|
40
|
+
SSH when necessary.
|
|
41
|
+
|
|
42
|
+
## Research
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
semrush-cli keyword-overview --keyword 'email marketing' --json
|
|
46
|
+
semrush-cli keyword-magic --keyword 'email marketing' --questions-only --page-size 20 --json
|
|
47
|
+
semrush-cli domain-keywords --domain openai.com --page 2 --page-size 20 --json
|
|
48
|
+
semrush-cli domain-competitors --domain openai.com --page-size 10 --json
|
|
49
|
+
semrush-cli keyword-gap --domains '["openai.com","anthropic.com"]' --json
|
|
50
|
+
semrush-cli backlink-gap --domains '["openai.com","anthropic.com"]' --json
|
|
51
|
+
semrush-cli backlink-links --domain openai.com --filters '+|type||nofollow' --json
|
|
52
|
+
semrush-cli domain-ai-visibility --domain openai.com --json
|
|
53
|
+
semrush-cli snapshot-dates --database us --json
|
|
54
|
+
semrush-cli reports --json
|
|
55
|
+
semrush-cli domain-keywords --help
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Area | Reports |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| Keywords | Overview, ideas, questions, SERPs, ads, clusters, paginated Keyword Magic and groups |
|
|
61
|
+
| Organic | Rankings, pages, subdomains, changes, competitors, topics and trends |
|
|
62
|
+
| Gaps | Keyword overlaps and gaps, backlink prospects and profile comparisons |
|
|
63
|
+
| Backlinks | Links, referring domains, anchors, indexed pages, outbound domains, history and authority |
|
|
64
|
+
| AI visibility | Visibility, mentions, citation sources and country distribution |
|
|
65
|
+
| Account | Limits, permissions, databases, snapshots, projects, folders, saved lists and audit campaigns |
|
|
66
|
+
|
|
67
|
+
All successful reports use `{authenticated: true, report, data}`. Domain ranking
|
|
68
|
+
and page reports include provider totals. Large integers remain exact as decimal
|
|
69
|
+
strings. Errors go to stderr with a nonzero exit. `--json` works before or after
|
|
70
|
+
the command. Objects and arrays use JSON flags; booleans support explicit `false`.
|
|
71
|
+
See the [full command reference](https://semrush.micr.dev/docs/reports).
|
|
72
|
+
|
|
73
|
+
## MCP
|
|
74
|
+
|
|
75
|
+
Complete setup and authentication, then configure your MCP client:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"semrush": {
|
|
81
|
+
"command": "semrush-cli",
|
|
82
|
+
"args": ["mcp"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use an absolute executable path if your client does not inherit the terminal's
|
|
89
|
+
PATH. `npx -y semrush-cli@0.1.1 mcp` also works. MCP tool names use underscores,
|
|
90
|
+
for example `keyword_overview` and `domain_keywords`. Input schemas and reports
|
|
91
|
+
match the CLI. All 48 tools are annotated read-only. Login, account mutations,
|
|
92
|
+
arbitrary URLs and JavaScript execution are not MCP tools.
|
|
93
|
+
|
|
94
|
+
## Sessions
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
semrush-cli auth status --check --json
|
|
98
|
+
semrush-cli auth logout
|
|
99
|
+
semrush-cli daemon stop
|
|
100
|
+
semrush-cli daemon start
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Galleton stores credentials under `~/.local/share/semrush-cli/galleton`. The
|
|
104
|
+
private daemon listens on `127.0.0.1:8768`. It continues after the terminal closes;
|
|
105
|
+
run `daemon start` after reboot. The CLI installs no system startup service.
|
|
106
|
+
|
|
107
|
+
Each CLI process restores its encrypted session into a fresh, transient Chromium
|
|
108
|
+
context. The MCP server reuses one context and serializes requests. Successful
|
|
109
|
+
reports capture cookie updates back to Galleton. Session expiry requires sign-in
|
|
110
|
+
or import again. No anonymous fallback or automatic request retries.
|
|
111
|
+
|
|
112
|
+
The unchanged Galleton TypeScript SDK is vendored at a pinned upstream commit
|
|
113
|
+
because it is not on npm. The bundled daemon adds a tested `static` provider for
|
|
114
|
+
storage without renewal. Semrush has no proven renewal contract in the reference
|
|
115
|
+
integration, so this tool does not invent one. See [architecture](docs/architecture.md)
|
|
116
|
+
and [SDK provenance](src/vendor/README.md).
|
|
117
|
+
|
|
118
|
+
| Variable | Default |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `SEMRUSH_GALLETON_URL` | `http://127.0.0.1:8768` |
|
|
121
|
+
| `SEMRUSH_GALLETON_TOKEN_FILE` | `~/.local/share/semrush-cli/galleton/api.token` |
|
|
122
|
+
| `SEMRUSH_SESSION` | `semrush-cli` |
|
|
123
|
+
| `SEMRUSH_BROWSER_EXECUTABLE` | Patchright's Chromium |
|
|
124
|
+
|
|
125
|
+
The setup/daemon commands manage only the default CLI-owned daemon. An external
|
|
126
|
+
daemon must support the documented static-provider extension. Never expose its
|
|
127
|
+
API token or credential directory.
|
|
128
|
+
|
|
129
|
+
## Limits
|
|
130
|
+
|
|
131
|
+
Semrush account permissions, quotas and product access apply. A read-only report
|
|
132
|
+
can consume an allowance. Internal web endpoints may change. Provider caps remain
|
|
133
|
+
visible; there is no unlimited-export claim. Galleton encrypts at rest but its key
|
|
134
|
+
and vault share a private local directory. Other processes running as your OS user
|
|
135
|
+
are within the same trust boundary.
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
npm ci
|
|
141
|
+
npm run check
|
|
142
|
+
npm --prefix docs-site ci
|
|
143
|
+
npm --prefix docs-site run build
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Development builds require Go 1.23+ and `tar`/`patch` to build the pinned Galleton
|
|
147
|
+
daemon with its storage-only extension. The build runs the upstream core suite
|
|
148
|
+
with the extension's integration test. Published users do not need Go.
|
|
149
|
+
|
|
150
|
+
Tests compare all 48 request shapes against the Python reference, use real
|
|
151
|
+
Chromium and Galleton with a local HTTP fixture, and exercise the built executable.
|
|
152
|
+
They never require a Semrush account. See [verification](docs/verification.md) for
|
|
153
|
+
the separate live-account evidence.
|
|
154
|
+
|
|
155
|
+
## Uninstall
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
semrush-cli auth logout
|
|
159
|
+
semrush-cli daemon stop
|
|
160
|
+
npm uninstall -g semrush-cli
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The private vault and browser cache remain on disk. Remove them only after
|
|
164
|
+
checking they contain nothing you need.
|
|
165
|
+
|
|
166
|
+
MIT. Independent project, not affiliated with Semrush.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## Galleton
|
|
4
|
+
|
|
5
|
+
The TypeScript SDK and bundled daemon derive from Microck/galleton, commit
|
|
6
|
+
`efe7b742536d6c118c37a269bf626c5927e2a1b9`, under MIT.
|
|
7
|
+
The original license is included at `src/vendor/GALLETON-LICENSE`.
|
|
8
|
+
The SDK source is unchanged. The daemon includes the documented storage-only
|
|
9
|
+
provider patch at `patches/galleton-static-provider.patch` in the source repository.
|
|
10
|
+
|
|
11
|
+
## Semrush web report contracts
|
|
12
|
+
|
|
13
|
+
The report catalog, schemas and request-shape fixtures derive from
|
|
14
|
+
Microck/semrush-web-mcp, commit `764d102f`, under MIT.
|
|
15
|
+
Copyright (c) 2026 Microck. See the repository LICENSE.
|
|
16
|
+
|
|
17
|
+
All package dependencies retain their own licenses in their installed packages.
|
|
18
|
+
This project is independent of Semrush and does not include Semrush trademarks
|
|
19
|
+
or credentials in its distribution.
|
package/assets/logo.png
ADDED
|
Binary file
|
package/assets/logo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>semrush-cli</title><path d="m9 19 12 13L9 45" fill="none" stroke="#fff" stroke-width="7" stroke-linejoin="miter"/><path fill="#ff8a3d" d="M27 36h7v12h-7zm11-9h7v21h-7zm11-11h7v32h-7z"/></svg>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { n as SessionError, r as UsageError } from "./errors-BhvanypE.js";
|
|
2
|
+
import { i as Sessions, n as origin } from "./session-layer-BUU0ZG9M.js";
|
|
3
|
+
import "./browser-service-BYYE-vSm.js";
|
|
4
|
+
import { t as readBootstrap } from "./browser-layer-D6mEyv9Y.js";
|
|
5
|
+
import { Config, Effect, Option } from "effect";
|
|
6
|
+
import { chromium } from "patchright";
|
|
7
|
+
|
|
8
|
+
//#region src/auth.ts
|
|
9
|
+
const authFailed = () => new SessionError({ message: "Could not import a signed-in Semrush session. Check the browser, complete sign-in, and retry. No credentials were logged." });
|
|
10
|
+
const authCall = (run) => Effect.tryPromise({
|
|
11
|
+
try: run,
|
|
12
|
+
catch: authFailed
|
|
13
|
+
});
|
|
14
|
+
const saveContext = Effect.fn("Auth.saveContext")(function* (context) {
|
|
15
|
+
const sessions = yield* Sessions;
|
|
16
|
+
const page = yield* Effect.acquireRelease(authCall(() => context.newPage()), (page$1) => Effect.promise(() => page$1.close()));
|
|
17
|
+
yield* authCall(() => page.goto(origin + "/robots.txt", {
|
|
18
|
+
waitUntil: "domcontentloaded",
|
|
19
|
+
timeout: 45e3
|
|
20
|
+
}));
|
|
21
|
+
const html = yield* authCall(() => page.evaluate(async () => {
|
|
22
|
+
const response = await fetch("/analytics/overview/?db=us", {
|
|
23
|
+
credentials: "include",
|
|
24
|
+
signal: AbortSignal.timeout(45e3)
|
|
25
|
+
});
|
|
26
|
+
return response.ok ? response.text() : "";
|
|
27
|
+
}));
|
|
28
|
+
yield* Effect.try({
|
|
29
|
+
try: () => readBootstrap(html),
|
|
30
|
+
catch: authFailed
|
|
31
|
+
});
|
|
32
|
+
const cookies = yield* authCall(() => context.cookies(origin));
|
|
33
|
+
if (!cookies.length) return yield* Effect.fail(authFailed());
|
|
34
|
+
return yield* sessions.save({
|
|
35
|
+
provider: "semrush",
|
|
36
|
+
cookie_origin: origin,
|
|
37
|
+
cookie_header: cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ")
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const importBrowser = Effect.fn("Auth.import")(function* (url) {
|
|
41
|
+
const valid = yield* Effect.try({
|
|
42
|
+
try: () => {
|
|
43
|
+
const parsed = new URL(url);
|
|
44
|
+
if (parsed.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(parsed.hostname) || parsed.username || parsed.password) throw new Error("Not loopback");
|
|
45
|
+
return parsed.origin;
|
|
46
|
+
},
|
|
47
|
+
catch: () => new UsageError({ message: "CDP must be a literal loopback HTTP URL. Forward remote browsers over SSH first." })
|
|
48
|
+
});
|
|
49
|
+
const browser = yield* authCall(() => chromium.connectOverCDP(valid, { timeout: 3e4 }));
|
|
50
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => browser.close()));
|
|
51
|
+
const context = browser.contexts()[0];
|
|
52
|
+
if (!context) return yield* Effect.fail(authFailed());
|
|
53
|
+
return {
|
|
54
|
+
logged_in: true,
|
|
55
|
+
source: "galleton",
|
|
56
|
+
session: (yield* saveContext(context)).id
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
const login = Effect.fn("Auth.login")(function* () {
|
|
60
|
+
const executable = yield* Config.option(Config.String("SEMRUSH_BROWSER_EXECUTABLE"));
|
|
61
|
+
const browser = yield* Effect.acquireRelease(authCall(() => chromium.launch({
|
|
62
|
+
headless: false,
|
|
63
|
+
executablePath: Option.getOrUndefined(executable)
|
|
64
|
+
})), (browser$1) => Effect.promise(() => browser$1.close()));
|
|
65
|
+
const context = yield* authCall(() => browser.newContext({ userAgent: "OpenAI File Downloader, XaiImageApiFetch/1.0" }));
|
|
66
|
+
const page = yield* authCall(() => context.newPage());
|
|
67
|
+
yield* authCall(() => page.goto(origin + "/login/", {
|
|
68
|
+
waitUntil: "domcontentloaded",
|
|
69
|
+
timeout: 45e3
|
|
70
|
+
}));
|
|
71
|
+
yield* Effect.sync(() => process.stderr.write("Sign into Semrush in the browser. Waiting up to 5 minutes.\n"));
|
|
72
|
+
yield* authCall(() => page.waitForURL((url) => url.hostname === "www.semrush.com" && !url.pathname.startsWith("/login") && !url.pathname.startsWith("/signup"), { timeout: 3e5 }));
|
|
73
|
+
return {
|
|
74
|
+
logged_in: true,
|
|
75
|
+
source: "galleton",
|
|
76
|
+
session: (yield* saveContext(context)).id
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { importBrowser, login };
|
|
82
|
+
//# sourceMappingURL=auth-BcUPx0Nk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-BcUPx0Nk.js","names":["page","browser"],"sources":["../src/auth.ts"],"sourcesContent":["import { Config, Effect, Option } from 'effect';\nimport { chromium, type BrowserContext } from 'patchright';\nimport { Sessions } from './session-service.js';\nimport { origin } from './session-layer.js';\nimport { SessionError, UsageError } from './errors.js';\nimport { readBootstrap } from './browser-layer.js';\n\nconst authFailed = () => new SessionError({ message: 'Could not import a signed-in Semrush session. Check the browser, complete sign-in, and retry. No credentials were logged.' });\nconst authCall = <A>(run: () => Promise<A>) => Effect.tryPromise({ try: run, catch: authFailed });\n\nconst saveContext = Effect.fn('Auth.saveContext')(function* (context: BrowserContext) {\n const sessions = yield* Sessions;\n const page = yield* Effect.acquireRelease(authCall(() => context.newPage()), page => Effect.promise(() => page.close()));\n yield* authCall(() => page.goto(origin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n const html = yield* authCall(() => page.evaluate(async () => {\n const response = await fetch('/analytics/overview/?db=us', { credentials: 'include', signal: AbortSignal.timeout(45000) });\n return response.ok ? response.text() : '';\n }));\n yield* Effect.try({ try: () => readBootstrap(html), catch: authFailed });\n const cookies = yield* authCall(() => context.cookies(origin));\n if (!cookies.length) return yield* Effect.fail(authFailed());\n // Bare cookies are intentionally bound to the single approved origin by Galleton.\n return yield* sessions.save({ provider: 'semrush', cookie_origin: origin, cookie_header: cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ') });\n});\n\nexport const importBrowser = Effect.fn('Auth.import')(function* (url: string) {\n const valid = yield* Effect.try({ try: () => {\n const parsed = new URL(url);\n if (parsed.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(parsed.hostname) || parsed.username || parsed.password) throw new Error('Not loopback');\n return parsed.origin;\n }, catch: () => new UsageError({ message: 'CDP must be a literal loopback HTTP URL. Forward remote browsers over SSH first.' }) });\n const browser = yield* authCall(() => chromium.connectOverCDP(valid, { timeout: 30000 }));\n // Closing a CDP connection disconnects this client and leaves the existing browser running.\n yield* Effect.addFinalizer(() => Effect.promise(() => browser.close()));\n const context = browser.contexts()[0];\n if (!context) return yield* Effect.fail(authFailed());\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n\nexport const login = Effect.fn('Auth.login')(function* () {\n const executable = yield* Config.option(Config.String('SEMRUSH_BROWSER_EXECUTABLE'));\n const browser = yield* Effect.acquireRelease(authCall(() => chromium.launch({ headless: false, executablePath: Option.getOrUndefined(executable) })), browser => Effect.promise(() => browser.close()));\n const context = yield* authCall(() => browser.newContext({ userAgent: 'OpenAI File Downloader, XaiImageApiFetch/1.0' }));\n const page = yield* authCall(() => context.newPage());\n yield* authCall(() => page.goto(origin + '/login/', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n yield* Effect.sync(() => process.stderr.write('Sign into Semrush in the browser. Waiting up to 5 minutes.\\n'));\n yield* authCall(() => page.waitForURL(url => url.hostname === 'www.semrush.com' && !url.pathname.startsWith('/login') && !url.pathname.startsWith('/signup'), { timeout: 300000 }));\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { i as reportParameters, n as SessionError, t as ReportError } from "./errors-BhvanypE.js";
|
|
2
|
+
import { i as Sessions, n as origin } from "./session-layer-BUU0ZG9M.js";
|
|
3
|
+
import { t as Semrush } from "./browser-service-BYYE-vSm.js";
|
|
4
|
+
import { Config, Effect, Layer, Option, Predicate, Schema, Semaphore } from "effect";
|
|
5
|
+
import { chromium } from "patchright";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import JSONbig from "json-bigint";
|
|
8
|
+
|
|
9
|
+
//#region src/browser-layer.ts
|
|
10
|
+
const transportPaths = new Map([
|
|
11
|
+
["domain", "/dpa/rpc"],
|
|
12
|
+
["direct", "/dpa/rpc"],
|
|
13
|
+
["magic", "/kmtgw/v2/webapi"],
|
|
14
|
+
["keyword", "/kwogw/v2/webapi"]
|
|
15
|
+
]);
|
|
16
|
+
const json = JSONbig({
|
|
17
|
+
storeAsString: true,
|
|
18
|
+
protoAction: "error",
|
|
19
|
+
constructorAction: "error"
|
|
20
|
+
});
|
|
21
|
+
const safeBrowserError = () => new ReportError({ message: "Semrush browser request failed. Run semrush-cli doctor. If your session expired, sign in again with semrush-cli auth login." });
|
|
22
|
+
const browserCall = (run) => Effect.tryPromise({
|
|
23
|
+
try: run,
|
|
24
|
+
catch: safeBrowserError
|
|
25
|
+
});
|
|
26
|
+
const objectValue = (value) => Predicate.isObject(value) && !Array.isArray(value);
|
|
27
|
+
/** JSON never passes through the browser's number parser, preserving 64-bit IDs. */
|
|
28
|
+
function parseWire(text) {
|
|
29
|
+
return Schema.decodeUnknownSync(Schema.Json)(json.parse(text));
|
|
30
|
+
}
|
|
31
|
+
/** Remove credential fields and echoed secrets, including nested provider errors. */
|
|
32
|
+
function redact(value, secrets) {
|
|
33
|
+
if (Array.isArray(value)) return value.map((item) => redact(item, secrets));
|
|
34
|
+
if (objectValue(value)) return Object.fromEntries(Object.entries(value).filter(([key]) => !/^(api_?key|key|user_?id|access_token|refresh_token|authorization|cookie|set-cookie)$/i.test(key)).map(([key, item]) => [key, redact(item, secrets)]));
|
|
35
|
+
if (Predicate.isString(value)) return secrets.filter((secret) => secret.length > 0).reduce((text, secret) => text.replaceAll(secret, "[redacted]"), value);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(function* () {
|
|
39
|
+
const sessions = yield* Sessions;
|
|
40
|
+
const executable = yield* Config.option(Config.String("SEMRUSH_BROWSER_EXECUTABLE"));
|
|
41
|
+
const headers = yield* sessions.headers();
|
|
42
|
+
const browser = yield* Effect.acquireRelease(browserCall(() => chromium.launch({
|
|
43
|
+
headless: true,
|
|
44
|
+
executablePath: Option.getOrUndefined(executable)
|
|
45
|
+
})), (browser$1) => Effect.promise(() => browser$1.close()));
|
|
46
|
+
const context = yield* browserCall(() => browser.newContext({ userAgent: "OpenAI File Downloader, XaiImageApiFetch/1.0" }));
|
|
47
|
+
const cookieHeader = headers.Cookie ?? headers.cookie ?? "";
|
|
48
|
+
yield* browserCall(() => context.addCookies(cookieHeader.split(";").filter((cookie) => cookie.includes("=")).map((cookie) => {
|
|
49
|
+
const separator = cookie.indexOf("=");
|
|
50
|
+
return {
|
|
51
|
+
name: cookie.slice(0, separator).trim(),
|
|
52
|
+
value: cookie.slice(separator + 1).trim(),
|
|
53
|
+
url: browserOrigin,
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
secure: browserOrigin.startsWith("https:"),
|
|
56
|
+
sameSite: "Lax"
|
|
57
|
+
};
|
|
58
|
+
})));
|
|
59
|
+
const page = yield* browserCall(() => context.newPage());
|
|
60
|
+
yield* browserCall(() => page.goto(browserOrigin + "/robots.txt", {
|
|
61
|
+
waitUntil: "domcontentloaded",
|
|
62
|
+
timeout: 45e3
|
|
63
|
+
}));
|
|
64
|
+
const bootstrap = yield* browserCall(() => page.evaluate(async () => {
|
|
65
|
+
const response = await fetch("/analytics/overview/?db=us", {
|
|
66
|
+
credentials: "include",
|
|
67
|
+
signal: AbortSignal.timeout(45e3)
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
status: response.status,
|
|
71
|
+
text: response.ok ? await response.text() : ""
|
|
72
|
+
};
|
|
73
|
+
}));
|
|
74
|
+
const auth = yield* Effect.try({
|
|
75
|
+
try: () => readBootstrap(bootstrap.text),
|
|
76
|
+
catch: () => new SessionError({ message: "Semrush sign-in metadata unavailable. Run semrush-cli auth login or import a signed-in browser." })
|
|
77
|
+
});
|
|
78
|
+
const semaphore = yield* Semaphore.make(1);
|
|
79
|
+
let spectrumToken = "";
|
|
80
|
+
const request = Effect.fn("Semrush.request")(function* (path, payload, get = false, extraHeaders = {}) {
|
|
81
|
+
const wire = yield* browserCall(() => page.evaluate(async ({ path: path$1, encodedPayload, get: get$1, extraHeaders: extraHeaders$1 }) => {
|
|
82
|
+
const payload$1 = JSON.parse(encodedPayload);
|
|
83
|
+
const query = new URLSearchParams();
|
|
84
|
+
if (get$1) for (const [key, value] of Object.entries(payload$1)) for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));
|
|
85
|
+
const options = {
|
|
86
|
+
method: get$1 ? "GET" : "POST",
|
|
87
|
+
credentials: "include",
|
|
88
|
+
headers: extraHeaders$1,
|
|
89
|
+
signal: AbortSignal.timeout(45e3)
|
|
90
|
+
};
|
|
91
|
+
if (!get$1) {
|
|
92
|
+
options.headers = {
|
|
93
|
+
...extraHeaders$1,
|
|
94
|
+
"Content-Type": "application/json"
|
|
95
|
+
};
|
|
96
|
+
options.body = JSON.stringify(payload$1);
|
|
97
|
+
}
|
|
98
|
+
const response = await fetch(get$1 ? path$1 + "?" + query : path$1, options);
|
|
99
|
+
return {
|
|
100
|
+
status: response.status,
|
|
101
|
+
text: response.ok ? await response.text() : ""
|
|
102
|
+
};
|
|
103
|
+
}, {
|
|
104
|
+
path,
|
|
105
|
+
encodedPayload: JSON.stringify(payload),
|
|
106
|
+
get,
|
|
107
|
+
extraHeaders
|
|
108
|
+
}));
|
|
109
|
+
if (wire.status !== 200) return yield* Effect.fail(new ReportError({ message: `Semrush report returned HTTP ${wire.status}. Check your session and account permissions.` }));
|
|
110
|
+
const body = yield* Effect.try({
|
|
111
|
+
try: () => parseWire(wire.text),
|
|
112
|
+
catch: () => new ReportError({ message: "Semrush returned a page or challenge instead of JSON. Sign in again or retry later." })
|
|
113
|
+
});
|
|
114
|
+
if (objectValue(body) && ("error" in body || "status" in body && !(body.status === null || body.status === "SUCCESS" || body.status === "success"))) return yield* Effect.fail(new ReportError({ message: `Semrush report failed: ${JSON.stringify(redact(body.error ?? body.status ?? null, [
|
|
115
|
+
auth.apiKey,
|
|
116
|
+
spectrumToken,
|
|
117
|
+
spectrumToken.replace(/^Bearer /, "")
|
|
118
|
+
])).slice(0, 500)}` }));
|
|
119
|
+
return body;
|
|
120
|
+
});
|
|
121
|
+
const rpc = Effect.fn("Semrush.rpc")(function* (method, params, transport = "keyword", webReport = "domain.overview") {
|
|
122
|
+
const payload = transport === "domain" ? {
|
|
123
|
+
request_id: randomUUID(),
|
|
124
|
+
report: webReport,
|
|
125
|
+
args: params,
|
|
126
|
+
...auth
|
|
127
|
+
} : transport === "direct" ? {
|
|
128
|
+
...params,
|
|
129
|
+
...auth
|
|
130
|
+
} : params;
|
|
131
|
+
const body = yield* request(transportPaths.get(transport) ?? "/kwogw/v2/webapi", {
|
|
132
|
+
id: 1,
|
|
133
|
+
jsonrpc: "2.0",
|
|
134
|
+
method,
|
|
135
|
+
params: payload
|
|
136
|
+
});
|
|
137
|
+
if (!objectValue(body) || !("result" in body)) return yield* Effect.fail(new ReportError({ message: `Semrush ${method}: missing report result.` }));
|
|
138
|
+
return body.result ?? null;
|
|
139
|
+
});
|
|
140
|
+
const report = Effect.fn("Semrush.report")(function* ({ name, definition, query }) {
|
|
141
|
+
if (!(yield* browserCall(() => page.evaluate(async () => {
|
|
142
|
+
const response = await fetch("/home/", {
|
|
143
|
+
method: "HEAD",
|
|
144
|
+
credentials: "include",
|
|
145
|
+
cache: "no-store",
|
|
146
|
+
signal: AbortSignal.timeout(15e3)
|
|
147
|
+
});
|
|
148
|
+
return response.ok && new URL(response.url).pathname === "/home/";
|
|
149
|
+
})))) return yield* Effect.fail(new SessionError({ message: "Semrush session expired. Run semrush-cli auth login or import a signed-in browser." }));
|
|
150
|
+
const params = reportParameters(definition, query);
|
|
151
|
+
const family = definition.transport;
|
|
152
|
+
let reportData;
|
|
153
|
+
if ([
|
|
154
|
+
"keyword",
|
|
155
|
+
"domain",
|
|
156
|
+
"direct",
|
|
157
|
+
"magic"
|
|
158
|
+
].includes(family)) reportData = yield* rpc(definition.method, params, family, definition.web_report);
|
|
159
|
+
else {
|
|
160
|
+
const extra = {};
|
|
161
|
+
if (family === "gap") {
|
|
162
|
+
if (!spectrumToken) spectrumToken = yield* browserCall(() => observeSpectrum(page, browserOrigin));
|
|
163
|
+
Object.assign(extra, {
|
|
164
|
+
authorization: spectrumToken,
|
|
165
|
+
"x-report": "gap.keywords",
|
|
166
|
+
"x-request-id": randomUUID()
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
let path = definition.method;
|
|
170
|
+
if (family.startsWith("backlink")) {
|
|
171
|
+
path = "/analytics/backlinks/webapi2/";
|
|
172
|
+
params.key = auth.apiKey;
|
|
173
|
+
if (family === "backlink_overview") {
|
|
174
|
+
path += "overview/" + definition.method;
|
|
175
|
+
extra["x-request-id"] = randomUUID();
|
|
176
|
+
yield* request("/analytics/backlinks/webapi2/overview/init", params, true, extra);
|
|
177
|
+
}
|
|
178
|
+
} else if (path.startsWith("/siteaudit/")) params.key = auth.apiKey;
|
|
179
|
+
reportData = yield* request(path, params, ![
|
|
180
|
+
"post",
|
|
181
|
+
"gap",
|
|
182
|
+
"projects"
|
|
183
|
+
].includes(family), extra);
|
|
184
|
+
}
|
|
185
|
+
if (definition.total_method) {
|
|
186
|
+
const totalParams = name === "domain_position_changes" ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "display")) : params;
|
|
187
|
+
const total = yield* rpc(definition.total_method, totalParams, "domain", definition.web_report);
|
|
188
|
+
reportData = {
|
|
189
|
+
rows: reportData,
|
|
190
|
+
total,
|
|
191
|
+
page: query.page ?? null,
|
|
192
|
+
page_size: query.page_size ?? null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (name === "keyword_overview") {
|
|
196
|
+
const rows = objectValue(reportData) ? reportData.keywords : void 0;
|
|
197
|
+
reportData = Array.isArray(rows) ? rows.find((item) => objectValue(item) && item.database === query.database) : void 0;
|
|
198
|
+
if (!reportData) return yield* Effect.fail(new ReportError({ message: "No keyword data for the selected database." }));
|
|
199
|
+
} else if (name === "domain_overview") {
|
|
200
|
+
const search = Array.isArray(reportData) ? reportData.find((item) => objectValue(item) && item.database === query.database) : void 0;
|
|
201
|
+
if (!search) return yield* Effect.fail(new ReportError({ message: "No domain data for the selected database." }));
|
|
202
|
+
reportData = {
|
|
203
|
+
search,
|
|
204
|
+
backlinks: yield* rpc("backlinks.Summary", {
|
|
205
|
+
searchItem: query.domain ?? null,
|
|
206
|
+
searchType: "domain"
|
|
207
|
+
}, "domain")
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (Predicate.isNumber(query.limit) && Array.isArray(reportData)) reportData = reportData.slice(0, query.limit);
|
|
211
|
+
yield* captureCookies(context, sessions.capture, browserOrigin);
|
|
212
|
+
return {
|
|
213
|
+
authenticated: true,
|
|
214
|
+
report: name,
|
|
215
|
+
data: redact(reportData ?? null, [
|
|
216
|
+
auth.apiKey,
|
|
217
|
+
spectrumToken,
|
|
218
|
+
spectrumToken.replace(/^Bearer /, "")
|
|
219
|
+
])
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
return Semrush.of({ report: (request$1) => semaphore.withPermits(1)(report(request$1)) });
|
|
223
|
+
}));
|
|
224
|
+
const semrushLive = makeSemrushLayer(origin);
|
|
225
|
+
function readBootstrap(html) {
|
|
226
|
+
const match = /window\.sm2\.user\s*=\s*/.exec(html);
|
|
227
|
+
if (!match) throw new Error("No bootstrap");
|
|
228
|
+
const start = match.index + match[0].length;
|
|
229
|
+
let depth = 0, quoted = false, escaped = false;
|
|
230
|
+
for (let index = start; index < html.length; index++) {
|
|
231
|
+
const char = html[index];
|
|
232
|
+
if (quoted) {
|
|
233
|
+
if (escaped) escaped = false;
|
|
234
|
+
else if (char === "\\") escaped = true;
|
|
235
|
+
else if (char === "\"") quoted = false;
|
|
236
|
+
} else if (char === "\"") quoted = true;
|
|
237
|
+
else if (char === "{") depth++;
|
|
238
|
+
else if (char === "}" && --depth === 0) {
|
|
239
|
+
const user = parseWire(html.slice(start, index + 1));
|
|
240
|
+
if (objectValue(user) && Predicate.isString(user.api_key) && (Predicate.isString(user.id) || Predicate.isNumber(user.id))) return {
|
|
241
|
+
apiKey: user.api_key,
|
|
242
|
+
userId: user.id
|
|
243
|
+
};
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw new Error("Invalid bootstrap");
|
|
248
|
+
}
|
|
249
|
+
async function observeSpectrum(page, browserOrigin) {
|
|
250
|
+
try {
|
|
251
|
+
const [observed] = await Promise.all([page.waitForRequest((request) => request.url().startsWith(browserOrigin + "/spectrum/") && Boolean(request.headers().authorization), { timeout: 2e4 }), page.goto(browserOrigin + "/analytics/keywordgap/?db=us", {
|
|
252
|
+
waitUntil: "domcontentloaded",
|
|
253
|
+
timeout: 45e3
|
|
254
|
+
})]);
|
|
255
|
+
return observed.headers().authorization ?? "";
|
|
256
|
+
} finally {
|
|
257
|
+
await page.goto(browserOrigin + "/robots.txt", {
|
|
258
|
+
waitUntil: "domcontentloaded",
|
|
259
|
+
timeout: 45e3
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const captureCookies = Effect.fn("Semrush.captureCookies")(function* (context, capture, cookieOrigin) {
|
|
264
|
+
yield* capture((yield* browserCall(() => context.cookies(cookieOrigin))).map((cookie) => `${cookie.name}=${cookie.value}; Path=${cookie.path}${cookieOrigin.startsWith("https:") ? "; Secure" : ""}${cookie.httpOnly ? "; HttpOnly" : ""}${cookie.expires > 0 ? "; Expires=" + (/* @__PURE__ */ new Date(cookie.expires * 1e3)).toUTCString() : ""}`));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
export { semrushLive as n, readBootstrap as t };
|
|
269
|
+
//# sourceMappingURL=browser-layer-D6mEyv9Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-layer-D6mEyv9Y.js","names":["browser","payload: Schema.JsonObject","get","payload","options: RequestInit","extraHeaders","path","reportData: Schema.Json | undefined","extra: Record<string, string>","request"],"sources":["../src/browser-layer.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { Config, Effect, Layer, Option, Semaphore, Schema, Predicate } from 'effect';\nimport { chromium, type BrowserContext, type Page } from 'patchright';\nimport JSONbig from 'json-bigint';\nimport { Semrush } from './browser-service.js';\nimport { Sessions } from './session-service.js';\nimport { origin } from './session-layer.js';\nimport { ReportError, SessionError } from './errors.js';\nimport { reportParameters, type Query } from './report-parameters.js';\nimport type { ReportRequest } from './validation.js';\n\nconst transportPaths = new Map([['domain', '/dpa/rpc'], ['direct', '/dpa/rpc'], ['magic', '/kmtgw/v2/webapi'], ['keyword', '/kwogw/v2/webapi']]);\nconst json = JSONbig({ storeAsString: true, protoAction: 'error', constructorAction: 'error' });\nconst safeBrowserError = () => new ReportError({ message: 'Semrush browser request failed. Run semrush-cli doctor. If your session expired, sign in again with semrush-cli auth login.' });\nconst browserCall = <A>(run: () => Promise<A>) => Effect.tryPromise({ try: run, catch: safeBrowserError });\nconst objectValue = (value: Schema.Json | undefined): value is Schema.JsonObject => Predicate.isObject(value) && !Array.isArray(value);\n\n/** JSON never passes through the browser's number parser, preserving 64-bit IDs. */\nexport function parseWire(text: string): Schema.Json { return Schema.decodeUnknownSync(Schema.Json)(json.parse(text)); }\n\n/** Remove credential fields and echoed secrets, including nested provider errors. */\nexport function redact(value: Schema.Json, secrets: string[]): Schema.Json {\n if (Array.isArray(value)) return value.map(item => redact(item, secrets));\n if (objectValue(value)) return Object.fromEntries(Object.entries(value).filter(([key]) => !/^(api_?key|key|user_?id|access_token|refresh_token|authorization|cookie|set-cookie)$/i.test(key)).map(([key, item]) => [key, redact(item, secrets)]));\n if (Predicate.isString(value)) return secrets.filter(secret => secret.length > 0).reduce((text, secret) => text.replaceAll(secret, '[redacted]'), value);\n return value;\n}\n\nexport const makeSemrushLayer = (browserOrigin: string) => Layer.effect(Semrush, Effect.gen(function* () {\n const sessions = yield* Sessions;\n const executable = yield* Config.option(Config.String('SEMRUSH_BROWSER_EXECUTABLE'));\n const headers = yield* sessions.headers();\n const browser = yield* Effect.acquireRelease(browserCall(() => chromium.launch({ headless: true, executablePath: Option.getOrUndefined(executable) })), browser => Effect.promise(() => browser.close()));\n const context = yield* browserCall(() => browser.newContext({ userAgent: 'OpenAI File Downloader, XaiImageApiFetch/1.0' }));\n const cookieHeader = headers.Cookie ?? headers.cookie ?? '';\n yield* browserCall(() => context.addCookies(cookieHeader.split(';').filter(cookie => cookie.includes('=')).map(cookie => {\n const separator = cookie.indexOf('=');\n return { name: cookie.slice(0, separator).trim(), value: cookie.slice(separator + 1).trim(), url: browserOrigin, httpOnly: true, secure: browserOrigin.startsWith('https:'), sameSite: 'Lax' as const };\n })));\n const page = yield* browserCall(() => context.newPage());\n yield* browserCall(() => page.goto(browserOrigin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n const bootstrap = yield* browserCall(() => page.evaluate(async () => {\n const response = await fetch('/analytics/overview/?db=us', { credentials: 'include', signal: AbortSignal.timeout(45000) });\n return { status: response.status, text: response.ok ? await response.text() : '' };\n }));\n // Extract the JSON value with a balanced scanner, never eval page-owned JavaScript.\n const auth = yield* Effect.try({ try: () => readBootstrap(bootstrap.text), catch: () => new SessionError({ message: 'Semrush sign-in metadata unavailable. Run semrush-cli auth login or import a signed-in browser.' }) });\n const semaphore = yield* Semaphore.make(1);\n let spectrumToken = '';\n const request = Effect.fn('Semrush.request')(function* (path: string, payload: Query, get = false, extraHeaders: Record<string, string> = {}) {\n const wire = yield* browserCall(() => page.evaluate(async ({ path, encodedPayload, get, extraHeaders }) => {\n const payload: Schema.JsonObject = JSON.parse(encodedPayload);\n const query = new URLSearchParams();\n if (get) for (const [key, value] of Object.entries(payload)) for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));\n const options: RequestInit = { method: get ? 'GET' : 'POST', credentials: 'include', headers: extraHeaders, signal: AbortSignal.timeout(45000) };\n if (!get) { options.headers = { ...extraHeaders, 'Content-Type': 'application/json' }; options.body = JSON.stringify(payload); }\n const response = await fetch(get ? path + '?' + query : path, options);\n return { status: response.status, text: response.ok ? await response.text() : '' };\n }, { path, encodedPayload: JSON.stringify(payload), get, extraHeaders }));\n if (wire.status !== 200) return yield* Effect.fail(new ReportError({ message: `Semrush report returned HTTP ${wire.status}. Check your session and account permissions.` }));\n const body = yield* Effect.try({ try: () => parseWire(wire.text), catch: () => new ReportError({ message: 'Semrush returned a page or challenge instead of JSON. Sign in again or retry later.' }) });\n if (objectValue(body) && ('error' in body || ('status' in body && !(body.status === null || body.status === 'SUCCESS' || body.status === 'success')))) return yield* Effect.fail(new ReportError({ message: `Semrush report failed: ${JSON.stringify(redact(body.error ?? body.status ?? null, [auth.apiKey, spectrumToken, spectrumToken.replace(/^Bearer /, '')])).slice(0, 500)}` }));\n return body;\n });\n const rpc = Effect.fn('Semrush.rpc')(function* (method: string, params: Query, transport = 'keyword', webReport = 'domain.overview') {\n const payload = transport === 'domain' ? { request_id: randomUUID(), report: webReport, args: params, ...auth } : transport === 'direct' ? { ...params, ...auth } : params;\n const body = yield* request(transportPaths.get(transport) ?? '/kwogw/v2/webapi', { id: 1, jsonrpc: '2.0', method, params: payload });\n if (!objectValue(body) || !('result' in body)) return yield* Effect.fail(new ReportError({ message: `Semrush ${method}: missing report result.` }));\n return body.result ?? null;\n });\n const report = Effect.fn('Semrush.report')(function* ({ name, definition, query }: ReportRequest) {\n const valid = yield* browserCall(() => page.evaluate(async () => {\n const response = await fetch('/home/', { method: 'HEAD', credentials: 'include', cache: 'no-store', signal: AbortSignal.timeout(15000) });\n return response.ok && new URL(response.url).pathname === '/home/';\n }));\n if (!valid) return yield* Effect.fail(new SessionError({ message: 'Semrush session expired. Run semrush-cli auth login or import a signed-in browser.' }));\n const params = reportParameters(definition, query);\n const family = definition.transport;\n let reportData: Schema.Json | undefined;\n if (['keyword', 'domain', 'direct', 'magic'].includes(family)) reportData = yield* rpc(definition.method, params, family, definition.web_report);\n else {\n const extra: Record<string, string> = {};\n if (family === 'gap') {\n if (!spectrumToken) spectrumToken = yield* browserCall(() => observeSpectrum(page, browserOrigin));\n Object.assign(extra, { authorization: spectrumToken, 'x-report': 'gap.keywords', 'x-request-id': randomUUID() });\n }\n let path = definition.method;\n if (family.startsWith('backlink')) {\n path = '/analytics/backlinks/webapi2/';\n params.key = auth.apiKey;\n if (family === 'backlink_overview') {\n path += 'overview/' + definition.method;\n extra['x-request-id'] = randomUUID();\n yield* request('/analytics/backlinks/webapi2/overview/init', params, true, extra);\n }\n } else if (path.startsWith('/siteaudit/')) params.key = auth.apiKey;\n reportData = yield* request(path, params, !['post', 'gap', 'projects'].includes(family), extra);\n }\n if (definition.total_method) {\n const totalParams = name === 'domain_position_changes' ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== 'display')) : params;\n const total = yield* rpc(definition.total_method, totalParams, 'domain', definition.web_report);\n reportData = { rows: reportData, total, page: query.page ?? null, page_size: query.page_size ?? null };\n }\n if (name === 'keyword_overview') {\n const rows = objectValue(reportData) ? reportData.keywords : undefined;\n reportData = Array.isArray(rows) ? rows.find(item => objectValue(item) && item.database === query.database) : undefined;\n if (!reportData) return yield* Effect.fail(new ReportError({ message: 'No keyword data for the selected database.' }));\n } else if (name === 'domain_overview') {\n const search = Array.isArray(reportData) ? reportData.find(item => objectValue(item) && item.database === query.database) : undefined;\n if (!search) return yield* Effect.fail(new ReportError({ message: 'No domain data for the selected database.' }));\n reportData = { search, backlinks: yield* rpc('backlinks.Summary', { searchItem: query.domain ?? null, searchType: 'domain' }, 'domain') };\n }\n if (Predicate.isNumber(query.limit) && Array.isArray(reportData)) reportData = reportData.slice(0, query.limit);\n yield* captureCookies(context, sessions.capture, browserOrigin);\n return { authenticated: true as const, report: name, data: redact(reportData ?? null, [auth.apiKey, spectrumToken, spectrumToken.replace(/^Bearer /, '')]) };\n });\n return Semrush.of({ report: request => semaphore.withPermits(1)(report(request)) });\n}));\n\nexport const semrushLive = makeSemrushLayer(origin);\n\ninterface BootstrapAuth { readonly apiKey: string; readonly userId: string | number; }\nexport function readBootstrap(html: string): BootstrapAuth {\n const match = /window\\.sm2\\.user\\s*=\\s*/.exec(html);\n if (!match) throw new Error('No bootstrap');\n const start = match.index + match[0].length;\n let depth = 0, quoted = false, escaped = false;\n for (let index = start; index < html.length; index++) {\n const char = html[index];\n if (quoted) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') quoted = false;\n } else if (char === '\"') quoted = true;\n else if (char === '{') depth++;\n else if (char === '}' && --depth === 0) {\n const user = parseWire(html.slice(start, index + 1));\n if (objectValue(user) && Predicate.isString(user.api_key) && (Predicate.isString(user.id) || Predicate.isNumber(user.id))) return { apiKey: user.api_key, userId: user.id };\n break;\n }\n }\n throw new Error('Invalid bootstrap');\n}\n\nasync function observeSpectrum(page: Page, browserOrigin: string): Promise<string> {\n try {\n // Observe and navigate together so either timeout is handled immediately.\n const [observed] = await Promise.all([\n page.waitForRequest(request => request.url().startsWith(browserOrigin + '/spectrum/') && Boolean(request.headers().authorization), { timeout: 20000 }),\n page.goto(browserOrigin + '/analytics/keywordgap/?db=us', { waitUntil: 'domcontentloaded', timeout: 45000 }),\n ]);\n return observed.headers().authorization ?? '';\n } finally {\n await page.goto(browserOrigin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 });\n }\n}\n\nconst captureCookies = Effect.fn('Semrush.captureCookies')(function* (context: BrowserContext, capture: (cookies: string[]) => Effect.Effect<void, SessionError>, cookieOrigin: string) {\n const cookies = yield* browserCall(() => context.cookies(cookieOrigin));\n yield* capture(cookies.map(cookie => `${cookie.name}=${cookie.value}; Path=${cookie.path}${cookieOrigin.startsWith('https:') ? '; Secure' : ''}${cookie.httpOnly ? '; HttpOnly' : ''}${cookie.expires > 0 ? '; Expires=' + new Date(cookie.expires * 1000).toUTCString() : ''}`));\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/browser-service.ts
|
|
4
|
+
/** One serialized browser context per runtime. Report callers never handle cookies or pages. */
|
|
5
|
+
var Semrush = class extends Context.Service()("semrush/Semrush") {};
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
export { Semrush as t };
|
|
9
|
+
//# sourceMappingURL=browser-service-BYYE-vSm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-service-BYYE-vSm.js","names":[],"sources":["../src/browser-service.ts"],"sourcesContent":["import { Context, Effect } from 'effect';\nimport type { ReportRequest } from './validation.js';\nimport type { ReportError, SessionError, UsageError } from './errors.js';\n\n/** One serialized browser context per runtime. Report callers never handle cookies or pages. */\nexport class Semrush extends Context.Service<Semrush, {\n readonly report: (request: ReportRequest) => Effect.Effect<{ authenticated: true; report: string; data: unknown }, ReportError | SessionError | UsageError>;\n}>()('semrush/Semrush') {}\n"],"mappings":";;;;AAKA,IAAa,UAAb,cAA6B,QAAQ,SAEjC,CAAC,kBAAkB,CAAC"}
|