token

package
v1.801.469 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package token mints and verifies the HS256 JWTs that the team SPA, the /v1/team/account API and the /v1/team/transactor data plane all share.

The wire format is byte-compatible with the platform `jwt-simple` HS256 tokens (foundations/core/packages/token/src/token.ts): header {"typ":"JWT","alg":"HS256"}, a COMPACT JSON payload whose keys appear in the fixed order {extra, account, workspace, sub, exp, nbf} with undefined/empty keys omitted, base64url WITHOUT padding, and a raw HMAC-SHA256 signature over `header.payload`.

account and workspace, when present, MUST be UUIDs — the upstream `generateToken` throws otherwise and the transactor client refuses a workspace token that is missing either, so we enforce the same invariant at mint time.

The secret is the shared `SERVER_SECRET` (synced from KMS in production). One secret, one wire format — every team surface signs and verifies through this package. Ported VERBATIM from github.com/hanzoai/team/pkg/token.

Index

Constants

View Source
const (
	RoleOwner  = "owner"
	RoleAdmin  = "admin"
	RoleMember = "member"
	RoleGuest  = "guest"
)

ErrMalformed is returned when a token is not three base64url segments. The workspace roles a token can carry, signed into extra.role at mint (clients/team selectWorkspace). This is the CLOSED set validInviteRole accepts.

Variables

View Source
var ErrExpired = errors.New("token: expired")

ErrExpired is returned by Decode(verify=true) when the token's `exp` has passed.

View Source
var ErrMalformed = errors.New("token: malformed")
View Source
var ErrNoSecret = errors.New("token: empty secret")

ErrNoSecret is returned when signing or verifying is attempted with an empty secret. There is NO fallback literal: an unset SERVER_SECRET must surface as a hard error, never as a token silently signed with a public value.

View Source
var ErrNotYetValid = errors.New("token: not yet valid")

ErrNotYetValid is returned by Decode(verify=true) when the token's `nbf` is in the future.

View Source
var ErrSignature = errors.New("token: signature mismatch")

ErrSignature is returned when HMAC verification fails.

Functions

func Generate

func Generate(account, workspace string, extra map[string]any, exp int64, secret string) (string, error)

Generate mints an HS256 token for account (always) and workspace (optional — pass "" for an account/login token that is not yet scoped to a workspace). extra is folded in as the leading `extra` claim when non-empty (e.g. {"org":"acme"}). exp is the expiry as a unix-second timestamp — pass 0 to mint a token WITHOUT an `exp` claim (the wire-golden / non-expiring case). Both ids are validated as UUIDs.

Types

type Token

type Token struct {
	Extra     map[string]any `json:"extra,omitempty"`
	Account   string         `json:"account"`
	Workspace string         `json:"workspace,omitempty"`
	Sub       string         `json:"sub,omitempty"`
	Exp       int64          `json:"exp,omitempty"`
	Nbf       int64          `json:"nbf,omitempty"`
}

Token is the decoded payload. Field order is the jwt-simple emit order so marshalling reproduces the upstream byte layout; `omitempty` drops the keys jwt-simple leaves `undefined`. account carries no omitempty: it is always present (the upstream payload always includes `account`).

func Decode

func Decode(tok, secret string, verify bool) (*Token, error)

Decode verifies (when verify is true) and parses an HS256 token. When verify is true it enforces the temporal claims with clockSkew tolerance: a token whose `exp` has passed is rejected (ErrExpired), a token whose `nbf` is in the future is rejected (ErrNotYetValid), and a token with NO `exp` is honored only until legacyExp (the pre-rollout grace) then rejected. This closes the permanent-replay window on a captured token (the transactor token rides in the URL path — log-prone — so a bounded lifetime is the mitigation until the front moves it off the URL, #60).

func (*Token) Org

func (t *Token) Org() string

Org is the signed tenant claim. Every capability decision that names an org must come from here, never from a request body or Host.

func (*Token) Privileged

func (t *Token) Privileged() bool

Privileged reports whether this token's role confers FULL capability on the workspace's data — writing rows nobody projected, minting a seat in a meeting.

FAIL-CLOSED, and the closed case is the important one: an ABSENT role is NOT privileged. A role is only absent on a token that has not proven a workspace role (a pre-selectWorkspace session token) or one minted before this claim existed, and neither has demonstrated the thing this predicate is asked about. Guests are excluded by being outside the allowlist rather than by being named, so a role added to the invite vocabulary tomorrow starts unprivileged instead of silently full.

This is the ONE predicate for reduced capability. It used to be two string comparisons written twice, against extra.guest/extra.readonly — claims NOTHING in this repo ever mints, so both copies were inert AND they disagreed about whitespace.

func (*Token) Role

func (t *Token) Role() string

Role is the signed workspace role, or "" when the token carries none — a session token (no workspace chosen yet), or one minted before roles were signed.

Jump to

Keyboard shortcuts

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