GitHub Viewer
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} 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}
*/
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}
*/
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}
*/
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;
}
}
}