security

package
v0.2.6 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package security is the compiled source for docs/guide/security.md.

It is one small chat room, chosen because a room is the smallest application with a real authorization question in it: some identities may post, some may only watch, and one of those refusals has to be visible to the person it refuses while the other must not be bypassable by anyone.

Index

Constants

View Source
const (
	// EventPost is a message the composer sends.
	EventPost = "room.post"
	// EventPurge clears the room.
	EventPurge = "room.purge"

	// FieldBody is the composer's form field.
	FieldBody = "body"

	// FragmentLog is the message list.
	FragmentLog = "room.log"
	// FragmentNotice is the one-line region a refusal is rendered into. It
	// exists because a denial has to have somewhere to appear.
	FragmentNotice = "room.notice"
)
View Source
const CSP = "default-src 'none'; " +
	"script-src 'self'; " +
	"style-src 'self'; " +
	"connect-src 'self'; " +
	"img-src 'self' data:; " +
	"object-src 'none'; " +
	"base-uri 'none'; " +
	"frame-ancestors 'none'"

CSP is the Content-Security-Policy the client runtime is known to work under, byte for byte: it is the policy test/internal/conformance's CP1-13 spec serves in front of a real application and drives with a real browser, asserting zero securitypolicyviolation events while the runtime boots, opens its WebSocket and patches the DOM.

Two clauses are worth reading twice:

  • connect-src 'self' is what the live WebSocket needs, and 'self' means the PAGE's origin. If the live handler is served from a different origin than the page, this clause has to name that origin explicitly — the library cannot help, because the browser is enforcing a policy about a URL your own page constructed.
  • script-src has no 'unsafe-eval' and needs none. The runtime contains no eval and no new Function, which is PRD NFR-4 and is scanned in CI over the shipped, minified artifact rather than over the sources.
View Source
const ObserverRefusal = "you are an observer in this room and may not post"

ObserverRefusal is what a read-only participant is told, in the words they see. It is a constant because the reducer sets it and a spec asserts on it, and a message asserted against a copy of itself asserts nothing.

Variables

This section is empty.

Functions

func Authorize

func Authorize(_ context.Context, sess live.Session[Member], ev live.Event) error

Authorize is the enforcement half of the rule, and it is deliberately not where the observer's refusal lives.

It runs before the reducer, for every event, at the single mailbox ingress, so a new event name cannot skip it. That is also why a denial here cannot be rendered: a *live.DenyError rejects the event before the reducer runs, so there is no transition, so there is no render, so there is nothing for the user to see. The library has no application hook that can render a denial — there is no patch hook, by design.

So the rules divide by whether the person refused has to be told:

  • An identity that is not a Member of this application is a session that should not be open: *live.FatalDenyError, and the connection closes with 4006 UNAUTHORIZED. Nobody needs to read an explanation of that.
  • Purging is a moderator's button and is not rendered for anyone else, so an event asking to purge did not come from a rendered control: *live.DenyError, the event is dropped, the session continues.
  • Posting as an observer is refused in the reducer instead, where it can be rendered — see Reduce. This hook lets it through on purpose.

Any error that is neither type is treated as a DenyError. An authorization hook that failed open on a shape it did not anticipate would have the one failure mode an authorization hook must not have.

func Config

func Config(room *Room, origins []string) live.Config[State, Member]

Config is the application. All four security fields are required — there is no nil that means "off" — so turning a check off is something written down and greppable.

func Origins

func Origins() []string

Origins is the whole of the allowlist for an application served from one public origin, and a literal list is the recommendation.

An entry is compared against the browser's Origin header with strings.EqualFold and nothing else: no prefix match, no wildcard, no normalisation. So an entry is exactly scheme + "://" + host, with the port only when the URL has one, and never a trailing slash and never a path. "https://app.example.com/" is a different string from what any browser sends and matches nothing.

Deny by default is the library's rule: a request whose Origin is absent is refused too, because an absent Origin is not an allowed one.

func Reducer

func Reducer(room *Room) live.Reducer[State, Member]

Reducer is the visible half of the rule.

The observer's refusal is here, not in Authorize, because this is the only place in the library where a refusal can become markup: the reducer returns a state whose Notice field is rendered by FragmentNotice, and the browser sees a sentence rather than an event that vanished.

The function it returns is pure: it performs no I/O, reads no clock, and returns the write it wants as a value for the library to perform.

func WithCSP

func WithCSP(next http.Handler) http.Handler

WithCSP sends the policy on every response, including the upgrade.

The library sends no security header of its own and will not start: which headers an application sends is the application's decision, and a library that set them would be overriding a policy it cannot see the rest of.

Types

type Member

type Member struct {
	Name string
	Role Role
}

Member is the application's identity. It satisfies live.IIdentity, is bound at the handshake by Authenticate, and is immutable for the connection's life: a session cannot outlive its connection, so there is no re-authentication and no privilege change mid-session. A role change takes effect on the next connection.

func Authenticate

func Authenticate(r *http.Request) (Member, error)

Authenticate derives the identity from the upgrade request. It runs on the HTTP request, before any per-session memory is allocated, and a failure is a 401 rather than a close code.

A real application reads whatever it already trusts here — a session cookie, a bearer token — and turns it into a live.IIdentity. This one reads a header so the sample has no session store in it.

func (Member) Subject

func (m Member) Subject() string

Subject is the stable, non-secret identifier the library logs and counts sessions against. It must not be a token: it reaches the provenance log, the per-identity session limit, and every span attribute for the session.

type Role

type Role int

Role is what an identity may do in the room.

const (
	// RoleObserver may read and may not post. Its refusal has to be SEEN.
	RoleObserver Role = iota
	// RoleMember may post.
	RoleMember
	// RoleModerator may post and may purge.
	RoleModerator
)

type Room

type Room struct{ Posted []string }

Room is the application-owned store the effect writes to.

func (*Room) PostEffect

func (r *Room) PostEffect(author, body string) live.Effect[Member]

PostEffect is the write that leaves the process, and it enforces the same rule a third time.

This is not redundant with the reducer. The reducer's refusal is what a reader SEES; this one is what a reader cannot get past, and it is here because an effect is reachable from anywhere a reducer can be wrong — a new event name, a refactor, a branch nobody replayed. The identity is a parameter of Run rather than something to fish out of a context, which is what makes an effect that forgot to ask impossible to write.

type State

type State struct {
	Me       string
	Role     Role
	Messages []string
	Notice   string
}

State is one session's view of the room.

func (State) CanPost

func (s State) CanPost() bool

CanPost is the same rule the markup uses to decide whether to render an enabled composer. Rendering it is courtesy; the two checks below are the enforcement, and both are needed.

Jump to

Keyboard shortcuts

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