tredi-sdk 0.1.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.
@@ -0,0 +1,798 @@
1
+ /**
2
+ * Logging hooks. The SDK never writes to the console itself; it calls the
3
+ * injected logger so the host app controls transport and level. The SDK's own
4
+ * log calls (in `http.ts`) only ever include a redacted URL — never raw
5
+ * request params or the access token — via {@link redactUrl}.
6
+ *
7
+ * {@link redactParams} is exported as a standalone utility for host apps that
8
+ * build a custom logger and want to log request params (not just the URL)
9
+ * safely; the SDK itself doesn't need it since it never logs params.
10
+ */
11
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
12
+ interface LogContext {
13
+ [key: string]: unknown;
14
+ }
15
+ interface Logger {
16
+ /**
17
+ * Receives a structured log event. Implementations must be non-throwing and
18
+ * fast; the SDK calls this synchronously on the request path.
19
+ */
20
+ log(level: LogLevel, message: string, context?: LogContext): void;
21
+ }
22
+ /** Discards all logs. Default when no logger is configured. */
23
+ declare const noopLogger: Logger;
24
+ /**
25
+ * Returns a copy of `params` with sensitive values masked. Non-sensitive
26
+ * values are preserved so logs stay useful for debugging.
27
+ */
28
+ declare function redactParams(params: Record<string, unknown> | undefined): Record<string, unknown>;
29
+ /**
30
+ * Masks sensitive query-string values in a URL. Falsy/relative inputs are
31
+ * returned with a best-effort regex redaction so we never throw on the log
32
+ * path.
33
+ */
34
+ declare function redactUrl(url: string): string;
35
+
36
+ /**
37
+ * The single HTTP engine used by both the client and the OAuth helpers.
38
+ *
39
+ * Responsibilities: build the request, enforce a timeout, send via the injected
40
+ * `fetch`, parse the response, map failures to typed errors, retry idempotent
41
+ * failures with exponential backoff + jitter, and emit redacted logs.
42
+ *
43
+ * Retry safety: non-idempotent (POST) requests are only retried when the server
44
+ * never processed them (HTTP 429) or no response was received (network error).
45
+ * A POST timeout or 5xx is *not* retried — it may have already taken effect
46
+ * (e.g. a published post), and re-sending could duplicate it.
47
+ */
48
+
49
+ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
50
+ type HttpMethod = 'GET' | 'POST' | 'DELETE';
51
+ interface RetryConfig {
52
+ maxRetries: number;
53
+ initialDelayMs: number;
54
+ maxDelayMs: number;
55
+ backoffFactor: number;
56
+ }
57
+
58
+ /**
59
+ * Shared contract between the client and its resource modules. Resources depend
60
+ * only on this narrow interface (not the concrete `ThreadsClient`), which keeps
61
+ * the dependency graph acyclic and makes resources trivial to unit test.
62
+ */
63
+
64
+ interface ResourceRequest {
65
+ method: HttpMethod;
66
+ /** Path appended after the version segment, e.g. `/me/threads`. */
67
+ path: string;
68
+ params?: Record<string, unknown>;
69
+ /** Optional per-call cancellation. */
70
+ signal?: AbortSignal;
71
+ }
72
+ interface ThreadsRequester {
73
+ /** Default node id for user-scoped endpoints (the configured user, or `me`). */
74
+ readonly userNode: string;
75
+ request<T>(req: ResourceRequest): Promise<T>;
76
+ }
77
+
78
+ /**
79
+ * Public API types. Field names mirror the Threads API exactly (snake_case);
80
+ * all object fields are optional because the API only returns the fields you
81
+ * request via `fields=`. Enum unions list only documented values.
82
+ */
83
+ /** `media_type` accepted when creating a publish container. */
84
+ type MediaContainerType = 'TEXT' | 'IMAGE' | 'VIDEO' | 'CAROUSEL';
85
+ /** `media_type` returned when reading a media object (differs from input). */
86
+ type MediaType = 'TEXT_POST' | 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'AUDIO' | 'REPOST_FACADE';
87
+ /** Who is allowed to reply to a post. */
88
+ type ReplyControl = 'everyone' | 'accounts_you_follow' | 'mentioned_only' | 'parent_post_author_only' | 'followers_only';
89
+ /** Lifecycle status of a publish container. */
90
+ type ContainerStatus = 'EXPIRED' | 'ERROR' | 'FINISHED' | 'IN_PROGRESS' | 'PUBLISHED';
91
+ /** Generic cursor-paginated list response. */
92
+ interface Paginated<T> {
93
+ data: T[];
94
+ paging?: {
95
+ cursors?: {
96
+ before?: string;
97
+ after?: string;
98
+ };
99
+ next?: string;
100
+ previous?: string;
101
+ };
102
+ }
103
+ /** A Threads user profile. */
104
+ interface ThreadsProfile {
105
+ id?: string;
106
+ username?: string;
107
+ name?: string;
108
+ threads_profile_picture_url?: string;
109
+ threads_biography?: string;
110
+ is_verified?: boolean;
111
+ }
112
+ /** A Threads media object (post). */
113
+ interface ThreadsMedia {
114
+ id?: string;
115
+ media_product_type?: string;
116
+ media_type?: MediaType;
117
+ media_url?: string;
118
+ permalink?: string;
119
+ owner?: {
120
+ id: string;
121
+ };
122
+ username?: string;
123
+ text?: string;
124
+ /** ISO 8601 timestamp. */
125
+ timestamp?: string;
126
+ shortcode?: string;
127
+ thumbnail_url?: string;
128
+ children?: {
129
+ data: ThreadsMedia[];
130
+ };
131
+ is_quote_post?: boolean;
132
+ quoted_post?: ThreadsMedia;
133
+ reposted_post?: ThreadsMedia;
134
+ alt_text?: string;
135
+ link_attachment_url?: string;
136
+ gif_url?: string;
137
+ topic_tag?: string;
138
+ is_verified?: boolean;
139
+ profile_picture_url?: string;
140
+ }
141
+ /** A reply object. Shares most fields with media plus reply-specific ones. */
142
+ interface ThreadsReply {
143
+ id?: string;
144
+ text?: string;
145
+ timestamp?: string;
146
+ media_product_type?: string;
147
+ media_type?: MediaType;
148
+ media_url?: string;
149
+ shortcode?: string;
150
+ thumbnail_url?: string;
151
+ children?: {
152
+ data: ThreadsReply[];
153
+ };
154
+ has_replies?: boolean;
155
+ root_post?: {
156
+ id: string;
157
+ };
158
+ replied_to?: {
159
+ id: string;
160
+ };
161
+ is_reply?: boolean;
162
+ username?: string;
163
+ permalink?: string;
164
+ /** Whether the reply is currently hidden. */
165
+ hide_status?: 'NOT_HUSHED' | 'UNHUSHED' | 'HIDDEN' | 'COVERED' | 'BLOCKED' | 'RESTRICTED';
166
+ reply_audience?: string;
167
+ }
168
+ /** One metric value in an insights response. */
169
+ interface InsightMetric {
170
+ name: string;
171
+ period?: string;
172
+ title?: string;
173
+ description?: string;
174
+ /** Present for "total value" metrics. */
175
+ total_value?: {
176
+ value: number;
177
+ };
178
+ /** Present for "time series" metrics. */
179
+ values?: {
180
+ value: number;
181
+ end_time?: string;
182
+ }[];
183
+ id?: string;
184
+ }
185
+ interface InsightsResponse {
186
+ data: InsightMetric[];
187
+ paging?: {
188
+ previous?: string;
189
+ next?: string;
190
+ };
191
+ }
192
+ /** A single quota bucket from the publishing-limit endpoint. */
193
+ interface QuotaBucket {
194
+ quota_usage?: number;
195
+ config?: {
196
+ quota_total?: number;
197
+ quota_duration?: number;
198
+ };
199
+ }
200
+ /** Response of `GET /{user}/threads_publishing_limit`. */
201
+ interface PublishingLimit {
202
+ quota_usage?: number;
203
+ config?: {
204
+ quota_total?: number;
205
+ quota_duration?: number;
206
+ };
207
+ reply_quota_usage?: number;
208
+ reply_config?: {
209
+ quota_total?: number;
210
+ quota_duration?: number;
211
+ };
212
+ delete_quota_usage?: number;
213
+ delete_config?: {
214
+ quota_total?: number;
215
+ quota_duration?: number;
216
+ };
217
+ location_search_quota_usage?: number;
218
+ location_search_config?: {
219
+ quota_total?: number;
220
+ quota_duration?: number;
221
+ };
222
+ }
223
+ /** Result of creating or publishing a container. */
224
+ interface ContainerRef {
225
+ id: string;
226
+ }
227
+ /** `{ "success": true }` responses from moderation endpoints. */
228
+ interface SuccessResponse {
229
+ success: boolean;
230
+ }
231
+
232
+ /** Insights — `GET /{media-id}/insights` and `GET /{user-id}/threads_insights`. */
233
+
234
+ /** Documented media-level metrics. */
235
+ type MediaMetric = 'views' | 'likes' | 'replies' | 'reposts' | 'quotes' | 'shares';
236
+ /** Documented user-level metrics. */
237
+ type UserMetric = 'views' | 'likes' | 'replies' | 'reposts' | 'quotes' | 'clicks' | 'followers_count' | 'follower_demographics';
238
+ /** Breakdown dimension for `follower_demographics`. */
239
+ type DemographicBreakdown = 'country' | 'city' | 'age' | 'gender';
240
+ interface MediaInsightsOptions {
241
+ metrics?: MediaMetric[];
242
+ signal?: AbortSignal;
243
+ }
244
+ interface UserInsightsOptions {
245
+ userId?: string;
246
+ metrics?: UserMetric[];
247
+ since?: number | Date;
248
+ until?: number | Date;
249
+ /** Required by the API when requesting `follower_demographics`. */
250
+ breakdown?: DemographicBreakdown;
251
+ signal?: AbortSignal;
252
+ }
253
+ declare class InsightsResource {
254
+ private readonly client;
255
+ constructor(client: ThreadsRequester);
256
+ /**
257
+ * Returns engagement metrics for a single post.
258
+ * Requires the `threads_manage_insights` permission.
259
+ */
260
+ media(mediaId: string, options?: MediaInsightsOptions): Promise<InsightsResponse>;
261
+ /**
262
+ * Returns account-level metrics for a user.
263
+ * Requires the `threads_manage_insights` permission.
264
+ */
265
+ user(options?: UserInsightsOptions): Promise<InsightsResponse>;
266
+ }
267
+
268
+ /** Mentions — `GET /{user-id}/mentions`. */
269
+
270
+ interface ListMentionsOptions {
271
+ userId?: string;
272
+ fields?: string[];
273
+ since?: number | Date;
274
+ until?: number | Date;
275
+ limit?: number;
276
+ before?: string;
277
+ after?: string;
278
+ signal?: AbortSignal;
279
+ }
280
+ declare class MentionsResource {
281
+ private readonly client;
282
+ constructor(client: ThreadsRequester);
283
+ /**
284
+ * Lists posts that mention the user. Requires the mentions permission in
285
+ * addition to `threads_basic`.
286
+ */
287
+ list(options?: ListMentionsOptions): Promise<Paginated<ThreadsMedia>>;
288
+ }
289
+
290
+ /** Retrieve & discover posts — `GET /{user-id}/threads` and `GET /{media-id}`. */
291
+
292
+ interface ListPostsOptions {
293
+ /** User id to list; defaults to the configured user (or `me`). */
294
+ userId?: string;
295
+ fields?: string[];
296
+ /** Unix timestamp or `Date` lower bound. */
297
+ since?: number | Date;
298
+ /** Unix timestamp or `Date` upper bound. */
299
+ until?: number | Date;
300
+ limit?: number;
301
+ /** Pagination cursors (from a previous response's `paging`). */
302
+ before?: string;
303
+ after?: string;
304
+ signal?: AbortSignal;
305
+ }
306
+ interface GetPostOptions {
307
+ fields?: string[];
308
+ signal?: AbortSignal;
309
+ }
310
+ declare class PostsResource {
311
+ private readonly client;
312
+ constructor(client: ThreadsRequester);
313
+ /**
314
+ * Lists a user's posts, most recent first. Cursor-paginated.
315
+ * Requires the `threads_basic` permission.
316
+ */
317
+ list(options?: ListPostsOptions): Promise<Paginated<ThreadsMedia>>;
318
+ /**
319
+ * Reads a single media object (post or reply) by id.
320
+ * Requires the `threads_basic` permission.
321
+ */
322
+ get(mediaId: string, options?: GetPostOptions): Promise<ThreadsMedia>;
323
+ }
324
+
325
+ /** Profile endpoint — `GET /me` / `GET /{user-id}`. */
326
+
327
+ interface GetProfileOptions {
328
+ /** User id to read; defaults to the configured user (or `me`). */
329
+ userId?: string;
330
+ /** Profile fields to return; defaults to all documented fields. */
331
+ fields?: string[];
332
+ signal?: AbortSignal;
333
+ }
334
+ declare class ProfileResource {
335
+ private readonly client;
336
+ constructor(client: ThreadsRequester);
337
+ /**
338
+ * Returns profile information for a Threads user.
339
+ * Requires the `threads_basic` permission.
340
+ */
341
+ get(options?: GetProfileOptions): Promise<ThreadsProfile>;
342
+ }
343
+
344
+ /**
345
+ * Publishing — the two-step container flow plus convenience wrappers.
346
+ *
347
+ * Flow: `createContainer()` → (optionally wait for processing) →
348
+ * `publishContainer()`. The `publishText` / `publishImage` / `publishVideo` /
349
+ * `publishCarousel` helpers run the whole flow for you.
350
+ */
351
+
352
+ interface CreateContainerInput {
353
+ mediaType: MediaContainerType;
354
+ text?: string;
355
+ /** Required when `mediaType` is `IMAGE`. */
356
+ imageUrl?: string;
357
+ /** Required when `mediaType` is `VIDEO`. */
358
+ videoUrl?: string;
359
+ /** Mark this container as a carousel child (not published on its own). */
360
+ isCarouselItem?: boolean;
361
+ /** Child container ids — required when `mediaType` is `CAROUSEL`. */
362
+ children?: string[];
363
+ /** Make this post a reply to the given media id. */
364
+ replyToId?: string;
365
+ replyControl?: ReplyControl;
366
+ /** Accessibility description (max 1,000 chars). */
367
+ altText?: string;
368
+ linkAttachment?: string;
369
+ locationId?: string;
370
+ quotePostId?: string;
371
+ topicTag?: string;
372
+ /** Attach a poll (TEXT posts only). 2–4 options. */
373
+ pollAttachment?: PollAttachment;
374
+ /** Publishing user; defaults to the configured user (or `me`). */
375
+ userId?: string;
376
+ signal?: AbortSignal;
377
+ }
378
+ interface PollAttachment {
379
+ optionA: string;
380
+ optionB: string;
381
+ optionC?: string;
382
+ optionD?: string;
383
+ }
384
+ interface ContainerStatusResult {
385
+ id: string;
386
+ status: ContainerStatus;
387
+ error_message?: string;
388
+ }
389
+ interface WaitOptions {
390
+ /** Poll until the container is ready before publishing. */
391
+ waitForReady?: boolean;
392
+ pollIntervalMs?: number;
393
+ maxWaitMs?: number;
394
+ }
395
+ type PublishImageInput = {
396
+ imageUrl: string;
397
+ } & Omit<CreateContainerInput, 'mediaType'>;
398
+ type PublishVideoInput = {
399
+ videoUrl: string;
400
+ } & Omit<CreateContainerInput, 'mediaType'>;
401
+ interface PublishCarouselInput {
402
+ items: {
403
+ imageUrl?: string;
404
+ videoUrl?: string;
405
+ altText?: string;
406
+ }[];
407
+ text?: string;
408
+ replyControl?: ReplyControl;
409
+ userId?: string;
410
+ signal?: AbortSignal;
411
+ wait?: WaitOptions;
412
+ }
413
+ declare class PublishingResource {
414
+ private readonly client;
415
+ constructor(client: ThreadsRequester);
416
+ /** Step 1: create a media container. Returns its creation id. */
417
+ createContainer(input: CreateContainerInput): Promise<ContainerRef>;
418
+ /** Step 2: publish a previously created container. Returns the post id. */
419
+ publishContainer(creationId: string, options?: {
420
+ userId?: string;
421
+ signal?: AbortSignal;
422
+ }): Promise<ContainerRef>;
423
+ /** Reads a container's processing status. */
424
+ getContainerStatus(containerId: string, options?: {
425
+ signal?: AbortSignal;
426
+ }): Promise<ContainerStatusResult>;
427
+ /**
428
+ * Creates a container, optionally waits for media processing, then publishes.
429
+ * `waitForReady` defaults to `true` for `VIDEO`/`CAROUSEL` (which need
430
+ * server-side processing) and `false` otherwise.
431
+ */
432
+ createAndPublish(input: CreateContainerInput, wait?: WaitOptions): Promise<ContainerRef>;
433
+ /** Convenience: publish a text-only post. */
434
+ publishText(text: string, options?: Omit<CreateContainerInput, 'mediaType' | 'text'>): Promise<ContainerRef>;
435
+ /** Convenience: publish a text post with a poll (2–4 options). */
436
+ publishPoll(text: string, poll: PollAttachment, options?: Omit<CreateContainerInput, 'mediaType' | 'text' | 'pollAttachment'>): Promise<ContainerRef>;
437
+ /** Delete a published post. Requires the `threads_delete` scope. */
438
+ deletePost(postId: string, options?: {
439
+ signal?: AbortSignal;
440
+ }): Promise<{
441
+ success: boolean;
442
+ }>;
443
+ /** Convenience: publish a single image post. */
444
+ publishImage(input: PublishImageInput): Promise<ContainerRef>;
445
+ /** Convenience: publish a single video post (waits for processing). */
446
+ publishVideo(input: PublishVideoInput): Promise<ContainerRef>;
447
+ /**
448
+ * Convenience: publish a carousel. Each item becomes a child container, then
449
+ * a parent `CAROUSEL` container is created and published.
450
+ */
451
+ publishCarousel(input: PublishCarouselInput): Promise<ContainerRef>;
452
+ /** Reads remaining publish/reply/delete quotas for the user. */
453
+ getPublishingLimit(options?: {
454
+ userId?: string;
455
+ signal?: AbortSignal;
456
+ }): Promise<PublishingLimit>;
457
+ /** Polls a container until it is ready to publish, or throws on failure. */
458
+ private waitForContainer;
459
+ }
460
+
461
+ /**
462
+ * Replies & moderation.
463
+ *
464
+ * Reading: `GET /{media}/replies` (top-level) and `GET /{media}/conversation`
465
+ * (flattened thread). Moderation: hide/unhide, plus the reply-approval queue.
466
+ * Publishing a reply reuses the publishing flow with `reply_to_id` set.
467
+ */
468
+
469
+ interface ListRepliesOptions {
470
+ fields?: string[];
471
+ /** Reverse chronological order. */
472
+ reverse?: boolean;
473
+ signal?: AbortSignal;
474
+ }
475
+ interface ListPendingRepliesOptions {
476
+ fields?: string[];
477
+ reverse?: boolean;
478
+ /** Filter by approval state. */
479
+ approvalStatus?: 'pending' | 'ignored';
480
+ signal?: AbortSignal;
481
+ }
482
+ declare class RepliesResource {
483
+ private readonly client;
484
+ private readonly publishing;
485
+ constructor(client: ThreadsRequester, publishing: PublishingResource);
486
+ /**
487
+ * Lists the top-level replies to a post.
488
+ * Requires `threads_read_replies` (or `threads_manage_replies`).
489
+ */
490
+ list(mediaId: string, options?: ListRepliesOptions): Promise<Paginated<ThreadsReply>>;
491
+ /**
492
+ * Lists the full conversation (all nested replies) under a post.
493
+ * Requires `threads_read_replies` (or `threads_manage_replies`).
494
+ */
495
+ conversation(mediaId: string, options?: ListRepliesOptions): Promise<Paginated<ThreadsReply>>;
496
+ /**
497
+ * Publishes a reply to an existing post. Requires `threads_content_publish`
498
+ * (and `threads_manage_replies` for replies you don't own).
499
+ */
500
+ publish(replyToId: string, text: string, options?: {
501
+ userId?: string;
502
+ signal?: AbortSignal;
503
+ }): Promise<ContainerRef>;
504
+ /** Hides a reply. Requires `threads_manage_replies`. */
505
+ hide(replyId: string, options?: {
506
+ signal?: AbortSignal;
507
+ }): Promise<SuccessResponse>;
508
+ /** Unhides a previously hidden reply. Requires `threads_manage_replies`. */
509
+ unhide(replyId: string, options?: {
510
+ signal?: AbortSignal;
511
+ }): Promise<SuccessResponse>;
512
+ /**
513
+ * Lists replies awaiting approval (when reply approvals are enabled).
514
+ * Requires `threads_manage_replies`.
515
+ */
516
+ listPending(mediaId: string, options?: ListPendingRepliesOptions): Promise<Paginated<ThreadsReply>>;
517
+ /**
518
+ * Approves or rejects a pending reply. Requires `threads_manage_replies`.
519
+ */
520
+ managePending(replyId: string, approve: boolean, options?: {
521
+ signal?: AbortSignal;
522
+ }): Promise<SuccessResponse>;
523
+ private setHidden;
524
+ }
525
+
526
+ /** Keyword search — `GET /keyword_search`. */
527
+
528
+ interface KeywordSearchOptions {
529
+ /** `TOP` (default) ranks by relevance; `RECENT` is reverse-chronological. */
530
+ searchType?: 'TOP' | 'RECENT';
531
+ /** `KEYWORD` (default) or `TAG`. */
532
+ searchMode?: 'KEYWORD' | 'TAG';
533
+ /** Restrict to a media type. */
534
+ mediaType?: Extract<MediaContainerType, 'TEXT' | 'IMAGE' | 'VIDEO'>;
535
+ since?: number | Date;
536
+ until?: number | Date;
537
+ /** Default 25, max 100. */
538
+ limit?: number;
539
+ /** Exact username to filter results by author. */
540
+ authorUsername?: string;
541
+ fields?: string[];
542
+ signal?: AbortSignal;
543
+ }
544
+ declare class SearchResource {
545
+ private readonly client;
546
+ constructor(client: ThreadsRequester);
547
+ /**
548
+ * Searches public Threads posts by keyword or tag. The `owner` field is never
549
+ * returned for search results. Requires the `threads_keyword_search`
550
+ * permission (plus `threads_basic`).
551
+ */
552
+ keyword(query: string, options?: KeywordSearchOptions): Promise<Paginated<ThreadsMedia>>;
553
+ }
554
+
555
+ /**
556
+ * `ThreadsClient` — the typed entry point. Holds resolved configuration and
557
+ * exposes resource namespaces. One client instance is bound to one access
558
+ * token; create a new instance (or use {@link ThreadsClient.withToken}) per
559
+ * user/token.
560
+ */
561
+
562
+ interface ThreadsClientConfig {
563
+ /** A Threads user access token (short- or long-lived). */
564
+ accessToken: string;
565
+ /**
566
+ * Default node id for user-scoped endpoints. Defaults to `me`, which resolves
567
+ * to the token's owner.
568
+ */
569
+ userId?: string;
570
+ /** Graph host override. Defaults to `https://graph.threads.net`. */
571
+ baseUrl?: string;
572
+ /** API version. Defaults to `v1.0`. */
573
+ version?: string;
574
+ /** Per-request timeout in ms. Defaults to 30000. */
575
+ timeoutMs?: number;
576
+ /**
577
+ * Retry policy for idempotent requests. Pass `false` to disable retries, or a
578
+ * partial object to override individual fields.
579
+ */
580
+ retry?: Partial<RetryConfig> | false;
581
+ /** Logging hook. Tokens/secrets are always redacted before logging. */
582
+ logger?: Logger;
583
+ /** Custom fetch (for tests or non-standard runtimes). Defaults to global. */
584
+ fetch?: FetchLike;
585
+ }
586
+ declare class ThreadsClient implements ThreadsRequester {
587
+ readonly profile: ProfileResource;
588
+ readonly posts: PostsResource;
589
+ readonly publishing: PublishingResource;
590
+ readonly replies: RepliesResource;
591
+ readonly insights: InsightsResource;
592
+ readonly mentions: MentionsResource;
593
+ readonly search: SearchResource;
594
+ private readonly config;
595
+ constructor(config: ThreadsClientConfig);
596
+ /** Default node id for user-scoped endpoints. */
597
+ get userNode(): string;
598
+ /**
599
+ * Low-level escape hatch: issue a request to any Threads endpoint with full
600
+ * typing of the response. Prefer the resource methods; use this for endpoints
601
+ * the SDK doesn't model yet.
602
+ */
603
+ request<T>(req: ResourceRequest): Promise<T>;
604
+ /** Returns a new client that shares this config but uses a different token. */
605
+ withToken(accessToken: string): ThreadsClient;
606
+ }
607
+
608
+ /**
609
+ * Static configuration for the Threads API.
610
+ *
611
+ * Values here mirror the official documentation
612
+ * (https://developers.facebook.com/docs/threads). Hosts and the API version
613
+ * are the only "magic strings" in the SDK and live in one place so they are
614
+ * easy to audit against the docs.
615
+ */
616
+ /** Graph host for all data/publishing calls. */
617
+ declare const DEFAULT_BASE_URL = "https://graph.threads.net";
618
+ /** Host that serves the OAuth authorization window (user-facing redirect). */
619
+ declare const AUTHORIZATION_BASE_URL = "https://threads.net";
620
+ /** Current Threads Graph API version. */
621
+ declare const DEFAULT_API_VERSION = "v1.0";
622
+ /**
623
+ * OAuth permission scopes supported by the Threads API, as documented.
624
+ * `threads_basic` is required for every call.
625
+ */
626
+ declare const THREADS_SCOPES: readonly ["threads_basic", "threads_content_publish", "threads_read_replies", "threads_manage_replies", "threads_manage_insights", "threads_keyword_search", "threads_delete", "threads_location_tagging"];
627
+ /**
628
+ * A documented Threads scope, or any other scope string the API may accept.
629
+ * The string fallback keeps autocomplete for known scopes without hardcoding
630
+ * scope names that aren't verified against the docs.
631
+ */
632
+ type ThreadsScope = (typeof THREADS_SCOPES)[number] | (string & {});
633
+
634
+ /**
635
+ * OAuth 2.0 helpers for the Threads authorization-code flow.
636
+ *
637
+ * These are standalone functions (not methods on the client) so a host app that
638
+ * only handles the auth handshake can import them without pulling in the rest
639
+ * of the SDK. Token-exchange calls must run server-side: they require the app
640
+ * secret, which must never reach a browser.
641
+ */
642
+
643
+ /** Options shared by the server-side token-exchange calls. */
644
+ interface ExchangeBaseOptions {
645
+ /** Defaults to the global `fetch`. */
646
+ fetch?: FetchLike;
647
+ /** Graph host override (defaults to `https://graph.threads.net`). */
648
+ baseUrl?: string;
649
+ /** Per-request timeout in ms. */
650
+ timeoutMs?: number;
651
+ /** Optional logging hook (token values are always redacted). */
652
+ logger?: Logger;
653
+ }
654
+ interface AuthorizationUrlOptions {
655
+ clientId: string;
656
+ redirectUri: string;
657
+ /** Requested scopes. `threads_basic` is required by the API. */
658
+ scopes: ThreadsScope[];
659
+ /** Opaque CSRF token echoed back to your redirect URI. Strongly recommended. */
660
+ state?: string;
661
+ /** Authorization host override (defaults to `https://threads.net`). */
662
+ baseUrl?: string;
663
+ }
664
+ interface ShortLivedTokenResponse {
665
+ access_token: string;
666
+ user_id: string;
667
+ }
668
+ interface LongLivedTokenResponse {
669
+ access_token: string;
670
+ token_type: string;
671
+ /** Seconds until expiry (~60 days). */
672
+ expires_in: number;
673
+ }
674
+ /**
675
+ * Builds the URL to redirect a user to in order to grant your app access.
676
+ *
677
+ * @example
678
+ * ```ts
679
+ * const url = getAuthorizationUrl({
680
+ * clientId: process.env.THREADS_APP_ID!,
681
+ * redirectUri: 'https://app.example.com/auth/callback',
682
+ * scopes: ['threads_basic', 'threads_content_publish'],
683
+ * state: csrfToken,
684
+ * })
685
+ * ```
686
+ */
687
+ declare function getAuthorizationUrl(options: AuthorizationUrlOptions): string;
688
+ /**
689
+ * Exchanges an authorization `code` for a short-lived access token. Server-side
690
+ * only (requires the app secret).
691
+ */
692
+ declare function exchangeCodeForToken(options: ExchangeBaseOptions & {
693
+ clientId: string;
694
+ clientSecret: string;
695
+ code: string;
696
+ redirectUri: string;
697
+ }): Promise<ShortLivedTokenResponse>;
698
+ /**
699
+ * Exchanges a short-lived token for a long-lived (~60 day) token. Server-side
700
+ * only (requires the app secret).
701
+ */
702
+ declare function exchangeForLongLivedToken(options: ExchangeBaseOptions & {
703
+ clientSecret: string;
704
+ shortLivedToken: string;
705
+ }): Promise<LongLivedTokenResponse>;
706
+ /**
707
+ * Refreshes a long-lived token, extending it ~60 days. The token must be at
708
+ * least 24 hours old and unexpired. No app secret required.
709
+ */
710
+ declare function refreshLongLivedToken(options: ExchangeBaseOptions & {
711
+ longLivedToken: string;
712
+ }): Promise<LongLivedTokenResponse>;
713
+
714
+ /**
715
+ * Typed error hierarchy. Every failure the SDK surfaces is an instance of
716
+ * {@link ThreadsError}, so callers can `catch` broadly or narrow with
717
+ * `instanceof` to a specific subtype.
718
+ *
719
+ * ```text
720
+ * ThreadsError
721
+ * ├─ ThreadsValidationError client-side input was invalid (no request sent)
722
+ * ├─ ThreadsTimeoutError request exceeded the configured timeout
723
+ * ├─ ThreadsNetworkError fetch failed before a response was received
724
+ * └─ ThreadsAPIError the API returned a non-2xx response
725
+ * ├─ ThreadsAuthError invalid/expired token or auth failure (401 / code 190)
726
+ * └─ ThreadsRateLimitError throttled (HTTP 429 or a known throttle code)
727
+ * ```
728
+ */
729
+ interface ApiErrorDetails {
730
+ message: string;
731
+ /** HTTP status code of the response. */
732
+ status?: number;
733
+ /** Graph API `error.code`. */
734
+ code?: number;
735
+ /** Graph API `error.error_subcode`. */
736
+ subcode?: number;
737
+ /** Graph API `error.type`. */
738
+ type?: string;
739
+ /** Graph API `error.fbtrace_id` — quote this when contacting Meta support. */
740
+ fbtraceId?: string;
741
+ }
742
+ /** Base class for every error thrown by the SDK. */
743
+ declare class ThreadsError extends Error {
744
+ constructor(message: string, options?: {
745
+ cause?: unknown;
746
+ });
747
+ }
748
+ /** Thrown when caller input fails validation before any request is made. */
749
+ declare class ThreadsValidationError extends ThreadsError {
750
+ }
751
+ /** Thrown when a request exceeds the configured timeout. */
752
+ declare class ThreadsTimeoutError extends ThreadsError {
753
+ }
754
+ /** Thrown when the request fails at the network layer (no HTTP response). */
755
+ declare class ThreadsNetworkError extends ThreadsError {
756
+ }
757
+ /** Thrown for any non-2xx API response. */
758
+ declare class ThreadsAPIError extends ThreadsError {
759
+ readonly status?: number;
760
+ readonly code?: number;
761
+ readonly subcode?: number;
762
+ readonly type?: string;
763
+ readonly fbtraceId?: string;
764
+ constructor(details: ApiErrorDetails, options?: {
765
+ cause?: unknown;
766
+ });
767
+ }
768
+ /** Invalid or expired access token / authorization failure. */
769
+ declare class ThreadsAuthError extends ThreadsAPIError {
770
+ }
771
+ /** Request was throttled. {@link retryAfterMs} is set when the API tells us. */
772
+ declare class ThreadsRateLimitError extends ThreadsAPIError {
773
+ readonly retryAfterMs?: number;
774
+ constructor(details: ApiErrorDetails & {
775
+ retryAfterMs?: number;
776
+ }, options?: {
777
+ cause?: unknown;
778
+ });
779
+ }
780
+ /** A `Headers`-like object (only `get` is needed). */
781
+ interface HeadersLike {
782
+ get(name: string): string | null;
783
+ }
784
+ /**
785
+ * Parses a `Retry-After` header (seconds or HTTP-date) into milliseconds.
786
+ * Returns `undefined` when absent or unparseable.
787
+ */
788
+ declare function parseRetryAfterMs(value: string | null): number | undefined;
789
+ /**
790
+ * Maps a non-2xx HTTP response into the most specific error subtype.
791
+ *
792
+ * @param status - HTTP status code.
793
+ * @param body - Parsed JSON body (may be undefined for empty/non-JSON bodies).
794
+ * @param headers - Response headers, used to read `Retry-After`.
795
+ */
796
+ declare function toApiError(status: number, body: unknown, headers?: HeadersLike): ThreadsAPIError;
797
+
798
+ export { AUTHORIZATION_BASE_URL, type ApiErrorDetails, type AuthorizationUrlOptions, type ContainerRef, type ContainerStatus, type ContainerStatusResult, type CreateContainerInput, DEFAULT_API_VERSION, DEFAULT_BASE_URL, type DemographicBreakdown, type FetchLike, type GetPostOptions, type GetProfileOptions, type HttpMethod, type InsightMetric, type InsightsResponse, type KeywordSearchOptions, type ListMentionsOptions, type ListPendingRepliesOptions, type ListPostsOptions, type ListRepliesOptions, type LogContext, type LogLevel, type Logger, type LongLivedTokenResponse, type MediaContainerType, type MediaInsightsOptions, type MediaMetric, type MediaType, type Paginated, type PublishCarouselInput, type PublishImageInput, type PublishVideoInput, type PublishingLimit, type QuotaBucket, type ReplyControl, type ResourceRequest, type RetryConfig, type ShortLivedTokenResponse, type SuccessResponse, THREADS_SCOPES, ThreadsAPIError, ThreadsAuthError, ThreadsClient, type ThreadsClientConfig, ThreadsError, type ThreadsMedia, ThreadsNetworkError, type ThreadsProfile, ThreadsRateLimitError, type ThreadsReply, type ThreadsRequester, type ThreadsScope, ThreadsTimeoutError, ThreadsValidationError, type UserInsightsOptions, type UserMetric, type WaitOptions, exchangeCodeForToken, exchangeForLongLivedToken, getAuthorizationUrl, noopLogger, parseRetryAfterMs, redactParams, redactUrl, refreshLongLivedToken, toApiError };