discord-message-transcript 1.3.1-dev.3.35 → 1.3.1

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.
Files changed (57) hide show
  1. package/dist/core/cdnResolver.d.ts +2 -2
  2. package/dist/core/cdnResolver.js +12 -9
  3. package/dist/core/componentToJson.js +1 -1
  4. package/dist/core/fetchMessages.d.ts +1 -1
  5. package/dist/core/fetchMessages.js +1 -1
  6. package/dist/core/getMentions.d.ts +1 -1
  7. package/dist/core/getMentions.js +1 -1
  8. package/dist/core/imageToBase64.d.ts +2 -1
  9. package/dist/core/imageToBase64.js +17 -4
  10. package/dist/core/mappers.d.ts +1 -1
  11. package/dist/core/mappers.js +1 -1
  12. package/dist/core/networkSecurity/constants.d.ts +3 -0
  13. package/dist/core/networkSecurity/constants.js +3 -0
  14. package/dist/core/networkSecurity/dns.d.ts +2 -0
  15. package/dist/core/networkSecurity/dns.js +29 -0
  16. package/dist/core/networkSecurity/index.d.ts +2 -0
  17. package/dist/core/networkSecurity/index.js +2 -0
  18. package/dist/core/networkSecurity/ip.d.ts +1 -0
  19. package/dist/core/networkSecurity/ip.js +110 -0
  20. package/dist/core/networkSecurity/lookup.d.ts +2 -0
  21. package/dist/core/networkSecurity/lookup.js +14 -0
  22. package/dist/core/networkSecurity/urlSafety.d.ts +3 -0
  23. package/dist/core/networkSecurity/urlSafety.js +71 -0
  24. package/dist/core/resolveImageUrl.d.ts +4 -0
  25. package/dist/core/resolveImageUrl.js +20 -0
  26. package/dist/core/urlResolver.d.ts +2 -2
  27. package/dist/core/urlResolver.js +20 -15
  28. package/dist/index.d.ts +3 -3
  29. package/dist/index.js +8 -8
  30. package/dist/renderers/json/json.d.ts +1 -1
  31. package/dist/renderers/json/json.js +2 -2
  32. package/dist/types/types.d.ts +12 -0
  33. package/package.json +4 -7
  34. package/dist/core/clientManager.d.ts +0 -3
  35. package/dist/core/clientManager.js +0 -9
  36. package/dist/core/componentHelpers.d.ts +0 -3
  37. package/dist/core/componentHelpers.js +0 -175
  38. package/dist/core/error.d.ts +0 -3
  39. package/dist/core/error.js +0 -7
  40. package/dist/core/markdown.d.ts +0 -2
  41. package/dist/core/markdown.js +0 -175
  42. package/dist/renderers/html/clientRenderer.d.ts +0 -0
  43. package/dist/renderers/html/clientRenderer.js +0 -73
  44. package/dist/renderers/html/css.d.ts +0 -11
  45. package/dist/renderers/html/css.js +0 -663
  46. package/dist/renderers/html/html copy.d.ts +0 -19
  47. package/dist/renderers/html/html copy.js +0 -371
  48. package/dist/renderers/html/html-backup.d.ts +0 -19
  49. package/dist/renderers/html/html-backup.js +0 -371
  50. package/dist/renderers/html/html.d.ts +0 -19
  51. package/dist/renderers/html/html.js +0 -415
  52. package/dist/renderers/html/html2.d.ts +0 -8
  53. package/dist/renderers/html/html2.js +0 -233
  54. package/dist/renderers/html/js.d.ts +0 -4
  55. package/dist/renderers/html/js.js +0 -174
  56. package/dist/types/types copy.d.ts +0 -284
  57. package/dist/types/types copy.js +0 -35
@@ -1,5 +1,5 @@
1
- import { CDNOptions } from "../types/types.js";
1
+ import { CDNOptions, safeUrlReturn } from "@/types";
2
2
  import { TranscriptOptionsBase } from "discord-message-transcript-base";
3
- export declare function cdnResolver(url: string, options: TranscriptOptionsBase, cdnOptions: CDNOptions): Promise<string>;
3
+ export declare function cdnResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions): Promise<string>;
4
4
  export declare function uploadCareResolver(url: string, publicKey: string, cdnDomain: string, disableWarnings: boolean): Promise<string>;
5
5
  export declare function cloudinaryResolver(url: string, fileName: string, cloudName: string, apiKey: string, apiSecret: string, disableWarnings: boolean): Promise<string>;
@@ -1,21 +1,24 @@
1
- import https from 'https';
2
- import http from 'http';
3
1
  import { CustomWarn } from "discord-message-transcript-base";
4
2
  import crypto from 'crypto';
5
3
  import { getCDNLimiter } from "./limiter.js";
6
- export async function cdnResolver(url, options, cdnOptions) {
4
+ import https from 'https';
5
+ import http from 'http';
6
+ import { createLookup } from "@/networkSecurity";
7
+ export async function cdnResolver(safeUrlObject, options, cdnOptions) {
8
+ const url = safeUrlObject.url;
7
9
  const limit = getCDNLimiter();
8
10
  return limit(async () => {
9
11
  return new Promise((resolve, reject) => {
10
- const client = url.startsWith('https') ? https : http;
11
- const request = client.request(url, {
12
- method: 'HEAD',
13
- headers: { "User-Agent": "discord-message-transcript" }
12
+ const client = safeUrlObject.url.startsWith('https') ? https : http;
13
+ const lookup = createLookup(safeUrlObject.safeIps);
14
+ const request = client.get(url, {
15
+ headers: { "User-Agent": "discord-message-transcript" },
16
+ lookup: lookup
14
17
  }, async (response) => {
15
18
  if (response.statusCode !== 200) {
16
19
  response.destroy();
17
20
  CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
18
- Failed to fetch attachment with status code: ${response.statusCode} from ${url}.`, options.disableWarnings);
21
+ Failed to fetch attachment with status code: ${response.statusCode} from ${safeUrlObject.url}.`, options.disableWarnings);
19
22
  return resolve(url);
20
23
  }
21
24
  const contentType = response.headers["content-type"];
@@ -47,7 +50,7 @@ Error: ${err.message}`, options.disableWarnings);
47
50
  request.destroy();
48
51
  CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
49
52
  Request timeout for ${url}.`, options.disableWarnings);
50
- resolve(url);
53
+ return resolve(url);
51
54
  });
52
55
  request.end();
53
56
  });
@@ -1,7 +1,7 @@
1
1
  import { ComponentType } from "discord.js";
2
2
  import { mapButtonStyle, mapSelectorType, mapSeparatorSpacing } from "./mappers.js";
3
3
  import { JsonComponentType } from "discord-message-transcript-base";
4
- import { isValidHexColor } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
4
+ import { isValidHexColor } from "discord-message-transcript-base";
5
5
  export async function componentsToJson(components, options) {
6
6
  const processedComponents = await Promise.all(components.filter(component => !(!options.includeV2Components && component.type != ComponentType.ActionRow))
7
7
  .map(async (component) => {
@@ -1,6 +1,6 @@
1
1
  import { TextBasedChannel } from "discord.js";
2
2
  import { JsonAuthor, JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
3
- import { MapMentions } from "../types/types.js";
3
+ import { MapMentions } from "@/types";
4
4
  export declare function fetchMessages(ctx: FetchMessagesContext): Promise<{
5
5
  messages: JsonMessage[];
6
6
  end: boolean;
@@ -1,7 +1,7 @@
1
1
  import { EmbedType } from "discord.js";
2
2
  import { componentsToJson } from "./componentToJson.js";
3
+ import { isValidHexColor, sanitize } from "discord-message-transcript-base";
3
4
  import { getMentions } from "./getMentions.js";
4
- import { isValidHexColor, sanitize } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
5
5
  export async function fetchMessages(ctx) {
6
6
  const { channel, options, transcriptState, lastMessageId } = ctx;
7
7
  const { authors, mentions } = transcriptState;
@@ -1,3 +1,3 @@
1
1
  import { Message } from "discord.js";
2
- import { MapMentions } from "../types/types.js";
2
+ import { MapMentions } from "@/types";
3
3
  export declare function getMentions(message: Message, mentions: MapMentions): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { ChannelType } from "discord.js";
2
- import { isValidHexColor, sanitize } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
2
+ import { isValidHexColor, sanitize } from "discord-message-transcript-base";
3
3
  export async function getMentions(message, mentions) {
4
4
  message.mentions.channels.forEach(channel => {
5
5
  if (!mentions.channels.has(channel.id)) {
@@ -1 +1,2 @@
1
- export declare function imageToBase64(url: string, disableWarnings: boolean): Promise<string>;
1
+ import { safeUrlReturn } from '@/types';
2
+ export declare function imageToBase64(safeUrlObject: safeUrlReturn, disableWarnings: boolean): Promise<string>;
@@ -1,13 +1,20 @@
1
- import https from 'https';
2
- import http from 'http';
3
1
  import { CustomWarn } from 'discord-message-transcript-base';
4
2
  import { getBase64Limiter } from './limiter.js';
5
- export async function imageToBase64(url, disableWarnings) {
3
+ import https from 'https';
4
+ import http from 'http';
5
+ import { createLookup } from '@/networkSecurity';
6
+ const MAX_BYTES = 25 * 1024 * 1024; // 25MB
7
+ export async function imageToBase64(safeUrlObject, disableWarnings) {
8
+ const url = safeUrlObject.url;
6
9
  const limit = getBase64Limiter();
7
10
  return limit(async () => {
8
11
  return new Promise((resolve, reject) => {
9
12
  const client = url.startsWith('https') ? https : http;
10
- const request = client.get(url, { headers: { "User-Agent": "discord-message-transcript" } }, (response) => {
13
+ const lookup = createLookup(safeUrlObject.safeIps);
14
+ const request = client.get(url, {
15
+ headers: { "User-Agent": "discord-message-transcript" },
16
+ lookup: lookup
17
+ }, (response) => {
11
18
  if (response.statusCode !== 200) {
12
19
  response.destroy();
13
20
  CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
@@ -19,8 +26,14 @@ Failed to fetch image with status code: ${response.statusCode} from ${url}.`, di
19
26
  response.destroy();
20
27
  return resolve(url);
21
28
  }
29
+ let total = 0;
22
30
  const chunks = [];
23
31
  response.on('data', (chunk) => {
32
+ total += chunk.length;
33
+ if (total > MAX_BYTES) {
34
+ response.destroy();
35
+ return resolve(url);
36
+ }
24
37
  chunks.push(chunk);
25
38
  });
26
39
  response.on('end', () => {
@@ -1,6 +1,6 @@
1
1
  import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
2
2
  import { JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
3
- import { ReturnType } from "../types/types.js";
3
+ import { ReturnType } from "@/types";
4
4
  export declare function mapButtonStyle(style: ButtonStyle): JsonButtonStyle;
5
5
  export declare function mapSeparatorSpacing(spacing: SeparatorSpacingSize): JsonSeparatorSpacingSize;
6
6
  export declare function mapComponentType(componentType: ComponentType): JsonComponentType;
@@ -1,6 +1,6 @@
1
1
  import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
2
2
  import { CustomError, JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
3
- import { ReturnType } from "../types/types.js";
3
+ import { ReturnType } from "@/types";
4
4
  export function mapButtonStyle(style) {
5
5
  switch (style) {
6
6
  case ButtonStyle.Primary:
@@ -0,0 +1,3 @@
1
+ export declare const DNS_SERVERS: string[];
2
+ export declare const DNS_LOOKUP_TIMEOUT = 5000;
3
+ export declare const TRUSTED_DISCORD_HOSTS: string[];
@@ -0,0 +1,3 @@
1
+ export const DNS_SERVERS = ["1.1.1.1", "8.8.8.8"];
2
+ export const DNS_LOOKUP_TIMEOUT = 5000;
3
+ export const TRUSTED_DISCORD_HOSTS = ["discordapp.com", "discordapp.net"];
@@ -0,0 +1,2 @@
1
+ import { LookupResult } from "@/types";
2
+ export declare function resolveAllIps(host: string): Promise<LookupResult[]>;
@@ -0,0 +1,29 @@
1
+ import { Resolver } from "dns/promises";
2
+ import { DNS_LOOKUP_TIMEOUT, DNS_SERVERS } from "./constants.js";
3
+ export async function resolveAllIps(host) {
4
+ const resolver = new Resolver();
5
+ resolver.setServers(DNS_SERVERS);
6
+ const lookupPromise = (async () => {
7
+ const results = [];
8
+ const [v4, v6] = await Promise.allSettled([
9
+ resolver.resolve4(host),
10
+ resolver.resolve6(host)
11
+ ]);
12
+ if (v4.status === "fulfilled") {
13
+ for (const ip of v4.value) {
14
+ results.push({ address: ip, family: 4 });
15
+ }
16
+ }
17
+ if (v6.status === "fulfilled") {
18
+ for (const ip of v6.value) {
19
+ results.push({ address: ip, family: 6 });
20
+ }
21
+ }
22
+ if (results.length === 0) {
23
+ throw new Error(`No DNS records found for ${host}`);
24
+ }
25
+ return results;
26
+ })();
27
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`DNS timeout for ${host}`)), DNS_LOOKUP_TIMEOUT));
28
+ return Promise.race([lookupPromise, timeoutPromise]);
29
+ }
@@ -0,0 +1,2 @@
1
+ export * from './urlSafety.js';
2
+ export * from './lookup.js';
@@ -0,0 +1,2 @@
1
+ export * from './urlSafety.js';
2
+ export * from './lookup.js';
@@ -0,0 +1 @@
1
+ export declare function isPrivateIp(ip: string): boolean;
@@ -0,0 +1,110 @@
1
+ import net from "node:net";
2
+ export function isPrivateIp(ip) {
3
+ const family = net.isIP(ip);
4
+ if (!family)
5
+ return true;
6
+ if (family === 4)
7
+ return isPrivateIPv4(ip);
8
+ return isPrivateIPv6(ip);
9
+ }
10
+ function isPrivateIPv4(ip) {
11
+ const parts = ip.split(".").map(Number);
12
+ if (parts.length !== 4 || parts.some(n => isNaN(n)))
13
+ return true;
14
+ const [a, b] = parts;
15
+ return (a === 0 ||
16
+ a === 10 ||
17
+ a === 127 ||
18
+ (a === 169 && b === 254) ||
19
+ (a === 172 && b >= 16 && b <= 31) ||
20
+ (a === 192 && b === 168) ||
21
+ (a === 100 && b >= 64 && b <= 127) ||
22
+ a >= 224);
23
+ }
24
+ function parseIPv6(ip) {
25
+ if (net.isIP(ip) !== 6)
26
+ return null;
27
+ // handle IPv4 at end
28
+ if (ip.includes(".")) {
29
+ const lastColon = ip.lastIndexOf(":");
30
+ const ipv4Part = ip.slice(lastColon + 1);
31
+ const nums = ipv4Part.split(".").map(Number);
32
+ if (nums.length === 4 && nums.every(n => !isNaN(n))) {
33
+ const hex = ((nums[0] << 8) | nums[1]).toString(16) +
34
+ ":" +
35
+ ((nums[2] << 8) | nums[3]).toString(16);
36
+ ip = ip.slice(0, lastColon) + ":" + hex;
37
+ }
38
+ }
39
+ const sections = ip.split("::");
40
+ let head = sections[0] ? sections[0].split(":") : [];
41
+ let tail = sections[1] ? sections[1].split(":") : [];
42
+ if (sections.length === 2) {
43
+ const missing = 8 - (head.length + tail.length);
44
+ head = [...head, ...Array(missing).fill("0"), ...tail];
45
+ }
46
+ if (head.length !== 8)
47
+ return null;
48
+ const bytes = [];
49
+ for (const part of head) {
50
+ const n = parseInt(part || "0", 16);
51
+ if (isNaN(n))
52
+ return null;
53
+ bytes.push((n >> 8) & 0xff);
54
+ bytes.push(n & 0xff);
55
+ }
56
+ return bytes;
57
+ }
58
+ function extractEmbeddedIPv4(bytes) {
59
+ const isMapped = bytes.slice(0, 10).every(b => b === 0) &&
60
+ bytes[10] === 0xff &&
61
+ bytes[11] === 0xff;
62
+ if (isMapped) {
63
+ return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
64
+ }
65
+ const isCompat = bytes.slice(0, 12).every(b => b === 0);
66
+ if (isCompat) {
67
+ return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
68
+ }
69
+ const isNat64 = bytes[0] === 0x00 &&
70
+ bytes[1] === 0x64 &&
71
+ bytes[2] === 0xff &&
72
+ bytes[3] === 0x9b &&
73
+ bytes.slice(4, 12).every(b => b === 0);
74
+ if (isNat64) {
75
+ return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
76
+ }
77
+ return null;
78
+ }
79
+ function isPrivateIPv6(ip) {
80
+ const bytes = parseIPv6(ip);
81
+ if (!bytes)
82
+ return true;
83
+ const embedded = extractEmbeddedIPv4(bytes);
84
+ if (embedded)
85
+ return isPrivateIPv4(embedded);
86
+ // ::
87
+ if (bytes.every(b => b === 0))
88
+ return true;
89
+ // ::1
90
+ if (bytes.slice(0, 15).every(b => b === 0) && bytes[15] === 1)
91
+ return true;
92
+ const first = bytes[0];
93
+ const second = bytes[1];
94
+ // fc00::/7
95
+ if ((first & 0xfe) === 0xfc)
96
+ return true;
97
+ // fe80::/10
98
+ if (first === 0xfe && (second & 0xc0) === 0x80)
99
+ return true;
100
+ // multicast
101
+ if (first === 0xff)
102
+ return true;
103
+ // 2001:db8::/32
104
+ if (bytes[0] === 0x20 &&
105
+ bytes[1] === 0x01 &&
106
+ bytes[2] === 0x0d &&
107
+ bytes[3] === 0xb8)
108
+ return true;
109
+ return false;
110
+ }
@@ -0,0 +1,2 @@
1
+ export declare function urlToIpUrl(url: string, ip: string): string;
2
+ export declare function createLookup(safeIps: string[]): ((_hostname: string, _opts: any, cb: any) => void) | undefined;
@@ -0,0 +1,14 @@
1
+ import net from "node:net";
2
+ export function urlToIpUrl(url, ip) {
3
+ // If got here shouldn't throw a error
4
+ const u = new URL(url);
5
+ return `${u.protocol}//${ip}` + `${u.port ? ":" + u.port : ""}` + `${u.pathname}${u.search}`;
6
+ }
7
+ export function createLookup(safeIps) {
8
+ if (safeIps.length == 0)
9
+ return undefined;
10
+ return (_hostname, _opts, cb) => {
11
+ const ip = safeIps[Math.floor(Math.random() * safeIps.length)];
12
+ cb(null, ip, net.isIP(ip));
13
+ };
14
+ }
@@ -0,0 +1,3 @@
1
+ import { TranscriptOptionsBase } from "discord-message-transcript-base";
2
+ import { safeUrlReturn } from "@/types";
3
+ export declare function isSafeForHTML(url: string, options: TranscriptOptionsBase): Promise<safeUrlReturn>;
@@ -0,0 +1,71 @@
1
+ import { CustomWarn } from "discord-message-transcript-base";
2
+ import { TRUSTED_DISCORD_HOSTS } from "./constants.js";
3
+ import { isPrivateIp } from "./ip.js";
4
+ import { resolveAllIps } from "./dns.js";
5
+ export async function isSafeForHTML(url, options) {
6
+ const { safeMode, disableWarnings } = options;
7
+ if (!safeMode)
8
+ return { safe: true, safeIps: [], url: url };
9
+ let u;
10
+ try {
11
+ u = new URL(url);
12
+ }
13
+ catch {
14
+ CustomWarn(`Unsafe URL rejected: Invalid URL format\nURL: ${url}`, disableWarnings);
15
+ return { safe: false, safeIps: [], url: url };
16
+ }
17
+ const host = u.hostname.toLowerCase();
18
+ // If is from discord accept
19
+ if (isTrustedDiscordHost(host))
20
+ return { safe: true, safeIps: [], url: url };
21
+ // Don't accept if isn't https or http
22
+ if (!["http:", "https:"].includes(u.protocol)) {
23
+ CustomWarn(`Unsafe URL rejected: Invalid protocol "${u.protocol}"\nURL: ${url}`, disableWarnings);
24
+ return { safe: false, safeIps: [], url: url };
25
+ }
26
+ if (u.username || u.password) {
27
+ CustomWarn(`Unsafe URL rejected: Contains username or password\nURL: ${url}`, disableWarnings);
28
+ return { safe: false, safeIps: [], url: url };
29
+ }
30
+ if (u.port && !["80", "443", ""].includes(u.port)) {
31
+ CustomWarn(`Unsafe URL rejected: Invalid port "${u.port}"\nURL: ${url}`, disableWarnings);
32
+ return { safe: false, safeIps: [], url: url };
33
+ }
34
+ // Block localhost and loopback addresses (SSRF protection)
35
+ if (host === "localhost" ||
36
+ host === "127.0.0.1" ||
37
+ host.startsWith("0.")) {
38
+ CustomWarn(`Unsafe URL rejected: Blacklisted host "${host}"\nURL: ${url}`, disableWarnings);
39
+ return { safe: false, safeIps: [], url: url };
40
+ }
41
+ let ips;
42
+ try {
43
+ ips = await resolveAllIps(host);
44
+ }
45
+ catch (e) {
46
+ CustomWarn(`Unsafe URL rejected: DNS lookup failed or timed out for host "${host}". Error: ${e.message}\nURL: ${url}`, disableWarnings);
47
+ return { safe: false, safeIps: [], url: url };
48
+ }
49
+ const safeIps = [];
50
+ // Block private/internal network IPs (SSRF protection)
51
+ for (const ip of ips) {
52
+ if (isPrivateIp(ip.address)) {
53
+ CustomWarn(`Unsafe URL rejected: Private IP address "${ip.address}" resolved for host "${host}"\nURL: ${url}`, disableWarnings);
54
+ return { safe: false, safeIps: [], url: url };
55
+ }
56
+ safeIps.push(ip.address);
57
+ }
58
+ const path = u.pathname.toLowerCase();
59
+ // External SVGs can execute scripts → allow only from Discord CDN
60
+ if (path.endsWith(".svg")) {
61
+ CustomWarn(`Unsafe URL rejected: External SVG not from Discord CDN\nURL: ${url}`, disableWarnings);
62
+ return { safe: false, safeIps: [], url: url };
63
+ }
64
+ return { safe: true, safeIps: safeIps, url: url };
65
+ }
66
+ function isTrustedDiscordHost(host) {
67
+ host = host.toLowerCase();
68
+ return TRUSTED_DISCORD_HOSTS.some(trusted => {
69
+ return host === trusted || host.endsWith("." + trusted);
70
+ });
71
+ }
@@ -0,0 +1,4 @@
1
+ import { JsonAttachment, TranscriptOptionsBase } from "discord-message-transcript-base";
2
+ import { safeUrlReturn } from "@/types";
3
+ export declare function resolveImageURL(url: string, options: TranscriptOptionsBase, canReturnNull: false, attachments?: JsonAttachment[]): Promise<safeUrlReturn>;
4
+ export declare function resolveImageURL(url: string | null, options: TranscriptOptionsBase, canReturnNull: true, attachments?: JsonAttachment[]): Promise<safeUrlReturn | null>;
@@ -0,0 +1,20 @@
1
+ import { FALLBACK_PIXEL } from "discord-message-transcript-base";
2
+ import { isSafeForHTML } from "@/networkSecurity";
3
+ export async function resolveImageURL(url, options, canReturnNull, attachments) {
4
+ if (!url)
5
+ return null;
6
+ // Resolve attachment:// references to actual attachment URL
7
+ if (url.startsWith("attachment://")) {
8
+ const name = url.slice("attachment://".length).trim();
9
+ const found = attachments?.find(a => a.name === name);
10
+ if (!found)
11
+ return { safe: true, safeIps: [], url: FALLBACK_PIXEL };
12
+ url = found.url;
13
+ }
14
+ const safeUrlReturn = await isSafeForHTML(url, options);
15
+ if (safeUrlReturn.safe)
16
+ return safeUrlReturn;
17
+ if (canReturnNull)
18
+ return null;
19
+ return { safe: true, safeIps: [], url: FALLBACK_PIXEL };
20
+ }
@@ -1,5 +1,5 @@
1
1
  import { JsonAuthor, JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
2
- import { CDNOptions } from "../types/types.js";
3
- export declare function urlResolver(url: string, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<string>;
2
+ import { CDNOptions, safeUrlReturn } from "@/types";
3
+ export declare function urlResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<string>;
4
4
  export declare function messagesUrlResolver(messages: JsonMessage[], options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonMessage[]>;
5
5
  export declare function authorUrlResolver(authors: Map<string, JsonAuthor>, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonAuthor[]>;
@@ -2,40 +2,44 @@ import { JsonComponentType } from "discord-message-transcript-base";
2
2
  import { cdnResolver } from "./cdnResolver.js";
3
3
  import { imageToBase64 } from "./imageToBase64.js";
4
4
  import { isJsonComponentInContainer } from "./componentToJson.js";
5
- import { FALLBACK_PIXEL, isSafeForHTML, resolveImageURL } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
6
- export async function urlResolver(url, options, cdnOptions, urlCache) {
7
- if (url == FALLBACK_PIXEL || url == "")
8
- return url;
9
- if (urlCache.has(url)) {
10
- const cache = urlCache.get(url);
5
+ import { FALLBACK_PIXEL } from "discord-message-transcript-base";
6
+ import { resolveImageURL } from "./resolveImageUrl.js";
7
+ import { isSafeForHTML } from "@/networkSecurity";
8
+ export async function urlResolver(safeUrlObject, options, cdnOptions, urlCache) {
9
+ if (safeUrlObject.safe == false)
10
+ return "";
11
+ if (safeUrlObject.url == FALLBACK_PIXEL)
12
+ return safeUrlObject.url;
13
+ if (urlCache.has(safeUrlObject.url)) {
14
+ const cache = urlCache.get(safeUrlObject.url);
11
15
  if (cache)
12
16
  return await cache;
13
17
  }
14
18
  let returnUrl;
15
19
  if (cdnOptions)
16
- returnUrl = cdnResolver(url, options, cdnOptions);
20
+ returnUrl = cdnResolver(safeUrlObject, options, cdnOptions);
17
21
  else if (options.saveImages)
18
- returnUrl = imageToBase64(url, options.disableWarnings);
22
+ returnUrl = imageToBase64(safeUrlObject, options.disableWarnings);
19
23
  if (returnUrl) {
20
- urlCache.set(url, returnUrl);
24
+ urlCache.set(safeUrlObject.url, returnUrl);
21
25
  return await returnUrl;
22
26
  }
23
- return url;
27
+ return safeUrlObject.url;
24
28
  }
25
29
  export async function messagesUrlResolver(messages, options, cdnOptions, urlCache) {
26
30
  return await Promise.all(messages.map(async (message) => {
27
31
  // Needs to wait for resolve correct when used attachment://
28
32
  const attachments = await Promise.all(message.attachments.map(async (attachment) => {
29
- let url;
33
+ let safeUrlObject;
30
34
  if (attachment.contentType?.startsWith("image/")) {
31
- url = await resolveImageURL(attachment.url, options, false, message.attachments);
35
+ safeUrlObject = await resolveImageURL(attachment.url, options, false, message.attachments);
32
36
  }
33
37
  else {
34
- url = await isSafeForHTML(attachment.url, options) ? attachment.url : "";
38
+ safeUrlObject = await isSafeForHTML(attachment.url, options);
35
39
  }
36
40
  return {
37
41
  ...attachment,
38
- url: await urlResolver(url, options, cdnOptions, urlCache)
42
+ url: await urlResolver(safeUrlObject, options, cdnOptions, urlCache)
39
43
  };
40
44
  }));
41
45
  const embedsPromise = Promise.all(message.embeds.map(async (embed) => {
@@ -78,9 +82,10 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
78
82
  };
79
83
  }
80
84
  if (component.type == JsonComponentType.File) {
85
+ const safeUrlObject = await isSafeForHTML(component.url, options);
81
86
  return {
82
87
  ...component,
83
- url: await urlResolver((await isSafeForHTML(component.url, options) ? component.url : ""), options, cdnOptions, urlCache),
88
+ url: await urlResolver(safeUrlObject, options, cdnOptions, urlCache),
84
89
  };
85
90
  }
86
91
  if (component.type == JsonComponentType.Container) {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType, CDNOptions, MimeType } from "./types/types.js";
1
+ export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType, CDNOptions, MimeType } from "@/types";
2
2
  export { ReturnFormat, LocalDate, TimeZone } from "discord-message-transcript-base";
3
- export { setBase64Concurrency, setCDNConcurrency } from './core/limiter.js';
3
+ export { setBase64Concurrency, setCDNConcurrency } from '@/core/limiter.js';
4
4
  import { TextBasedChannel } from "discord.js";
5
- import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "./types/types.js";
5
+ import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "@/types";
6
6
  /**
7
7
  * Creates a transcript of a Discord channel's messages.
8
8
  * Depending on the `returnType` option, this function can return an `AttachmentBuilder`,
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- export { ReturnType } from "./types/types.js";
1
+ export { ReturnType } from "@/types";
2
2
  export { ReturnFormat } from "discord-message-transcript-base";
3
- export { setBase64Concurrency, setCDNConcurrency } from './core/limiter.js';
3
+ export { setBase64Concurrency, setCDNConcurrency } from '@/core/limiter.js';
4
4
  import { AttachmentBuilder } from "discord.js";
5
- import { Json } from "./renderers/json/json.js";
6
- import { fetchMessages } from "./core/fetchMessages.js";
7
- import { ReturnType } from "./types/types.js";
8
- import { output } from "./core/output.js";
5
+ import { Json } from "@/renderers/json/json.js";
6
+ import { fetchMessages } from "@/core/fetchMessages.js";
7
+ import { ReturnType } from "@/types";
8
+ import { output } from "@/core/output.js";
9
9
  import { ReturnTypeBase, ReturnFormat, outputBase, CustomError, CustomWarn } from "discord-message-transcript-base";
10
- import { returnTypeMapper } from "./core/mappers.js";
11
- import { authorUrlResolver, messagesUrlResolver } from "./core/urlResolver.js";
10
+ import { returnTypeMapper } from "@/core/mappers.js";
11
+ import { authorUrlResolver, messagesUrlResolver } from "@/core/urlResolver.js";
12
12
  /**
13
13
  * Creates a transcript of a Discord channel's messages.
14
14
  * Depending on the `returnType` option, this function can return an `AttachmentBuilder`,
@@ -1,6 +1,6 @@
1
1
  import { Guild, TextBasedChannel } from "discord.js";
2
2
  import { ArrayMentions, JsonAuthor, JsonMessage, TranscriptOptionsBase, JsonData } from "discord-message-transcript-base";
3
- import { CDNOptions } from "../../types/types.js";
3
+ import { CDNOptions } from "@/types";
4
4
  export declare class Json {
5
5
  private guild;
6
6
  private channel;
@@ -1,6 +1,6 @@
1
1
  import { BaseGuildTextChannel, DMChannel } from "discord.js";
2
- import { urlResolver } from "../../core/urlResolver.js";
3
- import { resolveImageURL } from "../../../../discord-message-transcript-base/src/core/sanitizer.js";
2
+ import { urlResolver } from "@/core/urlResolver.js";
3
+ import { resolveImageURL } from "@/core/resolveImageUrl.js";
4
4
  export class Json {
5
5
  guild;
6
6
  channel;
@@ -286,3 +286,15 @@ export type CDNOptionsUploadcare = {
286
286
  */
287
287
  cdnDomain: string;
288
288
  };
289
+ /**
290
+ * Result from dns.lookup
291
+ */
292
+ export type LookupResult = {
293
+ address: string;
294
+ family: 4 | 6;
295
+ };
296
+ export interface safeUrlReturn {
297
+ safe: boolean;
298
+ safeIps: string[];
299
+ url: string;
300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discord-message-transcript",
3
- "version": "1.3.1-dev.3.35",
3
+ "version": "1.3.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -44,11 +44,8 @@
44
44
  "url": "https://github.com/HenriqueMairesse/discord-message-transcript/issues"
45
45
  },
46
46
  "homepage": "https://github.com/HenriqueMairesse/discord-message-transcript#readme",
47
- "devDependencies": {
48
- "typescript": "^5.9.3"
49
- },
50
47
  "dependencies": {
51
- "discord-message-transcript-base": "1.3.1-dev.3.35"
48
+ "discord-message-transcript-base": "1.3.1"
52
49
  },
53
50
  "peerDependencies": {
54
51
  "discord.js": ">=14.19.0 <15"
@@ -57,7 +54,7 @@
57
54
  "access": "public"
58
55
  },
59
56
  "scripts": {
60
- "test": "echo \"Error: no test specified\" && exit 1",
61
- "build": "tsc"
57
+ "clean": "pnpm exec rimraf dist",
58
+ "build": "pnpm run clean && tsc"
62
59
  }
63
60
  }