electron/shell/browser/api/electron_api_cookies.cc
electron-roller[bot] 3b421ef77f
chore: bump chromium to 134.0.6998.3 (35-x-y) (#45460)
* chore: bump chromium in DEPS to 134.0.6998.1

* chore: bump chromium in DEPS to 134.0.6998.5

* chore: bump chromium in DEPS to 134.0.6998.3

* chore: bump chromium to 134.0.6988.0 (main) (#45334)

* chore: bump chromium in DEPS to 134.0.6976.0

* chore: update mas_avoid_private_macos_api_usage.patch.patch
https://chromium-review.googlesource.com/c/chromium/src/+/6171046
process_info_mac.cc -> process_info_mac.mm

* chore: update build_do_not_depend_on_packed_resource_integrity.patch
https://chromium-review.googlesource.com/c/chromium/src/+/6196857

* chore: update feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch
https://chromium-review.googlesource.com/c/chromium/src/+/6182296
https://chromium-review.googlesource.com/c/chromium/src/+/6183404
https://chromium-review.googlesource.com/c/chromium/src/+/6187853

A lot changed in the upstream implementation. There's a good chance I got
this wrong as threading has changed and moved some variables into globals.

* chore: remove build_remove_vr_directx_helpers_dependency.patch
https://chromium-review.googlesource.com/c/chromium/src/+/6186102
This landed upstream

* chore: e patches all

* chore: update net::CookieInclusionStatus::ExclusionReason enum
https://chromium-review.googlesource.com/c/chromium/src/+/6183252
https://chromium-review.googlesource.com/c/chromium/src/+/6185544

* chore: update content::WebAuthenticationDelegate import
https://chromium-review.googlesource.com/c/chromium/src/+/6189769

* Revert "chore: disable focus handling test due to win32/ia32 regression"

This reverts commit 1a57ba5d59848d0c841ddda59c9299a4f957452a.

* chore: bump chromium in DEPS to 134.0.6978.0

* chore: bump chromium in DEPS to 134.0.6980.0

* chore: bump chromium in DEPS to 134.0.6982.0

* chore: bump chromium in DEPS to 134.0.6984.0

* 6196281: Allow direct embedder IsPdfInternalPluginAllowedOrigin() interaction
https://chromium-review.googlesource.com/c/chromium/src/+/6196281

* 6196283: Delete PdfInternalPluginDelegate
https://chromium-review.googlesource.com/c/chromium/src/+/6196283

* chore: update patches

* chore: bump chromium in DEPS to 134.0.6986.0

* chore: update patches

* 6205762: Support option to use window.showSaveFilePicker() in PDF attachment code
https://chromium-review.googlesource.com/c/chromium/src/+/6205762

See also:
* https://issues.chromium.org/issues/373852607
* 5939153: [PDF] Add PdfUseShowSaveFilePicker feature flag | https://chromium-review.googlesource.com/c/chromium/src/+/5939153
* 6205761: Delete spurious Ink-specific code in pdf_viewer.ts | https://chromium-review.googlesource.com/c/chromium/src/+/6205761

* 6209609: Remove WebVector: Automatic changes
https://chromium-review.googlesource.com/c/chromium/src/+/6209609

* 6205488: UI: make QT5 optional
https://chromium-review.googlesource.com/c/chromium/src/+/6205488

* 6178281: Rename pak files from branding strings
https://chromium-review.googlesource.com/c/chromium/src/+/6178281

* fixup! 6209609: Remove WebVector: Automatic changes https://chromium-review.googlesource.com/c/chromium/src/+/6209609

* 6193249: Switch from safe_browsing::EventResult to enterprise_connectors:EventResult
https://chromium-review.googlesource.com/c/chromium/src/+/6193249

* 6197457: Remove Pause/ResumeReadingBodyFromNet IPCs
https://chromium-review.googlesource.com/c/chromium/src/+/6197457

* 6191230: Record total time spent on a picture in picture window
https://chromium-review.googlesource.com/c/chromium/src/+/6191230

* chore: bump chromium in DEPS to 134.0.6988.0

* chore: update patches

* 6215440: Remove base/ranges/.
https://chromium-review.googlesource.com/c/chromium/src/+/6215440

* Disable unsafe buffers error

Not sure what changed, but we're now seeing unsafe buffer errors in Chromium code, at least when using reclient. Will update this comment if we find out the cause.

* 6187853: SelectFileDialogLinuxPortal: Use dbus_xdg::Request and DbusType
https://chromium-review.googlesource.com/c/chromium/src/+/6187853

* fix `setDisplayMediaRequestHandler` test

Given how this test is written, I would expect this assertion to be false. It seems the oppositue was true before, but that was also acknowledged to be suprising. Seems that the underlying implementation is now fixed and works as expected.

* fixup! 6187853: SelectFileDialogLinuxPortal: Use dbus_xdg::Request and DbusType https://chromium-review.googlesource.com/c/chromium/src/+/6187853

* chore: udpate patches

* Multiple PRS: https://chromium-review.googlesource.com/c/chromium/src/+/6185544 | https://chromium-review.googlesource.com/c/chromium/src/+/6183252

* fix: cast enum class to numeric type

* fix: add 1 to MAX_EXCLUSION_REASON because enum values are zero-based, and we want the total count of reasons.

* Reapply "chore: disable focus handling test due to win32/ia32 regression"

This reverts commit 760b1a519b5919b483c66bc3096eeefb4d7011f4.

* refactor: use ExclusionReasonBitset::kValueCount for size

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: Samuel Maddock <smaddock@slack-corp.com>
Co-authored-by: clavin <clavin@electronjs.org>
Co-authored-by: alice <alice@makenotion.com>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
(cherry picked from commit 213165a467b84b3fb979a869d9bf10ad21e2d78e)

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
2025-02-07 11:14:12 -05:00

487 lines
19 KiB
C++

// Copyright (c) 2015 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/api/electron_api_cookies.h"
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include "base/containers/fixed_flat_map.h"
#include "base/time/time.h"
#include "base/values.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "gin/dictionary.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_change_dispatcher.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/cookie_store.h"
#include "net/cookies/cookie_util.h"
#include "shell/browser/cookie_change_notifier.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/promise.h"
namespace gin {
template <>
struct Converter<net::CookieSameSite> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const net::CookieSameSite& val) {
switch (val) {
case net::CookieSameSite::UNSPECIFIED:
return ConvertToV8(isolate, "unspecified");
case net::CookieSameSite::NO_RESTRICTION:
return ConvertToV8(isolate, "no_restriction");
case net::CookieSameSite::LAX_MODE:
return ConvertToV8(isolate, "lax");
case net::CookieSameSite::STRICT_MODE:
return ConvertToV8(isolate, "strict");
}
DCHECK(false);
return ConvertToV8(isolate, "unknown");
}
};
template <>
struct Converter<net::CanonicalCookie> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const net::CanonicalCookie& val) {
gin::Dictionary dict(isolate, v8::Object::New(isolate));
dict.Set("name", val.Name());
dict.Set("value", val.Value());
dict.Set("domain", val.Domain());
dict.Set("hostOnly", net::cookie_util::DomainIsHostOnly(val.Domain()));
dict.Set("path", val.Path());
dict.Set("secure", val.SecureAttribute());
dict.Set("httpOnly", val.IsHttpOnly());
dict.Set("session", !val.IsPersistent());
if (val.IsPersistent())
dict.Set("expirationDate", val.ExpiryDate().InSecondsFSinceUnixEpoch());
dict.Set("sameSite", val.SameSite());
return ConvertToV8(isolate, dict).As<v8::Object>();
}
};
template <>
struct Converter<net::CookieChangeCause> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const net::CookieChangeCause& val) {
switch (val) {
case net::CookieChangeCause::INSERTED:
case net::CookieChangeCause::EXPLICIT:
return gin::StringToV8(isolate, "explicit");
case net::CookieChangeCause::OVERWRITE:
return gin::StringToV8(isolate, "overwrite");
case net::CookieChangeCause::EXPIRED:
return gin::StringToV8(isolate, "expired");
case net::CookieChangeCause::EVICTED:
return gin::StringToV8(isolate, "evicted");
case net::CookieChangeCause::EXPIRED_OVERWRITE:
return gin::StringToV8(isolate, "expired-overwrite");
default:
return gin::StringToV8(isolate, "unknown");
}
}
};
} // namespace gin
namespace electron::api {
namespace {
// Returns whether |domain| matches |filter|.
bool MatchesDomain(std::string filter, const std::string& domain) {
// Add a leading '.' character to the filter domain if it doesn't exist.
if (net::cookie_util::DomainIsHostOnly(filter))
filter.insert(0, ".");
std::string sub_domain(domain);
// Strip any leading '.' character from the input cookie domain.
if (!net::cookie_util::DomainIsHostOnly(sub_domain))
sub_domain = sub_domain.substr(1);
// Now check whether the domain argument is a subdomain of the filter domain.
for (sub_domain.insert(0, "."); sub_domain.length() >= filter.length();) {
if (sub_domain == filter)
return true;
const size_t next_dot = sub_domain.find('.', 1); // Skip over leading dot.
sub_domain.erase(0, next_dot);
}
return false;
}
// Returns whether |cookie| matches |filter|.
bool MatchesCookie(const base::Value::Dict& filter,
const net::CanonicalCookie& cookie) {
const std::string* str;
if ((str = filter.FindString("name")) && *str != cookie.Name())
return false;
if ((str = filter.FindString("path")) && *str != cookie.Path())
return false;
if ((str = filter.FindString("domain")) &&
!MatchesDomain(*str, cookie.Domain()))
return false;
std::optional<bool> secure_filter = filter.FindBool("secure");
if (secure_filter && *secure_filter != cookie.SecureAttribute())
return false;
std::optional<bool> session_filter = filter.FindBool("session");
if (session_filter && *session_filter == cookie.IsPersistent())
return false;
std::optional<bool> httpOnly_filter = filter.FindBool("httpOnly");
if (httpOnly_filter && *httpOnly_filter != cookie.IsHttpOnly())
return false;
return true;
}
// Remove cookies from |list| not matching |filter|, and pass it to |callback|.
void FilterCookies(base::Value::Dict filter,
gin_helper::Promise<net::CookieList> promise,
const net::CookieList& cookies) {
net::CookieList result;
for (const auto& cookie : cookies) {
if (MatchesCookie(filter, cookie))
result.push_back(cookie);
}
promise.Resolve(result);
}
void FilterCookieWithStatuses(
base::Value::Dict filter,
gin_helper::Promise<net::CookieList> promise,
const net::CookieAccessResultList& list,
const net::CookieAccessResultList& excluded_list) {
FilterCookies(std::move(filter), std::move(promise),
net::cookie_util::StripAccessResults(list));
}
// Parse dictionary property to CanonicalCookie time correctly.
base::Time ParseTimeProperty(const std::optional<double>& value) {
if (!value) // empty time means ignoring the parameter
return {};
if (*value == 0) // FromSecondsSinceUnixEpoch would convert 0 to empty Time
return base::Time::UnixEpoch();
return base::Time::FromSecondsSinceUnixEpoch(*value);
}
const std::string InclusionStatusToString(net::CookieInclusionStatus status) {
// See net/cookies/cookie_inclusion_status.h for cookie error descriptions.
using Reason = net::CookieInclusionStatus::ExclusionReason;
static constexpr auto Reasons =
base::MakeFixedFlatMap<Reason, std::string_view>(
{{Reason::EXCLUDE_UNKNOWN_ERROR, "Unknown error"},
{Reason::EXCLUDE_HTTP_ONLY,
"The cookie was HttpOnly, but the attempted access was through a "
"non-HTTP API."},
{Reason::EXCLUDE_SECURE_ONLY,
"The cookie was Secure, but the URL was not allowed to access "
"Secure cookies."},
{Reason::EXCLUDE_DOMAIN_MISMATCH,
"The cookie's domain attribute did not match the domain of the URL "
"attempting access."},
{Reason::EXCLUDE_NOT_ON_PATH,
"The cookie's path attribute did not match the path of the URL "
"attempting access."},
{Reason::EXCLUDE_SAMESITE_STRICT,
"The cookie had SameSite=Strict, and the attempted access did not "
"have an appropriate SameSiteCookieContext"},
{Reason::EXCLUDE_SAMESITE_LAX,
"The cookie had SameSite=Lax, and the attempted access did not "
"have "
"an appropriate SameSiteCookieContext"},
{Reason::EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX,
"The cookie did not specify a SameSite attribute, and therefore "
"was treated as if it were SameSite=Lax, and the attempted "
"access did not have an appropriate SameSiteCookieContext."},
{Reason::EXCLUDE_SAMESITE_NONE_INSECURE,
"The cookie specified SameSite=None, but it was not Secure."},
{Reason::EXCLUDE_USER_PREFERENCES,
"Caller did not allow access to the cookie."},
{Reason::EXCLUDE_FAILURE_TO_STORE,
"The cookie was malformed and could not be stored"},
{Reason::EXCLUDE_NONCOOKIEABLE_SCHEME,
"Attempted to set a cookie from a scheme that does not support "
"cookies."},
{Reason::EXCLUDE_OVERWRITE_SECURE,
"The cookie would have overwritten a Secure cookie, and was not "
"allowed to do so."},
{Reason::EXCLUDE_OVERWRITE_HTTP_ONLY,
"The cookie would have overwritten an HttpOnly cookie, and was not "
"allowed to do so."},
{Reason::EXCLUDE_INVALID_DOMAIN,
"The cookie was set with an invalid Domain attribute."},
{Reason::EXCLUDE_INVALID_PREFIX,
"The cookie was set with an invalid __Host- or __Secure- prefix."},
{Reason::EXCLUDE_INVALID_PARTITIONED,
"Cookie was set with an invalid Partitioned attribute, which is "
"only valid if the cookie has a __Host- prefix."},
{Reason::EXCLUDE_NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE,
"The cookie exceeded the name/value pair size limit."},
{Reason::EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE,
"Cookie exceeded the attribute size limit."},
{Reason::EXCLUDE_DOMAIN_NON_ASCII,
"The cookie was set with a Domain attribute containing non ASCII "
"characters."},
{Reason::EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET,
"The cookie is blocked by third-party cookie blocking but the two "
"sites are in the same First-Party Set"},
{Reason::EXCLUDE_PORT_MISMATCH,
"The cookie's source_port did not match the port of the request."},
{Reason::EXCLUDE_SCHEME_MISMATCH,
"The cookie's source_scheme did not match the scheme of the "
"request."},
{Reason::EXCLUDE_SHADOWING_DOMAIN,
"The cookie is a domain cookie and has the same name as an origin "
"cookie on this origin."},
{Reason::EXCLUDE_DISALLOWED_CHARACTER,
"The cookie contains ASCII control characters"},
{Reason::EXCLUDE_THIRD_PARTY_PHASEOUT,
"The cookie is blocked for third-party cookie phaseout."},
{Reason::EXCLUDE_NO_COOKIE_CONTENT,
"The cookie contains no content or only whitespace."},
{Reason::EXCLUDE_ALIASING,
"Cookie aliases that of another with a different source_port or "
"source scheme. I.e.: Two or more cookies share the same name "
"but have different ports/schemes."}});
static_assert(
Reasons.size() ==
net::CookieInclusionStatus::ExclusionReasonBitset::kValueCount,
"Please ensure all ExclusionReason variants are enumerated in "
"GetDebugString");
std::ostringstream reason;
reason << "Failed to set cookie - ";
for (const auto& [val, str] : Reasons) {
if (status.HasExclusionReason(val)) {
reason << str;
}
}
reason << status.GetDebugString();
return reason.str();
}
std::string StringToCookieSameSite(const std::string* str_ptr,
net::CookieSameSite* same_site) {
if (!str_ptr) {
*same_site = net::CookieSameSite::LAX_MODE;
return "";
}
const std::string& str = *str_ptr;
if (str == "unspecified") {
*same_site = net::CookieSameSite::UNSPECIFIED;
} else if (str == "no_restriction") {
*same_site = net::CookieSameSite::NO_RESTRICTION;
} else if (str == "lax") {
*same_site = net::CookieSameSite::LAX_MODE;
} else if (str == "strict") {
*same_site = net::CookieSameSite::STRICT_MODE;
} else {
return "Failed to convert '" + str +
"' to an appropriate cookie same site value";
}
return "";
}
} // namespace
gin::WrapperInfo Cookies::kWrapperInfo = {gin::kEmbedderNativeGin};
Cookies::Cookies(ElectronBrowserContext* browser_context)
: browser_context_{browser_context} {
cookie_change_subscription_ =
browser_context_->cookie_change_notifier()->RegisterCookieChangeCallback(
base::BindRepeating(&Cookies::OnCookieChanged,
base::Unretained(this)));
}
Cookies::~Cookies() = default;
v8::Local<v8::Promise> Cookies::Get(v8::Isolate* isolate,
const gin_helper::Dictionary& filter) {
gin_helper::Promise<net::CookieList> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
base::Value::Dict dict;
gin::ConvertFromV8(isolate, filter.GetHandle(), &dict);
std::string url;
filter.Get("url", &url);
if (url.empty()) {
manager->GetAllCookies(
base::BindOnce(&FilterCookies, std::move(dict), std::move(promise)));
} else {
net::CookieOptions options;
options.set_include_httponly();
options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext::MakeInclusive());
options.set_do_not_update_access_time();
manager->GetCookieList(GURL(url), options,
net::CookiePartitionKeyCollection::Todo(),
base::BindOnce(&FilterCookieWithStatuses,
std::move(dict), std::move(promise)));
}
return handle;
}
v8::Local<v8::Promise> Cookies::Remove(v8::Isolate* isolate,
const GURL& url,
const std::string& name) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto cookie_deletion_filter = network::mojom::CookieDeletionFilter::New();
cookie_deletion_filter->url = url;
cookie_deletion_filter->cookie_name = name;
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
manager->DeleteCookies(
std::move(cookie_deletion_filter),
base::BindOnce(
[](gin_helper::Promise<void> promise, uint32_t num_deleted) {
gin_helper::Promise<void>::ResolvePromise(std::move(promise));
},
std::move(promise)));
return handle;
}
v8::Local<v8::Promise> Cookies::Set(v8::Isolate* isolate,
base::Value::Dict details) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
const std::string* url_string = details.FindString("url");
if (!url_string) {
promise.RejectWithErrorMessage("Missing required option 'url'");
return handle;
}
const std::string* name = details.FindString("name");
const std::string* value = details.FindString("value");
const std::string* domain = details.FindString("domain");
const std::string* path = details.FindString("path");
bool http_only = details.FindBool("httpOnly").value_or(false);
const std::string* same_site_string = details.FindString("sameSite");
net::CookieSameSite same_site;
std::string error = StringToCookieSameSite(same_site_string, &same_site);
if (!error.empty()) {
promise.RejectWithErrorMessage(error);
return handle;
}
bool secure = details.FindBool("secure").value_or(
same_site == net::CookieSameSite::NO_RESTRICTION);
GURL url(url_string ? *url_string : "");
if (!url.is_valid()) {
net::CookieInclusionStatus cookie_inclusion_status;
cookie_inclusion_status.AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
promise.RejectWithErrorMessage(
InclusionStatusToString(cookie_inclusion_status));
return handle;
}
net::CookieInclusionStatus status;
auto canonical_cookie = net::CanonicalCookie::CreateSanitizedCookie(
url, name ? *name : "", value ? *value : "", domain ? *domain : "",
path ? *path : "", ParseTimeProperty(details.FindDouble("creationDate")),
ParseTimeProperty(details.FindDouble("expirationDate")),
ParseTimeProperty(details.FindDouble("lastAccessDate")), secure,
http_only, same_site, net::COOKIE_PRIORITY_DEFAULT, std::nullopt,
&status);
if (!canonical_cookie || !canonical_cookie->IsCanonical()) {
net::CookieInclusionStatus cookie_inclusion_status;
cookie_inclusion_status.AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_FAILURE_TO_STORE);
promise.RejectWithErrorMessage(InclusionStatusToString(
!status.IsInclude() ? status : cookie_inclusion_status));
return handle;
}
net::CookieOptions options;
if (http_only) {
options.set_include_httponly();
}
options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext::MakeInclusive());
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
manager->SetCanonicalCookie(
*canonical_cookie, url, options,
base::BindOnce(
[](gin_helper::Promise<void> promise, net::CookieAccessResult r) {
if (r.status.IsInclude()) {
promise.Resolve();
} else {
promise.RejectWithErrorMessage(InclusionStatusToString(r.status));
}
},
std::move(promise)));
return handle;
}
v8::Local<v8::Promise> Cookies::FlushStore(v8::Isolate* isolate) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
manager->FlushCookieStore(base::BindOnce(
gin_helper::Promise<void>::ResolvePromise, std::move(promise)));
return handle;
}
void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
Emit("changed", gin::ConvertToV8(isolate, change.cookie),
gin::ConvertToV8(isolate, change.cause),
gin::ConvertToV8(isolate,
change.cause != net::CookieChangeCause::INSERTED));
}
// static
gin::Handle<Cookies> Cookies::Create(v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {
return gin::CreateHandle(isolate, new Cookies{browser_context});
}
gin::ObjectTemplateBuilder Cookies::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin_helper::EventEmitterMixin<Cookies>::GetObjectTemplateBuilder(
isolate)
.SetMethod("get", &Cookies::Get)
.SetMethod("remove", &Cookies::Remove)
.SetMethod("set", &Cookies::Set)
.SetMethod("flushStore", &Cookies::FlushStore);
}
const char* Cookies::GetTypeName() {
return "Cookies";
}
} // namespace electron::api