├── .github └── stale.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── app └── html │ ├── menuItem │ └── config.js │ └── page │ ├── config.js │ └── invite.js ├── blob └── sync │ └── url.js ├── config └── sync │ └── load.js ├── index.js ├── main.js ├── manifest.json └── package.json /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | /coverage 4 | /.nyc_output 5 | /build 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "6" 3 | - "7" 4 | sudo: false 5 | language: node_js 6 | script: "npm run test:coverage && npm run test:coverage:report" 7 | after_script: "npm i -g codecov.io && cat ./coverage/lcov.info | codecov" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # patchlite 2 | 3 | A browser client for the Scuttlebutt network 4 | 5 | _work in progress_ 6 | 7 | ## Setup 8 | 9 | Install Scuttlebot (your gossip server) 10 | 11 | ```sh 12 | npm install scuttlebot@latest -g 13 | 14 | # make sure you have secure-scuttlebutt@15.2.0 15 | npm ls secure-scuttlebutt -g 16 | 17 | sbot server 18 | # if you are already running patchwork, that also works. 19 | # (must have at least >= 2.8) 20 | 21 | # then in another tab (these must be separate commands) 22 | sbot plugins.install ssb-links 23 | sbot plugins.install ssb-query 24 | sbot plugins.install ssb-ws 25 | sbot plugins.install ssb-fulltext # for faster searches (optional) 26 | 27 | # restart sbot server (go back to previous tab and kill it) 28 | ``` 29 | 30 | Set a WebSocket port in your config file (`~/.ssb/config`). 31 | 32 | ``` json 33 | { 34 | "ws": { 35 | "port": 8989 36 | } 37 | } 38 | ``` 39 | 40 | Install Patchlite (a browser interface for the your scuttlebutt database) 41 | 42 | ```sh 43 | git clone https://github.com/ssbc/patchlite.git 44 | cd patchlite 45 | npm install 46 | ``` 47 | 48 | Make sure scuttlebot is allowing private connections. Stop any running sbot server, restart it with the `--allowPrivate` option and create a new modern invite: 49 | 50 | ```sh 51 | sbot server --allowPrivate 52 | sbot invite.create --modern 53 | ``` 54 | 55 | From inside the Patchlite repo folder, run: 56 | 57 | ```sh 58 | npm start 59 | ``` 60 | 61 | This will build an html file at `build/index.html` and start a static file server on local port `8000`. 62 | 63 | Browse to with your invite code appended on the end of the address. (`#ws://localhost:8989...`) 64 | 65 | You should see a page load and then automatically refresh, that is the invite being consumed. 66 | 67 | When you load the page again, you will be loading your feed from your local `sbot` over a WebSocket, it will take some time. 68 | 69 | If you want to change your key or remote configuration, enter `/config` in the "location bar" (top-right text field). 70 | 71 | ## license 72 | 73 | AGPL-3.0 74 | -------------------------------------------------------------------------------- /app/html/menuItem/config.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const h = require('mutant/h') 3 | 4 | exports.gives = nest('app.html.menuItem') 5 | exports.create = function (api) { 6 | return nest('app.html.menuItem', menuItem) 7 | 8 | function menuItem (handleClick) { 9 | return h('a', { 10 | events: { 11 | click: () => handleClick('/config') 12 | } 13 | }, '/config') 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/html/page/config.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const h = require('mutant/h') 3 | 4 | exports.gives = nest('app.html.page') 5 | exports.needs = nest('keys.sync.load', 'first') 6 | exports.create = function (api) { 7 | return nest('app.html.page', configPage) 8 | 9 | function configPage (path) { 10 | if (path !== '/config') return 11 | 12 | const importKey = h('textarea', { 13 | placeholder: 'import an existing public/private key', 14 | name: 'textarea' 15 | }) 16 | 17 | const importRemote = h('textarea', { 18 | placeholder: 'import an existing remote', 19 | name: 'textarea' 20 | }) 21 | 22 | return h('Config', [ 23 | h('section.secret', [ 24 | h('p', [ 25 | `Your secret key is:`, 26 | h('pre', h('code', localStorage['browser/.ssb/secret'])) 27 | ]), 28 | h('form', [ 29 | importKey, 30 | h('button', { 31 | events: { 32 | click: function (ev) { 33 | ev.preventDefault() 34 | 35 | localStorage['browser/.ssb/secret'] = importKey.value.replace(/\s+/g, ' ') 36 | alert('Your public/private key has been updated') 37 | } 38 | } 39 | }, 'Import') 40 | ]) 41 | ]), 42 | h('section.remote', [ 43 | h('p', [ 44 | `Your WebSocket remote is:`, 45 | h('pre', h('code', localStorage.remote)), 46 | h('form', [ 47 | importRemote, 48 | h('button', { 49 | events: { 50 | click: function (ev) { 51 | ev.preventDefault() 52 | 53 | localStorage.remote = importRemote.value 54 | alert('Your WebSocket remote has been updated') 55 | } 56 | } 57 | }, 'Import') 58 | ]) 59 | ]) 60 | ]) 61 | ]) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/html/page/invite.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const ref = require('ssb-ref') 3 | const h = require('mutant/h') 4 | 5 | exports.gives = nest('app.html.page') 6 | exports.needs = nest({ 7 | 'config.sync.load': 'first', 8 | 'invite.async.accept': 'first' 9 | }) 10 | exports.create = function (api) { 11 | const loadConfig = api.config.sync.load 12 | const acceptInvite = api.invite.async.accept 13 | 14 | return nest('app.html.page', invitePage) 15 | 16 | function invitePage (path) { 17 | if (window.location.hash === '' || window.location.hash === '#') return 18 | const invite = location.hash.substring(1) 19 | const inviteData = ref.parseInvite(invite) 20 | if (!inviteData) return 21 | 22 | const progress = acceptInvite(invite, err => { 23 | if (err) throw err 24 | console.log('invited', err) 25 | window.location.hash = '' 26 | window.location.reload() 27 | }) 28 | 29 | // change config so we can connect to sbot 30 | // TODO this should only happen after invite is successful 31 | var config = loadConfig() 32 | localStorage.remote = inviteData.remote 33 | config.remote = inviteData.remote 34 | 35 | return h('Invitee', [ 36 | h('h1', 'Secure Scuttlebutt'), 37 | progress 38 | ]) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /blob/sync/url.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const URL = require('url') 3 | 4 | exports.gives = nest('blob.sync.url') 5 | exports.needs = nest('config.sync.load', 'first') 6 | exports.create = (api) => { 7 | var baseUrl 8 | return nest('blob.sync.url', (id) => { 9 | if (!baseUrl) { 10 | const config = api.config.sync.load() 11 | baseUrl = remoteToUrlBase(config.remote) 12 | } 13 | return baseUrl + id 14 | }) 15 | } 16 | 17 | function remoteToUrlBase (remote) { 18 | var r = URL.parse(remote.split('~')[0]) 19 | // this will work for ws and wss. 20 | r.protocol = r.protocol.replace('ws', 'http') 21 | r.pathname = '/blobs/get/' 22 | return URL.format(r) 23 | } 24 | -------------------------------------------------------------------------------- /config/sync/load.js: -------------------------------------------------------------------------------- 1 | const Config = require('ssb-config/inject') 2 | const nest = require('depnest') 3 | 4 | exports.gives = nest('config.sync.load') 5 | exports.create = (api) => { 6 | var config 7 | return nest('config.sync.load', () => { 8 | if (!config) { 9 | config = Config(process.env.ssb_appname) 10 | config.manifest = require('../../manifest.json') 11 | config.remote = localStorage.remote 12 | } 13 | return config 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const bulk = require('bulk-require') 2 | 3 | module.exports = { 4 | patchlite: bulk(__dirname, [ 5 | './!(node_modules)/**/*.js' 6 | ]) 7 | } 8 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const combine = require('depject') 2 | const entry = require('depject/entry') 3 | const nest = require('depnest') 4 | 5 | const patchlite = require('./') 6 | const patchbay = require('patchbay') 7 | const patchcore = require('patchcore') 8 | 9 | // polyfills 10 | require('setimmediate') 11 | require('array-includes').shim() 12 | 13 | // from more specialized to more general 14 | const sockets = combine( 15 | patchlite, 16 | patchbay, 17 | patchcore 18 | ) 19 | 20 | const api = entry(sockets, nest('app.html.app', 'first')) 21 | 22 | const app = api.app.html.app() 23 | document.body.appendChild(app) 24 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": "async", 3 | "address": "sync", 4 | "manifest": "sync", 5 | "get": "async", 6 | "createFeedStream": "source", 7 | "createLogStream": "source", 8 | "messagesByType": "source", 9 | "createHistoryStream": "source", 10 | "createUserStream": "source", 11 | "links": "source", 12 | "relatedMessages": "async", 13 | "add": "async", 14 | "publish": "async", 15 | "getAddress": "sync", 16 | "getLatest": "async", 17 | "latest": "source", 18 | "latestSequence": "async", 19 | "whoami": "sync", 20 | "usage": "sync", 21 | "plugins": { 22 | "install": "source", 23 | "uninstall": "source", 24 | "enable": "async", 25 | "disable": "async" 26 | }, 27 | "gossip": { 28 | "peers": "sync", 29 | "add": "sync", 30 | "remove": "sync", 31 | "ping": "duplex", 32 | "connect": "async", 33 | "changes": "source", 34 | "reconnect": "sync" 35 | }, 36 | "friends": { 37 | "all": "async", 38 | "hops": "async", 39 | "createFriendStream": "source", 40 | "get": "sync" 41 | }, 42 | "replicate": { 43 | "changes": "source", 44 | "upto": "source" 45 | }, 46 | "blobs": { 47 | "get": "source", 48 | "add": "sink", 49 | "ls": "source", 50 | "has": "async", 51 | "size": "async", 52 | "meta": "async", 53 | "want": "async", 54 | "push": "async", 55 | "changes": "source", 56 | "createWants": "source" 57 | }, 58 | "invite": { 59 | "create": "async", 60 | "accept": "async", 61 | "use": "async" 62 | }, 63 | "block": { 64 | "isBlocked": "sync" 65 | }, 66 | "private": { 67 | "publish": "async", 68 | "unbox": "sync" 69 | }, 70 | "links2": { 71 | "read": "source", 72 | "dump": "source" 73 | }, 74 | "query": { 75 | "read": "source", 76 | "dump": "source" 77 | }, 78 | "ws": { 79 | "getAddress": "sync" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patchlite", 3 | "version": "0.0.0", 4 | "description": "A browser client for the Scuttlebutt network", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "mkdir -p build && browserify main.js | indexhtmlify --title patchlite > build/index.html", 8 | "server": "ecstatic build", 9 | "start": "npm-run-all -s build server", 10 | "test:deps": "dependency-check . && dependency-check . --extra --no-dev -i es2040", 11 | "test:lint": "standard", 12 | "test:node": "NODE_ENV=test run-default tape test/*.js --", 13 | "test:coverage": "NODE_ENV=test nyc npm run test:node", 14 | "test:coverage:report": "nyc report --reporter=lcov npm run test:node", 15 | "test": "npm-run-all -s test:node test:lint test:deps" 16 | }, 17 | "browserify": { 18 | "transform": [ 19 | "bulkify", 20 | "es2040" 21 | ] 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/ahdinosaur/patchlite.git" 26 | }, 27 | "keywords": [], 28 | "author": "Mikey (http://dinosaur.is)", 29 | "license": "AGPL-3.0", 30 | "bugs": { 31 | "url": "https://github.com/ahdinosaur/patchlite/issues" 32 | }, 33 | "homepage": "https://github.com/ahdinosaur/patchlite#readme", 34 | "devDependencies": { 35 | "browserify": "^14.3.0", 36 | "dependency-check": "^2.7.0", 37 | "indexhtmlify": "^1.3.1", 38 | "node-dev": "^3.1.3", 39 | "npm-run-all": "^4.0.1", 40 | "nyc": "^10.1.2", 41 | "run-default": "^1.0.0", 42 | "standard": "^8.6.0", 43 | "tape": "^4.6.3" 44 | }, 45 | "dependencies": { 46 | "array-includes": "^3.0.2", 47 | "bulk-require": "^1.0.0", 48 | "bulkify": "^1.4.2", 49 | "depject": "^4.1.0", 50 | "depnest": "^1.3.0", 51 | "ecstatic": "^2.1.0", 52 | "es2040": "^1.2.3", 53 | "mutant": "github:ahdinosaur/mutant#request-idle-callback", 54 | "patchbay": "7.3.1", 55 | "patchcore": "^0.5.1", 56 | "setimmediate": "^1.0.5", 57 | "ssb-ref": "^2.6.2" 58 | } 59 | } 60 | --------------------------------------------------------------------------------