Documentation
¶
Overview ¶
Package pocketid is a thin HTTP client for the PocketID admin API.
API surface notes (verified against ghcr.io/pocket-id/pocket-id:v2.7.0, upstream v2.6.2 — Step 5.0 of Phase 1 / Task 5):
- Auth header is `X-API-Key` (case-insensitive). PocketID does NOT use `Authorization: Bearer`. JWT cookies and API keys are accepted by the same admin endpoints.
- Bootstrap is solved by the `STATIC_API_KEY` env variable on the PocketID container (added in v1229, ships in v2+). When set, PocketID auto-creates a "Static API User" admin on first request and accepts that env value as a valid `X-API-Key` indefinitely. There is no `/api/setup` endpoint and no first-run wizard exposed via the JSON API in the WebAuthn-only v2 line; the UI flow instead requires registering a passkey through the browser.
- Health endpoint is `/healthz` (returns 204), NOT `/api/health`.
- Group membership is keyed by group ID, not name: `PUT /api/user-groups/:id/users` with `{"userIds":[...]}`.
- `CreateUser` requires `firstName` plus a valid email if email is set.
- OIDC client registration accepts `callbackURLs` (camelCase). The client secret is created in a separate call: `POST /api/oidc/clients/:id/secret`.
Given those findings, BootstrapInitialAdmin is implemented as a verification call against `GET /api/users` using the configured admin token. If the token works, bootstrap is considered already complete and we return ErrAlreadyBootstrapped (the canonical "static-api-key path is already provisioned" signal). Callers (Tasks 6-9) are expected to render the static API key into the PocketID container env at deploy time, so the fact that the token works is the bootstrap success criterion.
Index ¶
- Variables
- type Client
- func (c *Client) AddUserToGroup(ctx context.Context, userID, groupID string) error
- func (c *Client) BootstrapInitialAdmin(ctx context.Context, _, _, _ string) (string, error)
- func (c *Client) CreateOIDCClientSecret(ctx context.Context, clientID string) (string, error)
- func (c *Client) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error)
- func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (*User, error)
- func (c *Client) CreateUserGroup(ctx context.Context, req CreateUserGroupRequest) (*UserGroup, error)
- func (c *Client) DeleteUser(ctx context.Context, userID string) error
- func (c *Client) FindUsersByUsername(ctx context.Context, username string) ([]User, error)
- func (c *Client) GetGroupIDByName(ctx context.Context, name string) (string, error)
- func (c *Client) GetOIDCClient(ctx context.Context, clientID string) (*OIDCClient, error)
- func (c *Client) GetUser(ctx context.Context, userID string) (*User, error)
- func (c *Client) ListUserWebAuthnCredentials(ctx context.Context, userID string) ([]WebAuthnCredential, error)
- func (c *Client) ListUsers(ctx context.Context) ([]User, error)
- func (c *Client) RegisterOIDCClient(ctx context.Context, req RegisterClientRequest) (*OIDCClient, error)
- func (c *Client) UpdateOIDCClientAllowedUserGroups(ctx context.Context, clientID string, groupIDs []string) (*OIDCClient, error)
- func (c *Client) UpdateUserGroups(ctx context.Context, userID string, groupIDs []string) (*User, error)
- func (c *Client) WaitHealthy(ctx context.Context, timeout time.Duration) error
- type CreateUserGroupRequest
- type CreateUserRequest
- type HTTPError
- type OIDCClient
- type RegisterClientRequest
- type User
- type UserGroup
- type WebAuthnCredential
Constants ¶
This section is empty.
Variables ¶
var ErrAlreadyBootstrapped = errors.New("pocketid: instance already bootstrapped")
ErrAlreadyBootstrapped is returned by BootstrapInitialAdmin when PocketID already accepts the configured admin token (typically because the STATIC_API_KEY env var is set and the static admin user has been materialized on a previous call).
var ErrAlreadyExists = errors.New("pocketid: resource already exists")
ErrAlreadyExists is returned by create-style methods (e.g. CreateUserGroup) when the resource is already present at the API. Callers can use errors.Is to detect this and treat it as success (idempotent semantics).
var ErrNotFound = errors.New("pocketid: resource not found")
ErrNotFound identifies an exact PocketID resource lookup miss.
Functions ¶
This section is empty.
Types ¶
type Client ¶
Client is a thin HTTP client for the PocketID admin API.
func NewClient ¶
NewClient creates a PocketID admin client. baseURL must NOT end in a slash; it is the public origin (e.g. "https://id.stack.local"). adminToken is the value rendered into the PocketID container as STATIC_API_KEY.
func (*Client) AddUserToGroup ¶
AddUserToGroup adds a user to a group identified by ID. PocketID's API uses group IDs (UUIDs), not names, so callers must resolve the name to an ID first via GetGroupIDByName.
func (*Client) BootstrapInitialAdmin ¶
BootstrapInitialAdmin verifies that the configured AdminToken is accepted by PocketID. PocketID v2 has no /api/setup JSON endpoint; admin bootstrap is performed by setting STATIC_API_KEY on the container, after which the first authenticated request materializes a built-in "Static API User" admin. This method exercises that path and reports success or failure.
The email/username/password parameters are accepted for API symmetry with the original Phase-1 plan but are ignored — they have no equivalent in the PocketID v2 setup flow. Callers should instead invoke CreateUser to provision the human owner account after this returns.
Returns:
- ErrAlreadyBootstrapped when the token is already accepted (the normal case once STATIC_API_KEY has been provisioned).
- A wrapped HTTP/network error otherwise.
The plan-spec signature returned (adminToken string, err error). The returned token is always c.AdminToken on success — callers that need to persist a token should use the value they passed into NewClient.
func (*Client) CreateOIDCClientSecret ¶
CreateOIDCClientSecret rotates the confidential client's secret. The raw value is returned once and must remain in local private custody.
func (*Client) CreateOneTimeAccessToken ¶
func (c *Client) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (string, error)
CreateOneTimeAccessToken issues a one-time-access token for the given user that the holder can redeem at `/setup-account?token=...` to enroll a WebAuthn credential. PocketID v2 is passkey-only, so this is the only way to bootstrap a freshly-provisioned owner account into a usable state.
PocketID v2.7 accepts the TTL as a Go-duration string in the `ttl` field. The returned string is the raw token (not a full URL); callers compose the setup URL themselves.
Endpoint: `POST /api/users/:id/one-time-access-token`.
func (*Client) CreateUser ¶
CreateUser registers a new PocketID account. Returns the created user (including server-assigned ID).
func (*Client) CreateUserGroup ¶
func (c *Client) CreateUserGroup(ctx context.Context, req CreateUserGroupRequest) (*UserGroup, error)
CreateUserGroup creates a new user group in PocketID and returns the created object. A 409 Conflict (group already exists) maps to ErrAlreadyExists so callers can treat re-runs as idempotent — chain it with errors.Is(err, pocketid.ErrAlreadyExists) at the call site.
Wire format empirically verified against PocketID v2.6.x by Task 14's integration test:
POST /api/user-groups
body: {"name":"...","friendlyName":"..."}
201 Created -> {"id":"...","name":"...","friendlyName":"...",...}
409 Conflict -> already exists (idempotent for callers)
func (*Client) DeleteUser ¶
DeleteUser removes one PocketID subject. Callers must refuse owner and break-glass identities before invoking this.
func (*Client) FindUsersByUsername ¶
FindUsersByUsername searches PocketID and returns only exact username matches. PocketID's server-side search is deliberately fuzzy, so the local owner binder must never accept a near-match as identity evidence.
func (*Client) GetGroupIDByName ¶
GetGroupIDByName resolves a user-group name to its server-side ID. Returns "" with a nil error if no group matches.
func (*Client) GetOIDCClient ¶
GetOIDCClient reads the exact client including its allowed group projection.
func (*Client) ListUserWebAuthnCredentials ¶
func (c *Client) ListUserWebAuthnCredentials(ctx context.Context, userID string) ([]WebAuthnCredential, error)
ListUserWebAuthnCredentials returns passkeys registered to one exact user.
func (*Client) ListUsers ¶
ListUsers returns the current PocketID user page. Callers filter groups themselves; this does not interpret identity roles.
func (*Client) RegisterOIDCClient ¶
func (c *Client) RegisterOIDCClient(ctx context.Context, req RegisterClientRequest) (*OIDCClient, error)
RegisterOIDCClient creates an OIDC client (e.g. TinyAuth) and immediately generates a client secret for it. The returned OIDCClient.Secret is the raw value — record it; PocketID will not return it again.
func (*Client) UpdateOIDCClientAllowedUserGroups ¶
func (c *Client) UpdateOIDCClientAllowedUserGroups( ctx context.Context, clientID string, groupIDs []string, ) (*OIDCClient, error)
UpdateOIDCClientAllowedUserGroups binds the client to the complete desired PocketID group-ID set.
type CreateUserGroupRequest ¶
type CreateUserGroupRequest struct {
Name string `json:"name"`
FriendlyName string `json:"friendlyName"`
}
CreateUserGroupRequest is the body for POST /api/user-groups. PocketID expects both `name` (machine identifier, must be unique) and `friendlyName` (display label).
type CreateUserRequest ¶
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email,omitempty"`
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
DisplayName string `json:"displayName,omitempty"`
IsAdmin bool `json:"isAdmin"`
EmailVerified bool `json:"emailVerified,omitempty"`
Disabled bool `json:"disabled,omitempty"`
UserGroupIDs []string `json:"userGroupIds,omitempty"`
}
CreateUserRequest is the payload for CreateUser. Email is optional but when set must be a valid address. FirstName is recommended (PocketID stores it as a non-nullable column). IsAdmin grants admin scope.
type OIDCClient ¶
type OIDCClient struct {
Credentials struct {
FederatedIdentities []json.RawMessage `json:"federatedIdentities"`
} `json:"credentials"`
RequiresReauthentication bool `json:"requiresReauthentication"`
PkceEnabled bool `json:"pkceEnabled"`
ID string `json:"id"`
Name string `json:"name"`
CallbackURLs []string `json:"callbackURLs"`
IsPublic bool `json:"isPublic"`
IsGroupRestricted bool `json:"isGroupRestricted"`
AllowedUserGroups []UserGroup `json:"allowedUserGroups,omitempty"`
Secret string `json:"-"`
}
OIDCClient describes a registered OIDC client. Secret is only populated after CreateClientSecret returns.
type RegisterClientRequest ¶
type RegisterClientRequest struct {
RequiresReauthentication bool `json:"requiresReauthentication"`
ID string `json:"id,omitempty"`
Name string `json:"name"`
CallbackURLs []string `json:"callbackURLs"`
IsPublic bool `json:"isPublic"`
PkceEnabled bool `json:"pkceEnabled,omitempty"`
IsGroupRestricted bool `json:"isGroupRestricted"`
}
RegisterClientRequest is the payload for RegisterOIDCClient.
type User ¶
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email,omitempty"`
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
DisplayName string `json:"displayName,omitempty"`
IsAdmin bool `json:"isAdmin"`
Disabled bool `json:"disabled,omitempty"`
UserGroups []UserGroup `json:"userGroups,omitempty"`
}
User is the subset of the PocketID user DTO we care about.
type UserGroup ¶
type UserGroup struct {
ID string `json:"id"`
Name string `json:"name"`
FriendlyName string `json:"friendlyName"`
}
UserGroup is the subset of the PocketID user-group DTO we care about.
type WebAuthnCredential ¶
WebAuthnCredential is the secret-free registration metadata returned by the admin user credential endpoint.