fxtwitter 0.0.0-reserved → 1.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,147 @@
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 { FxTwitterV1 } from "fxtwitter";
20
+
21
+ const fx = new FxTwitterV1();
22
+
23
+ const { tweet } = await fx.getStatus("20");
24
+ const { user } = await fx.getUser("jack");
25
+ ```
26
+
27
+ Types can be imported alongside the client:
28
+
29
+ ```ts
30
+ import { FxTwitterV1, type Tweet, type User } from "fxtwitter";
31
+ ```
32
+
33
+ The client is also available from the `fxtwitter/v1` subpath, which will keep
34
+ working unchanged as further API versions are added to the root:
35
+
36
+ ```ts
37
+ import { FxTwitterV1 } from "fxtwitter/v1";
38
+ ```
39
+
40
+ ## API
41
+
42
+ ### `new FxTwitterV1(options?)`
43
+
44
+ | Option | Type | Default | Description |
45
+ | --- | --- | --- | --- |
46
+ | `baseUrl` | `string` | `https://api.fxtwitter.com` | API host, e.g. a self-hosted instance |
47
+ | `headers` | `Record<string, string>` | — | Extra headers sent with every request |
48
+ | `timeout` | `number` | disabled | Abort a request after this many ms |
49
+ | `retry` | `number \| false` | `1` | Retries for failed requests |
50
+ | `retryDelay` | `number` | `0` | Delay between retries, in ms |
51
+ | `fetch` | `typeof fetch` | global `fetch` | Custom fetch implementation |
52
+
53
+ > The API requires a `User-Agent` identifying the caller and answers `401`
54
+ > without one. When you don't set one, a header describing the current runtime
55
+ > is sent — `Node.js/22.16.0`, `Bun/1.3.0`, `Cloudflare-Workers` and so on.
56
+ > Set your own to identify your app instead:
57
+ >
58
+ > ```ts
59
+ > new FxTwitterV1({
60
+ > headers: { "User-Agent": "MyApp/1.0 (+https://example.com)" },
61
+ > });
62
+ > ```
63
+ >
64
+ > Nothing is sent in browsers, where `User-Agent` is a forbidden header and the
65
+ > browser supplies its own.
66
+
67
+ ### `getStatus(id, options?)`
68
+
69
+ Fetches a single status by its snowflake ID.
70
+
71
+ ```ts
72
+ const { tweet } = await fx.getStatus("20");
73
+
74
+ // Translate the status
75
+ const { tweet } = await fx.getStatus("20", { translateTo: "es" });
76
+ tweet?.translation?.text;
77
+ ```
78
+
79
+ | Option | Type | Description |
80
+ | --- | --- | --- |
81
+ | `translateTo` | `string` | Target language — an ISO 639-1 code (`es`) or locale (`zh-cn`). Adds `translation` to the status |
82
+ | `screenName` | `string` | Author handle, for a readable URL. The API resolves the status from `id` alone and never checks it |
83
+ | `signal` | `AbortSignal` | Per-call cancellation |
84
+
85
+ Resolves to `{ code, message, tweet }`. `tweet` is `null` when the status could
86
+ not be retrieved.
87
+
88
+ ### `getUser(handle, options?)`
89
+
90
+ Fetches a user profile by handle, without a leading `@`.
91
+
92
+ ```ts
93
+ const { user } = await fx.getUser("jack");
94
+ ```
95
+
96
+ | Option | Type | Description |
97
+ | --- | --- | --- |
98
+ | `signal` | `AbortSignal` | Per-call cancellation |
99
+
100
+ Resolves to `{ code, message, user, reason? }`. `user` is absent when the
101
+ profile could not be retrieved, and `reason` is `"suspended"` for a suspended
102
+ account.
103
+
104
+ ### Errors
105
+
106
+ HTTP 4xx/5xx, network failures, timeouts, invalid input, and non-JSON responses
107
+ are thrown as `FxTwitterError`:
108
+
109
+ ```ts
110
+ import { FxTwitterError } from "fxtwitter";
111
+
112
+ try {
113
+ await fx.getStatus("20");
114
+ } catch (error) {
115
+ if (error instanceof FxTwitterError) {
116
+ error.status; // 404
117
+ error.code; // 404
118
+ error.message; // "NOT_FOUND"
119
+ error.body; // parsed response body, when available
120
+ error.cause; // underlying error, when available
121
+ }
122
+ }
123
+ ```
124
+
125
+ On success, `code` mirrors the HTTP status and `message` is one of `OK`,
126
+ `PRIVATE_TWEET`, `NOT_FOUND`, `UPSTREAM_UNAVAILABLE` or `API_FAIL`.
127
+
128
+ For a network failure or a timeout there is no response, so `status`, `code`
129
+ and `body` are all `undefined` and `message` is a generic description. The
130
+ original error is still available as `cause`.
131
+
132
+ Malformed input is rejected before a request is sent, because the API answers
133
+ those cases with HTML or a redirect rather than JSON: a status ID must be 2-20
134
+ digits, and a handle passed to `getUser` must match `\w{1,15}`. A well-formed
135
+ handle that does not exist is a normal `404`.
136
+
137
+ ## Requirements
138
+
139
+ - Node.js >= 22
140
+
141
+ ## Contributing
142
+
143
+ Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
144
+
145
+ ## License
146
+
147
+ Distributed under the [MIT License](./LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,4 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_v1 = require("./v1-Da3KbdIz.cjs");
3
+ exports.FxTwitterError = require_v1.FxTwitterError;
4
+ exports.FxTwitterV1 = require_v1.FxTwitterV1;
@@ -0,0 +1,20 @@
1
+ import { A as ClientOptions, C as User, D as Video, E as VerificationType, O as VideoFormat, S as TwitterCard, T as Verification, _ as StatusMessage, a as AnyMedia, b as Translation, c as ExternalMedia, d as Photo, f as Poll, g as StatusMedia, h as RepostedBy, i as GetUserOptions, k as Website, l as Facet, m as RawText, n as FxTwitterV1Options, o as Author, p as PollChoice, r as GetStatusOptions, s as CommunityNote, t as FxTwitterV1, u as MosaicPhoto, v as StatusRawText, w as UserResponse, x as Tweet, y as StatusResponse } from "./v1-BKUGwGbc.cjs";
2
+ //#region src/errors.d.ts
3
+ interface FxTwitterErrorOptions {
4
+ /** HTTP status code returned by the API. */
5
+ status?: number;
6
+ /** `code` field from the JSON response body (mirrors the HTTP status). */
7
+ code?: number;
8
+ /** Parsed response body, when available. */
9
+ body?: unknown;
10
+ cause?: unknown;
11
+ }
12
+ /** Thrown for any non-2xx response, network failure, timeout, or unparsable body. */
13
+ declare class FxTwitterError extends Error {
14
+ readonly status?: number;
15
+ readonly code?: number;
16
+ readonly body?: unknown;
17
+ constructor(message: string, options?: FxTwitterErrorOptions);
18
+ }
19
+ //#endregion
20
+ export { type AnyMedia, type Author, type ClientOptions, type CommunityNote, type ExternalMedia, type Facet, FxTwitterError, type FxTwitterErrorOptions, FxTwitterV1, type FxTwitterV1Options, 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 Verification, type VerificationType, type Video, type VideoFormat, type Website };
@@ -0,0 +1,20 @@
1
+ import { A as ClientOptions, C as User, D as Video, E as VerificationType, O as VideoFormat, S as TwitterCard, T as Verification, _ as StatusMessage, a as AnyMedia, b as Translation, c as ExternalMedia, d as Photo, f as Poll, g as StatusMedia, h as RepostedBy, i as GetUserOptions, k as Website, l as Facet, m as RawText, n as FxTwitterV1Options, o as Author, p as PollChoice, r as GetStatusOptions, s as CommunityNote, t as FxTwitterV1, u as MosaicPhoto, v as StatusRawText, w as UserResponse, x as Tweet, y as StatusResponse } from "./v1-BKUGwGbc.mjs";
2
+ //#region src/errors.d.ts
3
+ interface FxTwitterErrorOptions {
4
+ /** HTTP status code returned by the API. */
5
+ status?: number;
6
+ /** `code` field from the JSON response body (mirrors the HTTP status). */
7
+ code?: number;
8
+ /** Parsed response body, when available. */
9
+ body?: unknown;
10
+ cause?: unknown;
11
+ }
12
+ /** Thrown for any non-2xx response, network failure, timeout, or unparsable body. */
13
+ declare class FxTwitterError extends Error {
14
+ readonly status?: number;
15
+ readonly code?: number;
16
+ readonly body?: unknown;
17
+ constructor(message: string, options?: FxTwitterErrorOptions);
18
+ }
19
+ //#endregion
20
+ export { type AnyMedia, type Author, type ClientOptions, type CommunityNote, type ExternalMedia, type Facet, FxTwitterError, type FxTwitterErrorOptions, FxTwitterV1, type FxTwitterV1Options, 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 Verification, type VerificationType, type Video, type VideoFormat, type Website };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as FxTwitterError, t as FxTwitterV1 } from "./v1-KUPCoHD9.mjs";
2
+ export { FxTwitterError, FxTwitterV1 };
@@ -0,0 +1,267 @@
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
+ //#region src/types/v1.d.ts
25
+ interface Facet {
26
+ /** e.g. `url`, `mention`, `hashtag`, `media`. */
27
+ type: string;
28
+ /** Start/end UTF-16 indices. */
29
+ indices: [number, number];
30
+ original?: string;
31
+ replacement?: string;
32
+ display?: string;
33
+ id?: string;
34
+ }
35
+ interface RawText {
36
+ text: string;
37
+ facets: Facet[];
38
+ }
39
+ interface StatusRawText extends RawText {
40
+ display_text_range: [number, number];
41
+ }
42
+ interface Website {
43
+ url: string;
44
+ display_url: string;
45
+ }
46
+ type VerificationType = "organization" | "government" | "individual" | null;
47
+ interface Verification {
48
+ verified: boolean;
49
+ type: VerificationType;
50
+ verified_at?: string | null;
51
+ identity_verified?: boolean;
52
+ verified_by?: string;
53
+ }
54
+ /** The author embedded in a status. Same shape as {@link User}, plus `avatar_color`. */
55
+ interface Author {
56
+ id: string;
57
+ name: string;
58
+ screen_name: string;
59
+ url: string;
60
+ description: string;
61
+ raw_description: RawText;
62
+ location: string;
63
+ avatar_url: string | null;
64
+ banner_url: string | null;
65
+ /** @deprecated Always `null` upstream. */
66
+ avatar_color?: string | null;
67
+ followers: number;
68
+ following: number;
69
+ likes: number;
70
+ media_count: number;
71
+ joined: string;
72
+ protected: boolean;
73
+ website: Website | null;
74
+ verification?: Verification;
75
+ }
76
+ /** The user returned by `GET /:screen_name`. Uses `tweets` where v2 uses `statuses`. */
77
+ interface User extends Author {
78
+ /** Status count. Named `statuses` in the v2 API. */
79
+ tweets: number;
80
+ }
81
+ interface Translation {
82
+ text: string;
83
+ source_lang: string;
84
+ source_lang_en: string;
85
+ target_lang: string;
86
+ provider: string;
87
+ }
88
+ interface PollChoice {
89
+ label: string;
90
+ count: number;
91
+ /** 0-100. */
92
+ percentage: number;
93
+ }
94
+ interface Poll {
95
+ choices: PollChoice[];
96
+ total_votes: number;
97
+ ends_at: string;
98
+ time_left_en: string;
99
+ }
100
+ interface VideoFormat {
101
+ container?: "mp4" | "webm" | "m3u8";
102
+ codec?: "h264" | "hevc" | "vp9" | "av1";
103
+ bitrate?: number;
104
+ url: string;
105
+ size?: number;
106
+ height?: number;
107
+ width?: number;
108
+ }
109
+ interface Photo {
110
+ id?: string;
111
+ /** MIME type, e.g. `image/jpeg`. */
112
+ format?: string;
113
+ type: "photo" | "gif";
114
+ url: string;
115
+ width: number;
116
+ height: number;
117
+ transcode_url?: string | null;
118
+ altText?: string;
119
+ }
120
+ interface Video {
121
+ id?: string;
122
+ /** MIME type, e.g. `video/mp4`. */
123
+ format?: string;
124
+ type: "video" | "gif";
125
+ url: string;
126
+ width: number;
127
+ height: number;
128
+ thumbnail_url?: string | null;
129
+ transcode_url?: string | null;
130
+ duration: number;
131
+ filesize?: number;
132
+ formats?: VideoFormat[];
133
+ }
134
+ interface MosaicPhoto {
135
+ id?: string;
136
+ type: "mosaic_photo";
137
+ url?: string;
138
+ width: number;
139
+ height: number;
140
+ formats: {
141
+ webp: string;
142
+ jpeg: string;
143
+ };
144
+ }
145
+ interface ExternalMedia {
146
+ /** Currently always `video`. */
147
+ type: string;
148
+ url: string;
149
+ thumbnail_url?: string;
150
+ height?: number;
151
+ width?: number;
152
+ duration?: number;
153
+ }
154
+ type AnyMedia = Photo | Video | MosaicPhoto;
155
+ interface StatusMedia {
156
+ /** Preserves the original ordering across photos and videos. */
157
+ all?: AnyMedia[];
158
+ external?: ExternalMedia;
159
+ photos?: Photo[];
160
+ videos?: Video[];
161
+ mosaic?: MosaicPhoto;
162
+ }
163
+ interface CommunityNote {
164
+ text: string;
165
+ facets: Facet[];
166
+ }
167
+ interface RepostedBy {
168
+ id: string;
169
+ name: string;
170
+ screen_name: string;
171
+ avatar_url?: string | null;
172
+ url?: string;
173
+ }
174
+ type TwitterCard = "tweet" | "summary" | "summary_large_image" | "player";
175
+ interface Tweet {
176
+ id: string;
177
+ url: string;
178
+ text: string;
179
+ raw_text: StatusRawText;
180
+ created_at: string;
181
+ created_timestamp: number;
182
+ author: Author;
183
+ likes: number;
184
+ /** Named `reposts` in the v2 API. */
185
+ retweets: number;
186
+ replies: number;
187
+ quotes: number;
188
+ views: number | null;
189
+ bookmarks?: number | null;
190
+ lang: string | null;
191
+ /** Screen name of the account being replied to. An object in the v2 API. */
192
+ replying_to: string | null;
193
+ /** Status ID being replied to. Folded into `replying_to` in the v2 API. */
194
+ replying_to_status: string | null;
195
+ source: string | null;
196
+ /** Named `embed_card` in the v2 API. */
197
+ twitter_card: TwitterCard;
198
+ provider?: string;
199
+ possibly_sensitive?: boolean;
200
+ is_note_tweet: boolean;
201
+ community_note: CommunityNote | null;
202
+ reposted_by: RepostedBy | null;
203
+ /** @deprecated Always `null` upstream. */
204
+ color?: string | null;
205
+ quote?: Tweet;
206
+ poll?: Poll;
207
+ /** Only present when a translation target was requested. */
208
+ translation?: Translation;
209
+ media?: StatusMedia;
210
+ }
211
+ /**
212
+ * `message` values the API maps onto its status codes: 200, 401, 404, 503
213
+ * and 500 respectively.
214
+ */
215
+ type StatusMessage = "OK" | "PRIVATE_TWEET" | "NOT_FOUND" | "UPSTREAM_UNAVAILABLE" | "API_FAIL";
216
+ interface StatusResponse {
217
+ /** Mirrors the HTTP status code. */
218
+ code: number;
219
+ message: StatusMessage | string;
220
+ /** `null` when the status could not be retrieved. */
221
+ tweet: Tweet | null;
222
+ }
223
+ interface UserResponse {
224
+ /** Mirrors the HTTP status code. */
225
+ code: number;
226
+ message: string;
227
+ /** Omitted entirely when the user could not be retrieved. */
228
+ user?: User;
229
+ /** Set to `suspended` when the account is suspended. */
230
+ reason?: "suspended";
231
+ }
232
+ //#endregion
233
+ //#region src/v1.d.ts
234
+ type FxTwitterV1Options = ClientOptions;
235
+ interface GetStatusOptions {
236
+ /**
237
+ * Included in the URL for readability only. The API resolves the status from
238
+ * `id` alone and never validates this, so a wrong handle still succeeds.
239
+ */
240
+ screenName?: string;
241
+ /**
242
+ * Language to translate the status into, as an ISO 639-1 code (`es`) or a
243
+ * locale (`zh-cn`). Adds a `translation` object to the returned status.
244
+ */
245
+ translateTo?: string;
246
+ /** Per-call cancellation, composed with the client-level `timeout`. */
247
+ signal?: AbortSignal;
248
+ }
249
+ interface GetUserOptions {
250
+ signal?: AbortSignal;
251
+ }
252
+ /** Client for the FxTwitter v1 API. */
253
+ declare class FxTwitterV1 {
254
+ private readonly http;
255
+ constructor(options?: FxTwitterV1Options);
256
+ /**
257
+ * Fetches a single status by its snowflake ID.
258
+ *
259
+ * `GET /status/:id`, `GET /status/:id/:language`,
260
+ * `GET /:handle/status/:id`, `GET /:handle/status/:id/:language`
261
+ */
262
+ getStatus(id: string, options?: GetStatusOptions): Promise<StatusResponse>;
263
+ /** Fetches a user profile by handle (`GET /:handle`). */
264
+ getUser(screenName: string, options?: GetUserOptions): Promise<UserResponse>;
265
+ }
266
+ //#endregion
267
+ export { ClientOptions as A, User as C, Video as D, VerificationType as E, VideoFormat as O, TwitterCard as S, Verification as T, StatusMessage as _, AnyMedia as a, Translation as b, ExternalMedia as c, Photo as d, Poll as f, StatusMedia as g, RepostedBy as h, GetUserOptions as i, Website as k, Facet as l, RawText as m, FxTwitterV1Options as n, Author as o, PollChoice as p, GetStatusOptions as r, CommunityNote as s, FxTwitterV1 as t, MosaicPhoto as u, StatusRawText as v, UserResponse as w, Tweet as x, StatusResponse as y };
@@ -0,0 +1,267 @@
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
+ //#region src/types/v1.d.ts
25
+ interface Facet {
26
+ /** e.g. `url`, `mention`, `hashtag`, `media`. */
27
+ type: string;
28
+ /** Start/end UTF-16 indices. */
29
+ indices: [number, number];
30
+ original?: string;
31
+ replacement?: string;
32
+ display?: string;
33
+ id?: string;
34
+ }
35
+ interface RawText {
36
+ text: string;
37
+ facets: Facet[];
38
+ }
39
+ interface StatusRawText extends RawText {
40
+ display_text_range: [number, number];
41
+ }
42
+ interface Website {
43
+ url: string;
44
+ display_url: string;
45
+ }
46
+ type VerificationType = "organization" | "government" | "individual" | null;
47
+ interface Verification {
48
+ verified: boolean;
49
+ type: VerificationType;
50
+ verified_at?: string | null;
51
+ identity_verified?: boolean;
52
+ verified_by?: string;
53
+ }
54
+ /** The author embedded in a status. Same shape as {@link User}, plus `avatar_color`. */
55
+ interface Author {
56
+ id: string;
57
+ name: string;
58
+ screen_name: string;
59
+ url: string;
60
+ description: string;
61
+ raw_description: RawText;
62
+ location: string;
63
+ avatar_url: string | null;
64
+ banner_url: string | null;
65
+ /** @deprecated Always `null` upstream. */
66
+ avatar_color?: string | null;
67
+ followers: number;
68
+ following: number;
69
+ likes: number;
70
+ media_count: number;
71
+ joined: string;
72
+ protected: boolean;
73
+ website: Website | null;
74
+ verification?: Verification;
75
+ }
76
+ /** The user returned by `GET /:screen_name`. Uses `tweets` where v2 uses `statuses`. */
77
+ interface User extends Author {
78
+ /** Status count. Named `statuses` in the v2 API. */
79
+ tweets: number;
80
+ }
81
+ interface Translation {
82
+ text: string;
83
+ source_lang: string;
84
+ source_lang_en: string;
85
+ target_lang: string;
86
+ provider: string;
87
+ }
88
+ interface PollChoice {
89
+ label: string;
90
+ count: number;
91
+ /** 0-100. */
92
+ percentage: number;
93
+ }
94
+ interface Poll {
95
+ choices: PollChoice[];
96
+ total_votes: number;
97
+ ends_at: string;
98
+ time_left_en: string;
99
+ }
100
+ interface VideoFormat {
101
+ container?: "mp4" | "webm" | "m3u8";
102
+ codec?: "h264" | "hevc" | "vp9" | "av1";
103
+ bitrate?: number;
104
+ url: string;
105
+ size?: number;
106
+ height?: number;
107
+ width?: number;
108
+ }
109
+ interface Photo {
110
+ id?: string;
111
+ /** MIME type, e.g. `image/jpeg`. */
112
+ format?: string;
113
+ type: "photo" | "gif";
114
+ url: string;
115
+ width: number;
116
+ height: number;
117
+ transcode_url?: string | null;
118
+ altText?: string;
119
+ }
120
+ interface Video {
121
+ id?: string;
122
+ /** MIME type, e.g. `video/mp4`. */
123
+ format?: string;
124
+ type: "video" | "gif";
125
+ url: string;
126
+ width: number;
127
+ height: number;
128
+ thumbnail_url?: string | null;
129
+ transcode_url?: string | null;
130
+ duration: number;
131
+ filesize?: number;
132
+ formats?: VideoFormat[];
133
+ }
134
+ interface MosaicPhoto {
135
+ id?: string;
136
+ type: "mosaic_photo";
137
+ url?: string;
138
+ width: number;
139
+ height: number;
140
+ formats: {
141
+ webp: string;
142
+ jpeg: string;
143
+ };
144
+ }
145
+ interface ExternalMedia {
146
+ /** Currently always `video`. */
147
+ type: string;
148
+ url: string;
149
+ thumbnail_url?: string;
150
+ height?: number;
151
+ width?: number;
152
+ duration?: number;
153
+ }
154
+ type AnyMedia = Photo | Video | MosaicPhoto;
155
+ interface StatusMedia {
156
+ /** Preserves the original ordering across photos and videos. */
157
+ all?: AnyMedia[];
158
+ external?: ExternalMedia;
159
+ photos?: Photo[];
160
+ videos?: Video[];
161
+ mosaic?: MosaicPhoto;
162
+ }
163
+ interface CommunityNote {
164
+ text: string;
165
+ facets: Facet[];
166
+ }
167
+ interface RepostedBy {
168
+ id: string;
169
+ name: string;
170
+ screen_name: string;
171
+ avatar_url?: string | null;
172
+ url?: string;
173
+ }
174
+ type TwitterCard = "tweet" | "summary" | "summary_large_image" | "player";
175
+ interface Tweet {
176
+ id: string;
177
+ url: string;
178
+ text: string;
179
+ raw_text: StatusRawText;
180
+ created_at: string;
181
+ created_timestamp: number;
182
+ author: Author;
183
+ likes: number;
184
+ /** Named `reposts` in the v2 API. */
185
+ retweets: number;
186
+ replies: number;
187
+ quotes: number;
188
+ views: number | null;
189
+ bookmarks?: number | null;
190
+ lang: string | null;
191
+ /** Screen name of the account being replied to. An object in the v2 API. */
192
+ replying_to: string | null;
193
+ /** Status ID being replied to. Folded into `replying_to` in the v2 API. */
194
+ replying_to_status: string | null;
195
+ source: string | null;
196
+ /** Named `embed_card` in the v2 API. */
197
+ twitter_card: TwitterCard;
198
+ provider?: string;
199
+ possibly_sensitive?: boolean;
200
+ is_note_tweet: boolean;
201
+ community_note: CommunityNote | null;
202
+ reposted_by: RepostedBy | null;
203
+ /** @deprecated Always `null` upstream. */
204
+ color?: string | null;
205
+ quote?: Tweet;
206
+ poll?: Poll;
207
+ /** Only present when a translation target was requested. */
208
+ translation?: Translation;
209
+ media?: StatusMedia;
210
+ }
211
+ /**
212
+ * `message` values the API maps onto its status codes: 200, 401, 404, 503
213
+ * and 500 respectively.
214
+ */
215
+ type StatusMessage = "OK" | "PRIVATE_TWEET" | "NOT_FOUND" | "UPSTREAM_UNAVAILABLE" | "API_FAIL";
216
+ interface StatusResponse {
217
+ /** Mirrors the HTTP status code. */
218
+ code: number;
219
+ message: StatusMessage | string;
220
+ /** `null` when the status could not be retrieved. */
221
+ tweet: Tweet | null;
222
+ }
223
+ interface UserResponse {
224
+ /** Mirrors the HTTP status code. */
225
+ code: number;
226
+ message: string;
227
+ /** Omitted entirely when the user could not be retrieved. */
228
+ user?: User;
229
+ /** Set to `suspended` when the account is suspended. */
230
+ reason?: "suspended";
231
+ }
232
+ //#endregion
233
+ //#region src/v1.d.ts
234
+ type FxTwitterV1Options = ClientOptions;
235
+ interface GetStatusOptions {
236
+ /**
237
+ * Included in the URL for readability only. The API resolves the status from
238
+ * `id` alone and never validates this, so a wrong handle still succeeds.
239
+ */
240
+ screenName?: string;
241
+ /**
242
+ * Language to translate the status into, as an ISO 639-1 code (`es`) or a
243
+ * locale (`zh-cn`). Adds a `translation` object to the returned status.
244
+ */
245
+ translateTo?: string;
246
+ /** Per-call cancellation, composed with the client-level `timeout`. */
247
+ signal?: AbortSignal;
248
+ }
249
+ interface GetUserOptions {
250
+ signal?: AbortSignal;
251
+ }
252
+ /** Client for the FxTwitter v1 API. */
253
+ declare class FxTwitterV1 {
254
+ private readonly http;
255
+ constructor(options?: FxTwitterV1Options);
256
+ /**
257
+ * Fetches a single status by its snowflake ID.
258
+ *
259
+ * `GET /status/:id`, `GET /status/:id/:language`,
260
+ * `GET /:handle/status/:id`, `GET /:handle/status/:id/:language`
261
+ */
262
+ getStatus(id: string, options?: GetStatusOptions): Promise<StatusResponse>;
263
+ /** Fetches a user profile by handle (`GET /:handle`). */
264
+ getUser(screenName: string, options?: GetUserOptions): Promise<UserResponse>;
265
+ }
266
+ //#endregion
267
+ export { ClientOptions as A, User as C, Video as D, VerificationType as E, VideoFormat as O, TwitterCard as S, Verification as T, StatusMessage as _, AnyMedia as a, Translation as b, ExternalMedia as c, Photo as d, Poll as f, StatusMedia as g, RepostedBy as h, GetUserOptions as i, Website as k, Facet as l, RawText as m, FxTwitterV1Options as n, Author as o, PollChoice as p, GetStatusOptions as r, CommunityNote as s, FxTwitterV1 as t, MosaicPhoto as u, StatusRawText as v, UserResponse as w, Tweet as x, StatusResponse as y };
@@ -0,0 +1,213 @@
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
+ //#region src/v1.ts
148
+ /**
149
+ * The API resolves a status from the first 2-20 digit run in the path segment,
150
+ * and answers anything else with its embed HTML at HTTP 200 rather than JSON.
151
+ */
152
+ const STATUS_ID_PATTERN = /^\d{2,20}$/;
153
+ /**
154
+ * Handles are matched as `\w{1,15}` upstream; anything else is redirected to
155
+ * the project's GitHub page instead of returning JSON.
156
+ */
157
+ const HANDLE_PATTERN = /^\w{1,15}$/;
158
+ /** Client for the FxTwitter v1 API. */
159
+ var FxTwitterV1 = class {
160
+ http;
161
+ constructor(options = {}) {
162
+ this.http = new HttpClient(options);
163
+ }
164
+ /**
165
+ * Fetches a single status by its snowflake ID.
166
+ *
167
+ * `GET /status/:id`, `GET /status/:id/:language`,
168
+ * `GET /:handle/status/:id`, `GET /:handle/status/:id/:language`
169
+ */
170
+ async getStatus(id, options = {}) {
171
+ if (!STATUS_ID_PATTERN.test(id)) throw new FxTwitterError(`Invalid status ID ${JSON.stringify(id)}: expected 2-20 digits`);
172
+ if (options.screenName !== void 0 && options.screenName.trim() === "") throw new FxTwitterError("screenName must not be empty");
173
+ const segments = [
174
+ options.screenName,
175
+ "status",
176
+ id,
177
+ options.translateTo
178
+ ].filter((segment) => segment !== void 0);
179
+ return requireBody(await this.http.get({
180
+ path: `/${segments.map(encodeURIComponent).join("/")}`,
181
+ signal: options.signal
182
+ }));
183
+ }
184
+ /** Fetches a user profile by handle (`GET /:handle`). */
185
+ async getUser(screenName, options = {}) {
186
+ assertHandle(screenName);
187
+ return requireBody(await this.http.get({
188
+ path: `/${encodeURIComponent(screenName)}`,
189
+ signal: options.signal
190
+ }));
191
+ }
192
+ };
193
+ function assertHandle(handle) {
194
+ if (!HANDLE_PATTERN.test(handle)) throw new FxTwitterError(`Invalid handle ${JSON.stringify(handle)}: expected 1-15 letters, digits or underscores, without a leading @`);
195
+ }
196
+ /** v1 never answers 204, so an empty body here means something went wrong. */
197
+ function requireBody(body) {
198
+ if (body === null) throw new FxTwitterError("The API returned an empty response");
199
+ return body;
200
+ }
201
+ //#endregion
202
+ Object.defineProperty(exports, "FxTwitterError", {
203
+ enumerable: true,
204
+ get: function() {
205
+ return FxTwitterError;
206
+ }
207
+ });
208
+ Object.defineProperty(exports, "FxTwitterV1", {
209
+ enumerable: true,
210
+ get: function() {
211
+ return FxTwitterV1;
212
+ }
213
+ });
@@ -0,0 +1,202 @@
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
+ //#region src/v1.ts
148
+ /**
149
+ * The API resolves a status from the first 2-20 digit run in the path segment,
150
+ * and answers anything else with its embed HTML at HTTP 200 rather than JSON.
151
+ */
152
+ const STATUS_ID_PATTERN = /^\d{2,20}$/;
153
+ /**
154
+ * Handles are matched as `\w{1,15}` upstream; anything else is redirected to
155
+ * the project's GitHub page instead of returning JSON.
156
+ */
157
+ const HANDLE_PATTERN = /^\w{1,15}$/;
158
+ /** Client for the FxTwitter v1 API. */
159
+ var FxTwitterV1 = class {
160
+ http;
161
+ constructor(options = {}) {
162
+ this.http = new HttpClient(options);
163
+ }
164
+ /**
165
+ * Fetches a single status by its snowflake ID.
166
+ *
167
+ * `GET /status/:id`, `GET /status/:id/:language`,
168
+ * `GET /:handle/status/:id`, `GET /:handle/status/:id/:language`
169
+ */
170
+ async getStatus(id, options = {}) {
171
+ if (!STATUS_ID_PATTERN.test(id)) throw new FxTwitterError(`Invalid status ID ${JSON.stringify(id)}: expected 2-20 digits`);
172
+ if (options.screenName !== void 0 && options.screenName.trim() === "") throw new FxTwitterError("screenName must not be empty");
173
+ const segments = [
174
+ options.screenName,
175
+ "status",
176
+ id,
177
+ options.translateTo
178
+ ].filter((segment) => segment !== void 0);
179
+ return requireBody(await this.http.get({
180
+ path: `/${segments.map(encodeURIComponent).join("/")}`,
181
+ signal: options.signal
182
+ }));
183
+ }
184
+ /** Fetches a user profile by handle (`GET /:handle`). */
185
+ async getUser(screenName, options = {}) {
186
+ assertHandle(screenName);
187
+ return requireBody(await this.http.get({
188
+ path: `/${encodeURIComponent(screenName)}`,
189
+ signal: options.signal
190
+ }));
191
+ }
192
+ };
193
+ function assertHandle(handle) {
194
+ if (!HANDLE_PATTERN.test(handle)) throw new FxTwitterError(`Invalid handle ${JSON.stringify(handle)}: expected 1-15 letters, digits or underscores, without a leading @`);
195
+ }
196
+ /** v1 never answers 204, so an empty body here means something went wrong. */
197
+ function requireBody(body) {
198
+ if (body === null) throw new FxTwitterError("The API returned an empty response");
199
+ return body;
200
+ }
201
+ //#endregion
202
+ export { FxTwitterError as n, FxTwitterV1 as t };
package/dist/v1.cjs ADDED
@@ -0,0 +1,3 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_v1 = require("./v1-Da3KbdIz.cjs");
3
+ exports.FxTwitterV1 = require_v1.FxTwitterV1;
package/dist/v1.d.cts ADDED
@@ -0,0 +1,2 @@
1
+ import { C as User, D as Video, E as VerificationType, O as VideoFormat, S as TwitterCard, T as Verification, _ as StatusMessage, a as AnyMedia, b as Translation, c as ExternalMedia, d as Photo, f as Poll, g as StatusMedia, h as RepostedBy, i as GetUserOptions, k as Website, l as Facet, m as RawText, n as FxTwitterV1Options, o as Author, p as PollChoice, r as GetStatusOptions, s as CommunityNote, t as FxTwitterV1, u as MosaicPhoto, v as StatusRawText, w as UserResponse, x as Tweet, y as StatusResponse } from "./v1-BKUGwGbc.cjs";
2
+ export { type AnyMedia, type Author, type CommunityNote, type ExternalMedia, type Facet, FxTwitterV1, FxTwitterV1Options, GetStatusOptions, 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 Verification, type VerificationType, type Video, type VideoFormat, type Website };
package/dist/v1.d.mts ADDED
@@ -0,0 +1,2 @@
1
+ import { C as User, D as Video, E as VerificationType, O as VideoFormat, S as TwitterCard, T as Verification, _ as StatusMessage, a as AnyMedia, b as Translation, c as ExternalMedia, d as Photo, f as Poll, g as StatusMedia, h as RepostedBy, i as GetUserOptions, k as Website, l as Facet, m as RawText, n as FxTwitterV1Options, o as Author, p as PollChoice, r as GetStatusOptions, s as CommunityNote, t as FxTwitterV1, u as MosaicPhoto, v as StatusRawText, w as UserResponse, x as Tweet, y as StatusResponse } from "./v1-BKUGwGbc.mjs";
2
+ export { type AnyMedia, type Author, type CommunityNote, type ExternalMedia, type Facet, FxTwitterV1, FxTwitterV1Options, GetStatusOptions, 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 Verification, type VerificationType, type Video, type VideoFormat, type Website };
package/dist/v1.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { t as FxTwitterV1 } from "./v1-KUPCoHD9.mjs";
2
+ export { FxTwitterV1 };
package/package.json CHANGED
@@ -1,19 +1,84 @@
1
1
  {
2
2
  "name": "fxtwitter",
3
- "version": "0.0.0-reserved",
4
- "main": "index.js",
5
- "scripts": {
6
- "help": "node scripts/main.js --help",
7
- "reserve": "node scripts/main.js"
3
+ "version": "1.0.0",
4
+ "description": "Typed wrapper for the FxTwitter API.",
5
+ "keywords": [
6
+ "x",
7
+ "twitter",
8
+ "fxtwitter",
9
+ "wrapper",
10
+ "api"
11
+ ],
12
+ "homepage": "https://github.com/otnc/fxtwitter-wrapper#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/otnc/fxtwitter-wrapper/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/otnc/fxtwitter-wrapper.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "otoneko.",
22
+ "type": "module",
23
+ "main": "dist/index.cjs",
24
+ "module": "dist/index.mjs",
25
+ "types": "dist/index.d.mts",
26
+ "exports": {
27
+ ".": {
28
+ "types": {
29
+ "import": "./dist/index.d.mts",
30
+ "require": "./dist/index.d.cts"
31
+ },
32
+ "import": "./dist/index.mjs",
33
+ "require": "./dist/index.cjs"
34
+ },
35
+ "./v1": {
36
+ "types": {
37
+ "import": "./dist/v1.d.mts",
38
+ "require": "./dist/v1.d.cts"
39
+ },
40
+ "import": "./dist/v1.mjs",
41
+ "require": "./dist/v1.cjs"
42
+ },
43
+ "./package.json": "./package.json"
8
44
  },
9
45
  "files": [
10
- "index.js"
46
+ "dist"
11
47
  ],
12
- "keywords": [],
13
- "author": "otoneko.",
14
- "description": "[Reserved]",
48
+ "engines": {
49
+ "node": ">=22"
50
+ },
51
+ "scripts": {
52
+ "setup": "node scripts/setup.js",
53
+ "build": "tsdown",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "test:coverage": "vitest run --coverage",
57
+ "typecheck": "tsc --noEmit",
58
+ "format": "prettier --write .",
59
+ "format:check": "prettier --check .",
60
+ "lint": "eslint .",
61
+ "lint:fix": "eslint --fix .",
62
+ "check": "prettier --write . && eslint --fix .",
63
+ "ci": "prettier --check . && eslint .",
64
+ "prepublishOnly": "npm run build"
65
+ },
66
+ "dependencies": {
67
+ "defu": "^6.1.7",
68
+ "ofetch": "^1.5.1",
69
+ "std-env": "^4.2.0"
70
+ },
15
71
  "devDependencies": {
16
- "consola": "^3.4.2",
17
- "dotenv": "^17.3.1"
72
+ "@eslint/js": "^10.0.1",
73
+ "@types/node": "^22.20.1",
74
+ "@vitest/coverage-v8": "^4.1.10",
75
+ "eslint": "^10.8.0",
76
+ "eslint-config-prettier": "^10.1.8",
77
+ "globals": "^17.8.0",
78
+ "prettier": "^3.9.6",
79
+ "tsdown": "^0.22.14",
80
+ "typescript": "^6.0.3",
81
+ "typescript-eslint": "^8.65.0",
82
+ "vitest": "^4.1.10"
18
83
  }
19
- }
84
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = console.log("fxtwitter is reserved for otoneko.");