@ibiliaze/stringman 3.17.0 → 3.19.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.
@@ -0,0 +1,2 @@
1
+ export declare function encryptString(plainText: string, password: string): Promise<string>;
2
+ export declare function decryptString(cipherTextB64: string, password: string): Promise<string>;
package/dist/crypto.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encryptString = encryptString;
4
+ exports.decryptString = decryptString;
5
+ const enc = new TextEncoder();
6
+ const dec = new TextDecoder();
7
+ async function getKeyFromPassword(password, salt) {
8
+ const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
9
+ return crypto.subtle.deriveKey({
10
+ name: 'PBKDF2',
11
+ salt, // ArrayBuffer is valid BufferSource
12
+ iterations: 100_000,
13
+ hash: 'SHA-256',
14
+ }, keyMaterial, {
15
+ name: 'AES-GCM',
16
+ length: 256,
17
+ }, false, ['encrypt', 'decrypt']);
18
+ }
19
+ async function encryptString(plainText, password) {
20
+ const saltBytes = crypto.getRandomValues(new Uint8Array(16));
21
+ const iv = crypto.getRandomValues(new Uint8Array(12));
22
+ const key = await getKeyFromPassword(password, saltBytes.buffer);
23
+ const cipherBuffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plainText));
24
+ const cipherBytes = new Uint8Array(cipherBuffer);
25
+ const combined = new Uint8Array(saltBytes.length + iv.length + cipherBytes.length);
26
+ combined.set(saltBytes, 0);
27
+ combined.set(iv, saltBytes.length);
28
+ combined.set(cipherBytes, saltBytes.length + iv.length);
29
+ return bufferToBase64(combined.buffer);
30
+ }
31
+ async function decryptString(cipherTextB64, password) {
32
+ const combined = new Uint8Array(base64ToBuffer(cipherTextB64));
33
+ const saltBytes = combined.slice(0, 16);
34
+ const iv = combined.slice(16, 28);
35
+ const cipherBytes = combined.slice(28);
36
+ // 👇 again, .buffer
37
+ const key = await getKeyFromPassword(password, saltBytes.buffer);
38
+ const plainBuffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, cipherBytes);
39
+ return dec.decode(plainBuffer);
40
+ }
41
+ /* base64 helpers stay the same */
42
+ function bufferToBase64(buf) {
43
+ const bytes = new Uint8Array(buf);
44
+ let binary = '';
45
+ for (let i = 0; i < bytes.length; i++)
46
+ binary += String.fromCharCode(bytes[i]);
47
+ return btoa(binary);
48
+ }
49
+ function base64ToBuffer(b64) {
50
+ const binary = atob(b64);
51
+ const bytes = new Uint8Array(binary.length);
52
+ for (let i = 0; i < binary.length; i++)
53
+ bytes[i] = binary.charCodeAt(i);
54
+ return bytes.buffer;
55
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './seat';
2
2
  import { fishyMatchesAll } from './ticket';
3
+ import { decryptString, encryptString } from './crypto';
3
4
  /**
4
5
  * Clean up extra whitespace in a string.
5
6
  *
@@ -140,6 +141,18 @@ export declare const isVideoUrl: (url: string) => boolean;
140
141
  * @throws Error if no endpoint returns a valid IP.
141
142
  */
142
143
  export declare const getPublicIP: () => Promise<string>;
144
+ /**
145
+ * Convert an unknown value into a stable string key for deduping.
146
+ */
147
+ export declare const valueKey: (v: unknown) => string;
148
+ /**
149
+ * Dedupe an array by a key (dot-path supported), preserving the item type.
150
+ *
151
+ * - keepFirst=true => first wins
152
+ * - keepFirst=false => last wins
153
+ * - keepNoKey=true => keep items where key is missing/invalid (kept in original order, before deduped items)
154
+ */
155
+ export declare const dedupeBy: <T>(arr: T[] | undefined, key: string, { keepFirst, keepNoKey }?: DedupeByOptions) => T[];
143
156
  export declare const invalidPw: (password: string, passwordLength?: number) => string | void;
144
157
  export declare const b36: (n: number) => string;
145
158
  export declare const luhn36: (s: string) => string;
@@ -162,6 +175,14 @@ export declare const ticket: {
162
175
  validateTicketCode: (code: string) => boolean;
163
176
  fishyMatchesAll: typeof fishyMatchesAll;
164
177
  };
178
+ export declare const crypto: {
179
+ encryptString: typeof encryptString;
180
+ decryptString: typeof decryptString;
181
+ };
165
182
  export declare const order: {
166
183
  createNumericOrderId: () => string;
167
184
  };
185
+ type DedupeByOptions = {
186
+ keepFirst?: boolean;
187
+ keepNoKey?: boolean;
188
+ };
package/dist/index.js CHANGED
@@ -14,10 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.order = exports.ticket = exports.luhn36 = exports.b36 = exports.invalidPw = exports.getPublicIP = exports.isVideoUrl = exports.getRandomString = exports.extractImageSrcs = exports.select = exports.query = exports.getCloudinaryPublicId = exports.dp = exports.megaTrim = exports.superTrim = void 0;
17
+ exports.order = exports.crypto = exports.ticket = exports.luhn36 = exports.b36 = exports.invalidPw = exports.dedupeBy = exports.valueKey = exports.getPublicIP = exports.isVideoUrl = exports.getRandomString = exports.extractImageSrcs = exports.select = exports.query = exports.getCloudinaryPublicId = exports.dp = exports.megaTrim = exports.superTrim = void 0;
18
18
  __exportStar(require("./seat"), exports);
19
19
  const ticket_1 = require("./ticket");
20
20
  const order_1 = require("./order");
21
+ const crypto_1 = require("./crypto");
21
22
  /**
22
23
  * Clean up extra whitespace in a string.
23
24
  *
@@ -243,6 +244,76 @@ const getPublicIP = async () => {
243
244
  throw new Error('Public IP unavailable');
244
245
  };
245
246
  exports.getPublicIP = getPublicIP;
247
+ /**
248
+ * Convert an unknown value into a stable string key for deduping.
249
+ */
250
+ const valueKey = (v) => {
251
+ try {
252
+ if (v == null)
253
+ return '';
254
+ if (typeof v === 'string')
255
+ return v;
256
+ const key = String(v);
257
+ if (key === '[object Object]')
258
+ return '';
259
+ return key;
260
+ }
261
+ catch (e) {
262
+ console.error('valueKey(): failed to normalise value:', v, e);
263
+ return '';
264
+ }
265
+ };
266
+ exports.valueKey = valueKey;
267
+ /**
268
+ * Dedupe an array by a key (dot-path supported), preserving the item type.
269
+ *
270
+ * - keepFirst=true => first wins
271
+ * - keepFirst=false => last wins
272
+ * - keepNoKey=true => keep items where key is missing/invalid (kept in original order, before deduped items)
273
+ */
274
+ const dedupeBy = (arr = [], key, { keepFirst = true, keepNoKey = true } = {}) => {
275
+ try {
276
+ if (!Array.isArray(arr)) {
277
+ console.error('dedupeBy(): expected an array, got:', arr);
278
+ return [];
279
+ }
280
+ // Build a getter for a "dot.path" key like "user.id"
281
+ const getKey = (obj) => {
282
+ try {
283
+ return key.split('.').reduce((acc, k) => acc?.[k], obj);
284
+ }
285
+ catch {
286
+ return undefined;
287
+ }
288
+ };
289
+ // Map preserves insertion order, which gives stable output ordering for "first wins"
290
+ const map = new Map();
291
+ const noKey = [];
292
+ for (const item of arr) {
293
+ const raw = getKey(item);
294
+ const k = (0, exports.valueKey)(raw);
295
+ if (!k) {
296
+ if (keepNoKey)
297
+ noKey.push(item);
298
+ continue;
299
+ }
300
+ if (keepFirst) {
301
+ if (!map.has(k))
302
+ map.set(k, item);
303
+ }
304
+ else {
305
+ map.set(k, item); // last wins
306
+ }
307
+ }
308
+ const deduped = Array.from(map.values());
309
+ return keepNoKey ? noKey.concat(deduped) : deduped;
310
+ }
311
+ catch (e) {
312
+ console.error('dedupeBy(): failed:', e);
313
+ return Array.isArray(arr) ? arr : [];
314
+ }
315
+ };
316
+ exports.dedupeBy = dedupeBy;
246
317
  const invalidPw = (password, passwordLength = 8) => {
247
318
  try {
248
319
  if (password.length < passwordLength)
@@ -308,4 +379,8 @@ exports.ticket = {
308
379
  validateTicketCode: ticket_1.validateTicketCode,
309
380
  fishyMatchesAll: ticket_1.fishyMatchesAll,
310
381
  };
382
+ exports.crypto = {
383
+ encryptString: crypto_1.encryptString,
384
+ decryptString: crypto_1.decryptString,
385
+ };
311
386
  exports.order = { createNumericOrderId: order_1.createNumericOrderId };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@ibiliaze/stringman",
3
- "version": "3.17.0",
3
+ "version": "3.19.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
9
  "pub": "npm publish --access public",
10
- "git": "git add .; git commit -m 'changes'; git tag -a 3.17.0 -m '3.17.0'; git push origin 3.17.0; git push",
10
+ "git": "git add .; git commit -m 'changes'; git tag -a 3.19.0 -m '3.19.0'; git push origin 3.19.0; git push",
11
11
  "push": "npm run build; npm run git; npm run pub"
12
12
  },
13
13
  "author": "Ibi Hasanli",
package/src/crypto.ts ADDED
@@ -0,0 +1,69 @@
1
+ const enc = new TextEncoder();
2
+ const dec = new TextDecoder();
3
+
4
+ async function getKeyFromPassword(password: string, salt: ArrayBuffer): Promise<CryptoKey> {
5
+ const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
6
+
7
+ return crypto.subtle.deriveKey(
8
+ {
9
+ name: 'PBKDF2',
10
+ salt, // ArrayBuffer is valid BufferSource
11
+ iterations: 100_000,
12
+ hash: 'SHA-256',
13
+ },
14
+ keyMaterial,
15
+ {
16
+ name: 'AES-GCM',
17
+ length: 256,
18
+ },
19
+ false,
20
+ ['encrypt', 'decrypt']
21
+ );
22
+ }
23
+
24
+ export async function encryptString(plainText: string, password: string): Promise<string> {
25
+ const saltBytes = crypto.getRandomValues(new Uint8Array(16));
26
+ const iv = crypto.getRandomValues(new Uint8Array(12));
27
+
28
+ const key = await getKeyFromPassword(password, saltBytes.buffer);
29
+
30
+ const cipherBuffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plainText));
31
+
32
+ const cipherBytes = new Uint8Array(cipherBuffer);
33
+ const combined = new Uint8Array(saltBytes.length + iv.length + cipherBytes.length);
34
+ combined.set(saltBytes, 0);
35
+ combined.set(iv, saltBytes.length);
36
+ combined.set(cipherBytes, saltBytes.length + iv.length);
37
+
38
+ return bufferToBase64(combined.buffer);
39
+ }
40
+
41
+ export async function decryptString(cipherTextB64: string, password: string): Promise<string> {
42
+ const combined = new Uint8Array(base64ToBuffer(cipherTextB64));
43
+
44
+ const saltBytes = combined.slice(0, 16);
45
+ const iv = combined.slice(16, 28);
46
+ const cipherBytes = combined.slice(28);
47
+
48
+ // 👇 again, .buffer
49
+ const key = await getKeyFromPassword(password, saltBytes.buffer);
50
+
51
+ const plainBuffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, cipherBytes);
52
+
53
+ return dec.decode(plainBuffer);
54
+ }
55
+
56
+ /* base64 helpers stay the same */
57
+ function bufferToBase64(buf: ArrayBuffer): string {
58
+ const bytes = new Uint8Array(buf);
59
+ let binary = '';
60
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
61
+ return btoa(binary);
62
+ }
63
+
64
+ function base64ToBuffer(b64: string): ArrayBuffer {
65
+ const binary = atob(b64);
66
+ const bytes = new Uint8Array(binary.length);
67
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
68
+ return bytes.buffer;
69
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './seat';
2
2
  import { buildTicketCode, getTicketId, validateTicketCode, fishyMatchesAll } from './ticket';
3
3
  import { createNumericOrderId } from './order';
4
+ import { decryptString, encryptString } from './crypto';
4
5
 
5
6
  /**
6
7
  * Clean up extra whitespace in a string.
@@ -229,6 +230,79 @@ export const getPublicIP = async (): Promise<string> => {
229
230
  throw new Error('Public IP unavailable');
230
231
  };
231
232
 
233
+ /**
234
+ * Convert an unknown value into a stable string key for deduping.
235
+ */
236
+ export const valueKey = (v: unknown): string => {
237
+ try {
238
+ if (v == null) return '';
239
+ if (typeof v === 'string') return v;
240
+
241
+ const key = String(v);
242
+ if (key === '[object Object]') return '';
243
+
244
+ return key;
245
+ } catch (e) {
246
+ console.error('valueKey(): failed to normalise value:', v, e);
247
+ return '';
248
+ }
249
+ };
250
+
251
+ /**
252
+ * Dedupe an array by a key (dot-path supported), preserving the item type.
253
+ *
254
+ * - keepFirst=true => first wins
255
+ * - keepFirst=false => last wins
256
+ * - keepNoKey=true => keep items where key is missing/invalid (kept in original order, before deduped items)
257
+ */
258
+ export const dedupeBy = <T>(
259
+ arr: T[] = [],
260
+ key: string,
261
+ { keepFirst = true, keepNoKey = true }: DedupeByOptions = {}
262
+ ): T[] => {
263
+ try {
264
+ if (!Array.isArray(arr)) {
265
+ console.error('dedupeBy(): expected an array, got:', arr);
266
+ return [];
267
+ }
268
+
269
+ // Build a getter for a "dot.path" key like "user.id"
270
+ const getKey = (obj: any): unknown => {
271
+ try {
272
+ return key.split('.').reduce((acc, k) => acc?.[k], obj);
273
+ } catch {
274
+ return undefined;
275
+ }
276
+ };
277
+
278
+ // Map preserves insertion order, which gives stable output ordering for "first wins"
279
+ const map = new Map<string, T>();
280
+ const noKey: T[] = [];
281
+
282
+ for (const item of arr) {
283
+ const raw = getKey(item);
284
+ const k = valueKey(raw);
285
+
286
+ if (!k) {
287
+ if (keepNoKey) noKey.push(item);
288
+ continue;
289
+ }
290
+
291
+ if (keepFirst) {
292
+ if (!map.has(k)) map.set(k, item);
293
+ } else {
294
+ map.set(k, item); // last wins
295
+ }
296
+ }
297
+
298
+ const deduped = Array.from(map.values());
299
+ return keepNoKey ? noKey.concat(deduped) : deduped;
300
+ } catch (e) {
301
+ console.error('dedupeBy(): failed:', e);
302
+ return Array.isArray(arr) ? arr : [];
303
+ }
304
+ };
305
+
232
306
  export const invalidPw = (password: string, passwordLength: number = 8): string | void => {
233
307
  try {
234
308
  if (password.length < passwordLength) return `Password must be at least ${passwordLength} characters`;
@@ -285,4 +359,14 @@ export const ticket = {
285
359
  fishyMatchesAll,
286
360
  };
287
361
 
362
+ export const crypto = {
363
+ encryptString,
364
+ decryptString,
365
+ };
366
+
288
367
  export const order = { createNumericOrderId };
368
+
369
+ type DedupeByOptions = {
370
+ keepFirst?: boolean;
371
+ keepNoKey?: boolean;
372
+ };