fxtwitter 0.0.0-reserved → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 otoneko.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # fxtwitter
2
+
3
+ [![npm](https://img.shields.io/npm/v/fxtwitter)](https://www.npmjs.com/package/fxtwitter)
4
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/otnc/fxtwitter-wrapper/ci.yml?branch=main)](https://github.com/otnc/fxtwitter-wrapper/actions)
5
+ [![GitHub](https://img.shields.io/github/license/otnc/fxtwitter-wrapper)](https://github.com/otnc/fxtwitter-wrapper/blob/main/LICENSE)
6
+ [![Node](https://img.shields.io/node/v/fxtwitter)](https://www.npmjs.com/package/fxtwitter)
7
+
8
+ Typed wrapper for the [FxTwitter](https://github.com/FxEmbed/FxEmbed) API.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install fxtwitter
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { FxTwitterV2 } from "fxtwitter/v2";
20
+
21
+ const fx = new FxTwitterV2();
22
+
23
+ const { status } = await fx.getStatus("20");
24
+ const { user } = await fx.getProfile("X");
25
+ const { results } = await fx.search("puppies");
26
+ ```
27
+
28
+ Each version ships under its own subpath, where its types are exported
29
+ alongside the client:
30
+
31
+ ```ts
32
+ import { FxTwitterV2, type TwitterStatus } from "fxtwitter/v2";
33
+ import { FxTwitterV1, type Tweet } from "fxtwitter/v1";
34
+ ```
35
+
36
+ If you need both at once, `FxTwitter` bundles them. `v1` is only constructed —
37
+ and so only warns — on first access:
38
+
39
+ ```ts
40
+ import { FxTwitter } from "fxtwitter";
41
+
42
+ const fx = new FxTwitter({ v1: { silenceDeprecationWarning: true } });
43
+ await fx.v2.getStatus("20");
44
+ await fx.v1.getStatus("20");
45
+ ```
46
+
47
+ ## Options
48
+
49
+ Every client takes the same options:
50
+
51
+ | Option | Type | Default | Description |
52
+ | --- | --- | --- | --- |
53
+ | `baseUrl` | `string` | `https://api.fxtwitter.com` | API host, e.g. a self-hosted instance |
54
+ | `headers` | `Record<string, string>` | — | Extra headers sent with every request |
55
+ | `timeout` | `number` | disabled | Abort a request after this many ms |
56
+ | `retry` | `number \| false` | `1` | Retries for failed requests |
57
+ | `retryDelay` | `number` | `0` | Delay between retries, in ms |
58
+ | `fetch` | `typeof fetch` | global `fetch` | Custom fetch implementation |
59
+
60
+ Every method takes an optional `signal` for per-call cancellation.
61
+
62
+ > The API requires a `User-Agent` identifying the caller and answers `401`
63
+ > without one. When you don't set one, a header describing the current runtime
64
+ > is sent — `Node.js/22.16.0`, `Bun/1.3.0`, `Cloudflare-Workers` and so on.
65
+ > Set your own to identify your app instead:
66
+ >
67
+ > ```ts
68
+ > new FxTwitterV2({
69
+ > headers: { "User-Agent": "MyApp/1.0 (+https://example.com)" },
70
+ > });
71
+ > ```
72
+ >
73
+ > Nothing is sent in browsers, where `User-Agent` is a forbidden header and the
74
+ > browser supplies its own.
75
+
76
+ ## v2
77
+
78
+ ### `new FxTwitterV2(options?)`
79
+
80
+ | Method | Returns |
81
+ | --- | --- |
82
+ | `getStatus(id, options?)` | A single post |
83
+ | `getThread(id, options?)` | A post with its unrolled thread |
84
+ | `getConversation(id, options?)` | A post, its thread, and ranked replies |
85
+ | `getStatusReposts(id, options?)` | Users who reposted a post |
86
+ | `getStatusQuotes(id, options?)` | Posts quoting a post |
87
+ | `getProfile(handle, options?)` | A user profile |
88
+ | `getProfileStatuses(handle, options?)` | A user's posts |
89
+ | `getProfileArticles(handle, options?)` | A user's long-form articles |
90
+ | `getProfileMedia(handle, options?)` | A user's posts containing media |
91
+ | `getProfileAbout(handle, options?)` | Account metadata |
92
+ | `getProfileFollowers(handle, options?)` | A user's followers |
93
+ | `getProfileFollowing(handle, options?)` | Accounts a user follows |
94
+ | `search(query, options?)` | Post search results |
95
+ | `typeahead(query, options?)` | Autocomplete suggestions |
96
+ | `trends(options?)` | Trending topics |
97
+
98
+ Common options: `count` and `cursor` paginate list endpoints, `lang` requests an
99
+ inline translation, and `aboutAccount` adds `about_account` to an author.
100
+
101
+ ```ts
102
+ // Paginate
103
+ let cursor: string | undefined;
104
+ do {
105
+ const page = await fx.search("puppies", { feed: "top", count: 50, cursor });
106
+ cursor = page.cursor.bottom ?? undefined;
107
+ } while (cursor);
108
+
109
+ // Look a profile up by numeric ID
110
+ import { byUserId } from "fxtwitter/v2";
111
+ await fx.getProfile(byUserId("783214"));
112
+ ```
113
+
114
+ The list endpoints report "no results" as a `404` carrying an otherwise normal
115
+ body. An empty page is a result rather than an error, so it resolves with
116
+ `results: []`. The API uses that same `404` for an unknown handle, which it does
117
+ not distinguish from an empty timeline.
118
+
119
+ `getProfileStatuses` resolves to `null` when `since` is set without a `cursor`
120
+ and nothing is newer — the API's documented `204`. Every other method either
121
+ resolves with a body or throws.
122
+
123
+ ## v1
124
+
125
+ > **Deprecated.** v1 is kept only for backwards compatibility and does not
126
+ > receive new features — use [v2](#v2) instead. Constructing `FxTwitterV1`
127
+ > emits a one-time `DeprecationWarning` (so `--no-deprecation` and
128
+ > `--throw-deprecation` apply); pass `silenceDeprecationWarning: true` to
129
+ > suppress it. `FxTwitter`'s `v1` property only constructs the client, and so
130
+ > only warns, on first access.
131
+
132
+ ### `new FxTwitterV1(options?)`
133
+
134
+ #### `getStatus(id, options?)`
135
+
136
+ Fetches a single status by its snowflake ID.
137
+
138
+ ```ts
139
+ const { tweet } = await fx.getStatus("20", { translateTo: "es" });
140
+ tweet?.translation?.text;
141
+ ```
142
+
143
+ | Option | Type | Description |
144
+ | --- | --- | --- |
145
+ | `translateTo` | `string` | Target language — an ISO 639-1 code (`es`) or locale (`zh-cn`). Adds `translation` to the status |
146
+ | `screenName` | `string` | Author handle, for a readable URL. The API resolves the status from `id` alone and never checks it |
147
+
148
+ Resolves to `{ code, message, tweet }`. `tweet` is `null` when the status could
149
+ not be retrieved.
150
+
151
+ #### `getUser(handle, options?)`
152
+
153
+ Fetches a user profile by handle, without a leading `@`.
154
+
155
+ ```ts
156
+ const { user } = await fx.getUser("jack");
157
+ ```
158
+
159
+ Resolves to `{ code, message, user, reason? }`. `user` is absent when the
160
+ profile could not be retrieved, and `reason` is `"suspended"` for a suspended
161
+ account.
162
+
163
+ On success, `code` mirrors the HTTP status and `message` is one of `OK`,
164
+ `PRIVATE_TWEET`, `NOT_FOUND`, `UPSTREAM_UNAVAILABLE` or `API_FAIL`.
165
+
166
+ Malformed input is rejected before a request is sent, because v1 answers those
167
+ cases with HTML or a redirect rather than JSON: a status ID must be 2-20 digits,
168
+ and a handle passed to `getUser` must match `\w{1,15}`. A well-formed handle
169
+ that does not exist is a normal `404`.
170
+
171
+ ## Errors
172
+
173
+ HTTP 4xx/5xx, network failures, timeouts, invalid input, and non-JSON responses
174
+ are thrown as `FxTwitterError`:
175
+
176
+ ```ts
177
+ import { FxTwitterError } from "fxtwitter";
178
+
179
+ try {
180
+ await fx.getStatus("20");
181
+ } catch (error) {
182
+ if (error instanceof FxTwitterError) {
183
+ error.status; // 404
184
+ error.code; // 404
185
+ error.message; // "NOT_FOUND"
186
+ error.body; // parsed response body, when available
187
+ error.cause; // underlying error, when available
188
+ }
189
+ }
190
+ ```
191
+
192
+ For a network failure or a timeout there is no response, so `status`, `code`
193
+ and `body` are all `undefined` and `message` is a generic description. The
194
+ original error is still available as `cause`.
195
+
196
+ ## Requirements
197
+
198
+ - Node.js >= 22
199
+
200
+ ## Contributing
201
+
202
+ Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
203
+
204
+ ## License
205
+
206
+ Distributed under the [MIT License](./LICENSE).
@@ -0,0 +1,147 @@
1
+ import { defu } from "defu";
2
+ import { FetchError, createFetch } from "ofetch";
3
+ import { hasWindow, nodeVersion, runtime } from "std-env";
4
+ //#region src/errors.ts
5
+ /** Thrown for any non-2xx response, network failure, timeout, or unparsable body. */
6
+ var FxTwitterError = class extends Error {
7
+ status;
8
+ code;
9
+ body;
10
+ constructor(message, options = {}) {
11
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
12
+ this.name = "FxTwitterError";
13
+ this.status = options.status;
14
+ this.code = options.code;
15
+ this.body = options.body;
16
+ }
17
+ };
18
+ //#endregion
19
+ //#region src/user-agent.ts
20
+ /** Reads a version off a runtime global without assuming it exists. */
21
+ function globalVersion(name, path) {
22
+ let value = globalThis[name];
23
+ for (const key of path) {
24
+ if (typeof value !== "object" || value === null) return null;
25
+ value = value[key];
26
+ }
27
+ return typeof value === "string" ? value : null;
28
+ }
29
+ function product(name, version) {
30
+ return version ? `${name}/${version}` : name;
31
+ }
32
+ /**
33
+ * A `User-Agent` describing the runtime the request is made from, such as
34
+ * `Node.js/22.16.0`, `Bun/1.3.0` or `Cloudflare-Workers`.
35
+ *
36
+ * The API requires callers to identify themselves and answers `401` otherwise.
37
+ * Runtimes that fill the header in do so very tersely — Node sends just `node`
38
+ * — so this states the runtime and version instead. It deliberately says
39
+ * nothing about this package.
40
+ *
41
+ * Returns `null` in browsers, where `User-Agent` is a forbidden header: the
42
+ * request carries the browser's own regardless, and setting it only logs a
43
+ * warning.
44
+ */
45
+ function environmentUserAgent() {
46
+ if (hasWindow) return null;
47
+ switch (runtime) {
48
+ case "node": return product("Node.js", nodeVersion);
49
+ case "bun": return product("Bun", globalVersion("Bun", ["version"]));
50
+ case "deno": return product("Deno", globalVersion("Deno", ["version", "deno"]));
51
+ case "workerd": return "Cloudflare-Workers";
52
+ case "edge-light": return "Vercel-Edge";
53
+ case "fastly": return "Fastly-Compute";
54
+ case "netlify": return "Netlify-Edge";
55
+ default: return runtime || null;
56
+ }
57
+ }
58
+ /** Header lookup that ignores case, as HTTP header names are case-insensitive. */
59
+ function hasHeader(headers, name) {
60
+ if (!headers) return false;
61
+ const wanted = name.toLowerCase();
62
+ return Object.keys(headers).some((key) => key.toLowerCase() === wanted);
63
+ }
64
+ //#endregion
65
+ //#region src/http.ts
66
+ const DEFAULT_BASE_URL = "https://api.fxtwitter.com";
67
+ function isRecord(value) {
68
+ return typeof value === "object" && value !== null;
69
+ }
70
+ /**
71
+ * Wraps ofetch with the conventions shared by the clients: `null` for 204
72
+ * responses, and `FxTwitterError` for everything that fails.
73
+ */
74
+ var HttpClient = class {
75
+ request;
76
+ constructor(options = {}) {
77
+ const headers = { ...options.headers };
78
+ if (!hasHeader(headers, "User-Agent")) {
79
+ const userAgent = environmentUserAgent();
80
+ if (userAgent) headers["User-Agent"] = userAgent;
81
+ }
82
+ const globalOptions = { defaults: defu({
83
+ baseURL: options.baseUrl,
84
+ headers,
85
+ timeout: options.timeout,
86
+ retry: options.retry,
87
+ retryDelay: options.retryDelay
88
+ }, { baseURL: DEFAULT_BASE_URL }) };
89
+ if (options.fetch) globalOptions.fetch = options.fetch;
90
+ this.request = createFetch(globalOptions);
91
+ }
92
+ /** Runs a GET request and returns the parsed JSON body, or `null` on a 204. */
93
+ async get(options) {
94
+ let response;
95
+ try {
96
+ response = await this.request.raw(options.path, {
97
+ query: options.query,
98
+ signal: options.signal
99
+ });
100
+ } catch (cause) {
101
+ throw toFxTwitterError(cause);
102
+ }
103
+ if (response.status === 204) return null;
104
+ const body = response._data;
105
+ if (!isRecord(body)) throw new FxTwitterError(nonJsonMessage(body, response.status), {
106
+ status: response.status,
107
+ body
108
+ });
109
+ return body;
110
+ }
111
+ };
112
+ function nonJsonMessage(body, status) {
113
+ return `Expected a JSON response but got ${typeof body === "string" ? body.trimStart().startsWith("<") ? "an HTML document" : "a plain text body" : `a ${typeof body} body`} (HTTP ${status}). The endpoint may not exist, or the request was redirected.`;
114
+ }
115
+ /** ofetch wraps aborts in a FetchError, so the original name lives on `cause`. */
116
+ function isAbort(error) {
117
+ const names = /* @__PURE__ */ new Set(["AbortError", "TimeoutError"]);
118
+ if (names.has(error.name)) return true;
119
+ const cause = error.cause;
120
+ return isRecord(cause) && typeof cause.name === "string" ? names.has(cause.name) : false;
121
+ }
122
+ function toFxTwitterError(cause) {
123
+ if (cause instanceof FxTwitterError) return cause;
124
+ if (!(cause instanceof Error)) return new FxTwitterError("Network request failed", { cause });
125
+ if (isAbort(cause)) return new FxTwitterError("Request aborted or timed out", { cause });
126
+ if (!(cause instanceof FetchError)) return new FxTwitterError(cause.message, { cause });
127
+ const status = cause.status ?? cause.response?.status;
128
+ const data = cause.data;
129
+ if (isRecord(data)) return new FxTwitterError(typeof data.message === "string" ? data.message : typeof data.error === "string" ? data.error : `Request failed with HTTP ${status ?? "error"}`, {
130
+ status,
131
+ code: typeof data.code === "number" ? data.code : void 0,
132
+ body: data,
133
+ cause
134
+ });
135
+ if (data !== void 0 && status !== void 0) return new FxTwitterError(nonJsonMessage(data, status), {
136
+ status,
137
+ body: data,
138
+ cause
139
+ });
140
+ return new FxTwitterError(cause.message, {
141
+ status,
142
+ body: data,
143
+ cause
144
+ });
145
+ }
146
+ //#endregion
147
+ export { FxTwitterError as n, HttpClient as t };
@@ -0,0 +1,24 @@
1
+ //#region src/http.d.ts
2
+ /** Options shared by every client, forwarded to ofetch. */
3
+ interface ClientOptions {
4
+ /** Overrides the API host. Defaults to `https://api.fxtwitter.com`. */
5
+ baseUrl?: string;
6
+ /** Custom fetch implementation (e.g. a mock, or undici in older runtimes). */
7
+ fetch?: typeof fetch;
8
+ /**
9
+ * Extra headers sent with every request.
10
+ *
11
+ * When no `User-Agent` is given, one describing the current runtime is
12
+ * added, because the API answers `401` to requests that do not identify
13
+ * themselves. Set your own here to override it.
14
+ */
15
+ headers?: Record<string, string>;
16
+ /** Abort a request after this many milliseconds. Disabled by default. */
17
+ timeout?: number;
18
+ /** Retry count for failed requests. Defaults to ofetch's `1` for GET. */
19
+ retry?: number | false;
20
+ /** Delay between retries in milliseconds. Defaults to `0`. */
21
+ retryDelay?: number;
22
+ }
23
+ //#endregion
24
+ export { ClientOptions as t };
@@ -0,0 +1,24 @@
1
+ //#region src/http.d.ts
2
+ /** Options shared by every client, forwarded to ofetch. */
3
+ interface ClientOptions {
4
+ /** Overrides the API host. Defaults to `https://api.fxtwitter.com`. */
5
+ baseUrl?: string;
6
+ /** Custom fetch implementation (e.g. a mock, or undici in older runtimes). */
7
+ fetch?: typeof fetch;
8
+ /**
9
+ * Extra headers sent with every request.
10
+ *
11
+ * When no `User-Agent` is given, one describing the current runtime is
12
+ * added, because the API answers `401` to requests that do not identify
13
+ * themselves. Set your own here to override it.
14
+ */
15
+ headers?: Record<string, string>;
16
+ /** Abort a request after this many milliseconds. Disabled by default. */
17
+ timeout?: number;
18
+ /** Retry count for failed requests. Defaults to ofetch's `1` for GET. */
19
+ retry?: number | false;
20
+ /** Delay between retries in milliseconds. Defaults to `0`. */
21
+ retryDelay?: number;
22
+ }
23
+ //#endregion
24
+ export { ClientOptions as t };
@@ -0,0 +1,158 @@
1
+ let defu = require("defu");
2
+ let ofetch = require("ofetch");
3
+ let std_env = require("std-env");
4
+ //#region src/errors.ts
5
+ /** Thrown for any non-2xx response, network failure, timeout, or unparsable body. */
6
+ var FxTwitterError = class extends Error {
7
+ status;
8
+ code;
9
+ body;
10
+ constructor(message, options = {}) {
11
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
12
+ this.name = "FxTwitterError";
13
+ this.status = options.status;
14
+ this.code = options.code;
15
+ this.body = options.body;
16
+ }
17
+ };
18
+ //#endregion
19
+ //#region src/user-agent.ts
20
+ /** Reads a version off a runtime global without assuming it exists. */
21
+ function globalVersion(name, path) {
22
+ let value = globalThis[name];
23
+ for (const key of path) {
24
+ if (typeof value !== "object" || value === null) return null;
25
+ value = value[key];
26
+ }
27
+ return typeof value === "string" ? value : null;
28
+ }
29
+ function product(name, version) {
30
+ return version ? `${name}/${version}` : name;
31
+ }
32
+ /**
33
+ * A `User-Agent` describing the runtime the request is made from, such as
34
+ * `Node.js/22.16.0`, `Bun/1.3.0` or `Cloudflare-Workers`.
35
+ *
36
+ * The API requires callers to identify themselves and answers `401` otherwise.
37
+ * Runtimes that fill the header in do so very tersely — Node sends just `node`
38
+ * — so this states the runtime and version instead. It deliberately says
39
+ * nothing about this package.
40
+ *
41
+ * Returns `null` in browsers, where `User-Agent` is a forbidden header: the
42
+ * request carries the browser's own regardless, and setting it only logs a
43
+ * warning.
44
+ */
45
+ function environmentUserAgent() {
46
+ if (std_env.hasWindow) return null;
47
+ switch (std_env.runtime) {
48
+ case "node": return product("Node.js", std_env.nodeVersion);
49
+ case "bun": return product("Bun", globalVersion("Bun", ["version"]));
50
+ case "deno": return product("Deno", globalVersion("Deno", ["version", "deno"]));
51
+ case "workerd": return "Cloudflare-Workers";
52
+ case "edge-light": return "Vercel-Edge";
53
+ case "fastly": return "Fastly-Compute";
54
+ case "netlify": return "Netlify-Edge";
55
+ default: return std_env.runtime || null;
56
+ }
57
+ }
58
+ /** Header lookup that ignores case, as HTTP header names are case-insensitive. */
59
+ function hasHeader(headers, name) {
60
+ if (!headers) return false;
61
+ const wanted = name.toLowerCase();
62
+ return Object.keys(headers).some((key) => key.toLowerCase() === wanted);
63
+ }
64
+ //#endregion
65
+ //#region src/http.ts
66
+ const DEFAULT_BASE_URL = "https://api.fxtwitter.com";
67
+ function isRecord(value) {
68
+ return typeof value === "object" && value !== null;
69
+ }
70
+ /**
71
+ * Wraps ofetch with the conventions shared by the clients: `null` for 204
72
+ * responses, and `FxTwitterError` for everything that fails.
73
+ */
74
+ var HttpClient = class {
75
+ request;
76
+ constructor(options = {}) {
77
+ const headers = { ...options.headers };
78
+ if (!hasHeader(headers, "User-Agent")) {
79
+ const userAgent = environmentUserAgent();
80
+ if (userAgent) headers["User-Agent"] = userAgent;
81
+ }
82
+ const globalOptions = { defaults: (0, defu.defu)({
83
+ baseURL: options.baseUrl,
84
+ headers,
85
+ timeout: options.timeout,
86
+ retry: options.retry,
87
+ retryDelay: options.retryDelay
88
+ }, { baseURL: DEFAULT_BASE_URL }) };
89
+ if (options.fetch) globalOptions.fetch = options.fetch;
90
+ this.request = (0, ofetch.createFetch)(globalOptions);
91
+ }
92
+ /** Runs a GET request and returns the parsed JSON body, or `null` on a 204. */
93
+ async get(options) {
94
+ let response;
95
+ try {
96
+ response = await this.request.raw(options.path, {
97
+ query: options.query,
98
+ signal: options.signal
99
+ });
100
+ } catch (cause) {
101
+ throw toFxTwitterError(cause);
102
+ }
103
+ if (response.status === 204) return null;
104
+ const body = response._data;
105
+ if (!isRecord(body)) throw new FxTwitterError(nonJsonMessage(body, response.status), {
106
+ status: response.status,
107
+ body
108
+ });
109
+ return body;
110
+ }
111
+ };
112
+ function nonJsonMessage(body, status) {
113
+ return `Expected a JSON response but got ${typeof body === "string" ? body.trimStart().startsWith("<") ? "an HTML document" : "a plain text body" : `a ${typeof body} body`} (HTTP ${status}). The endpoint may not exist, or the request was redirected.`;
114
+ }
115
+ /** ofetch wraps aborts in a FetchError, so the original name lives on `cause`. */
116
+ function isAbort(error) {
117
+ const names = /* @__PURE__ */ new Set(["AbortError", "TimeoutError"]);
118
+ if (names.has(error.name)) return true;
119
+ const cause = error.cause;
120
+ return isRecord(cause) && typeof cause.name === "string" ? names.has(cause.name) : false;
121
+ }
122
+ function toFxTwitterError(cause) {
123
+ if (cause instanceof FxTwitterError) return cause;
124
+ if (!(cause instanceof Error)) return new FxTwitterError("Network request failed", { cause });
125
+ if (isAbort(cause)) return new FxTwitterError("Request aborted or timed out", { cause });
126
+ if (!(cause instanceof ofetch.FetchError)) return new FxTwitterError(cause.message, { cause });
127
+ const status = cause.status ?? cause.response?.status;
128
+ const data = cause.data;
129
+ if (isRecord(data)) return new FxTwitterError(typeof data.message === "string" ? data.message : typeof data.error === "string" ? data.error : `Request failed with HTTP ${status ?? "error"}`, {
130
+ status,
131
+ code: typeof data.code === "number" ? data.code : void 0,
132
+ body: data,
133
+ cause
134
+ });
135
+ if (data !== void 0 && status !== void 0) return new FxTwitterError(nonJsonMessage(data, status), {
136
+ status,
137
+ body: data,
138
+ cause
139
+ });
140
+ return new FxTwitterError(cause.message, {
141
+ status,
142
+ body: data,
143
+ cause
144
+ });
145
+ }
146
+ //#endregion
147
+ Object.defineProperty(exports, "FxTwitterError", {
148
+ enumerable: true,
149
+ get: function() {
150
+ return FxTwitterError;
151
+ }
152
+ });
153
+ Object.defineProperty(exports, "HttpClient", {
154
+ enumerable: true,
155
+ get: function() {
156
+ return HttpClient;
157
+ }
158
+ });
package/dist/index.cjs ADDED
@@ -0,0 +1,50 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_v1 = require("./v1-BFKgLuJi.cjs");
3
+ const require_http = require("./http-Cs4GJeg-.cjs");
4
+ const require_v2 = require("./v2.cjs");
5
+ //#region src/client.ts
6
+ /**
7
+ * Combined client exposing every API version.
8
+ *
9
+ * Prefer importing a single version when you only need one — `FxTwitterV2`
10
+ * from `fxtwitter/v2` — and reach for this only when you use both.
11
+ *
12
+ * ```ts
13
+ * const fx = new FxTwitter();
14
+ * const { status } = await fx.v2.getStatus("20");
15
+ * const { tweet } = await fx.v1.getStatus("20");
16
+ * ```
17
+ */
18
+ var FxTwitter = class {
19
+ v2;
20
+ v1Options;
21
+ v1Client;
22
+ constructor(options = {}) {
23
+ const { v1, v2, ...shared } = options;
24
+ this.v2 = new require_v2.FxTwitterV2({
25
+ ...shared,
26
+ baseUrl: v2?.baseUrl
27
+ });
28
+ this.v1Options = {
29
+ ...shared,
30
+ baseUrl: v1?.baseUrl,
31
+ silenceDeprecationWarning: v1?.silenceDeprecationWarning
32
+ };
33
+ }
34
+ /**
35
+ * The legacy v1 client, constructed on first access so its deprecation
36
+ * warning is only emitted if you actually use it.
37
+ *
38
+ * @deprecated Use {@link FxTwitter.v2} instead.
39
+ */
40
+ get v1() {
41
+ this.v1Client ??= new require_v1.FxTwitterV1(this.v1Options);
42
+ return this.v1Client;
43
+ }
44
+ };
45
+ //#endregion
46
+ exports.FxTwitter = FxTwitter;
47
+ exports.FxTwitterError = require_http.FxTwitterError;
48
+ exports.FxTwitterV1 = require_v1.FxTwitterV1;
49
+ exports.FxTwitterV2 = require_v2.FxTwitterV2;
50
+ exports.byUserId = require_v2.byUserId;
@@ -0,0 +1,58 @@
1
+ import { t as ClientOptions } from "./http-CidaL0TV.cjs";
2
+ import { AnyMedia, Author, CommunityNote, ExternalMedia, Facet, FxTwitterV1, FxTwitterV1Options, GetStatusOptions, GetUserOptions, MosaicPhoto, Photo, Poll, PollChoice, RawText, RepostedBy, StatusMedia, StatusMessage, StatusRawText, StatusResponse, Translation, Tweet, TwitterCard, User, UserResponse, Verification, VerificationType, Video, VideoFormat, Website } from "./v1.cjs";
3
+ import { FxTwitterV2, FxTwitterV2Options, byUserId, t as v2_d_exports } from "./v2.cjs";
4
+ //#region src/client.d.ts
5
+ type FxTwitterOptions = ClientOptions & {
6
+ /** Overrides applied only to the v1 client. */
7
+ v1?: Pick<ClientOptions, "baseUrl"> & {
8
+ /** Suppresses the one-time deprecation warning emitted when `v1` is first accessed. */
9
+ silenceDeprecationWarning?: boolean;
10
+ };
11
+ /** Overrides applied only to the v2 client. */
12
+ v2?: Pick<ClientOptions, "baseUrl">;
13
+ };
14
+ /**
15
+ * Combined client exposing every API version.
16
+ *
17
+ * Prefer importing a single version when you only need one — `FxTwitterV2`
18
+ * from `fxtwitter/v2` — and reach for this only when you use both.
19
+ *
20
+ * ```ts
21
+ * const fx = new FxTwitter();
22
+ * const { status } = await fx.v2.getStatus("20");
23
+ * const { tweet } = await fx.v1.getStatus("20");
24
+ * ```
25
+ */
26
+ declare class FxTwitter {
27
+ readonly v2: FxTwitterV2;
28
+ private readonly v1Options;
29
+ private v1Client?;
30
+ constructor(options?: FxTwitterOptions);
31
+ /**
32
+ * The legacy v1 client, constructed on first access so its deprecation
33
+ * warning is only emitted if you actually use it.
34
+ *
35
+ * @deprecated Use {@link FxTwitter.v2} instead.
36
+ */
37
+ get v1(): FxTwitterV1;
38
+ }
39
+ //#endregion
40
+ //#region src/errors.d.ts
41
+ interface FxTwitterErrorOptions {
42
+ /** HTTP status code returned by the API. */
43
+ status?: number;
44
+ /** `code` field from the JSON response body (mirrors the HTTP status). */
45
+ code?: number;
46
+ /** Parsed response body, when available. */
47
+ body?: unknown;
48
+ cause?: unknown;
49
+ }
50
+ /** Thrown for any non-2xx response, network failure, timeout, or unparsable body. */
51
+ declare class FxTwitterError extends Error {
52
+ readonly status?: number;
53
+ readonly code?: number;
54
+ readonly body?: unknown;
55
+ constructor(message: string, options?: FxTwitterErrorOptions);
56
+ }
57
+ //#endregion
58
+ export { type AnyMedia, type Author, type ClientOptions, type CommunityNote, type ExternalMedia, type Facet, FxTwitter, FxTwitterError, type FxTwitterErrorOptions, type FxTwitterOptions, FxTwitterV1, type FxTwitterV1Options, FxTwitterV2, type FxTwitterV2Options, type GetStatusOptions, type GetUserOptions, type MosaicPhoto, type Photo, type Poll, type PollChoice, type RawText, type RepostedBy, type StatusMedia, type StatusMessage, type StatusRawText, type StatusResponse, type Translation, type Tweet, type TwitterCard, type User, type UserResponse, type v2_d_exports as V2, type Verification, type VerificationType, type Video, type VideoFormat, type Website, byUserId };