pawpado

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 15 Imported by: 0

README

pawpado (Go)

Official Go SDK for Pawpado — the single-user AWS GPU gaming portal. Mirrors the Node and Python SDKs.

Install

go get github.com/hachimi-cat/pawpado-go

Quickstart

package main

import (
    "context"
    "log"

    pawpado "github.com/hachimi-cat/pawpado-go"
)

func main() {
    c, err := pawpado.NewClient(pawpado.ClientOptions{
        APIKey: "tok_...", // or pass a Session for refresh
    })
    if err != nil { log.Fatal(err) }

    ctx := context.Background()
    status, err := c.Sessions.Poll(ctx, "")
    if err != nil { log.Fatal(err) }
    log.Println("state:", status.State)
}
Using a Session (Bearer + refresh)
sess := &pawpado.Session{
    Issuer:   "https://huudis.com",
    ClientID: "oc_pawpado_cli",
}
sess.SetData(&pawpado.ProfileData{
    AccessToken:  bootstrapped.AccessToken,
    RefreshToken: bootstrapped.RefreshToken,
    ExpiresAt:    bootstrapped.ExpiresAt,
})

c, _ := pawpado.NewClient(pawpado.ClientOptions{Session: sess})

// /sessions/poll is a common loop; single-flight refresh is critical here.
for {
    s, err := c.Sessions.Poll(ctx, "")
    if err != nil { log.Fatal(err) }
    if s.Ready != nil && *s.Ready { break }
    time.Sleep(2 * time.Second)
}

Huudis revokes the entire refresh-token family on reuse, so any polling client must serialize refresh attempts. Session.Refresh is single-flight internally — concurrent callers all wait on one in-flight refresh.

Environment variables
Var Effect
PAWPADO_BASE_URL Base URL; defaults to https://pawpado.com.
PAWPADO_API_KEY Static bearer token used when no Session is set.

Surface

Namespace Methods
Sessions Start, Stop, Poll, Pair, Connect, TailscaleKey
Credits Me, Topup
Billing Storage
Settings Get, Update
Account Session, Delete
Admin Reconcile, Orphans
top-level Health, VerifyWebhookSignature

Every namespace method takes a trailing token string — pass "" to use the client's Session / APIKey, or a per-call override.

License

MIT

Documentation

Overview

Package pawpado is the official Go SDK for Pawpado — the single-user AWS GPU gaming portal.

Pick one auth strategy:

  • Session: bearer obtained via Huudis device-flow login (the pawpado CLI / portal stash); SDK does proactive + reactive refresh under a single-flight guard.
  • APIKey: static access token (mirrors the Node SDK's apiKey option).
  • Neither: only Health() works without auth.

Surface mirrors the Node and Python SDKs: sessions, credits, billing, settings, account, admin namespaces plus a top-level health() helper.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func VerifyWebhookSignature

func VerifyWebhookSignature(
	rawBody []byte,
	signatureHeader string,
	secret string,
	options *VerifyWebhookSignatureOptions,
) bool

VerifyWebhookSignature returns true iff the X-Pawpado-Signature header over the given raw request body validates against the provided secret within the tolerance window. Mount your webhook handler such that you can see the raw bytes — a framework that parses + re-serializes JSON will break the HMAC.

Types

type AccountResource

type AccountResource struct {
	// contains filtered or unexported fields
}

AccountResource is the /session + /account namespace.

func (*AccountResource) Delete

func (r *AccountResource) Delete(ctx context.Context, token string) error

Delete deletes /api/v1/account/delete.

func (*AccountResource) Session

func (r *AccountResource) Session(ctx context.Context, token string) (*AccountSession, error)

Session gets /api/v1/session.

type AccountSession

type AccountSession struct {
	User         UserProfile `json:"user"`
	SessionExpAt string      `json:"sessionExpAt"`
}

AccountSession is the response from GET /session.

type AdminReconcileResult

type AdminReconcileResult struct {
	Reconciled int            `json:"reconciled"`
	Details    map[string]any `json:"details,omitempty"`
}

AdminReconcileResult is the response from POST /admin/reconcile.

type AdminResource

type AdminResource struct {
	// contains filtered or unexported fields
}

AdminResource is the /admin namespace (operator-only).

func (*AdminResource) Orphans

func (r *AdminResource) Orphans(ctx context.Context, token string) (*OrphanList, error)

Orphans gets /api/v1/admin/orphans.

func (*AdminResource) Reconcile

func (r *AdminResource) Reconcile(ctx context.Context, token string) (*AdminReconcileResult, error)

Reconcile posts to /api/v1/admin/reconcile.

type BillingResource

type BillingResource struct {
	// contains filtered or unexported fields
}

BillingResource is the /billing namespace.

func (*BillingResource) Storage

func (r *BillingResource) Storage(ctx context.Context, token string) (*BillingStorage, error)

Storage gets /api/v1/billing/storage.

type BillingStorage

type BillingStorage struct {
	StorageGB  float64 `json:"storageGb"`
	RatePerGB  float64 `json:"ratePerGb"`
	PeriodFrom string  `json:"periodFrom"`
	PeriodTo   string  `json:"periodTo"`
}

BillingStorage is the response from GET /billing/storage.

type Client

type Client struct {
	BaseURL string
	HTTP    *http.Client

	// Auth — exactly one of these is typically set.
	Session *Session
	APIKey  string

	// RefreshBufferSec is how many seconds before ExpiresAt we proactively
	// refresh. Defaults to 300 (5min) — matches the Node + Python SDKs.
	RefreshBufferSec int

	// RetryOn5xx is how many extra attempts we make on 5xx responses.
	// Defaults to 1.
	RetryOn5xx int

	Sessions *SessionsResource
	Credits  *CreditsResource
	Billing  *BillingResource
	Settings *SettingsResource
	Account  *AccountResource
	Admin    *AdminResource
}

Client is the high-level Pawpado SDK surface.

Construct via NewClient. Namespaces are exposed as fields so the call site reads like the Node SDK:

c.Sessions.Start(ctx, nil)
c.Credits.Topup(ctx, &pawpado.TopupInput{AmountCents: 50000}, nil)

func NewClient

func NewClient(opts ClientOptions) (*Client, error)

NewClient constructs a Pawpado client. Reads PAWPADO_BASE_URL and PAWPADO_API_KEY from env when the corresponding fields are empty — same convenience the Node + Python SDKs offer.

func (*Client) Health

func (c *Client) Health(ctx context.Context) (*Health, error)

Health hits the public /api/v1/health endpoint. No auth required.

type ClientOptions

type ClientOptions struct {
	// BaseURL defaults to PAWPADO_BASE_URL env var, then https://pawpado.com.
	BaseURL string
	// Session enables Bearer + proactive/reactive refresh.
	Session *Session
	// APIKey is a static access token; mutually exclusive with Session in
	// practice (Session wins if both are set).
	APIKey string
	// HTTP overrides the default http.Client. Useful for tests.
	HTTP *http.Client
	// RefreshBufferSec — see Client.RefreshBufferSec.
	RefreshBufferSec int
	// RetryOn5xx — see Client.RetryOn5xx.
	RetryOn5xx int
}

ClientOptions wires up NewClient. Mirrors the Node SDK's PawpadoClientOptions and the env-var defaults of the huudis-go SDK.

type ConnectInfo

type ConnectInfo struct {
	Host     string  `json:"host"`
	Port     int     `json:"port"`
	Password *string `json:"password,omitempty"`
}

ConnectInfo is returned by GET /sessions/connect.

type Credits

type Credits struct {
	BalanceCents  int64  `json:"balanceCents"`
	Currency      string `json:"currency"` // IDR | USD
	StorageGB     *int   `json:"storageGb,omitempty"`
	FreeStorageGB *int   `json:"freeStorageGb,omitempty"`
	AsOf          string `json:"asOf"`
}

Credits is the wallet view.

type CreditsResource

type CreditsResource struct {
	// contains filtered or unexported fields
}

CreditsResource is the /credits namespace.

func (*CreditsResource) Me

func (r *CreditsResource) Me(ctx context.Context, token string) (*Credits, error)

Me gets /api/v1/credits/me.

func (*CreditsResource) Topup

func (r *CreditsResource) Topup(ctx context.Context, input *TopupInput, token string) (*TopupSession, error)

Topup posts to /api/v1/credits/topup.

type Error

type Error struct {
	Code      string
	Message   string
	Status    int    // HTTP status code; 0 for transport / auth errors.
	RequestID string // From response meta.requestId; empty if absent.
	Details   any    // Server-supplied error.details, if any.
}

Error is the typed error returned by every operation in this package.

Three sub-kinds, distinguished by Code prefix:

  • HTTP envelope errors from the Pawpado API surface this with the server-side .code (e.g. "INSUFFICIENT_CREDITS"), .Status set to the HTTP code, and .RequestID populated from the response meta.
  • Auth / refresh failures use codes like "MISSING_TOKEN", "SESSION_NOT_FOUND", "NO_REFRESH_TOKEN", "REFRESH_FAILED".
  • Transport failures use "NETWORK_ERROR".

Mirrors the Node SDK's PawpadoError / ApiError split and the Python SDK's PawpadoError / PawpadoAuthError / RefreshError / NetworkError. In Go a single typed error with .Code is more idiomatic than four classes — callers branch on Code or use errors.As.

func (*Error) Error

func (e *Error) Error() string

type Health

type Health struct {
	Status string `json:"status"`
}

Health is the response from GET /health.

type OrphanAMI

type OrphanAMI struct {
	ImageID   string  `json:"imageId"`
	SizeGB    float64 `json:"sizeGb"`
	CreatedAt string  `json:"createdAt"`
}

OrphanAMI is one entry in OrphanList.AMIs.

type OrphanEBS

type OrphanEBS struct {
	VolumeID  string  `json:"volumeId"`
	SizeGB    float64 `json:"sizeGb"`
	CreatedAt string  `json:"createdAt"`
}

OrphanEBS is one entry in OrphanList.EBS.

type OrphanList

type OrphanList struct {
	EBS  []OrphanEBS `json:"ebs"`
	AMIs []OrphanAMI `json:"amis,omitempty"`
}

OrphanList is the response from GET /admin/orphans.

type PairInfo

type PairInfo struct {
	PIN       string `json:"pin"`
	PairURL   string `json:"pairUrl"`
	ExpiresAt string `json:"expiresAt"`
}

PairInfo is returned by POST /sessions/pair.

type ProfileData

type ProfileData struct {
	AccessToken  string
	ExpiresAt    int64 // Unix seconds.
	RefreshToken string
	Scope        string
	AccountID    string
}

ProfileData is the in-memory bearer state.

type RefreshFunc

type RefreshFunc func(ctx context.Context, refreshToken string) (*RefreshResult, error)

RefreshFunc is the swap-point for Session refresh. Called with the current refresh token; returns the new token-set or an error.

type RefreshResult

type RefreshResult struct {
	AccessToken  string `json:"access_token"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
	Scope        string `json:"scope,omitempty"`
	TokenType    string `json:"token_type,omitempty"`
}

RefreshResult is what RefreshFunc returns. Mirrors the OIDC token response.

type Session

type Session struct {
	// Issuer is the Huudis base URL (e.g. https://huudis.com). Required.
	Issuer string
	// ClientID is the OIDC client identifier. Required.
	ClientID string

	// HTTP overrides the http.Client used for the token endpoint. Defaults
	// to http.DefaultClient.
	HTTP *http.Client

	// RefreshFunc, if set, replaces the default token-endpoint POST. Useful
	// for tests; production code should leave it nil.
	RefreshFunc RefreshFunc
	// contains filtered or unexported fields
}

Session is the slim in-process credentials holder + single-flight refresh guard. Mirrors the Python SDK's pawpado.session.Session (which itself ports @forjio/sdk's Session). When a central forjio-sdk-go is extracted later, this moves there unchanged.

Single-flight is critical for pawpado: the frontend polls /sessions/poll, so a flurry of in-flight requests can each see an expired access_token and race to call the OIDC refresh endpoint. Huudis revokes the entire refresh-token family on reuse — see memory note feedback_huudis_refresh_singleflight — so without the guard you burn the user's session.

This implementation:

  • Holds bearer state in memory only (no on-disk credentials store; the pawpado portal/CLI manages persistence outside the SDK).
  • Guards refresh with a single-flight Event-style channel: concurrent callers all wait on one fetch and share the result.
  • Defaults to the OIDC token endpoint at {Issuer}/api/v1/oidc/token; tests can inject a RefreshFunc.

func (*Session) Data

func (s *Session) Data() *ProfileData

Data returns the current in-memory ProfileData. nil if SetData has not been called.

func (*Session) IsExpired

func (s *Session) IsExpired() bool

IsExpired reports whether the current access token is past its ExpiresAt. Returns true when no data is loaded.

func (*Session) Refresh

func (s *Session) Refresh(ctx context.Context) error

Refresh atomically refreshes the access token. Concurrent callers share the same in-flight refresh and all receive the same result/error.

Critical for pawpado's polling endpoints: see Session godoc.

func (*Session) SetData

func (s *Session) SetData(d *ProfileData)

SetData installs an in-memory ProfileData. Useful for boot-from-disk or tests. Pass nil to clear.

func (*Session) WillExpireSoon

func (s *Session) WillExpireSoon(bufferSec int) bool

WillExpireSoon reports whether the access token will expire within bufferSec. Returns true when no data is loaded.

type SessionStatus

type SessionStatus struct {
	State      string  `json:"state"` // idle|provisioning|starting|running|stopping|stopped|failed
	InstanceID *string `json:"instanceId,omitempty"`
	PublicIP   *string `json:"publicIp,omitempty"`
	TailnetIP  *string `json:"tailnetIp,omitempty"`
	StartedAt  *string `json:"startedAt,omitempty"`
	Ready      *bool   `json:"ready,omitempty"`
	Message    *string `json:"message,omitempty"`
}

SessionStatus mirrors the Node SDK's SessionStatus type.

type SessionsResource

type SessionsResource struct {
	// contains filtered or unexported fields
}

SessionsResource is the /sessions namespace.

All methods accept an optional per-call bearer token. Pass "" to use the client's Session / APIKey.

func (*SessionsResource) Connect

func (r *SessionsResource) Connect(ctx context.Context, token string) (*ConnectInfo, error)

Connect gets /api/v1/sessions/connect.

func (*SessionsResource) Pair

func (r *SessionsResource) Pair(ctx context.Context, token string) (*PairInfo, error)

Pair posts to /api/v1/sessions/pair.

func (*SessionsResource) Poll

func (r *SessionsResource) Poll(ctx context.Context, token string) (*SessionStatus, error)

Poll gets /api/v1/sessions/poll. Designed to be called every few seconds — the Session's single-flight refresh exists specifically so this method can't burn a refresh-token family.

func (*SessionsResource) Start

func (r *SessionsResource) Start(ctx context.Context, token string) (*SessionStatus, error)

Start posts to /api/v1/sessions/start.

func (*SessionsResource) Stop

func (r *SessionsResource) Stop(ctx context.Context, input *StopInput, token string) (*SessionStatus, error)

Stop posts to /api/v1/sessions/stop. Pass nil for the default no-force behavior; pass &StopInput{Force: true} to force-stop a stuck instance.

func (*SessionsResource) TailscaleKey

func (r *SessionsResource) TailscaleKey(ctx context.Context, token string) (*TailscaleKey, error)

TailscaleKey posts to /api/v1/sessions/tailscale-key.

type Settings

type Settings struct {
	StorageGB       *int     `json:"storageGb,omitempty"`
	Region          *string  `json:"region,omitempty"`
	PreferredHours  []string `json:"preferredHours,omitempty"`
	AutoStopMinutes *int     `json:"autoStopMinutes,omitempty"`
}

Settings is the per-user pawpado settings document.

type SettingsResource

type SettingsResource struct {
	// contains filtered or unexported fields
}

SettingsResource is the /settings namespace.

func (*SettingsResource) Get

func (r *SettingsResource) Get(ctx context.Context, token string) (*Settings, error)

Get gets /api/v1/settings.

func (*SettingsResource) Update

func (r *SettingsResource) Update(ctx context.Context, patch *Settings, token string) (*Settings, error)

Update patches /api/v1/settings.

type StopInput

type StopInput struct {
	Force bool `json:"force,omitempty"`
}

StopInput is the body for POST /sessions/stop.

type TailscaleKey

type TailscaleKey struct {
	AuthKey   string `json:"authKey"`
	ExpiresAt string `json:"expiresAt"`
}

TailscaleKey is the short-lived auth key from POST /sessions/tailscale-key.

type TopupInput

type TopupInput struct {
	AmountCents int64  `json:"amountCents"`
	Currency    string `json:"currency,omitempty"` // IDR | USD
}

TopupInput is the body for POST /credits/topup.

type TopupSession

type TopupSession struct {
	URL       string `json:"url"`
	SessionID string `json:"sessionId"`
}

TopupSession is the hosted checkout URL returned by /credits/topup.

type UserProfile

type UserProfile struct {
	ID            string `json:"id"`
	Email         string `json:"email"`
	Name          string `json:"name"`
	EmailVerified *bool  `json:"emailVerified,omitempty"`
}

UserProfile is the embedded user in /session response.

type VerifyWebhookSignatureOptions

type VerifyWebhookSignatureOptions struct {
	// ToleranceSeconds rejects signatures with a timestamp older than this
	// many seconds. Zero or negative means "use the default" (300).
	ToleranceSeconds int64
	// Now is the clock used for the freshness check. Nil means time.Now.
	// Useful in tests.
	Now func() time.Time
}

VerifyWebhookSignatureOptions tunes the webhook verifier. Pass a pointer to a zero-value struct to use the defaults.

Pawpado's outbound webhook signing is identical to Huudis's: HMAC-SHA256 over "{unix}.{rawBody}" with the shared secret. Header looks like "t=<unix>,v1=<hex>".

Jump to

Keyboard shortcuts

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