@codybrom/denim 2.0.1 → 2.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [2.1.0] - 2026-09-20
10
+
11
+ ### Added
12
+
13
+ - Reply Approvals (Feb 13, 2026 API): `enableReplyApprovals` on
14
+ `ThreadsPostRequest`, `getPendingReplies()` (`GET /{media-id}/pending_replies`
15
+ with `approval_status` filter), `managePendingReply()`
16
+ (`POST /{reply-id}/manage_pending_reply`), `reply_approval_status` on
17
+ `ThreadsPost`, and `ApprovalStatus` / `PendingRepliesOptions` types. Ghost
18
+ posts cannot combine with reply approvals.
19
+ - Share to Instagram Stories (Mar 25, 2026 API): `shareToIgStory` and
20
+ `shareToIgStoryDarkMode` on `ThreadsPostRequest` (`crossreshare_to_ig` /
21
+ `crossreshare_to_ig_dark_mode`, requires `threads_share_to_instagram`
22
+ permission). `publishThreadsContainer` with `getPermalink: true` now also
23
+ returns `crossreshare_to_ig_status`.
24
+
25
+ ### Changed
26
+
27
+ - `getOEmbed()` no longer requires an access token (Mar 3, 2026 API). Preferred
28
+ order is now `getOEmbed(postUrl, accessToken?, maxWidth?)`; legacy
29
+ `getOEmbed(accessToken, postUrl, maxWidth?)` still works.
30
+ - `AuthCodeResponse` now includes optional `token_type` (Aug 12, 2026 API).
31
+
32
+ ## [2.0.2] - 2026-07-11
33
+
34
+ ### Fixed
35
+
36
+ - `GifAttachment.provider` is now `"GIPHY"` instead of `"TENOR"`. Meta retired
37
+ Tenor as a GIF provider on March 31, 2026; GIPHY is now the only accepted
38
+ provider for `gif_attachment`. The old `"TENOR"` literal could no longer
39
+ produce a valid GIF post.
40
+
9
41
  ## [2.0.1] - 2026-04-17
10
42
 
11
43
  ### Fixed
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codybrom/denim",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "A Deno/TypeScript library for the Threads API",
5
5
  "exports": {
6
6
  ".": "./mod.ts"
package/mod.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  // ─── Types ───────────────────────────────────────────────────────────────────
10
10
  import type {
11
+ ApprovalStatus,
11
12
  AuthCodeResponse,
12
13
  CursorPaginationOptions,
13
14
  DebugTokenInfo,
@@ -21,10 +22,12 @@ import type {
21
22
  MockThreadsAPI,
22
23
  OEmbedResponse,
23
24
  PaginationOptions,
25
+ PendingRepliesOptions,
24
26
  PollAttachment,
25
27
  PollAttachmentInput,
26
28
  PublicProfile,
27
29
  PublishingLimit,
30
+ PublishResult,
28
31
  QuotaConfig,
29
32
  ReplyControl,
30
33
  ResponseMediaType,
@@ -49,6 +52,7 @@ import type {
49
52
  } from "./src/types.ts";
50
53
 
51
54
  export type {
55
+ ApprovalStatus,
52
56
  AuthCodeResponse,
53
57
  CursorPaginationOptions,
54
58
  DebugTokenInfo,
@@ -62,10 +66,12 @@ export type {
62
66
  MockThreadsAPI,
63
67
  OEmbedResponse,
64
68
  PaginationOptions,
69
+ PendingRepliesOptions,
65
70
  PollAttachment,
66
71
  PollAttachmentInput,
67
72
  PublicProfile,
68
73
  PublishingLimit,
74
+ PublishResult,
69
75
  QuotaConfig,
70
76
  ReplyControl,
71
77
  ResponseMediaType,
@@ -108,8 +114,10 @@ export { lookupProfile } from "./src/api/lookupProfile.ts";
108
114
 
109
115
  // ─── Replies ─────────────────────────────────────────────────────────────────
110
116
  export { getConversation } from "./src/api/getConversation.ts";
117
+ export { getPendingReplies } from "./src/api/getPendingReplies.ts";
111
118
  export { getReplies } from "./src/api/getReplies.ts";
112
119
  export { getUserReplies } from "./src/api/getUserReplies.ts";
120
+ export { managePendingReply } from "./src/api/managePendingReply.ts";
113
121
  export { manageReply } from "./src/api/manageReply.ts";
114
122
 
115
123
  // ─── Mentions ────────────────────────────────────────────────────────────────
package/mod_test.ts CHANGED
@@ -141,7 +141,7 @@ Deno.test("Denim API Tests", async (t) => {
141
141
  text: "Check this GIF!",
142
142
  gifAttachment: {
143
143
  gif_id: "abc123",
144
- provider: "TENOR",
144
+ provider: "GIPHY",
145
145
  },
146
146
  };
147
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codybrom/denim",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "A Deno/TypeScript library for the Threads API",
5
5
  "main": "mod.ts",
6
6
  "directories": {
package/readme.md CHANGED
@@ -56,7 +56,7 @@ and usually `text`. Optional fields control the post type:
56
56
  | `isSpoilerMedia` | `boolean` | Hide media behind a spoiler overlay |
57
57
  | `textEntities` | `array` | Text spoiler ranges: `[{ entity_type, offset, length }]` |
58
58
  | `textAttachment` | `object` | Long-form text: `{ plaintext, link_attachment_url? }` |
59
- | `gifAttachment` | `object` | GIF: `{ gif_id, provider }` |
59
+ | `gifAttachment` | `object` | GIF: `{ gif_id, provider: "GIPHY" }` |
60
60
  | `locationId` | `string` | Location ID from `searchLocations` |
61
61
  | `children` | `string[]` | Carousel item IDs from `createCarouselItem` |
62
62
  | `autoPublishText` | `boolean` | Skip the publish step for text posts |
@@ -115,6 +115,15 @@ made by a user.
115
115
  `manageReply(replyId, accessToken, hide)` hides or unhides a reply. Pass `true`
116
116
  to hide, `false` to unhide.
117
117
 
118
+ `getPendingReplies(mediaId, accessToken, options?, fields?, reverse?,
119
+ approvalStatus?)`
120
+ returns replies awaiting approval on posts created with
121
+ `enableReplyApprovals: true`. Filter with `"pending"` or `"ignored"` (default
122
+ returns both).
123
+
124
+ `managePendingReply(replyId, accessToken, approve)` approves (`true`) or ignores
125
+ (`false`) a pending reply. Ignored replies can still be approved later.
126
+
118
127
  ## Insights
119
128
 
120
129
  `getMediaInsights(mediaId, accessToken, metrics)` returns metrics for a post.
@@ -150,7 +159,7 @@ objects with IDs you can pass to `createThreadsContainer` as `locationId`.
150
159
 
151
160
  `exchangeCodeForToken(clientId, clientSecret, code, redirectUri)` exchanges an
152
161
  OAuth authorization code for a short-lived access token. Returns
153
- `{ access_token, user_id }`.
162
+ `{ access_token, user_id, token_type? }` (`token_type` present since Aug 2026).
154
163
 
155
164
  `getAppAccessToken(clientId, clientSecret)` gets an app-level access token via
156
165
  client credentials. Returns `{ access_token, token_type }`.
@@ -172,8 +181,17 @@ quota, reply quota, and remaining usage.
172
181
  `getMentions(userId, accessToken, options?, fields?)` returns posts that mention
173
182
  the authenticated user.
174
183
 
175
- `getOEmbed(accessToken, url, maxWidth?)` returns embeddable HTML for a Threads
176
- post URL. Returns `{ html, provider_name, type, version, width }`.
184
+ `getOEmbed(postUrl, accessToken?, maxWidth?)` returns embeddable HTML for a
185
+ Threads post URL. No access token required since Mar 2026. Legacy
186
+ `getOEmbed(accessToken, url, maxWidth?)` order still works. Returns
187
+ `{ html, provider_name, type, version, width }`.
188
+
189
+ `createThreadsContainer` accepts `enableReplyApprovals?: boolean` (cannot be
190
+ combined with `isGhostPost`), plus `shareToIgStory?: boolean` and
191
+ `shareToIgStoryDarkMode?: boolean` for cross-sharing to the linked Instagram
192
+ account as a Story (requires `threads_share_to_instagram` permission).
193
+ `publishThreadsContainer(..., getPermalink=true)` may also return
194
+ `crossreshare_to_ig_status: "SUCCESS" | "FAILED"`.
177
195
 
178
196
  ## Utilities
179
197
 
@@ -68,6 +68,21 @@ export async function createThreadsContainer(
68
68
  if (request.isSpoilerMedia !== undefined) {
69
69
  body.append("is_spoiler_media", String(request.isSpoilerMedia));
70
70
  }
71
+ if (request.enableReplyApprovals !== undefined) {
72
+ body.append(
73
+ "enable_reply_approvals",
74
+ String(request.enableReplyApprovals),
75
+ );
76
+ }
77
+ if (request.shareToIgStory !== undefined) {
78
+ body.append("crossreshare_to_ig", String(request.shareToIgStory));
79
+ }
80
+ if (request.shareToIgStoryDarkMode !== undefined) {
81
+ body.append(
82
+ "crossreshare_to_ig_dark_mode",
83
+ String(request.shareToIgStoryDarkMode),
84
+ );
85
+ }
71
86
 
72
87
  // JSON-serialized parameters
73
88
  if (request.pollAttachment) {
@@ -2,31 +2,66 @@ import { THREADS_API_BASE_URL } from "../constants.ts";
2
2
  import type { OEmbedResponse } from "../types.ts";
3
3
  import { getAPI } from "../utils/getAPI.ts";
4
4
 
5
+ function isUrl(value: string): boolean {
6
+ return /^https?:\/\//i.test(value);
7
+ }
8
+
5
9
  /**
6
10
  * Retrieves oEmbed HTML for a Threads post.
11
+ * Since March 3, 2026 the oEmbed endpoint works without an access token.
7
12
  *
8
- * @param accessToken - The access token for authentication
9
- * @param postUrl - The URL of the Threads post to embed
13
+ * Supports both call orders for backwards compatibility:
14
+ * - `getOEmbed(accessToken, postUrl, maxWidth?)` (legacy)
15
+ * - `getOEmbed(postUrl, accessToken?, maxWidth?)` (preferred)
16
+ * - `getOEmbed(postUrl, maxWidth?)` — no token; pass maxWidth as second arg
17
+ * only via the 3-arg form `getOEmbed(postUrl, undefined, maxWidth)`
18
+ *
19
+ * @param accessTokenOrUrl - Legacy access token OR the post URL
20
+ * @param urlOrAccessTokenOrMaxWidth - Post URL, optional access token, or maxWidth
10
21
  * @param maxWidth - Optional maximum width of the embed in pixels
11
22
  * @returns A Promise that resolves to the OEmbedResponse
12
23
  * @throws Will throw an error if the API request fails
13
24
  */
14
25
  export async function getOEmbed(
15
- accessToken: string,
16
- postUrl: string,
26
+ accessTokenOrUrl: string,
27
+ urlOrAccessTokenOrMaxWidth?: string | number,
17
28
  maxWidth?: number,
18
29
  ): Promise<OEmbedResponse> {
30
+ let accessToken: string | undefined;
31
+ let postUrl: string;
32
+ let resolvedMaxWidth = maxWidth;
33
+
34
+ if (typeof urlOrAccessTokenOrMaxWidth === "number") {
35
+ // getOEmbed(postUrl, maxWidth)
36
+ postUrl = accessTokenOrUrl;
37
+ resolvedMaxWidth = urlOrAccessTokenOrMaxWidth;
38
+ } else if (urlOrAccessTokenOrMaxWidth === undefined) {
39
+ // Single-arg: must be the post URL (no token)
40
+ postUrl = accessTokenOrUrl;
41
+ } else if (isUrl(accessTokenOrUrl) && !isUrl(urlOrAccessTokenOrMaxWidth)) {
42
+ // New order: getOEmbed(postUrl, accessToken?, maxWidth?)
43
+ postUrl = accessTokenOrUrl;
44
+ accessToken = urlOrAccessTokenOrMaxWidth || undefined;
45
+ } else {
46
+ // Legacy order: getOEmbed(accessToken, postUrl, maxWidth?)
47
+ accessToken = accessTokenOrUrl || undefined;
48
+ postUrl = urlOrAccessTokenOrMaxWidth;
49
+ }
50
+
19
51
  const api = getAPI();
20
52
  if (api) {
21
- return api.getOEmbed(accessToken, postUrl, maxWidth);
53
+ // Mock preserves legacy (token, url) ordering
54
+ return api.getOEmbed(accessToken ?? "", postUrl, resolvedMaxWidth);
22
55
  }
23
56
 
24
57
  const url = new URL(`${THREADS_API_BASE_URL}/oembed`);
25
58
  url.searchParams.append("url", postUrl);
26
- url.searchParams.append("access_token", accessToken);
59
+ if (accessToken) {
60
+ url.searchParams.append("access_token", accessToken);
61
+ }
27
62
 
28
- if (maxWidth !== undefined) {
29
- url.searchParams.append("maxwidth", maxWidth.toString());
63
+ if (resolvedMaxWidth !== undefined) {
64
+ url.searchParams.append("maxwidth", resolvedMaxWidth.toString());
30
65
  }
31
66
 
32
67
  const response = await fetch(url.toString());
@@ -0,0 +1,69 @@
1
+ import { PENDING_REPLY_FIELDS, THREADS_API_BASE_URL } from "../constants.ts";
2
+ import type {
3
+ ApprovalStatus,
4
+ CursorPaginationOptions,
5
+ ThreadsListResponse,
6
+ } from "../types.ts";
7
+ import { getAPI } from "../utils/getAPI.ts";
8
+
9
+ /**
10
+ * Retrieves pending replies awaiting approval on a Threads post.
11
+ * Only applicable to posts created with reply approvals enabled.
12
+ *
13
+ * @param mediaId - The ID of the Threads media object
14
+ * @param accessToken - The access token for authentication
15
+ * @param options - Optional pagination parameters
16
+ * @param fields - Optional array of fields to return
17
+ * @param reverse - Optional boolean to sort in reverse chronological order (default: true)
18
+ * @param approvalStatus - Optional filter: `pending` or `ignored` (default returns both)
19
+ * @returns A Promise that resolves to the ThreadsListResponse
20
+ * @throws Will throw an error if the API request fails
21
+ */
22
+ export async function getPendingReplies(
23
+ mediaId: string,
24
+ accessToken: string,
25
+ options?: CursorPaginationOptions,
26
+ fields?: string[],
27
+ reverse?: boolean,
28
+ approvalStatus?: ApprovalStatus,
29
+ ): Promise<ThreadsListResponse> {
30
+ const api = getAPI();
31
+ if (api) {
32
+ return api.getPendingReplies(
33
+ mediaId,
34
+ accessToken,
35
+ options,
36
+ fields,
37
+ reverse,
38
+ approvalStatus,
39
+ );
40
+ }
41
+
42
+ const fieldList = (fields ?? PENDING_REPLY_FIELDS).join(",");
43
+ const url = new URL(`${THREADS_API_BASE_URL}/${mediaId}/pending_replies`);
44
+ url.searchParams.append("fields", fieldList);
45
+ url.searchParams.append("access_token", accessToken);
46
+
47
+ if (reverse !== undefined) {
48
+ url.searchParams.append("reverse", String(reverse));
49
+ }
50
+
51
+ if (approvalStatus !== undefined) {
52
+ url.searchParams.append("approval_status", approvalStatus);
53
+ }
54
+
55
+ if (options) {
56
+ if (options.after) url.searchParams.append("after", options.after);
57
+ if (options.before) url.searchParams.append("before", options.before);
58
+ }
59
+
60
+ const response = await fetch(url.toString());
61
+ if (!response.ok) {
62
+ const errorBody = await response.text();
63
+ throw new Error(
64
+ `Failed to get pending replies (${response.status}): ${errorBody}`,
65
+ );
66
+ }
67
+
68
+ return await response.json();
69
+ }
@@ -0,0 +1,42 @@
1
+ import { THREADS_API_BASE_URL } from "../constants.ts";
2
+ import { getAPI } from "../utils/getAPI.ts";
3
+
4
+ /**
5
+ * Approves or ignores a pending reply on a Threads post with reply approvals enabled.
6
+ * Ignored replies can still be approved later.
7
+ *
8
+ * @param replyId - The ID of the pending reply to manage
9
+ * @param accessToken - The access token for authentication
10
+ * @param approve - Whether to approve (true) or ignore (false) the reply
11
+ * @returns A Promise that resolves to an object indicating success
12
+ * @throws Will throw an error if the API request fails
13
+ */
14
+ export async function managePendingReply(
15
+ replyId: string,
16
+ accessToken: string,
17
+ approve: boolean,
18
+ ): Promise<{ success: boolean }> {
19
+ const api = getAPI();
20
+ if (api) {
21
+ return api.managePendingReply(replyId, accessToken, approve);
22
+ }
23
+
24
+ const url = `${THREADS_API_BASE_URL}/${replyId}/manage_pending_reply`;
25
+ const body = new URLSearchParams({
26
+ access_token: accessToken,
27
+ approve: String(approve),
28
+ });
29
+
30
+ const response = await fetch(url, {
31
+ method: "POST",
32
+ body: body,
33
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
34
+ });
35
+
36
+ if (!response.ok) {
37
+ const responseText = await response.text();
38
+ throw new Error(`Failed to manage pending reply: ${responseText}`);
39
+ }
40
+
41
+ return await response.json();
42
+ }
@@ -92,6 +92,11 @@ export async function publishThreadsContainer(
92
92
  return {
93
93
  id: publishData.id,
94
94
  permalink: threadData.permalink || "",
95
+ ...(publishData.crossreshare_to_ig_status
96
+ ? {
97
+ crossreshare_to_ig_status: publishData.crossreshare_to_ig_status,
98
+ }
99
+ : {}),
95
100
  };
96
101
  }
97
102
 
package/src/constants.ts CHANGED
@@ -65,6 +65,12 @@ export const REPLY_FIELDS = [
65
65
  "reposted_post",
66
66
  "gif_url",
67
67
  "topic_tag",
68
+ "reply_approval_status",
69
+ ] as const;
70
+
71
+ /** Fields for pending replies endpoint (same as replies, includes approval status) */
72
+ export const PENDING_REPLY_FIELDS = [
73
+ ...REPLY_FIELDS,
68
74
  ] as const;
69
75
 
70
76
  /** Fields for location endpoints */
package/src/types.ts CHANGED
@@ -84,8 +84,8 @@ export interface TextAttachmentInput {
84
84
  export interface GifAttachment {
85
85
  /** The GIF ID from the provider */
86
86
  gif_id: string;
87
- /** The GIF provider */
88
- provider: "TENOR";
87
+ /** The GIF provider. GIPHY is currently the only supported provider (Tenor was retired March 31, 2026). */
88
+ provider: "GIPHY";
89
89
  }
90
90
 
91
91
  /**
@@ -136,6 +136,12 @@ export interface ThreadsPostRequest {
136
136
  isGhostPost?: boolean;
137
137
  /** Location ID to tag (optional) */
138
138
  locationId?: string;
139
+ /** Enable reply approvals — replies must be approved before publishing (optional, cannot be used with isGhostPost) */
140
+ enableReplyApprovals?: boolean;
141
+ /** Cross-share the post to the linked Instagram account as a Story (optional, any media type, requires `threads_share_to_instagram` permission) */
142
+ shareToIgStory?: boolean;
143
+ /** Cross-share the post to the linked Instagram account as a Story in dark mode (optional, any media type, requires `threads_share_to_instagram` permission) */
144
+ shareToIgStoryDarkMode?: boolean;
139
145
  }
140
146
 
141
147
  /**
@@ -165,6 +171,33 @@ export interface CursorPaginationOptions {
165
171
  after?: string;
166
172
  }
167
173
 
174
+ /**
175
+ * Approval status filter for pending replies.
176
+ * `pending` shows only pending replies, `ignored` shows only ignored replies.
177
+ * Default (omitted) returns both.
178
+ */
179
+ export type ApprovalStatus = "pending" | "ignored";
180
+
181
+ /**
182
+ * Options for pending replies retrieval.
183
+ */
184
+ export interface PendingRepliesOptions extends CursorPaginationOptions {
185
+ /** Filter by approval status (optional, default returns both) */
186
+ approval_status?: ApprovalStatus;
187
+ }
188
+
189
+ /**
190
+ * Result of publishing a Threads media container.
191
+ */
192
+ export interface PublishResult {
193
+ /** The published Threads media ID */
194
+ id: string;
195
+ /** Permalink (only populated when requested via `getPermalink`) */
196
+ permalink?: string;
197
+ /** Cross-share to Instagram Story status (only when cross-sharing was requested) */
198
+ crossreshare_to_ig_status?: "SUCCESS" | "FAILED";
199
+ }
200
+
168
201
  // ─── Response Types (snake_case — matching API reality) ──────────────────────
169
202
 
170
203
  /**
@@ -319,6 +352,8 @@ export interface ThreadsPost {
319
352
  ghost_post_expiration_timestamp?: string;
320
353
  /** List of country codes where the post is visible */
321
354
  allowlisted_country_codes?: string[];
355
+ /** Approval status of a pending reply (`pending` or `ignored`) */
356
+ reply_approval_status?: "pending" | "ignored";
322
357
  }
323
358
 
324
359
  /**
@@ -552,12 +587,15 @@ export interface LocationSearchOptions {
552
587
 
553
588
  /**
554
589
  * Response from exchanging an OAuth authorization code for a short-lived token.
590
+ * Since August 12, 2026 the API also returns `token_type`.
555
591
  */
556
592
  export interface AuthCodeResponse {
557
593
  /** The short-lived access token */
558
594
  access_token: string;
559
595
  /** The user ID of the authenticated user */
560
596
  user_id: string;
597
+ /** Token type (e.g., "bearer"). Present since August 12, 2026. */
598
+ token_type?: string;
561
599
  }
562
600
 
563
601
  /**
@@ -760,7 +798,14 @@ export interface MockThreadsAPI {
760
798
  accessToken: string,
761
799
  containerId: string,
762
800
  getPermalink?: boolean,
763
- ): Promise<string | { id: string; permalink: string }>;
801
+ ): Promise<
802
+ | string
803
+ | {
804
+ id: string;
805
+ permalink: string;
806
+ crossreshare_to_ig_status?: "SUCCESS" | "FAILED";
807
+ }
808
+ >;
764
809
 
765
810
  createCarouselItem(
766
811
  request: Omit<ThreadsPostRequest, "mediaType"> & {
@@ -853,6 +898,21 @@ export interface MockThreadsAPI {
853
898
  hide: boolean,
854
899
  ): Promise<{ success: boolean }>;
855
900
 
901
+ getPendingReplies(
902
+ mediaId: string,
903
+ accessToken: string,
904
+ options?: CursorPaginationOptions,
905
+ fields?: string[],
906
+ reverse?: boolean,
907
+ approvalStatus?: ApprovalStatus,
908
+ ): Promise<ThreadsListResponse>;
909
+
910
+ managePendingReply(
911
+ replyId: string,
912
+ accessToken: string,
913
+ approve: boolean,
914
+ ): Promise<{ success: boolean }>;
915
+
856
916
  getMentions(
857
917
  userId: string,
858
918
  accessToken: string,
@@ -918,8 +978,8 @@ export interface MockThreadsAPI {
918
978
  ): Promise<DebugTokenInfo>;
919
979
 
920
980
  getOEmbed(
921
- accessToken: string,
922
- url: string,
981
+ accessTokenOrUrl: string,
982
+ urlOrUndefined?: string,
923
983
  maxWidth?: number,
924
984
  ): Promise<OEmbedResponse>;
925
985
  }
@@ -101,7 +101,13 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
101
101
  _accessToken: string,
102
102
  containerId: string,
103
103
  getPermalink: boolean = false,
104
- ): Promise<string | { id: string; permalink: string }> {
104
+ ): Promise<
105
+ string | {
106
+ id: string;
107
+ permalink: string;
108
+ crossreshare_to_ig_status?: "SUCCESS" | "FAILED";
109
+ }
110
+ > {
105
111
  if (this.errorMode) {
106
112
  return Promise.reject(new Error("Failed to publish Threads container"));
107
113
  }
@@ -377,6 +383,36 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
377
383
  return Promise.resolve({ success: true });
378
384
  }
379
385
 
386
+ getPendingReplies(
387
+ _mediaId: string,
388
+ _accessToken: string,
389
+ _options?: CursorPaginationOptions,
390
+ _fields?: string[],
391
+ _reverse?: boolean,
392
+ _approvalStatus?: "pending" | "ignored",
393
+ ): Promise<ThreadsListResponse> {
394
+ if (this.errorMode) {
395
+ return Promise.reject(new Error("Failed to get pending replies"));
396
+ }
397
+ return Promise.resolve({
398
+ data: Array.from(this.posts.values()).slice(0, 25),
399
+ paging: {
400
+ cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" },
401
+ },
402
+ });
403
+ }
404
+
405
+ managePendingReply(
406
+ _replyId: string,
407
+ _accessToken: string,
408
+ _approve: boolean,
409
+ ): Promise<{ success: boolean }> {
410
+ if (this.errorMode) {
411
+ return Promise.reject(new Error("Failed to manage pending reply"));
412
+ }
413
+ return Promise.resolve({ success: true });
414
+ }
415
+
380
416
  getMentions(
381
417
  _userId: string,
382
418
  _accessToken: string,
@@ -504,6 +540,7 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
504
540
  return Promise.resolve({
505
541
  access_token: "short_lived_token_abc123",
506
542
  user_id: "12345",
543
+ token_type: "bearer",
507
544
  });
508
545
  }
509
546
 
@@ -564,8 +601,8 @@ export class MockThreadsAPIImpl implements MockThreadsAPI {
564
601
  }
565
602
 
566
603
  getOEmbed(
567
- _accessToken: string,
568
- _url: string,
604
+ _accessTokenOrUrl: string,
605
+ _urlOrUndefined?: string,
569
606
  _maxWidth?: number,
570
607
  ): Promise<OEmbedResponse> {
571
608
  if (this.errorMode) {
@@ -51,6 +51,11 @@ export async function validateRequest(
51
51
  if (request.replyToId) {
52
52
  throw new Error("isGhostPost cannot be used together with replyToId");
53
53
  }
54
+ if (request.enableReplyApprovals) {
55
+ throw new Error(
56
+ "enableReplyApprovals cannot be used together with isGhostPost",
57
+ );
58
+ }
54
59
  }
55
60
 
56
61
  // Text attachment can only be used with TEXT posts and not with polls