Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions jest.config.js

This file was deleted.

22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "@internxt/lib",
"version": "1.3.1",
"version": "1.4.0",
"description": "Common logic shared between different projects of Internxt ",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "jest",
"test:cov": "jest --coverage",
"test": "vitest run",
"test:cov": "vitest run --coverage",
"build": "tsc",
"lint": "eslint src/**/*.ts",
"format": "prettier src/**/*.ts",
Expand All @@ -26,15 +26,17 @@
},
"homepage": "https://github.com/internxt/lib#readme",
"devDependencies": {
"@internxt/eslint-config-internxt": "2.0.0",
"@internxt/eslint-config-internxt": "2.0.1",
"@internxt/prettier-config": "internxt/prettier-config#v1.0.2",
"@types/jest": "30.0.0",
"@types/node": "24.0.10",
"eslint": "9.30.1",
"@types/node": "24.9.2",
"@vitest/coverage-istanbul": "4.0.5",
"eslint": "9.38.0",
"husky": "9.1.7",
"jest": "30.0.4",
"prettier": "3.6.2",
"ts-jest": "29.4.0",
"typescript": "5.8.3"
"typescript": "5.9.3",
"vitest": "4.0.5"
},
"dependencies": {
"uuid": "^13.0.0"
}
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import items from './items';
import auth from './auth';
import aes from './aes';
import request from './request';
import stringUtils from './utils/stringUtils';

export { items, auth, aes, request };
export { items, auth, aes, request, stringUtils };
120 changes: 120 additions & 0 deletions src/utils/stringUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { randomBytes } from 'crypto';
import { Buffer } from 'buffer';
import { validate as validateUuidv4 } from 'uuid';

/**
* Converts a standard Base64 string into a URL-safe Base64 variant:
* - Replaces all "+" characters with "-"
* - Replaces all "/" characters with "_"
* - Strips any trailing "=" padding characters
*
* @param base64 - A standard Base64–encoded string
* @returns The URL-safe Base64 string
*/
const toBase64UrlSafe = (base64: string): string => {
return base64
.replace(/\+/g, '-') // converts "+" to "-"
.replace(/\//g, '_') // converts "/" to "_"
.replace(/=+$/, ''); // removes trailing "="
};

/**
* Converts a URL-safe Base64 string back into a standard Base64 variant:
* - Replaces all "-" characters with "+"
* - Replaces all "_" characters with "/"
* - Adds "=" padding characters at the end until length is a multiple of 4
*
* @param urlSafe - A URL-safe Base64–encoded string
* @returns The standard Base64 string, including any necessary "=" padding
*/
const fromBase64UrlSafe = (urlSafe: string): string => {
let base64 = urlSafe
.replace(/-/g, '+') // convert "-" back to "+"
.replace(/_/g, '/'); // convert "_" back to "/"

const missingPadding = (4 - (base64.length % 4)) % 4;
if (missingPadding > 0) {
base64 += '='.repeat(missingPadding);
}
return base64;
};

/**
* Generates a cryptographically secure, URL-safe string of a given length.
*
* Internally:
* 1. Calculates how many raw bytes are needed to generate at least `size` Base64 chars.
* 2. Generates secure random bytes with `crypto.randomBytes()`.
* 3. Encodes to standard Base64, then makes it URL-safe.
* 4. Truncates the result to `size` characters.
*
* @param size - Desired length of the output string (must be ≥1)
* @returns A URL-safe string exactly `size` characters long
*/
const generateRandomStringUrlSafe = (size: number): string => {
if (size <= 0) {
throw new Error('Size must be a positive integer');
}
// Base64 yields 4 chars per 3 bytes, so it computes the minimum bytes required
const numBytes = Math.ceil((size * 3) / 4);
const buf = randomBytes(numBytes).toString('base64');

return toBase64UrlSafe(buf).substring(0, size);
};

/**
* Converts a base64 url safe string to uuid v4
*
* @example in: `8yqR2seZThOqF4xNngMjyQ` out: `f32a91da-c799-4e13-aa17-8c4d9e0323c9`
*/
function base64UrlSafetoUUID(base64UrlSafe: string): string {
const hex = Buffer.from(fromBase64UrlSafe(base64UrlSafe), 'base64').toString('hex');

return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}` +
`-${hex.substring(16, 20)}-${hex.substring(20)}`;
}

/**
* Extracts a UUIDv4 from the provided parameter. If the value is not a valid UUIDv4, it tries to convert
* the parameter from a Base64 URL-safe string to a UUID. In either case, it returns the provided value.
*
* @value value - The input parameter, which can be either a UUIDv4 or a Base64 URL-safe string.
* @returns The decoded v4 UUID string.
*/
const decodeV4Uuid = (value: string) => {
if (!validateUuidv4(value)) {
try {
return base64UrlSafetoUUID(value);
} catch {
return value;
}
}
return value;
};

/**
* Encodes a UUIDv4 by removing hyphens, converting it from hexadecimal to Base64,
* and then making it URL-safe.
*
* @param v4Uuid - The ID to be encoded, expected to be a UUIDv4 string.
* @returns The encoded send ID as a URL-safe Base64 string.
* @throws {Error} If the provided UUIDv4 is not valid.
*/
const encodeV4Uuid = (v4Uuid: string) => {
if (!validateUuidv4(v4Uuid)) {
throw new Error('The provided UUIDv4 is not valid');
}
const removedUuidDecoration = v4Uuid.replace(/-/g, '');
const base64endoded = Buffer.from(removedUuidDecoration, 'hex').toString('base64');
const encodedSendId = toBase64UrlSafe(base64endoded);
return encodedSendId;
};

export default {
toBase64UrlSafe,
fromBase64UrlSafe,
generateRandomStringUrlSafe,
base64UrlSafetoUUID,
decodeV4Uuid,
encodeV4Uuid,
};
1 change: 1 addition & 0 deletions test/aes/encrypt-decrypt.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { aes } from '../../src';

describe('AES encrypt/decrypt tests', () => {
Expand Down
1 change: 1 addition & 0 deletions test/auth/isValidEmail.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { auth } from '../../src';

const { isValidEmail } = auth;
Expand Down
1 change: 1 addition & 0 deletions test/auth/isValidPassword.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { auth } from '../../src';

const { isValidPassword } = auth;
Expand Down
1 change: 1 addition & 0 deletions test/auth/testPasswordStrength.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { auth } from '../../src';

const { testPasswordStrength } = auth;
Expand Down
1 change: 1 addition & 0 deletions test/items/getFilenameAndExt.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { items } from '../../src';

const { getFilenameAndExt } = items;
Expand Down
1 change: 1 addition & 0 deletions test/items/getItemDisplayName.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { items } from '../../src';

const { getItemDisplayName } = items;
Expand Down
1 change: 1 addition & 0 deletions test/items/isHiddenItem.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { items } from '../../src';

const { isHiddenItem } = items;
Expand Down
1 change: 1 addition & 0 deletions test/items/isInvalidFilename.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { items } from '../../src';

const { isInvalidFilename } = items;
Expand Down
1 change: 1 addition & 0 deletions test/items/renameIfNeeded.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { items } from '../../src';

const { renameIfNeeded } = items;
Expand Down
72 changes: 72 additions & 0 deletions test/utils/stringUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest';
import { stringUtils } from '../../src';

const { base64UrlSafetoUUID, fromBase64UrlSafe, generateRandomStringUrlSafe, toBase64UrlSafe } = stringUtils;

describe('stringUtils', () => {
describe('toBase64UrlSafe', () => {
it('converts standard Base64 to URL-safe Base64', () => {
const base64 = 'KHh+VVwlYjs/J3E=';
const urlSafe = toBase64UrlSafe(base64);
expect(urlSafe).toBe('KHh-VVwlYjs_J3E');
});

it('removes trailing "=" characters', () => {
const base64 = 'KHh+VVwlYjs/J3FiYw==';
const urlSafe = toBase64UrlSafe(base64);
expect(urlSafe).toBe('KHh-VVwlYjs_J3FiYw');
});

it('does not modify already URL-safe Base64 strings', () => {
const base64 = 'KHh-VVwlYjs_J3E';
const urlSafe = toBase64UrlSafe(base64);
expect(urlSafe).toBe(base64);
});
});

describe('fromBase64UrlSafe', () => {
it('converts URL-safe Base64 back to standard Base64', () => {
const urlSafe = 'KHh-VVwlYjs_J3E';
const base64 = fromBase64UrlSafe(urlSafe);
expect(base64).toBe('KHh+VVwlYjs/J3E=');
});

it('does not modify already standard Base64 strings', () => {
const urlSafe = 'KHh+VVwlYjs/J3FiYw==';
const base64 = fromBase64UrlSafe(urlSafe);
expect(base64).toBe(urlSafe);
});
});

describe('generateRandomStringUrlSafe', () => {
const getRandomNumber = (min: number, max: number) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};

it('generates a string of the specified length', () => {
const length = getRandomNumber(1, 1000);
const randomString = generateRandomStringUrlSafe(length);
expect(randomString).toHaveLength(length);
});

it('generates a URL-safe string', () => {
const randomString = generateRandomStringUrlSafe(1000);
expect(randomString).not.toMatch(/[+/=]/);
});

it('throws an error if size is not positive', () => {
expect(() => generateRandomStringUrlSafe(0)).toThrow('Size must be a positive integer');
expect(() => generateRandomStringUrlSafe(-5)).toThrow('Size must be a positive integer');
});
});

describe('base64UrlSafetoUUID', () => {
it('converts a Base64 URL-safe string to a UUID v4 format', () => {
const base64UrlSafe = '8yqR2seZThOqF4xNngMjyQ';
const uuid = base64UrlSafetoUUID(base64UrlSafe);
expect(uuid).toBe('f32a91da-c799-4e13-aa17-8c4d9e0323c9');
});
});
});
22 changes: 22 additions & 0 deletions vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { coverageConfigDefaults, defineConfig } from 'vitest/config';

export default defineConfig({
test: {
exclude: ['node_modules', 'dist'],
include: [
'src/**/*.test.{ts,tsx,js,jsx}',
'test/**/*.test.{ts,tsx,js,jsx}',
],
coverage: {
provider: 'istanbul',
reporter: ['text', 'lcov'],
include: [
'src/**/*.{js,ts,jsx,tsx}',
'test/**/*.{js,ts,jsx,tsx}'
],
exclude: [
...coverageConfigDefaults.exclude
]
},
},
});
Loading