bns
Advanced tools
Comparing version 0.1.6 to 0.1.7
@@ -16,3 +16,3 @@ 'use strict'; | ||
if (arg) { | ||
resolve(['/dev/stdin', argv[2], ...argv.slice(3)]); | ||
resolve(['/dev/stdin', ...argv.slice(2)]); | ||
return; | ||
@@ -19,0 +19,0 @@ } |
@@ -26,2 +26,3 @@ /*! | ||
const openpgpkey = require('./openpgpkey'); | ||
// const Ownership = require('./ownership'); | ||
const rdns = require('./rdns'); | ||
@@ -32,2 +33,3 @@ const RecursiveResolver = require('./resolver/recursive'); | ||
const ROOT_HINTS = require('./roothints'); | ||
// const RootResolver = require('./resolver/root'); | ||
const sig0 = require('./sig0'); | ||
@@ -65,2 +67,3 @@ const smimea = require('./smimea'); | ||
exports.openpgpkey = openpgpkey; | ||
// exports.Ownership = Ownership; | ||
exports.rdns = rdns; | ||
@@ -71,2 +74,3 @@ exports.RecursiveResolver = RecursiveResolver; | ||
exports.ROOT_HINTS = ROOT_HINTS; | ||
// exports.RootResolver = RootResolver; | ||
exports.sig0 = sig0; | ||
@@ -73,0 +77,0 @@ exports.smimea = smimea; |
@@ -493,2 +493,6 @@ /*! | ||
* DNSKEY flag values. | ||
* See RFC4034, Section 2.1.1 | ||
* Note that their endianness is backwards, | ||
* Subtract each bit value from 15 to convert. | ||
* @see https://www.ietf.org/rfc/rfc4034.txt | ||
* @enum {Number} | ||
@@ -499,5 +503,8 @@ * @default | ||
const keyFlags = { | ||
SEP: 1, | ||
KSK: 1 << 0, | ||
SEP: 1 << 0, | ||
// 1-6 reserved | ||
REVOKE: 1 << 7, | ||
ZONE: 1 << 8 | ||
// 9-15 reserved | ||
}; | ||
@@ -504,0 +511,0 @@ |
@@ -10,3 +10,3 @@ /*! | ||
const assert = require('assert'); | ||
const ccmp = require('bcrypto/lib/ccmp'); | ||
const safeEqual = require('bcrypto/lib/safe-equal'); | ||
const MD5 = require('bcrypto/lib/md5'); | ||
@@ -34,5 +34,10 @@ const SHA1 = require('bcrypto/lib/sha1'); | ||
exports.sha512 = SHA512; | ||
exports.ccmp = ccmp; | ||
/* | ||
* Helpers | ||
*/ | ||
exports.safeEqual = safeEqual; | ||
/* | ||
* RSA | ||
@@ -39,0 +44,0 @@ */ |
@@ -20,2 +20,3 @@ /*! | ||
const encoding = require('./encoding'); | ||
const keys = require('./keys'); | ||
const util = require('./util'); | ||
@@ -37,2 +38,3 @@ const wire = require('./wire'); | ||
const { | ||
classes, | ||
types, | ||
@@ -51,2 +53,3 @@ keyFlags, | ||
DSRecord, | ||
DNSKEYRecord, | ||
RRSIGRecord | ||
@@ -84,8 +87,44 @@ } = wire; | ||
dnssec.createDS = function createDS(dnskey, digestType) { | ||
assert(dnskey instanceof Record); | ||
assert(dnskey.type === types.DNSKEY); | ||
dnssec.filename = keys.filename; | ||
dnssec.privFile = keys.privFile; | ||
dnssec.pubFile = keys.pubFile; | ||
dnssec.createPrivate = keys.createPrivate; | ||
dnssec.createPublic = keys.createPublic; | ||
dnssec.encodePrivate = keys.encodePrivate; | ||
dnssec.decodePrivate = keys.decodePrivate; | ||
dnssec.createKey = function createKey(name, algorithm, publicKey, flags) { | ||
if (flags == null) | ||
flags = keyFlags.ZONE; | ||
assert(typeof name === 'string'); | ||
assert(util.isFQDN(name)); | ||
assert((algorithm & 0xff) === algorithm); | ||
assert(Buffer.isBuffer(publicKey)); | ||
assert((flags & 0xffff) === flags); | ||
const rr = new Record(); | ||
const rd = new DNSKEYRecord(); | ||
rr.name = name; | ||
rr.class = classes.IN; | ||
rr.type = types.DNSKEY; | ||
rr.ttl = 172800; | ||
rr.data = rd; | ||
rd.flags = flags; | ||
rd.protocol = 3; | ||
rd.algorithm = algorithm; | ||
rd.publicKey = publicKey; | ||
return rr; | ||
}; | ||
dnssec.createDS = function createDS(key, digestType) { | ||
assert(key instanceof Record); | ||
assert(key.type === types.DNSKEY); | ||
assert((digestType & 0xff) === digestType); | ||
const dk = dnskey.data; // DNSKEY | ||
const kd = key.data; | ||
const hash = hashToHash[digestType]; | ||
@@ -96,5 +135,5 @@ | ||
const raw = dk.encode(); | ||
const keyTag = dk.keyTag(raw); | ||
const owner = packName(dnskey.name); | ||
const raw = kd.encode(); | ||
const keyTag = kd.keyTag(raw); | ||
const owner = packName(key.name); | ||
@@ -107,37 +146,37 @@ const ctx = hash.hash(); | ||
const rr = new Record(); | ||
rr.name = dnskey.name; | ||
rr.class = dnskey.class; | ||
const rd = new DSRecord(); | ||
rr.name = key.name; | ||
rr.class = key.class; | ||
rr.type = types.DS; | ||
rr.ttl = dnskey.ttl; | ||
rr.ttl = key.ttl; | ||
rr.data = rd; | ||
const ds = new DSRecord(); | ||
ds.algorithm = dk.algorithm; | ||
ds.digestType = digestType; | ||
ds.keyTag = keyTag; | ||
ds.digest = ctx.final(); | ||
rd.algorithm = kd.algorithm; | ||
rd.digestType = digestType; | ||
rd.keyTag = keyTag; | ||
rd.digest = ctx.final(); | ||
rr.data = ds; | ||
return rr; | ||
}; | ||
dnssec.signType = function signType(section, type, key, priv, lifespan) { | ||
assert(Array.isArray(section)); | ||
dnssec.signType = function signType(rrs, type, key, priv, lifespan) { | ||
assert(Array.isArray(rrs)); | ||
assert((type & 0xffff) === type); | ||
const rrset = extractSet(section, '', type); | ||
const rrset = extractSet(rrs, '', type); | ||
if (rrset.length === 0) | ||
return section; | ||
return rrs; | ||
const sig = dnssec.rrsign(key, priv, rrset, lifespan); | ||
const sig = dnssec.sign(key, priv, rrset, lifespan); | ||
section.push(sig); | ||
rrs.push(sig); | ||
return section; | ||
return rrs; | ||
}; | ||
dnssec.rrsign = function rrsign(key, priv, rrset, lifespan) { | ||
dnssec.sign = function sign(key, priv, rrset, lifespan) { | ||
if (lifespan == null) | ||
lifespan = 14 * 24 * 60 * 60; | ||
lifespan = 365 * 24 * 60 * 60; | ||
@@ -149,21 +188,21 @@ assert(key instanceof Record); | ||
const sig = new Record(); | ||
const s = new RRSIGRecord(); | ||
const rr = new Record(); | ||
const rd = new RRSIGRecord(); | ||
sig.name = key.name; | ||
sig.ttl = key.ttl; | ||
sig.class = key.class; | ||
sig.type = types.RRSIG; | ||
sig.data = s; | ||
rr.name = key.name; | ||
rr.ttl = key.ttl; | ||
rr.class = key.class; | ||
rr.type = types.RRSIG; | ||
rr.data = rd; | ||
s.keyTag = key.data.keyTag(); | ||
s.signerName = key.name; | ||
s.algorithm = key.data.algorithm; | ||
s.inception = util.now() - lifespan; | ||
s.expiration = util.now() + lifespan; | ||
rd.keyTag = key.data.keyTag(); | ||
rd.signerName = key.name; | ||
rd.algorithm = key.data.algorithm; | ||
rd.inception = util.now() - (24 * 60 * 60); | ||
rd.expiration = util.now() + lifespan; | ||
return dnssec.sign(sig, priv, rrset); | ||
return dnssec.signRRSIG(rr, priv, rrset); | ||
}; | ||
dnssec.sign = function sign(sig, priv, rrset) { | ||
dnssec.signRRSIG = function signRRSIG(sig, priv, rrset) { | ||
assert(sig instanceof Record); | ||
@@ -174,3 +213,3 @@ assert(sig.type === types.RRSIG); | ||
const s = sig.data; // RRSIG | ||
const sd = sig.data; | ||
@@ -180,3 +219,3 @@ if (!isRRSet(rrset)) | ||
if (s.keyTag === 0 || s.signerName.length === 0 || s.algorithm === 0) | ||
if (sd.keyTag === 0 || sd.signerName.length === 0 || sd.algorithm === 0) | ||
throw new Error('Invalid signature record.'); | ||
@@ -187,12 +226,12 @@ | ||
sig.class = rrset[0].class; | ||
sig.data = s; | ||
sig.data = sd; | ||
if (s.origTTL === 0) | ||
s.origTTL = rrset[0].ttl; | ||
if (sd.origTTL === 0) | ||
sd.origTTL = rrset[0].ttl; | ||
s.typeCovered = rrset[0].type; | ||
s.labels = countLabels(rrset[0].name); | ||
sd.typeCovered = rrset[0].type; | ||
sd.labels = countLabels(rrset[0].name); | ||
if (rrset[0].name[0] === '*') | ||
s.labels -= 1; | ||
sd.labels -= 1; | ||
@@ -204,3 +243,3 @@ const data = dnssec.signatureHash(sig, rrset); | ||
s.signature = dnssec.signData(priv, data, s.algorithm); | ||
sd.signature = dnssec.signData(priv, data, sd.algorithm); | ||
@@ -329,3 +368,3 @@ return sig; | ||
const s = sig.data; // RRSIG | ||
const sd = sig.data; | ||
const records = []; | ||
@@ -340,4 +379,4 @@ | ||
// Server is using wildcards. | ||
if (labels.length > s.labels) { | ||
const i = labels.length - s.labels; | ||
if (labels.length > sd.labels) { | ||
const i = labels.length - sd.labels; | ||
const name = labels.slice(i).join('.'); | ||
@@ -348,7 +387,7 @@ rr.name = `*.${name}.`; | ||
// Invalid RR set. | ||
if (labels.length < s.labels) | ||
if (labels.length < sd.labels) | ||
return null; | ||
// Canonical TTL. | ||
rr.ttl = s.origTTL; | ||
rr.ttl = sd.origTTL; | ||
@@ -365,3 +404,3 @@ // Canonicalize all domain | ||
const tbs = s.toTBS(); | ||
const tbs = sd.toTBS(); | ||
@@ -413,5 +452,2 @@ let size = 0; | ||
if (rd.flags & keyFlags.REVOKE) | ||
continue; | ||
if (!(rd.flags & keyFlags.ZONE)) | ||
@@ -423,4 +459,6 @@ continue; | ||
if (rd.flags & keyFlags.SEP) | ||
kskMap.set(rd.keyTag(), rr); | ||
if (rd.flags & keyFlags.REVOKE) | ||
continue; | ||
kskMap.set(rd.keyTag(), rr); | ||
} | ||
@@ -435,8 +473,8 @@ | ||
const rd = rr.data; | ||
const dnskey = kskMap.get(rd.keyTag); | ||
const key = kskMap.get(rd.keyTag); | ||
if (!dnskey) | ||
if (!key) | ||
continue; | ||
const ds = dnssec.createDS(dnskey, rd.digestType); | ||
const ds = dnssec.createDS(key, rd.digestType); | ||
@@ -452,3 +490,3 @@ if (!ds) | ||
valid.set(rd.keyTag, dnskey); | ||
valid.set(rd.keyTag, key); | ||
@@ -498,2 +536,5 @@ continue; | ||
if (!rd.validityPeriod()) | ||
continue; // Invalid Signature Period | ||
sigs.push(rr); | ||
@@ -511,12 +552,7 @@ continue; | ||
for (const sig of sigs) { | ||
const s = sig.data; | ||
const dnskey = kskMap.get(s.keyTag); | ||
const sd = sig.data; | ||
const key = kskMap.get(sd.keyTag); | ||
assert(key); | ||
if (!dnskey) | ||
return false; // Missing DNS Key | ||
if (!s.validityPeriod()) | ||
return false; // Invalid Signature Period | ||
if (!dnssec.verify(sig, dnskey, keys)) | ||
if (!dnssec.verify(sig, key, keys)) | ||
return false; // Invalid Signature | ||
@@ -528,5 +564,6 @@ } | ||
dnssec.verifyRRSIG = function verifyRRSIG(msg, zskMap) { | ||
dnssec.verifyMessage = function verifyMessage(msg, zskMap, revSet) { | ||
assert(msg instanceof Message); | ||
assert(zskMap instanceof Map); | ||
assert(revSet instanceof Set); | ||
@@ -585,24 +622,27 @@ const isAnswer = msg.isAnswer(); | ||
const s = rr.data; | ||
const rd = rr.data; | ||
if (!set.has(s.typeCovered)) | ||
if (!rd.validityPeriod()) | ||
continue; // Invalid Signature Period | ||
if (!set.has(rd.typeCovered)) | ||
continue; // Useless | ||
const dnskey = zskMap.get(s.keyTag); | ||
if (revSet.has(rd.keyTag)) | ||
continue; // Revoked signature. | ||
if (!dnskey) | ||
const key = zskMap.get(rd.keyTag); | ||
if (!key) | ||
continue; // Missing DNS Key | ||
if (!s.validityPeriod()) | ||
continue; // Invalid Signature Period | ||
const rrset = extractSet(section, rr.name, rd.typeCovered); | ||
const rrset = extractSet(section, rr.name, s.typeCovered); | ||
if (rrset.length === 0) | ||
continue; // Missing Signed | ||
if (!dnssec.verify(rr, dnskey, rrset)) | ||
if (!dnssec.verify(rr, key, rrset)) | ||
continue; // Invalid Signature | ||
set.delete(s.typeCovered); | ||
set.delete(rd.typeCovered); | ||
} | ||
@@ -657,2 +697,12 @@ | ||
dnssec.stripSignatures = function stripSignatures(msg) { | ||
assert(msg instanceof Message); | ||
msg.answer = util.filterSet(msg.answer, types.RRSIG); | ||
msg.authority = util.filterSet(msg.authority, types.RRSIG); | ||
msg.additional = util.filterSet(msg.additional, types.RRSIG); | ||
return msg; | ||
}; | ||
/* | ||
@@ -659,0 +709,0 @@ * Helpers |
@@ -607,5 +607,6 @@ /*! | ||
verifyKey(key) { | ||
verifyKey(key, hardened = false) { | ||
assert(key instanceof Record); | ||
assert(key.type === types.DNSKEY); | ||
assert(typeof hardened === 'boolean'); | ||
@@ -625,2 +626,5 @@ const kd = key.data; | ||
return false; // Insecure Signature. | ||
if (hardened && bits < this.strongBits) | ||
return false; // Insecure Signature. | ||
} | ||
@@ -627,0 +631,0 @@ |
@@ -186,3 +186,3 @@ /*! | ||
|| res.code !== codes.NOERROR) { | ||
return new Map(); | ||
return [new Map(), new Set()]; | ||
} | ||
@@ -197,11 +197,11 @@ | ||
this.log('Invalid DNSKEY (DS absent).'); | ||
return null; | ||
return [null, null]; | ||
} | ||
// Pick out the valid KSK's. | ||
const valid = dnssec.verifyDS(res, ds, qs.name); | ||
const kskMap = dnssec.verifyDS(res, ds, qs.name); | ||
if (!valid) { | ||
if (!kskMap) { | ||
this.log('Invalid KSKs (DS mismatch).'); | ||
return null; | ||
return [null, null]; | ||
} | ||
@@ -211,5 +211,5 @@ | ||
// Verify all ZSK's with KSK's if we're not cached. | ||
if (!dnssec.verifyZSK(res, valid, qs.name)) { | ||
if (!dnssec.verifyZSK(res, kskMap, qs.name)) { | ||
this.log('Invalid KSKs (verification failure).'); | ||
return null; | ||
return [null, null]; | ||
} | ||
@@ -221,2 +221,5 @@ | ||
const zskMap = new Map(); | ||
const revSet = new Set(); | ||
// Grab all ZSK's from the answer. | ||
@@ -229,3 +232,3 @@ for (const rr of res.answer) { | ||
if (rd.flags & dnssec.keyFlags.REVOKE) | ||
if (!equal(rr.name, qs.name)) | ||
continue; | ||
@@ -236,15 +239,11 @@ | ||
if (!equal(rr.name, qs.name)) | ||
if (rd.flags & dnssec.keyFlags.REVOKE) { | ||
revSet.add(rd.revTag()); | ||
continue; | ||
} | ||
if (rd.flags & dnssec.keyFlags.SEP) | ||
continue; | ||
// Pushing onto the valid KSK map: | ||
// We allow KSK's to sign the zone | ||
// too (because why not?). | ||
valid.set(rd.keyTag(), rr); | ||
zskMap.set(rd.keyTag(), rr); | ||
} | ||
return valid; | ||
return [zskMap, revSet]; | ||
} | ||
@@ -257,3 +256,3 @@ | ||
const qs = new Question(auth.zone, types.DNSKEY); | ||
const zskMap = await this.lookupDNSKEY(qs, auth, ds); | ||
const [zskMap, revSet] = await this.lookupDNSKEY(qs, auth, ds); | ||
@@ -268,3 +267,3 @@ if (!zskMap) | ||
if (!dnssec.verifyRRSIG(msg, zskMap)) { | ||
if (!dnssec.verifyMessage(msg, zskMap, revSet)) { | ||
this.log('Invalid RRSIGs.'); | ||
@@ -271,0 +270,0 @@ return false; |
@@ -42,3 +42,2 @@ /*! | ||
typeToString, | ||
YEAR68, | ||
DEFAULT_TTL, | ||
@@ -1735,3 +1734,3 @@ LOC_EQUATOR, | ||
const tok = expect(iter, tokens.STRING, file); | ||
const time = parseTime(tok.string); | ||
const time = util.parseTime(tok.string); | ||
return [time, skip(iter, file)]; | ||
@@ -2176,37 +2175,2 @@ } | ||
function unpad(str, start, end) { | ||
const num = str.substring(start, end); | ||
return util.parseU16(num); | ||
} | ||
function parseTime(s) { | ||
assert(typeof s === 'string'); | ||
assert(s.length === 14); | ||
const y = unpad(s, 0, 4); | ||
const m = unpad(s, 4, 6); | ||
const d = unpad(s, 6, 8); | ||
const hr = unpad(s, 8, 10); | ||
const mn = unpad(s, 10, 12); | ||
const sc = unpad(s, 12, 14); | ||
const da = new Date(); | ||
da.setUTCFullYear(y); | ||
da.setUTCMonth(m - 1); | ||
da.setUTCDate(d); | ||
da.setUTCHours(hr); | ||
da.setUTCMinutes(mn); | ||
da.setUTCSeconds(sc); | ||
const t = Math.floor(da.getTime() / 1000); | ||
const div = util.now() / YEAR68; | ||
let mod = Math.floor(div) - 1; | ||
if (mod < 0) | ||
mod = 0; | ||
return (t - (mod * YEAR68)) >>> 0; | ||
} | ||
function parseNID32(str) { | ||
@@ -2213,0 +2177,0 @@ assert(typeof str === 'string'); |
@@ -27,3 +27,2 @@ /*! | ||
types, | ||
YEAR68, | ||
options, | ||
@@ -824,3 +823,3 @@ typeToString, | ||
case TIME: { | ||
return serializeTime(value); | ||
return util.serializeTime(value); | ||
} | ||
@@ -1334,34 +1333,2 @@ | ||
function pad(num, len) { | ||
let str = num.toString(10); | ||
while (str.length < len) | ||
str = '0' + str; | ||
return str; | ||
} | ||
function serializeTime(t) { | ||
assert(Number.isSafeInteger(t) && t >= 0); | ||
const div = (t - util.now()) / YEAR68; | ||
let mod = Math.floor(div) - 1; | ||
if (mod < 0) | ||
mod = 0; | ||
const ti = t - (mod * YEAR68); | ||
const da = new Date(); | ||
da.setTime(ti * 1000); | ||
const y = pad(da.getUTCFullYear(), 4); | ||
const m = pad(da.getUTCMonth() + 1, 2); | ||
const d = pad(da.getUTCDate(), 2); | ||
const hr = pad(da.getUTCHours(), 2); | ||
const mn = pad(da.getUTCMinutes(), 2); | ||
const sc = pad(da.getUTCSeconds(), 2); | ||
return `${y}${m}${d}${hr}${mn}${sc}`; | ||
} | ||
function quote(str) { | ||
@@ -1368,0 +1335,0 @@ assert(typeof str === 'string'); |
@@ -153,3 +153,3 @@ /*! | ||
// Constant time equals. | ||
return crypto.ccmp(rd.mac, hash); | ||
return crypto.safeEqual(rd.mac, hash); | ||
}; | ||
@@ -156,0 +156,0 @@ |
@@ -20,2 +20,3 @@ /*! | ||
const util = exports; | ||
const YEAR68 = (1 << 31) >>> 0; | ||
@@ -811,2 +812,16 @@ util.splitName = function splitName(s) { | ||
util.splitColon = function splitColon(str) { | ||
assert(typeof str === 'string'); | ||
const index = str.indexOf(':'); | ||
if (index === -1) | ||
return [str.toLowerCase(), '']; | ||
const left = str.substring(0, index).trim(); | ||
const right = str.substring(index + 1).trim(); | ||
return [left, right]; | ||
}; | ||
util.splitLines = function splitLines(str, escaped, limit) { | ||
@@ -894,2 +909,69 @@ assert(typeof str === 'string'); | ||
util.pad = function pad(num, len) { | ||
assert((num >>> 0) === num); | ||
assert((len >>> 0) === len); | ||
let str = num.toString(10); | ||
while (str.length < len) | ||
str = '0' + str; | ||
return str; | ||
}; | ||
util.parseTime = function parseTime(s) { | ||
assert(typeof s === 'string'); | ||
assert(s.length === 14); | ||
const y = unpad(s, 0, 4); | ||
const m = unpad(s, 4, 6); | ||
const d = unpad(s, 6, 8); | ||
const hr = unpad(s, 8, 10); | ||
const mn = unpad(s, 10, 12); | ||
const sc = unpad(s, 12, 14); | ||
const da = new Date(); | ||
da.setUTCFullYear(y); | ||
da.setUTCMonth(m - 1); | ||
da.setUTCDate(d); | ||
da.setUTCHours(hr); | ||
da.setUTCMinutes(mn); | ||
da.setUTCSeconds(sc); | ||
const t = Math.floor(da.getTime() / 1000); | ||
const div = util.now() / YEAR68; | ||
let mod = Math.floor(div) - 1; | ||
if (mod < 0) | ||
mod = 0; | ||
return (t - (mod * YEAR68)) >>> 0; | ||
}; | ||
util.serializeTime = function serializeTime(t) { | ||
assert(Number.isSafeInteger(t) && t >= 0); | ||
const div = (t - util.now()) / YEAR68; | ||
let mod = Math.floor(div) - 1; | ||
if (mod < 0) | ||
mod = 0; | ||
const ti = t - (mod * YEAR68); | ||
const da = new Date(); | ||
da.setTime(ti * 1000); | ||
const y = util.pad(da.getUTCFullYear(), 4); | ||
const m = util.pad(da.getUTCMonth() + 1, 2); | ||
const d = util.pad(da.getUTCDate(), 2); | ||
const hr = util.pad(da.getUTCHours(), 2); | ||
const mn = util.pad(da.getUTCMinutes(), 2); | ||
const sc = util.pad(da.getUTCSeconds(), 2); | ||
return `${y}${m}${d}${hr}${mn}${sc}`; | ||
}; | ||
/* | ||
@@ -902,1 +984,6 @@ * Helpers | ||
} | ||
function unpad(str, start, end) { | ||
const num = str.substring(start, end); | ||
return util.parseU16(num); | ||
} |
{ | ||
"name": "bns", | ||
"version": "0.1.6", | ||
"version": "0.1.7", | ||
"description": "DNS bike-shed", | ||
@@ -20,2 +20,3 @@ "keywords": [ | ||
"bin": { | ||
"bns-keygen": "./bin/bns-keygen", | ||
"dig.js": "./bin/dig.js", | ||
@@ -34,3 +35,3 @@ "named.js": "./bin/named.js", | ||
"clean": "rm -f bns.js", | ||
"lint": "eslint bin/ lib/ test/ || exit 0", | ||
"lint": "eslint bin/* lib/ test/ || exit 0", | ||
"test": "mocha --reporter spec test/*-test.js", | ||
@@ -37,0 +38,0 @@ "test-file": "mocha --reporter spec", |
643564
73
24758