xtrawl 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCUMENTATION.md +76 -4
- package/LICENSE +21 -0
- package/README.md +142 -19
- package/dist/auth/account-state.d.ts +3 -0
- package/dist/auth/account-state.d.ts.map +1 -0
- package/dist/auth/account-state.js +85 -0
- package/dist/auth/account-state.js.map +1 -0
- package/dist/client/accounts.d.ts +31 -0
- package/dist/client/accounts.d.ts.map +1 -0
- package/dist/client/accounts.js +163 -0
- package/dist/client/accounts.js.map +1 -0
- package/dist/client/client.d.ts +13 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +54 -18
- package/dist/client/client.js.map +1 -1
- package/dist/client/database.d.ts +2 -1
- package/dist/client/database.d.ts.map +1 -1
- package/dist/client/database.js +1 -1
- package/dist/client/database.js.map +1 -1
- package/dist/client/types.d.ts +2 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/domain/account-state.d.ts +68 -0
- package/dist/domain/account-state.d.ts.map +1 -0
- package/dist/domain/account-state.js +2 -0
- package/dist/domain/account-state.js.map +1 -0
- package/dist/domain/errors.d.ts +3 -0
- package/dist/domain/errors.d.ts.map +1 -1
- package/dist/domain/errors.js +5 -0
- package/dist/domain/errors.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/pool/account-pool.d.ts +12 -5
- package/dist/pool/account-pool.d.ts.map +1 -1
- package/dist/pool/account-pool.js +102 -13
- package/dist/pool/account-pool.js.map +1 -1
- package/dist/storage/account-repository.d.ts +8 -1
- package/dist/storage/account-repository.d.ts.map +1 -1
- package/dist/storage/account-repository.js +63 -26
- package/dist/storage/account-repository.js.map +1 -1
- package/docs/architecture/overview.md +13 -10
- package/docs/decisions/0003-caller-owned-account-state.md +33 -0
- package/docs/product/specification.md +8 -0
- package/docs/security/data-boundary.md +9 -2
- package/package.json +4 -3
package/DOCUMENTATION.md
CHANGED
|
@@ -8,6 +8,7 @@ the command line. XTrawl is an authenticated, read-only collector for public X d
|
|
|
8
8
|
- [Install](#install)
|
|
9
9
|
- [Authenticate](#authenticate)
|
|
10
10
|
- [Use multiple accounts](#use-multiple-accounts)
|
|
11
|
+
- [Own account state](#own-account-state)
|
|
11
12
|
- [Use a proxy](#use-a-proxy)
|
|
12
13
|
- [Use the TypeScript API](#use-the-typescript-api)
|
|
13
14
|
- [Search filters](#search-filters)
|
|
@@ -161,6 +162,64 @@ usable `auth_token` and `ct0` values. XTrawl does not perform interactive userna
|
|
|
161
162
|
If you want to reuse accounts already stored in the configured SQLite database without provisioning
|
|
162
163
|
new input, create the client with `provision: false`.
|
|
163
164
|
|
|
165
|
+
## Own account state
|
|
166
|
+
|
|
167
|
+
SQLite is the default account store. A TypeScript application can instead implement the exported
|
|
168
|
+
`AccountStateStore` interface and keep account state in its own database, vault, or service:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { XTrawl, type AccountStateStore } from "xtrawl";
|
|
172
|
+
|
|
173
|
+
const accountStore: AccountStateStore = createMyAccountStore();
|
|
174
|
+
|
|
175
|
+
const client = await XTrawl.create({
|
|
176
|
+
accountStore,
|
|
177
|
+
dbPath: "./state/runs-and-cursors.db",
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
A custom store must be passed to `await XTrawl.create()`. The synchronous constructor rejects it
|
|
182
|
+
because account provisioning and initialization may be asynchronous. `dbPath` remains active for
|
|
183
|
+
run records, pagination checkpoints, and manifest caching; only account state moves to the adapter.
|
|
184
|
+
|
|
185
|
+
The store owns these operations:
|
|
186
|
+
|
|
187
|
+
| Method | Required behavior |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `list`, `findByUsername`, `upsert`, `delete` | Persist records; `upsert` merges fields and cookie keys while preserving omitted fields |
|
|
190
|
+
| `replaceAll` | Atomically replace all account records |
|
|
191
|
+
| `acquireLease` | Atomically choose and lease one eligible account |
|
|
192
|
+
| `renewLease` | Extend only the matching active lease |
|
|
193
|
+
| `completeLease` | Atomically clear the lease, record usage, and apply health/cooldown state |
|
|
194
|
+
|
|
195
|
+
Every method may return its result directly or in a promise. Lease operations must be atomic across
|
|
196
|
+
all workers or processes sharing the store. The request passed to `acquireLease` includes the current
|
|
197
|
+
time, generated lease ID, expiry, UTC date, authentication requirement, and configured daily limits.
|
|
198
|
+
|
|
199
|
+
### Export and restore sessions
|
|
200
|
+
|
|
201
|
+
Use `client.accounts` when the application needs to persist or transfer reusable session state:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const state = await client.accounts.exportState({ includeSecrets: true });
|
|
205
|
+
await mySecretStore.set("xtrawl/accounts", state);
|
|
206
|
+
|
|
207
|
+
const saved: unknown = await mySecretStore.get("xtrawl/accounts");
|
|
208
|
+
await client.accounts.restoreState(saved, { mode: "merge" });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Snapshots are versioned and validated at restore time. They include cookies, authentication and
|
|
212
|
+
CSRF tokens, optional bearer tokens and proxies, usage counters, cooldown state, and last-error
|
|
213
|
+
metadata. They exclude storage IDs, active lease ownership, passwords, email credentials, and
|
|
214
|
+
two-factor secrets. `merge` upserts by username. `replace` calls the store's atomic `replaceAll`.
|
|
215
|
+
|
|
216
|
+
`includeSecrets: true` is intentionally required: exported state can authenticate as the supplied
|
|
217
|
+
accounts. Keep snapshots in an encrypted caller-owned secret store and never print, log, or commit
|
|
218
|
+
them.
|
|
219
|
+
|
|
220
|
+
Other async account operations include `summary()`, `list()`, `get()`, `import()`, `setProxy()`,
|
|
221
|
+
`repair()`, and `delete()`. Listings are redacted unless `revealSecrets` is explicitly requested.
|
|
222
|
+
|
|
164
223
|
## Use a proxy
|
|
165
224
|
|
|
166
225
|
Set one proxy for all accounts:
|
|
@@ -652,7 +711,8 @@ XTrawl caches it in SQLite and can use a stale cached value when a refresh fails
|
|
|
652
711
|
|
|
653
712
|
## Understand storage and account health
|
|
654
713
|
|
|
655
|
-
XTrawl uses SQLite for
|
|
714
|
+
XTrawl uses SQLite for run, checkpoint, and manifest state. SQLite is also the default account-state
|
|
715
|
+
adapter:
|
|
656
716
|
|
|
657
717
|
- Provisioned accounts and their health status
|
|
658
718
|
- Exclusive account leases and lease expiry
|
|
@@ -669,12 +729,23 @@ leased, is not cooling down, and remains within configured local limits. After t
|
|
|
669
729
|
- A rate-limit, network, proxy, or transient failure applies the corresponding cooldown.
|
|
670
730
|
- An authentication rejection marks the account unusable so it is not selected again.
|
|
671
731
|
|
|
672
|
-
|
|
673
|
-
account at the same time.
|
|
732
|
+
The selected account store coordinates leases so separate work does not intentionally use the same
|
|
733
|
+
account at the same time. A custom implementation must provide the same atomic lease guarantees.
|
|
674
734
|
|
|
675
735
|
## Manage local state
|
|
676
736
|
|
|
677
|
-
`client.
|
|
737
|
+
`client.accounts` is the storage-independent async account facade:
|
|
738
|
+
|
|
739
|
+
```ts
|
|
740
|
+
console.log(await client.accounts.summary());
|
|
741
|
+
console.log(await client.accounts.list({ eligibleOnly: true }));
|
|
742
|
+
|
|
743
|
+
await client.accounts.setProxy("collector-one", "socks5://127.0.0.1:1080");
|
|
744
|
+
await client.accounts.repair("collector-one", true);
|
|
745
|
+
await client.accounts.delete("old-account");
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
`client.db` provides SQLite-specific account maintenance and the run/checkpoint APIs:
|
|
678
749
|
|
|
679
750
|
```ts
|
|
680
751
|
console.log(client.db.accountsSummary());
|
|
@@ -703,6 +774,7 @@ Treat the following as sensitive:
|
|
|
703
774
|
- `auth_token`, `ct0`, bearer overrides, and complete cookie jars
|
|
704
775
|
- SQLite state files containing provisioned account records
|
|
705
776
|
- Proxy URLs containing usernames or passwords
|
|
777
|
+
- Account-state snapshots returned by `client.accounts.exportState()`
|
|
706
778
|
- Collected output that may contain personal data
|
|
707
779
|
|
|
708
780
|
Follow these rules:
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XTrawl contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,11 +4,24 @@
|
|
|
4
4
|
Scrape public X posts, profiles, followers, and following.
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/xtrawl"><img alt="npm version" src="https://img.shields.io/npm/v/xtrawl"></a>
|
|
9
|
+
<a href="https://github.com/ensp1re/xtrawl/actions/workflows/ci.yml"><img alt="CI status" src="https://github.com/ensp1re/xtrawl/actions/workflows/ci.yml/badge.svg"></a>
|
|
10
|
+
<a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
7
13
|
XTrawl collects public data from X without an official API key. Search posts, read profiles and
|
|
8
14
|
timelines, inspect individual posts, collect follower and following lists, and save results as CSV or
|
|
9
15
|
JSON. It splits long searches, rotates authorized accounts when requests fail, and saves resumable
|
|
10
16
|
progress in SQLite. Use it from TypeScript or the command line.
|
|
11
17
|
|
|
18
|
+
XTrawl is published on npm as [`xtrawl`](https://www.npmjs.com/package/xtrawl).
|
|
19
|
+
|
|
20
|
+
> [!WARNING]
|
|
21
|
+
> XTrawl is provided for educational and research purposes only. Automated scraping may violate
|
|
22
|
+
> X's rules and can cause any account used with XTrawl to be restricted, locked, or permanently
|
|
23
|
+
> banned. Use only accounts you own or are authorized to use, and proceed at your own risk.
|
|
24
|
+
|
|
12
25
|
## What XTrawl collects
|
|
13
26
|
|
|
14
27
|
- Search results with date, account, phrase, hashtag, language, location, media, and engagement filters
|
|
@@ -151,33 +164,128 @@ flowchart LR
|
|
|
151
164
|
Parse --> Result["Typed records"]
|
|
152
165
|
Result --> Output["stdout / CSV / JSON"]
|
|
153
166
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
Accounts[("Account state store")] <--> Pool
|
|
168
|
+
SQLite[("SQLite runs and checkpoints")] <--> Client
|
|
169
|
+
SQLite <--> Query
|
|
170
|
+
Accounts -.->|SQLite by default| SQLite
|
|
157
171
|
```
|
|
158
172
|
|
|
159
|
-
For each operation, XTrawl leases an eligible account from
|
|
173
|
+
For each operation, XTrawl leases an eligible account from the configured account store, verifies its proxy when configured,
|
|
160
174
|
creates an authenticated session, builds a read-only web request, and paginates until the requested
|
|
161
175
|
limit or another stop condition is reached. Searches default to the previous 30 days and divide that
|
|
162
176
|
interval into concurrent tasks. Failed requests use bounded backoff, account switching, and session
|
|
163
177
|
repair. Responses enter the application as unknown data and are narrowed into typed records at the
|
|
164
178
|
engine boundary.
|
|
165
179
|
|
|
166
|
-
SQLite tracks
|
|
167
|
-
|
|
180
|
+
SQLite tracks run history, resumable cursors, cached operation manifests, and—by default—account
|
|
181
|
+
health, request usage, and leases. Applications may replace only the account-state boundary with
|
|
182
|
+
their own store. XTrawl does not store collected posts or profiles unless you explicitly enable file
|
|
168
183
|
output.
|
|
169
184
|
|
|
170
|
-
##
|
|
185
|
+
## Use multiple accounts
|
|
186
|
+
|
|
187
|
+
Create a local `accounts.json` file containing one object per account. Each live account needs an
|
|
188
|
+
`auth_token` and `ct0` value from the same authorized browser session. You can provide them as named
|
|
189
|
+
fields or inside `cookies`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
"username": "collector-one",
|
|
195
|
+
"authToken": "replace-with-auth-token",
|
|
196
|
+
"csrfToken": "replace-with-ct0"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"username": "collector-two",
|
|
200
|
+
"cookies": {
|
|
201
|
+
"auth_token": "replace-with-auth-token",
|
|
202
|
+
"ct0": "replace-with-ct0"
|
|
203
|
+
},
|
|
204
|
+
"proxy": "socks5://proxy-user:proxy-password@127.0.0.1:1080"
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Keep this file outside version control. Load the complete pool with the CLI:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npx xtrawl \
|
|
213
|
+
--cookies-file ./accounts.json \
|
|
214
|
+
--db-path ./state/xtrawl.db \
|
|
215
|
+
--concurrency 5 \
|
|
216
|
+
search "typescript" \
|
|
217
|
+
--limit 100
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Or load it from the TypeScript API:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
const client = await XTrawl.create({
|
|
224
|
+
accountsFile: "./accounts.json",
|
|
225
|
+
dbPath: "./state/xtrawl.db",
|
|
226
|
+
concurrency: 5,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
console.log(client.poolSummary);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
XTrawl imports the accounts into SQLite and leases eligible accounts as work is scheduled. The
|
|
233
|
+
default concurrency is five; loading more accounts does not make every account run at once. There is
|
|
234
|
+
no configured account-count limit, but very large pools have not been load-tested. A failed page is
|
|
235
|
+
retried up to three times and may switch accounts twice by default. Rate-limit, network, proxy, and
|
|
236
|
+
transient failures place the affected account into cooldown. A rejected session marks the account
|
|
237
|
+
unusable and attempts a CSRF-cookie repair before another account is selected.
|
|
238
|
+
|
|
239
|
+
An account-level `proxy` takes precedence over the global `--proxy`. HTTP, HTTPS, and SOCKS5 proxies
|
|
240
|
+
are supported. XTrawl does not currently accept a separate proxy list or automatically assign and
|
|
241
|
+
reassign proxies; attach a proxy to each account when you need one-to-one account/proxy routing.
|
|
171
242
|
|
|
172
|
-
|
|
173
|
-
Netscape-cookie, or delimited account file with `--cookies-file`, or use the equivalent library
|
|
174
|
-
options. A global proxy can be set with `--proxy`; library account records may also define their own
|
|
175
|
-
HTTP, HTTPS, or SOCKS5 proxy.
|
|
243
|
+
### Cookie lifecycle
|
|
176
244
|
|
|
177
|
-
XTrawl
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
245
|
+
XTrawl does not refresh cookies after every request and does not persist `Set-Cookie` response
|
|
246
|
+
updates. It reuses each stored cookie jar until the session fails. `XTrawl.create()` tries to obtain a
|
|
247
|
+
missing `ct0` cookie when an `auth_token` is available, and an authentication failure triggers the
|
|
248
|
+
same repair path. If the `auth_token` has expired or been revoked, replace the stored cookies before
|
|
249
|
+
using that account again.
|
|
250
|
+
|
|
251
|
+
XTrawl does not perform username/password, email, or two-factor login. Supplying those fields without
|
|
252
|
+
valid session cookies does not make an account eligible for requests.
|
|
253
|
+
|
|
254
|
+
## Own account state
|
|
255
|
+
|
|
256
|
+
TypeScript applications can keep accounts in their own database or secret store by implementing
|
|
257
|
+
`AccountStateStore`. The adapter owns account records, atomic lease acquisition and completion, and
|
|
258
|
+
atomic replacement. Its methods may be synchronous or asynchronous; custom stores must be supplied
|
|
259
|
+
through `XTrawl.create()`:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { XTrawl, type AccountStateStore } from "xtrawl";
|
|
263
|
+
|
|
264
|
+
const accountStore: AccountStateStore = createMyAccountStore();
|
|
265
|
+
|
|
266
|
+
const client = await XTrawl.create({
|
|
267
|
+
accountStore,
|
|
268
|
+
dbPath: "./state/runs-and-cursors.db",
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`dbPath` still holds run history, checkpoints, and manifest cache. Account credentials, health,
|
|
273
|
+
limits, cooldowns, and leases use the custom store.
|
|
274
|
+
|
|
275
|
+
You can also move reusable session state between stores:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
const state = await client.accounts.exportState({ includeSecrets: true });
|
|
279
|
+
await mySecretStore.set("xtrawl/accounts", state);
|
|
280
|
+
|
|
281
|
+
const saved = await mySecretStore.get("xtrawl/accounts");
|
|
282
|
+
await client.accounts.restoreState(saved, { mode: "merge" });
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Export requires the explicit `includeSecrets: true` acknowledgement because the snapshot contains
|
|
286
|
+
authentication tokens, cookies, bearer overrides, and proxy credentials. It excludes account IDs,
|
|
287
|
+
active leases, passwords, email credentials, and two-factor secrets. `merge` updates matching
|
|
288
|
+
usernames; `replace` atomically replaces all accounts. Never log or commit a snapshot.
|
|
181
289
|
|
|
182
290
|
## Resume and save output
|
|
183
291
|
|
|
@@ -190,10 +298,11 @@ append records, and generated filenames include the query/date range or collecti
|
|
|
190
298
|
|
|
191
299
|
## Manage local state
|
|
192
300
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
301
|
+
Use the async `client.accounts` facade for redacted account listing, import, proxy assignment,
|
|
302
|
+
repair, deletion, and explicit state export/restore. It works with SQLite and custom stores.
|
|
303
|
+
|
|
304
|
+
`client.db` provides SQLite-specific account maintenance plus run and checkpoint inspection.
|
|
305
|
+
Destructive duplicate cleanup is a dry run unless explicitly enabled.
|
|
197
306
|
|
|
198
307
|
## Safety and limitations
|
|
199
308
|
|
|
@@ -230,3 +339,17 @@ npm run verify
|
|
|
230
339
|
The full gate checks formatting, linting, strict TypeScript compilation, unit tests, package and CLI
|
|
231
340
|
smoke behavior, harness integrity, and context-budget limits. Live integration tests require
|
|
232
341
|
caller-supplied credentials and are disabled unless explicitly enabled.
|
|
342
|
+
|
|
343
|
+
## Contributing and security
|
|
344
|
+
|
|
345
|
+
Bug reports, documentation fixes, and read-only collection improvements are welcome. Read
|
|
346
|
+
[CONTRIBUTING.md](https://github.com/ensp1re/xtrawl/blob/main/CONTRIBUTING.md) before opening a pull
|
|
347
|
+
request. Please do not propose posting, liking, following, messaging, or other account mutation
|
|
348
|
+
features.
|
|
349
|
+
|
|
350
|
+
Do not report vulnerabilities or credential leaks in a public issue. Follow the private disclosure
|
|
351
|
+
process in [SECURITY.md](https://github.com/ensp1re/xtrawl/blob/main/SECURITY.md). By participating in
|
|
352
|
+
the project, you agree to follow the
|
|
353
|
+
[Code of Conduct](https://github.com/ensp1re/xtrawl/blob/main/CODE_OF_CONDUCT.md).
|
|
354
|
+
|
|
355
|
+
XTrawl is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-state.d.ts","sourceRoot":"","sources":["../../src/auth/account-state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAA8B,MAAM,4BAA4B,CAAC;AAInG,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAqB9E"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { normalizeProxyPayload } from "../config/validation.js";
|
|
2
|
+
import { AccountStateError } from "../domain/errors.js";
|
|
3
|
+
import { isRecord } from "../utils/guards.js";
|
|
4
|
+
export function parseAccountStateSnapshot(value) {
|
|
5
|
+
if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.accounts))
|
|
6
|
+
throw new AccountStateError("Account state must use schemaVersion 1 and contain accounts.");
|
|
7
|
+
if (typeof value.exportedAt !== "string" ||
|
|
8
|
+
!Number.isFinite(Date.parse(value.exportedAt)) ||
|
|
9
|
+
new Date(value.exportedAt).toISOString() !== value.exportedAt)
|
|
10
|
+
throw new AccountStateError("Account state exportedAt must be an ISO date string.");
|
|
11
|
+
const accounts = value.accounts.map((account, index) => parseSnapshotAccount(account, index));
|
|
12
|
+
const usernames = new Set();
|
|
13
|
+
for (const account of accounts) {
|
|
14
|
+
if (usernames.has(account.username))
|
|
15
|
+
throw new AccountStateError(`Account state contains duplicate username: ${account.username}.`);
|
|
16
|
+
usernames.add(account.username);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
exportedAt: value.exportedAt,
|
|
21
|
+
accounts,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function parseSnapshotAccount(value, index) {
|
|
25
|
+
if (!isRecord(value) || typeof value.username !== "string" || !value.username.trim())
|
|
26
|
+
throw invalidAccount(index, "username must be a non-empty string");
|
|
27
|
+
if (!isRecord(value.cookies))
|
|
28
|
+
throw invalidAccount(index, "cookies must be an object");
|
|
29
|
+
const cookies = parseCookies(value.cookies, index);
|
|
30
|
+
const proxy = normalizeProxyPayload(value.proxy);
|
|
31
|
+
if (value.proxy !== undefined && proxy === undefined)
|
|
32
|
+
throw invalidAccount(index, "proxy must be a URL or host/port object");
|
|
33
|
+
const status = optionalStatus(value.status, index);
|
|
34
|
+
return {
|
|
35
|
+
username: value.username.trim(),
|
|
36
|
+
...optionalString("authToken", value.authToken, index),
|
|
37
|
+
...optionalString("csrfToken", value.csrfToken, index),
|
|
38
|
+
cookies,
|
|
39
|
+
...optionalString("bearerToken", value.bearerToken, index),
|
|
40
|
+
...(proxy === undefined ? {} : { proxy }),
|
|
41
|
+
...(status === undefined ? {} : { status }),
|
|
42
|
+
...optionalNonNegativeNumber("availableUntil", value.availableUntil, index),
|
|
43
|
+
...optionalNonNegativeNumber("dailyRequests", value.dailyRequests, index),
|
|
44
|
+
...optionalNonNegativeNumber("dailyTweets", value.dailyTweets, index),
|
|
45
|
+
...optionalNonNegativeNumber("totalTweets", value.totalTweets, index),
|
|
46
|
+
...optionalString("lastResetDate", value.lastResetDate, index),
|
|
47
|
+
...optionalNonNegativeNumber("lastUsed", value.lastUsed, index),
|
|
48
|
+
...optionalNonNegativeNumber("lastErrorCode", value.lastErrorCode, index),
|
|
49
|
+
...optionalString("cooldownReason", value.cooldownReason, index),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function parseCookies(value, index) {
|
|
53
|
+
const cookies = {};
|
|
54
|
+
for (const [name, cookie] of Object.entries(value)) {
|
|
55
|
+
if (typeof cookie !== "string")
|
|
56
|
+
throw invalidAccount(index, "cookie values must be strings");
|
|
57
|
+
cookies[name] = cookie;
|
|
58
|
+
}
|
|
59
|
+
return cookies;
|
|
60
|
+
}
|
|
61
|
+
function optionalString(key, value, index) {
|
|
62
|
+
if (value === undefined)
|
|
63
|
+
return {};
|
|
64
|
+
if (typeof value !== "string")
|
|
65
|
+
throw invalidAccount(index, `${key} must be a string`);
|
|
66
|
+
return { [key]: value };
|
|
67
|
+
}
|
|
68
|
+
function optionalNonNegativeNumber(key, value, index) {
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
return {};
|
|
71
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
72
|
+
throw invalidAccount(index, `${key} must be a non-negative number`);
|
|
73
|
+
return { [key]: value };
|
|
74
|
+
}
|
|
75
|
+
function optionalStatus(value, index) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
if (value !== 0 && value !== 1 && value !== 2)
|
|
79
|
+
throw invalidAccount(index, "status must be 0, 1, or 2");
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function invalidAccount(index, reason) {
|
|
83
|
+
return new AccountStateError(`Invalid account state at accounts[${index}]: ${reason}.`);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=account-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-state.js","sourceRoot":"","sources":["../../src/auth/account-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ClientConfig } from "../config/types.js";
|
|
2
|
+
import type { AccountStateExportOptions, AccountStateRestoreOptions, AccountStateSnapshot, AccountStateStore } from "../domain/account-state.js";
|
|
3
|
+
import type { AccountRecord, AccountSummary, ProxySettings } from "../domain/accounts.js";
|
|
4
|
+
import { type AccountImportOptions, type AccountListOptions } from "./database.js";
|
|
5
|
+
export declare class XTrawlAccounts {
|
|
6
|
+
private readonly store;
|
|
7
|
+
private readonly config;
|
|
8
|
+
private readonly location;
|
|
9
|
+
private readonly onAccountsChanged?;
|
|
10
|
+
private cachedAccounts;
|
|
11
|
+
constructor(store: AccountStateStore, config: ClientConfig, location: string, onAccountsChanged?: ((accounts: readonly AccountRecord[]) => void) | undefined);
|
|
12
|
+
refresh(): Promise<void>;
|
|
13
|
+
initialize(accounts: readonly AccountRecord[]): void;
|
|
14
|
+
inspectCached(): readonly Record<string, unknown>[];
|
|
15
|
+
summary(): Promise<AccountSummary>;
|
|
16
|
+
list(options?: AccountListOptions): Promise<readonly Record<string, unknown>[]>;
|
|
17
|
+
get(username: string, options?: Pick<AccountListOptions, "includeCookies" | "revealSecrets">): Promise<Record<string, unknown> | undefined>;
|
|
18
|
+
import(options: AccountImportOptions): Promise<{
|
|
19
|
+
readonly processed: number;
|
|
20
|
+
readonly eligible: number;
|
|
21
|
+
}>;
|
|
22
|
+
delete(username: string): Promise<boolean>;
|
|
23
|
+
setProxy(username: string, proxy: string | ProxySettings): Promise<boolean>;
|
|
24
|
+
repair(username: string, forceRefresh?: boolean): Promise<boolean>;
|
|
25
|
+
exportState(options: AccountStateExportOptions): Promise<AccountStateSnapshot>;
|
|
26
|
+
restoreState(state: unknown, options?: AccountStateRestoreOptions): Promise<{
|
|
27
|
+
readonly restored: number;
|
|
28
|
+
readonly mode: "merge" | "replace";
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=accounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/client/accounts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,oBAAoB,EAEpB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG1F,OAAO,EAAiB,KAAK,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAElG,qBAAa,cAAc;IAIvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IANrC,OAAO,CAAC,cAAc,CAAgC;gBAGnC,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,EAChB,iBAAiB,CAAC,GAAE,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,KAAK,IAAI,aAAA;IAGtE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAK9B,UAAU,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI;IAIpD,aAAa,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE;IAI7C,OAAO,IAAI,OAAO,CAAC,cAAc,CAAC;IAKlC,IAAI,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IASnF,GAAG,CACd,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,IAAI,CAAC,kBAAkB,EAAE,gBAAgB,GAAG,eAAe,CAAM,GACzE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAKlC,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;QAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,CAAC;IAOW,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAM1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ3E,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBhE,WAAW,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAW9E,YAAY,CACvB,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,CAAC;CAW9E"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { bootstrapCookiesFromAuthToken } from "../auth/bootstrap.js";
|
|
2
|
+
import { parseAccountStateSnapshot } from "../auth/account-state.js";
|
|
3
|
+
import { loadAccountsFileSync, loadInlineAccounts } from "../auth/loaders.js";
|
|
4
|
+
import { accountInputToRecord } from "../auth/records.js";
|
|
5
|
+
import { loadAccountFromEnvironmentSync } from "../config/environment.js";
|
|
6
|
+
import { AccountStateError } from "../domain/errors.js";
|
|
7
|
+
import { summarizeAccounts } from "../pool/account-pool.js";
|
|
8
|
+
import { redactAccount } from "./database.js";
|
|
9
|
+
export class XTrawlAccounts {
|
|
10
|
+
store;
|
|
11
|
+
config;
|
|
12
|
+
location;
|
|
13
|
+
onAccountsChanged;
|
|
14
|
+
cachedAccounts = [];
|
|
15
|
+
constructor(store, config, location, onAccountsChanged) {
|
|
16
|
+
this.store = store;
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.location = location;
|
|
19
|
+
this.onAccountsChanged = onAccountsChanged;
|
|
20
|
+
}
|
|
21
|
+
async refresh() {
|
|
22
|
+
this.cachedAccounts = await this.store.list();
|
|
23
|
+
this.onAccountsChanged?.(this.cachedAccounts);
|
|
24
|
+
}
|
|
25
|
+
initialize(accounts) {
|
|
26
|
+
this.cachedAccounts = accounts;
|
|
27
|
+
}
|
|
28
|
+
inspectCached() {
|
|
29
|
+
return this.cachedAccounts.map((account) => redactAccount(account, {}));
|
|
30
|
+
}
|
|
31
|
+
async summary() {
|
|
32
|
+
await this.refresh();
|
|
33
|
+
return summarizeAccounts(this.cachedAccounts, this.config, this.location);
|
|
34
|
+
}
|
|
35
|
+
async list(options = {}) {
|
|
36
|
+
await this.refresh();
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
return this.cachedAccounts
|
|
39
|
+
.filter((account) => !options.eligibleOnly || isEligible(account, this.config, now))
|
|
40
|
+
.filter((account) => !options.unusableOnly || account.status === 0)
|
|
41
|
+
.map((account) => redactAccount(account, options));
|
|
42
|
+
}
|
|
43
|
+
async get(username, options = {}) {
|
|
44
|
+
const account = await this.store.findByUsername(username);
|
|
45
|
+
return account ? redactAccount(account, options) : undefined;
|
|
46
|
+
}
|
|
47
|
+
async import(options) {
|
|
48
|
+
const accounts = loadImportAccounts(options);
|
|
49
|
+
for (const account of accounts)
|
|
50
|
+
await this.store.upsert({ ...account, proxy: account.proxy ?? options.proxy });
|
|
51
|
+
return { processed: accounts.length, eligible: (await this.summary()).eligible };
|
|
52
|
+
}
|
|
53
|
+
async delete(username) {
|
|
54
|
+
const deleted = await this.store.delete(username);
|
|
55
|
+
await this.refresh();
|
|
56
|
+
return deleted;
|
|
57
|
+
}
|
|
58
|
+
async setProxy(username, proxy) {
|
|
59
|
+
const account = await this.store.findByUsername(username);
|
|
60
|
+
if (!account)
|
|
61
|
+
return false;
|
|
62
|
+
await this.store.upsert({ ...account, proxy });
|
|
63
|
+
await this.refresh();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
async repair(username, forceRefresh = false) {
|
|
67
|
+
const account = await this.store.findByUsername(username);
|
|
68
|
+
if (!account?.authToken)
|
|
69
|
+
return false;
|
|
70
|
+
if (!forceRefresh && account.csrfToken) {
|
|
71
|
+
await this.store.upsert({ ...account, status: 1, availableUntil: 0 });
|
|
72
|
+
await this.refresh();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const cookies = await bootstrapCookiesFromAuthToken(account.authToken, undefined, account.proxy);
|
|
76
|
+
if (!cookies?.ct0)
|
|
77
|
+
return false;
|
|
78
|
+
await this.store.upsert({
|
|
79
|
+
...account,
|
|
80
|
+
status: 1,
|
|
81
|
+
availableUntil: 0,
|
|
82
|
+
csrfToken: cookies.ct0,
|
|
83
|
+
cookies: { ...account.cookies, ...cookies },
|
|
84
|
+
});
|
|
85
|
+
await this.refresh();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
async exportState(options) {
|
|
89
|
+
if (!options || options.includeSecrets !== true)
|
|
90
|
+
throw new AccountStateError("exportState requires includeSecrets: true.");
|
|
91
|
+
const accounts = await this.store.list();
|
|
92
|
+
return {
|
|
93
|
+
schemaVersion: 1,
|
|
94
|
+
exportedAt: new Date().toISOString(),
|
|
95
|
+
accounts: accounts.map(toSnapshotRecord),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async restoreState(state, options = {}) {
|
|
99
|
+
const snapshot = parseAccountStateSnapshot(state);
|
|
100
|
+
const accounts = snapshot.accounts.map(fromSnapshotRecord);
|
|
101
|
+
const mode = options.mode ?? "merge";
|
|
102
|
+
if (mode !== "merge" && mode !== "replace")
|
|
103
|
+
throw new AccountStateError("restoreState mode must be merge or replace.");
|
|
104
|
+
if (mode === "replace")
|
|
105
|
+
await this.store.replaceAll(accounts);
|
|
106
|
+
else
|
|
107
|
+
for (const account of accounts)
|
|
108
|
+
await this.store.upsert(account);
|
|
109
|
+
await this.refresh();
|
|
110
|
+
return { restored: accounts.length, mode };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function loadImportAccounts(options) {
|
|
114
|
+
const accounts = [];
|
|
115
|
+
if (options.accounts)
|
|
116
|
+
accounts.push(...options.accounts.map(accountInputToRecord));
|
|
117
|
+
if (options.cookies !== undefined)
|
|
118
|
+
accounts.push(...loadInlineAccounts(options.cookies));
|
|
119
|
+
if (options.accountsFile)
|
|
120
|
+
accounts.push(...loadAccountsFileSync(options.accountsFile));
|
|
121
|
+
if (options.cookiesFile)
|
|
122
|
+
accounts.push(...loadAccountsFileSync(options.cookiesFile));
|
|
123
|
+
if (options.envFile)
|
|
124
|
+
accounts.push(...loadAccountFromEnvironmentSync(options.envFile).map(accountInputToRecord));
|
|
125
|
+
return accounts;
|
|
126
|
+
}
|
|
127
|
+
function toSnapshotRecord(account) {
|
|
128
|
+
return {
|
|
129
|
+
username: account.username,
|
|
130
|
+
...(account.authToken ? { authToken: account.authToken } : {}),
|
|
131
|
+
...(account.csrfToken ? { csrfToken: account.csrfToken } : {}),
|
|
132
|
+
cookies: account.cookies,
|
|
133
|
+
...(account.bearerToken ? { bearerToken: account.bearerToken } : {}),
|
|
134
|
+
...(account.proxy ? { proxy: account.proxy } : {}),
|
|
135
|
+
...(account.status === 0 || account.status === 1 || account.status === 2
|
|
136
|
+
? { status: account.status }
|
|
137
|
+
: {}),
|
|
138
|
+
...copyOperationalState(account),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function fromSnapshotRecord(account) {
|
|
142
|
+
return { ...account, cookies: account.cookies };
|
|
143
|
+
}
|
|
144
|
+
function copyOperationalState(account) {
|
|
145
|
+
return {
|
|
146
|
+
...(account.availableUntil === undefined ? {} : { availableUntil: account.availableUntil }),
|
|
147
|
+
...(account.dailyRequests === undefined ? {} : { dailyRequests: account.dailyRequests }),
|
|
148
|
+
...(account.dailyTweets === undefined ? {} : { dailyTweets: account.dailyTweets }),
|
|
149
|
+
...(account.totalTweets === undefined ? {} : { totalTweets: account.totalTweets }),
|
|
150
|
+
...(account.lastResetDate === undefined ? {} : { lastResetDate: account.lastResetDate }),
|
|
151
|
+
...(account.lastUsed === undefined ? {} : { lastUsed: account.lastUsed }),
|
|
152
|
+
...(account.lastErrorCode === undefined ? {} : { lastErrorCode: account.lastErrorCode }),
|
|
153
|
+
...(account.cooldownReason === undefined ? {} : { cooldownReason: account.cooldownReason }),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function isEligible(account, config, now) {
|
|
157
|
+
return (account.status !== 0 &&
|
|
158
|
+
!(account.status === 2 && (account.availableUntil ?? 0) > now) &&
|
|
159
|
+
Boolean(account.authToken && account.csrfToken) &&
|
|
160
|
+
(account.dailyRequests ?? 0) < config.dailyRequestsLimit &&
|
|
161
|
+
(account.dailyTweets ?? 0) < config.dailyTweetsLimit);
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=accounts.js.map
|