Files
zip/authorize.go
T
zeekayandHanzo Dev 3407863fb5 the walk is a visitor; the occurrence type is gone
walk(app, visit func(n node, ctx scope, site callsite) error). The callback's
parameters WERE the payload, so the struct wrapping them was a noun invented to
carry what three arguments already carry — and retaining the slice made every
backend a consumer of a materialised intermediate rather than a reducer over a
traversal. The English word survives in prose because the definition/occurrence
DISTINCTION is load-bearing; the type does not.

Validation stays its own first pass (verify), so cycles and conflicts complete
with trails before any backend sees a visit. A generation retains what was
REDUCED — router, ops, host set — never the traversal.

Also: #4 verified, not changed. Own-op tool names already ride the operation-id
rule because they read Registry(); a plugin catalogue tool is invoked by NAME at
the plugin's own door and never consults a prefix, so ONE definition is ONE tool
and namespacing it would invent a callable that does not exist. Tests pin both.

#2: Drop and Include share one transact path, so the guarantee is identical, and
the lock discipline that makes it deadlock-free is now written down with the two
invariants it rests on. Documented honestly: commitment across servers, not
simultaneity.

Godoc: every bracket reference to a deleted symbol rewritten. LLM.md carries the
canonical three-document spec.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 12:55:22 -07:00

66 lines
2.9 KiB
Go

package zip
import "context"
// Op is the stable identity of a registered typed handler, handed to an
// Authorizer so the decision can key on the operation as well as the input.
// OperationID is the resolved id the OpenAPI document and the MCP tool surface
// share — the explicit WithOperationID, else the method+path default.
type Op struct {
Method string
Path string
OperationID string
}
// Authorizer authorizes a decoded, validated typed request at the op-invoke
// seam — the ONE point every projection of a typed handler funnels through. It
// runs after the request is decoded into the op's typed In and validated, and
// BEFORE the handler runs, over REST and MCP alike, so the value it authorizes
// is exactly the value the handler will act on: there is no second parse of the
// body for it to diverge from. in is the *In the handler will receive.
//
// Returning a non-nil error aborts the op before the handler runs, and that
// error is the response — return a zip.Err* (e.g. ErrForbidden) for a clean
// status.
type Authorizer func(ctx context.Context, op Op, in any) error
// Authorize installs fn as the op-invoke authorization hook. It is the op-level
// counterpart to Use: Use wraps the whole request with transport middleware,
// which for a body request sees only the raw bytes; Authorize runs one decision
// on the DECODED typed input of every op, REST and MCP alike — the seam a
// mounted subsystem gates on so the value it authorizes is the value the handler
// binds. Call once while mounting, before Listen. A nil fn clears it (every
// decoded request then runs unauthorized).
func (a *App) Authorize(fn Authorizer) { a.authorizer = fn }
// Build constructs and installs a generation without starting a listener: the
// walk runs, every validation runs, the projections are rendered, and the
// router goes live — exactly what [App.Listen] does minus the sockets.
//
// It RETURNS THE VERDICT, which is the whole reason it replaced Prepare. A
// composition can be invalid — two definitions claiming one address, a cycle —
// and the old Prepare returned nothing, so the only way to learn was to start
// a server. Now a codegen step, a test or a wiring file's own main can ask
// whether the program it just wrote is a program, and get every conflict at
// once with both claimants named.
//
// Idempotent for the projections (they render once) and monotonic for the
// freeze. Listen calls it; there is no other way to build.
func (a *App) Build() error {
// VALIDATE FIRST. prepare() renders the projections, and a projection of a
// program that does not compose now panics — so validating after it would
// turn every build error into a panic escaping the one function whose whole
// job is to return it.
if err := verify(a); err != nil {
return err
}
a.prepare()
a.buildMu.Lock()
g, err := a.build()
if err == nil {
a.install(g)
}
a.buildMu.Unlock()
return err
}