Ctrl K
ComunidadGuías…

internal/pat/pat.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package pat implements Cairn's personal access tokens: human-minted bearer
// credentials for the /v1 surface, created and revoked from the web Settings
// UI (issue #74). A PAT is the same "one Authenticator seam" ADR-0004
// establishes for CAIRN_API_TOKENS and OAuth 2.1 — this package is a third
// implementation over the same seam, not a parallel auth mechanism: it plugs
// into internal/httpapi/auth.go's chainAuthenticator exactly like the static
// token authenticator and the OAuth bearer authenticator do (see
// httpapi.PATAuthenticator).
//
// Unlike an OAuth grant (short access token + rotating refresh, issued by an
// authorization-code flow) a PAT is a single long-lived opaque secret the
// owner mints directly: simpler, and the intended shape for pasting into an
// agent's config or a CLI profile (foundation for #21). Scopes are a
// caller-chosen subset of the three ADR-0004 consent scopes — this package
// reuses internal/oauth's scope constants and ParseScope/JoinScope rather
// than redefining them, since a PAT's grantable scopes are exactly that set
// and nothing else (no sharing:manage, no delete: SPEC-0007 REQ "Exactly
// Three Consent Scopes"). The is_agent flag is independent of scope: it
// marks whether this token was minted for an agent acting on the owner's
// behalf, which keeps human-only capabilities (delete; see
// httpapi.requireHuman) off regardless of which scopes were granted.
//
// Governing: ADR-0004 (token seam), SPEC-0007 (three-scope model, hashed-at-
// rest secrets, database operation standards).
package pat

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"strings"
	"time"

	"github.com/stump-wtf/cairn/internal/oauth"
)

// The three grantable scopes, re-exported from internal/oauth so callers
// (and this package's own tests) never need to import both packages just to
// name a scope.
const (
	ScopeArtifactsRead    = oauth.ScopeArtifactsRead
	ScopeArtifactsWrite   = oauth.ScopeArtifactsWrite
	ScopeAnnotationsWrite = oauth.ScopeAnnotationsWrite
)

// AllScopes, ParseScope, and JoinScope are the same three-scope vocabulary
// ADR-0004 defines for OAuth grants (internal/oauth), re-exported here so a
// PAT can never be minted with a scope outside that set — there is exactly
// one definition of "the three scopes" in the codebase.
func AllScopes() []string { return oauth.AllScopes() }

// ParseScope validates and canonicalizes a space-delimited scope string
// against the three-scope set (see internal/oauth.ParseScope).
func ParseScope(raw string) ([]string, error) { return oauth.ParseScope(raw) }

// JoinScope renders a canonical scope list as its space-delimited wire form.
func JoinScope(scopes []string) string { return oauth.JoinScope(scopes) }

// Token is a personal access token's metadata — everything except the
// plaintext secret, which exists only in memory at creation time (Service.Create
// return value) and is never persisted or re-derivable.
type Token struct {
	ID string
	// UserID is the owning user (SPEC-0023 REQ "Owner Model"); Actor is that
	// user as rendered on the wire.
	UserID     string
	Actor      string
	Name       string
	Scopes     []string
	IsAgent    bool
	CreatedAt  time.Time
	LastUsedAt *time.Time
	RevokedAt  *time.Time
}

// secretPrefix marks a Cairn PAT so leaked strings are identifiable in
// scanners without weakening entropy — the same convention internal/oauth
// uses for its own secrets ("cairn_ac_", "cairn_at_", "cairn_rt_").
const secretPrefix = "cairn_pat_"

// IsSecret reports whether a bearer token looks like a Cairn personal access
// token (the "cairn_pat_" prefix). Callers use it to route a bearer to the PAT
// authenticator vs. the OAuth access-token verifier; the prefixes are disjoint
// ("cairn_pat_" vs "cairn_at_"), so this never misroutes a real token.
func IsSecret(token string) bool { return strings.HasPrefix(token, secretPrefix) }

// newSecret mints a 32-byte high-entropy URL-safe PAT secret.
func newSecret() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("pat: mint secret: %w", err)
	}
	return secretPrefix + base64.RawURLEncoding.EncodeToString(b), nil
}

// hashSecret maps a raw secret to the lowercase-hex SHA-256 stored at rest,
// matching internal/oauth's convention so both packages' token_hash columns
// are directly comparable in shape (SPEC-0007 REQ "Database Operation
// Standards").
func hashSecret(secret string) string {
	sum := sha256.Sum256([]byte(secret))
	return hex.EncodeToString(sum[:])
}