Skip to the content.

Wampy.js

About

Feature-rich, lightweight and zero dependency (by default) WAMP (WebSocket Application Messaging Protocol) Javascript implementation (for browser and node.js)

NPM version Build Status Code coverage MIT License Known Vulnerabilities

Table of Contents

Description

Wampy.js is javascript library, that runs both in browser and node.js environments, and even in react native environment. It implements WAMP v2 specification on top of WebSocket object, also provides additional features like auto-reconnecting. It has no external dependencies (by default) and is easy to use. Also, it provides (starting from v7.1) command line wamp client which can be extremely helpful in quick check/debug existing WAMP-based APIs.

Wampy.js supports the following WAMP roles and features:

Wampy supports the following serializers:

In node.js environment Wampy is compatible with the following websocket clients:

For convenience, all API documentation is also available as a GitBook here.

Back to Table of Contents

Usage example

const wampy = new Wampy('/ws/', { realm: 'AppRealm' });

try {
    await wampy.connect();
} catch (e) {
    console.log('connection failed', e);
}

try {
    await wampy.subscribe('system.monitor.update', (eventData) => {
         console.log('Received event:', eventData);
    });
} catch (e) {
    console.log('subscription failed', e);
}

try {
    const res = await wampy.call('get.server.time');
    console.log('RPC called. Server time:', res.argsDict.serverTime);
} catch (e) {
    console.log('RPC call failed', e);
}

// Somewhere else for example
await wampy.publish('system.monitor.update');

// or just ignore promise if you don't need it
wampy.publish('client.message', 'Hi guys!');

Back to Table of Contents

Installation

Wampy.js can be installed using npm:

npm install -S wampy

For browser usage download the latest browser.zip archive and add wampy-all.min.js file to your page. It contains msgpack encoder plus wampy itself.

<script src="browser/wampy-all.min.js"></script>

If you don’t plan to use msgpack, just include wampy.min.js.

<script src="browser/wampy.min.js"></script>

In case you are using build tools or bundlers like grunt/gulp/webpack/rollup/vite/etc, your entry point can be * *src/wampy.js** if you transpile you code, or dist/wampy.js (default package entry point) which is already transpiled to “env” preset, which means it works out of the box, just bundle modules.

Back to Table of Contents

CLI tool

Wampy cli tool exposes almost the same API options to the command line interface. You can use all types of authorization, publish, subscribe, register and call any URI. Some WAMP Actions provides additional helper options (e.g. mirror option in register command that allows to return back the invocation payload to the caller).

Cli tool is charged with rich help descriptions, examples and even shell auto-completion script. All parameters may be passed as cmd args or via related ENV Vars for convenience. So you can for example export WAMP Router URI and realm to the environment and provide only wamp action parameters via cmd.

You can install wampy cli tool globally or call it by npx:

npm install -g wampy
# After this you can invoke wampy
wampy -h
# or just run wampy with npx
npx wampy -h

Check the wampy -h or wampy --help for the available commands and global options and check the help for a specific command by issuing wampy <call|register|publish|subscribe> -h.

To make use of shell auto-completion features just add output of wampy completion to your shell config:

wampy completion >> ~/.zshrc
# or
wampy completion >> ~/.bashrc

The completion command is hidden from the wampy -h output to not pollute the main use flow as it is only needed once.

Migrating or Updating versions

Please refer to Migrating.md for instructions on upgrading major versions.

Back to Table of Contents

API

Below is a description of the exposed public API. Wampy also has type definitions available at DefinitelyTyped.org, but they are only for versions < 7.x for now. Feel free to update!)

Constructor([url[, options]])

Wampy constructor can take 2 parameters:

// in browser
wampy = new Wampy();
wampy = new Wampy('/my-socket-path');
wampy = new Wampy('wss://socket.server.com:5000/ws', { autoReconnect: false });
wampy = new Wampy({ reconnectInterval: 1*1000 });

// in node.js
w3cws = require('websocket').w3cwebsocket;
wampy = new Wampy(null, { ws: w3cws });
wampy = new Wampy('/my-socket-path', { ws: w3cws });
wampy = new Wampy('wss://socket.server.com:5000/ws', { autoReconnect: false, ws: w3cws });
wampy = new Wampy({ reconnectInterval: 1*1000, ws: w3cws });

// or using ws example
import WebSocket from 'ws';
wampy = new Wampy(null, { ws: WebSocket });
wampy = new Wampy('/my-socket-path', { ws: WebSocket });
wampy = new Wampy('wss://socket.server.com:5000/ws', { autoReconnect: false, ws: WebSocket });
wampy = new Wampy({ reconnectInterval: 1*1000, ws: WebSocket });

Json serializer will be used by default. If you want to use msgpack or cbor serializer, pass it through options. Also, you can use your own serializer if it is supported on the WAMP router side.

// in browser
wampy = new Wampy('wss://socket.server.com:5000/ws', {
    serializer: new MsgpackSerializer()
});
wampy = new Wampy({
    serializer: new CborSerializer()
});

// in node.js
import { Wampy } from 'wampy';
import { MsgpackSerializer } from 'wampy/MsgpackSerializer';
import { CborSerializer } from 'wampy/CborSerializer';
import WebSocket from 'ws';

wampy = new Wampy('wss://socket.server.com:5000/ws', {
    ws: WebSocket,
    serializer: new MsgpackSerializer()
});
wampy = new Wampy({
    ws: w3cws,
    serializer: new CborSerializer()
});

Back to Table of Contents

options([opts])

.options() method is now deprecated, so this is here only for documentation purposes. Please use getOptions()/setOptions() instead.

.options() can be called in two forms: – without parameters it will behave the same as new method getOptions() – with one parameter as a hash-table it will behave the same as new method setOptions()

wampy.options(); // same as wampy.getOptions

wampy.options({ // same as wampy.setOptions
    authPlugins: {
        ticket: ((userPassword) => (() => userPassword ))(),
        wampcra: wampyCra.sign(secret),
        cryptosign: wampyCryptosign.sign(privateKey)
    },
    authMode: 'auto'
});

getOptions()

Returns Wampy configuration options. See setOptions() down below for the full list of available options.

wampy.getOptions();

setOptions([newOptions])

Receives a newOptions object as a parameter, where each property is a new option to be set and returns a Wampy instance.

Options attributes description:

const wampyCra = require('wampy-cra');
const wampyCryptosign = require('wampy-cryptosign');

wampy.setOptions({
    authPlugins: {
        // No need to process challenge data in ticket flow, as it is empty
        ticket: ((userPassword) => (() => userPassword ))(),
        wampcra: wampyCra.sign(secret),
        cryptosign: wampyCryptosign.sign(privateKey)
    },
    authMode: 'auto'
});
wampy.setOptions({
    reconnectInterval: 1000,
    maxRetries: 999,
    onClose: () => { console.log('See you next time!'); },
    onError: () => { console.log('Breakdown happened'); },
    onReconnect: () => { console.log('Reconnecting...'); },
    onReconnectSuccess: (welcomeDetails) => { console.log('Reconnection succeeded. Details:', welcomeDetails); }
});

Back to Table of Contents

getOpStatus()

Returns the status of last operation. This method returns an object with attributes:

const defer = ws.publish('system.monitor.update');
console.log(ws.getOpStatus());
// may return
//    { code: 1, error: UriError instance }
// or { code: 2, error: NoBrokerError instance }
// or { code: 0, error: null }

Back to Table of Contents

getSessionId()

Returns the WAMP Session ID.

ws.getSessionId();

Back to Table of Contents

connect([url])

Connects to wamp server. url parameter is the same as specified in Constructor. Returns a Promise that’s either:

try {
    await wampy.connect();
} catch (e) {
    console.log('connection failed', e);
}

await ws.connect('/my-socket-path');

const defer = ws.connect('wss://socket.server.com:5000/ws');

Back to Table of Contents

disconnect()

Disconnects from wamp server. Clears all queues, subscription, calls. Returns a Promise that’s either:

await ws.disconnect();

Back to Table of Contents

abort()

Aborts WAMP session and closes a websocket connection. If it is called on handshake stage - it sends the abort message to wamp server (as described in spec). Also clears all queues, subscription, calls. Returns wampy instance back.

ws.abort();

Back to Table of Contents

Ticket-based Authentication

With Ticket-based authentication, the client needs to present the server an authentication “ticket” - some magic value to authenticate itself to the server. It could be a user password, an authentication token or any other kind of client secret. To use it you need to provide “ticket” in “authmethods”, “authid” and the “onChallenge” callback as wampy instance options.

'use strict';

// Ticket authentication
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'joe',
    authmethods: ['ticket'],
    onChallenge: (method, info) => {
        console.log('Requested challenge with ', method, info);
        return 'joe secret key or password';
    }
});

// Promise-based ticket authentication
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'micky',
    authmethods: ['ticket'],
    onChallenge: (method, info) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('Requested challenge with ', method, info);
                resolve('micky secret key or password');
            }, 2000);
        });
    }
});

Challenge Response Authentication

Wampy.js supports challenge response authentication. To use it you need to provide the “authid” and the “onChallenge” callback as wampy instance options. Also, Wampy.js supports wampcra authentication method with a little helper plugin “wampy-cra”. Just add “wampy-cra” package and use provided methods as shown below.

'use strict';

const Wampy = require('wampy').Wampy;
const wampyCra = require('wampy-cra');
const w3cws = require('websocket').w3cwebsocket;

// Manual authentication using signed message
wampy = new Wampy('wss://wamp.router.url', {
    ws: w3cws,  // just for example in node.js env
    realm: 'realm1',
    authid: 'joe',
    authmethods: ['wampcra'],
    onChallenge: (method, info) => {
        console.log('Requested challenge with ', method, info);
        return wampyCra.signManual('joe secret key or password', info.challenge);
    }
});

// Promise-based manual authentication using signed message
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'micky',
    authmethods: ['wampcra'],
    onChallenge: (method, info) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('Requested challenge with ', method, info);
                resolve(wampyCra.signManual('micky secret key or password', info.challenge));
            }, 2000);
        });
    }
});

// Manual authentication using salted key and pbkdf2 scheme
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'peter',
    authmethods: ['wampcra'],
    onChallenge: (method, info) => {
        const iterations = 100;
        const keylen = 16;
        const salt = 'password salt for user peter';

        console.log('Requested challenge with ', method, info);
        return wampyCra.signManual(wampyCra.deriveKey('peter secret key or password', salt, iterations, keylen), info.challenge);
    }
});

// Automatic CRA authentication
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'patrik',
    authmethods: ['wampcra'],
    onChallenge: wampyCra.sign('patrik secret key or password')
});

Back to Table of Contents

Cryptosign-based Authentication

Wampy.js supports cryptosign-based authentication. To use it you need to provide authid, onChallenge callback and authextra as wampy instance options. Also, Wampy.js supports cryptosign authentication method with a little helper plugin “wampy-cryptosign”. Just add “wampy-cryptosign” package and use provided methods as shown below.

The authextra option may contain the following properties for WAMP-Cryptosign:

Field Type Required Description
pubkey string yes The client public key (32 bytes) as a Hex encoded string, e.g. 545efb0a2192db8d43f118e9bf9aee081466e1ef36c708b96ee6f62dddad9122
channel_binding* string no If TLS channel binding is in use, the TLS channel binding type, e.g. "tls-unique".
challenge string no A client chosen, random challenge (32 bytes) as a Hex encoded string, to be signed by the router.
trustroot string no When the client includes a client certificate, the Ethereum address of the trustroot of the certificate chain to be used, e.g. 0x72b3486d38E9f49215b487CeAaDF27D6acf22115, which can be a Standalone Trustroot or an On-chain Trustroot

*: channel_binding is not supported yet. And may be supported only in node.js environment.

'use strict';

import { Wampy } from 'wampy';
import * as wampyCS from 'wampy-cryptosign';
// or you can import only the "sign" method
// import { sign } from 'wampy-cryptosign';

// Manual authentication using signed message
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'joe',
    authmethods: ['cryptosign'],
    authextra: {
        pubkey: '545efb0a2192db8d43f118e9bf9aee081466e1ef36c708b96ee6f62dddad9122'
    },
    onChallenge: (method, info) => {
        console.log('Requested challenge with ', method, info);
        return wampyCS.sign('joe secret (private) key')(method, info);
    }
});

// Promise-based manual authentication using signed message
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'micky',
    authmethods: ['cryptosign'],
    authextra: {
        pubkey: '545efb0a2192db8d43f118e9bf9aee081466e1ef36c708b96ee6f62dddad9122'
    },
    onChallenge: (method, info) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('Requested challenge with ', method, info);
                resolve(wampyCS.sign('micky secret (private) key')(method, info));
            }, 2000);
        });
    }
});

// Automatic CryptoSign authentication
wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'patrik',
    authmethods: ['cryptosign'],
    authextra: {
        pubkey: '545efb0a2192db8d43f118e9bf9aee081466e1ef36c708b96ee6f62dddad9122'
    },
    onChallenge: wampyCS.sign('patrik secret (private) key')
});

Back to Table of Contents

Automatically chosen Authentication

If you server provides multiple options for authorization, you can configure wampy.js to automatically choose required authorization flow based on authmethod requested by server. For this flow you need to configure the following options:

import { Wampy } from 'wampy';
import * as wampyCra from 'wampy-cra';
import * as wampyCS from 'wampy-cryptosign';

wampy = new Wampy('wss://wamp.router.url', {
    realm: 'realm1',
    authid: 'patrik',
    authmethods: ['ticket', 'wampcra', 'cryptosign'],
    authextra: {    // User public key for Cryptosign-based Authentication
        pubkey: '545efb0a2192db8d43f118e9bf9aee081466e1ef36c708b96ee6f62dddad9122'
    },
    authPlugins: {
        ticket: ((userPassword) => (() => userPassword ))(),
        wampcra: wampyCra.sign(userSecret),
        cryptosign: wampyCS.sign(userPrivateKey)
    },
    authMode: 'auto',
    onChallenge: null
});

Back to Table of Contents

subscribe(topicURI, onEvent[, advancedOptions])

Subscribes for topicURI events.

Input Parameters:

Returns a Promise that’s either:

await ws.subscribe('chat.message.received', (eventData) => { console.log('Received new chat message!', eventData); });

try {
    const res = await ws.subscribe('some.another.topic',
        (eventData) => {
            console.log('Received topic event', eventData);
        }
    );
    console.log('Successfully subscribed to topic: ' + res.topic);

} catch (e) {
    console.log('Subscription error:' + e.error);
}

Back to Table of Contents

unsubscribe(subscriptionIdKey[, onEvent])

Unsubscribe subscription from receiving events.

Parameters:

Returns a Promise that’s either:

const f1 = (data) => { console.log('this was event handler for topic') };
await ws.unsubscribe('subscribed.topic', f1);

const defer = ws.unsubscribe('chat.message.received');

Back to Table of Contents

publish(topicURI[, payload[, advancedOptions]])

Publish a new event to topic.

Parameters:

Returns a Promise that’s either:

await ws.publish('user.logged.in');
await ws.publish('chat.message.received', 'user message');  // will be sent as ['user message1']
await ws.publish('chat.message.received', ['user message1', 'user message2']);
await ws.publish('user.modified', { field1: 'field1', field2: true, field3: 123 });
await ws.publish('chat.message.received', ['Private message'], { eligible: 123456789 });

try {
    await ws.publish('user.modified', { field1: 'field1', field2: true, field3: 123 });
    console.log('User successfully modified');
} catch (e) {
    console.log('User modification failed', e.error, e.details);
}

Back to Table of Contents

call(topicURI[, payload[, advancedOptions]])

Make an RPC call to topicURI.

Parameters:

Returns a Promise that’s either:

Important note on progressive call results:

For getting a progressive call results you need to specify progress_callback in advancedOptions. This callback will be fired on every intermediate result. But the last one result or error will be processed on promise returned from the .call(). That means that final call result will be received by call promise resolve handler.

const result = await ws.call('server.time');
console.log('Server time is ' + result.argsList[0]);

try {
    await ws.call('start.migration');
    console.log('RPC successfully called');
} catch (e) {
    console.log('RPC call failed!', e.error);
}

try {
    await ws.call('restore.backup', { backupFile: 'backup.zip' });
    console.log('Backup successfully restored');
} catch (e) {
    console.log('Restore failed!', e.error, e.details);
}

Back to Table of Contents

cancel(reqId[, advancedOptions])

RPC invocation cancelling.

Parameters:

Returns a Boolean or throws an Error:

const defer = ws.call('start.migration');
defer
    .then((result) => console.log('RPC successfully called'))
    .catch((e) => console.log('RPC call failed!', e));

status = ws.getOpStatus();

ws.cancel(status.reqId);

Back to Table of Contents

register(topicURI, rpc[, advancedOptions])

RPC registration for invocation.

Parameters:

Returns a Promise that’s either:

Registered PRC during invocation will receive one hash-table argument with following attributes:

RPC can return no result (undefined), any single value, array or hash-table object:

const sqrt_f = function (data) { return { result: data.argsList[0]*data.argsList[0] } };

await ws.register('sqrt.value', sqrt_f);

try {
    await ws.register('sqrt.value', sqrt_f);
    console.log('RPC successfully registered');
} catch (e) {
    console.log('RPC registration failed!', e);
}

Also, wampy supports rpc with asynchronous code, such as some user interactions or xhr, using promises. For using this functionality in old browsers you should use polyfills, like es6-promise. Check browser support at can i use site.

const getUserName = () => {
    return new Promise((resolve, reject) => {
        /* Ask user to input his username somehow,
           and resolve promise with user input at the end */
        resolve({ argsList: userInput });
    });
};

ws.register('get.user.name', getUserName);

Also, it is possible to abort rpc processing and throw error with custom application specific data. This data will be passed to caller onError callback.

Exception object with custom data may have the following attributes:

Note: Any other type of errors (like built in Javascript runtime TypeErrors, ReferenceErrors) and exceptions are caught by wampy and sent back to the client’s side, not just this type of custom errors. In this case the details of the error can be lost.

const getSystemInfo = () => {

    // Application logic

    // for example, if you need to get data from db
    // and at this time you can't connect to db
    // you can throw exception with some details for client application

    const UserException = () => {
        this.error = 'app.error.no_database_connection';
        this.details = {
         errorCode: 'ECONNREFUSED',
         errorMessage: 'Connection refused by a remote host.',
         database: 'db',
         host: '1.2.3.4',
         port: 5432,
         dbtype: 'postgres'
       };
        this.argsList = ['Not able to connect to the database.'];
        this.argsDict = {};
    };

    throw new UserException();
};

await wampy.register('get.system.info', getSystemInfo);

try {
    await wampy.call('get.system.info');
} catch (error) {
    console.log('Error happened', error);
}

Back to Table of Contents

unregister(topicURI)

RPC unregistration from invocations.

Parameters:

Returns a Promise that’s either:

await ws.unregister('sqrt.value');

try {
    ws.unregister('sqrt.value');
    console.log('RPC successfully unregistered');
} catch (e) {
    console.log('RPC unregistration failed!', e);
}

Error handling

During wampy instance lifetime there can be many cases when error happens: some made by developer mistake, some are bound to WAMP protocol violation, some came from other peers. Errors that can be caught by wampy instance itself are stored in opStatus.error, while others are just thrown.

This allows, for example, convenient handling of different types of errors:

import {Wampy, Errors} from 'wampy';
const wampy = new Wampy('/ws/', { realm: 'AppRealm' });

try {
    await wampy.call('start.migration');
    console.log('RPC successfully called');
} catch (error) {
    console.log('Error happened!');
    if (error instanceof Errors.UriError) {
        // statements to handle UriError exceptions
    } else if (error instanceof Errors.InvalidParamError) {
        // statements to handle InvalidParamError exceptions
    } else if (error instanceof Errors.NoSerializerAvailableError) {
        // statements to handle NoSerializerAvailableError exceptions
    } else {
        // statements to handle any unspecified exceptions
    }
}

Wampy package exposes the following Error classes:

For errors attributes look at src/errors.js file.

Back to Table of Contents

Using custom serializer

From v5.0 version there is option to provide custom serializer.

Custom serializer instance must meet a few requirements:

Take a look at JsonSerializer.js or MsgpackSerializer.js as examples.

Back to Table of Contents

Connecting through TLS in node environment

Starting from v6.2.0 version you can pass additional HTTP Headers and TLS parameters to underlying socket connection in node.js environment (thnx websocket library). See example below. For wsRequestOptions you can pass any option, described in tls.connect options documentation.

const Wampy = require('wampy').Wampy;
const w3cws = require('websocket').w3cwebsocket;

wampy = new Wampy('wss://wamp.router.url:8888/wamp-router', {
    ws: w3cws,
    realm: 'realm1',
    additionalHeaders: {
        'X-ACL-custom-token': 'dkfjhsdkjfhdkjs',
        'X-another-custom-header': 'header-value'
    },
    wsRequestOptions: {
        ca: fs.readFileSync('ca-crt.pem'),
        key: fs.readFileSync('client1-key.pem'),
        cert: fs.readFileSync('client1-crt.pem'),
        host: 'wamp.router.url',
        port: 8888,
        rejectUnauthorized: false,   // this setting allow to connect to untrusted (or self signed) TLS certificate,
        checkServerIdentity: (servername, cert) => {
            // A callback function to be used (instead of the builtin tls.checkServerIdentity() function)
            // when checking the server's hostname (or the provided servername when explicitly set)
            // against the certificate. This should return an <Error> if verification fails.
            // The method should return undefined if the servername and cert are verified.
            if (servername !== 'MyTrustedServerName') {
                return new Error('Bad server!');
            }
        }
    }
});

Back to Table of Contents

Tests and code coverage

Wampy.js uses mocha and chai for tests and c8/istanbul for code coverage. Wampy sources are mostly all covered with tests! :)

# use standard npm test command
> npm test
# or only some tests
> npm run test:node
> npm run test:node-no-crossbar
> npm run test:browser

# for code coverage report run
> npm run cover
# and then open coverage/lcov-report/index.html

Back to Table of Contents

Wampy.js library is licensed under the MIT License (MIT).

Copyright (c) 2014 Konstantin Burkalev

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Back to Table of Contents

See Also

Back to Table of Contents

Thanks JetBrains for support! Best IDEs for every language!

jetbrains logo