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.
Files changed (46) hide show
  1. package/DOCUMENTATION.md +76 -4
  2. package/LICENSE +21 -0
  3. package/README.md +142 -19
  4. package/dist/auth/account-state.d.ts +3 -0
  5. package/dist/auth/account-state.d.ts.map +1 -0
  6. package/dist/auth/account-state.js +85 -0
  7. package/dist/auth/account-state.js.map +1 -0
  8. package/dist/client/accounts.d.ts +31 -0
  9. package/dist/client/accounts.d.ts.map +1 -0
  10. package/dist/client/accounts.js +163 -0
  11. package/dist/client/accounts.js.map +1 -0
  12. package/dist/client/client.d.ts +13 -3
  13. package/dist/client/client.d.ts.map +1 -1
  14. package/dist/client/client.js +54 -18
  15. package/dist/client/client.js.map +1 -1
  16. package/dist/client/database.d.ts +2 -1
  17. package/dist/client/database.d.ts.map +1 -1
  18. package/dist/client/database.js +1 -1
  19. package/dist/client/database.js.map +1 -1
  20. package/dist/client/types.d.ts +2 -0
  21. package/dist/client/types.d.ts.map +1 -1
  22. package/dist/domain/account-state.d.ts +68 -0
  23. package/dist/domain/account-state.d.ts.map +1 -0
  24. package/dist/domain/account-state.js +2 -0
  25. package/dist/domain/account-state.js.map +1 -0
  26. package/dist/domain/errors.d.ts +3 -0
  27. package/dist/domain/errors.d.ts.map +1 -1
  28. package/dist/domain/errors.js +5 -0
  29. package/dist/domain/errors.js.map +1 -1
  30. package/dist/index.d.ts +5 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/pool/account-pool.d.ts +12 -5
  35. package/dist/pool/account-pool.d.ts.map +1 -1
  36. package/dist/pool/account-pool.js +102 -13
  37. package/dist/pool/account-pool.js.map +1 -1
  38. package/dist/storage/account-repository.d.ts +8 -1
  39. package/dist/storage/account-repository.d.ts.map +1 -1
  40. package/dist/storage/account-repository.js +63 -26
  41. package/dist/storage/account-repository.js.map +1 -1
  42. package/docs/architecture/overview.md +13 -10
  43. package/docs/decisions/0003-caller-owned-account-state.md +33 -0
  44. package/docs/product/specification.md +8 -0
  45. package/docs/security/data-boundary.md +9 -2
  46. 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 operational state:
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
- SQLite coordinates account leases so separate work does not intentionally use the same stored
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.db` provides scoped operational maintenance without exposing the storage implementation:
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
- State[("SQLite state")] <--> Client
155
- State <--> Pool
156
- State <--> Query
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 SQLite, verifies its proxy when configured,
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 account health, request usage, leases, run history, resumable cursors, and cached
167
- operation manifests. It does not store collected posts or profiles unless you explicitly enable file
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
- ## Accounts and proxies
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
- For one account, pass `X_AUTH_TOKEN` and `X_CSRF_TOKEN` directly. For an account pool, provide a JSON,
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 selects only accounts that have usable authentication, are outside cooldown, are not already
178
- leased, and remain within configured daily limits. Rate-limit and transient failures trigger a
179
- cooldown and can switch the current task to another account. Authentication failures attempt a CSRF
180
- cookie repair before the account remains unusable.
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
- The library exposes safe account and run maintenance through `client.db`. It can list redacted
194
- accounts, import accounts, assign proxies, repair or disable an account, clear expired leases and
195
- checkpoints, reset local counters, inspect run history, and remove a named account. Destructive
196
- duplicate cleanup is a dry run unless explicitly enabled.
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,3 @@
1
+ import type { AccountStateSnapshot } from "../domain/account-state.js";
2
+ export declare function parseAccountStateSnapshot(value: unknown): AccountStateSnapshot;
3
+ //# sourceMappingURL=account-state.d.ts.map
@@ -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