Official Go SDK for AltsCodex DeOAuth — a decentralized identity layer that bridges OAuth with on-chain account abstraction. This module is the Go port of github.com/webxcom/auth-sdk (legacy) and the third sibling next to:
📚 Docs · Support · Sign up — developers.altscodex.com
🏠 Platform — altscodex.com
It keeps the server-side DeOAuth flow and exposes helper functions for login URL generation, but it does not try to re-create browser-only runtime behavior like popup control, postMessage, or localStorage.
Table of Contents
What this SDK does
AltsCodex DeOAuth is a three-party OAuth flow extended with an on-chain identity layer. The browser obtains a short-lived JWT from the AltsCodex platform server, then your backend uses this SDK to exchange that JWT for the user's slot information (account id, content address, etc.).
Two responsibilities live in this package:
Backend — runs the authorize → callback → get_token chain against the DeOAuth server (api.altscodex.com). One method (GetSlotInfo) and one HTTP handler (HandleCallback) do the whole thing.
- Frontend helpers —
BuildLoginURL + GenerateState. Use these when you render the login page from Go (e.g. server-side templating) and want to redirect the browser to the AltsCodex OAuth server. For the popup-based browser flow, use the JavaScript SDK instead.
What is included
- Browser-facing helpers for login URL construction and OAuth state generation
- Server-side backend flow for
authorize → callback → get_token
- Server-side refresh and logout API calls
- In-memory pending state management for a single process instance
What is not included
- Popup window management
postMessage handling
- Browser token persistence such as
localStorage
- Distributed pending-state storage for multi-instance deployments
Installation
go get github.com/alts-codex/auth-sdk@latest
import sdk "github.com/alts-codex/auth-sdk"
Compatibility
| Item |
Range |
| Go |
1.23+ (per go.mod) |
| Standard library only |
no third-party deps |
| Operating systems |
any platform supported by Go |
Base URLs
| Surface |
Default URL |
Description |
Frontend helper (FrontendConfig.AltsCodexURL) |
https://altscodex.com |
AltsCodex platform server |
Backend (BackendConfig.AuthServerURL) |
https://api.altscodex.com |
DeOAuth server (A-Server) |
Both defaults point to production. Override them for local development or staging.
Apps and games must be registered at the Developer Center. You can also find the detailed protocol documentation there.
Quick start
Frontend helper
Use this when your app needs to generate the login URL but handles browser behavior itself (or hands it off to the JavaScript SDK):
package main
import (
"fmt"
sdk "github.com/alts-codex/auth-sdk"
)
func main() {
state, err := sdk.GenerateState()
if err != nil {
panic(err)
}
loginURL, err := sdk.BuildLoginURL(sdk.FrontendConfig{
ClientID: "YOUR_CLIENT_ID",
RedirectURI: "https://yourapp.com/callback",
// AltsCodexURL defaults to https://altscodex.com
}, sdk.LoginParams{State: state})
if err != nil {
panic(err)
}
fmt.Println(loginURL)
}
Backend HTTP integration
Use this when your backend receives a JWT and needs slot information from the DeOAuth server:
package main
import (
"encoding/json"
"log"
"net/http"
"time"
sdk "github.com/alts-codex/auth-sdk"
)
func main() {
client, err := sdk.NewBackend(sdk.BackendConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET", // never expose to browser
RedirectURI: "https://yourapp.com/getinfo",
// AuthServerURL defaults to https://api.altscodex.com
})
if err != nil {
log.Fatal(err)
}
defer func() { _ = client.Shutdown(nil) }()
// DeOAuth callback (POST). Path must match RedirectURI exactly.
http.HandleFunc("/getinfo", client.HandleCallback)
// Game / app login endpoint
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
jwt := r.URL.Query().Get("jwt")
slotInfo, err := client.GetSlotInfo(r.Context(), jwt, sdk.AuthorizeOptions{Timeout: 15 * time.Second})
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
_ = json.NewEncoder(w).Encode(slotInfo)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
How the full flow works
[Browser] [Your Backend] [DeOAuth Server]
| | |
| 1. JS SDK opens popup ---> | (no backend involvement) |
| 2. user logs in <-------- popup on altscodex.com |
| 3. JS SDK receives JWT | |
| 4. POST /login (jwt) ----> | |
| | 5. GetSlotInfo(ctx, jwt)|
| | register state in |
| | pending map |
| | 6. GET /authorize -----> |
| | (Bearer jwt + state) |
| | <---- success: true -|
| | |
| | ... DeOAuth fires |
| | callback |
| | 7. POST /getinfo <------|
| | (state, code, |
| | success=1) |
| | 8. HandleCallback acks |
| | 200 + detached |
| | ExchangeCode |
| | 9. POST /get_token ---> |
| | (Basic id:secret) |
| | <---- slot info -----|
| | 10. resolve waiter chan |
| 11. {slotInfo} <----------- | |
Key invariants:
- Step 5 happens before step 6.
GetSlotInfo registers the pending channel before dispatching the authorize request, so a callback that arrives faster than the authorize response is still routed correctly.
- Step 8 is synchronous, step 9 is detached.
HandleCallback writes {"received": true} immediately and runs ExchangeCode in a goroutine with a fresh context.Background() so the DeOAuth server's HTTP connection doesn't block on the token round-trip.
API reference
Frontend helpers
type FrontendConfig struct {
AltsCodexURL string // default: https://altscodex.com
ClientID string // required
RedirectURI string // required
ResponseType string // default: "code"
}
type LoginParams struct {
State string // required
}
func GenerateState() (string, error)
func BuildLoginURL(cfg FrontendConfig, params LoginParams) (string, error)
GenerateState returns 16 bytes of crypto/rand data encoded as hex (~128 bits of entropy). Suitable for CSRF protection.
BuildLoginURL builds <AltsCodexURL>/oauth/login?client_id=...&redirect_uri=...&response_type=code&state=....
Backend
type BackendConfig struct {
AuthServerURL string // default: https://api.altscodex.com
ClientID string // required
ClientSecret string // required — held privately
RedirectURI string // required, exact match against Developer Center registration
HTTPClient *http.Client // optional; default http.Client{Timeout: 15s}
}
type AuthorizeOptions struct {
Timeout time.Duration // default: 15s
}
type RefreshOptions struct {
RefreshToken string
Code string
}
func NewBackend(cfg BackendConfig) (*Backend, error)
func (b *Backend) GetSlotInfo(ctx context.Context, jwt string, opts AuthorizeOptions) (SlotInfo, error)
func (b *Backend) HandleCallback(w http.ResponseWriter, r *http.Request)
func (b *Backend) ExchangeCode(ctx context.Context, code string) (SlotInfo, error)
func (b *Backend) RefreshTokens(ctx context.Context, opts RefreshOptions) (TokenSet, error)
func (b *Backend) Logout(ctx context.Context, jwt string) error
func (b *Backend) Shutdown(ctx context.Context) error
ClientSecret is stored on the private Backend.clientSecret field — there is no exported accessor and it never leaks through any of the public methods or types.
(*Backend).GetSlotInfo
Runs the full chain. Returns the user's SlotInfo or an error in one of these shapes:
context.Canceled / context.DeadlineExceeded — caller-supplied ctx was cancelled
authorize callback timeout — opts.Timeout (or default 15s) elapsed
authorize failed (CODE) / authorize failed: msg — DeOAuth /authorize returned success: false
authorize callback rejected (code: ...) — DeOAuth callback arrived with success != "1"
shutdown — Shutdown() was called while this request was pending
(*Backend).HandleCallback
Mount on a POST route whose path matches RedirectURI exactly:
http.HandleFunc("/getinfo", client.HandleCallback)
// or with a custom mux
mux.HandleFunc("/getinfo", client.HandleCallback)
Returns {"received": true} immediately. The get_token exchange runs in a detached goroutine with a fresh context.Background() so the DeOAuth server's connection isn't blocked on the round-trip.
(*Backend).Shutdown
Rejects every still-pending request with error("shutdown") and prevents future GetSlotInfo calls (ErrBackendShutdown). Safe to call multiple times. Call this from signal.Notify(SIGTERM) or your graceful-shutdown path.
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
<-c
_ = client.Shutdown(context.Background())
}()
SlotInfo and TokenSet
type SlotInfo struct {
ID string `json:"id"` // stable user identifier
AccessToken string `json:"access_token"` // DeOAuth access token (not your session token)
ContentAddress string `json:"content_address"` // on-chain wallet address
TokenNickname string `json:"token_nickname"` // slot nickname chosen by the user
TRCnt int `json:"tr_cnt"` // transfer count (on-chain activity counter)
Code string `json:"code"` // the OAuth code that was just exchanged
}
type TokenSet struct {
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
Code string `json:"code,omitempty"`
JWT string `json:"jwt,omitempty"`
}
Use SlotInfo.ID as your stable user identifier (DB primary key).
Concurrency model
- The pending map is
map[string]chan callbackResult guarded by sync.Mutex. All add / remove operations hold the lock.
- Each pending entry holds a buffered channel of size 1.
HandleCallback does a non-blocking send so a late callback after timeout/cancellation never blocks the goroutine.
- State generation uses
crypto/rand 16 bytes hex (~128 bits). Collision-resistant for any realistic concurrency level.
- Authorize dispatch is synchronous within
GetSlotInfo because the Go SDK relies on the request goroutine being alive to read from the channel. The pending entry is added before authorize is sent.
HandleCallback returns synchronously; the get_token exchange runs in a goroutine with context.Background() so the framework's request lifecycle does not cancel the exchange.
- The SDK is safe under multiple concurrent
GetSlotInfo calls with the same JWT or different JWTs. Each call gets its own state and its own channel. The state-keyed map prevents any cross-talk.
- The SDK is not safe across processes — pending state is in-memory. If you run multiple replicas behind a load balancer and the callback hits a different replica than the one that initiated
authorize, the callback silently no-ops and the originating request will time out. Pin to one replica, use sticky sessions, or implement a shared pending store (Redis pub/sub keyed on state).
Configuration
The SDK reads no environment variables. All configuration is passed explicitly to NewBackend / BuildLoginURL. This keeps secrets out of os.Environ() leaks and makes per-request configuration possible (multi-tenant).
A typical environment-variable layout for production deployments:
# .env (server side only — never expose to the browser)
ALTSCODEX_AUTH_SERVER_URL=https://api.altscodex.com
ALTSCODEX_CLIENT_ID=your-registered-client-id
ALTSCODEX_CLIENT_SECRET=your-client-secret
ALTSCODEX_REDIRECT_URI=https://yourapp.com/getinfo
client, err := sdk.NewBackend(sdk.BackendConfig{
AuthServerURL: os.Getenv("ALTSCODEX_AUTH_SERVER_URL"),
ClientID: os.Getenv("ALTSCODEX_CLIENT_ID"),
ClientSecret: os.Getenv("ALTSCODEX_CLIENT_SECRET"),
RedirectURI: os.Getenv("ALTSCODEX_REDIRECT_URI"),
})
Tuning
| Knob |
Default |
When to change |
AuthorizeOptions.Timeout |
15s |
Increase for slow networks; decrease for faster failure |
BackendConfig.HTTPClient |
http.Client{Timeout: 15s} |
Inject a client with custom transport (proxies, mTLS, retries) |
AuthServerURL |
https://api.altscodex.com |
Override for staging / local DeOAuth server |
Error handling & HTTP status mapping
Currently the SDK returns plain error values. Use errors.Is / strings.Contains to branch, or check the message substring. Recommended mapping for your own endpoints:
| Error substring / signal |
HTTP |
EXPIRED_TOKEN (from authorize failed (EXPIRED_TOKEN)) |
401 |
Other authorize failed codes |
502 |
authorize callback timeout |
408 |
authorize callback rejected |
502 |
shutdown |
503 |
context.Canceled / context.DeadlineExceeded |
499 / 504 (per your conventions) |
| Anything else |
500 |
Example handler:
slot, err := client.GetSlotInfo(r.Context(), jwt, sdk.AuthorizeOptions{Timeout: 15 * time.Second})
if err != nil {
msg := err.Error()
switch {
case strings.Contains(msg, "EXPIRED_TOKEN"):
http.Error(w, msg, http.StatusUnauthorized)
case strings.Contains(msg, "authorize callback timeout"):
http.Error(w, msg, http.StatusRequestTimeout)
case strings.Contains(msg, "authorize callback rejected"),
strings.Contains(msg, "authorize failed"):
http.Error(w, msg, http.StatusBadGateway)
case strings.Contains(msg, "shutdown"):
http.Error(w, msg, http.StatusServiceUnavailable)
default:
http.Error(w, msg, http.StatusInternalServerError)
}
return
}
Local development & test server
Override base URLs for staging or local servers:
client, err := sdk.NewBackend(sdk.BackendConfig{
AuthServerURL: "http://localhost:3000",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
RedirectURI: "http://localhost:3070/getinfo",
})
loginURL, err := sdk.BuildLoginURL(sdk.FrontendConfig{
AltsCodexURL: "http://localhost:3000",
ClientID: "YOUR_CLIENT_ID",
RedirectURI: "http://localhost:3070/getinfo",
}, sdk.LoginParams{State: "custom-state"})
Bundled local test server
The module ships with a self-contained mock you can run without depending on external network routing:
go run ./cmd/localtestserver
By default it starts two local HTTP servers:
- frontend mock:
http://127.0.0.1:8888
- backend test server:
http://127.0.0.1:9999
Useful routes:
GET http://127.0.0.1:9999/frontend/login-url — returns a generated login URL and state
GET http://127.0.0.1:9999/login?jwt=test-jwt — runs the mocked authorize → callback → get_token flow and returns SlotInfo
POST http://127.0.0.1:9999/callback — callback endpoint used by the local backend flow
Example:
curl http://127.0.0.1:9999/frontend/login-url
curl "http://127.0.0.1:9999/login?jwt=test-jwt"
You can override the defaults with environment variables: SDK_CLIENT_ID, SDK_CLIENT_SECRET, SDK_FRONTEND_ADDR, SDK_BACKEND_ADDR, SDK_FRONTEND_BASE_URL, SDK_BACKEND_BASE_URL.
Testing your integration
The standard-library httptest package is enough to fully test integrations without a real DeOAuth server. The SDK accepts an injected *http.Client via BackendConfig.HTTPClient, so you can also wire in a custom transport for advanced cases (proxies, retry budgets, mTLS).
A minimal integration test:
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/oauth-meta/authorize":
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
case "/v1/oauth-meta/get_token":
_ = json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]any{{"id": "slot-1", "access_token": "t"}},
})
}
}))
defer authServer.Close()
client, _ := sdk.NewBackend(sdk.BackendConfig{
AuthServerURL: authServer.URL,
ClientID: "cid", ClientSecret: "cs",
RedirectURI: "http://localhost/cb",
})
// Drive the test by manually invoking HandleCallback with a fabricated request.
The repository's own sdk_test.go is a full reference covering the happy path, server-side refresh, logout, and the detached-context exchange.
go test ./...
go test -race ./...
Security
- Never put
ClientSecret in client-side code. It's only valid in server-side environments where Backend lives.
ClientSecret is stored on the private Backend.clientSecret field — no public accessor, no exported method returns it.
RedirectURI must match the Developer Center registration exactly — including scheme (https), host, port, path, and trailing slash. Mismatches surface as invalid_client / redirect_uri mismatch 401 errors.
- CSRF
state is mandatory. BuildLoginURL requires it (LoginParams.State). Store state in your user session and compare on callback.
- Don't log JWTs or
AccessToken values. They grant DeOAuth-level access for their lifetime.
- Use HTTPS in production. Never deploy a non-TLS callback handler.
Common pitfalls
1. Wrong subdomain
| Purpose |
Production |
Local |
| Frontend (platform) |
https://altscodex.com (or www.) |
http://localhost:3000 |
| Backend / API |
https://api.altscodex.com |
http://localhost:3000 |
| Developer Center |
https://developers.altscodex.com |
— |
Do NOT invent subdomains like oauth.altscodex.com, auth.altscodex.com, login.altscodex.com. They resolve to NXDOMAIN and every GetSlotInfo call fails with a network error.
2. POST callback route only — never GET
Always wire HandleCallback on a POST route. If wired as GET, the framework returns 405 and the originating GetSlotInfo call hangs until its timeout.
http.HandleFunc("/getinfo", client.HandleCallback) // matches POST as well
// Some routers require explicit POST mounting:
mux.MethodFunc(http.MethodPost, "/getinfo", client.HandleCallback)
3. The respose_type typo is intentional
The DeOAuth server expects the misspelled query parameter respose_type (not response_type). All three sibling SDKs (@altscodex/sdk, altscodex-sdk Python, github.com/alts-codex/auth-sdk) preserve it. Do not "fix" it — the server contract will break.
4. Multi-replica deployments
The pending map is per-process. If your service runs multiple replicas behind a load balancer and the DeOAuth callback hits a replica different from the one that called GetSlotInfo, the callback silently no-ops and the originating request times out. Pin to one replica, use sticky sessions on state, or implement a Redis-backed pending store.
5. Forgetting to call Shutdown
If your process exits without calling client.Shutdown(ctx), pending channels stay unbuffered and connected clients see indefinite hang. Hook Shutdown into your SIGTERM / SIGINT handler.
6. RedirectURI and the callback handler path mismatch
The path you wire HandleCallback to must match RedirectURI exactly — including trailing slash. A mismatch produces a 404 from your own server, and GetSlotInfo times out without explanation.
Comparison with the JS / Python SDKs
| Feature |
JS (@altscodex/sdk) |
Python (altscodex-sdk) |
Go (github.com/alts-codex/auth-sdk) |
| Browser popup login |
✅ |
❌ |
❌ |
| Server-side login URL builder |
❌ |
✅ AltsCodex().build_login_url() |
✅ sdk.BuildLoginURL(...) |
| Parse callback query |
(inside backend SDK) |
✅ AltsCodex.parse_callback() |
(use HandleCallback) |
authorize → get_token chain |
✅ getSlotInfo |
✅ await get_slot_info |
✅ GetSlotInfo (blocking with ctx) |
| Refresh / logout API |
✅ |
(frontend only) |
✅ RefreshTokens / Logout |
| Concurrency-safe pending map |
✅ |
✅ (asyncio.Future) |
✅ (map[string]chan + mutex) |
| Graceful shutdown |
✅ sdk.shutdown() |
✅ await sdk.shutdown() |
✅ client.Shutdown(ctx) |
ClientSecret privacy |
closure |
name-mangled attribute |
private struct field |
| HTTP client |
fetch / http |
httpx.AsyncClient |
*http.Client (injectable) |
| Test mocks |
jest global.fetch |
httpx.MockTransport |
httptest.Server |
| Bundled local test server |
(none — see local dev section) |
(none — examples folder) |
✅ cmd/localtestserver |
All three SDKs are designed to interoperate. Typical deployment: JS SDK on the frontend, one of Python / Go on the backend.
The new module is a near-exact port. To migrate:
// Module path
import sdk "github.com/webxcom/auth-sdk" // ← old
import sdk "github.com/alts-codex/auth-sdk" // ← new
// Frontend config field rename
sdk.FrontendConfig{ WebXCOMURL: "..." } // ← old
sdk.FrontendConfig{ AltsCodexURL: "..." } // ← new
// Default URLs (these update automatically when you swap the import)
// https://webxcom.com → https://altscodex.com
// https://api.webxcom.com → https://api.altscodex.com
Everything else is unchanged: NewBackend, GetSlotInfo, HandleCallback, ExchangeCode, RefreshTokens, Logout, Shutdown, SlotInfo, TokenSet, AuthorizeOptions, RefreshOptions, BuildLoginURL, GenerateState, LoginParams.
Side note: a pre-existing test bug in the legacy repo (TestGetSlotInfoUsesResponseTypeAndResolvesAfterCallback) asserted the corrected spelling response_type while production code preserved the DeOAuth server's respose_type typo. The assertion is fixed in this port to match production reality.
Release strategy
- Standard semantic version tags on the repository root:
v0.1.0, v0.2.0, v1.0.0.
- Module path is
github.com/alts-codex/auth-sdk — you can ship v0 and v1 tags without changing the module path.
- If you ever publish
v2 or later with breaking changes, the module path must become github.com/alts-codex/auth-sdk/v2 and the code must live under that versioned module path.
- Tag from the repository root that contains this
go.mod, not from a parent mono-repo path.
git tag v0.1.0
git push origin v0.1.0
# Consumers immediately can:
go get github.com/alts-codex/auth-sdk@v0.1.0
Development checks
go test ./...
go test -race ./...
go vet ./...
Resources
License
MIT — see LICENSE.