> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 认证

> 使用 API Key 认证保护您的 API 请求

## 认证概述

Waffo Pancake 支持两种 API 访问认证方式：

| 方式             | 使用场景      | 说明                                             |
| -------------- | --------- | ---------------------------------------------- |
| **API Key**    | 服务端到服务端调用 | 使用 RSA-SHA256 签名的永久认证方式                        |
| **Store Slug** | 公开收银台流程   | 使用 `X-Store-Slug` 和 `X-Environment` 请求头的公开访问方式 |

***

## API Key 认证

API Key 提供基于 RSA-SHA256 签名的永久服务端到服务端认证。私钥始终保存在您的服务器上。

### 请求头

```bash theme={"system"}
X-Merchant-Id: MER_2aUyqjCzEIiEcYMKj7TZtw
X-Timestamp: 1705312200
X-Signature: BASE64_ENCODED_SIGNATURE
Content-Type: application/json
```

<Note>
  **API Key 认证不需要 `X-Environment` 请求头。** 每个 API Key 在创建时已绑定到 test 或 prod 环境。系统通过验证签名成功的密钥来确定环境。
</Note>

### 使用 SDK（推荐）

```typescript theme={"system"}
import { WaffoPancake } from "@waffo/pancake-ts";

const client = new WaffoPancake({
  merchantId: process.env.WAFFO_MERCHANT_ID!,
  privateKey: process.env.WAFFO_PRIVATE_KEY!,
});

// All requests are automatically signed
const { store } = await client.stores.create({ name: "My Store" });
```

### 签名算法（手动集成）

如果您不使用 SDK，需要自行实现 RSA-SHA256 请求签名：

```
1. Build the canonical request:
   canonicalRequest = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + SHA256_BASE64(BODY)

2. Sign with RSA-SHA256:
   signature = RSA-SHA256(canonicalRequest, privateKey)

3. Base64 encode:
   X-Signature = Base64(signature)
```

### 手动签名示例

<CodeGroup>
  ```javascript Node.js theme={"system"}
  const crypto = require('crypto');

  const MERCHANT_ID = 'MER_2aUyqjCzEIiEcYMKj7TZtw';
  const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
  ...your private key...
  -----END RSA PRIVATE KEY-----`;

  async function callApiWithSignature(method, path, body) {
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const bodyStr = JSON.stringify(body);
    const bodyHash = crypto.createHash('sha256').update(bodyStr).digest('base64');

    // Build canonical request
    const canonicalRequest = `${method}\n${path}\n${timestamp}\n${bodyHash}`;

    // RSA-SHA256 sign
    const signature = crypto.sign('sha256', Buffer.from(canonicalRequest), PRIVATE_KEY).toString('base64');

    const response = await fetch(`https://api.waffo.ai${path}`, {
      method,
      headers: {
        'Content-Type': 'application/json',
        'X-Merchant-Id': MERCHANT_ID,
        'X-Timestamp': timestamp,
        'X-Signature': signature
      },
      body: bodyStr
    });

    return response.json();
  }

  // Usage
  const result = await callApiWithSignature('POST', '/v1/actions/store/create-store', {
    name: 'My Store'
  });
  ```

  ```bash cURL theme={"system"}
  MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
  TIMESTAMP=$(date +%s)
  BODY='{"name":"My Store"}'
  BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0)
  CANONICAL_REQUEST="POST
  /v1/actions/store/create-store
  $TIMESTAMP
  $BODY_HASH"

  # Sign with openssl (requires private_key.pem file)
  SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0)

  curl -X POST "https://api.waffo.ai/v1/actions/store/create-store" \
    -H "Content-Type: application/json" \
    -H "X-Merchant-Id: $MERCHANT_ID" \
    -H "X-Timestamp: $TIMESTAMP" \
    -H "X-Signature: $SIGNATURE" \
    -d "$BODY"
  ```

  ```python Python theme={"system"}
  import hashlib
  import time
  import base64
  import json
  import requests
  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import padding

  MERCHANT_ID = 'MER_2aUyqjCzEIiEcYMKj7TZtw'
  PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY-----
  ...your private key...
  -----END RSA PRIVATE KEY-----'''

  def call_api_with_signature(method, path, body):
      timestamp = str(int(time.time()))
      body_str = json.dumps(body, separators=(',', ':'))
      body_hash = base64.b64encode(hashlib.sha256(body_str.encode()).digest()).decode()

      # Build canonical request
      canonical_request = f"{method}\n{path}\n{timestamp}\n{body_hash}"

      # RSA-SHA256 sign
      private_key = serialization.load_pem_private_key(PRIVATE_KEY.encode(), password=None)
      signature = private_key.sign(canonical_request.encode(), padding.PKCS1v15(), hashes.SHA256())
      signature_b64 = base64.b64encode(signature).decode()

      response = requests.post(
          f'https://api.waffo.ai{path}',
          headers={
              'Content-Type': 'application/json',
              'X-Merchant-Id': MERCHANT_ID,
              'X-Timestamp': timestamp,
              'X-Signature': signature_b64
          },
          data=body_str
      )

      return response.json()

  # Usage
  result = call_api_with_signature('POST', '/v1/actions/store/create-store', {
      'name': 'My Store'
  })
  ```
</CodeGroup>

<Warning>
  **私钥安全**

  * 切勿在客户端代码中暴露私钥
  * 不要将私钥提交到版本控制系统
  * 将私钥存储在环境变量中
  * 定期轮换密钥，尤其是团队人员变动后
  * 时间戳必须在服务器时间的 **5 分钟**以内
</Warning>

***

## Store Slug 认证

对于面向公众的收银台流程，使用 Store Slug 认证。这允许访客在无需 API Key 凭证的情况下创建收银台会话和查询公开的门店数据。

### 请求头

```bash theme={"system"}
X-Store-Slug: my-awesome-store-k8x2m9ab
X-Environment: test | prod
Content-Type: application/json
```

### 示例

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \
    -H "X-Store-Slug: my-awesome-store-k8x2m9ab" \
    -H "X-Environment: test" \
    -H "Content-Type: application/json" \
    -d '{"productId": "PROD_4cWAslE1GKkGeaOMl9Vbmy", "productType": "onetime", "currency": "USD"}'
  ```

  ```javascript JavaScript theme={"system"}
  const response = await fetch('https://api.waffo.ai/v1/graphql', {
    method: 'POST',
    headers: {
      'X-Store-Slug': 'my-awesome-store-k8x2m9ab',
      'Content-Type': 'application/json',
      'X-Environment': 'prod'
    },
    body: JSON.stringify({
      query: `query { store { id name onetimeProducts { id name prices } } }`
    })
  });
  ```
</CodeGroup>

***

## 创建 API Key

<Steps>
  <Step title="进入 API 与开发页面">
    导航至 Dashboard → API 与开发 → API Keys
  </Step>

  <Step title="创建 API Key">
    点击 "Create API Key" 生成新的 RSA 密钥对。公钥会自动发送到服务器。
  </Step>

  <Step title="命名和配置">
    为其设置一个描述性名称（例如 "Production Server"），并选择目标环境（Test 或 Production）。
  </Step>

  <Step title="下载私钥">
    **立即下载您的私钥。** 私钥不会再次显示。
  </Step>
</Steps>

<Warning>
  删除 API Key 是即时且不可逆的操作。使用已删除密钥签名的所有请求都将返回 `401 Unauthorized`。
</Warning>

***

## 认证方式对比

| 功能                  |  API Key | Store Slug |
| ------------------- | :------: | :--------: |
| 服务端使用               |     是    |      否     |
| 客户端使用               |     否    |      是     |
| 需要 X-Environment    |     否    |      是     |
| 签发 Session Token    |     是    |      否     |
| 创建 Checkout Session |     是    |      是     |
| 商品管理                |     是    |      否     |
| GraphQL 查询          |     是    |  是（仅公开数据）  |
| 有效期                 | 永久（密钥控制） |      -     |

***

## 认证错误

| 状态码 | 错误                       | 解决方案                     |
| --- | ------------------------ | ------------------------ |
| 401 | Invalid signature        | 检查签名算法、私钥和时间戳新鲜度（5 分钟窗口） |
| 401 | Invalid or expired token | 重新认证或使用有效的 API Key       |
| 403 | Insufficient permissions | 检查角色是否有权访问该端点            |
| 400 | Missing authentication   | 确保所需的请求头已提供              |

```json theme={"system"}
{
  "data": null,
  "errors": [
    {
      "message": "Invalid signature",
      "layer": "gateway"
    }
  ]
}
```

***

## 安全最佳实践

1. **切勿暴露私钥** -- 不要放在客户端代码、版本控制或公开仓库中
2. **使用 HTTPS** 进行所有 API 请求
3. **验证 Webhook 签名** 以防止伪造请求
4. **分离测试和生产密钥** -- 为每个环境创建独立的密钥
5. **定期轮换密钥**，尤其是团队成员变动后
6. **监控 API 使用情况** -- 在 Dashboard 中查看异常活动
