Documentation
¶
Index ¶
- Constants
- type APIKeyEntity
- type APIKeyStore
- func (s *APIKeyStore) CreateAPIKey(ctx context.Context, req *core.CreateAPIKeyRequest) (*core.CreateAPIKeyResponse, error)
- func (s *APIKeyStore) GetAPIKeyByID(ctx context.Context, req *core.GetAPIKeyByIDRequest) (*core.GetAPIKeyByIDResponse, error)
- func (s *APIKeyStore) ListSubjectAPIKeys(ctx context.Context, req *core.ListSubjectAPIKeysRequest) (*core.ListSubjectAPIKeysResponse, error)
- func (s *APIKeyStore) RevokeAPIKey(ctx context.Context, req *core.RevokeAPIKeyRequest) (*core.RevokeAPIKeyResponse, error)
- func (s *APIKeyStore) UpdateAPIKeyLastUsed(ctx context.Context, req *core.UpdateAPIKeyLastUsedRequest) (*core.UpdateAPIKeyLastUsedResponse, error)
- func (s *APIKeyStore) ValidateAPIKey(ctx context.Context, req *core.ValidateAPIKeyRequest) (*core.ValidateAPIKeyResponse, error)
- type AppRegistrationEntity
- type ChannelEntity
- type ChannelStore
- func (s *ChannelStore) GetChannel(ctx context.Context, req *accounts.GetChannelRequest) (*accounts.GetChannelResponse, error)
- func (s *ChannelStore) GetChannelsByIdentity(ctx context.Context, req *accounts.GetChannelsByIdentityRequest) (*accounts.GetChannelsByIdentityResponse, error)
- func (s *ChannelStore) SaveChannel(ctx context.Context, req *accounts.SaveChannelRequest) (*accounts.SaveChannelResponse, error)
- type DeviceAuthorizationEntity
- type GAEAppStore
- func (s *GAEAppStore) DeleteApp(ctx context.Context, req *core.DeleteAppRequest) (*core.DeleteAppResponse, error)
- func (s *GAEAppStore) GetApp(ctx context.Context, req *core.GetAppRequest) (*core.GetAppResponse, error)
- func (s *GAEAppStore) ListApps(ctx context.Context, req *core.ListAppsRequest) (*core.ListAppsResponse, error)
- func (s *GAEAppStore) SaveApp(ctx context.Context, req *core.SaveAppRequest) (*core.SaveAppResponse, error)
- type GAEDeviceAuthStore
- func (s *GAEDeviceAuthStore) ApproveDeviceAuthorization(ctx context.Context, req *core.ApproveDeviceAuthorizationRequest) (*core.ApproveDeviceAuthorizationResponse, error)
- func (s *GAEDeviceAuthStore) CleanupExpired(ctx context.Context, _ *core.CleanupExpiredDeviceAuthsRequest) (*core.CleanupExpiredDeviceAuthsResponse, error)
- func (s *GAEDeviceAuthStore) CreateDeviceAuthorization(ctx context.Context, req *core.CreateDeviceAuthorizationRequest) (*core.CreateDeviceAuthorizationResponse, error)
- func (s *GAEDeviceAuthStore) DeleteDeviceAuthorization(ctx context.Context, req *core.DeleteDeviceAuthorizationRequest) (*core.DeleteDeviceAuthorizationResponse, error)
- func (s *GAEDeviceAuthStore) DenyDeviceAuthorization(ctx context.Context, req *core.DenyDeviceAuthorizationRequest) (*core.DenyDeviceAuthorizationResponse, error)
- func (s *GAEDeviceAuthStore) GetByDeviceCode(ctx context.Context, req *core.GetByDeviceCodeRequest) (*core.GetByDeviceCodeResponse, error)
- func (s *GAEDeviceAuthStore) GetByUserCode(ctx context.Context, req *core.GetByUserCodeRequest) (*core.GetByUserCodeResponse, error)
- func (s *GAEDeviceAuthStore) UpdatePollingState(ctx context.Context, req *core.UpdatePollingStateRequest) (*core.UpdatePollingStateResponse, error)
- type GAEKeyStore
- func (s *GAEKeyStore) DeleteKey(ctx context.Context, req *keys.DeleteKeyRequest) (*keys.DeleteKeyResponse, error)
- func (s *GAEKeyStore) GetKey(ctx context.Context, req *keys.GetKeyRequest) (*keys.GetKeyResponse, error)
- func (s *GAEKeyStore) GetKeyByKid(ctx context.Context, req *keys.GetKeyByKidRequest) (*keys.GetKeyByKidResponse, error)
- func (s *GAEKeyStore) ListKeyIDs(ctx context.Context, req *keys.ListKeyIDsRequest) (*keys.ListKeyIDsResponse, error)
- func (s *GAEKeyStore) PutKey(ctx context.Context, req *keys.PutKeyRequest) (*keys.PutKeyResponse, error)
- type GAEKidStore
- func (s *GAEKidStore) Add(ctx context.Context, req *keys.AddKidRequest) (*keys.AddKidResponse, error)
- func (s *GAEKidStore) CleanExpired(ctx context.Context, req *keys.CleanExpiredRequest) (*keys.CleanExpiredResponse, error)
- func (s *GAEKidStore) GetKey(ctx context.Context, req *keys.GetKeyRequest) (*keys.GetKeyResponse, error)
- func (s *GAEKidStore) GetKeyByKid(ctx context.Context, req *keys.GetKeyByKidRequest) (*keys.GetKeyByKidResponse, error)
- func (s *GAEKidStore) Remove(ctx context.Context, req *keys.RemoveKidRequest) (*keys.RemoveKidResponse, error)
- type GAEUser
- type IdentityEntity
- type IdentityStore
- func (s *IdentityStore) GetIdentity(ctx context.Context, req *accounts.GetIdentityRequest) (*accounts.GetIdentityResponse, error)
- func (s *IdentityStore) GetUserIdentities(ctx context.Context, req *accounts.GetUserIdentitiesRequest) (*accounts.GetUserIdentitiesResponse, error)
- func (s *IdentityStore) MarkIdentityVerified(ctx context.Context, req *accounts.MarkIdentityVerifiedRequest) (*accounts.MarkIdentityVerifiedResponse, error)
- func (s *IdentityStore) SaveIdentity(ctx context.Context, req *accounts.SaveIdentityRequest) (*accounts.SaveIdentityResponse, error)
- func (s *IdentityStore) SetUserForIdentity(ctx context.Context, req *accounts.SetUserForIdentityRequest) (*accounts.SetUserForIdentityResponse, error)
- type KidKeyEntity
- type RefreshTokenEntity
- type RefreshTokenStore
- func (s *RefreshTokenStore) CleanupExpiredTokens(ctx context.Context, req *core.CleanupExpiredTokensRequest) (*core.CleanupExpiredTokensResponse, error)
- func (s *RefreshTokenStore) CreateRefreshToken(ctx context.Context, req *core.CreateRefreshTokenRequest) (*core.CreateRefreshTokenResponse, error)
- func (s *RefreshTokenStore) GetRefreshToken(ctx context.Context, req *core.GetRefreshTokenRequest) (*core.GetRefreshTokenResponse, error)
- func (s *RefreshTokenStore) GetSubjectTokens(ctx context.Context, req *core.GetSubjectTokensRequest) (*core.GetSubjectTokensResponse, error)
- func (s *RefreshTokenStore) RevokeRefreshToken(ctx context.Context, req *core.RevokeRefreshTokenRequest) (*core.RevokeRefreshTokenResponse, error)
- func (s *RefreshTokenStore) RevokeSubjectTokens(ctx context.Context, req *core.RevokeSubjectTokensRequest) (*core.RevokeSubjectTokensResponse, error)
- func (s *RefreshTokenStore) RevokeTokenFamily(ctx context.Context, req *core.RevokeTokenFamilyRequest) (*core.RevokeTokenFamilyResponse, error)
- func (s *RefreshTokenStore) RotateRefreshToken(ctx context.Context, req *core.RotateRefreshTokenRequest) (*core.RotateRefreshTokenResponse, error)
- type SigningKeyEntity
- type TokenStore
- func (s *TokenStore) CreateToken(ctx context.Context, req *localauth.CreateVerificationTokenRequest) (*localauth.CreateVerificationTokenResponse, error)
- func (s *TokenStore) DeleteSubjectTokens(ctx context.Context, req *localauth.DeleteSubjectVerificationTokensRequest) (*localauth.DeleteSubjectVerificationTokensResponse, error)
- func (s *TokenStore) DeleteToken(ctx context.Context, req *localauth.DeleteVerificationTokenRequest) (*localauth.DeleteVerificationTokenResponse, error)
- func (s *TokenStore) GetToken(ctx context.Context, req *localauth.GetVerificationTokenRequest) (*localauth.GetVerificationTokenResponse, error)
- type UserEntity
- type UserStore
- func (s *UserStore) CreateUser(ctx context.Context, req *accounts.CreateUserRequest) (*accounts.CreateUserResponse, error)
- func (s *UserStore) GetUserById(ctx context.Context, req *accounts.GetUserByIDRequest) (*accounts.GetUserByIDResponse, error)
- func (s *UserStore) SaveUser(ctx context.Context, req *accounts.SaveUserRequest) (*accounts.SaveUserResponse, error)
- type UsernameEntity
- type UsernameStore
- func (s *UsernameStore) ChangeUsername(ctx context.Context, req *accounts.ChangeUsernameRequest) (*accounts.ChangeUsernameResponse, error)
- func (s *UsernameStore) GetUserByUsername(ctx context.Context, req *accounts.GetUserByUsernameRequest) (*accounts.GetUserByUsernameResponse, error)
- func (s *UsernameStore) ReleaseUsername(ctx context.Context, req *accounts.ReleaseUsernameRequest) (*accounts.ReleaseUsernameResponse, error)
- func (s *UsernameStore) ReserveUsername(ctx context.Context, req *accounts.ReserveUsernameRequest) (*accounts.ReserveUsernameResponse, error)
- type VerificationTokenEntity
Constants ¶
const ( KindUser = "User" KindIdentity = "Identity" KindChannel = "Channel" KindAuthToken = "AuthToken" KindRefreshToken = "RefreshToken" KindAPIKey = "APIKey" KindUsername = "Username" )
Kind constants for Datastore entities
const KindAppRegistration = "AppRegistration"
const KindDeviceAuthorization = "DeviceAuthorization"
KindDeviceAuthorization is the Datastore Kind for RFC 8628 device authorization records. Exported so tests can drive bulk cleanup without poking the entity name string from inside the test setup.
const KindKidKey = "KidKey"
const KindSigningKey = "SigningKey"
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type APIKeyEntity ¶
type APIKeyEntity struct {
Key *datastore.Key `datastore:"__key__"` // Key is the KeyID
KeyHash string `datastore:"key_hash,noindex"`
Subject string `datastore:"subject"`
Name string `datastore:"name"`
Scopes []byte `datastore:"scopes,noindex"` // JSON encoded
CreatedAt time.Time `datastore:"created_at"`
ExpiresAt time.Time `datastore:"expires_at,omitempty"`
HasExpiry bool `datastore:"has_expiry"`
LastUsedAt time.Time `datastore:"last_used_at"`
RevokedAt time.Time `datastore:"revoked_at,omitempty"`
Revoked bool `datastore:"revoked"`
}
APIKeyEntity is the Datastore entity for API keys
type APIKeyStore ¶
type APIKeyStore struct {
// contains filtered or unexported fields
}
APIKeyStore implements core.APIKeyStore using Google Cloud Datastore
func NewAPIKeyStore ¶
func NewAPIKeyStore(client *datastore.Client, namespace string) *APIKeyStore
NewAPIKeyStore creates a new Datastore-backed APIKeyStore
func (*APIKeyStore) CreateAPIKey ¶
func (s *APIKeyStore) CreateAPIKey(ctx context.Context, req *core.CreateAPIKeyRequest) (*core.CreateAPIKeyResponse, error)
CreateAPIKey mints a new key. The KeyID (the entity key) is returned in FullKey concatenated with the secret as "<keyID>_<secret>" — only the bcrypt hash of the secret is persisted, so the raw secret can never be retrieved again. Callers must surface FullKey to the end-user immediately; losing it means rotating. HasExpiry tracks whether the caller supplied an ExpiresAt — needed because Datastore stores a zero time.Time identically to a not-set value, but the contract distinguishes "never expires" from "set to 1970-01-01."
func (*APIKeyStore) GetAPIKeyByID ¶
func (s *APIKeyStore) GetAPIKeyByID(ctx context.Context, req *core.GetAPIKeyByIDRequest) (*core.GetAPIKeyByIDResponse, error)
GetAPIKeyByID returns the metadata row for req.KeyID, or core.ErrAPIKeyNotFound when absent. Does NOT validate secret, revocation, or expiry — use ValidateAPIKey for the full check.
func (*APIKeyStore) ListSubjectAPIKeys ¶ added in v0.1.3
func (s *APIKeyStore) ListSubjectAPIKeys(ctx context.Context, req *core.ListSubjectAPIKeysRequest) (*core.ListSubjectAPIKeysResponse, error)
ListSubjectAPIKeys returns every API key (revoked or active) for req.Subject. The KeyHash field is cleared from results — listing the hash to an admin UI would leak bcrypt-protected material that's only meant to live inside validation. Order is unspecified.
func (*APIKeyStore) RevokeAPIKey ¶
func (s *APIKeyStore) RevokeAPIKey(ctx context.Context, req *core.RevokeAPIKeyRequest) (*core.RevokeAPIKeyResponse, error)
RevokeAPIKey flips Revoked=true under transaction. Returns core.ErrAPIKeyNotFound when the key doesn't exist; already-revoked is a no-op success (admin "revoke twice" calls must remain safe).
func (*APIKeyStore) UpdateAPIKeyLastUsed ¶
func (s *APIKeyStore) UpdateAPIKeyLastUsed(ctx context.Context, req *core.UpdateAPIKeyLastUsedRequest) (*core.UpdateAPIKeyLastUsedResponse, error)
UpdateAPIKeyLastUsed bumps LastUsedAt to time.Now() under transaction. Returns an unwrapped datastore.ErrNoSuchEntity (not core.ErrAPIKeyNotFound) when the key is missing — this is on the validation hot path and a missing key already implies an upstream lookup bug rather than user-facing flow. Callers may swallow this error since the bump is advisory.
func (*APIKeyStore) ValidateAPIKey ¶
func (s *APIKeyStore) ValidateAPIKey(ctx context.Context, req *core.ValidateAPIKeyRequest) (*core.ValidateAPIKeyResponse, error)
ValidateAPIKey performs the full check that GetAPIKeyByID skips. Three error sentinels: core.ErrAPIKeyNotFound for "no such key, malformed input, or wrong secret" (collapsed deliberately so a probe can't distinguish); core.ErrTokenRevoked for revoked keys; core.ErrTokenExpired for expired keys. The FullKey format is "oa_<idTail>_<secret>" — first segment fixed, second the id tail, third the secret. Any deviation maps to ErrAPIKeyNotFound.
type AppRegistrationEntity ¶ added in v0.1.12
type AppRegistrationEntity struct {
Key *datastore.Key `datastore:"__key__"`
ClientID string `datastore:"client_id,noindex"`
ClientDomain string `datastore:"client_domain,noindex"`
SigningAlg string `datastore:"signing_alg,noindex"`
AuthorizationDetailsTypes []string `datastore:"authorization_details_types,noindex"`
CreatedAt int64 `datastore:"created_at,noindex"` // unix nanos — keeps Datastore microsecond truncation out of the equality check
Revoked bool `datastore:"revoked,noindex"`
ClientName string `datastore:"client_name,noindex"`
ClientURI string `datastore:"client_uri,noindex"`
RedirectURIs []string `datastore:"redirect_uris,noindex"`
GrantTypes []string `datastore:"grant_types,noindex"`
Scope string `datastore:"scope,noindex"`
TokenEndpointAuthMethod string `datastore:"token_endpoint_auth_method,noindex"`
RegistrationAccessToken string `datastore:"registration_access_token,noindex"`
RegistrationClientURI string `datastore:"registration_client_uri,noindex"`
}
AppRegistrationEntity is the Datastore entity for app registrations. All non-key fields are noindex — we only ever look up by the entity key (client_id), and Datastore's 1500-byte per-property index limit would reject long values like a registration_client_uri or many redirect_uris.
type ChannelEntity ¶
type ChannelEntity struct {
Key *datastore.Key `datastore:"__key__"`
Provider string `datastore:"provider"`
IdentityKey string `datastore:"identity_key"`
Credentials []byte `datastore:"credentials,noindex"` // JSON encoded
Profile []byte `datastore:"profile,noindex"` // JSON encoded
CreatedAt time.Time `datastore:"created_at"`
UpdatedAt time.Time `datastore:"updated_at"`
ExpiresAt time.Time `datastore:"expires_at"` // when channel auth expires
Version int `datastore:"version"`
}
ChannelEntity is the Datastore entity for authentication channels Key format: Provider + ":" + IdentityKey
type ChannelStore ¶
type ChannelStore struct {
// contains filtered or unexported fields
}
ChannelStore implements accounts.ChannelStore using Google Cloud Datastore
func NewChannelStore ¶
func NewChannelStore(client *datastore.Client, namespace string) *ChannelStore
NewChannelStore creates a new Datastore-backed ChannelStore
func (*ChannelStore) GetChannel ¶
func (s *ChannelStore) GetChannel(ctx context.Context, req *accounts.GetChannelRequest) (*accounts.GetChannelResponse, error)
GetChannel returns the (provider, identity) channel, or fmt.Errorf("channel not found") when absent. With req.CreateIfMissing set, an empty channel is created in the same call and NewCreated is set — pairs with provider-bind flows ("first time logging in with Google") that want a "fetch or provision" step without a separate write. The entity key derives from "<provider>:<identityKey>" so the same identity bound to two providers (e.g., Google and GitHub) is two distinct rows.
func (*ChannelStore) GetChannelsByIdentity ¶
func (s *ChannelStore) GetChannelsByIdentity(ctx context.Context, req *accounts.GetChannelsByIdentityRequest) (*accounts.GetChannelsByIdentityResponse, error)
GetChannelsByIdentity returns every channel for a given identity_key across all providers — backed by an indexed query on the identity_key property. Returns an empty slice (not an error) when nothing matches. Order is unspecified.
func (*ChannelStore) SaveChannel ¶
func (s *ChannelStore) SaveChannel(ctx context.Context, req *accounts.SaveChannelRequest) (*accounts.SaveChannelResponse, error)
SaveChannel upserts req.Channel and increments Version each call. CreatedAt is preserved across overwrites; on first write it's stamped from time.Now() and Version starts at 1. Credentials and Profile maps are JSON-encoded into noindex byte properties — Datastore's 1500-byte index limit makes indexing them impractical, and nothing queries by their contents anyway.
type DeviceAuthorizationEntity ¶ added in v0.1.29
type DeviceAuthorizationEntity struct {
Key *datastore.Key `datastore:"__key__"`
DeviceCode string `datastore:"device_code,noindex"`
UserCode string `datastore:"user_code,noindex"`
UserCodeUpper string `datastore:"user_code_upper"`
ClientID string `datastore:"client_id,noindex"`
Scopes []string `datastore:"scopes,noindex"`
RequestedAudience string `datastore:"requested_audience,noindex"`
Status string `datastore:"status,noindex"`
ApprovedSubject string `datastore:"approved_subject,noindex"`
CreatedAt int64 `datastore:"created_at,noindex"`
ExpiresAt int64 `datastore:"expires_at"`
LastPolledAt int64 `datastore:"last_polled_at,noindex"`
IntervalSeconds int `datastore:"interval_seconds,noindex"`
}
DeviceAuthorizationEntity is the Datastore entity for a single device authorization. Two properties carry indexes:
- UserCodeUpper — the case/dash-normalized form used by GetByUserCode. Indexed because it's the only secondary lookup path. The original UserCode column preserves the display form (XXXX-XXXX) the device showed the user.
- ExpiresAt — indexed so CleanupExpired's range query stays selective as the table grows.
Every other property is noindex. Datastore's 1500-byte per-property index limit is irrelevant for the unindexed string fields, and disabling indexing saves write cost.
Times are stored as unix nanos (int64) — Datastore truncates time.Time to microsecond precision, and the unix-nanos round-trip matches what GAEAppStore already does for the same reason.
type GAEAppStore ¶ added in v0.1.12
type GAEAppStore struct {
// contains filtered or unexported fields
}
GAEAppStore implements core.AppRegistrationStore using Google Cloud Datastore. Multi-node compatible — Datastore is the shared source of truth — and slots into stores/gae/ alongside GAEKeyStore and GAEKidStore.
func NewAppStore ¶ added in v0.1.12
func NewAppStore(client *datastore.Client, namespace string) *GAEAppStore
NewAppStore creates a new Datastore-backed AppRegistrationStore.
func (*GAEAppStore) DeleteApp ¶ added in v0.1.12
func (s *GAEAppStore) DeleteApp(ctx context.Context, req *core.DeleteAppRequest) (*core.DeleteAppResponse, error)
DeleteApp removes the registration for req.ClientID. Returns core.ErrAppNotFound if no such registration exists, matching the other AppRegistrationStore backends — Datastore's bare Delete is idempotent and silently succeeds on missing keys, so we Get-then-Delete to honor the contract (the conformance suite's TestDeleteNonexistent asserts on ErrAppNotFound).
func (*GAEAppStore) GetApp ¶ added in v0.1.12
func (s *GAEAppStore) GetApp(ctx context.Context, req *core.GetAppRequest) (*core.GetAppResponse, error)
GetApp returns the registration for req.ClientID, or core.ErrAppNotFound when the entity is absent. Other Datastore errors (network, auth, quota) surface unwrapped so callers can distinguish "not registered" from "infrastructure problem."
func (*GAEAppStore) ListApps ¶ added in v0.1.12
func (s *GAEAppStore) ListApps(ctx context.Context, req *core.ListAppsRequest) (*core.ListAppsResponse, error)
ListApps returns every registration in this namespace. Order is unspecified (Datastore queries without an explicit Order return results in key order, which is implementation-defined). Returns an empty slice (not an error) for an empty namespace, matching the other backends' "fresh store has no apps" shape.
func (*GAEAppStore) SaveApp ¶ added in v0.1.12
func (s *GAEAppStore) SaveApp(ctx context.Context, req *core.SaveAppRequest) (*core.SaveAppResponse, error)
SaveApp inserts or replaces the registration for req.App.ClientID. Empty client_id is rejected with the same error message as InMemoryAppStore / FSAppStore / GORMAppStore so the shared appstoretest contract passes uniformly across backends.
type GAEDeviceAuthStore ¶ added in v0.1.29
type GAEDeviceAuthStore struct {
// contains filtered or unexported fields
}
GAEDeviceAuthStore implements core.DeviceAuthorizationStore on Google Cloud Datastore. Multi-node compatible (Datastore is the shared source of truth) and slots into stores/gae/ alongside GAEAppStore, GAEKeyStore, and GAEKidStore (issue 270).
func NewDeviceAuthStore ¶ added in v0.1.29
func NewDeviceAuthStore(client *datastore.Client, namespace string) *GAEDeviceAuthStore
NewDeviceAuthStore creates a new Datastore-backed DeviceAuthorizationStore. The namespace lets multiple oneauth deployments share a single Datastore project without collisions.
func (*GAEDeviceAuthStore) ApproveDeviceAuthorization ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) ApproveDeviceAuthorization(ctx context.Context, req *core.ApproveDeviceAuthorizationRequest) (*core.ApproveDeviceAuthorizationResponse, error)
ApproveDeviceAuthorization transitions a pending authorization to approved and binds the subject + scopes. Wrapped in a Datastore transaction so two concurrent approvers don't both win — the second transaction commits against stale state and returns ErrDeviceAuthorizationNotFound (matching InMemoryDeviceAuthorizationStore semantics: the second writer sees the no-pending-record state).
func (*GAEDeviceAuthStore) CleanupExpired ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) CleanupExpired(ctx context.Context, _ *core.CleanupExpiredDeviceAuthsRequest) (*core.CleanupExpiredDeviceAuthsResponse, error)
CleanupExpired enumerates the store and removes every record whose ExpiresAt is at or before the current wall-clock time. Uses the indexed expires_at property + DeleteMulti so the call cost scales with the number of expired records, not the table size.
func (*GAEDeviceAuthStore) CreateDeviceAuthorization ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) CreateDeviceAuthorization(ctx context.Context, req *core.CreateDeviceAuthorizationRequest) (*core.CreateDeviceAuthorizationResponse, error)
CreateDeviceAuthorization inserts a new authorization. Empty device_code or user_code is rejected; collisions on either return a generic error matching the in-memory store's wording so a downstream shared contract suite stays uniform.
The collision check is a two-step probe (Get by device_code key, then query by user_code_upper). Without a transaction this is racy under concurrent writes — the entity-key Put at the end provides serializing last-write-wins semantics, but two simultaneous create-with-same-code requests could both pass the pre-check and both write. That race is acceptable for the device-code path (256 bits of entropy makes collision a programmer error, not a real failure mode) and for the user_code path (the caller draws from the same CSPRNG).
func (*GAEDeviceAuthStore) DeleteDeviceAuthorization ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) DeleteDeviceAuthorization(ctx context.Context, req *core.DeleteDeviceAuthorizationRequest) (*core.DeleteDeviceAuthorizationResponse, error)
DeleteDeviceAuthorization removes the authorization. Returns ErrDeviceAuthorizationNotFound when no record matches. Datastore's bare Delete is idempotent (silent success on missing keys), so we Get-then-Delete to honor the contract — same pattern GAEAppStore uses for ErrAppNotFound parity.
func (*GAEDeviceAuthStore) DenyDeviceAuthorization ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) DenyDeviceAuthorization(ctx context.Context, req *core.DenyDeviceAuthorizationRequest) (*core.DenyDeviceAuthorizationResponse, error)
DenyDeviceAuthorization transitions a pending authorization to denied. Wrapped in a transaction for the same reason as ApproveDeviceAuthorization.
func (*GAEDeviceAuthStore) GetByDeviceCode ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) GetByDeviceCode(ctx context.Context, req *core.GetByDeviceCodeRequest) (*core.GetByDeviceCodeResponse, error)
GetByDeviceCode returns the authorization for the given device_code, or ErrDeviceAuthorizationNotFound. Does NOT filter by status or expiry — the caller (the token endpoint handler) checks both.
func (*GAEDeviceAuthStore) GetByUserCode ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) GetByUserCode(ctx context.Context, req *core.GetByUserCodeRequest) (*core.GetByUserCodeResponse, error)
GetByUserCode returns the authorization for the given user_code (case- and dash-insensitive comparison), or ErrDeviceAuthorizationNotFound. Query by the indexed user_code_upper property — O(log n) at the Datastore tier.
func (*GAEDeviceAuthStore) UpdatePollingState ¶ added in v0.1.29
func (s *GAEDeviceAuthStore) UpdatePollingState(ctx context.Context, req *core.UpdatePollingStateRequest) (*core.UpdatePollingStateResponse, error)
UpdatePollingState records a poll attempt — always updates LastPolledAt; raises IntervalSeconds by 5 when SlowDown is true (RFC 8628 §3.5). Read-modify-write inside a transaction so concurrent polls don't lose interval bumps.
type GAEKeyStore ¶
type GAEKeyStore struct {
// contains filtered or unexported fields
}
GAEKeyStore implements keys.KeyStorage using Google Cloud Datastore.
func NewKeyStore ¶
func NewKeyStore(client *datastore.Client, namespace string) *GAEKeyStore
NewKeyStore creates a new Datastore-backed KeyStore.
func (*GAEKeyStore) DeleteKey ¶
func (s *GAEKeyStore) DeleteKey(ctx context.Context, req *keys.DeleteKeyRequest) (*keys.DeleteKeyResponse, error)
DeleteKey removes the signing-key entry for req.ClientID. Returns keys.ErrKeyNotFound when the entry is absent — Datastore's bare Delete is idempotent and silently succeeds on missing keys, so the implementation does a Get-then-Delete to honor the KeyStorage contract (the conformance suite asserts on this sentinel). Other Datastore errors (network, auth, quota) surface unwrapped so callers can distinguish "no such key" from "infrastructure problem."
func (*GAEKeyStore) GetKey ¶
func (s *GAEKeyStore) GetKey(ctx context.Context, req *keys.GetKeyRequest) (*keys.GetKeyResponse, error)
GetKey returns the signing-key record for req.ClientID, or keys.ErrKeyNotFound when the entry is absent. KeyBytes are returned as []byte (the same shape PutKey accepted) — callers are responsible for parsing back to the algorithm-specific key type.
func (*GAEKeyStore) GetKeyByKid ¶
func (s *GAEKeyStore) GetKeyByKid(ctx context.Context, req *keys.GetKeyByKidRequest) (*keys.GetKeyByKidResponse, error)
GetKeyByKid resolves a key by its kid (the JWT header identifier), used by JWKS-driven validators that only know the kid, not the client_id. Returns keys.ErrKidNotFound when no entry carries the requested kid. The query runs against the "kid" property — the only indexed non-key field on SigningKeyEntity — and reads at most one entity (Limit(1)) since kids are unique per JWKS surface.
func (*GAEKeyStore) ListKeyIDs ¶
func (s *GAEKeyStore) ListKeyIDs(ctx context.Context, req *keys.ListKeyIDsRequest) (*keys.ListKeyIDsResponse, error)
ListKeyIDs enumerates every client_id with a registered signing key in this namespace. Returns an empty slice (not an error) for an empty namespace, matching the other KeyStorage backends' "fresh store has no keys" shape. Order is unspecified — Datastore key-only queries return results in implementation-defined order; callers that need deterministic order must sort.
func (*GAEKeyStore) PutKey ¶
func (s *GAEKeyStore) PutKey(ctx context.Context, req *keys.PutKeyRequest) (*keys.PutKeyResponse, error)
PutKey upserts a per-client signing key. The record's Key field must be []byte (HMAC secret or marshaled PEM); any other concrete type is rejected with keys.ErrAlgorithmMismatch — KeyStorage callers serialize before reaching the backend, and a wrong type here means the caller skipped that step. Kid is filled in from utils.ComputeKid(KeyBytes, Algorithm) when the caller leaves it empty so JWKS lookups stay deterministic across restarts. Existing entries with the same ClientID are overwritten — KeyStorage has no separate "create vs update" verb.
type GAEKidStore ¶ added in v0.0.83
type GAEKidStore struct {
// contains filtered or unexported fields
}
GAEKidStore implements keys.KidStorage using Google Cloud Datastore.
func NewKidStore ¶ added in v0.0.83
func NewKidStore(client *datastore.Client, namespace string) *GAEKidStore
NewKidStore creates a new Datastore-backed KidStorage.
func (*GAEKidStore) Add ¶ added in v0.0.83
func (s *GAEKidStore) Add(ctx context.Context, req *keys.AddKidRequest) (*keys.AddKidResponse, error)
Add registers a kid→key entry in the verification-only grace cache used during key rotation. The Key field must be []byte (HMAC secret or marshaled PEM); any other concrete type is rejected with keys.ErrAlgorithmMismatch. ExpiresAt of the zero time means "never expires"; otherwise GetKeyByKid treats now > ExpiresAt as not-found. Existing entries with the same kid are overwritten — KidStorage has no separate "create vs update" verb.
func (*GAEKidStore) CleanExpired ¶ added in v0.0.83
func (s *GAEKidStore) CleanExpired(ctx context.Context, req *keys.CleanExpiredRequest) (*keys.CleanExpiredResponse, error)
CleanExpired sweeps the grace cache for entries whose ExpiresAt has passed and returns the number removed. Implementation reads every entity in the namespace and filters in process — Datastore doesn't expose a "delete where" verb, and the grace cache is small (bounded by rotation cardinality × grace period). Entries with zero ExpiresAt are immortal and skipped. Safe to call concurrently with GetKeyByKid; reads filter on their own independently.
func (*GAEKidStore) GetKey ¶ added in v0.0.83
func (s *GAEKidStore) GetKey(ctx context.Context, req *keys.GetKeyRequest) (*keys.GetKeyResponse, error)
GetKey always returns keys.ErrKeyNotFound. KidStorage is a verification-only grace cache keyed by kid, not by client_id — there is no way to ask "give me the rotation key for this client" because rotation entries are scoped to a kid, not a subject. Callers should reach for GetKeyByKid instead. The method exists only because keys.KidStorage embeds keys.KeyLookup (which carries this verb); the not-found sentinel is the documented behavior across all KidStorage backends, not a GAE-specific gap.
func (*GAEKidStore) GetKeyByKid ¶ added in v0.0.83
func (s *GAEKidStore) GetKeyByKid(ctx context.Context, req *keys.GetKeyByKidRequest) (*keys.GetKeyByKidResponse, error)
GetKeyByKid resolves a kid to its grace-period key during rotation. Returns keys.ErrKidNotFound when no entry exists or when ExpiresAt is non-zero and already in the past. The expiry check runs on read so a cleanup job that hasn't yet swept stale entries doesn't return them as valid — CleanExpired's role is reclaiming storage, not gating reads.
func (*GAEKidStore) Remove ¶ added in v0.0.83
func (s *GAEKidStore) Remove(ctx context.Context, req *keys.RemoveKidRequest) (*keys.RemoveKidResponse, error)
Remove is idempotent — Datastore's Delete silently succeeds on missing keys, and KidStorage's contract permits that ("removing a kid that was never added is not an error"). The verification-only nature of the grace cache means a stale Remove during rotation is harmless.
type GAEUser ¶
type GAEUser struct {
UserID string `json:"user_id"`
Active bool `json:"is_active"`
UserProfile map[string]any `json:"profile"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
GAEUser implements the accounts.User interface
type IdentityEntity ¶
type IdentityEntity struct {
Key *datastore.Key `datastore:"__key__"`
Type string `datastore:"type"`
Value string `datastore:"value"`
UserID string `datastore:"user_id"`
Verified bool `datastore:"verified"`
CreatedAt time.Time `datastore:"created_at"`
UpdatedAt time.Time `datastore:"updated_at"`
Version int `datastore:"version"`
}
IdentityEntity is the Datastore entity for identities Key format: Type + ":" + Value
func IdentityToEntity ¶
func IdentityToEntity(i *accounts.Identity, key *datastore.Key) *IdentityEntity
IdentityToEntity is the write-path counterpart of ToIdentity. The caller supplies the namespaced datastore key — this helper does not derive it because the store struct (IdentityStore) owns the namespace, not this package-level function. The entity name follows the "<type>:<value>" format documented on IdentityEntity.
func (*IdentityEntity) ToIdentity ¶
func (e *IdentityEntity) ToIdentity() *accounts.Identity
ToIdentity copies every IdentityEntity field into a new accounts.Identity. The Datastore Key is NOT carried over — the returned domain object has no pointer back to its storage row, so callers that still need the key must hold the entity. Used by IdentityStore methods immediately after tx.Get to hand domain objects back to the accounts layer.
type IdentityStore ¶
type IdentityStore struct {
// contains filtered or unexported fields
}
IdentityStore implements accounts.IdentityStore using Google Cloud Datastore
func NewIdentityStore ¶
func NewIdentityStore(client *datastore.Client, namespace string) *IdentityStore
NewIdentityStore creates a new Datastore-backed IdentityStore
func (*IdentityStore) GetIdentity ¶
func (s *IdentityStore) GetIdentity(ctx context.Context, req *accounts.GetIdentityRequest) (*accounts.GetIdentityResponse, error)
GetIdentity returns the identity for (req.IdentityType, req.IdentityValue), or fmt.Errorf("identity not found") when absent. With req.CreateIfMissing set, an unbound identity (UserID="") is created in the same call and NewCreated is set in the response — pairs with the signup flow's "ensure-or-create" need without forcing two round trips. The entity key derives from "<type>:<value>" so identities of different types with the same value (e.g., email:foo@bar vs phone:foo@bar) are distinct rows.
func (*IdentityStore) GetUserIdentities ¶
func (s *IdentityStore) GetUserIdentities(ctx context.Context, req *accounts.GetUserIdentitiesRequest) (*accounts.GetUserIdentitiesResponse, error)
GetUserIdentities returns every identity bound to req.UserID. Backed by an indexed query on the user_id property — unlike most properties in this package, user_id is intentionally indexed because this lookup is on the hot path of "show me all the ways this user can log in." Returns an empty slice (not an error) for a user with no identities yet.
func (*IdentityStore) MarkIdentityVerified ¶
func (s *IdentityStore) MarkIdentityVerified(ctx context.Context, req *accounts.MarkIdentityVerifiedRequest) (*accounts.MarkIdentityVerifiedResponse, error)
MarkIdentityVerified flips Verified=true and bumps Version under a transaction. Idempotent at the result level — re-marking an already-verified identity still succeeds and still bumps Version (useful for audit trails that want to count verification attempts).
func (*IdentityStore) SaveIdentity ¶
func (s *IdentityStore) SaveIdentity(ctx context.Context, req *accounts.SaveIdentityRequest) (*accounts.SaveIdentityResponse, error)
SaveIdentity writes req.Identity unconditionally — used after callers have mutated fields not covered by the dedicated SetUserForIdentity / MarkIdentityVerified helpers. Does NOT bump Version; the dedicated helpers do that under transaction. Callers that need version increments should reach for the appropriate helper instead.
func (*IdentityStore) SetUserForIdentity ¶
func (s *IdentityStore) SetUserForIdentity(ctx context.Context, req *accounts.SetUserForIdentityRequest) (*accounts.SetUserForIdentityResponse, error)
SetUserForIdentity binds an identity to a user. Runs under a Datastore transaction so two concurrent calls can't observe a stale UserID and race each other; Version is incremented on every write for optimistic-lock tooling downstream. Identity must already exist — there is no CreateIfMissing here; pair with GetIdentity to provision first.
type KidKeyEntity ¶ added in v0.0.83
type KidKeyEntity struct {
Key *datastore.Key `datastore:"__key__"`
KeyBytes []byte `datastore:"key_bytes,noindex"`
Algorithm string `datastore:"algorithm"`
ClientID string `datastore:"client_id"`
ExpiresAt time.Time `datastore:"expires_at,noindex"`
}
KidKeyEntity is the Datastore entity for kid→key grace entries.
type RefreshTokenEntity ¶
type RefreshTokenEntity struct {
Key *datastore.Key `datastore:"__key__"` // Key is the token hash
Subject string `datastore:"subject"`
ClientID string `datastore:"client_id,omitempty"`
DeviceInfo []byte `datastore:"device_info,noindex"` // JSON encoded
Family string `datastore:"family"`
Generation int `datastore:"generation"`
Scopes []byte `datastore:"scopes,noindex"` // JSON encoded
AuthorizationDetails []byte `datastore:"authorization_details,noindex"` // JSON encoded, RFC 9396
CreatedAt time.Time `datastore:"created_at"`
ExpiresAt time.Time `datastore:"expires_at"`
LastUsedAt time.Time `datastore:"last_used_at"`
RevokedAt time.Time `datastore:"revoked_at,omitempty"`
Revoked bool `datastore:"revoked"`
}
RefreshTokenEntity is the Datastore entity for refresh tokens
type RefreshTokenStore ¶
type RefreshTokenStore struct {
// contains filtered or unexported fields
}
RefreshTokenStore implements core.RefreshTokenStore using Google Cloud Datastore
func NewRefreshTokenStore ¶
func NewRefreshTokenStore(client *datastore.Client, namespace string) *RefreshTokenStore
NewRefreshTokenStore creates a new Datastore-backed RefreshTokenStore
func (*RefreshTokenStore) CleanupExpiredTokens ¶
func (s *RefreshTokenStore) CleanupExpiredTokens(ctx context.Context, req *core.CleanupExpiredTokensRequest) (*core.CleanupExpiredTokensResponse, error)
CleanupExpiredTokens removes (a) every token whose ExpiresAt has passed and (b) every revoked token whose RevokedAt is older than 24 hours. The 24-hour grace on revoked rows lets ongoing rotation paths still detect reuse against recently-revoked tokens before they vanish entirely. Intended for a periodic background sweep, not per-request invocation.
func (*RefreshTokenStore) CreateRefreshToken ¶
func (s *RefreshTokenStore) CreateRefreshToken(ctx context.Context, req *core.CreateRefreshTokenRequest) (*core.CreateRefreshTokenResponse, error)
CreateRefreshToken mints a new refresh-token row with a fresh family id (the first generation in its family). The token string itself is returned to the caller; only its SHA-256 hash is persisted as the entity key — the raw token must never round-trip through the database. Family is a 16-character prefix of a secure random; rotation chains keep this Family constant so RevokeTokenFamily can hit the whole lineage at once on reuse detection.
func (*RefreshTokenStore) GetRefreshToken ¶
func (s *RefreshTokenStore) GetRefreshToken(ctx context.Context, req *core.GetRefreshTokenRequest) (*core.GetRefreshTokenResponse, error)
GetRefreshToken returns the token row for req.Token (looked up by SHA-256 hash). Returns core.ErrTokenNotFound when absent. Does NOT validate Revoked / ExpiresAt — callers that need that should use RotateRefreshToken (which enforces both under transaction). This method exists for read-only inspection paths (admin tooling, audit).
func (*RefreshTokenStore) GetSubjectTokens ¶ added in v0.1.3
func (s *RefreshTokenStore) GetSubjectTokens(ctx context.Context, req *core.GetSubjectTokensRequest) (*core.GetSubjectTokensResponse, error)
GetSubjectTokens returns every active (non-revoked) refresh token for req.Subject — backs the "list active sessions" admin / self-service flow. The plaintext Token field is intentionally cleared from each result; only the hash and metadata travel back to callers, since the store never holds the raw token to begin with.
func (*RefreshTokenStore) RevokeRefreshToken ¶
func (s *RefreshTokenStore) RevokeRefreshToken(ctx context.Context, req *core.RevokeRefreshTokenRequest) (*core.RevokeRefreshTokenResponse, error)
RevokeRefreshToken flips Revoked=true under transaction. Idempotent: a missing or already-revoked token returns success, not an error — explicit logout / RFC 7009 revocation must be safe to call multiple times.
func (*RefreshTokenStore) RevokeSubjectTokens ¶ added in v0.1.3
func (s *RefreshTokenStore) RevokeSubjectTokens(ctx context.Context, req *core.RevokeSubjectTokensRequest) (*core.RevokeSubjectTokensResponse, error)
RevokeSubjectTokens revokes every active (non-revoked) refresh token for req.Subject — used by "log out everywhere" and admin force-revoke paths. Iterates the indexed subject query and stamps Revoked/RevokedAt outside a transaction (the working set can be larger than Datastore's per-tx limit); callers tolerate a brief window where some tokens are revoked and others aren't, since the worst case is a not-yet-revoked token getting one more rotation that then fails reuse detection on the next try.
func (*RefreshTokenStore) RevokeTokenFamily ¶
func (s *RefreshTokenStore) RevokeTokenFamily(ctx context.Context, req *core.RevokeTokenFamilyRequest) (*core.RevokeTokenFamilyResponse, error)
RevokeTokenFamily revokes every active token in a rotation lineage. Called by RotateRefreshToken's reuse-attack path (also addressable from admin tooling). Like RevokeSubjectTokens, runs outside a transaction — the family can have more entities than a single transaction permits.
func (*RefreshTokenStore) RotateRefreshToken ¶
func (s *RefreshTokenStore) RotateRefreshToken(ctx context.Context, req *core.RotateRefreshTokenRequest) (*core.RotateRefreshTokenResponse, error)
RotateRefreshToken implements OAuth refresh-token rotation with reuse detection. Under transaction: revoke the old token, mint a new one in the same Family (Generation+1), preserve Subject/ClientID/Scopes/DeviceInfo/ AuthorizationDetails. Three error sentinels:
- core.ErrTokenNotFound — old token doesn't exist
- core.ErrTokenExpired — old token's ExpiresAt has passed
- core.ErrTokenReused — old token was already revoked (reuse attack)
On reuse, the entire family is revoked OUTSIDE the transaction (Datastore transactions can touch at most 25 entity groups; a family can outgrow that). The transaction itself is best-effort: if the family revoke fails, the reuse is still reported to the caller, and a subsequent admin sweep or RevokeSubjectTokens call can finish the cleanup.
type SigningKeyEntity ¶
type SigningKeyEntity struct {
Key *datastore.Key `datastore:"__key__"`
KeyBytes []byte `datastore:"key_bytes,noindex"`
Algorithm string `datastore:"algorithm"`
Kid string `datastore:"kid"`
}
SigningKeyEntity is the Datastore entity for per-client signing keys.
type TokenStore ¶
type TokenStore struct {
// contains filtered or unexported fields
}
TokenStore implements core.TokenStore using Google Cloud Datastore
func NewTokenStore ¶
func NewTokenStore(client *datastore.Client, namespace string) *TokenStore
NewTokenStore creates a new Datastore-backed TokenStore
func (*TokenStore) CreateToken ¶
func (s *TokenStore) CreateToken(ctx context.Context, req *localauth.CreateVerificationTokenRequest) (*localauth.CreateVerificationTokenResponse, error)
CreateToken mints a fresh verification token via core.GenerateSecureToken (the token is the entity key, so collisions would manifest as overwrites; the secure-random width makes collision negligible). ExpiresAt is computed from req.ExpiryDuration at insert time — the persisted absolute time, not the relative duration, drives GetToken's expiry check.
func (*TokenStore) DeleteSubjectTokens ¶ added in v0.1.3
func (s *TokenStore) DeleteSubjectTokens(ctx context.Context, req *localauth.DeleteSubjectVerificationTokensRequest) (*localauth.DeleteSubjectVerificationTokensResponse, error)
DeleteSubjectTokens removes every verification token of req.Type for req.Subject. Used during password-reset / email-change cleanup to invalidate every outstanding token a subject holds for the same action. Returns success (not an error) when the subject has no matching tokens — the no-op case is normal.
func (*TokenStore) DeleteToken ¶
func (s *TokenStore) DeleteToken(ctx context.Context, req *localauth.DeleteVerificationTokenRequest) (*localauth.DeleteVerificationTokenResponse, error)
DeleteToken is idempotent — Datastore's Delete silently succeeds on missing keys, and verification tokens are single-use by design (delete after the action they unlock fires). No "not found" error.
func (*TokenStore) GetToken ¶
func (s *TokenStore) GetToken(ctx context.Context, req *localauth.GetVerificationTokenRequest) (*localauth.GetVerificationTokenResponse, error)
GetToken returns the token or one of two error strings: "token not found" when absent, "token expired" when the entity exists but its ExpiresAt has passed. Expired entities are deleted on read — a single-shot self-clean so stale rows don't pile up between explicit DeleteSubjectTokens sweeps. Callers that need to distinguish the two failures match the error string (a sentinel introduction is tracked separately).
type UserEntity ¶
type UserEntity struct {
Key *datastore.Key `datastore:"__key__"`
IsActive bool `datastore:"is_active"`
Profile []byte `datastore:"profile,noindex"` // JSON encoded
CreatedAt time.Time `datastore:"created_at"`
UpdatedAt time.Time `datastore:"updated_at"`
Version int `datastore:"version"`
}
UserEntity is the Datastore entity for users
type UserStore ¶
type UserStore struct {
// contains filtered or unexported fields
}
UserStore implements accounts.UserStore using Google Cloud Datastore
func NewUserStore ¶
NewUserStore creates a new Datastore-backed UserStore
func (*UserStore) CreateUser ¶
func (s *UserStore) CreateUser(ctx context.Context, req *accounts.CreateUserRequest) (*accounts.CreateUserResponse, error)
CreateUser inserts a new user keyed by req.UserID. The caller picks the ID — this backend doesn't generate one — and re-creating the same ID silently overwrites (Datastore's Put is upsert-shaped, not insert-only). CreatedAt and UpdatedAt are stamped from time.Now() on the way in; the returned GAEUser mirrors the persisted entity so callers don't have to re-read.
func (*UserStore) GetUserById ¶
func (s *UserStore) GetUserById(ctx context.Context, req *accounts.GetUserByIDRequest) (*accounts.GetUserByIDResponse, error)
GetUserById returns the user for req.UserID. Absent users surface as fmt.Errorf("user not found: %s", req.UserID) — not a sentinel — diverging from the keys.ErrKeyNotFound / core.ErrAppNotFound pattern used elsewhere in this package. Callers that need to distinguish "missing" from other failures currently match on the string prefix; introducing a sentinel is tracked separately (it's an interface change, not a doc fix).
func (*UserStore) SaveUser ¶
func (s *UserStore) SaveUser(ctx context.Context, req *accounts.SaveUserRequest) (*accounts.SaveUserResponse, error)
SaveUser writes req.User, preserving CreatedAt across overwrites (re-saving an existing user keeps the original create time and only bumps UpdatedAt). IsActive defaults to true unless the concrete *GAEUser carries an explicit false — the accounts.User interface has no Active() getter, so non-GAEUser implementations always end up active. Callers that need a different default should pass a *GAEUser.
type UsernameEntity ¶
type UsernameEntity struct {
Key *datastore.Key `datastore:"__key__"`
Username string `datastore:"username"` // Original case-preserved username
UserID string `datastore:"user_id"`
CreatedAt time.Time `datastore:"created_at"`
}
UsernameEntity is the Datastore entity for username -> userID mapping Key is the username (lowercased for case-insensitive lookup)
type UsernameStore ¶
type UsernameStore struct {
// contains filtered or unexported fields
}
UsernameStore implements accounts.UsernameStore using Google Cloud Datastore
func NewUsernameStore ¶
func NewUsernameStore(client *datastore.Client, namespace string) *UsernameStore
NewUsernameStore creates a new Datastore-backed UsernameStore
func (*UsernameStore) ChangeUsername ¶
func (s *UsernameStore) ChangeUsername(ctx context.Context, req *accounts.ChangeUsernameRequest) (*accounts.ChangeUsernameResponse, error)
ChangeUsername atomically moves a reservation from OldUsername to NewUsername. Two paths:
- Case-only change (lowercased forms equal) — single-entity transaction that just updates the display Username; ownership is verified before write ("username not owned by user").
- True rename — multi-entity transaction that asserts the new lowercased form is free, deletes the old entity, and inserts the new one in the same transaction so concurrent reservers can't win the race in the gap.
Error strings: "old username not found", "old username not owned by user", "new username already taken". CreatedAt resets to now on the new row — the reservation is conceptually fresh for the new username.
func (*UsernameStore) GetUserByUsername ¶
func (s *UsernameStore) GetUserByUsername(ctx context.Context, req *accounts.GetUserByUsernameRequest) (*accounts.GetUserByUsernameResponse, error)
GetUserByUsername resolves a username (case-insensitively) to its UserID. Returns fmt.Errorf("username not found") when absent — same string-error pattern as the other accounts-package lookups; sentinel introduction is tracked separately.
func (*UsernameStore) ReleaseUsername ¶
func (s *UsernameStore) ReleaseUsername(ctx context.Context, req *accounts.ReleaseUsernameRequest) (*accounts.ReleaseUsernameResponse, error)
ReleaseUsername drops the reservation. Idempotent — Datastore's Delete silently succeeds on missing keys, and account-cleanup paths must remain safe to retry. Does NOT verify ownership; callers gating release on "the right user is asking" must check upstream (account deletion, admin release).
func (*UsernameStore) ReserveUsername ¶
func (s *UsernameStore) ReserveUsername(ctx context.Context, req *accounts.ReserveUsernameRequest) (*accounts.ReserveUsernameResponse, error)
ReserveUsername binds req.Username to req.UserID. The entity key is the lowercased form (case-insensitive uniqueness); the Username field preserves the original casing for display. Idempotent for the same (username, user) pair — re-reserving by the owner refreshes the display casing but doesn't error. A reservation owned by a different user returns fmt.Errorf("username already taken"). Runs under transaction to close the check-then-write race between two concurrent reservations.
type VerificationTokenEntity ¶ added in v0.1.0
type VerificationTokenEntity struct {
Key *datastore.Key `datastore:"__key__"`
Type localauth.VerificationType `datastore:"type"`
Subject string `datastore:"subject"`
Email string `datastore:"email"`
CreatedAt time.Time `datastore:"created_at"`
ExpiresAt time.Time `datastore:"expires_at"`
}
VerificationTokenEntity is the Datastore entity for localauth verification tokens.
func VerificationTokenToEntity ¶ added in v0.1.0
func VerificationTokenToEntity(t *localauth.VerificationToken, key *datastore.Key) *VerificationTokenEntity
VerificationTokenToEntity is the write-path counterpart of ToVerificationToken. Caller supplies the namespaced datastore key whose name carries the token string; the t.Token field is NOT persisted as a separate property — it lives only in the key, matching the ToVerificationToken contract.
func (*VerificationTokenEntity) ToVerificationToken ¶ added in v0.1.0
func (e *VerificationTokenEntity) ToVerificationToken() *localauth.VerificationToken
ToVerificationToken reconstructs the plaintext Token from e.Key.Name. Non-obvious invariant: the token string is the entity key name, not a stored property — consumers must hold the entity (not just its struct-tagged property bag) to recover the token. This is also why the helper is a method on *VerificationTokenEntity rather than a free function: it depends on the key, which lives on the entity, not on the property struct.