@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.
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.js +55 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +76 -1
- package/package.json +2 -2
- package/src/crypto.ts +69 -0
- package/src/index.ts +84 -0
package/dist/crypto.d.ts
ADDED
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.
|
|
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.
|
|
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
|
+
};
|