import { readFile } from 'node:fs/promises'; import querystring from 'node:querystring'; import { inflateSync } from 'node:zlib'; import { Command } from './Command.js'; import { ErrorCode } from './ErrorCode.js'; import { Language } from './Language.js'; import { NewLine } from './NewLine.js'; /** * Stateful HTTP client: pass the activation key to the constructor, configure other options via * setters, then {@link StringEncrypt#send}. * * POST field `code` (activation key) is the value given at construction (empty string = demo). * * For {@link Command.Encrypt} with {@link StringEncrypt#setCompression}`(true)`, the API returns * `source` as base64-encoded gzip of the decryptor text. {@link StringEncrypt#send} decodes that * automatically so `source` is always plain text on success (unless * {@link StringEncrypt#setDecompressEncryptSource}`(false)`). */ export class StringEncrypt { static DEFAULT_API_URL = 'https://www.stringencrypt.com/api.php'; /** @type {boolean} */ #preferCurl = false; /** When true (default), decompress `source` after successful encrypt+compression responses. */ #decompressEncryptSource = true; /** @type {string | null} */ #command = null; /** Activation code sent as `code` (empty = demo). */ #apiKey = ''; // —— encrypt —— /** @type {string} */ #label = 'Label'; /** @type {string | null} */ #inputString = null; /** @type {Buffer | null} */ #inputBytes = null; /** @type {boolean} */ #compression = false; /** @type {string} */ #language = Language.Php; /** `false`, or highlight mode string such as `geshi` / `js` when supported server-side. */ #highlight = false; /** @type {number} */ #cmdMin = 1; /** @type {number} */ #cmdMax = 3; /** @type {boolean} */ #local = false; /** @type {boolean} */ #unicode = true; /** @type {string} */ #langLocale = 'en_US.utf8'; /** @type {string} */ #ansiEncoding = 'WINDOWS-1250'; /** @type {string} */ #newLines = NewLine.Lf; /** @type {string | null} */ #template = null; /** @type {boolean} */ #returnTemplate = false; /** @type {boolean} */ #includeTags = false; /** @type {boolean} */ #includeExample = false; /** When true, server emits a trace comment block above the encrypted array (hashes, VM JSON). */ #includeDebugComments = false; /** * @param {string} [apiKey] * @param {boolean} [preferCurl] Ignored; the JS client always uses `fetch` (kept for parity with the PHP SDK). */ constructor(apiKey = '', preferCurl = false) { this.#apiKey = apiKey; this.#preferCurl = preferCurl; } /** * Activation status and limits for the activation key passed to the constructor. * * @returns {Promise | false>} Parsed JSON on success, or false on transport/parse failure. */ async isDemo() { const previousCommand = this.#command; this.setCommand(Command.IsDemo); const result = await this.send(); this.#command = previousCommand; return result; } /** * Encrypt raw file contents (binary). Uses other encrypt options already set on this client. * * @param {string} filePath * @param {string} label * @returns {Promise | false>} */ async encryptFileContents(filePath, label) { let raw; try { raw = await readFile(filePath); } catch { return false; } if (!raw.length) { return false; } const saved = { command: this.#command, inputString: this.#inputString, inputBytes: this.#inputBytes, label: this.#label, }; this.setCommand(Command.Encrypt).setBytes(raw).setLabel(label); const result = await this.send(); this.#command = saved.command; this.#inputString = saved.inputString; this.#inputBytes = saved.inputBytes; this.#label = saved.label; return result; } /** * Encrypt a UTF-8 string. Uses other encrypt options already set on this client (language, * compression, cmd range, etc.). * * @param {string} string * @param {string} label * @returns {Promise | false>} */ async encryptString(string, label) { const saved = { command: this.#command, inputString: this.#inputString, inputBytes: this.#inputBytes, label: this.#label, }; this.setCommand(Command.Encrypt).setString(string).setLabel(label); const result = await this.send(); this.#command = saved.command; this.#inputString = saved.inputString; this.#inputBytes = saved.inputBytes; this.#label = saved.label; return result; } getPreferCurl() { return this.#preferCurl; } /** @param {boolean} preferCurl */ setPreferCurl(preferCurl) { this.#preferCurl = preferCurl; return this; } getDecompressEncryptSource() { return this.#decompressEncryptSource; } /** * Disable automatic gzip+base64 decoding of `source` for encrypt+compression responses * if you need the raw payload from the API. * * @param {boolean} decompressEncryptSource */ setDecompressEncryptSource(decompressEncryptSource) { this.#decompressEncryptSource = decompressEncryptSource; return this; } getCommand() { return this.#command; } /** @param {string} command One of {@link Command} values. */ setCommand(command) { this.#command = command; return this; } getLabel() { return this.#label; } /** @param {string} label */ setLabel(label) { this.#label = label; return this; } /** UTF-8 text input; clears raw bytes input. */ setString(string) { this.#inputString = string; this.#inputBytes = null; return this; } /** Raw binary input; clears string input. */ setBytes(bytes) { this.#inputBytes = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes); this.#inputString = null; return this; } getCompression() { return this.#compression; } /** @param {boolean} compression */ setCompression(compression) { this.#compression = compression; return this; } getLanguage() { return this.#language; } /** @param {string} language One of {@link Language} values. */ setLanguage(language) { this.#language = language; return this; } getHighlight() { return this.#highlight; } /** @param {string | boolean} highlight */ setHighlight(highlight) { this.#highlight = highlight; return this; } getCmdMin() { return this.#cmdMin; } /** @param {number} cmdMin */ setCmdMin(cmdMin) { this.#cmdMin = cmdMin; return this; } getCmdMax() { return this.#cmdMax; } /** @param {number} cmdMax */ setCmdMax(cmdMax) { this.#cmdMax = cmdMax; return this; } getLocal() { return this.#local; } /** @param {boolean} local */ setLocal(local) { this.#local = local; return this; } getUnicode() { return this.#unicode; } /** @param {boolean} unicode */ setUnicode(unicode) { this.#unicode = unicode; return this; } getLangLocale() { return this.#langLocale; } /** @param {string} langLocale */ setLangLocale(langLocale) { this.#langLocale = langLocale; return this; } getAnsiEncoding() { return this.#ansiEncoding; } /** @param {string} ansiEncoding */ setAnsiEncoding(ansiEncoding) { this.#ansiEncoding = ansiEncoding; return this; } getNewLines() { return this.#newLines; } /** @param {string} newLines One of {@link NewLine} values. */ setNewLines(newLines) { this.#newLines = newLines; return this; } getTemplate() { return this.#template; } /** @param {string | null} template */ setTemplate(template) { this.#template = template; return this; } getReturnTemplate() { return this.#returnTemplate; } /** @param {boolean} returnTemplate */ setReturnTemplate(returnTemplate) { this.#returnTemplate = returnTemplate; return this; } getIncludeTags() { return this.#includeTags; } /** @param {boolean} includeTags */ setIncludeTags(includeTags) { this.#includeTags = includeTags; return this; } getIncludeExample() { return this.#includeExample; } /** @param {boolean} includeExample */ setIncludeExample(includeExample) { this.#includeExample = includeExample; return this; } getIncludeDebugComments() { return this.#includeDebugComments; } /** @param {boolean} includeDebugComments */ setIncludeDebugComments(includeDebugComments) { this.#includeDebugComments = includeDebugComments; return this; } /** * Reset request fields to defaults (reuse the same client for another call). */ reset() { this.#command = null; this.#label = '$label'; this.#inputString = null; this.#inputBytes = null; this.#compression = false; this.#language = Language.Php; this.#highlight = false; this.#cmdMin = 1; this.#cmdMax = 3; this.#local = false; this.#unicode = true; this.#langLocale = 'en_US.utf8'; this.#ansiEncoding = 'WINDOWS-1250'; this.#newLines = NewLine.Lf; this.#template = null; this.#returnTemplate = false; this.#includeTags = false; this.#includeExample = false; this.#includeDebugComments = false; return this; } /** * Build the POST body the client would send (for debugging and tests). * * @returns {Record} */ toRequestArray() { if (this.#command === null) { throw new Error('Command must be set (use setCommand()).'); } if (this.#command === Command.Info) { return this.#buildInfoParams(); } if (this.#command === Command.IsDemo) { return this.#buildIsDemoParams(); } if (this.#command === Command.Encrypt) { return this.#buildEncryptParams(); } throw new Error('Unknown command.'); } /** * Send the request and return decoded JSON, or false on transport / JSON failure. * * @returns {Promise | false>} */ async send() { const params = this.#formEncodeParams(this.toRequestArray()); const raw = await this.#post(StringEncrypt.DEFAULT_API_URL, params); if (raw === false || raw === '') { return false; } let decoded; try { decoded = JSON.parse(raw); } catch { return false; } if (decoded === null || typeof decoded !== 'object' || Array.isArray(decoded)) { return false; } return this.#applyDecryptorSourceDecompression(/** @type {Record} */ (decoded)); } /** * Replace `source` with plain text when the request used compression (API returns * base64(gzip(decryptor source))). * * @param {Record} response */ #applyDecryptorSourceDecompression(response) { if (this.#command !== Command.Encrypt || !this.#compression || !this.#decompressEncryptSource) { return response; } if (response.error !== ErrorCode.SUCCESS) { return response; } const src = response.source; if (typeof src !== 'string') { return response; } let binary; try { binary = Buffer.from(src, 'base64'); } catch { return response; } let plain; try { plain = inflateSync(binary); } catch { return response; } return { ...response, source: plain.toString('utf8') }; } /** @returns {Record} */ #buildInfoParams() { return { command: Command.Info, code: this.#apiKey, }; } /** @returns {Record} */ #buildIsDemoParams() { return { command: Command.IsDemo, code: this.#apiKey, }; } /** @returns {Record} */ #buildEncryptParams() { /** @type {Record} */ const p = { command: Command.Encrypt, code: this.#apiKey, label: this.#label, compression: this.#compression, lang: this.#language, cmd_min: this.#cmdMin, cmd_max: this.#cmdMax, local: this.#local, unicode: this.#unicode, lang_locale: this.#langLocale, ansi_encoding: this.#ansiEncoding, new_lines: this.#newLines, return_template: this.#returnTemplate, include_tags: this.#includeTags, include_example: this.#includeExample, include_debug_comments: this.#includeDebugComments, }; if (this.#inputString !== null) { p.string = this.#inputString; } else if (this.#inputBytes !== null) { p.bytes = this.#inputBytes; } if (this.#highlight !== false) { p.highlight = this.#highlight; } if (this.#template !== null) { p.template = this.#template; } return p; } /** * Match PHP `http_build_query`: booleans become "0"/"1"; `Buffer` values (e.g. `bytes`) become * a byte-per-character string so `querystring.stringify` percent-encodes like PHP. * * @param {Record} data */ #formEncodeParams(data) { /** @type {Record} */ const flat = {}; for (const [key, value] of Object.entries(data)) { if (typeof value === 'boolean') { flat[key] = value ? 1 : 0; } else if (Buffer.isBuffer(value)) { flat[key] = value.toString('latin1'); } else { flat[key] = value; } } return querystring.stringify(flat); } /** * @param {string} url * @param {string} body * @returns {Promise} */ async #post(url, body) { try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'pelock/stringencrypt (+https://www.stringencrypt.com)', }, body, }); const text = await res.text(); return text; } catch { return false; } } }