// Package ids defines anchorage's typed identifier system. // // Every domain entity gets its own compile-time-distinct ID type built on // the TypeID spec (https://github.com/jetify-com/typeid-go): a human-readable // prefix joined to a base32-encoded UUIDv7 by an underscore. For example: // // org_01h7rfxv6qefr4twn2jg5p4k3z // pin_01h7rfxz5pehmy08b3tybczg2y // // Properties: // // - UUIDv7 is time-ordered, giving Postgres index locality close to a // sequential bigint without the coordination cost. // - The prefix is self-describing in logs, audit rows, and UIs, and is // enforced by a per-table CHECK constraint in the schema. // - Each subtype (OrgID, PinID, ...) is a distinct Go type. Passing an // OrgID where a PinID is expected is a compile-time error. // // To accept an ID on an API boundary, use the matching Parse* helper — it // will reject a value with the wrong prefix, giving tenant isolation a // second layer of defense at the type level. package ids import ( "fmt" "go.jetify.com/typeid" ) // Entity prefix tokens. Kept as constants so schema migrations, CHECK // constraints, and log filters can reference a single source of truth. const ( OrgPrefixToken = "org" UserPrefixToken = "usr" PinPrefixToken = "pin" TokenPrefixToken = "tok" NodePrefixToken = "nod" ) // OrgPrefix tags an Organization ID. type OrgPrefix struct{} // Prefix returns OrgPrefixToken. func (OrgPrefix) Prefix() string { return OrgPrefixToken } // UserPrefix tags a User ID. type UserPrefix struct{} // Prefix returns UserPrefixToken. func (UserPrefix) Prefix() string { return UserPrefixToken } // PinPrefix tags a Pin request ID. type PinPrefix struct{} // Prefix returns PinPrefixToken. func (PinPrefix) Prefix() string { return PinPrefixToken } // TokenPrefix tags an API-token JTI. type TokenPrefix struct{} // Prefix returns TokenPrefixToken. func (TokenPrefix) Prefix() string { return TokenPrefixToken } // NodePrefix tags a cluster Node ID. type NodePrefix struct{} // Prefix returns NodePrefixToken. func (NodePrefix) Prefix() string { return NodePrefixToken } // OrgID identifies an Organization. type OrgID struct{ typeid.TypeID[OrgPrefix] } // UserID identifies a User. type UserID struct{ typeid.TypeID[UserPrefix] } // PinID identifies a pin request (returned to clients as PinStatus.requestid). type PinID struct{ typeid.TypeID[PinPrefix] } // TokenID identifies an API token (the JWT's jti claim). type TokenID struct{ typeid.TypeID[TokenPrefix] } // NodeID identifies a cluster node. type NodeID struct{ typeid.TypeID[NodePrefix] } // NewOrg returns a fresh OrgID backed by a random UUIDv7. func NewOrg() (OrgID, error) { return typeid.New[OrgID]() } // NewUser returns a fresh UserID backed by a random UUIDv7. func NewUser() (UserID, error) { return typeid.New[UserID]() } // NewPin returns a fresh PinID backed by a random UUIDv7. func NewPin() (PinID, error) { return typeid.New[PinID]() } // NewToken returns a fresh TokenID backed by a random UUIDv7. func NewToken() (TokenID, error) { return typeid.New[TokenID]() } // NewNode returns a fresh NodeID backed by a random UUIDv7. func NewNode() (NodeID, error) { return typeid.New[NodeID]() } // MustNewOrg is the panic-on-error companion of NewOrg. // // The underlying UUIDv7 generator only fails on a crypto/rand read error; // if that happens, the process is in a state where panicking is the right // response, so call-site ergonomics win. func MustNewOrg() OrgID { return typeid.Must(NewOrg()) } // MustNewUser is the panic-on-error companion of NewUser. func MustNewUser() UserID { return typeid.Must(NewUser()) } // MustNewPin is the panic-on-error companion of NewPin. func MustNewPin() PinID { return typeid.Must(NewPin()) } // MustNewToken is the panic-on-error companion of NewToken. func MustNewToken() TokenID { return typeid.Must(NewToken()) } // MustNewNode is the panic-on-error companion of NewNode. func MustNewNode() NodeID { return typeid.Must(NewNode()) } // ParseOrg parses s as an OrgID, rejecting any value that does not carry // the "org" prefix. func ParseOrg(s string) (OrgID, error) { return parseTyped[OrgID](s, OrgPrefixToken) } // ParseUser parses s as a UserID. func ParseUser(s string) (UserID, error) { return parseTyped[UserID](s, UserPrefixToken) } // ParsePin parses s as a PinID. func ParsePin(s string) (PinID, error) { return parseTyped[PinID](s, PinPrefixToken) } // ParseToken parses s as a TokenID. func ParseToken(s string) (TokenID, error) { return parseTyped[TokenID](s, TokenPrefixToken) } // ParseNode parses s as a NodeID. func ParseNode(s string) (NodeID, error) { return parseTyped[NodeID](s, NodePrefixToken) } // parseTyped is a small wrapper that normalises the "wrong prefix" error so // callers can print a useful message without unwrapping typeid's internal // validation error type. func parseTyped[T typeid.Subtype, PT typeid.SubtypePtr[T]](s, want string) (T, error) { id, err := typeid.Parse[T, PT](s) if err != nil { var zero T return zero, fmt.Errorf("parse %s id %q: %w", want, s, err) } return id, nil }