diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..befb97d2
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,75 @@
+name: ci
+
+on: [push, pull_request]
+
+env:
+ project: 'release-node'
+
+jobs:
+
+ test:
+ strategy:
+ matrix:
+ node-version: [14.x] # [12.x, 14.x]
+ os: [ubuntu-latest] #, macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ # verify the version in package.json matches the release tag
+ - name: Version
+ uses: tcurdt/action-verify-version-npm@master
+
+ - name: Setup Node ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - name: Cache
+ id: cache-modules
+ uses: actions/cache@v2
+ with:
+ path: node_modules
+ key: ${{ matrix.node-version }}-${{ runner.OS }}-build-${{ hashFiles('package.json') }}
+
+ - name: Install
+ if: steps.cache-modules.outputs.cache-hit != 'true'
+ run: npm install
+
+ - name: Test
+ run: npm test
+
+ # create release artifacts to publish as github release
+ # - name: Upload
+ # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ # uses: actions/upload-artifact@v2
+ # with:
+ # name: ${{ env.project }}_${{ matrix.os }}_${{ matrix.node-version }}
+ # path: |
+ # !.git
+ # !.github
+ # !node_modules
+ # .
+
+ release:
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ needs: [test]
+ runs-on: ubuntu-latest
+ steps:
+
+ # - name: Download
+ # uses: actions/download-artifact@v2
+ # with:
+ # path: artifacts
+ # - name: Archives
+ # run: find artifacts -mindepth 1 -maxdepth 1 -exec tar -C {} -cvzf {}.tgz . \;
+
+ - name: Release
+ uses: softprops/action-gh-release@v1
+ # with:
+ # files: |
+ # artifacts/*.tgz
+ env:
+ GITHUB_TOKEN: ${{ secrets.PAT }}
diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml
new file mode 100644
index 00000000..067e221e
--- /dev/null
+++ b/.github/workflows/cleanup.yml
@@ -0,0 +1,17 @@
+name: cleanup
+
+on:
+ schedule:
+ - cron: '1 1 * * 1' # once a week clean out old artifacts
+
+jobs:
+
+ expire:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Expire Artifacts
+ uses: kolpav/purge-artifacts-action@v1
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ expire-in: 1hour
diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml
new file mode 100644
index 00000000..17eb638a
--- /dev/null
+++ b/.github/workflows/dockerhub.yml
@@ -0,0 +1,35 @@
+name: dockerhub
+
+on:
+ release:
+ types: [published]
+
+env:
+ project: 'release-node'
+
+jobs:
+
+ dockerhub:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Build
+ run: |
+ docker build -t ${{ env.project }} .
+
+ - name: Login
+ env:
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
+ run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
+
+ - name: Tag
+ run: |
+ docker tag ${{ env.project }} ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.project }}:${GITHUB_REF/refs\/tags\/v/}
+ docker tag ${{ env.project }} ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.project }}:latest
+
+ - name: Push
+ run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.project }}
diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml
new file mode 100644
index 00000000..6af0892d
--- /dev/null
+++ b/.github/workflows/npm.yml
@@ -0,0 +1,22 @@
+name: npm
+
+on:
+ release:
+ types: [published]
+
+jobs:
+
+ npm:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Publish
+ env:
+ NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
+ run: |
+ npm config set //registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN
+ npm install
+ npm publish --access=public
diff --git a/lib/meta.js b/lib/meta.js
index 1c2ab297..58d3f740 100644
--- a/lib/meta.js
+++ b/lib/meta.js
@@ -168,7 +168,8 @@
}
}
var $m = $('
').attr('id', 'meta');
- $m.append($('
').html('☰').addClass('meta-start'));
+ //$m.append($('').html('☰').addClass('meta-start'));
+ $m.append($('').html('+').addClass('meta-start'));
$m.append($('').addClass('meta-menu meta-none').append('
'));
$m.on('mouseenter', function(){
if (meta.flip.active || meta.flip.is()) return;
@@ -186,7 +187,6 @@
position: 'fixed',
bottom: '2em',
right: '2em',
- background: 'white',
'font-size': '18pt',
'font-family': 'Tahoma, arial',
'border-radius': '1em',
@@ -197,15 +197,15 @@
width: '2em',
height: '2em',
outline: 'none',
- color: '#000044',
overflow: 'visible',
+ background: 'rgba(0,0,0,0.5)', color: 'white',
transition: 'all 0.2s ease-in'
},
'#meta *': {outline: 'none'},
'#meta .meta-none': {display: 'none'},
'#meta span': {'line-height': '2em'},
'#meta .meta-menu': {
- background: 'rgba(0,0,0,0.1)',
+ background: 'rgba(0,0,0,0.2)',
width: '12em',
right: '-2em',
bottom: '-2em',
@@ -224,16 +224,15 @@
'#meta .meta-menu ul li': {
display: 'block',
'float': 'right',
- background: 'white',
- opacity: 0.7,
padding: '0.5em 1em',
'border-radius': '1em',
'margin-left': '0.25em',
'margin-top': '0.25em',
+ background: 'rgba(0,0,0,0.2)', 'backdrop-filter': 'blur(10px)', color: 'white',
'cursor': 'pointer'
},
'#meta .meta-menu ul li:hover': {
- opacity: 1
+ background: 'rgba(0,0,0,0.5)'
},
'#meta a': {color: 'black'},
'#meta .meta-menu ul:before': {
@@ -289,4 +288,4 @@
meta.flip()
})
})(USE, './metaEvents');
-}());
\ No newline at end of file
+}());
diff --git a/sea.js b/sea.js
index b71d070a..9a8dc843 100644
--- a/sea.js
+++ b/sea.js
@@ -650,6 +650,67 @@
module.exports = SEA.secret;
})(USE, './secret');
+ ;USE(function(module){
+ var SEA = USE('./root');
+
+ // This is to certify that a group of "certificants" can "put" anything at a group of matched "paths" to the certificate issuer's graph
+ SEA.certify = SEA.certify || (async (certificants, patterns, issuer, cb, opt = {}) => { try {
+ /*
+ IMPORTANT: A Certificate is like a Signature. No one knows who (issuer) created/signed a cert until you put it into their graph.
+ "certificants": A string (~Bobpub) || a pair || an array of pubs/pairs. These people will have the rights.
+ "patterns": A string (^inbox.*), or an array of strings [^inbox.*, ^secret\-group.*]. These patterns will be used to check against soul+'/'+key
+ "issuer": Key pair or priv of the certificate issuer
+ "cb": A callback function after all things are done
+ "opt": If opt.expiry (a timestamp) is set, SEA won't sync data after opt.expiry
+ */
+
+ // We need some logic here to verify that all params are valid
+
+ console.log('SEA.certify() is an early experimental community supported method that may change API behavior without warning in any future version.')
+
+ certificants = (() => {
+ var data = []
+ if (certificants) {
+ if (typeof certificants === 'string') {
+ data.push(certificants)
+ }
+
+ if (Array.isArray(certificants)) {
+ certificants.map(person => {
+ if (typeof person ==='string') data.push(person)
+ else if (typeof person === 'object' && person.pub) data.push(person.pub)
+ })
+ }
+
+ if (typeof certificants === 'object' && certificants.pub) data.push(certificants.pub)
+ }
+ return data
+ })()
+
+ patterns = patterns ? typeof patterns === 'string' ? [patterns] : Array.isArray(patterns) ? patterns : null : null
+
+ const data = JSON.stringify({
+ c: certificants,
+ p: patterns,
+ ...(opt.expiry && typeof opt.expiry === 'number' ? {e: opt.expiry} : {}) // inject expiry if possible
+ })
+
+ const certificate = await SEA.sign(data, issuer, null, {raw:1})
+
+ var r = certificate
+ if(!opt.raw){ r = 'SEA'+JSON.stringify(r) }
+ if(cb){ try{ cb(r) }catch(e){console.log(e)} }
+ return r;
+ } catch(e) {
+ SEA.err = e;
+ if(SEA.throw){ throw e }
+ if(cb){ cb() }
+ return;
+ }});
+
+ module.exports = SEA.certify;
+ })(USE, './certify');
+
;USE(function(module){
var shim = USE('./shim');
// Practical examples about usage found in tests.
@@ -659,6 +720,7 @@
SEA.verify = USE('./verify');
SEA.encrypt = USE('./encrypt');
SEA.decrypt = USE('./decrypt');
+ SEA.certify = USE('./certify');
//SEA.opt.aeskey = USE('./aeskey'); // not official! // this causes problems in latest WebCrypto.
SEA.random = SEA.random || shim.random;
@@ -1137,6 +1199,7 @@
;USE(function(module){
var SEA = USE('./sea')
+ var S = USE('./settings')
var Gun = SEA.Gun;
// After we have a GUN extension to make user registration/login easy, we then need to handle everything else.
@@ -1217,6 +1280,7 @@
return; // omit!
}
}
+
if('~@' === soul){ // special case for shared system data, the list of aliases.
check.alias(eve, msg, val, key, soul, at, no); return;
}
@@ -1249,27 +1313,83 @@
if(key === link_is(val)){ return eve.to.next(msg) } // and the ID must be EXACTLY equal to its property
no("Alias not same!"); // that way nobody can tamper with the list of public keys.
};
- check.pub = function(eve, msg, val, key, soul, at, no, user, pub){ var tmp; // Example: {_:#~asdf, hello:'world'~fdsa}}
- if('pub' === key && '~'+pub === soul){
- if(val === pub){ return eve.to.next(msg) } // the account MUST match `pub` property that equals the ID of the public key.
- return no("Account not same!");
+ check.pub = function(eve, msg, val, key, soul, at, no, user, pub){ var tmp // Example: {_:#~asdf, hello:'world'~fdsa}}
+ const raw = S.parse(val) || {}
+
+ const verify = (certificate, certificant, cb) => {
+ if (certificate['m'] && certificate['s'] && certificant && pub) {
+ // now verify certificate
+ return SEA.verify(certificate, pub, data => { // check if "pub" (of the graph owner) really issued this cert
+ if (u !== data && u !== data.e && msg.put['>'] && msg.put['>'] > parseFloat(data.e)) return no("Certificate expired.")
+ // "data.c" = a list of certificants/certified users, "data.p" = a list of allowed patterns
+ if (u !== data && data.c && data.p && (data.c.indexOf('*') || data.c.indexOf(certificant))) {
+ // ok, now "certificant" is in the "certificants" list, but is "path" allowed? Check path
+ let path = soul + '/' + key
+ path = path.replace(path.substring(0, path.indexOf('/') + 1), '')
+ for (p of data.p) {
+ if (new RegExp(p).test(path)) {
+ return cb(data)
+ }
+ }
+ return no("Certificate verification fail.")
+ }
+ })
+ }
+ return
}
- if((tmp = user.is) && pub === tmp.pub){
+
+ if ('pub' === key && '~'+pub === soul) {
+ if(val === pub){ return eve.to.next(msg) } // the account MUST match `pub` property that equals the ID of the public key.
+ return no("Account not same!")
+ }
+
+ if ((tmp = user.is) && tmp.pub && !raw['*'] && !raw['+'] && (pub === tmp.pub || (pub !== tmp.pub && ((msg._.out || {}).opt || {}).cert))){
SEA.sign(SEA.opt.pack(msg.put), (user._).sea, function(data){
if(u === data){ return no(SEA.err || 'Signature fail.') }
- if(tmp = link_is(val)){ (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 }
- msg.put[':'] = JSON.stringify({':': tmp = SEA.opt.unpack(data.m), '~': data.s});
- msg.put['='] = tmp;
- eve.to.next(msg);
- }, {raw: 1});
+ msg.put[':'] = {':': tmp = SEA.opt.unpack(data.m), '~': data.s}
+ msg.put['='] = tmp
+
+ // if writing to own graph, just allow it
+ if (pub === user.is.pub) {
+ if (tmp = link_is(val)) { (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 }
+ msg.put[':'] = JSON.stringify(msg.put[':'])
+ return eve.to.next(msg)
+ }
+
+ // if writing to other's graph, check if cert exists then try to inject cert into put, also inject self pub so that everyone can verify the put
+ if (pub !== user.is.pub && ((msg._.out || {}).opt || {}).cert) {
+ const cert = S.parse(msg._.out.opt.cert)
+ // even if cert exists, we must verify it
+ if (cert && cert.m && cert.s) {
+ verify(cert, user.is.pub, _ => {
+ msg.put[':']['+'] = cert // '+' is a certificate
+ msg.put[':']['*'] = user.is.pub // '*' is pub of the user who puts
+ msg.put[':'] = JSON.stringify(msg.put[':'])
+ return eve.to.next(msg)
+ })
+ }
+ }
+ }, {raw: 1})
return;
}
- SEA.verify(SEA.opt.pack(msg.put), pub, function(data){ var tmp;
+
+ SEA.verify(SEA.opt.pack(msg.put), raw['*'] || pub, function(data){ var tmp;
data = SEA.opt.unpack(data);
if(u === data){ return no("Unverified data.") } // make sure the signature matches the account it claims to be on. // reject any updates that are signed with a mismatched account.
if((tmp = link_is(data)) && pub === SEA.opt.pub(tmp)){ (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 }
- msg.put['='] = data;
- eve.to.next(msg);
+
+ // check if cert ('+') and putter's pub ('*') exist
+ if (raw['+'] && raw['+']['m'] && raw['+']['s'] && raw['*']) {
+ // now verify certificate
+ verify(raw['+'], raw['*'], _ => {
+ msg.put['='] = data;
+ return eve.to.next(msg);
+ })
+ }
+ else {
+ msg.put['='] = data;
+ return eve.to.next(msg);
+ }
});
};
check.any = function(eve, msg, val, key, soul, at, no, user){ var tmp, pub;
diff --git a/test/sea/sea.js b/test/sea/sea.js
index 0f5979bd..5ffc1c89 100755
--- a/test/sea/sea.js
+++ b/test/sea/sea.js
@@ -1,3 +1,5 @@
+const expect = require('../expect')
+
var root;
var Gun;
(function(){
@@ -523,6 +525,119 @@ describe('SEA', function(){
gun.user().auth(alice);
});
});
+
+ var alice = {
+ epriv: "Odtnqn-gng-NCLAULCdhxcG7KE26WSWdnNTBSYf8Dsw",
+ epub:
+ "rOWulaGGaNOKhrS9XtZUbdWjcIfTM5k5pImolyNwLe0.9Ks7JRrOQl3e401dSgCGlNWgvIC_DQm0EA9jGKXBDg0",
+ priv: "ijke9inZcbIpNUy5p3wiMRxUvqM12xU8WLewGzUXj8E",
+ pub:
+ "Zpf4KFmDmxNnHbRcTkZcAvPnke8_4hLv_FtNhBLcSps.ICAIjzky_T0ENNFIC5cjE-dN87dWp7cb88y0Rb3Nbvo"
+ }
+
+ var bob = {
+ epriv: "z5OC6iWYPVZO-sNqxd20t_qAPsA5nn9d-_yg5uW2mZM",
+ epub:
+ "bHUUjC-xP9QoTEyY5rubZJwft_szXgvetGOGUPOT8Mw.5J2j9SBZ8lqSHKgeFRbMZDs0EuNgM-VVWgMHE3YMFSI",
+ priv: "dWDbrbKUinmSxrlilKmyPzKAgmZCzm9i14bTydUf0kQ",
+ pub:
+ "naP2o7Ebn5tFF2V-z8pDFwOgOazduoiKogWnZ0cTtEE.K-sa7v6DXkb_saMFlCepqPUH--C-6rv6cO1t3wEo6-M"
+ }
+
+ var dave = {
+ epriv: "1eBCIIk30bzfTN50uqSTIN10TWP2AqkqExioV5P-oCE",
+ epub:
+ "PneRg1oMRw3mrrnjbRq8YSADLrdbg8BGEBoC0_P6It4.Vhv8QIkLhurxU5-LhHctaaNn5u3LNujqMNRdh6JbzvU",
+ priv: "XKJrGFWoERdHfhXfhc-VY0nIWeI2eAIDAfkX0nu_O0A",
+ pub:
+ "AXEGD2GABu1lnzTKML_wXHSlznwI5ZFebF-MLPKxI8Y.0Sx-Sd5GpF_1kzrUcliWRdhppq7FFehQi41oZ-WOJmI"
+ }
+
+ it('certify + the good', function(done){(async function(){
+ var cert = await SEA.certify(bob, ["^private.*"], alice)
+
+ user.auth(bob, () => {
+ var data = Gun.state.lex()
+ gun.get("~" + alice.pub)
+ .get("private")
+ .get("asdf")
+ .get("qwerty")
+ .put(data, () => {
+ gun.get("~" + alice.pub)
+ .get("private")
+ .get("asdf")
+ .get("qwerty").once(_data=>{
+ expect(_data).to.be(data)
+ user.leave()
+ done()
+ })
+ }, { opt: { cert } })
+ })
+ }())})
+
+ it('certify + the public', function(done){(async function(){
+ var cert = await SEA.certify(bob, ["^private.*"], alice)
+
+ user.auth(bob, () => {
+ var data = Gun.state.lex()
+ gun.get("~" + alice.pub)
+ .get("private")
+ .get("asdf")
+ .get("qwerty")
+ .put(data, () => {
+ user.leave()
+ gun.get("~" + alice.pub)
+ .get("private")
+ .get("asdf")
+ .get("qwerty").once(_data=>{
+ expect(_data).to.be(data)
+ done()
+ })
+ }, { opt: { cert } })
+ })
+ }())})
+
+ it('certify + someone', function(done){(async function(){
+ var cert = await SEA.certify(bob, ["^private.*"], alice)
+
+ user.auth(bob, () => {
+ var data = Gun.state.lex()
+ gun.get("~" + alice.pub)
+ .get("private")
+ .get("asdf")
+ .get("qwerty")
+ .put(data, () => {
+ user.leave()
+ user.auth(dave, () => {
+ gun.get("~" + alice.pub)
+ .get("private")
+ .get("asdf")
+ .get("qwerty").once(_data=>{
+ expect(_data).to.be(data)
+ done()
+ })
+ })
+
+ }, { opt: { cert } })
+ })
+ }())})
+
+ it('certify + the evil', function(done){(async function(){
+ var cert = await SEA.certify(bob, ["^private.*"], alice)
+
+ user.auth(bob, () => {
+ var data = Gun.state.lex()
+ gun.get("~" + alice.pub)
+ .get("wrongway")
+ .get("asdf")
+ .get("qwerty")
+ .put(data, ack => {
+ expect(ack.err).to.be.ok()
+ done()
+ }, { opt: { cert } })
+ })
+ }())})
+
});
})