@wu529778790/open-im 1.6.1-beta.2 → 1.6.1-beta.3

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.
@@ -1,5 +1,8 @@
1
1
  export declare function inferExtensionFromContentType(contentType: string): string;
2
+ export declare function inferExtensionFromBuffer(buffer: Buffer): string;
2
3
  export declare function createMediaTargetPath(extension: string, basenameHint?: string): string;
4
+ export declare function saveBufferMedia(buffer: Buffer, extension: string, basenameHint?: string): Promise<string>;
5
+ export declare function decryptAes256CbcMedia(buffer: Buffer, aesKey: string): Buffer;
3
6
  export declare function saveBase64Media(base64: string, extension: string, basenameHint?: string): Promise<string>;
4
7
  export declare function downloadMediaFromUrl(url: string, options?: {
5
8
  basenameHint?: string;
@@ -1,3 +1,4 @@
1
+ import { createDecipheriv } from "node:crypto";
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
3
  import { extname, join } from "node:path";
3
4
  import { IMAGE_DIR } from "../constants.js";
@@ -22,6 +23,30 @@ export function inferExtensionFromContentType(contentType) {
22
23
  return ".json";
23
24
  return "";
24
25
  }
26
+ export function inferExtensionFromBuffer(buffer) {
27
+ if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
28
+ return ".jpg";
29
+ if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
30
+ return ".png";
31
+ if (buffer.length >= 6) {
32
+ const gifHeader = buffer.subarray(0, 6).toString("ascii");
33
+ if (gifHeader === "GIF87a" || gifHeader === "GIF89a")
34
+ return ".gif";
35
+ }
36
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP")
37
+ return ".webp";
38
+ if (buffer.length >= 2 && buffer.subarray(0, 2).toString("ascii") === "BM")
39
+ return ".bmp";
40
+ if (buffer.length >= 4 && (buffer.subarray(0, 4).toString("ascii") === "II*\0" || buffer.subarray(0, 4).toString("ascii") === "MM\0*"))
41
+ return ".tif";
42
+ if (buffer.length >= 4 && buffer.subarray(0, 4).toString("ascii") === "%PDF")
43
+ return ".pdf";
44
+ if (buffer.length >= 4 && buffer.subarray(0, 4).toString("ascii") === "OggS")
45
+ return ".ogg";
46
+ if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp")
47
+ return ".mp4";
48
+ return "";
49
+ }
25
50
  export function createMediaTargetPath(extension, basenameHint) {
26
51
  const safeExtension = extension.startsWith(".") ? extension : `.${extension}`;
27
52
  const safeBasename = basenameHint ? sanitizeName(basenameHint) : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -29,12 +54,32 @@ export function createMediaTargetPath(extension, basenameHint) {
29
54
  const filename = existingExtension ? safeBasename : `${safeBasename}${safeExtension}`;
30
55
  return join(IMAGE_DIR, filename);
31
56
  }
32
- export async function saveBase64Media(base64, extension, basenameHint) {
57
+ export async function saveBufferMedia(buffer, extension, basenameHint) {
33
58
  await mkdir(IMAGE_DIR, { recursive: true });
34
59
  const path = createMediaTargetPath(extension, basenameHint);
35
- await writeFile(path, Buffer.from(base64, "base64"));
60
+ await writeFile(path, buffer);
36
61
  return path;
37
62
  }
63
+ function decodeAesKey(aesKey) {
64
+ const normalized = aesKey.trim().replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
65
+ const withPadding = normalized.length % 4 === 0 ? normalized : normalized.padEnd(normalized.length + (4 - (normalized.length % 4)), "=");
66
+ const decoded = Buffer.from(withPadding, "base64");
67
+ if (decoded.length === 32)
68
+ return decoded;
69
+ const utf8 = Buffer.from(aesKey, "utf8");
70
+ if (utf8.length === 32)
71
+ return utf8;
72
+ throw new Error(`Invalid AES key length: expected 32 bytes, got ${decoded.length || utf8.length}`);
73
+ }
74
+ export function decryptAes256CbcMedia(buffer, aesKey) {
75
+ const key = decodeAesKey(aesKey);
76
+ const iv = key.subarray(0, 16);
77
+ const decipher = createDecipheriv("aes-256-cbc", key, iv);
78
+ return Buffer.concat([decipher.update(buffer), decipher.final()]);
79
+ }
80
+ export async function saveBase64Media(base64, extension, basenameHint) {
81
+ return saveBufferMedia(Buffer.from(base64, "base64"), extension, basenameHint);
82
+ }
38
83
  export async function downloadMediaFromUrl(url, options) {
39
84
  await mkdir(IMAGE_DIR, { recursive: true });
40
85
  const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
@@ -1,5 +1,6 @@
1
+ import { createCipheriv, randomBytes } from "node:crypto";
1
2
  import { describe, expect, it } from "vitest";
2
- import { createMediaTargetPath, inferExtensionFromContentType } from "./media-storage.js";
3
+ import { createMediaTargetPath, decryptAes256CbcMedia, inferExtensionFromBuffer, inferExtensionFromContentType, } from "./media-storage.js";
3
4
  describe("createMediaTargetPath", () => {
4
5
  it("does not append a fallback extension when basename already has one", () => {
5
6
  const path = createMediaTargetPath("bin", "report.pdf");
@@ -19,3 +20,20 @@ describe("inferExtensionFromContentType", () => {
19
20
  expect(inferExtensionFromContentType("application/pdf")).toBe(".pdf");
20
21
  });
21
22
  });
23
+ describe("inferExtensionFromBuffer", () => {
24
+ it("detects jpeg files from the magic header", () => {
25
+ const buffer = Buffer.from([0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43]);
26
+ expect(inferExtensionFromBuffer(buffer)).toBe(".jpg");
27
+ });
28
+ });
29
+ describe("decryptAes256CbcMedia", () => {
30
+ it("decrypts WeWork-style AES-256-CBC media buffers", () => {
31
+ const key = randomBytes(32);
32
+ const iv = key.subarray(0, 16);
33
+ const plaintext = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46]);
34
+ const cipher = createCipheriv("aes-256-cbc", key, iv);
35
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
36
+ const aesKey = key.toString("base64").replace(/=+$/g, "");
37
+ expect(decryptAes256CbcMedia(encrypted, aesKey)).toEqual(plaintext);
38
+ });
39
+ });
@@ -15,7 +15,7 @@ import { setChatUser } from '../shared/chat-user-map.js';
15
15
  import { createLogger } from '../logger.js';
16
16
  import { buildUnsupportedInboundMessage } from '../channels/capabilities.js';
17
17
  import { buildMediaMetadataPrompt } from '../shared/media-prompt.js';
18
- import { downloadMediaFromUrl, saveBase64Media } from '../shared/media-storage.js';
18
+ import { decryptAes256CbcMedia, downloadMediaFromUrl, inferExtensionFromBuffer, inferExtensionFromContentType, saveBase64Media, saveBufferMedia, } from '../shared/media-storage.js';
19
19
  import { buildSavedMediaPrompt } from '../shared/media-analysis-prompt.js';
20
20
  import { buildMediaContext } from '../shared/media-context.js';
21
21
  const log = createLogger('WeWorkHandler');
@@ -87,10 +87,26 @@ async function buildMediaPrompt(data, kind) {
87
87
  }
88
88
  else if (typeof imagePayload.url === 'string' && imagePayload.url.length > 0) {
89
89
  try {
90
- const savedPath = await downloadMediaFromUrl(imagePayload.url, {
91
- basenameHint: imagePayload.md5,
92
- fallbackExtension: 'jpg',
93
- });
90
+ let savedPath = '';
91
+ if (typeof imagePayload.aeskey === 'string' && imagePayload.aeskey.trim().length > 0) {
92
+ const response = await fetch(imagePayload.url, { signal: AbortSignal.timeout(30000) });
93
+ if (!response.ok) {
94
+ throw new Error(`Failed to download media: HTTP ${response.status}`);
95
+ }
96
+ const encryptedBuffer = Buffer.from(await response.arrayBuffer());
97
+ const decryptedBuffer = decryptAes256CbcMedia(encryptedBuffer, imagePayload.aeskey);
98
+ const extension = inferExtensionFromBuffer(decryptedBuffer) ||
99
+ inferExtensionFromContentType(response.headers.get('content-type') ?? '') ||
100
+ '.jpg';
101
+ savedPath = await saveBufferMedia(decryptedBuffer, extension, imagePayload.md5);
102
+ log.info(`Downloaded and decrypted WeWork image: ${savedPath}`);
103
+ }
104
+ else {
105
+ savedPath = await downloadMediaFromUrl(imagePayload.url, {
106
+ basenameHint: imagePayload.md5,
107
+ fallbackExtension: 'jpg',
108
+ });
109
+ }
94
110
  return buildSavedMediaPrompt({
95
111
  source: 'WeWork',
96
112
  kind: 'image',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.6.1-beta.2",
3
+ "version": "1.6.1-beta.3",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",