From 89cc2ef5234835c82ea29ff54a4887d630921ae3 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Fri, 18 Feb 2022 16:27:47 +0200 Subject: [PATCH] feat: convert to typescript (#32) - Converts to typescript - Only named exports - No more CJS, only ESM - Runs tests on all supported environments - Adds auto-publish - Adds dependabot BREAKING CHANGE: switch to named exports, ESM only --- .github/dependabot.yml | 8 + LICENSE | 23 +- LICENSE-APACHE | 5 + LICENSE-MIT | 19 ++ README.md | 23 +- package.json | 197 +++++++++++++----- src/index.js | 7 - src/{record/index.js => index.ts} | 56 +++-- src/{record => }/record.d.ts | 0 src/{record => }/record.js | 12 +- src/{record => }/record.proto | 0 src/{selection.js => selectors.ts} | 31 +-- src/selectors/index.js | 5 - src/selectors/public-key.js | 15 -- src/{utils.js => utils.ts} | 16 +- src/validator.js | 42 ---- .../public-key.js => validators.ts} | 44 +++- src/validators/index.js | 5 - test/fixtures/go-key-records.js | 9 - test/fixtures/go-key-records.ts | 5 + test/fixtures/{go-record.js => go-record.ts} | 19 +- test/{record.spec.js => record.spec.ts} | 20 +- test/{selection.spec.js => selection.spec.ts} | 16 +- test/utils.spec.js | 48 ----- test/utils.spec.ts | 47 +++++ test/validator.spec.js | 133 ------------ test/validator.spec.ts | 143 +++++++++++++ tsconfig.json | 9 +- 28 files changed, 512 insertions(+), 445 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT delete mode 100644 src/index.js rename src/{record/index.js => index.ts} (52%) rename src/{record => }/record.d.ts (100%) rename src/{record => }/record.js (95%) rename src/{record => }/record.proto (100%) rename src/{selection.js => selectors.ts} (53%) delete mode 100644 src/selectors/index.js delete mode 100644 src/selectors/public-key.js rename src/{utils.js => utils.ts} (86%) delete mode 100644 src/validator.js rename src/{validators/public-key.js => validators.ts} (50%) delete mode 100644 src/validators/index.js delete mode 100644 test/fixtures/go-key-records.js create mode 100644 test/fixtures/go-key-records.ts rename test/fixtures/{go-record.js => go-record.ts} (51%) rename test/{record.spec.js => record.spec.ts} (63%) rename test/{selection.spec.js => selection.spec.ts} (76%) delete mode 100644 test/utils.spec.js create mode 100644 test/utils.spec.ts delete mode 100644 test/validator.spec.js create mode 100644 test/validator.spec.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..290ad02 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "10:00" + open-pull-requests-limit: 10 diff --git a/LICENSE b/LICENSE index bbfffbf..20ce483 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,4 @@ -MIT License +This project is dual licensed under MIT and Apache-2.0. -Copyright (c) 2017 libp2p - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..14478a3 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..72dc60d --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index ac9141e..32c4fd7 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,13 @@ [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) [![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-record/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-record?branch=master) -[![Travis CI](https://travis-ci.org/libp2p/js-libp2p-record.svg?branch=master)](https://travis-ci.org/libp2p/js-libp2p-record) -[![Circle CI](https://circleci.com/gh/libp2p/js-libp2p-record.svg?style=svg)](https://circleci.com/gh/libp2p/js-libp2p-record) +[![Build Status](https://github.com/libp2p/js-libp2p-record/actions/workflows/js-test-and-release.yml/badge.svg?branch=main)](https://github.com/libp2p/js-libp2p-record/actions/workflows/js-test-and-release.yml) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-record.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-record) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) ![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square) ![](https://img.shields.io/badge/Node.js-%3E%3D6.0.0-orange.svg?style=flat-square) > JavaScript implementation of libp2p record. -## Lead Maintainer - -[Vasco Santos](https://github.com/vasco-santos). - ## Description Implementation of [go-libp2p-record](https://github.com/libp2p/go-libp2p-record) in JavaScript. @@ -26,6 +21,20 @@ Implementation of [go-libp2p-record](https://github.com/libp2p/go-libp2p-record) See https://libp2p.github.io/js-libp2p-record/ +## Contribute + +The libp2p implementation in JavaScript is a work in progress. As such, there are a few things you can do right now to help out: + + - Go through the modules and **check out existing issues**. This is especially useful for modules in active development. Some knowledge of IPFS/libp2p may be required, as well as the infrastructure behind it - for instance, you may need to read up on p2p and more complex operations like muxing to be able to help technically. + - **Perform code reviews**. More eyes will help a) speed the project along b) ensure quality and c) reduce possible future bugs. + ## License -MIT +Licensed under either of + + * Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0) + * MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT) + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/package.json b/package.json index 2fe586b..d53ab58 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,164 @@ { - "name": "libp2p-record", + "name": "@libp2p/record", "version": "0.10.6", "description": "libp2p record implementation", - "leadMaintainer": "Vasco Santos ", - "main": "src/index.js", - "scripts": { - "test": "aegir test", - "lint": "aegir lint", - "test:node": "aegir test -t node", - "test:browser": "aegir test -t browser -t webworker", - "prepare": "npm run build", - "build": "run-s build:*", - "build:types": "aegir build --no-bundle", - "build:proto": "pbjs -t static-module -w commonjs -r libp2p-record --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/record.js ./src/record/record.proto", - "build:proto-types": "pbts -o src/record/record.d.ts src/record/record.js", - "docs": "aegir docs", - "release": "aegir release", - "release-minor": "aegir release --type minor", - "release-major": "aegir release --type major", - "coverage": "aegir coverage" - }, + "author": "Friedel Ziegelmayer ", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-record#readme", "repository": { "type": "git", - "url": "https://github.com/libp2p/js-libp2p-record.git" + "url": "git+https://github.com/libp2p/js-libp2p-record.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-record/issues" }, "keywords": [ "IPFS" ], "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0", + "npm": ">=7.0.0" }, - "pre-push": [ - "lint" - ], - "author": "Friedel Ziegelmayer ", - "license": "MIT", - "bugs": { - "url": "https://github.com/libp2p/js-libp2p-record/issues" + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } }, - "homepage": "https://github.com/libp2p/js-libp2p-record", "files": [ "src", - "dist" + "dist/src", + "!dist/test", + "!**/*.tsbuildinfo" ], + "exports": { + ".": { + "import": "./dist/src/index.js" + }, + "./selectors": { + "import": "./dist/src/selectors.js" + }, + "./validators": { + "import": "./dist/src/validators.js" + } + }, "eslintConfig": { "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, "ignorePatterns": [ - "src/record/record.d.ts" + "src/record.d.ts" ] }, - "types": "dist/src/index.d.ts", - "devDependencies": { - "aegir": "^35.0.1", - "libp2p-crypto": "^0.19.6", - "libp2p-interfaces": "^1.0.1", - "npm-run-all": "^4.1.5", - "peer-id": "^0.15.2", - "util": "^0.12.4" + "release": { + "branches": [ + "master" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "chore", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Trivial Changes" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "lint": "aegir lint", + "pretest": "npm run build", + "test": "aegir test -f ./dist/test", + "test:node": "npm run test -- -t node", + "test:chrome": "npm run test -- -t browser", + "test:chrome-webworker": "npm run test -- -t webworker", + "test:firefox": "npm run test -- -t browser -- --browser firefox", + "test:firefox-webworker": "npm run test -- -t webworker -- --browser firefox", + "build": "tsc", + "postbuild": "npm run build:copy-proto-files", + "generate:proto": "pbjs -t static-module -w es6 -r libp2p-record --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/record.js ./src/record/record.proto", + "generate:proto-types": "pbts -o src/record/record.d.ts src/record/record.js", + "build:copy-proto-files": "cp src/record* dist/src", + "release": "semantic-release" }, "dependencies": { "err-code": "^3.0.1", @@ -64,16 +166,9 @@ "protobufjs": "^6.11.2", "uint8arrays": "^3.0.0" }, - "contributors": [ - "Vasco Santos ", - "David Dias ", - "Alex Potsides ", - "Hugo Dias ", - "Jacob Heun ", - "Friedel Ziegelmayer ", - "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ ", - "Matt Joiner ", - "dirkmc ", - "Alan Shaw " - ] + "devDependencies": { + "@libp2p/crypto": "^0.22.7", + "@libp2p/interfaces": "^1.3.3", + "aegir": "^36.1.3" + } } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 1d7a4e1..0000000 --- a/src/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports = { - Record: require('./record'), - validator: require('./validator'), - selection: require('./selection') -} diff --git a/src/record/index.js b/src/index.ts similarity index 52% rename from src/record/index.js rename to src/index.ts index 04de40b..660ccfa 100644 --- a/src/record/index.js +++ b/src/index.ts @@ -1,21 +1,15 @@ -'use strict' +import { + IRecord, + Record as PBRecord +} from './record.js' +import * as utils from './utils.js' -const { - Record: PBRecord -} = require('./record') -const utils = require('../utils') +export class Libp2pRecord { + public key: Uint8Array + public value: Uint8Array + public timeReceived?: Date -/** - * @typedef {{ key: Uint8Array, value: Uint8Array, timeReceived: string }} ProtobufRecord - */ - -class Record { - /** - * @param {Uint8Array} [key] - * @param {Uint8Array} [value] - * @param {Date} [timeReceived] - */ - constructor (key, value, timeReceived) { + constructor (key: Uint8Array, value: Uint8Array, timeReceived?: Date) { if (!(key instanceof Uint8Array)) { throw new Error('key must be a Uint8Array') } @@ -40,18 +34,16 @@ class Record { return { key: this.key, value: this.value, - timeReceived: this.timeReceived && utils.toRFC3339(this.timeReceived) + timeReceived: this.timeReceived != null ? utils.toRFC3339(this.timeReceived) : undefined } } /** - * Decode a protobuf encoded record. - * - * @param {Uint8Array} raw + * Decode a protobuf encoded record */ - static deserialize (raw) { + static deserialize (raw: Uint8Array) { const message = PBRecord.decode(raw) - return Record.fromDeserialized(PBRecord.toObject(message, { + return Libp2pRecord.fromDeserialized(PBRecord.toObject(message, { defaults: false, arrays: true, longs: Number, @@ -60,22 +52,26 @@ class Record { } /** - * Create a record from the raw object returned from the protobuf library. - * - * @param {{ [k: string]: any }} obj + * Create a record from the raw object returned from the protobuf library */ - static fromDeserialized (obj) { + static fromDeserialized (obj: IRecord) { let recvtime - if (obj.timeReceived) { + if (obj.timeReceived != null) { recvtime = utils.parseRFC3339(obj.timeReceived) } - const rec = new Record( + if (obj.key == null) { + throw new Error('key missing from deserialized object') + } + + if (obj.value == null) { + throw new Error('value missing from deserialized object') + } + + const rec = new Libp2pRecord( obj.key, obj.value, recvtime ) return rec } } - -module.exports = Record diff --git a/src/record/record.d.ts b/src/record.d.ts similarity index 100% rename from src/record/record.d.ts rename to src/record.d.ts diff --git a/src/record/record.js b/src/record.js similarity index 95% rename from src/record/record.js rename to src/record.js index 02c2dbb..200703c 100644 --- a/src/record/record.js +++ b/src/record.js @@ -1,15 +1,13 @@ /*eslint-disable*/ -"use strict"; - -var $protobuf = require("protobufjs/minimal"); +import $protobuf from "protobufjs/minimal.js"; // Common aliases -var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; +const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; // Exported root namespace -var $root = $protobuf.roots["libp2p-record"] || ($protobuf.roots["libp2p-record"] = {}); +const $root = $protobuf.roots["libp2p-record"] || ($protobuf.roots["libp2p-record"] = {}); -$root.Record = (function() { +export const Record = $root.Record = (() => { /** * Properties of a Record. @@ -201,4 +199,4 @@ $root.Record = (function() { return Record; })(); -module.exports = $root; +export { $root as default }; diff --git a/src/record/record.proto b/src/record.proto similarity index 100% rename from src/record/record.proto rename to src/record.proto diff --git a/src/selection.js b/src/selectors.ts similarity index 53% rename from src/selection.js rename to src/selectors.ts index 939ae03..5bc2024 100644 --- a/src/selection.js +++ b/src/selectors.ts @@ -1,16 +1,11 @@ -'use strict' - -const errcode = require('err-code') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') +import errcode from 'err-code' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Selectors } from '@libp2p/interfaces/dht' /** - * Select the best record out of the given records. - * - * @param {import('libp2p-interfaces/src/types').DhtSelectors} selectors - * @param {Uint8Array} k - * @param {Array} records + * Select the best record out of the given records */ -const bestRecord = (selectors, k, records) => { +export function bestRecord (selectors: Selectors, k: Uint8Array, records: Uint8Array[]) { if (records.length === 0) { const errMsg = 'No records given' @@ -28,7 +23,7 @@ const bestRecord = (selectors, k, records) => { const selector = selectors[parts[1].toString()] - if (!selector) { + if (selector == null) { const errMsg = `Unrecognized key prefix: ${parts[1]}` throw errcode(new Error(errMsg), 'ERR_UNRECOGNIZED_KEY_PREFIX') @@ -41,7 +36,15 @@ const bestRecord = (selectors, k, records) => { return selector(k, records) } -module.exports = { - bestRecord: bestRecord, - selectors: require('./selectors') +/** + * Best record selector, for public key records. + * Simply returns the first record, as all valid public key + * records are equal + */ +function publickKey (k: Uint8Array, records: Uint8Array[]) { + return 0 +} + +export const selectors = { + publickKey } diff --git a/src/selectors/index.js b/src/selectors/index.js deleted file mode 100644 index d11b25d..0000000 --- a/src/selectors/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = { - pk: require('./public-key') -} diff --git a/src/selectors/public-key.js b/src/selectors/public-key.js deleted file mode 100644 index ee62ef3..0000000 --- a/src/selectors/public-key.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -/** - * Best record selector, for public key records. - * Simply returns the first record, as all valid public key - * records are equal. - * - * @param {Uint8Array} k - * @param {Array} records - */ -const publicKeySelector = (k, records) => { - return 0 -} - -module.exports = publicKeySelector diff --git a/src/utils.js b/src/utils.ts similarity index 86% rename from src/utils.js rename to src/utils.ts index b9bd032..ddf5b64 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,12 +1,8 @@ -'use strict' - /** * Convert a JavaScript date into an `RFC3339Nano` formatted - * string. - * - * @param {Date} time + * string */ -module.exports.toRFC3339 = (time) => { +export function toRFC3339 (time: Date) { const year = time.getUTCFullYear() const month = String(time.getUTCMonth() + 1).padStart(2, '0') const day = String(time.getUTCDate()).padStart(2, '0') @@ -21,11 +17,9 @@ module.exports.toRFC3339 = (time) => { /** * Parses a date string formatted as `RFC3339Nano` into a - * JavaScript Date object. - * - * @param {string} time + * JavaScript Date object */ -module.exports.parseRFC3339 = (time) => { +export function parseRFC3339 (time: string) { const rfc3339Matcher = new RegExp( // 2006-01-02T '(\\d{4})-(\\d{2})-(\\d{2})T' + @@ -36,7 +30,7 @@ module.exports.parseRFC3339 = (time) => { ) const m = String(time).trim().match(rfc3339Matcher) - if (!m) { + if (m == null) { throw new Error('Invalid format') } diff --git a/src/validator.js b/src/validator.js deleted file mode 100644 index 1e6fc36..0000000 --- a/src/validator.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' - -const errcode = require('err-code') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') - -/** - * @typedef {import('./record')} Record - */ - -/** - * Checks a record and ensures it is still valid. - * It runs the needed validators. - * If verification fails the returned Promise will reject with the error. - * - * @param {import('libp2p-interfaces/src/types').DhtValidators} validators - * @param {Record} record - */ -const verifyRecord = (validators, record) => { - const key = record.key - const keyString = uint8ArrayToString(key) - const parts = keyString.split('/') - - if (parts.length < 3) { - // No validator available - return - } - - const validator = validators[parts[1].toString()] - - if (!validator) { - const errMsg = 'Invalid record keytype' - - throw errcode(new Error(errMsg), 'ERR_INVALID_RECORD_KEY_TYPE') - } - - return validator.func(key, record.value) -} - -module.exports = { - verifyRecord: verifyRecord, - validators: require('./validators') -} diff --git a/src/validators/public-key.js b/src/validators.ts similarity index 50% rename from src/validators/public-key.js rename to src/validators.ts index 7ca4d7f..a30f499 100644 --- a/src/validators/public-key.js +++ b/src/validators.ts @@ -1,9 +1,35 @@ -'use strict' +import errcode from 'err-code' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Libp2pRecord } from './index.js' +import type { Validators } from '@libp2p/interfaces/dht' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' -const { sha256 } = require('multiformats/hashes/sha2') -const errcode = require('err-code') -const { toString: uint8ArrayToString } = require('uint8arrays/to-string') -const { equals: uint8ArrayEquals } = require('uint8arrays/equals') +/** + * Checks a record and ensures it is still valid. + * It runs the needed validators. + * If verification fails the returned Promise will reject with the error. + */ +export function verifyRecord (validators: Validators, record: Libp2pRecord) { + const key = record.key + const keyString = uint8ArrayToString(key) + const parts = keyString.split('/') + + if (parts.length < 3) { + // No validator available + return + } + + const validator = validators[parts[1].toString()] + + if (validator == null) { + const errMsg = 'Invalid record keytype' + + throw errcode(new Error(errMsg), 'ERR_INVALID_RECORD_KEY_TYPE') + } + + return validator.func(key, record.value) +} /** * Validator for public key records. @@ -14,7 +40,7 @@ const { equals: uint8ArrayEquals } = require('uint8arrays/equals') * @param {Uint8Array} key - A valid key is of the form `'/pk/'` * @param {Uint8Array} publicKey - The public key to validate against (protobuf encoded). */ -const validatePublicKeyRecord = async (key, publicKey) => { +const validatePublicKeyRecord = async (key: Uint8Array, publicKey: Uint8Array) => { if (!(key instanceof Uint8Array)) { throw errcode(new Error('"key" must be a Uint8Array'), 'ERR_INVALID_RECORD_KEY_NOT_BUFFER') } @@ -38,7 +64,11 @@ const validatePublicKeyRecord = async (key, publicKey) => { } } -module.exports = { +const publicKey = { func: validatePublicKeyRecord, sign: false } + +export const validators = { + publicKey +} diff --git a/src/validators/index.js b/src/validators/index.js deleted file mode 100644 index d11b25d..0000000 --- a/src/validators/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = { - pk: require('./public-key') -} diff --git a/test/fixtures/go-key-records.js b/test/fixtures/go-key-records.js deleted file mode 100644 index bf5244f..0000000 --- a/test/fixtures/go-key-records.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const { base64pad } = require('multiformats/bases/base64') - -module.exports = { - publicKey: base64pad.decode( - 'MCAASXjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDjXAQQMal4SB2tSnX6NJIPmC69/BT8A8jc7/gDUZNkEhdhYHvc7k7S4vntV/c92nJGxNdop9fKJyevuNMuXhhHAgMBAAE=' - ) -} diff --git a/test/fixtures/go-key-records.ts b/test/fixtures/go-key-records.ts new file mode 100644 index 0000000..0bba87e --- /dev/null +++ b/test/fixtures/go-key-records.ts @@ -0,0 +1,5 @@ +import { base64pad } from 'multiformats/bases/base64' + +export const publicKey = base64pad.decode( + 'MCAASXjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDjXAQQMal4SB2tSnX6NJIPmC69/BT8A8jc7/gDUZNkEhdhYHvc7k7S4vntV/c92nJGxNdop9fKJyevuNMuXhhHAgMBAAE=' +) diff --git a/test/fixtures/go-record.js b/test/fixtures/go-record.ts similarity index 51% rename from test/fixtures/go-record.js rename to test/fixtures/go-record.ts index 6bef8c6..5582f81 100644 --- a/test/fixtures/go-record.js +++ b/test/fixtures/go-record.ts @@ -1,6 +1,4 @@ -'use strict' - -const { base16 } = require('multiformats/bases/base16') +import { base16 } from 'multiformats/bases/base16' // Fixtures generated using gore (https://github.com/motemen/gore) // @@ -19,11 +17,10 @@ const { base16 } = require('multiformats/bases/base16') // :import io/ioutil // ioutil.WriteFile("js-libp2p-record/test/fixtures/record.bin", enc, 0644) // ioutil.WriteFile("js-libp2p-record/test/fixtures/record-signed.bin", enc2, 0644) -module.exports = { - serialized: base16.decode( - 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116' - ), - serializedSigned: base16.decode( - 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116228001500fe7505698b8a873ccde6f1d36a2be662d57807490d9a9959540f2645a454bf615215092e10123f6ffc4ed694711bfbb1d5ccb62f3da83cf4528ee577a96b6cf0272eef9a920bd56459993690060353b72c22b8c03ad2a33894522dac338905b201179a85cb5e2fc68ed58be96cf89beec6dc0913887dddc10f202a2a1b117' - ) -} +export const serialized = base16.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116' +) + +export const serializedSigned = base16.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116228001500fe7505698b8a873ccde6f1d36a2be662d57807490d9a9959540f2645a454bf615215092e10123f6ffc4ed694711bfbb1d5ccb62f3da83cf4528ee577a96b6cf0272eef9a920bd56459993690060353b72c22b8c03ad2a33894522dac338905b201179a85cb5e2fc68ed58be96cf89beec6dc0913887dddc10f202a2a1b117' +) diff --git a/test/record.spec.js b/test/record.spec.ts similarity index 63% rename from test/record.spec.js rename to test/record.spec.ts index 35db942..c8ed0e3 100644 --- a/test/record.spec.js +++ b/test/record.spec.ts @@ -1,18 +1,14 @@ /* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const libp2pRecord = require('../src') -const Record = libp2pRecord.Record - -const fixture = require('./fixtures/go-record.js') +import { expect } from 'aegir/utils/chai.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Libp2pRecord } from '../src/index.js' +import * as fixture from './fixtures/go-record.js' const date = new Date(Date.UTC(2012, 1, 25, 10, 10, 10, 10)) describe('record', () => { it('new', () => { - const rec = new Record( + const rec = new Libp2pRecord( uint8ArrayFromString('hello'), uint8ArrayFromString('world') ) @@ -22,8 +18,8 @@ describe('record', () => { }) it('serialize & deserialize', () => { - const rec = new Record(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), date) - const dec = Record.deserialize(rec.serialize()) + const rec = new Libp2pRecord(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), date) + const dec = Libp2pRecord.deserialize(rec.serialize()) expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) @@ -32,7 +28,7 @@ describe('record', () => { describe('go interop', () => { it('no signature', () => { - const dec = Record.deserialize(fixture.serialized) + const dec = Libp2pRecord.deserialize(fixture.serialized) expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) }) diff --git a/test/selection.spec.js b/test/selection.spec.ts similarity index 76% rename from test/selection.spec.js rename to test/selection.spec.ts index 2f27823..22d6555 100644 --- a/test/selection.spec.js +++ b/test/selection.spec.ts @@ -1,11 +1,10 @@ /* eslint max-nested-callbacks: ["error", 8] */ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const libp2pRecord = require('../src') -const selection = libp2pRecord.selection +import { expect } from 'aegir/utils/chai.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as selection from '../src/selectors.js' +import type { Selectors } from '@libp2p/interfaces/dht' const records = [new Uint8Array(), uint8ArrayFromString('hello')] @@ -29,6 +28,7 @@ describe('selection', () => { it('throws on unknown key prefix', () => { expect( + // @ts-expect-error invalid input () => selection.bestRecord({ world () {} }, uint8ArrayFromString('/hello/'), records) ).to.throw( /Unrecognized key prefix: hello/ @@ -36,7 +36,7 @@ describe('selection', () => { }) it('returns the index from the matching selector', () => { - const selectors = { + const selectors: Selectors = { hello (k, recs) { expect(k).to.be.eql(uint8ArrayFromString('/hello/world')) expect(recs).to.be.eql(records) @@ -56,7 +56,7 @@ describe('selection', () => { describe('selectors', () => { it('public key', () => { expect( - selection.selectors.pk(uint8ArrayFromString('/hello/world'), records) + selection.selectors.publickKey(uint8ArrayFromString('/hello/world'), records) ).to.equal( 0 ) @@ -64,7 +64,7 @@ describe('selection', () => { it('returns the first record when there is only one to select', () => { expect( - selection.selectors.pk(uint8ArrayFromString('/hello/world'), [records[0]]) + selection.selectors.publickKey(uint8ArrayFromString('/hello/world'), [records[0]]) ).to.equal( 0 ) diff --git a/test/utils.spec.js b/test/utils.spec.js deleted file mode 100644 index 89e6799..0000000 --- a/test/utils.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const utils = require('../src/utils') - -const dates = [[ - new Date(Date.UTC(2016, 0, 1, 8, 22, 33, 392)), - '2016-01-01T08:22:33.392000000Z' -], [ - new Date(Date.UTC(2016, 11, 30, 20, 2, 3, 392)), - '2016-12-30T20:02:03.392000000Z' -], [ - new Date(Date.UTC(2016, 11, 30, 20, 2, 5, 297)), - '2016-12-30T20:02:05.297000000Z' -], [ - new Date(Date.UTC(2012, 1, 25, 10, 10, 10, 10)), - '2012-02-25T10:10:10.10000000Z' -]] - -describe('utils', () => { - it('toRFC3339', () => { - dates.forEach((c) => { - expect(utils.toRFC3339(c[0])).to.be.eql(c[1]) - }) - }) - - it('parseRFC3339', () => { - dates.forEach((c) => { - expect(utils.parseRFC3339(c[1])).to.be.eql(c[0]) - }) - }) - - it('to and from RFC3339', () => { - dates.forEach((c) => { - expect( - utils.parseRFC3339(utils.toRFC3339(c[0])) - ).to.be.eql( - c[0] - ) - expect( - utils.toRFC3339(utils.parseRFC3339(c[1])) - ).to.be.eql( - c[1] - ) - }) - }) -}) diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 0000000..9811730 --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,47 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/utils/chai.js' +import * as utils from '../src/utils.js' + +const dates = [{ + obj: new Date(Date.UTC(2016, 0, 1, 8, 22, 33, 392)), + str: '2016-01-01T08:22:33.392000000Z' +}, { + obj: new Date(Date.UTC(2016, 11, 30, 20, 2, 3, 392)), + str: '2016-12-30T20:02:03.392000000Z' +}, { + obj: new Date(Date.UTC(2016, 11, 30, 20, 2, 5, 297)), + str: '2016-12-30T20:02:05.297000000Z' +}, { + obj: new Date(Date.UTC(2012, 1, 25, 10, 10, 10, 10)), + str: '2012-02-25T10:10:10.10000000Z' +}] + +describe('utils', () => { + it('toRFC3339', () => { + dates.forEach((c) => { + expect(utils.toRFC3339(c.obj)).to.be.eql(c.str) + }) + }) + + it('parseRFC3339', () => { + dates.forEach((c) => { + expect(utils.parseRFC3339(c.str)).to.be.eql(c.obj) + }) + }) + + it('to and from RFC3339', () => { + dates.forEach((c) => { + expect( + utils.parseRFC3339(utils.toRFC3339(c.obj)) + ).to.be.eql( + c.obj + ) + expect( + utils.toRFC3339(utils.parseRFC3339(c.str)) + ).to.be.eql( + c.str + ) + }) + }) +}) diff --git a/test/validator.spec.js b/test/validator.spec.js deleted file mode 100644 index 2f7cf44..0000000 --- a/test/validator.spec.js +++ /dev/null @@ -1,133 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const crypto = require('libp2p-crypto') -const PeerId = require('peer-id') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const libp2pRecord = require('../src') -const validator = libp2pRecord.validator -const Record = libp2pRecord.Record - -const fixture = require('./fixtures/go-key-records.js') - -const generateCases = (hash) => { - return { - valid: { - publicKey: [ - Uint8Array.of( - ...uint8ArrayFromString('/pk/'), - ...hash - ) - ] - }, - invalid: { - publicKey: [ - // missing hashkey - [uint8ArrayFromString('/pk/'), 'ERR_INVALID_RECORD_KEY_TOO_SHORT'], - // not the hash of a key - [Uint8Array.of(...uint8ArrayFromString('/pk/'), - ...uint8ArrayFromString('random') - ), 'ERR_INVALID_RECORD_HASH_MISMATCH'], - // missing prefix - [hash, 'ERR_INVALID_RECORD_KEY_BAD_PREFIX'], - // not a buffer - ['not a buffer', 'ERR_INVALID_RECORD_KEY_NOT_BUFFER'] - ] - } - } -} - -describe('validator', () => { - let key - let hash - let cases - - before(async () => { - key = await crypto.keys.generateKeyPair('rsa', 1024) - hash = await key.public.hash() - cases = generateCases(hash) - }) - - describe('verifyRecord', () => { - it('calls matching validator', () => { - const k = uint8ArrayFromString('/hello/you') - const rec = new Record(k, uint8ArrayFromString('world'), new PeerId(hash)) - - const validators = { - hello: { - func (key, value) { - expect(key).to.eql(k) - expect(value).to.eql(uint8ArrayFromString('world')) - }, - sign: false - } - } - return validator.verifyRecord(validators, rec) - }) - - it('calls not matching any validator', () => { - const k = uint8ArrayFromString('/hallo/you') - const rec = new Record(k, uint8ArrayFromString('world'), new PeerId(hash)) - - const validators = { - hello: { - func (key, value) { - expect(key).to.eql(k) - expect(value).to.eql(uint8ArrayFromString('world')) - }, - sign: false - } - } - return expect( - () => validator.verifyRecord(validators, rec) - ).to.throw( - /Invalid record keytype/ - ) - }) - }) - - describe('validators', () => { - it('exports pk', () => { - expect(validator.validators).to.have.keys(['pk']) - }) - - describe('public key', () => { - it('exports func and sign', () => { - const pk = validator.validators.pk - - expect(pk).to.have.property('func') - expect(pk).to.have.property('sign', false) - }) - - it('does not error on valid record', () => { - return Promise.all(cases.valid.publicKey.map((k) => { - return validator.validators.pk.func(k, key.public.bytes) - })) - }) - - it('throws on invalid records', () => { - return Promise.all(cases.invalid.publicKey.map(async ([k, errCode]) => { - try { - await validator.validators.pk.func(k, key.public.bytes) - } catch (err) { - expect(err.code).to.eql(errCode) - return - } - expect.fail('did not throw an error with code ' + errCode) - })) - }) - }) - }) - - describe('go interop', () => { - it('record with key from from go', async () => { - const pubKey = crypto.keys.unmarshalPublicKey(fixture.publicKey) - - const hash = await pubKey.hash() - const k = Uint8Array.of(...uint8ArrayFromString('/pk/'), ...hash) - return validator.validators.pk.func(k, pubKey.bytes) - }) - }) -}) diff --git a/test/validator.spec.ts b/test/validator.spec.ts new file mode 100644 index 0000000..a33e378 --- /dev/null +++ b/test/validator.spec.ts @@ -0,0 +1,143 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import { expect } from 'aegir/utils/chai.js' +import { generateKeyPair, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as validator from '../src/validators.js' +import { Libp2pRecord } from '../src/index.js' +import * as fixture from './fixtures/go-key-records.js' +import type { Validators } from '@libp2p/interfaces/dht' + +interface Cases { + valid: { + publicKey: Uint8Array[] + } + invalid: { + publicKey: Array<{ + data: Uint8Array + code: string + }> + } +} + +const generateCases = (hash: Uint8Array): Cases => { + return { + valid: { + publicKey: [ + Uint8Array.of( + ...uint8ArrayFromString('/pk/'), + ...hash + ) + ] + }, + invalid: { + publicKey: [{ + data: uint8ArrayFromString('/pk/'), + code: 'ERR_INVALID_RECORD_KEY_TOO_SHORT' + }, { + data: Uint8Array.of(...uint8ArrayFromString('/pk/'), ...uint8ArrayFromString('random')), + code: 'ERR_INVALID_RECORD_HASH_MISMATCH' + }, { + data: hash, + code: 'ERR_INVALID_RECORD_KEY_BAD_PREFIX' + }, { + // @ts-expect-error invalid input + data: 'not a buffer', + code: 'ERR_INVALID_RECORD_KEY_NOT_BUFFER' + }] + } + } +} + +describe('validator', () => { + let key: any + let hash: Uint8Array + let cases: Cases + + before(async () => { + key = await generateKeyPair('RSA', 1024) + hash = await key.public.hash() + cases = generateCases(hash) + }) + + describe('verifyRecord', () => { + it('calls matching validator', () => { + const k = uint8ArrayFromString('/hello/you') + const rec = new Libp2pRecord(k, uint8ArrayFromString('world')) + + const validators: Validators = { + hello: { + async func (key, value) { + expect(key).to.eql(k) + expect(value).to.eql(uint8ArrayFromString('world')) + } + } + } + return validator.verifyRecord(validators, rec) + }) + + it('calls not matching any validator', () => { + const k = uint8ArrayFromString('/hallo/you') + const rec = new Libp2pRecord(k, uint8ArrayFromString('world')) + + const validators: Validators = { + hello: { + async func (key, value) { + expect(key).to.eql(k) + expect(value).to.eql(uint8ArrayFromString('world')) + } + } + } + return expect( + () => validator.verifyRecord(validators, rec) + ).to.throw( + /Invalid record keytype/ + ) + }) + }) + + describe('validators', () => { + it('exports pk', () => { + expect(validator.validators).to.have.keys(['publicKey']) + }) + + describe('public key', () => { + it('exports func and sign', () => { + const pk = validator.validators.publicKey + + expect(pk).to.have.property('func') + expect(pk).to.have.property('sign', false) + }) + + it('does not error on valid record', async () => { + return await Promise.all(cases.valid.publicKey.map(async (k) => { + return await validator.validators.publicKey.func(k, key.public.bytes) + })) + }) + + it('throws on invalid records', async () => { + return await Promise.all(cases.invalid.publicKey.map(async ({ data, code }) => { + try { + // + await validator.validators.publicKey.func(data, key.public.bytes) + } catch (err: any) { + expect(err.code).to.eql(code) + return + } + expect.fail('did not throw an error with code ' + code) + })) + }) + }) + }) + + describe('go interop', () => { + it('record with key from from go', async () => { + const pubKey = unmarshalPublicKey(fixture.publicKey) + + const hash = await pubKey.hash() + const k = Uint8Array.of(...uint8ArrayFromString('/pk/'), ...hash) + return await validator.validators.publicKey.func(k, pubKey.bytes) + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 909bb2b..a919553 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,15 @@ { "extends": "aegir/src/config/tsconfig.aegir.json", "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" }, "include": [ - "src" + "src", + "test" ], "exclude": [ - "src/record/record.js" // exclude generated file + "src/record.js" // exclude generated file ] }