diff --git a/examples/contact/index.html b/examples/contact/index.html index 19dd8169..38679545 100644 --- a/examples/contact/index.html +++ b/examples/contact/index.html @@ -153,7 +153,7 @@ // for how to build a simplified version // of this example: https://scrimba.com/c/c2gBgt4 var gun = Gun(location.origin+'/gun'); - var app = gun.get('example/contacts/6'); + var app = gun.get('example/contacts/7'); var user = gun.user(); var c = window.c = {}; (function(){ @@ -169,7 +169,7 @@ Gun.node.is(at.put.users, function(val, key){ Gun.SEA.verify(val, false, function(val){ //Gun.SEA.read(val, false, function(val){ - if('alias/'+key === Gun.val.rel.is(val)){ return } + if('~@'+key === Gun.val.rel.is(val)){ return } no = true; }) if(no){ return no } @@ -207,7 +207,7 @@ if(!ack.wait){ but.removeClass('pulse') } if(ack.err){ c.tell(ack.err); return } if(ack.pub){ - gun.get('users').get(data.alias).put(gun.get('alias/'+data.alias)); + gun.get('users').get(data.alias).put(gun.get('~@'+data.alias)); } session(data); user.auth(data.alias, data.pass); @@ -276,7 +276,7 @@ if(!user.is){ return as.route('sign') } var pub = location.hash.split('/').slice(-1)[0]; (pub === user._.pub? $('#say').show() : $('#say').hide()); - as('#person', window.PUB = gun.get('pub/'+pub)); + as('#person', window.PUB = gun.get('~'+pub)); }); diff --git a/gun.js b/gun.js index 725830d8..79e4dd46 100644 --- a/gun.js +++ b/gun.js @@ -1296,7 +1296,7 @@ var gun = this, at = (gun._), root = at.root.gun, tmp; as = as || {}; as.data = data; - as.gun = as.gun || gun; + as.via = as.gun = as.via || as.gun || gun; if(typeof cb === 'string'){ as.soul = cb; } else { @@ -1311,9 +1311,9 @@ if(as.res){ as.res() } return gun; } - as.soul = as.soul || (as.not = Gun.node.soul(as.data) || ((root._).opt.uuid || Gun.text.random)()); + as.soul = as.soul || (as.not = Gun.node.soul(as.data) || (as.via.back('opt.uuid') || Gun.text.random)()); if(!as.soul){ // polyfill async uuid for SEA - (root._).opt.uuid(function(err, soul){ // TODO: improve perf without anonymous callback + as.via.back('opt.uuid')(function(err, soul){ // TODO: improve perf without anonymous callback if(err){ return Gun.log(err) } // TODO: Handle error! (as.ref||as.gun).put(as.data, as.soul = soul, as); }); @@ -1415,10 +1415,10 @@ ref = ref.get(path[i]); } if(Gun.node.soul(at.obj)){ - var id = Gun.node.soul(at.obj) || (ref.back('opt.uuid') || Gun.text.random)(); + var id = Gun.node.soul(at.obj) || (as.via.back('opt.uuid') || Gun.text.random)(); if(!id){ // polyfill async uuid for SEA (as.stun = as.stun || {})[path] = true; // make DRY - ref.back('opt.uuid')(function(err, id){ // TODO: improve perf without anonymous callback + as.via.back('opt.uuid')(function(err, id){ // TODO: improve perf without anonymous callback if(err){ return Gun.log(err) } // TODO: Handle error. ref.back(-1).get(id); at.soul(id); @@ -1443,9 +1443,9 @@ var _id = (msg.put||empty)['#']; ev.off(); at = (msg.gun._.back); // go up 1! - var id = id || Gun.node.soul(cat.obj) || Gun.node.soul(at.put) || Gun.val.rel.is(at.put) || _id || at_._id || (as.gun.back('opt.uuid') || Gun.text.random)(); // TODO: BUG!? Do we really want the soul of the object given to us? Could that be dangerous? + var id = id || Gun.node.soul(cat.obj) || Gun.node.soul(at.put) || Gun.val.rel.is(at.put) || _id || at_._id || (as.via.back('opt.uuid') || Gun.text.random)(); // TODO: BUG!? Do we really want the soul of the object given to us? Could that be dangerous? if(!id){ // polyfill async uuid for SEA - at.gun.back('opt.uuid')(function(err, id){ // TODO: improve perf without anonymous callback + at.via.back('opt.uuid')(function(err, id){ // TODO: improve perf without anonymous callback if(err){ return Gun.log(err) } // TODO: Handle error. solve(at, at_._id = at_._id || id, cat, as); }); @@ -1495,16 +1495,16 @@ } if(!as.not && !(as.soul = Gun.node.soul(data))){ if(as.path && obj_is(as.data)){ // Apparently necessary - as.soul = (opt.uuid || cat.root.opt.uuid || Gun.text.random)(); + as.soul = (opt.uuid || as.via.back('opt.uuid') || Gun.text.random)(); } else { //as.data = obj_put({}, as.gun._.get, as.data); if(node_ == at.get){ as.soul = (at.put||empty)['#'] || at._id; } - as.soul = as.soul || at.soul || cat.soul || (opt.uuid || cat.root.opt.uuid || Gun.text.random)(); + as.soul = as.soul || at.soul || cat.soul || (opt.uuid || as.via.back('opt.uuid') || Gun.text.random)(); } if(!as.soul){ // polyfill async uuid for SEA - as.ref.back('opt.uuid')(function(err, soul){ // TODO: improve perf without anonymous callback + as.via.back('opt.uuid')(function(err, soul){ // TODO: improve perf without anonymous callback if(err){ return Gun.log(err) } // Handle error. as.ref.put(as.data, as.soul = soul, as); }); @@ -1727,11 +1727,11 @@ opt = opt || {}; opt.item = opt.item || item; if(soul = Gun.node.soul(item)){ return gun.set(gun.back(-1).get(soul), cb, opt) } if(!Gun.is(item)){ - var id = gun._.root.opt.uuid(); + var id = gun.back('opt.uuid')(); if(id && Gun.obj.is(item)){ return gun.set(gun._.root.gun.put(item, id), cb, opt); } - return gun.get(id || (Gun.state.lex() + Gun.text.random(12))).put(item, cb, opt); + return gun.get((Gun.state.lex() + Gun.text.random(7))).put(item, cb, opt); } item.get('_').get(function(at, ev){ if(!at.gun || !at.gun._.back){ return } diff --git a/lib/store.js b/lib/store.js index 359c8607..bdf031e2 100644 --- a/lib/store.js +++ b/lib/store.js @@ -63,7 +63,7 @@ function Store(opt){ var random = Math.random().toString(36).slice(-3) fs.writeFile(opt.file+'-'+random+'.tmp', data, function(err, ok){ if(err){ return cb(err) } - fs.rename(opt.file+'-'+random+'.tmp', opt.file+'/'+file, cb); + move(opt.file+'-'+random+'.tmp', opt.file+'/'+file, cb); }); }; store.get = function(file, cb){ @@ -88,6 +88,30 @@ function Store(opt){ return store; } +function move(oldPath, newPath, cb) { + fs.rename(oldPath, newPath, function (err) { + if (err) { + if (err.code === 'EXDEV') { + var readStream = fs.createReadStream(oldPath); + var writeStream = fs.createWriteStream(newPath); + + readStream.on('error', cb); + writeStream.on('error', cb); + + readStream.on('close', function () { + fs.unlink(oldPath, cb); + }); + + readStream.pipe(writeStream); + } else { + cb(err); + } + } else { + cb(); + } + }); +}; + module.exports = Store; diff --git a/package.json b/package.json index 009460b0..f99dcede 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gun", - "version": "0.9.998", + "version": "0.9.999", "description": "A realtime, decentralized, offline-first, graph data synchronization engine.", "main": "index.js", "browser": "gun.min.js", diff --git a/sea.js b/sea.js index 775da383..23fa57ab 100644 --- a/sea.js +++ b/sea.js @@ -108,7 +108,7 @@ } else if (enc === 'binary') { buf = SeaArray.from(input) } else { - console.info(`SafeBuffer.from unknown encoding: '${enc}'`) + console.info('SafeBuffer.from unknown encoding: '+enc) } return buf } @@ -639,7 +639,7 @@ // This is internal func queries public key(s) for alias. const queryGunAliases = (alias, gunRoot) => new Promise((resolve, reject) => { // load all public keys associated with the username alias we want to log in with. - gunRoot.get(`alias/${alias}`).get((rat, rev) => { + gunRoot.get('~@'+alias).get((rat, rev) => { rev.off(); if (!rat.put) { // if no user, don't do anything. @@ -652,14 +652,14 @@ let c = 0 // TODO: how about having real chainable map without callback ? Gun.obj.map(rat.put, (at, pub) => { - if (!pub.slice || 'pub/' !== pub.slice(0, 4)) { + if (!pub.slice || '~' !== pub.slice(0, 1)) { // TODO: ... this would then be .filter((at, pub)) return } ++c // grab the account associated with this public key. gunRoot.get(pub).get((at, ev) => { - pub = pub.slice(4) + pub = pub.slice(1) ev.off() --c if (at.put){ @@ -835,7 +835,9 @@ const { user } = gunRoot._ // add our credentials in-memory only to our root gun instance //var tmp = user._.tag; - user._ = key.at.gun._ + var opt = user._.opt; + user._ = key.at.gun._; + user._.opt = opt; //user._.tag = tmp || user._.tag; // so that way we can use the credentials to encrypt/decrypt data // that is input/output through gun (see below) @@ -989,7 +991,7 @@ } catch (e) { // TODO: right log message ? Gun.log('Failed to finalize login with new password!') const { err = '' } = e || {} - throw { err: `Finalizing new password login failed! Reason: ${err}` } + throw { err: 'Finalizing new password login failed! Reason: '+err } } } module.exports = authRecall @@ -1042,9 +1044,19 @@ // let's extend the gun chain with a `user` function. // only one user can be logged in at a time, per gun instance. Gun.chain.user = function(pub){ - var gun = this, root = gun.back(-1); - if(pub){ return root.get('pub/'+pub) } - return root.back('user') || ((root._).user = gun.chain(new User)); + var gun = this, root = gun.back(-1), user; + if(pub){ return root.get('~'+pub) } + if(user = root.back('user')){ return user } + var root = (root._), at = root, uuid = at.opt.uuid || Gun.state.lex; + (at = (user = at.user = gun.chain(new User))._).opt = {}; + at.opt.uuid = function(cb){ + var id = uuid(), pub = root.user; + if(!pub || !(pub = (pub._).sea) || !(pub = pub.pub)){ return id } + id = id + '~' + pub + '.'; + if(cb && cb.call){ cb(null, id) } + return id; + } + return user; } module.exports = User; })(USE, './user'); @@ -1078,7 +1090,7 @@ var resolve = function(){}, reject = resolve; // Because more than 1 user might have the same username, we treat the alias as a list of those users. if(cb){ resolve = reject = cb } - gunRoot.get(`alias/${username}`).get(async (at, ev) => { + gunRoot.get('~@'+username).get(async (at, ev) => { ev.off() if (at.put) { // If we can enforce that a user name is already taken, it might be nice to try, but this is not guaranteed. @@ -1109,14 +1121,14 @@ // ) ).catch((e) => { Gun.log('SEA.en or SEA.write calls failed!'); cat.ing = false; gun.leave(); reject(e) }) const user = { alias, pub, epub, auth } - const tmp = `pub/${pairs.pub}` + const tmp = '~'+pairs.pub; // awesome, now we can actually save the user with their public key as their ID. try{ gunRoot.get(tmp).put(user) }catch(e){console.log(e)} // next up, we want to associate the alias with the public key. So we add it to the alias list. - gunRoot.get(`alias/${username}`).put(Gun.obj.put({}, tmp, Gun.val.rel.ify(tmp))) + gunRoot.get('~@'+username).put(Gun.obj.put({}, tmp, Gun.val.rel.ify(tmp))) // callback that the user has been created. (Note: ok = 0 because we didn't wait for disk to ack) setTimeout(() => { cat.ing = false; resolve({ ok: 0, pub: pairs.pub}) }, 10) // TODO: BUG! If `.auth` happens synchronously after `create` finishes, auth won't work. This setTimeout is a temporary hack until we can properly fix it. } catch (e) { @@ -1157,7 +1169,7 @@ const putErr = (msg) => (e) => { const { message, err = message || '' } = e Gun.log(msg) - var error = { err: `${msg} Reason: ${err}` } + var error = { err: msg+' Reason: '+err } return cat.ing = false, gun.leave(), cb(error), gun; } @@ -1186,7 +1198,7 @@ epub: signedEpub } // awesome, now we can update the user using public key ID. - gunRoot.get(`pub/${user.pub}`).put(user) + gunRoot.get('~'+user.pub).put(user) // then we're done const login = finalizeLogin(alias, keys, gunRoot, { pin }) login.catch(putErr('Failed to finalize login with new password!')) @@ -1219,7 +1231,7 @@ const { pub } = await authenticate(alias, pass, gunRoot) await authLeave(gunRoot, alias) // Delete user data - gunRoot.get(`pub/${pub}`).put(null) + gunRoot.get('~'+pub).put(null) // Wipe user data from memory const { user = { _: {} } } = gunRoot._; // TODO: is this correct way to 'logout' user from Gun.User ? @@ -1354,14 +1366,6 @@ Gun.on('opt', function(at){ if(!at.sea){ // only add SEA once per instance, on the "at" context. at.sea = {own: {}}; - var uuid = at.opt.uuid || Gun.state.lex; - at.opt.uuid = function(cb){ // TODO: consider async/await and drop callback pattern... - var id = uuid(), pub = at.user; - if(!pub || !(pub = at.user._.sea) || !(pub = pub.pub)){ return id } - id = id + '~' + pub; - if(cb && cb.call){ cb(null, id) } - return id; - } at.on('in', security, at); // now listen to all input data, acting as a firewall. at.on('out', signature, at); // and output listeners, to encrypt outgoing data. at.on('node', each, at); @@ -1376,7 +1380,7 @@ // Now NOTE! Some data is "system" data, not user data. Example: List of public keys, aliases, etc. // This data is self-enforced (the value can only match its ID), but that is handled in the `security` function. // From the self-enforced data, we can see all the edges in the graph that belong to a public key. - // Example: pub/ASDF is the ID of a node with ASDF as its public key, signed alias and salt, and + // Example: ~ASDF is the ID of a node with ASDF as its public key, signed alias and salt, and // its encrypted private key, but it might also have other signed values on it like `profile = ` edge. // Using that directed edge's ID, we can then track (in memory) which IDs belong to which keys. // Here is a problem: Multiple public keys can "claim" any node's ID, so this is dangerous! @@ -1420,7 +1424,7 @@ if('alias' === soul){ // Allow reading the list of usernames/aliases in the system? return to.next(msg); // yes. } else - if('alias/' === soul.slice(0,6)){ // Allow reading the list of public keys associated with an alias? + if('~@' === soul.slice(0,2)){ // Allow reading the list of public keys associated with an alias? return to.next(msg); // yes. } else { // Allow reading everything? return to.next(msg); // yes // TODO: No! Make this a callback/event that people can filter on. @@ -1437,29 +1441,29 @@ each.way = function(val, key){ var soul = this.soul, node = this.node, tmp; if('_' === key){ return } // ignore meta data - if('alias' === soul){ // special case for shared system data, the list of aliases. + if('~@' === soul){ // special case for shared system data, the list of aliases. each.alias(val, key, node, soul); return; } - if('alias/' === soul.slice(0,6)){ // special case for shared system data, the list of public keys for an alias. + if('~@' === soul.slice(0,2)){ // special case for shared system data, the list of public keys for an alias. each.pubs(val, key, node, soul); return; } - if('pub/' === soul.slice(0,4)){ // special case, account data for a public key. - each.pub(val, key, node, soul, soul.slice(4), msg.user); return; + if('~' === soul.slice(0,1) && 2 === (tmp = soul.slice(1)).split('.').length){ // special case, account data for a public key. + each.pub(val, key, node, soul, tmp, msg.user); return; } each.any(val, key, node, soul, msg.user); return; return each.end({err: "No other data allowed!"}); }; - each.alias = function(val, key, node, soul){ // Example: {_:#alias, alias/alice: {#alias/alice}} + each.alias = function(val, key, node, soul){ // Example: {_:#~@, ~@alice: {#~@alice}} if(!val){ return each.end({err: "Data must exist!"}) } // data MUST exist - if('alias/'+key === Gun.val.rel.is(val)){ return check['alias'+key] = 0 } // in fact, it must be EXACTLY equal to itself + if('~@'+key === Gun.val.rel.is(val)){ return check['alias'+key] = 0 } // in fact, it must be EXACTLY equal to itself each.end({err: "Mismatching alias."}); // if it isn't, reject. }; - each.pubs = function(val, key, node, soul){ // Example: {_:#alias/alice, pub/asdf: {#pub/asdf}} + each.pubs = function(val, key, node, soul){ // Example: {_:#~@alice, ~asdf: {#~asdf}} if(!val){ return each.end({err: "Alias must exist!"}) } // data MUST exist if(key === Gun.val.rel.is(val)){ return check['pubs'+soul+key] = 0 } // and the ID must be EXACTLY equal to its property each.end({err: "Alias must match!"}); // that way nobody can tamper with the list of public keys. }; - each.pub = function(val, key, node, soul, pub, user){ // Example: {_:#pub/asdf, hello:SEA['world',fdsa]} + each.pub = function(val, key, node, soul, pub, user){ // Example: {_:#~asdf, hello:SEA{'world',fdsa}} if('pub' === key){ if(val === pub){ return (check['pub'+soul+key] = 0) } // the account MUST match `pub` property that equals the ID of the public key. return each.end({err: "Account must match!"}); @@ -1479,30 +1483,34 @@ // TODO: Handle error!!!! return; } - // TODO: consider async/await and drop callback pattern... SEA.verify(val, pub, function(data){ var rel, tmp; if(u === data){ // make sure the signature matches the account it claims to be on. return each.end({err: "Unverified data."}); // reject any updates that are signed with a mismatched account. } - if((rel = Gun.val.rel.is(data)) && (tmp = rel.split('~')) && 2 === tmp.length){ - if(pub === tmp[1]){ - (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; - } + if((rel = Gun.val.rel.is(data)) && pub === relpub(rel)){ + (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; } check['user'+soul+key] = 0; each.end({ok: 1}); }); }; + function relpub(s){ + if(!s){ return } + s = s.split('~'); + if(!s || !(s = s[1])){ return } + s = s.split('.'); + if(!s || 2 > s.length){ return } + s = s.slice(0,2).join('.'); + return s; + } each.any = function(val, key, node, soul, user){ var tmp, pub; if(!user || !(user = user._) || !(user = user.sea)){ - if((tmp = soul.split('~')) && 2 == tmp.length){ + if(tmp = relpub(soul)){ check['any'+soul+key] = 1; - SEA.verify(val, (pub = tmp[1]), function(data){ var rel; + SEA.verify(val, pub = tmp, function(data){ var rel; if(!data){ return each.end({err: "Mismatched owner on '" + key + "'."}) } - if((rel = Gun.val.rel.is(data)) && (tmp = rel.split('~')) && 2 === tmp.length){ - if(pub === tmp[1]){ - (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; - } + if((rel = Gun.val.rel.is(data)) && pub === relpub(rel)){ + (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; } check['any'+soul+key] = 0; each.end({ok: 1}); @@ -1518,7 +1526,7 @@ //each.end({err: "Data cannot be modified."}); return; } - if(!(tmp = soul.split('~')) || 2 !== tmp.length){ + if(!(tmp = relpub(soul))){ if(at.opt.secure){ each.end({err: "Soul is missing public key at '" + key + "'."}); return; @@ -1528,16 +1536,16 @@ each.end({ok: 1}); return; } - check['any'+soul+key] = 1; - SEA.sign(val, user, function(data){ - if(u === data){ return each.end({err: 'Any signature failed.'}) } - node[key] = data; + //check['any'+soul+key] = 1; + //SEA.sign(val, user, function(data){ + // if(u === data){ return each.end({err: 'Any signature failed.'}) } + // node[key] = data; check['any'+soul+key] = 0; each.end({ok: 1}); - }); + //}); return; } - var pub = tmp[1]; + var pub = tmp; if(pub !== user.pub){ each.any(val, key, node, soul); return;