@salespark/toolkit 2.1.16 → 2.1.18

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/README.md CHANGED
@@ -32,7 +32,7 @@ npm i @salespark/toolkit
32
32
  - **Defer utilities**: post-return microtask scheduling, non-critical timers, after-response hooks.
33
33
  - **Boolean utilities**: safe boolean conversion with common representations
34
34
  - **Validation utilities**: IBAN validator (ISO 13616), Portuguese tax ID validator
35
- - **Security utilities**: Markdown XSS protection, content sanitization, risk assessment, obfuscation helpers
35
+ - **Security utilities**: Markdown XSS protection, content sanitization, risk assessment, obfuscation helpers, reversible base36 code encoding/decoding
36
36
  - **Environment detection**: `isBrowser`, `isNode` runtime checks
37
37
 
38
38
  ---
@@ -544,6 +544,22 @@ isNilOrEmpty(undefined);
544
544
  // Result: true
545
545
  ```
546
546
 
547
+ **`isNilEmptyOrEmptyObject(value: unknown): boolean`** — Checks if value is nil, empty string, or an empty plain object (no own keys).
548
+
549
+ ```javascript
550
+ isNilEmptyOrEmptyObject(null);
551
+ // Result: true
552
+
553
+ isNilEmptyOrEmptyObject("");
554
+ // Result: true
555
+
556
+ isNilEmptyOrEmptyObject({});
557
+ // Result: true
558
+
559
+ isNilEmptyOrEmptyObject({ a: 1 });
560
+ // Result: false
561
+ ```
562
+
547
563
  **`hasNilOrEmpty(array: unknown): boolean`** — Checks if any element in array is nil or empty.
548
564
 
549
565
  ```javascript
@@ -836,17 +852,17 @@ assessSecurityRisks([]);
836
852
  // Result: { score: 0, level: "safe", recommendations: ["Content appears safe to use"] }
837
853
  ```
838
854
 
839
- **`scrambleString(value: string, secret: string): SalesParkContract<object>`** — Scrambles a string using a repeating secret (XOR) and Base64. Reversible obfuscation only (not crypto).
855
+ **`encodeString(input: string, secret: string): SalesParkContract<any>`** — Base64-encodes a string and scrambles it with the provided secret (obfuscation only).
840
856
 
841
857
  ```javascript
842
- const scrambled = scrambleString("Hello", "secret");
858
+ const encoded = encodeString("Hello", "secret");
843
859
  // Result: { status: true, data: "..." }
844
860
 
845
- const original = descrambleString(scrambled.data, "secret");
861
+ const decoded = decodeString(encoded.data, "secret");
846
862
  // Result: { status: true, data: "Hello" }
847
863
  ```
848
864
 
849
- **`descrambleString(value: string, secret: string): SalesParkContract<object>`** — Reverses `scrambleString` using the same secret.
865
+ **`decodeString(encoded: string, secret: string): SalesParkContract<any>`** — Reverses `encodeString` using the same secret.
850
866
 
851
867
  **`encodeObject(input: object, secret: string): SalesParkContract<object>`** — JSON-stringifies an object, Base64-encodes it, and scrambles the result (obfuscation only).
852
868
 
@@ -860,6 +876,50 @@ const decoded = decodeObject(encoded.data, "secret");
860
876
 
861
877
  **`decodeObject(encoded: string, secret: string): SalesParkContract<object>`** — Reverses `encodeObject` using the same secret.
862
878
 
879
+ **`encodeBase36Code(identifier: string, config: EncodeDecodeConfig): SalesParkContract<{ code: string }>`** — Encodes a base36 identifier into a reversible lower-case base36 code using secret-based XOR + rotation.
880
+
881
+ ```typescript
882
+ import { encodeBase36Code, decodeBase36Code } from "@salespark/toolkit";
883
+
884
+ const config = {
885
+ secret: "my-super-secret-key",
886
+ bitSize: 80,
887
+ rotateBits: 17,
888
+ addConstant: "0x1fd0a5b7c3",
889
+ };
890
+
891
+ const encoded = encodeBase36Code("AB12CD34", config);
892
+ // Result: { status: true, data: { code: "..." } }
893
+
894
+ const decoded = decodeBase36Code(encoded.data.code, config);
895
+ // Result: { status: true, data: { identifier: "AB12CD34" } }
896
+ ```
897
+
898
+ **`decodeBase36Code(code: string, config: EncodeDecodeConfig): SalesParkContract<{ identifier: string }>`** — Decodes a previously encoded base36 code back to the original identifier (upper-case).
899
+
900
+ ```typescript
901
+ const bad = encodeBase36Code("AB-12", {
902
+ secret: "my-super-secret-key",
903
+ });
904
+ // Result: { status: false, data: { message: "Identifier must be base36 (0-9, A-Z)" } }
905
+
906
+ const weakSecret = decodeBase36Code("abc123", {
907
+ secret: "short",
908
+ });
909
+ // Result: { status: false, data: { message: "Missing or weak secret" } }
910
+ ```
911
+
912
+ **`EncodeDecodeConfig`** — Configuration object used by `encodeBase36Code` and `decodeBase36Code`.
913
+
914
+ ```typescript
915
+ type EncodeDecodeConfig = {
916
+ secret: string; // required, minimum 12 chars
917
+ bitSize?: number; // default: 80
918
+ rotateBits?: number; // default: 17
919
+ addConstant?: string; // default: "0x1fd0a5b7c3"
920
+ };
921
+ ```
922
+
863
923
  ### ✅ Validation Utilities
864
924
 
865
925
  **`isPTTaxId(value: string | number): boolean`** — Validates Portuguese Tax ID (NIF) with MOD-11 algorithm and format checking.
@@ -993,5 +1053,5 @@ MIT © [SalesPark](https://salespark.io)
993
1053
 
994
1054
  ---
995
1055
 
996
- _Document version: 13_
997
- _Last update: 09-01-2026_
1056
+ _Document version: 15_
1057
+ _Last update: 16-02-2026_
package/dist/index.cjs CHANGED
@@ -506,6 +506,17 @@ var isNilOrEmpty = (value) => {
506
506
  return true;
507
507
  }
508
508
  };
509
+ var isNilEmptyOrEmptyObject = (value) => {
510
+ try {
511
+ if (isNilOrEmpty(value)) return true;
512
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0) {
513
+ return true;
514
+ }
515
+ return false;
516
+ } catch {
517
+ return true;
518
+ }
519
+ };
509
520
  var hasNilOrEmpty = (array) => {
510
521
  try {
511
522
  if (!Array.isArray(array)) return true;
@@ -1881,6 +1892,24 @@ var encodeObject = (input, secret) => {
1881
1892
  return { status: false, data: error };
1882
1893
  }
1883
1894
  };
1895
+ var encodeString = (input, secret) => {
1896
+ try {
1897
+ if (typeof input !== "string") {
1898
+ return { status: false, data: "Input must be a string" };
1899
+ }
1900
+ if (!secret || typeof secret !== "string") {
1901
+ return { status: false, data: "Secret must be a non-empty string" };
1902
+ }
1903
+ const base64 = toBase64(input);
1904
+ const scrambledResponse = scrambleString(base64, secret);
1905
+ if (!scrambledResponse.status) {
1906
+ return { status: false, data: "Scrambling failed" };
1907
+ }
1908
+ return { status: true, data: scrambledResponse.data };
1909
+ } catch (error) {
1910
+ return { status: false, data: error };
1911
+ }
1912
+ };
1884
1913
  var decodeObject = (encoded, secret) => {
1885
1914
  try {
1886
1915
  if (typeof encoded !== "string") {
@@ -1899,6 +1928,24 @@ var decodeObject = (encoded, secret) => {
1899
1928
  return { status: false, data: error };
1900
1929
  }
1901
1930
  };
1931
+ var decodeString = (encoded, secret) => {
1932
+ try {
1933
+ if (typeof encoded !== "string") {
1934
+ return { status: false, data: "Encoded value must be a string" };
1935
+ }
1936
+ if (!secret || typeof secret !== "string") {
1937
+ return { status: false, data: "Secret must be a non-empty string" };
1938
+ }
1939
+ const descrambledResponse = descrambleString(encoded, secret);
1940
+ if (!descrambledResponse.status) {
1941
+ return { status: false, data: "Descrambling failed" };
1942
+ }
1943
+ const value = fromBase64(descrambledResponse.data);
1944
+ return { status: true, data: value };
1945
+ } catch (error) {
1946
+ return { status: false, data: error };
1947
+ }
1948
+ };
1902
1949
 
1903
1950
  // src/utils/defer.ts
1904
1951
  var swallow = (p) => p.catch(() => {
@@ -1984,6 +2031,195 @@ var deferAfterResponseNonCritical = (res, fn) => {
1984
2031
  }
1985
2032
  };
1986
2033
 
2034
+ // src/utils/base36.ts
2035
+ var DEFAULTS = {
2036
+ bitSize: 80,
2037
+ rotateBits: 17,
2038
+ addConstant: "0x1fd0a5b7c3"
2039
+ };
2040
+ var isValidBase36 = (value) => /^[a-z0-9]+$/i.test(value);
2041
+ var normalizeInput = (value) => (value || "").trim();
2042
+ var assertSecret = (secret) => {
2043
+ if (typeof secret !== "string" || secret.trim().length < 12) {
2044
+ return { status: false, data: { message: "Missing or weak secret" } };
2045
+ }
2046
+ return { status: true, data: true };
2047
+ };
2048
+ var parseBase36BigInt = (value) => {
2049
+ try {
2050
+ const normalizedValue = normalizeInput(value);
2051
+ if (!normalizedValue) {
2052
+ return { status: false, data: { message: "Empty input" } };
2053
+ }
2054
+ if (!isValidBase36(normalizedValue)) {
2055
+ return { status: false, data: { message: "Invalid base36 input" } };
2056
+ }
2057
+ const safeValue = normalizedValue.toLowerCase();
2058
+ let output = 0n;
2059
+ for (let i = 0; i < safeValue.length; i++) {
2060
+ const char = safeValue[i];
2061
+ const digit = char >= "0" && char <= "9" ? BigInt(char.charCodeAt(0) - 48) : BigInt(char.charCodeAt(0) - 87);
2062
+ output = output * 36n + digit;
2063
+ }
2064
+ return { status: true, data: output };
2065
+ } catch (error) {
2066
+ return {
2067
+ status: false,
2068
+ data: { message: "Failed to parse base36 input", error }
2069
+ };
2070
+ }
2071
+ };
2072
+ var toBase36Lower = (value) => {
2073
+ try {
2074
+ if (value < 0n) {
2075
+ return {
2076
+ status: false,
2077
+ data: { message: "Negative values are not supported" }
2078
+ };
2079
+ }
2080
+ return { status: true, data: value.toString(36) };
2081
+ } catch (error) {
2082
+ return {
2083
+ status: false,
2084
+ data: { message: "Failed to convert to base36", error }
2085
+ };
2086
+ }
2087
+ };
2088
+ var getParams = (config) => {
2089
+ try {
2090
+ const bitSizeRaw = BigInt(config.bitSize ?? DEFAULTS.bitSize);
2091
+ if (bitSizeRaw <= 0n) {
2092
+ return {
2093
+ status: false,
2094
+ data: { message: "bitSize must be greater than 0" }
2095
+ };
2096
+ }
2097
+ const rotateRaw = BigInt(config.rotateBits ?? DEFAULTS.rotateBits);
2098
+ const rotateBits = (rotateRaw % bitSizeRaw + bitSizeRaw) % bitSizeRaw;
2099
+ const addConstant = BigInt(config.addConstant ?? DEFAULTS.addConstant);
2100
+ const mask = (1n << bitSizeRaw) - 1n;
2101
+ return {
2102
+ status: true,
2103
+ data: { bitSize: bitSizeRaw, rotateBits, addConstant, mask }
2104
+ };
2105
+ } catch (error) {
2106
+ return { status: false, data: { message: "Invalid configuration", error } };
2107
+ }
2108
+ };
2109
+ var secretToKey = (secret, mask) => {
2110
+ let key = 0n;
2111
+ const safeSecret = secret.trim();
2112
+ for (let i = 0; i < safeSecret.length; i++) {
2113
+ key = key * 131n + BigInt(safeSecret.charCodeAt(i)) & mask;
2114
+ }
2115
+ return key;
2116
+ };
2117
+ var rotl = (x, r, bitSize, mask) => {
2118
+ if (r === 0n) return x & mask;
2119
+ return (x << r | x >> bitSize - r) & mask;
2120
+ };
2121
+ var rotr = (x, r, bitSize, mask) => {
2122
+ if (r === 0n) return x & mask;
2123
+ return (x >> r | x << bitSize - r) & mask;
2124
+ };
2125
+ var encodeBase36Code = (identifier, config) => {
2126
+ try {
2127
+ const secretCheck = assertSecret(config?.secret);
2128
+ if (!secretCheck.status) {
2129
+ return { status: false, data: secretCheck.data };
2130
+ }
2131
+ const input = normalizeInput(identifier);
2132
+ if (!input) {
2133
+ return { status: false, data: { message: "Identifier is required" } };
2134
+ }
2135
+ if (!isValidBase36(input)) {
2136
+ return {
2137
+ status: false,
2138
+ data: { message: "Identifier must be base36 (0-9, A-Z)" }
2139
+ };
2140
+ }
2141
+ const parsed = parseBase36BigInt(input);
2142
+ if (!parsed.status) {
2143
+ return { status: false, data: parsed.data };
2144
+ }
2145
+ const params = getParams(config);
2146
+ if (!params.status) {
2147
+ return { status: false, data: params.data };
2148
+ }
2149
+ const {
2150
+ bitSize,
2151
+ rotateBits,
2152
+ addConstant,
2153
+ mask
2154
+ } = params.data;
2155
+ const key = secretToKey(config.secret, mask);
2156
+ let value = parsed.data & mask;
2157
+ value = value ^ key;
2158
+ value = value + addConstant & mask;
2159
+ value = rotl(value, rotateBits, bitSize, mask);
2160
+ const codeResult = toBase36Lower(value);
2161
+ if (!codeResult.status) {
2162
+ return { status: false, data: codeResult.data };
2163
+ }
2164
+ return { status: true, data: { code: codeResult.data } };
2165
+ } catch (error) {
2166
+ return {
2167
+ status: false,
2168
+ data: { message: "Failed to encode code", error }
2169
+ };
2170
+ }
2171
+ };
2172
+ var decodeBase36Code = (code, config) => {
2173
+ try {
2174
+ const secretCheck = assertSecret(config?.secret);
2175
+ if (!secretCheck.status) {
2176
+ return { status: false, data: secretCheck.data };
2177
+ }
2178
+ const input = normalizeInput(code);
2179
+ if (!input) {
2180
+ return { status: false, data: { message: "Code is required" } };
2181
+ }
2182
+ if (!isValidBase36(input)) {
2183
+ return {
2184
+ status: false,
2185
+ data: { message: "Code must be base36 (0-9, a-z)" }
2186
+ };
2187
+ }
2188
+ const parsed = parseBase36BigInt(input);
2189
+ if (!parsed.status) {
2190
+ return { status: false, data: parsed.data };
2191
+ }
2192
+ const params = getParams(config);
2193
+ if (!params.status) {
2194
+ return { status: false, data: params.data };
2195
+ }
2196
+ const {
2197
+ bitSize,
2198
+ rotateBits,
2199
+ addConstant,
2200
+ mask
2201
+ } = params.data;
2202
+ const key = secretToKey(config.secret, mask);
2203
+ let value = parsed.data & mask;
2204
+ value = rotr(value, rotateBits, bitSize, mask);
2205
+ value = value - addConstant & mask;
2206
+ value = value ^ key;
2207
+ const identifierResult = toBase36Lower(value);
2208
+ if (!identifierResult.status) {
2209
+ return { status: false, data: identifierResult.data };
2210
+ }
2211
+ return {
2212
+ status: true,
2213
+ data: { identifier: identifierResult.data.toUpperCase() }
2214
+ };
2215
+ } catch (error) {
2216
+ return {
2217
+ status: false,
2218
+ data: { message: "Failed to decode code", error }
2219
+ };
2220
+ }
2221
+ };
2222
+
1987
2223
  // src/index.ts
1988
2224
  var isBrowser = typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
1989
2225
  var isNode = typeof process !== "undefined" && !!process.versions?.node;
@@ -2004,15 +2240,18 @@ exports.compact = compact;
2004
2240
  exports.currencyToSymbol = currencyToSymbol;
2005
2241
  exports.debounce = debounce;
2006
2242
  exports.deburr = deburr;
2243
+ exports.decodeBase36Code = decodeBase36Code;
2007
2244
  exports.decodeObject = decodeObject;
2245
+ exports.decodeString = decodeString;
2008
2246
  exports.deferAfterResponse = deferAfterResponse;
2009
2247
  exports.deferAfterResponseNonCritical = deferAfterResponseNonCritical;
2010
2248
  exports.deferNonCritical = deferNonCritical;
2011
2249
  exports.deferPostReturn = deferPostReturn;
2012
2250
  exports.delay = delay;
2013
- exports.descrambleString = descrambleString;
2014
2251
  exports.difference = difference;
2252
+ exports.encodeBase36Code = encodeBase36Code;
2015
2253
  exports.encodeObject = encodeObject;
2254
+ exports.encodeString = encodeString;
2016
2255
  exports.fill = fill;
2017
2256
  exports.flatten = flatten;
2018
2257
  exports.flattenDepth = flattenDepth;
@@ -2030,6 +2269,7 @@ exports.intersection = intersection;
2030
2269
  exports.isBrowser = isBrowser;
2031
2270
  exports.isFlattenable = isFlattenable;
2032
2271
  exports.isNil = isNil;
2272
+ exports.isNilEmptyOrEmptyObject = isNilEmptyOrEmptyObject;
2033
2273
  exports.isNilEmptyOrZeroLen = isNilEmptyOrZeroLen;
2034
2274
  exports.isNilEmptyOrZeroLength = isNilEmptyOrZeroLength;
2035
2275
  exports.isNilOrEmpty = isNilOrEmpty;
@@ -2072,7 +2312,6 @@ exports.safeParseInt = safeParseInt;
2072
2312
  exports.safeSubtract = safeSubtract;
2073
2313
  exports.sanitize = sanitize;
2074
2314
  exports.sanitizeMarkdown = sanitizeMarkdown;
2075
- exports.scrambleString = scrambleString;
2076
2315
  exports.sentenceCase = sentenceCase;
2077
2316
  exports.shuffle = shuffle;
2078
2317
  exports.slugify = slugify;