feat: support chrome.storage.sync in extensions (#54088)

* feat: support chrome.storage.sync in extensions

Register a ValueStoreCache for the SYNC settings namespace from
ElectronExtensionsAPIClient::AddAdditionalValueStoreCaches. Until now
Electron never overrode that hook, so StorageFrontend only knew about
LOCAL (and the in-memory SESSION area) and every chrome.storage.sync
call failed with '"sync" is not available in this instance of Chrome'.

ElectronSyncValueStoreCache mirrors Chromium's LocalValueStoreCache but
uses settings_namespace::SYNC, which gives it its own LevelDB directory
under the partition path (kSyncExtensionSettingsDirectoryName), so sync
and local never alias. It enforces the chrome.storage.sync quotas from
extensions/common/api/storage.json (QUOTA_BYTES 102400,
QUOTA_BYTES_PER_ITEM 8192, MAX_ITEMS 512); the per-minute and per-hour
write limits are already applied to kSync by storage_api.cc. There is
no sync service in Electron, so the area behaves like Chrome with sync
disabled: a second local store with no cross-device sync.
chrome.storage.managed stays unsupported because there is no policy
source to back it.

This answers the question raised on #53995 about supporting
chrome.storage instead of dropping sync from the test fixture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFucMLGyVVqSUAKXrqbaUT

* fix: include value_store_factory.h in ElectronExtensionsAPIClient

The linux-x64 and linux-x64-ubsan builds failed compiling
electron_extensions_api_client.cc:

  ../../base/memory/scoped_refptr.h:398:6: error: member access into
  incomplete type 'value_store::ValueStoreFactory'
  ../../base/memory/scoped_refptr.h:405:6: error: member access into
  incomplete type 'value_store::ValueStoreFactory'
  note: in instantiation of ... requested here
    electron_extensions_api_client.cc:97:39:
      new ElectronSyncValueStoreCache(factory);

AddAdditionalValueStoreCaches copies the
scoped_refptr<value_store::ValueStoreFactory> into the new
ElectronSyncValueStoreCache, which instantiates AddRef/Release on the
pointee. extensions_api_client.h and electron_sync_value_store_cache.h
only forward-declare ValueStoreFactory, so the translation unit needs
the full definition. Include components/value_store/value_store_factory.h
the same way chrome_extensions_api_client.cc does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFucMLGyVVqSUAKXrqbaUT

* test: use vitest matchers in chrome.storage.sync spec

main dropped chai in favour of vitest's own matchers and now lints
against chai-style chains, so convert the chrome.storage.sync spec's
assertions to the equivalent vitest matchers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFucMLGyVVqSUAKXrqbaUT

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
claude[bot]andClaude Opus 5.5 authored and GitHub committed 2026-10-23 09:16:18 -04:00
1 parent 0e77ca92cd
commit 2fb6c26db6
9 files changed
+290 -1

No files matched your search

+5 -1
View File
@@ -144,8 +144,12 @@ See [official documentation](https://developer.chrome.com/docs/extensions/refere
The following methods of `chrome.storage` are supported:
- `chrome.storage.local`
- `chrome.storage.sync` (partial support)
- stored on disk next to `chrome.storage.local` in a separate storage area
and subject to Chrome's `sync` quotas; data is **not** synced across
devices, matching Chrome with sync disabled.
`chrome.storage.sync` and `chrome.storage.managed` are **not** supported.
`chrome.storage.managed` is **not** supported.
See [official documentation](https://developer.chrome.com/docs/extensions/reference/storage) for more information.
+2
View File
@@ -862,6 +862,8 @@ filenames = {
"shell/browser/extensions/api/resources_private/resources_private_api.h",
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.cc",
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.h",
"shell/browser/extensions/api/storage/electron_sync_value_store_cache.cc",
"shell/browser/extensions/api/storage/electron_sync_value_store_cache.h",
"shell/browser/extensions/api/streams_private/streams_private_api.cc",
"shell/browser/extensions/api/streams_private/streams_private_api.h",
"shell/browser/extensions/api/tabs/tabs_api.cc",
@@ -0,0 +1,87 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/extensions/api/storage/electron_sync_value_store_cache.h"
#include <utility>
#include "components/value_store/value_store.h"
#include "components/value_store/value_store_factory.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/api/storage/backend_task_runner.h"
#include "extensions/browser/api/storage/settings_namespace.h"
#include "extensions/browser/api/storage/value_store_util.h"
#include "extensions/common/api/storage.h"
#include "extensions/common/extension.h"
namespace extensions {
namespace {
// Returns the quota limits for sync storage, taken from the schema in
// extensions/common/api/storage.json.
SettingsStorageQuotaEnforcer::Limits GetSyncQuotaLimits() {
return {static_cast<size_t>(api::storage::sync::QUOTA_BYTES),
static_cast<size_t>(api::storage::sync::QUOTA_BYTES_PER_ITEM),
static_cast<size_t>(api::storage::sync::MAX_ITEMS)};
}
} // namespace
ElectronSyncValueStoreCache::ElectronSyncValueStoreCache(
scoped_refptr<value_store::ValueStoreFactory> factory)
: storage_factory_(std::move(factory)), quota_(GetSyncQuotaLimits()) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
ElectronSyncValueStoreCache::~ElectronSyncValueStoreCache() {
DCHECK(IsOnBackendSequence());
}
void ElectronSyncValueStoreCache::RunWithValueStoreForExtension(
StorageCallback callback,
scoped_refptr<const Extension> extension) {
DCHECK(IsOnBackendSequence());
// Unlike LOCAL, the unlimitedStorage permission does not lift the sync
// quotas, so the enforcer is always handed out directly.
std::move(callback).Run(GetStorage(extension.get()));
}
void ElectronSyncValueStoreCache::DeleteStorageSoon(
const ExtensionId& extension_id) {
DCHECK(IsOnBackendSequence());
storage_map_.erase(extension_id);
value_store_util::DeleteValueStore(settings_namespace::SYNC,
value_store_util::ModelType::APP,
extension_id, storage_factory_);
value_store_util::DeleteValueStore(settings_namespace::SYNC,
value_store_util::ModelType::EXTENSION,
extension_id, storage_factory_);
}
value_store::ValueStore* ElectronSyncValueStoreCache::GetStorage(
const Extension* extension) {
auto iter = storage_map_.find(extension->id());
if (iter != storage_map_.end())
return iter->second.get();
value_store_util::ModelType model_type =
extension->is_app() ? value_store_util::ModelType::APP
: value_store_util::ModelType::EXTENSION;
std::unique_ptr<value_store::ValueStore> store =
value_store_util::CreateSettingsStore(settings_namespace::SYNC,
model_type, extension->id(),
storage_factory_);
auto storage =
std::make_unique<SettingsStorageQuotaEnforcer>(quota_, std::move(store));
DCHECK(storage);
value_store::ValueStore* storage_ptr = storage.get();
storage_map_[extension->id()] = std::move(storage);
return storage_ptr;
}
} // namespace extensions
@@ -0,0 +1,62 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_EXTENSIONS_API_STORAGE_ELECTRON_SYNC_VALUE_STORE_CACHE_H_
#define ELECTRON_SHELL_BROWSER_EXTENSIONS_API_STORAGE_ELECTRON_SYNC_VALUE_STORE_CACHE_H_
#include <map>
#include <memory>
#include "base/memory/scoped_refptr.h"
#include "extensions/browser/api/storage/settings_storage_quota_enforcer.h"
#include "extensions/browser/api/storage/value_store_cache.h"
#include "extensions/common/extension_id.h"
namespace value_store {
class ValueStore;
class ValueStoreFactory;
} // namespace value_store
namespace extensions {
// ValueStoreCache for the SYNC namespace. Electron has no sync backend, so
// this behaves like Chrome with sync disabled: a second on-disk storage area,
// separate from LOCAL, that enforces the chrome.storage.sync quotas but never
// leaves the device.
class ElectronSyncValueStoreCache : public ValueStoreCache {
public:
explicit ElectronSyncValueStoreCache(
scoped_refptr<value_store::ValueStoreFactory> factory);
ElectronSyncValueStoreCache(const ElectronSyncValueStoreCache&) = delete;
ElectronSyncValueStoreCache& operator=(const ElectronSyncValueStoreCache&) =
delete;
~ElectronSyncValueStoreCache() override;
// ValueStoreCache implementation:
void RunWithValueStoreForExtension(
StorageCallback callback,
scoped_refptr<const Extension> extension) override;
void DeleteStorageSoon(const ExtensionId& extension_id) override;
private:
using StorageMap =
std::map<ExtensionId, std::unique_ptr<value_store::ValueStore>>;
value_store::ValueStore* GetStorage(const Extension* extension);
// The factory to use for creating new ValueStores.
const scoped_refptr<value_store::ValueStoreFactory> storage_factory_;
// Quota limits (see SettingsStorageQuotaEnforcer).
const SettingsStorageQuotaEnforcer::Limits quota_;
// The collection of ValueStores for sync storage.
StorageMap storage_map_;
};
} // namespace extensions
#endif // ELECTRON_SHELL_BROWSER_EXTENSIONS_API_STORAGE_ELECTRON_SYNC_VALUE_STORE_CACHE_H_
@@ -4,14 +4,18 @@
#include "shell/browser/extensions/electron_extensions_api_client.h"
#include <map>
#include <memory>
#include <string>
#include "components/value_store/value_store_factory.h"
#include "extensions/browser/api/storage/settings_namespace.h"
#include "extensions/browser/guest_view/extensions_guest_view_manager_delegate.h"
#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest_delegate.h"
#include "printing/buildflags/buildflags.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/extensions/api/management/electron_management_api_delegate.h"
#include "shell/browser/extensions/api/storage/electron_sync_value_store_cache.h"
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
#include "shell/browser/extensions/electron_messaging_delegate.h"
#include "shell/common/gin_helper/handle.h"
@@ -80,6 +84,20 @@ class ElectronMimeHandlerViewGuestDelegate
ElectronExtensionsAPIClient::ElectronExtensionsAPIClient() = default;
ElectronExtensionsAPIClient::~ElectronExtensionsAPIClient() = default;
void ElectronExtensionsAPIClient::AddAdditionalValueStoreCaches(
content::BrowserContext* context,
const scoped_refptr<value_store::ValueStoreFactory>& factory,
SettingsChangedCallback observer,
std::map<settings_namespace::Namespace,
raw_ptr<ValueStoreCache, CtnExperimental>>* caches) {
// Add support for chrome.storage.sync. There is no sync service in
// Electron, so the cache is backed by a second local store, the same as
// Chrome when sync is disabled. chrome.storage.managed stays unsupported
// because there is no policy source to read from.
(*caches)[settings_namespace::SYNC] =
new ElectronSyncValueStoreCache(factory);
}
MessagingDelegate* ElectronExtensionsAPIClient::GetMessagingDelegate() {
if (!messaging_delegate_)
messaging_delegate_ = std::make_unique<ElectronMessagingDelegate>();
@@ -5,6 +5,7 @@
#ifndef ELECTRON_SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSIONS_API_CLIENT_H_
#define ELECTRON_SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSIONS_API_CLIENT_H_
#include <map>
#include <memory>
#include "extensions/browser/api/extensions_api_client.h"
@@ -19,6 +20,12 @@ class ElectronExtensionsAPIClient : public ExtensionsAPIClient {
~ElectronExtensionsAPIClient() override;
// ExtensionsAPIClient
void AddAdditionalValueStoreCaches(
content::BrowserContext* context,
const scoped_refptr<value_store::ValueStoreFactory>& factory,
SettingsChangedCallback observer,
std::map<settings_namespace::Namespace,
raw_ptr<ValueStoreCache, CtnExperimental>>* caches) override;
MessagingDelegate* GetMessagingDelegate() override;
void AttachWebContentsHelpers(
content::WebContents* web_contents) const override;
+36
View File
@@ -453,6 +453,42 @@ describe('chrome extensions', () => {
w.destroy();
}
});
it('supports chrome.storage.sync as a storage area separate from local', async () => {
const customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage-sync'));
const w = new BrowserWindow({
show: false,
webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false }
});
try {
const p = once(ipcMain, 'storage-sync-result');
await w.loadURL(url);
const [, json] = await p;
const result = JSON.parse(json);
expect(result.error).toBeUndefined();
expect(result.change).toEqual({
key: { newValue: 'sync-value' },
other: { newValue: 'sync-other' }
});
expect(result.sync).toEqual({ key: 'sync-value', other: 'sync-other' });
expect(result.local).toEqual({ key: 'local-value' });
expect(result.syncAfterRemove).toEqual({ key: 'sync-value' });
expect(result.bytesInUse).toBeTypeOf('number');
expect(result.bytesInUse).toBeGreaterThan(0);
expect(result.perItemError).toBeTypeOf('string');
expect(result.perItemError).toContain('kQuotaBytesPerItem quota exceeded');
expect(result.syncAfterClear).toEqual({});
expect(result.localAfterSyncClear).toEqual({ key: 'local-value' });
expect(result.constants).toEqual({
QUOTA_BYTES: 102400,
QUOTA_BYTES_PER_ITEM: 8192,
MAX_ITEMS: 512
});
} finally {
w.destroy();
}
});
});
describe('chrome.webRequest', () => {
+58
View File
@@ -0,0 +1,58 @@
/* global chrome */
const call = (area, method, ...args) =>
new Promise((resolve, reject) => {
area[method](...args, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(result);
}
});
});
const firstSyncChange = new Promise((resolve) => {
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'sync') resolve(changes);
});
});
(async () => {
const { sync, local } = chrome.storage;
const result = {};
try {
await call(sync, 'set', { key: 'sync-value', other: 'sync-other' });
result.change = await firstSyncChange;
await call(local, 'set', { key: 'local-value' });
result.sync = await call(sync, 'get', null);
result.local = await call(local, 'get', null);
await call(sync, 'remove', 'other');
result.syncAfterRemove = await call(sync, 'get', null);
result.bytesInUse = await call(sync, 'getBytesInUse', null);
try {
await call(sync, 'set', { big: 'x'.repeat(sync.QUOTA_BYTES_PER_ITEM) });
result.perItemError = null;
} catch (error) {
result.perItemError = error.message;
}
await call(sync, 'clear');
result.syncAfterClear = await call(sync, 'get', null);
result.localAfterSyncClear = await call(local, 'get', null);
result.constants = {
QUOTA_BYTES: sync.QUOTA_BYTES,
QUOTA_BYTES_PER_ITEM: sync.QUOTA_BYTES_PER_ITEM,
MAX_ITEMS: sync.MAX_ITEMS
};
} catch (error) {
result.error = error.message;
}
const script = document.createElement('script');
script.textContent = `require('electron').ipcRenderer.send('storage-sync-result', ${JSON.stringify(JSON.stringify(result))})`;
document.documentElement.appendChild(script);
})();
@@ -0,0 +1,15 @@
{
"name": "chrome-storage-sync",
"version": "1.0",
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["main.js"],
"run_at": "document_start"
}
],
"permissions": [
"storage"
],
"manifest_version": 2
}