Documentation
¶
Overview ¶
Package httpx builds the single Huma API the application serves, records every operation the adapter mounts, and enforces that each one declares exactly one authorization: a permission, public, or any signed-in user.
The declaration ¶
Register is the only way a module mounts a handler, and it takes an Auth alongside the operation, so "I forgot to say who may call this" is not a state a route can be in. The declaration is written into op.Extensions, which is the same object the OpenAPI document renders and ctx.Operation() returns at request time: the reviewer, the document and the middleware read one value.
Recording ¶
huma adds an operation to the OpenAPI document only when it is not hidden, so walking the document would miss exactly the routes most likely to be forgotten. Everything, hidden or not, passes through the adapter's Handle, so this package records there instead — huma's own documentation routes included, declared Public where they are mounted. ValidateDeclarations reads that recording and kit/app refuses to start when it reports anything.
The huma.API and the adapter are both unexported fields, and neither has an accessor. That is not tidiness: a handler mounted straight on the adapter is recorded, so it passes the boot gate, and yet it is mounted below this package's middleware, so it resolves no tenant, opens no transaction and is never authorized. A door that satisfies the gate and skips the enforcement is worse than no gate, so there is no door. Everything mounts through Register.
The transaction ¶
This is the one place a request obtains a tenant transaction. The middleware chain resolves the tenant from the request host and puts a pending transaction on the context; TxFrom opens it on the first query, so a request that never touches the database never opens one. A response of 400 or worse rolls it back, and the response itself is held until the commit succeeds.
Index ¶
- Constants
- func ConnFrom(ctx context.Context) (*db.Conn, bool)
- func CookieName(base string, secure bool) string
- func HTML[I any](api *API, op huma.Operation, auth Auth, ...)
- func HostOnly(host string) string
- func LocalPath(p string) bool
- func Nonce(ctx context.Context) string
- func Register[I, O any](api *API, op huma.Operation, auth Auth, ...)
- func RequestFrom(ctx context.Context) (*http.Request, bool)
- func SameSite(r *http.Request) bool
- func SessionCookieOf(r *http.Request) (*http.Cookie, bool)
- func SignIn(op *huma.Operation, path string)
- func StreamedBody() (string, any)
- func TxFrom(ctx context.Context) (db.Tx[db.Tenant], bool)
- func ValidPermission(token string) bool
- func WithConn(ctx context.Context, c *db.Conn) context.Context
- type API
- func (a *API) AddCommand(module, entity string, c Command)
- func (a *API) Declare(grants []tenancy.Grant)
- func (a *API) Events() []string
- func (a *API) InvalidateHost(host string)
- func (a *API) Permissions() []tenancy.Grant
- func (a *API) Probes(h http.Handler, paths ...string)
- func (a *API) Recorded() []*huma.Operation
- func (a *API) RegisterResource(r Resource)
- func (a *API) Required() []tenancy.Grant
- func (a *API) Resources() []Resource
- func (a *API) Static(prefix string, fsys fs.FS)
- func (a *API) SystemToken() tenancy.SystemToken
- func (a *API) ValidateDeclarations() error
- type Auth
- type Authorizer
- type Command
- type Entitler
- type Options
- type Page
- type Resource
- type SeeOther
- type TenantLoader
Constants ¶
const AuthExtension = "x-platformkit-auth"
AuthExtension is the OpenAPI extension an operation's declaration is written to. It is a document key as well as a runtime one: the declaration a reviewer reads in /openapi.json is the value the middleware enforces, because Register writes it once and both sides read that same map.
const EventsExtension = "x-platformkit-events"
EventsExtension is the OpenAPI extension an operation lists the events its handler publishes under. kit/rest writes it when it mounts a Spec, and kit/app reads it back to check that some module declared each one — the same recording, the same object and the same gate as the authorization declaration, rather than a second channel to keep in step.
const ExpectedPrincipalHeader = "X-Expected-Principal"
ExpectedPrincipalHeader optionally pins a protected, state-changing request to the actor who prepared it. A mismatch is refused before authorization or the handler runs. It is a request precondition, never an authentication input.
const HTMLContentType = "text/html; charset=utf-8"
HTMLContentType is what every Page carrying markup declares. It is exported because the renderer lives in ui/page and a handler that builds a Page itself — one that serves bytes it did not render from a node — should not spell it a second way.
const (
MaxBodyBytes = 1 << 20
)
The two body ceilings, and the reason there are two.
A request body with a schema is huma's business: it decodes one and it bounds one, at op.MaxBodyBytes, which is a megabyte unless a route says otherwise. A route that reads the request itself has no such bound, and there is exactly one of those — the file upload. A review trickled a body into it at a byte a second and nothing anywhere said stop.
So every request gets an http.MaxBytesReader before it reaches a handler: a megabyte, or MaxUpload plus an envelope for the route that streams. The envelope is a megabyte because a multipart form's part headers and boundaries are measured in hundreds of bytes and a round number nobody has to compute is worth more here than a tight one.
const RequestIDHeader = "X-Request-ID"
RequestIDHeader is the header a request id arrives in and leaves in.
const SessionCookie = "platformkit_session"
SessionCookie is the base name of the cookie a browser session travels in. It is here, and not in the auth module that mints it, because the kernel has to recognise it twice without knowing anything else about sessions: to refuse a cross-site write (csrf) and to decide that a request is worth authenticating at all.
const SignInExtension = "x-platformkit-signin"
SignInExtension is where an operation names the form an anonymous caller should be sent to instead of being refused.
It is an operation's own declaration rather than an option of the API, because "there is a login page and it is at /admin/login" is knowledge the module that serves that page has and the kernel does not. modules/admin writes it on every HTML page it mounts; nothing else writes it, and an application with no shell has no redirect and no line of configuration saying so.
const StreamedBodyExtension = "x-platformkit-streams-body"
StreamedBodyExtension marks an operation that reads the request body itself instead of declaring a schema for it. Such a route gets Options.MaxUpload plus an envelope rather than MaxBodyBytes; nothing else about it changes.
It is an extension on the operation and not a field on Options because the kernel must not know which module happens to own the route. See StreamedBody.
Variables ¶
This section is empty.
Functions ¶
func ConnFrom ¶
ConnFrom is the application connection this request is served on.
It is the second half of SystemToken: db.RunSystem takes a connection and a capability, and a module holds neither until it is handed them. A module that has not taken a token can do nothing with this that it could not already do with the request's own transaction.
func CookieName ¶
CookieName is the name a first-party cookie is set under, which depends on whether it will carry Secure.
__Host- is a rule the browser enforces: a cookie named with it is accepted only with Secure, Path=/ and no Domain, and a page on another host cannot set one the browser will send here. That last part is why it matters here — every tenant is reached at its own host, often as siblings under one registrable domain, so without the prefix a page at one customer's host can set platformkit_session for the parent domain and have it attached at every other customer's. It is dropped when the cookie is not Secure, because a browser refuses one of those over http://localhost; both names are recognised on the way in, and a deployment only ever presents one.
func HTML ¶
func HTML[I any](api *API, op huma.Operation, auth Auth, handler func(context.Context, *I) (*Page, error))
HTML mounts one page, with the authorization it declares, exactly like every other operation in the application. Hidden keeps it out of the OpenAPI document — a page is not an API — and the kernel records it anyway, so the boot gate still sees it. See this package's comment.
The caller builds the operation, because what a page belongs to is the caller's to say: its tag, its summary, and the sign-in form an anonymous visitor is sent to (SignIn). What is not the caller's is the shape of the answer, which is why a SeeOther returned by the handler becomes the redirect here rather than in each of the seven screens that return one.
func HostOnly ¶
HostOnly is the loader's key: the Host header without its port and without the brackets an IPv6 literal carries, lower-cased, without the trailing dot a fully qualified name may have. Normalising here means every TenantLoader is spared doing it, and doing it differently.
It is exported because the module that stores a host has to spell it the same way as the middleware that looks one up, and two normalisations that drift is a domain that resolves for nobody.
func LocalPath ¶
LocalPath reports whether p is a path on this site and nothing else: no scheme, no authority, a leading slash, no backslash.
It is the answer to "may I send a browser here", and there is one of it because there were two and one of them was wrong. The admin sign-in form checked a leading slash and a second character that was not a slash, so `next=/\evil.example` passed — and every browser resolves a slash followed by a backslash as an authority, so the review's login redirect left the site. modules/site had the correct rule for its navigation; this is that rule, in the kernel, and both callers read it.
url.Parse is what tells the rest apart: a path on this site parses to no scheme, no authority and a path that begins with a slash. The two prefix checks come first because the parse alone is not enough — url.Parse reports "//evil.example" as an empty host and a path, and a browser collapsing the slashes goes to evil.example — and the backslash is refused outright, because it has no meaning in a path and its only use here is to look like something else.
func Nonce ¶ added in v1.1.0
Nonce is the content security policy nonce of the request ctx belongs to, or "" outside one. ui/page's InlineScript is the intended caller and should stay the only one: a template that reaches for the nonce itself is a template that can forget it, and the policy in htmlPolicy drops what it forgets.
func Register ¶
func Register[I, O any](api *API, op huma.Operation, auth Auth, handler func(context.Context, *I) (*O, error))
Register mounts an operation together with the authorization it declares. It is the only way a module registers a handler, and the declaration is a parameter rather than a field, so an operation cannot be written without one.
func RequestFrom ¶
RequestFrom is the request being served.
A handler wants it for the things huma's typed input cannot express and that are properties of the connection rather than of the operation: the host an absolute redirect has to be built for, and the address and user agent a session records so that a person can recognise it in a list. Reading the body through it is a mistake — huma has already decoded it — and reading the context off it is another, because this is the request as it was before the tenant, the transaction and the principal were put on it.
func SameSite ¶
SameSite is the browser's own account of where a request came from: Sec-Fetch-Site, which a page cannot forge, and an Origin whose host is this one for the older clients that do not send it. A request that says neither is not a browser, and reads as same-site — see the middleware above for why that is the deliberate answer and not a hole.
It is exported for the one route the middleware cannot cover: a sign-in carries no session cookie, so nothing was attached on the caller's behalf and the middleware lets it through. That is right for the general rule and wrong for that route, because a cross-site sign-in mints a credential rather than spending one — the attacker signs the visitor into the attacker's own account and then reads what the visitor does in it. modules/auth asks this directly.
func SessionCookieOf ¶
SessionCookieOf is the session cookie the request presents, under either name. It is exported because the auth module reads the one the kernel recognised: two spellings of "which cookie is the session" is a session one half of the program can see and the other cannot.
func SignIn ¶
SignIn declares that this operation is a page and names the sign-in form. It is written into op.Extensions, beside the authorization, so a reviewer reading /openapi.json sees both.
func StreamedBody ¶
StreamedBody is what a route that reads its own request puts in its Extensions, beside EventsExtension. There is one caller, and there should stay one: a second route that reads its own body is a second route with no schema, no generated client and no bound but this one.
func TxFrom ¶
TxFrom returns the request's tenant transaction, opening it if this is the first query of the request. It reports false for a request that resolved to no tenant, and for one whose transaction could not be opened — the middleware logs that failure with its cause, and the handler's own error becomes the response.
Opening on demand rather than on arrival is what lets a liveness probe reach a tenant host while the database is down: a request that never queries never needs a database.
This is how a handler reaches the database: there is no other door, and a repository that takes db.Tx[db.Tenant] cannot be called without going through one.
func ValidPermission ¶
ValidPermission reports whether token is a well-formed permission. kit/module checks a manifest's permission keys with it, so the grammar exists once.
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API is the application's Huma API: the huma.API every registration goes through, plus the recording that makes boot-time validation possible.
func New ¶
New builds the API and the router it is mounted on. The router is returned rather than hidden because static assets and the server itself need it; every route that is an API operation goes through Register.
func (*API) AddCommand ¶ added in v1.1.0
AddCommand records a command on the resource of module and entity. kit/rest calls it from Command, beside the route, so a catalog and its API cannot disagree about which doors exist. It does not look the resource up: a module registers its commands in a call of its own, which may run before or after the Spec — a test mounts them without it — so Resources joins the two.
func (*API) Declare ¶
Declare records every permission the composition defines. kit/app calls it once, from the manifests, before any module registers its routes.
It is here rather than in a module's Deps because the one module that needs the list — auth, which refuses a role naming a permission nobody defines — would otherwise be handed what every other module declares, and a module that knows the catalogue knows its neighbours. Module.Routes is given this API, which is the one moment the kernel has the whole list.
func (*API) InvalidateHost ¶
InvalidateHost forgets a cached resolution, so a rename or a removal takes effect now rather than within hostTTL. The tenant module calls it when it changes a host; nothing else has any reason to.
func (*API) Permissions ¶
Permissions is every permission the composition defines, in name order. A module validating a list somebody typed asks this, not its neighbours.
func (*API) Probes ¶
Probes mounts handlers beside the API, on the router that carries neither the request middleware nor a transaction. kit/health is the only caller, and the two paths it names are /health and /ready.
A probe is not an operation. It has no tenant — an orchestrator reaches an instance at a pod address that names no site — no session, and nothing to declare. Going through the chain would make liveness depend on the one query every request makes before it is a request: the host lookup, which has a two second budget of its own and never hits the cache for a pod address, because only a successful resolution is cached. A liveness probe with a two second timeout therefore fired during a database outage and the kubelet restarted pods whose only problem was that their database was unreachable, which is the exact failure the probes exist to avoid.
This is a narrow door and not a general "mount a handler" one, for the reason the package comment gives: a handler mounted below this package's middleware resolves no tenant, opens no transaction and is never authorized, so the only handlers that may take it are the ones that must answer without any of the three. Static is the other. Both are named for what they carry.
func (*API) Recorded ¶
Recorded returns every operation the adapter has handled, in registration order, hidden ones included.
func (*API) RegisterResource ¶
RegisterResource records a resource, with this API's authorization wrapped around each of its operations. kit/rest calls it from Spec.Mount, in the same breath as the routes, so a resource and its API cannot disagree about a permission or a path.
The wrapping is here rather than in kit/rest because this is where the Authorizer is: a Resource is the entity without its routes, and the routes are where the permission used to live. A hand-written page holds a Resource and calls an operation directly, so a closure that did not ask would be a page that reads past the permission whenever whoever wrote it forgot to. Now forgetting is not available.
func (*API) Required ¶
Required lists, once each, every grant a recorded operation asks for, with the operator flag the route declared. kit/app checks it against the manifests, so a permission nobody defines — or one the two sides disagree about — fails startup rather than denying everyone forever or, worse, letting a customer's wildcard through the control plane.
func (*API) Resources ¶
Resources is every registered resource, in mount order, each carrying the commands recorded for it. modules/admin reads it in Routes, which is why the shell is composed last: a module that mounts after it registers a resource no screen was generated for.
func (*API) Static ¶
Static mounts a file tree beside the API, on the router that carries neither the request middleware nor the transaction. Static assets are not operations: there is no handler to authorize, no tenant transaction to open and nothing to declare, so they never appear in Recorded and never hold a response in memory waiting for a commit.
func (*API) SystemToken ¶
func (a *API) SystemToken() tenancy.SystemToken
SystemToken is the capability that opens a cross-tenant transaction, handed to a module at the one moment it is being wired: Module.Routes is given this API, and a module that registers a control-plane route takes the token there.
It is a method rather than something kit/internal/syscap would mint for anybody, because the point is that the set of modules holding one is short and visible. `grep -rn 'SystemToken()' modules/` is that list, and there is no other door: nothing outside kit/ can construct or implement a token, so a module that wants to cross tenants has to write this call where a reviewer reading the manifest will see it. See docs/adr/0006.
A route that holds one still runs inside its own tenant's transaction, so it opens the system transaction on a detached context (db.Detached) — two transactions, and the control-plane one commits on its own.
func (*API) ValidateDeclarations ¶
ValidateDeclarations names every operation that does not carry an authorization this package minted. kit/app calls it once every route is registered and refuses to serve when it returns anything; the request-time middleware denies the same operations, so this turns a 403 nobody notices into a startup failure someone has to fix.
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth is the authorization an operation declares. Its fields are unexported and its constructors are the only way to build a usable value, so "some operation declares an authorization I did not think of" is not expressible.
func OperatorPermission ¶
OperatorPermission requires the caller to hold token, and requires the tenant the request resolved to be the operator's own.
It exists because the control plane is served at every tenant's host — an installation has no host of its own, only its customers' — so a permission alone does not guard it: a customer's administrator holds the wildcard in their own tenant, and that wildcard used to list, create and suspend the tenants beside them. The kernel refuses such a route on an ordinary tenant before it asks the Authorizer anything, and no wildcard satisfies one.
func Permission ¶
Permission requires the caller to hold token, checked against Options.Authorize in the tenant the request resolved to.
An ill-formed token is a wiring mistake rather than a request-time condition, so it panics at the registration site instead of turning into a permission nobody can ever hold.
func Public ¶
func Public() Auth
Public admits every caller, signed in or not. It is the declaration that has to be justified in review.
func SignedIn ¶
func SignedIn() Auth
SignedIn admits any caller carrying a principal for the resolved tenant, whatever that principal may do. It is for operations about the caller themselves, where there is no resource to name a permission on.
func (Auth) Declared ¶
Declared reports whether a came from one of the four constructors. The zero Auth is not a declaration: Go lets any package write httpx.Auth{}, and an empty struct must never read as "public".
It is exported because kit/rest asks it about an optional field — a command that names no Auth takes its Spec's — and that question is the same one this package asks before it mounts anything.
func (Auth) Feature ¶ added in v1.1.0
Feature is the plan feature this declaration requires, or empty when it requires none. kit/app reads it to refuse a startup that declares features with nothing to answer them.
func (Auth) MarshalJSON ¶
MarshalJSON writes the declaration into the OpenAPI document as {"kind":"permission|public|signed_in","permission":"..."}. huma renders the YAML spec by converting the JSON one, so this is the only encoder needed.
func (Auth) Needing ¶ added in v1.1.0
Needing returns this declaration for an operation that is part of a plan feature: the caller must hold the permission and the tenant's plan must include the named feature, in that order.
It is a method on Auth rather than a field of the operation because the two questions belong together — "who may do this, and is it in what they are paying for" — and because Auth is already the one typed value the document publishes and the middleware enforces. A second extension key would be a second thing to forget.
The name is the plan's, not a permission: what a feature entitles somebody to is the module's business, so a plan can be re-priced without touching an authorization. See modules/billing.Plan.Features.
type Authorizer ¶
type Authorizer interface {
Allowed(ctx context.Context, tenant tenancy.Tenant, grant tenancy.Grant) (bool, error)
}
Authorizer decides whether the caller of a request may exercise a grant in a tenant. The auth module implements it; a test implements it in three lines. It is the only thing this package knows about roles. It runs inside the request, so an implementation that needs the tenant's own rows reaches them with TxFrom.
The grant carries the operator flag as well as the permission, and the implementation is held to it: a wildcard must not satisfy an operator grant, because "everything in my tenant" is not "everything to every tenant". This package has already refused such a grant on a tenant that is not the operator's, so the question left is whether the caller's roles name it.
type Command ¶ added in v1.1.0
type Command struct {
Verb string
Summary, Description string
Collection bool
Auth Auth
Fields []entity.Field
}
Command is one lifecycle route on a resource, as kit/rest.Command mounted it: a rule about the state a row is in, with an event of its own, which is what a generic form cannot express. Verb is the last path segment — POST {Path}/{id}/{Verb}, or {Path}/{Verb} when Collection — Summary and Description are the API document's own, Auth is who may call it (not always the resource's write permission), and Fields is the shape of its argument, derived from the request body the way an entity's is derived from T.
type Entitler ¶ added in v1.1.0
type Entitler interface {
Includes(ctx context.Context, tenant tenancy.Tenant, feature string) (bool, error)
}
Entitler answers what the tenant's plan includes, for the operations that declare a feature with Auth.Needing. It is a second question and not a second authorizer: a permission is what a person may do, a feature is what their tenant is paying for, and conflating them makes a price change a permission change.
An installation that sells nothing implements none of this: an application whose operations declare no feature is never asked.
type Options ¶
type Options struct {
// PublicHost is the host the application believes it is reached at. It
// names the server in the OpenAPI document; the tenant of a request always
// comes from that request's own Host header, never from this.
PublicHost string
// Docs serves /openapi.json, /openapi.yaml and /docs. They are public and
// unauthenticated by construction, and they publish every route and every
// permission the application has, which is a map worth having before an
// attack and worth withholding during one. The reference app turns them on;
// a deployment that would rather not can turn them off. The JSON Schema
// route stays either way, because response bodies link to it.
Docs bool
// Tenants maps a request host to a tenant.
Tenants TenantLoader
// Conn is the application connection every request transaction opens on.
Conn *db.Conn
// Authorize answers the permission questions the declarations ask.
Authorize Authorizer
// Entitle answers the plan questions the declarations ask. It is required
// only when an operation declares a feature, and ValidateDeclarations
// refuses a startup where one does and this is nil — a feature nothing can
// answer is a door that would be either always open or always shut, and
// both are wrong in a way nobody would notice until a customer did.
Entitle Entitler
// Authenticate recognises the caller. It runs after the host has resolved
// to a tenant and is handed that request's own transaction, so a session
// lookup is an ordinary tenant-scoped query under row-level security: the
// auth module neither resolves the host a second time nor asks for a
// capability to read across tenants, because a credential belonging to
// somebody else's tenant is a row it cannot see.
//
// It reports false for an anonymous caller, which is not an error — a
// Public operation serves them. An error is an outage: the request is a 500
// and the reason is logged, because a session store that cannot be reached
// must not read as "you are not signed in".
Authenticate func(ctx context.Context, tx db.Tx[db.Tenant], r *http.Request) (tenancy.Principal, bool, error)
// Log receives the reason behind every denial and every rolled-back
// transaction. Defaults to slog.Default().
Log *slog.Logger
// MaxUpload is the largest file this deployment accepts, and it bounds the
// body of the one kind of route that reads its own request rather than
// declaring a schema for it — see StreamedBody. The envelope a multipart
// form wraps the file in is allowed for on top of it.
//
// Zero means MaxBodyBytes, which is what every other route gets: a
// deployment that mounts no streaming route needs no larger number, and a
// test that mounts one is not testing the ceiling.
MaxUpload int64
}
Options are the collaborators main chooses for the HTTP layer. Every field except Log, PublicHost and Docs is required: an API missing one of them could only fail closed on every request, which is worse than failing at New.
type Page ¶
type Page struct {
Status int
ContentType string `header:"Content-Type"`
Location string `header:"Location"`
HXRedirect string `header:"HX-Redirect"`
// Isolated previews and private representations set their own response policy.
ContentSecurityPolicy string `header:"Content-Security-Policy"`
FrameOptions string `header:"X-Frame-Options"`
CacheControl string `header:"Cache-Control"`
ReferrerPolicy string `header:"Referrer-Policy"`
ContentLanguage string `header:"Content-Language"`
Vary string `header:"Vary"`
Body []byte
}
Page is an HTML response. huma writes a []byte body verbatim, so this is the whole of "serve HTML" — no second router, no second middleware chain, and the same recording, the same authorization declaration and the same transaction as every JSON route in the application.
func Redirect ¶ added in v1.1.0
Redirect sends the caller somewhere else after a write. htmx does not follow a 303 the way a browser does — it would swap the target page into a fragment — so a request it made is answered with the header it understands instead. The caller may attach response policy before returning the page.
type Resource ¶
type Resource struct {
Module, Entity, Path string
// Read and Write are the permissions the Spec declared. A screen carries
// the same ones, so a person who cannot use the API cannot use the screen.
Read, Write string
// OperatorRead restricts reads and discovery to the operator's own tenant.
// It is independent of OperatorWrite; leaving it false keeps shared rows
// such as prices readable by ordinary tenants.
OperatorRead bool
// OperatorWrite restricts writes to the operator's own tenant. Routes and
// resource closures carry the same declaration, so customer wildcards
// cannot bypass the operator boundary. See docs/adr/0008.
OperatorWrite bool
// Immutable are the fields a command owns, shown read-only in a form.
Immutable []string
Schema entity.Schema
// Commands are the lifecycle routes beyond the five, filled in by
// Resources from what AddCommand recorded.
Commands []Command
// Singleton says a tenant has exactly one of these and it has no id in its
// path: the API is a GET and a PUT on Path itself, with no list, no create
// and no delete. A shell that did not know would render a collection whose
// list is one row, offering doors — New, Delete — that no route serves.
// See rest.Singleton.
Singleton bool
// Count reads the total without a page. Registration guards it with Read
// and supplies a List-based fallback for resources with custom loading.
Count func(ctx context.Context) (int64, error)
List func(ctx context.Context, q crud.Query) ([]map[string]any, int64, error)
Get func(ctx context.Context, id uuid.UUID) (map[string]any, error)
Create func(ctx context.Context, values map[string]any) (map[string]any, error)
Update func(ctx context.Context, id uuid.UUID, values map[string]any) (map[string]any, error)
Delete func(ctx context.Context, id uuid.UUID) error
// contains filtered or unexported fields
}
Resource is one entity as a screen sees it: what it is called, where its API lives, which permissions guard it, what shape it has, and its operations bound to its type.
The operations are closures because generics do not survive the trip: a screen knows a resource by name and cannot name its Go type, so the type is closed over at registration instead. Each runs inside the request's own transaction, which it takes from the context — a screen holds no capability the route beside it does not.
Rows are maps because that is what a schema-driven screen renders: a column is a field name from Schema, and a value is whatever the entity's own JSON says it is. There is no second serialization; it is encoding/json, once.
func (Resource) CommandsFor ¶ added in v1.1.0
CommandsFor is the commands this caller may call. One they may not is left out rather than shown disabled, for the reason an unreadable resource is left out: what somebody may not do, they are not told about.
Each is answered the way the request middleware answers it — public admits everybody, signed-in a recognised caller, a permission the Authorizer's question — so the catalog cannot promise a door the API would refuse. An undeclared Auth admits nobody, as at the route.
func (Resource) ReadAuth ¶ added in v1.1.0
ReadAuth carries the same read declaration as the guarded resource closures.
func (Resource) Readable ¶
Readable reports whether the caller in ctx holds this resource's Read permission. Use it to decide whether to show navigation; operations already carry their own guard and need no preceding Readable check.
type SeeOther ¶
type SeeOther string
SeeOther is a handler saying the answer is somewhere else: the row a write just created, or the list a delete just changed.
It is an error because "not this page" is a return a handler already has: keeping it there is what lets every page have one signature.
type TenantLoader ¶
type TenantLoader interface {
ByHost(ctx context.Context, tx db.Tx[db.System], host string) (tenancy.Tenant, error)
}
TenantLoader maps an incoming host to a tenant. The tenant module implements it in E2; it returns tenancy.ErrNoSuchHost when there is simply no site at the host, and any other error when it could not tell.
It takes a db.Tx[db.System] because the answer is a query and the row it looks for belongs to no tenant yet — the request has not resolved one. This package mints the capability, since no module can, and passes the transaction in: an implementation cannot open a cross-tenant transaction of its own, and the one it is handed lasts only for the call.
It is declared here rather than in kit/tenancy because kit/db imports kit/tenancy; this package already imports both.