From ecf7a23a9c22ba0351f5f2fe05bf719782bb1b1a Mon Sep 17 00:00:00 2001 From: Dmitri Zagidulin Date: Mon, 27 Jan 2025 00:58:18 -0500 Subject: [PATCH] Convert to Typescript. Signed-off-by: Dmitri Zagidulin --- .editorconfig | 11 + .eslintrc.cjs | 14 + .eslintrc.js | 19 -- .github/workflows/main.yml | 48 ++-- CHANGELOG.md | 5 + README.md | 12 +- build-dist.sh | 19 -- karma.conf.js => karma.conf.cjs | 37 +-- lib/baseX.js | 4 - lib/util-browser.js | 21 -- lib/util-reactnative.js | 19 -- lib/util.js | 17 -- package.json | 104 +++---- rollup.config.js | 37 --- src/baseX.ts | 4 + src/declarations.d.ts | 1 + lib/index.js => src/index.ts | 412 +++++++++++++++------------ src/util-browser.ts | 22 ++ src/util-reactnative.ts | 20 ++ src/util.ts | 17 ++ test/.eslintrc.js | 8 - test/benchmark.js | 98 ------- test/test-karma.js | 7 - test/{test.spec.js => test.spec.mjs} | 12 +- tsconfig.json | 24 ++ tsconfig.spec.json | 29 ++ webpack.config.js | 19 -- 27 files changed, 464 insertions(+), 576 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc.cjs delete mode 100644 .eslintrc.js delete mode 100755 build-dist.sh rename karma.conf.js => karma.conf.cjs (70%) delete mode 100644 lib/baseX.js delete mode 100644 lib/util-browser.js delete mode 100644 lib/util-reactnative.js delete mode 100644 lib/util.js delete mode 100644 rollup.config.js create mode 100644 src/baseX.ts create mode 100644 src/declarations.d.ts rename lib/index.js => src/index.ts (53%) create mode 100644 src/util-browser.ts create mode 100644 src/util-reactnative.ts create mode 100644 src/util.ts delete mode 100644 test/.eslintrc.js delete mode 100644 test/benchmark.js delete mode 100644 test/test-karma.js rename test/{test.spec.js => test.spec.mjs} (99%) create mode 100644 tsconfig.json create mode 100644 tsconfig.spec.json delete mode 100644 webpack.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a1c0c00 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..62df0b6 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + overrides: [ + { + files: ['*.js', '*.jsx', '*.ts', '*.tsx'], + extends: 'standard-with-typescript', + parserOptions: { + project: './tsconfig.spec.json' + }, + rules: { + '@typescript-eslint/no-unused-expressions': 'off' + } + } + ] +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 173f814..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - root: true, - extends: [ - 'digitalbazaar', - // 'digitalbazaar/jsdoc' - ], - env: { - node: true, - browser: true - }, - parserOptions: { - // this is required for dynamic import() - ecmaVersion: 2020 - }, - ignorePatterns: ['node_modules', 'dist'], - rules: { - 'jsdoc/check-examples': 0 - } -}; diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index da197eb..2fc9d49 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,38 +3,22 @@ name: Node.js CI on: [push] jobs: - test-node: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - name: Run test with Node.js ${{ matrix.node-version }} - run: npm run test-node - env: - CI: true - test-karma: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - name: Run karma tests - run: npm run test-karma - env: - CI: true +# test-node: +# runs-on: ubuntu-latest +# strategy: +# matrix: +# node-version: [18.x] +# steps: +# - uses: actions/checkout@v2 +# - name: Use Node.js ${{ matrix.node-version }} +# uses: actions/setup-node@v1 +# with: +# node-version: ${{ matrix.node-version }} +# - run: npm install +# - name: Run test with Node.js ${{ matrix.node-version }} +# run: npm run test-node +# env: +# CI: true lint: runs-on: ubuntu-latest strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a6ab3..01cf0bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # bnid ChangeLog +## 4.0.0 - + +### Changed +- **BREAKING**: Convert to Typescript (still also exports Javascript). + ## 3.0.1 - 2024-01-23 ### Changed diff --git a/README.md b/README.md index fb05694..9787126 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# JavaScript Base-N Id Generator _(@digitalcredentials/bnid)_ +# JS/TS Base-N Id Generator _(@digitalcredentials/bnid)_ [![Node.js CI](https://github.com/digitalcredentials/bnid/workflows/Node.js%20CI/badge.svg)](https://github.com/digitalcredentials/bnid/actions?query=workflow%3A%22Node.js+CI%22) [![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/bnid.svg)](https://npm.im/@digitalcredentials/bnid) -> A JavaScript library for Web browsers, React Native, and Node.js apps to generate random +> A Typescript/JavaScript library for Web browsers, React Native, and Node.js apps to generate random > ids and encode and decode them using various base-N encodings. ## Table of Contents @@ -19,15 +19,15 @@ ## Background -This library provides tools for Web and Node.js to generate random ids and -encode and decode them in various base-N encodings. +This library provides tools for Web browsers, React Native, and Node.js apps to +generate random ids and encode and decode them in various base-N encodings. ## Install ### NPM ``` -npm install bnid +npm install @digitalcredentials/bnid ``` ### Git @@ -92,7 +92,7 @@ Some setup overhead can be avoided by using the component `IdGenerator` and `IdEncoder` classes. ```js -import {IdGenerator, IdEncoder} from 'bnid'; +import { IdGenerator, IdEncoder } from 'bnid'; // 64 bit random id generator const generator = new IdGenerator({ diff --git a/build-dist.sh b/build-dist.sh deleted file mode 100755 index 112b311..0000000 --- a/build-dist.sh +++ /dev/null @@ -1,19 +0,0 @@ -mkdir ./dist/esm -cat >dist/esm/index.js <dist/esm/package.json < d.toString(16).padStart(2, '0')).join(''); -} - -// adapted from: -/* eslint-disable-next-line max-len */ -// https://stackoverflow.com/questions/43131242/how-to-convert-a-hexadecimal-string-of-data-to-an-arraybuffer-in-javascript -export function bytesFromHex(hex) { - if(hex.length === 0) { - return new Uint8Array(); - } - return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))); -} diff --git a/lib/util-reactnative.js b/lib/util-reactnative.js deleted file mode 100644 index 9c2ab93..0000000 --- a/lib/util-reactnative.js +++ /dev/null @@ -1,19 +0,0 @@ -import {generateSecureRandom} from 'react-native-securerandom'; - -export async function getRandomBytes(buf) { - return generateSecureRandom(buf); -} - -export function bytesToHex(bytes) { - return Array.from(bytes).map(d => d.toString(16).padStart(2, '0')).join(''); -} - -// adapted from: -/* eslint-disable-next-line max-len */ -// https://stackoverflow.com/questions/43131242/how-to-convert-a-hexadecimal-string-of-data-to-an-arraybuffer-in-javascript -export function bytesFromHex(hex) { - if(hex.length === 0) { - return new Uint8Array(); - } - return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))); -} diff --git a/lib/util.js b/lib/util.js deleted file mode 100644 index a859e02..0000000 --- a/lib/util.js +++ /dev/null @@ -1,17 +0,0 @@ -// Node.js support -import * as crypto from 'crypto'; -import {promisify} from 'util'; - -const randomFill = promisify(crypto.randomFill); - -export async function getRandomBytes(buf) { - return randomFill(buf); -} - -export function bytesToHex(bytes) { - return Buffer.from(bytes).toString('hex'); -} - -export function bytesFromHex(hex) { - return Buffer.from(hex, 'hex'); -} diff --git a/package.json b/package.json index 5e7332d..3667d9c 100644 --- a/package.json +++ b/package.json @@ -3,69 +3,62 @@ "version": "3.0.1", "description": "Base-N Id Generator", "license": "BSD-3-Clause", - "author": { - "name": "Digital Bazaar, Inc.", - "email": "support@digitalbazaar.com", - "url": "https://digitalbazaar.com/" + "scripts": { + "build": "npm run clear && tsc -d", + "clear": "rimraf dist/*", + "lint": "ts-standard --fix --project tsconfig.spec.json", + "prepare": "npm run build", + "rebuild": "npm run clear && npm run build", + "test": "npm run lint && npm run test-node", + "test-node": "npx tsx --test test/*.spec.ts" }, - "homepage": "https://github.com/digitalbazaar/bnid", + "homepage": "https://github.com/digitalcredentials/bnid", "repository": { "type": "git", - "url": "https://github.com/digitalbazaar/bnid" + "url": "git+https://github.com/digitalcredentials/bnid.git" }, "bugs": { - "url": "https://github.com/digitalbazaar/bnid/issues" + "url": "https://github.com/digitalcredentials/bnid/issues" }, "files": [ "dist", - "lib", - "rollup.config.js", - "build-dist.sh", + "src", + "CHANGELOG.md", "README.md", "LICENSE" ], - "main": "dist/index.js", - "module": "dist/esm/index.js", + "type": "module", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", "exports": { ".": { - "require": "./dist/index.js", - "import": "./dist/esm/index.js" - }, - "./package.json": "./package.json" + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } }, "dependencies": { - "base-x": "^4.0.0", + "base-x": "^4.0.1", "react-native-securerandom": "^1.0.0" }, "devDependencies": { - "benchmark": "^2.1.4", - "chai": "^4.2.0", - "chai-bytes": "^0.1.2", + "@types/chai": "^5.2.1", + "@types/mocha": "^10.0.1", + "@types/node": "^22.14.1", "cross-env": "^7.0.3", - "eslint": "^7.0.0", - "eslint-config-digitalbazaar": "^2.5.0", - "eslint-plugin-jsdoc": "^25.4.2", - "esm": "^3.2.25", - "karma": "^6.3.12", - "karma-babel-preprocessor": "^8.0.1", - "karma-chai": "^0.1.0", - "karma-chrome-launcher": "^3.1.0", - "karma-mocha": "^2.0.1", - "karma-mocha-reporter": "^2.2.5", - "karma-sourcemap-loader": "^0.3.8", - "karma-webpack": "^5.0.0", - "mocha": "^8.3.2", - "mocha-lcov-reporter": "^1.3.0", - "nyc": "^15.0.0", + "mocha": "^10.2.0", "rimraf": "^3.0.2", - "rollup": "^2.47.0", - "webpack": "^5.36.2", - "webpack-bundle-analyzer": "^4.4.1", - "webpack-cli": "^4.6.0" + "ts-node": "^10.9.1", + "ts-standard": "^12.0.2", + "typescript": "^5.8.3", + "tsx": "^4.19.4" + }, + "publishConfig": { + "access": "public" }, "browser": { - "./lib/util.js": "./lib/util-browser.js", + "./src/util.ts": "./src/util-browser.ts", "./dist/util.js": "./dist/util-browser.js", + "react-native-securerandom": false, "buffer": false, "crypto": false, "util": false @@ -74,35 +67,28 @@ "buffer": false, "crypto": false, "util": false, - "./lib/util.js": "./lib/util-reactnative.js", + "./src/util.ts": "./src/util-reactnative.ts", "./dist/util.js": "./dist/util-reactnative.js" }, "engines": { "node": ">=18" }, - "scripts": { - "rollup": "rollup -c rollup.config.js", - "build": "npm run clear && npm run rollup && ./build-dist.sh", - "clear": "rimraf dist/*", - "prepare": "npm run build", - "prepack": "npm run build", - "rebuild": "npm run clear && npm run build", - "test": "npm run lint && npm run test-node && npm run test-karma", - "test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 10000 test/*.spec.js", - "test-karma": "karma start karma.conf.js", - "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text-summary npm run test-node", - "coverage-ci": "cross-env NODE_ENV=test nyc --reporter=lcovonly npm run test-node", - "coverage-report": "nyc report", - "lint": "eslint ." + "ts-standard": { + "env": [ "mocha" ], + "ignore": [ + "dist", + "test" + ], + "globals": [ "it"] }, "keywords": [ "id", "identifier", "random" ], - "nyc": { - "exclude": [ - "tests" - ] + "author": { + "name": "Digital Bazaar, Inc.", + "email": "support@digitalbazaar.com", + "url": "https://digitalbazaar.com/" } } diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 6106128..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,37 +0,0 @@ -import pkg from './package.json'; - -export default [ - { - input: './lib/index.js', - output: [ - { - dir: 'dist', - format: 'cjs', - preserveModules: true - } - ], - external: Object.keys(pkg.dependencies).concat(['crypto', 'util']) - }, - { - input: './lib/util-browser.js', - output: [ - { - dir: 'dist', - format: 'cjs', - preserveModules: true - } - ], - external: Object.keys(pkg.dependencies).concat(['crypto', 'util']) - }, - { - input: './lib/util-reactnative.js', - output: [ - { - dir: 'dist', - format: 'cjs', - preserveModules: true - } - ], - external: Object.keys(pkg.dependencies).concat(['crypto', 'util']) - } -]; diff --git a/src/baseX.ts b/src/baseX.ts new file mode 100644 index 0000000..e685f68 --- /dev/null +++ b/src/baseX.ts @@ -0,0 +1,4 @@ +import baseX from 'base-x' + +const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +export const base58btc = baseX(BASE58) diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 0000000..9d786bf --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1 @@ +declare module 'chai' diff --git a/lib/index.js b/src/index.ts similarity index 53% rename from lib/index.js rename to src/index.ts index b5f0987..cecde76 100644 --- a/lib/index.js +++ b/src/index.ts @@ -1,111 +1,124 @@ /*! * Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. */ -import {base58btc} from './baseX.js'; +import { base58btc } from './baseX.js' import { getRandomBytes, bytesToHex, bytesFromHex -} from './util.js'; +} from './util.js' // multihash identity function code -const MULTIHASH_IDENTITY_FUNCTION_CODE = 0x00; +const MULTIHASH_IDENTITY_FUNCTION_CODE = 0x00 -function _calcOptionsBitLength({ +function _calcOptionsBitLength ({ defaultLength, // TODO: allow any bit length minLength = 8, // TODO: support maxLength - //maxLength = Infinity, + // maxLength = Infinity, bitLength -}) { - if(bitLength === undefined) { - return defaultLength; +}: { defaultLength: number, minLength?: number, bitLength?: number }): number { + if (bitLength === undefined) { + return defaultLength } // TODO: allow any bit length - if(bitLength % 8 !== 0) { - throw new Error('Bit length must be a multiple of 8.'); + if (bitLength % 8 !== 0) { + throw new Error('Bit length must be a multiple of 8.') } - if(bitLength < minLength) { - throw new Error(`Minimum bit length is ${minLength}.`); + if (bitLength < minLength) { + throw new Error(`Minimum bit length is ${minLength}.`) } // TODO: support maxLength - //if(bitLength > maxLength) { + // if(bitLength > maxLength) { // throw new Error(`Maximum bit length is ${maxLength}.`); - //} - return bitLength; + // } + return bitLength } -function _calcDataBitLength({ +function _calcDataBitLength ({ bitLength, maxLength -}) { - if(maxLength === 0) { - return bitLength; +}: { bitLength: number, maxLength?: number }): number { + if (maxLength === 0) { + return bitLength } - if(bitLength > maxLength) { - throw new Error(`Input length greater than ${maxLength} bits.`); + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (maxLength && bitLength > maxLength) { + throw new Error(`Input length greater than ${maxLength} bits.`) } - return maxLength; + // @ts-expect-error + return maxLength } -function _bytesWithBitLength({ +function _bytesWithBitLength ({ bytes, bitLength -}) { - const length = bytes.length * 8; - if(length === bitLength) { - return bytes; +}: { bytes: Uint8Array, bitLength: number }): Uint8Array { + const length = bytes.length * 8 + if (length === bitLength) { + return bytes } - if(length < bitLength) { + if (length < bitLength) { // pad start - const data = new Uint8Array(bitLength / 8); - data.set(bytes, data.length - bytes.length); - return data; + const data = new Uint8Array(bitLength / 8) + data.set(bytes, data.length - bytes.length) + return data } // trim start, ensure trimmed data is zero - const start = (length - bitLength) / 8; - if(bytes.subarray(0, start).some(d => d !== 0)) { + const start = (length - bitLength) / 8 + if (bytes.subarray(0, start).some(d => d !== 0)) { throw new Error( - `Data length greater than ${bitLength} bits.`); + `Data length greater than ${bitLength} bits.`) } - return bytes.subarray(start); + return bytes.subarray(start) } -const _log2_16 = 4; -function _base16Encoder({bytes, idEncoder}) { - let encoded = bytesToHex(bytes); - if(idEncoder.encoding === 'base16upper') { - encoded = encoded.toUpperCase(); +export interface IEncoder { + bytes: Uint8Array + idEncoder: IdEncoder +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +const _log2_16 = 4 + +function _base16Encoder ({ bytes, idEncoder }: IEncoder): string { + let encoded = bytesToHex(bytes) + if (idEncoder.encoding === 'base16upper') { + encoded = encoded.toUpperCase() } - if(idEncoder.fixedLength) { + if (idEncoder.fixedLength && idEncoder.fixedLength !== undefined) { const fixedBitLength = _calcDataBitLength({ bitLength: bytes.length * 8, maxLength: idEncoder.fixedBitLength - }); - const wantLength = Math.ceil(fixedBitLength / _log2_16); + }) + const wantLength = Math.ceil(fixedBitLength / _log2_16) // pad start with 0s - return encoded.padStart(wantLength, '0'); + return encoded.padStart(wantLength, '0') } - return encoded; + return encoded } -const _log2_58 = Math.log2(58); -function _base58Encoder({bytes, idEncoder}) { - const encoded = base58btc.encode(bytes); - if(idEncoder.fixedLength) { +// eslint-disable-next-line @typescript-eslint/naming-convention +const _log2_58 = Math.log2(58) + +function _base58Encoder ({ bytes, idEncoder }: IEncoder): string { + const encoded = base58btc.encode(bytes) + if (idEncoder.fixedLength) { const fixedBitLength = _calcDataBitLength({ bitLength: bytes.length * 8, maxLength: idEncoder.fixedBitLength - }); - const wantLength = Math.ceil(fixedBitLength / _log2_58); + }) + const wantLength = Math.ceil(fixedBitLength / _log2_58) // pad start with 0s (encoded as '1's) - return encoded.padStart(wantLength, '1'); + return encoded.padStart(wantLength, '1') } - return encoded; + return encoded } export class IdGenerator { + public bitLength: number + /** * Creates a new IdGenerator instance. * @@ -116,16 +129,16 @@ export class IdGenerator { * * @returns {IdGenerator} - New IdGenerator. */ - constructor({ + constructor ({ bitLength - } = {}) { + }: { bitLength?: number } = {}) { this.bitLength = _calcOptionsBitLength({ // default to 128 bits / 16 bytes defaultLength: 128, // TODO: allow any bit length minLength: 8, - bitLength, - }); + bitLength + }) } /** @@ -133,14 +146,31 @@ export class IdGenerator { * * @returns {Uint8Array} - Array of random id bytes. */ - async generate() { - const buf = new Uint8Array(this.bitLength / 8); - await getRandomBytes(buf); - return buf; + async generate (): Promise { + const buf = new Uint8Array(this.bitLength / 8) + await getRandomBytes(buf) + return buf } } +export interface IIdEncoder { + encoding?: string + fixedLength?: boolean + fixedBitLength?: number + bitLength?: number + multibase?: boolean + multihash?: boolean +} + export class IdEncoder { + public encoder: ({ bytes, idEncoder }: IEncoder) => string + public encoding: string + public multibasePrefix: string + public fixedLength: boolean + public fixedBitLength?: number + public multibase: boolean = true + public multihash: boolean = false + /** * Creates a new IdEncoder instance. * @@ -157,42 +187,42 @@ export class IdEncoder { * * @returns {IdEncoder} - New IdEncoder. */ - constructor({ + constructor ({ encoding = 'base58', fixedLength = false, fixedBitLength, multibase = true, - multihash = false, - } = {}) { - switch(encoding) { + multihash = false + }: IIdEncoder = {}) { + switch (encoding) { case 'hex': case 'base16': - this.encoder = _base16Encoder; - this.multibasePrefix = 'f'; - break; + this.encoder = _base16Encoder + this.multibasePrefix = 'f' + break case 'base16upper': - this.encoder = _base16Encoder; - this.multibasePrefix = 'F'; - break; + this.encoder = _base16Encoder + this.multibasePrefix = 'F' + break case 'base58': case 'base58btc': - this.encoder = _base58Encoder; - this.multibasePrefix = 'z'; - break; + this.encoder = _base58Encoder + this.multibasePrefix = 'z' + break default: - throw new Error(`Unknown encoding type: "${encoding}".`); + throw new Error(`Unknown encoding type: "${encoding}".`) } - this.fixedLength = fixedLength || fixedBitLength !== undefined; - if(this.fixedLength) { + this.fixedLength = fixedLength || fixedBitLength !== undefined + if (this.fixedLength) { this.fixedBitLength = _calcOptionsBitLength({ // default of 0 calculates from input size defaultLength: 0, bitLength: fixedBitLength - }); + }) } - this.encoding = encoding; - this.multibase = multibase; - this.multihash = multihash; + this.encoding = encoding + this.multibase = multibase + this.multihash = multihash } /** @@ -202,33 +232,47 @@ export class IdEncoder { * * @returns {string} - Encoded string. */ - encode(bytes) { - if(this.multihash) { - const byteSize = bytes.length; + encode (bytes: Uint8Array): string { + if (this.multihash) { + const byteSize = bytes.length - if(byteSize > 127) { - throw new RangeError('Identifier size too large.'); + if (byteSize > 127) { + throw new RangeError('Identifier size too large.') } // // - const multihash = new Uint8Array(2 + byteSize); + const multihash = new Uint8Array(2 + byteSize) // : identity function - multihash.set([MULTIHASH_IDENTITY_FUNCTION_CODE]); + multihash.set([MULTIHASH_IDENTITY_FUNCTION_CODE]) // - multihash.set([byteSize], 1); + multihash.set([byteSize], 1) // : identifier bytes - multihash.set(bytes, 2); - bytes = multihash; + multihash.set(bytes, 2) + bytes = multihash } - const encoded = this.encoder({bytes, idEncoder: this}); - if(this.multibase) { - return this.multibasePrefix + encoded; + const encoded = this.encoder({ bytes, idEncoder: this }) + if (this.multibase) { + return this.multibasePrefix + encoded } - return encoded; + return encoded } } +export interface IIdDecoder { + encoding?: string + fixedBitLength?: number + multibase?: boolean + multihash?: boolean + expectedSize?: number +} + export class IdDecoder { + public encoding: string + public fixedBitLength?: number = 0 + public multibase: boolean + public multihash: boolean + public expectedSize: number + /** * Creates a new IdDecoder instance. * @@ -250,18 +294,18 @@ export class IdDecoder { * check. * @returns {IdDecoder} - New IdDecoder. */ - constructor({ + constructor ({ encoding = 'base58', - fixedBitLength, + fixedBitLength = 0, multibase = true, multihash = false, expectedSize = 32 - } = {}) { - this.encoding = encoding; - this.fixedBitLength = fixedBitLength; - this.multibase = multibase; - this.multihash = multihash; - this.expectedSize = expectedSize; + }: IIdDecoder = {}) { + this.encoding = encoding + this.fixedBitLength = fixedBitLength + this.multibase = multibase + this.multihash = multihash + this.expectedSize = expectedSize } /** @@ -271,85 +315,89 @@ export class IdDecoder { * * @returns {Uint8Array} - Array of decoded id bytes. */ - decode(id) { - let encoding; - let data; - if(this.multibase) { - if(id.length < 1) { - throw new Error('Multibase encoding not found.'); + decode (id: string): Uint8Array { + let encoding + let data + if (this.multibase) { + if (id.length < 1) { + throw new Error('Multibase encoding not found.') } - const prefix = id[0]; - data = id.substring(1); - switch(id[0]) { + const prefix = id[0] + data = id.substring(1) + switch (id[0]) { case 'f': - encoding = 'base16'; - break; + encoding = 'base16' + break case 'F': - encoding = 'base16upper'; - break; + encoding = 'base16upper' + break case 'z': - encoding = 'base58'; - break; + encoding = 'base58' + break default: - throw new Error(`Unknown multibase prefix "${prefix}".`); + throw new Error(`Unknown multibase prefix "${prefix}".`) } } else { - encoding = this.encoding; - data = id; + encoding = this.encoding + data = id } - let decoded; - switch(encoding) { + let decoded + switch (encoding) { case 'hex': case 'base16': case 'base16upper': - if(data.length % 2 !== 0) { - throw new Error('Invalid base16 data length.'); + if (data.length % 2 !== 0) { + throw new Error('Invalid base16 data length.') } - decoded = bytesFromHex(data); - break; + decoded = bytesFromHex(data) + break case 'base58': - decoded = base58btc.decode(data); - break; + decoded = base58btc.decode(data) + break default: - throw new Error(`Unknown encoding "${encoding}".`); + throw new Error(`Unknown encoding "${encoding}".`) } - if(!decoded) { - throw new Error(`Invalid encoded data "${data}".`); + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!decoded) { + throw new Error(`Invalid encoded data "${data}".`) } - if(this.fixedBitLength) { + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (this.fixedBitLength) { return _bytesWithBitLength({ bytes: decoded, - bitLength: this.fixedBitLength - }); + bitLength: this.fixedBitLength ?? 0 + }) } - if(this.multihash) { + if (this.multihash) { // : identity function - const [hashFnCode] = decoded; + const [hashFnCode] = decoded - if(hashFnCode !== MULTIHASH_IDENTITY_FUNCTION_CODE) { - throw new Error('Invalid multihash function code.'); + if (hashFnCode !== MULTIHASH_IDENTITY_FUNCTION_CODE) { + throw new Error('Invalid multihash function code.') } // - const digestSize = decoded[1]; + const digestSize = decoded[1] - if(digestSize > 127) { - throw new RangeError('Decoded identifier size too large.'); + if (digestSize > 127) { + throw new RangeError('Decoded identifier size too large.') } - const bytes = decoded.subarray(2); + const bytes = decoded.subarray(2) - if(bytes.byteLength !== digestSize) { - throw new RangeError('Unexpected identifier size.'); + if (bytes.byteLength !== digestSize) { + throw new RangeError('Unexpected identifier size.') } - if(this.expectedSize && bytes.byteLength !== this.expectedSize) { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (this.expectedSize && bytes.byteLength !== this.expectedSize) { throw new RangeError( - `Invalid decoded identifier size. Identifier must be ` + - `"${this.expectedSize}" bytes.`); + 'Invalid decoded identifier size. Identifier must be ' + + `"${this.expectedSize}" bytes.`) } - decoded = bytes; + decoded = bytes } - return decoded; + return decoded } } @@ -361,9 +409,9 @@ export class IdDecoder { * * @returns {string} - Encoded string id. */ -export async function generateId(options) { +export async function generateId (options: IIdEncoder): Promise { return new IdEncoder(options) - .encode(await new IdGenerator(options).generate()); + .encode(await new IdGenerator(options).generate()) } /** @@ -375,8 +423,8 @@ export async function generateId(options) { * * @returns {Uint8Array} - Decoded array of id bytes. */ -export function decodeId(options) { - return new IdDecoder(options).decode(options.id); +export function decodeId (options: IIdDecoder & { id: string }): Uint8Array { + return new IdDecoder(options).decode(options.id) } /** @@ -389,26 +437,26 @@ export function decodeId(options) { * * @returns {number} - The minimum number of encoded bytes. */ -export function minEncodedIdBytes({ +export function minEncodedIdBytes ({ encoding = 'base58', bitLength = 128, multibase = true -} = {}) { - let plainBytes; - switch(encoding) { +}: IIdEncoder = {}): number { + let plainBytes + switch (encoding) { case 'hex': case 'base16': case 'base16upper': - plainBytes = bitLength / 4; - break; + plainBytes = bitLength / 4 + break case 'base58': case 'base58btc': - plainBytes = bitLength / 8; - break; + plainBytes = bitLength / 8 + break default: - throw new Error(`Unknown encoding type: "${encoding}".`); + throw new Error(`Unknown encoding type: "${encoding}".`) } - return plainBytes + (multibase ? 1 : 0); + return plainBytes + (multibase ? 1 : 0) } /** @@ -421,26 +469,26 @@ export function minEncodedIdBytes({ * * @returns {number} - The maximum number of encoded bytes. */ -export function maxEncodedIdBytes({ +export function maxEncodedIdBytes ({ encoding = 'base58', bitLength = 128, multibase = true -} = {}) { - let plainBytes; - switch(encoding) { +}: IIdEncoder = {}): number { + let plainBytes + switch (encoding) { case 'hex': case 'base16': case 'base16upper': - plainBytes = bitLength / 4; - break; + plainBytes = bitLength / 4 + break case 'base58': case 'base58btc': - plainBytes = Math.ceil(bitLength / Math.log2(58)); - break; + plainBytes = Math.ceil(bitLength / Math.log2(58)) + break default: - throw new Error(`Unknown encoding type: "${encoding}".`); + throw new Error(`Unknown encoding type: "${encoding}".`) } - return plainBytes + (multibase ? 1 : 0); + return plainBytes + (multibase ? 1 : 0) } /** @@ -456,20 +504,20 @@ export function maxEncodedIdBytes({ * @returns {string} - Secret key seed encoded as a string. */ -export async function generateSecretKeySeed({ +export async function generateSecretKeySeed ({ bitLength = 32 * 8, encoding = 'base58', multibase = true, multihash = true -} = {}) { +}: IIdEncoder = {}): Promise { // reuse `generateId` for convenience, but a key seed is *SECRET* and // not an identifier itself, rather it is used to generate an identifier via // a public key // Note: Setting fixedLength to false even though that's the (current) // default as not using a fixed length of false for a seed is a security // problem - return generateId( - {bitLength, encoding, fixedLength: false, multibase, multihash}); + return await generateId( + { bitLength, encoding, fixedLength: false, multibase, multihash }) } /** @@ -488,13 +536,13 @@ export async function generateSecretKeySeed({ * @returns {Uint8Array} - An array of secret key seed bytes (default size: * 32 bytes). */ -export function decodeSecretKeySeed({ +export function decodeSecretKeySeed ({ multibase = true, multihash = true, expectedSize = 32, - secretKeySeed, -}) { + secretKeySeed +}: { multibase?: boolean, multihash?: boolean, expectedSize?: number, secretKeySeed: string }): Uint8Array { // reuse `decodeId` for convenience, but key seed bytes are *SECRET* and // are NOT identifiers, they are used to generate identifiers from public keys - return decodeId({multihash, multibase, expectedSize, id: secretKeySeed}); + return decodeId({ multihash, multibase, expectedSize, id: secretKeySeed }) } diff --git a/src/util-browser.ts b/src/util-browser.ts new file mode 100644 index 0000000..ff49af9 --- /dev/null +++ b/src/util-browser.ts @@ -0,0 +1,22 @@ +// browser support +/* eslint-env browser */ +const crypto = (globalThis.crypto) + +export async function getRandomBytes (buf: Uint8Array): Promise { + return crypto.getRandomValues(buf) +} + +export function bytesToHex (bytes: Uint8Array): string { + return Array.from(bytes).map(d => d.toString(16).padStart(2, '0')).join('') +} + +// adapted from: +/* eslint-disable-next-line max-len */ +// https://stackoverflow.com/questions/43131242/how-to-convert-a-hexadecimal-string-of-data-to-an-arraybuffer-in-javascript +export function bytesFromHex (hex: string): Uint8Array { + if (hex.length === 0) { + return new Uint8Array() + } + // @ts-expect-error + return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))) +} diff --git a/src/util-reactnative.ts b/src/util-reactnative.ts new file mode 100644 index 0000000..6596866 --- /dev/null +++ b/src/util-reactnative.ts @@ -0,0 +1,20 @@ +import { generateSecureRandom } from 'react-native-securerandom' + +export async function getRandomBytes (buf: Uint8Array): Promise { + return await generateSecureRandom(buf.length) +} + +export function bytesToHex (bytes: Uint8Array): string { + return Array.from(bytes).map(d => d.toString(16).padStart(2, '0')).join('') +} + +// adapted from: +/* eslint-disable-next-line max-len */ +// https://stackoverflow.com/questions/43131242/how-to-convert-a-hexadecimal-string-of-data-to-an-arraybuffer-in-javascript +export function bytesFromHex (hex: string): Uint8Array { + if (hex.length === 0) { + return new Uint8Array() + } + // @ts-expect-error + return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))) +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..b512ebc --- /dev/null +++ b/src/util.ts @@ -0,0 +1,17 @@ +// Node.js support +import * as crypto from 'crypto' +import { promisify } from 'util' + +const randomFill = promisify(crypto.randomFill) + +export async function getRandomBytes (buf: Uint8Array): Promise { + return await randomFill(buf) +} + +export function bytesToHex (bytes: Uint8Array): string { + return Buffer.from(bytes).toString('hex') +} + +export function bytesFromHex (hex: string): Buffer { + return Buffer.from(hex, 'hex') +} diff --git a/test/.eslintrc.js b/test/.eslintrc.js deleted file mode 100644 index f76d0f3..0000000 --- a/test/.eslintrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - env: { - mocha: true - }, - globals: { - should: true - } -}; diff --git a/test/benchmark.js b/test/benchmark.js deleted file mode 100644 index 458c325..0000000 --- a/test/benchmark.js +++ /dev/null @@ -1,98 +0,0 @@ -/*! -* Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. -*/ -const Benchmark = require('benchmark'); - -const suite = new Benchmark.Suite(); - -const { - IdGenerator, IdEncoder, IdDecoder, generateId, decodeId -} = require('..'); -const crypto = require('crypto'); - -// shared state -const generator = new IdGenerator(); -const encoder = new IdEncoder(); -const decoder = new IdDecoder(); - -const idBytes1 = new Uint8Array(128 / 8); -crypto.randomFillSync(idBytes1); -const id1 = encoder.encode(idBytes1); - -suite - .add('generateId base16 128b not-fixed', { - defer: true, - fn: async deferred => { - await generateId({ - encoding: 'base16', - bitLength: 128, - fixedLength: false - }); - deferred.resolve(); - } - }) - .add('generateId base58 128b not-fixed', { - defer: true, - fn: async deferred => { - await generateId({ - encoding: 'base16', - bitLength: 128, - fixedLength: false - }); - deferred.resolve(); - } - }) - .add('generateId base58 128b fixed', { - defer: true, - fn: async deferred => { - await generateId({ - encoding: 'base16', - bitLength: 128, - fixedLength: true - }); - deferred.resolve(); - } - }) - .add('IdGenerator 128b', { - defer: true, - fn: async deferred => { - await new IdGenerator(128).generate(); - deferred.resolve(); - } - }) - .add('IdGenerator shared 128b', { - defer: true, - fn: async deferred => { - await generator.generate(); - deferred.resolve(); - } - }) - .add('IdEncoder shared, static data', { - fn: () => { - encoder.encode(idBytes1); - } - }) - .add('IdDecoder shared, static data', { - fn: () => { - decoder.decode(id1); - } - }) - .add('decodeId', { - fn: () => { - decodeId(id1); - } - }) - .add('encode shared', { - defer: true, - fn: async deferred => { - encoder.encode(await generator.generate()); - deferred.resolve(); - } - }) - .on('cycle', event => { - console.log(String(event.target)); - }) - .on('complete', function() { - console.log('Fastest is ' + this.filter('fastest').map('name')); - }) - .run(); diff --git a/test/test-karma.js b/test/test-karma.js deleted file mode 100644 index c27f111..0000000 --- a/test/test-karma.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Karma test support. - * - * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. - */ -// load polyfills -// no polyfills are currently required. diff --git a/test/test.spec.js b/test/test.spec.mjs similarity index 99% rename from test/test.spec.js rename to test/test.spec.mjs index 518091b..990dc96 100644 --- a/test/test.spec.js +++ b/test/test.spec.mjs @@ -1,14 +1,12 @@ /*! -* Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. +* Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. */ - import { default as chai, expect } from 'chai'; +// eslint-disable-next-line import/no-named-default import {default as chaiBytes} from 'chai-bytes'; -chai.use(chaiBytes); -global.should = chai.should(); import { IdEncoder, @@ -20,7 +18,10 @@ import { maxEncodedIdBytes, generateSecretKeySeed, decodeSecretKeySeed, -} from '../lib/index.js'; +} from '../src/index.js'; + +chai.use(chaiBytes); +const should = chai.should(); describe('bnid', () => { describe('utilities', () => { @@ -463,6 +464,7 @@ describe('bnid', () => { }); const e = '00'; const b = d.decode(e); + should.exist(b); b.should.be.instanceof(Uint8Array); b.length.should.equal(1); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c408d6f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es2022", + "lib": ["es2020"], + "module": "es6", + "moduleResolution": "node", + "declarationMap": true, + "declaration": true, + "outDir": "dist", + "noImplicitAny": true, + "removeComments": false, + "preserveConstEnums": true, + "baseUrl": ".", + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "checkJs": true, + "allowJs": true + }, + "include": [ + "src/**/*" + ], + "exclude": ["test", "node_modules", "dist", ".eslintrc.cjs"] +} diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..fff677c --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es2022", + "lib": ["es2020"], + "module": "es6", + "moduleResolution": "node", + "declarationMap": true, + "declaration": true, + "outDir": "dist", + "noImplicitAny": true, + "removeComments": false, + "preserveConstEnums": true, + "baseUrl": ".", + "skipLibCheck": true, + "checkJs": true, + "allowJs": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "ts-node": { + "files": true + }, + "include": [ + "src/**/*", + "test/**/*.spec.ts" + ], + "exclude": ["node_modules", "dist", ".eslintrc.cjs"] +} diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 332a5cb..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,19 +0,0 @@ -const path = require('path'); - -module.exports = { - mode: 'production', - target: 'web', - output: { - path: path.join(__dirname, 'dist'), - filename: '[name].min.js', - library: '[name]', - libraryTarget: 'umd' - }, - node: false, - resolve: { - fallback: { - util: false, - crypto: false - } - } -};