Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add types #106

Merged
merged 5 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .aegir.js

This file was deleted.

16 changes: 9 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ stages:
- test
- cov

branches:
only:
- master
- /^release\/.*$/

node_js:
- 'lts/*'
- 'node'
Expand All @@ -16,7 +21,7 @@ os:

before_install:
# modules with pre-built binaries may not have deployed versions for bleeding-edge node so this lets us fall back to building from source
- npm install -g node-pre-gyp
- npm install -g @mapbox/node-pre-gyp

script: npx nyc -s npm run test:node -- --bail
after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov
Expand All @@ -25,24 +30,21 @@ jobs:
include:
- stage: check
script:
- npx aegir dep-check -- -i wrtc -i electron-webrtc
- npx aegir dep-check
- npm run lint

- stage: test
name: chrome
addons:
chrome: stable
script:
- npx aegir test -t browser
- npx aegir test -t webworker
- npx aegir test -t browser -t webworker

- stage: test
name: firefox
addons:
firefox: latest
script:
- npx aegir test -t browser -- --browsers FirefoxHeadless
- npx aegir test -t webworker -- --browsers FirefoxHeadless
script: npx aegir test -t browser -t webworker -- --browser firefox

notifications:
email: false
20 changes: 12 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
"leadMaintainer": "Vasco Santos <[email protected]>",
"main": "src/index.js",
"scripts": {
"build": "aegir build",
"prepare": "run-s prepare:*",
"prepare:proto": "pbjs -t static-module -w commonjs --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/pb/ipns.js src/pb/ipns.proto",
"prepare:proto-types": "pbts -o src/pb/ipns.d.ts src/pb/ipns.js",
"prepare:types": "aegir build --no-bundle",
"prepublishOnly": "aegir build",
achingbrain marked this conversation as resolved.
Show resolved Hide resolved
"lint": "aegir lint",
"release": "aegir release",
"release-minor": "aegir release --type minor",
Expand Down Expand Up @@ -37,22 +41,22 @@
"err-code": "^3.0.1",
"interface-datastore": "^3.0.1",
"libp2p-crypto": "^0.19.0",
"multibase": "^3.0.1",
"multihashes": "^3.0.1",
"multibase": "^4.0.2",
"multihashes": "^4.0.2",
"peer-id": "^0.14.2",
"protons": "^2.0.0",
"protobufjs": "^6.10.2",
"timestamp-nano": "^1.0.0",
"uint8arrays": "^2.0.5"
},
"devDependencies": {
"aegir": "^30.2.0",
"@types/chai-string": "^1.4.2",
"@types/debug": "^4.1.5",
"aegir": "^31.0.4",
"chai": "^4.2.0",
"chai-bytes": "~0.1.2",
"chai-string": "^1.5.0",
"dirty-chai": "^2.0.1",
"ipfs": "^0.54.2",
"ipfs-http-client": "^49.0.2",
"ipfsd-ctl": "^7.0.3"
"npm-run-all": "^4.1.5"
},
"contributors": [
"Vasco Santos <[email protected]>",
Expand Down
120 changes: 85 additions & 35 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,35 @@ const uint8ArrayToString = require('uint8arrays/to-string')
const uint8ArrayConcat = require('uint8arrays/concat')

const debug = require('debug')
const log = debug('jsipns')
log.error = debug('jsipns:error')
const log = Object.assign(debug('jsipns'), {
error: debug('jsipns:error')
})

const ipnsEntryProto = require('./pb/ipns.proto')
const {
IpnsEntry: ipnsEntryProto
} = require('./pb/ipns.js')
const { parseRFC3339 } = require('./utils')
const ERRORS = require('./errors')

const ID_MULTIHASH_CODE = multihash.names.id
const ID_MULTIHASH_CODE = multihash.names.identity
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be .id but apparently not flagged as a breaking change: multiformats/js-multihash#66

Also, somehow we don't cover this in the tests 😱


const namespace = '/ipns/'

/**
* IPNS entry
*
* @typedef {Object} IpnsEntry
* @property {string} value - value to be stored in the record
* @property {Uint8Array} signature - signature of the record
* @property {number} validityType - Type of validation being used
* @property {string} validity - expiration datetime for the record in RFC3339 format
* @property {number} sequence - number representing the version of the record
* @typedef {import('./types').IPNSEntry} IPNSEntry
* @typedef {import('libp2p-crypto').PublicKey} PublicKey
* @typedef {import('libp2p-crypto').PrivateKey} PrivateKey
*/

/**
* Creates a new ipns entry and signs it with the given private key.
* The ipns entry validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
* Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`.
*
* @param {Object} privateKey - private key for signing the record.
* @param {PrivateKey} privateKey - private key for signing the record.
* @param {string} value - value to be stored in the record.
* @param {number} seq - number representing the current version of the record.
* @param {number|string} lifetime - lifetime of the record (in milliseconds).
* @returns {Promise<IpnsEntry>} entry
*/
const create = (privateKey, value, seq, lifetime) => {
// Validity in ISOString with nanoseconds precision and validity type EOL
Expand All @@ -56,17 +53,23 @@ const create = (privateKey, value, seq, lifetime) => {
* Same as create(), but instead of generating a new Date, it receives the intended expiration time
* WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided.
*
* @param {Object} privateKey - private key for signing the record.
* @param {PrivateKey} privateKey - private key for signing the record.
* @param {string} value - value to be stored in the record.
* @param {number} seq - number representing the current version of the record.
* @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
* @returns {Promise<IpnsEntry>} entry
*/
const createWithExpiration = (privateKey, value, seq, expiration) => {
const validityType = ipnsEntryProto.ValidityType.EOL
return _create(privateKey, value, seq, expiration, validityType)
}

/**
* @param {PrivateKey} privateKey
* @param {string} value
* @param {number} seq
* @param {string} isoValidity
* @param {number} validityType
*/
const _create = async (privateKey, value, seq, isoValidity, validityType) => {
const signature = await sign(privateKey, value, validityType, isoValidity)

Expand All @@ -85,9 +88,8 @@ const _create = async (privateKey, value, seq, isoValidity, validityType) => {
/**
* Validates the given ipns entry against the given public key.
*
* @param {Object} publicKey - public key for validating the record.
* @param {IpnsEntry} entry - ipns entry record.
* @returns {Promise}
* @param {PublicKey} publicKey - public key for validating the record.
* @param {IPNSEntry} entry - ipns entry record.
*/
const validate = async (publicKey, entry) => {
const { value, validityType, validity } = entry
Expand Down Expand Up @@ -116,7 +118,7 @@ const validate = async (publicKey, entry) => {
throw errCode(new Error('unrecognized validity format (not an rfc3339 format)'), ERRORS.ERR_UNRECOGNIZED_FORMAT)
}

if (validityDate < Date.now()) {
if (validityDate.getTime() < Date.now()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱

log.error('record has expired')
throw errCode(new Error('record has expired'), ERRORS.ERR_IPNS_EXPIRED_RECORD)
}
Expand All @@ -137,9 +139,8 @@ const validate = async (publicKey, entry) => {
* send this as part of the record itself. For newer ed25519 keys, the public key
* can be embedded in the peerId.
*
* @param {Object} publicKey - public key to embed.
* @param {Object} entry - ipns entry record.
* @returns {IpnsEntry} entry with public key embedded
* @param {PublicKey} publicKey - public key to embed.
* @param {IPNSEntry} entry - ipns entry record.
*/
const embedPublicKey = async (publicKey, entry) => {
if (!publicKey || !publicKey.bytes || !entry) {
Expand Down Expand Up @@ -182,9 +183,8 @@ const embedPublicKey = async (publicKey, entry) => {
/**
* Extracts a public key matching `pid` from the ipns record.
*
* @param {Object} peerId - peer identifier object.
* @param {IpnsEntry} entry - ipns entry record.
* @returns {Object} the public key
* @param {PeerId} peerId - peer identifier object.
* @param {IPNSEntry} entry - ipns entry record.
*/
const extractPublicKey = (peerId, entry) => {
if (!entry || !peerId) {
Expand All @@ -211,15 +211,18 @@ const extractPublicKey = (peerId, entry) => {
throw Object.assign(new Error('no public key is available'), { code: ERRORS.ERR_UNDEFINED_PARAMETER })
}

// rawStdEncoding with RFC4648
/**
* rawStdEncoding with RFC4648
*
* @param {Uint8Array} key
*/
const rawStdEncoding = (key) => multibase.encode('base32', key).toString().slice(1).toUpperCase()

/**
* Get key for storing the record locally.
* Format: /ipns/${base32(<HASH>)}
*
* @param {Uint8Array} key - peer identifier object.
* @returns {string}
*/
const getLocalKey = (key) => new Key(`/ipns/${rawStdEncoding(key)}`)

Expand All @@ -228,7 +231,6 @@ const getLocalKey = (key) => new Key(`/ipns/${rawStdEncoding(key)}`)
* Format: ${base32(/ipns/<HASH>)}, ${base32(/pk/<HASH>)}
*
* @param {Uint8Array} pid - peer identifier represented by the multihash of the public key as Uint8Array.
* @returns {Object} containing the `nameKey` and the `ipnsKey`.
*/
const getIdKeys = (pid) => {
const pkBuffer = uint8ArrayFromString('/pk/')
Expand All @@ -242,7 +244,14 @@ const getIdKeys = (pid) => {
}
}

// Sign ipns record data
/**
* Sign ipns record data
*
* @param {PrivateKey} privateKey
* @param {string} value
* @param {number} validityType
* @param {Uint8Array | string} validity
*/
const sign = (privateKey, value, validityType, validity) => {
try {
const dataForSignature = ipnsEntryDataForSig(value, validityType, validity)
Expand All @@ -254,7 +263,11 @@ const sign = (privateKey, value, validityType, validity) => {
}
}

// Utility for getting the validity type code name of a validity
/**
* Utility for getting the validity type code name of a validity
*
* @param {number} validityType
*/
const getValidityType = (validityType) => {
if (validityType.toString() === '0') {
return 'EOL'
Expand All @@ -265,7 +278,13 @@ const getValidityType = (validityType) => {
throw errCode(error, ERRORS.ERR_UNRECOGNIZED_VALIDITY)
}

// Utility for creating the record data for being signed
/**
* Utility for creating the record data for being signed
*
* @param {string | Uint8Array} value
* @param {number} validityType
* @param {string | Uint8Array} validity
*/
const ipnsEntryDataForSig = (value, validityType, validity) => {
if (!(value instanceof Uint8Array)) {
value = uint8ArrayFromString(value)
Expand All @@ -280,7 +299,11 @@ const ipnsEntryDataForSig = (value, validityType, validity) => {
return uint8ArrayConcat([value, validity, validityTypeBuffer])
}

// Utility for extracting the public key from a peer-id
/**
* Utility for extracting the public key from a peer-id
*
* @param {PeerId} peerId
*/
const extractPublicKeyFromId = (peerId) => {
const decodedId = multihash.decode(peerId.id)

Expand All @@ -291,11 +314,34 @@ const extractPublicKeyFromId = (peerId) => {
return crypto.keys.unmarshalPublicKey(decodedId.digest)
}

const marshal = ipnsEntryProto.encode
/**
* @param {IPNSEntry} obj
*/
const marshal = (obj) => {
return ipnsEntryProto.encode(obj).finish()
}

const unmarshal = ipnsEntryProto.decode
/**
* @param {Uint8Array} buf
* @returns {IPNSEntry}
*/
const unmarshal = (buf) => {
const message = ipnsEntryProto.decode(buf)

// @ts-ignore
return ipnsEntryProto.toObject(message, {
defaults: false,
arrays: true,
longs: Number,
objects: false
})
}

const validator = {
/**
* @param {Uint8Array} marshalledData
* @param {Uint8Array} key
*/
validate: async (marshalledData, key) => {
const receivedEntry = unmarshal(marshalledData)
const bufferId = key.slice('/ipns/'.length)
Expand All @@ -308,6 +354,10 @@ const validator = {
await validate(pubKey, receivedEntry)
return true
},
/**
* @param {Uint8Array} dataA
* @param {Uint8Array} dataB
*/
select: (dataA, dataB) => {
const entryA = unmarshal(dataA)
const entryB = unmarshal(dataB)
Expand Down
Loading