diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..4b86d719 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Getting Help on IPFS + url: https://ipfs.io/help + about: All information about how and where to get help on IPFS. + - name: IPFS Official Forum + url: https://discuss.ipfs.io + about: Please post general questions, support requests, and discussions here. diff --git a/.github/ISSUE_TEMPLATE/open_an_issue.md b/.github/ISSUE_TEMPLATE/open_an_issue.md new file mode 100644 index 00000000..4fcbd00a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/open_an_issue.md @@ -0,0 +1,19 @@ +--- +name: Open an issue +about: Only for actionable issues relevant to this repository. +title: '' +labels: need/triage +assignees: '' + +--- + diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..ed26646a --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,68 @@ +# Configuration for welcome - https://github.com/behaviorbot/welcome + +# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome +# Comment to be posted to on first time issues +newIssueWelcomeComment: > + Thank you for submitting your first issue to this repository! A maintainer + will be here shortly to triage and review. + + In the meantime, please double-check that you have provided all the + necessary information to make this process easy! Any information that can + help save additional round trips is useful! We currently aim to give + initial feedback within **two business days**. If this does not happen, feel + free to leave a comment. + + Please keep an eye on how this issue will be labeled, as labels give an + overview of priorities, assignments and additional actions requested by the + maintainers: + + - "Priority" labels will show how urgent this is for the team. + - "Status" labels will show if this is ready to be worked on, blocked, or in progress. + - "Need" labels will indicate if additional input or analysis is required. + + Finally, remember to use https://discuss.ipfs.io if you just need general + support. + +# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome +# Comment to be posted to on PRs from first time contributors in your repository +newPRWelcomeComment: > + Thank you for submitting this PR! + + A maintainer will be here shortly to review it. + + We are super grateful, but we are also overloaded! Help us by making sure + that: + + * The context for this PR is clear, with relevant discussion, decisions + and stakeholders linked/mentioned. + + * Your contribution itself is clear (code comments, self-review for the + rest) and in its best form. Follow the [code contribution + guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines) + if they apply. + + Getting other community members to do a review would be great help too on + complex PRs (you can ask in the chats/forums). If you are unsure about + something, just leave us a comment. + + Next steps: + + * A maintainer will triage and assign priority to this PR, commenting on + any missing things and potentially assigning a reviewer for high + priority items. + + * The PR gets reviews, discussed and approvals as needed. + + * The PR is merged by maintainers when it has been approved and comments addressed. + + We currently aim to provide initial feedback/triaging within **two business + days**. Please keep an eye on any labelling actions, as these will indicate + priorities and status of your contribution. + + We are very grateful for your contribution! + + +# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge +# Comment to be posted to on pull requests merged by a first time user +# Currently disabled +#firstPRMergeComment: "" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..9e81b21c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,67 @@ +name: ci +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir lint + - uses: gozala/typescript-error-reporter-action@v1.0.8 + - run: npx aegir build + - run: npx aegir dep-check + - uses: ipfs/aegir/actions/bundle-size@master + name: size + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + test-node: + needs: check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: [12, 14] + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npx nyc --reporter=lcov aegir test -t node -- --bail + - uses: codecov/codecov-action@v1 + test-chrome: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir test -t browser -t webworker --bail + test-firefox: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir test -t browser -t webworker --bail -- --browsers FirefoxHeadless + test-electron-main: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx xvfb-maybe aegir test -t electron-main --bail + test-electron-renderer: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx xvfb-maybe aegir test -t electron-renderer --bail \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4e54a69e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: node_js -cache: npm -stages: - - check - - test - - cov - -node_js: - - '10' - -os: - - linux - - osx - -script: npx nyc -s npm run test:node -- --bail -after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov - -jobs: - include: - - os: windows - filter_secrets: false - cache: false - - - stage: check - script: - - npx aegir commitlint --travis - - npx aegir dep-check - - npm run lint - - - stage: test - name: chrome - addons: - chrome: stable - script: npx aegir test -t browser -t webworker - - - stage: test - name: firefox - addons: - firefox: latest - script: npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless - -notifications: - email: false diff --git a/example.js b/example.js deleted file mode 100644 index 59ea8377..00000000 --- a/example.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const multihashing = require('multihashing-async') -const bytes = new TextEncoder().encode('beep boop') - -function print (err, mh) { - if (err) { - throw err - } - // eslint-disable-next-line - console.log(mh) -} -multihashing(bytes, 'sha1', print) -// => - -multihashing(bytes, 'sha2-256', print) -// => - -multihashing(bytes, 'sha2-512', print) -// => diff --git a/package.json b/package.json index 97dae4af..30e872ce 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,15 @@ "browser": { "./src/sha.js": "./src/sha.browser.js" }, + "types": "dist/src/index.d.ts", "repository": "github:multiformats/js-multihashing-async", "scripts": { "test": "aegir test", "test:browser": "aegir test -t browser", "test:node": "aegir test -t node", "lint": "aegir lint", + "check": "aegir ts -p check", + "prepare": "aegir build --no-bundle", "docs": "aegir docs", "release": "aegir release --docs", "release-minor": "aegir release --type minor --docs", @@ -35,17 +38,22 @@ }, "dependencies": { "blakejs": "^1.1.0", - "err-code": "^2.0.0", + "err-code": "^3.0.0", "js-sha3": "^0.8.0", - "multihashes": "^3.0.1", + "multihashes": "^3.1.2", "murmurhash3js-revisited": "^3.0.0", - "uint8arrays": "^1.0.0" + "uint8arrays": "^2.0.5" }, "devDependencies": { - "aegir": "^25.0.0", + "@types/err-code": "^2.0.0", + "@types/sinon": "^9.0.10", + "aegir": "^30.3.0", "benchmark": "^2.1.4", "sinon": "^9.0.2" }, + "eslintConfig": { + "extends": "ipfs" + }, "engines": { "node": ">=10.0.0", "npm": ">=6.0.0" diff --git a/src/blake.js b/src/blake.js index 4b433056..32741d94 100644 --- a/src/blake.js +++ b/src/blake.js @@ -1,5 +1,6 @@ 'use strict' +// @ts-ignore - no types available const blake = require('blakejs') const minB = 0xb201 @@ -21,12 +22,21 @@ const blake2s = { // the function as async because it must return a Promise to match the API // for other functions that do perform asynchronous work (see sha.browser.js) // eslint-disable-next-line + +/** + * @param {number} size + * @param {any} hf + * @returns {import('./types').Digest} + */ const makeB2Hash = (size, hf) => async (data) => { const ctx = hf.init(size, null) hf.update(ctx, data) return hf.digest(ctx) } +/** + * @param {Record} table + */ module.exports = (table) => { for (let i = 0; i < 64; i++) { table[minB + i] = makeB2Hash(i + 1, blake2b) diff --git a/src/crypto.js b/src/crypto.js index f35aefec..643343ea 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -1,6 +1,7 @@ 'use strict' const sha3 = require('js-sha3') +// @ts-ignore - no types available const mur = require('murmurhash3js-revisited') const { factory: sha } = require('./sha') const { fromNumberTo32BitBuf } = require('./utils') @@ -10,6 +11,10 @@ const uint8ArrayFromString = require('uint8arrays/from-string') // the function as async because it must return a Promise to match the API // for other functions that do perform asynchronous work (see sha.browser.js) // eslint-disable-next-line +/** + * @param {string} algorithm + * @returns {import('./types').Digest} + */ const hash = (algorithm) => async (data) => { switch (algorithm) { case 'sha3-224': @@ -42,6 +47,7 @@ const hash = (algorithm) => async (data) => { } } +/** @type {import('./types').Digest} */ const identity = data => data module.exports = { diff --git a/src/index.js b/src/index.js index 079ff8c5..6fa7fcb5 100644 --- a/src/index.js +++ b/src/index.js @@ -6,14 +6,20 @@ const crypto = require('./crypto') const equals = require('uint8arrays/equals') /** - * Hash the given `buf` using the algorithm specified by `alg`. - * @param {Uint8Array} buf - The value to hash. - * @param {number|string} alg - The algorithm to use eg 'sha1' + * @typedef {import("./types").Digest} Digest + * @typedef {import("multihashes").HashName} HashName + */ + +/** + * Hash the given `bytes` using the algorithm specified by `alg`. + * + * @param {Uint8Array} bytes - The value to hash. + * @param {HashName} alg - The algorithm to use eg 'sha1' * @param {number} [length] - Optionally trim the result to this length. * @returns {Promise} */ -async function Multihashing (buf, alg, length) { - const digest = await Multihashing.digest(buf, alg, length) +async function Multihashing (bytes, alg, length) { + const digest = await Multihashing.digest(bytes, alg, length) return multihash.encode(digest, alg, length) } @@ -23,41 +29,43 @@ async function Multihashing (buf, alg, length) { Multihashing.multihash = multihash /** - * @param {Uint8Array} buf - The value to hash. - * @param {number|string} alg - The algorithm to use eg 'sha1' + * @param {Uint8Array} bytes - The value to hash. + * @param {HashName} alg - The algorithm to use eg 'sha1' * @param {number} [length] - Optionally trim the result to this length. * @returns {Promise} */ -Multihashing.digest = async (buf, alg, length) => { +Multihashing.digest = async (bytes, alg, length) => { const hash = Multihashing.createHash(alg) - const digest = await hash(buf) + const digest = await hash(bytes) return length ? digest.slice(0, length) : digest } /** * Creates a function that hashes with the given algorithm * - * @param {string|number} alg - The algorithm to use eg 'sha1' - * - * @returns {function} - The hash function corresponding to `alg` + * @param {HashName} alg - The algorithm to use eg 'sha1' + * @returns {Digest} - The hash function corresponding to `alg` */ Multihashing.createHash = function (alg) { if (!alg) { - throw errcode(new Error('hash algorithm must be specified'), 'ERR_HASH_ALGORITHM_NOT_SPECIFIED') + const e = errcode(new Error('hash algorithm must be specified'), 'ERR_HASH_ALGORITHM_NOT_SPECIFIED') + throw e } - alg = multihash.coerceCode(alg) - if (!Multihashing.functions[alg]) { + const code = multihash.coerceCode(alg) + if (!Multihashing.functions[code]) { throw errcode(new Error(`multihash function '${alg}' not yet supported`), 'ERR_HASH_ALGORITHM_NOT_SUPPORTED') } - return Multihashing.functions[alg] + return Multihashing.functions[code] } /** * Mapping of multihash codes to their hashing functions. - * @type {Object} + * + * @type {Record} */ +// @ts-ignore - most of those functions aren't typed Multihashing.functions = { // identity 0x00: crypto.identity, @@ -98,8 +106,13 @@ Multihashing.functions = { // add blake functions crypto.addBlake(Multihashing.functions) -Multihashing.validate = async (buf, hash) => { - const newHash = await Multihashing(buf, multihash.decode(hash).name) +/** + * @param {Uint8Array} bytes + * @param {Uint8Array} hash + * @returns {Promise} + */ +Multihashing.validate = async (bytes, hash) => { + const newHash = await Multihashing(bytes, multihash.decode(hash).name) return equals(hash, newHash) } diff --git a/src/sha.browser.js b/src/sha.browser.js index ad4dac0a..329e17cc 100644 --- a/src/sha.browser.js +++ b/src/sha.browser.js @@ -2,11 +2,28 @@ 'use strict' const multihash = require('multihashes') +/** + * @typedef {import('multihashes').HashName} HashName + * @typedef {import('./types').Digest} Digest + */ -const crypto = self.crypto || self.msCrypto +/** + * @type {Crypto} + */ +const crypto = + self.crypto || + /** @type {typeof window.crypto} */ + // @ts-ignore - unknown property + (self.msCrypto) +/** + * + * @param {Uint8Array} data + * @param {HashName} alg + * @returns {Promise} + */ const digest = async (data, alg) => { - if (typeof self === 'undefined' || (!self.crypto && !self.msCrypto)) { + if (typeof self === 'undefined' || !crypto) { throw new Error( 'Please use a browser with webcrypto support and ensure the code has been delivered securely via HTTPS/TLS and run within a Secure Context' ) @@ -28,12 +45,21 @@ const digest = async (data, alg) => { } module.exports = { + /** + * @param {HashName} alg + * @returns {Digest} + */ factory: (alg) => async (data) => { return digest(data, alg) }, digest, + /** + * @param {Uint8Array} buf + * @param {HashName} alg + * @param {number} [length] + */ multihashing: async (buf, alg, length) => { - const h = await digest(buf, alg, length) + const h = await digest(buf, alg) return multihash.encode(h, alg, length) } } diff --git a/src/sha.js b/src/sha.js index ec499eb8..7f8c6389 100644 --- a/src/sha.js +++ b/src/sha.js @@ -3,10 +3,21 @@ const crypto = require('crypto') const multihash = require('multihashes') +/** + * @typedef {import('multihashes').HashName} HashName + * @typedef {import('./types').Digest} Digest + */ + // Note that although this function doesn't do any asynchronous work, we mark // the function as async because it must return a Promise to match the API // for other functions that do perform asynchronous work (see sha.browser.js) // eslint-disable-next-line + +/** + * @param {Uint8Array} data + * @param {HashName} alg + * @returns {Promise} + */ const digest = async (data, alg) => { switch (alg) { case 'sha1': @@ -25,12 +36,21 @@ const digest = async (data, alg) => { } module.exports = { + /** + * @param {HashName} alg + * @returns {Digest} + */ factory: (alg) => async (data) => { return digest(data, alg) }, digest, + /** + * @param {Uint8Array} buf + * @param {HashName} alg + * @param {number} [length] + */ multihashing: async (buf, alg, length) => { - const h = await digest(buf, alg, length) + const h = await digest(buf, alg) return multihash.encode(h, alg, length) } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..d20430c4 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,3 @@ +export interface Digest { + (data: Uint8Array): Promise | Uint8Array +} diff --git a/src/utils.js b/src/utils.js index e8a65c5e..e3b753cf 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,9 @@ 'use strict' +/** + * @param {number} number + * @returns {Uint8Array} + */ const fromNumberTo32BitBuf = (number) => { const bytes = new Uint8Array(4) diff --git a/test/fixtures/encodes.js b/test/fixtures/encodes.js index 5397fd7b..5633604d 100644 --- a/test/fixtures/encodes.js +++ b/test/fixtures/encodes.js @@ -1,5 +1,8 @@ 'use strict' +/** + * @type {Array<[string, import('multihashes').HashName, string]>} + */ module.exports = [[ 'beep boop', 'identity', diff --git a/test/index.spec.js b/test/index.spec.js index 4cd35efe..93c2c307 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -8,6 +8,9 @@ const uint8ArrayToString = require('uint8arrays/to-string') const multihashing = require('../src') const fixtures = require('./fixtures/encodes') +/** + * @typedef {import('multihashes').HashName} HashName + */ describe('multihashing', () => { for (const fixture of fixtures) { @@ -61,6 +64,10 @@ describe('error handling', () => { const methods = { multihashing: multihashing, digest: multihashing.digest, + /** + * @param {Uint8Array} buff + * @param {HashName} alg + */ createHash: (buff, alg) => multihashing.createHash(alg) } @@ -70,6 +77,7 @@ describe('error handling', () => { const buf = uint8ArrayFromString('beep boop') try { + // @ts-expect-error - alg argument is expected await fn(buf) } catch (err) { expect(err).to.exist() @@ -82,8 +90,10 @@ describe('error handling', () => { it('throws an error when the hashing algorithm is not supported', async () => { const buf = uint8ArrayFromString('beep boop') + // @ts-ignore - sinon is inferring that snake-oil isn't a valid alg const stub = sinon.stub(require('multihashes'), 'coerceCode').returns('snake-oil') try { + // @ts-expect-error - non valid algorithm await fn(buf, 'snake-oil') } catch (err) { expect(err).to.exist() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..05372eb7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "test", + "src" + ] +}