diff --git a/src/backends/backend.js b/src/backends/backend.js
index db6f5388..bde9ec1e 100644
--- a/src/backends/backend.js
+++ b/src/backends/backend.js
@@ -18,6 +18,7 @@ import { sanitizeSlug } from "Lib/urlHelper";
import TestRepoBackend from "./test-repo/implementation";
import GitHubBackend from "./github/implementation";
import GitLabBackend from "./gitlab/implementation";
+import BitBucketBackend from "./bitbucket/implementation";
import GitGatewayBackend from "./git-gateway/implementation";
import { registerBackend, getBackend } from 'Lib/registry';
import Cursor, { CURSOR_COMPATIBILITY_SYMBOL } from '../valueObjects/Cursor';
@@ -28,6 +29,7 @@ import Cursor, { CURSOR_COMPATIBILITY_SYMBOL } from '../valueObjects/Cursor';
registerBackend('git-gateway', GitGatewayBackend);
registerBackend('github', GitHubBackend);
registerBackend('gitlab', GitLabBackend);
+registerBackend('bitbucket', BitBucketBackend);
registerBackend('test-repo', TestRepoBackend);
@@ -124,8 +126,8 @@ const sortByScore = (a, b) => {
};
class Backend {
- constructor(implementation, backendName, authStore = null) {
- this.implementation = implementation;
+ constructor(implementation, { authStore = null, backendName, config } = {}) {
+ this.implementation = implementation.init(config, { updateUserCredentials: this.updateUserCredentials });
this.backendName = backendName;
this.authStore = authStore;
if (this.implementation === null) {
@@ -147,6 +149,15 @@ class Backend {
return Promise.resolve(null);
}
+ updateUserCredentials = updatedCredentials => {
+ const storedUser = this.authStore && this.authStore.retrieve();
+ if (storedUser && storedUser.backendName === this.backendName) {
+ const newUser = { ...storedUser, ...updatedCredentials };
+ this.authStore.store(newUser);
+ return newUser;
+ }
+ };
+
authComponent() {
return this.implementation.authComponent();
}
@@ -476,7 +487,7 @@ export function resolveBackend(config) {
if (!getBackend(name)) {
throw new Error(`Backend not found: ${ name }`);
} else {
- return new Backend(getBackend(name).init(config), name, authStore);
+ return new Backend(getBackend(name), { backendName: name, authStore, config });
}
}
diff --git a/src/backends/bitbucket/API.js b/src/backends/bitbucket/API.js
new file mode 100644
index 00000000..ccbb3541
--- /dev/null
+++ b/src/backends/bitbucket/API.js
@@ -0,0 +1,149 @@
+import { flow } from "lodash";
+import LocalForage from "Lib/LocalForage";
+import unsentRequest from "Lib/unsentRequest";
+import { responseParser } from "Lib/backendHelper";
+import { then } from "Lib/promiseHelper";
+import { basename } from "Lib/pathHelper";
+import AssetProxy from "ValueObjects/AssetProxy";
+import Cursor from "ValueObjects/Cursor";
+import { APIError } from "ValueObjects/errors";
+
+export default class API {
+ constructor(config) {
+ this.api_root = config.api_root || "https://api.bitbucket.org/2.0";
+ this.branch = config.branch || "master";
+ this.repo = config.repo || "";
+ this.requestFunction = config.requestFunction || unsentRequest.performRequest;
+ this.repoURL = `/repositories/${ this.repo }`;
+ this.repoURL = this.repo ? `/repositories/${ this.repo }` : "";
+ }
+
+ buildRequest = req => flow([
+ unsentRequest.withRoot(this.api_root),
+ unsentRequest.withTimestamp,
+ ])(req);
+
+ request = req => flow([
+ this.buildRequest,
+ this.requestFunction,
+ p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
+ ])(req);
+
+ requestJSON = req => flow([
+ unsentRequest.withDefaultHeaders({ "Content-Type": "application/json" }),
+ this.request,
+ then(responseParser({ format: "json" })),
+ p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
+ ])(req);
+ requestText = req => flow([
+ unsentRequest.withDefaultHeaders({ "Content-Type": "text/plain" }),
+ this.request,
+ then(responseParser({ format: "text" })),
+ p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
+ ])(req);
+
+ user = () => this.request("/user");
+ hasWriteAccess = user => this.request(this.repoURL).then(res => res.ok);
+
+ isFile = ({ type }) => type === "commit_file";
+ processFile = file => ({
+ ...file,
+ name: basename(file.path),
+ download_url: file.links.self.href,
+
+ // BitBucket does not return file SHAs, but it does give us the
+ // commit SHA. Since the commit SHA will change if any files do,
+ // we can construct an ID using the commit SHA and the file path
+ // that will help with caching (though not as well as a normal
+ // SHA, since it will change even if the individual file itself
+ // doesn't.)
+ ...(file.commit && file.commit.hash
+ ? { id: `${ file.commit.hash }/${ file.path }` }
+ : {}),
+ });
+ processFiles = files => files.filter(this.isFile).map(this.processFile);
+
+ readFile = async (path, sha, { ref = this.branch, parseText = true } = {}) => {
+ const cacheKey = parseText ? `bb.${ sha }` : `bb.${ sha }.blob`;
+ const cachedFile = sha ? await LocalForage.getItem(cacheKey) : null;
+ if (cachedFile) { return cachedFile; }
+ const result = await this.request({
+ url: `${ this.repoURL }/src/${ ref }/${ path }`,
+ cache: "no-store",
+ }).then(parseText ? responseParser({ format: "text" }) : responseParser({ format: "blob" }));
+ if (sha) { LocalForage.setItem(cacheKey, result); }
+ return result;
+ }
+
+ getEntriesAndCursor = jsonResponse => {
+ const { size: count, page: index, pagelen: pageSize, next, previous: prev, values: entries } = jsonResponse;
+ const pageCount = (pageSize && count) ? Math.ceil(count / pageSize) : undefined;
+ return {
+ entries,
+ cursor: Cursor.create({
+ actions: [...(next ? ["next"] : []), ...(prev ? ["prev"] : [])],
+ meta: { index, count, pageSize, pageCount },
+ data: { links: { next, prev } },
+ }),
+ };
+ }
+
+ listFiles = async path => {
+ const { entries, cursor } = await flow([
+ // sort files by filename ascending
+ unsentRequest.withParams({ sort: "-path" }),
+ this.requestJSON,
+ then(this.getEntriesAndCursor),
+ ])(`${ this.repoURL }/src/${ this.branch }/${ path }`);
+ return { entries: this.processFiles(entries), cursor };
+ }
+
+ traverseCursor = async (cursor, action) => flow([
+ this.requestJSON,
+ then(this.getEntriesAndCursor),
+ then(({ cursor: newCursor, entries }) => ({ cursor: newCursor, entries: this.processFiles(entries) })),
+ ])(cursor.data.getIn(["links", action]));
+
+ listAllFiles = async path => {
+ const { cursor: initialCursor, entries: initialEntries } = await this.listFiles(path);
+ const entries = [...initialEntries];
+ let currentCursor = initialCursor;
+ while (currentCursor && currentCursor.actions.has("next")) {
+ const { cursor: newCursor, entries: newEntries } = await this.traverseCursor(currentCursor, "next");
+ entries.push(...newEntries);
+ currentCursor = newCursor;
+ }
+ return this.processFiles(entries);
+ };
+
+ uploadBlob = async item => {
+ const contentBase64 = await (item instanceof AssetProxy ? item.toBase64() : Promise.resolve(item.raw));
+ const formData = new FormData();
+ formData.append(item.path, contentBase64);
+
+ return flow([
+ unsentRequest.withMethod("POST"),
+ unsentRequest.withBody(formData),
+ this.request,
+ then(() => ({ ...item, uploaded: true })),
+ ])(`${ this.repoURL }/src`);
+ };
+
+ persistFiles = (files, { commitMessage, newEntry }) => Promise.all(
+ files.filter(({ uploaded }) => !uploaded).map(this.uploadBlob)
+ );
+
+ deleteFile = (path, message, options={}) => {
+ const branch = options.branch || this.branch;
+ const body = new FormData();
+ body.append('files', path);
+ if (message && message !== "") {
+ body.append("message", message);
+ }
+ return flow([
+ unsentRequest.withMethod("POST"),
+ unsentRequest.withBody(body),
+ this.request,
+ ])(`${ this.repoURL }/src`);
+ };
+}
diff --git a/src/backends/bitbucket/AuthenticationPage.js b/src/backends/bitbucket/AuthenticationPage.js
new file mode 100644
index 00000000..bce564a4
--- /dev/null
+++ b/src/backends/bitbucket/AuthenticationPage.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Authenticator from 'Lib/netlify-auth';
+import { Icon } from 'UI';
+
+export default class AuthenticationPage extends React.Component {
+ static propTypes = {
+ onLogin: PropTypes.func.isRequired,
+ inProgress: PropTypes.bool,
+ };
+
+ state = {};
+
+ handleLogin = (e) => {
+ e.preventDefault();
+ const cfg = {
+ base_url: this.props.base_url,
+ site_id: (document.location.host.split(':')[0] === 'localhost') ? 'cms.netlify.com' : this.props.site_id,
+ auth_endpoint: this.props.authEndpoint,
+ };
+ const auth = new Authenticator(cfg);
+
+ auth.authenticate({ provider: 'bitbucket', scope: 'repo' }, (err, data) => {
+ if (err) {
+ this.setState({ loginError: err.toString() });
+ return;
+ }
+ this.props.onLogin(data);
+ });
+ };
+
+ render() {
+ const { loginError } = this.state;
+ const { inProgress } = this.props;
+
+ return (
+
+
+ {loginError && {loginError}
}
+
+
+ );
+ }
+}
diff --git a/src/backends/bitbucket/implementation.js b/src/backends/bitbucket/implementation.js
new file mode 100644
index 00000000..6933dbab
--- /dev/null
+++ b/src/backends/bitbucket/implementation.js
@@ -0,0 +1,222 @@
+import semaphore from "semaphore";
+import { flow, trimStart } from "lodash";
+import { EDITORIAL_WORKFLOW } from "Constants/publishModes";
+import { CURSOR_COMPATIBILITY_SYMBOL } from "ValueObjects/Cursor";
+import { filterByPropExtension } from "Lib/backendHelper";
+import { resolvePromiseProperties, then } from "Lib/promiseHelper";
+import unsentRequest from "Lib/unsentRequest";
+import AuthenticationPage from "./AuthenticationPage";
+import Authenticator from 'Lib/netlify-auth';
+import API from "./API";
+
+const MAX_CONCURRENT_DOWNLOADS = 10;
+
+// Implementation wrapper class
+export default class Bitbucket {
+ constructor(config, options={}) {
+ this.config = config;
+ this.options = {
+ proxied: false,
+ API: null,
+ updateUserCredentials: async () => null,
+ ...options,
+ };
+
+ if (config.getIn(["publish_mode"]) === EDITORIAL_WORKFLOW) {
+ throw new Error("The BitBucket backend does not support the Editorial Workflow.");
+ }
+
+ if (!this.options.proxied && !config.getIn(["backend", "repo"], false)) {
+ throw new Error("The BitBucket backend needs a \"repo\ in the backend configuration.");
+ }
+
+ this.api = this.options.API || null;
+
+ this.updateUserCredentials = this.options.updateUserCredentials;
+
+ this.repo = config.getIn(["backend", "repo"], "");
+ this.branch = config.getIn(["backend", "branch"], "master");
+ this.api_root = config.getIn(["backend", "api_root"], "https://api.bitbucket.org/2.0");
+ this.base_url = config.get("base_url");
+ this.site_id = config.get("site_id");
+ this.token = "";
+ }
+
+ authComponent() {
+ return AuthenticationPage;
+ }
+
+ setUser(user) {
+ this.token = user.token;
+ this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo });
+ }
+
+ restoreUser(user) {
+ return this.authenticate(user);
+ }
+
+ authenticate(state) {
+ this.token = state.token;
+ this.refreshToken = state.refresh_token;
+ this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo, api_root: this.api_root });
+
+ return this.api.user().then(user =>
+ this.api.hasWriteAccess(user).then(isCollab => {
+ if (!isCollab) throw new Error("Your BitBucker user account does not have access to this repo.");
+ return Object.assign({}, user, { token: state.token, refresh_token: state.refresh_token });
+ })
+ );
+ }
+
+ getRefreshedAccessToken() {
+ if (this.refreshedTokenPromise) {
+ return this.refreshedTokenPromise;
+ }
+
+ // instantiating a new Authenticator on each refresh isn't ideal,
+ if (!this.auth) {
+ const cfg = {
+ base_url: this.base_url,
+ site_id: this.site_id,
+ };
+ this.authenticator = new Authenticator(cfg);
+ }
+
+ this.refreshedTokenPromise = this.authenticator.refresh({ provider: "bitbucket", refresh_token: this.refreshToken })
+ .then(({ token, refresh_token }) => {
+ this.token = token;
+ this.refreshToken = refresh_token;
+ this.refreshedTokenPromise = undefined;
+ this.updateUserCredentials({ token, refresh_token });
+ return token;
+ });
+
+ return this.refreshedTokenPromise;
+ }
+
+ logout() {
+ this.token = null;
+ return;
+ }
+
+ getToken() {
+ if (this.refreshedTokenPromise) {
+ return this.refreshedTokenPromise;
+ }
+
+ return Promise.resolve(this.token);
+ }
+
+ apiRequestFunction = async req => {
+ const token = this.refreshedTokenPromise ? await this.refreshedTokenPromise : this.token;
+ return flow([
+ unsentRequest.withHeaders({ Authorization: `Bearer ${ token }` }),
+ unsentRequest.performRequest,
+ then(async res => {
+ if (res.status === 401) {
+ const json = (await res.json().catch(() => null));
+ if (json && json.type === "error" && /^access token expired/i.test(json.error.message)) {
+ const newToken = await this.getRefreshedAccessToken();
+ const reqWithNewToken = unsentRequest.withHeaders({ Authorization: `Bearer ${ newToken }` }, req);
+ return unsentRequest.performRequest(reqWithNewToken);
+ }
+ }
+ return res;
+ }),
+ ])(req);
+ };
+
+ entriesByFolder(collection, extension) {
+ const listPromise = this.api.listFiles(collection.get("folder"));
+ return resolvePromiseProperties({
+ files: listPromise
+ .then(({ entries }) => entries)
+ .then(filterByPropExtension(extension, "path"))
+ .then(this.fetchFiles),
+ cursor: listPromise.then(({ cursor }) => cursor),
+ }).then(({ files, cursor }) => {
+ files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
+ return files;
+ });
+ }
+
+ allEntriesByFolder(collection, extension) {
+ return this.api.listAllFiles(collection.get("folder"))
+ .then(filterByPropExtension(extension, "path"))
+ .then(this.fetchFiles);
+ }
+
+ entriesByFiles(collection) {
+ const files = collection.get("files").map(collectionFile => ({
+ path: collectionFile.get("file"),
+ label: collectionFile.get("label"),
+ }));
+ return this.fetchFiles(files);
+ }
+
+ fetchFiles = (files) => {
+ const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
+ const promises = [];
+ files.forEach((file) => {
+ promises.push(new Promise((resolve, reject) => (
+ sem.take(() => this.api.readFile(file.path, file.id).then((data) => {
+ resolve({ file, data });
+ sem.leave();
+ }).catch((error = true) => {
+ sem.leave();
+ console.error(`failed to load file from BitBucket: ${ file.path }`);
+ resolve({ error });
+ }))
+ )));
+ });
+ return Promise.all(promises)
+ .then(loadedEntries => loadedEntries.filter(loadedEntry => !loadedEntry.error));
+ }
+
+ getEntry(collection, slug, path) {
+ return this.api.readFile(path).then(data => ({
+ file: { path },
+ data,
+ }));
+ }
+
+ getMedia() {
+ const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
+
+ return this.api.listAllFiles(this.config.get("media_folder"))
+ .then(files => files.map(({ id, name, download_url, path }) => {
+ const getBlobPromise = () => new Promise((resolve, reject) =>
+ sem.take(() =>
+ this.api.readFile(path, id, { parseText: false })
+ .then(resolve, reject)
+ .finally(() => sem.leave())
+ )
+ );
+
+ return { id, name, getBlobPromise, url: download_url, path };
+ }));
+ }
+
+ persistEntry(entry, mediaFiles, options = {}) {
+ return this.api.persistFiles([entry], options);
+ }
+
+ async persistMedia(mediaFile, options = {}) {
+ await this.api.persistFiles([mediaFile], options);
+ const { value, path, fileObj } = mediaFile;
+ const getBlobPromise = () => Promise.resolve(fileObj);
+ return { name: value, size: fileObj.size, getBlobPromise, path: trimStart(path, '/k') };
+ }
+
+ deleteFile(path, commitMessage, options) {
+ return this.api.deleteFile(path, commitMessage, options);
+ }
+
+ traverseCursor(cursor, action) {
+ return this.api.traverseCursor(cursor, action)
+ .then(async ({ entries, cursor: newCursor }) => ({
+ entries: await Promise.all(entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data })))),
+ cursor: newCursor,
+ }));
+ }
+}
diff --git a/src/components/UI/Icon/images/_index.js b/src/components/UI/Icon/images/_index.js
index 352280f4..8d46b6ec 100644
--- a/src/components/UI/Icon/images/_index.js
+++ b/src/components/UI/Icon/images/_index.js
@@ -1,6 +1,7 @@
import iconAdd from './add.svg';
import iconAddWith from './add-with.svg';
import iconArrow from './arrow.svg';
+import iconBitbucket from './bitbucket.svg';
import iconBold from './bold.svg';
import iconCheck from './check.svg';
import iconChevron from './chevron.svg';
@@ -44,6 +45,7 @@ const images = {
'add': iconAdd,
'add-with': iconAddWith,
'arrow': iconArrow,
+ 'bitbucket': iconBitbucket,
'bold': iconBold,
'check': iconCheck,
'chevron': iconChevron,
diff --git a/src/components/UI/Icon/images/bitbucket.svg b/src/components/UI/Icon/images/bitbucket.svg
new file mode 100644
index 00000000..cd8a5d76
--- /dev/null
+++ b/src/components/UI/Icon/images/bitbucket.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/lib/backendHelper.js b/src/lib/backendHelper.js
new file mode 100644
index 00000000..5d69a1a2
--- /dev/null
+++ b/src/lib/backendHelper.js
@@ -0,0 +1,44 @@
+import { get } from "lodash";
+import { fromJS } from "immutable";
+import { fileExtension } from "Lib/pathHelper";
+import unsentRequest from "Lib/unsentRequest";
+import { CURSOR_COMPATIBILITY_SYMBOL } from "../valueObjects/Cursor";
+
+export const filterByPropExtension = (extension, propName) => arr =>
+ arr.filter(el => fileExtension(get(el, propName)) === extension);
+
+const catchFormatErrors = (format, formatter) => res => {
+ try {
+ return formatter(res);
+ } catch (err) {
+ throw new Error(`Response cannot be parsed into the expected format (${ format }): ${ err.message }`);
+ }
+};
+
+const responseFormatters = fromJS({
+ json: async res => {
+ const contentType = res.headers.get("Content-Type");
+ if (!contentType.startsWith("application/json") && !contentType.startsWith("text/json")) {
+ throw new Error(`${ contentType } is not a valid JSON Content-Type`);
+ }
+ return res.json();
+ },
+ text: async res => res.text(),
+ blob: async res => res.blob(),
+}).mapEntries(
+ ([format, formatter]) => [format, catchFormatErrors(format, formatter)]
+);
+
+export const parseResponse = async (res, { expectingOk = true, format = "text" } = {}) => {
+ if (expectingOk && !res.ok) {
+ throw new Error(`Expected an ok response, but received an error status: ${ res.status }.`);
+ }
+ const formatter = responseFormatters.get(format, false);
+ if (!formatter) {
+ throw new Error(`${ format } is not a supported response format.`);
+ }
+ const body = await formatter(res);
+ return body;
+};
+
+export const responseParser = options => res => parseResponse(res, options);
diff --git a/src/lib/netlify-auth.js b/src/lib/netlify-auth.js
index 3d766f44..4d43a5ee 100644
--- a/src/lib/netlify-auth.js
+++ b/src/lib/netlify-auth.js
@@ -51,17 +51,17 @@ class Authenticator {
authorizeCallback(options, cb) {
const fn = (e) => {
- var data, err;
if (e.origin !== this.base_url) { return; }
+
if (e.data.indexOf('authorization:' + options.provider + ':success:') === 0) {
- data = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':success:(.+)$'))[1]);
+ const data = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':success:(.+)$'))[1]);
window.removeEventListener('message', fn, false);
this.authWindow.close();
cb(null, data);
}
if (e.data.indexOf('authorization:' + options.provider + ':error:') === 0) {
console.log('Got authorization error');
- err = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':error:(.+)$'))[1]);
+ const err = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':error:(.+)$'))[1]);
window.removeEventListener('message', fn, false);
this.authWindow.close();
cb(new NetlifyError(err));
@@ -75,29 +75,29 @@ class Authenticator {
return this.site_id;
}
const host = document.location.host.split(':')[0];
- return host === 'localhost' ? null : host;
+ return host === 'localhost' ? 'cms.netlify.com' : host;
}
authenticate(options, cb) {
- var left, top, url,
- siteID = this.getSiteID(),
- provider = options.provider;
+ const { provider } = options;
+ const siteID = this.getSiteID();
+
if (!provider) {
return cb(new NetlifyError({
- message: 'You must specify a provider when calling netlify.authenticate'
+ message: 'You must specify a provider when calling netlify.authenticate',
}));
}
if (!siteID) {
return cb(new NetlifyError({
- message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make authentication work from localhost'
+ message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make authentication work from localhost',
}));
}
const conf = PROVIDERS[provider] || PROVIDERS.github;
- left = (screen.width / 2) - (conf.width / 2);
- top = (screen.height / 2) - (conf.height / 2);
+ const left = (screen.width / 2) - (conf.width / 2);
+ const top = (screen.height / 2) - (conf.height / 2);
window.addEventListener('message', this.handshakeCallback(options, cb), false);
- url = `${this.base_url}/${this.auth_endpoint}?provider=${options.provider}&site_id=${siteID}`;
+ let url = `${ this.base_url }/${ this.auth_endpoint }?provider=${ options.provider }&site_id=${ siteID }`;
if (options.scope) {
url += '&scope=' + options.scope;
}
@@ -118,6 +118,33 @@ class Authenticator {
);
this.authWindow.focus();
}
+
+ refresh(options, cb) {
+ const { provider, refresh_token } = options;
+ const siteID = this.getSiteID();
+ const onError = cb || Promise.reject.bind(Promise);
+
+ if (!provider || !refresh_token) {
+ return onError(new NetlifyError({
+ message: 'You must specify a provider and refresh token when calling netlify.refresh',
+ }));
+ }
+ if (!siteID) {
+ return onError(new NetlifyError({
+ message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make token refresh work from localhost',
+ }));
+ }
+ const url = `${ this.base_url }/${ this.auth_endpoint }/refresh?provider=${ provider }&site_id=${ siteID }&refresh_token=${ refresh_token }`;
+ const refreshPromise = fetch(url, { method: "POST", body: "" }).then(res => res.json());
+
+ // Return a promise if a callback wasn't provided
+ if (!cb) {
+ return refreshPromise;
+ }
+
+ // Otherwise, use the provided callback.
+ refreshPromise.then(data => cb(null, data)).catch(cb);
+ }
}
export default Authenticator;
diff --git a/src/lib/registry.js b/src/lib/registry.js
index 6679636d..ad998e0f 100644
--- a/src/lib/registry.js
+++ b/src/lib/registry.js
@@ -104,7 +104,7 @@ export function registerBackend(name, BackendClass) {
console.error(`Backend [${ name }] already registered. Please choose a different name.`);
} else {
registry.backends[name] = {
- init: config => new BackendClass(config),
+ init: (...args) => new BackendClass(...args),
};
}
}
diff --git a/src/lib/unsentRequest.js b/src/lib/unsentRequest.js
index 19f3cec7..2abffba2 100644
--- a/src/lib/unsentRequest.js
+++ b/src/lib/unsentRequest.js
@@ -44,7 +44,7 @@ const getPropMergeFunctions = path => [
];
const [withMethod, withDefaultMethod] = getPropSetFunctions(["method"]);
-const [withBody, withDefaultBody] = getPropSetFunctions(["method"]);
+const [withBody, withDefaultBody] = getPropSetFunctions(["body"]);
const [withParams, withDefaultParams] = getPropMergeFunctions(["params"]);
const [withHeaders, withDefaultHeaders] = getPropMergeFunctions(["headers"]);