AltsCodex SDK — Backend Guide

— NODE.JS + PYTHON (FastAPI) + GO SERVER SDK —

A guide for handling the full De-OAuth server authorize → get_token flow using the SDK in a Node.js, Python (FastAPI), or Go backend. All three SDKs share identical protocol semantics; pick the language that matches your stack. Node.js, Python (FastAPI) 또는 Go 백엔드에서 De-OAuth 서버 authorize → get_token 전체 플로우를 SDK로 처리하는 가이드입니다. 세 SDK 모두 동일한 프로토콜 의미를 공유하므로 자신의 스택에 맞는 언어를 선택하세요.

📦 npm · @altscodex/sdk 🐍 PyPI · altscodex-sdk 🐹 pkg.go.dev · auth-sdk ⌥ GitHub · Python source ⌥ GitHub · Go source
🎮 Building a Unity game client? Unity 게임 클라이언트를 만드시나요?
The Unity SDK (WebGL + Standalone only) handles the client-side login and hands the JWT to your game backend — which then calls one of the server SDKs on this page. See the Frontend / Client Guide for the full Unity SDK reference. Unity SDK (WebGL + Standalone 전용) 가 클라이언트 측 로그인을 처리해 JWT 를 게임 백엔드로 넘겨주고, 게임 백엔드가 이 페이지의 서버 SDK 중 하나로 슬롯 정보를 조회합니다. 전체 Unity SDK 레퍼런스는 Frontend / Client 가이드 참조.

🚀 Quick Start

Integrate the AltsCodex backend SDK in 4 steps. The flow is identical in Node.js, Python, and Go — the SDK abstracts the OAuth chain so your code stays small. 4단계로 AltsCodex 백엔드 SDK를 연동하세요. Node.js, Python, Go 모두 동일한 흐름이며, SDK가 OAuth 체인을 추상화해 코드가 매우 짧아집니다.

1
Install
npm / pip / go get npm · pip · go get 중 하나로 설치
2
Initialize
Create a AltsCodexBackend / Backend instance AltsCodexBackend / Backend 인스턴스 생성
3
Handle Callback
Mount De-OAuth server callback route (POST) De-OAuth 서버 콜백 라우트 마운트 (POST)
4
Get Slot Info
Query slot info with JWT JWT로 슬롯 정보 조회

E2E Authentication Flow

Identical sequence in Node.js, Python, and Go — only the method names differ (camelCase / snake_case / PascalCase). Node.js, Python, Go에서 동일한 시퀀스 — 메서드명만 다릅니다 (camelCase / snake_case / PascalCase).

sequenceDiagram participant B as Browser participant G as Game Backend participant SDK as AltsCodex Backend SDK participant A as De-OAuth 서버 B->>G: POST /login (jwt) G->>SDK: getSlotInfo / get_slot_info / GetSlotInfo (jwt) SDK->>A: GET /v1/oauth-meta/authorize A-->>SDK: 200 OK (authorize accepted) A->>SDK: POST /getinfo (callback: code, state) SDK-->>A: 200 OK (received) SDK->>A: POST /v1/oauth-meta/get_token (code) A-->>SDK: slot info (id, access_token, content_address) SDK-->>G: slotInfo object G-->>B: { success: true, data: slotInfo }

Install Installation 설치

Install the SDK for your stack. All three SDKs declare HTTP / web-framework dependencies in a way that plugs into your existing setup with minimal noise. 스택에 맞는 SDK를 설치하세요. 세 SDK 모두 HTTP / 웹 프레임워크 의존성을 최소 침습 방식으로 설계해 기존 환경에 그대로 끼워 넣을 수 있습니다.

npm install @altscodex/sdk express

# Import
const AltsCodexBackend = require('@altscodex/sdk/backend');
pip install altscodex-sdk

# FastAPI 통합 헬퍼까지 포함
pip install "altscodex-sdk[fastapi]"

# Import
from altscodex import AltsCodexBackend
go get github.com/alts-codex/auth-sdk@latest

# Import
import sdk "github.com/alts-codex/auth-sdk"

# 의존성 = 표준 라이브러리만. 외부 패키지 0개.
ℹ️ optional dependencies
Node — express is a peerDependency. Python — fastapi is an optional extra ([fastapi]); core depends only on httpx. Go — standard library only, no third-party deps. Node — express 는 peerDependency. Python — fastapi 는 optional extra([fastapi]), 코어는 httpx 만 의존. Go — 표준 라이브러리만 사용, 외부 의존성 0개.

Initialize Initialization 초기화

Create a backend instance. The client secret is held privately in all three SDKs — Node.js stores it in a closure, Python in a name-mangled attribute, Go in a private struct field — so it never appears on the public surface. 백엔드 인스턴스를 생성합니다. 클라이언트 시크릿은 세 SDK 모두 비공개로 보관됩니다 — Node.js는 클로저, Python은 name-mangled 속성, Go는 비공개 구조체 필드로 보관 — 공개 surface에 절대 노출되지 않습니다.

JS / Python / GoTypeRequiredDescription
authServerUrl / auth_server_url / AuthServerURL string Optional De-OAuth server base URL. Default: https://api.altscodex.com. De-OAuth 서버 base URL. 기본값: https://api.altscodex.com.
clientId / client_id / ClientID string Required Client ID issued from the developer center 개발자센터에서 발급받은 클라이언트 ID
clientSecret / client_secret / ClientSecret string Required Client secret (held privately, never exposed on the instance) 클라이언트 시크릿 (비공개로 보관, 인스턴스에 노출되지 않음)
redirectUri / redirect_uri / RedirectURI string Required URL to receive De-OAuth server callbacks (POST route, exact match) De-OAuth 서버 콜백 수신 URL (POST 라우트, 정확 일치 필수)
http_client / HTTPClient httpx.AsyncClient / *http.Client Optional (Python / Go only) Inject a shared / customized HTTP client (proxies, mTLS, MockTransport / httptest in tests) (Python / Go 전용) 공유/커스텀 HTTP 클라이언트 주입 (프록시, mTLS, 테스트에서 MockTransport / httptest)
const AltsCodexBackend = require('@altscodex/sdk/backend');

const sdk = new AltsCodexBackend({
  authServerUrl: 'https://api.altscodex.com',
  clientId:      'your-client-id',
  clientSecret:  process.env.CLIENT_SECRET,
  redirectUri:   'http://localhost:3070/getinfo',
});
import os
from altscodex import AltsCodexBackend

sdk = AltsCodexBackend(
    auth_server_url="https://api.altscodex.com",
    client_id="your-client-id",
    client_secret=os.environ["CLIENT_SECRET"],
    redirect_uri="http://localhost:3070/getinfo",
)
package main

import (
    "log"
    "os"

    sdk "github.com/alts-codex/auth-sdk"
)

func main() {
    client, err := sdk.NewBackend(sdk.BackendConfig{
        AuthServerURL: "https://api.altscodex.com",
        ClientID:      "your-client-id",
        ClientSecret:  os.Getenv("CLIENT_SECRET"),
        RedirectURI:   "http://localhost:3070/getinfo",
    })
    if err != nil {
        log.Fatal(err)
    }
    _ = client
}
⚠️ clientSecret Security ⚠️ clientSecret 보안
Never hardcode the client secret. Inject it via environment variables (process.env.CLIENT_SECRET / os.environ["CLIENT_SECRET"] / os.Getenv("CLIENT_SECRET")) and never include it in frontend bundles. All three SDKs hold the secret on a private field with no exported accessor. 클라이언트 시크릿을 절대 하드코딩하지 마세요. 환경 변수(process.env.CLIENT_SECRET / os.environ["CLIENT_SECRET"] / os.Getenv("CLIENT_SECRET"))로 주입하고 프론트엔드 번들에 포함하지 마세요. 세 SDK 모두 시크릿을 비공개 필드에 보관하며, 노출되는 접근자가 없습니다.

Callback Callback Handling 콜백 처리

sdk.handleCallback(req, res) / await sdk.handle_callback(request) / client.HandleCallback(w, r)Handles OAuth callbacks sent from the De-OAuth server. Mount it at the exact same path as redirectUri. De-OAuth 서버에서 보내는 OAuth 콜백을 처리합니다. redirectUri와 정확히 같은 경로에 마운트하세요.

app.post('/getinfo', (req, res) => sdk.handleCallback(req, res));
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/getinfo")
async def getinfo(request: Request):
    return await sdk.handle_callback(request)
// net/http (default mux)
http.HandleFunc("/getinfo", client.HandleCallback)

// chi router 등에서는 명시적으로 POST 메서드만 매칭
// r.MethodFunc(http.MethodPost, "/getinfo", client.HandleCallback)
⚠️ Important: Use POST Method ⚠️ 중요: POST 메서드 사용
Must be mounted at the same path as redirectUri using POST. Not GET. The DeOAuth server posts the callback body. If you wire it as app.get / @app.get / a GET-only handler, the framework returns 405 and your pending request times out. 반드시 redirectUri와 동일한 경로에 POST로 마운트하세요. GET이 아닙니다. DeOAuth 서버는 콜백을 POST로 보내며, app.get / @app.get / GET-only 핸들러로 묶으면 프레임워크가 405를 반환해 대기 중인 요청이 타임아웃됩니다.
How It Works 동작 방식
1. Immediately responds 200 to release the De-OAuth server connection
2. Finds the pending Promise / Future / channel using the state parameter
3. If success=1, exchanges code via get_token and resolves with slot info
4. The get_token exchange runs as a detached task/goroutine — the callback response itself is not blocked on the round-trip
1. De-OAuth 서버 연결 해제를 위해 즉시 200 응답
2. state 파라미터로 대기 중인 Promise / Future / channel 찾기
3. success=1이면 code를 get_token으로 교환해 슬롯 정보로 resolve
4. get_token 교환은 분리된 task/goroutine 로 실행 — 콜백 응답 자체는 라운드트립에 블록되지 않음

Slot Info Slot Info Query 슬롯 정보 조회

sdk.getSlotInfo(jwt, options?) / await sdk.get_slot_info(jwt, *, timeout=15.0) / client.GetSlotInfo(ctx, jwt, sdk.AuthorizeOptions{Timeout: 15*time.Second})Queries the user's slot info using JWT. Runs the full authorize → callback → get_token chain. JWT로 사용자의 슬롯 정보를 조회합니다. authorize → callback → get_token 전체 체인을 실행합니다.

ParameterTypeRequiredDescription
jwt string Required JWT received from the frontend 프론트엔드에서 전달받은 JWT
timeout ms (JS) / seconds (Python) / time.Duration (Go) Optional Callback wait timeout. Default: 15000 ms (JS) / 15.0 s (Python) / 15 * time.Second (Go). 콜백 대기 타임아웃. 기본값: 15000 ms (JS) / 15.0 s (Python) / 15 * time.Second (Go).

Response structure (SlotInfo): 응답 구조 (SlotInfo):

{
  "id": "slot-unique-id",
  "access_token": "eyJhbG...",
  "content_address": "0x742d...",
  "token_nickname": "user-nick",
  "tr_cnt": 3,
  "code": "auth-code"
}

Full server example: 전체 서버 예시:

const express = require('express');
const AltsCodexBackend = require('@altscodex/sdk/backend');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const sdk = new AltsCodexBackend({
  authServerUrl: process.env.AUTH_SERVER_URL,
  clientId:      process.env.CLIENT_ID,
  clientSecret:  process.env.CLIENT_SECRET,
  redirectUri:   process.env.REDIRECT_URI,
});

// De-OAuth 서버 콜백 수신 (POST)
app.post('/getinfo', (req, res) => sdk.handleCallback(req, res));

// 게임 로그인 엔드포인트
app.post('/login', async (req, res) => {
  const jwt = req.body.jwt;
  if (!jwt) return res.status(400).json({ error: 'JWT required' });

  try {
    const slotInfo = await sdk.getSlotInfo(jwt);
    res.json({ success: true, data: slotInfo });
  } catch (err) {
    if (err.message.includes('EXPIRED_TOKEN'))
      return res.status(401).json({ error: 'JWT expired' });
    if (err.message.includes('timeout'))
      return res.status(408).json({ error: 'Timeout' });
    return res.status(500).json({ error: 'Server error' });
  }
});

app.listen(3070, () => console.log('Game server running on port 3070'));
import os
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Request

from altscodex import (
    AltsCodexBackend,
    AuthorizeCallbackTimeoutError,
    AuthorizeFailedError,
    AuthorizeRejectedError,
    ShutdownError,
)

# SDK 인스턴스 — 프로세스당 하나만 생성하고 lifespan에서 정리
sdk = AltsCodexBackend(
    auth_server_url=os.environ.get("ALTSCODEX_AUTH_SERVER_URL"),
    client_id=os.environ["ALTSCODEX_CLIENT_ID"],
    client_secret=os.environ["ALTSCODEX_CLIENT_SECRET"],
    redirect_uri=os.environ["ALTSCODEX_REDIRECT_URI"],
)

@asynccontextmanager
async def lifespan(_app: FastAPI):
    try:
        yield
    finally:
        await sdk.shutdown()

app = FastAPI(lifespan=lifespan)

@app.post("/getinfo")
async def getinfo(request: Request):
    return await sdk.handle_callback(request)

@app.post("/login")
async def login(payload: dict):
    jwt = payload.get("jwt")
    if not jwt:
        raise HTTPException(400, "JWT required")
    try:
        slot = await sdk.get_slot_info(jwt, timeout=15.0)
    except AuthorizeFailedError as err:
        status = 401 if err.code == "EXPIRED_TOKEN" else 502
        raise HTTPException(status, str(err)) from err
    except AuthorizeCallbackTimeoutError as err:
        raise HTTPException(408, str(err)) from err
    except AuthorizeRejectedError as err:
        raise HTTPException(502, str(err)) from err
    except ShutdownError as err:
        raise HTTPException(503, str(err)) from err

    return {"success": True, "data": slot}

# 실행: uvicorn app:app --port 3070
package main

import (
    "context"
    "encoding/json"
    "errors"
    "log"
    "net/http"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"

    sdk "github.com/alts-codex/auth-sdk"
)

func main() {
    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"),
    })
    if err != nil {
        log.Fatal(err)
    }

    mux := http.NewServeMux()

    // De-OAuth 서버 콜백 (POST). RedirectURI 경로와 정확 일치
    mux.HandleFunc("/getinfo", client.HandleCallback)

    // 게임 로그인 엔드포인트
    mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
        jwt := r.URL.Query().Get("jwt")
        if jwt == "" {
            http.Error(w, "jwt required", http.StatusBadRequest)
            return
        }
        slot, err := client.GetSlotInfo(r.Context(), jwt, sdk.AuthorizeOptions{Timeout: 15 * time.Second})
        if err != nil {
            mapErr(w, err)
            return
        }
        _ = json.NewEncoder(w).Encode(map[string]any{"success": true, "data": slot})
    })

    server := &http.Server{Addr: ":3070", Handler: mux}

    // SIGTERM/SIGINT 에서 graceful shutdown — pending future reject + http close
    go func() {
        sigc := make(chan os.Signal, 1)
        signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
        <-sigc
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        _ = client.Shutdown(ctx)
        _ = server.Shutdown(ctx)
    }()

    log.Println("Game server running on port 3070")
    if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}

// Go SDK 에러 → HTTP 상태 매핑 (Python/Node 와 동일한 의미)
func mapErr(w http.ResponseWriter, err error) {
    msg := err.Error()
    switch {
    case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
        http.Error(w, msg, http.StatusGatewayTimeout)
    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)
    }
}

Manage Shutdown & Error Handling 종료 & 에러 처리

sdk.shutdown() / await sdk.shutdown() / client.Shutdown(ctx)Rejects all pending Promises / Futures / channels when the server shuts down. Python additionally closes the SDK-owned httpx.AsyncClient; Go drains the pending map and prevents further GetSlotInfo calls. 서버 종료 시 대기 중인 모든 Promise / Future / channel 을 실패 처리합니다. Python은 추가로 SDK 소유의 httpx.AsyncClient를 닫고, Go는 pending map을 비워 추가 GetSlotInfo 호출을 차단합니다.

// 서버 종료 시 정리
process.on('SIGTERM', () => {
  sdk.shutdown();
  server.close(() => process.exit(0));
});

process.on('SIGINT', () => {
  sdk.shutdown();
  process.exit(0);
});
from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(_app: FastAPI):
    try:
        yield
    finally:
        # pending future reject + http 클라이언트 close
        await sdk.shutdown()

app = FastAPI(lifespan=lifespan)
import (
    "context"
    "os"
    "os/signal"
    "syscall"
    "time"
)

// graceful shutdown — SIGTERM/SIGINT 에서 SDK + HTTP 서버 동시 정리
go func() {
    sigc := make(chan os.Signal, 1)
    signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
    <-sigc
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    _ = client.Shutdown(ctx)
    _ = server.Shutdown(ctx)
}()

Error reference — Node.js / Python / Go all map to the same HTTP status: 에러 매핑 — Node.js / Python / Go 모두 동일한 HTTP 상태로 매핑됩니다:

Node.js error message Python exception Go error substring HTTP
authorize callback timeout AuthorizeCallbackTimeoutError "authorize callback timeout" 408
EXPIRED_TOKEN AuthorizeFailedError (.code == "EXPIRED_TOKEN") strings.Contains("EXPIRED_TOKEN") 401
AUTHORIZE_ERROR AuthorizeFailedError (other codes) strings.Contains("authorize failed") 502
authorize callback rejected AuthorizeRejectedError strings.Contains("authorize callback rejected") 502
shutdown ShutdownError strings.Contains("shutdown") 503
AltsCodexHTTPError mirror .status
errors.Is(ctx canceled / deadline) 499 / 504

Python: every exception inherits from AltsCodexError, so a single @app.exception_handler(AltsCodexError) centralises the mapping. Go: errors are plain error values for now — use strings.Contains on the message, or wrap with your own sentinel via fmt.Errorf("...: %w", err). Python: 모든 예외는 AltsCodexError 를 상속하므로 @app.exception_handler(AltsCodexError) 하나로 매핑을 중앙화할 수 있습니다. Go: 현재는 일반 error 값이므로 메시지 substring 매칭, 또는 fmt.Errorf("...: %w", err) 로 wrapping 후 sentinel 비교를 권장합니다.

Environment Environment Setup 환경변수 설정

All three SDKs read NO environment variables on their own — they accept plain options. Inject them from your server-side environment so the client secret never reaches the browser bundle. 세 SDK 모두 자체적으로 환경 변수를 읽지 않습니다. 옵션으로 직접 전달받습니다. 서버 측 환경변수로 주입해 클라이언트 시크릿이 절대 브라우저 번들에 도달하지 않도록 하세요.

# .env.local — 절대 git 에 커밋 금지, .gitignore 에 추가하세요
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
const AltsCodexBackend = require('@altscodex/sdk/backend');

const sdk = new AltsCodexBackend({
  authServerUrl: process.env.ALTSCODEX_AUTH_SERVER_URL,
  clientId:      process.env.ALTSCODEX_CLIENT_ID,
  clientSecret:  process.env.ALTSCODEX_CLIENT_SECRET,    // server side ONLY
  redirectUri:   process.env.ALTSCODEX_REDIRECT_URI,
});
# .env — 서버 측 전용, .gitignore 에 반드시 추가
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
# 단순 os.environ 방식
import os
from altscodex import AltsCodexBackend

sdk = AltsCodexBackend(
    auth_server_url=os.environ.get("ALTSCODEX_AUTH_SERVER_URL"),
    client_id=os.environ["ALTSCODEX_CLIENT_ID"],
    client_secret=os.environ["ALTSCODEX_CLIENT_SECRET"],
    redirect_uri=os.environ["ALTSCODEX_REDIRECT_URI"],
)
# Pydantic Settings 사용 시
from pydantic_settings import BaseSettings
from altscodex import AltsCodexBackend

class AltsCodexSettings(BaseSettings):
    auth_server_url: str = "https://api.altscodex.com"
    client_id: str
    client_secret: str
    redirect_uri: str

    model_config = {"env_prefix": "ALTSCODEX_"}

settings = AltsCodexSettings()
sdk = AltsCodexBackend(**settings.model_dump())
# .env — systemd EnvironmentFile / docker --env-file 로 주입
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
// os.Getenv 직접 사용
import (
    "log"
    "os"

    sdk "github.com/alts-codex/auth-sdk"
)

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"),
})
if err != nil {
    log.Fatal(err)
}
// .env 로컬 로딩이 필요하면 caarlos0/env 또는 joho/godotenv 같은 작은 패키지 권장
// 프로덕션에서는 systemd EnvironmentFile=, docker --env-file, k8s Secret/ConfigMap 으로 주입
// SDK 자체는 dotenv 종속성을 가지지 않음 (표준 라이브러리만 사용)

Use a separate .env.<mode> per environment (local / staging / production). Next.js auto-loads .env.local; for plain Node use dotenv or PM2 ecosystem files. Python — use python-dotenv + a process manager (systemd / uvicorn --env-file). Go — inject via systemd EnvironmentFile, docker --env-file, or Kubernetes Secret/ConfigMap. 환경별로 .env.<mode> 파일을 분리하세요. Next.js 는 .env.local 을 자동 로드합니다. 순수 Node 는 dotenv 또는 PM2 ecosystem 파일 주입. Python 은 python-dotenv + 프로세스 매니저(systemd / uvicorn --env-file) 조합. Go 는 systemd EnvironmentFile, docker --env-file, Kubernetes Secret/ConfigMap 으로 주입.

Pitfalls ⚠️ Common Pitfalls ⚠️ 자주 부딪히는 함정

1. Use the registered hosts only 1. 등록된 호스트만 사용하세요

Purpose Production Local development
Frontend https://altscodex.com (or www.) http://localhost:3000
Backend / API (authServerUrl / auth_server_url / AuthServerURL) https://api.altscodex.com http://localhost:3000
Developer Center https://developers.altscodex.com

Do NOT point authServerUrl at invented subdomains like oauth.altscodex.com, auth.altscodex.com. They resolve to NXDOMAIN and every getSlotInfo() / get_slot_info() / GetSlotInfo call will fail with a network error. authServerUrloauth.altscodex.com, auth.altscodex.com 같은 미등록 서브도메인으로 박지 마세요. NXDOMAIN 이 떨어지고 모든 getSlotInfo() / get_slot_info() / GetSlotInfo 호출이 네트워크 오류로 실패합니다.

2. redirectUri must match the registered value EXACTLY 2. redirectUri 는 등록된 값과 정확히 일치해야 합니다

The redirectUri / redirect_uri / RedirectURI you pass must exactly match the value registered at the Developer Center — including scheme (https), host, port, path, and even trailing slash. Any difference triggers redirect_uri mismatch 401 at the OAuth callback step. redirectUri / redirect_uri / RedirectURI 는 개발자 센터에 등록된 값과 정확히 일치해야 합니다 — scheme(https), 호스트, 포트, 경로, 트레일링 슬래시까지. 한 글자라도 다르면 OAuth 콜백 단계에서 redirect_uri mismatch 401 이 발생합니다.

3. POST callback route only — never GET 3. 콜백 라우트는 POST 전용 — GET 금지

Express → app.post('/getinfo', ...). FastAPI → @app.post("/getinfo"). Go → http.HandleFunc handles all methods, but routers like chi/gin should pin to POST: r.MethodFunc(http.MethodPost, "/getinfo", client.HandleCallback). If wired as GET, the framework returns 405 and the originating call hangs until timeout. Express → app.post('/getinfo', ...). FastAPI → @app.post("/getinfo"). Go → http.HandleFunc 는 모든 메서드를 받지만, chi/gin 같은 라우터에서는 POST 로 명시: r.MethodFunc(http.MethodPost, "/getinfo", client.HandleCallback). GET 으로 묶으면 프레임워크가 405를 반환하고 호출 측이 타임아웃까지 hang 합니다.

4. The respose_type typo is intentional 4. respose_type 오타는 의도된 것입니다

The DeOAuth server expects the misspelled query parameter respose_type (not response_type). All three SDKs preserve it. Do not "fix" it in any fork — the server contract will break. DeOAuth 서버는 오타 형태의 쿼리 파라미터 respose_type (정상 스펠링 response_type 가 아닙니다) 을 기대합니다. 세 SDK 모두 이를 그대로 보존합니다. fork 시 절대 "수정" 하지 마세요 — 서버 계약이 깨집니다.

5. Migrating from @webxcom/sdk / github.com/webxcom/auth-sdk 5. @webxcom/sdk / github.com/webxcom/auth-sdk 마이그레이션

Old (legacy webxcom) New (altscodex)
require('@webxcom/sdk/backend') require('@altscodex/sdk/backend')
from webxcom_sdk import WebXCOMBackend from altscodex import AltsCodexBackend
import sdk "github.com/webxcom/auth-sdk" import sdk "github.com/alts-codex/auth-sdk"
WebXCOMBackend AltsCodexBackend (JS/Python) / Backend (Go)
option webxcomUrl / field WebXCOMURL option authServerUrl / auth_server_url / field AltsCodexURL

All three SDKs talk to the same backend. v1.x apps continue working unchanged during gradual migration — the platform server emits postMessage in dual-broadcast mode (from: "altscodex" + from: "webxcom") so the frontend SDK of either version stays compatible. 세 SDK 모두 같은 백엔드와 통신합니다. v1.x 앱은 코드 변경 없이 동작합니다 — 플랫폼 서버가 dual-broadcast(from: "altscodex" + from: "webxcom") 로 postMessage 를 발송해 양 버전 프론트엔드 SDK 가 모두 호환됩니다.

6. Always call shutdown() on process exit 6. 프로세스 종료 시 반드시 shutdown() 호출

If your server exits without calling shutdown, pending callback promises / futures / channels stay unresolved and connected clients see indefinite hang. Node — hook on SIGTERM/SIGINT. Python — wire it into FastAPI lifespan (preferred — also closes the SDK-owned httpx.AsyncClient). Go — hook client.Shutdown(ctx) alongside http.Server.Shutdown(ctx) in a signal.Notify handler. 서버가 shutdown 호출 없이 종료되면 대기 중인 콜백 promise / future / channel 들이 미완료 상태로 남아 연결된 클라이언트가 무한 hang 합니다. Node 는 SIGTERM/SIGINT 훅, Python 은 FastAPI lifespan 에 연결 (권장 — SDK 소유의 httpx.AsyncClient 도 함께 close), Go 는 signal.Notify 핸들러에서 client.Shutdown(ctx)http.Server.Shutdown(ctx) 를 동시에 호출.

7. Python-specific: single-worker pending map 7. Python 전용: 단일 워커 pending map 제약

The Python SDK's pending map is in-process. If you run uvicorn --workers 4 and the DeOAuth callback hits a different worker than the one that called get_slot_info, the callback silently no-ops. For now, run a single worker (--workers 1) or use sticky sessions. Python SDK 의 pending map 은 프로세스 내 메모리에 있습니다. uvicorn --workers 4 환경에서 DeOAuth 콜백이 get_slot_info 를 호출한 워커와 다른 워커로 들어오면 콜백이 조용히 no-op 됩니다. 당분간은 단일 워커(--workers 1) 또는 sticky session 운영을 권장합니다.

8. Python-specific: must be called inside a running event loop 8. Python 전용: 실행 중인 event loop 안에서만 호출

The Python SDK is fully async. Calling sdk.get_slot_info(...) without await returns a coroutine object, not a result. From sync code, wrap with asyncio.run(...) or anyio.from_thread.run(...). Python SDK 는 완전 비동기입니다. sdk.get_slot_info(...)await 없이 호출하면 coroutine 객체만 반환됩니다. 동기 코드에서는 asyncio.run(...) 또는 anyio.from_thread.run(...) 으로 감싸세요.

9. Go-specific: multi-replica deployments 9. Go 전용: 멀티 레플리카 배포 제약

The Go SDK's pending map is per-process (map[string]chan callbackResult). 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. Go SDK 의 pending map 은 프로세스별로 존재합니다 (map[string]chan callbackResult). 로드밸런서 뒤에서 멀티 레플리카로 실행 시 DeOAuth 콜백이 GetSlotInfo 를 호출한 레플리카와 다른 곳으로 들어오면 콜백이 조용히 no-op 되고 원 요청은 타임아웃됩니다. 단일 레플리카, sticky session(state 기준), 또는 Redis 기반 pending store 구현 중 하나를 선택하세요.

10. Go-specific: context cancellation does not cancel HandleCallback's token exchange 10. Go 전용: context 취소는 HandleCallback 의 token exchange 를 취소하지 않음

HandleCallback intentionally runs get_token in a goroutine with a fresh context.Background() so the DeOAuth server's HTTP connection isn't blocked. Cancelling the caller's request context will NOT abort the exchange. Use client.Shutdown(ctx) for that. HandleCallback 은 의도적으로 get_token 을 새 context.Background() 의 goroutine 에서 실행해 DeOAuth 서버의 HTTP 연결이 블록되지 않도록 합니다. 호출자의 request context 를 취소해도 exchange 는 중단되지 않습니다. 그 용도로는 client.Shutdown(ctx) 를 사용하세요.

Python+ Python SDK — Bonus Patterns Python SDK — 보너스 패턴

Testing with httpx.MockTransport httpx.MockTransport 로 테스트하기

The Python SDK accepts an injected httpx.AsyncClient, so you can run unit tests without a real DeOAuth server. The SDK's own test suite (6 ported Jest scenarios + contract checks) uses this exact pattern. Python SDK는 httpx.AsyncClient 를 주입받으므로 실제 DeOAuth 서버 없이 단위 테스트를 돌릴 수 있습니다. SDK 자체 테스트(Jest 시나리오 6개 포팅 + 계약 테스트)도 이 패턴을 사용합니다.

import json, httpx, pytest, asyncio
from altscodex import AltsCodexBackend

@pytest.mark.asyncio
async def test_login_succeeds():
    def handler(request):
        if "/authorize" in request.url.path:
            return httpx.Response(200, content=json.dumps({"success": True}).encode(),
                                  headers={"content-type": "application/json"})
        if "/get_token" in request.url.path:
            return httpx.Response(200, content=json.dumps({"id": "u-1"}).encode(),
                                  headers={"content-type": "application/json"})
        return httpx.Response(404)

    client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
    sdk = AltsCodexBackend(
        client_id="cid", client_secret="cs",
        redirect_uri="http://localhost/cb", http_client=client,
    )
    try:
        task = asyncio.create_task(sdk.get_slot_info("jwt"))
        await asyncio.sleep(0.05)
        state = next(iter(sdk._pending_by_state))
        await sdk.handle_callback({"query": {"success": "1", "code": "c", "state": state}})
        result = await task
        assert result["id"] == "u-1"
    finally:
        await sdk.shutdown()

Multi-tenant SDK registry 멀티테넌트 SDK 레지스트리

class SdkRegistry:
    def __init__(self):
        self._by_client: dict[str, AltsCodexBackend] = {}

    def get(self, client_id: str) -> AltsCodexBackend:
        if client_id not in self._by_client:
            cfg = load_client_config(client_id)  # 너의 DB
            self._by_client[client_id] = AltsCodexBackend(
                client_id=client_id,
                client_secret=cfg.secret,
                redirect_uri=cfg.redirect_uri,
            )
        return self._by_client[client_id]

    async def shutdown(self):
        for sdk in self._by_client.values():
            await sdk.shutdown()

Resources 참고 자료

Go+ Go SDK — Bonus Patterns Go SDK — 보너스 패턴

Bundled local test server 번들된 로컬 테스트 서버

The Go module ships with cmd/localtestserver — a self-contained mock that runs the full authorize → callback → get_token flow locally without any external network dependency. Useful for integration tests and local UI development. Go 모듈은 cmd/localtestserver 를 함께 배포합니다 — 외부 네트워크 의존성 없이 전체 authorize → callback → get_token 플로우를 로컬에서 돌리는 mock 입니다. 통합 테스트 / 로컬 UI 개발에 유용합니다.

# 두 개의 로컬 HTTP 서버를 동시 기동
go run ./cmd/localtestserver

# 출력 예:
# local frontend mock listening on http://127.0.0.1:8888
# local backend test server listening on http://127.0.0.1:9999

# 사용 가능한 라우트
curl http://127.0.0.1:9999/frontend/login-url
curl "http://127.0.0.1:9999/login?jwt=test-jwt"

# 환경변수로 포트/URL 오버라이드 가능
# SDK_CLIENT_ID, SDK_CLIENT_SECRET, SDK_FRONTEND_ADDR, SDK_BACKEND_ADDR,
# SDK_FRONTEND_BASE_URL, SDK_BACKEND_BASE_URL

Injecting a custom *http.Client (proxies, mTLS, retries) 커스텀 *http.Client 주입 (프록시 / mTLS / 재시도)

Pass BackendConfig.HTTPClient to override the default http.Client{Timeout: 15s}. Useful for corporate proxies, mTLS certificates, custom retry budgets, or instrumented transports. 기본 http.Client{Timeout: 15s} 를 오버라이드하려면 BackendConfig.HTTPClient 를 전달하세요. 기업 프록시, mTLS 인증서, 커스텀 재시도, 계측된 transport 에 유용합니다.

import (
    "crypto/tls"
    "net/http"
    "time"

    sdk "github.com/alts-codex/auth-sdk"
)

// mTLS + 30초 타임아웃 클라이언트
tlsCert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
customClient := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{tlsCert}},
    },
}

client, err := sdk.NewBackend(sdk.BackendConfig{
    ClientID:     os.Getenv("ALTSCODEX_CLIENT_ID"),
    ClientSecret: os.Getenv("ALTSCODEX_CLIENT_SECRET"),
    RedirectURI:  os.Getenv("ALTSCODEX_REDIRECT_URI"),
    HTTPClient:   customClient,
})

Testing with httptest.Server httptest.Server 로 테스트하기

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"

    sdk "github.com/alts-codex/auth-sdk"
)

func TestLoginFlow(t *testing.T) {
    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",
    })

    // HandleCallback 을 직접 호출해 콜백 도착을 흉내낸다 (httptest.NewRecorder 사용)
    _ = client
    _ = time.Second
}

Resources 참고 자료