From 08c6d06fb8b81d8412afbd64afdd42baec91a6e4 Mon Sep 17 00:00:00 2001 From: leecoder Date: Tue, 7 Jul 2026 16:23:52 +0900 Subject: [PATCH 1/5] feat: add multi-user mode with server-side token DB - Add token-store.js (SQLite via better-sqlite3) for caching/refreshing tokens - Add getAccessTokenFromRequest() to token-reader.js for header-based auth - Support X-Kiro-Access-Token / X-Kiro-Refresh-Token headers - Enable via --multi-user flag or MULTI_USER=true env - Multi-user client cache with LRU eviction (max 50) - Databricks Apps compatible (DATABRICKS_APP_PORT, 0.0.0.0 binding) - Add scripts/extract-token.sh helper - Add README_KR.md, update README.md (zh) and README_EN.md --- .gitignore | 1 + README.md | 156 ++++++++++++++++++++++-------------- README_EN.md | 152 ++++++++++++++++++++++-------------- README_KR.md | 165 +++++++++++++++++++++++++++++++++++++++ package.json | 10 ++- scripts/extract-token.sh | 47 +++++++++++ server.js | 118 ++++++++++++++++++++++------ token-reader.js | 61 +++++++++++++++ token-store.js | 107 +++++++++++++++++++++++++ 9 files changed, 670 insertions(+), 147 deletions(-) create mode 100644 README_KR.md create mode 100755 scripts/extract-token.sh create mode 100644 token-store.js diff --git a/.gitignore b/.gitignore index c2658d7..8d2f53b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +app.yaml diff --git a/README.md b/README.md index 74afd12..fa7d094 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,121 @@ -[English](README_EN.md) | 中文 +[English](README_EN.md) | [한국어](README_KR.md) | 中文 # kiro-proxy -让 [Kiro](https://kiro.dev) 订阅内含的 Claude 模型可以在 Claude Code 中使用。 +将 Kiro 订阅中的 Claude 模型通过 OpenAI/Anthropic 兼容 API 暴露出来。 -通过读取 Kiro 的认证 token,代理请求到 Amazon Q Developer,暴露 OpenAI 和 Anthropic 兼容的 API 接口。 +读取 Kiro 认证 token,代理请求到 Amazon Q Developer,提供 OpenAI 和 Anthropic 兼容端点。 + +## 模式 + +### 本地模式(默认) + +从本机 `~/.aws/sso/cache/kiro-auth-token.json` 读取 token。 + +```bash +node server.js +``` + +### 多用户模式 + +客户端通过请求头传递 token,服务器缓存到 SQLite DB 并在过期时自动刷新。多个用户可同时使用各自 token。 + +```bash +# CLI 参数 +node server.js --multi-user + +# 或环境变量 +MULTI_USER=true node server.js +``` + +多用户模式下,客户端需传递以下请求头: + +| 请求头 | 必需 | 说明 | +|--------|------|------| +| `X-Kiro-Access-Token` | 是* | 当前 access token | +| `X-Kiro-Refresh-Token` | 是* | refresh token(服务器用来自动续期) | +| `X-Kiro-Auth-Method` | 否 | `social` 或 `IdC` | +| `X-Kiro-Profile-Arn` | 否 | profile ARN | +| `X-Kiro-Region` | 否 | AWS region | +| `X-Kiro-Provider` | 否 | provider 类型 | + +\* 至少提供其中一个。建议同时传两个以启用自动续期。 + +提取 token 示例: + +```bash +# 使用内置脚本 +./scripts/extract-token.sh headers # 输出 -H 参数 +./scripts/extract-token.sh env # 输出 export 语句 +./scripts/extract-token.sh curl # 输出单行 curl headers + +# 配合 curl 使用 +eval curl http://localhost:3456/v1/messages \ + -H "Content-Type: application/json" \ + $(./scripts/extract-token.sh curl) \ + -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' +``` + +Token 流程: +1. 首次请求:验证 token 后存入 DB +2. 后续请求:使用 DB 中的缓存 token +3. 过期时:服务器自动刷新并更新 DB +4. refresh token 本身过期:返回 401,客户端需提交新 token ## 前提 -需要先安装并登录 Kiro,确保 `~/.aws/sso/cache/kiro-auth-token.json` 存在且未过期。 +安装并登录 Kiro,确保 `~/.aws/sso/cache/kiro-auth-token.json` 存在。 ## 快速开始 ```bash -npx kiro-proxy +npx @leecoder/kiro-proxy ``` -服务默认监听 `http://localhost:3456`。 +默认端口:`http://localhost:3456` ## 配置 | 环境变量 | 默认值 | 说明 | |----------|--------|------| -| `PORT` | `3456` | 监听端口 | -| `PROXY_API_KEY` | 无 | 设置后所有请求需携带此 key 进行鉴权,未设置则不校验 | -| `HTTPS_PROXY` | 无 | HTTP/HTTPS 代理地址,如 `http://127.0.0.1:7890` | +| `PORT` | `3456` | 监听端口 | +| `DATABRICKS_APP_PORT` | - | 设置后优先于 PORT | +| `PROXY_API_KEY` | - | 设置后所有请求需 Bearer 认证 | +| `MULTI_USER` | `false` | `true` 启用多用户模式(等同 `--multi-user` 参数) | +| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | Token 数据库路径 | +| `HTTPS_PROXY` | - | 出站代理地址 | ## API -### GET /v1/models — 查询可用模型 - -```bash -curl http://localhost:3456/v1/models -``` - ### POST /v1/messages — Anthropic 兼容 ```bash -# 非流式 curl http://localhost:3456/v1/messages \ -H "Content-Type: application/json" \ - -H "x-api-key: any" \ -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' - -# 流式 -curl http://localhost:3456/v1/messages \ - -H "Content-Type: application/json" \ - -H "x-api-key: any" \ - -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}], "stream": true}' ``` ### POST /v1/chat/completions — OpenAI 兼容 ```bash -# 非流式 curl http://localhost:3456/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}' - -# 流式 -curl http://localhost:3456/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}], "stream": true}' ``` -### GET /health - -检查 token 状态及过期时间。 - -### GET /credits - -查询积分消耗统计,支持 `period` 参数: +### GET /v1/models -```bash -# 今日消耗(默认) -curl http://localhost:3456/credits +查询可用模型列表。 -# 最近 7 天 -curl http://localhost:3456/credits?period=7d +### GET /health -# 最近 30 天 -curl http://localhost:3456/credits?period=30d +检查 token 状态及过期时间。 -# 全部 -curl http://localhost:3456/credits?period=all -``` +### GET /credits?period=today|7d|30d|all -## 与 Claude Code 集成 +积分使用统计。 -Claude Code 默认使用 Anthropic 官方 model ID,需要通过环境变量映射到 Q Developer 的 model ID。 +## Claude Code 集成 在 `~/.claude/settings.json` 中添加: @@ -105,25 +132,34 @@ Claude Code 默认使用 Anthropic 官方 model ID,需要通过环境变量映 } ``` -`model` 可选值:`sonnet`、`opus`、`haiku`,添加 `[1m]` 后缀可启用 1M 上下文窗口(如 `"opus[1m]"`)。 +## Databricks Apps 部署 -> 注意:不要设置 `ANTHROPIC_MODEL` 环境变量,它会覆盖 `model` 字段,导致上下文窗口等配置失效。 +`app.yaml` 示例(已加入 `.gitignore`,仅本地维护): -## 代理设置 +```yaml +command: + - node + - server.js + - --multi-user +env: + - name: PROXY_API_KEY + valueFrom: secret + - name: TOKEN_DB_PATH + value: "/tmp/kiro-proxy/tokens.db" +``` -自 2026 年 5 月 1 日起,Kiro 上的 Claude 模型无法在中国大陆及港澳台地区使用。如果遇到 `Invalid model` 错误,请配置代理。 +```bash +databricks apps deploy kiro-proxy --source-code-path "/Workspace/Users/$USER/kiro-proxy" +``` -> 注意:代理节点需选择其他地区(如新加坡、泰国、韩国等)。 +## 代理设置 -通过环境变量设置 HTTP 代理: +遇到 `Invalid model` 错误时: ```bash -# 设置代理后启动 -HTTPS_PROXY=http://127.0.0.1:7890 npx kiro-proxy +HTTPS_PROXY=http://127.0.0.1:7890 node server.js ``` -支持的环境变量:`HTTPS_PROXY`、`https_proxy`、`HTTP_PROXY`、`http_proxy`,优先级从左到右。 - -## 相关项目 +## 原始项目 -- [kiro-web-search](https://github.com/Colin3191/kiro-web-search) — 将 Kiro 内置的联网搜索封装为 MCP server,可在 Claude Code 等客户端中使用 +Fork from [Colin3191/kiro-proxy](https://github.com/Colin3191/kiro-proxy) diff --git a/README_EN.md b/README_EN.md index 52acc9e..506b253 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,95 +1,122 @@ -English | [中文](README.md) +[한국어](README_KR.md) | [中文](README.md) | English # kiro-proxy -Use the Claude models bundled with your [Kiro](https://kiro.dev) subscription in Claude Code. +Proxy that exposes Claude models from your Kiro subscription as OpenAI/Anthropic-compatible API endpoints. -Reads Kiro's auth token, proxies requests to Amazon Q Developer, and exposes OpenAI and Anthropic-compatible API endpoints. +Reads Kiro auth tokens, proxies requests to Amazon Q Developer, and serves OpenAI and Anthropic-compatible APIs. + +## Modes + +### Local mode (default) + +Reads token from `~/.aws/sso/cache/kiro-auth-token.json` on the local machine. + +```bash +node server.js +``` + +### Multi-user mode + +Clients pass their Kiro token via request headers. The server caches tokens in a SQLite DB and auto-refreshes them on expiry. Multiple users can use the proxy simultaneously with their own tokens. + +```bash +# CLI flag +node server.js --multi-user + +# Or environment variable +MULTI_USER=true node server.js +``` + +In multi-user mode, clients must include the following headers: + +| Header | Required | Description | +|--------|----------|-------------| +| `X-Kiro-Access-Token` | Yes* | Current access token | +| `X-Kiro-Refresh-Token` | Yes* | Refresh token (used by server to auto-renew) | +| `X-Kiro-Auth-Method` | No | `social` or `IdC` | +| `X-Kiro-Profile-Arn` | No | Profile ARN | +| `X-Kiro-Region` | No | AWS region | +| `X-Kiro-Provider` | No | Provider type | + +\* At least one is required. Provide both to enable auto-renewal. + +Extracting tokens: + +```bash +# Using the built-in script +./scripts/extract-token.sh headers # Output -H flags +./scripts/extract-token.sh env # Output export statements +./scripts/extract-token.sh curl # Output single-line curl headers + +# Use with curl +eval curl http://localhost:3456/v1/messages \ + -H "Content-Type: application/json" \ + $(./scripts/extract-token.sh curl) \ + -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' +``` + +Token flow: +1. First request: validate token and store in DB +2. Subsequent requests: use cached token from DB +3. On expiry: server auto-refreshes and updates DB +4. If refresh token itself expires: returns 401, client must submit a fresh token ## Prerequisites -Install and log in to Kiro so that `~/.aws/sso/cache/kiro-auth-token.json` exists and is valid. +Install and log in to Kiro so that `~/.aws/sso/cache/kiro-auth-token.json` exists. ## Quick Start ```bash -npx kiro-proxy +npx @leecoder/kiro-proxy ``` -Server listens on `http://localhost:3456` by default. +Default port: `http://localhost:3456` ## Configuration | Environment Variable | Default | Description | |---------------------|---------|-------------| | `PORT` | `3456` | Listen port | -| `PROXY_API_KEY` | None | When set, all requests must include this key for authentication. No validation when unset | -| `HTTPS_PROXY` | None | HTTP/HTTPS proxy URL, e.g. `http://127.0.0.1:7890` | +| `DATABRICKS_APP_PORT` | - | Overrides PORT when set | +| `PROXY_API_KEY` | - | When set, all requests require Bearer auth | +| `MULTI_USER` | `false` | `true` enables multi-user mode (same as `--multi-user` flag) | +| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | Token database path | +| `HTTPS_PROXY` | - | Outbound proxy URL | ## API -### GET /v1/models — List available models - -```bash -curl http://localhost:3456/v1/models -``` - ### POST /v1/messages — Anthropic-compatible ```bash -# Non-streaming curl http://localhost:3456/v1/messages \ -H "Content-Type: application/json" \ - -H "x-api-key: any" \ -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' - -# Streaming -curl http://localhost:3456/v1/messages \ - -H "Content-Type: application/json" \ - -H "x-api-key: any" \ - -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}], "stream": true}' ``` ### POST /v1/chat/completions — OpenAI-compatible ```bash -# Non-streaming curl http://localhost:3456/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}' - -# Streaming -curl http://localhost:3456/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}], "stream": true}' ``` -### GET /health - -Check token status and expiration. - -### GET /credits +### GET /v1/models -Query credit usage statistics. Supports `period` parameter: +List available models. -```bash -# Today's usage (default) -curl http://localhost:3456/credits +### GET /health -# Last 7 days -curl http://localhost:3456/credits?period=7d +Check token status and expiration. -# Last 30 days -curl http://localhost:3456/credits?period=30d +### GET /credits?period=today|7d|30d|all -# All time -curl http://localhost:3456/credits?period=all -``` +Credit usage statistics. ## Claude Code Integration -Claude Code uses Anthropic's official model IDs by default. Map them to Q Developer model IDs via environment variables. - Add to `~/.claude/settings.json`: ```json @@ -105,25 +132,34 @@ Add to `~/.claude/settings.json`: } ``` -`model` accepts `sonnet`, `opus`, or `haiku`. Append `[1m]` to enable the 1M context window (e.g. `"opus[1m]"`). +## Databricks Apps Deployment -> Note: Do not set the `ANTHROPIC_MODEL` environment variable — it overrides the `model` field and disables context window configuration. +Example `app.yaml` (in `.gitignore`, managed locally): -## Proxy Setup +```yaml +command: + - node + - server.js + - --multi-user +env: + - name: PROXY_API_KEY + valueFrom: secret + - name: TOKEN_DB_PATH + value: "/tmp/kiro-proxy/tokens.db" +``` -Since May 1, 2026, Claude models on Kiro are unavailable in mainland China, Hong Kong, Macau, and Taiwan. If you encounter an `Invalid model` error, configure a proxy. +```bash +databricks apps deploy kiro-proxy --source-code-path "/Workspace/Users/$USER/kiro-proxy" +``` -> Note: Proxy nodes must be in other regions (e.g. Singapore, Thailand, South Korea). +## Proxy Setup -Set the proxy via environment variable: +If you encounter `Invalid model` errors: ```bash -# Start with proxy -HTTPS_PROXY=http://127.0.0.1:7890 npx kiro-proxy +HTTPS_PROXY=http://127.0.0.1:7890 node server.js ``` -Supported environment variables: `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`, `http_proxy` (priority from left to right). - -## Related Projects +## Origin -- [kiro-web-search](https://github.com/Colin3191/kiro-web-search) — MCP server exposing Kiro's web search for use in Claude Code and other clients +Forked from [Colin3191/kiro-proxy](https://github.com/Colin3191/kiro-proxy) diff --git a/README_KR.md b/README_KR.md new file mode 100644 index 0000000..50a7f3c --- /dev/null +++ b/README_KR.md @@ -0,0 +1,165 @@ +[English](README_EN.md) | [中文](README.md) | 한국어 + +# kiro-proxy + +Kiro 구독에 포함된 Claude 모델을 OpenAI/Anthropic 호환 API로 노출하는 프록시. + +Kiro 인증 토큰을 읽어서 Amazon Q Developer로 요청을 프록시하고, OpenAI 및 Anthropic 호환 엔드포인트를 제공합니다. + +## 모드 + +### 로컬 모드 (기본) + +로컬 머신의 `~/.aws/sso/cache/kiro-auth-token.json`에서 토큰을 읽습니다. + +```bash +node server.js +``` + +### 멀티유저 모드 + +클라이언트가 요청 헤더로 토큰을 전달하면 서버가 SQLite DB에 캐시하고, 만료 시 자동으로 refresh합니다. 여러 유저가 각자 토큰으로 동시에 사용 가능. + +```bash +# CLI 플래그 +node server.js --multi-user + +# 또는 환경변수 +MULTI_USER=true node server.js +``` + +멀티유저 모드에서 클라이언트는 다음 헤더를 포함해야 합니다: + +| 헤더 | 필수 | 설명 | +|------|------|------| +| `X-Kiro-Access-Token` | 예* | 현재 access token | +| `X-Kiro-Refresh-Token` | 예* | refresh token (서버가 자동 갱신에 사용) | +| `X-Kiro-Auth-Method` | 아니오 | `social` 또는 `IdC` | +| `X-Kiro-Profile-Arn` | 아니오 | profile ARN | +| `X-Kiro-Region` | 아니오 | AWS region | +| `X-Kiro-Provider` | 아니오 | provider 타입 | + +\* 둘 중 하나는 필수. 자동 갱신을 위해 둘 다 보내는 것을 권장. + +토큰 추출: + +```bash +# 내장 스크립트 사용 +./scripts/extract-token.sh headers # -H 플래그 출력 +./scripts/extract-token.sh env # export 구문 출력 +./scripts/extract-token.sh curl # 한 줄 curl headers 출력 + +# curl과 함께 사용 +eval curl http://localhost:3456/v1/messages \ + -H "Content-Type: application/json" \ + $(./scripts/extract-token.sh curl) \ + -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' +``` + +토큰 흐름: +1. 첫 요청 시 토큰 유효성 검증 후 DB에 저장 +2. 이후 요청에서 DB 캐시 사용 +3. 만료 시 서버가 자동 refresh → DB 갱신 +4. refresh token 자체 만료 시 401 반환 → 클라이언트가 새 토큰 제출 + +## 전제조건 + +Kiro를 설치하고 로그인해서 `~/.aws/sso/cache/kiro-auth-token.json`이 존재해야 합니다. + +## 빠른 시작 + +```bash +npx @leecoder/kiro-proxy +``` + +서버 기본 포트: `http://localhost:3456` + +## 설정 + +| 환경변수 | 기본값 | 설명 | +|----------|--------|------| +| `PORT` | `3456` | 수신 포트 | +| `DATABRICKS_APP_PORT` | - | 설정 시 PORT보다 우선 | +| `PROXY_API_KEY` | - | 설정 시 모든 요청에 Bearer 인증 필요 | +| `MULTI_USER` | `false` | `true`이면 멀티유저 모드 (`--multi-user` 플래그와 동일) | +| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | 토큰 DB 경로 | +| `HTTPS_PROXY` | - | 아웃바운드 프록시 주소 | + +## API + +### POST /v1/messages — Anthropic 호환 + +```bash +curl http://localhost:3456/v1/messages \ + -H "Content-Type: application/json" \ + -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' +``` + +### POST /v1/chat/completions — OpenAI 호환 + +```bash +curl http://localhost:3456/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}' +``` + +### GET /v1/models + +사용 가능한 모델 목록 조회. + +### GET /health + +토큰 상태 및 만료 시간 확인. + +### GET /credits?period=today|7d|30d|all + +크레딧 사용량 통계. + +## Claude Code 연동 + +`~/.claude/settings.json`: + +```json +{ + "env": { + "ANTHROPIC_AUTH_TOKEN": "any", + "ANTHROPIC_BASE_URL": "http://localhost:3456", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4.6", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4.5" + }, + "model": "sonnet" +} +``` + +## Databricks Apps 배포 + +`app.yaml` 예시 (`.gitignore`에 포함, 로컬에서만 관리): + +```yaml +command: + - node + - server.js + - --multi-user +env: + - name: PROXY_API_KEY + valueFrom: secret + - name: TOKEN_DB_PATH + value: "/tmp/kiro-proxy/tokens.db" +``` + +```bash +databricks apps deploy kiro-proxy --source-code-path "/Workspace/Users/$USER/kiro-proxy" +``` + +## 프록시 설정 + +`Invalid model` 에러 발생 시: + +```bash +HTTPS_PROXY=http://127.0.0.1:7890 node server.js +``` + +## 원본 프로젝트 + +Fork from [Colin3191/kiro-proxy](https://github.com/Colin3191/kiro-proxy) diff --git a/package.json b/package.json index 52d6fea..93f2542 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@colin3191/kiro-proxy", - "version": "0.2.4", - "description": "Kiro API proxy with OpenAI and Anthropic compatible endpoints", + "name": "@leecoder/kiro-proxy", + "version": "0.3.0", + "description": "Kiro API proxy with OpenAI and Anthropic compatible endpoints — Databricks Apps edition", "type": "module", "bin": { "kiro-proxy": "./server.js" @@ -14,16 +14,18 @@ "q-client.js", "token-reader.js", "token-counter.js", + "token-store.js", "usage-tracker.js", "logger.js", "proxy-config.js" ], "repository": { "type": "git", - "url": "https://github.com/Colin3191/kiro-proxy.git" + "url": "https://github.com/leecoder/kiro-proxy.git" }, "dependencies": { "@aws/codewhisperer-streaming-client": "^1.0.34", + "better-sqlite3": "^11.0.0", "express": "^4.21.0", "https-proxy-agent": "^7.0.0", "undici": "^6.19.0" diff --git a/scripts/extract-token.sh b/scripts/extract-token.sh new file mode 100755 index 0000000..c517181 --- /dev/null +++ b/scripts/extract-token.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +TOKEN_FILE="${KIRO_TOKEN_FILE:-$HOME/.aws/sso/cache/kiro-auth-token.json}" + +if [ ! -f "$TOKEN_FILE" ]; then + echo "Error: $TOKEN_FILE not found. Login to Kiro first." >&2 + exit 1 +fi + +ACCESS_TOKEN=$(python3 -c "import sys,json;print(json.load(sys.stdin)['accessToken'])" < "$TOKEN_FILE") +REFRESH_TOKEN=$(python3 -c "import sys,json;print(json.load(sys.stdin).get('refreshToken',''))" < "$TOKEN_FILE") +AUTH_METHOD=$(python3 -c "import sys,json;print(json.load(sys.stdin).get('authMethod',''))" < "$TOKEN_FILE") +PROFILE_ARN=$(python3 -c "import sys,json;print(json.load(sys.stdin).get('profileArn',''))" < "$TOKEN_FILE") +REGION=$(python3 -c "import sys,json;print(json.load(sys.stdin).get('region',''))" < "$TOKEN_FILE") + +case "${1:-headers}" in + headers) + echo "-H \"X-Kiro-Access-Token: $ACCESS_TOKEN\"" + [ -n "$REFRESH_TOKEN" ] && echo "-H \"X-Kiro-Refresh-Token: $REFRESH_TOKEN\"" + [ -n "$AUTH_METHOD" ] && echo "-H \"X-Kiro-Auth-Method: $AUTH_METHOD\"" + [ -n "$PROFILE_ARN" ] && echo "-H \"X-Kiro-Profile-Arn: $PROFILE_ARN\"" + [ -n "$REGION" ] && echo "-H \"X-Kiro-Region: $REGION\"" + ;; + env) + echo "export X_KIRO_ACCESS_TOKEN=\"$ACCESS_TOKEN\"" + [ -n "$REFRESH_TOKEN" ] && echo "export X_KIRO_REFRESH_TOKEN=\"$REFRESH_TOKEN\"" + [ -n "$AUTH_METHOD" ] && echo "export X_KIRO_AUTH_METHOD=\"$AUTH_METHOD\"" + [ -n "$PROFILE_ARN" ] && echo "export X_KIRO_PROFILE_ARN=\"$PROFILE_ARN\"" + [ -n "$REGION" ] && echo "export X_KIRO_REGION=\"$REGION\"" + ;; + curl) + HEADERS="-H \"X-Kiro-Access-Token: $ACCESS_TOKEN\"" + [ -n "$REFRESH_TOKEN" ] && HEADERS="$HEADERS -H \"X-Kiro-Refresh-Token: $REFRESH_TOKEN\"" + [ -n "$AUTH_METHOD" ] && HEADERS="$HEADERS -H \"X-Kiro-Auth-Method: $AUTH_METHOD\"" + [ -n "$PROFILE_ARN" ] && HEADERS="$HEADERS -H \"X-Kiro-Profile-Arn: $PROFILE_ARN\"" + [ -n "$REGION" ] && HEADERS="$HEADERS -H \"X-Kiro-Region: $REGION\"" + echo "$HEADERS" + ;; + *) + echo "Usage: $(basename "$0") [headers|env|curl]" >&2 + echo " headers — print -H flags (default)" >&2 + echo " env — print export statements (eval-able)" >&2 + echo " curl — print single-line curl headers" >&2 + exit 1 + ;; +esac diff --git a/server.js b/server.js index 5e4a315..3fed341 100755 --- a/server.js +++ b/server.js @@ -1,20 +1,26 @@ #!/usr/bin/env node import express from 'express'; import crypto from 'crypto'; -import { getAccessToken } from './token-reader.js'; +import { getAccessToken, getAccessTokenFromRequest } from './token-reader.js'; import { createClient, chat, chatStream, listAvailableModels } from './q-client.js'; import { c, log, tagLog, logSummary, reqId, tagError } from './logger.js'; import { countMessages, countContent } from './token-counter.js'; import { recordUsage, queryUsage, todaySummary } from './usage-tracker.js'; import { initGlobalProxy } from './proxy-config.js'; +import { initTokenStore } from './token-store.js'; const proxyUrl = initGlobalProxy(); if (proxyUrl) tagLog('proxy', `Using proxy: ${proxyUrl}`); +const args = process.argv.slice(2); +const MULTI_USER = args.includes('--multi-user') || process.env.MULTI_USER === '1' || process.env.MULTI_USER === 'true'; +if (MULTI_USER) initTokenStore(); + const app = express(); app.use(express.json({ limit: '10mb' })); -const PORT = process.env.PORT || 3456; +const PORT = process.env.DATABRICKS_APP_PORT || process.env.PORT || 3456; +const HOST = '0.0.0.0'; const PROXY_API_KEY = process.env.PROXY_API_KEY; function authMiddleware(req, res, next) { @@ -29,9 +35,47 @@ app.use(authMiddleware); let cachedClient = null; let cachedToken = null; +const clientCache = new Map(); + +function extractKiroHeaders(req) { + return { + accessToken: req.headers['x-kiro-access-token'], + refreshToken: req.headers['x-kiro-refresh-token'], + authMethod: req.headers['x-kiro-auth-method'], + profileArn: req.headers['x-kiro-profile-arn'], + region: req.headers['x-kiro-region'], + provider: req.headers['x-kiro-provider'], + clientIdHash: req.headers['x-kiro-client-id-hash'], + }; +} + +async function getClient(req) { + let tokenData; + + if (MULTI_USER) { + const headers = extractKiroHeaders(req); + if (!headers.accessToken && !headers.refreshToken) { + throw new Error('X-Kiro-Access-Token or X-Kiro-Refresh-Token header required'); + } + tokenData = await getAccessTokenFromRequest(headers); + + if (clientCache.has(tokenData.accessToken)) { + return { client: clientCache.get(tokenData.accessToken), tokenData }; + } + const client = createClient(tokenData.accessToken, { + authMethod: tokenData.authMethod, + profileArn: tokenData.profileArn, + provider: tokenData.provider, + }); + clientCache.set(tokenData.accessToken, client); + if (clientCache.size > 50) { + const oldest = clientCache.keys().next().value; + clientCache.delete(oldest); + } + return { client, tokenData }; + } -async function getClient() { - const tokenData = await getAccessToken(); + tokenData = await getAccessToken(); if (!cachedClient || cachedToken !== tokenData.accessToken) { cachedClient = createClient(tokenData.accessToken, { authMethod: tokenData.authMethod, @@ -57,7 +101,7 @@ app.post('/v1/messages', async (req, res) => { return res.status(400).json({ type: 'error', error: { type: 'invalid_request_error', message: 'messages required' } }); } - const { client, tokenData } = await getClient(); + const { client, tokenData } = await getClient(req); const opts = { messages, system, tools, profileArn: tokenData.profileArn, modelId: model }; const rid = reqId(); const start = Date.now(); @@ -226,7 +270,7 @@ app.post('/v1/messages', async (req, res) => { } } catch (err) { tagError('anthropic', err.message || err); - const status = err.message?.includes('expired') ? 401 : 500; + const status = err.message?.includes('expired') ? 401 : err.message?.includes('X-Kiro-') ? 401 : 500; res.status(status).json({ type: 'error', error: { type: status === 401 ? 'authentication_error' : 'api_error', message: err.message } }); } }); @@ -247,7 +291,7 @@ app.post('/v1/chat/completions', async (req, res) => { messages.push({ role: m.role, content: m.content }); } - const { client, tokenData } = await getClient(); + const { client, tokenData } = await getClient(req); const opts = { messages, system, profileArn: tokenData.profileArn, modelId: model }; const rid = reqId(); const start = Date.now(); @@ -317,9 +361,16 @@ app.post('/v1/chat/completions', async (req, res) => { // ============================================================ // GET /v1/models // ============================================================ -app.get('/v1/models', async (_req, res) => { +app.get('/v1/models', async (req, res) => { try { - const tokenData = await getAccessToken(); + let tokenData; + if (MULTI_USER) { + const headers = extractKiroHeaders(req); + if (!headers.accessToken && !headers.refreshToken) return res.status(401).json({ error: { message: 'X-Kiro-Access-Token or X-Kiro-Refresh-Token header required' } }); + tokenData = await getAccessTokenFromRequest(headers); + } else { + tokenData = await getAccessToken(); + } const { models, defaultModel } = await listAvailableModels(tokenData.accessToken, { profileArn: tokenData.profileArn, authMethod: tokenData.authMethod, provider: tokenData.provider, }); @@ -336,9 +387,16 @@ app.get('/v1/models', async (_req, res) => { } }); -app.get('/q/models', async (_req, res) => { +app.get('/q/models', async (req, res) => { try { - const tokenData = await getAccessToken(); + let tokenData; + if (MULTI_USER) { + const headers = extractKiroHeaders(req); + if (!headers.accessToken && !headers.refreshToken) return res.status(401).json({ error: { message: 'X-Kiro-Access-Token or X-Kiro-Refresh-Token header required' } }); + tokenData = await getAccessTokenFromRequest(headers); + } else { + tokenData = await getAccessToken(); + } const result = await listAvailableModels(tokenData.accessToken, { profileArn: tokenData.profileArn, authMethod: tokenData.authMethod, provider: tokenData.provider, }); @@ -348,11 +406,18 @@ app.get('/q/models', async (_req, res) => { } }); -app.get('/health', async (_req, res) => { +app.get('/health', async (req, res) => { try { - const tokenData = await getAccessToken(); + let tokenData; + if (MULTI_USER) { + const headers = extractKiroHeaders(req); + if (!headers.accessToken && !headers.refreshToken) return res.json({ status: 'ok', mode: 'multi-user', message: 'Send X-Kiro-Access-Token header for token health' }); + tokenData = await getAccessTokenFromRequest(headers); + } else { + tokenData = await getAccessToken(); + } const expired = tokenData.expiresAt && new Date(tokenData.expiresAt) < new Date(); - res.json({ status: expired ? 'token_expired' : 'ok', provider: tokenData.provider || 'unknown', expiresAt: tokenData.expiresAt }); + res.json({ status: expired ? 'token_expired' : 'ok', mode: MULTI_USER ? 'multi-user' : 'local', provider: tokenData.provider || 'unknown', expiresAt: tokenData.expiresAt }); } catch (err) { res.status(503).json({ status: 'error', message: err.message }); } @@ -366,18 +431,21 @@ app.get('/credits', (_req, res) => { res.json(queryUsage(period)); }); -app.listen(PORT, async () => { - console.log(`${c.cyan}Kiro Proxy${c.reset} running on ${c.green}http://localhost:${PORT}${c.reset}`); - console.log(` ${c.gray}Anthropic:${c.reset} http://localhost:${PORT}/v1/messages`); - console.log(` ${c.gray}OpenAI: ${c.reset} http://localhost:${PORT}/v1/chat/completions`); - console.log(` ${c.gray}Models: ${c.reset} http://localhost:${PORT}/v1/models`); - console.log(` ${c.gray}Credits: ${c.reset} http://localhost:${PORT}/credits`); +app.listen(PORT, HOST, async () => { + const modeLabel = MULTI_USER ? `${c.magenta}multi-user${c.reset} (token DB)` : `${c.green}local${c.reset}`; + console.log(`${c.cyan}Kiro Proxy${c.reset} running on ${c.green}http://${HOST}:${PORT}${c.reset} [${modeLabel}]`); + console.log(` ${c.gray}Anthropic:${c.reset} http://${HOST}:${PORT}/v1/messages`); + console.log(` ${c.gray}OpenAI: ${c.reset} http://${HOST}:${PORT}/v1/chat/completions`); + console.log(` ${c.gray}Models: ${c.reset} http://${HOST}:${PORT}/v1/models`); + console.log(` ${c.gray}Credits: ${c.reset} http://${HOST}:${PORT}/credits`); console.log(` ${c.gray}Auth: ${c.reset} ${PROXY_API_KEY ? `${c.green}enabled${c.reset} (PROXY_API_KEY)` : `${c.yellow}disabled${c.reset} (no PROXY_API_KEY set)`}`); - try { - const t = await getAccessToken(); - console.log(` ${c.gray}Provider: ${c.yellow}${t.provider || 'unknown'}${c.reset}, Expires: ${c.dim}${t.expiresAt || 'unknown'}${c.reset}`); - } catch (err) { - console.warn(` ${c.yellow}Warning:${c.reset} ${err.message}`); + if (!MULTI_USER) { + try { + const t = await getAccessToken(); + console.log(` ${c.gray}Provider: ${c.yellow}${t.provider || 'unknown'}${c.reset}, Expires: ${c.dim}${t.expiresAt || 'unknown'}${c.reset}`); + } catch (err) { + console.warn(` ${c.yellow}Warning:${c.reset} ${err.message}`); + } } }); diff --git a/token-reader.js b/token-reader.js index 0b86b44..319e08e 100644 --- a/token-reader.js +++ b/token-reader.js @@ -2,6 +2,7 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; import { tagLog, tagWarn, tagError } from './logger.js'; +import { hashToken, getStoredToken, upsertToken, deleteToken } from './token-store.js'; const SSO_CACHE_DIR = path.join(os.homedir(), '.aws', 'sso', 'cache'); const KIRO_TOKEN_FILE = 'kiro-auth-token.json'; @@ -244,3 +245,63 @@ function enrichWithProfile(tokenData) { } return tokenData; } + +// ============================================================ +// Remote mode: 클라이언트가 보낸 kiro token으로 동작 +// ============================================================ + +const refreshLocks = new Map(); + +/** + * @param {object} headers — { accessToken, refreshToken, ?authMethod, ?profileArn, ?region, ?provider } + */ +export async function getAccessTokenFromRequest(headers) { + const { accessToken, refreshToken, authMethod, profileArn, region, provider, clientIdHash } = headers; + + if (!accessToken && !refreshToken) { + throw new Error('X-Kiro-Access-Token or X-Kiro-Refresh-Token required'); + } + + const keySource = refreshToken || accessToken; + const keyHash = hashToken(keySource); + + const stored = getStoredToken(keyHash); + + if (stored && !isTokenExpired(stored)) { + return stored; + } + + if (refreshLocks.has(keyHash)) { + tagLog('token', `[multi] Waiting for ongoing refresh (${keyHash.slice(0, 8)}...)`); + return refreshLocks.get(keyHash); + } + + const promise = (async () => { + try { + const tokenToRefresh = stored || { accessToken, refreshToken, authMethod, profileArn, region, provider, clientIdHash }; + + if (!tokenToRefresh.refreshToken) { + if (tokenToRefresh.accessToken && tokenToRefresh.expiresAt && new Date(tokenToRefresh.expiresAt) > new Date()) { + upsertToken(keyHash, tokenToRefresh); + return tokenToRefresh; + } + throw new Error('Token expired and no refreshToken available. Client must re-login in Kiro.'); + } + + tagLog('token', `[multi] Refreshing token (${keyHash.slice(0, 8)}...)`); + const refreshed = await refreshToken(tokenToRefresh); + upsertToken(keyHash, refreshed); + tagLog('token', `[multi] Token refreshed, new expiry: ${refreshed.expiresAt}`); + return refreshed; + } catch (err) { + tagError('token', `[multi] Refresh failed (${keyHash.slice(0, 8)}...):`, err.message); + deleteToken(keyHash); + throw err; + } finally { + refreshLocks.delete(keyHash); + } + })(); + + refreshLocks.set(keyHash, promise); + return promise; +} diff --git a/token-store.js b/token-store.js new file mode 100644 index 0000000..bd89b9f --- /dev/null +++ b/token-store.js @@ -0,0 +1,107 @@ +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import Database from 'better-sqlite3'; +import { tagLog } from './logger.js'; + +// DB 경로: 환경변수 또는 기본값 +const DB_PATH = process.env.TOKEN_DB_PATH || path.join(os.homedir(), '.kiro-proxy', 'tokens.db'); + +let db; + +function getDb() { + if (db) return db; + const dir = path.dirname(DB_PATH); + fs.mkdirSync(dir, { recursive: true }); + db = new Database(DB_PATH); + db.pragma('journal_mode = WAL'); + db.exec(` + CREATE TABLE IF NOT EXISTS tokens ( + key_hash TEXT PRIMARY KEY, + access_token TEXT NOT NULL, + refresh_token TEXT, + expires_at TEXT NOT NULL, + auth_method TEXT, + profile_arn TEXT, + region TEXT, + provider TEXT, + client_id_hash TEXT, + updated_at INTEGER NOT NULL + ) + `); + return db; +} + +export function hashToken(rawToken) { + return crypto.createHash('sha256').update(rawToken).digest('hex'); +} + +export function getStoredToken(keyHash) { + const row = getDb().prepare('SELECT * FROM tokens WHERE key_hash = ?').get(keyHash); + if (!row) return null; + return { + accessToken: row.access_token, + refreshToken: row.refresh_token, + expiresAt: row.expires_at, + authMethod: row.auth_method, + profileArn: row.profile_arn, + region: row.region, + provider: row.provider, + clientIdHash: row.client_id_hash, + }; +} + +export function upsertToken(keyHash, tokenData) { + const stmt = getDb().prepare(` + INSERT INTO tokens (key_hash, access_token, refresh_token, expires_at, auth_method, profile_arn, region, provider, client_id_hash, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(key_hash) DO UPDATE SET + access_token = excluded.access_token, + refresh_token = excluded.refresh_token, + expires_at = excluded.expires_at, + auth_method = excluded.auth_method, + profile_arn = excluded.profile_arn, + region = excluded.region, + provider = excluded.provider, + client_id_hash = excluded.client_id_hash, + updated_at = excluded.updated_at + `); + stmt.run( + keyHash, + tokenData.accessToken, + tokenData.refreshToken || null, + tokenData.expiresAt, + tokenData.authMethod || null, + tokenData.profileArn || null, + tokenData.region || null, + tokenData.provider || null, + tokenData.clientIdHash || null, + Date.now(), + ); +} + +export function deleteToken(keyHash) { + getDb().prepare('DELETE FROM tokens WHERE key_hash = ?').run(keyHash); +} + +/** + * 오래된 토큰 정리 — 기본 7일 미갱신 엔트리 삭제 + */ +export function purgeExpired(maxAgeMs = 7 * 24 * 60 * 60 * 1000) { + const cutoff = Date.now() - maxAgeMs; + const result = getDb().prepare('DELETE FROM tokens WHERE updated_at < ?').run(cutoff); + if (result.changes > 0) { + tagLog('token-store', `Purged ${result.changes} stale token(s)`); + } + return result.changes; +} + +/** + * 서버 시작 시 1회 purge 실행 + */ +export function initTokenStore() { + getDb(); // ensure table exists + purgeExpired(); + tagLog('token-store', `Initialized at ${DB_PATH}`); +} From ac739a2bd0f7e878919de5bd4a369489dbc8ad40 Mon Sep 17 00:00:00 2001 From: leecoder Date: Tue, 7 Jul 2026 16:27:54 +0900 Subject: [PATCH 2/5] fix: use upstream package name in quick start --- README.md | 2 +- README_EN.md | 2 +- README_KR.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa7d094..807bbb1 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Token 流程: ## 快速开始 ```bash -npx @leecoder/kiro-proxy +npx kiro-proxy ``` 默认端口:`http://localhost:3456` diff --git a/README_EN.md b/README_EN.md index 506b253..481b892 100644 --- a/README_EN.md +++ b/README_EN.md @@ -69,7 +69,7 @@ Install and log in to Kiro so that `~/.aws/sso/cache/kiro-auth-token.json` exist ## Quick Start ```bash -npx @leecoder/kiro-proxy +npx kiro-proxy ``` Default port: `http://localhost:3456` diff --git a/README_KR.md b/README_KR.md index 50a7f3c..186752a 100644 --- a/README_KR.md +++ b/README_KR.md @@ -69,7 +69,7 @@ Kiro를 설치하고 로그인해서 `~/.aws/sso/cache/kiro-auth-token.json`이 ## 빠른 시작 ```bash -npx @leecoder/kiro-proxy +npx kiro-proxy ``` 서버 기본 포트: `http://localhost:3456` From 8d625aa544ce05ce719cc0272b8cb10576a3554f Mon Sep 17 00:00:00 2001 From: leecoder Date: Tue, 7 Jul 2026 16:28:48 +0900 Subject: [PATCH 3/5] fix: remove Databricks Apps deployment section from READMEs --- README.md | 20 -------------------- README_EN.md | 20 -------------------- README_KR.md | 20 -------------------- 3 files changed, 60 deletions(-) diff --git a/README.md b/README.md index 807bbb1..7180735 100644 --- a/README.md +++ b/README.md @@ -132,26 +132,6 @@ curl http://localhost:3456/v1/chat/completions \ } ``` -## Databricks Apps 部署 - -`app.yaml` 示例(已加入 `.gitignore`,仅本地维护): - -```yaml -command: - - node - - server.js - - --multi-user -env: - - name: PROXY_API_KEY - valueFrom: secret - - name: TOKEN_DB_PATH - value: "/tmp/kiro-proxy/tokens.db" -``` - -```bash -databricks apps deploy kiro-proxy --source-code-path "/Workspace/Users/$USER/kiro-proxy" -``` - ## 代理设置 遇到 `Invalid model` 错误时: diff --git a/README_EN.md b/README_EN.md index 481b892..9a516d8 100644 --- a/README_EN.md +++ b/README_EN.md @@ -132,26 +132,6 @@ Add to `~/.claude/settings.json`: } ``` -## Databricks Apps Deployment - -Example `app.yaml` (in `.gitignore`, managed locally): - -```yaml -command: - - node - - server.js - - --multi-user -env: - - name: PROXY_API_KEY - valueFrom: secret - - name: TOKEN_DB_PATH - value: "/tmp/kiro-proxy/tokens.db" -``` - -```bash -databricks apps deploy kiro-proxy --source-code-path "/Workspace/Users/$USER/kiro-proxy" -``` - ## Proxy Setup If you encounter `Invalid model` errors: diff --git a/README_KR.md b/README_KR.md index 186752a..96749dd 100644 --- a/README_KR.md +++ b/README_KR.md @@ -132,26 +132,6 @@ curl http://localhost:3456/v1/chat/completions \ } ``` -## Databricks Apps 배포 - -`app.yaml` 예시 (`.gitignore`에 포함, 로컬에서만 관리): - -```yaml -command: - - node - - server.js - - --multi-user -env: - - name: PROXY_API_KEY - valueFrom: secret - - name: TOKEN_DB_PATH - value: "/tmp/kiro-proxy/tokens.db" -``` - -```bash -databricks apps deploy kiro-proxy --source-code-path "/Workspace/Users/$USER/kiro-proxy" -``` - ## 프록시 설정 `Invalid model` 에러 발생 시: From e3e0221c64aca6ce845cb5b3419f0258a14ba881 Mon Sep 17 00:00:00 2001 From: leecoder Date: Tue, 7 Jul 2026 16:31:21 +0900 Subject: [PATCH 4/5] docs: restore original content, add multi-user section --- README.md | 115 +++++++++++++++++++++++++++++++----------------- README_EN.md | 111 ++++++++++++++++++++++++++++++---------------- README_KR.md | 121 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 226 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 7180735..e03f086 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,36 @@ # kiro-proxy -将 Kiro 订阅中的 Claude 模型通过 OpenAI/Anthropic 兼容 API 暴露出来。 +让 [Kiro](https://kiro.dev) 订阅内含的 Claude 模型可以在 Claude Code 中使用。 -读取 Kiro 认证 token,代理请求到 Amazon Q Developer,提供 OpenAI 和 Anthropic 兼容端点。 +通过读取 Kiro 的认证 token,代理请求到 Amazon Q Developer,暴露 OpenAI 和 Anthropic 兼容的 API 接口。 -## 模式 +## 前提 -### 本地模式(默认) +需要先安装并登录 Kiro,确保 `~/.aws/sso/cache/kiro-auth-token.json` 存在且未过期。 -从本机 `~/.aws/sso/cache/kiro-auth-token.json` 读取 token。 +## 快速开始 ```bash -node server.js +npx kiro-proxy ``` -### 多用户模式 +服务默认监听 `http://localhost:3456`。 + +## 配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `PORT` | `3456` | 监听端口 | +| `PROXY_API_KEY` | 无 | 设置后所有请求需携带此 key 进行鉴权,未设置则不校验 | +| `HTTPS_PROXY` | 无 | HTTP/HTTPS 代理地址,如 `http://127.0.0.1:7890` | +| `MULTI_USER` | `false` | `true` 启用多用户模式(等同 `--multi-user` 参数) | +| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | 多用户模式下的 Token 数据库路径 | +| `DATABRICKS_APP_PORT` | - | 设置后优先于 PORT | + +## 多用户模式 -客户端通过请求头传递 token,服务器缓存到 SQLite DB 并在过期时自动刷新。多个用户可同时使用各自 token。 +支持多客户端各自使用自己的 Kiro token 同时使用代理。服务器将 token 缓存到 SQLite DB,过期时自动刷新。 ```bash # CLI 参数 @@ -28,7 +41,7 @@ node server.js --multi-user MULTI_USER=true node server.js ``` -多用户模式下,客户端需传递以下请求头: +客户端需传递以下请求头: | 请求头 | 必需 | 说明 | |--------|------|------| @@ -41,7 +54,7 @@ MULTI_USER=true node server.js \* 至少提供其中一个。建议同时传两个以启用自动续期。 -提取 token 示例: +提取 token: ```bash # 使用内置脚本 @@ -62,60 +75,71 @@ Token 流程: 3. 过期时:服务器自动刷新并更新 DB 4. refresh token 本身过期:返回 401,客户端需提交新 token -## 前提 +不使用 `--multi-user` 时行为与原版完全一致(读取本地 token 文件)。 -安装并登录 Kiro,确保 `~/.aws/sso/cache/kiro-auth-token.json` 存在。 +## API -## 快速开始 +### GET /v1/models — 查询可用模型 ```bash -npx kiro-proxy +curl http://localhost:3456/v1/models ``` -默认端口:`http://localhost:3456` - -## 配置 - -| 环境变量 | 默认值 | 说明 | -|----------|--------|------| -| `PORT` | `3456` | 监听端口 | -| `DATABRICKS_APP_PORT` | - | 设置后优先于 PORT | -| `PROXY_API_KEY` | - | 设置后所有请求需 Bearer 认证 | -| `MULTI_USER` | `false` | `true` 启用多用户模式(等同 `--multi-user` 参数) | -| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | Token 数据库路径 | -| `HTTPS_PROXY` | - | 出站代理地址 | - -## API - ### POST /v1/messages — Anthropic 兼容 ```bash +# 非流式 curl http://localhost:3456/v1/messages \ -H "Content-Type: application/json" \ + -H "x-api-key: any" \ -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' + +# 流式 +curl http://localhost:3456/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: any" \ + -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}], "stream": true}' ``` ### POST /v1/chat/completions — OpenAI 兼容 ```bash +# 非流式 curl http://localhost:3456/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}' -``` -### GET /v1/models - -查询可用模型列表。 +# 流式 +curl http://localhost:3456/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}], "stream": true}' +``` ### GET /health 检查 token 状态及过期时间。 -### GET /credits?period=today|7d|30d|all +### GET /credits + +查询积分消耗统计,支持 `period` 参数: -积分使用统计。 +```bash +# 今日消耗(默认) +curl http://localhost:3456/credits + +# 最近 7 天 +curl http://localhost:3456/credits?period=7d + +# 最近 30 天 +curl http://localhost:3456/credits?period=30d + +# 全部 +curl http://localhost:3456/credits?period=all +``` + +## 与 Claude Code 集成 -## Claude Code 集成 +Claude Code 默认使用 Anthropic 官方 model ID,需要通过环境变量映射到 Q Developer 的 model ID。 在 `~/.claude/settings.json` 中添加: @@ -132,14 +156,25 @@ curl http://localhost:3456/v1/chat/completions \ } ``` +`model` 可选值:`sonnet`、`opus`、`haiku`,添加 `[1m]` 后缀可启用 1M 上下文窗口(如 `"opus[1m]"`)。 + +> 注意:不要设置 `ANTHROPIC_MODEL` 环境变量,它会覆盖 `model` 字段,导致上下文窗口等配置失效。 + ## 代理设置 -遇到 `Invalid model` 错误时: +自 2026 年 5 月 1 日起,Kiro 上的 Claude 模型无法在中国大陆及港澳台地区使用。如果遇到 `Invalid model` 错误,请配置代理。 + +> 注意:代理节点需选择其他地区(如新加坡、泰国、韩国等)。 + +通过环境变量设置 HTTP 代理: ```bash -HTTPS_PROXY=http://127.0.0.1:7890 node server.js +# 设置代理后启动 +HTTPS_PROXY=http://127.0.0.1:7890 npx kiro-proxy ``` -## 原始项目 +支持的环境变量:`HTTPS_PROXY`、`https_proxy`、`HTTP_PROXY`、`http_proxy`,优先级从左到右。 + +## 相关项目 -Fork from [Colin3191/kiro-proxy](https://github.com/Colin3191/kiro-proxy) +- [kiro-web-search](https://github.com/Colin3191/kiro-web-search) — 将 Kiro 内置的联网搜索封装为 MCP server,可在 Claude Code 等客户端中使用 diff --git a/README_EN.md b/README_EN.md index 9a516d8..064edd9 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,23 +2,36 @@ # kiro-proxy -Proxy that exposes Claude models from your Kiro subscription as OpenAI/Anthropic-compatible API endpoints. +Use the Claude models bundled with your [Kiro](https://kiro.dev) subscription in Claude Code. -Reads Kiro auth tokens, proxies requests to Amazon Q Developer, and serves OpenAI and Anthropic-compatible APIs. +Reads Kiro's auth token, proxies requests to Amazon Q Developer, and exposes OpenAI and Anthropic-compatible API endpoints. -## Modes +## Prerequisites -### Local mode (default) +Install and log in to Kiro so that `~/.aws/sso/cache/kiro-auth-token.json` exists and is valid. -Reads token from `~/.aws/sso/cache/kiro-auth-token.json` on the local machine. +## Quick Start ```bash -node server.js +npx kiro-proxy ``` -### Multi-user mode +Server listens on `http://localhost:3456` by default. + +## Configuration + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `PORT` | `3456` | Listen port | +| `PROXY_API_KEY` | None | When set, all requests must include this key for authentication. No validation when unset | +| `HTTPS_PROXY` | None | HTTP/HTTPS proxy URL, e.g. `http://127.0.0.1:7890` | +| `MULTI_USER` | `false` | `true` enables multi-user mode (same as `--multi-user` flag) | +| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | Token database path for multi-user mode | +| `DATABRICKS_APP_PORT` | - | Overrides PORT when set | + +## Multi-user Mode -Clients pass their Kiro token via request headers. The server caches tokens in a SQLite DB and auto-refreshes them on expiry. Multiple users can use the proxy simultaneously with their own tokens. +Allows multiple clients to use the proxy simultaneously with their own Kiro tokens. The server caches tokens in a SQLite DB and auto-refreshes on expiry. ```bash # CLI flag @@ -28,7 +41,7 @@ node server.js --multi-user MULTI_USER=true node server.js ``` -In multi-user mode, clients must include the following headers: +Clients must include the following headers: | Header | Required | Description | |--------|----------|-------------| @@ -62,61 +75,72 @@ Token flow: 3. On expiry: server auto-refreshes and updates DB 4. If refresh token itself expires: returns 401, client must submit a fresh token -## Prerequisites +Without `--multi-user`, behaves exactly as before (reads local token file). -Install and log in to Kiro so that `~/.aws/sso/cache/kiro-auth-token.json` exists. +## API -## Quick Start +### GET /v1/models — List available models ```bash -npx kiro-proxy +curl http://localhost:3456/v1/models ``` -Default port: `http://localhost:3456` - -## Configuration - -| Environment Variable | Default | Description | -|---------------------|---------|-------------| -| `PORT` | `3456` | Listen port | -| `DATABRICKS_APP_PORT` | - | Overrides PORT when set | -| `PROXY_API_KEY` | - | When set, all requests require Bearer auth | -| `MULTI_USER` | `false` | `true` enables multi-user mode (same as `--multi-user` flag) | -| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | Token database path | -| `HTTPS_PROXY` | - | Outbound proxy URL | - -## API - ### POST /v1/messages — Anthropic-compatible ```bash +# Non-streaming curl http://localhost:3456/v1/messages \ -H "Content-Type: application/json" \ + -H "x-api-key: any" \ -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' + +# Streaming +curl http://localhost:3456/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: any" \ + -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}], "stream": true}' ``` ### POST /v1/chat/completions — OpenAI-compatible ```bash +# Non-streaming curl http://localhost:3456/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}' -``` - -### GET /v1/models -List available models. +# Streaming +curl http://localhost:3456/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}], "stream": true}' +``` ### GET /health Check token status and expiration. -### GET /credits?period=today|7d|30d|all +### GET /credits -Credit usage statistics. +Query credit usage statistics. Supports `period` parameter: + +```bash +# Today's usage (default) +curl http://localhost:3456/credits + +# Last 7 days +curl http://localhost:3456/credits?period=7d + +# Last 30 days +curl http://localhost:3456/credits?period=30d + +# All time +curl http://localhost:3456/credits?period=all +``` ## Claude Code Integration +Claude Code uses Anthropic's official model IDs by default. Map them to Q Developer model IDs via environment variables. + Add to `~/.claude/settings.json`: ```json @@ -132,14 +156,25 @@ Add to `~/.claude/settings.json`: } ``` +`model` accepts `sonnet`, `opus`, or `haiku`. Append `[1m]` to enable the 1M context window (e.g. `"opus[1m]"`). + +> Note: Do not set the `ANTHROPIC_MODEL` environment variable — it overrides the `model` field and disables context window configuration. + ## Proxy Setup -If you encounter `Invalid model` errors: +Since May 1, 2026, Claude models on Kiro are unavailable in mainland China, Hong Kong, Macau, and Taiwan. If you encounter an `Invalid model` error, configure a proxy. + +> Note: Proxy nodes must be in other regions (e.g. Singapore, Thailand, South Korea). + +Set the proxy via environment variable: ```bash -HTTPS_PROXY=http://127.0.0.1:7890 node server.js +# Start with proxy +HTTPS_PROXY=http://127.0.0.1:7890 npx kiro-proxy ``` -## Origin +Supported environment variables: `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`, `http_proxy` (priority from left to right). + +## Related Projects -Forked from [Colin3191/kiro-proxy](https://github.com/Colin3191/kiro-proxy) +- [kiro-web-search](https://github.com/Colin3191/kiro-web-search) — MCP server exposing Kiro's web search for use in Claude Code and other clients diff --git a/README_KR.md b/README_KR.md index 96749dd..49669e6 100644 --- a/README_KR.md +++ b/README_KR.md @@ -2,23 +2,36 @@ # kiro-proxy -Kiro 구독에 포함된 Claude 모델을 OpenAI/Anthropic 호환 API로 노출하는 프록시. +[Kiro](https://kiro.dev) 구독에 포함된 Claude 모델을 Claude Code에서 사용할 수 있게 해주는 프록시. -Kiro 인증 토큰을 읽어서 Amazon Q Developer로 요청을 프록시하고, OpenAI 및 Anthropic 호환 엔드포인트를 제공합니다. +Kiro 인증 토큰을 읽어서 Amazon Q Developer로 요청을 프록시하고, OpenAI 및 Anthropic 호환 API 엔드포인트를 제공합니다. -## 모드 +## 전제조건 -### 로컬 모드 (기본) +Kiro를 설치하고 로그인해서 `~/.aws/sso/cache/kiro-auth-token.json`이 존재하고 유효해야 합니다. -로컬 머신의 `~/.aws/sso/cache/kiro-auth-token.json`에서 토큰을 읽습니다. +## 빠른 시작 ```bash -node server.js +npx kiro-proxy ``` -### 멀티유저 모드 +서버 기본 포트: `http://localhost:3456` + +## 설정 + +| 환경변수 | 기본값 | 설명 | +|----------|--------|------| +| `PORT` | `3456` | 수신 포트 | +| `PROXY_API_KEY` | 없음 | 설정 시 모든 요청에 이 키로 인증 필요. 미설정 시 검증 안 함 | +| `HTTPS_PROXY` | 없음 | HTTP/HTTPS 프록시 주소, 예: `http://127.0.0.1:7890` | +| `MULTI_USER` | `false` | `true`이면 멀티유저 모드 (`--multi-user` 플래그와 동일) | +| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | 멀티유저 모드 토큰 DB 경로 | +| `DATABRICKS_APP_PORT` | - | 설정 시 PORT보다 우선 | + +## 멀티유저 모드 -클라이언트가 요청 헤더로 토큰을 전달하면 서버가 SQLite DB에 캐시하고, 만료 시 자동으로 refresh합니다. 여러 유저가 각자 토큰으로 동시에 사용 가능. +여러 클라이언트가 각자의 Kiro 토큰으로 프록시를 동시에 사용 가능. 서버가 토큰을 SQLite DB에 캐시하고 만료 시 자동 refresh. ```bash # CLI 플래그 @@ -28,7 +41,7 @@ node server.js --multi-user MULTI_USER=true node server.js ``` -멀티유저 모드에서 클라이언트는 다음 헤더를 포함해야 합니다: +클라이언트는 다음 헤더를 포함해야 합니다: | 헤더 | 필수 | 설명 | |------|------|------| @@ -57,67 +70,78 @@ eval curl http://localhost:3456/v1/messages \ ``` 토큰 흐름: -1. 첫 요청 시 토큰 유효성 검증 후 DB에 저장 -2. 이후 요청에서 DB 캐시 사용 -3. 만료 시 서버가 자동 refresh → DB 갱신 -4. refresh token 자체 만료 시 401 반환 → 클라이언트가 새 토큰 제출 +1. 첫 요청: 토큰 검증 후 DB에 저장 +2. 이후 요청: DB 캐시 사용 +3. 만료 시: 서버가 자동 refresh → DB 갱신 +4. refresh token 자체 만료: 401 반환 → 클라이언트가 새 토큰 제출 -## 전제조건 +`--multi-user` 없이 실행하면 기존과 완전히 동일하게 동작 (로컬 토큰 파일 읽기). -Kiro를 설치하고 로그인해서 `~/.aws/sso/cache/kiro-auth-token.json`이 존재해야 합니다. +## API -## 빠른 시작 +### GET /v1/models — 사용 가능한 모델 조회 ```bash -npx kiro-proxy +curl http://localhost:3456/v1/models ``` -서버 기본 포트: `http://localhost:3456` - -## 설정 - -| 환경변수 | 기본값 | 설명 | -|----------|--------|------| -| `PORT` | `3456` | 수신 포트 | -| `DATABRICKS_APP_PORT` | - | 설정 시 PORT보다 우선 | -| `PROXY_API_KEY` | - | 설정 시 모든 요청에 Bearer 인증 필요 | -| `MULTI_USER` | `false` | `true`이면 멀티유저 모드 (`--multi-user` 플래그와 동일) | -| `TOKEN_DB_PATH` | `~/.kiro-proxy/tokens.db` | 토큰 DB 경로 | -| `HTTPS_PROXY` | - | 아웃바운드 프록시 주소 | - -## API - ### POST /v1/messages — Anthropic 호환 ```bash +# 비스트리밍 curl http://localhost:3456/v1/messages \ -H "Content-Type: application/json" \ + -H "x-api-key: any" \ -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' + +# 스트리밍 +curl http://localhost:3456/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: any" \ + -d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}], "stream": true}' ``` ### POST /v1/chat/completions — OpenAI 호환 ```bash +# 비스트리밍 curl http://localhost:3456/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}' -``` - -### GET /v1/models -사용 가능한 모델 목록 조회. +# 스트리밍 +curl http://localhost:3456/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}], "stream": true}' +``` ### GET /health 토큰 상태 및 만료 시간 확인. -### GET /credits?period=today|7d|30d|all +### GET /credits -크레딧 사용량 통계. +크레딧 사용량 통계. `period` 파라미터 지원: + +```bash +# 오늘 사용량 (기본) +curl http://localhost:3456/credits + +# 최근 7일 +curl http://localhost:3456/credits?period=7d + +# 최근 30일 +curl http://localhost:3456/credits?period=30d + +# 전체 +curl http://localhost:3456/credits?period=all +``` ## Claude Code 연동 -`~/.claude/settings.json`: +Claude Code는 기본적으로 Anthropic 공식 model ID를 사용합니다. 환경변수로 Q Developer model ID에 매핑해야 합니다. + +`~/.claude/settings.json`에 추가: ```json { @@ -132,14 +156,25 @@ curl http://localhost:3456/v1/chat/completions \ } ``` +`model` 옵션: `sonnet`, `opus`, `haiku`. `[1m]` 접미사로 1M 컨텍스트 윈도우 활성화 (예: `"opus[1m]"`). + +> 주의: `ANTHROPIC_MODEL` 환경변수를 설정하지 마세요. `model` 필드를 덮어써서 컨텍스트 윈도우 설정이 무효화됩니다. + ## 프록시 설정 -`Invalid model` 에러 발생 시: +2026년 5월 1일부터 Kiro의 Claude 모델은 중국 대륙 및 홍콩/마카오/대만에서 사용할 수 없습니다. `Invalid model` 에러가 발생하면 프록시를 설정하세요. + +> 주의: 프록시 노드는 다른 지역(싱가포르, 태국, 한국 등)을 선택해야 합니다. + +환경변수로 HTTP 프록시 설정: ```bash -HTTPS_PROXY=http://127.0.0.1:7890 node server.js +# 프록시 설정 후 시작 +HTTPS_PROXY=http://127.0.0.1:7890 npx kiro-proxy ``` -## 원본 프로젝트 +지원 환경변수: `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`, `http_proxy` (왼쪽부터 우선순위). + +## 관련 프로젝트 -Fork from [Colin3191/kiro-proxy](https://github.com/Colin3191/kiro-proxy) +- [kiro-web-search](https://github.com/Colin3191/kiro-web-search) — Kiro 내장 웹 검색을 MCP server로 래핑, Claude Code 등에서 사용 가능 From a84bd7f348d9501d2d9865980eed18bbb20e3c1b Mon Sep 17 00:00:00 2001 From: leecoder Date: Tue, 7 Jul 2026 16:58:36 +0900 Subject: [PATCH 5/5] fix: resolve variable shadowing and missing expiresAt in multi-user mode --- package.json | 5 ++++- token-reader.js | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 93f2542..2ab8266 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "@leecoder/kiro-proxy", "version": "0.3.0", - "description": "Kiro API proxy with OpenAI and Anthropic compatible endpoints — Databricks Apps edition", + "description": "Kiro API proxy with OpenAI and Anthropic compatible endpoints — multi-user edition", "type": "module", "bin": { "kiro-proxy": "./server.js" }, + "scripts": { + "start": "node server.js" + }, "engines": { "node": ">=18" }, diff --git a/token-reader.js b/token-reader.js index 319e08e..e921455 100644 --- a/token-reader.js +++ b/token-reader.js @@ -256,13 +256,13 @@ const refreshLocks = new Map(); * @param {object} headers — { accessToken, refreshToken, ?authMethod, ?profileArn, ?region, ?provider } */ export async function getAccessTokenFromRequest(headers) { - const { accessToken, refreshToken, authMethod, profileArn, region, provider, clientIdHash } = headers; + const { accessToken, refreshToken: clientRefreshToken, authMethod, profileArn, region, provider, clientIdHash } = headers; - if (!accessToken && !refreshToken) { + if (!accessToken && !clientRefreshToken) { throw new Error('X-Kiro-Access-Token or X-Kiro-Refresh-Token required'); } - const keySource = refreshToken || accessToken; + const keySource = clientRefreshToken || accessToken; const keyHash = hashToken(keySource); const stored = getStoredToken(keyHash); @@ -278,16 +278,24 @@ export async function getAccessTokenFromRequest(headers) { const promise = (async () => { try { - const tokenToRefresh = stored || { accessToken, refreshToken, authMethod, profileArn, region, provider, clientIdHash }; + const tokenToRefresh = stored || { accessToken, refreshToken: clientRefreshToken, authMethod, profileArn, region, provider, clientIdHash }; if (!tokenToRefresh.refreshToken) { - if (tokenToRefresh.accessToken && tokenToRefresh.expiresAt && new Date(tokenToRefresh.expiresAt) > new Date()) { + if (tokenToRefresh.accessToken) { upsertToken(keyHash, tokenToRefresh); return tokenToRefresh; } throw new Error('Token expired and no refreshToken available. Client must re-login in Kiro.'); } + // 첫 요청: expiresAt 없으면 accessToken이 유효하다고 가정하고 저장만 + if (!stored && accessToken) { + const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); + const tokenToStore = { accessToken, refreshToken: clientRefreshToken, authMethod, profileArn, region, provider, clientIdHash, expiresAt }; + upsertToken(keyHash, tokenToStore); + return tokenToStore; + } + tagLog('token', `[multi] Refreshing token (${keyHash.slice(0, 8)}...)`); const refreshed = await refreshToken(tokenToRefresh); upsertToken(keyHash, refreshed);