@salespark/toolkit 2.1.17 → 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
  ---
@@ -876,6 +876,50 @@ const decoded = decodeObject(encoded.data, "secret");
876
876
 
877
877
  **`decodeObject(encoded: string, secret: string): SalesParkContract<object>`** — Reverses `encodeObject` using the same secret.
878
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
+
879
923
  ### ✅ Validation Utilities
880
924
 
881
925
  **`isPTTaxId(value: string | number): boolean`** — Validates Portuguese Tax ID (NIF) with MOD-11 algorithm and format checking.
@@ -1009,5 +1053,5 @@ MIT © [SalesPark](https://salespark.io)
1009
1053
 
1010
1054
  ---
1011
1055
 
1012
- _Document version: 14_
1013
- _Last update: 06-02-2026_
1056
+ _Document version: 15_
1057
+ _Last update: 16-02-2026_
package/dist/index.cjs CHANGED
@@ -2031,6 +2031,195 @@ var deferAfterResponseNonCritical = (res, fn) => {
2031
2031
  }
2032
2032
  };
2033
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
+
2034
2223
  // src/index.ts
2035
2224
  var isBrowser = typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
2036
2225
  var isNode = typeof process !== "undefined" && !!process.versions?.node;
@@ -2051,6 +2240,7 @@ exports.compact = compact;
2051
2240
  exports.currencyToSymbol = currencyToSymbol;
2052
2241
  exports.debounce = debounce;
2053
2242
  exports.deburr = deburr;
2243
+ exports.decodeBase36Code = decodeBase36Code;
2054
2244
  exports.decodeObject = decodeObject;
2055
2245
  exports.decodeString = decodeString;
2056
2246
  exports.deferAfterResponse = deferAfterResponse;
@@ -2059,6 +2249,7 @@ exports.deferNonCritical = deferNonCritical;
2059
2249
  exports.deferPostReturn = deferPostReturn;
2060
2250
  exports.delay = delay;
2061
2251
  exports.difference = difference;
2252
+ exports.encodeBase36Code = encodeBase36Code;
2062
2253
  exports.encodeObject = encodeObject;
2063
2254
  exports.encodeString = encodeString;
2064
2255
  exports.fill = fill;