Skip to content
This repository has been archived by the owner on Sep 19, 2018. It is now read-only.

Improve Error Categorization and Formatting #86

Merged
merged 7 commits into from
Feb 12, 2018
Merged
Show file tree
Hide file tree
Changes from 6 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
17 changes: 8 additions & 9 deletions lib/datastore.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const HKDF_INFO_HASHING = "pz8gGLGYNLV6haKwjJ1dR-YKX5zDMhPHw2DuXGNu6cw";

async function deriveKeys(appKey, salt) {
if (!appKey) {
throw new TypeError("invalid app key");
throw new DataStoreError(DataStoreError.MISSING_APP_KEY);
}
appKey = await jose.JWK.asKey(appKey);

Expand Down Expand Up @@ -54,10 +54,10 @@ async function deriveKeys(appKey, salt) {

function checkState(ds, unlocking) {
if (!ds.initialized) {
throw new DataStoreError("data is not initialized", DataStoreError.NOT_INITIALIZED);
throw new DataStoreError(DataStoreError.NOT_INITIALIZED);
}
if (!unlocking && ds.locked) {
throw new DataStoreError("datastore is locked", DataStoreError.LOCKED);
throw new DataStoreError(DataStoreError.LOCKED);
}
}

Expand Down Expand Up @@ -195,9 +195,9 @@ class DataStore {
let listing = undefined;
if (self.keystore.encrypted) {
if (!opts.rebase) {
throw new DataStoreError("already initialized", DataStoreError.INITIALIZED);
throw new DataStoreError(DataStoreError.INITIALIZED);
} else if (opts.rebase && this.locked) {
throw new DataStoreError("must to unlocked in order to rebase", DataStoreError.LOCKED);
throw new DataStoreError(DataStoreError.LOCKED, "must be unlocked in order to rebase");
}

// migrate it out
Expand Down Expand Up @@ -344,8 +344,7 @@ class DataStore {

let self = instance.get(this);
if (!item) {
// TODO: custom errors
throw new TypeError("expected item");
throw new DataStoreError(DataStoreError.INVALID_ITEM);
}

// validate, and fill defaults into, {item}
Expand Down Expand Up @@ -389,14 +388,14 @@ class DataStore {
let self = instance.get(this);
if (!item || !item.id) {
// TODO: custom errors
throw new TypeError("invalid item");
throw new DataStoreError(DataStoreError.INVALID_ITEM);
}

let id = item.id;
let orig = await self.ldb.items.get(id),
encrypted;
if (!orig) {
throw new Error("item does not exist");
throw new DataStoreError(DataStoreError.MISSING_ITEM);
} else {
encrypted = orig.encrypted;
}
Expand Down
15 changes: 8 additions & 7 deletions lib/itemkeystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/

const instance = require("./util/instance"),
jose = require("node-jose");
jose = require("node-jose"),
DataStoreError = require("./util/errors");

/**
* Manages a set of item keys. This includes generation, encryption, and
Expand Down Expand Up @@ -86,10 +87,10 @@ class ItemKeyStore {
}

if (!encryptKey) {
throw new Error("invalid master key");
throw new DataStoreError(DataStoreError.MISSING_APP_KEY, "missing master encrypt key");
}
if (!encrypted) {
throw new Error("not encrypted");
throw new DataStoreError(DataStoreError.CRYPTO, "keystore not encrypted");
}

let keystore = jose.JWK.createKeyStore(),
Expand Down Expand Up @@ -130,7 +131,7 @@ class ItemKeyStore {
} = self;

if (!jose.JWK.isKey(encryptKey)) {
throw new Error("invalid master key");
throw new DataStoreError(DataStoreError.MISSING_APP_KEY, "missing master encrypt key");
}

let encrypted = {};
Expand Down Expand Up @@ -242,7 +243,7 @@ class ItemKeyStore {
*/
async protect(item) {
if (!item || !item.id) {
throw new Error("invalid item");
throw new DataStoreError(DataStoreError.INVALID_ITEM);
}

let { id } = item;
Expand All @@ -264,12 +265,12 @@ class ItemKeyStore {
*/
async unprotect(id, jwe) {
if (!jwe || "string" !== typeof jwe) {
throw new Error("invalid encrypted item");
throw new DataStoreError(DataStoreError.CRYPTO, "invalid encrypted item");
}

let key = await this.get(id);
if (!key) {
throw new Error("unknown item key");
throw new DataStoreError(DataStoreError.CRYPTO, "unknown item key");
}

let item = await jose.JWE.createDecrypt(key).decrypt(jwe);
Expand Down
8 changes: 4 additions & 4 deletions lib/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
const jsonmergepatch = require("json-merge-patch"),
joi = require("joi"),
UUID = require("uuid");

const DataStoreError = require("./util/errors");

const STRING_500 = joi.string().max(500).allow("").allow(null).default("");
Expand Down Expand Up @@ -41,23 +41,23 @@ function prepare(item, source) {
let { error, value: destination } = SCHEMA.validate(item, VALIDATE_OPTIONS);
if (error) {
let details = error.details;
let thrown = new DataStoreError("item changes invalid", DataStoreError.INVALID_ITEM);
let thrown = new DataStoreError(DataStoreError.INVALID_ITEM);
thrown.details = {};
details.forEach((d) => {
let path = Array.isArray(d.path) ? d.path.join(".") : d.path;
thrown.details[path] = d.type;
});
throw thrown;
}

// apply read-only values
source = source || {};
destination.id = source.id || UUID();
destination.disabled = source.disabled || false;
destination.created = source.created || new Date().toISOString();
// always assume the item is modified
destination.modified = new Date().toISOString();

// generate history patch (to go backward)
let history = [];
if (source && source.entry) {
Expand Down
10 changes: 7 additions & 3 deletions lib/localdatabase.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ async function open(bucket) {
DATABASES.add(db);
}

await db.open();
if (db.verno !== DATABASE_VERSION) {
throw new DataStoreError("invalid indexeddb version", DataStoreError.LOCALDB_VERSION);
try {
await db.open();
} catch (err) {
if (err instanceof Dexie.VersionError) {
throw new DataStoreError(DataStoreError.LOCALDB_VERSION);
}
throw new DataStoreError(DataStoreError.GENERIC_ERROR, err.message);
}
return db;
}
Expand Down
101 changes: 71 additions & 30 deletions lib/util/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,47 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

const REASONS = [
"LOCALDB_VERSION",
"NOT_INITIALIZED",
"INITIALIZED",
"MISSING_APP_KEY",
"WRONG_APP_KEY",
"CRYPTO",
"LOCKED",
"INVALID_ITEM",
"MISSING_ITEM",
"OFFLINE",
"AUTH",
"NETWORK",
"SYNC_LOCKED",
"GENERIC_ERROR"
];

/**
* Errors specific to DataStore operations.
*
*/
class DataStoreError extends Error {
/**
* @constructs
* Creates a new DataStoreError with the given message and reason.
*
* @param {string} message - The error message
* @param {Symbol} [reason=DataStoreError.GENERIC_ERROR] - The reason for the error
* @param {string} [reason=DataStoreError.GENERIC_ERROR] - The reason for the error
* @param {string} [message] - The error message
*/
constructor(message, reason) {
if ("symbol" === typeof message) {
reason = message;
message = "";
constructor(reason, message) {
if (-1 === REASONS.indexOf(reason)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if we want to use Array#includes() versus indexOf() === -1. On that note, I'm not sure if we have any list of minimum browser support that we are planning, or if we should run this library through something like Babel to make it ES5 compliant (and minify it, etc).

Also, not sure if we should log any REASON misses. Like, if somebody typed DataStoreError.GENERIC_ERRROR instead of DataStoreError.GENERIC_ERROR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can take a look at Array#includes(), but I've been hesitant to use APIs that I haven't had personal experience with on other platforms ... notably iOS WebKit (which is the target for lockbox-ios).

As far as minifying ... prove to me the size is an actual problem to deal with here, and not something that the consumers are better equiped for.

message = reason;
reason = null;
}
if (!reason) {
reason = DataStoreError.GENERIC_ERROR;
}
if (!message) {
message = /^Symbol\((.*)\)$/.exec(reason.toString())[1];
message = reason;
} else {
message = `${reason}: ${message}`;
}

super(message);
Expand All @@ -36,50 +56,71 @@ class DataStoreError extends Error {
}

/**
* The reason for this DataStoreError.
* The reason name for this DataStoreError.
*
* @member {Symbol}
* @member {string}
*/
this.reason = reason;
}
}

REASONS.forEach((r) => DataStoreError[r] = r);

/**
* When opening a local database, the actual database version does not match the expected version.
* @member {string} DataStoreError.LOCALDB_VERSION
*/
/**
* The datastore is not yet initialized.
*
* @type Symbol
* @member {string} DataStoreError.NOT_INITIALIZED
*/
DataStoreError.NOT_INITIALIZED = Symbol("NOT_INITIALIZED");
/**
* An attempt was made to initialize a datastore that is already initialized.
*
* @type Symbol
* @member {string} DataStoreError.INITIALIZED
*/
DataStoreError.INITIALIZED = Symbol("INITIALIZED");
/**
* When opening the local database, the actual database version does not
* match the expected version.
*
* @type Symbol
* No master key was provided.
* @member {string} DataStoreError.MISSING_APP_KEY
*/
/**
* The master key is not valid for the encrypted data.
* @member {string} DataStoreError.WRONG_APP_KEY
*/
DataStoreError.LOCALDB_VERSION = Symbol("LOCALDB_VERSION");
/**
* An attempt was made to use a datastore that is still locked.
*
* @type Symbol
* @member {string} DataStoreError.LOCKED
*/
DataStoreError.LOCKED = Symbol("LOCKED");
/**
* The item to be added/updated is invalid.
*
* @type Symbol
* @member {string} DataStoreError.INVALID_ITEM
*/
DataStoreError.INVALID_ITEM = Symbol("INVALID_ITEM");
/**
* An otherwise unspecified error occured with the datastore.
*
* @type Symbol
* The item to be updated does not exist.
* @member {string} DataStoreError.MISSING_ITEM
*/
/**
* There was a cryptographic error.
* @member {string} DataStoreError.CRYPTO
*/
/**
* An operation requires network connectivety, but there is none.

Choose a reason for hiding this comment

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

connectivity*

* @member {string} DataStoreError.OFFLINE
*/
/**
* An operation requires (remote) authentication before it can be performed.
* @member {string} DataStoreError.AUTH
*/
/**
* An operation encountered a (generic) network error.
* @member {string} DataStoreError.NETWORK
*/
/**
* An attempt was made to sync a datastore, but cannot be completed until unlocked.
* @member {string} DataStoreError.SYNC_LOCKED
*/
/**
* An otherwise unspecified error occurred with the datastore.
* @member {string} DataStoreError.GENERIC_ERROR
*/
DataStoreError.GENERIC_ERROR = Symbol("GENERIC_ERROR");

module.exports = DataStoreError;
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"lint": "eslint . --ext=.js --ext=.json",
"pretest": "npm run lint && npm run :clean:coverage",
":test:karma": "karma start",
":test:node": "mocha test",
"test": "npm run :test:node && npm run :test:karma",
":test:node": "mocha --recursive test",
"test": "npm run :test:karma",
"predebug": "npm run lint",
"debug": "karma start karma.conf-debug.js",
"watch": "watch --interval=10 'npm run test && npm run doc' ./lib ./test",
Expand Down
Loading