@open-wa/decrypt 5.0.0-alpha.8 → 5.0.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/LICENSE.md +2 -2
- package/README.md +1 -1
- package/dist/index.cjs +3 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -6
package/LICENSE.md
CHANGED
|
@@ -55,9 +55,9 @@ Licensor hereby grants permission by this license ("License"), free of charge,
|
|
|
55
55
|
4. Compliance with Human Rights Laws and Human Rights Principles:
|
|
56
56
|
|
|
57
57
|
a. **Human Rights Laws**. The Software shall not be used by any person or entity for any systems, activities, or other uses that violate any applicable laws, regulations, or rules that protect human, civil, labor, privacy, political, environmental, security, economic, due process, or similar rights (the "Human Rights Laws"). Where the Human Rights Laws of more than one jurisdiction are applicable to the use of the Software, the Human Rights Laws that are most protective of the individuals or groups harmed shall apply.
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
b. **Human Rights Principles**. Licensee is advised to consult the articles of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/) and the United Nations Global Compact (https://www.unglobalcompact.org/ what-is-gc/mission/principles) that define recognized principles of international human rights (the "Human Rights Principles"). It is Licensor's express intent that all use of the Software be consistent with Human Rights Principles. If Licensor receives notification or otherwise learns of an alleged violation of any Human Rights Principles relating to Licensee's use of the Software, Licensor may in its discretion and without obligation (i) (a) notify Licensee of such allegation and (b) allow Licensee 90 days from notification under (i)(a) to investigate and respond to Licensor regarding the allegation and (ii) (a) after the earlier of 90 days from notification under (i)(a), or Licensee's response under (i)(b), notify Licensee of License termination and (b) allow Licensee an additional 90 days from notification under (ii)(a) to cease use of the Software.
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
c. **Indemnity**. Licensee shall hold harmless and indemnify Licensor against all losses, damages, liabilities, deficiencies, claims, actions, judgments, settlements, interest, awards, penalties, fines, costs, or expenses of whatever kind, including Licensor's reasonable attorneys' fees, arising out of or relating to Licensee's non-compliance with this License or use of the Software in violation of Human Rights Laws or Human Rights Principles.
|
|
62
62
|
|
|
63
63
|
5. Enforceability: If any portion or provision of this License is determined to be invalid, illegal, or unenforceable by a court of competent jurisdiction, then such invalidity, illegality, or unenforceability shall not affect any other term or provision of this License or invalidate or render unenforceable such term or provision in any other jurisdiction. Upon a determination that any term or provision is invalid, illegal, or unenforceable, to the extent permitted by applicable law, the court may modify this License to affect the original intent of the parties as closely as possible. The section headings are for convenience only and are not intended to affect the construction or interpretation of this License. Any rule of construction to the effect that ambiguities are to be resolved against the drafting party shall not apply in interpreting this License. The language in this License shall be interpreted as to its fair meaning and not strictly for or against any party.
|
package/README.md
CHANGED
|
@@ -16,4 +16,4 @@ See the [docs site](https://openwa.dev).
|
|
|
16
16
|
|
|
17
17
|
## License
|
|
18
18
|
|
|
19
|
-
[H-DNH
|
|
19
|
+
[H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm
|
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,8 @@ function decryptBuffer(encryptedData, mediaKeyBase64, mediaType, expectedSize, m
|
|
|
36
36
|
const info = `WhatsApp ${mediaTypes[mediaType.toUpperCase()] ?? mediaTypes[Object.keys(mediaTypes).find((t) => mimetype?.includes(t.toLowerCase())) ?? ""]} Keys`;
|
|
37
37
|
const expanded = Buffer.from((0, node_crypto.hkdfSync)("sha256", mediaKeyBytes, Buffer.alloc(32), info, 112));
|
|
38
38
|
const iv = expanded.subarray(0, 16);
|
|
39
|
-
const
|
|
39
|
+
const cipherKey = expanded.subarray(16, 48);
|
|
40
|
+
const decipher = (0, node_crypto.createDecipheriv)("aes-256-cbc", cipherKey, iv);
|
|
40
41
|
decipher.setAutoPadding(false);
|
|
41
42
|
const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
|
42
43
|
return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;
|
|
@@ -136,7 +137,7 @@ function validateMessage(message) {
|
|
|
136
137
|
* Useful for minimizing sensitive data in transit — removes sender info,
|
|
137
138
|
* chat context, etc.
|
|
138
139
|
*/
|
|
139
|
-
const BLEACH_KEEP = new Set([
|
|
140
|
+
const BLEACH_KEEP = /* @__PURE__ */ new Set([
|
|
140
141
|
"type",
|
|
141
142
|
"clientUrl",
|
|
142
143
|
"mimetype",
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/constants.ts","../src/crypto.ts","../src/download.ts","../src/decrypt.ts","../src/bleach.ts"],"sourcesContent":["/** Mapping from message type to WhatsApp's internal media-type key name. */\nexport const mediaTypes: Record<string, string> = {\n IMAGE: 'Image',\n VIDEO: 'Video',\n AUDIO: 'Audio',\n PTT: 'Audio',\n DOCUMENT: 'Document',\n STICKER: 'Image',\n} as const;\n\n/** Message types where a missing `size` field is tolerable. */\nexport const NON_SIZE_TYPES = ['sticker'] as const;\n\n/** The default user-agent used for fetching encrypted media blobs. */\nexport const DEFAULT_USER_AGENT =\n 'WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36';\n\n/** Thrown when a message is missing fields required for decryption. */\nexport class MissingCriticalDataError extends Error {\n override readonly name = 'MissingCriticalDataError';\n\n constructor(message: string) {\n super(message);\n }\n}\n","/**\n * Core crypto routines for WhatsApp media decryption.\n *\n * Uses only `node:crypto` builtins — no external HKDF library needed.\n * `crypto.hkdfSync` landed in Node 15 and is available in Bun.\n */\nimport { createDecipheriv, hkdfSync } from 'node:crypto';\n\nimport { mediaTypes } from './constants.js';\n\n/**\n * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,\n * then decrypt the ciphertext.\n */\nexport function decryptBuffer(\n encryptedData: Uint8Array,\n mediaKeyBase64: string,\n mediaType: string,\n expectedSize?: number,\n mimetype?: string,\n): Buffer {\n const mediaKeyBytes = Buffer.from(mediaKeyBase64, 'base64');\n\n // Resolve the info string: \"WhatsApp <Type> Keys\"\n const typeKey =\n mediaTypes[mediaType.toUpperCase()] ??\n mediaTypes[\n Object.keys(mediaTypes).find((t) =>\n mimetype?.includes(t.toLowerCase()),\n ) ?? ''\n ];\n\n const info = `WhatsApp ${typeKey} Keys`;\n\n // HKDF expand to 112 bytes (iv=16 + cipherKey=32 + macKey=32 + refKey=32)\n const expanded = Buffer.from(\n hkdfSync('sha256', mediaKeyBytes, Buffer.alloc(32), info, 112),\n );\n\n const iv = expanded.subarray(0, 16);\n const cipherKey = expanded.subarray(16, 48);\n\n const decipher = createDecipheriv('aes-256-cbc', cipherKey, iv);\n // WhatsApp handles padding manually — disable OpenSSL auto-padding\n decipher.setAutoPadding(false);\n const decrypted = Buffer.concat([\n decipher.update(encryptedData),\n decipher.final(),\n ]);\n\n return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;\n}\n\n/**\n * Correct PKCS#7-style padding that WhatsApp applies inconsistently.\n */\nfunction fixPadding(data: Buffer, expectedSize: number): Buffer {\n const padding = (16 - (expectedSize % 16)) & 0xf;\n if (padding > 0) {\n if (expectedSize + padding === data.length) {\n // Trim trailing padding bytes\n return data.subarray(0, data.length - padding);\n }\n if (data.length + padding === expectedSize) {\n // Append missing padding\n const pad = Buffer.alloc(padding, padding);\n return Buffer.concat([data, pad]);\n }\n }\n return data;\n}\n","/**\n * Download encrypted media from WhatsApp CDN using native fetch.\n *\n * Replaces the legacy axios-based downloader.\n */\nimport {\n DEFAULT_USER_AGENT,\n} from './constants.js';\n\nfunction buildUserAgent(override?: string): string {\n let ua = override ?? DEFAULT_USER_AGENT;\n if (!ua.includes('WhatsApp')) ua = `WhatsApp/2.16.352 ${ua}`;\n return ua;\n}\n\n/**\n * Download an encrypted media blob from the given URL.\n * Uses the global `fetch` API (Bun native / Node 18+).\n */\nexport async function downloadEncryptedMedia(\n url: string,\n options?: { useragent?: string; maxRetries?: number; retryDelay?: number },\n): Promise<Uint8Array> {\n const { useragent, maxRetries = 3, retryDelay = 2_000 } = options ?? {};\n\n const headers: Record<string, string> = {\n 'User-Agent': buildUserAgent(useragent),\n DNT: '1',\n 'Upgrade-Insecure-Requests': '1',\n Origin: 'https://web.whatsapp.com/',\n Referer: 'https://web.whatsapp.com/',\n };\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const res = await fetch(url, { headers });\n\n if (res.ok) {\n return new Uint8Array(await res.arrayBuffer());\n }\n\n if (res.status === 404) {\n throw new Error(\n 'This media does not exist or is no longer available on the server. ' +\n 'See: https://openwa.dev/pages/How%20to/decrypt-media.html#40439d',\n );\n }\n\n // Retry on transient errors\n if (attempt < maxRetries - 1) {\n await new Promise((r) => setTimeout(r, retryDelay));\n }\n }\n\n throw new Error(`Failed to download media after ${maxRetries} attempts`);\n}\n","/**\n * Main decryptMedia entry point.\n *\n * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.\n */\nimport { MissingCriticalDataError, NON_SIZE_TYPES } from './constants.js';\nimport { decryptBuffer } from './crypto.js';\nimport { downloadEncryptedMedia } from './download.js';\nimport type { DecryptableMessage, DecryptOptions } from './types.js';\n\n/**\n * Download and decrypt a WhatsApp media message.\n *\n * @param message — A message object with at least the required decryption fields.\n * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.\n * @returns The decrypted media as a Buffer.\n *\n * @example\n * ```ts\n * import { decryptMedia } from '@open-wa/decrypt';\n *\n * const buffer = await decryptMedia(message);\n * ```\n */\nexport async function decryptMedia(\n message: DecryptableMessage,\n optionsOrUa?: DecryptOptions | string,\n): Promise<Buffer> {\n // Normalise options — accept legacy string signature for backward compat\n const opts: DecryptOptions =\n typeof optionsOrUa === 'string'\n ? { useragent: optionsOrUa }\n : optionsOrUa ?? {};\n\n validateMessage(message);\n\n const url = (message.deprecatedMms3Url ?? message.clientUrl)!.trim();\n const encrypted = await downloadEncryptedMedia(url, opts);\n\n return decryptBuffer(\n encrypted,\n message.mediaKey,\n message.type,\n message.size,\n message.mimetype,\n );\n}\n\nfunction validateMessage(message: DecryptableMessage): void {\n const missing: string[] = [];\n\n if (!message.mediaKey) missing.push('mediaKey');\n if (!message.filehash) missing.push('filehash');\n if (!message.mimetype) missing.push('mimetype');\n if (!message.type) missing.push('type');\n if (!message.size) missing.push('size');\n\n if (missing.length === 0) return;\n\n // Size-only omission is a warning for non-sticker types, not an error\n if (missing.length === 1 && missing[0] === 'size') {\n if (!NON_SIZE_TYPES.includes(message.type as (typeof NON_SIZE_TYPES)[number])) {\n console.warn(\n '@open-wa/decrypt: size property is missing. File will fail an integrity check.',\n );\n }\n return;\n }\n\n throw new MissingCriticalDataError(\n `Message is missing critical data: ${missing.join(', ')}`,\n );\n}\n","/**\n * Strip a message down to the minimum fields needed for decryption.\n *\n * Useful for minimizing sensitive data in transit — removes sender info,\n * chat context, etc.\n */\n\nconst BLEACH_KEEP = new Set([\n 'type',\n 'clientUrl',\n 'mimetype',\n 'mediaKey',\n 'size',\n 'filehash',\n 'uploadhash',\n 'deprecatedMms3Url',\n]);\n\n/**\n * Return a copy of the message containing only the fields required for decryption.\n */\nexport function bleachMessage<T extends Record<string, unknown>>(\n message: T,\n): Partial<T> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(message)) {\n if (BLEACH_KEEP.has(key)) {\n result[key] = message[key];\n }\n }\n return result as Partial<T>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["hkdfSync","createDecipheriv"],"sources":["../src/constants.ts","../src/crypto.ts","../src/download.ts","../src/decrypt.ts","../src/bleach.ts"],"sourcesContent":["/** Mapping from message type to WhatsApp's internal media-type key name. */\nexport const mediaTypes: Record<string, string> = {\n IMAGE: 'Image',\n VIDEO: 'Video',\n AUDIO: 'Audio',\n PTT: 'Audio',\n DOCUMENT: 'Document',\n STICKER: 'Image',\n} as const;\n\n/** Message types where a missing `size` field is tolerable. */\nexport const NON_SIZE_TYPES = ['sticker'] as const;\n\n/** The default user-agent used for fetching encrypted media blobs. */\nexport const DEFAULT_USER_AGENT =\n 'WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36';\n\n/** Thrown when a message is missing fields required for decryption. */\nexport class MissingCriticalDataError extends Error {\n override readonly name = 'MissingCriticalDataError';\n\n constructor(message: string) {\n super(message);\n }\n}\n","/**\n * Core crypto routines for WhatsApp media decryption.\n *\n * Uses only `node:crypto` builtins — no external HKDF library needed.\n * `crypto.hkdfSync` landed in Node 15 and is available in Bun.\n */\nimport { createDecipheriv, hkdfSync } from 'node:crypto';\n\nimport { mediaTypes } from './constants';\n\n/**\n * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,\n * then decrypt the ciphertext.\n */\nexport function decryptBuffer(\n encryptedData: Uint8Array,\n mediaKeyBase64: string,\n mediaType: string,\n expectedSize?: number,\n mimetype?: string,\n): Buffer {\n const mediaKeyBytes = Buffer.from(mediaKeyBase64, 'base64');\n\n // Resolve the info string: \"WhatsApp <Type> Keys\"\n const typeKey =\n mediaTypes[mediaType.toUpperCase()] ??\n mediaTypes[\n Object.keys(mediaTypes).find((t) =>\n mimetype?.includes(t.toLowerCase()),\n ) ?? ''\n ];\n\n const info = `WhatsApp ${typeKey} Keys`;\n\n // HKDF expand to 112 bytes (iv=16 + cipherKey=32 + macKey=32 + refKey=32)\n const expanded = Buffer.from(\n hkdfSync('sha256', mediaKeyBytes, Buffer.alloc(32), info, 112),\n );\n\n const iv = expanded.subarray(0, 16);\n const cipherKey = expanded.subarray(16, 48);\n\n const decipher = createDecipheriv('aes-256-cbc', cipherKey, iv);\n // WhatsApp handles padding manually — disable OpenSSL auto-padding\n decipher.setAutoPadding(false);\n const decrypted = Buffer.concat([\n decipher.update(encryptedData),\n decipher.final(),\n ]);\n\n return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;\n}\n\n/**\n * Correct PKCS#7-style padding that WhatsApp applies inconsistently.\n */\nfunction fixPadding(data: Buffer, expectedSize: number): Buffer {\n const padding = (16 - (expectedSize % 16)) & 0xf;\n if (padding > 0) {\n if (expectedSize + padding === data.length) {\n // Trim trailing padding bytes\n return data.subarray(0, data.length - padding);\n }\n if (data.length + padding === expectedSize) {\n // Append missing padding\n const pad = Buffer.alloc(padding, padding);\n return Buffer.concat([data, pad]);\n }\n }\n return data;\n}\n","/**\n * Download encrypted media from WhatsApp CDN using native fetch.\n *\n * Replaces the legacy axios-based downloader.\n */\nimport {\n DEFAULT_USER_AGENT,\n} from './constants';\n\nfunction buildUserAgent(override?: string): string {\n let ua = override ?? DEFAULT_USER_AGENT;\n if (!ua.includes('WhatsApp')) ua = `WhatsApp/2.16.352 ${ua}`;\n return ua;\n}\n\n/**\n * Download an encrypted media blob from the given URL.\n * Uses the global `fetch` API (Bun native / Node 18+).\n */\nexport async function downloadEncryptedMedia(\n url: string,\n options?: { useragent?: string; maxRetries?: number; retryDelay?: number },\n): Promise<Uint8Array> {\n const { useragent, maxRetries = 3, retryDelay = 2_000 } = options ?? {};\n\n const headers: Record<string, string> = {\n 'User-Agent': buildUserAgent(useragent),\n DNT: '1',\n 'Upgrade-Insecure-Requests': '1',\n Origin: 'https://web.whatsapp.com/',\n Referer: 'https://web.whatsapp.com/',\n };\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const res = await fetch(url, { headers });\n\n if (res.ok) {\n return new Uint8Array(await res.arrayBuffer());\n }\n\n if (res.status === 404) {\n throw new Error(\n 'This media does not exist or is no longer available on the server. ' +\n 'See: https://openwa.dev/pages/How%20to/decrypt-media.html#40439d',\n );\n }\n\n // Retry on transient errors\n if (attempt < maxRetries - 1) {\n await new Promise((r) => setTimeout(r, retryDelay));\n }\n }\n\n throw new Error(`Failed to download media after ${maxRetries} attempts`);\n}\n","/**\n * Main decryptMedia entry point.\n *\n * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.\n */\nimport { MissingCriticalDataError, NON_SIZE_TYPES } from './constants';\nimport { decryptBuffer } from './crypto';\nimport { downloadEncryptedMedia } from './download';\nimport type { DecryptableMessage, DecryptOptions } from './types';\n\n/**\n * Download and decrypt a WhatsApp media message.\n *\n * @param message — A message object with at least the required decryption fields.\n * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.\n * @returns The decrypted media as a Buffer.\n *\n * @example\n * ```ts\n * import { decryptMedia } from '@open-wa/decrypt';\n *\n * const buffer = await decryptMedia(message);\n * ```\n */\nexport async function decryptMedia(\n message: DecryptableMessage,\n optionsOrUa?: DecryptOptions | string,\n): Promise<Buffer> {\n // Normalise options — accept legacy string signature for backward compat\n const opts: DecryptOptions =\n typeof optionsOrUa === 'string'\n ? { useragent: optionsOrUa }\n : optionsOrUa ?? {};\n\n validateMessage(message);\n\n const url = (message.deprecatedMms3Url ?? message.clientUrl)!.trim();\n const encrypted = await downloadEncryptedMedia(url, opts);\n\n return decryptBuffer(\n encrypted,\n message.mediaKey,\n message.type,\n message.size,\n message.mimetype,\n );\n}\n\nfunction validateMessage(message: DecryptableMessage): void {\n const missing: string[] = [];\n\n if (!message.mediaKey) missing.push('mediaKey');\n if (!message.filehash) missing.push('filehash');\n if (!message.mimetype) missing.push('mimetype');\n if (!message.type) missing.push('type');\n if (!message.size) missing.push('size');\n\n if (missing.length === 0) return;\n\n // Size-only omission is a warning for non-sticker types, not an error\n if (missing.length === 1 && missing[0] === 'size') {\n if (!NON_SIZE_TYPES.includes(message.type as (typeof NON_SIZE_TYPES)[number])) {\n console.warn(\n '@open-wa/decrypt: size property is missing. File will fail an integrity check.',\n );\n }\n return;\n }\n\n throw new MissingCriticalDataError(\n `Message is missing critical data: ${missing.join(', ')}`,\n );\n}\n","/**\n * Strip a message down to the minimum fields needed for decryption.\n *\n * Useful for minimizing sensitive data in transit — removes sender info,\n * chat context, etc.\n */\n\nconst BLEACH_KEEP = new Set([\n 'type',\n 'clientUrl',\n 'mimetype',\n 'mediaKey',\n 'size',\n 'filehash',\n 'uploadhash',\n 'deprecatedMms3Url',\n]);\n\n/**\n * Return a copy of the message containing only the fields required for decryption.\n */\nexport function bleachMessage<T extends Record<string, unknown>>(\n message: T,\n): Partial<T> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(message)) {\n if (BLEACH_KEEP.has(key)) {\n result[key] = message[key];\n }\n }\n return result as Partial<T>;\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Utility type: at least one of the specified keys must be present.
|
|
9
9
|
* Replaces the `type-fest` RequireAtLeastOne import.
|
|
10
10
|
*/
|
|
11
|
-
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K
|
|
11
|
+
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>; }[Keys];
|
|
12
12
|
/** The absolute minimum fields needed for decryption. */
|
|
13
13
|
interface RequiredDecryptionMessage {
|
|
14
14
|
mediaKey: string;
|
|
@@ -50,7 +50,7 @@ interface DecryptOptions {
|
|
|
50
50
|
* const buffer = await decryptMedia(message);
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
|
-
declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: DecryptOptions | string): Promise<Buffer>;
|
|
53
|
+
export declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: DecryptOptions | string): Promise<Buffer>;
|
|
54
54
|
//#endregion
|
|
55
55
|
//#region src/bleach.d.ts
|
|
56
56
|
/**
|
|
@@ -62,16 +62,16 @@ declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: Decrypt
|
|
|
62
62
|
/**
|
|
63
63
|
* Return a copy of the message containing only the fields required for decryption.
|
|
64
64
|
*/
|
|
65
|
-
declare function bleachMessage<T extends Record<string, unknown>>(message: T): Partial<T>;
|
|
65
|
+
export declare function bleachMessage<T extends Record<string, unknown>>(message: T): Partial<T>;
|
|
66
66
|
//#endregion
|
|
67
67
|
//#region src/constants.d.ts
|
|
68
68
|
/** Mapping from message type to WhatsApp's internal media-type key name. */
|
|
69
|
-
declare const mediaTypes: Record<string, string>;
|
|
69
|
+
export declare const mediaTypes: Record<string, string>;
|
|
70
70
|
/** Thrown when a message is missing fields required for decryption. */
|
|
71
|
-
declare class MissingCriticalDataError extends Error {
|
|
71
|
+
export declare class MissingCriticalDataError extends Error {
|
|
72
72
|
readonly name = "MissingCriticalDataError";
|
|
73
73
|
constructor(message: string);
|
|
74
74
|
}
|
|
75
75
|
//#endregion
|
|
76
|
-
export {
|
|
76
|
+
export type { DecryptOptions, DecryptableMessage, RequiredDecryptionMessage };
|
|
77
77
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/decrypt.ts","../src/bleach.ts","../src/constants.ts"],"mappings":";;;;;;;;;;KAUK,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/decrypt.ts","../src/bleach.ts","../src/constants.ts"],"mappings":";;;;;;;;;;KAUK,kBAAkB,GAAG,mBAAmB,UAAU,KAAK,KAC1D,GACA,cAAc,GAAG,YAGd,KAAK,SAAS,SAAS,KAAK,GAAG,MAAM,QAAQ,KAAK,GAAG,QAAQ,MAAM,QACpE;;UAGa;EACf;EACA;EACA;EACA;EACA;;;;;;KAOU,qBAAqB;EAE7B;EACA;wCAIF;;UAGe;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;wBCvBoB,aACpB,SAAS,oBACT,cAAc,0BACb,QAAQ;;;;;;;;;;;;wBCNK,cAAc,UAAU,yBACtC,SAAS,IACR,QAAQ;;;;qBCtBE,YAAY;;qBAiBZ,iCAAiC;WAC1B;EAElB,YAAY"}
|
package/dist/index.d.mts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Utility type: at least one of the specified keys must be present.
|
|
9
9
|
* Replaces the `type-fest` RequireAtLeastOne import.
|
|
10
10
|
*/
|
|
11
|
-
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K
|
|
11
|
+
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>; }[Keys];
|
|
12
12
|
/** The absolute minimum fields needed for decryption. */
|
|
13
13
|
interface RequiredDecryptionMessage {
|
|
14
14
|
mediaKey: string;
|
|
@@ -50,7 +50,7 @@ interface DecryptOptions {
|
|
|
50
50
|
* const buffer = await decryptMedia(message);
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
|
-
declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: DecryptOptions | string): Promise<Buffer>;
|
|
53
|
+
export declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: DecryptOptions | string): Promise<Buffer>;
|
|
54
54
|
//#endregion
|
|
55
55
|
//#region src/bleach.d.ts
|
|
56
56
|
/**
|
|
@@ -62,16 +62,16 @@ declare function decryptMedia(message: DecryptableMessage, optionsOrUa?: Decrypt
|
|
|
62
62
|
/**
|
|
63
63
|
* Return a copy of the message containing only the fields required for decryption.
|
|
64
64
|
*/
|
|
65
|
-
declare function bleachMessage<T extends Record<string, unknown>>(message: T): Partial<T>;
|
|
65
|
+
export declare function bleachMessage<T extends Record<string, unknown>>(message: T): Partial<T>;
|
|
66
66
|
//#endregion
|
|
67
67
|
//#region src/constants.d.ts
|
|
68
68
|
/** Mapping from message type to WhatsApp's internal media-type key name. */
|
|
69
|
-
declare const mediaTypes: Record<string, string>;
|
|
69
|
+
export declare const mediaTypes: Record<string, string>;
|
|
70
70
|
/** Thrown when a message is missing fields required for decryption. */
|
|
71
|
-
declare class MissingCriticalDataError extends Error {
|
|
71
|
+
export declare class MissingCriticalDataError extends Error {
|
|
72
72
|
readonly name = "MissingCriticalDataError";
|
|
73
73
|
constructor(message: string);
|
|
74
74
|
}
|
|
75
75
|
//#endregion
|
|
76
|
-
export {
|
|
76
|
+
export type { DecryptOptions, DecryptableMessage, RequiredDecryptionMessage };
|
|
77
77
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/decrypt.ts","../src/bleach.ts","../src/constants.ts"],"mappings":";;;;;;;;;;KAUK,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/decrypt.ts","../src/bleach.ts","../src/constants.ts"],"mappings":";;;;;;;;;;KAUK,kBAAkB,GAAG,mBAAmB,UAAU,KAAK,KAC1D,GACA,cAAc,GAAG,YAGd,KAAK,SAAS,SAAS,KAAK,GAAG,MAAM,QAAQ,KAAK,GAAG,QAAQ,MAAM,QACpE;;UAGa;EACf;EACA;EACA;EACA;EACA;;;;;;KAOU,qBAAqB;EAE7B;EACA;wCAIF;;UAGe;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;wBCvBoB,aACpB,SAAS,oBACT,cAAc,0BACb,QAAQ;;;;;;;;;;;;wBCNK,cAAc,UAAU,yBACtC,SAAS,IACR,QAAQ;;;;qBCtBE,YAAY;;qBAiBZ,iCAAiC;WAC1B;EAElB,YAAY"}
|
package/dist/index.mjs
CHANGED
|
@@ -35,7 +35,8 @@ function decryptBuffer(encryptedData, mediaKeyBase64, mediaType, expectedSize, m
|
|
|
35
35
|
const info = `WhatsApp ${mediaTypes[mediaType.toUpperCase()] ?? mediaTypes[Object.keys(mediaTypes).find((t) => mimetype?.includes(t.toLowerCase())) ?? ""]} Keys`;
|
|
36
36
|
const expanded = Buffer.from(hkdfSync("sha256", mediaKeyBytes, Buffer.alloc(32), info, 112));
|
|
37
37
|
const iv = expanded.subarray(0, 16);
|
|
38
|
-
const
|
|
38
|
+
const cipherKey = expanded.subarray(16, 48);
|
|
39
|
+
const decipher = createDecipheriv("aes-256-cbc", cipherKey, iv);
|
|
39
40
|
decipher.setAutoPadding(false);
|
|
40
41
|
const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
|
41
42
|
return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;
|
|
@@ -135,7 +136,7 @@ function validateMessage(message) {
|
|
|
135
136
|
* Useful for minimizing sensitive data in transit — removes sender info,
|
|
136
137
|
* chat context, etc.
|
|
137
138
|
*/
|
|
138
|
-
const BLEACH_KEEP = new Set([
|
|
139
|
+
const BLEACH_KEEP = /* @__PURE__ */ new Set([
|
|
139
140
|
"type",
|
|
140
141
|
"clientUrl",
|
|
141
142
|
"mimetype",
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/crypto.ts","../src/download.ts","../src/decrypt.ts","../src/bleach.ts"],"sourcesContent":["/** Mapping from message type to WhatsApp's internal media-type key name. */\nexport const mediaTypes: Record<string, string> = {\n IMAGE: 'Image',\n VIDEO: 'Video',\n AUDIO: 'Audio',\n PTT: 'Audio',\n DOCUMENT: 'Document',\n STICKER: 'Image',\n} as const;\n\n/** Message types where a missing `size` field is tolerable. */\nexport const NON_SIZE_TYPES = ['sticker'] as const;\n\n/** The default user-agent used for fetching encrypted media blobs. */\nexport const DEFAULT_USER_AGENT =\n 'WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36';\n\n/** Thrown when a message is missing fields required for decryption. */\nexport class MissingCriticalDataError extends Error {\n override readonly name = 'MissingCriticalDataError';\n\n constructor(message: string) {\n super(message);\n }\n}\n","/**\n * Core crypto routines for WhatsApp media decryption.\n *\n * Uses only `node:crypto` builtins — no external HKDF library needed.\n * `crypto.hkdfSync` landed in Node 15 and is available in Bun.\n */\nimport { createDecipheriv, hkdfSync } from 'node:crypto';\n\nimport { mediaTypes } from './constants.js';\n\n/**\n * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,\n * then decrypt the ciphertext.\n */\nexport function decryptBuffer(\n encryptedData: Uint8Array,\n mediaKeyBase64: string,\n mediaType: string,\n expectedSize?: number,\n mimetype?: string,\n): Buffer {\n const mediaKeyBytes = Buffer.from(mediaKeyBase64, 'base64');\n\n // Resolve the info string: \"WhatsApp <Type> Keys\"\n const typeKey =\n mediaTypes[mediaType.toUpperCase()] ??\n mediaTypes[\n Object.keys(mediaTypes).find((t) =>\n mimetype?.includes(t.toLowerCase()),\n ) ?? ''\n ];\n\n const info = `WhatsApp ${typeKey} Keys`;\n\n // HKDF expand to 112 bytes (iv=16 + cipherKey=32 + macKey=32 + refKey=32)\n const expanded = Buffer.from(\n hkdfSync('sha256', mediaKeyBytes, Buffer.alloc(32), info, 112),\n );\n\n const iv = expanded.subarray(0, 16);\n const cipherKey = expanded.subarray(16, 48);\n\n const decipher = createDecipheriv('aes-256-cbc', cipherKey, iv);\n // WhatsApp handles padding manually — disable OpenSSL auto-padding\n decipher.setAutoPadding(false);\n const decrypted = Buffer.concat([\n decipher.update(encryptedData),\n decipher.final(),\n ]);\n\n return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;\n}\n\n/**\n * Correct PKCS#7-style padding that WhatsApp applies inconsistently.\n */\nfunction fixPadding(data: Buffer, expectedSize: number): Buffer {\n const padding = (16 - (expectedSize % 16)) & 0xf;\n if (padding > 0) {\n if (expectedSize + padding === data.length) {\n // Trim trailing padding bytes\n return data.subarray(0, data.length - padding);\n }\n if (data.length + padding === expectedSize) {\n // Append missing padding\n const pad = Buffer.alloc(padding, padding);\n return Buffer.concat([data, pad]);\n }\n }\n return data;\n}\n","/**\n * Download encrypted media from WhatsApp CDN using native fetch.\n *\n * Replaces the legacy axios-based downloader.\n */\nimport {\n DEFAULT_USER_AGENT,\n} from './constants.js';\n\nfunction buildUserAgent(override?: string): string {\n let ua = override ?? DEFAULT_USER_AGENT;\n if (!ua.includes('WhatsApp')) ua = `WhatsApp/2.16.352 ${ua}`;\n return ua;\n}\n\n/**\n * Download an encrypted media blob from the given URL.\n * Uses the global `fetch` API (Bun native / Node 18+).\n */\nexport async function downloadEncryptedMedia(\n url: string,\n options?: { useragent?: string; maxRetries?: number; retryDelay?: number },\n): Promise<Uint8Array> {\n const { useragent, maxRetries = 3, retryDelay = 2_000 } = options ?? {};\n\n const headers: Record<string, string> = {\n 'User-Agent': buildUserAgent(useragent),\n DNT: '1',\n 'Upgrade-Insecure-Requests': '1',\n Origin: 'https://web.whatsapp.com/',\n Referer: 'https://web.whatsapp.com/',\n };\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const res = await fetch(url, { headers });\n\n if (res.ok) {\n return new Uint8Array(await res.arrayBuffer());\n }\n\n if (res.status === 404) {\n throw new Error(\n 'This media does not exist or is no longer available on the server. ' +\n 'See: https://openwa.dev/pages/How%20to/decrypt-media.html#40439d',\n );\n }\n\n // Retry on transient errors\n if (attempt < maxRetries - 1) {\n await new Promise((r) => setTimeout(r, retryDelay));\n }\n }\n\n throw new Error(`Failed to download media after ${maxRetries} attempts`);\n}\n","/**\n * Main decryptMedia entry point.\n *\n * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.\n */\nimport { MissingCriticalDataError, NON_SIZE_TYPES } from './constants.js';\nimport { decryptBuffer } from './crypto.js';\nimport { downloadEncryptedMedia } from './download.js';\nimport type { DecryptableMessage, DecryptOptions } from './types.js';\n\n/**\n * Download and decrypt a WhatsApp media message.\n *\n * @param message — A message object with at least the required decryption fields.\n * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.\n * @returns The decrypted media as a Buffer.\n *\n * @example\n * ```ts\n * import { decryptMedia } from '@open-wa/decrypt';\n *\n * const buffer = await decryptMedia(message);\n * ```\n */\nexport async function decryptMedia(\n message: DecryptableMessage,\n optionsOrUa?: DecryptOptions | string,\n): Promise<Buffer> {\n // Normalise options — accept legacy string signature for backward compat\n const opts: DecryptOptions =\n typeof optionsOrUa === 'string'\n ? { useragent: optionsOrUa }\n : optionsOrUa ?? {};\n\n validateMessage(message);\n\n const url = (message.deprecatedMms3Url ?? message.clientUrl)!.trim();\n const encrypted = await downloadEncryptedMedia(url, opts);\n\n return decryptBuffer(\n encrypted,\n message.mediaKey,\n message.type,\n message.size,\n message.mimetype,\n );\n}\n\nfunction validateMessage(message: DecryptableMessage): void {\n const missing: string[] = [];\n\n if (!message.mediaKey) missing.push('mediaKey');\n if (!message.filehash) missing.push('filehash');\n if (!message.mimetype) missing.push('mimetype');\n if (!message.type) missing.push('type');\n if (!message.size) missing.push('size');\n\n if (missing.length === 0) return;\n\n // Size-only omission is a warning for non-sticker types, not an error\n if (missing.length === 1 && missing[0] === 'size') {\n if (!NON_SIZE_TYPES.includes(message.type as (typeof NON_SIZE_TYPES)[number])) {\n console.warn(\n '@open-wa/decrypt: size property is missing. File will fail an integrity check.',\n );\n }\n return;\n }\n\n throw new MissingCriticalDataError(\n `Message is missing critical data: ${missing.join(', ')}`,\n );\n}\n","/**\n * Strip a message down to the minimum fields needed for decryption.\n *\n * Useful for minimizing sensitive data in transit — removes sender info,\n * chat context, etc.\n */\n\nconst BLEACH_KEEP = new Set([\n 'type',\n 'clientUrl',\n 'mimetype',\n 'mediaKey',\n 'size',\n 'filehash',\n 'uploadhash',\n 'deprecatedMms3Url',\n]);\n\n/**\n * Return a copy of the message containing only the fields required for decryption.\n */\nexport function bleachMessage<T extends Record<string, unknown>>(\n message: T,\n): Partial<T> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(message)) {\n if (BLEACH_KEEP.has(key)) {\n result[key] = message[key];\n }\n }\n return result as Partial<T>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/crypto.ts","../src/download.ts","../src/decrypt.ts","../src/bleach.ts"],"sourcesContent":["/** Mapping from message type to WhatsApp's internal media-type key name. */\nexport const mediaTypes: Record<string, string> = {\n IMAGE: 'Image',\n VIDEO: 'Video',\n AUDIO: 'Audio',\n PTT: 'Audio',\n DOCUMENT: 'Document',\n STICKER: 'Image',\n} as const;\n\n/** Message types where a missing `size` field is tolerable. */\nexport const NON_SIZE_TYPES = ['sticker'] as const;\n\n/** The default user-agent used for fetching encrypted media blobs. */\nexport const DEFAULT_USER_AGENT =\n 'WhatsApp/2.16.352 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36';\n\n/** Thrown when a message is missing fields required for decryption. */\nexport class MissingCriticalDataError extends Error {\n override readonly name = 'MissingCriticalDataError';\n\n constructor(message: string) {\n super(message);\n }\n}\n","/**\n * Core crypto routines for WhatsApp media decryption.\n *\n * Uses only `node:crypto` builtins — no external HKDF library needed.\n * `crypto.hkdfSync` landed in Node 15 and is available in Bun.\n */\nimport { createDecipheriv, hkdfSync } from 'node:crypto';\n\nimport { mediaTypes } from './constants';\n\n/**\n * Derive the AES-256-CBC key and IV from the media key using HKDF-SHA256,\n * then decrypt the ciphertext.\n */\nexport function decryptBuffer(\n encryptedData: Uint8Array,\n mediaKeyBase64: string,\n mediaType: string,\n expectedSize?: number,\n mimetype?: string,\n): Buffer {\n const mediaKeyBytes = Buffer.from(mediaKeyBase64, 'base64');\n\n // Resolve the info string: \"WhatsApp <Type> Keys\"\n const typeKey =\n mediaTypes[mediaType.toUpperCase()] ??\n mediaTypes[\n Object.keys(mediaTypes).find((t) =>\n mimetype?.includes(t.toLowerCase()),\n ) ?? ''\n ];\n\n const info = `WhatsApp ${typeKey} Keys`;\n\n // HKDF expand to 112 bytes (iv=16 + cipherKey=32 + macKey=32 + refKey=32)\n const expanded = Buffer.from(\n hkdfSync('sha256', mediaKeyBytes, Buffer.alloc(32), info, 112),\n );\n\n const iv = expanded.subarray(0, 16);\n const cipherKey = expanded.subarray(16, 48);\n\n const decipher = createDecipheriv('aes-256-cbc', cipherKey, iv);\n // WhatsApp handles padding manually — disable OpenSSL auto-padding\n decipher.setAutoPadding(false);\n const decrypted = Buffer.concat([\n decipher.update(encryptedData),\n decipher.final(),\n ]);\n\n return expectedSize ? fixPadding(decrypted, expectedSize) : decrypted;\n}\n\n/**\n * Correct PKCS#7-style padding that WhatsApp applies inconsistently.\n */\nfunction fixPadding(data: Buffer, expectedSize: number): Buffer {\n const padding = (16 - (expectedSize % 16)) & 0xf;\n if (padding > 0) {\n if (expectedSize + padding === data.length) {\n // Trim trailing padding bytes\n return data.subarray(0, data.length - padding);\n }\n if (data.length + padding === expectedSize) {\n // Append missing padding\n const pad = Buffer.alloc(padding, padding);\n return Buffer.concat([data, pad]);\n }\n }\n return data;\n}\n","/**\n * Download encrypted media from WhatsApp CDN using native fetch.\n *\n * Replaces the legacy axios-based downloader.\n */\nimport {\n DEFAULT_USER_AGENT,\n} from './constants';\n\nfunction buildUserAgent(override?: string): string {\n let ua = override ?? DEFAULT_USER_AGENT;\n if (!ua.includes('WhatsApp')) ua = `WhatsApp/2.16.352 ${ua}`;\n return ua;\n}\n\n/**\n * Download an encrypted media blob from the given URL.\n * Uses the global `fetch` API (Bun native / Node 18+).\n */\nexport async function downloadEncryptedMedia(\n url: string,\n options?: { useragent?: string; maxRetries?: number; retryDelay?: number },\n): Promise<Uint8Array> {\n const { useragent, maxRetries = 3, retryDelay = 2_000 } = options ?? {};\n\n const headers: Record<string, string> = {\n 'User-Agent': buildUserAgent(useragent),\n DNT: '1',\n 'Upgrade-Insecure-Requests': '1',\n Origin: 'https://web.whatsapp.com/',\n Referer: 'https://web.whatsapp.com/',\n };\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n const res = await fetch(url, { headers });\n\n if (res.ok) {\n return new Uint8Array(await res.arrayBuffer());\n }\n\n if (res.status === 404) {\n throw new Error(\n 'This media does not exist or is no longer available on the server. ' +\n 'See: https://openwa.dev/pages/How%20to/decrypt-media.html#40439d',\n );\n }\n\n // Retry on transient errors\n if (attempt < maxRetries - 1) {\n await new Promise((r) => setTimeout(r, retryDelay));\n }\n }\n\n throw new Error(`Failed to download media after ${maxRetries} attempts`);\n}\n","/**\n * Main decryptMedia entry point.\n *\n * Downloads the encrypted blob → decrypts with AES-256-CBC via HKDF-derived keys.\n */\nimport { MissingCriticalDataError, NON_SIZE_TYPES } from './constants';\nimport { decryptBuffer } from './crypto';\nimport { downloadEncryptedMedia } from './download';\nimport type { DecryptableMessage, DecryptOptions } from './types';\n\n/**\n * Download and decrypt a WhatsApp media message.\n *\n * @param message — A message object with at least the required decryption fields.\n * @param optionsOrUa — Either a {@link DecryptOptions} object or a legacy user-agent string.\n * @returns The decrypted media as a Buffer.\n *\n * @example\n * ```ts\n * import { decryptMedia } from '@open-wa/decrypt';\n *\n * const buffer = await decryptMedia(message);\n * ```\n */\nexport async function decryptMedia(\n message: DecryptableMessage,\n optionsOrUa?: DecryptOptions | string,\n): Promise<Buffer> {\n // Normalise options — accept legacy string signature for backward compat\n const opts: DecryptOptions =\n typeof optionsOrUa === 'string'\n ? { useragent: optionsOrUa }\n : optionsOrUa ?? {};\n\n validateMessage(message);\n\n const url = (message.deprecatedMms3Url ?? message.clientUrl)!.trim();\n const encrypted = await downloadEncryptedMedia(url, opts);\n\n return decryptBuffer(\n encrypted,\n message.mediaKey,\n message.type,\n message.size,\n message.mimetype,\n );\n}\n\nfunction validateMessage(message: DecryptableMessage): void {\n const missing: string[] = [];\n\n if (!message.mediaKey) missing.push('mediaKey');\n if (!message.filehash) missing.push('filehash');\n if (!message.mimetype) missing.push('mimetype');\n if (!message.type) missing.push('type');\n if (!message.size) missing.push('size');\n\n if (missing.length === 0) return;\n\n // Size-only omission is a warning for non-sticker types, not an error\n if (missing.length === 1 && missing[0] === 'size') {\n if (!NON_SIZE_TYPES.includes(message.type as (typeof NON_SIZE_TYPES)[number])) {\n console.warn(\n '@open-wa/decrypt: size property is missing. File will fail an integrity check.',\n );\n }\n return;\n }\n\n throw new MissingCriticalDataError(\n `Message is missing critical data: ${missing.join(', ')}`,\n );\n}\n","/**\n * Strip a message down to the minimum fields needed for decryption.\n *\n * Useful for minimizing sensitive data in transit — removes sender info,\n * chat context, etc.\n */\n\nconst BLEACH_KEEP = new Set([\n 'type',\n 'clientUrl',\n 'mimetype',\n 'mediaKey',\n 'size',\n 'filehash',\n 'uploadhash',\n 'deprecatedMms3Url',\n]);\n\n/**\n * Return a copy of the message containing only the fields required for decryption.\n */\nexport function bleachMessage<T extends Record<string, unknown>>(\n message: T,\n): Partial<T> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(message)) {\n if (BLEACH_KEEP.has(key)) {\n result[key] = message[key];\n }\n }\n return result as Partial<T>;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-wa/decrypt",
|
|
3
|
-
"version": "5.0.0
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Lightweight WhatsApp media decryption — zero external dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
21
|
+
"@open-wa/schema": "5.0.0",
|
|
22
|
+
"@types/node": "^26.6.3",
|
|
23
|
+
"typescript": "^7.0.2",
|
|
24
|
+
"vitest": "5.0.2"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@open-wa/schema": "5.0.0
|
|
27
|
+
"@open-wa/schema": "5.0.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"@open-wa/schema": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"type": "git",
|
|
45
45
|
"url": "https://github.com/open-wa/wa-automate-nodejs.git"
|
|
46
46
|
},
|
|
47
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
47
48
|
"scripts": {
|
|
48
49
|
"build": "tsdown",
|
|
49
50
|
"dev": "tsdown --watch",
|