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
- func ActivationToken(link *url.URL) (string, bool)
- func ActivationURL(origin, token string) string
- 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) UpdateOIDCClient(ctx context.Context, clientID string, 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 ¶
func ActivationToken ¶ added in v0.47.3
ActivationToken returns the one-time code of an exact ActivationURL link and reports whether the link has exactly that shape (no user info, fragment or extra query). Callers still check the scheme and host.
func ActivationURL ¶ added in v0.47.3
ActivationURL is the link a new owner or household member follows to enroll a passkey. PocketID v2 serves one-time codes at /lc/<code> (an alias for /login/alternative/code); it has no /setup-account route, which would drop the token and land on /login.
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 redeems through ActivationURL (`/lc/<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) UpdateOIDCClient ¶ added in v0.47.3
func (c *Client) UpdateOIDCClient(ctx context.Context, clientID string, req RegisterClientRequest) (*OIDCClient, error)
UpdateOIDCClient replaces the mutable settings of an existing client (name, callbacks, public/PKCE and group restriction). The secret is unchanged.
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.