v2.1: Bundle Bird X search - no external CLI needed
Vendor Bird's Twitter GraphQL search client directly into /last30days, eliminating the dependency on `npm install -g @steipete/bird`. X search now works out of the box with just Node.js 22+ and browser cookies. - Add vendored bird-search.mjs wrapper (search-only subset of Bird v0.8.0) - Vendor @steipete/sweet-cookie for browser cookie extraction - Update bird_x.py to call vendored Node.js module instead of `bird` binary - Update README.md and SKILL.md for v2.1 (remove Bird CLI install steps) - Include Bird's MIT LICENSE in vendor directory The fallback chain is: vendored search -> xAI API key -> web-only mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
# /last30days v2
|
# /last30days v2.1
|
||||||
|
|
||||||
**The AI world reinvents itself every month. This Claude Code skill keeps you current.** /last30days researches your topic across Reddit, X, and the web from the last 30 days, finds what the community is actually upvoting and sharing, and writes you a prompt that works today, not six months ago. Whether it's Ralph Wiggum loops, Suno music prompts, or the latest Midjourney techniques, you'll prompt like someone who's been paying attention.
|
**The AI world reinvents itself every month. This Claude Code skill keeps you current.** /last30days researches your topic across Reddit, X, and the web from the last 30 days, finds what the community is actually upvoting and sharing, and writes you a prompt that works today, not six months ago. Whether it's Ralph Wiggum loops, Suno music prompts, or the latest Midjourney techniques, you'll prompt like someone who's been paying attention.
|
||||||
|
|
||||||
**New in V2:** Dramatically better search results. Smarter query construction finds posts that V1 missed entirely, and a new two-phase search automatically discovers key @handles and subreddits from initial results, then drills deeper. Also: free X search via [Bird CLI](https://github.com/steipete/bird) (no xAI key needed), `--days=N` for flexible lookback, and automatic model fallback. [Full changelog below.](#whats-new-in-v2)
|
**New in V2.1:** X search is now fully bundled - no external `bird` CLI or xAI API key needed for X. Just have Node.js 22+ installed. Uses a vendored subset of Bird's Twitter GraphQL client (MIT licensed, originally by [@steipete](https://x.com/steipete)).
|
||||||
|
|
||||||
**The tradeoff:** V2 finds way more content but takes longer — typically 2-8 minutes depending on how niche your topic is. The old V1 was faster but regularly missed results (like returning 0 X posts on trending topics). We think the depth is worth the wait, but if you'd use a faster "quick mode" that trades some depth for speed, let us know: [@mvanhorn](https://x.com/mvanhorn) / [@slashlast30days](https://x.com/slashlast30days).
|
**New in V2:** Dramatically better search results. Smarter query construction finds posts that V1 missed entirely, and a new two-phase search automatically discovers key @handles and subreddits from initial results, then drills deeper. Free X search (no xAI key needed), `--days=N` for flexible lookback, and automatic model fallback. [Full changelog below.](#whats-new-in-v2)
|
||||||
|
|
||||||
|
**The tradeoff:** V2 finds way more content but takes longer - typically 2-8 minutes depending on how niche your topic is. The old V1 was faster but regularly missed results (like returning 0 X posts on trending topics). We think the depth is worth the wait, but if you'd use a faster "quick mode" that trades some depth for speed, let us know: [@mvanhorn](https://x.com/mvanhorn) / [@slashlast30days](https://x.com/slashlast30days).
|
||||||
|
|
||||||
**Best for prompt research**: discover what prompting techniques actually work for any tool (ChatGPT, Midjourney, Claude, Figma AI, etc.) by learning from real community discussions and best practices.
|
**Best for prompt research**: discover what prompting techniques actually work for any tool (ChatGPT, Midjourney, Claude, Figma AI, etc.) by learning from real community discussions and best practices.
|
||||||
|
|
||||||
@@ -20,21 +22,22 @@ git clone https://github.com/mvanhorn/last30days-skill.git ~/.claude/skills/last
|
|||||||
mkdir -p ~/.config/last30days
|
mkdir -p ~/.config/last30days
|
||||||
cat > ~/.config/last30days/.env << 'EOF'
|
cat > ~/.config/last30days/.env << 'EOF'
|
||||||
OPENAI_API_KEY=sk-...
|
OPENAI_API_KEY=sk-...
|
||||||
XAI_API_KEY=xai-... # optional if using Bird CLI
|
XAI_API_KEY=xai-... # optional - not needed for X search in v2.1
|
||||||
EOF
|
EOF
|
||||||
chmod 600 ~/.config/last30days/.env
|
chmod 600 ~/.config/last30days/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional: Bird CLI for free X search
|
### X Search Authentication
|
||||||
|
|
||||||
[Bird CLI](https://github.com/steipete/bird) lets you search X without an xAI API key. If installed and authenticated, /last30days uses it automatically.
|
V2.1 bundles X search directly - no external tools needed. Just be logged into x.com in Safari, Chrome, or Firefox and it auto-detects your session cookies.
|
||||||
|
|
||||||
|
Alternatively, set environment variables:
|
||||||
```bash
|
```bash
|
||||||
npm install -g @steipete/bird
|
export AUTH_TOKEN=your_auth_token # from x.com cookies
|
||||||
bird login
|
export CT0=your_ct0_token # from x.com cookies
|
||||||
```
|
```
|
||||||
|
|
||||||
Bird is free and doesn't require an xAI key. If both Bird and an xAI key are available, Bird is preferred.
|
**Requirements:** Node.js 22+ (for the vendored Twitter GraphQL client).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -780,11 +783,11 @@ This example shows /last30days discovering **emerging developer workflows** - re
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **OpenAI API key** - For Reddit research (uses web search via Responses API)
|
- **OpenAI API key** - For Reddit research (uses web search via Responses API)
|
||||||
- **X search** (one of):
|
- **Node.js 22+** - For X search (bundled Twitter GraphQL client)
|
||||||
- **Bird CLI** (free) - `npm install -g @steipete/bird && bird login`
|
- **X session** - Be logged into x.com in your browser, or set `AUTH_TOKEN`/`CT0` env vars
|
||||||
- **xAI API key** - Paid, uses Grok's live X search
|
- **xAI API key** (optional fallback) - If the bundled search can't authenticate, falls back to xAI's Grok API
|
||||||
|
|
||||||
At least one API key is required. Bird CLI is recommended for X search since it's free.
|
At least one API key is required. X search works automatically if you're logged into x.com in your browser.
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
@@ -792,7 +795,7 @@ At least one API key is required. Bird CLI is recommended for X search since it'
|
|||||||
|
|
||||||
**Phase 1: Broad discovery**
|
**Phase 1: Broad discovery**
|
||||||
- OpenAI Responses API with `web_search` tool scoped to reddit.com
|
- OpenAI Responses API with `web_search` tool scoped to reddit.com
|
||||||
- Bird CLI (or xAI API) for X/Twitter search
|
- Vendored Twitter GraphQL search (or xAI API fallback) for X search
|
||||||
- WebSearch for blogs, news, docs, tutorials
|
- WebSearch for blogs, news, docs, tutorials
|
||||||
- Reddit JSON enrichment for real engagement metrics (upvotes, comments)
|
- Reddit JSON enrichment for real engagement metrics (upvotes, comments)
|
||||||
- Scoring algorithm weighing recency, relevance, and engagement
|
- Scoring algorithm weighing recency, relevance, and engagement
|
||||||
@@ -825,9 +828,9 @@ V2 finds significantly more content than V1. Two major improvements:
|
|||||||
|
|
||||||
**Reddit JSON enrichment** - Fetches real upvote and comment counts from Reddit's free API for every thread, giving you actual engagement signals instead of estimates.
|
**Reddit JSON enrichment** - Fetches real upvote and comment counts from Reddit's free API for every thread, giving you actual engagement signals instead of estimates.
|
||||||
|
|
||||||
### Free X search via Bird CLI
|
### Bundled X search (v2.1)
|
||||||
|
|
||||||
**Bird CLI integration** - Search X without an xAI API key. Just `npm install -g @steipete/bird && bird login`. Auto-detected at runtime — if Bird is installed, it's used automatically. If both Bird and an xAI key are available, Bird is preferred.
|
**X search is fully self-contained** - No external `bird` CLI or xAI API key needed. /last30days bundles a vendored subset of Bird's Twitter GraphQL client (MIT licensed, by Peter Steinberger). Just be logged into x.com in your browser and it auto-detects your session. Falls back to xAI API if bundled search can't authenticate.
|
||||||
|
|
||||||
### Everything else
|
### Everything else
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: last30days
|
name: last30days
|
||||||
|
version: "2.1"
|
||||||
description: Research a topic from the last 30 days on Reddit + X + Web, become an expert, and write copy-paste-ready prompts for the user's target tool.
|
description: Research a topic from the last 30 days on Reddit + X + Web, become an expert, and write copy-paste-ready prompts for the user's target tool.
|
||||||
argument-hint: 'nano banana pro prompts, NVIDIA news, best AI video tools'
|
argument-hint: 'nano banana pro prompts, NVIDIA news, best AI video tools'
|
||||||
allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch
|
allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch
|
||||||
---
|
---
|
||||||
|
|
||||||
# last30days: Research Any Topic from the Last 30 Days
|
# last30days v2.1: Research Any Topic from the Last 30 Days
|
||||||
|
|
||||||
Research ANY topic across Reddit, X, and the web. Surface what people are actually discussing, recommending, and debating right now.
|
Research ANY topic across Reddit, X, and the web. Surface what people are actually discussing, recommending, and debating right now.
|
||||||
|
|
||||||
@@ -251,7 +252,7 @@ KEY PATTERNS from the research:
|
|||||||
---
|
---
|
||||||
✅ All agents reported back!
|
✅ All agents reported back!
|
||||||
├─ 🟠 Reddit: {N} threads │ {N} upvotes │ {N} comments
|
├─ 🟠 Reddit: {N} threads │ {N} upvotes │ {N} comments
|
||||||
├─ 🔵 X: {N} posts │ {N} likes │ {N} reposts (via Bird/xAI)
|
├─ 🔵 X: {N} posts │ {N} likes │ {N} reposts
|
||||||
├─ 🌐 Web: {N} pages (supplementary)
|
├─ 🌐 Web: {N} pages (supplementary)
|
||||||
└─ 🗣️ Top voices: @{handle1} ({N} likes), @{handle2} │ r/{sub1}, r/{sub2}
|
└─ 🗣️ Top voices: @{handle1} ({N} likes), @{handle2} │ r/{sub1}, r/{sub2}
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
title: "feat: Bundle Bird X search client to eliminate npm dependency"
|
||||||
|
type: feat
|
||||||
|
date: 2026-02-07
|
||||||
|
---
|
||||||
|
|
||||||
|
# feat: Bundle Bird X search client to eliminate npm dependency
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Replace the `subprocess.run(["bird", "search", ...])` dependency in `bird_x.py` with a vendored Node.js module that calls Twitter's GraphQL search API directly. This eliminates the need for users to `npm install -g @steipete/bird` and protects against the package being removed from npm.
|
||||||
|
|
||||||
|
Bird is MIT-licensed. We have the full compiled package archived at `vendor/steipete-bird-0.8.0.tgz` and forked to `github.com/mvanhorn/bird-cli-archive`.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
@steipete deleted Bird's GitHub repo on 2026-02-07. The npm package still works today, but if he unpublishes it from npm:
|
||||||
|
- New users can't `npm install -g @steipete/bird`
|
||||||
|
- The `bird` binary disappears from PATH on fresh installs
|
||||||
|
- `bird_x.py` returns 0 X results for everyone without an xAI API key
|
||||||
|
- /last30days V2's headline feature ("free X search") stops working for new users
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
**Vendor Bird's search-only subset as a Node.js module inside /last30days, called from Python via `subprocess.run(["node", ...])`.**
|
||||||
|
|
||||||
|
This is the minimal-change approach:
|
||||||
|
- Keep Python as the orchestrator (bird_x.py stays mostly the same)
|
||||||
|
- Replace `subprocess.run(["bird", "search", ...])` with `subprocess.run(["node", "vendor/bird-search.mjs", ...])`
|
||||||
|
- Extract only the search-related code from Bird (not posting, bookmarks, lists, etc.)
|
||||||
|
- Cookie auth stays the same (environment variables or browser extraction)
|
||||||
|
|
||||||
|
### Why not rewrite in pure Python?
|
||||||
|
|
||||||
|
Bird's search client uses Twitter's internal GraphQL API with:
|
||||||
|
- Rotating QueryIDs (hardcoded + runtime refresh from x.com)
|
||||||
|
- Specific request header construction (bearer token, csrf, client UUIDs)
|
||||||
|
- Cursor-based pagination with Twitter-specific response parsing
|
||||||
|
- The `@steipete/sweet-cookie` dependency for browser cookie extraction
|
||||||
|
|
||||||
|
Porting all of this to Python is ~1000 lines of fragile reverse-engineering. Vendoring the working JS code is faster, safer, and easier to maintain since the archive includes source maps for debugging.
|
||||||
|
|
||||||
|
## Technical Approach
|
||||||
|
|
||||||
|
### What we need from Bird
|
||||||
|
|
||||||
|
Only 8 files from `dist/lib/` (out of 30+):
|
||||||
|
|
||||||
|
1. `twitter-client-base.js` - HTTP client, auth headers, rate limiting
|
||||||
|
2. `twitter-client-search.js` - Search mixin (the core feature)
|
||||||
|
3. `twitter-client-utils.js` - Tweet parsing, cursor extraction
|
||||||
|
4. `twitter-client-constants.js` - API endpoints, QueryIDs
|
||||||
|
5. `twitter-client-types.js` - TypeScript type stubs
|
||||||
|
6. `cookies.js` - Cookie resolution (env vars, browser extraction)
|
||||||
|
7. `runtime-query-ids.js` - QueryID refresh from x.com
|
||||||
|
8. `paginate-cursor.js` - Cursor pagination helper
|
||||||
|
|
||||||
|
Plus:
|
||||||
|
- `features.json` - GraphQL feature flags
|
||||||
|
- `query-ids.json` - Hardcoded QueryID fallbacks
|
||||||
|
|
||||||
|
### What we DON'T need
|
||||||
|
|
||||||
|
Posting, bookmarks, lists, timelines, engagement, follow, media, news, user lookup, user tweets - all the non-search mixins. This cuts the vendored code roughly in half.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/
|
||||||
|
lib/
|
||||||
|
bird_x.py # MODIFIED - calls node instead of bird binary
|
||||||
|
vendor/
|
||||||
|
bird-search/
|
||||||
|
bird-search.mjs # NEW - thin CLI wrapper, ~40 lines
|
||||||
|
lib/ # VENDORED - subset of Bird's dist/lib/
|
||||||
|
twitter-client-base.js
|
||||||
|
twitter-client-search.js
|
||||||
|
twitter-client-utils.js
|
||||||
|
twitter-client-constants.js
|
||||||
|
twitter-client-types.js
|
||||||
|
cookies.js
|
||||||
|
runtime-query-ids.js
|
||||||
|
paginate-cursor.js
|
||||||
|
features.json
|
||||||
|
query-ids.json
|
||||||
|
node_modules/ # VENDORED - sweet-cookie only
|
||||||
|
@steipete/
|
||||||
|
sweet-cookie/
|
||||||
|
package.json # Minimal, points to bird-search.mjs
|
||||||
|
LICENSE # Bird's MIT license (required by MIT terms)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
#### 1. Create `bird-search.mjs` wrapper (~40 lines)
|
||||||
|
|
||||||
|
A minimal Node.js script that:
|
||||||
|
- Accepts: `node bird-search.mjs <query> --count <n> --json`
|
||||||
|
- Creates a TwitterClient with search mixin only
|
||||||
|
- Resolves cookies (env vars first, then browser extraction)
|
||||||
|
- Calls `client.search(query, count)`
|
||||||
|
- Outputs JSON to stdout
|
||||||
|
- Exits with code 0 on success, 1 on error
|
||||||
|
|
||||||
|
This replaces the full `bird` CLI binary. Same interface, fraction of the code.
|
||||||
|
|
||||||
|
#### 2. Modify `bird_x.py` - change subprocess target
|
||||||
|
|
||||||
|
```python
|
||||||
|
# BEFORE (current)
|
||||||
|
cmd = ["bird", "search", query, "-n", str(count), "--json"]
|
||||||
|
|
||||||
|
# AFTER (vendored)
|
||||||
|
bird_search = Path(__file__).parent / "vendor" / "bird-search" / "bird-search.mjs"
|
||||||
|
cmd = ["node", bird_search, query, "--count", str(count), "--json"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Same subprocess pattern. Same JSON output format. Minimal diff.
|
||||||
|
|
||||||
|
#### 3. Update auth check functions
|
||||||
|
|
||||||
|
```python
|
||||||
|
# BEFORE
|
||||||
|
def is_bird_installed() -> bool:
|
||||||
|
return shutil.which("bird") is not None
|
||||||
|
|
||||||
|
# AFTER
|
||||||
|
def is_bird_installed() -> bool:
|
||||||
|
bird_search = Path(__file__).parent / "vendor" / "bird-search" / "bird-search.mjs"
|
||||||
|
return bird_search.exists() and shutil.which("node") is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
`is_bird_authenticated()` changes from `bird whoami` to a quick Node.js cookie check or environment variable check.
|
||||||
|
|
||||||
|
`install_bird()` becomes a no-op (already vendored) or removes itself entirely.
|
||||||
|
|
||||||
|
#### 4. Vendor sweet-cookie
|
||||||
|
|
||||||
|
`@steipete/sweet-cookie` is the only runtime dependency. It handles browser cookie extraction on macOS/Linux. Options:
|
||||||
|
|
||||||
|
**Option A (recommended):** Vendor sweet-cookie into `vendor/bird-search/node_modules/`. It's small (one file). This makes the skill fully self-contained with zero npm installs.
|
||||||
|
|
||||||
|
**Option B:** Fall back to environment variables only (no browser cookie extraction). Users would need to manually set `AUTH_TOKEN` and `CT0` env vars. Simpler but worse UX.
|
||||||
|
|
||||||
|
Recommend Option A - vendor it.
|
||||||
|
|
||||||
|
#### 5. Update user-facing docs
|
||||||
|
|
||||||
|
- `README.md` - Remove "Install Bird CLI" section, replace with "Requires Node.js 22+"
|
||||||
|
- `SKILL.md` - Remove Bird CLI installation instructions
|
||||||
|
- Keep the fallback chain: vendored Bird search -> xAI API key -> web-only
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `bird_x.py` calls vendored Node.js module instead of `bird` binary
|
||||||
|
- [ ] `search_x()` returns identical JSON format (no downstream changes needed)
|
||||||
|
- [ ] `search_handles()` works with vendored module
|
||||||
|
- [ ] Cookie auth works via environment variables (`AUTH_TOKEN`, `CT0`)
|
||||||
|
- [ ] Cookie auth works via browser extraction (sweet-cookie)
|
||||||
|
- [ ] `is_bird_installed()` checks for vendored module + Node.js
|
||||||
|
- [ ] `install_bird()` removed or returns success immediately
|
||||||
|
- [ ] No `npm install -g @steipete/bird` required anywhere
|
||||||
|
- [ ] Bird's MIT LICENSE included in vendor directory
|
||||||
|
- [ ] README updated to remove Bird CLI install steps
|
||||||
|
- [ ] SKILL.md updated to remove Bird CLI references
|
||||||
|
- [ ] Works on macOS (primary) and Linux
|
||||||
|
- [ ] Fallback to xAI API key still works if vendored search fails
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Action | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `scripts/lib/bird_x.py` | MODIFY | Replace `["bird", ...]` subprocess calls with `["node", "vendor/bird-search/bird-search.mjs", ...]` |
|
||||||
|
| `scripts/lib/vendor/bird-search/bird-search.mjs` | CREATE | Thin Node.js wrapper that imports Bird's search client and outputs JSON |
|
||||||
|
| `scripts/lib/vendor/bird-search/lib/*.js` | VENDOR | 8 files from Bird's dist/lib/ (search subset only) |
|
||||||
|
| `scripts/lib/vendor/bird-search/lib/features.json` | VENDOR | GraphQL feature flags |
|
||||||
|
| `scripts/lib/vendor/bird-search/lib/query-ids.json` | VENDOR | Hardcoded QueryID fallbacks |
|
||||||
|
| `scripts/lib/vendor/bird-search/node_modules/` | VENDOR | sweet-cookie package |
|
||||||
|
| `scripts/lib/vendor/bird-search/package.json` | CREATE | Minimal package.json for module resolution |
|
||||||
|
| `scripts/lib/vendor/bird-search/LICENSE` | COPY | Bird's MIT license |
|
||||||
|
| `README.md` | MODIFY | Remove Bird CLI install section, add Node.js 22+ requirement |
|
||||||
|
| `SKILL.md` | MODIFY | Remove Bird CLI references |
|
||||||
|
|
||||||
|
## Dependencies & Risks
|
||||||
|
|
||||||
|
**Node.js 22+ required** - Users who had Bird CLI already have Node.js. This is not a new dependency, just a version requirement. Claude Code environments typically have Node.js.
|
||||||
|
|
||||||
|
**Twitter API changes** - The GraphQL QueryIDs may rotate. Bird includes a runtime refresh mechanism (`runtime-query-ids.js`) that fetches new IDs from x.com. This is vendored and will continue working.
|
||||||
|
|
||||||
|
**sweet-cookie platform support** - Browser cookie extraction only works on macOS (Safari, Chrome, Firefox) and Linux (Chrome, Firefox). Windows users need manual env vars. This matches Bird CLI's existing behavior.
|
||||||
|
|
||||||
|
**Legal** - Bird is MIT licensed. MIT requires including the license notice in copies. We include `LICENSE` in the vendor directory. Using Twitter's internal API is the same legal gray area Bird always operated in - user accepted this when they used Bird.
|
||||||
|
|
||||||
|
## What This Does NOT Change
|
||||||
|
|
||||||
|
- Python remains the orchestrator - bird_x.py still does query construction, retry logic, response parsing
|
||||||
|
- The fallback chain stays: vendored search -> xAI API -> web-only
|
||||||
|
- Cookie auth mechanism is identical (env vars or browser extraction)
|
||||||
|
- JSON output format is identical - no changes needed in score.py or format.py
|
||||||
|
- xai_x.py is completely untouched
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Bird CLI archive: `github.com/mvanhorn/bird-cli-archive`
|
||||||
|
- Local vendor tarball: `vendor/steipete-bird-0.8.0.tgz`
|
||||||
|
- Bird search implementation: `bird-cli-archive/dist/lib/twitter-client-search.js`
|
||||||
|
- Current bird_x.py: `scripts/lib/bird_x.py`
|
||||||
|
- Fallback chain: `scripts/lib/env.py:get_x_source()`
|
||||||
+35
-41
@@ -1,12 +1,20 @@
|
|||||||
"""Bird CLI client for X (Twitter) search."""
|
"""Bird X search client - vendored Twitter GraphQL search for /last30days v2.1.
|
||||||
|
|
||||||
|
Uses a vendored subset of @steipete/bird v0.8.0 (MIT License) to search X
|
||||||
|
via Twitter's GraphQL API. No external `bird` CLI binary needed - just Node.js 22+.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# Path to the vendored bird-search wrapper
|
||||||
|
_BIRD_SEARCH_MJS = Path(__file__).parent / "vendor" / "bird-search" / "bird-search.mjs"
|
||||||
|
|
||||||
# Depth configurations: number of results to request
|
# Depth configurations: number of results to request
|
||||||
DEPTH_CONFIG = {
|
DEPTH_CONFIG = {
|
||||||
"quick": 12,
|
"quick": 12,
|
||||||
@@ -77,32 +85,33 @@ def _extract_core_subject(topic: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def is_bird_installed() -> bool:
|
def is_bird_installed() -> bool:
|
||||||
"""Check if Bird CLI is installed.
|
"""Check if vendored Bird search module is available.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if 'bird' command is available in PATH, False otherwise.
|
True if bird-search.mjs exists and Node.js 22+ is in PATH.
|
||||||
"""
|
"""
|
||||||
return shutil.which("bird") is not None
|
if not _BIRD_SEARCH_MJS.exists():
|
||||||
|
return False
|
||||||
|
return shutil.which("node") is not None
|
||||||
|
|
||||||
|
|
||||||
def is_bird_authenticated() -> Optional[str]:
|
def is_bird_authenticated() -> Optional[str]:
|
||||||
"""Check if Bird is authenticated by running 'bird whoami'.
|
"""Check if X credentials are available (env vars or browser cookies).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Username if authenticated, None otherwise.
|
Auth source string if authenticated, None otherwise.
|
||||||
"""
|
"""
|
||||||
if not is_bird_installed():
|
if not is_bird_installed():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bird", "whoami"],
|
["node", str(_BIRD_SEARCH_MJS), "--whoami"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=10,
|
timeout=15,
|
||||||
)
|
)
|
||||||
if result.returncode == 0 and result.stdout.strip():
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
# Output is typically the username
|
|
||||||
return result.stdout.strip().split('\n')[0]
|
return result.stdout.strip().split('\n')[0]
|
||||||
return None
|
return None
|
||||||
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
||||||
@@ -110,7 +119,7 @@ def is_bird_authenticated() -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def check_npm_available() -> bool:
|
def check_npm_available() -> bool:
|
||||||
"""Check if npm is available for installation.
|
"""Check if npm is available (kept for API compatibility).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if 'npm' command is available in PATH, False otherwise.
|
True if 'npm' command is available in PATH, False otherwise.
|
||||||
@@ -119,52 +128,37 @@ def check_npm_available() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def install_bird() -> Tuple[bool, str]:
|
def install_bird() -> Tuple[bool, str]:
|
||||||
"""Install Bird CLI via npm.
|
"""No-op - Bird search is vendored in v2.1, no installation needed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (success, message).
|
Tuple of (success, message).
|
||||||
"""
|
"""
|
||||||
if not check_npm_available():
|
if is_bird_installed():
|
||||||
return False, "npm not found. Install Node.js first, or install Bird manually: https://github.com/steipete/bird"
|
return True, "Bird search is bundled with /last30days v2.1 - no installation needed."
|
||||||
|
if not shutil.which("node"):
|
||||||
try:
|
return False, "Node.js 22+ is required for X search. Install Node.js first."
|
||||||
_log("Installing Bird CLI...")
|
return False, f"Vendored bird-search.mjs not found at {_BIRD_SEARCH_MJS}"
|
||||||
result = subprocess.run(
|
|
||||||
["npm", "install", "-g", "@steipete/bird"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return True, "Bird CLI installed successfully!"
|
|
||||||
else:
|
|
||||||
error = result.stderr.strip() or result.stdout.strip() or "Unknown error"
|
|
||||||
return False, f"Installation failed: {error}"
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False, "Installation timed out"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Installation error: {e}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_bird_status() -> Dict[str, Any]:
|
def get_bird_status() -> Dict[str, Any]:
|
||||||
"""Get comprehensive Bird status.
|
"""Get comprehensive Bird search status.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with keys: installed, authenticated, username, can_install
|
Dict with keys: installed, authenticated, username, can_install
|
||||||
"""
|
"""
|
||||||
installed = is_bird_installed()
|
installed = is_bird_installed()
|
||||||
username = is_bird_authenticated() if installed else None
|
auth_source = is_bird_authenticated() if installed else None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
"authenticated": username is not None,
|
"authenticated": auth_source is not None,
|
||||||
"username": username,
|
"username": auth_source, # Now returns auth source (e.g., "Safari", "env AUTH_TOKEN")
|
||||||
"can_install": check_npm_available(),
|
"can_install": True, # Always vendored in v2.1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _run_bird_search(query: str, count: int, timeout: int) -> Dict[str, Any]:
|
def _run_bird_search(query: str, count: int, timeout: int) -> Dict[str, Any]:
|
||||||
"""Run a single Bird CLI search and return raw response.
|
"""Run a search using the vendored bird-search.mjs module.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Full search query string (including since: filter)
|
query: Full search query string (including since: filter)
|
||||||
@@ -175,9 +169,9 @@ def _run_bird_search(query: str, count: int, timeout: int) -> Dict[str, Any]:
|
|||||||
Raw Bird JSON response or error dict.
|
Raw Bird JSON response or error dict.
|
||||||
"""
|
"""
|
||||||
cmd = [
|
cmd = [
|
||||||
"bird", "search",
|
"node", str(_BIRD_SEARCH_MJS),
|
||||||
query,
|
query,
|
||||||
"-n", str(count),
|
"--count", str(count),
|
||||||
"--json",
|
"--json",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -276,9 +270,9 @@ def search_handles(
|
|||||||
query = f"from:{handle} {core_topic} since:{from_date}"
|
query = f"from:{handle} {core_topic} since:{from_date}"
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"bird", "search",
|
"node", str(_BIRD_SEARCH_MJS),
|
||||||
query,
|
query,
|
||||||
"-n", str(count_per),
|
"--count", str(count_per),
|
||||||
"--json",
|
"--json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Peter Steinberger
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* bird-search.mjs - Vendored Bird CLI search wrapper for /last30days.
|
||||||
|
* Subset of @steipete/bird v0.8.0 (MIT License, Peter Steinberger).
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node bird-search.mjs <query> [--count N] [--json]
|
||||||
|
* node bird-search.mjs --whoami
|
||||||
|
* node bird-search.mjs --check
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { resolveCredentials } from './lib/cookies.js';
|
||||||
|
import { TwitterClientBase } from './lib/twitter-client-base.js';
|
||||||
|
import { withSearch } from './lib/twitter-client-search.js';
|
||||||
|
|
||||||
|
// Build a search-only client (no posting, bookmarks, etc.)
|
||||||
|
const SearchClient = withSearch(TwitterClientBase);
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
// --check: verify that credentials can be resolved
|
||||||
|
if (args.includes('--check')) {
|
||||||
|
try {
|
||||||
|
const { cookies, warnings } = await resolveCredentials({});
|
||||||
|
if (cookies.authToken && cookies.ct0) {
|
||||||
|
process.stdout.write(JSON.stringify({ authenticated: true, source: cookies.source }));
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(JSON.stringify({ authenticated: false, warnings }));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
process.stdout.write(JSON.stringify({ authenticated: false, error: err.message }));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --whoami: check auth and output source
|
||||||
|
if (args.includes('--whoami')) {
|
||||||
|
try {
|
||||||
|
const { cookies } = await resolveCredentials({});
|
||||||
|
if (cookies.authToken && cookies.ct0) {
|
||||||
|
process.stdout.write(cookies.source || 'authenticated');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
process.stderr.write('Not authenticated\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(`Auth check failed: ${err.message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse search args
|
||||||
|
let query = null;
|
||||||
|
let count = 20;
|
||||||
|
let jsonOutput = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--count' && args[i + 1]) {
|
||||||
|
count = parseInt(args[i + 1], 10);
|
||||||
|
i++;
|
||||||
|
} else if (args[i] === '-n' && args[i + 1]) {
|
||||||
|
count = parseInt(args[i + 1], 10);
|
||||||
|
i++;
|
||||||
|
} else if (args[i] === '--json') {
|
||||||
|
jsonOutput = true;
|
||||||
|
} else if (!args[i].startsWith('-')) {
|
||||||
|
query = args[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
process.stderr.write('Usage: node bird-search.mjs <query> [--count N] [--json]\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve credentials (env vars, then browser cookies)
|
||||||
|
const { cookies, warnings } = await resolveCredentials({});
|
||||||
|
|
||||||
|
if (!cookies.authToken || !cookies.ct0) {
|
||||||
|
const msg = warnings.length > 0 ? warnings.join('; ') : 'No Twitter credentials found';
|
||||||
|
if (jsonOutput) {
|
||||||
|
process.stdout.write(JSON.stringify({ error: msg, items: [] }));
|
||||||
|
} else {
|
||||||
|
process.stderr.write(`Error: ${msg}\n`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create search client
|
||||||
|
const client = new SearchClient({
|
||||||
|
cookies: {
|
||||||
|
authToken: cookies.authToken,
|
||||||
|
ct0: cookies.ct0,
|
||||||
|
cookieHeader: cookies.cookieHeader,
|
||||||
|
},
|
||||||
|
timeoutMs: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run search
|
||||||
|
const result = await client.search(query, count);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
if (jsonOutput) {
|
||||||
|
process.stdout.write(JSON.stringify({ error: result.error, items: [] }));
|
||||||
|
} else {
|
||||||
|
process.stderr.write(`Search failed: ${result.error}\n`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output results
|
||||||
|
const tweets = result.tweets || [];
|
||||||
|
if (jsonOutput) {
|
||||||
|
process.stdout.write(JSON.stringify(tweets));
|
||||||
|
} else {
|
||||||
|
for (const tweet of tweets) {
|
||||||
|
const author = tweet.author?.username || 'unknown';
|
||||||
|
process.stdout.write(`@${author}: ${tweet.text?.slice(0, 200)}\n\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
if (jsonOutput) {
|
||||||
|
process.stdout.write(JSON.stringify({ error: err.message, items: [] }));
|
||||||
|
} else {
|
||||||
|
process.stderr.write(`Error: ${err.message}\n`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Browser cookie extraction for Twitter authentication.
|
||||||
|
* Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads.
|
||||||
|
*/
|
||||||
|
import { getCookies } from '@steipete/sweet-cookie';
|
||||||
|
const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0'];
|
||||||
|
const TWITTER_URL = 'https://x.com/';
|
||||||
|
const TWITTER_ORIGINS = ['https://x.com/', 'https://twitter.com/'];
|
||||||
|
const DEFAULT_COOKIE_TIMEOUT_MS = 30_000;
|
||||||
|
function normalizeValue(value) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
}
|
||||||
|
function cookieHeader(authToken, ct0) {
|
||||||
|
return `auth_token=${authToken}; ct0=${ct0}`;
|
||||||
|
}
|
||||||
|
function buildEmpty() {
|
||||||
|
return { authToken: null, ct0: null, cookieHeader: null, source: null };
|
||||||
|
}
|
||||||
|
function readEnvCookie(cookies, keys, field) {
|
||||||
|
if (cookies[field]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = normalizeValue(process.env[key]);
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cookies[field] = value;
|
||||||
|
if (!cookies.source) {
|
||||||
|
cookies.source = `env ${key}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function resolveSources(cookieSource) {
|
||||||
|
if (Array.isArray(cookieSource)) {
|
||||||
|
return cookieSource;
|
||||||
|
}
|
||||||
|
if (cookieSource) {
|
||||||
|
return [cookieSource];
|
||||||
|
}
|
||||||
|
return ['safari', 'chrome', 'firefox'];
|
||||||
|
}
|
||||||
|
function labelForSource(source, profile) {
|
||||||
|
if (source === 'safari') {
|
||||||
|
return 'Safari';
|
||||||
|
}
|
||||||
|
if (source === 'chrome') {
|
||||||
|
return profile ? `Chrome profile "${profile}"` : 'Chrome default profile';
|
||||||
|
}
|
||||||
|
return profile ? `Firefox profile "${profile}"` : 'Firefox default profile';
|
||||||
|
}
|
||||||
|
function pickCookieValue(cookies, name) {
|
||||||
|
const matches = cookies.filter((c) => c?.name === name && typeof c.value === 'string');
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const preferred = matches.find((c) => (c.domain ?? '').endsWith('x.com'));
|
||||||
|
if (preferred?.value) {
|
||||||
|
return preferred.value;
|
||||||
|
}
|
||||||
|
const twitter = matches.find((c) => (c.domain ?? '').endsWith('twitter.com'));
|
||||||
|
if (twitter?.value) {
|
||||||
|
return twitter.value;
|
||||||
|
}
|
||||||
|
return matches[0]?.value ?? null;
|
||||||
|
}
|
||||||
|
async function readTwitterCookiesFromBrowser(options) {
|
||||||
|
const warnings = [];
|
||||||
|
const out = buildEmpty();
|
||||||
|
const { cookies, warnings: providerWarnings } = await getCookies({
|
||||||
|
url: TWITTER_URL,
|
||||||
|
origins: TWITTER_ORIGINS,
|
||||||
|
names: [...TWITTER_COOKIE_NAMES],
|
||||||
|
browsers: [options.source],
|
||||||
|
mode: 'merge',
|
||||||
|
chromeProfile: options.chromeProfile,
|
||||||
|
firefoxProfile: options.firefoxProfile,
|
||||||
|
timeoutMs: options.cookieTimeoutMs,
|
||||||
|
});
|
||||||
|
warnings.push(...providerWarnings);
|
||||||
|
const authToken = pickCookieValue(cookies, 'auth_token');
|
||||||
|
const ct0 = pickCookieValue(cookies, 'ct0');
|
||||||
|
if (authToken) {
|
||||||
|
out.authToken = authToken;
|
||||||
|
}
|
||||||
|
if (ct0) {
|
||||||
|
out.ct0 = ct0;
|
||||||
|
}
|
||||||
|
if (out.authToken && out.ct0) {
|
||||||
|
out.cookieHeader = cookieHeader(out.authToken, out.ct0);
|
||||||
|
out.source = labelForSource(options.source, options.source === 'chrome' ? options.chromeProfile : options.firefoxProfile);
|
||||||
|
return { cookies: out, warnings };
|
||||||
|
}
|
||||||
|
if (options.source === 'safari') {
|
||||||
|
warnings.push('No Twitter cookies found in Safari. Make sure you are logged into x.com in Safari.');
|
||||||
|
}
|
||||||
|
else if (options.source === 'chrome') {
|
||||||
|
warnings.push('No Twitter cookies found in Chrome. Make sure you are logged into x.com in Chrome.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warnings.push('No Twitter cookies found in Firefox. Make sure you are logged into x.com in Firefox and the profile exists.');
|
||||||
|
}
|
||||||
|
return { cookies: out, warnings };
|
||||||
|
}
|
||||||
|
export async function extractCookiesFromSafari() {
|
||||||
|
return readTwitterCookiesFromBrowser({ source: 'safari' });
|
||||||
|
}
|
||||||
|
export async function extractCookiesFromChrome(profile) {
|
||||||
|
return readTwitterCookiesFromBrowser({ source: 'chrome', chromeProfile: profile });
|
||||||
|
}
|
||||||
|
export async function extractCookiesFromFirefox(profile) {
|
||||||
|
return readTwitterCookiesFromBrowser({ source: 'firefox', firefoxProfile: profile });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resolve Twitter credentials from multiple sources.
|
||||||
|
* Priority: CLI args > environment variables > browsers (ordered).
|
||||||
|
*/
|
||||||
|
export async function resolveCredentials(options) {
|
||||||
|
const warnings = [];
|
||||||
|
const cookies = buildEmpty();
|
||||||
|
const cookieTimeoutMs = typeof options.cookieTimeoutMs === 'number' &&
|
||||||
|
Number.isFinite(options.cookieTimeoutMs) &&
|
||||||
|
options.cookieTimeoutMs > 0
|
||||||
|
? options.cookieTimeoutMs
|
||||||
|
: process.platform === 'darwin'
|
||||||
|
? DEFAULT_COOKIE_TIMEOUT_MS
|
||||||
|
: undefined;
|
||||||
|
if (options.authToken) {
|
||||||
|
cookies.authToken = options.authToken;
|
||||||
|
cookies.source = 'CLI argument';
|
||||||
|
}
|
||||||
|
if (options.ct0) {
|
||||||
|
cookies.ct0 = options.ct0;
|
||||||
|
if (!cookies.source) {
|
||||||
|
cookies.source = 'CLI argument';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readEnvCookie(cookies, ['AUTH_TOKEN', 'TWITTER_AUTH_TOKEN'], 'authToken');
|
||||||
|
readEnvCookie(cookies, ['CT0', 'TWITTER_CT0'], 'ct0');
|
||||||
|
if (cookies.authToken && cookies.ct0) {
|
||||||
|
cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0);
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
const sourcesToTry = resolveSources(options.cookieSource);
|
||||||
|
for (const source of sourcesToTry) {
|
||||||
|
const res = await readTwitterCookiesFromBrowser({
|
||||||
|
source,
|
||||||
|
chromeProfile: options.chromeProfile,
|
||||||
|
firefoxProfile: options.firefoxProfile,
|
||||||
|
cookieTimeoutMs,
|
||||||
|
});
|
||||||
|
warnings.push(...res.warnings);
|
||||||
|
if (res.cookies.authToken && res.cookies.ct0) {
|
||||||
|
return { cookies: res.cookies, warnings };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cookies.authToken) {
|
||||||
|
warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox');
|
||||||
|
}
|
||||||
|
if (!cookies.ct0) {
|
||||||
|
warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Safari/Chrome/Firefox');
|
||||||
|
}
|
||||||
|
if (cookies.authToken && cookies.ct0) {
|
||||||
|
cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0);
|
||||||
|
}
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=cookies.js.map
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"responsive_web_grok_annotations_enabled": false,
|
||||||
|
"post_ctas_fetch_enabled": true,
|
||||||
|
"responsive_web_graphql_exclude_directive_enabled": true
|
||||||
|
},
|
||||||
|
"sets": {
|
||||||
|
"lists": {
|
||||||
|
"blue_business_profile_image_shape_enabled": true,
|
||||||
|
"tweetypie_unmention_optimization_enabled": true,
|
||||||
|
"responsive_web_text_conversations_enabled": false,
|
||||||
|
"interactive_text_enabled": true,
|
||||||
|
"vibe_api_enabled": true,
|
||||||
|
"responsive_web_twitter_blue_verified_badge_is_enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export async function paginateCursor(opts) {
|
||||||
|
const { maxPages, pageDelayMs = 1000 } = opts;
|
||||||
|
const seen = new Set();
|
||||||
|
const items = [];
|
||||||
|
let cursor = opts.cursor;
|
||||||
|
let pagesFetched = 0;
|
||||||
|
while (true) {
|
||||||
|
if (pagesFetched > 0 && pageDelayMs > 0) {
|
||||||
|
await opts.sleep(pageDelayMs);
|
||||||
|
}
|
||||||
|
const page = await opts.fetchPage(cursor);
|
||||||
|
if (!page.success) {
|
||||||
|
if (items.length > 0) {
|
||||||
|
return { success: false, error: page.error, items, nextCursor: cursor };
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
pagesFetched += 1;
|
||||||
|
for (const item of page.items) {
|
||||||
|
const key = opts.getKey(item);
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
const pageCursor = page.cursor;
|
||||||
|
if (!pageCursor || pageCursor === cursor) {
|
||||||
|
return { success: true, items, nextCursor: undefined };
|
||||||
|
}
|
||||||
|
if (maxPages !== undefined && pagesFetched >= maxPages) {
|
||||||
|
return { success: true, items, nextCursor: pageCursor };
|
||||||
|
}
|
||||||
|
cursor = pageCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=paginate-cursor.js.map
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"CreateTweet": "nmdAQXJDxw6-0KKF2on7eA",
|
||||||
|
"CreateRetweet": "LFho5rIi4xcKO90p9jwG7A",
|
||||||
|
"CreateFriendship": "8h9JVdV8dlSyqyRDJEPCsA",
|
||||||
|
"DestroyFriendship": "ppXWuagMNXgvzx6WoXBW0Q",
|
||||||
|
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
|
||||||
|
"DeleteBookmark": "Wlmlj2-xzyS1GN3a6cj-mQ",
|
||||||
|
"TweetDetail": "_NvJCnIjOW__EP5-RF197A",
|
||||||
|
"SearchTimeline": "6AAys3t42mosm_yTI_QENg",
|
||||||
|
"Bookmarks": "RV1g3b8n_SGOHwkqKYSCFw",
|
||||||
|
"BookmarkFolderTimeline": "KJIQpsvxrTfRIlbaRIySHQ",
|
||||||
|
"Following": "mWYeougg_ocJS2Vr1Vt28w",
|
||||||
|
"Followers": "SFYY3WsgwjlXSLlfnEUE4A",
|
||||||
|
"Likes": "ETJflBunfqNa1uE1mBPCaw",
|
||||||
|
"ExploreSidebar": "lpSN4M6qpimkF4nRFPE3nQ",
|
||||||
|
"ExplorePage": "kheAINB_4pzRDqkzG3K-ng",
|
||||||
|
"GenericTimelineById": "uGSr7alSjR9v6QJAIaqSKQ",
|
||||||
|
"TrendHistory": "Sj4T-jSB9pr0Mxtsc1UKZQ",
|
||||||
|
"AboutAccountQuery": "zs_jFPFT78rBpXv9Z3U2YQ"
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension.
|
||||||
|
import defaultOverrides from './features.json' with { type: 'json' };
|
||||||
|
const DEFAULT_CACHE_FILENAME = 'features.json';
|
||||||
|
let cachedOverrides = null;
|
||||||
|
function normalizeFeatureMap(value) {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const result = {};
|
||||||
|
for (const [key, entry] of Object.entries(value)) {
|
||||||
|
if (typeof entry === 'boolean') {
|
||||||
|
result[key] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function normalizeOverrides(value) {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return { global: {}, sets: {} };
|
||||||
|
}
|
||||||
|
const record = value;
|
||||||
|
const global = normalizeFeatureMap(record.global);
|
||||||
|
const sets = {};
|
||||||
|
const rawSets = record.sets && typeof record.sets === 'object' && !Array.isArray(record.sets)
|
||||||
|
? record.sets
|
||||||
|
: {};
|
||||||
|
for (const [setName, setValue] of Object.entries(rawSets)) {
|
||||||
|
const normalized = normalizeFeatureMap(setValue);
|
||||||
|
if (Object.keys(normalized).length > 0) {
|
||||||
|
sets[setName] = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { global, sets };
|
||||||
|
}
|
||||||
|
function mergeOverrides(base, next) {
|
||||||
|
const sets = { ...base.sets };
|
||||||
|
for (const [setName, overrides] of Object.entries(next.sets)) {
|
||||||
|
const existing = sets[setName];
|
||||||
|
sets[setName] = existing ? { ...existing, ...overrides } : { ...overrides };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
global: { ...base.global, ...next.global },
|
||||||
|
sets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function toFeatureOverrides(overrides) {
|
||||||
|
const result = {};
|
||||||
|
if (Object.keys(overrides.global).length > 0) {
|
||||||
|
result.global = overrides.global;
|
||||||
|
}
|
||||||
|
const setEntries = Object.entries(overrides.sets).filter(([, value]) => Object.keys(value).length > 0);
|
||||||
|
if (setEntries.length > 0) {
|
||||||
|
result.sets = Object.fromEntries(setEntries);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function resolveFeaturesCachePath() {
|
||||||
|
const override = process.env.BIRD_FEATURES_CACHE ?? process.env.BIRD_FEATURES_PATH;
|
||||||
|
if (override && override.trim().length > 0) {
|
||||||
|
return path.resolve(override.trim());
|
||||||
|
}
|
||||||
|
return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME);
|
||||||
|
}
|
||||||
|
function readOverridesFromFile(cachePath) {
|
||||||
|
if (!existsSync(cachePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = readFileSync(cachePath, 'utf8');
|
||||||
|
return normalizeOverrides(JSON.parse(raw));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function readOverridesFromEnv() {
|
||||||
|
const raw = process.env.BIRD_FEATURES_JSON;
|
||||||
|
if (!raw || raw.trim().length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return normalizeOverrides(JSON.parse(raw));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function writeOverridesToDisk(cachePath, overrides) {
|
||||||
|
const payload = toFeatureOverrides(overrides);
|
||||||
|
return mkdir(path.dirname(cachePath), { recursive: true }).then(() => writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'));
|
||||||
|
}
|
||||||
|
export function loadFeatureOverrides() {
|
||||||
|
if (cachedOverrides) {
|
||||||
|
return cachedOverrides;
|
||||||
|
}
|
||||||
|
const base = normalizeOverrides(defaultOverrides);
|
||||||
|
const fromFile = readOverridesFromFile(resolveFeaturesCachePath());
|
||||||
|
const fromEnv = readOverridesFromEnv();
|
||||||
|
let merged = base;
|
||||||
|
if (fromFile) {
|
||||||
|
merged = mergeOverrides(merged, fromFile);
|
||||||
|
}
|
||||||
|
if (fromEnv) {
|
||||||
|
merged = mergeOverrides(merged, fromEnv);
|
||||||
|
}
|
||||||
|
cachedOverrides = merged;
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
export function getFeatureOverridesSnapshot() {
|
||||||
|
const overrides = toFeatureOverrides(loadFeatureOverrides());
|
||||||
|
return {
|
||||||
|
cachePath: resolveFeaturesCachePath(),
|
||||||
|
overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function applyFeatureOverrides(setName, base) {
|
||||||
|
const overrides = loadFeatureOverrides();
|
||||||
|
const globalOverrides = overrides.global;
|
||||||
|
const setOverrides = overrides.sets[setName];
|
||||||
|
if (Object.keys(globalOverrides).length === 0 && (!setOverrides || Object.keys(setOverrides).length === 0)) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
if (setOverrides) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
...globalOverrides,
|
||||||
|
...setOverrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
...globalOverrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export async function refreshFeatureOverridesCache() {
|
||||||
|
const cachePath = resolveFeaturesCachePath();
|
||||||
|
const base = normalizeOverrides(defaultOverrides);
|
||||||
|
const fromFile = readOverridesFromFile(cachePath);
|
||||||
|
const merged = mergeOverrides(base, fromFile ?? { global: {}, sets: {} });
|
||||||
|
await writeOverridesToDisk(cachePath, merged);
|
||||||
|
cachedOverrides = null;
|
||||||
|
return { cachePath, overrides: toFeatureOverrides(merged) };
|
||||||
|
}
|
||||||
|
export function clearFeatureOverridesCache() {
|
||||||
|
cachedOverrides = null;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=runtime-features.js.map
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
const DEFAULT_CACHE_FILENAME = 'query-ids-cache.json';
|
||||||
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const DISCOVERY_PAGES = [
|
||||||
|
'https://x.com/?lang=en',
|
||||||
|
'https://x.com/explore',
|
||||||
|
'https://x.com/notifications',
|
||||||
|
'https://x.com/settings/profile',
|
||||||
|
];
|
||||||
|
const BUNDLE_URL_REGEX = /https:\/\/abs\.twimg\.com\/responsive-web\/client-web(?:-legacy)?\/[A-Za-z0-9.-]+\.js/g;
|
||||||
|
const QUERY_ID_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||||
|
const OPERATION_PATTERNS = [
|
||||||
|
{
|
||||||
|
regex: /e\.exports=\{queryId\s*:\s*["']([^"']+)["']\s*,\s*operationName\s*:\s*["']([^"']+)["']/gs,
|
||||||
|
operationGroup: 2,
|
||||||
|
queryIdGroup: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /e\.exports=\{operationName\s*:\s*["']([^"']+)["']\s*,\s*queryId\s*:\s*["']([^"']+)["']/gs,
|
||||||
|
operationGroup: 1,
|
||||||
|
queryIdGroup: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /operationName\s*[:=]\s*["']([^"']+)["'](.{0,4000}?)queryId\s*[:=]\s*["']([^"']+)["']/gs,
|
||||||
|
operationGroup: 1,
|
||||||
|
queryIdGroup: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /queryId\s*[:=]\s*["']([^"']+)["'](.{0,4000}?)operationName\s*[:=]\s*["']([^"']+)["']/gs,
|
||||||
|
operationGroup: 3,
|
||||||
|
queryIdGroup: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
|
||||||
|
Accept: 'text/html,application/json;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
};
|
||||||
|
async function fetchText(fetchImpl, url) {
|
||||||
|
const response = await fetchImpl(url, { headers: HEADERS });
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text().catch(() => '');
|
||||||
|
throw new Error(`HTTP ${response.status} for ${url}: ${body.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function resolveDefaultCachePath() {
|
||||||
|
const override = process.env.BIRD_QUERY_IDS_CACHE;
|
||||||
|
if (override && override.trim().length > 0) {
|
||||||
|
return path.resolve(override.trim());
|
||||||
|
}
|
||||||
|
return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME);
|
||||||
|
}
|
||||||
|
function parseSnapshot(raw) {
|
||||||
|
if (!raw || typeof raw !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const record = raw;
|
||||||
|
const fetchedAt = typeof record.fetchedAt === 'string' ? record.fetchedAt : null;
|
||||||
|
const ttlMs = typeof record.ttlMs === 'number' && Number.isFinite(record.ttlMs) ? record.ttlMs : null;
|
||||||
|
const ids = record.ids && typeof record.ids === 'object' ? record.ids : null;
|
||||||
|
const discovery = record.discovery && typeof record.discovery === 'object' ? record.discovery : null;
|
||||||
|
if (!fetchedAt || !ttlMs || !ids || !discovery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const pages = Array.isArray(discovery.pages) ? discovery.pages : null;
|
||||||
|
const bundles = Array.isArray(discovery.bundles) ? discovery.bundles : null;
|
||||||
|
if (!pages || !bundles) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalizedIds = {};
|
||||||
|
for (const [key, value] of Object.entries(ids)) {
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
normalizedIds[key] = value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
fetchedAt,
|
||||||
|
ttlMs,
|
||||||
|
ids: normalizedIds,
|
||||||
|
discovery: {
|
||||||
|
pages: pages.filter((p) => typeof p === 'string'),
|
||||||
|
bundles: bundles.filter((b) => typeof b === 'string'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function readSnapshotFromDisk(cachePath) {
|
||||||
|
try {
|
||||||
|
const raw = await readFile(cachePath, 'utf8');
|
||||||
|
return parseSnapshot(JSON.parse(raw));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function writeSnapshotToDisk(cachePath, snapshot) {
|
||||||
|
await mkdir(path.dirname(cachePath), { recursive: true });
|
||||||
|
await writeFile(cachePath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
||||||
|
}
|
||||||
|
async function discoverBundles(fetchImpl) {
|
||||||
|
const bundles = new Set();
|
||||||
|
for (const page of DISCOVERY_PAGES) {
|
||||||
|
try {
|
||||||
|
const html = await fetchText(fetchImpl, page);
|
||||||
|
for (const match of html.matchAll(BUNDLE_URL_REGEX)) {
|
||||||
|
bundles.add(match[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore discovery page failures; other pages often work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const discovered = [...bundles];
|
||||||
|
if (discovered.length === 0) {
|
||||||
|
throw new Error('No client bundles discovered; x.com layout may have changed.');
|
||||||
|
}
|
||||||
|
return discovered;
|
||||||
|
}
|
||||||
|
function extractOperations(bundleContents, bundleLabel, targets, discovered) {
|
||||||
|
for (const pattern of OPERATION_PATTERNS) {
|
||||||
|
pattern.regex.lastIndex = 0;
|
||||||
|
while (true) {
|
||||||
|
const match = pattern.regex.exec(bundleContents);
|
||||||
|
if (match === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const operationName = match[pattern.operationGroup];
|
||||||
|
const queryId = match[pattern.queryIdGroup];
|
||||||
|
if (!operationName || !queryId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!targets.has(operationName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!QUERY_ID_REGEX.test(queryId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (discovered.has(operationName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
discovered.set(operationName, { queryId, bundle: bundleLabel });
|
||||||
|
if (discovered.size === targets.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function fetchAndExtract(fetchImpl, bundleUrls, targets) {
|
||||||
|
const discovered = new Map();
|
||||||
|
const CONCURRENCY = 6;
|
||||||
|
for (let i = 0; i < bundleUrls.length; i += CONCURRENCY) {
|
||||||
|
const chunk = bundleUrls.slice(i, i + CONCURRENCY);
|
||||||
|
await Promise.all(chunk.map(async (url) => {
|
||||||
|
if (discovered.size === targets.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const label = url.split('/').at(-1) ?? url;
|
||||||
|
try {
|
||||||
|
const js = await fetchText(fetchImpl, url);
|
||||||
|
extractOperations(js, label, targets, discovered);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore failed bundles
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
if (discovered.size === targets.size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return discovered;
|
||||||
|
}
|
||||||
|
export function createRuntimeQueryIdStore(options = {}) {
|
||||||
|
const fetchImpl = options.fetchImpl ?? fetch;
|
||||||
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
||||||
|
const cachePath = options.cachePath ? path.resolve(options.cachePath) : resolveDefaultCachePath();
|
||||||
|
let memorySnapshot = null;
|
||||||
|
let loadOnce = null;
|
||||||
|
let refreshInFlight = null;
|
||||||
|
const loadSnapshot = async () => {
|
||||||
|
if (memorySnapshot) {
|
||||||
|
return memorySnapshot;
|
||||||
|
}
|
||||||
|
if (!loadOnce) {
|
||||||
|
loadOnce = (async () => {
|
||||||
|
const fromDisk = await readSnapshotFromDisk(cachePath);
|
||||||
|
memorySnapshot = fromDisk;
|
||||||
|
return fromDisk;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return loadOnce;
|
||||||
|
};
|
||||||
|
const getSnapshotInfo = async () => {
|
||||||
|
const snapshot = await loadSnapshot();
|
||||||
|
if (!snapshot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fetchedAtMs = new Date(snapshot.fetchedAt).getTime();
|
||||||
|
const ageMs = Number.isFinite(fetchedAtMs) ? Math.max(0, Date.now() - fetchedAtMs) : Number.POSITIVE_INFINITY;
|
||||||
|
const effectiveTtl = Number.isFinite(snapshot.ttlMs) ? snapshot.ttlMs : ttlMs;
|
||||||
|
const isFresh = ageMs <= effectiveTtl;
|
||||||
|
return { snapshot, cachePath, ageMs, isFresh };
|
||||||
|
};
|
||||||
|
const getQueryId = async (operationName) => {
|
||||||
|
const info = await getSnapshotInfo();
|
||||||
|
if (!info) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return info.snapshot.ids[operationName] ?? null;
|
||||||
|
};
|
||||||
|
const refresh = async (operationNames, opts = {}) => {
|
||||||
|
if (refreshInFlight) {
|
||||||
|
return refreshInFlight;
|
||||||
|
}
|
||||||
|
refreshInFlight = (async () => {
|
||||||
|
const current = await getSnapshotInfo();
|
||||||
|
if (!opts.force && current?.isFresh) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
const targets = new Set(operationNames);
|
||||||
|
const bundleUrls = await discoverBundles(fetchImpl);
|
||||||
|
const discovered = await fetchAndExtract(fetchImpl, bundleUrls, targets);
|
||||||
|
if (discovered.size === 0) {
|
||||||
|
return current ?? null;
|
||||||
|
}
|
||||||
|
const ids = {};
|
||||||
|
for (const name of operationNames) {
|
||||||
|
const entry = discovered.get(name);
|
||||||
|
if (entry?.queryId) {
|
||||||
|
ids[name] = entry.queryId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const snapshot = {
|
||||||
|
fetchedAt: new Date().toISOString(),
|
||||||
|
ttlMs,
|
||||||
|
ids,
|
||||||
|
discovery: {
|
||||||
|
pages: [...DISCOVERY_PAGES],
|
||||||
|
bundles: bundleUrls.map((url) => url.split('/').at(-1) ?? url),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await writeSnapshotToDisk(cachePath, snapshot);
|
||||||
|
memorySnapshot = snapshot;
|
||||||
|
return getSnapshotInfo();
|
||||||
|
})().finally(() => {
|
||||||
|
refreshInFlight = null;
|
||||||
|
});
|
||||||
|
return refreshInFlight;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
cachePath,
|
||||||
|
ttlMs,
|
||||||
|
getSnapshotInfo,
|
||||||
|
getQueryId,
|
||||||
|
refresh,
|
||||||
|
clearMemory() {
|
||||||
|
memorySnapshot = null;
|
||||||
|
loadOnce = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const runtimeQueryIds = createRuntimeQueryIdStore();
|
||||||
|
//# sourceMappingURL=runtime-query-ids.js.map
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { randomBytes, randomUUID } from 'node:crypto';
|
||||||
|
import { runtimeQueryIds } from './runtime-query-ids.js';
|
||||||
|
import { QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twitter-client-constants.js';
|
||||||
|
import { normalizeQuoteDepth } from './twitter-client-utils.js';
|
||||||
|
export class TwitterClientBase {
|
||||||
|
authToken;
|
||||||
|
ct0;
|
||||||
|
cookieHeader;
|
||||||
|
userAgent;
|
||||||
|
timeoutMs;
|
||||||
|
quoteDepth;
|
||||||
|
clientUuid;
|
||||||
|
clientDeviceId;
|
||||||
|
clientUserId;
|
||||||
|
constructor(options) {
|
||||||
|
if (!options.cookies.authToken || !options.cookies.ct0) {
|
||||||
|
throw new Error('Both authToken and ct0 cookies are required');
|
||||||
|
}
|
||||||
|
this.authToken = options.cookies.authToken;
|
||||||
|
this.ct0 = options.cookies.ct0;
|
||||||
|
this.cookieHeader = options.cookies.cookieHeader || `auth_token=${this.authToken}; ct0=${this.ct0}`;
|
||||||
|
this.userAgent =
|
||||||
|
options.userAgent ||
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||||
|
this.timeoutMs = options.timeoutMs;
|
||||||
|
this.quoteDepth = normalizeQuoteDepth(options.quoteDepth);
|
||||||
|
this.clientUuid = randomUUID();
|
||||||
|
this.clientDeviceId = randomUUID();
|
||||||
|
}
|
||||||
|
async sleep(ms) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
async getQueryId(operationName) {
|
||||||
|
const cached = await runtimeQueryIds.getQueryId(operationName);
|
||||||
|
return cached ?? QUERY_IDS[operationName];
|
||||||
|
}
|
||||||
|
async refreshQueryIds() {
|
||||||
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await runtimeQueryIds.refresh(TARGET_QUERY_ID_OPERATIONS, { force: true });
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore refresh failures; callers will fall back to baked-in IDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async withRefreshedQueryIdsOn404(attempt) {
|
||||||
|
const firstAttempt = await attempt();
|
||||||
|
if (firstAttempt.success || !firstAttempt.had404) {
|
||||||
|
return { result: firstAttempt, refreshed: false };
|
||||||
|
}
|
||||||
|
await this.refreshQueryIds();
|
||||||
|
const secondAttempt = await attempt();
|
||||||
|
return { result: secondAttempt, refreshed: true };
|
||||||
|
}
|
||||||
|
async getTweetDetailQueryIds() {
|
||||||
|
const primary = await this.getQueryId('TweetDetail');
|
||||||
|
return Array.from(new Set([primary, '97JF30KziU00483E_8elBA', 'aFvUsJm2c-oDkJV75blV6g']));
|
||||||
|
}
|
||||||
|
async getSearchTimelineQueryIds() {
|
||||||
|
const primary = await this.getQueryId('SearchTimeline');
|
||||||
|
return Array.from(new Set([primary, 'M1jEez78PEfVfbQLvlWMvQ', '5h0kNbk3ii97rmfY6CdgAA', 'Tp1sewRU1AsZpBWhqCZicQ']));
|
||||||
|
}
|
||||||
|
async fetchWithTimeout(url, init) {
|
||||||
|
if (!this.timeoutMs || this.timeoutMs <= 0) {
|
||||||
|
return fetch(url, init);
|
||||||
|
}
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||||
|
try {
|
||||||
|
return await fetch(url, { ...init, signal: controller.signal });
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getHeaders() {
|
||||||
|
return this.getJsonHeaders();
|
||||||
|
}
|
||||||
|
createTransactionId() {
|
||||||
|
return randomBytes(16).toString('hex');
|
||||||
|
}
|
||||||
|
getBaseHeaders() {
|
||||||
|
const headers = {
|
||||||
|
accept: '*/*',
|
||||||
|
'accept-language': 'en-US,en;q=0.9',
|
||||||
|
authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
|
||||||
|
'x-csrf-token': this.ct0,
|
||||||
|
'x-twitter-auth-type': 'OAuth2Session',
|
||||||
|
'x-twitter-active-user': 'yes',
|
||||||
|
'x-twitter-client-language': 'en',
|
||||||
|
'x-client-uuid': this.clientUuid,
|
||||||
|
'x-twitter-client-deviceid': this.clientDeviceId,
|
||||||
|
'x-client-transaction-id': this.createTransactionId(),
|
||||||
|
cookie: this.cookieHeader,
|
||||||
|
'user-agent': this.userAgent,
|
||||||
|
origin: 'https://x.com',
|
||||||
|
referer: 'https://x.com/',
|
||||||
|
};
|
||||||
|
if (this.clientUserId) {
|
||||||
|
headers['x-twitter-client-user-id'] = this.clientUserId;
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
getJsonHeaders() {
|
||||||
|
return {
|
||||||
|
...this.getBaseHeaders(),
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getUploadHeaders() {
|
||||||
|
// Note: do not set content-type; URLSearchParams/FormData need to set it (incl boundary) themselves.
|
||||||
|
return this.getBaseHeaders();
|
||||||
|
}
|
||||||
|
async ensureClientUserId() {
|
||||||
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.clientUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await this.getCurrentUser();
|
||||||
|
if (result.success && result.user?.id) {
|
||||||
|
this.clientUserId = result.user.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=twitter-client-base.js.map
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension.
|
||||||
|
import queryIds from './query-ids.json' with { type: 'json' };
|
||||||
|
export const TWITTER_API_BASE = 'https://x.com/i/api/graphql';
|
||||||
|
export const TWITTER_GRAPHQL_POST_URL = 'https://x.com/i/api/graphql';
|
||||||
|
export const TWITTER_UPLOAD_URL = 'https://upload.twitter.com/i/media/upload.json';
|
||||||
|
export const TWITTER_MEDIA_METADATA_URL = 'https://x.com/i/api/1.1/media/metadata/create.json';
|
||||||
|
export const TWITTER_STATUS_UPDATE_URL = 'https://x.com/i/api/1.1/statuses/update.json';
|
||||||
|
export const SETTINGS_SCREEN_NAME_REGEX = /"screen_name":"([^"]+)"/;
|
||||||
|
export const SETTINGS_USER_ID_REGEX = /"user_id"\s*:\s*"(\d+)"/;
|
||||||
|
export const SETTINGS_NAME_REGEX = /"name":"([^"\\]*(?:\\.[^"\\]*)*)"/;
|
||||||
|
// Query IDs rotate frequently; the values in query-ids.json are refreshed by
|
||||||
|
// scripts/update-query-ids.ts. The fallback values keep the client usable if
|
||||||
|
// the file is missing or incomplete.
|
||||||
|
export const FALLBACK_QUERY_IDS = {
|
||||||
|
CreateTweet: 'TAJw1rBsjAtdNgTdlo2oeg',
|
||||||
|
CreateRetweet: 'ojPdsZsimiJrUGLR1sjUtA',
|
||||||
|
DeleteRetweet: 'iQtK4dl5hBmXewYZuEOKVw',
|
||||||
|
CreateFriendship: '8h9JVdV8dlSyqyRDJEPCsA',
|
||||||
|
DestroyFriendship: 'ppXWuagMNXgvzx6WoXBW0Q',
|
||||||
|
FavoriteTweet: 'lI07N6Otwv1PhnEgXILM7A',
|
||||||
|
UnfavoriteTweet: 'ZYKSe-w7KEslx3JhSIk5LA',
|
||||||
|
CreateBookmark: 'aoDbu3RHznuiSkQ9aNM67Q',
|
||||||
|
DeleteBookmark: 'Wlmlj2-xzyS1GN3a6cj-mQ',
|
||||||
|
TweetDetail: '97JF30KziU00483E_8elBA',
|
||||||
|
SearchTimeline: 'M1jEez78PEfVfbQLvlWMvQ',
|
||||||
|
UserArticlesTweets: '8zBy9h4L90aDL02RsBcCFg',
|
||||||
|
UserTweets: 'Wms1GvIiHXAPBaCr9KblaA',
|
||||||
|
Bookmarks: 'RV1g3b8n_SGOHwkqKYSCFw',
|
||||||
|
Following: 'BEkNpEt5pNETESoqMsTEGA',
|
||||||
|
Followers: 'kuFUYP9eV1FPoEy4N-pi7w',
|
||||||
|
Likes: 'JR2gceKucIKcVNB_9JkhsA',
|
||||||
|
BookmarkFolderTimeline: 'KJIQpsvxrTfRIlbaRIySHQ',
|
||||||
|
ListOwnerships: 'wQcOSjSQ8NtgxIwvYl1lMg',
|
||||||
|
ListMemberships: 'BlEXXdARdSeL_0KyKHHvvg',
|
||||||
|
ListLatestTweetsTimeline: '2TemLyqrMpTeAmysdbnVqw',
|
||||||
|
ListByRestId: 'wXzyA5vM_aVkBL9G8Vp3kw',
|
||||||
|
HomeTimeline: 'edseUwk9sP5Phz__9TIRnA',
|
||||||
|
HomeLatestTimeline: 'iOEZpOdfekFsxSlPQCQtPg',
|
||||||
|
ExploreSidebar: 'lpSN4M6qpimkF4nRFPE3nQ',
|
||||||
|
ExplorePage: 'kheAINB_4pzRDqkzG3K-ng',
|
||||||
|
GenericTimelineById: 'uGSr7alSjR9v6QJAIaqSKQ',
|
||||||
|
TrendHistory: 'Sj4T-jSB9pr0Mxtsc1UKZQ',
|
||||||
|
AboutAccountQuery: 'zs_jFPFT78rBpXv9Z3U2YQ',
|
||||||
|
};
|
||||||
|
export const QUERY_IDS = {
|
||||||
|
...FALLBACK_QUERY_IDS,
|
||||||
|
...queryIds,
|
||||||
|
};
|
||||||
|
export const TARGET_QUERY_ID_OPERATIONS = Object.keys(FALLBACK_QUERY_IDS);
|
||||||
|
//# sourceMappingURL=twitter-client-constants.js.map
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
import { applyFeatureOverrides } from './runtime-features.js';
|
||||||
|
export function buildArticleFeatures() {
|
||||||
|
return applyFeatureOverrides('article', {
|
||||||
|
rweb_video_screen_enabled: true,
|
||||||
|
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||||
|
responsive_web_profile_redirect_enabled: true,
|
||||||
|
rweb_tipjar_consumption_enabled: true,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||||
|
responsive_web_graphql_exclude_directive_enabled: true,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
premium_content_api_read_enabled: false,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||||
|
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||||
|
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||||
|
responsive_web_grok_annotations_enabled: false,
|
||||||
|
responsive_web_jetfuel_frame: true,
|
||||||
|
post_ctas_fetch_enabled: true,
|
||||||
|
responsive_web_grok_share_attachment_enabled: true,
|
||||||
|
articles_preview_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||||
|
view_counts_everywhere_api_enabled: true,
|
||||||
|
longform_notetweets_consumption_enabled: true,
|
||||||
|
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: false,
|
||||||
|
responsive_web_grok_show_grok_translated_post: false,
|
||||||
|
responsive_web_grok_analysis_button_from_backend: true,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||||
|
standardized_nudges_misinfo: true,
|
||||||
|
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
responsive_web_grok_image_annotation_enabled: true,
|
||||||
|
responsive_web_grok_imagine_annotation_enabled: true,
|
||||||
|
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||||
|
responsive_web_enhance_cards_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildTweetDetailFeatures() {
|
||||||
|
return applyFeatureOverrides('tweetDetail', {
|
||||||
|
...buildArticleFeatures(),
|
||||||
|
responsive_web_graphql_exclude_directive_enabled: true,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
responsive_web_twitter_article_plain_text_enabled: true,
|
||||||
|
responsive_web_twitter_article_seed_tweet_detail_enabled: true,
|
||||||
|
responsive_web_twitter_article_seed_tweet_summary_enabled: true,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: false,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildArticleFieldToggles() {
|
||||||
|
return {
|
||||||
|
withPayments: false,
|
||||||
|
withAuxiliaryUserLabels: false,
|
||||||
|
withArticleRichContentState: true,
|
||||||
|
withArticlePlainText: true,
|
||||||
|
withGrokAnalyze: false,
|
||||||
|
withDisallowedReplyControls: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function buildSearchFeatures() {
|
||||||
|
return applyFeatureOverrides('search', {
|
||||||
|
rweb_video_screen_enabled: true,
|
||||||
|
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||||
|
responsive_web_profile_redirect_enabled: true,
|
||||||
|
rweb_tipjar_consumption_enabled: true,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||||
|
responsive_web_graphql_exclude_directive_enabled: true,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
premium_content_api_read_enabled: false,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||||
|
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||||
|
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||||
|
responsive_web_grok_annotations_enabled: false,
|
||||||
|
responsive_web_jetfuel_frame: true,
|
||||||
|
post_ctas_fetch_enabled: true,
|
||||||
|
responsive_web_grok_share_attachment_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||||
|
view_counts_everywhere_api_enabled: true,
|
||||||
|
longform_notetweets_consumption_enabled: true,
|
||||||
|
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: false,
|
||||||
|
responsive_web_grok_show_grok_translated_post: false,
|
||||||
|
responsive_web_grok_analysis_button_from_backend: true,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||||
|
standardized_nudges_misinfo: true,
|
||||||
|
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||||
|
rweb_video_timestamps_enabled: true,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
responsive_web_grok_image_annotation_enabled: true,
|
||||||
|
responsive_web_grok_imagine_annotation_enabled: true,
|
||||||
|
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||||
|
articles_preview_enabled: true,
|
||||||
|
responsive_web_enhance_cards_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildTweetCreateFeatures() {
|
||||||
|
return applyFeatureOverrides('tweetCreate', {
|
||||||
|
rweb_video_screen_enabled: true,
|
||||||
|
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||||
|
premium_content_api_read_enabled: false,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||||
|
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||||
|
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||||
|
responsive_web_grok_annotations_enabled: false,
|
||||||
|
responsive_web_jetfuel_frame: true,
|
||||||
|
post_ctas_fetch_enabled: true,
|
||||||
|
responsive_web_grok_share_attachment_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||||
|
view_counts_everywhere_api_enabled: true,
|
||||||
|
longform_notetweets_consumption_enabled: true,
|
||||||
|
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: false,
|
||||||
|
responsive_web_grok_show_grok_translated_post: false,
|
||||||
|
responsive_web_grok_analysis_button_from_backend: true,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||||
|
responsive_web_profile_redirect_enabled: false,
|
||||||
|
rweb_tipjar_consumption_enabled: true,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
articles_preview_enabled: true,
|
||||||
|
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||||
|
standardized_nudges_misinfo: true,
|
||||||
|
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||||
|
responsive_web_grok_image_annotation_enabled: true,
|
||||||
|
responsive_web_grok_imagine_annotation_enabled: true,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||||
|
responsive_web_enhance_cards_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildTimelineFeatures() {
|
||||||
|
return applyFeatureOverrides('timeline', {
|
||||||
|
...buildSearchFeatures(),
|
||||||
|
blue_business_profile_image_shape_enabled: true,
|
||||||
|
responsive_web_text_conversations_enabled: false,
|
||||||
|
tweetypie_unmention_optimization_enabled: true,
|
||||||
|
vibe_api_enabled: true,
|
||||||
|
responsive_web_twitter_blue_verified_badge_is_enabled: true,
|
||||||
|
interactive_text_enabled: true,
|
||||||
|
longform_notetweets_richtext_consumption_enabled: true,
|
||||||
|
responsive_web_media_download_video_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildBookmarksFeatures() {
|
||||||
|
return applyFeatureOverrides('bookmarks', {
|
||||||
|
...buildTimelineFeatures(),
|
||||||
|
graphql_timeline_v2_bookmark_timeline: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildLikesFeatures() {
|
||||||
|
return applyFeatureOverrides('likes', buildTimelineFeatures());
|
||||||
|
}
|
||||||
|
export function buildListsFeatures() {
|
||||||
|
return applyFeatureOverrides('lists', {
|
||||||
|
rweb_video_screen_enabled: true,
|
||||||
|
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||||
|
responsive_web_profile_redirect_enabled: true,
|
||||||
|
rweb_tipjar_consumption_enabled: true,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||||
|
responsive_web_graphql_exclude_directive_enabled: true,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
premium_content_api_read_enabled: false,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||||
|
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||||
|
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||||
|
responsive_web_grok_annotations_enabled: false,
|
||||||
|
responsive_web_jetfuel_frame: true,
|
||||||
|
post_ctas_fetch_enabled: true,
|
||||||
|
responsive_web_grok_share_attachment_enabled: true,
|
||||||
|
articles_preview_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||||
|
view_counts_everywhere_api_enabled: true,
|
||||||
|
longform_notetweets_consumption_enabled: true,
|
||||||
|
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: false,
|
||||||
|
responsive_web_grok_show_grok_translated_post: false,
|
||||||
|
responsive_web_grok_analysis_button_from_backend: true,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||||
|
standardized_nudges_misinfo: true,
|
||||||
|
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
responsive_web_grok_image_annotation_enabled: true,
|
||||||
|
responsive_web_grok_imagine_annotation_enabled: true,
|
||||||
|
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||||
|
responsive_web_enhance_cards_enabled: false,
|
||||||
|
blue_business_profile_image_shape_enabled: false,
|
||||||
|
responsive_web_text_conversations_enabled: false,
|
||||||
|
tweetypie_unmention_optimization_enabled: true,
|
||||||
|
vibe_api_enabled: false,
|
||||||
|
interactive_text_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildHomeTimelineFeatures() {
|
||||||
|
return applyFeatureOverrides('homeTimeline', {
|
||||||
|
...buildTimelineFeatures(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildUserTweetsFeatures() {
|
||||||
|
return applyFeatureOverrides('userTweets', {
|
||||||
|
rweb_video_screen_enabled: false,
|
||||||
|
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||||
|
responsive_web_profile_redirect_enabled: false,
|
||||||
|
rweb_tipjar_consumption_enabled: true,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
premium_content_api_read_enabled: false,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||||
|
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||||
|
responsive_web_grok_analyze_post_followups_enabled: true,
|
||||||
|
responsive_web_jetfuel_frame: true,
|
||||||
|
post_ctas_fetch_enabled: true,
|
||||||
|
responsive_web_grok_share_attachment_enabled: true,
|
||||||
|
responsive_web_grok_annotations_enabled: false,
|
||||||
|
articles_preview_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||||
|
view_counts_everywhere_api_enabled: true,
|
||||||
|
longform_notetweets_consumption_enabled: true,
|
||||||
|
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: false,
|
||||||
|
responsive_web_grok_show_grok_translated_post: true,
|
||||||
|
responsive_web_grok_analysis_button_from_backend: true,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||||
|
standardized_nudges_misinfo: true,
|
||||||
|
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
responsive_web_grok_image_annotation_enabled: true,
|
||||||
|
responsive_web_grok_imagine_annotation_enabled: true,
|
||||||
|
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||||
|
responsive_web_enhance_cards_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildFollowingFeatures() {
|
||||||
|
return applyFeatureOverrides('following', {
|
||||||
|
rweb_video_screen_enabled: true,
|
||||||
|
profile_label_improvements_pcf_label_in_post_enabled: false,
|
||||||
|
responsive_web_profile_redirect_enabled: true,
|
||||||
|
rweb_tipjar_consumption_enabled: true,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
premium_content_api_read_enabled: true,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||||
|
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||||
|
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||||
|
responsive_web_grok_annotations_enabled: false,
|
||||||
|
responsive_web_jetfuel_frame: false,
|
||||||
|
post_ctas_fetch_enabled: true,
|
||||||
|
responsive_web_grok_share_attachment_enabled: false,
|
||||||
|
articles_preview_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||||
|
view_counts_everywhere_api_enabled: true,
|
||||||
|
longform_notetweets_consumption_enabled: true,
|
||||||
|
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: true,
|
||||||
|
responsive_web_grok_show_grok_translated_post: false,
|
||||||
|
responsive_web_grok_analysis_button_from_backend: false,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||||
|
standardized_nudges_misinfo: true,
|
||||||
|
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
responsive_web_grok_image_annotation_enabled: false,
|
||||||
|
responsive_web_grok_imagine_annotation_enabled: false,
|
||||||
|
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||||
|
responsive_web_enhance_cards_enabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function buildExploreFeatures() {
|
||||||
|
return applyFeatureOverrides('explore', {
|
||||||
|
rweb_video_screen_enabled: true,
|
||||||
|
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||||
|
responsive_web_profile_redirect_enabled: true,
|
||||||
|
rweb_tipjar_consumption_enabled: true,
|
||||||
|
verified_phone_label_enabled: false,
|
||||||
|
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||||
|
responsive_web_graphql_exclude_directive_enabled: true,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
premium_content_api_read_enabled: false,
|
||||||
|
communities_web_enable_tweet_community_results_fetch: true,
|
||||||
|
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||||
|
responsive_web_grok_analyze_button_fetch_trends_enabled: true,
|
||||||
|
responsive_web_grok_analyze_post_followups_enabled: true,
|
||||||
|
responsive_web_grok_annotations_enabled: true,
|
||||||
|
responsive_web_jetfuel_frame: true,
|
||||||
|
responsive_web_grok_share_attachment_enabled: true,
|
||||||
|
articles_preview_enabled: true,
|
||||||
|
responsive_web_edit_tweet_api_enabled: true,
|
||||||
|
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||||
|
view_counts_everywhere_api_enabled: true,
|
||||||
|
longform_notetweets_consumption_enabled: true,
|
||||||
|
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||||
|
tweet_awards_web_tipping_enabled: false,
|
||||||
|
responsive_web_grok_show_grok_translated_post: true,
|
||||||
|
responsive_web_grok_analysis_button_from_backend: true,
|
||||||
|
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||||
|
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||||
|
standardized_nudges_misinfo: true,
|
||||||
|
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||||
|
longform_notetweets_rich_text_read_enabled: true,
|
||||||
|
longform_notetweets_inline_media_enabled: true,
|
||||||
|
responsive_web_grok_image_annotation_enabled: true,
|
||||||
|
responsive_web_grok_imagine_annotation_enabled: true,
|
||||||
|
responsive_web_grok_community_note_auto_translation_is_enabled: true,
|
||||||
|
responsive_web_enhance_cards_enabled: false,
|
||||||
|
// Additional features required for ExploreSidebar
|
||||||
|
post_ctas_fetch_enabled: true,
|
||||||
|
rweb_video_timestamps_enabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=twitter-client-features.js.map
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||||
|
import { buildSearchFeatures } from './twitter-client-features.js';
|
||||||
|
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
|
||||||
|
const RAW_QUERY_MISSING_REGEX = /must be defined/i;
|
||||||
|
function isQueryIdMismatch(payload) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(payload);
|
||||||
|
return (parsed.errors?.some((error) => {
|
||||||
|
if (error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (error?.path?.includes('rawQuery') && RAW_QUERY_MISSING_REGEX.test(error.message ?? '')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}) ?? false);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function withSearch(Base) {
|
||||||
|
class TwitterClientSearch extends Base {
|
||||||
|
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Search for tweets matching a query
|
||||||
|
*/
|
||||||
|
async search(query, count = 20, options = {}) {
|
||||||
|
return this.searchPaged(query, count, options);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get all search results (paged)
|
||||||
|
*/
|
||||||
|
async getAllSearchResults(query, options) {
|
||||||
|
return this.searchPaged(query, Number.POSITIVE_INFINITY, options);
|
||||||
|
}
|
||||||
|
async searchPaged(query, limit, options = {}) {
|
||||||
|
const features = buildSearchFeatures();
|
||||||
|
const pageSize = 20;
|
||||||
|
const seen = new Set();
|
||||||
|
const tweets = [];
|
||||||
|
let cursor = options.cursor;
|
||||||
|
let nextCursor;
|
||||||
|
let pagesFetched = 0;
|
||||||
|
const { includeRaw = false, maxPages } = options;
|
||||||
|
const fetchPage = async (pageCount, pageCursor) => {
|
||||||
|
let lastError;
|
||||||
|
let had404 = false;
|
||||||
|
const queryIds = await this.getSearchTimelineQueryIds();
|
||||||
|
for (const queryId of queryIds) {
|
||||||
|
const variables = {
|
||||||
|
rawQuery: query,
|
||||||
|
count: pageCount,
|
||||||
|
querySource: 'typed_query',
|
||||||
|
product: 'Latest',
|
||||||
|
...(pageCursor ? { cursor: pageCursor } : {}),
|
||||||
|
};
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
variables: JSON.stringify(variables),
|
||||||
|
});
|
||||||
|
const url = `${TWITTER_API_BASE}/${queryId}/SearchTimeline?${params.toString()}`;
|
||||||
|
try {
|
||||||
|
const response = await this.fetchWithTimeout(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
body: JSON.stringify({ features, queryId }),
|
||||||
|
});
|
||||||
|
if (response.status === 404) {
|
||||||
|
had404 = true;
|
||||||
|
lastError = `HTTP ${response.status}`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
const shouldRefreshQueryIds = (response.status === 400 || response.status === 422) && isQueryIdMismatch(text);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `HTTP ${response.status}: ${text.slice(0, 200)}`,
|
||||||
|
had404: had404 || shouldRefreshQueryIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const data = (await response.json());
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
const shouldRefreshQueryIds = data.errors.some((error) => error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: data.errors.map((e) => e.message).join(', '),
|
||||||
|
had404: had404 || shouldRefreshQueryIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const instructions = data.data?.search_by_raw_query?.search_timeline?.timeline?.instructions;
|
||||||
|
const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
|
||||||
|
const nextCursor = extractCursorFromInstructions(instructions);
|
||||||
|
return { success: true, tweets: pageTweets, cursor: nextCursor, had404 };
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: false, error: lastError ?? 'Unknown error fetching search results', had404 };
|
||||||
|
};
|
||||||
|
const fetchWithRefresh = async (pageCount, pageCursor) => {
|
||||||
|
const firstAttempt = await fetchPage(pageCount, pageCursor);
|
||||||
|
if (firstAttempt.success) {
|
||||||
|
return firstAttempt;
|
||||||
|
}
|
||||||
|
if (firstAttempt.had404) {
|
||||||
|
await this.refreshQueryIds();
|
||||||
|
const secondAttempt = await fetchPage(pageCount, pageCursor);
|
||||||
|
if (secondAttempt.success) {
|
||||||
|
return secondAttempt;
|
||||||
|
}
|
||||||
|
return { success: false, error: secondAttempt.error };
|
||||||
|
}
|
||||||
|
return { success: false, error: firstAttempt.error };
|
||||||
|
};
|
||||||
|
const unlimited = limit === Number.POSITIVE_INFINITY;
|
||||||
|
while (unlimited || tweets.length < limit) {
|
||||||
|
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
|
||||||
|
const page = await fetchWithRefresh(pageCount, cursor);
|
||||||
|
if (!page.success) {
|
||||||
|
return { success: false, error: page.error };
|
||||||
|
}
|
||||||
|
pagesFetched += 1;
|
||||||
|
let added = 0;
|
||||||
|
for (const tweet of page.tweets) {
|
||||||
|
if (seen.has(tweet.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(tweet.id);
|
||||||
|
tweets.push(tweet);
|
||||||
|
added += 1;
|
||||||
|
if (!unlimited && tweets.length >= limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pageCursor = page.cursor;
|
||||||
|
if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
|
||||||
|
nextCursor = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (maxPages && pagesFetched >= maxPages) {
|
||||||
|
nextCursor = pageCursor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor = pageCursor;
|
||||||
|
nextCursor = pageCursor;
|
||||||
|
}
|
||||||
|
return { success: true, tweets, nextCursor };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TwitterClientSearch;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=twitter-client-search.js.map
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export {};
|
||||||
|
//# sourceMappingURL=twitter-client-types.js.map
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
export function normalizeQuoteDepth(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.floor(value));
|
||||||
|
}
|
||||||
|
export function firstText(...values) {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
export function collectTextFields(value, keys, output) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
collectTextFields(item, keys, output);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
for (const [key, nested] of Object.entries(value)) {
|
||||||
|
if (keys.has(key)) {
|
||||||
|
if (typeof nested === 'string') {
|
||||||
|
const trimmed = nested.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
output.push(trimmed);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectTextFields(nested, keys, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function uniqueOrdered(values) {
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
for (const value of values) {
|
||||||
|
if (seen.has(value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
|
result.push(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Renders a Draft.js content_state into readable markdown/text format.
|
||||||
|
* Handles blocks (paragraphs, headers, lists) and entities (code blocks, links, tweets, dividers).
|
||||||
|
*/
|
||||||
|
export function renderContentState(contentState) {
|
||||||
|
if (!contentState?.blocks || contentState.blocks.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Build entity lookup map from array/object formats
|
||||||
|
const entityMap = new Map();
|
||||||
|
const rawEntityMap = contentState.entityMap ?? [];
|
||||||
|
if (Array.isArray(rawEntityMap)) {
|
||||||
|
for (const entry of rawEntityMap) {
|
||||||
|
const key = Number.parseInt(entry.key, 10);
|
||||||
|
if (!Number.isNaN(key)) {
|
||||||
|
entityMap.set(key, entry.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const [key, value] of Object.entries(rawEntityMap)) {
|
||||||
|
const keyNumber = Number.parseInt(key, 10);
|
||||||
|
if (!Number.isNaN(keyNumber)) {
|
||||||
|
entityMap.set(keyNumber, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const outputLines = [];
|
||||||
|
let orderedListCounter = 0;
|
||||||
|
let previousBlockType;
|
||||||
|
for (const block of contentState.blocks) {
|
||||||
|
// Reset ordered list counter when leaving ordered list context
|
||||||
|
if (block.type !== 'ordered-list-item' && previousBlockType === 'ordered-list-item') {
|
||||||
|
orderedListCounter = 0;
|
||||||
|
}
|
||||||
|
switch (block.type) {
|
||||||
|
case 'unstyled': {
|
||||||
|
// Plain paragraph - just output text with any inline formatting
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'header-one': {
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(`# ${text}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'header-two': {
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(`## ${text}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'header-three': {
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(`### ${text}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'unordered-list-item': {
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(`- ${text}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ordered-list-item': {
|
||||||
|
orderedListCounter++;
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(`${orderedListCounter}. ${text}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'blockquote': {
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(`> ${text}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'atomic': {
|
||||||
|
// Atomic blocks are placeholders for embedded entities
|
||||||
|
const entityContent = renderAtomicBlock(block, entityMap);
|
||||||
|
if (entityContent) {
|
||||||
|
outputLines.push(entityContent);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// Fallback: just output the text
|
||||||
|
const text = renderBlockText(block, entityMap);
|
||||||
|
if (text) {
|
||||||
|
outputLines.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousBlockType = block.type;
|
||||||
|
}
|
||||||
|
const result = outputLines.join('\n\n');
|
||||||
|
return result.trim() || undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Renders text content of a block, applying inline link entities.
|
||||||
|
*/
|
||||||
|
function renderBlockText(block, entityMap) {
|
||||||
|
let text = block.text;
|
||||||
|
// Handle LINK entities by appending URL in markdown format
|
||||||
|
// Process in reverse order to not mess up offsets
|
||||||
|
const linkRanges = (block.entityRanges ?? [])
|
||||||
|
.filter((range) => {
|
||||||
|
const entity = entityMap.get(range.key);
|
||||||
|
return entity?.type === 'LINK' && entity.data.url;
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.offset - a.offset);
|
||||||
|
for (const range of linkRanges) {
|
||||||
|
const entity = entityMap.get(range.key);
|
||||||
|
if (entity?.data.url) {
|
||||||
|
const linkText = text.slice(range.offset, range.offset + range.length);
|
||||||
|
const markdownLink = `[${linkText}](${entity.data.url})`;
|
||||||
|
text = text.slice(0, range.offset) + markdownLink + text.slice(range.offset + range.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text.trim();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Renders an atomic block by looking up its entity and returning appropriate content.
|
||||||
|
*/
|
||||||
|
function renderAtomicBlock(block, entityMap) {
|
||||||
|
const entityRanges = block.entityRanges ?? [];
|
||||||
|
if (entityRanges.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const entityKey = entityRanges[0].key;
|
||||||
|
const entity = entityMap.get(entityKey);
|
||||||
|
if (!entity) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
switch (entity.type) {
|
||||||
|
case 'MARKDOWN':
|
||||||
|
// Code blocks and other markdown content - output as-is
|
||||||
|
return entity.data.markdown?.trim();
|
||||||
|
case 'DIVIDER':
|
||||||
|
return '---';
|
||||||
|
case 'TWEET':
|
||||||
|
if (entity.data.tweetId) {
|
||||||
|
return `[Embedded Tweet: https://x.com/i/status/${entity.data.tweetId}]`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
case 'LINK':
|
||||||
|
if (entity.data.url) {
|
||||||
|
return `[Link: ${entity.data.url}]`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
case 'IMAGE':
|
||||||
|
// Images in atomic blocks - could extract URL if available
|
||||||
|
return '[Image]';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function extractArticleText(result) {
|
||||||
|
const article = result?.article;
|
||||||
|
if (!article) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const articleResult = article.article_results?.result ?? article;
|
||||||
|
if (process.env.BIRD_DEBUG_ARTICLE === '1') {
|
||||||
|
console.error('[bird][debug][article] payload:', JSON.stringify({
|
||||||
|
rest_id: result?.rest_id,
|
||||||
|
article: articleResult,
|
||||||
|
note_tweet: result?.note_tweet?.note_tweet_results?.result ?? null,
|
||||||
|
}, null, 2));
|
||||||
|
}
|
||||||
|
const title = firstText(articleResult.title, article.title);
|
||||||
|
// Try to render from rich content_state first (Draft.js format with blocks + entityMap)
|
||||||
|
// This preserves code blocks, embedded tweets, markdown, etc.
|
||||||
|
const contentState = article.article_results?.result?.content_state;
|
||||||
|
const richBody = renderContentState(contentState);
|
||||||
|
if (richBody) {
|
||||||
|
// Rich content found - prepend title if not already included
|
||||||
|
if (title) {
|
||||||
|
const normalizedTitle = title.trim();
|
||||||
|
const trimmedBody = richBody.trimStart();
|
||||||
|
const headingMatches = [`# ${normalizedTitle}`, `## ${normalizedTitle}`, `### ${normalizedTitle}`];
|
||||||
|
const hasTitle = trimmedBody === normalizedTitle ||
|
||||||
|
trimmedBody.startsWith(`${normalizedTitle}\n`) ||
|
||||||
|
headingMatches.some((heading) => trimmedBody.startsWith(heading));
|
||||||
|
if (!hasTitle) {
|
||||||
|
return `${title}\n\n${richBody}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return richBody;
|
||||||
|
}
|
||||||
|
// Fallback to plain text extraction for articles without rich content_state
|
||||||
|
let body = firstText(articleResult.plain_text, article.plain_text, articleResult.body?.text, articleResult.body?.richtext?.text, articleResult.body?.rich_text?.text, articleResult.content?.text, articleResult.content?.richtext?.text, articleResult.content?.rich_text?.text, articleResult.text, articleResult.richtext?.text, articleResult.rich_text?.text, article.body?.text, article.body?.richtext?.text, article.body?.rich_text?.text, article.content?.text, article.content?.richtext?.text, article.content?.rich_text?.text, article.text, article.richtext?.text, article.rich_text?.text);
|
||||||
|
if (body && title && body.trim() === title.trim()) {
|
||||||
|
body = undefined;
|
||||||
|
}
|
||||||
|
if (!body) {
|
||||||
|
const collected = [];
|
||||||
|
collectTextFields(articleResult, new Set(['text', 'title']), collected);
|
||||||
|
collectTextFields(article, new Set(['text', 'title']), collected);
|
||||||
|
const unique = uniqueOrdered(collected);
|
||||||
|
const filtered = title ? unique.filter((value) => value !== title) : unique;
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
body = filtered.join('\n\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (title && body && !body.startsWith(title)) {
|
||||||
|
return `${title}\n\n${body}`;
|
||||||
|
}
|
||||||
|
return body ?? title;
|
||||||
|
}
|
||||||
|
export function extractNoteTweetText(result) {
|
||||||
|
const note = result?.note_tweet?.note_tweet_results?.result;
|
||||||
|
if (!note) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return firstText(note.text, note.richtext?.text, note.rich_text?.text, note.content?.text, note.content?.richtext?.text, note.content?.rich_text?.text);
|
||||||
|
}
|
||||||
|
export function extractTweetText(result) {
|
||||||
|
return extractArticleText(result) ?? extractNoteTweetText(result) ?? firstText(result?.legacy?.full_text);
|
||||||
|
}
|
||||||
|
export function extractArticleMetadata(result) {
|
||||||
|
const article = result?.article;
|
||||||
|
if (!article) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const articleResult = article.article_results?.result ?? article;
|
||||||
|
const title = firstText(articleResult.title, article.title);
|
||||||
|
if (!title) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// preview_text is available in home timeline responses
|
||||||
|
const previewText = firstText(articleResult.preview_text, article.preview_text);
|
||||||
|
return { title, previewText };
|
||||||
|
}
|
||||||
|
export function extractMedia(result) {
|
||||||
|
// Prefer extended_entities (has video info), fall back to entities
|
||||||
|
const rawMedia = result?.legacy?.extended_entities?.media ?? result?.legacy?.entities?.media;
|
||||||
|
if (!rawMedia || rawMedia.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const media = [];
|
||||||
|
for (const item of rawMedia) {
|
||||||
|
if (!item.type || !item.media_url_https) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const mediaItem = {
|
||||||
|
type: item.type,
|
||||||
|
url: item.media_url_https,
|
||||||
|
};
|
||||||
|
// Get dimensions from largest available size
|
||||||
|
const sizes = item.sizes;
|
||||||
|
if (sizes?.large) {
|
||||||
|
mediaItem.width = sizes.large.w;
|
||||||
|
mediaItem.height = sizes.large.h;
|
||||||
|
}
|
||||||
|
else if (sizes?.medium) {
|
||||||
|
mediaItem.width = sizes.medium.w;
|
||||||
|
mediaItem.height = sizes.medium.h;
|
||||||
|
}
|
||||||
|
// For thumbnails/previews
|
||||||
|
if (sizes?.small) {
|
||||||
|
mediaItem.previewUrl = `${item.media_url_https}:small`;
|
||||||
|
}
|
||||||
|
// Extract video URL for video/animated_gif
|
||||||
|
if ((item.type === 'video' || item.type === 'animated_gif') && item.video_info?.variants) {
|
||||||
|
// Prefer highest bitrate MP4, fall back to first MP4 when bitrate is missing.
|
||||||
|
const mp4Variants = item.video_info.variants.filter((v) => v.content_type === 'video/mp4' && typeof v.url === 'string');
|
||||||
|
const mp4WithBitrate = mp4Variants
|
||||||
|
.filter((v) => typeof v.bitrate === 'number')
|
||||||
|
.sort((a, b) => b.bitrate - a.bitrate);
|
||||||
|
const selectedVariant = mp4WithBitrate[0] ?? mp4Variants[0];
|
||||||
|
if (selectedVariant) {
|
||||||
|
mediaItem.videoUrl = selectedVariant.url;
|
||||||
|
}
|
||||||
|
if (typeof item.video_info.duration_millis === 'number') {
|
||||||
|
mediaItem.durationMs = item.video_info.duration_millis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
media.push(mediaItem);
|
||||||
|
}
|
||||||
|
return media.length > 0 ? media : undefined;
|
||||||
|
}
|
||||||
|
export function unwrapTweetResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (result.tweet) {
|
||||||
|
return result.tweet;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
export function mapTweetResult(result, quoteDepthOrOptions) {
|
||||||
|
const options = typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions;
|
||||||
|
const { quoteDepth, includeRaw = false } = options;
|
||||||
|
const userResult = result?.core?.user_results?.result;
|
||||||
|
const userLegacy = userResult?.legacy;
|
||||||
|
const userCore = userResult?.core;
|
||||||
|
const username = userLegacy?.screen_name ?? userCore?.screen_name;
|
||||||
|
const name = userLegacy?.name ?? userCore?.name ?? username;
|
||||||
|
const userId = userResult?.rest_id;
|
||||||
|
if (!result?.rest_id || !username) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const text = extractTweetText(result);
|
||||||
|
if (!text) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let quotedTweet;
|
||||||
|
if (quoteDepth > 0) {
|
||||||
|
const quotedResult = unwrapTweetResult(result.quoted_status_result?.result);
|
||||||
|
if (quotedResult) {
|
||||||
|
quotedTweet = mapTweetResult(quotedResult, { quoteDepth: quoteDepth - 1, includeRaw });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const media = extractMedia(result);
|
||||||
|
const article = extractArticleMetadata(result);
|
||||||
|
const tweetData = {
|
||||||
|
id: result.rest_id,
|
||||||
|
text,
|
||||||
|
createdAt: result.legacy?.created_at,
|
||||||
|
replyCount: result.legacy?.reply_count,
|
||||||
|
retweetCount: result.legacy?.retweet_count,
|
||||||
|
likeCount: result.legacy?.favorite_count,
|
||||||
|
conversationId: result.legacy?.conversation_id_str,
|
||||||
|
inReplyToStatusId: result.legacy?.in_reply_to_status_id_str ?? undefined,
|
||||||
|
author: {
|
||||||
|
username,
|
||||||
|
name: name || username,
|
||||||
|
},
|
||||||
|
authorId: userId,
|
||||||
|
quotedTweet,
|
||||||
|
media,
|
||||||
|
article,
|
||||||
|
};
|
||||||
|
if (includeRaw) {
|
||||||
|
tweetData._raw = result;
|
||||||
|
}
|
||||||
|
return tweetData;
|
||||||
|
}
|
||||||
|
export function findTweetInInstructions(instructions, tweetId) {
|
||||||
|
if (!instructions) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
for (const instruction of instructions) {
|
||||||
|
for (const entry of instruction.entries || []) {
|
||||||
|
const result = entry.content?.itemContent?.tweet_results?.result;
|
||||||
|
if (result?.rest_id === tweetId) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
export function collectTweetResultsFromEntry(entry) {
|
||||||
|
const results = [];
|
||||||
|
const pushResult = (result) => {
|
||||||
|
if (result?.rest_id) {
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const content = entry.content;
|
||||||
|
pushResult(content?.itemContent?.tweet_results?.result);
|
||||||
|
pushResult(content?.item?.itemContent?.tweet_results?.result);
|
||||||
|
for (const item of content?.items ?? []) {
|
||||||
|
pushResult(item?.item?.itemContent?.tweet_results?.result);
|
||||||
|
pushResult(item?.itemContent?.tweet_results?.result);
|
||||||
|
pushResult(item?.content?.itemContent?.tweet_results?.result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
export function parseTweetsFromInstructions(instructions, quoteDepthOrOptions) {
|
||||||
|
const options = typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions;
|
||||||
|
const { quoteDepth, includeRaw = false } = options;
|
||||||
|
const tweets = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const instruction of instructions ?? []) {
|
||||||
|
for (const entry of instruction.entries ?? []) {
|
||||||
|
const results = collectTweetResultsFromEntry(entry);
|
||||||
|
for (const result of results) {
|
||||||
|
const mapped = mapTweetResult(result, { quoteDepth, includeRaw });
|
||||||
|
if (!mapped || seen.has(mapped.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(mapped.id);
|
||||||
|
tweets.push(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tweets;
|
||||||
|
}
|
||||||
|
export function extractCursorFromInstructions(instructions, cursorType = 'Bottom') {
|
||||||
|
for (const instruction of instructions ?? []) {
|
||||||
|
for (const entry of instruction.entries ?? []) {
|
||||||
|
const content = entry.content;
|
||||||
|
if (content?.cursorType === cursorType && typeof content.value === 'string' && content.value.length > 0) {
|
||||||
|
return content.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
export function parseUsersFromInstructions(instructions) {
|
||||||
|
if (!instructions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const users = [];
|
||||||
|
for (const instruction of instructions) {
|
||||||
|
if (!instruction.entries) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const entry of instruction.entries) {
|
||||||
|
const content = entry?.content;
|
||||||
|
const rawUserResult = content?.itemContent?.user_results?.result;
|
||||||
|
const userResult = rawUserResult?.__typename === 'UserWithVisibilityResults' && rawUserResult.user
|
||||||
|
? rawUserResult.user
|
||||||
|
: rawUserResult;
|
||||||
|
if (!userResult || userResult.__typename !== 'User') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const legacy = userResult.legacy;
|
||||||
|
const core = userResult.core;
|
||||||
|
const username = legacy?.screen_name ?? core?.screen_name;
|
||||||
|
if (!userResult.rest_id || !username) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
users.push({
|
||||||
|
id: userResult.rest_id,
|
||||||
|
username,
|
||||||
|
name: legacy?.name ?? core?.name ?? username,
|
||||||
|
description: legacy?.description,
|
||||||
|
followersCount: legacy?.followers_count,
|
||||||
|
followingCount: legacy?.friends_count,
|
||||||
|
isBlueVerified: userResult.is_blue_verified,
|
||||||
|
profileImageUrl: legacy?.profile_image_url_https ?? userResult.avatar?.image_url,
|
||||||
|
createdAt: legacy?.created_at ?? core?.created_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=twitter-client-utils.js.map
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Peter Steinberger
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# @steipete/sweet-cookie
|
||||||
|
|
||||||
|
Inline-first browser cookie extraction for local tooling (no native addons).
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Inline payloads (JSON / base64 / file) — most reliable path.
|
||||||
|
- Local browser reads (best effort): Chrome, Edge, Firefox, Safari (macOS).
|
||||||
|
|
||||||
|
Install:
|
||||||
|
```bash
|
||||||
|
npm i @steipete/sweet-cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```ts
|
||||||
|
import { getCookies, toCookieHeader } from '@steipete/sweet-cookie';
|
||||||
|
|
||||||
|
const { cookies, warnings } = await getCookies({
|
||||||
|
url: 'https://example.com/',
|
||||||
|
names: ['session', 'csrf'],
|
||||||
|
browsers: ['chrome', 'edge', 'firefox', 'safari'],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const w of warnings) console.warn(w);
|
||||||
|
const cookieHeader = toCookieHeader(cookies, { dedupeByName: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
Docs + extension exporter: see the repo root README.
|
||||||
|
|
||||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
export { getCookies, toCookieHeader } from './public.js';
|
||||||
|
export type { BrowserName, Cookie, CookieHeaderOptions, CookieMode, CookieSameSite, GetCookiesOptions, GetCookiesResult, } from './types.js';
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EACX,WAAW,EACX,MAAM,EACN,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GAChB,MAAM,YAAY,CAAC"}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { getCookies, toCookieHeader } from './public.js';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
||||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromChrome(options: {
|
||||||
|
profile?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=chrome.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chrome.d.ts","sourceRoot":"","sources":["../../src/providers/chrome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAK5D,wBAAsB,oBAAoB,CACzC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EAC5F,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CA0B3B"}
|
||||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { getCookiesFromChromeSqliteLinux } from './chromeSqliteLinux.js';
|
||||||
|
import { getCookiesFromChromeSqliteMac } from './chromeSqliteMac.js';
|
||||||
|
import { getCookiesFromChromeSqliteWindows } from './chromeSqliteWindows.js';
|
||||||
|
export async function getCookiesFromChrome(options, origins, allowlistNames) {
|
||||||
|
const warnings = [];
|
||||||
|
// Platform dispatch only. All real logic lives in the per-OS providers.
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const r = await getCookiesFromChromeSqliteMac(options, origins, allowlistNames);
|
||||||
|
warnings.push(...r.warnings);
|
||||||
|
const cookies = r.cookies;
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
const r = await getCookiesFromChromeSqliteLinux(options, origins, allowlistNames);
|
||||||
|
warnings.push(...r.warnings);
|
||||||
|
const cookies = r.cookies;
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const r = await getCookiesFromChromeSqliteWindows(options, origins, allowlistNames);
|
||||||
|
warnings.push(...r.warnings);
|
||||||
|
const cookies = r.cookies;
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=chrome.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chrome.js","sourceRoot":"","sources":["../../src/providers/chrome.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,iCAAiC,EAAE,MAAM,0BAA0B,CAAC;AAE7E,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,OAA4F,EAC5F,OAAiB,EACjB,cAAkC;IAElC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,wEAAwE;IACxE,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,MAAM,6BAA6B,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QAChF,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAa,CAAC,CAAC,OAAO,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,MAAM,+BAA+B,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QAClF,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAa,CAAC,CAAC,OAAO,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,MAAM,iCAAiC,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QACpF,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAa,CAAC,CAAC,OAAO,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;AAClC,CAAC"}
|
||||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
export declare function deriveAes128CbcKeyFromPassword(password: string, options: {
|
||||||
|
iterations: number;
|
||||||
|
}): Buffer;
|
||||||
|
export declare function decryptChromiumAes128CbcCookieValue(encryptedValue: Uint8Array, keyCandidates: readonly Buffer[], options: {
|
||||||
|
stripHashPrefix: boolean;
|
||||||
|
treatUnknownPrefixAsPlaintext?: boolean;
|
||||||
|
}): string | null;
|
||||||
|
export declare function decryptChromiumAes256GcmCookieValue(encryptedValue: Uint8Array, key: Buffer, options: {
|
||||||
|
stripHashPrefix: boolean;
|
||||||
|
}): string | null;
|
||||||
|
//# sourceMappingURL=crypto.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/providers/chromeSqlite/crypto.ts"],"names":[],"mappings":"AAIA,wBAAgB,8BAA8B,CAC7C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7B,MAAM,CAIR;AAED,wBAAgB,mCAAmC,CAClD,cAAc,EAAE,UAAU,EAC1B,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,OAAO,EAAE;IAAE,eAAe,EAAE,OAAO,CAAC;IAAC,6BAA6B,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5E,MAAM,GAAG,IAAI,CA2Bf;AAED,wBAAgB,mCAAmC,CAClD,cAAc,EAAE,UAAU,EAC1B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;IAAE,eAAe,EAAE,OAAO,CAAA;CAAE,GACnC,MAAM,GAAG,IAAI,CAyBf"}
|
||||||
Generated
Vendored
+100
@@ -0,0 +1,100 @@
|
|||||||
|
import { createDecipheriv, pbkdf2Sync } from 'node:crypto';
|
||||||
|
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
||||||
|
export function deriveAes128CbcKeyFromPassword(password, options) {
|
||||||
|
// Chromium derives the AES-128-CBC key from "Chrome Safe Storage" using PBKDF2.
|
||||||
|
// The salt/length/digest are fixed by Chromium ("saltysalt", 16 bytes, sha1).
|
||||||
|
return pbkdf2Sync(password, 'saltysalt', options.iterations, 16, 'sha1');
|
||||||
|
}
|
||||||
|
export function decryptChromiumAes128CbcCookieValue(encryptedValue, keyCandidates, options) {
|
||||||
|
const buf = Buffer.from(encryptedValue);
|
||||||
|
if (buf.length < 3)
|
||||||
|
return null;
|
||||||
|
// Chromium prefixes encrypted cookies with `v10`, `v11`, ... (three bytes).
|
||||||
|
const prefix = buf.subarray(0, 3).toString('utf8');
|
||||||
|
const hasVersionPrefix = /^v\d\d$/.test(prefix);
|
||||||
|
if (!hasVersionPrefix) {
|
||||||
|
// Some platforms (notably macOS) can store plaintext values in `encrypted_value`.
|
||||||
|
// Callers decide whether unknown prefixes should be treated as plaintext.
|
||||||
|
if (options.treatUnknownPrefixAsPlaintext === false)
|
||||||
|
return null;
|
||||||
|
return decodeCookieValueBytes(buf, false);
|
||||||
|
}
|
||||||
|
const ciphertext = buf.subarray(3);
|
||||||
|
if (!ciphertext.length)
|
||||||
|
return '';
|
||||||
|
for (const key of keyCandidates) {
|
||||||
|
// Try multiple candidates because Linux may fall back to empty passwords depending on keyring state.
|
||||||
|
const decrypted = tryDecryptAes128Cbc(ciphertext, key);
|
||||||
|
if (!decrypted)
|
||||||
|
continue;
|
||||||
|
const decoded = decodeCookieValueBytes(decrypted, options.stripHashPrefix);
|
||||||
|
if (decoded !== null)
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export function decryptChromiumAes256GcmCookieValue(encryptedValue, key, options) {
|
||||||
|
const buf = Buffer.from(encryptedValue);
|
||||||
|
if (buf.length < 3)
|
||||||
|
return null;
|
||||||
|
const prefix = buf.subarray(0, 3).toString('utf8');
|
||||||
|
if (!/^v\d\d$/.test(prefix))
|
||||||
|
return null;
|
||||||
|
// AES-256-GCM layout:
|
||||||
|
// - 12-byte nonce
|
||||||
|
// - ciphertext
|
||||||
|
// - 16-byte authentication tag
|
||||||
|
const payload = buf.subarray(3);
|
||||||
|
if (payload.length < 12 + 16)
|
||||||
|
return null;
|
||||||
|
const nonce = payload.subarray(0, 12);
|
||||||
|
const authenticationTag = payload.subarray(payload.length - 16);
|
||||||
|
const ciphertext = payload.subarray(12, payload.length - 16);
|
||||||
|
try {
|
||||||
|
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
||||||
|
decipher.setAuthTag(authenticationTag);
|
||||||
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||||
|
return decodeCookieValueBytes(plaintext, options.stripHashPrefix);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function tryDecryptAes128Cbc(ciphertext, key) {
|
||||||
|
try {
|
||||||
|
// Chromium's legacy AES-128-CBC uses an IV of 16 spaces.
|
||||||
|
const iv = Buffer.alloc(16, 0x20);
|
||||||
|
const decipher = createDecipheriv('aes-128-cbc', key, iv);
|
||||||
|
decipher.setAutoPadding(false);
|
||||||
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||||
|
return removePkcs7Padding(plaintext);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function removePkcs7Padding(value) {
|
||||||
|
if (!value.length)
|
||||||
|
return value;
|
||||||
|
const padding = value[value.length - 1];
|
||||||
|
if (!padding || padding > 16)
|
||||||
|
return value;
|
||||||
|
return value.subarray(0, value.length - padding);
|
||||||
|
}
|
||||||
|
function decodeCookieValueBytes(value, stripHashPrefix) {
|
||||||
|
// Chromium >= 24 prepends a 32-byte hash to cookie values.
|
||||||
|
const bytes = stripHashPrefix && value.length >= 32 ? value.subarray(32) : value;
|
||||||
|
try {
|
||||||
|
return stripLeadingControlChars(UTF8_DECODER.decode(bytes));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function stripLeadingControlChars(value) {
|
||||||
|
let i = 0;
|
||||||
|
while (i < value.length && value.charCodeAt(i) < 0x20)
|
||||||
|
i += 1;
|
||||||
|
return value.slice(i);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=crypto.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/providers/chromeSqlite/crypto.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
export type LinuxKeyringBackend = 'gnome' | 'kwallet' | 'basic';
|
||||||
|
/**
|
||||||
|
* Read the "Safe Storage" password from a Linux keyring.
|
||||||
|
*
|
||||||
|
* Chromium browsers typically store their cookie encryption password under:
|
||||||
|
* - service: "<Browser> Safe Storage"
|
||||||
|
* - account: "<Browser>"
|
||||||
|
*
|
||||||
|
* We keep this logic in JS (no native deps) and return an empty password on failure
|
||||||
|
* (Chromium may still have v10 cookies, and callers can use inline/export escape hatches).
|
||||||
|
*/
|
||||||
|
export declare function getLinuxChromiumSafeStoragePassword(options: {
|
||||||
|
backend?: LinuxKeyringBackend;
|
||||||
|
app: 'chrome' | 'edge';
|
||||||
|
}): Promise<{
|
||||||
|
password: string;
|
||||||
|
warnings: string[];
|
||||||
|
}>;
|
||||||
|
export declare function getLinuxChromeSafeStoragePassword(options?: {
|
||||||
|
backend?: LinuxKeyringBackend;
|
||||||
|
}): Promise<{
|
||||||
|
password: string;
|
||||||
|
warnings: string[];
|
||||||
|
}>;
|
||||||
|
//# sourceMappingURL=linuxKeyring.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"linuxKeyring.d.ts","sourceRoot":"","sources":["../../../src/providers/chromeSqlite/linuxKeyring.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhE;;;;;;;;;GASG;AACH,wBAAsB,mCAAmC,CAAC,OAAO,EAAE;IAClE,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,GAAG,EAAE,QAAQ,GAAG,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA8DpD;AAED,wBAAsB,iCAAiC,CACtD,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,mBAAmB,CAAA;CAAO,GAC7C,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAInD"}
|
||||||
Generated
Vendored
+104
@@ -0,0 +1,104 @@
|
|||||||
|
import { execCapture } from '../../util/exec.js';
|
||||||
|
/**
|
||||||
|
* Read the "Safe Storage" password from a Linux keyring.
|
||||||
|
*
|
||||||
|
* Chromium browsers typically store their cookie encryption password under:
|
||||||
|
* - service: "<Browser> Safe Storage"
|
||||||
|
* - account: "<Browser>"
|
||||||
|
*
|
||||||
|
* We keep this logic in JS (no native deps) and return an empty password on failure
|
||||||
|
* (Chromium may still have v10 cookies, and callers can use inline/export escape hatches).
|
||||||
|
*/
|
||||||
|
export async function getLinuxChromiumSafeStoragePassword(options) {
|
||||||
|
const warnings = [];
|
||||||
|
// Escape hatch: if callers already know the password (or want deterministic CI behavior),
|
||||||
|
// they can bypass keyring probing entirely.
|
||||||
|
const overrideKey = options.app === 'edge'
|
||||||
|
? 'SWEET_COOKIE_EDGE_SAFE_STORAGE_PASSWORD'
|
||||||
|
: 'SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD';
|
||||||
|
const override = readEnv(overrideKey);
|
||||||
|
if (override !== undefined)
|
||||||
|
return { password: override, warnings };
|
||||||
|
const backend = options.backend ?? parseLinuxKeyringBackend() ?? chooseLinuxKeyringBackend();
|
||||||
|
// `basic` means "don't try keyrings" (Chrome will fall back to older/less-secure schemes on some setups).
|
||||||
|
if (backend === 'basic')
|
||||||
|
return { password: '', warnings };
|
||||||
|
const service = options.app === 'edge' ? 'Microsoft Edge Safe Storage' : 'Chrome Safe Storage';
|
||||||
|
const account = options.app === 'edge' ? 'Microsoft Edge' : 'Chrome';
|
||||||
|
const folder = `${account} Keys`;
|
||||||
|
if (backend === 'gnome') {
|
||||||
|
// GNOME keyring: `secret-tool` is the simplest way to read libsecret entries.
|
||||||
|
const res = await execCapture('secret-tool', ['lookup', 'service', service, 'account', account], { timeoutMs: 3_000 });
|
||||||
|
if (res.code === 0)
|
||||||
|
return { password: res.stdout.trim(), warnings };
|
||||||
|
warnings.push('Failed to read Linux keyring via secret-tool; v11 cookies may be unavailable.');
|
||||||
|
return { password: '', warnings };
|
||||||
|
}
|
||||||
|
// KDE keyring: query KWallet via `kwallet-query`, but the wallet name differs across KDE versions.
|
||||||
|
const kdeVersion = (readEnv('KDE_SESSION_VERSION') ?? '').trim();
|
||||||
|
const serviceName = kdeVersion === '6'
|
||||||
|
? 'org.kde.kwalletd6'
|
||||||
|
: kdeVersion === '5'
|
||||||
|
? 'org.kde.kwalletd5'
|
||||||
|
: 'org.kde.kwalletd';
|
||||||
|
const walletPath = kdeVersion === '6'
|
||||||
|
? '/modules/kwalletd6'
|
||||||
|
: kdeVersion === '5'
|
||||||
|
? '/modules/kwalletd5'
|
||||||
|
: '/modules/kwalletd';
|
||||||
|
const wallet = await getKWalletNetworkWallet(serviceName, walletPath);
|
||||||
|
const passwordRes = await execCapture('kwallet-query', ['--read-password', service, '--folder', folder, wallet], { timeoutMs: 3_000 });
|
||||||
|
if (passwordRes.code !== 0) {
|
||||||
|
warnings.push('Failed to read Linux keyring via kwallet-query; v11 cookies may be unavailable.');
|
||||||
|
return { password: '', warnings };
|
||||||
|
}
|
||||||
|
if (passwordRes.stdout.toLowerCase().startsWith('failed to read'))
|
||||||
|
return { password: '', warnings };
|
||||||
|
return { password: passwordRes.stdout.trim(), warnings };
|
||||||
|
}
|
||||||
|
export async function getLinuxChromeSafeStoragePassword(options = {}) {
|
||||||
|
const args = { app: 'chrome' };
|
||||||
|
if (options.backend !== undefined)
|
||||||
|
args.backend = options.backend;
|
||||||
|
return await getLinuxChromiumSafeStoragePassword(args);
|
||||||
|
}
|
||||||
|
function parseLinuxKeyringBackend() {
|
||||||
|
const raw = readEnv('SWEET_COOKIE_LINUX_KEYRING');
|
||||||
|
if (!raw)
|
||||||
|
return undefined;
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
if (normalized === 'gnome')
|
||||||
|
return 'gnome';
|
||||||
|
if (normalized === 'kwallet')
|
||||||
|
return 'kwallet';
|
||||||
|
if (normalized === 'basic')
|
||||||
|
return 'basic';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function chooseLinuxKeyringBackend() {
|
||||||
|
const xdg = readEnv('XDG_CURRENT_DESKTOP') ?? '';
|
||||||
|
const isKde = xdg.split(':').some((p) => p.trim().toLowerCase() === 'kde') || !!readEnv('KDE_FULL_SESSION');
|
||||||
|
return isKde ? 'kwallet' : 'gnome';
|
||||||
|
}
|
||||||
|
async function getKWalletNetworkWallet(serviceName, walletPath) {
|
||||||
|
const res = await execCapture('dbus-send', [
|
||||||
|
'--session',
|
||||||
|
'--print-reply=literal',
|
||||||
|
`--dest=${serviceName}`,
|
||||||
|
walletPath,
|
||||||
|
'org.kde.KWallet.networkWallet',
|
||||||
|
], { timeoutMs: 3_000 });
|
||||||
|
const fallback = 'kdewallet';
|
||||||
|
if (res.code !== 0)
|
||||||
|
return fallback;
|
||||||
|
const raw = res.stdout.trim();
|
||||||
|
if (!raw)
|
||||||
|
return fallback;
|
||||||
|
return raw.replaceAll('"', '').trim() || fallback;
|
||||||
|
}
|
||||||
|
function readEnv(key) {
|
||||||
|
const value = process.env[key];
|
||||||
|
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||||
|
return trimmed.length ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=linuxKeyring.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"linuxKeyring.js","sourceRoot":"","sources":["../../../src/providers/chromeSqlite/linuxKeyring.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import type { GetCookiesResult } from '../../types.js';
|
||||||
|
export declare function getCookiesFromChromeSqliteDb(options: {
|
||||||
|
dbPath: string;
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null, decrypt: (encryptedValue: Uint8Array, options: {
|
||||||
|
stripHashPrefix: boolean;
|
||||||
|
}) => string | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=shared.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/providers/chromeSqlite/shared.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAA0B,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAkB/E,wBAAsB,4BAA4B,CACjD,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EACxF,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAClC,OAAO,EAAE,CAAC,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE;IAAE,eAAe,EAAE,OAAO,CAAA;CAAE,KAAK,MAAM,GAAG,IAAI,GAC3F,OAAO,CAAC,gBAAgB,CAAC,CAsD3B"}
|
||||||
Generated
Vendored
+293
@@ -0,0 +1,293 @@
|
|||||||
|
import { copyFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { normalizeExpiration } from '../../util/expire.js';
|
||||||
|
import { hostMatchesCookieDomain } from '../../util/hostMatch.js';
|
||||||
|
import { importNodeSqlite, supportsReadBigInts } from '../../util/nodeSqlite.js';
|
||||||
|
import { isBunRuntime } from '../../util/runtime.js';
|
||||||
|
export async function getCookiesFromChromeSqliteDb(options, origins, allowlistNames, decrypt) {
|
||||||
|
const warnings = [];
|
||||||
|
// Chrome can keep its cookie DB locked and/or rely on WAL sidecars.
|
||||||
|
// Copying to a temp dir gives us a stable snapshot that both node:sqlite and bun:sqlite can open.
|
||||||
|
const tempDir = mkdtempSync(path.join(tmpdir(), 'sweet-cookie-chrome-'));
|
||||||
|
const tempDbPath = path.join(tempDir, 'Cookies');
|
||||||
|
try {
|
||||||
|
copyFileSync(options.dbPath, tempDbPath);
|
||||||
|
// If WAL is enabled, the latest writes might live in `Cookies-wal`/`Cookies-shm`.
|
||||||
|
// Copy them too when present so our snapshot reflects the current browser state.
|
||||||
|
copySidecar(options.dbPath, `${tempDbPath}-wal`, '-wal');
|
||||||
|
copySidecar(options.dbPath, `${tempDbPath}-shm`, '-shm');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
warnings.push(`Failed to copy Chrome cookie DB: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const hosts = origins.map((o) => new URL(o).hostname);
|
||||||
|
const where = buildHostWhereClause(hosts, 'host_key');
|
||||||
|
const metaVersion = await readChromiumMetaVersion(tempDbPath);
|
||||||
|
// Chromium >= 24 stores a 32-byte hash prefix in decrypted cookie values.
|
||||||
|
// We detect this via the `meta` table version and strip it when present.
|
||||||
|
const stripHashPrefix = metaVersion >= 24;
|
||||||
|
const rowsResult = await readChromeRows(tempDbPath, where);
|
||||||
|
if (!rowsResult.ok) {
|
||||||
|
warnings.push(rowsResult.error);
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const collectOptions = {};
|
||||||
|
if (options.profile)
|
||||||
|
collectOptions.profile = options.profile;
|
||||||
|
if (options.includeExpired !== undefined)
|
||||||
|
collectOptions.includeExpired = options.includeExpired;
|
||||||
|
const cookies = collectChromeCookiesFromRows(rowsResult.rows, collectOptions, hosts, allowlistNames, (encryptedValue) => decrypt(encryptedValue, { stripHashPrefix }), warnings);
|
||||||
|
return { cookies: dedupeCookies(cookies), warnings };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function collectChromeCookiesFromRows(rows, options, hosts, allowlistNames, decrypt, warnings) {
|
||||||
|
const cookies = [];
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
let warnedEncryptedType = false;
|
||||||
|
for (const row of rows) {
|
||||||
|
const name = typeof row.name === 'string' ? row.name : null;
|
||||||
|
if (!name)
|
||||||
|
continue;
|
||||||
|
if (allowlistNames && allowlistNames.size > 0 && !allowlistNames.has(name))
|
||||||
|
continue;
|
||||||
|
const hostKey = typeof row.host_key === 'string' ? row.host_key : null;
|
||||||
|
if (!hostKey)
|
||||||
|
continue;
|
||||||
|
if (!hostMatchesAny(hosts, hostKey))
|
||||||
|
continue;
|
||||||
|
const rowPath = typeof row.path === 'string' ? row.path : '';
|
||||||
|
const valueString = typeof row.value === 'string' ? row.value : null;
|
||||||
|
let value = valueString;
|
||||||
|
if (value === null || value.length === 0) {
|
||||||
|
// Many modern Chromium cookies keep `value` empty and only store `encrypted_value`.
|
||||||
|
// We decrypt on demand and drop rows we can't interpret.
|
||||||
|
const encryptedBytes = getEncryptedBytes(row);
|
||||||
|
if (!encryptedBytes) {
|
||||||
|
if (!warnedEncryptedType && row.encrypted_value !== undefined) {
|
||||||
|
warnings.push('Chrome cookie encrypted_value is in an unsupported type.');
|
||||||
|
warnedEncryptedType = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value = decrypt(encryptedBytes);
|
||||||
|
}
|
||||||
|
if (value === null)
|
||||||
|
continue;
|
||||||
|
const expiresRaw = typeof row.expires_utc === 'number' || typeof row.expires_utc === 'bigint'
|
||||||
|
? row.expires_utc
|
||||||
|
: tryParseInt(row.expires_utc);
|
||||||
|
const expires = normalizeExpiration(expiresRaw ?? undefined);
|
||||||
|
if (!options.includeExpired) {
|
||||||
|
if (expires && expires < now)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const secure = row.is_secure === 1 ||
|
||||||
|
row.is_secure === 1n ||
|
||||||
|
row.is_secure === '1' ||
|
||||||
|
row.is_secure === true;
|
||||||
|
const httpOnly = row.is_httponly === 1 ||
|
||||||
|
row.is_httponly === 1n ||
|
||||||
|
row.is_httponly === '1' ||
|
||||||
|
row.is_httponly === true;
|
||||||
|
const sameSite = normalizeChromiumSameSite(row.samesite);
|
||||||
|
const source = { browser: 'chrome' };
|
||||||
|
if (options.profile)
|
||||||
|
source.profile = options.profile;
|
||||||
|
const cookie = {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
domain: hostKey.startsWith('.') ? hostKey.slice(1) : hostKey,
|
||||||
|
path: rowPath || '/',
|
||||||
|
secure,
|
||||||
|
httpOnly,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
if (expires !== undefined)
|
||||||
|
cookie.expires = expires;
|
||||||
|
if (sameSite !== undefined)
|
||||||
|
cookie.sameSite = sameSite;
|
||||||
|
cookies.push(cookie);
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
function tryParseInt(value) {
|
||||||
|
if (typeof value === 'bigint') {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string')
|
||||||
|
return null;
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
function normalizeChromiumSameSite(value) {
|
||||||
|
if (typeof value === 'bigint') {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? normalizeChromiumSameSite(parsed) : undefined;
|
||||||
|
}
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
if (value === 2)
|
||||||
|
return 'Strict';
|
||||||
|
if (value === 1)
|
||||||
|
return 'Lax';
|
||||||
|
if (value === 0)
|
||||||
|
return 'None';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
if (Number.isFinite(parsed))
|
||||||
|
return normalizeChromiumSameSite(parsed);
|
||||||
|
const normalized = value.toLowerCase();
|
||||||
|
if (normalized === 'strict')
|
||||||
|
return 'Strict';
|
||||||
|
if (normalized === 'lax')
|
||||||
|
return 'Lax';
|
||||||
|
if (normalized === 'none' || normalized === 'no_restriction')
|
||||||
|
return 'None';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function getEncryptedBytes(row) {
|
||||||
|
const raw = row.encrypted_value;
|
||||||
|
if (raw instanceof Uint8Array)
|
||||||
|
return raw;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function readChromiumMetaVersion(dbPath) {
|
||||||
|
const sql = `SELECT value FROM meta WHERE key = 'version'`;
|
||||||
|
const result = isBunRuntime()
|
||||||
|
? await queryNodeOrBun({ kind: 'bun', dbPath, sql })
|
||||||
|
: await queryNodeOrBun({ kind: 'node', dbPath, sql });
|
||||||
|
if (!result.ok)
|
||||||
|
return 0;
|
||||||
|
const first = result.rows[0];
|
||||||
|
const value = first?.value;
|
||||||
|
if (typeof value === 'number')
|
||||||
|
return Math.floor(value);
|
||||||
|
if (typeof value === 'bigint') {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? Math.floor(parsed) : 0;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
async function readChromeRows(dbPath, where) {
|
||||||
|
const sqliteKind = isBunRuntime() ? 'bun' : 'node';
|
||||||
|
const sqliteLabel = sqliteKind === 'bun' ? 'bun:sqlite' : 'node:sqlite';
|
||||||
|
const sql = `SELECT name, value, host_key, path, expires_utc, samesite, encrypted_value, ` +
|
||||||
|
`is_secure AS is_secure, is_httponly AS is_httponly ` +
|
||||||
|
`FROM cookies WHERE (${where}) ORDER BY expires_utc DESC;`;
|
||||||
|
const result = await queryNodeOrBun({ kind: sqliteKind, dbPath, sql });
|
||||||
|
if (result.ok)
|
||||||
|
return { ok: true, rows: result.rows };
|
||||||
|
// Intentionally strict: only support modern Chromium cookie DB schemas.
|
||||||
|
// If this fails, assume the local Chrome/Chromium is too old or uses a non-standard schema.
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `${sqliteLabel} failed reading Chrome cookies (requires modern Chromium, e.g. Chrome >= 100): ${result.error}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function queryNodeOrBun(options) {
|
||||||
|
try {
|
||||||
|
if (options.kind === 'node') {
|
||||||
|
// Node's `node:sqlite` is synchronous and returns plain JS values. Keep it boxed in a
|
||||||
|
// small scope so callers don't need to care about runtime differences.
|
||||||
|
const { DatabaseSync } = await importNodeSqlite();
|
||||||
|
const dbOptions = { readOnly: true };
|
||||||
|
if (supportsReadBigInts()) {
|
||||||
|
dbOptions.readBigInts = true;
|
||||||
|
}
|
||||||
|
const db = new DatabaseSync(options.dbPath, dbOptions);
|
||||||
|
try {
|
||||||
|
const rows = db.prepare(options.sql).all();
|
||||||
|
return { ok: true, rows };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Bun's sqlite API has a different surface (`Database` + `.query().all()`).
|
||||||
|
const { Database } = await import('bun:sqlite');
|
||||||
|
const db = new Database(options.dbPath, { readonly: true });
|
||||||
|
try {
|
||||||
|
const rows = db.query(options.sql).all();
|
||||||
|
return { ok: true, rows };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function copySidecar(sourceDbPath, target, suffix) {
|
||||||
|
const sidecar = `${sourceDbPath}${suffix}`;
|
||||||
|
if (!existsSync(sidecar))
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
copyFileSync(sidecar, target);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildHostWhereClause(hosts, column) {
|
||||||
|
const clauses = [];
|
||||||
|
for (const host of hosts) {
|
||||||
|
// Chrome cookies often live on parent domains (e.g. .google.com for gemini.google.com).
|
||||||
|
// Include parent domains so the SQL filter doesn't drop valid session cookies.
|
||||||
|
for (const candidate of expandHostCandidates(host)) {
|
||||||
|
const escaped = sqlLiteral(candidate);
|
||||||
|
const escapedDot = sqlLiteral(`.${candidate}`);
|
||||||
|
const escapedLike = sqlLiteral(`%.${candidate}`);
|
||||||
|
clauses.push(`${column} = ${escaped}`);
|
||||||
|
clauses.push(`${column} = ${escapedDot}`);
|
||||||
|
clauses.push(`${column} LIKE ${escapedLike}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clauses.length ? clauses.join(' OR ') : '1=0';
|
||||||
|
}
|
||||||
|
function sqlLiteral(value) {
|
||||||
|
const escaped = value.replaceAll("'", "''");
|
||||||
|
return `'${escaped}'`;
|
||||||
|
}
|
||||||
|
function expandHostCandidates(host) {
|
||||||
|
const parts = host.split('.').filter(Boolean);
|
||||||
|
if (parts.length <= 1)
|
||||||
|
return [host];
|
||||||
|
const candidates = new Set();
|
||||||
|
candidates.add(host);
|
||||||
|
// Include parent domains down to two labels (avoid TLD-only fragments).
|
||||||
|
for (let i = 1; i <= parts.length - 2; i += 1) {
|
||||||
|
const candidate = parts.slice(i).join('.');
|
||||||
|
if (candidate)
|
||||||
|
candidates.add(candidate);
|
||||||
|
}
|
||||||
|
return Array.from(candidates);
|
||||||
|
}
|
||||||
|
function hostMatchesAny(hosts, cookieHost) {
|
||||||
|
const cookieDomain = cookieHost.startsWith('.') ? cookieHost.slice(1) : cookieHost;
|
||||||
|
return hosts.some((host) => hostMatchesCookieDomain(host, cookieDomain));
|
||||||
|
}
|
||||||
|
function dedupeCookies(cookies) {
|
||||||
|
const merged = new Map();
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
const key = `${cookie.name}|${cookie.domain ?? ''}|${cookie.path ?? ''}`;
|
||||||
|
if (!merged.has(key))
|
||||||
|
merged.set(key, cookie);
|
||||||
|
}
|
||||||
|
return Array.from(merged.values());
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=shared.js.map
|
||||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
export declare function dpapiUnprotect(data: Buffer, options?: {
|
||||||
|
timeoutMs?: number;
|
||||||
|
}): Promise<{
|
||||||
|
ok: true;
|
||||||
|
value: Buffer;
|
||||||
|
} | {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
//# sourceMappingURL=windowsDpapi.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"windowsDpapi.d.ts","sourceRoot":"","sources":["../../../src/providers/chromeSqlite/windowsDpapi.ts"],"names":[],"mappings":"AAEA,wBAAsB,cAAc,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CA+BrE"}
|
||||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import { execCapture } from '../../util/exec.js';
|
||||||
|
export async function dpapiUnprotect(data, options = {}) {
|
||||||
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
||||||
|
// There is no cross-platform JS API for Windows DPAPI, and we explicitly avoid native addons.
|
||||||
|
// PowerShell can call ProtectedData.Unprotect for the current user, which matches Chrome's behavior.
|
||||||
|
const inputB64 = data.toString('base64');
|
||||||
|
const prelude = 'try { Add-Type -AssemblyName System.Security.Cryptography.ProtectedData -ErrorAction Stop } catch { try { Add-Type -AssemblyName System.Security -ErrorAction Stop } catch {} };';
|
||||||
|
const script = prelude +
|
||||||
|
`$in=[Convert]::FromBase64String('${inputB64}');` +
|
||||||
|
`$out=[System.Security.Cryptography.ProtectedData]::Unprotect($in,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser);` +
|
||||||
|
`[Convert]::ToBase64String($out)`;
|
||||||
|
const res = await execCapture('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
||||||
|
timeoutMs,
|
||||||
|
});
|
||||||
|
if (res.code !== 0) {
|
||||||
|
return { ok: false, error: res.stderr.trim() || `powershell exit ${res.code}` };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const out = Buffer.from(res.stdout.trim(), 'base64');
|
||||||
|
return { ok: true, value: out };
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=windowsDpapi.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"windowsDpapi.js","sourceRoot":"","sources":["../../../src/providers/chromeSqlite/windowsDpapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,IAAY,EACZ,UAAkC,EAAE;IAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAE7C,8FAA8F;IAC9F,qGAAqG;IACrG,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,OAAO,GACZ,kLAAkL,CAAC;IACpL,MAAM,MAAM,GACX,OAAO;QACP,oCAAoC,QAAQ,KAAK;QACjD,0IAA0I;QAC1I,iCAAiC,CAAC;IAEnC,MAAM,GAAG,GAAG,MAAM,WAAW,CAC5B,YAAY,EACZ,CAAC,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,CAAC,EACrD;QACC,SAAS;KACT,CACD,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,mBAAmB,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;IACjF,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;QACrD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACrF,CAAC;AACF,CAAC"}
|
||||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromChromeSqliteLinux(options: {
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=chromeSqliteLinux.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chromeSqliteLinux.d.ts","sourceRoot":"","sources":["../../src/providers/chromeSqliteLinux.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AASpD,wBAAsB,+BAA+B,CACpD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EACxE,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAkD3B"}
|
||||||
Generated
Vendored
+51
@@ -0,0 +1,51 @@
|
|||||||
|
import { decryptChromiumAes128CbcCookieValue, deriveAes128CbcKeyFromPassword, } from './chromeSqlite/crypto.js';
|
||||||
|
import { getLinuxChromeSafeStoragePassword } from './chromeSqlite/linuxKeyring.js';
|
||||||
|
import { getCookiesFromChromeSqliteDb } from './chromeSqlite/shared.js';
|
||||||
|
import { resolveChromiumCookiesDbLinux } from './chromium/linuxPaths.js';
|
||||||
|
export async function getCookiesFromChromeSqliteLinux(options, origins, allowlistNames) {
|
||||||
|
const args = {
|
||||||
|
configDirName: 'google-chrome',
|
||||||
|
};
|
||||||
|
if (options.profile !== undefined)
|
||||||
|
args.profile = options.profile;
|
||||||
|
const dbPath = resolveChromiumCookiesDbLinux(args);
|
||||||
|
if (!dbPath) {
|
||||||
|
return { cookies: [], warnings: ['Chrome cookies database not found.'] };
|
||||||
|
}
|
||||||
|
const { password, warnings: keyringWarnings } = await getLinuxChromeSafeStoragePassword();
|
||||||
|
// Linux uses multiple schemes depending on distro/keyring availability.
|
||||||
|
// - v10 often uses the hard-coded "peanuts" password
|
||||||
|
// - v11 uses "Chrome Safe Storage" from the keyring (may be empty/unavailable)
|
||||||
|
const v10Key = deriveAes128CbcKeyFromPassword('peanuts', { iterations: 1 });
|
||||||
|
const emptyKey = deriveAes128CbcKeyFromPassword('', { iterations: 1 });
|
||||||
|
const v11Key = deriveAes128CbcKeyFromPassword(password, { iterations: 1 });
|
||||||
|
const decrypt = (encryptedValue, opts) => {
|
||||||
|
const prefix = Buffer.from(encryptedValue).subarray(0, 3).toString('utf8');
|
||||||
|
if (prefix === 'v10') {
|
||||||
|
return decryptChromiumAes128CbcCookieValue(encryptedValue, [v10Key, emptyKey], {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
treatUnknownPrefixAsPlaintext: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (prefix === 'v11') {
|
||||||
|
return decryptChromiumAes128CbcCookieValue(encryptedValue, [v11Key, emptyKey], {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
treatUnknownPrefixAsPlaintext: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const dbOptions = {
|
||||||
|
dbPath,
|
||||||
|
};
|
||||||
|
if (options.profile)
|
||||||
|
dbOptions.profile = options.profile;
|
||||||
|
if (options.includeExpired !== undefined)
|
||||||
|
dbOptions.includeExpired = options.includeExpired;
|
||||||
|
if (options.debug !== undefined)
|
||||||
|
dbOptions.debug = options.debug;
|
||||||
|
const result = await getCookiesFromChromeSqliteDb(dbOptions, origins, allowlistNames, decrypt);
|
||||||
|
result.warnings.unshift(...keyringWarnings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=chromeSqliteLinux.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chromeSqliteLinux.js","sourceRoot":"","sources":["../../src/providers/chromeSqliteLinux.ts"],"names":[],"mappings":"AACA,OAAO,EACN,mCAAmC,EACnC,8BAA8B,GAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,iCAAiC,EAAE,MAAM,gCAAgC,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACpD,OAAwE,EACxE,OAAiB,EACjB,cAAkC;IAElC,MAAM,IAAI,GAAwD;QACjE,aAAa,EAAE,eAAe;KAC9B,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAClE,MAAM,MAAM,GAAG,6BAA6B,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,oCAAoC,CAAC,EAAE,CAAC;IAC1E,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,iCAAiC,EAAE,CAAC;IAE1F,wEAAwE;IACxE,qDAAqD;IACrD,+EAA+E;IAC/E,MAAM,MAAM,GAAG,8BAA8B,CAAC,SAAS,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5E,MAAM,QAAQ,GAAG,8BAA8B,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IACvE,MAAM,MAAM,GAAG,8BAA8B,CAAC,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IAE3E,MAAM,OAAO,GAAG,CACf,cAA0B,EAC1B,IAAkC,EAClB,EAAE;QAClB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC3E,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACtB,OAAO,mCAAmC,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;gBAC9E,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,6BAA6B,EAAE,KAAK;aACpC,CAAC,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACtB,OAAO,mCAAmC,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;gBAC9E,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,6BAA6B,EAAE,KAAK;aACpC,CAAC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;IAEF,MAAM,SAAS,GACd;QACC,MAAM;KACN,CAAC;IACH,IAAI,OAAO,CAAC,OAAO;QAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACzD,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS;QAAE,SAAS,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAC5F,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAEjE,MAAM,MAAM,GAAG,MAAM,4BAA4B,CAAC,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;IAC/F,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,eAAe,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC;AACf,CAAC"}
|
||||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromChromeSqliteMac(options: {
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=chromeSqliteMac.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chromeSqliteMac.d.ts","sourceRoot":"","sources":["../../src/providers/chromeSqliteMac.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AASpD,wBAAsB,6BAA6B,CAClD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EACxE,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CA6C3B"}
|
||||||
scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.js
Generated
Vendored
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import { homedir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { decryptChromiumAes128CbcCookieValue, deriveAes128CbcKeyFromPassword, } from './chromeSqlite/crypto.js';
|
||||||
|
import { getCookiesFromChromeSqliteDb } from './chromeSqlite/shared.js';
|
||||||
|
import { readKeychainGenericPasswordFirst } from './chromium/macosKeychain.js';
|
||||||
|
import { resolveCookiesDbFromProfileOrRoots } from './chromium/paths.js';
|
||||||
|
export async function getCookiesFromChromeSqliteMac(options, origins, allowlistNames) {
|
||||||
|
const dbPath = resolveChromeCookiesDb(options.profile);
|
||||||
|
if (!dbPath) {
|
||||||
|
return { cookies: [], warnings: ['Chrome cookies database not found.'] };
|
||||||
|
}
|
||||||
|
const warnings = [];
|
||||||
|
// On macOS, Chrome stores its "Safe Storage" secret in Keychain.
|
||||||
|
// `security find-generic-password` is stable and avoids any native Node keychain modules.
|
||||||
|
const passwordResult = await readKeychainGenericPasswordFirst({
|
||||||
|
account: 'Chrome',
|
||||||
|
services: ['Chrome Safe Storage'],
|
||||||
|
timeoutMs: 3_000,
|
||||||
|
label: 'Chrome Safe Storage',
|
||||||
|
});
|
||||||
|
if (!passwordResult.ok) {
|
||||||
|
warnings.push(passwordResult.error);
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const chromePassword = passwordResult.password.trim();
|
||||||
|
if (!chromePassword) {
|
||||||
|
warnings.push('macOS Keychain returned an empty Chrome Safe Storage password.');
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
// Chromium uses PBKDF2(password, "saltysalt", 1003, 16, sha1) for AES-128-CBC cookie values on macOS.
|
||||||
|
const key = deriveAes128CbcKeyFromPassword(chromePassword, { iterations: 1003 });
|
||||||
|
const decrypt = (encryptedValue, opts) => decryptChromiumAes128CbcCookieValue(encryptedValue, [key], {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
treatUnknownPrefixAsPlaintext: true,
|
||||||
|
});
|
||||||
|
const dbOptions = {
|
||||||
|
dbPath,
|
||||||
|
};
|
||||||
|
if (options.profile)
|
||||||
|
dbOptions.profile = options.profile;
|
||||||
|
if (options.includeExpired !== undefined)
|
||||||
|
dbOptions.includeExpired = options.includeExpired;
|
||||||
|
if (options.debug !== undefined)
|
||||||
|
dbOptions.debug = options.debug;
|
||||||
|
const result = await getCookiesFromChromeSqliteDb(dbOptions, origins, allowlistNames, decrypt);
|
||||||
|
result.warnings.unshift(...warnings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function resolveChromeCookiesDb(profile) {
|
||||||
|
const home = homedir();
|
||||||
|
/* c8 ignore next */
|
||||||
|
const roots = process.platform === 'darwin'
|
||||||
|
? [path.join(home, 'Library', 'Application Support', 'Google', 'Chrome')]
|
||||||
|
: [];
|
||||||
|
const args = { roots };
|
||||||
|
if (profile !== undefined)
|
||||||
|
args.profile = profile;
|
||||||
|
return resolveCookiesDbFromProfileOrRoots(args);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=chromeSqliteMac.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chromeSqliteMac.js","sourceRoot":"","sources":["../../src/providers/chromeSqliteMac.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromChromeSqliteWindows(options: {
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=chromeSqliteWindows.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chromeSqliteWindows.d.ts","sourceRoot":"","sources":["../../src/providers/chromeSqliteWindows.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAMpD,wBAAsB,iCAAiC,CACtD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EACxE,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAmC3B"}
|
||||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { decryptChromiumAes256GcmCookieValue } from './chromeSqlite/crypto.js';
|
||||||
|
import { getCookiesFromChromeSqliteDb } from './chromeSqlite/shared.js';
|
||||||
|
import { getWindowsChromiumMasterKey } from './chromium/windowsMasterKey.js';
|
||||||
|
import { resolveChromiumPathsWindows } from './chromium/windowsPaths.js';
|
||||||
|
export async function getCookiesFromChromeSqliteWindows(options, origins, allowlistNames) {
|
||||||
|
const resolveArgs = {
|
||||||
|
localAppDataVendorPath: path.join('Google', 'Chrome', 'User Data'),
|
||||||
|
};
|
||||||
|
if (options.profile !== undefined)
|
||||||
|
resolveArgs.profile = options.profile;
|
||||||
|
const { dbPath, userDataDir } = resolveChromiumPathsWindows(resolveArgs);
|
||||||
|
if (!dbPath || !userDataDir) {
|
||||||
|
return { cookies: [], warnings: ['Chrome cookies database not found.'] };
|
||||||
|
}
|
||||||
|
// On Windows, Chrome stores an AES key in `Local State` encrypted with DPAPI (CurrentUser).
|
||||||
|
// That master key is then used for AES-256-GCM cookie values (`v10`/`v11`/`v20` prefixes).
|
||||||
|
const masterKey = await getWindowsChromiumMasterKey(userDataDir, 'Chrome');
|
||||||
|
if (!masterKey.ok) {
|
||||||
|
return { cookies: [], warnings: [masterKey.error] };
|
||||||
|
}
|
||||||
|
const decrypt = (encryptedValue, opts) => {
|
||||||
|
return decryptChromiumAes256GcmCookieValue(encryptedValue, masterKey.value, {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const dbOptions = {
|
||||||
|
dbPath,
|
||||||
|
};
|
||||||
|
if (options.profile)
|
||||||
|
dbOptions.profile = options.profile;
|
||||||
|
if (options.includeExpired !== undefined)
|
||||||
|
dbOptions.includeExpired = options.includeExpired;
|
||||||
|
if (options.debug !== undefined)
|
||||||
|
dbOptions.debug = options.debug;
|
||||||
|
return await getCookiesFromChromeSqliteDb(dbOptions, origins, allowlistNames, decrypt);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=chromeSqliteWindows.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chromeSqliteWindows.js","sourceRoot":"","sources":["../../src/providers/chromeSqliteWindows.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,mCAAmC,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACtD,OAAwE,EACxE,OAAiB,EACjB,cAAkC;IAElC,MAAM,WAAW,GAAsD;QACtE,sBAAsB,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,CAAC;KAClE,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,WAAW,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACzE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC7B,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,oCAAoC,CAAC,EAAE,CAAC;IAC1E,CAAC;IAED,4FAA4F;IAC5F,2FAA2F;IAC3F,MAAM,SAAS,GAAG,MAAM,2BAA2B,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC3E,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;IACrD,CAAC;IAED,MAAM,OAAO,GAAG,CACf,cAA0B,EAC1B,IAAkC,EAClB,EAAE;QAClB,OAAO,mCAAmC,CAAC,cAAc,EAAE,SAAS,CAAC,KAAK,EAAE;YAC3E,eAAe,EAAE,IAAI,CAAC,eAAe;SACrC,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,SAAS,GACd;QACC,MAAM;KACN,CAAC;IACH,IAAI,OAAO,CAAC,OAAO;QAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACzD,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS;QAAE,SAAS,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAC5F,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAEjE,OAAO,MAAM,4BAA4B,CAAC,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;AACxF,CAAC"}
|
||||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
export declare function resolveChromiumCookiesDbLinux(options: {
|
||||||
|
configDirName: string;
|
||||||
|
profile?: string;
|
||||||
|
}): string | null;
|
||||||
|
//# sourceMappingURL=linuxPaths.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"linuxPaths.d.ts","sourceRoot":"","sources":["../../../src/providers/chromium/linuxPaths.ts"],"names":[],"mappings":"AAMA,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACtD,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,GAAG,IAAI,CA0BhB"}
|
||||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { expandPath, looksLikePath } from './paths.js';
|
||||||
|
export function resolveChromiumCookiesDbLinux(options) {
|
||||||
|
const home = homedir();
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: process.env is an index signature under strict TS.
|
||||||
|
const configHome = process.env['XDG_CONFIG_HOME']?.trim() || path.join(home, '.config');
|
||||||
|
const root = path.join(configHome, options.configDirName);
|
||||||
|
if (options.profile && looksLikePath(options.profile)) {
|
||||||
|
const candidate = expandPath(options.profile);
|
||||||
|
if (candidate.endsWith('Cookies') && existsSync(candidate))
|
||||||
|
return candidate;
|
||||||
|
const direct = path.join(candidate, 'Cookies');
|
||||||
|
if (existsSync(direct))
|
||||||
|
return direct;
|
||||||
|
const network = path.join(candidate, 'Network', 'Cookies');
|
||||||
|
if (existsSync(network))
|
||||||
|
return network;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const profileDir = options.profile && options.profile.trim().length > 0 ? options.profile.trim() : 'Default';
|
||||||
|
const candidates = [
|
||||||
|
path.join(root, profileDir, 'Cookies'),
|
||||||
|
path.join(root, profileDir, 'Network', 'Cookies'),
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (existsSync(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=linuxPaths.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"linuxPaths.js","sourceRoot":"","sources":["../../../src/providers/chromium/linuxPaths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,UAAU,6BAA6B,CAAC,OAG7C;IACA,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,kGAAkG;IAClG,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACxF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAE1D,IAAI,OAAO,CAAC,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACvD,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;QAC7E,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC/C,IAAI,UAAU,CAAC,MAAM,CAAC;YAAE,OAAO,MAAM,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QAC3D,IAAI,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAC;QACxC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,UAAU,GACf,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3F,MAAM,UAAU,GAAG;QAClB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,CAAC;KACjD,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
|
||||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
export declare function readKeychainGenericPassword(options: {
|
||||||
|
account: string;
|
||||||
|
service: string;
|
||||||
|
timeoutMs: number;
|
||||||
|
}): Promise<{
|
||||||
|
ok: true;
|
||||||
|
password: string;
|
||||||
|
} | {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
export declare function readKeychainGenericPasswordFirst(options: {
|
||||||
|
account: string;
|
||||||
|
services: string[];
|
||||||
|
timeoutMs: number;
|
||||||
|
label: string;
|
||||||
|
}): Promise<{
|
||||||
|
ok: true;
|
||||||
|
password: string;
|
||||||
|
} | {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
//# sourceMappingURL=macosKeychain.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"macosKeychain.d.ts","sourceRoot":"","sources":["../../../src/providers/chromium/macosKeychain.ts"],"names":[],"mappings":"AAEA,wBAAsB,2BAA2B,CAAC,OAAO,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAczE;AAED,wBAAsB,gCAAgC,CAAC,OAAO,EAAE;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBzE"}
|
||||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import { execCapture } from '../../util/exec.js';
|
||||||
|
export async function readKeychainGenericPassword(options) {
|
||||||
|
const res = await execCapture('security', ['find-generic-password', '-w', '-a', options.account, '-s', options.service], { timeoutMs: options.timeoutMs });
|
||||||
|
if (res.code === 0) {
|
||||||
|
const password = res.stdout.trim();
|
||||||
|
return { ok: true, password };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `${res.stderr.trim() || `exit ${res.code}`}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export async function readKeychainGenericPasswordFirst(options) {
|
||||||
|
let lastError = null;
|
||||||
|
for (const service of options.services) {
|
||||||
|
const r = await readKeychainGenericPassword({
|
||||||
|
account: options.account,
|
||||||
|
service,
|
||||||
|
timeoutMs: options.timeoutMs,
|
||||||
|
});
|
||||||
|
if (r.ok)
|
||||||
|
return r;
|
||||||
|
lastError = r.error;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Failed to read macOS Keychain (${options.label}): ${lastError ?? 'permission denied / keychain locked / entry missing.'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=macosKeychain.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"macosKeychain.js","sourceRoot":"","sources":["../../../src/providers/chromium/macosKeychain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,OAIjD;IACA,MAAM,GAAG,GAAG,MAAM,WAAW,CAC5B,UAAU,EACV,CAAC,uBAAuB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,EAC7E,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAChC,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACnC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC/B,CAAC;IACD,OAAO;QACN,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,QAAQ,GAAG,CAAC,IAAI,EAAE,EAAE;KACnD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gCAAgC,CAAC,OAKtD;IACA,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,MAAM,2BAA2B,CAAC;YAC3C,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO;YACP,SAAS,EAAE,OAAO,CAAC,SAAS;SAC5B,CAAC,CAAC;QACH,IAAI,CAAC,CAAC,EAAE;YAAE,OAAO,CAAC,CAAC;QACnB,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,OAAO;QACN,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,kCAAkC,OAAO,CAAC,KAAK,MAAM,SAAS,IAAI,sDAAsD,EAAE;KACjI,CAAC;AACH,CAAC"}
|
||||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
export declare function looksLikePath(value: string): boolean;
|
||||||
|
export declare function expandPath(input: string): string;
|
||||||
|
export declare function safeStat(candidate: string): {
|
||||||
|
isFile: () => boolean;
|
||||||
|
isDirectory: () => boolean;
|
||||||
|
} | null;
|
||||||
|
export declare function resolveCookiesDbFromProfileOrRoots(options: {
|
||||||
|
profile?: string;
|
||||||
|
roots: string[];
|
||||||
|
}): string | null;
|
||||||
|
//# sourceMappingURL=paths.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/providers/chromium/paths.ts"],"names":[],"mappings":"AAIA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhD;AAED,wBAAgB,QAAQ,CACvB,SAAS,EAAE,MAAM,GACf;IAAE,MAAM,EAAE,MAAM,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,OAAO,CAAA;CAAE,GAAG,IAAI,CAM9D;AAED,wBAAgB,kCAAkC,CAAC,OAAO,EAAE;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB,GAAG,MAAM,GAAG,IAAI,CAuBhB"}
|
||||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { existsSync, statSync } from 'node:fs';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
export function looksLikePath(value) {
|
||||||
|
return value.includes('/') || value.includes('\\');
|
||||||
|
}
|
||||||
|
export function expandPath(input) {
|
||||||
|
if (input.startsWith('~/'))
|
||||||
|
return path.join(homedir(), input.slice(2));
|
||||||
|
return path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
|
||||||
|
}
|
||||||
|
export function safeStat(candidate) {
|
||||||
|
try {
|
||||||
|
return statSync(candidate);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function resolveCookiesDbFromProfileOrRoots(options) {
|
||||||
|
const candidates = [];
|
||||||
|
if (options.profile && looksLikePath(options.profile)) {
|
||||||
|
const expanded = expandPath(options.profile);
|
||||||
|
const stat = safeStat(expanded);
|
||||||
|
if (stat?.isFile())
|
||||||
|
return expanded;
|
||||||
|
candidates.push(path.join(expanded, 'Cookies'));
|
||||||
|
candidates.push(path.join(expanded, 'Network', 'Cookies'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const profileDir = options.profile && options.profile.trim().length > 0 ? options.profile.trim() : 'Default';
|
||||||
|
for (const root of options.roots) {
|
||||||
|
candidates.push(path.join(root, profileDir, 'Cookies'));
|
||||||
|
candidates.push(path.join(root, profileDir, 'Network', 'Cookies'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (existsSync(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=paths.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/providers/chromium/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,UAAU,aAAa,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,QAAQ,CACvB,SAAiB;IAEjB,IAAI,CAAC;QACJ,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,OAGlD;IACA,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,IAAI,OAAO,CAAC,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChC,IAAI,IAAI,EAAE,MAAM,EAAE;YAAE,OAAO,QAAQ,CAAC;QACpC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;QAChD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;IAC5D,CAAC;SAAM,CAAC;QACP,MAAM,UAAU,GACf,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3F,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;YACxD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;QACpE,CAAC;IACF,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC7C,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC"}
|
||||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
export declare function getWindowsChromiumMasterKey(userDataDir: string, label: string): Promise<{
|
||||||
|
ok: true;
|
||||||
|
value: Buffer;
|
||||||
|
} | {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
//# sourceMappingURL=windowsMasterKey.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"windowsMasterKey.d.ts","sourceRoot":"","sources":["../../../src/providers/chromium/windowsMasterKey.ts"],"names":[],"mappings":"AAKA,wBAAsB,2BAA2B,CAChD,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACX,OAAO,CACP;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3B;IACA,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACb,CACH,CAsCA"}
|
||||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { dpapiUnprotect } from '../chromeSqlite/windowsDpapi.js';
|
||||||
|
export async function getWindowsChromiumMasterKey(userDataDir, label) {
|
||||||
|
const localStatePath = path.join(userDataDir, 'Local State');
|
||||||
|
if (!existsSync(localStatePath)) {
|
||||||
|
return { ok: false, error: `${label} Local State file not found.` };
|
||||||
|
}
|
||||||
|
let encryptedKeyB64 = null;
|
||||||
|
try {
|
||||||
|
const raw = readFileSync(localStatePath, 'utf8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
encryptedKeyB64 =
|
||||||
|
typeof parsed.os_crypt?.encrypted_key === 'string' ? parsed.os_crypt.encrypted_key : null;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Failed to parse ${label} Local State: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!encryptedKeyB64)
|
||||||
|
return { ok: false, error: `${label} Local State missing os_crypt.encrypted_key.` };
|
||||||
|
let encryptedKey;
|
||||||
|
try {
|
||||||
|
encryptedKey = Buffer.from(encryptedKeyB64, 'base64');
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return { ok: false, error: `${label} Local State contains an invalid encrypted_key.` };
|
||||||
|
}
|
||||||
|
const prefix = Buffer.from('DPAPI', 'utf8');
|
||||||
|
if (!encryptedKey.subarray(0, prefix.length).equals(prefix)) {
|
||||||
|
return { ok: false, error: `${label} encrypted_key does not start with DPAPI prefix.` };
|
||||||
|
}
|
||||||
|
const unprotected = await dpapiUnprotect(encryptedKey.subarray(prefix.length));
|
||||||
|
if (!unprotected.ok) {
|
||||||
|
return { ok: false, error: `DPAPI decrypt failed: ${unprotected.error}` };
|
||||||
|
}
|
||||||
|
return { ok: true, value: unprotected.value };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=windowsMasterKey.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"windowsMasterKey.js","sourceRoot":"","sources":["../../../src/providers/chromium/windowsMasterKey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAChD,WAAmB,EACnB,KAAa;IAQb,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC7D,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,8BAA8B,EAAE,CAAC;IACrE,CAAC;IACD,IAAI,eAAe,GAAkB,IAAI,CAAC;IAC1C,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA+C,CAAC;QAC7E,eAAe;YACd,OAAO,MAAM,CAAC,QAAQ,EAAE,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5F,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO;YACN,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,mBAAmB,KAAK,iBAAiB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;SACxG,CAAC;IACH,CAAC;IAED,IAAI,CAAC,eAAe;QACnB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,8CAA8C,EAAE,CAAC;IAErF,IAAI,YAAoB,CAAC;IACzB,IAAI,CAAC;QACJ,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,iDAAiD,EAAE,CAAC;IACxF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,kDAAkD,EAAE,CAAC;IACzF,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/E,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC;QACrB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,WAAW,CAAC,KAAK,EAAE,EAAE,CAAC;IAC3E,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,EAAE,CAAC;AAC/C,CAAC"}
|
||||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
export declare function resolveChromiumPathsWindows(options: {
|
||||||
|
localAppDataVendorPath: string;
|
||||||
|
profile?: string;
|
||||||
|
}): {
|
||||||
|
dbPath: string | null;
|
||||||
|
userDataDir: string | null;
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=windowsPaths.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"windowsPaths.d.ts","sourceRoot":"","sources":["../../../src/providers/chromium/windowsPaths.ts"],"names":[],"mappings":"AAKA,wBAAgB,2BAA2B,CAAC,OAAO,EAAE;IACpD,sBAAsB,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAmCxD"}
|
||||||
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { expandPath, looksLikePath } from './paths.js';
|
||||||
|
export function resolveChromiumPathsWindows(options) {
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: process.env is an index signature under strict TS.
|
||||||
|
const localAppData = process.env['LOCALAPPDATA'];
|
||||||
|
const root = localAppData ? path.join(localAppData, options.localAppDataVendorPath) : null;
|
||||||
|
if (options.profile && looksLikePath(options.profile)) {
|
||||||
|
const expanded = expandPath(options.profile);
|
||||||
|
const candidates = expanded.endsWith('Cookies')
|
||||||
|
? [expanded]
|
||||||
|
: [
|
||||||
|
path.join(expanded, 'Network', 'Cookies'),
|
||||||
|
path.join(expanded, 'Cookies'),
|
||||||
|
path.join(expanded, 'Default', 'Network', 'Cookies'),
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!existsSync(candidate))
|
||||||
|
continue;
|
||||||
|
const userDataDir = findUserDataDir(candidate);
|
||||||
|
return { dbPath: candidate, userDataDir };
|
||||||
|
}
|
||||||
|
if (existsSync(path.join(expanded, 'Local State'))) {
|
||||||
|
return { dbPath: null, userDataDir: expanded };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const profileDir = options.profile && options.profile.trim().length > 0 ? options.profile.trim() : 'Default';
|
||||||
|
if (!root)
|
||||||
|
return { dbPath: null, userDataDir: null };
|
||||||
|
const candidates = [
|
||||||
|
path.join(root, profileDir, 'Network', 'Cookies'),
|
||||||
|
path.join(root, profileDir, 'Cookies'),
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (existsSync(candidate))
|
||||||
|
return { dbPath: candidate, userDataDir: root };
|
||||||
|
}
|
||||||
|
return { dbPath: null, userDataDir: root };
|
||||||
|
}
|
||||||
|
function findUserDataDir(cookiesDbPath) {
|
||||||
|
let current = path.dirname(cookiesDbPath);
|
||||||
|
for (let i = 0; i < 6; i += 1) {
|
||||||
|
const localState = path.join(current, 'Local State');
|
||||||
|
if (existsSync(localState))
|
||||||
|
return current;
|
||||||
|
const next = path.dirname(current);
|
||||||
|
if (next === current)
|
||||||
|
break;
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=windowsPaths.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"windowsPaths.js","sourceRoot":"","sources":["../../../src/providers/chromium/windowsPaths.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromEdge(options: {
|
||||||
|
profile?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=edge.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edge.d.ts","sourceRoot":"","sources":["../../src/providers/edge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAK5D,wBAAsB,kBAAkB,CACvC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EAC5F,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CA0B3B"}
|
||||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { getCookiesFromEdgeSqliteLinux } from './edgeSqliteLinux.js';
|
||||||
|
import { getCookiesFromEdgeSqliteMac } from './edgeSqliteMac.js';
|
||||||
|
import { getCookiesFromEdgeSqliteWindows } from './edgeSqliteWindows.js';
|
||||||
|
export async function getCookiesFromEdge(options, origins, allowlistNames) {
|
||||||
|
const warnings = [];
|
||||||
|
// Platform dispatch only. All real logic lives in the per-OS providers.
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const r = await getCookiesFromEdgeSqliteMac(options, origins, allowlistNames);
|
||||||
|
warnings.push(...r.warnings);
|
||||||
|
const cookies = r.cookies;
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
const r = await getCookiesFromEdgeSqliteLinux(options, origins, allowlistNames);
|
||||||
|
warnings.push(...r.warnings);
|
||||||
|
const cookies = r.cookies;
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const r = await getCookiesFromEdgeSqliteWindows(options, origins, allowlistNames);
|
||||||
|
warnings.push(...r.warnings);
|
||||||
|
const cookies = r.cookies;
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=edge.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edge.js","sourceRoot":"","sources":["../../src/providers/edge.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,OAA4F,EAC5F,OAAiB,EACjB,cAAkC;IAElC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,wEAAwE;IACxE,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,MAAM,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAa,CAAC,CAAC,OAAO,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,MAAM,6BAA6B,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QAChF,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAa,CAAC,CAAC,OAAO,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,MAAM,+BAA+B,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QAClF,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAa,CAAC,CAAC,OAAO,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;AAClC,CAAC"}
|
||||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromEdgeSqliteLinux(options: {
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=edgeSqliteLinux.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edgeSqliteLinux.d.ts","sourceRoot":"","sources":["../../src/providers/edgeSqliteLinux.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AASpD,wBAAsB,6BAA6B,CAClD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EACxE,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAoD3B"}
|
||||||
scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.js
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
|||||||
|
import { decryptChromiumAes128CbcCookieValue, deriveAes128CbcKeyFromPassword, } from './chromeSqlite/crypto.js';
|
||||||
|
import { getLinuxChromiumSafeStoragePassword } from './chromeSqlite/linuxKeyring.js';
|
||||||
|
import { getCookiesFromChromeSqliteDb } from './chromeSqlite/shared.js';
|
||||||
|
import { resolveChromiumCookiesDbLinux } from './chromium/linuxPaths.js';
|
||||||
|
export async function getCookiesFromEdgeSqliteLinux(options, origins, allowlistNames) {
|
||||||
|
const args = {
|
||||||
|
configDirName: 'microsoft-edge',
|
||||||
|
};
|
||||||
|
if (options.profile !== undefined)
|
||||||
|
args.profile = options.profile;
|
||||||
|
const dbPath = resolveChromiumCookiesDbLinux(args);
|
||||||
|
if (!dbPath) {
|
||||||
|
return { cookies: [], warnings: ['Edge cookies database not found.'] };
|
||||||
|
}
|
||||||
|
const { password, warnings: keyringWarnings } = await getLinuxChromiumSafeStoragePassword({
|
||||||
|
app: 'edge',
|
||||||
|
});
|
||||||
|
// Linux uses multiple schemes depending on distro/keyring availability.
|
||||||
|
// - v10 often uses the hard-coded "peanuts" password
|
||||||
|
// - v11 uses "<browser> Safe Storage" from the keyring (may be empty/unavailable)
|
||||||
|
const v10Key = deriveAes128CbcKeyFromPassword('peanuts', { iterations: 1 });
|
||||||
|
const emptyKey = deriveAes128CbcKeyFromPassword('', { iterations: 1 });
|
||||||
|
const v11Key = deriveAes128CbcKeyFromPassword(password, { iterations: 1 });
|
||||||
|
const decrypt = (encryptedValue, opts) => {
|
||||||
|
const prefix = Buffer.from(encryptedValue).subarray(0, 3).toString('utf8');
|
||||||
|
if (prefix === 'v10') {
|
||||||
|
return decryptChromiumAes128CbcCookieValue(encryptedValue, [v10Key, emptyKey], {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
treatUnknownPrefixAsPlaintext: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (prefix === 'v11') {
|
||||||
|
return decryptChromiumAes128CbcCookieValue(encryptedValue, [v11Key, emptyKey], {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
treatUnknownPrefixAsPlaintext: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const dbOptions = {
|
||||||
|
dbPath,
|
||||||
|
};
|
||||||
|
if (options.profile)
|
||||||
|
dbOptions.profile = options.profile;
|
||||||
|
if (options.includeExpired !== undefined)
|
||||||
|
dbOptions.includeExpired = options.includeExpired;
|
||||||
|
if (options.debug !== undefined)
|
||||||
|
dbOptions.debug = options.debug;
|
||||||
|
const result = await getCookiesFromChromeSqliteDb(dbOptions, origins, allowlistNames, decrypt);
|
||||||
|
result.warnings.unshift(...keyringWarnings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=edgeSqliteLinux.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edgeSqliteLinux.js","sourceRoot":"","sources":["../../src/providers/edgeSqliteLinux.ts"],"names":[],"mappings":"AACA,OAAO,EACN,mCAAmC,EACnC,8BAA8B,GAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,mCAAmC,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAClD,OAAwE,EACxE,OAAiB,EACjB,cAAkC;IAElC,MAAM,IAAI,GAAwD;QACjE,aAAa,EAAE,gBAAgB;KAC/B,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAClE,MAAM,MAAM,GAAG,6BAA6B,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAC;IACxE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,mCAAmC,CAAC;QACzF,GAAG,EAAE,MAAM;KACX,CAAC,CAAC;IAEH,wEAAwE;IACxE,qDAAqD;IACrD,kFAAkF;IAClF,MAAM,MAAM,GAAG,8BAA8B,CAAC,SAAS,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5E,MAAM,QAAQ,GAAG,8BAA8B,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IACvE,MAAM,MAAM,GAAG,8BAA8B,CAAC,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IAE3E,MAAM,OAAO,GAAG,CACf,cAA0B,EAC1B,IAAkC,EAClB,EAAE;QAClB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC3E,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACtB,OAAO,mCAAmC,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;gBAC9E,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,6BAA6B,EAAE,KAAK;aACpC,CAAC,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACtB,OAAO,mCAAmC,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;gBAC9E,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,6BAA6B,EAAE,KAAK;aACpC,CAAC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;IAEF,MAAM,SAAS,GACd;QACC,MAAM;KACN,CAAC;IACH,IAAI,OAAO,CAAC,OAAO;QAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACzD,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS;QAAE,SAAS,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAC5F,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAEjE,MAAM,MAAM,GAAG,MAAM,4BAA4B,CAAC,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;IAC/F,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,eAAe,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC;AACf,CAAC"}
|
||||||
scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.d.ts
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromEdgeSqliteMac(options: {
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
timeoutMs?: number;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=edgeSqliteMac.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edgeSqliteMac.d.ts","sourceRoot":"","sources":["../../src/providers/edgeSqliteMac.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AASpD,wBAAsB,2BAA2B,CAChD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5F,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CA8C3B"}
|
||||||
Generated
Vendored
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import { homedir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { decryptChromiumAes128CbcCookieValue, deriveAes128CbcKeyFromPassword, } from './chromeSqlite/crypto.js';
|
||||||
|
import { getCookiesFromChromeSqliteDb } from './chromeSqlite/shared.js';
|
||||||
|
import { readKeychainGenericPasswordFirst } from './chromium/macosKeychain.js';
|
||||||
|
import { resolveCookiesDbFromProfileOrRoots } from './chromium/paths.js';
|
||||||
|
export async function getCookiesFromEdgeSqliteMac(options, origins, allowlistNames) {
|
||||||
|
const dbPath = resolveEdgeCookiesDb(options.profile);
|
||||||
|
if (!dbPath) {
|
||||||
|
return { cookies: [], warnings: ['Edge cookies database not found.'] };
|
||||||
|
}
|
||||||
|
const warnings = [];
|
||||||
|
// On macOS, Edge stores its "Safe Storage" secret in Keychain (same scheme as Chrome).
|
||||||
|
// `security find-generic-password` is stable and avoids any native Node keychain modules.
|
||||||
|
const passwordResult = await readKeychainGenericPasswordFirst({
|
||||||
|
account: 'Microsoft Edge',
|
||||||
|
services: ['Microsoft Edge Safe Storage', 'Microsoft Edge'],
|
||||||
|
timeoutMs: options.timeoutMs ?? 3_000,
|
||||||
|
label: 'Microsoft Edge Safe Storage',
|
||||||
|
});
|
||||||
|
if (!passwordResult.ok) {
|
||||||
|
warnings.push(passwordResult.error);
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const edgePassword = passwordResult.password.trim();
|
||||||
|
if (!edgePassword) {
|
||||||
|
warnings.push('macOS Keychain returned an empty Microsoft Edge Safe Storage password.');
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
// Chromium uses PBKDF2(password, "saltysalt", 1003, 16, sha1) for AES-128-CBC cookie values on macOS.
|
||||||
|
const key = deriveAes128CbcKeyFromPassword(edgePassword, { iterations: 1003 });
|
||||||
|
const decrypt = (encryptedValue, opts) => decryptChromiumAes128CbcCookieValue(encryptedValue, [key], {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
treatUnknownPrefixAsPlaintext: true,
|
||||||
|
});
|
||||||
|
const dbOptions = {
|
||||||
|
dbPath,
|
||||||
|
};
|
||||||
|
if (options.profile)
|
||||||
|
dbOptions.profile = options.profile;
|
||||||
|
if (options.includeExpired !== undefined)
|
||||||
|
dbOptions.includeExpired = options.includeExpired;
|
||||||
|
if (options.debug !== undefined)
|
||||||
|
dbOptions.debug = options.debug;
|
||||||
|
const result = await getCookiesFromChromeSqliteDb(dbOptions, origins, allowlistNames, decrypt);
|
||||||
|
result.warnings.unshift(...warnings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function resolveEdgeCookiesDb(profile) {
|
||||||
|
const home = homedir();
|
||||||
|
/* c8 ignore next */
|
||||||
|
const roots = process.platform === 'darwin'
|
||||||
|
? [path.join(home, 'Library', 'Application Support', 'Microsoft Edge')]
|
||||||
|
: [];
|
||||||
|
const args = { roots };
|
||||||
|
if (profile !== undefined)
|
||||||
|
args.profile = profile;
|
||||||
|
return resolveCookiesDbFromProfileOrRoots(args);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=edgeSqliteMac.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edgeSqliteMac.js","sourceRoot":"","sources":["../../src/providers/edgeSqliteMac.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromEdgeSqliteWindows(options: {
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=edgeSqliteWindows.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edgeSqliteWindows.d.ts","sourceRoot":"","sources":["../../src/providers/edgeSqliteWindows.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAMpD,wBAAsB,+BAA+B,CACpD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EACxE,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAmC3B"}
|
||||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { decryptChromiumAes256GcmCookieValue } from './chromeSqlite/crypto.js';
|
||||||
|
import { getCookiesFromChromeSqliteDb } from './chromeSqlite/shared.js';
|
||||||
|
import { getWindowsChromiumMasterKey } from './chromium/windowsMasterKey.js';
|
||||||
|
import { resolveChromiumPathsWindows } from './chromium/windowsPaths.js';
|
||||||
|
export async function getCookiesFromEdgeSqliteWindows(options, origins, allowlistNames) {
|
||||||
|
const resolveArgs = {
|
||||||
|
localAppDataVendorPath: path.join('Microsoft', 'Edge', 'User Data'),
|
||||||
|
};
|
||||||
|
if (options.profile !== undefined)
|
||||||
|
resolveArgs.profile = options.profile;
|
||||||
|
const { dbPath, userDataDir } = resolveChromiumPathsWindows(resolveArgs);
|
||||||
|
if (!dbPath || !userDataDir) {
|
||||||
|
return { cookies: [], warnings: ['Edge cookies database not found.'] };
|
||||||
|
}
|
||||||
|
// On Windows, Edge stores an AES key in `Local State` encrypted with DPAPI (CurrentUser).
|
||||||
|
// That master key is then used for AES-256-GCM cookie values (`v10`/`v11`/`v20` prefixes).
|
||||||
|
const masterKey = await getWindowsChromiumMasterKey(userDataDir, 'Edge');
|
||||||
|
if (!masterKey.ok) {
|
||||||
|
return { cookies: [], warnings: [masterKey.error] };
|
||||||
|
}
|
||||||
|
const decrypt = (encryptedValue, opts) => {
|
||||||
|
return decryptChromiumAes256GcmCookieValue(encryptedValue, masterKey.value, {
|
||||||
|
stripHashPrefix: opts.stripHashPrefix,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const dbOptions = {
|
||||||
|
dbPath,
|
||||||
|
};
|
||||||
|
if (options.profile)
|
||||||
|
dbOptions.profile = options.profile;
|
||||||
|
if (options.includeExpired !== undefined)
|
||||||
|
dbOptions.includeExpired = options.includeExpired;
|
||||||
|
if (options.debug !== undefined)
|
||||||
|
dbOptions.debug = options.debug;
|
||||||
|
return await getCookiesFromChromeSqliteDb(dbOptions, origins, allowlistNames, decrypt);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=edgeSqliteWindows.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"edgeSqliteWindows.js","sourceRoot":"","sources":["../../src/providers/edgeSqliteWindows.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,mCAAmC,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACpD,OAAwE,EACxE,OAAiB,EACjB,cAAkC;IAElC,MAAM,WAAW,GAAsD;QACtE,sBAAsB,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC;KACnE,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,WAAW,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACzE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC7B,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAC;IACxE,CAAC;IAED,0FAA0F;IAC1F,2FAA2F;IAC3F,MAAM,SAAS,GAAG,MAAM,2BAA2B,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACzE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;IACrD,CAAC;IAED,MAAM,OAAO,GAAG,CACf,cAA0B,EAC1B,IAAkC,EAClB,EAAE;QAClB,OAAO,mCAAmC,CAAC,cAAc,EAAE,SAAS,CAAC,KAAK,EAAE;YAC3E,eAAe,EAAE,IAAI,CAAC,eAAe;SACrC,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,SAAS,GACd;QACC,MAAM;KACN,CAAC;IACH,IAAI,OAAO,CAAC,OAAO;QAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACzD,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS;QAAE,SAAS,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAC5F,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAEjE,OAAO,MAAM,4BAA4B,CAAC,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;AACxF,CAAC"}
|
||||||
scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.d.ts
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
export declare function getCookiesFromFirefox(options: {
|
||||||
|
profile?: string;
|
||||||
|
includeExpired?: boolean;
|
||||||
|
}, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
//# sourceMappingURL=firefoxSqlite.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"firefoxSqlite.d.ts","sourceRoot":"","sources":["../../src/providers/firefoxSqlite.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAA0B,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAK5E,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,EACvD,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAmD3B"}
|
||||||
Generated
Vendored
+257
@@ -0,0 +1,257 @@
|
|||||||
|
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
||||||
|
import { homedir, tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { hostMatchesCookieDomain } from '../util/hostMatch.js';
|
||||||
|
import { importNodeSqlite } from '../util/nodeSqlite.js';
|
||||||
|
import { isBunRuntime } from '../util/runtime.js';
|
||||||
|
export async function getCookiesFromFirefox(options, origins, allowlistNames) {
|
||||||
|
const warnings = [];
|
||||||
|
const dbPath = resolveFirefoxCookiesDb(options.profile);
|
||||||
|
if (!dbPath) {
|
||||||
|
warnings.push('Firefox cookies database not found.');
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const tempDir = mkdtempSync(path.join(tmpdir(), 'sweet-cookie-firefox-'));
|
||||||
|
const tempDbPath = path.join(tempDir, 'cookies.sqlite');
|
||||||
|
try {
|
||||||
|
copyFileSync(dbPath, tempDbPath);
|
||||||
|
copySidecar(dbPath, `${tempDbPath}-wal`, '-wal');
|
||||||
|
copySidecar(dbPath, `${tempDbPath}-shm`, '-shm');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
warnings.push(`Failed to copy Firefox cookie DB: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const hosts = origins.map((o) => new URL(o).hostname);
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const where = buildHostWhereClause(hosts);
|
||||||
|
const expiryClause = options.includeExpired ? '' : ` AND (expiry = 0 OR expiry > ${now})`;
|
||||||
|
const sql = `SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite ` +
|
||||||
|
`FROM moz_cookies WHERE (${where})${expiryClause} ORDER BY expiry DESC;`;
|
||||||
|
try {
|
||||||
|
if (isBunRuntime()) {
|
||||||
|
const bunResult = await queryFirefoxCookiesWithBunSqlite(tempDbPath, sql);
|
||||||
|
if (!bunResult.ok) {
|
||||||
|
warnings.push(`bun:sqlite failed reading Firefox cookies: ${bunResult.error}`);
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const cookies = collectFirefoxCookiesFromRows(bunResult.rows, options, hosts, allowlistNames);
|
||||||
|
return { cookies: dedupeCookies(cookies), warnings };
|
||||||
|
}
|
||||||
|
const nodeResult = await queryFirefoxCookiesWithNodeSqlite(tempDbPath, sql);
|
||||||
|
if (!nodeResult.ok) {
|
||||||
|
warnings.push(`node:sqlite failed reading Firefox cookies: ${nodeResult.error}`);
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const cookies = collectFirefoxCookiesFromRows(nodeResult.rows, options, hosts, allowlistNames);
|
||||||
|
return { cookies: dedupeCookies(cookies), warnings };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function queryFirefoxCookiesWithNodeSqlite(dbPath, sql) {
|
||||||
|
try {
|
||||||
|
const { DatabaseSync } = await importNodeSqlite();
|
||||||
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
||||||
|
try {
|
||||||
|
const rows = db.prepare(sql).all();
|
||||||
|
return { ok: true, rows };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function queryFirefoxCookiesWithBunSqlite(dbPath, sql) {
|
||||||
|
try {
|
||||||
|
const { Database } = await import('bun:sqlite');
|
||||||
|
const db = new Database(dbPath, { readonly: true });
|
||||||
|
try {
|
||||||
|
const rows = db.query(sql).all();
|
||||||
|
return { ok: true, rows };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function collectFirefoxCookiesFromRows(rows, options, hosts, allowlistNames) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const cookies = [];
|
||||||
|
for (const row of rows) {
|
||||||
|
const name = typeof row.name === 'string' ? row.name : null;
|
||||||
|
const value = typeof row.value === 'string' ? row.value : null;
|
||||||
|
const host = typeof row.host === 'string' ? row.host : null;
|
||||||
|
const cookiePath = typeof row.path === 'string' ? row.path : '';
|
||||||
|
if (!name || value === null || !host)
|
||||||
|
continue;
|
||||||
|
if (allowlistNames && allowlistNames.size > 0 && !allowlistNames.has(name))
|
||||||
|
continue;
|
||||||
|
if (!hostMatchesAny(hosts, host))
|
||||||
|
continue;
|
||||||
|
const expiryText = typeof row.expiry === 'number'
|
||||||
|
? String(row.expiry)
|
||||||
|
: typeof row.expiry === 'string'
|
||||||
|
? row.expiry
|
||||||
|
: undefined;
|
||||||
|
const expires = normalizeFirefoxExpiry(expiryText);
|
||||||
|
if (!options.includeExpired && expires && expires < now)
|
||||||
|
continue;
|
||||||
|
const isSecure = row.isSecure === 1 || row.isSecure === '1' || row.isSecure === true;
|
||||||
|
const isHttpOnly = row.isHttpOnly === 1 || row.isHttpOnly === '1' || row.isHttpOnly === true;
|
||||||
|
const cookie = {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
domain: host.startsWith('.') ? host.slice(1) : host,
|
||||||
|
path: cookiePath || '/',
|
||||||
|
secure: isSecure,
|
||||||
|
httpOnly: isHttpOnly,
|
||||||
|
};
|
||||||
|
if (expires !== undefined)
|
||||||
|
cookie.expires = expires;
|
||||||
|
const normalizedSameSite = normalizeFirefoxSameSite(typeof row.sameSite === 'number'
|
||||||
|
? String(row.sameSite)
|
||||||
|
: typeof row.sameSite === 'string'
|
||||||
|
? row.sameSite
|
||||||
|
: undefined);
|
||||||
|
if (normalizedSameSite !== undefined)
|
||||||
|
cookie.sameSite = normalizedSameSite;
|
||||||
|
const source = { browser: 'firefox' };
|
||||||
|
if (options.profile)
|
||||||
|
source.profile = options.profile;
|
||||||
|
cookie.source = source;
|
||||||
|
cookies.push(cookie);
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
function resolveFirefoxCookiesDb(profile) {
|
||||||
|
const home = homedir();
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: process.env is an index signature under strict TS.
|
||||||
|
const appData = process.env['APPDATA'];
|
||||||
|
/* c8 ignore next 10 */
|
||||||
|
const roots = process.platform === 'darwin'
|
||||||
|
? [path.join(home, 'Library', 'Application Support', 'Firefox', 'Profiles')]
|
||||||
|
: process.platform === 'linux'
|
||||||
|
? [path.join(home, '.mozilla', 'firefox')]
|
||||||
|
: process.platform === 'win32'
|
||||||
|
? appData
|
||||||
|
? [path.join(appData, 'Mozilla', 'Firefox', 'Profiles')]
|
||||||
|
: []
|
||||||
|
: [];
|
||||||
|
if (profile && looksLikePath(profile)) {
|
||||||
|
const candidate = profile.endsWith('cookies.sqlite')
|
||||||
|
? profile
|
||||||
|
: path.join(profile, 'cookies.sqlite');
|
||||||
|
return existsSync(candidate) ? candidate : null;
|
||||||
|
}
|
||||||
|
for (const root of roots) {
|
||||||
|
if (!root || !existsSync(root))
|
||||||
|
continue;
|
||||||
|
if (profile) {
|
||||||
|
const candidate = path.join(root, profile, 'cookies.sqlite');
|
||||||
|
if (existsSync(candidate))
|
||||||
|
return candidate;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entries = safeReaddir(root);
|
||||||
|
const defaultRelease = entries.find((e) => e.includes('default-release'));
|
||||||
|
const picked = defaultRelease ?? entries[0];
|
||||||
|
if (!picked)
|
||||||
|
continue;
|
||||||
|
const candidate = path.join(root, picked, 'cookies.sqlite');
|
||||||
|
if (existsSync(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function safeReaddir(dir) {
|
||||||
|
try {
|
||||||
|
return readdirSync(dir, { withFileTypes: true })
|
||||||
|
.filter((e) => e.isDirectory())
|
||||||
|
.map((e) => e.name);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function looksLikePath(value) {
|
||||||
|
return value.includes('/') || value.includes('\\');
|
||||||
|
}
|
||||||
|
function copySidecar(sourceDbPath, target, suffix) {
|
||||||
|
const sidecar = `${sourceDbPath}${suffix}`;
|
||||||
|
if (!existsSync(sidecar))
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
copyFileSync(sidecar, target);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildHostWhereClause(hosts) {
|
||||||
|
const clauses = [];
|
||||||
|
for (const host of hosts) {
|
||||||
|
const escaped = sqlLiteral(host);
|
||||||
|
const escapedDot = sqlLiteral(`.${host}`);
|
||||||
|
const escapedLike = sqlLiteral(`%.${host}`);
|
||||||
|
clauses.push(`host = ${escaped}`);
|
||||||
|
clauses.push(`host = ${escapedDot}`);
|
||||||
|
clauses.push(`host LIKE ${escapedLike}`);
|
||||||
|
}
|
||||||
|
return clauses.length ? clauses.join(' OR ') : '1=0';
|
||||||
|
}
|
||||||
|
function sqlLiteral(value) {
|
||||||
|
const escaped = value.replaceAll("'", "''");
|
||||||
|
return `'${escaped}'`;
|
||||||
|
}
|
||||||
|
function normalizeFirefoxExpiry(expiry) {
|
||||||
|
if (!expiry)
|
||||||
|
return undefined;
|
||||||
|
const value = Number.parseInt(expiry, 10);
|
||||||
|
if (!Number.isFinite(value) || value <= 0)
|
||||||
|
return undefined;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function normalizeFirefoxSameSite(raw) {
|
||||||
|
if (!raw)
|
||||||
|
return undefined;
|
||||||
|
const value = Number.parseInt(raw, 10);
|
||||||
|
if (Number.isFinite(value)) {
|
||||||
|
if (value === 2)
|
||||||
|
return 'Strict';
|
||||||
|
if (value === 1)
|
||||||
|
return 'Lax';
|
||||||
|
if (value === 0)
|
||||||
|
return 'None';
|
||||||
|
}
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
if (normalized === 'strict')
|
||||||
|
return 'Strict';
|
||||||
|
if (normalized === 'lax')
|
||||||
|
return 'Lax';
|
||||||
|
if (normalized === 'none')
|
||||||
|
return 'None';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function hostMatchesAny(hosts, cookieHost) {
|
||||||
|
const cookieDomain = cookieHost.startsWith('.') ? cookieHost.slice(1) : cookieHost;
|
||||||
|
return hosts.some((host) => hostMatchesCookieDomain(host, cookieDomain));
|
||||||
|
}
|
||||||
|
function dedupeCookies(cookies) {
|
||||||
|
const merged = new Map();
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
const key = `${cookie.name}|${cookie.domain ?? ''}|${cookie.path ?? ''}`;
|
||||||
|
if (!merged.has(key))
|
||||||
|
merged.set(key, cookie);
|
||||||
|
}
|
||||||
|
return Array.from(merged.values());
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=firefoxSqlite.js.map
|
||||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { GetCookiesResult } from '../types.js';
|
||||||
|
type InlineSource = {
|
||||||
|
source: string;
|
||||||
|
payload: string;
|
||||||
|
};
|
||||||
|
export declare function getCookiesFromInline(inline: InlineSource, origins: string[], allowlistNames: Set<string> | null): Promise<GetCookiesResult>;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=inline.d.ts.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"inline.d.ts","sourceRoot":"","sources":["../../src/providers/inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAK5D,KAAK,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,wBAAsB,oBAAoB,CACzC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,EAAE,EACjB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAkC3B"}
|
||||||
Generated
Vendored
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { tryDecodeBase64Json } from '../util/base64.js';
|
||||||
|
import { readTextFileIfExists } from '../util/fs.js';
|
||||||
|
import { hostMatchesCookieDomain } from '../util/hostMatch.js';
|
||||||
|
export async function getCookiesFromInline(inline, origins, allowlistNames) {
|
||||||
|
const warnings = [];
|
||||||
|
// Inline sources can be:
|
||||||
|
// - the payload itself (JSON or base64)
|
||||||
|
// - a file path that contains JSON/base64
|
||||||
|
//
|
||||||
|
// We do a small heuristic: treat `*.json`/`*.base64` and explicit "file" sources as file paths first.
|
||||||
|
const rawPayload = inline.source.endsWith('file') ||
|
||||||
|
inline.payload.endsWith('.json') ||
|
||||||
|
inline.payload.endsWith('.base64')
|
||||||
|
? ((await readTextFileIfExists(inline.payload)) ?? inline.payload)
|
||||||
|
: inline.payload;
|
||||||
|
// If it looks like base64, decode it to JSON. Otherwise use it as-is.
|
||||||
|
const decoded = tryDecodeBase64Json(rawPayload) ?? rawPayload;
|
||||||
|
const parsed = tryParseCookiePayload(decoded);
|
||||||
|
if (!parsed) {
|
||||||
|
return { cookies: [], warnings };
|
||||||
|
}
|
||||||
|
const hostAllow = new Set(origins.map((o) => new URL(o).hostname));
|
||||||
|
const cookies = [];
|
||||||
|
for (const cookie of parsed.cookies) {
|
||||||
|
if (!cookie?.name)
|
||||||
|
continue;
|
||||||
|
if (allowlistNames && allowlistNames.size > 0 && !allowlistNames.has(cookie.name))
|
||||||
|
continue;
|
||||||
|
const domain = cookie.domain ?? (cookie.url ? safeHostnameFromUrl(cookie.url) : undefined);
|
||||||
|
if (domain && hostAllow.size > 0 && !matchesAnyHost(hostAllow, domain))
|
||||||
|
continue;
|
||||||
|
cookies.push(cookie);
|
||||||
|
}
|
||||||
|
return { cookies, warnings };
|
||||||
|
}
|
||||||
|
function tryParseCookiePayload(input) {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed)
|
||||||
|
return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return { cookies: parsed };
|
||||||
|
}
|
||||||
|
if (parsed &&
|
||||||
|
typeof parsed === 'object' &&
|
||||||
|
Array.isArray(parsed.cookies)) {
|
||||||
|
return { cookies: parsed.cookies };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function matchesAnyHost(hosts, cookieDomain) {
|
||||||
|
for (const host of hosts) {
|
||||||
|
if (hostMatchesCookieDomain(host, cookieDomain))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function safeHostnameFromUrl(url) {
|
||||||
|
try {
|
||||||
|
return new URL(url).hostname;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=inline.js.map
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"inline.js","sourceRoot":"","sources":["../../src/providers/inline.ts"],"names":[],"mappings":"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"}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user