Skip to content

Commit aaca084

Browse files
authored
Merge pull request #16 from internxt/feat/add-string-utils
[_]: feat/add-string-utils
2 parents 0d57af7 + a1c7e1b commit aaca084

16 files changed

+1058
-2113
lines changed

jest.config.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

package.json

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
22
"name": "@internxt/lib",
3-
"version": "1.3.1",
3+
"version": "1.4.0",
44
"description": "Common logic shared between different projects of Internxt ",
55
"main": "dist/src/index.js",
66
"types": "dist/src/index.d.ts",
77
"files": [
88
"dist"
99
],
1010
"scripts": {
11-
"test": "jest",
12-
"test:cov": "jest --coverage",
11+
"test": "vitest run",
12+
"test:cov": "vitest run --coverage",
1313
"build": "tsc",
1414
"lint": "eslint src/**/*.ts",
1515
"format": "prettier src/**/*.ts",
@@ -26,15 +26,17 @@
2626
},
2727
"homepage": "https://github.com/internxt/lib#readme",
2828
"devDependencies": {
29-
"@internxt/eslint-config-internxt": "2.0.0",
29+
"@internxt/eslint-config-internxt": "2.0.1",
3030
"@internxt/prettier-config": "internxt/prettier-config#v1.0.2",
31-
"@types/jest": "30.0.0",
32-
"@types/node": "24.0.10",
33-
"eslint": "9.30.1",
31+
"@types/node": "24.9.2",
32+
"@vitest/coverage-istanbul": "4.0.5",
33+
"eslint": "9.38.0",
3434
"husky": "9.1.7",
35-
"jest": "30.0.4",
3635
"prettier": "3.6.2",
37-
"ts-jest": "29.4.0",
38-
"typescript": "5.8.3"
36+
"typescript": "5.9.3",
37+
"vitest": "4.0.5"
38+
},
39+
"dependencies": {
40+
"uuid": "^13.0.0"
3941
}
4042
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import items from './items';
22
import auth from './auth';
33
import aes from './aes';
44
import request from './request';
5+
import stringUtils from './utils/stringUtils';
56

6-
export { items, auth, aes, request };
7+
export { items, auth, aes, request, stringUtils };

src/utils/stringUtils.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { randomBytes } from 'crypto';
2+
import { Buffer } from 'buffer';
3+
import { validate as validateUuidv4 } from 'uuid';
4+
5+
/**
6+
* Converts a standard Base64 string into a URL-safe Base64 variant:
7+
* - Replaces all "+" characters with "-"
8+
* - Replaces all "/" characters with "_"
9+
* - Strips any trailing "=" padding characters
10+
*
11+
* @param base64 - A standard Base64–encoded string
12+
* @returns The URL-safe Base64 string
13+
*/
14+
const toBase64UrlSafe = (base64: string): string => {
15+
return base64
16+
.replace(/\+/g, '-') // converts "+" to "-"
17+
.replace(/\//g, '_') // converts "/" to "_"
18+
.replace(/=+$/, ''); // removes trailing "="
19+
};
20+
21+
/**
22+
* Converts a URL-safe Base64 string back into a standard Base64 variant:
23+
* - Replaces all "-" characters with "+"
24+
* - Replaces all "_" characters with "/"
25+
* - Adds "=" padding characters at the end until length is a multiple of 4
26+
*
27+
* @param urlSafe - A URL-safe Base64–encoded string
28+
* @returns The standard Base64 string, including any necessary "=" padding
29+
*/
30+
const fromBase64UrlSafe = (urlSafe: string): string => {
31+
let base64 = urlSafe
32+
.replace(/-/g, '+') // convert "-" back to "+"
33+
.replace(/_/g, '/'); // convert "_" back to "/"
34+
35+
const missingPadding = (4 - (base64.length % 4)) % 4;
36+
if (missingPadding > 0) {
37+
base64 += '='.repeat(missingPadding);
38+
}
39+
return base64;
40+
};
41+
42+
/**
43+
* Generates a cryptographically secure, URL-safe string of a given length.
44+
*
45+
* Internally:
46+
* 1. Calculates how many raw bytes are needed to generate at least `size` Base64 chars.
47+
* 2. Generates secure random bytes with `crypto.randomBytes()`.
48+
* 3. Encodes to standard Base64, then makes it URL-safe.
49+
* 4. Truncates the result to `size` characters.
50+
*
51+
* @param size - Desired length of the output string (must be ≥1)
52+
* @returns A URL-safe string exactly `size` characters long
53+
*/
54+
const generateRandomStringUrlSafe = (size: number): string => {
55+
if (size <= 0) {
56+
throw new Error('Size must be a positive integer');
57+
}
58+
// Base64 yields 4 chars per 3 bytes, so it computes the minimum bytes required
59+
const numBytes = Math.ceil((size * 3) / 4);
60+
const buf = randomBytes(numBytes).toString('base64');
61+
62+
return toBase64UrlSafe(buf).substring(0, size);
63+
};
64+
65+
/**
66+
* Converts a base64 url safe string to uuid v4
67+
*
68+
* @example in: `8yqR2seZThOqF4xNngMjyQ` out: `f32a91da-c799-4e13-aa17-8c4d9e0323c9`
69+
*/
70+
function base64UrlSafetoUUID(base64UrlSafe: string): string {
71+
const hex = Buffer.from(fromBase64UrlSafe(base64UrlSafe), 'base64').toString('hex');
72+
73+
return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}` +
74+
`-${hex.substring(16, 20)}-${hex.substring(20)}`;
75+
}
76+
77+
/**
78+
* Extracts a UUIDv4 from the provided parameter. If the value is not a valid UUIDv4, it tries to convert
79+
* the parameter from a Base64 URL-safe string to a UUID. In either case, it returns the provided value.
80+
*
81+
* @value value - The input parameter, which can be either a UUIDv4 or a Base64 URL-safe string.
82+
* @returns The decoded v4 UUID string.
83+
*/
84+
const decodeV4Uuid = (value: string) => {
85+
if (!validateUuidv4(value)) {
86+
try {
87+
return base64UrlSafetoUUID(value);
88+
} catch {
89+
return value;
90+
}
91+
}
92+
return value;
93+
};
94+
95+
/**
96+
* Encodes a UUIDv4 by removing hyphens, converting it from hexadecimal to Base64,
97+
* and then making it URL-safe.
98+
*
99+
* @param v4Uuid - The ID to be encoded, expected to be a UUIDv4 string.
100+
* @returns The encoded send ID as a URL-safe Base64 string.
101+
* @throws {Error} If the provided UUIDv4 is not valid.
102+
*/
103+
const encodeV4Uuid = (v4Uuid: string) => {
104+
if (!validateUuidv4(v4Uuid)) {
105+
throw new Error('The provided UUIDv4 is not valid');
106+
}
107+
const removedUuidDecoration = v4Uuid.replace(/-/g, '');
108+
const base64endoded = Buffer.from(removedUuidDecoration, 'hex').toString('base64');
109+
const encodedSendId = toBase64UrlSafe(base64endoded);
110+
return encodedSendId;
111+
};
112+
113+
export default {
114+
toBase64UrlSafe,
115+
fromBase64UrlSafe,
116+
generateRandomStringUrlSafe,
117+
base64UrlSafetoUUID,
118+
decodeV4Uuid,
119+
encodeV4Uuid,
120+
};

test/aes/encrypt-decrypt.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, expect, it } from 'vitest';
12
import { aes } from '../../src';
23

34
describe('AES encrypt/decrypt tests', () => {

test/auth/isValidEmail.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, expect, it } from 'vitest';
12
import { auth } from '../../src';
23

34
const { isValidEmail } = auth;

test/auth/isValidPassword.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, expect, it } from 'vitest';
12
import { auth } from '../../src';
23

34
const { isValidPassword } = auth;

test/auth/testPasswordStrength.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, expect, it } from 'vitest';
12
import { auth } from '../../src';
23

34
const { testPasswordStrength } = auth;

test/items/getFilenameAndExt.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, expect, it } from 'vitest';
12
import { items } from '../../src';
23

34
const { getFilenameAndExt } = items;

test/items/getItemDisplayName.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, expect, it } from 'vitest';
12
import { items } from '../../src';
23

34
const { getItemDisplayName } = items;

0 commit comments

Comments
 (0)