├── README.md ├── package.json ├── sandbox.config.json ├── src └── gun-appendOnly.js └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 | ## What 2 | An extension to [gundb](https://github.com/amark/gun). 3 | 4 | Allows marking node as append only, this means fields of AO nodes (or refs from AO nodes) can only be added and their value can't be modify. 5 | There is an exception that if the ref is to a SEA signed node then the user of the signed node can change the ref. 6 | 7 | ## Why 8 | 9 | For example its a way to claim a username that reference a profile. 10 | gun.rootAO('indexbyusername').get(username).put(profileId). 11 | This makes the node 'indexbyusername' append only and thus once the key is set to it can not be modified. But other keys could be added to 'indexbyusername'. 12 | Once that username is defined no one else could change where it points to. Currently in gundb evereything is world writable, so theoretically anyone could change it. 13 | 14 | ## How to use 15 | 16 | - npm i gun @gooddollar/gun-appendOnly --save 17 | - import Gun from '@gooddollar/gun-appendOnly' 18 | - gun = new Gun() 19 | 20 | ## API 21 | 22 | gun.rootAO(key) - creates a root gundb node that is marked as append only 23 | 24 | gun.get(key).putAO(node) - put a ref to at making the node append only 25 | 26 | ## How it works 27 | 28 | Behind the scenes it appends '!@' to the soul of the node (souls in gundb are like ids) 29 | Extends gun wire protocol to check if the node being written to is marked with '!@' and if the put request contains an existing field. (check out the source code) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gooddollar/gun-appendonly", 3 | "version": "1.0.1", 4 | "description": "create append only nodes. their keys values can not be modified unless signed by same SEA user", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/GoodDollar/gun-appendOnly.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/GoodDollar/gun-appendOnly/issues" 11 | }, 12 | "license": "MIT", 13 | "source": "src/gun-appendOnly.js", 14 | "main": "dist/gun-appendOnly.js", 15 | "scripts": { 16 | "build": "microbundle --external gun, gun/sea", 17 | "dev": "microbundle watch" 18 | }, 19 | "dependencies": { 20 | "gun": "^0.2019.422", 21 | "lodash": "^4.17.11" 22 | }, 23 | "peerDependencies": { 24 | "gun": "^0.9" 25 | }, 26 | "files": [ 27 | "dist/gun-appendOnly.js", 28 | "src/gun-appendOnly.js" 29 | ], 30 | "devDependencies": { 31 | "@babel/core": "7.2.0", 32 | "jest": "^24.7.1", 33 | "microbundle": "^0.11.0", 34 | "parcel-bundler": "^1.6.1" 35 | }, 36 | "keywords": [] 37 | } 38 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "template": "node" 5 | } 6 | -------------------------------------------------------------------------------- /src/gun-appendOnly.js: -------------------------------------------------------------------------------- 1 | import Gun from "gun"; 2 | import SEA from "gun/sea"; 3 | import get from "lodash/get"; 4 | import map from "lodash/map"; 5 | import isObject from "lodash/isObject"; 6 | import find from "lodash/find"; 7 | import findKey from "lodash/findKey"; 8 | 9 | Gun.appendOnly = {}; 10 | /** 11 | * turn into a gun node and add the '!@' prefix to mark appendonly node 12 | * @param {} data the data to turn into a node (ie adding a soul) 13 | */ 14 | Gun.appendOnly.ify = function(data) { 15 | for (var i in data) { 16 | if (i !== "_" && isObject(data[i])) 17 | throw new Error("Nested objects not supported"); 18 | } 19 | const soulified = Gun.node.ify(data); 20 | const soul = Gun.node.soul(soulified); 21 | 22 | if (soul.slice(0, 2) !== "!@") soulified["_"]["#"] = "!@" + soul; 23 | return soulified; 24 | }; 25 | 26 | /** 27 | * Extends gun .get, this can be used to create a root append only key 28 | * @param {} key 29 | * @param {} cb 30 | * @param {} as 31 | */ 32 | Gun.chain.rootAO = function(key, cb, as) { 33 | const root = this._.root.$; 34 | as = as || {}; 35 | if (this !== root) { 36 | throw new Error("Use putAO for non root nodes"); 37 | } 38 | if (key.slice(0, 2) !== "!@") key = "!@" + key; 39 | return this.get(key, cb, as); 40 | }; 41 | /** 42 | * Extends gun put. this can be used to put a non root node as append only node. 43 | * This will try to change the current gun node, ie gun.get('node').putAO({z:1}) will try to overwrite 'node' 44 | * @param {} data 45 | * @param {} cb 46 | * @param {} as 47 | */ 48 | Gun.chain.putAO = function(data, cb, as) { 49 | if (!Gun.node.is(data)) { 50 | data = Gun.appendOnly.ify(data); 51 | } 52 | return this.put(data, cb, as); 53 | }; 54 | 55 | Gun.on("opt", async function(at) { 56 | if (!at.ao) { 57 | // only add once per instance, on the "at" context. 58 | at.ao = {}; 59 | at.on("in", appendOnly, at); // now listen to all input data, acting as a firewall. 60 | // at.on('out', signature, at); // and output listeners, to encrypt outgoing data. 61 | // at.on('node', each, at); 62 | } 63 | this.to.next(at); // make sure to call the "next" middleware adapter. 64 | }); 65 | 66 | function testNodeKeyVal(val, key) { 67 | const { soul, curNode } = this; 68 | if (key === "_") return true; 69 | const curValue = get(curNode, key); 70 | console.log("testKeyVal", { soul, val, key, curValue }); 71 | const curOwnerPub = SEA.opt.pub(Gun.val.link.is(curValue)); 72 | const newOwnerPub = SEA.opt.pub(Gun.val.link.is(val)); 73 | //if value is reference to another node and is a user signed SEA node 74 | if (curOwnerPub && newOwnerPub && newOwnerPub === curOwnerPub) { 75 | console.log("testKeyVal allowing SEA overwrite for same user", { 76 | curOwnerPub, 77 | newOwnerPub 78 | }); 79 | return true; 80 | } 81 | if (curValue !== undefined) return false; 82 | else return true; 83 | } 84 | function inverseTestNodeKeyVal(val, key) { 85 | return !testNodeKeyVal.bind(this)(val, key); 86 | } 87 | function appendOnly(msg) { 88 | const to = this.to, 89 | gun = this.as.gun; 90 | 91 | if (msg.put) { 92 | // console.log({ msg, x: msg.x }); 93 | //find additions to a node violating append only 94 | const promises = map(msg.put, async function(node, soul) { 95 | //if node is not append only then skip 96 | if (soul.slice(0, 2) !== "!@") return; 97 | if (Gun.obj.empty(node, "_")) { 98 | to.next(msg); 99 | } // ignore empty updates, don't reject them. 100 | //fetch current data 101 | const curNode = await gun.get(soul).then(); 102 | // console.log("promise msg.put iterate", { 103 | // node, 104 | // soul, 105 | // curNode, 106 | // keys: Object.keys(node) 107 | // }); 108 | //check if msg tries to modify existing keys 109 | const invalidKey = findKey( 110 | node, 111 | inverseTestNodeKeyVal.bind({ soul, node, curNode, gun }) 112 | ); 113 | if (invalidKey) return { soul, invalidKey }; 114 | return; 115 | }); 116 | //make sure we have atleast one promise 117 | promises.push(Promise.resolve()) 118 | return Promise.all(promises).then(r => { 119 | let invalid = find(r, x => x !== undefined); 120 | if (invalid) { 121 | console.error("invalid existing append only key", invalid); 122 | return; 123 | } 124 | // console.log("Appendonly ok, passing msg"); 125 | return to.next(msg); 126 | }); 127 | } else { 128 | // console.log("passing msg", { msg }); 129 | return to.next(msg); 130 | } 131 | } 132 | 133 | //examples 134 | /* 135 | let aoRoot = gun.rootAO('byname')//create an append only root node, basically simply create it with a soul '!@byname' 136 | aoRoot.get('mark').put({name:'mark nadal'}) 137 | aoRoot.get('mark').put(null)//FAIL 138 | aoRoot.get('mark').put({name:'daniel nadal'})//FAIL 139 | //PUT 140 | aoRoot.get('mark').putAO({name:'mark nadal'})//OK 141 | aoRoot.get('mark').put({name:'daniel nadal'})//OK 142 | aoRoot.get('daniel').put({name:'daniel nadal'})//OK 143 | aoRoot.put({mark:'z'}) // FAIL 144 | */ 145 | 146 | export default Gun; 147 | --------------------------------------------------------------------------------