cookies

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 20, 2026 License: MIT Imports: 17 Imported by: 0

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:

  1. 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.

  2. 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.

  3. 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

View Source
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.

View Source
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).

View Source
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

View Source
var (
	// ErrHostUnavailable is returned when the native host is not
	// 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.

View Source
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.

View Source
var ErrFrameTooLarge = errors.New("cookies: native-messaging frame exceeds size limit")

ErrFrameTooLarge is returned when a frame's declared length exceeds MaxFrameSize.

View Source
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

func EncodeMessage(m Message) ([]byte, error)

EncodeMessage marshals a Message to frame payload bytes.

func ExtensionOrigin

func ExtensionOrigin(id string) string

ExtensionOrigin formats an allowed-origin entry for a given extension ID, as Chrome expects: "chrome-extension://<id>/".

func FetchHTML

func FetchHTML(ctx context.Context, url string, headers map[string]string) (string, error)

FetchHTML fetches URL via the extension and returns the response body as a string. Headers is optional.

func FetchJSON

func FetchJSON(ctx context.Context, url string, headers map[string]string, dst any) error

FetchJSON fetches URL via the extension and decodes the JSON body into dst. Headers is optional; Accept defaults to application/json.

func HostAvailable

func HostAvailable(ctx context.Context) bool

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

func IsAllowed(host string) bool

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

func IsHostRegistered(hostName string) bool

IsHostRegistered reports false on unsupported platforms.

func ListenIPC

func ListenIPC() (net.Listener, error)

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

func ReadFrame(r io.Reader) ([]byte, error)

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

func RegisterHost(hostName, binaryPath string, allowedOrigins []string) error

RegisterHost returns errUnsupported on unsupported platforms.

func ServeNativeHost

func ServeNativeHost(ctx context.Context, r io.Reader, w io.Writer, handle Handler) error

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

func URLAllowed(rawURL string) bool

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

func UnregisterHost(hostName string) error

UnregisterHost returns errUnsupported on unsupported platforms.

func WriteFrame

func WriteFrame(w io.Writer, payload []byte) error

WriteFrame writes one native-messaging frame to w.

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

func (b *Bridge) Handle(ctx context.Context, m Message, send func(Message) error) error

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

func (b *Bridge) HandlePluginConn(ctx context.Context, conn net.Conn)

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

func (b *Bridge) StartKeepalive(ctx context.Context, interval time.Duration)

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

type Handler func(ctx context.Context, in Message, send func(Message) error) error

Handler reacts to inbound native-messaging messages. send is safe for concurrent use; call it zero or more times per inbound message.

func EchoHandler

func EchoHandler() Handler

EchoHandler returns inbound messages verbatim with Kind="echo". Kept for unit tests of the message loop in isolation.

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

func DecodeMessage(data []byte) (Message, error)

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.

func Fetch

func Fetch(ctx context.Context, r Request) (Response, error)

Fetch dispatches a Request through the extension and returns the raw response. Non-2xx statuses return a *httputil.Error so providers can use the same errors.As(err, *httputil.Error) checks they use for the direct-HTTP fallback.

type StatusInfo

type StatusInfo struct {
	Ready     bool
	UserAgent string
	Version   string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL