Merge mozilla-central into inbound
authorGregory Szorc <gps@mozilla.com>
Wed, 11 Apr 2012 18:51:10 -0700
changeset 91492 b774546790d72c97debc66afc69ebd9f8a6bcfa9
parent 91491 bfa27d58769d5c1d83a2b6d3f6e52d2e98443884 (current diff)
parent 91388 1711e06ca9f7b8cba7ecb27a48b9fd8ad1980a26 (diff)
child 91493 54f5a1ebcf4315b23defb6bf3d6e914e971e21a5
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
milestone14.0a1
Merge mozilla-central into inbound
dom/wifi/WifiWorker.js
mobile/android/base/db/BrowserProvider.java.in
services/sync/modules/async.js
services/sync/modules/ext/Observers.js
services/sync/modules/ext/Preferences.js
services/sync/modules/ext/StringBundle.js
services/sync/modules/log4moz.js
services/sync/tests/unit/test_Observers.js
services/sync/tests/unit/test_Preferences.js
services/sync/tests/unit/test_async_chain.js
services/sync/tests/unit/test_async_querySpinningly.js
services/sync/tests/unit/test_log4moz.js
services/sync/tests/unit/test_restrequest.js
services/sync/tests/unit/test_utils_atob.js
services/sync/tests/unit/test_utils_makeURI.js
services/sync/tests/unit/test_utils_namedTimer.js
services/sync/tests/unit/test_utils_stackTrace.js
services/sync/tests/unit/test_utils_utf8.js
--- a/browser/base/content/sync/notification.xml
+++ b/browser/base/content/sync/notification.xml
@@ -52,27 +52,27 @@
         <children includes="notification"/>
       </xul:vbox>
       <children/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
         let temp = {};
-        Cu.import("resource://services-sync/ext/Observers.js", temp);
+        Cu.import("resource://services-common/observers.js", temp);
         temp.Observers.add("weave:notification:added", this.onNotificationAdded, this);
         temp.Observers.add("weave:notification:removed", this.onNotificationRemoved, this);
 
         for each (var notification in Weave.Notifications.notifications)
           this._appendNotification(notification);
       ]]></constructor>
 
       <destructor><![CDATA[
         let temp = {};
-        Cu.import("resource://services-sync/ext/Observers.js", temp);
+        Cu.import("resource://services-common/observers.js", temp);
         temp.Observers.remove("weave:notification:added", this.onNotificationAdded, this);
         temp.Observers.remove("weave:notification:removed", this.onNotificationRemoved, this);
       ]]></destructor>
 
       <method name="onNotificationAdded">
         <parameter name="subject"/>
         <parameter name="data"/>
         <body><![CDATA[
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -1026,16 +1026,23 @@ xpicleanup@BIN_SUFFIX@
   modules/PlacesUIUtils.jsm
   modules/PlacesUtils.jsm
   modules/PluginProvider.jsm
   modules/PluralForm.jsm
   modules/PopupNotifications.jsm
   modules/PropertyPanel.jsm
   modules/reflect.jsm
   modules/Services.jsm
+  modules/services-common/async.js
+  modules/services-common/log4moz.js
+  modules/services-common/observers.js
+  modules/services-common/preferences.js
+  modules/services-common/rest.js
+  modules/services-common/stringbundle.js
+  modules/services-common/utils.js
   modules/services-sync/auth.js
   modules/services-sync/base_records/collection.js
   modules/services-sync/base_records/crypto.js
   modules/services-sync/base_records/keys.js
   modules/services-sync/base_records/wbo.js
   modules/services-sync/constants.js
   modules/services-sync/engines/bookmarks.js
   modules/services-sync/engines/clients.js
--- a/content/base/src/nsDOMMutationObserver.cpp
+++ b/content/base/src/nsDOMMutationObserver.cpp
@@ -580,16 +580,46 @@ nsDOMMutationObserver::Disconnect()
     mReceivers[i]->Disconnect(false);
   }
   mReceivers.Clear();
   mCurrentMutations.Clear();
   mPendingMutations.Clear();
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDOMMutationObserver::TakeRecords(nsIVariant** aRetVal)
+{
+  *aRetVal = TakeRecords().get();
+  return NS_OK;
+}
+
+already_AddRefed<nsIVariant>
+nsDOMMutationObserver::TakeRecords()
+{
+  nsCOMPtr<nsIWritableVariant> mutations =
+    do_CreateInstance("@mozilla.org/variant;1");
+  PRInt32 len = mPendingMutations.Count();
+  if (len == 0) {
+    mutations->SetAsEmptyArray();
+  } else {
+    nsTArray<nsIDOMMutationRecord*> mods(len);
+    for (PRInt32 i = 0; i < len; ++i) {
+      mods.AppendElement(mPendingMutations[i]);
+    }
+
+    mutations->SetAsArray(nsIDataType::VTYPE_INTERFACE,
+                          &NS_GET_IID(nsIDOMMutationRecord),
+                          mods.Length(),
+                          const_cast<void*>(
+                            static_cast<const void*>(mods.Elements())));
+    mPendingMutations.Clear();
+  }
+  return mutations.forget();
+}
 
 NS_IMETHODIMP
 nsDOMMutationObserver::Initialize(nsISupports* aOwner, JSContext* cx,
                                   JSObject* obj, PRUint32 argc, jsval* argv)
 {
   mOwner = do_QueryInterface(aOwner);
   if (!mOwner) {
     NS_WARNING("Unexpected nsIJSNativeInitializer owner");
@@ -634,31 +664,18 @@ nsDOMMutationObserver::HandleMutation()
     return;
   }
   nsCxPusher pusher;
   nsCOMPtr<nsIDOMEventTarget> et = do_QueryInterface(mOwner);
   if (!mCallback || !pusher.Push(et)) {
     mPendingMutations.Clear();
     return;
   }
-  
-  PRInt32 len = mPendingMutations.Count();
-  nsTArray<nsIDOMMutationRecord*> mods(len);
-  for (PRInt32 i = 0; i < len; ++i) {
-    mods.AppendElement(mPendingMutations[i]);
-  }
-  
-  nsCOMPtr<nsIWritableVariant> mutations =
-    do_CreateInstance("@mozilla.org/variant;1");
-  mutations->SetAsArray(nsIDataType::VTYPE_INTERFACE,
-                        &NS_GET_IID(nsIDOMMutationRecord),
-                        mods.Length(),
-                        const_cast<void*>(
-                          static_cast<const void*>(mods.Elements())));
-  mPendingMutations.Clear();
+
+  nsCOMPtr<nsIVariant> mutations = TakeRecords();
   nsAutoMicroTask mt;
   sCurrentObserver = this; // For 'this' handling.
   mCallback->HandleMutations(mutations, this);
   sCurrentObserver = nsnull;
 }
 
 class AsyncMutationHandler : public nsRunnable
 {
--- a/content/base/src/nsDOMMutationObserver.h
+++ b/content/base/src/nsDOMMutationObserver.h
@@ -315,16 +315,18 @@ public:
 
   static void Shutdown();
 protected:
   friend class nsMutationReceiver;
   friend class nsAutoMutationBatch;
   nsMutationReceiver* GetReceiverFor(nsINode* aNode, bool aMayCreate);
   void RemoveReceiver(nsMutationReceiver* aReceiver);
 
+  already_AddRefed<nsIVariant> TakeRecords();
+
   void GetAllSubtreeObserversFor(nsINode* aNode,
                                  nsTArray<nsMutationReceiver*>& aObservers);
   void ScheduleForRun();
   void RescheduleForRun();
 
   nsDOMMutationRecord* CurrentRecord(const nsAString& aType);
   bool HasCurrentRecord(const nsAString& aType);
 
--- a/content/base/test/test_mutationobservers.html
+++ b/content/base/test/test_mutationobservers.html
@@ -472,17 +472,55 @@ function testModalDialog() {
       observer.disconnect();
       m = null;
       didHandleCallback = true;
     });
   m.observe(div, { childList: true });
   div.innerHTML = "<span><span>foo</span></span>";
   window.showModalDialog("mutationobserver_dialog.html");
   ok(didHandleCallback, "Should have called the callback while showing modal dialog!");
-  then();
+  then(testTakeRecords);
+}
+
+function testTakeRecords() {
+  var s = "<span>1</span><span>2</span>";
+  div.innerHTML = s;
+  var takenRecords;
+  m = new M(function(records, observer) {
+      is(records.length, 3, "Should have got 3 records");
+
+      is(records[0].type, "attributes", "Should have got attributes");
+      is(records[0].attributeName, "foo", "");
+      is(records[1].type, "childList", "Should have got childList");
+      is(records[1].removedNodes.length, 2, "Should have got removedNodes");
+      is(records[1].addedNodes.length, 2, "Should have got addedNodes");
+      is(records[2].type, "attributes", "Should have got attributes");
+      is(records[2].attributeName, "foo", "");
+      
+      is(records.length, takenRecords.length, "Should have had similar mutations");
+      is(records[0].type, takenRecords[0].type, "Should have had similar mutations");
+      is(records[1].type, takenRecords[1].type, "Should have had similar mutations");
+      is(records[2].type, takenRecords[2].type, "Should have had similar mutations");
+      
+      is(records[1].removedNodes.length, takenRecords[1].removedNodes.length, "Should have had similar mutations");
+      is(records[1].addedNodes.length, takenRecords[1].addedNodes.length, "Should have had similar mutations");
+
+      is(m.takeRecords().length, 0, "Shouldn't have any records");
+      observer.disconnect();
+      then();
+      m = null;
+    });
+  m.observe(div, { childList: true, attributes: true });
+  div.setAttribute("foo", "bar");
+  div.innerHTML = s;
+  div.removeAttribute("foo");
+  takenRecords = m.takeRecords();
+  div.setAttribute("foo", "bar");
+  div.innerHTML = s;
+  div.removeAttribute("foo");
 }
 
 SimpleTest.waitForExplicitFinish();
 
 </script>
 </pre>
 <div id="log">
 </div>
--- a/dom/interfaces/core/nsIDOMMutationObserver.idl
+++ b/dom/interfaces/core/nsIDOMMutationObserver.idl
@@ -51,21 +51,22 @@ dictionary MutationObserverInit
   boolean characterData;
   boolean subtree;
   boolean attributeOldValue;
   boolean characterDataOldValue;
   jsval   attributeFilter; // DOMString[]
 };
 
 //[Constructor(in nsIMutationCallback aDoneCallback)]
-[scriptable, builtinclass, uuid(daeba265-9aa7-45ab-8de2-b6b039c13ced)]
+[scriptable, builtinclass, uuid(156e2ce4-e44a-45f3-92c2-e6611f391dae)]
 interface nsIDOMMozMutationObserver : nsISupports
 {
   [implicit_jscontext]
   void observe(in nsIDOMNode aTarget, in jsval aOptions);
   void disconnect();
+  nsIVariant takeRecords();
 };
 
 [scriptable, function, uuid(fb539590-b088-4d07-96ff-2cefbc90a198)]
 interface nsIMutationObserverCallback : nsISupports
 {
   void handleMutations(in nsIVariant aRecords, in nsIDOMMozMutationObserver aObserver);
 };
--- a/mobile/android/base/db/BrowserProvider.java.in
+++ b/mobile/android/base/db/BrowserProvider.java.in
@@ -450,17 +450,17 @@ public class BrowserProvider extends Con
                 R.string.bookmarks_folder_places, 0);
 
             createOrUpdateAllSpecialFolders(db);
 
             createDefaultBookmarks(db, "^bookmarkdefaults_title_");
         }
 
         private void createDefaultBookmarks(SQLiteDatabase db, String pattern) {
-            Class stringsClass = R.string.class;
+            Class<?> stringsClass = R.string.class;
 
             Field[] fields = stringsClass.getFields();
             Pattern p = Pattern.compile(pattern);
 
             ContentValues bookmarksValues = new ContentValues();
             bookmarksValues.put(Bookmarks.PARENT, guidToID(db, Bookmarks.MOBILE_FOLDER_GUID));
             long now = System.currentTimeMillis();
             bookmarksValues.put(Bookmarks.DATE_CREATED, now);
@@ -493,17 +493,17 @@ public class BrowserProvider extends Con
                     Log.e(LOGTAG, "Can't create bookmark " + name, ex);
                 } catch (java.lang.NoSuchFieldException ex) {
                     Log.e(LOGTAG, "Can't create bookmark " + name, ex);
                 }
             }
         }
 
         private void setDefaultFavicon(SQLiteDatabase db, String name, String url) {
-            Class drawablesClass = R.drawable.class;
+            Class<?> drawablesClass = R.drawable.class;
             ByteArrayOutputStream stream = null;
             try {
                 // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
                 Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
                 if (faviconField == null)
                   return;
 
                 int faviconId = faviconField.getInt(null);
--- a/mobile/android/base/sqlite/SQLiteBridge.java
+++ b/mobile/android/base/sqlite/SQLiteBridge.java
@@ -247,17 +247,17 @@ public class SQLiteBridge {
      * The second two parameters here are just provided for compatbility with SQLiteDatabase
      * Support for them is not currently implemented
     */
     public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
         throws SQLiteException {
         SQLiteBridge bridge = null;
         try {
             bridge = new SQLiteBridge(path);
-            bridge.mDbPointer = bridge.openDatabase(path);
+            bridge.mDbPointer = SQLiteBridge.openDatabase(path);
         } catch(SQLiteBridgeException ex) {
             // catch and rethrow as a SQLiteException to match SQLiteDatabase
             throw new SQLiteException(ex.getMessage());
         }
         return bridge;
     }
 
     public void close() {
--- a/services/Makefile.in
+++ b/services/Makefile.in
@@ -38,12 +38,12 @@
 DEPTH     = ..
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 ifdef MOZ_SERVICES_SYNC
-PARALLEL_DIRS += crypto sync
+PARALLEL_DIRS += common crypto sync
 endif
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/common/Makefile.in
@@ -0,0 +1,34 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH     = ../..
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+PREF_JS_EXPORTS = $(srcdir)/services-common.js
+
+modules := \
+  async.js \
+  log4moz.js \
+  observers.js \
+  preferences.js \
+  rest.js \
+  stringbundle.js \
+  tokenserverclient.js \
+  utils.js \
+  $(NULL)
+
+source_modules = $(foreach module,$(modules),$(srcdir)/$(module))
+module_dir = $(FINAL_TARGET)/modules/services-common
+
+libs::
+	$(NSINSTALL) -D $(module_dir)
+	$(NSINSTALL) -l $(source_modules) $(module_dir)
+
+TEST_DIRS += tests
+
+include $(topsrcdir)/config/rules.mk
rename from services/sync/modules/async.js
rename to services/common/async.js
--- a/services/sync/modules/async.js
+++ b/services/common/async.js
@@ -1,51 +1,15 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Firefox Sync.
- *
- * The Initial Developer of the Original Code is
- * the Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *  Richard Newman <rnewman@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const EXPORTED_SYMBOLS = ['Async'];
+const EXPORTED_SYMBOLS = ["Async"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 // Constants for makeSyncCallback, waitForSyncCallback.
 const CB_READY = {};
 const CB_COMPLETE = {};
 const CB_FAIL = {};
 
 const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR;
 
@@ -56,20 +20,20 @@ Cu.import("resource://gre/modules/Servic
  */
 let Async = {
 
   /**
    * Execute an arbitrary number of asynchronous functions one after the
    * other, passing the callback arguments on to the next one.  All functions
    * must take a callback function as their last argument.  The 'this' object
    * will be whatever chain()'s is.
-   * 
+   *
    * @usage this._chain = Async.chain;
    *        this._chain(this.foo, this.bar, this.baz)(args, for, foo)
-   * 
+   *
    * This is equivalent to:
    *
    *   let self = this;
    *   self.foo(args, for, foo, function (bars, args) {
    *     self.bar(bars, args, function (baz, params) {
    *       self.baz(baz, params);
    *     });
    *   });
rename from services/sync/modules/log4moz.js
rename to services/common/log4moz.js
--- a/services/sync/modules/log4moz.js
+++ b/services/common/log4moz.js
@@ -34,20 +34,17 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ['Log4Moz'];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 const ONE_BYTE = 1;
 const ONE_KILOBYTE = 1024 * ONE_BYTE;
 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
 
 const STREAM_SEGMENT_SIZE = 4096;
 const PR_UINT32_MAX = 0xffffffff;
 
@@ -368,17 +365,17 @@ Formatter.prototype = {
 function BasicFormatter(dateFormat) {
   if (dateFormat)
     this.dateFormat = dateFormat;
 }
 BasicFormatter.prototype = {
   __proto__: Formatter.prototype,
 
   format: function BF_format(message) {
-    return message.time + "\t" + message.loggerName + "\t" + message.levelDesc 
+    return message.time + "\t" + message.loggerName + "\t" + message.levelDesc
            + "\t" + message.message + "\n";
   }
 };
 
 /*
  * Appenders
  * These can be attached to Loggers to log to different places
  * Simply subclass and override doAppend to implement a new one
@@ -437,17 +434,17 @@ ConsoleAppender.prototype = {
     }
     Cc["@mozilla.org/consoleservice;1"].
       getService(Ci.nsIConsoleService).logStringMessage(message);
   }
 };
 
 /**
  * Base implementation for stream based appenders.
- * 
+ *
  * Caution: This writes to the output stream synchronously, thus logging calls
  * block as the data is written to the stream. This can have negligible impact
  * for in-memory streams, but should be taken into account for I/O streams
  * (files, network, etc.)
  */
 function BlockingStreamAppender(formatter) {
   this._name = "BlockingStreamAppender";
   Appender.call(this, formatter);
@@ -455,17 +452,17 @@ function BlockingStreamAppender(formatte
 BlockingStreamAppender.prototype = {
   __proto__: Appender.prototype,
 
   _converterStream: null, // holds the nsIConverterOutputStream
   _outputStream: null,    // holds the underlying nsIOutputStream
 
   /**
    * Output stream to write to.
-   * 
+   *
    * This will automatically open the stream if it doesn't exist yet by
    * calling newOutputStream. The resulting raw stream is wrapped in a
    * nsIConverterOutputStream to ensure text is written as UTF-8.
    */
   get outputStream() {
     if (!this._outputStream) {
       // First create a raw stream. We can bail out early if that fails.
       this._outputStream = this.newOutputStream();
@@ -476,17 +473,17 @@ BlockingStreamAppender.prototype = {
       // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
       // the instance if we already have one.
       if (!this._converterStream) {
         this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
                                   .createInstance(Ci.nsIConverterOutputStream);
       }
       this._converterStream.init(
         this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
-        Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);      
+        Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
     }
     return this._converterStream;
   },
 
   newOutputStream: function newOutputStream() {
     throw "Stream-based appenders need to implement newOutputStream()!";
   },
 
@@ -516,27 +513,27 @@ BlockingStreamAppender.prototype = {
         }
       }
     }
   }
 };
 
 /**
  * Append to an nsIStorageStream
- * 
+ *
  * This writes logging output to an in-memory stream which can later be read
  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
  * during logging. Instead, one can periodically consume the input stream and
  * e.g. write it to disk asynchronously.
  */
 function StorageStreamAppender(formatter) {
   this._name = "StorageStreamAppender";
   BlockingStreamAppender.call(this, formatter);
 }
-StorageStreamAppender.prototype = { 
+StorageStreamAppender.prototype = {
   __proto__: BlockingStreamAppender.prototype,
 
   _ss: null,
   newOutputStream: function newOutputStream() {
     let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
                           .createInstance(Ci.nsIStorageStream);
     ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
     return ss.getOutputStream(0);
@@ -585,17 +582,17 @@ FileAppender.prototype = {
     } catch (e) {
       // File didn't exist in the first place, or we're on Windows. Meh.
     }
   }
 };
 
 /**
  * Rotating file appender (discouraged)
- * 
+ *
  * Similar to FileAppender, but rotates logs when they become too large.
  */
 function RotatingFileAppender(file, formatter, maxSize, maxBackups) {
   if (maxSize === undefined)
     maxSize = ONE_MEGABYTE * 2;
 
   if (maxBackups === undefined)
     maxBackups = 0;
rename from services/sync/modules/ext/Observers.js
rename to services/common/observers.js
rename from services/sync/modules/ext/Preferences.js
rename to services/common/preferences.js
new file mode 100644
--- /dev/null
+++ b/services/common/rest.js
@@ -0,0 +1,578 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const EXPORTED_SYMBOLS = ["RESTRequest", "RESTResponse"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/utils.js");
+
+const Prefs = new Preferences("services.common.rest.");
+
+/**
+ * Single use HTTP requests to RESTish resources.
+ *
+ * @param uri
+ *        URI for the request. This can be an nsIURI object or a string
+ *        that can be used to create one. An exception will be thrown if
+ *        the string is not a valid URI.
+ *
+ * Examples:
+ *
+ * (1) Quick GET request:
+ *
+ *   new RESTRequest("http://server/rest/resource").get(function (error) {
+ *     if (error) {
+ *       // Deal with a network error.
+ *       processNetworkErrorCode(error.result);
+ *       return;
+ *     }
+ *     if (!this.response.success) {
+ *       // Bail out if we're not getting an HTTP 2xx code.
+ *       processHTTPError(this.response.status);
+ *       return;
+ *     }
+ *     processData(this.response.body);
+ *   });
+ *
+ * (2) Quick PUT request (non-string data is automatically JSONified)
+ *
+ *   new RESTRequest("http://server/rest/resource").put(data, function (error) {
+ *     ...
+ *   });
+ *
+ * (3) Streaming GET
+ *
+ *   let request = new RESTRequest("http://server/rest/resource");
+ *   request.setHeader("Accept", "application/newlines");
+ *   request.onComplete = function (error) {
+ *     if (error) {
+ *       // Deal with a network error.
+ *       processNetworkErrorCode(error.result);
+ *       return;
+ *     }
+ *     callbackAfterRequestHasCompleted()
+ *   });
+ *   request.onProgress = function () {
+ *     if (!this.response.success) {
+ *       // Bail out if we're not getting an HTTP 2xx code.
+ *       return;
+ *     }
+ *     // Process body data and reset it so we don't process the same data twice.
+ *     processIncrementalData(this.response.body);
+ *     this.response.body = "";
+ *   });
+ *   request.get();
+ */
+function RESTRequest(uri) {
+  this.status = this.NOT_SENT;
+
+  // If we don't have an nsIURI object yet, make one. This will throw if
+  // 'uri' isn't a valid URI string.
+  if (!(uri instanceof Ci.nsIURI)) {
+    uri = Services.io.newURI(uri, null, null);
+  }
+  this.uri = uri;
+
+  this._headers = {};
+  this._log = Log4Moz.repository.getLogger(this._logName);
+  this._log.level =
+    Log4Moz.Level[Prefs.get("log.logger.rest.request")];
+}
+RESTRequest.prototype = {
+
+  _logName: "Services.Common.RESTRequest",
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIBadCertListener2,
+    Ci.nsIInterfaceRequestor,
+    Ci.nsIChannelEventSink
+  ]),
+
+  /*** Public API: ***/
+
+  /**
+   * URI for the request (an nsIURI object).
+   */
+  uri: null,
+
+  /**
+   * HTTP method (e.g. "GET")
+   */
+  method: null,
+
+  /**
+   * RESTResponse object
+   */
+  response: null,
+
+  /**
+   * nsIRequest load flags. Don't do any caching by default.
+   */
+  loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING,
+
+  /**
+   * nsIHttpChannel
+   */
+  channel: null,
+
+  /**
+   * Flag to indicate the status of the request.
+   *
+   * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
+   */
+  status: null,
+
+  NOT_SENT:    0,
+  SENT:        1,
+  IN_PROGRESS: 2,
+  COMPLETED:   4,
+  ABORTED:     8,
+
+  /**
+   * Request timeout (in seconds, though decimal values can be used for
+   * up to millisecond granularity.)
+   *
+   * 0 for no timeout.
+   */
+  timeout: null,
+
+  /**
+   * Called when the request has been completed, including failures and
+   * timeouts.
+   *
+   * @param error
+   *        Error that occurred while making the request, null if there
+   *        was no error.
+   */
+  onComplete: function onComplete(error) {
+  },
+
+  /**
+   * Called whenever data is being received on the channel. If this throws an
+   * exception, the request is aborted and the exception is passed as the
+   * error to onComplete().
+   */
+  onProgress: function onProgress() {
+  },
+
+  /**
+   * Set a request header.
+   */
+  setHeader: function setHeader(name, value) {
+    this._headers[name.toLowerCase()] = value;
+  },
+
+  /**
+   * Perform an HTTP GET.
+   *
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  get: function get(onComplete, onProgress) {
+    return this.dispatch("GET", null, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP PUT.
+   *
+   * @param data
+   *        Data to be used as the request body. If this isn't a string
+   *        it will be JSONified automatically.
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  put: function put(data, onComplete, onProgress) {
+    return this.dispatch("PUT", data, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP POST.
+   *
+   * @param data
+   *        Data to be used as the request body. If this isn't a string
+   *        it will be JSONified automatically.
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  post: function post(data, onComplete, onProgress) {
+    return this.dispatch("POST", data, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP DELETE.
+   *
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  delete: function delete_(onComplete, onProgress) {
+    return this.dispatch("DELETE", null, onComplete, onProgress);
+  },
+
+  /**
+   * Abort an active request.
+   */
+  abort: function abort() {
+    if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
+      throw "Can only abort a request that has been sent.";
+    }
+
+    this.status = this.ABORTED;
+    this.channel.cancel(Cr.NS_BINDING_ABORTED);
+
+    if (this.timeoutTimer) {
+      // Clear the abort timer now that the channel is done.
+      this.timeoutTimer.clear();
+    }
+  },
+
+  /*** Implementation stuff ***/
+
+  dispatch: function dispatch(method, data, onComplete, onProgress) {
+    if (this.status != this.NOT_SENT) {
+      throw "Request has already been sent!";
+    }
+
+    this.method = method;
+    if (onComplete) {
+      this.onComplete = onComplete;
+    }
+    if (onProgress) {
+      this.onProgress = onProgress;
+    }
+
+    // Create and initialize HTTP channel.
+    let channel = Services.io.newChannelFromURI(this.uri, null, null)
+                          .QueryInterface(Ci.nsIRequest)
+                          .QueryInterface(Ci.nsIHttpChannel);
+    this.channel = channel;
+    channel.loadFlags |= this.loadFlags;
+    channel.notificationCallbacks = this;
+
+    // Set request headers.
+    let headers = this._headers;
+    for (let key in headers) {
+      if (key == 'authorization') {
+        this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
+      } else {
+        this._log.trace("HTTP Header " + key + ": " + headers[key]);
+      }
+      channel.setRequestHeader(key, headers[key], false);
+    }
+
+    // Set HTTP request body.
+    if (method == "PUT" || method == "POST") {
+      // Convert non-string bodies into JSON.
+      if (typeof data != "string") {
+        data = JSON.stringify(data);
+      }
+
+      this._log.debug(method + " Length: " + data.length);
+      if (this._log.level <= Log4Moz.Level.Trace) {
+        this._log.trace(method + " Body: " + data);
+      }
+
+      let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+                     .createInstance(Ci.nsIStringInputStream);
+      stream.setData(data, data.length);
+
+      let type = headers["content-type"] || "text/plain";
+      channel.QueryInterface(Ci.nsIUploadChannel);
+      channel.setUploadStream(stream, type, data.length);
+    }
+    // We must set this after setting the upload stream, otherwise it
+    // will always be 'PUT'. Yeah, I know.
+    channel.requestMethod = method;
+
+    // Blast off!
+    channel.asyncOpen(this, null);
+    this.status = this.SENT;
+    this.delayTimeout();
+    return this;
+  },
+
+  /**
+   * Create or push back the abort timer that kills this request.
+   */
+  delayTimeout: function delayTimeout() {
+    if (this.timeout) {
+      CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
+                             "timeoutTimer");
+    }
+  },
+
+  /**
+   * Abort the request based on a timeout.
+   */
+  abortTimeout: function abortTimeout() {
+    this.abort();
+    let error = Components.Exception("Aborting due to channel inactivity.",
+                                     Cr.NS_ERROR_NET_TIMEOUT);
+    if (!this.onComplete) {
+      this._log.error("Unexpected error: onComplete not defined in " +
+                      "abortTimeout.")
+      return;
+    }
+    this.onComplete(error);
+  },
+
+  /*** nsIStreamListener ***/
+
+  onStartRequest: function onStartRequest(channel) {
+    if (this.status == this.ABORTED) {
+      this._log.trace("Not proceeding with onStartRequest, request was aborted.");
+      return;
+    }
+
+    try {
+      channel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
+      this.status = this.ABORTED;
+      channel.cancel(Cr.NS_BINDING_ABORTED);
+      return;
+    }
+
+    this.status = this.IN_PROGRESS;
+
+    this._log.trace("onStartRequest: " + channel.requestMethod + " " +
+                    channel.URI.spec);
+
+    // Create a response object and fill it with some data.
+    let response = this.response = new RESTResponse();
+    response.request = this;
+    response.body = "";
+
+    // Define this here so that we don't have make a new one each time
+    // onDataAvailable() gets called.
+    this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                          .createInstance(Ci.nsIScriptableInputStream);
+
+    this.delayTimeout();
+  },
+
+  onStopRequest: function onStopRequest(channel, context, statusCode) {
+    if (this.timeoutTimer) {
+      // Clear the abort timer now that the channel is done.
+      this.timeoutTimer.clear();
+    }
+
+    // We don't want to do anything for a request that's already been aborted.
+    if (this.status == this.ABORTED) {
+      this._log.trace("Not proceeding with onStopRequest, request was aborted.");
+      return;
+    }
+
+    try {
+      channel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel not nsIHttpChannel!");
+      this.status = this.ABORTED;
+      return;
+    }
+    this.status = this.COMPLETED;
+
+    let statusSuccess = Components.isSuccessCode(statusCode);
+    let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
+    this._log.trace("Channel for " + channel.requestMethod + " " + uri +
+                    " returned status code " + statusCode);
+
+    if (!this.onComplete) {
+      this._log.error("Unexpected error: onComplete not defined in " +
+                      "abortRequest.");
+      this.onProgress = null;
+      return;
+    }
+
+    // Throw the failure code and stop execution.  Use Components.Exception()
+    // instead of Error() so the exception is QI-able and can be passed across
+    // XPCOM borders while preserving the status code.
+    if (!statusSuccess) {
+      let message = Components.Exception("", statusCode).name;
+      let error = Components.Exception(message, statusCode);
+      this.onComplete(error);
+      this.onComplete = this.onProgress = null;
+      return;
+    }
+
+    this._log.debug(this.method + " " + uri + " " + this.response.status);
+
+    // Additionally give the full response body when Trace logging.
+    if (this._log.level <= Log4Moz.Level.Trace) {
+      this._log.trace(this.method + " body: " + this.response.body);
+    }
+
+    delete this._inputStream;
+
+    this.onComplete(null);
+    this.onComplete = this.onProgress = null;
+  },
+
+  onDataAvailable: function onDataAvailable(req, cb, stream, off, count) {
+    this._inputStream.init(stream);
+    try {
+      this.response.body += this._inputStream.read(count);
+    } catch (ex) {
+      this._log.warn("Exception thrown reading " + count +
+                     " bytes from the channel.");
+      this._log.debug(CommonUtils.exceptionStr(ex));
+      throw ex;
+    }
+
+    try {
+      this.onProgress();
+    } catch (ex) {
+      this._log.warn("Got exception calling onProgress handler, aborting " +
+                     this.method + " " + req.URI.spec);
+      this._log.debug("Exception: " + CommonUtils.exceptionStr(ex));
+      this.abort();
+
+      if (!this.onComplete) {
+        this._log.error("Unexpected error: onComplete not defined in " +
+                        "onDataAvailable.");
+        this.onProgress = null;
+        return;
+      }
+
+      this.onComplete(ex);
+      this.onComplete = this.onProgress = null;
+      return;
+    }
+
+    this.delayTimeout();
+  },
+
+  /*** nsIInterfaceRequestor ***/
+
+  getInterface: function(aIID) {
+    return this.QueryInterface(aIID);
+  },
+
+  /*** nsIBadCertListener2 ***/
+
+  notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
+    this._log.warn("Invalid HTTPS certificate encountered!");
+    // Suppress invalid HTTPS certificate warnings in the UI.
+    // (The request will still fail.)
+    return true;
+  },
+
+  /*** nsIChannelEventSink ***/
+  asyncOnChannelRedirect:
+    function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+
+    try {
+      newChannel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel not nsIHttpChannel!");
+      callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
+      return;
+    }
+
+    this.channel = newChannel;
+
+    // We let all redirects proceed.
+    callback.onRedirectVerifyCallback(Cr.NS_OK);
+  }
+};
+
+/**
+ * Response object for a RESTRequest. This will be created automatically by
+ * the RESTRequest.
+ */
+function RESTResponse() {
+  this._log = Log4Moz.repository.getLogger(this._logName);
+  this._log.level =
+    Log4Moz.Level[Prefs.get("log.logger.rest.response")];
+}
+RESTResponse.prototype = {
+
+  _logName: "Sync.RESTResponse",
+
+  /**
+   * Corresponding REST request
+   */
+  request: null,
+
+  /**
+   * HTTP status code
+   */
+  get status() {
+    let status;
+    try {
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      status = channel.responseStatus;
+    } catch (ex) {
+      this._log.debug("Caught exception fetching HTTP status code:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+    delete this.status;
+    return this.status = status;
+  },
+
+  /**
+   * Boolean flag that indicates whether the HTTP status code is 2xx or not.
+   */
+  get success() {
+    let success;
+    try {
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      success = channel.requestSucceeded;
+    } catch (ex) {
+      this._log.debug("Caught exception fetching HTTP success flag:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+    delete this.success;
+    return this.success = success;
+  },
+
+  /**
+   * Object containing HTTP headers (keyed as lower case)
+   */
+  get headers() {
+    let headers = {};
+    try {
+      this._log.trace("Processing response headers.");
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      channel.visitResponseHeaders(function (header, value) {
+        headers[header.toLowerCase()] = value;
+      });
+    } catch (ex) {
+      this._log.debug("Caught exception processing response headers:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+
+    delete this.headers;
+    return this.headers = headers;
+  },
+
+  /**
+   * HTTP body (string)
+   */
+  body: null
+
+};
new file mode 100644
--- /dev/null
+++ b/services/common/services-common.js
@@ -0,0 +1,7 @@
+// This file contains default preference values for components in
+// services-common.
+
+pref("services.common.log.logger.rest.request", "Debug");
+pref("services.common.log.logger.rest.response", "Debug");
+
+pref("services.common.tokenserverclient.logger.level", "Info");
rename from services/sync/modules/ext/StringBundle.js
rename to services/common/stringbundle.js
--- a/services/sync/modules/ext/StringBundle.js
+++ b/services/common/stringbundle.js
@@ -29,22 +29,19 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-let EXPORTED_SYMBOLS = ["StringBundle"];
+const EXPORTED_SYMBOLS = ["StringBundle"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 /**
  * A string bundle.
  *
  * This object presents two APIs: a deprecated one that is equivalent to the API
  * for the stringbundle XBL binding, to make it easy to switch from that binding
  * to this module, and a new one that is simpler and easier to use.
  *
new file mode 100644
--- /dev/null
+++ b/services/common/tests/Makefile.in
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH          = ../../..
+topsrcdir      = @top_srcdir@
+srcdir         = @srcdir@
+VPATH          = @srcdir@
+relativesrcdir = services/common/tests
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = test_services_common
+XPCSHELL_TESTS = unit
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/head_global.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Where to bind test HTTP servers to.
+const TEST_SERVER_URL = "http://localhost:8080/";
+
+// This has the side-effect of populating Cc, Ci, Cu, Cr. It's best not to
+// ask questions and just accept it.
+do_load_httpd_js();
+const Cm = Components.manager;
+
+let gSyncProfile = do_get_profile();
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let XULAppInfo = {
+  vendor: "Mozilla",
+  name: "XPCShell",
+  ID: "xpcshell@tests.mozilla.org",
+  version: "1",
+  appBuildID: "20100621",
+  platformVersion: "",
+  platformBuildID: "20100621",
+  inSafeMode: false,
+  logConsoleErrors: true,
+  OS: "XPCShell",
+  XPCOMABI: "noarch-spidermonkey",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
+  invalidateCachesOnRestart: function invalidateCachesOnRestart() { }
+};
+
+let XULAppInfoFactory = {
+  createInstance: function (outer, iid) {
+    if (outer != null)
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    return XULAppInfo.QueryInterface(iid);
+  }
+};
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
+                          "XULAppInfo", "@mozilla.org/xre/app-info;1",
+                          XULAppInfoFactory);
+
+function addResourceAlias() {
+  Cu.import("resource://gre/modules/Services.jsm");
+  const handler = Services.io.getProtocolHandler("resource")
+                  .QueryInterface(Ci.nsIResProtocolHandler);
+
+  let uri = Services.io.newURI("resource:///modules/services-common/", null,
+                               null);
+  handler.setSubstitution("services-common", uri);
+}
+addResourceAlias();
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/head_helpers.js
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Cu.import("resource://services-common/log4moz.js");
+
+function do_check_empty(obj) {
+  do_check_attribute_count(obj, 0);
+}
+
+function do_check_attribute_count(obj, c) {
+  do_check_eq(c, Object.keys(obj).length);
+}
+
+function do_check_throws(aFunc, aResult, aStack) {
+  if (!aStack) {
+    try {
+      // We might not have a 'Components' object.
+      aStack = Components.stack.caller;
+    } catch (e) {}
+  }
+
+  try {
+    aFunc();
+  } catch (e) {
+    do_check_eq(e.result, aResult, aStack);
+    return;
+  }
+  do_throw("Expected result " + aResult + ", none thrown.", aStack);
+}
+
+/**
+ * Print some debug message to the console. All arguments will be printed,
+ * separated by spaces.
+ *
+ * @param [arg0, arg1, arg2, ...]
+ *        Any number of arguments to print out
+ * @usage _("Hello World") -> prints "Hello World"
+ * @usage _(1, 2, 3) -> prints "1 2 3"
+ */
+let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" "));
+
+function initTestLogging(level) {
+  function LogStats() {
+    this.errorsLogged = 0;
+  }
+  LogStats.prototype = {
+    format: function BF_format(message) {
+      if (message.level == Log4Moz.Level.Error)
+        this.errorsLogged += 1;
+      return message.loggerName + "\t" + message.levelDesc + "\t" +
+        message.message + "\n";
+    }
+  };
+  LogStats.prototype.__proto__ = new Log4Moz.Formatter();
+
+  var log = Log4Moz.repository.rootLogger;
+  var logStats = new LogStats();
+  var appender = new Log4Moz.DumpAppender(logStats);
+
+  if (typeof(level) == "undefined")
+    level = "Debug";
+  getTestLogger().level = Log4Moz.Level[level];
+
+  log.level = Log4Moz.Level.Trace;
+  appender.level = Log4Moz.Level.Trace;
+  // Overwrite any other appenders (e.g. from previous incarnations)
+  log.ownAppenders = [appender];
+  log.updateAppenders();
+
+  return logStats;
+}
+
+function getTestLogger(component) {
+  return Log4Moz.repository.getLogger("Testing");
+}
+
+function httpd_setup (handlers, port) {
+  let port   = port || 8080;
+  let server = new nsHttpServer();
+  for (let path in handlers) {
+    server.registerPathHandler(path, handlers[path]);
+  }
+  try {
+    server.start(port);
+  } catch (ex) {
+    _("==========================================");
+    _("Got exception starting HTTP server on port " + port);
+    _("Error: " + Utils.exceptionStr(ex));
+    _("Is there a process already listening on port " + port + "?");
+    _("==========================================");
+    do_throw(ex);
+  }
+
+  return server;
+}
+
+function httpd_handler(statusCode, status, body) {
+  return function handler(request, response) {
+    _("Processing request");
+    // Allow test functions to inspect the request.
+    request.body = readBytesFromInputStream(request.bodyInputStream);
+    handler.request = request;
+
+    response.setStatusLine(request.httpVersion, statusCode, status);
+    if (body) {
+      response.bodyOutputStream.write(body, body.length);
+    }
+  };
+}
+
+/*
+ * Read bytes string from an nsIInputStream.  If 'count' is omitted,
+ * all available input is read.
+ */
+function readBytesFromInputStream(inputStream, count) {
+  var BinaryInputStream = Components.Constructor(
+      "@mozilla.org/binaryinputstream;1",
+      "nsIBinaryInputStream",
+      "setInputStream");
+  if (!count) {
+    count = inputStream.available();
+  }
+  return new BinaryInputStream(inputStream).readBytes(count);
+}
+
+/**
+ * Proxy auth helpers.
+ */
+
+/**
+ * Fake a PAC to prompt a channel replacement.
+ */
+let PACSystemSettings = {
+  CID: Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}"),
+  contractID: "@mozilla.org/system-proxy-settings;1",
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
+                                         Ci.nsISystemProxySettings]),
+
+  createInstance: function createInstance(outer, iid) {
+    if (outer) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(iid);
+  },
+
+  lockFactory: function lockFactory(lock) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  // Replace this URI for each test to avoid caching. We want to ensure that
+  // each test gets a completely fresh setup.
+  PACURI: null,
+  getProxyForURI: function getProxyForURI(aURI) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+};
+
+function installFakePAC() {
+  _("Installing fake PAC.");
+  Cm.nsIComponentRegistrar
+    .registerFactory(PACSystemSettings.CID,
+                     "Fake system proxy-settings",
+                     PACSystemSettings.contractID,
+                     PACSystemSettings);
+}
+
+function uninstallFakePAC() {
+  _("Uninstalling fake PAC.");
+  let CID = PACSystemSettings.CID;
+  Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
+}
rename from services/sync/tests/unit/test_async_chain.js
rename to services/common/tests/unit/test_async_chain.js
--- a/services/sync/tests/unit/test_async_chain.js
+++ b/services/common/tests/unit/test_async_chain.js
@@ -1,9 +1,12 @@
-Cu.import("resource://services-sync/async.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/async.js");
 
 function run_test() {
   _("Chain a few async methods, making sure the 'this' object is correct.");
 
   let methods = {
     save: function(x, callback) {
       this.x = x;
       callback(x);
rename from services/sync/tests/unit/test_async_querySpinningly.js
rename to services/common/tests/unit/test_async_querySpinningly.js
--- a/services/sync/tests/unit/test_async_querySpinningly.js
+++ b/services/common/tests/unit/test_async_querySpinningly.js
@@ -1,26 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/utils.js");
+
 _("Make sure querySpinningly will synchronously fetch rows for a query asyncly");
-Cu.import("resource://services-sync/async.js");
 
 const SQLITE_CONSTRAINT_VIOLATION = 19;  // http://www.sqlite.org/c3ref/c_abort.html
 
+let Svc = {};
+XPCOMUtils.defineLazyServiceGetter(Svc, "Form",
+                                   "@mozilla.org/satchel/form-history;1",
+                                   "nsIFormHistory2");
+
 function querySpinningly(query, names) {
   let q = Svc.Form.DBConnection.createStatement(query);
   let r = Async.querySpinningly(q, names);
-  q.finalize();    
+  q.finalize();
   return r;
 }
 
 function run_test() {
   initTestLogging("Trace");
 
   _("Make sure the call is async and allows other events to process");
   let isAsync = false;
-  Utils.nextTick(function() { isAsync = true; });
+  CommonUtils.nextTick(function() { isAsync = true; });
   do_check_false(isAsync);
 
   _("Empty out the formhistory table");
   let r0 = querySpinningly("DELETE FROM moz_formhistory");
   do_check_eq(r0, null);
 
   _("Make sure there's nothing there");
   let r1 = querySpinningly("SELECT 1 FROM moz_formhistory");
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_load_modules.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const modules = [
+  "async.js",
+  "log4moz.js",
+  "preferences.js",
+  "rest.js",
+  "stringbundle.js",
+  "tokenserverclient.js",
+  "utils.js",
+];
+
+function run_test() {
+  for each (let m in modules) {
+    let resource = "resource://services-common/" + m;
+    Components.utils.import(resource, {});
+  }
+}
rename from services/sync/tests/unit/test_log4moz.js
rename to services/common/tests/unit/test_log4moz.js
--- a/services/sync/tests/unit/test_log4moz.js
+++ b/services/common/tests/unit/test_log4moz.js
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Components.utils.import("resource://services-sync/log4moz.js");
-Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+Cu.import("resource://services-common/log4moz.js");
 
 let testFormatter = {
   format: function format(message) {
     return message.loggerName + "\t" + message.levelDesc + "\t" +
            message.message + "\n";
   }
 };
 
rename from services/sync/tests/unit/test_Observers.js
rename to services/common/tests/unit/test_observers.js
--- a/services/sync/tests/unit/test_Observers.js
+++ b/services/common/tests/unit/test_observers.js
@@ -1,13 +1,20 @@
-Components.utils.import("resource://services-sync/ext/Observers.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://services-common/observers.js");
 
 let gSubject = {};
 
-function test_function_observer() {
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_function_observer() {
   let foo = false;
 
   let onFoo = function(subject, data) {
     foo = !foo;
     do_check_eq(subject, gSubject);
     do_check_eq(data, "some data");
   };
 
@@ -17,19 +24,21 @@ function test_function_observer() {
   // The observer was notified after being added.
   do_check_true(foo);
 
   Observers.remove("foo", onFoo);
   Observers.notify("foo");
 
   // The observer was not notified after being removed.
   do_check_true(foo);
-}
 
-function test_method_observer() {
+  run_next_test();
+});
+
+add_test(function test_method_observer() {
   let obj = {
     foo: false,
     onFoo: function(subject, data) {
       this.foo = !this.foo;
       do_check_eq(subject, gSubject);
       do_check_eq(data, "some data");
     }
   };
@@ -38,19 +47,21 @@ function test_method_observer() {
   Observers.add("foo", obj.onFoo, obj);
   Observers.notify("foo", gSubject, "some data");
   do_check_true(obj.foo);
 
   // The observer is not notified after being removed.
   Observers.remove("foo", obj.onFoo, obj);
   Observers.notify("foo");
   do_check_true(obj.foo);
-}
 
-function test_object_observer() {
+  run_next_test();
+});
+
+add_test(function test_object_observer() {
   let obj = {
     foo: false,
     observe: function(subject, topic, data) {
       this.foo = !this.foo;
 
       do_check_eq(subject, gSubject);
       do_check_eq(topic, "foo");
       do_check_eq(data, "some data");
@@ -63,15 +74,11 @@ function test_object_observer() {
   // The observer is notified after being added.
   do_check_true(obj.foo);
 
   Observers.remove("foo", obj);
   Observers.notify("foo");
 
   // The observer is not notified after being removed.
   do_check_true(obj.foo);
-}
 
-function run_test() {
-  test_function_observer();
-  test_method_observer();
-  test_object_observer();
-}
+  run_next_test();
+});
rename from services/sync/tests/unit/test_Preferences.js
rename to services/common/tests/unit/test_preferences.js
--- a/services/sync/tests/unit/test_Preferences.js
+++ b/services/common/tests/unit/test_preferences.js
@@ -1,119 +1,144 @@
-Components.utils.import("resource://services-sync/ext/Preferences.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/preferences.js");
 
-function test_set_get_pref() {
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_set_get_pref() {
   Preferences.set("test_set_get_pref.integer", 1);
   do_check_eq(Preferences.get("test_set_get_pref.integer"), 1);
 
   Preferences.set("test_set_get_pref.string", "foo");
   do_check_eq(Preferences.get("test_set_get_pref.string"), "foo");
 
   Preferences.set("test_set_get_pref.boolean", true);
   do_check_eq(Preferences.get("test_set_get_pref.boolean"), true);
 
   // Clean up.
   Preferences.resetBranch("test_set_get_pref.");
-}
 
-function test_set_get_branch_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_branch_pref() {
   let prefs = new Preferences("test_set_get_branch_pref.");
 
   prefs.set("something", 1);
   do_check_eq(prefs.get("something"), 1);
   do_check_false(Preferences.has("something"));
 
   // Clean up.
   prefs.reset("something");
-}
 
-function test_set_get_multiple_prefs() {
+  run_next_test();
+});
+
+add_test(function test_set_get_multiple_prefs() {
   Preferences.set({ "test_set_get_multiple_prefs.integer":  1,
                     "test_set_get_multiple_prefs.string":   "foo",
                     "test_set_get_multiple_prefs.boolean":  true });
 
   let [i, s, b] = Preferences.get(["test_set_get_multiple_prefs.integer",
                                    "test_set_get_multiple_prefs.string",
                                    "test_set_get_multiple_prefs.boolean"]);
 
   do_check_eq(i, 1);
   do_check_eq(s, "foo");
   do_check_eq(b, true);
 
   // Clean up.
   Preferences.resetBranch("test_set_get_multiple_prefs.");
-}
 
-function test_get_multiple_prefs_with_default_value() {
+  run_next_test();
+});
+
+add_test(function test_get_multiple_prefs_with_default_value() {
   Preferences.set({ "test_get_multiple_prefs_with_default_value.a":  1,
                     "test_get_multiple_prefs_with_default_value.b":  2 });
 
   let [a, b, c] = Preferences.get(["test_get_multiple_prefs_with_default_value.a",
                                    "test_get_multiple_prefs_with_default_value.b",
                                    "test_get_multiple_prefs_with_default_value.c"],
                                   0);
 
   do_check_eq(a, 1);
   do_check_eq(b, 2);
   do_check_eq(c, 0);
 
   // Clean up.
   Preferences.resetBranch("test_get_multiple_prefs_with_default_value.");
-}
 
-function test_set_get_unicode_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_unicode_pref() {
   Preferences.set("test_set_get_unicode_pref", String.fromCharCode(960));
   do_check_eq(Preferences.get("test_set_get_unicode_pref"), String.fromCharCode(960));
 
   // Clean up.
   Preferences.reset("test_set_get_unicode_pref");
-}
 
-function test_set_null_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_null_pref() {
   try {
     Preferences.set("test_set_null_pref", null);
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
 
-function test_set_undefined_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_undefined_pref() {
   try {
     Preferences.set("test_set_undefined_pref");
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
 
-function test_set_unsupported_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_unsupported_pref() {
   try {
     Preferences.set("test_set_unsupported_pref", new Array());
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
+
+  run_next_test();
+});
 
 // Make sure that we can get a string pref that we didn't set ourselves
 // (i.e. that the way we get a string pref using getComplexValue doesn't
 // hork us getting a string pref that wasn't set using setComplexValue).
-function test_get_string_pref() {
+add_test(function test_get_string_pref() {
   let svc = Cc["@mozilla.org/preferences-service;1"].
             getService(Ci.nsIPrefService).
             getBranch("");
   svc.setCharPref("test_get_string_pref", "a normal string");
   do_check_eq(Preferences.get("test_get_string_pref"), "a normal string");
 
   // Clean up.
   Preferences.reset("test_get_string_pref");
-}
 
-function test_set_get_number_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_number_pref() {
   Preferences.set("test_set_get_number_pref", 5);
   do_check_eq(Preferences.get("test_set_get_number_pref"), 5);
 
   // Non-integer values get converted to integers.
   Preferences.set("test_set_get_number_pref", 3.14159);
   do_check_eq(Preferences.get("test_set_get_number_pref"), 3);
 
   // Values outside the range -(2^31-1) to 2^31-1 overflow.
@@ -121,61 +146,73 @@ function test_set_get_number_pref() {
     Preferences.set("test_set_get_number_pref", Math.pow(2, 31));
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
 
   // Clean up.
   Preferences.reset("test_set_get_number_pref");
-}
 
-function test_reset_pref() {
+  run_next_test();
+});
+
+add_test(function test_reset_pref() {
   Preferences.set("test_reset_pref", 1);
   Preferences.reset("test_reset_pref");
   do_check_eq(Preferences.get("test_reset_pref"), undefined);
-}
 
-function test_reset_pref_branch() {
+  run_next_test();
+});
+
+add_test(function test_reset_pref_branch() {
   Preferences.set("test_reset_pref_branch.foo", 1);
   Preferences.set("test_reset_pref_branch.bar", 2);
   Preferences.resetBranch("test_reset_pref_branch.");
   do_check_eq(Preferences.get("test_reset_pref_branch.foo"), undefined);
   do_check_eq(Preferences.get("test_reset_pref_branch.bar"), undefined);
-}
+
+  run_next_test();
+});
 
 // Make sure the module doesn't throw an exception when asked to reset
 // a nonexistent pref.
-function test_reset_nonexistent_pref() {
+add_test(function test_reset_nonexistent_pref() {
   Preferences.reset("test_reset_nonexistent_pref");
-}
+
+  run_next_test();
+});
 
 // Make sure the module doesn't throw an exception when asked to reset
 // a nonexistent pref branch.
-function test_reset_nonexistent_pref_branch() {
+add_test(function test_reset_nonexistent_pref_branch() {
   Preferences.resetBranch("test_reset_nonexistent_pref_branch.");
-}
 
-function test_observe_prefs_function() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_function() {
   let observed = false;
   let observer = function() { observed = !observed };
 
   Preferences.observe("test_observe_prefs_function", observer);
   Preferences.set("test_observe_prefs_function", "something");
   do_check_true(observed);
 
   Preferences.ignore("test_observe_prefs_function", observer);
   Preferences.set("test_observe_prefs_function", "something else");
   do_check_true(observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_function");
-}
 
-function test_observe_prefs_object() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_object() {
   let observer = {
     observed: false,
     observe: function() {
       this.observed = !this.observed;
     }
   };
 
   Preferences.observe("test_observe_prefs_object", observer.observe, observer);
@@ -183,19 +220,21 @@ function test_observe_prefs_object() {
   do_check_true(observer.observed);
 
   Preferences.ignore("test_observe_prefs_object", observer.observe, observer);
   Preferences.set("test_observe_prefs_object", "something else");
   do_check_true(observer.observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_object");
-}
 
-function test_observe_prefs_nsIObserver() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_nsIObserver() {
   let observer = {
     observed: false,
     observe: function(subject, topic, data) {
       this.observed = !this.observed;
       do_check_true(subject instanceof Ci.nsIPrefBranch);
       do_check_eq(topic, "nsPref:changed");
       do_check_eq(data, "test_observe_prefs_nsIObserver");
     }
@@ -206,84 +245,99 @@ function test_observe_prefs_nsIObserver(
   do_check_true(observer.observed);
 
   Preferences.ignore("test_observe_prefs_nsIObserver", observer);
   Preferences.set("test_observe_prefs_nsIObserver", "something else");
   do_check_true(observer.observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_nsIObserver");
-}
 
-function test_observe_exact_pref() {
+  run_next_test();
+});
+
+/*
+add_test(function test_observe_exact_pref() {
   let observed = false;
   let observer = function() { observed = !observed };
 
   Preferences.observe("test_observe_exact_pref", observer);
   Preferences.set("test_observe_exact_pref.sub-pref", "something");
   do_check_false(observed);
 
   // Clean up.
   Preferences.ignore("test_observe_exact_pref", observer);
   Preferences.reset("test_observe_exact_pref.sub-pref");
-}
 
-function test_observe_value_of_set_pref() {
+  run_next_test();
+});
+*/
+
+add_test(function test_observe_value_of_set_pref() {
   let observer = function(newVal) { do_check_eq(newVal, "something") };
 
   Preferences.observe("test_observe_value_of_set_pref", observer);
   Preferences.set("test_observe_value_of_set_pref", "something");
 
   // Clean up.
   Preferences.ignore("test_observe_value_of_set_pref", observer);
   Preferences.reset("test_observe_value_of_set_pref");
-}
 
-function test_observe_value_of_reset_pref() {
+  run_next_test();
+});
+
+add_test(function test_observe_value_of_reset_pref() {
   let observer = function(newVal) { do_check_true(typeof newVal == "undefined") };
 
   Preferences.set("test_observe_value_of_reset_pref", "something");
   Preferences.observe("test_observe_value_of_reset_pref", observer);
   Preferences.reset("test_observe_value_of_reset_pref");
 
   // Clean up.
   Preferences.ignore("test_observe_value_of_reset_pref", observer);
-}
 
-function test_has_pref() {
+  run_next_test();
+});
+
+add_test(function test_has_pref() {
   do_check_false(Preferences.has("test_has_pref"));
   Preferences.set("test_has_pref", "foo");
   do_check_true(Preferences.has("test_has_pref"));
 
   Preferences.set("test_has_pref.foo", "foo");
   Preferences.set("test_has_pref.bar", "bar");
   let [hasFoo, hasBar, hasBaz] = Preferences.has(["test_has_pref.foo",
                                                   "test_has_pref.bar",
                                                   "test_has_pref.baz"]);
   do_check_true(hasFoo);
   do_check_true(hasBar);
   do_check_false(hasBaz);
 
   // Clean up.
   Preferences.resetBranch("test_has_pref");
-}
 
-function test_isSet_pref() {
+  run_next_test();
+});
+
+add_test(function test_isSet_pref() {
   // Use a pref that we know has a default value but no user-set value.
   // This feels dangerous; perhaps we should create some other default prefs
   // that we can use for testing.
   do_check_false(Preferences.isSet("toolkit.defaultChromeURI"));
   Preferences.set("toolkit.defaultChromeURI", "foo");
   do_check_true(Preferences.isSet("toolkit.defaultChromeURI"));
 
   // Clean up.
   Preferences.reset("toolkit.defaultChromeURI");
-}
 
-function test_lock_prefs() {
+  run_next_test();
+});
+
+/*
+add_test(function test_lock_prefs() {
   // Use a pref that we know has a default value.
   // This feels dangerous; perhaps we should create some other default prefs
   // that we can use for testing.
   do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
   Preferences.lock("toolkit.defaultChromeURI");
   do_check_true(Preferences.locked("toolkit.defaultChromeURI"));
   Preferences.unlock("toolkit.defaultChromeURI");
   do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
@@ -293,19 +347,22 @@ function test_lock_prefs() {
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
   Preferences.lock("toolkit.defaultChromeURI");
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), val);
   Preferences.unlock("toolkit.defaultChromeURI");
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
 
   // Clean up.
   Preferences.reset("toolkit.defaultChromeURI");
-}
 
-function test_site_prefs() {
+  run_next_test();
+});
+*/
+
+add_test(function test_site_prefs() {
   let prefs = Preferences.site("www.example.com");
 
   prefs.set("test_site_prefs.integer", 1);
   do_check_eq(prefs.get("test_site_prefs.integer"), 1);
   do_check_true(prefs.has("test_site_prefs.integer"));
   do_check_false(Preferences.has("test_site_prefs.integer"));
   prefs.reset("test_site_prefs.integer");
   do_check_false(prefs.has("test_site_prefs.integer"));
@@ -318,37 +375,11 @@ function test_site_prefs() {
   do_check_false(prefs.has("test_site_prefs.string"));
 
   prefs.set("test_site_prefs.boolean", true);
   do_check_eq(prefs.get("test_site_prefs.boolean"), true);
   do_check_true(prefs.has("test_site_prefs.boolean"));
   do_check_false(Preferences.has("test_site_prefs.boolean"));
   prefs.reset("test_site_prefs.boolean");
   do_check_false(prefs.has("test_site_prefs.boolean"));
-}
 
-
-function run_test() {
-  test_set_get_pref();
-  test_set_get_branch_pref();
-  test_set_get_multiple_prefs();
-  test_get_multiple_prefs_with_default_value();
-  test_set_get_unicode_pref();
-  test_set_null_pref();
-  test_set_undefined_pref();
-  test_set_unsupported_pref();
-  test_get_string_pref();
-  test_set_get_number_pref();
-  test_reset_pref();
-  test_reset_pref_branch();
-  test_reset_nonexistent_pref();
-  test_reset_nonexistent_pref_branch();
-  test_observe_prefs_function();
-  test_observe_prefs_object();
-  test_observe_prefs_nsIObserver();
-  //test_observe_exact_pref();
-  test_observe_value_of_set_pref();
-  test_observe_value_of_reset_pref();
-  test_has_pref();
-  test_isSet_pref();
-  //test_lock_prefs();
-  test_site_prefs();
-}
+  run_next_test();
+});
rename from services/sync/tests/unit/test_restrequest.js
rename to services/common/tests/unit/test_restrequest.js
--- a/services/sync/tests/unit/test_restrequest.js
+++ b/services/common/tests/unit/test_restrequest.js
@@ -1,19 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/rest.js");
-Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://services-common/utils.js");
 
 const TEST_RESOURCE_URL = TEST_SERVER_URL + "resource";
 
+//DEBUG = true;
+
 function run_test() {
-  Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Services.Common.RESTRequest").level =
+    Log4Moz.Level.Trace;
   initTestLogging();
 
   run_next_test();
 }
 
 /**
  * Initializing a RESTRequest with an invalid URI throws
  * NS_ERROR_MALFORMED_URI.
@@ -135,17 +139,17 @@ add_test(function test_get() {
 
     do_check_eq(this.status, this.COMPLETED);
     do_check_true(this.response.success);
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Huzzah!");
     do_check_eq(handler.request.method, "GET");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.get(onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -184,17 +188,17 @@ add_test(function test_put() {
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
 
     do_check_eq(handler.request.method, "PUT");
     do_check_eq(handler.request.body, "Hullo?");
     do_check_eq(handler.request.getHeader("Content-Type"), "text/plain");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.put("Hullo?", onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -233,17 +237,17 @@ add_test(function test_post() {
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
 
     do_check_eq(handler.request.method, "POST");
     do_check_eq(handler.request.body, "Hullo?");
     do_check_eq(handler.request.getHeader("Content-Type"), "text/plain");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.post("Hullo?", onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -279,17 +283,17 @@ add_test(function test_delete() {
 
     do_check_eq(this.status, this.COMPLETED);
     do_check_true(this.response.success);
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
     do_check_eq(handler.request.method, "DELETE");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.delete(onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -462,17 +466,17 @@ add_test(function test_get_no_headers() 
 /**
  * Test changing the URI after having created the request.
  */
 add_test(function test_changing_uri() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest("http://localhost:8080/the-wrong-resource");
-  request.uri = Utils.makeURI(TEST_RESOURCE_URL);
+  request.uri = CommonUtils.makeURI(TEST_RESOURCE_URL);
   request.get(function (error) {
     do_check_eq(error, null);
     do_check_eq(this.response.status, 200);
     server.stop(run_next_test);
   });
 });
 
 /**
@@ -567,17 +571,17 @@ add_test(function test_abort() {
   request.abort();
 
   // Aborting an already aborted request is pointless and will throw.
   do_check_throws(function () {
     request.abort();
   });
 
   do_check_eq(request.status, request.ABORTED);
-  Utils.nextTick(function () {
+  CommonUtils.nextTick(function () {
     server.stop(run_next_test);
   });
 });
 
 /**
  * A non-zero 'timeout' property specifies the amount of seconds to wait after
  * channel activity until the request is automatically canceled.
  */
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_tokenserverclient.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+
+function run_test() {
+  initTestLogging("Trace");
+
+  run_next_test();
+}
+
+add_test(function test_working_bid_exchange() {
+  _("Ensure that working BrowserID token exchange works as expected.");
+
+  let service = "http://example.com/foo";
+
+  let server = httpd_setup({
+    "/1.0/foo/1.0": function(request, response) {
+      do_check_true(request.hasHeader("accept"));
+      do_check_eq("application/json", request.getHeader("accept"));
+
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+
+      let body = JSON.stringify({
+        id:           "id",
+        secret:       "key",
+        api_endpoint: service,
+        uid:          "uid",
+      });
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let client = new TokenServerClient();
+  let cb = Async.makeSpinningCallback();
+  let url = TEST_SERVER_URL + "1.0/foo/1.0";
+  client.getTokenFromBrowserIDAssertion(url, "assertion", cb);
+  let result = cb.wait();
+  do_check_eq("object", typeof(result));
+  do_check_attribute_count(result, 4);
+  do_check_eq(service, result.endpoint);
+  do_check_eq("id", result.id);
+  do_check_eq("key", result.key);
+  do_check_eq("uid", result.uid);
+
+  server.stop(run_next_test);
+});
+
+add_test(function test_invalid_arguments() {
+  _("Ensure invalid arguments to APIs are rejected.");
+
+  let args = [
+    [null, "assertion", function() {}],
+    ["http://example.com/", null, function() {}],
+    ["http://example.com/", "assertion", null]
+  ];
+
+  for each (let arg in args) {
+    try {
+      let client = new TokenServerClient();
+      client.getTokenFromBrowserIDAssertion(arg[0], arg[1], arg[2]);
+      do_throw("Should never get here.");
+    } catch (ex) {
+      do_check_true(ex instanceof TokenServerClientError);
+    }
+  }
+
+  run_next_test();
+});
+
+add_test(function test_error_404() {
+  _("Ensure that 404 responses result in error.");
+
+  let server = httpd_setup();
+
+  let client = new TokenServerClient();
+  let url = TEST_SERVER_URL + "foo";
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    do_check_neq(null, error);
+    do_check_eq("TokenServerClientServerError", error.name);
+    do_check_neq(null, error.response);
+    do_check_eq(null, r);
+
+    server.stop(run_next_test);
+  });
+});
+
+add_test(function test_bad_json() {
+  _("Ensure that malformed JSON is handled properly.");
+
+  let server = httpd_setup({
+    "/1.0/foo/1.0": function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+
+      let body = '{"id": "id", baz}'
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let client = new TokenServerClient();
+  let url = TEST_SERVER_URL + "1.0/foo/1.0";
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    _(error);
+    do_check_neq(null, error);
+    do_check_eq("TokenServerClientServerError", error.name);
+    do_check_neq(null, error.response);
+    do_check_eq(null, r);
+
+    server.stop(run_next_test);
+  });
+});
+
+add_test(function test_unhandled_media_type() {
+  _("Ensure that unhandled media types throw an error.");
+
+  let server = httpd_setup({
+    "/1.0/foo/1.0": function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "text/plain");
+
+      let body = "hello, world";
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let url = TEST_SERVER_URL + "1.0/foo/1.0";
+  let client = new TokenServerClient();
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    do_check_neq(null, error);
+    do_check_eq("TokenServerClientError", error.name);
+    do_check_neq(null, error.response);
+    do_check_eq(null, r);
+
+    server.stop(run_next_test);
+  });
+});
+
+add_test(function test_rich_media_types() {
+  _("Ensure that extra tokens in the media type aren't rejected.");
+
+  let server = httpd_setup({
+    "/foo": function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json; foo=bar; bar=foo");
+
+      let body = JSON.stringify({
+        id:           "id",
+        secret:       "key",
+        api_endpoint: "foo",
+        uid:          "uid",
+      });
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let url = TEST_SERVER_URL + "foo";
+  let client = new TokenServerClient();
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    do_check_eq(null, error);
+
+    server.stop(run_next_test);
+  });
+});
rename from services/sync/tests/unit/test_utils_atob.js
rename to services/common/tests/unit/test_utils_atob.js
--- a/services/sync/tests/unit/test_utils_atob.js
+++ b/services/common/tests/unit/test_utils_atob.js
@@ -1,8 +1,11 @@
-Cu.import("resource://services-sync/util.js");
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   let data = ["Zm9vYmE=", "Zm9vYmE==", "Zm9vYmE==="];
   for (let d in data) {
-    do_check_eq(Utils.safeAtoB(data[d]), "fooba");
+    do_check_eq(CommonUtils.safeAtoB(data[d]), "fooba");
   }
 }
rename from services/sync/tests/unit/test_utils_makeURI.js
rename to services/common/tests/unit/test_utils_makeURI.js
--- a/services/sync/tests/unit/test_utils_makeURI.js
+++ b/services/common/tests/unit/test_utils_makeURI.js
@@ -1,66 +1,66 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 _("Make sure uri strings are converted to nsIURIs");
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   _test_makeURI();
 }
 
 function _test_makeURI() {
   _("Check http uris");
   let uri1 = "http://mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uri1).spec, uri1);
+  do_check_eq(CommonUtils.makeURI(uri1).spec, uri1);
   let uri2 = "http://www.mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uri2).spec, uri2);
+  do_check_eq(CommonUtils.makeURI(uri2).spec, uri2);
   let uri3 = "http://mozillalabs.com/path";
-  do_check_eq(Utils.makeURI(uri3).spec, uri3);
+  do_check_eq(CommonUtils.makeURI(uri3).spec, uri3);
   let uri4 = "http://mozillalabs.com/multi/path";
-  do_check_eq(Utils.makeURI(uri4).spec, uri4);
+  do_check_eq(CommonUtils.makeURI(uri4).spec, uri4);
   let uri5 = "http://mozillalabs.com/?query";
-  do_check_eq(Utils.makeURI(uri5).spec, uri5);
+  do_check_eq(CommonUtils.makeURI(uri5).spec, uri5);
   let uri6 = "http://mozillalabs.com/#hash";
-  do_check_eq(Utils.makeURI(uri6).spec, uri6);
+  do_check_eq(CommonUtils.makeURI(uri6).spec, uri6);
 
   _("Check https uris");
   let uris1 = "https://mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uris1).spec, uris1);
+  do_check_eq(CommonUtils.makeURI(uris1).spec, uris1);
   let uris2 = "https://www.mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uris2).spec, uris2);
+  do_check_eq(CommonUtils.makeURI(uris2).spec, uris2);
   let uris3 = "https://mozillalabs.com/path";
-  do_check_eq(Utils.makeURI(uris3).spec, uris3);
+  do_check_eq(CommonUtils.makeURI(uris3).spec, uris3);
   let uris4 = "https://mozillalabs.com/multi/path";
-  do_check_eq(Utils.makeURI(uris4).spec, uris4);
+  do_check_eq(CommonUtils.makeURI(uris4).spec, uris4);
   let uris5 = "https://mozillalabs.com/?query";
-  do_check_eq(Utils.makeURI(uris5).spec, uris5);
+  do_check_eq(CommonUtils.makeURI(uris5).spec, uris5);
   let uris6 = "https://mozillalabs.com/#hash";
-  do_check_eq(Utils.makeURI(uris6).spec, uris6);
+  do_check_eq(CommonUtils.makeURI(uris6).spec, uris6);
 
   _("Check chrome uris");
   let uric1 = "chrome://browser/content/browser.xul";
-  do_check_eq(Utils.makeURI(uric1).spec, uric1);
+  do_check_eq(CommonUtils.makeURI(uric1).spec, uric1);
   let uric2 = "chrome://browser/skin/browser.css";
-  do_check_eq(Utils.makeURI(uric2).spec, uric2);
+  do_check_eq(CommonUtils.makeURI(uric2).spec, uric2);
   let uric3 = "chrome://browser/locale/browser.dtd";
-  do_check_eq(Utils.makeURI(uric3).spec, uric3);
+  do_check_eq(CommonUtils.makeURI(uric3).spec, uric3);
 
   _("Check about uris");
   let uria1 = "about:weave";
-  do_check_eq(Utils.makeURI(uria1).spec, uria1);
+  do_check_eq(CommonUtils.makeURI(uria1).spec, uria1);
   let uria2 = "about:weave/";
-  do_check_eq(Utils.makeURI(uria2).spec, uria2);
+  do_check_eq(CommonUtils.makeURI(uria2).spec, uria2);
   let uria3 = "about:weave/path";
-  do_check_eq(Utils.makeURI(uria3).spec, uria3);
+  do_check_eq(CommonUtils.makeURI(uria3).spec, uria3);
   let uria4 = "about:weave/multi/path";
-  do_check_eq(Utils.makeURI(uria4).spec, uria4);
+  do_check_eq(CommonUtils.makeURI(uria4).spec, uria4);
   let uria5 = "about:weave/?query";
-  do_check_eq(Utils.makeURI(uria5).spec, uria5);
+  do_check_eq(CommonUtils.makeURI(uria5).spec, uria5);
   let uria6 = "about:weave/#hash";
-  do_check_eq(Utils.makeURI(uria6).spec, uria6);
+  do_check_eq(CommonUtils.makeURI(uria6).spec, uria6);
 
   _("Invalid uris are undefined");
-  do_check_eq(Utils.makeURI("mozillalabs.com"), undefined);
-  do_check_eq(Utils.makeURI("chrome://badstuff"), undefined);
-  do_check_eq(Utils.makeURI("this is a test"), undefined);
+  do_check_eq(CommonUtils.makeURI("mozillalabs.com"), undefined);
+  do_check_eq(CommonUtils.makeURI("chrome://badstuff"), undefined);
+  do_check_eq(CommonUtils.makeURI("this is a test"), undefined);
 }
rename from services/sync/tests/unit/test_utils_namedTimer.js
rename to services/common/tests/unit/test_utils_namedTimer.js
--- a/services/sync/tests/unit/test_utils_namedTimer.js
+++ b/services/common/tests/unit/test_utils_namedTimer.js
@@ -1,69 +1,69 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_required_args() {
   try {
-    Utils.namedTimer(function callback() {
+    CommonUtils.namedTimer(function callback() {
       do_throw("Shouldn't fire.");
     }, 0);
     do_throw("Should have thrown!");
   } catch(ex) {
     run_next_test();
   }
 });
 
 add_test(function test_simple() {
-  _("Test basic properties of Utils.namedTimer.");
+  _("Test basic properties of CommonUtils.namedTimer.");
 
   const delay = 200;
   let that = {};
   let t0 = Date.now();
-  Utils.namedTimer(function callback(timer) {
+  CommonUtils.namedTimer(function callback(timer) {
     do_check_eq(this, that);
     do_check_eq(this._zetimer, null);
     do_check_true(timer instanceof Ci.nsITimer);
     // Difference should be ~delay, but hard to predict on all platforms,
     // particularly Windows XP.
     do_check_true(Date.now() > t0);
     run_next_test();
   }, delay, that, "_zetimer");
 });
 
 add_test(function test_delay() {
   _("Test delaying a timer that hasn't fired yet.");
-  
+
   const delay = 100;
   let that = {};
   let t0 = Date.now();
   function callback(timer) {
     // Difference should be ~2*delay, but hard to predict on all platforms,
     // particularly Windows XP.
     do_check_true((Date.now() - t0) > delay);
     run_next_test();
   }
-  Utils.namedTimer(callback, delay, that, "_zetimer");
-  Utils.namedTimer(callback, 2 * delay, that, "_zetimer");
+  CommonUtils.namedTimer(callback, delay, that, "_zetimer");
+  CommonUtils.namedTimer(callback, 2 * delay, that, "_zetimer");
   run_next_test();
 });
 
 add_test(function test_clear() {
   _("Test clearing a timer that hasn't fired yet.");
 
   const delay = 0;
   let that = {};
-  Utils.namedTimer(function callback(timer) {
+  CommonUtils.namedTimer(function callback(timer) {
     do_throw("Shouldn't fire!");
   }, delay, that, "_zetimer");
 
   that._zetimer.clear();
   do_check_eq(that._zetimer, null);
-  Utils.nextTick(run_next_test);
+  CommonUtils.nextTick(run_next_test);
 
   run_next_test();
 });
rename from services/sync/tests/unit/test_utils_stackTrace.js
rename to services/common/tests/unit/test_utils_stackTrace.js
--- a/services/sync/tests/unit/test_utils_stackTrace.js
+++ b/services/common/tests/unit/test_utils_stackTrace.js
@@ -1,30 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Define some functions in well defined line positions for the test");
 function foo(v) bar(v + 1); // line 2
 function bar(v) baz(v + 1); // line 3
 function baz(v) { throw new Error(v + 1); } // line 4
 
 _("Make sure lazy constructor calling/assignment works");
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   _("Make sure functions, arguments, files are pretty printed in the trace");
   let trace = "";
   try {
     foo(0);
   }
   catch(ex) {
-    trace = Utils.stackTrace(ex);
+    trace = CommonUtils.stackTrace(ex);
   }
   _("Got trace:", trace);
   do_check_neq(trace, "");
 
-  let bazPos = trace.indexOf("baz(2)@test_utils_stackTrace.js:4");
-  let barPos = trace.indexOf("bar(1)@test_utils_stackTrace.js:3");
-  let fooPos = trace.indexOf("foo(0)@test_utils_stackTrace.js:2");
+  let bazPos = trace.indexOf("baz(2)@test_utils_stackTrace.js:7");
+  let barPos = trace.indexOf("bar(1)@test_utils_stackTrace.js:6");
+  let fooPos = trace.indexOf("foo(0)@test_utils_stackTrace.js:5");
   _("String positions:", bazPos, barPos, fooPos);
 
   _("Make sure the desired messages show up");
   do_check_true(bazPos >= 0);
   do_check_true(barPos > bazPos);
   do_check_true(fooPos > barPos);
 }
rename from services/sync/tests/unit/test_utils_utf8.js
rename to services/common/tests/unit/test_utils_utf8.js
--- a/services/sync/tests/unit/test_utils_utf8.js
+++ b/services/common/tests/unit/test_utils_utf8.js
@@ -1,8 +1,11 @@
-Cu.import("resource://services-sync/util.js");
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   let str = "Umlaute: \u00FC \u00E4\n"; // Umlaute: ü ä
-  let encoded = Utils.encodeUTF8(str);
-  let decoded = Utils.decodeUTF8(encoded);
+  let encoded = CommonUtils.encodeUTF8(str);
+  let decoded = CommonUtils.decodeUTF8(encoded);
   do_check_eq(decoded, str);
 }
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/xpcshell.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = head_global.js head_helpers.js
+tail =
+
+# Test load modules first so syntax failures are caught early.
+[test_load_modules.js]
+
+[test_utils_atob.js]
+[test_utils_makeURI.js]
+[test_utils_namedTimer.js]
+[test_utils_stackTrace.js]
+[test_utils_utf8.js]
+
+[test_async_chain.js]
+[test_async_querySpinningly.js]
+[test_log4moz.js]
+[test_observers.js]
+[test_preferences.js]
+[test_restrequest.js]
+[test_tokenserverclient.js]
new file mode 100644
--- /dev/null
+++ b/services/common/tokenserverclient.js
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+  "TokenServerClient",
+  "TokenServerClientError",
+  "TokenServerClientNetworkError",
+  "TokenServerClientServerError"
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/rest.js");
+
+const Prefs = new Preferences("services.common.tokenserverclient.");
+
+/**
+ * Represents a TokenServerClient error that occurred on the client.
+ *
+ * This is the base type for all errors raised by client operations.
+ *
+ * @param message
+ *        (string) Error message.
+ */
+function TokenServerClientError(message) {
+  this.name = "TokenServerClientError";
+  this.message = message || "Client error.";
+}
+TokenServerClientError.prototype = new Error();
+TokenServerClientError.prototype.constructor = TokenServerClientError;
+
+/**
+ * Represents a TokenServerClient error that occurred in the network layer.
+ *
+ * @param error
+ *        The underlying error thrown by the network layer.
+ */
+function TokenServerClientNetworkError(error) {
+  this.name = "TokenServerClientNetworkError";
+  this.error = error;
+}
+TokenServerClientNetworkError.prototype = new TokenServerClientError();
+TokenServerClientNetworkError.prototype.constructor =
+  TokenServerClientNetworkError;
+
+/**
+ * Represents a TokenServerClient error that occurred on the server.
+ *
+ * This type will be encountered for all non-200 response codes from the
+ * server.
+ *
+ * @param message
+ *        (string) Error message.
+ */
+function TokenServerClientServerError(message) {
+  this.name = "TokenServerClientServerError";
+  this.message = message || "Server error.";
+}
+TokenServerClientServerError.prototype = new TokenServerClientError();
+TokenServerClientServerError.prototype.constructor =
+  TokenServerClientServerError;
+
+/**
+ * Represents a client to the Token Server.
+ *
+ * http://docs.services.mozilla.com/token/index.html
+ *
+ * The Token Server supports obtaining tokens for arbitrary apps by
+ * constructing URI paths of the form <app>/<app_version>. However, the service
+ * discovery mechanism emphasizes the use of full URIs and tries to not force
+ * the client to manipulate URIs. This client currently enforces this practice
+ * by not implementing an API which would perform URI manipulation.
+ *
+ * If you are tempted to implement this API in the future, consider this your
+ * warning that you may be doing it wrong and that you should store full URIs
+ * instead.
+ *
+ * Areas to Improve:
+ *
+ *  - The server sends a JSON response on error. The client does not currently
+ *    parse this. It might be convenient if it did.
+ *  - Currently all non-200 status codes are rolled into one error type. It
+ *    might be helpful if callers had a richer API that communicated who was
+ *    at fault (e.g. differentiating a 503 from a 401).
+ */
+function TokenServerClient() {
+  this._log = Log4Moz.repository.getLogger("Common.TokenServerClient");
+  this._log.level = Log4Moz.Level[Prefs.get("logger.level")];
+}
+TokenServerClient.prototype = {
+  /**
+   * Logger instance.
+   */
+  _log: null,
+
+  /**
+   * Obtain a token from a BrowserID assertion against a specific URL.
+   *
+   * This asynchronously obtains the token. The callback receives 2 arguments.
+   * The first signifies an error and is a TokenServerClientError (or derived)
+   * type when an error occurs. If an HTTP response was seen, a RESTResponse
+   * instance will be stored in the "response" property of this object.
+   *
+   * The second argument to the callback is a map containing the results from
+   * the server. This map has the following keys:
+   *
+   *   id       (string) HTTP MAC public key identifier.
+   *   key      (string) HTTP MAC shared symmetric key.
+   *   endpoint (string) URL where service can be connected to.
+   *   uid      (string) user ID for requested service.
+   *
+   * e.g.
+   *
+   *   let client = new TokenServerClient();
+   *   let assertion = getBrowserIDAssertionFromSomewhere();
+   *   let url = "https://token.services.mozilla.com/1.0/sync/2.0";
+   *
+   *   client.getTokenFromBrowserIDAssertion(url, assertion,
+   *                                         function(error, result) {
+   *     if (error) {
+   *       // Do error handling.
+   *       return;
+   *     }
+   *
+   *     let {id: id, key: key, uid: uid, endpoint: endpoint} = result;
+   *     // Do stuff with data and carry on.
+   *   });
+   *
+   * @param  url
+   *         (string) URL to fetch token from.
+   * @param  assertion
+   *         (string) BrowserID assertion to exchange token for.
+   * @param  cb
+   *         (function) Callback to be invoked with result of operation.
+   */
+  getTokenFromBrowserIDAssertion:
+    function getTokenFromBrowserIDAssertion(url, assertion, cb) {
+    if (!url) {
+      throw new TokenServerClientError("url argument is not valid.");
+    }
+
+    if (!assertion) {
+      throw new TokenServerClientError("assertion argument is not valid.");
+    }
+
+    if (!cb) {
+      throw new TokenServerClientError("cb argument is not valid.");
+    }
+
+    this._log.debug("Beginning BID assertion exchange: " + url);
+
+    let req = new RESTRequest(url);
+    req.setHeader("accept", "application/json");
+    req.setHeader("authorization", "Browser-ID " + assertion);
+    let client = this;
+    req.get(function onResponse(error) {
+      if (error) {
+        cb(new TokenServerClientNetworkError(error), null);
+        return;
+      }
+
+      try {
+        client._processTokenResponse(this.response, cb);
+      } catch (ex) {
+        let error = new TokenServerClientError(ex);
+        error.response = this.response;
+        cb(error, null);
+        return;
+      }
+    });
+  },
+
+  /**
+   * Handler to process token request responses.
+   *
+   * @param response
+   *        RESTResponse from token HTTP request.
+   * @param cb
+   *        The original callback passed to the public API.
+   */
+  _processTokenResponse: function processTokenResponse(response, cb) {
+    this._log.debug("Got token response.");
+
+    if (!response.success) {
+      this._log.info("Non-200 response code to token request: " +
+                     response.status);
+      this._log.debug("Response body: " + response.body);
+      let error = new TokenServerClientServerError("Non 200 response code: " +
+                                                   response.status);
+      error.response = response;
+      cb(error, null);
+      return;
+    }
+
+    let ct = response.headers["content-type"];
+    if (ct != "application/json" && ct.indexOf("application/json;") != 0) {
+      let error =  new TokenServerClientError("Unsupported media type: " + ct);
+      error.response = response;
+      cb(error, null);
+      return;
+    }
+
+    let result;
+    try {
+      result = JSON.parse(response.body);
+    } catch (ex) {
+      let error = new TokenServerClientServerError("Invalid JSON returned " +
+                                                   "from server.");
+      error.response = response;
+      cb(error, null);
+      return;
+    }
+
+    for each (let k in ["id", "secret", "api_endpoint", "uid"]) {
+      if (!(k in result)) {
+        let error = new TokenServerClientServerError("Expected key not " +
+                                                     " present in result: " +
+                                                     k);
+        error.response = response;
+        cb(error, null);
+        return;
+      }
+    }
+
+    this._log.debug("Successful token response: " + result.id);
+    cb(null, {
+      id:       result.id,
+      key:      result.secret,
+      endpoint: result.api_endpoint,
+      uid:      result.uid,
+    });
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/common/utils.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const EXPORTED_SYMBOLS = ["CommonUtils"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/log4moz.js");
+
+let CommonUtils = {
+  exceptionStr: function exceptionStr(e) {
+    let message = e.message ? e.message : e;
+    return message + " " + CommonUtils.stackTrace(e);
+  },
+
+  stackTrace: function stackTrace(e) {
+    // Wrapped nsIException
+    if (e.location) {
+      let frame = e.location;
+      let output = [];
+      while (frame) {
+        // Works on frames or exceptions, munges file:// URIs to shorten the paths
+        // FIXME: filename munging is sort of hackish, might be confusing if
+        // there are multiple extensions with similar filenames
+        let str = "<file:unknown>";
+
+        let file = frame.filename || frame.fileName;
+        if (file){
+          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
+        }
+
+        if (frame.lineNumber){
+          str += ":" + frame.lineNumber;
+        }
+        if (frame.name){
+          str = frame.name + "()@" + str;
+        }
+
+        if (str){
+          output.push(str);
+        }
+        frame = frame.caller;
+      }
+      return "Stack trace: " + output.join(" < ");
+    }
+    // Standard JS exception
+    if (e.stack){
+      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
+        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
+    }
+
+    return "No traceback available";
+  },
+
+  /**
+   * Create a nsIURI instance from a string.
+   */
+  makeURI: function makeURI(URIString) {
+    if (!URIString)
+      return null;
+    try {
+      return Services.io.newURI(URIString, null, null);
+    } catch (e) {
+      let log = Log4Moz.repository.getLogger("Common.Utils");
+      log.debug("Could not create URI: " + CommonUtils.exceptionStr(e));
+      return null;
+    }
+  },
+
+  /**
+   * Execute a function on the next event loop tick.
+   *
+   * @param callback
+   *        Function to invoke.
+   * @param thisObj [optional]
+   *        Object to bind the callback to.
+   */
+  nextTick: function nextTick(callback, thisObj) {
+    if (thisObj) {
+      callback = callback.bind(thisObj);
+    }
+    Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  /**
+   * Return a timer that is scheduled to call the callback after waiting the
+   * provided time or as soon as possible. The timer will be set as a property
+   * of the provided object with the given timer name.
+   */
+  namedTimer: function namedTimer(callback, wait, thisObj, name) {
+    if (!thisObj || !name) {
+      throw "You must provide both an object and a property name for the timer!";
+    }
+
+    // Delay an existing timer if it exists
+    if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
+      thisObj[name].delay = wait;
+      return;
+    }
+
+    // Create a special timer that we can add extra properties
+    let timer = {};
+    timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+    // Provide an easy way to clear out the timer
+    timer.clear = function() {
+      thisObj[name] = null;
+      timer.cancel();
+    };
+
+    // Initialize the timer with a smart callback
+    timer.initWithCallback({
+      notify: function notify() {
+        // Clear out the timer once it's been triggered
+        timer.clear();
+        callback.call(thisObj, timer);
+      }
+    }, wait, timer.TYPE_ONE_SHOT);
+
+    return thisObj[name] = timer;
+  },
+
+  encodeUTF8: function encodeUTF8(str) {
+    try {
+      str = this._utf8Converter.ConvertFromUnicode(str);
+      return str + this._utf8Converter.Finish();
+    } catch (ex) {
+      return null;
+    }
+  },
+
+  decodeUTF8: function decodeUTF8(str) {
+    try {
+      str = this._utf8Converter.ConvertToUnicode(str);
+      return str + this._utf8Converter.Finish();
+    } catch (ex) {
+      return null;
+    }
+  },
+
+  /**
+   * Trim excess padding from a Base64 string and atob().
+   *
+   * See bug 562431 comment 4.
+   */
+  safeAtoB: function safeAtoB(b64) {
+    let len = b64.length;
+    let over = len % 4;
+    return over ? atob(b64.substr(0, len - over)) : atob(b64);
+  },
+};
+
+XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  return converter;
+});
--- a/services/makefiles.sh
+++ b/services/makefiles.sh
@@ -30,30 +30,24 @@
 # use your version of this file under the terms of the MPL, indicate your
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
-MAKEFILES_crypto="
+add_makefiles "
+  services/Makefile
+  services/common/Makefile
   services/crypto/Makefile
   services/crypto/component/Makefile
-"
-
-MAKEFILES_sync="
   services/sync/Makefile
   services/sync/locales/Makefile
 "
 
-add_makefiles "
-  services/Makefile
-  $MAKEFILES_crypto
-  $MAKEFILES_sync
-"
-
 if [ "$ENABLE_TESTS" ]; then
   add_makefiles "
+    services/common/tests/Makefile
     services/crypto/tests/Makefile
     services/sync/tests/Makefile
   "
 fi
--- a/services/sync/SyncComponents.manifest
+++ b/services/sync/SyncComponents.manifest
@@ -1,9 +1,10 @@
 # Weave.js
 component {74b89fb0-f200-4ae8-a3ec-dd164117f6de} Weave.js
 contract @mozilla.org/weave/service;1 {74b89fb0-f200-4ae8-a3ec-dd164117f6de}
 category app-startup WeaveService service,@mozilla.org/weave/service;1
 component {d28f8a0b-95da-48f4-b712-caf37097be41} Weave.js
 contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4-b712-caf37097be41}
 # Register resource aliases
 resource services-sync resource:///modules/services-sync/
+resource services-common resource:///modules/services-common/
 resource services-crypto resource:///modules/services-crypto/
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -47,17 +47,17 @@
  * standalone file so it could be more easily understood, tested, and
  * hopefully ported.
  */
 
 "use strict";
 
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 
 const DEFAULT_STATE_FILE = "addonsreconciler";
 
 const CHANGE_INSTALLED   = 1;
 const CHANGE_UNINSTALLED = 2;
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -41,22 +41,22 @@
 const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine',
                           'Tracker', 'Store'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/main.js");    // So we can get to Service for callbacks.
 
 /*
  * Trackers are associated with a single engine and deal with
  * listening for changes to their particular data type.
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -67,18 +67,18 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-sync/addonsreconciler.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/async.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/preferences.js");
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/AddonRepository.jsm");
 
 const EXPORTED_SYMBOLS = ["AddonsEngine"];
 
 // 7 days in milliseconds.
 const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -46,17 +46,17 @@ const EXPORTED_SYMBOLS = ['BookmarksEngi
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
 Cu.import("resource://services-sync/main.js");      // For access to Service.
 
 const ALLBOOKMARKS_ANNO    = "AllBookmarks";
 const DESCRIPTION_ANNO     = "bookmarkProperties/description";
 const SIDEBAR_ANNO         = "bookmarkProperties/loadInSidebar";
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -37,19 +37,19 @@
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ["Clients", "ClientsRec"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+Cu.import("resource://services-common/stringbundle.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/main.js");
 
 const CLIENTS_TTL = 1814400; // 21 days
 const CLIENTS_TTL_REFRESH = 604800; // 7 days
 
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -38,20 +38,20 @@ const EXPORTED_SYMBOLS = ['FormEngine', 
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const FORMS_TTL = 5184000; // 60 days
 
 function FormRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 FormRec.prototype = {
   __proto__: CryptoWrapper.prototype,
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -46,19 +46,19 @@ const Cu = Components.utils;
 const Cr = Components.results;
 
 const HISTORY_TTL = 5184000; // 60 days
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function HistoryRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 HistoryRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.History",
   ttl: HISTORY_TTL
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -42,17 +42,17 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const WEAVE_SYNC_PREFS = "services.sync.prefs.sync.";
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
 
 const PREFS_GUID = Utils.encodeBase64url(Services.appinfo.ID);
 
 function PrefRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 PrefRec.prototype = {
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -46,17 +46,17 @@ const TABS_TTL = 604800; // 7 days
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 // It is safer to inspect the private browsing preferences rather than
 // the flags of nsIPrivateBrowsingService.  The user may have turned on
 // "Never remember history" in the same session, or Firefox was started
 // with the -private command line argument.  In both cases, the
 // "autoStarted" flag of nsIPrivateBrowsingService will be wrong.
 const PBPrefs = new Preferences("browser.privatebrowsing.");
 
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -5,17 +5,17 @@
 "use strict";
 
 const EXPORTED_SYMBOLS = ["Identity", "IdentityManager"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 XPCOMUtils.defineLazyGetter(this, "Identity", function() {
   return new IdentityManager();
 });
 
 /**
  * Manages identity and authentication for Sync.
--- a/services/sync/modules/jpakeclient.js
+++ b/services/sync/modules/jpakeclient.js
@@ -35,18 +35,18 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 const EXPORTED_SYMBOLS = ["JPAKEClient"];
 
 const REQUEST_TIMEOUT         = 60; // 1 minute
 const KEYEXCHANGE_VERSION     = 3;
 
--- a/services/sync/modules/keys.js
+++ b/services/sync/modules/keys.js
@@ -7,17 +7,17 @@
 const EXPORTED_SYMBOLS = [
   "BulkKeyBundle",
   "SyncKeyBundle"
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 /**
  * Represents a pair of keys.
  *
  * Each key stored in a key bundle is 256 bits. One key is used for symmetric
  * encryption. The other is used for HMAC.
  *
--- a/services/sync/modules/notifications.js
+++ b/services/sync/modules/notifications.js
@@ -36,18 +36,18 @@
 
 const EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 let Notifications = {
   // Match the referenced values in toolkit/content/widgets/notification.xml.
   get PRIORITY_INFO()     1, // PRIORITY_INFO_LOW
   get PRIORITY_WARNING()  4, // PRIORITY_WARNING_LOW
   get PRIORITY_ERROR()    7, // PRIORITY_CRITICAL_LOW
 
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -40,17 +40,17 @@
 
 const EXPORTED_SYMBOLS = ["SyncScheduler",
                           "ErrorHandler",
                           "SendCredentialsController"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/status.js");
 
 Cu.import("resource://services-sync/main.js");    // So we can get to Service for callbacks.
 
 let SyncScheduler = {
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -46,17 +46,17 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 const CRYPTO_COLLECTION = "crypto";
 const KEYS_WBO = "keys";
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 function WBORecord(collection, id) {
   this.data = {};
   this.payload = {};
   this.collection = collection;      // Optional.
   this.id = id;                      // Optional.
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -42,22 +42,22 @@ const EXPORTED_SYMBOLS = [
   "Resource"
 ];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 /*
  * AsyncResource represents a remote network resource, identified by a URI.
  * Create an instance like so:
  * 
  *   let resource = new AsyncResource("http://foobar.com/path/to/resource");
  * 
--- a/services/sync/modules/rest.js
+++ b/services/sync/modules/rest.js
@@ -35,592 +35,27 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/constants.js");
 
-const EXPORTED_SYMBOLS = ["RESTRequest", "SyncStorageRequest"];
+const EXPORTED_SYMBOLS = ["SyncStorageRequest"];
 
 const STORAGE_REQUEST_TIMEOUT = 5 * 60; // 5 minutes
 
 /**
- * Single use HTTP requests to RESTish resources.
- * 
- * @param uri
- *        URI for the request. This can be an nsIURI object or a string
- *        that can be used to create one. An exception will be thrown if
- *        the string is not a valid URI.
- *
- * Examples:
- *
- * (1) Quick GET request:
- *
- *   new RESTRequest("http://server/rest/resource").get(function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     if (!this.response.success) {
-
- *  *       // Bail out if we're not getting an HTTP 2xx code.
- *       processHTTPError(this.response.status);
- *       return;
- *     }
- *     processData(this.response.body);
- *   });
- *
- * (2) Quick PUT request (non-string data is automatically JSONified)
- *
- *   new RESTRequest("http://server/rest/resource").put(data, function (error) {
- *     ...
- *   });
- *
- * (3) Streaming GET
- *
- *   let request = new RESTRequest("http://server/rest/resource");
- *   request.setHeader("Accept", "application/newlines");
- *   request.onComplete = function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     callbackAfterRequestHasCompleted()
- *   });
- *   request.onProgress = function () {
- *     if (!this.response.success) {
- *       // Bail out if we're not getting an HTTP 2xx code.
- *       return;
- *     }
- *     // Process body data and reset it so we don't process the same data twice.
- *     processIncrementalData(this.response.body);
- *     this.response.body = "";
- *   });
- *   request.get();
- */
-function RESTRequest(uri) {
-  this.status = this.NOT_SENT;
-
-  // If we don't have an nsIURI object yet, make one. This will throw if
-  // 'uri' isn't a valid URI string.
-  if (!(uri instanceof Ci.nsIURI)) {
-    uri = Services.io.newURI(uri, null, null);
-  }
-  this.uri = uri;
-
-  this._headers = {};
-  this._log = Log4Moz.repository.getLogger(this._logName);
-  this._log.level =
-    Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
-}
-RESTRequest.prototype = {
-
-  _logName: "Sync.RESTRequest",
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIBadCertListener2,
-    Ci.nsIInterfaceRequestor,
-    Ci.nsIChannelEventSink
-  ]),
-
-  /*** Public API: ***/
-
-  /**
-   * URI for the request (an nsIURI object).
-   */
-  uri: null,
-
-  /**
-   * HTTP method (e.g. "GET")
-   */
-  method: null,
-
-  /**
-   * RESTResponse object
-   */
-  response: null,
-
-  /**
-   * nsIRequest load flags. Don't do any caching by default.
-   */
-  loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING,
-
-  /**
-   * nsIHttpChannel
-   */
-  channel: null,
-
-  /**
-   * Flag to indicate the status of the request.
-   *
-   * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
-   */
-  status: null,
-
-  NOT_SENT:    0,
-  SENT:        1,
-  IN_PROGRESS: 2,
-  COMPLETED:   4,
-  ABORTED:     8,
-
-  /**
-   * Request timeout (in seconds, though decimal values can be used for
-   * up to millisecond granularity.)
-   *
-   * 0 for no timeout.
-   */
-  timeout: null,
-
-  /**
-   * Called when the request has been completed, including failures and
-   * timeouts.
-   * 
-   * @param error
-   *        Error that occurred while making the request, null if there
-   *        was no error.
-   */
-  onComplete: function onComplete(error) {
-  },
-
-  /**
-   * Called whenever data is being received on the channel. If this throws an
-   * exception, the request is aborted and the exception is passed as the
-   * error to onComplete().
-   */
-  onProgress: function onProgress() {
-  },
-
-  /**
-   * Set a request header.
-   */
-  setHeader: function setHeader(name, value) {
-    this._headers[name.toLowerCase()] = value;
-  },
-
-  /**
-   * Perform an HTTP GET.
-   *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  get: function get(onComplete, onProgress) {
-    return this.dispatch("GET", null, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP PUT.
-   *
-   * @param data
-   *        Data to be used as the request body. If this isn't a string
-   *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  put: function put(data, onComplete, onProgress) {
-    return this.dispatch("PUT", data, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP POST.
-   *
-   * @param data
-   *        Data to be used as the request body. If this isn't a string
-   *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  post: function post(data, onComplete, onProgress) {
-    return this.dispatch("POST", data, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP DELETE.
-   *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  delete: function delete_(onComplete, onProgress) {
-    return this.dispatch("DELETE", null, onComplete, onProgress);
-  },
-
-  /**
-   * Abort an active request.
-   */
-  abort: function abort() {
-    if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
-      throw "Can only abort a request that has been sent.";
-    }
-
-    this.status = this.ABORTED;
-    this.channel.cancel(Cr.NS_BINDING_ABORTED);
-
-    if (this.timeoutTimer) {
-      // Clear the abort timer now that the channel is done.
-      this.timeoutTimer.clear();
-    }
-  },
-
-  /*** Implementation stuff ***/
-
-  dispatch: function dispatch(method, data, onComplete, onProgress) {
-    if (this.status != this.NOT_SENT) {
-      throw "Request has already been sent!";
-    }
-
-    this.method = method;
-    if (onComplete) {
-      this.onComplete = onComplete;
-    }
-    if (onProgress) {
-      this.onProgress = onProgress;
-    }
-
-    // Create and initialize HTTP channel.
-    let channel = Services.io.newChannelFromURI(this.uri, null, null)
-                          .QueryInterface(Ci.nsIRequest)
-                          .QueryInterface(Ci.nsIHttpChannel);
-    this.channel = channel;
-    channel.loadFlags |= this.loadFlags;
-    channel.notificationCallbacks = this;
-
-    // Set request headers.
-    let headers = this._headers;
-    for (let key in headers) {
-      if (key == 'authorization') {
-        this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
-      } else {
-        this._log.trace("HTTP Header " + key + ": " + headers[key]);
-      }
-      channel.setRequestHeader(key, headers[key], false);
-    }
-
-    // Set HTTP request body.
-    if (method == "PUT" || method == "POST") {
-      // Convert non-string bodies into JSON.
-      if (typeof data != "string") {
-        data = JSON.stringify(data);
-      }
-
-      this._log.debug(method + " Length: " + data.length);
-      if (this._log.level <= Log4Moz.Level.Trace) {
-        this._log.trace(method + " Body: " + data);
-      }
-
-      let stream = Cc["@mozilla.org/io/string-input-stream;1"]
-                     .createInstance(Ci.nsIStringInputStream);
-      stream.setData(data, data.length);
-
-      let type = headers["content-type"] || "text/plain";
-      channel.QueryInterface(Ci.nsIUploadChannel);
-      channel.setUploadStream(stream, type, data.length);
-    }
-    // We must set this after setting the upload stream, otherwise it
-    // will always be 'PUT'. Yeah, I know.
-    channel.requestMethod = method;
-
-    // Blast off!
-    channel.asyncOpen(this, null);
-    this.status = this.SENT;
-    this.delayTimeout();
-    return this;
-  },
-
-  /**
-   * Create or push back the abort timer that kills this request.
-   */
-  delayTimeout: function delayTimeout() {
-    if (this.timeout) {
-      Utils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
-                       "timeoutTimer");
-    }
-  },
-
-  /**
-   * Abort the request based on a timeout.
-   */
-  abortTimeout: function abortTimeout() {
-    this.abort();
-    let error = Components.Exception("Aborting due to channel inactivity.",
-                                     Cr.NS_ERROR_NET_TIMEOUT);
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortTimeout.")
-      return;
-    }
-    this.onComplete(error);
-  },
-
-  /*** nsIStreamListener ***/
-
-  onStartRequest: function onStartRequest(channel) {
-    if (this.status == this.ABORTED) {
-      this._log.trace("Not proceeding with onStartRequest, request was aborted.");
-      return;
-    }
-
-    try {
-      channel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
-      this.status = this.ABORTED;
-      channel.cancel(Cr.NS_BINDING_ABORTED);
-      return;
-    }
-
-    this.status = this.IN_PROGRESS;
-
-    this._log.trace("onStartRequest: " + channel.requestMethod + " " +
-                    channel.URI.spec);
-
-    // Create a response object and fill it with some data.
-    let response = this.response = new RESTResponse();
-    response.request = this;
-    response.body = "";
-
-    // Define this here so that we don't have make a new one each time
-    // onDataAvailable() gets called.
-    this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
-                          .createInstance(Ci.nsIScriptableInputStream);
-
-    this.delayTimeout();
-  },
-
-  onStopRequest: function onStopRequest(channel, context, statusCode) {
-    if (this.timeoutTimer) {
-      // Clear the abort timer now that the channel is done.
-      this.timeoutTimer.clear();
-    }
-
-    // We don't want to do anything for a request that's already been aborted.
-    if (this.status == this.ABORTED) {
-      this._log.trace("Not proceeding with onStopRequest, request was aborted.");
-      return;
-    }
-
-    try {
-      channel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel not nsIHttpChannel!");
-      this.status = this.ABORTED;
-      return;
-    }
-    this.status = this.COMPLETED;
-
-    let statusSuccess = Components.isSuccessCode(statusCode);
-    let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
-    this._log.trace("Channel for " + channel.requestMethod + " " + uri +
-                    " returned status code " + statusCode);
-
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortRequest.");
-      this.onProgress = null;
-      return;
-    }
-
-    // Throw the failure code and stop execution.  Use Components.Exception()
-    // instead of Error() so the exception is QI-able and can be passed across
-    // XPCOM borders while preserving the status code.
-    if (!statusSuccess) {
-      let message = Components.Exception("", statusCode).name;
-      let error = Components.Exception(message, statusCode);
-      this.onComplete(error);
-      this.onComplete = this.onProgress = null;
-      return;
-    }
-
-    this._log.debug(this.method + " " + uri + " " + this.response.status);
-
-    // Additionally give the full response body when Trace logging.
-    if (this._log.level <= Log4Moz.Level.Trace) {
-      this._log.trace(this.method + " body: " + this.response.body);
-    }
-
-    delete this._inputStream;
-
-    this.onComplete(null);
-    this.onComplete = this.onProgress = null;
-  },
-
-  onDataAvailable: function onDataAvailable(req, cb, stream, off, count) {
-    this._inputStream.init(stream);
-    try {
-      this.response.body += this._inputStream.read(count);
-    } catch (ex) {
-      this._log.warn("Exception thrown reading " + count +
-                     " bytes from the channel.");
-      this._log.debug(Utils.exceptionStr(ex));
-      throw ex;
-    }
-
-    try {
-      this.onProgress();
-    } catch (ex) {
-      this._log.warn("Got exception calling onProgress handler, aborting " +
-                     this.method + " " + req.URI.spec);
-      this._log.debug("Exception: " + Utils.exceptionStr(ex));
-      this.abort();
-
-      if (!this.onComplete) {
-        this._log.error("Unexpected error: onComplete not defined in " +
-                        "onDataAvailable.");
-        this.onProgress = null;
-        return;
-      }
-
-      this.onComplete(ex);
-      this.onComplete = this.onProgress = null;
-      return;
-    }
-
-    this.delayTimeout();
-  },
-
-  /*** nsIInterfaceRequestor ***/
-
-  getInterface: function(aIID) {
-    return this.QueryInterface(aIID);
-  },
-
-  /*** nsIBadCertListener2 ***/
-
-  notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
-    this._log.warn("Invalid HTTPS certificate encountered!");
-    // Suppress invalid HTTPS certificate warnings in the UI.
-    // (The request will still fail.)
-    return true;
-  },
-
-  /*** nsIChannelEventSink ***/
-  asyncOnChannelRedirect:
-    function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
-
-    try {
-      newChannel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel not nsIHttpChannel!");
-      callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
-      return;
-    }
-
-    this.channel = newChannel;
-
-    // We let all redirects proceed.
-    callback.onRedirectVerifyCallback(Cr.NS_OK);
-  }
-};
-
-
-/**
- * Response object for a RESTRequest. This will be created automatically by
- * the RESTRequest.
- */
-function RESTResponse() {
-  this._log = Log4Moz.repository.getLogger(this._logName);
-  this._log.level =
-    Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
-}
-RESTResponse.prototype = {
-
-  _logName: "Sync.RESTResponse",
-
-  /**
-   * Corresponding REST request
-   */
-  request: null,
-
-  /**
-   * HTTP status code
-   */
-  get status() {
-    let status;
-    try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      status = channel.responseStatus;
-    } catch (ex) {
-      this._log.debug("Caught exception fetching HTTP status code:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-    delete this.status;
-    return this.status = status;
-  },
-
-  /**
-   * Boolean flag that indicates whether the HTTP status code is 2xx or not.
-   */
-  get success() {
-    let success;
-    try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      success = channel.requestSucceeded;
-    } catch (ex) {
-      this._log.debug("Caught exception fetching HTTP success flag:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-    delete this.success;
-    return this.success = success;
-  },
-
-  /**
-   * Object containing HTTP headers (keyed as lower case)
-   */
-  get headers() {
-    let headers = {};
-    try {
-      this._log.trace("Processing response headers.");
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      channel.visitResponseHeaders(function (header, value) {
-        headers[header.toLowerCase()] = value;
-      });
-    } catch (ex) {
-      this._log.debug("Caught exception processing response headers:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-
-    delete this.headers;
-    return this.headers = headers;
-  },
-
-  /**
-   * HTTP body (string)
-   */
-  body: null
-
-};
-
-
-/**
  * RESTRequest variant for use against a Sync storage server.
  */
 function SyncStorageRequest(uri) {
   RESTRequest.call(this, uri);
 }
 SyncStorageRequest.prototype = {
 
   __proto__: RESTRequest.prototype,
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -58,19 +58,19 @@ const KEYS_WBO = "keys";
 
 const LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/main.js");
 
 const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -36,17 +36,17 @@
 const EXPORTED_SYMBOLS = ["Status"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://gre/modules/Services.jsm");
 
 let Status = {
   _log: Log4Moz.repository.getLogger("Sync.Status"),
   _authManager: Identity,
   ready: false,
 
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -33,38 +33,47 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "NetUtil", "PlacesUtils",
                           "FileUtils", "Utils", "Async", "Svc", "Str"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/stringbundle.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 
 /*
  * Utility functions
  */
 
 let Utils = {
+  // Alias in functions from CommonUtils. These previously were defined here.
+  // In the ideal world, references to these would be removed.
+  nextTick: CommonUtils.nextTick,
+  namedTimer: CommonUtils.namedTimer,
+  exceptionStr: CommonUtils.exceptionStr,
+  stackTrace: CommonUtils.stackTrace,
+  makeURI: CommonUtils.makeURI,
+  encodeUTF8: CommonUtils.encodeUTF8,
+  decodeUTF8: CommonUtils.decodeUTF8,
+  safeAtoB: CommonUtils.safeAtoB,
+
   /**
    * Wrap a function to catch all exceptions and log them
    *
    * @usage MyObj._catch = Utils.catch;
    *        MyObj.foo = function() { this._catch(func)(); }
    *        
    * Optionally pass a function which will be called if an
    * exception occurs.
@@ -264,72 +273,28 @@ let Utils = {
     // Do the same for b's keys but skip those that we already checked
     for (let k in b)
       if (!(k in a) && !eq(a[k], b[k]))
         return false;
 
     return true;
   },
 
-  exceptionStr: function Weave_exceptionStr(e) {
-    let message = e.message ? e.message : e;
-    return message + " " + Utils.stackTrace(e);
-  },
-  
-  stackTrace: function Weave_stackTrace(e) {
-    // Wrapped nsIException
-    if (e.location){
-      let frame = e.location; 
-      let output = [];
-      while (frame) {
-      	// Works on frames or exceptions, munges file:// URIs to shorten the paths
-        // FIXME: filename munging is sort of hackish, might be confusing if
-        // there are multiple extensions with similar filenames
-        let str = "<file:unknown>";
-
-        let file = frame.filename || frame.fileName;
-        if (file){
-          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
-        }
-
-        if (frame.lineNumber){
-          str += ":" + frame.lineNumber;
-        }
-        if (frame.name){
-          str = frame.name + "()@" + str;
-        }
-
-        if (str){
-          output.push(str);
-        }
-        frame = frame.caller;
-      }
-      return "Stack trace: " + output.join(" < ");
-    }
-    // Standard JS exception
-    if (e.stack){
-      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
-        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
-    }
-
-    return "No traceback available";
-  },
-  
   // Generator and discriminator for HMAC exceptions.
-  // Split these out in case we want to make them richer in future, and to 
+  // Split these out in case we want to make them richer in future, and to
   // avoid inevitable confusion if the message changes.
   throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
     throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is;
   },
-  
+
   isHMACMismatch: function isHMACMismatch(ex) {
     const hmacFail = "Record SHA256 HMAC mismatch: ";
     return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0);
   },
-  
+
   /**
    * UTF8-encode a message and hash it with the given hasher. Returns a
    * string containing bytes. The hasher is reset if it's an HMAC hasher.
    */
   digestUTF8: function digestUTF8(message, hasher) {
     let data = this._utf8Converter.convertToByteArray(message, {});
     hasher.update(data, data.length);
     let result = hasher.finish(false);
@@ -827,28 +792,16 @@ let Utils = {
 
     if (!ext) {
       return header;
     }
 
     return header += ', ext="' + ext +'"';
   },
 
-  makeURI: function Weave_makeURI(URIString) {
-    if (!URIString)
-      return null;
-    try {
-      return Services.io.newURI(URIString, null, null);
-    } catch (e) {
-      let log = Log4Moz.repository.getLogger("Sync.Utils");
-      log.debug("Could not create URI: " + Utils.exceptionStr(e));
-      return null;
-    }
-  },
-
   /**
    * Load a json object from disk
    *
    * @param filePath
    *        Json file path load from weave/[filePath].json
    * @param that
    *        Object to use for logging and "this" for callback
    * @param callback
@@ -912,69 +865,16 @@ let Utils = {
     let is = this._utf8Converter.convertToInputStream(out);
     NetUtil.asyncCopy(is, fos, function (result) {
       if (typeof callback == "function") {
         callback.call(that);        
       }
     });
   },
 
-  /**
-   * Execute a function on the next event loop tick.
-   * 
-   * @param callback
-   *        Function to invoke.
-   * @param thisObj [optional]
-   *        Object to bind the callback to.
-   */
-  nextTick: function nextTick(callback, thisObj) {
-    if (thisObj) {
-      callback = callback.bind(thisObj);
-    }
-    Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
-  },
-
-  /**
-   * Return a timer that is scheduled to call the callback after waiting the
-   * provided time or as soon as possible. The timer will be set as a property
-   * of the provided object with the given timer name.
-   */
-  namedTimer: function delay(callback, wait, thisObj, name) {
-    if (!thisObj || !name) {
-      throw "You must provide both an object and a property name for the timer!";
-    }
-
-    // Delay an existing timer if it exists
-    if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
-      thisObj[name].delay = wait;
-      return;
-    }
-
-    // Create a special timer that we can add extra properties
-    let timer = {};
-    timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-
-    // Provide an easy way to clear out the timer
-    timer.clear = function() {
-      thisObj[name] = null;
-      timer.cancel();
-    };
-
-    // Initialize the timer with a smart callback
-    timer.initWithCallback({
-      notify: function notify() {
-        // Clear out the timer once it's been triggered
-        timer.clear();
-        callback.call(thisObj, timer);
-      }
-    }, wait, timer.TYPE_ONE_SHOT);
-
-    return thisObj[name] = timer;
-  },
-
   getIcon: function(iconUri, defaultIcon) {
     try {
       let iconURI = Utils.makeURI(iconUri);
       return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
     }
     catch(ex) {}
 
     // Just give the provided default icon or the system's default
@@ -985,34 +885,16 @@ let Utils = {
     try {
       return Str.errors.get(error, args || null);
     } catch (e) {}
 
     // basically returns "Unknown Error"
     return Str.errors.get("error.reason.unknown");
   },
 
-  encodeUTF8: function(str) {
-    try {
-      str = this._utf8Converter.ConvertFromUnicode(str);
-      return str + this._utf8Converter.Finish();
-    } catch(ex) {
-      return null;
-    }
-  },
-
-  decodeUTF8: function(str) {
-    try {
-      str = this._utf8Converter.ConvertToUnicode(str);
-      return str + this._utf8Converter.Finish();
-    } catch(ex) {
-      return null;
-    }
-  },
-
   /**
    * Generate 26 characters.
    */
   generatePassphrase: function generatePassphrase() {
     // Note that this is a different base32 alphabet to the one we use for
     // other tasks. It's lowercase, uses different letters, and needs to be
     // decoded with decodeKeyBase32, not just decodeBase32.
     return Utils.encodeKeyBase32(Utils.generateRandomBytes(16));
@@ -1103,25 +985,16 @@ let Utils = {
     // Something else -- just return.
     return pp;
   },
   
   normalizeAccount: function normalizeAccount(acc) {
     return acc.trim();
   },
 
-  // WeaveCrypto returns bad base64 strings. Truncate excess padding
-  // and decode.
-  // See Bug 562431, comment 4.
-  safeAtoB: function safeAtoB(b64) {
-    let len = b64.length;
-    let over = len % 4;
-    return over ? atob(b64.substr(0, len - over)) : atob(b64);
-  },
-
   /**
    * Create an array like the first but without elements of the second. Reuse
    * arrays if possible.
    */
   arraySub: function arraySub(minuend, subtrahend) {
     if (!minuend.length || !subtrahend.length)
       return minuend;
     return minuend.filter(function(i) subtrahend.indexOf(i) == -1);
--- a/services/sync/tests/unit/head_appinfo.js
+++ b/services/sync/tests/unit/head_appinfo.js
@@ -44,15 +44,15 @@ registrar.registerFactory(Components.ID(
                           XULAppInfoFactory);
 
 
 // Register resource aliases. Normally done in SyncComponents.manifest.
 function addResourceAlias() {
   Cu.import("resource://gre/modules/Services.jsm");
   const resProt = Services.io.getProtocolHandler("resource")
                           .QueryInterface(Ci.nsIResProtocolHandler);
-  let uri;
-  uri = Services.io.newURI("resource:///modules/services-sync/", null, null);
-  resProt.setSubstitution("services-sync", uri);
-  uri = Services.io.newURI("resource:///modules/services-crypto/", null, null);
-  resProt.setSubstitution("services-crypto", uri);
+  for each (let s in ["common", "sync", "crypto"]) {
+    let uri = Services.io.newURI("resource:///modules/services-" + s + "/", null,
+                                 null);
+    resProt.setSubstitution("services-" + s, uri);
+  }
 }
 addResourceAlias();
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 let btoa;
 let atob;
 
 let provider = {
@@ -35,52 +36,18 @@ function waitForZeroTimer(callback) {
       Utils.nextTick(wait);
       return;
     }
     callback();
   }
   timer = Utils.namedTimer(wait, 150, {}, "timer");
 }
 
-btoa = Cu.import("resource://services-sync/log4moz.js").btoa;
-atob = Cu.import("resource://services-sync/log4moz.js").atob;
-function getTestLogger(component) {
-  return Log4Moz.repository.getLogger("Testing");
-}
-
-function initTestLogging(level) {
-  function LogStats() {
-    this.errorsLogged = 0;
-  }
-  LogStats.prototype = {
-    format: function BF_format(message) {
-      if (message.level == Log4Moz.Level.Error)
-        this.errorsLogged += 1;
-      return message.loggerName + "\t" + message.levelDesc + "\t" +
-        message.message + "\n";
-    }
-  };
-  LogStats.prototype.__proto__ = new Log4Moz.Formatter();
-
-  var log = Log4Moz.repository.rootLogger;
-  var logStats = new LogStats();
-  var appender = new Log4Moz.DumpAppender(logStats);
-
-  if (typeof(level) == "undefined")
-    level = "Debug";
-  getTestLogger().level = Log4Moz.Level[level];
-
-  log.level = Log4Moz.Level.Trace;
-  appender.level = Log4Moz.Level.Trace;
-  // Overwrite any other appenders (e.g. from previous incarnations)
-  log.ownAppenders = [appender];
-  log.updateAppenders();
-
-  return logStats;
-}
+btoa = Cu.import("resource://services-common/log4moz.js").btoa;
+atob = Cu.import("resource://services-common/log4moz.js").atob;
 
 // This is needed for loadAddonTestFunctions().
 let gGlobalScope = this;
 
 function ExtensionsTestPath(path) {
   if (path[0] != "/") {
     throw Error("Path must begin with '/': " + path);
   }
@@ -293,31 +260,19 @@ function ensureThrows(func) {
       func.apply(this, arguments);
     } catch (ex) {
       do_throw(ex);
     }
   };
 }
 
 
-/**
- * Print some debug message to the console. All arguments will be printed,
- * separated by spaces.
- *
- * @param [arg0, arg1, arg2, ...]
- *        Any number of arguments to print out
- * @usage _("Hello World") -> prints "Hello World"
- * @usage _(1, 2, 3) -> prints "1 2 3"
- */
-let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" "));
-
 _("Setting the identity for passphrase");
 Cu.import("resource://services-sync/identity.js");
 
-
 /*
  * Test setup helpers.
  */
 
 // Turn WBO cleartext into fake "encrypted" payload as it goes over the wire.
 function encryptPayload(cleartext) {
   if (typeof cleartext == "object") {
     cleartext = JSON.stringify(cleartext);
@@ -329,42 +284,16 @@ function encryptPayload(cleartext) {
 }
 
 function generateNewKeys(collections) {
   let wbo = CollectionKeys.generateNewKeysWBO(collections);
   let modified = new_timestamp();
   CollectionKeys.setContents(wbo.cleartext, modified);
 }
 
-function do_check_empty(obj) {
-  do_check_attribute_count(obj, 0);
-}
-
-function do_check_attribute_count(obj, c) {
-  do_check_eq(c, Object.keys(obj).length);
-}
-
-function do_check_throws(aFunc, aResult, aStack)
-{
-  if (!aStack) {
-    try {
-      // We might not have a 'Components' object.
-      aStack = Components.stack.caller;
-    } catch (e) {}
-  }
-
-  try {
-    aFunc();
-  } catch (e) {
-    do_check_eq(e.result, aResult, aStack);
-    return;
-  }
-  do_throw("Expected result " + aResult + ", none thrown.", aStack);
-}
-
 /*
  * A fake engine implementation.
  * This is used all over the place.
  *
  * Complete with record, store, and tracker implementations.
  */
 
 function RotaryRecord(collection, id) {
@@ -482,9 +411,9 @@ deepCopy: function deepCopy(thing, noSor
     let props = [p for (p in thing)];
     if (!noSort){
       props = props.sort();
     }
     props.forEach(function(k) ret[k] = deepCopy(thing[k], noSort));
   }
 
   return ret;
-};
\ No newline at end of file
+};
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -1,15 +1,15 @@
 const Cm = Components.manager;
 
 const TEST_CLUSTER_URL = "http://localhost:8080/";
 const TEST_SERVER_URL  = "http://localhost:8080/";
 
 // Shared logging for all HTTP server functions.
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 const SYNC_HTTP_LOGGER = "Sync.Test.Server";
 const SYNC_API_VERSION = "1.1";
 
 // Use the same method that record.js does, which mirrors the server.
 // The server returns timestamps with 1/100 sec granularity. Note that this is
 // subject to change: see Bug 650435.
 function new_timestamp() {
   return Math.round(Date.now() / 10) / 100;
@@ -21,49 +21,16 @@ function return_timestamp(request, respo
   }
   let body = "" + timestamp;
   response.setHeader("X-Weave-Timestamp", body);
   response.setStatusLine(request.httpVersion, 200, "OK");
   response.bodyOutputStream.write(body, body.length);
   return timestamp;
 }
 
-function httpd_setup (handlers, port) {
-  let port   = port || 8080;
-  let server = new nsHttpServer();
-  for (let path in handlers) {
-    server.registerPathHandler(path, handlers[path]);
-  }
-  try {
-    server.start(port);
-  } catch (ex) {
-    _("==========================================");
-    _("Got exception starting HTTP server on port " + port);
-    _("Error: " + Utils.exceptionStr(ex));
-    _("Is there a process already listening on port " + port + "?");
-    _("==========================================");
-    do_throw(ex);
-  }
-
-  return server;
-}
-
-function httpd_handler(statusCode, status, body) {
-  return function handler(request, response) {
-    // Allow test functions to inspect the request.
-    request.body = readBytesFromInputStream(request.bodyInputStream);
-    handler.request = request;
-
-    response.setStatusLine(request.httpVersion, statusCode, status);
-    if (body) {
-      response.bodyOutputStream.write(body, body.length);
-    }
-  };
-}
-
 function basic_auth_header(user, password) {
   return "Basic " + btoa(user + ":" + Utils.encodeUTF8(password));
 }
 
 function basic_auth_matches(req, user, password) {
   if (!req.hasHeader("Authorization")) {
     return false;
   }
@@ -80,31 +47,16 @@ function httpd_basic_auth_handler(body, 
     body = "This path exists and is protected - failed";
     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
     response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
   }
   response.bodyOutputStream.write(body, body.length);
 }
 
 /*
- * Read bytes string from an nsIInputStream.  If 'count' is omitted,
- * all available input is read.
- */
-function readBytesFromInputStream(inputStream, count) {
-  var BinaryInputStream = Components.Constructor(
-      "@mozilla.org/binaryinputstream;1",
-      "nsIBinaryInputStream",
-      "setInputStream");
-  if (!count) {
-    count = inputStream.available();
-  }
-  return new BinaryInputStream(inputStream).readBytes(count);
-}
-
-/*
  * Represent a WBO on the server
  */
 function ServerWBO(id, initialPayload, modified) {
   if (!id) {
     throw "No ID for ServerWBO!";
   }
   this.id = id;
   if (!initialPayload) {
@@ -1039,56 +991,8 @@ function serverForUsers(users, contents,
   let server = new SyncServer(callback);
   for (let [user, pass] in Iterator(users)) {
     server.registerUser(user, pass);
     server.createContents(user, contents);
   }
   server.start();
   return server;
 }
-
-/**
- * Proxy auth helpers.
- */
-
-/**
- * Fake a PAC to prompt a channel replacement.
- */
-let PACSystemSettings = {
-  CID: Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}"),
-  contractID: "@mozilla.org/system-proxy-settings;1",
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
-                                         Ci.nsISystemProxySettings]),
-
-  createInstance: function createInstance(outer, iid) {
-    if (outer) {
-      throw Cr.NS_ERROR_NO_AGGREGATION;
-    }
-    return this.QueryInterface(iid);
-  },
-
-  lockFactory: function lockFactory(lock) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  },
-  
-  // Replace this URI for each test to avoid caching. We want to ensure that
-  // each test gets a completely fresh setup.
-  PACURI: null,
-  getProxyForURI: function getProxyForURI(aURI) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  }
-};
-
-function installFakePAC() {
-  _("Installing fake PAC.");
-  Cm.nsIComponentRegistrar
-    .registerFactory(PACSystemSettings.CID,
-                     "Fake system proxy-settings",
-                     PACSystemSettings.contractID,
-                     PACSystemSettings);
-}
-
-function uninstallFakePAC() {
-  _("Uninstalling fake PAC.");
-  let CID = PACSystemSettings.CID;
-  Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
-}
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -1,19 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-sync/addonsreconciler.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/service.js");
 
 let prefs = new Preferences();
 prefs.set("extensions.getAddons.get.url",
           "http://localhost:8888/search/guid:%IDS%");
 
 loadAddonTestFunctions();
 startupManager();
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 const HTTP_PORT = 8888;
 
 let prefs = new Preferences();
 
 Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
 prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
 loadAddonTestFunctions();
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -1,13 +1,13 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 Engines.register(BookmarksEngine);
 var syncTesting = new SyncTestingInfrastructure();
 
--- a/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
+++ b/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that Sync can correctly handle a legacy microsummary record
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 const GENERATORURI_ANNO = "microsummary/generatorURI";
 const STATICTITLE_ANNO = "bookmarks/staticTitle";
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ b/services/sync/tests/unit/test_bookmark_livemarks.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
--- a/services/sync/tests/unit/test_bookmark_record.js
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 function prepareBookmarkItem(collection, id) {
   let b = new Bookmark(collection, id);
   b.cleartext.stuff = "my payload here";
   return b;
 }
 
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -1,12 +1,12 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
 var IOService = Cc["@mozilla.org/network/io-service;1"]
                 .getService(Ci.nsIIOService);
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -2,17 +2,17 @@ Cu.import("resource://services-sync/main
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/tabs.js");
 Cu.import("resource://services-sync/engines/history.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
   
 add_test(function test_locally_changed_keys() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
   let hmacErrorCount = 0;
   function counting(f) {
     return function() {
       hmacErrorCount++;
--- a/services/sync/tests/unit/test_engine.js
+++ b/services/sync/tests/unit/test_engine.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/util.js");
 
 
 function SteamStore() {
   Store.call(this, "Steam");
   this.wasWiped = false;
 }
 SteamStore.prototype = {
--- a/services/sync/tests/unit/test_errorhandler_filelog.js
+++ b/services/sync/tests/unit/test_errorhandler_filelog.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const logsdir            = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 const CLEANUP_DELAY      = 1000; // delay to age files for cleanup (ms)
 const DELAY_BUFFER       = 50; // buffer for timers on different OS platforms
 
 const PROLONGED_ERROR_DURATION =
--- a/services/sync/tests/unit/test_forms_tracker.js
+++ b/services/sync/tests/unit/test_forms_tracker.js
@@ -1,11 +1,11 @@
 Cu.import("resource://services-sync/engines/forms.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = new FormEngine()._tracker;
   do_check_empty(tracker.changedIDs);
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   try {
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -1,11 +1,11 @@
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines/history.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 
 const TIMESTAMP1 = (Date.now() - 103406528) * 1000;
 const TIMESTAMP2 = (Date.now() - 6592903) * 1000;
 const TIMESTAMP3 = (Date.now() - 123894) * 1000;
 
 function queryPlaces(uri, options) {
   let query = PlacesUtils.history.getNewQuery();
--- a/services/sync/tests/unit/test_httpd_sync_server.js
+++ b/services/sync/tests/unit/test_httpd_sync_server.js
@@ -54,17 +54,17 @@ add_test(function test_url_parsing() {
   parts = server.storageRE.exec("storage");
   let [all, storage, collection, id] = parts;
   do_check_eq(all, "storage");
   do_check_eq(collection, undefined);
 
   run_next_test();
 });
 
-Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-common/rest.js");
 function localRequest(path) {
   _("localRequest: " + path);
   let url = "http://127.0.0.1:8080" + path;
   _("url: " + url);
   return new RESTRequest(url);
 }
 
 add_test(function test_basic_http() {
--- a/services/sync/tests/unit/test_jpakeclient.js
+++ b/services/sync/tests/unit/test_jpakeclient.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/jpakeclient.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 const JPAKE_LENGTH_SECRET     = 8;
 const JPAKE_LENGTH_CLIENTID   = 256;
 const KEYEXCHANGE_VERSION     = 3;
@@ -185,17 +185,18 @@ function run_test() {
   // sure the J-PAKE requests don't include those data.
   setBasicCredentials("johndoe", "ilovejane");
 
   server = httpd_setup({"/new_channel": server_new_channel,
                         "/report":      server_report});
 
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace;
-  Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Common.RESTRequest").level =
+    Log4Moz.Level.Trace;
   run_next_test();
 }
 
 
 add_test(function test_success_receiveNoPIN() {
   _("Test a successful exchange started by receiveNoPIN().");
 
   let snd = new JPAKEClient({
--- a/services/sync/tests/unit/test_load_modules.js
+++ b/services/sync/tests/unit/test_load_modules.js
@@ -1,27 +1,23 @@
 const modules = [
                  "addonsreconciler.js",
-                 "async.js",
                  "constants.js",
                  "engines/addons.js",
                  "engines/bookmarks.js",
                  "engines/clients.js",
                  "engines/forms.js",
                  "engines/history.js",
                  "engines/passwords.js",
                  "engines/prefs.js",
                  "engines/tabs.js",
                  "engines.js",
-                 "ext/Observers.js",
-                 "ext/Preferences.js",
                  "identity.js",
                  "jpakeclient.js",
                  "keys.js",
-                 "log4moz.js",
                  "main.js",
                  "notifications.js",
                  "policies.js",
                  "record.js",
                  "resource.js",
                  "rest.js",
                  "service.js",
                  "status.js",
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -2,22 +2,22 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 _("Test that node reassignment responses are respected on all kinds of " +
   "requests.");
 
 // Don't sync any engines by default.
 Svc.DefaultPrefs.set("registerEngines", "")
 
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/policies.js");
-Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.ErrorHandler").level  = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Resource").level      = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.RESTRequest").level   = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Service").level       = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
--- a/services/sync/tests/unit/test_places_guid_downgrade.js
+++ b/services/sync/tests/unit/test_places_guid_downgrade.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/history.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 
 const kDBName = "places.sqlite";
 const storageSvc = Cc["@mozilla.org/storage/service;1"]
                      .getService(Ci.mozIStorageService);
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -1,11 +1,11 @@
 Cu.import("resource://services-sync/engines/prefs.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const PREFS_GUID = Utils.encodeBase64url(Services.appinfo.ID);
 
 function makePersona(id) {
   return {
     id: id || Math.random().toString(),
--- a/services/sync/tests/unit/test_prefs_tracker.js
+++ b/services/sync/tests/unit/test_prefs_tracker.js
@@ -1,12 +1,12 @@
 Cu.import("resource://services-sync/engines/prefs.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 function run_test() {
   let engine = new PrefsEngine();
   let tracker = engine._tracker;
   let prefs = new Preferences();
 
   try {
 
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 let cryptoWrap;
 
 function crypted_resource_handler(metadata, response) {
   let obj = {id: "resource",
              modified: cryptoWrap.modified,
              payload: JSON.stringify(cryptoWrap.payload)};
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 let logger;
 
 let fetched = false;
 function server_open(metadata, response) {
   let body;
--- a/services/sync/tests/unit/test_resource_async.js
+++ b/services/sync/tests/unit/test_resource_async.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 const RES_UPLOAD_URL = "http://localhost:8080/upload";
 const RES_HEADERS_URL = "http://localhost:8080/headers";
 
 let logger;
 
--- a/services/sync/tests/unit/test_service_changePassword.js
+++ b/services/sync/tests/unit/test_service_changePassword.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Resource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
 
   run_next_test();
--- a/services/sync/tests/unit/test_service_detect_upgrade.js
+++ b/services/sync/tests/unit/test_service_detect_upgrade.js
@@ -5,17 +5,17 @@ Cu.import("resource://services-sync/main
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/engines/tabs.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
   
 Engines.register(TabEngine);
 
 add_test(function v4_upgrade() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
--- a/services/sync/tests/unit/test_service_getStorageInfo.js
+++ b/services/sync/tests/unit/test_service_getStorageInfo.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 let collections = {steam:  65.11328,
                    petrol: 82.488281,
                    diesel: 2.25488281};
 
 function run_test() {
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/policies.js");
 
 function login_handling(handler) {
   return function (request, response) {
     if (basic_auth_matches(request, "johndoe", "ilovejane") ||
--- a/services/sync/tests/unit/test_service_migratePrefs.js
+++ b/services/sync/tests/unit/test_service_migratePrefs.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 function test_migrate_logging() {
   _("Testing log pref migration.");
   Svc.Prefs.set("log.appender.debugLog", "Warn");
   Svc.Prefs.set("log.appender.debugLog.enabled", true);
   do_check_true(Svc.Prefs.get("log.appender.debugLog.enabled"));
   do_check_eq(Svc.Prefs.get("log.appender.file.level"), "Trace");
   do_check_eq(Svc.Prefs.get("log.appender.file.logOnSuccess"), false);
--- a/services/sync/tests/unit/test_service_startup.js
+++ b/services/sync/tests/unit/test_service_startup.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 
 function run_test() {
   _("When imported, Service.onStartup is called");
 
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   let guidSvc = new FakeGUIDService();
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 
 function login_handling(handler) {
   return function (request, response) {
     if (basic_auth_matches(request, "johndoe", "ilovejane")) {
       handler(request, response);
--- a/services/sync/tests/unit/test_syncstoragerequest.js
+++ b/services/sync/tests/unit/test_syncstoragerequest.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const STORAGE_REQUEST_RESOURCE_URL = TEST_SERVER_URL + "resource";
 
 function run_test() {
   Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
   initTestLogging();
 
   run_next_test();
--- a/services/sync/tests/unit/test_utils_lazyStrings.js
+++ b/services/sync/tests/unit/test_utils_lazyStrings.js
@@ -1,10 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/stringbundle.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
 
 function run_test() {
     let fn = Utils.lazyStrings("sync");
     do_check_eq(typeof fn, "function");
     let bundle = fn();
     do_check_true(bundle instanceof StringBundle);
     let url = bundle.url;
     do_check_eq(url, "chrome://weave/locale/services/sync.properties");
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,54 +1,41 @@
 [DEFAULT]
-head = head_appinfo.js head_helpers.js head_http_server.js
+head = head_appinfo.js ../../../common/tests/unit/head_helpers.js head_helpers.js head_http_server.js
 tail =
 
-[test_load_modules.js]
-
 # The manifest is roughly ordered from low-level to high-level. When making
 # systemic sweeping changes, this makes it easier to identify errors closer to
 # the source.
 
 # Ensure we can import everything.
 [test_load_modules.js]
 
 # util contains a bunch of functionality used throughout.
-[test_utils_atob.js]
 [test_utils_catch.js]
 [test_utils_deepCopy.js]
 [test_utils_deepEquals.js]
 [test_utils_deferGetSet.js]
 [test_utils_deriveKey.js]
 [test_utils_encodeBase32.js]
 [test_utils_getErrorString.js]
 [test_utils_getIcon.js]
 [test_utils_hkdfExpand.js]
 [test_utils_httpmac.js]
 [test_utils_json.js]
 [test_utils_lazyStrings.js]
 [test_utils_lock.js]
 [test_utils_makeGUID.js]
-[test_utils_makeURI.js]
-[test_utils_namedTimer.js]
 [test_utils_notify.js]
 [test_utils_passphrase.js]
 [test_utils_pbkdf2.js]
 [test_utils_sha1.js]
-[test_utils_stackTrace.js]
-[test_utils_utf8.js]
 
 # We have a number of other libraries that are pretty much standalone.
-[test_Observers.js]
-[test_Preferences.js]
-[test_async_chain.js]
-[test_async_querySpinningly.js]
 [test_httpd_sync_server.js]
-[test_log4moz.js]
-[test_restrequest.js]
 [test_jpakeclient.js]
 # Bug 618233: this test produces random failures on Windows 7.
 # Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
 skip-if = os == "win" || os == "android"
 
 # HTTP layers.
 [test_resource.js]
 [test_resource_async.js]
--- a/services/sync/tps/extensions/tps/modules/addons.jsm
+++ b/services/sync/tps/extensions/tps/modules/addons.jsm
@@ -37,17 +37,17 @@
 
 let EXPORTED_SYMBOLS = ["Addon", "STATE_ENABLED", "STATE_DISABLED"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/AddonRepository.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://tps/logger.jsm");
 
 const ADDONSGETURL = 'http://127.0.0.1:4567/';
 const STATE_ENABLED = 1;
 const STATE_DISABLED = 2;
 
--- a/services/sync/tps/extensions/tps/modules/bookmarks.jsm
+++ b/services/sync/tps/extensions/tps/modules/bookmarks.jsm
@@ -45,17 +45,17 @@ var EXPORTED_SYMBOLS = ["PlacesItem", "B
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://tps/logger.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://gre/modules/PlacesUtils.jsm");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 
 var DumpBookmarks = function TPS_Bookmarks__DumpBookmarks() {
   let writer = {
     value: "",
     write: function PlacesItem__dump__write(aStr, aLen) {
       this.value += aStr;
     }
   };
--- a/services/sync/tps/extensions/tps/modules/history.jsm
+++ b/services/sync/tps/extensions/tps/modules/history.jsm
@@ -44,17 +44,17 @@ var EXPORTED_SYMBOLS = ["HistoryEntry", 
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://gre/modules/PlacesUtils.jsm");
 CU.import("resource://tps/logger.jsm");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 
 var DumpHistory = function TPS_History__DumpHistory() {
   let writer = {
     value: "",
     write: function PlacesItem__dump__write(aStr, aLen) {
       this.value += aStr;
     }
   };
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ b/services/sync/tps/extensions/tps/modules/tps.jsm
@@ -42,17 +42,17 @@
 
 let EXPORTED_SYMBOLS = ["TPS"];
 
 const {classes: CC, interfaces: CI, utils: CU} = Components;
 
 CU.import("resource://services-sync/service.js");
 CU.import("resource://services-sync/constants.js");
 CU.import("resource://services-sync/engines.js");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 CU.import("resource://services-sync/util.js");
 CU.import("resource://gre/modules/XPCOMUtils.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://tps/addons.jsm");
 CU.import("resource://tps/bookmarks.jsm");
 CU.import("resource://tps/logger.jsm");
 CU.import("resource://tps/passwords.jsm");
 CU.import("resource://tps/history.jsm");
--- a/testing/tps/setup.py
+++ b/testing/tps/setup.py
@@ -35,17 +35,17 @@
 #
 # ***** END LICENSE BLOCK *****
 
 import sys
 from setuptools import setup, find_packages
 
 version = '0.2.40'
 
-deps = ['pulsebuildmonitor >= 0.2', 'MozillaPulse == .4', 
+deps = ['pulsebuildmonitor >= 0.62', 'MozillaPulse == 0.61', 
         'mozinfo == 0.3.1', 'mozprofile == 0.1t',
         'mozprocess == 0.1a', 'mozrunner == 3.0a', 'mozregression == 0.3',
         'mozautolog >= 0.2.1']
 
 # we only support python 2.6+ right now
 assert sys.version_info[0] == 2
 assert sys.version_info[1] >= 6
 
--- a/testing/tps/tps/pulse.py
+++ b/testing/tps/tps/pulse.py
@@ -46,55 +46,52 @@ from tps.thread import TPSTestThread
 class TPSPulseMonitor(PulseBuildMonitor):
   """Listens to pulse messages, and initiates a TPS test run when
      a relevant 'build complete' message is received.
   """
 
   def __init__(self, extensionDir, platform='linux', config=None,
                autolog=False, emailresults=False, testfile=None,
                logfile=None, rlock=None, **kwargs):
-    self.buildtype = 'opt'
+    self.buildtype = ['opt']
     self.autolog = autolog
     self.emailresults = emailresults
     self.testfile = testfile
     self.logfile = logfile
     self.rlock = rlock
     self.extensionDir = extensionDir
     self.config = config
-    self.tree = self.config.get('tree', ['services-central', 'places'])
-    self.platform = self.config.get('platform', 'linux')
+    self.tree = self.config.get('tree', ['services-central'])
+    self.platform = [self.config.get('platform', 'linux')]
     self.label=('crossweave@mozilla.com|tps_build_monitor_' +
                 socket.gethostname())
 
     self.logger = logging.getLogger('tps_pulse')
     self.logger.setLevel(logging.DEBUG)
     handler = logging.FileHandler('tps_pulse.log')
     self.logger.addHandler(handler)
 
     PulseBuildMonitor.__init__(self,
-                               tree=self.tree,
+                               trees=self.tree,
                                label=self.label,
-                               mobile=False,
                                logger=self.logger,
+                               platforms=self.platform,
+                               buildtypes=self.buildtype,
+                               builds=True,
                                **kwargs)
 
   def onPulseMessage(self, data):
     key = data['_meta']['routing_key']
     #print key
 
   def onBuildComplete(self, builddata):
     print "================================================================="
     print json.dumps(builddata)
     print "================================================================="
-    try:
-      if not (builddata['platform'] == self.platform and
-              builddata['buildtype'] == self.buildtype):
-        return
-    except KeyError:
-      return
+
     thread = TPSTestThread(self.extensionDir,
                            builddata=builddata,
                            emailresults=self.emailresults,
                            autolog=self.autolog,
                            testfile=self.testfile,
                            logfile=self.logfile,
                            rlock=self.rlock,
                            config=self.config)
--- a/testing/xpcshell/xpcshell.ini
+++ b/testing/xpcshell/xpcshell.ini
@@ -62,16 +62,17 @@ skip-if = os == "android"
 [include:extensions/cookie/test/unit/xpcshell.ini]
 [include:storage/test/unit/xpcshell.ini]
 [include:rdf/tests/unit/xpcshell.ini]
 [include:gfx/tests/unit/xpcshell.ini]
 [include:widget/tests/unit/xpcshell.ini]
 [include:content/base/test/unit/xpcshell.ini]
 [include:content/test/unit/xpcshell.ini]
 [include:toolkit/components/url-classifier/tests/unit/xpcshell.ini]
+[include:services/common/tests/unit/xpcshell.ini]
 [include:services/crypto/tests/unit/xpcshell.ini]
 [include:services/crypto/components/tests/unit/xpcshell.ini]
 [include:services/sync/tests/unit/xpcshell.ini]
 # Bug 676978: tests hang on Android 
 skip-if = os == "android"
 [include:browser/components/dirprovider/tests/unit/xpcshell.ini]
 [include:browser/components/feeds/test/unit/xpcshell.ini]
 [include:browser/components/migration/tests/unit/xpcshell.ini]