ravelinjs is a JavaScript library for the browser to augment your integration with:
- an identifier for the customer's browser to be attached to an order (core);
- simple page events like loading, pasting and resizing (track); and
- cardholder data encrypted for transmission through your server (encrypt).
Gathering these values accurately, and ensuring they are made available to Ravelin through our API or by calls made directly from this SDK, is critical to a successful Ravelin integration.
Please feel welcome to create issues or submit pull requests on the project. The Contribution Guidelines detail how to write and test code for ravelinjs.
Note that this documentation is for version 1 of ravelinjs. For version 0, please see its usage guide, reference and source.
- Quickstart
- Bundles
- Content-Security-Policy
- Script Integrity
- Browser Compatibility
- Examples
- Reference
- Vendored Code
- Upgrading
Add https://*.ravelin.click
to your site's Content-Security-Policy
connect-src
directive. Get a copy of
ravelin-core+track+encrypt+promise.min.js on Github releases and
instantiate your Ravelin instance on the page:
<script src="ravelin-core+track+encrypt+promise.min.js"></script>
<script>var ravelin = new Ravelin({key: 'publishable_key_...'})</script>
If you have a build system, you can instead install ravelinjs with npm using
npm i ravelinjs@1
and require or import Ravelin for instantiating:import Ravelin from 'ravelinjs/core+track+encrypt+promise'; /* or */ const Ravelin = require('ravelinjs/core+track+encrypt+promise'); var ravelin = new Ravelin({key: 'publishable_key_...'});
This will set the ravelinDeviceId
cookie on your domain, send a page-load
event, and then allow you to call:
ravelin.core.id().then(function(id) { ... })
to get the deviceId.ravelin.encrypt.card({pan: "4111 ..."})
to encrypt cardholder data to be sent to Ravelin.ravelin.track.load()
to track a page load.
If you are wanting to track paste events then lastly add a data-rvn-pan
attribute to any inputs the user types a credit/debit card number into, and a
data-rvn-sensitive
to or around any elements you don't want Ravelin to report
any content from.
Read on for more details.
The quickstart suggests using ravelin-core+track+encrypt+promise.min.js
which
contains all functionality offered by ravelinjs and is therefore the easiest to
get started with but also the largest file. If you are not using all of the
functionality of ravelinjs you can choose a bundle with only the components you
need.
The components are:
- core: API and error-reporting functionality used by all bundles, and Basic
device identification with
ravelin.core.id()
or aravelinDeviceId
cookie. - encrypt: Cardholder data encryption with
ravelin.encrypt.card()
. - track: Automatically send page-load, resize and paste events, or manually
with
ravelin.track.load()
. - promise: Provide a fallback Promise polyfill required for Internet Explorer support. Optional if you already have your own polyfill or do not want to support any version of Internet Explorer.
The release files indicate which components they include using a
+component
naming convention. For example, ravelin-core+track.min.js
contains only the core and track components and so cannot be used to encrypt
cards and doesn't guarantee Internet Explorer compatibility.
If you have a JavaScript build system and would prefer to include ravelinjs using it, you can install ravelinjs from npm with:
npm install ravelinjs@1
You can then import the desired bundle within the ravelinjs library. For
example, to load the core+track bundle using require
is:
var Ravelin = require('ravelinjs/core+track');
Or to load card encryption with ES6 imports is:
import Ravelin from 'ravelinjs/core+encrypt';
The bundles published to npm are in Universal Module Definition format.
RavelinJS will send track events and error reports back to the Ravelin API as
configured in the api
initialisation property, or inferred from your API key.
If your site is configured with a Content-Security-Policy, be sure to add the
API to the connect-src
directive:
Content-Security-Policy: connect-src 'self' https://*.ravelin.click;
If you are including a ravelin bundle directly on your page, rather than in your
build system, we recommended setting the integrity
attribute on the script tag
to the corresponding value from the integrity file of the release. For example,
if the integrity file reads:
sha384-8de9e022e2f67e2072bb114e670d2fb37cab8eaf81616bcc3951087aa473e62a8b9fcc4c780a8d8d09df55c8b63bfd7c ravelin-1.0.0-rc1-core+promise.js
then your HTML becomes:
<script src="ravelin-1.0.0-rc1-core+promise.js" integrity="sha384-8de9e022e2f67e2072bb114e670d2fb37cab8eaf81616bcc3951087aa473e62a8b9fcc4c780a8d8d09df55c8b63bfd7c">
If the integrity file is next to the script in question, you can validate the contents using:
sed s/^sha384-// integrity | shasum -c
RavelinJS v1.0.0 is tested on IE8-11 and all newer browsers. We plan to drop support for IE8-IE10 soon, so please contact us if you still support these browsers.
A Promise/A+ polyfill is required for Internet Explorer support. If you do not have one, or are not sure, then use a +promise ravelinjs bundle.
Card encryption uses window.crypto where available, and otherwise falls back to a pseudo-random number generator which collects user movements and keypresses as a source of entropy. If insufficient events have been collected before encryption is attempted, an Error is thrown to prevent insecure transmission of cardholder data.
RavelinJS prefixes the deviceId with rjs-
by default. If you wish to use
something else, for example if you are upgrading from a previous
version and wish to maintain the opaque string format, simply specify your desired
prefix or omit entirely:
var rav = new Ravelin({key: 'publishable_key_...', prefix: ''})
or
var rav = new Ravelin({key: 'publishable_key_...', prefix: 'myid-'})
RavelinJS supports sending events to a user defined API base URL, useful for bypassing Ad-Blockers that rely on matching the ravelin.click domain. If you do this, there's no need to update your Content-Security-Policy.
Speak to your Ravelin Account Manager for help setting up a custom subdomain.
var rav = new Ravelin({key: 'publishable_key_...', api: 'https://foo.store.com'})
During your page load you need to instantiate your Ravelin
instance:
var rav = new Ravelin({
/**
* @prop {string} key The publishable key used to authenticate with the API.
*/
key: 'publishable_key_...',
/**
* @prop {string} [api] The base URL of the Ravelin API. Defaults to
* production, or another environment identified by the key. If you set a
* Content-Security-Policy then add the api to the connect-src directive.
*/
// api: 'https://live.ravelin.click/',
/**
* @prop {string} [prefix=rjs-] The prefix of the generated deviceId.
*/
// prefix: 'rjs-',
/**
* @prop {string|Promise<string>} [id] An explicit deviceId to use. If set,
* Ravelin won't attempt to maintain a deviceId of its own. However, if the
* given Promise errors or resolves to an empty value, we fall back to the
* built-in behaviour.
*/
// id: 'my-device-id',
// id: new Promise(r => r('my-device-id')),
/**
* @prop {string} [cookie=ravelinDeviceId] The cookie that the deviceId is
* persisted in.
*/
// cookie: 'my-guid',
/**
* @prop {number} [cookieExpiryDays] The number of days that a device ID will live.
* Defaults to 365 in accordance with the GDPR's ePrivacy Directive.
*/
// cookieExpiryDays: 365,
/**
* @prop {string} [cookieDomain] The top-most domain that we can store
* cookies on. If you expect your customer to navigate between multiple
* subdomains, e.g. catalog.store.com, checkout.store.com, then set
* cookieDomain to store.com.
*/
// cookieDomain: 'store.com',
/**
* @prop {PromiseConstructor} [Promise] An injectable Promise implementation
* to use. If not provided, defaults to window.Promise or a polyfill if the
* +promise component is included. Ravelin.Promise contains the default.
*/
// Promise: window.Promise,
/**
* @prop {string} [rsaKey] The public key used to encrypt cardholder data.
*/
// rsaKey: '0|...',
/**
* @prop {Object|Boolean} [page] Additional properties to describe the
* initial page load event. If false, suppresses the initial page-load event.
* Must be JSON-encodable.
*/
// page: {section: 'about'}
/**
* @prop {function} [classifyPaste] Override logic for detecting pasted PANs or sensitive values.
* @param {ClipboardEvent} e The paste event
* @returns {Object} c
* @returns {Boolean} [c.pan] Whether the user pasted into a PAN field. If omitted, we look for the data-rvn-pan attribute.
* @returns {Boolean} [c.sensitive] Whether the user pasted into a sensitive field. Prevents the pasted value's shape being shared with Ravelin if true. If omitted, we look for the data-rvn-sensitive attribute.
*/
// classifyPaste: e => ({
// pan: e.target.hasAttribute('data-rvn-pan'),
// sensitive: treeHasAttr(e.target, 'data-rvn-sensitive')
// })
});
ravelin.core.id
returns a Promise which resolves to the device ID
string. This will eventually match the ravelinDeviceId
cookie. Your goal is to
make a server-side API request to Ravelin where you send the customer's order
and device - using this deviceId - together in a v2/checkout
or [v2/order][postv2order] API request, or the customer and device in a
v2/connect API request.
HTML example:
<form action=pay>
<input type=hidden name=device-id id=rav-device-id>
</form>
<script src="ravelin-core.min.js">
<script>
var ravelin = new Ravelin({
key: 'publishable_key_...'
});
ravelin.core.id().then(function(deviceId) {
document.getElementById('rav-device-id').value = deviceId;
});
</script>
If you are using a modern bundler and transpiler you can declare:
const deviceId = await ravelin.core.id();
Server-side example:
var card = JSON.parse(form.getValue('card-cipher'));
var action = fetch('https://api.ravelin.com/v2/order?score=true', {
method: 'POST',
headers: {...},
body: JSON.stringify({
timestamp: (new Date).getTime(),
customerId: customerId,
order: {...},
device: {
deviceId: form.getValue('device-id'),
userAgent: req.header('User-Agent'),
ipAddress: req.ip, // X-Forwarded-For in Express JS.
language: req.header('Accept-Language'),
}
})
});
The device ID in the ravelinDeviceId
cookie or returned by ravelin.core.id()
should be treated as an opaque string. Do not attempt to parse or validate the
format of the ID as we may change it without warning in the future.
ravelin.encrypt.card
returns an object describing the encrypted form of
cardholder data for use with Ravelin's client-side
encryption.
This object can then be sent via your server to Ravelin without increasing the
scope of PCI compliance required of your server. The object can be used directly
as a paymentMethod in a v2/checkout,
v2/paymentmethod or v2/connect request,
for example.
Encrypting cardholder data is only necessary for non-PCI compliant merchants (PCI
SAQ-A or SAQ-AEP merchants) who are otherwise unable to provide cardholder data
(including a valid
instrumentId
)
to Ravelin when scoring an order.
The full set of fields are:
var cipher = ravelin.encrypt.card({
/** @prop {string} pan The full primary account number of the card. */
pan: '4111 1111 1111 1111',
/** @prop {string|number} year The expiry year on the card. 12 => 2012. */
year: '2020',
/** @prop {string|number} month The expiry month on the card. 1 => Jan. */
month: '1',
/** @prop {string} [nameOnCard] Optional cardholder name. */
nameOnCard: 'Tom Johnson'
/** @prop {string} [rsaKey] Optional RSA public key to use. Can be set during instantiation. */
// rsaKey: '0|...',
});
HTML example:
<form action=pay id=payment-form>
Card Number: <input name=pan>
Name: <input name=name>
Expiry Year: <input name=year>
Expiry Month: <input name=month>
<input type=hidden name=card-cipher>
</form>
<script src="ravelin-core+encrypt.min.js">
<script>
var ravelin = new Ravelin({
key: 'publishable_key_...',
rsaKey: '0|...'
});
var form = document.getElementById('payment-form');
form.onsubmit = function() {
// Encrypt the cardholder data in the form.
var cipher = ravelin.encrypt.card({
pan: form['pan'].value,
year: form['year'].value,
month: form['month'].value,
nameOnCard: form['name'].value
});
// Send the cipher to the server.
form['card-cipher'].value = JSON.stringify(cipher);
// Don't send the PAN to the server.
form['pan'].value = '';
};
</script>
Server-side usage example:
var card = JSON.parse(form.getValue('card-cipher'));
var action = fetch('https://api.ravelin.com/v2/checkout?score=true', {
method: 'POST',
headers: {...},
body: JSON.stringify({
timestamp: (new Date).getTime(),
customer: {...},
order: {...},
transaction: {...},
paymentMethod: card,
device: {...}
})
});
Note that browsers which do not support
window.crypto
(including IE8-IE10) rely on
a pseudo-random number generator based on collecting user events from the page
and that if this generator has not collected enough events it may throw an
exception when trying to encrypt.
Send a page-load event. This is automatically triggered when Ravelin is instantiated, but should be invoked manually after page navigation in a single-page app. To ensure the correct page title is collected, call after the page content has loaded - so the Window popstate event may be too early.
Send a named event to attach to the session, with optional descriptive properties. Most event names use "UPPER_SNAKE_CASE" but the most important thing is to have consistency between your browser and mobile applications where they have common events. Returns a Promise that resolves once the event has been sent.
Send a paste event to Ravelin. This is done automatically if the paste happens in the same frame Ravelin is instantiated - except on IE8 which does not support paste-event listening at the document level.
To correctly identify the paste contents you should annotate your forms with attributes:
data-rvn-pan
if the user enters a credit-card number into that input; ordata-rvn-sensitive
if no values should be shared in the event back to Ravelin.
Note: It is possible to override these attributes by providing custom
classifyPaste
logic in yourRavelin
instance. See Reference.
The paste event contains information about where the paste happened and
approximate shape of the paste content. For example, if a user pastes "h3ll0,
wor1d." into a field, Ravelin will receive "X0XX0, XXX0X.". However, if the
pasted content is an <input type=password>
, a <input data-rvn-sensitive>
or
a child of any <div data-rvn-sensitive>
(if using the default attributes) field we will not include any form of
pasted value - only that a paste event occurred.
This library would not have been possible without the stellar works upon which it relies:
Note that the format of the deviceId was changed in v1 to include a "rjs-" prefix. If you do any validation or parsing that checks for a particular format of the deviceId, please remove this logic and instead treat the deviceId as an opaque string.
If you are using RavelinJS v0 from a script or loaded via npm then equivalent functionality is now covered by bundles with the core+track+encrypt components. Please review which components you need in the bundles and complete the quickstart setup instructions. You can now remove cdn.ravelin.net from your Content-Security-Policy and make the following substitutions to complete the upgrade:
ravelinjs.setFallbackJS(src)
→ Removed.ravelinjs.setCookieDomain(domain)
→ Set during instantiation innew Ravelin({cookieDomain: 'c.com'})
.ravelinjs.setPublicAPIKey(apiKey)
→ Set during instantiation innew Ravelin({key: apiKey})
.ravelinjs.setRSAKey(rawPubKey)
→ Set during instantiation innew Ravelin({rsaKey: rawPubKey})
.ravelinjs.setCustomerId(customerId)
→ Removed.ravelinjs.setTempCustomer
→ Removed.ravelinjs.encrypt(card)
→JSON.stringify(ravelin.encrypt.card(card))
ravelinjs.encryptAsObject(card)
→ravelin.encrypt.card(card)
ravelinjs.track(eventName, meta)
→ Removed.ravelinjs.trackPage(meta)
→ravelin.track.load()
ravelinjs.trackLogout(meta)
→ Removed.ravelinjs.trackFingerprint(customer)
→ Removed. This method implemented some privacy-insensitive browser fingerprinting that Ravelin no longer wishes to be part of. Instead, follow the instructions ofravelin.core.id()
to send the device via your server.ravelinjs.setOrderId(orderId)
→ Removed.
If you previously used a snippet such as
(function(r,a,v,e,l,i,n){r[l]=r[l]||function(){(r[l].q=r[l].q||[]).push(arguments)};i=a.createElement(v);i.async=i.defer=1;i.src=e;a.body.appendChild(i)})(window,document,'script','https://cdn.ravelin.net/ravelin-beta.min.js','ravelin');
or
(function r(a,v,e,l,i,n){a[e]=a[e]||function(){(a[e].q=a[e].q||[]).push(arguments)};n=v.createElement("script");n.async=n.defer=1;n.src=l;if(i)n.onerror=function(){r(a,v,e,i)};v.body.appendChild(n)})(window,document,"ravelin","https://cdn.ravelin.net/js/rvn-beta.min.js","/rvn-lite.min.js")
then the functionality you were using is covered by bundles with the core+track components. After following the quickstart instructions you can remove cdn.ravelin.net from your Content-Security-Policy, and make the following substitutions to complete the upgrade:
ravelin('setApiKey', 'k')
→ Set during instantiation innew Ravelin({key: 'k'})
.ravelin('setCookieDomain', 'c')
→ Set during instantiation innew Ravelin({cookieDomain: 'c.com'})
.ravelin('track')
→ Removed.ravelin('trackPage')
→ravelin.track.load()
is now called when Ravelin is instantiated, but you can call this method again when the user navigates if you have a single-page application.ravelin('trackLogin')
→ Removed.ravelin('trackLogout')
→ Removed.ravelin('fingerprint')
→ Removed. This method implemented some privacy-insensitive browser fingerprinting that Ravelin no longer wishes to be part of. Instead, follow the instructions ofravelin.core.id()
to send the device via your server.ravelin('send')
→ Removedravelin('setCustomerId')
→ Removed.ravelin('setTempCustomerId')
→ Removed.ravelin('setOrderId')
→ Removed.