Skip to content

Commit

Permalink
feat: add types (#106)
Browse files Browse the repository at this point in the history
* feat: add types

Adds ts types, fixes all tsc errors.

Removes ipfs/ipfsd-ctl/ipfs-http-client dev deps as we only spin up
a node to get an IPFS id which we can just hard code for testing purposes.

Replaces protons with protobufjs as elsewhere in the stack.

* chore: do not double-build

* chore: add types directive to package.json

* chore: update ubuntu version

* chore: remove redundant pre publish step
  • Loading branch information
achingbrain authored Mar 5, 2021
1 parent a7d1aa0 commit 135552b
Show file tree
Hide file tree
Showing 11 changed files with 603 additions and 107 deletions.
16 changes: 0 additions & 16 deletions .aegir.js

This file was deleted.

17 changes: 10 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
language: node_js
dist: bionic
cache: npm
stages:
- check
- test
- cov

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

node_js:
- 'lts/*'
- 'node'
Expand All @@ -16,7 +22,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 +31,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 @@ -4,8 +4,12 @@
"description": "ipns record definitions",
"leadMaintainer": "Vasco Santos <[email protected]>",
"main": "src/index.js",
"types": "dist/src/index.d.ts",
"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",
"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

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()) {
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

0 comments on commit 135552b

Please sign in to comment.