diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/packages/auto-id/img/subspace-auto-id-ts-arch.png b/packages/auto-id/img/subspace-auto-id-ts-arch.png new file mode 100644 index 00000000..7b67a55a Binary files /dev/null and b/packages/auto-id/img/subspace-auto-id-ts-arch.png differ diff --git a/packages/auto-id/jest.config.ts b/packages/auto-id/jest.config.ts new file mode 100644 index 00000000..650c513d --- /dev/null +++ b/packages/auto-id/jest.config.ts @@ -0,0 +1,3 @@ +module.exports = { + preset: 'ts-jest', +} diff --git a/packages/auto-id/package.json b/packages/auto-id/package.json index e5919adb..558810bc 100644 --- a/packages/auto-id/package.json +++ b/packages/auto-id/package.json @@ -3,13 +3,24 @@ "version": "0.1.0", "main": "dist/index.js", "scripts": { - "build": "tsc" + "build": "tsc", + "clean": "rm -rf dist", + "format": "prettier --write \"src/**/*.ts\"", + "test": "jest" + }, + "dependencies": { + "@autonomys/auto-utils": "workspace:*", + "@types/node": "^20.12.12" }, "files": [ "dist", "README.md" ], "devDependencies": { + "@types/jest": "^29.5.12", + "jest": "^29.7.0", + "ts-jest": "^29.1.4", + "ts-node": "^10.9.2", "typescript": "^5.4.5" } } diff --git a/packages/auto-id/src/index.ts b/packages/auto-id/src/index.ts index 05d50938..310afd44 100644 --- a/packages/auto-id/src/index.ts +++ b/packages/auto-id/src/index.ts @@ -1,3 +1 @@ -export const helloWorld = () => { - console.log('Hello World'); -}; +export * from './keyManagement' diff --git a/packages/auto-id/src/keyManagement.ts b/packages/auto-id/src/keyManagement.ts new file mode 100644 index 00000000..391f2618 --- /dev/null +++ b/packages/auto-id/src/keyManagement.ts @@ -0,0 +1,289 @@ +import { read, save } from '@autonomys/auto-utils' +import { KeyObject, createPrivateKey, createPublicKey, generateKeyPairSync } from 'crypto' + +/** + * Generates an RSA key pair. + * @param keySize The size of the key in bits. Default is 2048. + * @returns A tuple containing the the RSA private key and public key. + */ +export function generateRsaKeyPair(keySize: number = 2048): [string, string] { + const { publicKey, privateKey } = generateKeyPairSync('rsa', { + modulusLength: keySize, + publicExponent: 65537, + // TODO: Need to select the correct type - `"pkcs1" | "spki"` + publicKeyEncoding: { type: 'spki', format: 'pem' }, + // TODO: Need to select the correct type - `"pkcs1" | "pkcs8"` + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }) + + return [privateKey, publicKey] +} + +/** + * Generates an Ed25519 key pair. + * @returns A tuple containing the Ed25519 private key and public key. + */ +export function generateEd25519KeyPair(): [string, string] { + const { publicKey, privateKey } = generateKeyPairSync('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }) + + return [privateKey, publicKey] +} + +/** + * Converts a cryptographic key object into a PEM formatted string. + * This function can handle both private and public key objects. + * For private keys, it supports optional encryption using a passphrase. + * + * @param key The cryptographic key object to be exported. It must be either a private or public key object. + * @param password Optional passphrase for encrypting the private key. If provided, the private key + * will be exported in an encrypted form using AES-256-CBC cipher. This parameter is ignored + * for public keys. + * + * @returns Returns the PEM formatted string of the key. If a private key is provided with a passphrase, + * it returns the key in an encrypted format. Otherwise, it returns an unencrypted PEM. + * + * @throws Will throw an error if the provided `key` is neither a private nor a public key object. + * + * @example + * Follow "../examples/eg3.ts" & "../examples/eg4.ts" for a complete example. + * // Create a private key object (assuming you have the appropriate private key data) + * const privateKey = createPrivateKey({ + * key: '-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANB ...', + * format: 'pem' + * }); + * + * // Create a public key object from the private key + * const publicKey = createPublicKey(privateKey); + * + * // Export the private key without encryption + * console.log(keyToPem(privateKey)); + * + * // Export the private key with encryption + * console.log(keyToPem(privateKey, 'your-secure-password')); + * + * // Export the public key + * console.log(keyToPem(publicKey)); + */ +export function keyToPem(key: KeyObject, password?: string): string { + if (key.asymmetricKeyType) { + // Handle private key + if (key.type === 'private') { + const options: any = { + type: 'pkcs8' as 'pkcs8', // type for private keys + format: 'pem' as 'pem', // Output format set to 'pem' + } + // If a password is provided, apply encryption + if (password) { + options.cipher = 'aes-256-cbc' // Cipher type + options.passphrase = password // Passphrase as a string + } + return key.export(options) as string + } + // Handle public key + else if (key.type === 'public') { + const options = { + type: 'spki' as 'spki', // type for public keys + format: 'pem' as 'pem', // Output format set to 'pem' + } + return key.export(options) as string + } + } + throw new Error('Invalid key type. Key must be a private or public key object.') +} + +/** + * Saves a cryptographic key object to a file in PEM format. If it's a private key and a password is provided, + * the key will be encrypted before being written to the file. + * + * @param key The cryptographic key object to be saved. It must be either a private or public key object. + * @param filePath The file system path where the key should be saved. + * @param password Optional password for encrypting the private key. + * + * @example + * // Assuming privateKey is a valid KeyObject + * saveKey(privateKey, './myPrivateKey.pem', 'strongpassword') + * .then(() => console.log('Key saved successfully')) + * .catch(err => console.error('Error saving key:', err)); + */ +export async function saveKey(key: KeyObject, filePath: string, password?: string): Promise { + try { + const pem = keyToPem(key, password) + await save(filePath, pem) + } catch (e: any) { + throw new Error(`Failed to save key: ${e.message}`) + } +} + +/** + * Converts a PEM-encoded string to a private key object. If the PEM string is encrypted, + * a password must be provided to decrypt it. + * + * @param pemData The PEM string to convert to a private key. + * @param password Optional password used to decrypt the encrypted PEM string. + * @returns The private key object. + * + * @example + * const pemString = '-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFDjBABgkqhk ...'; + * const privateKey = pemToPrivateKey(pemString, 'mypassword'); + * console.log(privateKey); + */ +export function pemToPrivateKey(pemData: string, password?: string): KeyObject { + const options: any = { + key: pemData, + format: 'pem' as 'pem', + } + + // Add password to options if it is provided + if (password) { + options.passphrase = password + } + + return createPrivateKey(options) +} + +/** + * Loads a private key from a file. If the file is encrypted, a password must be provided. + * + * @param filePath Path to the private key file. + * @param password Optional password used to decrypt the encrypted key file. + * @returns The private key object. + * + * @example + * async function main() { + * try { + * const privateKey = await loadPrivateKey('./path/to/private/key.pem', 'optional-password'); + * console.log('Private Key:', privateKey); + * } catch (error) { + * console.error('Error loading private key:', error); + * } + * } + * + * main(); + */ +export async function loadPrivateKey(filePath: string, password?: string): Promise { + try { + const keyData = await read(filePath) + const privateKey = pemToPrivateKey(keyData, password) + return privateKey; + } catch (error: any) { + throw new Error(`Failed to load private key: ${error.message}`) + } +} + +/** + * Converts a PEM-encoded string to a public key object. + * + * @param pemData The PEM string to convert to a public key. + * @returns The public key object. + * + * @example + * const pemString = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...'; + * const publicKey = pemToPublicKey(pemString); + * console.log('Public Key:', publicKey); + */ +export function pemToPublicKey(pemData: string): KeyObject { + return createPublicKey({ + key: pemData, + format: 'pem' as 'pem', + }) +} + +/** + * Loads a public key from a file. + * + * @param filePath Path to the public key file. + * @returns The public key object. + * + * @example + * async function main() { + * try { + * const publicKey = await loadPublicKey('./path/to/public/key.pem'); + * console.log('Public Key:', publicKey); + * } catch (error) { + * console.error('Error loading public key:', error); + * } + * } + * + * main(); + */ +export async function loadPublicKey(filePath: string): Promise { + try { + const keyData = await read(filePath) + const publicKey = pemToPublicKey(keyData) + return publicKey + } catch (error: any) { + throw new Error(`Failed to load public key: ${error.message}`) + } +} + +/** + * Converts a private or public key to a hex string representation. + * + * @param key The key to convert (either a private or public key). + * @returns The hex string representation of the key. + * + * @example + * const keyHex = keyToHex(privateKeyObject); // privateKeyObject should be a valid KeyObject + * console.log('Key Hex:', keyHex); + */ +export function keyToHex(key: KeyObject): string { + let keyData: Buffer + + // Check the type of the key to determine how to handle it + if (key.type === 'private') { + // Convert private key to DER format + keyData = key.export({ + type: 'pkcs8', + format: 'der', + }) + } else if (key.type === 'public') { + // Convert public key to DER format + keyData = key.export({ + type: 'spki', + format: 'der', + }) + } else { + throw new Error('Unsupported key type') + } + + // Convert the binary data to a hexadecimal string + return keyData.toString('hex') +} + +/** + * Checks if two public keys match. + * + * @param publicKey1 The first public key as a KeyObject. + * @param publicKey2 The second public key as a KeyObject. + * @returns True if the keys match, False otherwise. + * + * @example + * const key1 = createPublicKey({ + * key: publicKeyPem1, + * format: 'pem' + * }); + * const key2 = createPublicKey({ + * key: publicKeyPem2, + * format: 'pem' + * }); + * const match = doPublicKeysMatch(key1, key2); + * console.log('Keys match:', match); + */ +export function doPublicKeysMatch(publicKey1: KeyObject, publicKey2: KeyObject): boolean { + // Serialize both public keys to DER format for comparison + const publicKey1Der = publicKey1.export({ + type: 'spki', + format: 'der', + }) + + const publicKey2Der = publicKey2.export({ + type: 'spki', + format: 'der', + }) + + // Compare the serialized public key data + return publicKey1Der.equals(publicKey2Der) +} \ No newline at end of file diff --git a/packages/auto-id/tests/keyManagement.test.ts b/packages/auto-id/tests/keyManagement.test.ts new file mode 100644 index 00000000..c343c0c3 --- /dev/null +++ b/packages/auto-id/tests/keyManagement.test.ts @@ -0,0 +1,338 @@ +import { expect, test } from '@jest/globals' +import { KeyObject, createPrivateKey, createPublicKey } from 'crypto' +import { promises as fs } from 'fs' +import * as path from 'path' +import { + doPublicKeysMatch, + generateEd25519KeyPair, + generateRsaKeyPair, + keyToHex, + keyToPem, + loadPrivateKey, + loadPublicKey, + pemToPrivateKey, + pemToPublicKey, + saveKey, +} from '../src/keyManagement' + +describe('Generate keypair for', () => { + test('RSA', () => { + const [privateKey, publicKey] = generateRsaKeyPair() + expect(privateKey).toStrictEqual(expect.any(String)) + expect(publicKey).toStrictEqual(expect.any(String)) + }) + + test('Ed25519', () => { + const [privateKey, publicKey] = generateEd25519KeyPair() + expect(privateKey).toStrictEqual(expect.any(String)) + expect(publicKey).toStrictEqual(expect.any(String)) + }) +}) + +describe('Private/Public key to PEM with/without password for', () => { + let privateKey: string, publicKey: string + + test('RSA', () => { + ;[privateKey, publicKey] = generateRsaKeyPair() + }) + + test('Ed25519', () => { + ;[privateKey, publicKey] = generateEd25519KeyPair() + }) + + afterEach(() => { + const privateKeyObject = createPrivateKey({ + key: privateKey, + format: 'pem', // Input can still be PEM + }) + const publicKeyObject = createPublicKey(privateKeyObject) + + expect(keyToPem(privateKeyObject)).toStrictEqual(privateKey) + expect(keyToPem(privateKeyObject, 'subspace')).not.toEqual(privateKey) // unequal because of password encryption + expect(keyToPem(publicKeyObject)).toStrictEqual(publicKey) + }) +}) + +describe('PEM to Private/Public key for', () => { + const keyGenerators = [ + { name: 'RSA', generator: generateRsaKeyPair }, + { name: 'Ed25519', generator: generateEd25519KeyPair }, + ] + + for (const { name, generator } of keyGenerators) { + describe(`${name}`, () => { + let privateKeyObject: KeyObject, publicKeyObject: KeyObject + let originalPemPrivKey: string, originalPemPubKey: string + + beforeEach(() => { + const [privateKey, publicKey] = generator() + + privateKeyObject = createPrivateKey({ + key: privateKey, + format: 'pem', // Input format is PEM + }) + + publicKeyObject = createPublicKey(privateKeyObject) + + // Export original private/public keys back to PEM for comparison + originalPemPrivKey = privateKeyObject.export({ type: 'pkcs8', format: 'pem' }) as string + originalPemPubKey = publicKeyObject.export({ type: 'spki', format: 'pem' }) as string + }) + + test('without any password', () => { + // Convert the privateKeyObject back to PEM ensure consistent serialization + // And then convert it back to a private key object + const privateKeyFromPem = pemToPrivateKey(keyToPem(privateKeyObject)) + + // Export derived key back to PEM for comparison + const derivedPemPrivKey = privateKeyFromPem.export({ + type: 'pkcs8', + format: 'pem', + }) as string + + expect(derivedPemPrivKey).toStrictEqual(originalPemPrivKey) + + // Convert the publicKeyObject back to PEM ensure consistent serialization + // And then convert it back to a public key object + const publicKeyFromPem = pemToPublicKey(keyToPem(publicKeyObject)) + + // Export derived key back to PEM for comparison + const derivedPemPubKey = publicKeyFromPem.export({ + type: 'spki', + format: 'pem', + }) as string + + expect(derivedPemPubKey).toStrictEqual(originalPemPubKey) + }) + + test('with password in 1/2 function', () => { + // Convert the privateKeyObject back to PEM ensure consistent serialization + // And then convert it back to a private key object with password + const privateKeyFromPemPassword = pemToPrivateKey(keyToPem(privateKeyObject), 'subspace') + + // Export both original and derived keys back to PEM and compare those + const derivedPemPasswordPrivKey = privateKeyFromPemPassword.export({ + type: 'pkcs8', + format: 'pem', + }) as string + + expect(derivedPemPasswordPrivKey).toStrictEqual(originalPemPrivKey) + }) + + test('with password in 2/2 functions', () => { + // Convert the privateKeyObject back to PEM with password ensure consistent serialization + // And then convert it back to a private key object with password + const privateKeyFromPemPassword = pemToPrivateKey( + keyToPem(privateKeyObject, 'subspace'), + 'subspace', + ) + + // Export both original and derived keys back to PEM and compare those + const derivedPemPassword = privateKeyFromPemPassword.export({ + type: 'pkcs8', + format: 'pem', + }) as string + + expect(derivedPemPassword).toStrictEqual(originalPemPrivKey) + }) + }) + } +}) + +describe('Save Key', () => { + const keyGenerators = [ + { name: 'RSA', generator: generateRsaKeyPair }, + { name: 'Ed25519', generator: generateEd25519KeyPair }, + ] + + for (const { name, generator } of keyGenerators) { + describe(`${name}`, () => { + // Directory for test output + const testDir = path.join(__dirname, 'test_keys') + let privateKey: string + + // Create a directory for test outputs before any tests run + beforeAll(async () => { + await fs.mkdir(testDir, { recursive: true }) + }) + + beforeEach(() => { + ;[privateKey] = generator() + }) + + // Cleanup: remove test directory after all tests + afterAll(async () => { + await fs.rm(testDir, { recursive: true, force: true }) + }) + + test('should save a private key to a file', async () => { + const filePath = path.join(testDir, 'testPrivateKey.pem') + + const privateKeyObject = pemToPrivateKey(privateKey) + + await saveKey(privateKeyObject, filePath) + const fileContents = await fs.readFile(filePath, { encoding: 'utf8' }) + + // Check if the PEM string matches expected, considering JSON.stringify use + expect(fileContents).toBe(JSON.stringify(keyToPem(privateKeyObject))) + }) + + test('should save an encrypted private key to a file', async () => { + const filePath = path.join(testDir, 'testEncryptedPrivateKey.pem') + const password = 'testpassword' + + await saveKey(pemToPrivateKey(privateKey), filePath, password) + const fileContents = await fs.readFile(filePath, { encoding: 'utf8' }) + + // Parse it back to normal string + const actualPemContent = JSON.parse(fileContents) + + // Check if the file content starts and ends with the expected encrypted private key headers + expect(actualPemContent.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')).toBe(true) + expect(actualPemContent.endsWith('-----END ENCRYPTED PRIVATE KEY-----\n')).toBe(true) + }) + test('should throw an error when trying to save to an invalid path', async () => { + const filePath = path.join(testDir, 'non_existent_directory', 'testPrivateKey.pem') + + await expect(saveKey(pemToPrivateKey(privateKey), filePath)).rejects.toThrow() + }) + }) + } +}) + +describe('Load Key', () => { + const keyGenerators = [ + { name: 'RSA', generator: generateRsaKeyPair }, + { name: 'Ed25519', generator: generateEd25519KeyPair }, + ] + + for (const { name, generator } of keyGenerators) { + describe(`${name}`, () => { + const testDir = path.join(__dirname, 'test_keys') + + // Directory setup for test keys + beforeAll(async () => { + await fs.mkdir(testDir, { recursive: true }) + }) + + afterAll(async () => { + await fs.rm(testDir, { recursive: true, force: true }) + }) + + // Load Private Key Tests + describe('loadPrivateKey', () => { + const [privateKey] = generator() + const filePath = path.join(testDir, 'testPrivateKey.pem') + const password = 'testpassword' + + beforeAll(async () => { + // Saving a regular and an encrypted private key for tests + await saveKey(pemToPrivateKey(privateKey), filePath) + await saveKey(pemToPrivateKey(privateKey), `${filePath}.enc`, password) + }) + + test('should load a private key from a file', async () => { + const loadedPrivateKey = await loadPrivateKey(filePath) + expect(loadedPrivateKey.export({ type: 'pkcs8', format: 'pem' })).toBe(privateKey) + }) + + test('should load an encrypted private key from a file using a password', async () => { + const loadedPrivateKey = await loadPrivateKey(`${filePath}.enc`, password) + expect(loadedPrivateKey.export({ type: 'pkcs8', format: 'pem' })).toBe(privateKey) + }) + + test('should throw an error when the password for encrypted key is wrong', async () => { + await expect(loadPrivateKey(`${filePath}.enc`, 'wrongpassword')).rejects.toThrow() + }) + }) + + // Load Public Key Tests + describe('loadPublicKey', () => { + const [_, publicKey] = generator() + const filePath = path.join(testDir, 'testPublicKey.pem') + + beforeAll(async () => { + // Saving a public key for test + await saveKey(pemToPublicKey(publicKey), filePath) + }) + + test('should load a public key from a file', async () => { + const loadedPublicKey = await loadPublicKey(filePath) + expect(loadedPublicKey.export({ type: 'spki', format: 'pem' })).toBe(publicKey) + }) + + test('should throw an error when file does not exist', async () => { + await expect( + loadPublicKey(path.join(testDir, 'nonexistentPublicKey.pem')), + ).rejects.toThrow() + }) + }) + }) + } +}) + +describe('Private/Public key to hex for', () => { + let privateKey: string, publicKey: string + + test('RSA', () => { + ;[privateKey, publicKey] = generateRsaKeyPair() + }) + + test('Ed25519', () => { + ;[privateKey, publicKey] = generateEd25519KeyPair() + }) + + afterEach(() => { + const privateKeyObject = createPrivateKey({ + key: privateKey, + format: 'pem', // Input can still be PEM + }) + const publicKeyObject = createPublicKey(privateKeyObject) + + expect(keyToHex(privateKeyObject)).toStrictEqual(expect.any(String)) + expect(keyToHex(publicKeyObject)).toStrictEqual(expect.any(String)) + }) +}) + +describe('Do public keys match for', () => { + const keyTypes = [ + { label: 'RSA', keyGenerator: generateRsaKeyPair }, + { label: 'Ed25519', keyGenerator: generateEd25519KeyPair }, + ] + + keyTypes.forEach(({ label, keyGenerator }) => { + describe(`${label}`, () => { + let privateKeyPem1: string, + publicKeyPem1: string, + publicKeyPem2: string, + privateKeyPem3: string, + publicKeyPem3: string + let publicKey1: KeyObject, publicKey2: KeyObject, publicKey3: KeyObject + + beforeEach(() => { + ;[privateKeyPem1, publicKeyPem1] = keyGenerator() + publicKeyPem2 = publicKeyPem1 // The same key to ensure a match + ;[privateKeyPem3, publicKeyPem3] = keyGenerator() + + publicKey1 = createPublicKey({ key: publicKeyPem1, format: 'pem' }) + publicKey2 = createPublicKey({ key: publicKeyPem2, format: 'pem' }) + publicKey3 = createPublicKey({ key: publicKeyPem3, format: 'pem' }) + }) + + test('should return true if two public keys match', () => { + const match = doPublicKeysMatch(publicKey1, publicKey2) + expect(match).toBe(true) + }) + + test('should return false if two public keys do not match', () => { + const noMatch = doPublicKeysMatch(publicKey1, publicKey3) + expect(noMatch).toBe(false) + }) + + test('should handle comparison of the same key object', () => { + const selfMatch = doPublicKeysMatch(publicKey1, publicKey1) + expect(selfMatch).toBe(true) + }) + }) + }) +}) diff --git a/packages/auto-id/tsconfig.json b/packages/auto-id/tsconfig.json index 1a123464..aa702d83 100644 --- a/packages/auto-id/tsconfig.json +++ b/packages/auto-id/tsconfig.json @@ -1,10 +1,13 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - - } - \ No newline at end of file + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./" + }, + "include": [ + "src/**/*", + ], + "exclude": [ + "examples/**/*" + ] +} \ No newline at end of file diff --git a/packages/auto-utils/src/index.ts b/packages/auto-utils/src/index.ts index 5f9ea5e0..d38eecfc 100644 --- a/packages/auto-utils/src/index.ts +++ b/packages/auto-utils/src/index.ts @@ -1,3 +1,4 @@ export * from './api' export * from './network' +export * from './read' export * from './save' diff --git a/packages/auto-utils/src/read.ts b/packages/auto-utils/src/read.ts new file mode 100644 index 00000000..fd5a7e1e --- /dev/null +++ b/packages/auto-utils/src/read.ts @@ -0,0 +1,30 @@ +export const read = async (key: string) => { + // detect if we are in the browser or in node + if (typeof window !== 'undefined') return readFromLocalStorage(key) + else return readFromFileSystem(key) +} + +export const readFromLocalStorage = async (key: string) => { + if (typeof window !== 'undefined') { + // read from local storage + const value = localStorage.getItem(key) + try { + return value ? JSON.parse(value) : null + } catch (error) { + throw new Error('Failed to parse data from localStorage: ' + error) + } + } else throw new Error('This function can only be used in the browser') +} + +export const readFromFileSystem = async (key: string) => { + if (typeof window === 'undefined') { + // read from file system + const fs = await import('fs/promises') + try { + const data = await fs.readFile(key, { encoding: 'utf-8' }) + return JSON.parse(data); + } catch (error) { + throw new Error('Failed to read or parse file: ' + error) + } + } else throw new Error('This function can only be used in node') +} \ No newline at end of file diff --git a/packages/auto-utils/src/save.ts b/packages/auto-utils/src/save.ts index a83c084f..5fa575a6 100644 --- a/packages/auto-utils/src/save.ts +++ b/packages/auto-utils/src/save.ts @@ -15,6 +15,8 @@ export const saveOnFileSystem = async (key: string, value: any) => { if (typeof window === 'undefined') { // save on file system const fs = await import('fs/promises') - await fs.writeFile(key, JSON.stringify(value)) + // Check if value is already a string to avoid unnecessary JSON string conversion + const data = typeof value === 'string' ? value : JSON.stringify(value); + await fs.writeFile(key, JSON.stringify(data)) } else throw new Error('This function can only be used in node') -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8eb0a262..85701739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,12 @@ __metadata: version: 0.0.0-use.local resolution: "@autonomys/auto-id@workspace:packages/auto-id" dependencies: + "@autonomys/auto-utils": "workspace:*" + "@types/jest": "npm:^29.5.12" + "@types/node": "npm:^20.12.12" + jest: "npm:^29.7.0" + ts-jest: "npm:^29.1.4" + ts-node: "npm:^10.9.2" typescript: "npm:^5.4.5" languageName: unknown linkType: soft @@ -451,6 +457,15 @@ __metadata: languageName: node linkType: hard +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10c0/05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -793,7 +808,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.1.0": +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e @@ -814,6 +829,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" @@ -1452,6 +1477,34 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11" + checksum: 10c0/28a0710e5d039e0de484bdf85fee883bfd3f6a8980601f4d44066b0a6bcd821d31c4e231d1117731c4e24268bd4cf2a788a6787c12fc7f8d11014c07d582783c + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10c0/dddca2b553e2bee1308a056705103fc8304e42bb2d2cbd797b84403a223b25c78f2c683ec3e24a095e82cd435387c877239bffcb15a590ba817cd3f6b9a99fd9 + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10c0/67c1316d065fdaa32525bc9449ff82c197c4c19092b9663b23213c8cbbf8d88b6ed6a17898e0cbc2711950fbfaf40388938c1c748a2ee89f7234fc9e7fe2bf44 + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10c0/05f8f2734e266fb1839eb1d57290df1664fe2aa3b0fdd685a9035806daa635f7519bf6d5d9b33f6e69dd545b8c46bd6e2b5c79acb2b1f146e885f7f11a42a5bb + languageName: node + linkType: hard + "@types/babel__core@npm:^7.1.14": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -1555,6 +1608,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.12.12": + version: 20.14.1 + resolution: "@types/node@npm:20.14.1" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/12b7879047f50cc217bbea3add7c45e542070f6e9fb2092be97542152b7022512bcb2bf848d04f77e295c4c8699acd484e79a4a4dbe9bcfa4e89dd543d530611 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -1601,7 +1663,14 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.9.0": +"acorn-walk@npm:^8.1.1": + version: 8.3.2 + resolution: "acorn-walk@npm:8.3.2" + checksum: 10c0/7e2a8dad5480df7f872569b9dccff2f3da7e65f5353686b1d6032ab9f4ddf6e3a2cb83a9b52cf50b1497fd522154dda92f0abf7153290cc79cd14721ff121e52 + languageName: node + linkType: hard + +"acorn@npm:^8.4.1, acorn@npm:^8.9.0": version: 8.11.3 resolution: "acorn@npm:8.11.3" bin: @@ -1706,6 +1775,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10c0/070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a + languageName: node + linkType: hard + "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -2071,6 +2147,13 @@ __metadata: languageName: node linkType: hard +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10c0/157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -2153,6 +2236,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 + languageName: node + linkType: hard + "doctrine@npm:^3.0.0": version: 3.0.0 resolution: "doctrine@npm:3.0.0" @@ -3639,7 +3729,7 @@ __metadata: languageName: node linkType: hard -"make-error@npm:1.x": +"make-error@npm:1.x, make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f @@ -4661,6 +4751,44 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 10c0/5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2 + languageName: node + linkType: hard + "tslib@npm:^2.1.0, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" @@ -4766,6 +4894,13 @@ __metadata: languageName: node linkType: hard +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 10c0/bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391 + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.1": version: 9.2.0 resolution: "v8-to-istanbul@npm:9.2.0" @@ -4919,6 +5054,13 @@ __metadata: languageName: node linkType: hard +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 10c0/0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0"