├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── PLUGINS.md ├── README.md ├── lib ├── api.js ├── bare.js ├── core.js ├── index.js ├── plugins │ ├── net.js │ └── shs.js ├── types.js └── util.js ├── package.json ├── test ├── api.js ├── app-key.js ├── auth.js ├── auth2.js ├── close.js ├── flood.js ├── local.js ├── merge.js ├── seeds.js ├── server.js └── timeout.js └── tsconfig.json /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | strategy: 15 | matrix: 16 | node-version: [16.x, 18.x, 20.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 8.1.0 2 | 3 | - **Feature:** _Asserting dependencies between plugins_. Plugin objects can now have an optional `plugin.needs` field, which is an array of strings. Those strings are names of other plugins that the current plugin depends on. Secret-stack will throw an error if one of the specified dependencies in `plugin.needs` is missing. 4 | 5 | # 8.0.0 6 | 7 | - **Breaking change**: the config object now has the `config.global` namespace, and `config[pluginName]` namespace. A plugin can only access its own config, plus the global config. This is to prevent plugins from accessing each other's config, for preventive security and better code organization. 8 | 9 | # 7.0.0 10 | 11 | - **Breaking change**: Node.js >=16.0.0 is now required, due to the use of new JavaScript syntax 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dominic Tarr 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and 7 | associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, 10 | merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom 12 | the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 22 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /PLUGINS.md: -------------------------------------------------------------------------------- 1 | ## Secret-Stack Plugins 2 | 3 | Secret-Stack provides a minimal core for creating peer-to-peer networks 4 | like Secure-Scuttlebutt. It is highly extensible via plugins. 5 | 6 | ## Example Usage 7 | 8 | Plugins are simply NodeJS modules that export an `object` of form `{ name, version, manifest, init }`. 9 | 10 | ```js 11 | // bluetooth-plugin.js 12 | 13 | module.exports = { 14 | name: 'bluetooth', 15 | needs: ['conn'], 16 | version: '5.0.1', 17 | manifest: { 18 | localPeers: 'async', 19 | updates: 'source' 20 | }, 21 | init: (api, opts) => { 22 | // .. do things 23 | 24 | // In opts, only opts.bluetooth and opts.global are available 25 | 26 | // return things promised by the manifest: 27 | return { 28 | localPeers, // an async function (takes a callback) 29 | updates // a function which returns a pull-stream source 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | Plugins are then added to a `Secret-Stack` instance using the `.use` 36 | method. 37 | 38 | ```js 39 | // index.js 40 | 41 | var SecretStack = require('secret-stack') 42 | 43 | var App = SecretStack({ global: { appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' } }) 44 | .use(require('./bluetooth-plugin')) 45 | 46 | var app = App() 47 | ``` 48 | 49 | The plugin has now been mounted on the `secret-stack` instance and 50 | methods exposed by the plugin can be accessed at `app.pluginName.methodName` 51 | (e.g. `app.bluetooth.updates`) 52 | 53 | --- 54 | 55 | Plugins can be used to for a number of different use cases, like adding 56 | a persistent underlying database ([ssb-db](https://github.com/ssbc/ssb-db')) 57 | or layering indexes on top of the underlying store ([ssb-links](https://github.com/ssbc/ssb-links)). 58 | 59 | It becomes very easy to lump a bunch of plugins together and create a 60 | more sophisticated application. 61 | 62 | ```js 63 | var SecretStack = require('secret-stack') 64 | var config = require('./some-config-file') 65 | 66 | var Server = SecretStack({ global: { appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' } }) 67 | .use(require('ssb-db')) // added persistent log storage 68 | .use(require('ssb-gossip')) // added peer gossip capabilities 69 | .use(require('ssb-replicate')) // can now replicate other logs with peers 70 | .use(require('ssb-friends')) // which peer's logs should be replicated 71 | 72 | var server = Server(config) // start application 73 | ``` 74 | 75 | ## Plugin Format 76 | 77 | A valid plugin is an `Object` of form `{ name, version, manifest, init }` 78 | 79 | ### `plugin.name` (String) 80 | 81 | A string that will also be used as the mount point for a plugin. a 82 | plugin's methods with `plugin.name = 'foo'` will be available at `node.foo` on a 83 | `secret-stack` instance. 84 | 85 | Names will also be automatically camelCased so `plugin.name = "foo bar"` 86 | will be available at `node.fooBar`. 87 | 88 | A `plugin.name` can also be an `'object`. This object will be merged 89 | directly with the 90 | 91 | ### `plugin.needs` (Array) _optional_ 92 | 93 | An array of strings which are the names of other plugins that this plugin 94 | depends on. If those plugins are not present, then secret-stack will throw 95 | an error indicating that the dependency is missing. 96 | 97 | Use this field to declare dependencies on other plugins, and this should 98 | facilitate the correct usage of your plugin. 99 | 100 | ### `plugin.version` (String) _optional_ 101 | 102 | NOTE - not currently used anywhere functionally 103 | 104 | A plugin's current version number. These generally follow `semver` 105 | guidelines. 106 | 107 | ### `plugin.init(api, opts, permissions, manifest)` (Function) 108 | 109 | When the secret-stack app is instantiated/ created, all init functions 110 | of plugins will be called in the order they were registered with `use`. 111 | 112 | The `init` function of a plugin will be passed: 113 | - `api` - _Object_ the secret-stack app so far 114 | - `opts` - configurations available to this plugin are `opts.global` and `opts[plugin.name]` 115 | - `permissions` - _Object_ the permissions so far 116 | - `manifest` - _Object_ the manifest so far 117 | 118 | If `plugin.name` is a string, then the return value of init is mounted like `api[plugin.name] = plugin.init(api, opts)` 119 | 120 | (If there's no `plugin.name` then the results of `init` are merged directly with the `api` object!) 121 | 122 | Note, each method on the api gets wrapped with [hoox](https://github.com/dominictarr/hoox) 123 | so that plugins may intercept that function. 124 | 125 | ### `plugin.manifest` (Object) 126 | 127 | An object containing the mapping of a plugin's exported methods and the 128 | `muxrpc` method type. See the 129 | [muxrpc#manifest](https://github.com/ssbc/muxrpc#manifest) documentation 130 | for more details. 131 | 132 | 133 | ### `plugin.permissions` (Object) _optional_ 134 | 135 | Any permissions provided will be merged into the main permissions, 136 | prefixed with the plugin name. 137 | 138 | e.g. In this case we're giving anyone access to `api.bluetooth.localPeers`, 139 | and the permission would be listed `'bluetooth.localPeers'` 140 | 141 | ```js 142 | module.exports = { 143 | name: 'bluetooth', 144 | version: '5.0.1', 145 | manifest: { 146 | localPeers: 'async', 147 | updates: 'source' 148 | }, 149 | permissions: { 150 | anonymous: [ 'localPeers' ] 151 | }, 152 | init: (api, opts) => { 153 | // .. do things 154 | 155 | // return things promised by the manifest: 156 | return { 157 | localPeers, // an async function (takes a callback) 158 | updates // a function which returns a pull-stream source 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | 165 | ## Deprecated Plugin Format 166 | 167 | A plugin can also be a function which returns an object. 168 | This is not currently recommended, as it's less clear to readers what the outcome is. 169 | 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secret-stack 2 | 3 | create secure peer to peer networks using secret-handshakes. 4 | 5 | This provides a framework to make building secure, decentralized systems easier. 6 | (such as [ssb-server](https://github.com/ssbc/ssb-server) which this was refactored out of ;) 7 | 8 | This module: 9 | 10 | * uses [secret-handshake](https://github.com/auditdrivencrypto/secret-handshake) to set up a shared key for use between two peers with public / private keys (and verify the other peer) 11 | * uses [multiserver](https://github.com/ssb-js/multiserver) to handle different ways of connecting to other peers over different protocols (who you then handshake with) 12 | * uses [muxrpc](https://github.com/ssb-js/muxrpc) to provide a remote process call (rpc) interface to peers who have authenticated and connected allowing them to call methods on each other (like "please give me all mix's messages since yesterday"). This is all encrypted with the shared key set up by secret handshake. 13 | * provides a plugin stack which allows you to 14 | - add new protocols to multiserver 15 | - add muxrpc methods 16 | - add plugins which persist state (like ssb-db, which when you add it essentially turns this into secure-scuttlebutt) 17 | - add plugins which let you listen and automate some things (eg replicate my friends when I connect to nicoth) 18 | 19 | ## Example 20 | 21 | ``` js 22 | var SecretStack = require('secret-stack') 23 | var databasePlugin = require('./some-database') 24 | var bluetoothPlugin = require('./bluetooth') 25 | var config = require('./some-config') 26 | 27 | var App = SecretStack({ global: { appKey: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=' } }) 28 | .use(databasePlugin) 29 | .use(bluetoothPlugin) 30 | 31 | var app = App(config) 32 | ``` 33 | 34 | For documentation on plugins, see [PLUGINS.md](./PLUGINS.md). 35 | 36 | 37 | ## API 38 | 39 | ### `SecretStack(opts) => App` 40 | 41 | Initialize a new app factory. 42 | 43 | `opts` is an Object with properties: 44 | - `appKey` - _String, 32 bytes_ a high entropy (i.e. random) key which is fixed for your app. Actors who do not know this value will not be able to connect to instances of your app. 45 | - `permissions` - _Object_ (optional), you can set default permissions which will be the foundation for all permissions subsequently added. See [muxrpc permissions](https://github.com/ssb-js/muxrpc#permissions) 46 | 47 | NOTE - you can also add other properties to opts. These will be merged with `config` later to form the final config passed to each plugin. (i.e. `merge(opts, config)`) 48 | 49 | 50 | ### `App.use(plugin) => App` 51 | 52 | Add a plugin to the factory. See [PLUGINS.md](PLUGINS.md) for more details. 53 | 54 | Returns the App (with plugin now installed) 55 | 56 | ### `App(config) => app` 57 | 58 | Start the app and returns an EventEmitter with methods (core and plugin) attached. 59 | 60 | `config` is an (optional) Object with: 61 | - `config.global` - an object containing data available for all plugins 62 | - `config.global.keys` - _String_ a sodium ed25519 key pair 63 | - `config[pluginName]` - an object containing data only available to the plugin with name `pluginName`. Note that `pluginName` is the camelCase of `plugin.name`. 64 | 65 | `config` will be passed to each plugin as they're initialised (as `merge(opts, config)` which opts were those options `SecretStack` factory was initialised with), with only `config.global` and `config[pluginName]` available to each plugin. 66 | 67 | This `app` as an EventEmitter emits the following events: 68 | 69 | - `'multiserver:listening'`: emitted once the app's multiserver server is set up successfully, with no arguments 70 | - `'rpc:connect'`: emitted every time a peer has been successfully connected with us, with the arguments: 71 | - `rpc`: the muxrpc object to call the peer's remote functions, includes `rpc.stream` and `rpc.stream.address` (the multiserver address for this remote peer) 72 | - `isClient`: a boolean indicating whether we are the client (true) or the server (false) 73 | - `'close'`: emitted once `app.close()` has finished teardown logic, with the arguments: 74 | - `err`: if there was any error during closing 75 | 76 | ### app.getAddress() 77 | 78 | get a string representing the address of this node. 79 | it will be `ip:port:`. 80 | 81 | ### app.connect(address, cb) 82 | 83 | create a rpc connection to another instance. 84 | Address should be the form returned by `getAddress` 85 | 86 | ### app.auth(publicKey, cb) 87 | 88 | Query what permissions a given public key is assigned. 89 | it's not intended for this to be exposed over the network, 90 | but rather to extend this method to create plugable permissions systems. 91 | 92 | ``` js 93 | app.auth.hook(function (auth, args) { 94 | var pub = args[0] 95 | var cb = args[1] 96 | //call the first auth fn, and then hook the callback. 97 | auth(pub, function (err, perms) { 98 | if(err) cb(err) 99 | //optionally set your own perms for this pubkey. 100 | else if(accepted) 101 | cb(null, permissions) 102 | 103 | //or if you wish to reject them 104 | else if(rejected) 105 | cb(new Error('reject')) 106 | 107 | //fallback to default (the next hook, or the anonymous config, if defined) 108 | else 109 | cb() 110 | }) 111 | }) 112 | ``` 113 | 114 | ### `app.close()` 115 | 116 | close the app! 117 | 118 | Optionally takes `(err, callback)` 119 | 120 | ---- 121 | 122 | ## TODO document 123 | 124 | > mix: I think some of these are exposed over muxrpc (as they're in the manifest) 125 | and some can only be run locally if you have access to the instance of `app` you 126 | got returned after initialising it. 127 | 128 | ### `app.id => String` (alias `publicKey`) 129 | 130 | ### `app.getManifest() => Object` (alias: `manifest`) 131 | 132 | 133 | ### `auth: 'async'` 134 | ### `address: 'sync'` 135 | ### `config => Object'` 136 | ### `multiserver.parse: 'sync',` 137 | ### `multiserver.address: 'sync'` 138 | ### `multiserver.transport: 'sync'` 139 | ### `multiserver.transform: 'sync'` 140 | 141 | 142 | ## License 143 | 144 | MIT 145 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | const u = require('./util') 2 | const EventEmitter = require('events') 3 | // @ts-ignore 4 | const Hookable = require('hoox') 5 | 6 | /** 7 | * @template T 8 | * @param {T} x 9 | * @returns {T} 10 | */ 11 | function identity (x) { 12 | return x 13 | } 14 | 15 | /** 16 | * @param {any} a 17 | * @param {any} b 18 | * @param {any=} mapper 19 | */ 20 | function merge (a, b, mapper) { 21 | mapper = mapper ?? identity 22 | for (const k in b) { 23 | if ( 24 | b[k] && 25 | typeof b[k] === 'object' && 26 | !Buffer.isBuffer(b[k]) && 27 | !(b[k] instanceof Uint8Array) && 28 | !Array.isArray(b[k]) 29 | ) { 30 | a[k] ??= {} 31 | merge(a[k], b[k], mapper) 32 | } else { 33 | a[k] = mapper(b[k], k) 34 | } 35 | } 36 | return a 37 | } 38 | 39 | /** 40 | * @param {Record} fullConfig 41 | * @param {{name?: string}} plugin 42 | */ 43 | function buildPluginConfig (fullConfig, plugin) { 44 | if (plugin.name) { 45 | const camelCaseName = /** @type {string} */ (u.toCamelCase(plugin.name)) 46 | return { 47 | [camelCaseName]: fullConfig[camelCaseName], 48 | global: fullConfig.global ?? {} 49 | } 50 | } else { 51 | return { 52 | global: fullConfig.global ?? {} 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * @param {Array} plugins 59 | * @param {any} defaultConfig 60 | */ 61 | function Api (plugins, defaultConfig) { 62 | /** 63 | * @param {any} inputConfig 64 | */ 65 | function create (inputConfig) { 66 | const config = merge(merge({}, defaultConfig), inputConfig) 67 | // change event emitter to something with more rigorous security? 68 | let api = new EventEmitter() 69 | for (const plugin of create.plugins) { 70 | const pluginConfig = buildPluginConfig(config, plugin) 71 | let _api = plugin.init.call( 72 | {}, 73 | api, 74 | pluginConfig, 75 | create.permissions, 76 | create.manifest 77 | ) 78 | if (plugin.name) { 79 | const camelCaseName = u.toCamelCase(plugin.name) 80 | if (camelCaseName) { 81 | /** @type {Record} */ 82 | const o = {} 83 | o[camelCaseName] = _api 84 | _api = o 85 | } 86 | } 87 | api = merge( 88 | api, 89 | _api, 90 | /** 91 | * @param {any} val 92 | * @param {number | string} key 93 | */ 94 | (val, key) => { 95 | if (typeof val === 'function') { 96 | val = Hookable(val) 97 | if (plugin.manifest && plugin.manifest[key] === 'sync') { 98 | u.hookOptionalCB(val) 99 | } 100 | } 101 | return val 102 | } 103 | ) 104 | } 105 | return api 106 | } 107 | 108 | create.plugins = /** @type {Array} */ ([]) 109 | create.manifest = {} 110 | create.permissions = {} 111 | 112 | create.use = 113 | /** 114 | * @param {any} plugin 115 | */ 116 | function use (plugin) { 117 | if (Array.isArray(plugin)) { 118 | plugin.forEach(create.use) 119 | return create 120 | } 121 | 122 | if (!plugin.init) { 123 | if (typeof plugin === 'function') { 124 | create.plugins.push({ init: plugin }) 125 | return create 126 | } else { 127 | throw new Error('plugins *must* have "init" method') 128 | } 129 | } 130 | 131 | if (plugin.name && typeof plugin.name === 'string') { 132 | if (plugin.name === 'global') { 133 | throw new Error('plugin named "global" is reserved') 134 | } 135 | const found = create.plugins.some((p) => p.name === plugin.name) 136 | if (found) { 137 | // prettier-ignore 138 | console.error('plugin named:' + plugin.name + ' is already loaded, skipping') 139 | return create 140 | } 141 | } 142 | 143 | const name = plugin.name 144 | 145 | if (plugin.needs) { 146 | queueMicrotask(() => { 147 | for (const needed of plugin.needs) { 148 | const found = create.plugins.some((p) => p.name === needed) 149 | if (!found) { 150 | throw new Error(`secret-stack plugin "${name ?? '?'}" needs plugin "${needed}" but not found`) 151 | } 152 | } 153 | }) 154 | } 155 | 156 | if (plugin.manifest) { 157 | create.manifest = u.merge.manifest( 158 | create.manifest, 159 | plugin.manifest, 160 | u.toCamelCase(name) 161 | ) 162 | } 163 | if (plugin.permissions) { 164 | create.permissions = u.merge.permissions( 165 | create.permissions, 166 | plugin.permissions, 167 | u.toCamelCase(name) 168 | ) 169 | } 170 | create.plugins.push(plugin) 171 | 172 | return create 173 | } 174 | 175 | for (const plugin of (plugins ?? [])) { 176 | if (plugin) { 177 | create.use(plugin) 178 | } 179 | } 180 | 181 | return create 182 | } 183 | 184 | module.exports = Api 185 | -------------------------------------------------------------------------------- /lib/bare.js: -------------------------------------------------------------------------------- 1 | const Api = require('./api') 2 | 3 | /** 4 | * @param {unknown} config 5 | */ 6 | module.exports = function SecretStack (config) { 7 | const create = Api([], config ?? {}) 8 | 9 | return create.use(require('./core')) 10 | } 11 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | const u = require('./util') 2 | // @ts-ignore 3 | const Muxrpc = require('muxrpc') 4 | // @ts-ignore 5 | const pull = require('pull-stream') 6 | // const Rate = require('pull-rate') 7 | // @ts-ignore 8 | const MultiServer = require('multiserver') 9 | // @ts-ignore 10 | const Inactive = require('pull-inactivity') 11 | const debug = require('debug')('secret-stack') 12 | 13 | /** 14 | * @typedef {import('./types').Config} Config 15 | * @typedef {import('./types').Outgoing} Outgoing 16 | * @typedef {import('./types').Incoming} Incoming 17 | * @typedef {import('./types').Transport} Transport 18 | * @typedef {import('./types').Transform} Transform 19 | * @typedef {import('./types').ScopeStr} ScopeStr 20 | */ 21 | 22 | /** 23 | * @param {unknown} o 24 | * @returns {o is Record} 25 | */ 26 | function isPlainObject (o) { 27 | return !!o && typeof o === 'object' && !Array.isArray(o) 28 | } 29 | 30 | /** 31 | * @param {Buffer | string} s 32 | * @returns {string} 33 | */ 34 | function toBase64 (s) { 35 | if (typeof s === 'string') return s 36 | else return s.toString('base64') // assume a buffer 37 | } 38 | 39 | /** 40 | * @template T 41 | * @param {Record | Array} objOrArr 42 | * @param {(t: T, k: string | number, o: Record | Array) => void} iter 43 | */ 44 | function each (objOrArr, iter) { 45 | if (Array.isArray(objOrArr)) { 46 | objOrArr.forEach(iter) 47 | } else { 48 | for (const key in objOrArr) iter(objOrArr[key], key, objOrArr) 49 | } 50 | } 51 | 52 | /** 53 | * 54 | * @param {Transform | Transport} obj 55 | * @param {'transform' | 'transport'} type 56 | */ 57 | function assertHasNameAndCreate (obj, type) { 58 | if ( 59 | !isPlainObject(obj) || 60 | typeof obj.name !== 'string' || 61 | typeof obj.create !== 'function' 62 | ) { 63 | throw new Error(type + ' must be {name: string, create: function}') 64 | } 65 | } 66 | 67 | /** 68 | * TODO: should probably replace this with ssb-ref#toMultiServerAddress or 69 | * just delete this and let multiserver handle invalid addresses. The 2nd option 70 | * sounds better, because we might already have address validation in ssb-conn 71 | * and so we don't need that kind of logic in secret-stack anymore. 72 | * 73 | * @param {string | Record} address 74 | * @returns {string} 75 | */ 76 | function coearseAddress (address) { 77 | if (isPlainObject(address)) { 78 | let protocol = 'net' 79 | if (typeof address.host === 'string' && address.host.endsWith('.onion')) { 80 | protocol = 'onion' 81 | } 82 | return ( 83 | [protocol, address.host, address.port].join(':') + 84 | '~' + 85 | ['shs', toBase64(/** @type {string} */ (address.key))].join(':') 86 | ) 87 | } 88 | return address 89 | } 90 | 91 | /* 92 | // Could be useful 93 | function msLogger (stream) { 94 | const meta = { tx: 0, rx: 0, pk: 0 } 95 | stream = Rate(stream, function (len, up) { 96 | meta.pk++ 97 | if (up) meta.tx += len 98 | else meta.rx += len 99 | }) 100 | stream.meta = meta 101 | return stream 102 | } 103 | */ 104 | 105 | /** 106 | * @param {unknown} list 107 | */ 108 | function isPermsList (list) { 109 | if (list === null) return true 110 | if (typeof list === 'undefined') return true 111 | return Array.isArray(list) && list.every((x) => typeof x === 'string') 112 | } 113 | 114 | /** 115 | * @param {unknown} perms 116 | */ 117 | function isPermissions (perms) { 118 | // allow: null means enable everything. 119 | return ( 120 | perms && 121 | isPlainObject(perms) && 122 | isPermsList(perms.allow) && 123 | isPermsList(perms.deny) 124 | ) 125 | } 126 | 127 | module.exports = { 128 | manifest: { 129 | auth: 'async', 130 | address: 'sync', 131 | manifest: 'sync', 132 | multiserver: { 133 | parse: 'sync', 134 | address: 'sync' 135 | } 136 | }, 137 | permissions: { 138 | anonymous: { 139 | allow: ['manifest'] 140 | } 141 | }, 142 | 143 | /** 144 | * 145 | * @param {any} api 146 | * @param {Config} opts 147 | * @param {any} permissions 148 | * @param {any} manifest 149 | * @returns 150 | */ 151 | init (api, opts, permissions, manifest) { 152 | /** @type {number} */ 153 | let timeoutInactivity 154 | if (u.isNumber(opts.global.timers?.inactivity)) { 155 | timeoutInactivity = /** @type {number} */ (opts.global.timers?.inactivity) 156 | } 157 | // if opts.timers are set, pick a longer default 158 | // but if not, set a short default (as needed in the tests) 159 | timeoutInactivity ??= opts.global.timers ? 600e3 : 5e3 160 | 161 | if (!opts.global.connections) { 162 | /** @type {Incoming} */ 163 | const netIn = { 164 | scope: ['device', 'local', 'public'], 165 | transform: 'shs', 166 | ...(opts.global.host ? { host: opts.global.host } : null), 167 | ...(opts.global.port ? { port: opts.global.port } : null) 168 | } 169 | /** @type {Outgoing} */ 170 | const netOut = { 171 | transform: 'shs' 172 | } 173 | opts.global.connections = { 174 | incoming: { 175 | net: [netIn] 176 | }, 177 | outgoing: { 178 | net: [netOut] 179 | } 180 | } 181 | } 182 | 183 | /** @type {Record>} */ 184 | const peers = (api.peers = {}) 185 | 186 | /** @type {Array} */ 187 | const transports = [] 188 | 189 | /** @type {Array} */ 190 | const transforms = [] 191 | 192 | /** @type {any} */ 193 | let server 194 | /** @type {any} */ 195 | let ms 196 | /** @type {any} */ 197 | let msClient 198 | 199 | function setupMultiserver () { 200 | if (api.closed) return 201 | if (server) return server 202 | if (transforms.length < 1) { 203 | throw new Error('secret-stack needs at least 1 transform protocol') 204 | } 205 | 206 | /** @type {Array<[unknown, unknown]>} */ 207 | const serverSuites = [] 208 | /** @type {Array<[unknown, unknown]>} */ 209 | const clientSuites = [] 210 | 211 | for (const incTransport in opts.global.connections?.incoming) { 212 | for (const inc of opts.global.connections.incoming[incTransport]) { 213 | for (const transform of transforms) { 214 | for (const transport of transports) { 215 | if ( 216 | transport.name === incTransport && 217 | transform.name === inc.transform 218 | ) { 219 | const msPlugin = transport.create(inc) 220 | const msTransformPlugin = transform.create() 221 | if (msPlugin.scope() !== inc.scope) { 222 | // prettier-ignore 223 | throw new Error('transport:' + transport.name + ' did not remember scope, expected:' + inc.scope + ' got:' + msPlugin.scope()) 224 | } 225 | // prettier-ignore 226 | debug('creating server %s %s host=%s port=%d scope=%s', incTransport, transform.name, inc.host, inc.port, inc.scope ?? 'undefined') 227 | serverSuites.push([msPlugin, msTransformPlugin]) 228 | } 229 | } 230 | } 231 | } 232 | } 233 | 234 | for (const outTransport in opts.global.connections?.outgoing) { 235 | for (const out of opts.global.connections.outgoing[outTransport]) { 236 | for (const transform of transforms) { 237 | for (const transport of transports) { 238 | if ( 239 | transport.name === outTransport && 240 | transform.name === out.transform 241 | ) { 242 | const msPlugin = transport.create(out) 243 | const msTransformPlugin = transform.create() 244 | clientSuites.push([msPlugin, msTransformPlugin]) 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | msClient = MultiServer(clientSuites) 252 | 253 | ms = MultiServer(serverSuites) 254 | server = ms.server(setupRPC, null, () => { 255 | api.emit('multiserver:listening') // XXX return all scopes listing on? 256 | }) 257 | if (!server) throw new Error('expected server') 258 | return server 259 | } 260 | 261 | setImmediate(setupMultiserver) 262 | 263 | /** 264 | * @param {any} stream 265 | * @param {unknown} manf 266 | * @param {boolean=} isClient 267 | */ 268 | function setupRPC (stream, manf, isClient) { 269 | // idea: make muxrpc part of the multiserver stream so that we can upgrade it. 270 | // we'd need to fallback to using default muxrpc on ordinary connections. 271 | // but maybe the best way to represent that would be to coearse addresses to 272 | // include ~mux1 at the end if they didn't specify a muxrpc version. 273 | 274 | const perms = 275 | isClient 276 | ? permissions.anonymous 277 | : isPermissions(stream.auth) 278 | ? stream.auth 279 | : permissions.anonymous 280 | const rpc = Muxrpc(manifest, manf ?? manifest, api, perms) 281 | // Legacy ID: 282 | rpc.id = '@' + u.toId(stream.remote) 283 | // Modern IDs: 284 | for (const transform of transforms) { 285 | if (transform.identify) { 286 | const identified = transform.identify(stream.remote) 287 | Object.defineProperty(rpc, transform.name, { 288 | get () { 289 | return identified 290 | } 291 | }) 292 | } 293 | } 294 | let rpcStream = rpc.stream 295 | if (timeoutInactivity > 0 && api.id !== rpc.id) { 296 | rpcStream = Inactive(rpcStream, timeoutInactivity) 297 | } 298 | rpc.meta = stream.meta 299 | rpc.stream.address = stream.address 300 | 301 | pull(stream, rpcStream, stream) 302 | 303 | // keep track of current connections. 304 | peers[rpc.id] ??= [] 305 | peers[rpc.id].push(rpc) 306 | rpc.once('closed', () => { 307 | peers[rpc.id].splice(peers[rpc.id].indexOf(rpc), 1) 308 | }) 309 | 310 | api.emit('rpc:connect', rpc, !!isClient) 311 | 312 | return rpc 313 | } 314 | 315 | return { 316 | config: opts, 317 | /** 318 | * `auth` can be called remotely 319 | * @param {unknown} _pub 320 | * @param {Function} cb 321 | */ 322 | auth (_pub, cb) { 323 | cb() 324 | }, 325 | 326 | /** 327 | * @param {ScopeStr=} scope 328 | */ 329 | address (scope) { 330 | return api.getAddress(scope) 331 | }, 332 | 333 | /** 334 | * @param {ScopeStr=} scope 335 | */ 336 | getAddress (scope) { 337 | setupMultiserver() 338 | return ms.stringify(scope) ?? null 339 | }, 340 | 341 | manifest () { 342 | return manifest 343 | }, 344 | 345 | getManifest () { 346 | return this.manifest() 347 | }, 348 | 349 | /** 350 | * `connect` cannot be called remotely 351 | * @param {string | Record} address 352 | * @param {Function} cb 353 | */ 354 | connect (address, cb) { 355 | setupMultiserver() 356 | msClient.client( 357 | coearseAddress(address), 358 | /** 359 | * @param {unknown} err 360 | * @param {unknown} stream 361 | */ 362 | (err, stream) => { 363 | if (err) cb(err) 364 | else cb(null, setupRPC(stream, null, true)) 365 | } 366 | ) 367 | }, 368 | 369 | multiserver: { 370 | /** 371 | * @param {Transport} transport 372 | */ 373 | transport (transport) { 374 | if (server) { 375 | throw new Error('cannot add protocol after server initialized') 376 | } 377 | assertHasNameAndCreate(transport, 'transport') 378 | debug('Adding transport %s', transport.name) 379 | transports.push(transport) 380 | return this 381 | }, 382 | 383 | /** 384 | * @param {Transform} transform 385 | */ 386 | transform (transform) { 387 | assertHasNameAndCreate(transform, 'transform') 388 | debug('Adding transform %s', transform.name) 389 | transforms.push(transform) 390 | return this 391 | }, 392 | 393 | /** 394 | * @param {string} str 395 | */ 396 | parse (str) { 397 | return ms.parse(str) 398 | }, 399 | 400 | /** 401 | * @param {ScopeStr=} scope 402 | */ 403 | address (scope) { 404 | setupMultiserver() 405 | return ms.stringify(scope) || null 406 | } 407 | }, 408 | 409 | /** 410 | * @param {unknown} err 411 | * @param {Function=} cb 412 | */ 413 | close (err, cb) { 414 | if (typeof err === 'function') { 415 | cb = err 416 | err = null 417 | } 418 | api.closed = true 419 | if (!server) cb?.() 420 | else { 421 | (server.close ?? server)((/** @type {any} */ err) => { 422 | api.emit('close', err) 423 | cb?.(err) 424 | }) 425 | } 426 | 427 | if (err) { 428 | each(peers, (rpcs) => { 429 | each(rpcs, (/** @type {any} */ rpc) => { 430 | rpc.close(err) 431 | }) 432 | }) 433 | } 434 | } 435 | } 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const Api = require('./api') 2 | 3 | /** 4 | * @param {unknown} config 5 | */ 6 | module.exports = function SecretStack (config) { 7 | const create = Api([], config ?? {}) 8 | 9 | return ( 10 | create 11 | .use(require('./core')) 12 | // default network plugins 13 | .use(require('./plugins/net')) 14 | .use(require('./plugins/shs')) 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /lib/plugins/net.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const Net = require('multiserver/plugins/net') 3 | const debug = require('debug')('secret-stack net plugin') 4 | 5 | /** 6 | * @typedef {import('../types').Incoming} Incoming 7 | * @typedef {import('../types').Outgoing} Outgoing 8 | * @typedef {Incoming | Outgoing} Opts 9 | */ 10 | 11 | module.exports = { 12 | name: 'multiserver-net', 13 | version: '1.0.0', 14 | init (/** @type {any} */ api) { 15 | api.multiserver.transport({ 16 | name: 'net', 17 | create: (/** @type {Opts}} */ opts) => { 18 | // prettier-ignore 19 | debug('creating transport host=%s port=%d scope=%s', opts.host, opts.port, opts.scope) 20 | return Net(opts) // let multiserver figure out the defaults 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/plugins/shs.js: -------------------------------------------------------------------------------- 1 | const u = require('../util') 2 | // @ts-ignore 3 | const Shs = require('multiserver/plugins/shs') 4 | 5 | /** 6 | * @typedef {import('../types').Config} Config 7 | */ 8 | 9 | /** 10 | * @param {string | Buffer} base64 11 | * @returns {Buffer} 12 | */ 13 | function toBuffer (base64) { 14 | if (Buffer.isBuffer(base64)) return base64 15 | const i = base64.indexOf('.') 16 | return Buffer.from(~i ? base64.substring(0, i) : base64, 'base64') 17 | } 18 | 19 | /** 20 | * 21 | * @param {NonNullable} keys 22 | * @returns 23 | */ 24 | function toSodiumKeys (keys) { 25 | if (typeof keys.public !== 'string' || typeof keys.private !== 'string') { 26 | return keys 27 | } 28 | return { 29 | publicKey: toBuffer(keys.public), 30 | secretKey: toBuffer(keys.private) 31 | } 32 | } 33 | 34 | module.exports = { 35 | name: 'multiserver-shs', 36 | version: '1.0.0', 37 | 38 | /** 39 | * @param {any} api 40 | * @param {Config & {multiserverShs?: {cap?: string; seed?: Buffer}}} config 41 | */ 42 | init (api, config) { 43 | /** @type {number | undefined} */ 44 | let timeoutHandshake 45 | if (u.isNumber(config.global.timers?.handshake)) { 46 | timeoutHandshake = config.global.timers?.handshake 47 | } 48 | if (!timeoutHandshake) { 49 | timeoutHandshake = config.global.timers ? 15e3 : 5e3 50 | } 51 | // set all timeouts to one setting, needed in the tests. 52 | if (config.global.timeout) { 53 | timeoutHandshake = config.global.timeout 54 | } 55 | 56 | const shsCap = config.multiserverShs?.cap ?? config.global.caps?.shs ?? config.global.appKey 57 | if (!shsCap) { 58 | throw new Error('secret-stack/plugins/shs must have caps.shs configured') 59 | } 60 | const seed = config.multiserverShs?.seed ?? config.global.seed 61 | 62 | const shs = Shs({ 63 | keys: config.global.keys && toSodiumKeys(config.global.keys), 64 | seed, 65 | appKey: toBuffer(shsCap), 66 | timeout: timeoutHandshake, 67 | 68 | /** 69 | * @param {string} pub 70 | * @param {Function} cb 71 | */ 72 | authenticate (pub, cb) { 73 | const id = '@' + u.toId(pub) 74 | api.auth(id, (/** @type {any} */ err, /** @type {any} */ auth) => { 75 | if (err) cb(err) 76 | else cb(null, auth ?? true) 77 | }) 78 | } 79 | }) 80 | 81 | /** 82 | * @param {Buffer} publicKey 83 | */ 84 | function identify (publicKey) { 85 | const pubkey = publicKey.toString('base64') 86 | return { 87 | get pubkey () { 88 | return pubkey 89 | } 90 | } 91 | } 92 | 93 | const id = '@' + u.toId(shs.publicKey) 94 | api.id = id // Legacy ID 95 | api.publicKey = id 96 | // Modern ID 97 | const identified = identify(shs.publicKey) 98 | Object.defineProperty(api, 'shs', { 99 | get () { 100 | return identified 101 | } 102 | }) 103 | 104 | api.multiserver.transform({ 105 | name: 'shs', 106 | create: () => shs, 107 | identify 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {'device' | 'local' | 'private' | 'public'} ScopeStr 3 | * 4 | * @typedef {{ 5 | * scope: ScopeStr | Array, 6 | * transform: 'shs' | 'noauth', 7 | * port?: number, 8 | * host?: string, 9 | * }} Incoming 10 | * 11 | * @typedef {{ 12 | * scope?: undefined, 13 | * transform: 'shs' | 'noauth', 14 | * port?: undefined, 15 | * host?: undefined, 16 | * }} Outgoing 17 | * 18 | * @typedef {{ 19 | * name: string, 20 | * create: (opts: Incoming | Outgoing) => any, 21 | * }} Transport 22 | * 23 | * @typedef {{ 24 | * name: string, 25 | * create: () => unknown, 26 | * identify?: (publicKey: Buffer) => any, 27 | * }} Transform 28 | * 29 | * @typedef {{ 30 | * global: { 31 | * caps?: { 32 | * shs?: Buffer | string; 33 | * }; 34 | * appKey?: Buffer | string; 35 | * keys?: { 36 | * public?: string; 37 | * private?: string; 38 | * id?: string; 39 | * }; 40 | * seed?: unknown; 41 | * host?: string; 42 | * port?: number; 43 | * connections?: { 44 | * incoming?: { 45 | * [name: string]: Array; 46 | * }; 47 | * outgoing?: { 48 | * [name: string]: Array; 49 | * }; 50 | * }; 51 | * timeout?: number; 52 | * timers?: { 53 | * handshake?: number; 54 | * inactivity?: number; 55 | * }; 56 | * } 57 | * }} Config 58 | */ 59 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const mapMerge = require('map-merge') 3 | const camelize = require('to-camel-case') 4 | 5 | /** 6 | * @param {any} x 7 | * @return {x is Record} 8 | */ 9 | function isObject (x) { 10 | return !!x && typeof x === 'object' 11 | } 12 | 13 | /** 14 | * @param {any} x 15 | * @return {x is number} 16 | */ 17 | function isNumber (x) { 18 | return typeof x === 'number' && !isNaN(x) 19 | } 20 | 21 | /** 22 | * @param {unknown} obj 23 | * @param {Function} mapper 24 | * @return {any} 25 | */ 26 | function clone (obj, mapper) { 27 | /** 28 | * @param {unknown} v 29 | * @param {string | number} [k] 30 | */ 31 | function map (v, k) { 32 | return isObject(v) ? clone(v, mapper) : mapper(v, k) 33 | } 34 | 35 | if (Array.isArray(obj)) { 36 | return obj.map(map) 37 | } else if (isObject(obj)) { 38 | /** @type {any} */ 39 | const o = {} 40 | for (const k in obj) { 41 | o[k] = map(obj[k], k) 42 | } 43 | return o 44 | } else { 45 | return map(obj) 46 | } 47 | } 48 | 49 | /** 50 | * @param {Buffer | string} pub 51 | * @return {string} 52 | */ 53 | function toId (pub) { 54 | return Buffer.isBuffer(pub) ? pub.toString('base64') + '.ed25519' : pub 55 | } 56 | 57 | const merge = { 58 | /** 59 | * @param {unknown} perms 60 | * @param {unknown} _perms 61 | * @param {string=} name 62 | */ 63 | permissions (perms, _perms, name) { 64 | return mapMerge( 65 | perms, 66 | clone(_perms, (/** @type {any} */ v) => (name ? name + '.' + v : v)) 67 | ) 68 | }, 69 | 70 | /** 71 | * @param {unknown} manf 72 | * @param {unknown} _manf 73 | * @param {string=} name 74 | */ 75 | manifest (manf, _manf, name) { 76 | if (name) { 77 | /** @type {any} */ 78 | const o = {} 79 | o[name] = _manf 80 | _manf = o 81 | } 82 | return mapMerge(manf, _manf) 83 | } 84 | } 85 | 86 | /** 87 | * @param {any} syncFn 88 | */ 89 | function hookOptionalCB (syncFn) { 90 | // syncFn is a function that's expected to return its result or throw an error 91 | // we're going to hook it so you can optionally pass a callback 92 | syncFn.hook( 93 | /** 94 | * @this {unknown} 95 | * @param {Function} fn 96 | * @param {Array} args 97 | */ 98 | function (fn, args) { 99 | // if a function is given as the last argument, treat it as a callback 100 | const cb = args[args.length - 1] 101 | if (typeof cb === 'function') { 102 | let res 103 | args.pop() // remove cb from the arguments 104 | try { 105 | res = fn.apply(this, args) 106 | } catch (e) { 107 | return cb(e) 108 | } 109 | cb(null, res) 110 | } else { 111 | // no cb provided, regular usage 112 | return fn.apply(this, args) 113 | } 114 | } 115 | ) 116 | } 117 | 118 | /** 119 | * @param {string | undefined} n 120 | * @return {string | undefined} 121 | */ 122 | function toCamelCase (n) { 123 | return n ? camelize(n) : n 124 | } 125 | 126 | module.exports = { 127 | isObject, 128 | isNumber, 129 | clone, 130 | toId, 131 | merge, 132 | hookOptionalCB, 133 | toCamelCase 134 | } 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secret-stack", 3 | "description": "create secure peer to peer networks using secret-handshakes", 4 | "version": "8.1.0", 5 | "homepage": "https://github.com/ssb-js/secret-stack", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/ssb-js/secret-stack.git" 9 | }, 10 | "type": "commonjs", 11 | "main": "lib/index.js", 12 | "types": "lib/index.d.ts", 13 | "files": [ 14 | "lib/**/*" 15 | ], 16 | "exports": { 17 | ".": { 18 | "require": "./lib/index.js" 19 | }, 20 | "./bare": { 21 | "require": "./lib/bare.js" 22 | }, 23 | "./plugins/net": { 24 | "require": "./lib/plugins/net.js" 25 | }, 26 | "./plugins/shs": { 27 | "require": "./lib/plugins/shs.js" 28 | } 29 | }, 30 | "engines": { 31 | "node": ">=16" 32 | }, 33 | "dependencies": { 34 | "debug": "^4.3.0", 35 | "hoox": "0.0.1", 36 | "map-merge": "^1.1.0", 37 | "multiserver": "^3.1.0", 38 | "muxrpc": "^8.0.0", 39 | "pull-inactivity": "~2.1.1", 40 | "pull-rate": "^1.0.2", 41 | "pull-stream": "^3.4.5", 42 | "to-camel-case": "^1.0.0" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^12.12.2", 46 | "@types/debug": "4.1.8", 47 | "@types/to-camel-case": "1.0.0", 48 | "@typescript-eslint/eslint-plugin": "^5.13.0", 49 | "@typescript-eslint/parser": "^5.13.0", 50 | "pull-pushable": "^2.0.1", 51 | "ssb-keys": "^8.2.0", 52 | "mkdirp": "~1.0.4", 53 | "standardx": "^7.0.0", 54 | "tape": "^5.5.2", 55 | "typescript": "~5.1.0" 56 | }, 57 | "scripts": { 58 | "clean-check": "tsc --build --clean && tsc; tsc --build --clean", 59 | "prepublishOnly": "npm run clean-check && tsc --build && npm test", 60 | "postpublish": "npm run clean-check", 61 | "test": "npm run clean-check && npm run lint && npm run tape", 62 | "lint-fix": "standardx --fix 'lib/**/*.js'", 63 | "lint": "standardx 'lib/**/*.js'", 64 | "tape": "tape test/*.js" 65 | }, 66 | "author": "Dominic Tarr (http://dominictarr.com)", 67 | "license": "MIT" 68 | } 69 | -------------------------------------------------------------------------------- /test/api.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var Api = require('../lib/api') 3 | 4 | tape('add a core api + a plugin', function (t) { 5 | var Create = Api([{ 6 | init: function (api, opts) { 7 | t.deepEqual(opts, { global: { okay: true } }) 8 | return { 9 | hello: function (name) { 10 | return 'Hello, ' + name + '.' 11 | } 12 | } 13 | } 14 | }]) 15 | 16 | var api = Create({ global: { okay: true } }) 17 | 18 | t.equal(api.hello('Foo'), 'Hello, Foo.') 19 | 20 | Create.use({ 21 | init: function (api, opts) { 22 | t.deepEqual(opts, { global: { okay: true } }) 23 | api.hello.hook(function (greet, args) { 24 | var value = greet(args[0]) 25 | return value.substring(0, value.length - 1) + '!!!' 26 | }) 27 | } 28 | }) 29 | 30 | var api2 = Create({ global: { okay: true } }) 31 | t.equal(api2.hello('Foo'), 'Hello, Foo!!!') 32 | t.end() 33 | }) 34 | 35 | tape('named plugin', function (t) { 36 | // core, not a plugin. 37 | var Create = Api([{ 38 | manifest: { 39 | hello: 'sync' 40 | }, 41 | init: function (api) { 42 | return { 43 | hello: function (name) { 44 | return 'Hello, ' + name + '.' 45 | } 46 | } 47 | } 48 | }]) 49 | 50 | // console.log(Create) 51 | 52 | Create.use({ 53 | name: 'foo', 54 | manifest: { 55 | goodbye: 'async' 56 | }, 57 | init: function () { 58 | return { goodbye: function (n, cb) { cb(null, n) } } 59 | } 60 | }) 61 | 62 | t.deepEqual(Create.manifest, { 63 | hello: 'sync', 64 | foo: { 65 | goodbye: 'async' 66 | } 67 | }) 68 | 69 | t.end() 70 | }) 71 | 72 | tape('camel-case plugin', function (t) { 73 | // core, not a plugin. 74 | var Create = Api([{ 75 | manifest: {}, 76 | init: function (api) { 77 | return {} 78 | } 79 | }]) 80 | 81 | // console.log(Create) 82 | 83 | Create.use({ 84 | name: 'foo-bar', 85 | manifest: { 86 | goodbye: 'async' 87 | }, 88 | init: function () { 89 | return { goodbye: function (n, cb) { cb(null, n) } } 90 | } 91 | }) 92 | 93 | t.deepEqual(Create.manifest, { 94 | fooBar: { 95 | goodbye: 'async' 96 | } 97 | }) 98 | 99 | t.end() 100 | }) 101 | 102 | tape('plugin cannot read other plugin config', function (t) { 103 | t.plan(2) 104 | // core, not a plugin. 105 | var Create = Api([{ 106 | init: () => {} 107 | }]) 108 | 109 | Create.use({ 110 | name: 'foo', 111 | init(api, config) { 112 | t.deepEqual(config.foo, { x: 10 }) 113 | t.notOk(config.bar) 114 | return { } 115 | } 116 | }) 117 | 118 | Create({ 119 | foo: { x: 10 }, 120 | bar: { y: 20 } 121 | }) 122 | }) 123 | 124 | tape('plugin cannot be named global', function (t) { 125 | // core, not a plugin. 126 | var Create = Api([{ 127 | manifest: {}, 128 | init: function (api) { 129 | return {} 130 | } 131 | }]) 132 | 133 | t.throws(() => { 134 | Create.use({ 135 | name: 'global', 136 | init: function () { } 137 | }) 138 | }, 'throws on global plugin') 139 | 140 | t.end() 141 | }) 142 | 143 | tape('plugin needs another plugin', function (t) { 144 | // core, not a plugin. 145 | var Create = Api([{ 146 | manifest: {}, 147 | init: function (api) { 148 | return {} 149 | } 150 | }]) 151 | 152 | function uncaughtExceptionListener(err) { 153 | t.equals(err.message, 'secret-stack plugin "x" needs plugin "y" but not found') 154 | 155 | // Wait for potentially other errors 156 | setTimeout(() => { 157 | process.off('uncaughtException', uncaughtExceptionListener) 158 | t.end() 159 | }, 100) 160 | } 161 | 162 | process.on('uncaughtException', uncaughtExceptionListener) 163 | 164 | // Should throw 165 | Create.use({ 166 | name: 'x', 167 | needs: ['y'], 168 | init: function () { } 169 | }) 170 | 171 | // Should NOT throw, even though 'foo' is loaded after 'bar' 172 | Create.use({ 173 | name: 'bar', 174 | needs: ['foo'], 175 | init: function () { } 176 | }) 177 | 178 | Create.use({ 179 | name: 'foo', 180 | init: function () { } 181 | }) 182 | }) 183 | 184 | tape('compound (array) plugins', function (t) { 185 | // core, not a plugin. 186 | var Create = Api([{ 187 | manifest: { 188 | hello: 'sync' 189 | }, 190 | init: function (api) { 191 | return { 192 | hello: function (name) { 193 | return 'Hello, ' + name + '.' 194 | } 195 | } 196 | } 197 | }]) 198 | 199 | // console.log(Create) 200 | 201 | Create.use([ 202 | { 203 | name: 'foo', 204 | manifest: { 205 | goodbye: 'async' 206 | }, 207 | init: function () { 208 | return { goodbye: function (n, cb) { cb(null, n) } } 209 | } 210 | }, { 211 | name: 'bar', 212 | manifest: { 213 | farewell: 'async' 214 | }, 215 | init: function () { 216 | return { farewell: function (n, cb) { cb(null, n) } } 217 | } 218 | } 219 | ]) 220 | 221 | t.deepEqual(Create.manifest, { 222 | hello: 'sync', 223 | foo: { 224 | goodbye: 'async' 225 | }, 226 | bar: { 227 | farewell: 'async' 228 | } 229 | }) 230 | 231 | t.end() 232 | }) 233 | 234 | tape('optional cb hook for sync api methods', function (t) { 235 | // core, not a plugin. 236 | var Create = Api([{ 237 | manifest: { 238 | hello: 'sync' 239 | }, 240 | init: function (api) { 241 | return { 242 | hello: function (name) { 243 | return 'Hello, ' + name + '.' 244 | } 245 | } 246 | } 247 | }]) 248 | 249 | // console.log(Create) 250 | 251 | Create.use({ 252 | name: 'foo', 253 | manifest: { 254 | goodbye: 'sync' 255 | }, 256 | init: function () { 257 | return { 258 | goodbye: function (n) { 259 | if (n === 0) { throw new Error('bad input!') } 260 | return n 261 | } 262 | } 263 | } 264 | }) 265 | 266 | var api = Create({ okay: true }) 267 | 268 | // sync usages 269 | t.equal(api.hello('Foo'), 'Hello, Foo.') 270 | t.equal(api.foo.goodbye(5), 5) 271 | try { 272 | api.foo.goodbye(0) 273 | t.fail('should have thrown') 274 | } catch (e) { 275 | t.ok(e) 276 | } 277 | 278 | // async usages 279 | api.hello('Foo', function (err, res) { 280 | if (err) throw err 281 | t.equal(res, 'Hello, Foo.') 282 | 283 | api.foo.goodbye(5, function (err, res) { 284 | if (err) throw err 285 | t.equal(res, 5) 286 | 287 | api.foo.goodbye(0, function (err) { 288 | t.ok(err) 289 | t.end() 290 | }) 291 | }) 292 | }) 293 | }) 294 | -------------------------------------------------------------------------------- /test/app-key.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var crypto = require('crypto') 3 | var SecretStack = require('../lib') 4 | var seeds = require('./seeds') 5 | 6 | // deterministic keys make testing easy. 7 | function hash (s) { 8 | return crypto.createHash('sha256').update(s).digest() 9 | } 10 | 11 | var appkey0 = hash('test_key0') 12 | var appkey1 = hash('test_key1').toString('base64') 13 | var appkey2 = hash('test_key2') 14 | 15 | // set a default appkey, this will not actually be used 16 | // because everything downstream sets their appkey via config 17 | var create = SecretStack({ appKey: appkey0 }) 18 | 19 | create.use({ 20 | manifest: { 21 | hello: 'sync', 22 | aliceOnly: 'sync' 23 | }, 24 | permissions: { 25 | anonymous: { allow: ['hello'], deny: null } 26 | }, 27 | init: function (api) { 28 | return { 29 | hello: function (name) { 30 | return 'Hello, ' + name + '.' 31 | }, 32 | aliceOnly: function () { 33 | console.log('alice only') 34 | return 'hihihi' 35 | } 36 | } 37 | } 38 | }) 39 | .use(function (api) { 40 | api.auth.hook(function (fn, args) { 41 | var cb = args.pop() 42 | var id = args.shift() 43 | fn(id, function (err, res) { 44 | if (err) return cb(err) 45 | if (id === alice.id) { cb(null, { allow: ['hello', 'aliceOnly'] }) } else cb() 46 | }) 47 | }) 48 | }) 49 | 50 | var alice = create({ 51 | multiserverShs: { 52 | seed: seeds.alice, 53 | cap: appkey1 54 | } 55 | }) 56 | 57 | var bob = create({ 58 | multiserverShs: { 59 | seed: seeds.bob, 60 | cap: appkey1 61 | } 62 | }) 63 | 64 | var carol = create({ 65 | multiserverShs: { 66 | seed: seeds.carol, 67 | cap: appkey1 68 | } 69 | }) 70 | 71 | tape('alice *can* use alice_only api', function (t) { 72 | alice.connect(bob.address(), function (err, rpc) { 73 | if (err) throw err 74 | rpc.aliceOnly(function (err, data) { 75 | if (err) throw err 76 | t.equal(data, 'hihihi') 77 | t.end() 78 | }) 79 | }) 80 | }) 81 | 82 | tape('carol *cannot* use alice_only api', function (t) { 83 | carol.connect(bob.address(), function (err, rpc) { 84 | if (err) throw err 85 | rpc.aliceOnly(function (err, data) { 86 | t.ok(err) 87 | t.end() 88 | }) 89 | }) 90 | }) 91 | 92 | var antialice = create({ 93 | global: { 94 | seed: seeds.alice, appKey: appkey2 95 | } 96 | }) 97 | 98 | var antibob = create({ 99 | global: { 100 | seed: seeds.bob, appKey: appkey2 101 | } 102 | }) 103 | 104 | tape('antialice cannot connect to alice because they use different appkeys', function (t) { 105 | antialice.connect(alice.address(), function (err, rpc) { 106 | t.ok(err) 107 | if (rpc) throw new Error('should not have connected successfully') 108 | t.end() 109 | }) 110 | }) 111 | 112 | tape('antialice can connect to antibob because they use the same appkeys', function (t) { 113 | antialice.connect(antibob.address(), function (err, rpc) { 114 | t.notOk(err) 115 | t.end() 116 | }) 117 | }) 118 | 119 | tape('cleanup', function (t) { 120 | alice.close(true) 121 | antialice.close(true) 122 | bob.close(true) 123 | antibob.close(true) 124 | carol.close(true) 125 | t.end() 126 | }) 127 | -------------------------------------------------------------------------------- /test/auth.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var crypto = require('crypto') 3 | var SecretStack = require('../lib') 4 | var seeds = require('./seeds') 5 | 6 | // deterministic keys make testing easy. 7 | function hash (s) { 8 | return crypto.createHash('sha256').update(s).digest() 9 | } 10 | 11 | var appkey = hash('test_key') 12 | 13 | var create = SecretStack({ 14 | global: { 15 | appKey: appkey 16 | } 17 | }) 18 | 19 | create.use({ 20 | manifest: { 21 | hello: 'sync', 22 | aliceOnly: 'sync' 23 | }, 24 | permissions: { 25 | anonymous: { allow: ['hello'], deny: null } 26 | }, 27 | init: function (api) { 28 | return { 29 | hello: function (name) { 30 | return 'Hello, ' + name + '.' 31 | }, 32 | aliceOnly: function () { 33 | console.log('alice only') 34 | return 'hihihi' 35 | } 36 | } 37 | } 38 | }) 39 | .use(function (api) { 40 | api.auth.hook(function (fn, args) { 41 | var cb = args.pop() 42 | var id = args.shift() 43 | fn(id, function (err, res) { 44 | if (err) return cb(err) 45 | if (id === alice.id) { cb(null, { allow: ['hello', 'aliceOnly'] }) } else cb() 46 | }) 47 | }) 48 | }) 49 | 50 | var alice = create({ 51 | multiserverShs: { 52 | seed: seeds.alice 53 | } 54 | }) 55 | 56 | var bob = create({ 57 | multiserverShs: { 58 | seed: seeds.bob 59 | } 60 | }) 61 | 62 | var carol = create({ 63 | multiserverShs: { 64 | seed: seeds.carol 65 | } 66 | }) 67 | 68 | tape('alice *can* use alice_only api', function (t) { 69 | alice.connect(bob.address(), function (err, rpc) { 70 | if (err) throw err 71 | rpc.aliceOnly(function (err, data) { 72 | if (err) throw err 73 | t.equal(data, 'hihihi') 74 | t.end() 75 | }) 76 | }) 77 | }) 78 | 79 | tape('carol *cannot* use alice_only api', function (t) { 80 | carol.connect(bob.address(), function (err, rpc) { 81 | if (err) throw err 82 | rpc.aliceOnly(function (err, data) { 83 | t.ok(err) 84 | rpc.close(function () { 85 | t.end() 86 | }) 87 | }) 88 | }) 89 | }) 90 | 91 | tape('bob calls back to a client connection', function (t) { 92 | bob.on('rpc:connect', function (rpc) { 93 | rpc.hello(function (err, data) { 94 | t.notOk(err) 95 | t.ok(data) 96 | rpc.aliceOnly(function (err, data) { 97 | t.ok(err) 98 | t.end() 99 | }) 100 | }) 101 | }) 102 | carol.connect(bob.address(), function (err, rpc) { 103 | t.error(err) 104 | }) 105 | }) 106 | 107 | tape('cleanup', function (t) { 108 | alice.close(true) 109 | bob.close(true) 110 | carol.close(true) 111 | t.end() 112 | }) 113 | -------------------------------------------------------------------------------- /test/auth2.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var crypto = require('crypto') 3 | var ssbKeys = require('ssb-keys') 4 | var SecretStack = require('../lib') 5 | var seeds = require('./seeds') 6 | 7 | var keys = { 8 | alice: ssbKeys.generate(null, seeds.alice), 9 | bob: ssbKeys.generate(null, seeds.bob), 10 | carol: ssbKeys.generate(null, seeds.carol) 11 | } 12 | 13 | // deterministic keys make testing easy. 14 | function hash (s) { 15 | return crypto.createHash('sha256').update(s).digest() 16 | } 17 | 18 | var appkey = hash('test_key') 19 | 20 | var create = SecretStack({ 21 | global: { 22 | appKey: appkey 23 | } 24 | }) 25 | 26 | create.use({ 27 | manifest: { 28 | hello: 'sync', 29 | aliceOnly: 'sync' 30 | }, 31 | permissions: { 32 | anonymous: { allow: ['hello'], deny: null } 33 | }, 34 | init: function (api) { 35 | return { 36 | hello: function (name) { 37 | return 'Hello, ' + name + '.' 38 | }, 39 | aliceOnly: function () { 40 | console.log('alice only') 41 | return 'hihihi' 42 | } 43 | } 44 | } 45 | }) 46 | .use(function (api) { 47 | api.auth.hook(function (fn, args) { 48 | var cb = args.pop() 49 | var id = args.shift() 50 | fn(id, function (err, res) { 51 | if (err) return cb(err) 52 | console.log('AUTH', id, keys.alice.id) 53 | if (id === keys.alice.id) { cb(null, { allow: ['hello', 'aliceOnly'] }) } else cb() 54 | }) 55 | }) 56 | }) 57 | 58 | var alice = create({ 59 | global: { 60 | keys: keys.alice 61 | } 62 | }) 63 | 64 | var bob = create({ 65 | global: { 66 | keys: keys.bob 67 | } 68 | }) 69 | 70 | var carol = create({ 71 | global: { 72 | keys: keys.carol 73 | } 74 | }) 75 | 76 | tape('bob has address', function (t) { 77 | setTimeout(function () { 78 | t.ok(bob.getAddress('device') || bob.getAddress('local')) 79 | t.end() 80 | }, 1000) 81 | }) 82 | 83 | tape('client calls server: alice -> bob', function (t) { 84 | t.ok(alice.id, 'has local legacy ID') 85 | t.ok(alice.shs.pubkey, 'has local modern ID') 86 | const before = alice.shs.pubkey 87 | alice.shs.pubkey = 'mutated' 88 | const after = alice.shs.pubkey 89 | t.equal(before, after, 'modern ID cannot be mutated') 90 | 91 | alice.connect(bob.getAddress('device') || bob.getAddress(), function (err, bobRpc) { 92 | if (err) throw err 93 | t.ok(bobRpc.id, 'has remote legacy ID') 94 | t.ok(bobRpc.shs.pubkey, 'has remote modern ID') 95 | 96 | const before = bob.shs.pubkey 97 | bob.shs.pubkey = 'mutated' 98 | const after = bob.shs.pubkey 99 | t.equal(before, after, 'modern ID cannot be mutated') 100 | 101 | bobRpc.hello(function (err, data) { 102 | t.notOk(err) 103 | t.ok(data) 104 | bobRpc.aliceOnly(function (err, data) { 105 | t.notOk(err) 106 | t.ok(data) 107 | bobRpc.close(function () { 108 | t.end() 109 | }) 110 | }) 111 | }) 112 | }) 113 | }) 114 | 115 | tape('server calls client: alice <- bob', function (t) { 116 | alice.connect(bob.getAddress('device') || bob.getAddress(), function (err, _bobRpc) { 117 | if (err) throw err 118 | }) 119 | 120 | bob.on('rpc:connect', function (aliceRpc) { 121 | // console.log(aliceRpc) 122 | aliceRpc.hello(function (err, data) { 123 | t.notOk(err) 124 | t.ok(data) 125 | aliceRpc.aliceOnly(function (err, data) { 126 | t.ok(err) 127 | t.notOk(data) 128 | aliceRpc.close(function () { 129 | t.end() 130 | }) 131 | }) 132 | }) 133 | }) 134 | }) 135 | 136 | tape('cleanup', function (t) { 137 | alice.close(() => {}) 138 | bob.close(() => {}) 139 | carol.close(() => {}) 140 | t.end() 141 | }) 142 | -------------------------------------------------------------------------------- /test/close.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto') 2 | var SecretStack = require('../lib') 3 | var seeds = require('./seeds') 4 | 5 | // deterministic keys make testing easy. 6 | function hash (s) { 7 | return crypto.createHash('sha256').update(s).digest() 8 | } 9 | 10 | var create = SecretStack({ 11 | global: { 12 | appKey: hash('test_flood'), 13 | }, 14 | }) 15 | .use({ 16 | manifest: { 17 | testSource: 'source' 18 | }, 19 | init: function () { 20 | return { 21 | testSource: function (abort, cb) { 22 | 23 | } 24 | } 25 | } 26 | }) 27 | function createPeer (name) { 28 | var alice = create({ multiserverShs: { seed: seeds[name] } }) 29 | return alice.on('flood:message', function (msg) { 30 | console.log(name, 'received', msg) 31 | }) 32 | } 33 | 34 | var alice = createPeer('alice') 35 | var bob = createPeer('bob') 36 | var carol = createPeer('carol') 37 | 38 | bob.connect(alice.address(), function (err, rpc) { 39 | if (err) throw err 40 | var n = 2 41 | rpc.testSource() 42 | rpc.once('closed', next) 43 | alice.connect(carol.address(), function (err, rpc) { 44 | if (err) throw err 45 | rpc.once('closed', next) 46 | alice.close(true, () => {}) 47 | rpc.testSource() 48 | }) 49 | 50 | function next () { 51 | if (--n) return 52 | console.log('closed') 53 | alice.close(() => {}) 54 | bob.close(() => {}) 55 | carol.close(() => {}) 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /test/flood.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream') 2 | var crypto = require('crypto') 3 | var SecretStack = require('../lib') 4 | var seeds = require('./seeds') 5 | 6 | var Pushable = require('pull-pushable') 7 | 8 | // deterministic keys make testing easy. 9 | function hash (s) { 10 | return crypto.createHash('sha256').update(s).digest() 11 | } 12 | 13 | var create = SecretStack({ 14 | global: { 15 | appKey: hash('test_flood') 16 | } 17 | }) 18 | .use({ 19 | manifest: { 20 | flood: 'source' 21 | }, 22 | permissions: { 23 | anonymous: { allow: ['flood'] } 24 | }, 25 | init: function (api) { 26 | // clients will call flood. 27 | var messages = {} 28 | var senders = {} 29 | 30 | function receive (msg) { 31 | var msgId = hash(JSON.stringify(msg)) 32 | if (messages[msgId]) return false 33 | api.emit('flood:message', msg) 34 | messages[msgId] = msg 35 | return msg 36 | } 37 | 38 | api.on('rpc:connect', function (rpc) { 39 | pull( 40 | rpc.flood(), 41 | pull.drain(function (msg) { 42 | // broadcast this message, 43 | // but do not send it back to the person 44 | // you received it from. 45 | api.broadcast(msg, this.id) 46 | }) 47 | ) 48 | }) 49 | 50 | return { 51 | // local 52 | broadcast: function (msg, id) { 53 | // never send a message twice. 54 | if (!receive(msg)) return 55 | for (var k in senders) { if (k !== id) senders[k].push(msg) } 56 | }, 57 | flood: function () { 58 | var id = this.id 59 | 60 | if (senders[id]) senders.abort(true) 61 | var pushable = senders[id] = Pushable(function () { 62 | if (senders[id] === pushable) { delete senders[id] } 63 | }) 64 | for (var k in messages) { senders[id].push(messages[k]) } 65 | return senders[id] 66 | } 67 | } 68 | } 69 | }) 70 | 71 | function createPeer (name) { 72 | var alice = create({ multiserverShs: { seed: seeds[name] } }) 73 | return alice.on('flood:message', function (msg) { 74 | console.log(name, 'received', msg) 75 | }) 76 | } 77 | 78 | var alice = createPeer('alice') 79 | var bob = createPeer('bob') 80 | var carol = createPeer('carol') 81 | 82 | // for simplicity, we are connecting these manually 83 | // but for extra points, use a gossip protocol, etc! 84 | 85 | carol.connect(alice.address(), function (err) { 86 | if (err) throw err 87 | alice.connect(bob.address(), function (err) { 88 | if (err) throw err 89 | alice.broadcast('Hello!') 90 | bob.broadcast({ okay: true }) 91 | }) 92 | }) 93 | 94 | var i = 10 95 | var int = setInterval(function () { 96 | var d = new Date() 97 | if (--i) return carol.broadcast({ date: d.toString(), ts: +d }) 98 | clearInterval(int) 99 | console.log('CLOSE CLOSE CLOSE') 100 | alice.close(true, function () { 101 | console.log('alice closes') 102 | }) 103 | bob.close(true, function () { 104 | console.log('bob closes') 105 | }) 106 | carol.close(true, function () { 107 | console.log('carol closes') 108 | }) 109 | }, 100) 110 | -------------------------------------------------------------------------------- /test/local.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var SecretStack = require('../lib') 3 | var seeds = require('./seeds') 4 | 5 | var appKey = Buffer.alloc(32) 6 | 7 | var create = SecretStack({ global: { appKey } }) 8 | create.use({ 9 | manifest: { 10 | ping: 'sync' 11 | }, 12 | permissions: { 13 | anonymous: { allow: ['ping'], deny: null } 14 | }, 15 | init: function (api) { 16 | return { 17 | ping: function () { 18 | return 'pong' 19 | } 20 | } 21 | } 22 | }) 23 | 24 | var alice = create({ 25 | global: { 26 | seed: seeds.alice, 27 | timeout: 100 28 | } 29 | }) 30 | 31 | tape('do not timeout local client rpc', function (t) { 32 | alice.connect(alice.address(), function (err, rpc) { 33 | t.error(err, 'connect') 34 | setTimeout(function () { 35 | rpc.ping(function (err, pong) { 36 | t.error(err, 'ping') 37 | t.end() 38 | }) 39 | }, 200) 40 | }) 41 | }) 42 | 43 | tape.onFinish(function (t) { 44 | alice.close(true) 45 | }) 46 | -------------------------------------------------------------------------------- /test/merge.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var merge = require('../lib/util').merge 3 | 4 | tape('merge permissions', function (t) { 5 | var m = merge.permissions({ 6 | anonymous: { allow: [], deny: null } 7 | }, { 8 | anonymous: { allow: ['foo'] } 9 | }) 10 | 11 | t.deepEqual(m, { anonymous: { allow: ['foo'], deny: null } }) 12 | 13 | var m2 = merge.permissions(m, { 14 | anonymous: { allow: ['baz'] } 15 | }, 'BAR') 16 | 17 | t.deepEqual(m2, { anonymous: { allow: ['foo', 'BAR.baz'], deny: null } }) 18 | 19 | t.end() 20 | }) 21 | 22 | tape('merge manifest', function (t) { 23 | var m = merge.manifest({ 24 | req: 'async', 25 | source: 'source', 26 | sink: 'sink' 27 | }, { 28 | more: { 29 | req: 'async', 30 | source: 'source', 31 | sink: 'sink' 32 | } 33 | }) 34 | 35 | t.deepEqual(m, { 36 | req: 'async', 37 | source: 'source', 38 | sink: 'sink', 39 | more: { 40 | req: 'async', 41 | source: 'source', 42 | sink: 'sink' 43 | } 44 | }) 45 | 46 | var m2 = merge.manifest({ 47 | req: 'async' 48 | }, { 49 | req: 'async' 50 | }, 'NAME') 51 | 52 | t.deepEqual(m2, { 53 | req: 'async', 54 | NAME: { 55 | req: 'async' 56 | } 57 | }) 58 | 59 | t.end() 60 | }) 61 | -------------------------------------------------------------------------------- /test/seeds.js: -------------------------------------------------------------------------------- 1 | // it became easier to debug by making the keys deterministic 2 | // and it becomes easier still if the pubkeys are recognisable. 3 | // so i mined for keys that started with the names I wanted. 4 | // using vanity-ed25519 module. 5 | 6 | function S (base64) { 7 | return Buffer.from(base64, 'base64') 8 | } 9 | 10 | exports.alice = S('8C37zWNNunT5q2K8hS9WX6FitXQ9kfU6uZLJV+Swc/s=') 11 | exports.bob = S('mRw/ScuApTnmNNfRXf85YCSA1bHTsdyM0uJcI/3OoNk=') 12 | exports.carol = S('EwS1uQPplLvG006DMhMoTSdpitB5adyWP2kt/H7/su0=') 13 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var crypto = require('crypto') 3 | var SecretStack = require('../lib/bare') 4 | var seeds = require('./seeds') 5 | 6 | // deterministic keys make testing easy. 7 | function hash (s) { 8 | return crypto.createHash('sha256').update(s).digest() 9 | } 10 | 11 | var appKey = hash('test_key') 12 | 13 | var create = SecretStack({ global: { appKey } }) 14 | .use(require('../lib/plugins/net')) 15 | .use(require('../lib/plugins/shs')) 16 | .use({ 17 | manifest: { 18 | hello: 'sync' 19 | }, 20 | permissions: { 21 | anonymous: { allow: ['hello'], deny: null } 22 | }, 23 | init: function (api) { 24 | return { 25 | hello: function (name) { 26 | return 'Hello, ' + name + '.' 27 | } 28 | } 29 | } 30 | }) 31 | 32 | var alice = create({ global: { seed: seeds.alice } }) 33 | var bob = create({ global: { seed: seeds.bob } }) 34 | 35 | tape('alice connects to bob', function (t) { 36 | alice.connect(bob.address(), function (err, rpc) { 37 | if (err) throw err 38 | 39 | rpc.hello('Alice', function (err, data) { 40 | if (err) throw err 41 | t.equal(data, 'Hello, Alice.') 42 | // alice.close(true); bob.close(true) 43 | // console.log(data) 44 | t.end() 45 | }) 46 | }) 47 | }) 48 | 49 | tape('alice is client, bob is server', function (t) { 50 | alice.on('rpc:connect', function (rpc, isClient) { 51 | t.true(rpc.stream.address.substr(0, 4) === 'net:' && rpc.stream.address.length > 40) 52 | t.ok(isClient) 53 | }) 54 | bob.on('rpc:connect', function (rpc, isClient) { 55 | t.true(rpc.stream.address.substr(0, 4) === 'net:' && rpc.stream.address.length > 40) 56 | t.notOk(isClient) 57 | }) 58 | 59 | alice.connect(bob.address(), function (err, rpc) { 60 | t.error(err) 61 | setTimeout(() => { 62 | rpc.close(true, t.end) 63 | }, 50) 64 | }) 65 | }) 66 | 67 | tape('cleanup', function (t) { 68 | alice.close(true, () => { 69 | bob.close(true, t.end) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/timeout.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var crypto = require('crypto') 3 | var SecretStack = require('../lib') 4 | var seeds = require('./seeds') 5 | 6 | // deterministic keys make testing easy. 7 | function hash (s) { 8 | return crypto.createHash('sha256').update(s).digest() 9 | } 10 | 11 | var appKey = hash('test_key') 12 | 13 | var create = SecretStack({ global: { appKey } }).use({ 14 | manifest: { 15 | hello: 'sync' 16 | }, 17 | permissions: { 18 | anonymous: { allow: ['hello'], deny: null } 19 | }, 20 | init: function (api) { 21 | return { 22 | hello: function (name) { 23 | return 'Hello, ' + name + '.' 24 | } 25 | } 26 | } 27 | }) 28 | 29 | var alice = create({ global: { seed: seeds.alice, timeout: 200, defaultTimeout: 5e3 } }) 30 | var carol = create({ global: { seed: seeds.alice, timeout: 0, defaultTimeout: 10 } }) 31 | var bob = create({ global: { seed: seeds.bob, timeout: 200, defaultTimeout: 2000 } }) 32 | 33 | tape('delay startup', function (t) { 34 | setTimeout(t.end, 500) 35 | }) 36 | 37 | tape('alice connects to bob', function (t) { 38 | var connected = false; var disconnected = false 39 | 40 | alice.connect(bob.address(), function (err, rpc) { 41 | if (err) throw err 42 | var start = Date.now() 43 | rpc.on('closed', function () { 44 | console.log('time to close:', Date.now() - start) 45 | t.ok(connected) 46 | t.notOk(disconnected) 47 | t.end() 48 | }) 49 | }) 50 | 51 | carol.connect(bob.address(), function (err, rpc) { 52 | if (err) throw err 53 | connected = true 54 | carol.on('closed', function (t) { 55 | disconnected = true 56 | }) 57 | }) 58 | }) 59 | 60 | tape('cleanup', function (t) { 61 | alice.close(true); bob.close(true); carol.close(true) 62 | t.end() 63 | }) 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib/**/*.js"], 3 | "exclude": ["coverage/", "node_modules/", "test/"], 4 | "compilerOptions": { 5 | "checkJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["es2022", "dom"], 11 | "module": "node16", 12 | "skipLibCheck": true, 13 | "strict": true, 14 | "target": "es2021" 15 | } 16 | } --------------------------------------------------------------------------------