pocketid

package
v0.40.0 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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).

View Source
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).

View Source
var ErrNotFound = errors.New("pocketid: resource not found")

ErrNotFound identifies an exact PocketID resource lookup miss.

Functions

This section is empty.

Types

type Client

type Client struct {
	BaseURL    string
	AdminToken string
	HTTP       *http.Client
}

Client is a thin HTTP client for the PocketID admin API.

func NewClient

func NewClient(baseURL, adminToken string) *Client

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

func (c *Client) AddUserToGroup(ctx context.Context, userID, groupID string) error

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

func (c *Client) BootstrapInitialAdmin(ctx context.Context, _, _, _ string) (string, error)

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

func (c *Client) CreateOIDCClientSecret(ctx context.Context, clientID string) (string, error)

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

func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (*User, error)

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

func (c *Client) DeleteUser(ctx context.Context, userID string) error

DeleteUser removes one PocketID subject. Callers must refuse owner and break-glass identities before invoking this.

func (*Client) FindUsersByUsername

func (c *Client) FindUsersByUsername(ctx context.Context, username string) ([]User, error)

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

func (c *Client) GetGroupIDByName(ctx context.Context, name string) (string, error)

GetGroupIDByName resolves a user-group name to its server-side ID. Returns "" with a nil error if no group matches.

func (*Client) GetOIDCClient

func (c *Client) GetOIDCClient(ctx context.Context, clientID string) (*OIDCClient, error)

GetOIDCClient reads the exact client including its allowed group projection.

func (*Client) GetUser

func (c *Client) GetUser(ctx context.Context, userID string) (*User, error)

GetUser reads the exact PocketID subject including its current groups.

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

func (c *Client) ListUsers(ctx context.Context) ([]User, error)

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.

func (*Client) UpdateUserGroups

func (c *Client) UpdateUserGroups(ctx context.Context, userID string, groupIDs []string) (*User, error)

UpdateUserGroups replaces PocketID's group-ID set in one request and returns the server readback. Callers must pass the complete desired set.

func (*Client) WaitHealthy

func (c *Client) WaitHealthy(ctx context.Context, timeout time.Duration) error

WaitHealthy polls the PocketID `/healthz` endpoint until it returns 2xx or the context/timeout fires.

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 HTTPError

type HTTPError struct {
	StatusCode int
	Method     string
	Path       string
	Body       string
}

HTTPError represents a non-2xx response from PocketID.

func (*HTTPError) Error

func (e *HTTPError) Error() string

Error implements error.

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

Unwrap exposes a lookup miss as ErrNotFound so callers can distinguish "this resource is gone" from "this resource answered something unexpected" on every endpoint, not only the ones that map the status explicitly.

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

type WebAuthnCredential struct {
	ID   string `json:"id"`
	Name string `json:"name,omitempty"`
}

WebAuthnCredential is the secret-free registration metadata returned by the admin user credential endpoint.

Jump to

Keyboard shortcuts

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