Files
zeekay b90ee3f34a fix: drop passport-oauth2 from external so static require lands in cjs
When passport-oauth2 was external, tsup emitted __require("passport-oauth2")
(dynamic-require wrapper) in dist/passport.cjs. Downstream bundlers
(esbuild, webpack) cannot trace through __require — the team monorepo
ships a single bundle.js without node_modules and hit
MODULE_NOT_FOUND at runtime.

Removing passport-oauth2 from external lets tsup emit a static
require('passport-oauth2') (still externalized via the dependencies
field, so the SDK doesn't inline it — but esbuild can now resolve it
statically from the consumer's node_modules). 0.9.3 ships this fix.
2026-05-14 11:17:52 -07:00

85 lines
2.9 KiB
Go

// Copyright 2026 Hanzo AI.
// Licensed under the Apache License, Version 2.0.
// Package tenant resolves the caller's tenant identity from the request
// context. The IAM gateway (api.hanzo.ai) validates the JWT and injects
// identity headers; downstream services trust those headers because the
// gateway strips any client-supplied copies before forwarding.
//
// Hickey lens: one verb (Require), one noun (Identity), one interface
// (Verifier). The interface lets tests swap a fake without standing up
// a real IAM. Production wiring (HeaderVerifier) reads X-* headers per
// the 2026-03-27 convention.
package tenant
import (
"context"
"errors"
"net/http"
)
// Identity is the authenticated caller resolved from the request.
type Identity struct {
// Org is the org slug from JWT "owner" claim (X-Org-Id header).
Org string
// UserID is the user ID from JWT "sub" claim (X-User-Id header).
UserID string
// Email is the user email from JWT "email" claim (X-User-Email header).
Email string
}
// Verifier resolves an Identity from an HTTP request. Implementations
// must return ErrUnauthenticated when no identity is present.
type Verifier interface {
Verify(r *http.Request) (Identity, error)
}
// ErrUnauthenticated is returned when the request carries no identity.
var ErrUnauthenticated = errors.New("tenant: unauthenticated")
// HeaderVerifier reads identity from X-* headers set by the gateway.
// The gateway is the trust boundary; it strips client-supplied copies
// before forwarding, so the headers are authoritative inside the mesh.
type HeaderVerifier struct{}
// Verify reads X-Org-Id, X-User-Id, X-User-Email and returns Identity.
func (HeaderVerifier) Verify(r *http.Request) (Identity, error) {
id := Identity{
Org: r.Header.Get("X-Org-Id"),
UserID: r.Header.Get("X-User-Id"),
Email: r.Header.Get("X-User-Email"),
}
if id.Org == "" || id.UserID == "" {
return Identity{}, ErrUnauthenticated
}
return id, nil
}
type ctxKey struct{}
// WithIdentity stores id on ctx for handlers to retrieve via FromContext.
func WithIdentity(ctx context.Context, id Identity) context.Context {
return context.WithValue(ctx, ctxKey{}, id)
}
// FromContext returns the Identity placed on ctx by Require.
// The bool is false when no identity is present.
func FromContext(ctx context.Context) (Identity, bool) {
id, ok := ctx.Value(ctxKey{}).(Identity)
return id, ok
}
// Require is a net/http middleware that verifies the request using v,
// stashes the Identity on the request context, and calls next. It
// responds 401 when v returns ErrUnauthenticated.
func Require(v Verifier, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, err := v.Verify(r)
if err != nil {
http.Error(w, "unauthenticated", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r.WithContext(WithIdentity(r.Context(), id)))
})
}