feed

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package feed asks a third party whether a destination is malicious.

It is the one place in this program that sends a user's destination off the box, and everything about it is shaped by that. Read the four properties below before changing anything here; each is a promise made somewhere an operator can read it, and three of them are asserted by tests that will fail rather than explain themselves.

**It does not exist unless an operator names a feed.** New returns a nil Client when LINKCTRL_FEED_URL is empty, and a nil Client is not a disabled client — it is no client at all, held in a nil interface by internal/link.Service, with no branch anywhere that could be got wrong. The default instance therefore has no code path that reaches the network with a destination in it, which is what "zero destination URLs leave the instance" means when it is asserted rather than promised.

**It answers a question and never an instruction.** Check reports Malicious or not. It cannot say "allow this", it has no way to name a tier, and internal/link stamps every verdict it produces TierLowConfidence — the same confinement the heuristics have, for the same reason. A feed that could promote its own answer would be a third party writing into a tier this product tells operators costs a rebuild to overrule.

**A failure is not a refusal.** Every error path — a timeout, a 500, a response that will not parse, a body that is too long — returns ResultError and the caller carries on to accept the destination. The built-in tiers have already had their say by then, so failing open loses the feed's opinion and nothing else. The alternative is a third party's outage deciding that this instance may not create links, which is precisely the dependency the opt-in exists to keep out of the default deployment.

**It sends the destination and nothing else.** No identity, no workspace, no address, no instance name. The disclosure page tells users their destinations are sent; anything beyond that would make that sentence untrue.

Index

Constants

View Source
const (
	MethodGET  = "GET"
	MethodPOST = "POST"
)

Methods a feed endpoint may be called with. Two, because a reputation API either takes the URL in the query string or in a JSON body, and a third option would be a compatibility matrix rather than a feature.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client checks destinations against one configured feed.

func New

func New(cfg Config) (*Client, error)

New builds a client, or returns nil when no feed is configured.

A nil *Client with a nil error is the ordinary result on a default instance, and callers store it in an interface that they then test for nil. It is not an error state and it is not logged as one.

func (*Client) Check

func (c *Client) Check(ctx context.Context, destination string) (Result, error)

Check asks the feed about one destination.

The destination is the entire payload. Returns ResultError with the cause for anything that is not a usable answer, and the caller is required to treat that as "no opinion" rather than as a refusal — see the package comment.

func (*Client) Describe

func (c *Client) Describe() Disclosure

Describe reports what this instance does with destinations. Safe on a nil client, which is the default instance and reports Enabled false.

func (*Client) Endpoint

func (c *Client) Endpoint() string

Endpoint is where destinations are sent, for the disclosure. Safe on a nil client.

Assembled from the parts of the configured URL that are safe to show — scheme, host, path — rather than by removing the parts known not to be. That inversion is finding F35. The first version cut everything from the first "?" or "#", which is a denylist, and a denylist is only as complete as the list: it missed `https://apikey:SECRET@feed.example/v1/check`, which Go's client really does send as Basic auth, so the credential worked *and* was printed verbatim to every signed-in user on /feeds. Building from an allowlist cannot miss a spelling nobody thought of, including one a future net/url field introduces.

**The path is kept, deliberately.** It is most of what "where your destinations go" means, and dropping it would pay for this fix with the disclosure's precision. A credential written into a path segment is indistinguishable from a path and survives this — which is why config validation refuses userinfo outright rather than relying on the redaction here, and why docs/configuration.md tells the operator that the path and the host are shown.

func (*Client) Name

func (c *Client) Name() string

Name is the third party, for the disclosure. Safe on a nil client.

type Config

type Config struct {
	// Name is the third party, in words, for the disclosure. Required whenever
	// URL is set: a page that says "your destinations are sent to a third party"
	// without saying which one is not a disclosure.
	Name string
	// URL is the endpoint, and the switch. Empty means no feed.
	URL string
	// Method is GET or POST.
	Method string
	// Param names the field carrying the destination — a query parameter on GET,
	// a JSON object key on POST.
	Param string
	// AuthHeader and AuthToken authenticate. Both or neither; the header is only
	// set when the token is non-empty.
	AuthHeader string
	AuthToken  string
	// VerdictField is the dotted path into the response JSON holding the
	// verdict. "data.malicious" reads {"data":{"malicious":true}}.
	VerdictField string
	// Timeout bounds one check end to end. It is spent inside a link creation
	// somebody is waiting on, so it is small and it is enforced here as well as
	// by the caller's context.
	Timeout time.Duration
	// Transport is for tests. Nil uses a transport built here, which is the only
	// one production ever has.
	Transport http.RoundTripper
}

Config is the generic HTTP adapter, as an operator configures it.

One adapter and not a plugin system. Which feeds get first-class support is a product call nobody has made, and shipping a named integration would be choosing one; a generic POST-a-URL-get-a-boolean adapter covers the shape every reputation API has and commits this product to none of them.

type Disclosure

type Disclosure struct {
	// Enabled is false on a default instance, and false is the whole of *this*
	// channel's disclosure: no destination reaches a feed.
	//
	// It is not the whole disclosure, and reading it as one is F135. A webhook a
	// workspace registered is a second channel with no operator setting anywhere
	// in its path, so the page and the API report both — see
	// link.DestinationDisclosure, which embeds this type rather than replacing
	// it.
	Enabled bool `json:"enabled"`
	// Name is the third party. Empty when disabled.
	Name string `json:"third_party,omitempty"`
	// Endpoint is where destinations are sent: scheme, host and path, and
	// nothing else. A feed URL often carries an API key in its query string or
	// its userinfo, and this is shown to every signed-in user rather than to the
	// operator alone. See Endpoint for why it is built up rather than cut down.
	Endpoint string `json:"endpoint,omitempty"`
	// Method is GET or POST.
	Method string `json:"method,omitempty"`
	// TimeoutSeconds is how long a check may take before it fails open.
	TimeoutSeconds float64 `json:"timeout_seconds,omitempty"`
}

Disclosure is what an instance tells the people using it about this feature.

It is built from the live client rather than from configuration, so the page cannot describe a feed the service is not actually using — a disclosure assembled from the environment would keep saying "on" for a client that failed to construct, which is the one direction it must never be wrong in.

Every field is on the JSON API as well as the page. This is a statement about what happens to somebody's data; a person who reads it through a client is owed the same answer as one who reads it in a browser.

type Result

type Result string

Result is what one check produced. Counted by label, so the vocabulary is fixed here rather than assembled at the call site.

const (
	// ResultClean means the feed answered and did not object.
	ResultClean Result = "clean"
	// ResultMalicious means the feed answered and objected.
	ResultMalicious Result = "malicious"
	// ResultError means the feed did not answer usefully. The caller fails open.
	ResultError Result = "error"
)

Jump to

Keyboard shortcuts

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