npm package: @headless-commerce/sdk
Description: TypeScript SDK for Headless Commerce API
Latest version: 0.2.0

# @headless-commerce/sdk

Headless Commerce API를 위한 공식 TypeScript SDK입니다.

## Features

- **Type-safe**: 모든 API 요청/응답에 대한 완전한 TypeScript 타입
- **Zero dependencies**: `fetch` API만 사용 (Node.js 18+, Deno, Bun, 브라우저)
- **Dual format**: ESM + CJS 동시 지원
- **Auto-pagination**: `for await` 기반 자동 페이지네이션
- **Retry & Timeout**: 429/5xx 자동 재시도 + 요청 타임아웃
- **Stripe-like DX**: `client.products.list()` 패턴의 직관적인 API
- **Idempotency**: 중복 요청 방지를 위한 `Idempotency-Key` 지원
- **Debug logging**: 요청/응답 디버깅 로그 + 커스텀 로거
- **Webhook verification**: HMAC-SHA256 기반 웹훅 서명 검증

## Installation

```bash
npm install @headless-commerce/sdk
# or
pnpm add @headless-commerce/sdk
# or
yarn add @headless-commerce/sdk
```

## Setup

### API Keys

Headless Commerce는 두 종류의 API 키를 사용합니다:

| 키 종류 | Prefix | 용도 |
|---------|--------|------|
| **Publishable Key** | `pk_` | 프론트엔드 (Storefront) — 브라우저에 노출 가능 |
| **Secret Key** | `sk_` | 백엔드 (Admin) — 서버에서만 사용. **절대 프론트엔드에 노출하지 마세요** |

### Base URL

```typescript
// 프로덕션 (기본값)
const client = new HeadlessCommerce({ apiKey: 'pk_xxx' });
// → https://api.headlesscommerce.io/v1

// 로컬 개발
const client = new HeadlessCommerce({
  apiKey: 'pk_test_xxx',
  baseUrl: 'http://localhost:3000/v1',
});
```

## Quick Start

### Storefront (프론트엔드) — 타입 안전 팩토리

```typescript
import { createStorefrontClient } from '@headless-commerce/sdk';

// StorefrontClient 타입 — admin 메서드가 autocomplete에 노출되지 않음
const client = createStorefrontClient({
  apiKey: 'pk_test_xxx',           // Publishable key
  baseUrl: 'http://localhost:3000/v1',
});

// 상품 목록 조회
const products = await client.products.list({ limit: 10 });
console.log(products.data);

// 장바구니 생성 → 상품 추가
const cart = await client.carts.create();
await client.carts.addItem(cart.id, {
  variant_id: 'var_xxx',
  quantity: 2,
});

// 체크아웃
const order = await client.checkout.create(cart.id, {
  email: 'customer@example.com',
  shipping_address: {
    name: 'Hong Gildong',
    line1: '123 Main St',
    city: 'Seoul',
    postal_code: '06000',
    country: 'KR',
  },
});
```

### Admin (백엔드) — 풀 액세스

```typescript
import { createAdminClient } from '@headless-commerce/sdk';

const admin = createAdminClient({
  apiKey: 'sk_test_xxx',           // Secret key
});

// 상품 생성
const product = await admin.products.adminCreate({
  name: 'New T-Shirt',
  slug: 'new-t-shirt',
  type: 'physical',
  status: 'active',
});

// 주문 관리
const orders = await admin.orders.adminList({ status: 'pending' });
await admin.orders.adminConfirm('ord_xxx');
```

## Customer Authentication

서버에서 고객 토큰을 발급하고 클라이언트에서 사용합니다.

```typescript
// Server: 토큰 발급
const admin = new HeadlessCommerce({ apiKey: 'sk_test_xxx' });
const { token } = await admin.customers.adminCreateToken('cus_xxx');

// Client: 토큰 설정
const client = new HeadlessCommerce({ apiKey: 'pk_test_xxx' });
client.setCustomerToken(token);

// 인증된 요청
const me = await client.customers.me();
const addresses = await client.customers.listAddresses();
```

## Auto-Pagination

`for await`로 전체 데이터를 자동 순회합니다.

```typescript
// 리소스에 내장된 자동 페이지네이션
for await (const product of client.products.listAutoPaginate({ limit: 50 })) {
  console.log(product.name);
}

// 또는 autoPaginate 헬퍼 직접 사용
import { autoPaginate } from '@headless-commerce/sdk';

for await (const order of autoPaginate(
  (params) => client.orders.list(params),
  { limit: 20 },
)) {
  console.log(order.number);
}
```

## Retry & Timeout

```typescript
const client = new HeadlessCommerce({
  apiKey: 'pk_test_xxx',
  maxRetries: 3,    // 429, 5xx 에러 시 최대 3회 재시도 (기본값: 2)
  timeout: 30000,   // 요청 타임아웃 30초 (기본값: 없음)
});
```

재시도는 지수 백오프(exponential backoff)를 사용하며, `429 Too Many Requests`와 `5xx` 서버 에러에 대해서만 재시도합니다.

## Idempotency

결제, 주문 생성 등 중복 방지가 필요한 요청에 `idempotencyKey`를 전달합니다.

```typescript
await client.checkout.create(cart.id, checkoutInput, {
  idempotencyKey: 'unique-checkout-key-123',
});

await admin.orders.adminCreate(orderInput, {
  idempotencyKey: `order-${Date.now()}`,
});
```

## Debug Logging

```typescript
// 기본 콘솔 로깅
const client = new HeadlessCommerce({
  apiKey: 'pk_test_xxx',
  debug: true,
});
// 출력: [HeadlessCommerce] GET https://.../ 200 45ms

// 커스텀 로거 (Datadog, Sentry 등 연동)
const client = new HeadlessCommerce({
  apiKey: 'pk_test_xxx',
  logger: (entry) => {
    myLogger.info('api_request', {
      method: entry.method,
      url: entry.url,
      status: entry.status,
      duration: entry.duration,
    });
  },
});
```

## Webhook Verification

웹훅 수신 시 서명을 검증하여 요청의 진위를 확인합니다.

```typescript
import { verifyWebhookSignature } from '@headless-commerce/sdk';

app.post('/webhooks', async (req, res) => {
  const signature = req.headers['x-webhook-signature'] as string;
  const isValid = await verifyWebhookSignature(
    req.body,       // raw body string
    signature,
    WEBHOOK_SECRET,
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);
  // event.type: 'order.created', 'payment.completed', ...
});
```

## Error Handling

```typescript
import { HeadlessCommerce, HeadlessCommerceError } from '@headless-commerce/sdk';

try {
  await client.products.get('invalid_id');
} catch (err) {
  if (err instanceof HeadlessCommerceError) {
    console.error(err.message);   // "Product not found"
    console.error(err.code);      // "RESOURCE_NOT_FOUND"
    console.error(err.status);    // 404
    console.error(err.details);   // { resource: "Product", id: "invalid_id" }

    // Boolean helpers로 에러 유형 분기
    if (err.isNotFound) {
      // 404 — 리소스 없음
    } else if (err.isValidationError) {
      // 400/422 — 입력값 오류
    } else if (err.isAuthError) {
      // 401 — 인증 실패
    } else if (err.isRateLimitError) {
      // 429 — 속도 제한
    }

    // 네트워크/타임아웃 에러는 .cause로 원인 추적
    if (err.code === 'NETWORK_ERROR' || err.code === 'TIMEOUT') {
      console.error('Original error:', err.cause);
    }
  }
}
```

### Error Codes

#### API Error Codes

| Code | HTTP | 설명 |
|------|------|------|
| `MISSING_API_KEY` | 401 | API 키 누락 |
| `INVALID_API_KEY` | 401 | 유효하지 않은 API 키 |
| `INVALID_CUSTOMER_TOKEN` | 401 | 유효하지 않은 고객 토큰 |
| `VALIDATION_ERROR` | 400 | 요청 데이터 검증 실패 |
| `INVALID_CURRENCY` | 400 | 지원하지 않는 통화 코드 |
| `RESOURCE_NOT_FOUND` | 404 | 리소스를 찾을 수 없음 |
| `INSUFFICIENT_STOCK` | 409 | 재고 부족 |
| `INVALID_STATE_TRANSITION` | 409 | 유효하지 않은 상태 전환 (예: cancelled → confirmed) |
| `CART_NOT_ACTIVE` | 409 | 비활성 장바구니에 대한 조작 시도 |
| `DISCOUNT_EXPIRED` | 409 | 만료된 할인 코드 |
| `RATE_LIMIT_EXCEEDED` | 429 | 요청 속도 제한 초과 |
| `INTERNAL_ERROR` | 500 | 서버 내부 오류 |

#### SDK Error Codes

| Code | 설명 |
|------|------|
| `NETWORK_ERROR` | 네트워크 연결 실패 (`.cause`에 원본 에러 포함) |
| `TIMEOUT` | 요청 타임아웃 초과 (`.cause`에 AbortError 포함) |
| `UNKNOWN` | 서버에서 JSON이 아닌 응답을 반환한 경우 |

## API Reference

### Storefront Resources

| Resource | Methods |
|----------|---------|
| `client.products` | `list()`, `get()`, `getBySlug()`, `listAutoPaginate()` |
| `client.categories` | `list()`, `getBySlug()` |
| `client.collections` | `list()`, `get()`, `getBySlug()`, `listAutoPaginate()` |
| `client.carts` | `create()`, `get()`, `getBySession()`, `update()`, `addItem()`, `updateItem()`, `removeItem()`, `applyDiscount()`, `removeDiscount()`, `merge()` |
| `client.checkout` | `create()` |
| `client.orders` | `list()`, `get()`, `lookup()`, `listAutoPaginate()` |
| `client.customers` | `me()`, `updateMe()`, `listAddresses()`, `createAddress()`, `updateAddress()`, `deleteAddress()` |
| `client.shipping` | `list()` |

### Admin Resources

| Resource | Methods |
|----------|---------|
| `client.products` | `adminList()`, `adminGet()`, `adminCreate()`, `adminUpdate()`, `adminDelete()`, `adminAddImage()`, `adminRemoveImage()`, `adminListBundleItems()`, `adminAddBundleItem()`, `adminRemoveBundleItem()`, `adminListAutoPaginate()` |
| `client.categories` | `adminList()`, `adminCreate()`, `adminUpdate()`, `adminDelete()` |
| `client.collections` | `adminList()`, `adminCreate()`, `adminUpdate()`, `adminDelete()`, `adminListAutoPaginate()` |
| `client.orders` | `adminList()`, `adminGet()`, `adminCreate()`, `adminUpdate()`, `adminConfirm()`, `adminProcess()`, `adminComplete()`, `adminCancel()`, `adminListAutoPaginate()` |
| `client.customers` | `adminList()`, `adminGet()`, `adminCreate()`, `adminUpdate()`, `adminDelete()`, `adminCreateToken()`, `adminListAutoPaginate()` |
| `client.variants` | `adminList()`, `adminCreate()`, `adminUpdate()`, `adminDelete()` |
| `client.inventory` | `adminList()`, `adminGet()`, `adminUpdateSettings()`, `adminAdjust()`, `adminListAutoPaginate()` |
| `client.payments` | `adminCreate()`, `adminComplete()` |
| `client.fulfillments` | `adminCreate()`, `adminUpdate()`, `adminShip()`, `adminDeliver()` |
| `client.discounts` | `adminList()`, `adminGet()`, `adminCreate()`, `adminUpdate()`, `adminDelete()`, `adminListAutoPaginate()` |
| `client.shippingMethods` | `adminList()`, `adminGet()`, `adminCreate()`, `adminUpdate()`, `adminDelete()` |
| `client.apiKeys` | `adminList()`, `adminCreate()`, `adminDelete()`, `adminRotate()` |
| `client.organization` | `adminGet()`, `adminUpdate()`, `adminListMembers()`, `adminAddMember()`, `adminUpdateMember()`, `adminRemoveMember()` |
| `client.store` | `adminGet()`, `adminUpdate()` |
| `client.webhooks` | `adminList()`, `adminGet()`, `adminCreate()`, `adminUpdate()`, `adminDelete()`, `adminTest()` |

## Type Exports

모든 타입을 re-export합니다.

```typescript
import type {
  Product, Variant, Category, Collection,
  Cart, CartItem, Order, OrderLine,
  Customer, CustomerAddress,
  ShippingMethod, Discount,
  PaginatedResponse, Money,
} from '@headless-commerce/sdk';
```

## Requirements

- Node.js 18+ (native `fetch` 필요)
- TypeScript 5.0+ (권장)

## License

MIT