@postfetch/core 0.13.2 → 0.13.4

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/dist/index.js CHANGED
@@ -409,13 +409,13 @@ async function instagramUnavailable(net, code, pageReason) {
409
409
  url.searchParams.set("url", `https://www.instagram.com/p/${code}/`);
410
410
  const response = await net(url.href, { headers: mobileHeaders() }, 1).catch(() => null);
411
411
  const body = response ? await response.text().catch(() => "") : "";
412
- const reason = pageReason ?? instagramUnavailableReason(response?.status ?? 0, body);
412
+ const reason = pageReason ?? instagramUnavailableReason(response?.status ?? 0, body, code);
413
413
  const probe = response ? `oembed ${response.status}` : "oembed unreachable";
414
414
  return new PostfetchError(reasonStatus(reason), `Instagram post unavailable: ${reason} (${probe})`, reason);
415
415
  }
416
- function instagramUnavailableReason(status, body) {
416
+ function instagramUnavailableReason(status, body, code) {
417
417
  if (status === 404) {
418
- return "notFound";
418
+ return code && isPrivateShortcode(code) ? "private" : "notFound";
419
419
  }
420
420
  if (status === 429) {
421
421
  return "rateLimited";
@@ -434,6 +434,9 @@ function instagramUnavailableReason(status, body) {
434
434
  }
435
435
  return "unavailable";
436
436
  }
437
+ function isPrivateShortcode(code) {
438
+ return code.length > 11;
439
+ }
437
440
  function reasonStatus(reason) {
438
441
  switch (reason) {
439
442
  case "ageRestricted":
@@ -1642,14 +1645,17 @@ async function appToken(net) {
1642
1645
  return token;
1643
1646
  }
1644
1647
  async function fetchPost(net, token, id) {
1645
- const response = await net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=1`, {
1646
- headers: { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() }
1647
- });
1648
+ const headers = { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() };
1649
+ let response = await net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=1`, { headers });
1650
+ if (response.status === 403) {
1651
+ await response.body?.cancel();
1652
+ response = await net(`https://oauth.reddit.com/by_id/t3_${id}?raw_json=1`, { headers });
1653
+ }
1648
1654
  if (!response.ok) {
1649
1655
  throw new Error(`Reddit post failed: ${response.status}`);
1650
1656
  }
1651
1657
  const payload = await response.json().catch(() => null);
1652
- const listing = Array.isArray(payload) ? payload[0] : null;
1658
+ const listing = Array.isArray(payload) ? payload[0] : payload;
1653
1659
  const children = object(listing) && object(listing.data) && Array.isArray(listing.data.children) ? listing.data.children : [];
1654
1660
  const first = children[0];
1655
1661
  const post = object(first) && object(first.data) ? first.data : null;
@@ -1,6 +1,6 @@
1
1
  import { type InstagramExtra, type PostMetadata, type PostfetchReason, type ResolveContext, type Json, type PostfetchResult } from "./internal";
2
2
  export declare function resolveInstagram(input: ResolveContext): Promise<PostfetchResult>;
3
- export declare function instagramUnavailableReason(status: number, body: string): PostfetchReason;
3
+ export declare function instagramUnavailableReason(status: number, body: string, code?: string): PostfetchReason;
4
4
  export declare function instagramMetadata(media: Json): PostMetadata & {
5
5
  extra?: InstagramExtra;
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postfetch/core",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "Zero-dependency typed core that turns Facebook, Instagram, LinkedIn, Pinterest, Reddit, SoundCloud, Spotify, TikTok, X and YouTube post URLs into media files.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/instagram.ts CHANGED
@@ -91,7 +91,7 @@ async function instagramUnavailable(net: Net, code: string, pageReason?: Postfet
91
91
  const body = response ? await response.text().catch(() => "") : "";
92
92
  // The page can identify an age gate even when oembed only reports a generic
93
93
  // geoblock_required error with no MIN_AGE_ACCOUNT marker.
94
- const reason = pageReason ?? instagramUnavailableReason(response?.status ?? 0, body);
94
+ const reason = pageReason ?? instagramUnavailableReason(response?.status ?? 0, body, code);
95
95
  // Carry the oembed status so an unclassified failure still leaves a raw clue
96
96
  // (200 = post is public but no media could be parsed; 429 = throttled; …).
97
97
  const probe = response ? `oembed ${response.status}` : "oembed unreachable";
@@ -101,10 +101,11 @@ async function instagramUnavailable(net: Net, code: string, pageReason?: Postfet
101
101
  // Classifies why the post is gone from the oembed endpoint, which returns a
102
102
  // telling status and body (404 when removed; 429 when throttled; 400 with a
103
103
  // MIN_AGE_ACCOUNT block for 18+ accounts) while the media endpoints just come
104
- // back empty.
105
- export function instagramUnavailableReason(status: number, body: string): PostfetchReason {
104
+ // back empty. Share links to posts from private accounts carry a long
105
+ // shortcode, and oembed answers those with the same 404 as a removed post.
106
+ export function instagramUnavailableReason(status: number, body: string, code?: string): PostfetchReason {
106
107
  if (status === 404) {
107
- return "notFound";
108
+ return code && isPrivateShortcode(code) ? "private" : "notFound";
108
109
  }
109
110
  if (status === 429) {
110
111
  return "rateLimited";
@@ -124,6 +125,12 @@ export function instagramUnavailableReason(status: number, body: string): Postfe
124
125
  return "unavailable";
125
126
  }
126
127
 
128
+ // Public shortcodes are at most 11 characters; private share links append an
129
+ // owner-scoped suffix (for example 39 characters in total).
130
+ function isPrivateShortcode(code: string): boolean {
131
+ return code.length > 11;
132
+ }
133
+
127
134
  function reasonStatus(reason: PostfetchReason): number {
128
135
  switch (reason) {
129
136
  case "ageRestricted":
package/src/reddit.ts CHANGED
@@ -100,14 +100,19 @@ async function appToken(net: Net): Promise<string> {
100
100
  }
101
101
 
102
102
  async function fetchPost(net: Net, token: string, id: string): Promise<Json> {
103
- const response = await net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=1`, {
104
- headers: { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() },
105
- });
103
+ const headers = { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() };
104
+ let response = await net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=1`, { headers });
105
+ // Reddit can block the comments route while still allowing direct post
106
+ // listings with the same token. This route returns the post, not its replies.
107
+ if (response.status === 403) {
108
+ await response.body?.cancel();
109
+ response = await net(`https://oauth.reddit.com/by_id/t3_${id}?raw_json=1`, { headers });
110
+ }
106
111
  if (!response.ok) {
107
112
  throw new Error(`Reddit post failed: ${response.status}`);
108
113
  }
109
114
  const payload = await response.json().catch(() => null);
110
- const listing = Array.isArray(payload) ? payload[0] : null;
115
+ const listing = Array.isArray(payload) ? payload[0] : payload;
111
116
  const children = object(listing) && object(listing.data) && Array.isArray(listing.data.children) ? listing.data.children : [];
112
117
  const first = children[0];
113
118
  const post = object(first) && object(first.data) ? first.data : null;