oob

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Nov 10, 2025 License: Apache-2.0 Imports: 8 Imported by: 0

README

OOB Pending Primitives

Minimal, generic primitives to manage out‑of‑band (OOB) interactions with strong namespace remapping and no opinions about UI. You define device pages, forms, or redirects; this package ensures every callback maps back to the correct namespace.

What It Provides

  • Generic, typed Store[T]: source of truth for id → namespace (+ typed payload).
  • Manager[T]: resolves namespace on create, stores pending, builds a callback URL, and returns it.
  • HTTP helper NamespaceFromPending[T]: loads pending by id and injects the correct namespace into context before running your handler.

What It Doesn’t Do

  • No UI or templates, no device page HTML, no OAuth redirect logic. Those stay in your service.

Types

  • Pending[T]: typed entry bound to a namespace. Fields: ID, Namespace, Kind, Alias, Resource, ElicitID, CreatedAt, ExpiresAt, Data T.
  • Spec[T]: inputs to create a pending (same minus ids/timestamps).
  • Store[T]: generic interface: Put, Get, Complete, Cancel, ListNamespace, ClearNamespace.
  • Manager[T]: Create(ctx, spec) (id, callbackURL), Complete(ctx, id), Cancel(ctx, id).
  • NamespaceFromPending[T]: HTTP wrapper that injects the correct namespace.Descriptor and the loaded Pending[T] into context.

A simple in‑memory MemoryStore[T] is included.

Typical Flow

  1. Tool detects missing credentials and starts an OOB interaction
  • id, callbackURL, err := mgr.Create(ctx, Spec[T]{Kind: kind, Alias: alias, Resource: resource, Data: payload})
  • If client supports MCP Elicit, you can send ElicitID = id and include callbackURL in the message.
  1. Service registers an interaction route and renders OOB UI
  • mux.Handle("/service/auth/interaction/", oob.NamespaceFromPending(store, extractID, yourHandler))
  • yourHandler(ctx, pending, w, r) fully controls presentation (device code, form, redirect).
  1. On success, complete and persist under the correct namespace
  • pending, err := mgr.Complete(ctx, id) // returns the pending with Namespace
  • Save credentials/secrets under pending.Namespace and activate resources only for that namespace.

Example: Device Code (Typed Payload)

// Define a payload type for your service
type DevicePayload struct {
    VerifyURL string
    UserCode  string
    Message   string
}

store := oob.NewMemoryStore[DevicePayload]()
mgr := &oob.Manager[DevicePayload]{
    Provider:        nsProvider, // github.com/viant/mcp/server/namespace.Provider
    Store:           store,
    CallbackBuilder: func(id string) string { return "/service/auth/interaction/" + id },
}

// When auth is needed
id, callbackURL, _ := mgr.Create(ctx, oob.Spec[DevicePayload]{
    Kind:     "device_code",
    Alias:    alias,
    Resource: "github.com",
    Data:     DevicePayload{VerifyURL: vURL, UserCode: code, Message: msg},
})
// Optionally send an MCP Elicit with ElicitID=id and a link to callbackURL

// Interaction route
extractID := func(r *http.Request) (string, error) { parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/"); return parts[len(parts)-1], nil }
mux.Handle("/service/auth/interaction/", oob.NamespaceFromPending(store, extractID, func(ctx context.Context, p oob.Pending[DevicePayload], w http.ResponseWriter, r *http.Request) error {
    // p.Namespace is correct; namespace.Descriptor is injected into ctx
    // Render your device page using p.Data.VerifyURL and p.Data.UserCode
    // On completion (e.g., after polling), call mgr.Complete(ctx, p.ID)
    return nil
}))

Example: Secret Form (SQL‑style)

type SecretForm struct { RequestedSchema any; Title string }
store := oob.NewMemoryStore[SecretForm]()
mgr := &oob.Manager[SecretForm]{ Provider: nsProvider, Store: store, CallbackBuilder: func(id string) string { return "/db/auth/interaction/"+id } }

id, callbackURL, _ := mgr.Create(ctx, oob.Spec[SecretForm]{ Kind: "basic_credentials", Alias: connectorName, Resource: driverDB, Data: SecretForm{RequestedSchema: schema} })
// If MCP Elicit is available, send a message linking to callbackURL

mux.Handle("/db/auth/interaction/", oob.NamespaceFromPending(store, extractID, func(ctx context.Context, p oob.Pending[SecretForm], w http.ResponseWriter, r *http.Request) error {
    // Render a form based on p.Data.RequestedSchema, submit to this URL
    // On submit success: pend, _ := mgr.Complete(ctx, p.ID); save under pend.Namespace
    return nil
}))

Notes

  • Namespace is derived once at create time from the request token and stored with the pending; all callbacks map back to it by id.
  • NamespaceFromPending injects a minimal Descriptor{Name: pending.Namespace}; if you need FS‑friendly pathing, compute it in your service using the same policy you use for namespace path derivation.
  • Include TTLs in your store or validate ExpiresAt to avoid stale interactions.
  • You can add dedup/waiters separately if your service needs them to prevent prompt storms or to block until completion.

Documentation

Overview

Package oob provides minimal, generic primitives to manage out-of-band (OOB) interactions using typed pending entries bound to a namespace.

The package centralizes only the lifecycle and remapping concerns: - Create a pending interaction tied to the caller's namespace - Look up by ID and map back to the correct namespace - Complete or cancel a pending interaction - Minimal HTTP helper to inject the correct namespace into context based on ID

UI and flow specifics (device code pages, forms, redirects) are left entirely to the service. This keeps services free to present OOB in any way while guaranteeing that callbacks are mapped to the right namespace.

Index

Constants

This section is empty.

Variables

View Source
var ErrMisconfigured = errors.New("oob: misconfigured manager (missing Provider, Store or CallbackBuilder)")

ErrMisconfigured indicates a missing Provider, Store, or CallbackBuilder.

Functions

func IntoContext

func IntoContext[T any](ctx context.Context, p Pending[T]) context.Context

IntoContext stores a Pending[T] in context.

func NamespaceFromPending

func NamespaceFromPending[T any](store Store[T], extract idExtractor, next func(context.Context, Pending[T], http.ResponseWriter, *http.Request) error) http.Handler

NamespaceFromPending wraps a handler to remap the request to the correct namespace based on the pending id stored in the provided Store. It loads the pending, injects a minimal namespace descriptor into context, then calls next.

Types

type CallbackBuilder

type CallbackBuilder func(id string) string

CallbackBuilder returns a callback URL for the given id. The service should register a route that extracts this id and uses NamespaceFromPending to remap to the correct namespace.

type Manager

type Manager[T any] struct {
	Provider        namespace.Provider
	Store           Store[T]
	CallbackBuilder CallbackBuilder
}

Manager coordinates creation and completion of typed pendings with namespace resolution and callback URL generation.

func (*Manager[T]) Cancel

func (m *Manager[T]) Cancel(ctx context.Context, id string) (Pending[T], error)

Cancel removes and returns the pending entry for the given id without signaling completion.

func (*Manager[T]) Complete

func (m *Manager[T]) Complete(ctx context.Context, id string) (Pending[T], error)

Complete removes and returns the pending entry for the given id.

func (*Manager[T]) Create

func (m *Manager[T]) Create(ctx context.Context, spec Spec[T]) (string, string, error)

Create resolves caller namespace, creates a new pending with a generated ID, stores it, and returns id and callbackURL. The Namespace field in spec is ignored and always set from the request context.

type MemoryStore

type MemoryStore[T any] struct {
	// contains filtered or unexported fields
}

MemoryStore is an in-memory implementation of Store[T]. It is concurrency-safe and intended for single-process deployments or testing.

func NewMemoryStore

func NewMemoryStore[T any]() *MemoryStore[T]

func (*MemoryStore[T]) Cancel

func (s *MemoryStore[T]) Cancel(ctx context.Context, id string) (Pending[T], bool, error)

func (*MemoryStore[T]) ClearNamespace

func (s *MemoryStore[T]) ClearNamespace(_ context.Context, ns string) ([]string, error)

func (*MemoryStore[T]) Complete

func (s *MemoryStore[T]) Complete(_ context.Context, id string) (Pending[T], bool, error)

func (*MemoryStore[T]) Get

func (s *MemoryStore[T]) Get(_ context.Context, id string) (Pending[T], bool, error)

func (*MemoryStore[T]) ListNamespace

func (s *MemoryStore[T]) ListNamespace(_ context.Context, ns string) ([]Pending[T], error)

func (*MemoryStore[T]) Put

func (s *MemoryStore[T]) Put(_ context.Context, p Pending[T]) error

type Pending

type Pending[T any] struct {
	ID        string
	Namespace string
	Kind      string // e.g., "device_code", "basic_credentials", "oauth_redirect", etc.
	Alias     string // optional account/connector alias
	Resource  string // provider domain/tenant/connector
	ElicitID  string // optional: if MCP Elicit was used to notify client

	CreatedAt time.Time
	ExpiresAt time.Time

	Data T // service-specific payload
}

Pending represents a typed pending interaction bound to a namespace. T holds service-specific payload (e.g., device-code message, form schema).

func FromContext

func FromContext[T any](ctx context.Context) (Pending[T], bool)

FromContext retrieves a Pending[T] from context.

type Spec

type Spec[T any] struct {
	Namespace string // ignored; filled from context at Create
	Kind      string
	Alias     string
	Resource  string
	ElicitID  string

	ExpiresAt time.Time // optional expiry time

	Data T
}

Spec carries inputs to create a Pending; Namespace is ignored on input and assigned from the caller context when creating the pending.

type Store

type Store[T any] interface {
	Put(ctx context.Context, p Pending[T]) error
	Get(ctx context.Context, id string) (Pending[T], bool, error)
	Complete(ctx context.Context, id string) (Pending[T], bool, error)
	Cancel(ctx context.Context, id string) (Pending[T], bool, error)

	// Optional helper endpoints
	ListNamespace(ctx context.Context, namespace string) ([]Pending[T], error)
	ClearNamespace(ctx context.Context, namespace string) ([]string, error)
}

Store is a generic backing registry for typed pendings; it is the source of truth mapping id -> namespace (and payload). Implementations may be in-memory or persistent.

Jump to

Keyboard shortcuts

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