Documentation
¶
Overview ¶
Package cookies gives cookie-gated providers (Claude web extras, Cursor, Ollama) a way to call their Cloudflare-protected APIs through Chrome's actual network stack — without ever handling the user's cookies in-process.
A small MV3 companion extension exposes a single capability: "fetch(url) for a narrow allowlist of domains, with credentials". When the plugin wants, say, Claude's overage-spend-limit endpoint, it sends a fetch request to a tiny native host (cmd/native-host) via a local TCP loopback connection; the host relays the request to the extension over Chrome's native-messaging stdin/stdout protocol; the extension issues the fetch — with Chrome's real TLS fingerprint, real User-Agent, and the browser's own cookie jar — and ships the response body back.
Why this shape (fetch-proxy) instead of exposing cookies directly:
- Cloudflare-proof: Chrome's TLS stack and UA. Go's net/http has a distinct JA3 fingerprint; if CF ever starts fingerprinting these endpoints, cookies-out would silently break.
- Smaller blast radius: plugin never sees cf_clearance / sessionKey. Extension doesn't need the "cookies" permission at all — just "nativeMessaging" plus narrow host_permissions.
- Web-Store-friendlier: the extension's purpose is "proxy for 3 specific APIs," not "exfiltrate cookies to a local binary."
Three safety rails this package enforces:
Cookie-gated providers MUST check HostAvailable before firing any request. When the extension isn't installed, Chrome isn't running, or the extension hasn't handshaken this session, they return a quiet "waiting on browser" snapshot instead of a guaranteed-fail request.
HostAvailable means "the extension has said hello this session," not merely "the IPC endpoint is listening." Cold-start (Stream Deck launched before Chrome) stays in the quiet state.
The allowlist of domains this package will fetch is hardcoded in Go (see allowed.go) AND mirrored in the extension's service worker. Adding a provider requires coordinated changes to both plus a new extension release.
Index ¶
- Constants
- Variables
- func EncodeMessage(m Message) ([]byte, error)
- func ExtensionOrigin(id string) string
- func FetchHTML(ctx context.Context, url string, headers map[string]string) (string, error)
- func FetchJSON(ctx context.Context, url string, headers map[string]string, dst any) error
- func HostAvailable(ctx context.Context) bool
- func IPCAddress() string
- func IsAllowed(host string) bool
- func IsHostRegistered(hostName string) bool
- func ListenIPC() (net.Listener, error)
- func LogPath() string
- func MarshalHostManifest(m HostManifest) ([]byte, error)
- func ReadFrame(r io.Reader) ([]byte, error)
- func RegisterHost(hostName, binaryPath string, allowedOrigins []string) error
- func ServeNativeHost(ctx context.Context, r io.Reader, w io.Writer, handle Handler) error
- func URLAllowed(rawURL string) bool
- func UnregisterHost(hostName string) error
- func WriteFrame(w io.Writer, payload []byte) error
- type Bridge
- type Handler
- type HostManifest
- type Message
- type Request
- type Response
- type StatusInfo
Constants ¶
const DefaultExtensionID = "ggablblpfclemapimphpjdhlbhdombnm"
DefaultExtensionID is the deterministic Chrome extension ID derived from the pinned public key in chrome-extension/manifest.json. Chrome computes this ID from SHA-256(SubjectPublicKeyInfo), so the ID is stable across machines, reinstalls, and sideloads — which means the plugin can auto-register the native-messaging manifest without asking the user to paste an ID. The private key that corresponds to this public half is gitignored as chrome-extension-private.pem and is only needed for future Chrome Web Store uploads.
const HostName = "io.github.anthonybaldwin.usagebuttons"
HostName is the native-messaging host identifier. It must match the "name" field in the host manifest and the string extensions pass to chrome.runtime.connectNative (or browser.runtime.connectNative in Firefox).
const MaxFrameSize = 1 << 20 // 1 MiB
MaxFrameSize limits a single native-messaging frame payload. Chrome caps host→extension messages at 1 MiB; we apply the same bound in both directions, which is ample for cookie bundles and prevents a malformed header from triggering a runaway allocation.
Variables ¶
var ( // reachable — extension not installed, browser not running, or the // extension has not yet handshaken this session. Providers should // treat this as a quiet "waiting on browser" state. ErrHostUnavailable = errors.New("cookies: native host unavailable") // ErrOriginNotAllowed is returned when a Request targets a URL // whose host isn't covered by the compile-time allowlist. The // extension enforces the same allowlist independently. ErrOriginNotAllowed = errors.New("cookies: url origin not in allowlist") )
Sentinel errors. Callers should prefer errors.Is over string compare.
var Allowed = []string{
"claude.ai",
"cursor.com",
"ollama.com",
"chatgpt.com",
}
Allowed is the compile-time allowlist of domains the companion extension will fetch for. The extension mirrors this list in its service worker and in manifest.json host_permissions. Changes here require a coordinated extension release.
var ErrFrameTooLarge = errors.New("cookies: native-messaging frame exceeds size limit")
ErrFrameTooLarge is returned when a frame's declared length exceeds MaxFrameSize.
var LogSink func(string)
LogSink is optionally wired by the plugin so this package can emit diagnostic log lines through the Stream Deck connection. Nil is a valid value — lines are silently dropped when it's unset.
Functions ¶
func EncodeMessage ¶
EncodeMessage marshals a Message to frame payload bytes.
func ExtensionOrigin ¶
ExtensionOrigin formats an allowed-origin entry for a given extension ID, as Chrome expects: "chrome-extension://<id>/".
func FetchHTML ¶
FetchHTML fetches URL via the extension and returns the response body as a string. Headers is optional.
func FetchJSON ¶
FetchJSON fetches URL via the extension and decodes the JSON body into dst. Headers is optional; Accept defaults to application/json.
func HostAvailable ¶
HostAvailable returns true only when the native host IPC endpoint is reachable AND the extension has handshaken this session. Gate cookie-gated provider requests on this.
func IPCAddress ¶
func IPCAddress() string
IPCAddress reports the current TCP listener address ("127.0.0.1:<port>") by reading the port file the native host wrote. Returns "" when no listener is published. Used for diagnostic logging from the PI.
func IsAllowed ¶
IsAllowed reports whether host is covered by the allowlist. A host matches if it equals an allowlist entry or is a subdomain of one. Leading dots and case are tolerated.
func IsHostRegistered ¶ added in v0.4.0
IsHostRegistered reports false on unsupported platforms.
func ListenIPC ¶
ListenIPC opens the listener the native host uses to serve the plugin. Binds 127.0.0.1 on an OS-assigned port and atomically publishes that port to ipcPortPath so dialers can discover it. The returned listener removes the port file on Close.
func LogPath ¶
func LogPath() string
LogPath returns a sensible sidecar log path for the native host. We can't log to stdout — the browser owns it.
func MarshalHostManifest ¶
func MarshalHostManifest(m HostManifest) ([]byte, error)
MarshalHostManifest renders the manifest JSON with stable indentation — Chrome is whitespace-tolerant but humans debug these files.
func ReadFrame ¶
ReadFrame reads one Chrome native-messaging frame from r: a 4-byte little-endian length prefix followed by that many bytes of UTF-8 JSON payload. Returns io.EOF cleanly if the reader is closed between frames, and io.ErrUnexpectedEOF if the header or body is truncated.
func RegisterHost ¶
RegisterHost returns errUnsupported on unsupported platforms.
func ServeNativeHost ¶
ServeNativeHost runs Chrome's stdin/stdout native-messaging loop, invoking handle for each inbound message. Returns nil on clean EOF (port closed by browser) and an error on framing or I/O failure.
func URLAllowed ¶
URLAllowed parses rawURL and reports whether its host is in the allowlist and uses an https scheme. Malformed URLs and non-https schemes are rejected.
func UnregisterHost ¶
UnregisterHost returns errUnsupported on unsupported platforms.
Types ¶
type Bridge ¶
type Bridge struct {
// contains filtered or unexported fields
}
Bridge wires the browser extension (over stdin/stdout native messaging) to the plugin (over the local IPC socket). It tracks handshake state, serializes extension writes, and correlates plugin requests to extension replies by the request ID.
func NewBridge ¶
func NewBridge() *Bridge
NewBridge constructs an idle bridge. Wire it into the host by using Handle as the native-messaging Handler and calling HandlePluginConn for each accepted IPC connection.
func (*Bridge) Handle ¶
Handle is the cookies.Handler the native-messaging loop invokes for each message from the extension. It also captures the send closure so inbound plugin requests can forward messages the other way.
func (*Bridge) HandlePluginConn ¶
HandlePluginConn reads one request frame from conn, services it, writes one response frame, and closes.
func (*Bridge) OnExtensionDisconnect ¶
func (b *Bridge) OnExtensionDisconnect()
OnExtensionDisconnect is called when the stdin port closes. It flips ready to false and releases any plugin connections currently waiting on an inflight reply.
func (*Bridge) StartKeepalive ¶ added in v0.5.0
StartKeepalive pings the extension on a fixed interval so Chrome's service worker idle timer (~30s) keeps resetting. Without this the SW suspends, closes the native port, and the host exits — leaving the plugin with no bridge until the next chrome.alarms heartbeat (up to a minute away). Sends are best-effort; missing toExt / write errors are ignored so the ticker keeps running across disconnects.
type Handler ¶
Handler reacts to inbound native-messaging messages. send is safe for concurrent use; call it zero or more times per inbound message.
type HostManifest ¶
type HostManifest struct {
Name string `json:"name"`
Description string `json:"description"`
Path string `json:"path"`
Type string `json:"type"` // always "stdio" for our use
AllowedOrigins []string `json:"allowed_origins"`
}
HostManifest is the JSON shape Chrome expects for a native-messaging host manifest file. See https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host
type Message ¶
type Message struct {
ID string `json:"id,omitempty"`
Kind string `json:"kind"`
URL string `json:"url,omitempty"`
Method string `json:"method,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
// Body carries the base64-encoded request or response body,
// direction implied by Kind. Base64 keeps binary bytes + UTF-8
// text alike JSON-safe.
Body string `json:"body,omitempty"`
Status int `json:"status,omitempty"`
StatusText string `json:"statusText,omitempty"`
ContentType string `json:"contentType,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
Version string `json:"version,omitempty"`
Error string `json:"error,omitempty"`
Ready bool `json:"ready,omitempty"`
}
Message is the tagged-union envelope exchanged between the native host and the extension's service worker (and the plugin ↔ host IPC). Only fields relevant to Kind are populated; the rest serialize as omitempty.
Host → extension kinds: "fetch", "ping". Extension → host kinds: "ready", "fetchResult", "pong", "error". Plugin ↔ host kinds: "status" (Ready flag), "fetch" / "fetchResult" relayed through to/from the extension.
func DecodeMessage ¶
DecodeMessage parses a native-messaging frame payload.
type Request ¶
type Request struct {
URL string
Method string // default "GET"
Headers map[string]string
Body []byte // optional, for POST/PUT
}
Request is what the plugin asks the extension to fetch on its behalf. The browser handles cookies and User-Agent automatically via credentials:"include"; do NOT set a Cookie header explicitly (browsers refuse it anyway).
type Response ¶
type Response struct {
Status int
StatusText string
Body []byte
ContentType string
// UserAgent is the browser's UA at fetch time — informational.
UserAgent string
}
Response carries the extension's fetch result.
type StatusInfo ¶
StatusInfo is a richer snapshot of the bridge state. The PI uses it to display the extension's reported version and drive an update nudge when a newer plugin release is available.
func Status ¶
func Status(ctx context.Context) StatusInfo
Status probes the native host and reports the full handshake state. Not-connected / no-host / IPC-down all return a zero StatusInfo.
func StatusDetail ¶ added in v0.5.0
func StatusDetail(ctx context.Context) (StatusInfo, bool, error)
StatusDetail is like Status but also surfaces whether the native host IPC socket was reachable at all, and the underlying dial/read error when it wasn't. Callers that log diagnostics use this to distinguish "host unreachable / plugin stuck" from "host up, extension not attached" — two failure modes that both produce a zero StatusInfo.