eniac-slack 0.1.14 → 0.1.16

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
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.16] - 2026-03-12
4
+
5
+ - fix(slack): Slack 파일 다운로드 인증 실패 - redirect 수동 처리 (#25)
6
+
7
+
8
+ ## [0.1.15] - 2026-03-12
9
+
10
+ - fix(slack): 이미지 로깅 강화 + magic bytes 불일치 허용 (#24)
11
+
12
+
3
13
  ## [0.1.14] - 2026-03-12
4
14
 
5
15
  - fix(slack): sharp 정규화 + AsyncIterable로 이미지 직접 전달 (#23)
@@ -5,12 +5,14 @@ export interface SlackImageFile {
5
5
  }
6
6
  /**
7
7
  * Download image files from a Slack message's `files` array.
8
- *
9
- * Only images with supported media types are returned; non-image files are skipped.
10
8
  */
11
9
  export declare function downloadSlackImages(client: WebClient, files: Array<{
12
10
  url_private_download?: string;
13
11
  url_private?: string;
14
12
  mimetype?: string;
13
+ name?: string;
14
+ filetype?: string;
15
+ size?: number;
16
+ id?: string;
15
17
  }>): Promise<SlackImageFile[]>;
16
18
  //# sourceMappingURL=slack-files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack-files.d.ts","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;CACpE;AA8CD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,KAAK,CAAC;IAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACvF,OAAO,CAAC,cAAc,EAAE,CAAC,CAoD3B"}
1
+ {"version":3,"file":"slack-files.d.ts","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;CACpE;AAmED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,KAAK,CAAC;IACX,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC,GACD,OAAO,CAAC,cAAc,EAAE,CAAC,CAkF3B"}
@@ -5,94 +5,135 @@ const SUPPORTED_TYPES = new Set([
5
5
  "image/gif",
6
6
  "image/webp",
7
7
  ]);
8
+ const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
8
9
  /**
9
- * Validate that a buffer actually contains image data by checking magic bytes.
10
+ * Detect actual image format from magic bytes.
10
11
  */
11
- function isValidImage(buf, expectedMime) {
12
- if (buf.length < 4)
13
- return false;
14
- switch (expectedMime) {
15
- case "image/png":
16
- return buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47;
17
- case "image/jpeg":
18
- return buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff;
19
- case "image/gif":
20
- return buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46;
21
- case "image/webp":
22
- return buf.length >= 12 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
23
- && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50;
24
- default:
25
- return false;
26
- }
12
+ function detectImageFormat(buf) {
13
+ if (buf.length < 12)
14
+ return null;
15
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
16
+ return "image/png";
17
+ if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
18
+ return "image/jpeg";
19
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46)
20
+ return "image/gif";
21
+ if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
22
+ && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50)
23
+ return "image/webp";
24
+ return null;
27
25
  }
28
- const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
29
26
  /**
30
- * Normalize an image buffer: resize to max dimensions and re-encode as PNG.
31
- * This fixes issues with exotic color spaces, corrupted chunks, and oversized images.
27
+ * Download a file from Slack, handling cross-origin redirects that strip the Authorization header.
28
+ * Slack's file URLs often redirect to a CDN, and fetch() strips auth headers on cross-origin redirects.
32
29
  */
33
- async function normalizeImage(buf) {
34
- const normalized = await sharp(buf)
35
- .resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, {
36
- fit: "inside",
37
- withoutEnlargement: true,
38
- })
39
- .png()
40
- .toBuffer();
41
- return { data: normalized, mediaType: "image/png" };
30
+ async function fetchSlackFile(url, token) {
31
+ // First try: manual redirect to preserve auth through redirect chain
32
+ const resp = await fetch(url, {
33
+ headers: { Authorization: `Bearer ${token}` },
34
+ redirect: "manual",
35
+ });
36
+ console.log(`[slack-files] initial response: status=${resp.status}, content-type=${resp.headers.get("content-type")}, location=${resp.headers.get("location")?.substring(0, 80) ?? "none"}`);
37
+ // If redirect, follow it manually with auth header
38
+ if (resp.status >= 300 && resp.status < 400) {
39
+ const location = resp.headers.get("location");
40
+ if (location) {
41
+ console.log(`[slack-files] following redirect to: ${location.substring(0, 80)}...`);
42
+ const resp2 = await fetch(location, {
43
+ headers: { Authorization: `Bearer ${token}` },
44
+ });
45
+ console.log(`[slack-files] redirect response: status=${resp2.status}, content-type=${resp2.headers.get("content-type")}`);
46
+ const buf = Buffer.from(await resp2.arrayBuffer());
47
+ return { ok: resp2.ok, status: resp2.status, contentType: resp2.headers.get("content-type"), buffer: buf };
48
+ }
49
+ }
50
+ // No redirect or non-redirect response
51
+ if (resp.ok || resp.status === 200) {
52
+ const buf = Buffer.from(await resp.arrayBuffer());
53
+ return { ok: true, status: resp.status, contentType: resp.headers.get("content-type"), buffer: buf };
54
+ }
55
+ // If first approach failed (got HTML), try with token as query parameter
56
+ console.log(`[slack-files] Bearer auth failed (status=${resp.status}), trying token as query param`);
57
+ const urlWithToken = new URL(url);
58
+ urlWithToken.searchParams.set("t", token);
59
+ const resp3 = await fetch(urlWithToken.toString());
60
+ console.log(`[slack-files] query param response: status=${resp3.status}, content-type=${resp3.headers.get("content-type")}`);
61
+ const buf = Buffer.from(await resp3.arrayBuffer());
62
+ return { ok: resp3.ok, status: resp3.status, contentType: resp3.headers.get("content-type"), buffer: buf };
42
63
  }
43
64
  /**
44
65
  * Download image files from a Slack message's `files` array.
45
- *
46
- * Only images with supported media types are returned; non-image files are skipped.
47
66
  */
48
67
  export async function downloadSlackImages(client, files) {
49
68
  const token = client.token;
50
- if (!token)
69
+ if (!token) {
70
+ console.warn("[slack-files] no token found on WebClient");
51
71
  return [];
72
+ }
73
+ console.log(`[slack-files] token type: ${token.substring(0, 5)}..., length=${token.length}`);
52
74
  const results = [];
53
75
  for (const file of files) {
54
76
  const mime = file.mimetype;
55
- if (!mime || !SUPPORTED_TYPES.has(mime))
77
+ console.log(`[slack-files] processing file: id=${file.id}, name=${file.name}, filetype=${file.filetype}, mimetype=${mime}, size=${file.size}`);
78
+ if (!mime || !SUPPORTED_TYPES.has(mime)) {
79
+ console.log(`[slack-files] skipping unsupported mime: ${mime}`);
56
80
  continue;
81
+ }
57
82
  const url = file.url_private_download ?? file.url_private;
58
- if (!url)
83
+ if (!url) {
84
+ console.warn(`[slack-files] no download URL for file ${file.id}`);
59
85
  continue;
86
+ }
87
+ console.log(`[slack-files] download URL: ${url}`);
60
88
  try {
61
- const resp = await fetch(url, {
62
- headers: { Authorization: `Bearer ${token}` },
63
- });
64
- if (!resp.ok) {
65
- console.warn(`[slack-files] failed to download ${url}: ${resp.status}`);
89
+ const { ok, status, contentType, buffer: buf } = await fetchSlackFile(url, token);
90
+ if (!ok) {
91
+ console.warn(`[slack-files] download failed: status=${status}`);
66
92
  continue;
67
93
  }
68
- const buf = Buffer.from(await resp.arrayBuffer());
69
- if (!isValidImage(buf, mime)) {
70
- console.warn(`[slack-files] downloaded data is not a valid ${mime} image (${buf.length} bytes), skipping`);
94
+ const hexDump = buf.subarray(0, 16).toString("hex").match(/../g)?.join(" ") ?? "";
95
+ const detectedFormat = detectImageFormat(buf);
96
+ console.log(`[slack-files] downloaded ${buf.length} bytes, content-type=${contentType}`);
97
+ console.log(`[slack-files] first 16 bytes: ${hexDump}`);
98
+ console.log(`[slack-files] declared mime=${mime}, detected format=${detectedFormat}`);
99
+ // If the data looks like HTML (Slack error/login page), log and skip
100
+ const head = buf.subarray(0, 50).toString("utf8");
101
+ if (head.includes("<!DOCTYPE") || head.includes("<html")) {
102
+ const preview = buf.subarray(0, 300).toString("utf8");
103
+ console.warn(`[slack-files] downloaded data is HTML, not an image! Preview: ${preview}`);
71
104
  continue;
72
105
  }
73
- // Normalize: resize + re-encode to fix exotic formats and oversized images
106
+ // Normalize with sharp (handles format detection internally)
74
107
  try {
75
- const { data, mediaType } = await normalizeImage(buf);
76
- const b64 = data.toString("base64");
77
- console.log(`[slack-files] normalized image: ${buf.length} → ${data.length} bytes, base64=${b64.length} chars, mime=${mediaType}`);
78
- console.log(`[slack-files] PNG header check: ${data[0]?.toString(16)}-${data[1]?.toString(16)}-${data[2]?.toString(16)}-${data[3]?.toString(16)}`);
79
- results.push({
80
- base64: b64,
81
- mediaType,
82
- });
108
+ const metadata = await sharp(buf).metadata();
109
+ console.log(`[slack-files] sharp metadata: format=${metadata.format}, ${metadata.width}x${metadata.height}, channels=${metadata.channels}, space=${metadata.space}`);
110
+ const normalized = await sharp(buf)
111
+ .resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, {
112
+ fit: "inside",
113
+ withoutEnlargement: true,
114
+ })
115
+ .png()
116
+ .toBuffer();
117
+ const b64 = normalized.toString("base64");
118
+ console.log(`[slack-files] normalized: ${buf.length} → ${normalized.length} bytes, base64=${b64.length} chars`);
119
+ results.push({ base64: b64, mediaType: "image/png" });
83
120
  }
84
- catch (normErr) {
85
- console.warn(`[slack-files] failed to normalize image, using raw:`, normErr);
86
- results.push({
87
- base64: buf.toString("base64"),
88
- mediaType: mime,
89
- });
121
+ catch (sharpErr) {
122
+ console.error(`[slack-files] sharp failed:`, sharpErr);
123
+ if (detectedFormat && SUPPORTED_TYPES.has(detectedFormat)) {
124
+ console.log(`[slack-files] fallback: raw buffer with ${detectedFormat}`);
125
+ results.push({
126
+ base64: buf.toString("base64"),
127
+ mediaType: detectedFormat,
128
+ });
129
+ }
90
130
  }
91
131
  }
92
132
  catch (err) {
93
- console.warn(`[slack-files] error downloading file:`, err);
133
+ console.error(`[slack-files] error downloading file ${file.id}:`, err);
94
134
  }
95
135
  }
136
+ console.log(`[slack-files] total images processed: ${results.length}`);
96
137
  return results;
97
138
  }
98
139
  //# sourceMappingURL=slack-files.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack-files.js","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC9B,YAAY;IACZ,WAAW;IACX,WAAW;IACX,YAAY;CACb,CAAC,CAAC;AAEH;;GAEG;AACH,SAAS,YAAY,CAAC,GAAW,EAAE,YAAoB;IACrD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACjC,QAAQ,YAAY,EAAE,CAAC;QACrB,KAAK,WAAW;YACd,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;QAClF,KAAK,YAAY;YACf,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;QAC/D,KAAK,WAAW;YACd,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;QAC/D,KAAK,YAAY;YACf,OAAO,GAAG,CAAC,MAAM,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;mBAC9F,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;QAClF;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,mBAAmB,GAAG,IAAI,CAAC,CAAC,6BAA6B;AAE/D;;;GAGG;AACH,KAAK,UAAU,cAAc,CAAC,GAAW;IACvC,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC;SAChC,MAAM,CAAC,mBAAmB,EAAE,mBAAmB,EAAE;QAChD,GAAG,EAAE,QAAQ;QACb,kBAAkB,EAAE,IAAI;KACzB,CAAC;SACD,GAAG,EAAE;SACL,QAAQ,EAAE,CAAC;IACd,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAiB,EACjB,KAAwF;IAExF,MAAM,KAAK,GAAI,MAAwC,CAAC,KAAK,CAAC;IAC9D,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IAEtB,MAAM,OAAO,GAAqB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC3B,IAAI,CAAC,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAElD,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,WAAW,CAAC;QAC1D,IAAI,CAAC,GAAG;YAAE,SAAS;QAEnB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC5B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;aAC9C,CAAC,CAAC;YAEH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC,oCAAoC,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;gBACxE,SAAS;YACX,CAAC;YAED,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YAClD,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC;gBAC7B,OAAO,CAAC,IAAI,CAAC,gDAAgD,IAAI,WAAW,GAAG,CAAC,MAAM,mBAAmB,CAAC,CAAC;gBAC3G,SAAS;YACX,CAAC;YAED,2EAA2E;YAC3E,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;gBACtD,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACpC,OAAO,CAAC,GAAG,CAAC,mCAAmC,GAAG,CAAC,MAAM,MAAM,IAAI,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAAM,gBAAgB,SAAS,EAAE,CAAC,CAAC;gBACnI,OAAO,CAAC,GAAG,CAAC,mCAAmC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBACnJ,OAAO,CAAC,IAAI,CAAC;oBACX,MAAM,EAAE,GAAG;oBACX,SAAS;iBACV,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,OAAO,EAAE,CAAC;gBACjB,OAAO,CAAC,IAAI,CAAC,qDAAqD,EAAE,OAAO,CAAC,CAAC;gBAC7E,OAAO,CAAC,IAAI,CAAC;oBACX,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBAC9B,SAAS,EAAE,IAAmC;iBAC/C,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,uCAAuC,EAAE,GAAG,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"slack-files.js","sourceRoot":"","sources":["../../src/utils/slack-files.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eniac-slack",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "scripts": {
@@ -13,101 +13,158 @@ const SUPPORTED_TYPES = new Set([
13
13
  "image/webp",
14
14
  ]);
15
15
 
16
+ const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
17
+
16
18
  /**
17
- * Validate that a buffer actually contains image data by checking magic bytes.
19
+ * Detect actual image format from magic bytes.
18
20
  */
19
- function isValidImage(buf: Buffer, expectedMime: string): boolean {
20
- if (buf.length < 4) return false;
21
- switch (expectedMime) {
22
- case "image/png":
23
- return buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47;
24
- case "image/jpeg":
25
- return buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff;
26
- case "image/gif":
27
- return buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46;
28
- case "image/webp":
29
- return buf.length >= 12 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
30
- && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50;
31
- default:
32
- return false;
33
- }
21
+ function detectImageFormat(buf: Buffer): string | null {
22
+ if (buf.length < 12) return null;
23
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return "image/png";
24
+ if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return "image/jpeg";
25
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return "image/gif";
26
+ if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
27
+ && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) return "image/webp";
28
+ return null;
34
29
  }
35
30
 
36
- const MAX_IMAGE_DIMENSION = 1568; // Claude API recommended max
37
-
38
31
  /**
39
- * Normalize an image buffer: resize to max dimensions and re-encode as PNG.
40
- * This fixes issues with exotic color spaces, corrupted chunks, and oversized images.
32
+ * Download a file from Slack, handling cross-origin redirects that strip the Authorization header.
33
+ * Slack's file URLs often redirect to a CDN, and fetch() strips auth headers on cross-origin redirects.
41
34
  */
42
- async function normalizeImage(buf: Buffer): Promise<{ data: Buffer; mediaType: SlackImageFile["mediaType"] }> {
43
- const normalized = await sharp(buf)
44
- .resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, {
45
- fit: "inside",
46
- withoutEnlargement: true,
47
- })
48
- .png()
49
- .toBuffer();
50
- return { data: normalized, mediaType: "image/png" };
35
+ async function fetchSlackFile(url: string, token: string): Promise<{ ok: boolean; status: number; contentType: string | null; buffer: Buffer }> {
36
+ // First try: manual redirect to preserve auth through redirect chain
37
+ const resp = await fetch(url, {
38
+ headers: { Authorization: `Bearer ${token}` },
39
+ redirect: "manual",
40
+ });
41
+
42
+ console.log(`[slack-files] initial response: status=${resp.status}, content-type=${resp.headers.get("content-type")}, location=${resp.headers.get("location")?.substring(0, 80) ?? "none"}`);
43
+
44
+ // If redirect, follow it manually with auth header
45
+ if (resp.status >= 300 && resp.status < 400) {
46
+ const location = resp.headers.get("location");
47
+ if (location) {
48
+ console.log(`[slack-files] following redirect to: ${location.substring(0, 80)}...`);
49
+ const resp2 = await fetch(location, {
50
+ headers: { Authorization: `Bearer ${token}` },
51
+ });
52
+ console.log(`[slack-files] redirect response: status=${resp2.status}, content-type=${resp2.headers.get("content-type")}`);
53
+ const buf = Buffer.from(await resp2.arrayBuffer());
54
+ return { ok: resp2.ok, status: resp2.status, contentType: resp2.headers.get("content-type"), buffer: buf };
55
+ }
56
+ }
57
+
58
+ // No redirect or non-redirect response
59
+ if (resp.ok || resp.status === 200) {
60
+ const buf = Buffer.from(await resp.arrayBuffer());
61
+ return { ok: true, status: resp.status, contentType: resp.headers.get("content-type"), buffer: buf };
62
+ }
63
+
64
+ // If first approach failed (got HTML), try with token as query parameter
65
+ console.log(`[slack-files] Bearer auth failed (status=${resp.status}), trying token as query param`);
66
+ const urlWithToken = new URL(url);
67
+ urlWithToken.searchParams.set("t", token);
68
+ const resp3 = await fetch(urlWithToken.toString());
69
+ console.log(`[slack-files] query param response: status=${resp3.status}, content-type=${resp3.headers.get("content-type")}`);
70
+ const buf = Buffer.from(await resp3.arrayBuffer());
71
+ return { ok: resp3.ok, status: resp3.status, contentType: resp3.headers.get("content-type"), buffer: buf };
51
72
  }
52
73
 
53
74
  /**
54
75
  * Download image files from a Slack message's `files` array.
55
- *
56
- * Only images with supported media types are returned; non-image files are skipped.
57
76
  */
58
77
  export async function downloadSlackImages(
59
78
  client: WebClient,
60
- files: Array<{ url_private_download?: string; url_private?: string; mimetype?: string }>
79
+ files: Array<{
80
+ url_private_download?: string;
81
+ url_private?: string;
82
+ mimetype?: string;
83
+ name?: string;
84
+ filetype?: string;
85
+ size?: number;
86
+ id?: string;
87
+ }>
61
88
  ): Promise<SlackImageFile[]> {
62
89
  const token = (client as unknown as { token?: string }).token;
63
- if (!token) return [];
90
+ if (!token) {
91
+ console.warn("[slack-files] no token found on WebClient");
92
+ return [];
93
+ }
94
+ console.log(`[slack-files] token type: ${token.substring(0, 5)}..., length=${token.length}`);
64
95
 
65
96
  const results: SlackImageFile[] = [];
66
97
 
67
98
  for (const file of files) {
68
99
  const mime = file.mimetype;
69
- if (!mime || !SUPPORTED_TYPES.has(mime)) continue;
100
+ console.log(`[slack-files] processing file: id=${file.id}, name=${file.name}, filetype=${file.filetype}, mimetype=${mime}, size=${file.size}`);
101
+
102
+ if (!mime || !SUPPORTED_TYPES.has(mime)) {
103
+ console.log(`[slack-files] skipping unsupported mime: ${mime}`);
104
+ continue;
105
+ }
70
106
 
71
107
  const url = file.url_private_download ?? file.url_private;
72
- if (!url) continue;
108
+ if (!url) {
109
+ console.warn(`[slack-files] no download URL for file ${file.id}`);
110
+ continue;
111
+ }
112
+ console.log(`[slack-files] download URL: ${url}`);
73
113
 
74
114
  try {
75
- const resp = await fetch(url, {
76
- headers: { Authorization: `Bearer ${token}` },
77
- });
115
+ const { ok, status, contentType, buffer: buf } = await fetchSlackFile(url, token);
78
116
 
79
- if (!resp.ok) {
80
- console.warn(`[slack-files] failed to download ${url}: ${resp.status}`);
117
+ if (!ok) {
118
+ console.warn(`[slack-files] download failed: status=${status}`);
81
119
  continue;
82
120
  }
83
121
 
84
- const buf = Buffer.from(await resp.arrayBuffer());
85
- if (!isValidImage(buf, mime)) {
86
- console.warn(`[slack-files] downloaded data is not a valid ${mime} image (${buf.length} bytes), skipping`);
122
+ const hexDump = buf.subarray(0, 16).toString("hex").match(/../g)?.join(" ") ?? "";
123
+ const detectedFormat = detectImageFormat(buf);
124
+ console.log(`[slack-files] downloaded ${buf.length} bytes, content-type=${contentType}`);
125
+ console.log(`[slack-files] first 16 bytes: ${hexDump}`);
126
+ console.log(`[slack-files] declared mime=${mime}, detected format=${detectedFormat}`);
127
+
128
+ // If the data looks like HTML (Slack error/login page), log and skip
129
+ const head = buf.subarray(0, 50).toString("utf8");
130
+ if (head.includes("<!DOCTYPE") || head.includes("<html")) {
131
+ const preview = buf.subarray(0, 300).toString("utf8");
132
+ console.warn(`[slack-files] downloaded data is HTML, not an image! Preview: ${preview}`);
87
133
  continue;
88
134
  }
89
135
 
90
- // Normalize: resize + re-encode to fix exotic formats and oversized images
136
+ // Normalize with sharp (handles format detection internally)
91
137
  try {
92
- const { data, mediaType } = await normalizeImage(buf);
93
- const b64 = data.toString("base64");
94
- console.log(`[slack-files] normalized image: ${buf.length} → ${data.length} bytes, base64=${b64.length} chars, mime=${mediaType}`);
95
- console.log(`[slack-files] PNG header check: ${data[0]?.toString(16)}-${data[1]?.toString(16)}-${data[2]?.toString(16)}-${data[3]?.toString(16)}`);
96
- results.push({
97
- base64: b64,
98
- mediaType,
99
- });
100
- } catch (normErr) {
101
- console.warn(`[slack-files] failed to normalize image, using raw:`, normErr);
102
- results.push({
103
- base64: buf.toString("base64"),
104
- mediaType: mime as SlackImageFile["mediaType"],
105
- });
138
+ const metadata = await sharp(buf).metadata();
139
+ console.log(`[slack-files] sharp metadata: format=${metadata.format}, ${metadata.width}x${metadata.height}, channels=${metadata.channels}, space=${metadata.space}`);
140
+
141
+ const normalized = await sharp(buf)
142
+ .resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, {
143
+ fit: "inside",
144
+ withoutEnlargement: true,
145
+ })
146
+ .png()
147
+ .toBuffer();
148
+
149
+ const b64 = normalized.toString("base64");
150
+ console.log(`[slack-files] normalized: ${buf.length} ${normalized.length} bytes, base64=${b64.length} chars`);
151
+
152
+ results.push({ base64: b64, mediaType: "image/png" });
153
+ } catch (sharpErr) {
154
+ console.error(`[slack-files] sharp failed:`, sharpErr);
155
+ if (detectedFormat && SUPPORTED_TYPES.has(detectedFormat)) {
156
+ console.log(`[slack-files] fallback: raw buffer with ${detectedFormat}`);
157
+ results.push({
158
+ base64: buf.toString("base64"),
159
+ mediaType: detectedFormat as SlackImageFile["mediaType"],
160
+ });
161
+ }
106
162
  }
107
163
  } catch (err) {
108
- console.warn(`[slack-files] error downloading file:`, err);
164
+ console.error(`[slack-files] error downloading file ${file.id}:`, err);
109
165
  }
110
166
  }
111
167
 
168
+ console.log(`[slack-files] total images processed: ${results.length}`);
112
169
  return results;
113
170
  }