From 808e701ced89e2c6820c184e84269f247fa47258 Mon Sep 17 00:00:00 2001 From: zly <3268007793@qq.com> Date: Fri, 1 May 2026 15:47:39 +0800 Subject: [PATCH] chore(release): prepare v1.2.0 --- CHANGELOG.md | 17 ++++ README.md | 4 +- docs/releases/1.2.0.md | 49 ++++++++++ package.json | 9 +- pnpm-lock.yaml | 37 -------- src/proxy.ts | 8 +- tests/integration/lib/anthropic-compat.ts | 2 - tests/integration/lib/providers.ts | 5 +- tests/integration/provider-cli-e2e.ts | 1 - tests/integration/proxy-local.test.ts | 108 +++++++++++++++++++++- 10 files changed, 187 insertions(+), 53 deletions(-) create mode 100644 docs/releases/1.2.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c2865d8..4789ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. +## [1.2.0] - 2026-05-01 + +Minor release of `@sunflower0305/claude-proxy`. + +### Changed + +- `.env` loading now uses Node.js built-in `.env` file support instead of the external `dotenv` package +- minimum supported Node.js version is now 20.12 +- CORS is no longer enabled by default + +### Removed + +- removed runtime dependency on `dotenv` +- removed unused `cors` and `@types/cors` dependencies + +Detailed release notes: [docs/releases/1.2.0.md](docs/releases/1.2.0.md) + ## [1.1.3] - 2026-04-24 Patch release of `@sunflower0305/claude-proxy`. diff --git a/README.md b/README.md index 72d8c84..8569965 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ It currently supports `qwen`, `deepseek`, `glm`, `minimax`, and `kimi`. ## Install +Requires Node.js 20.12 or newer. + Run without installing: ```bash @@ -29,7 +31,7 @@ claude-proxy ## Configure -The proxy reads configuration from environment variables. You can export them in your shell or create a `.env` file in the directory where you run `claude-proxy`. +The proxy reads configuration from environment variables. You can export them in your shell or create a `.env` file in the directory where you run `claude-proxy`; `.env` loading uses Node.js built-in `.env` file support. Example `.env`: diff --git a/docs/releases/1.2.0.md b/docs/releases/1.2.0.md new file mode 100644 index 0000000..fb9a70c --- /dev/null +++ b/docs/releases/1.2.0.md @@ -0,0 +1,49 @@ +# `@sunflower0305/claude-proxy` v1.2.0 + +Minor release of `claude-proxy`, published as `@sunflower0305/claude-proxy`. + +## Highlights + +- `.env` loading now uses Node.js built-in `.env` file support +- the external `dotenv` package is no longer required +- default CORS middleware has been removed +- the package now requires Node.js 20.12 or newer + +## Upgrade Notes + +If you run `claude-proxy` with a local `.env` file, keep running it from the directory that contains that file. Existing environment variables still take precedence over values loaded from `.env`. + +Browser clients that relied on permissive default CORS headers must now put CORS handling in front of the proxy explicitly. + +## Installation + +```bash +npx @sunflower0305/claude-proxy +``` + +or: + +```bash +npm install -g @sunflower0305/claude-proxy +claude-proxy +``` + +## Verification Summary + +Local release gates to complete for `v1.2.0` before publication: + +- local TypeScript build completes successfully +- local integration test suite passes +- coverage generation completes successfully +- `npm pack --dry-run` confirms the published artifact includes `dist/`, `README.md`, `CHANGELOG.md`, `LICENSE`, and `.env.example` + +Post-publish verification plan: + +- confirm the `CD` workflow runs successfully for tag `v1.2.0` +- verify npm publication completes and the `latest` dist-tag points to `1.2.0` +- confirm the GitHub Release `v1.2.0` is created from these release notes + +## Source + +- GitHub: +- npm: diff --git a/package.json b/package.json index 9b44fe8..ec320cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sunflower0305/claude-proxy", - "version": "1.1.3", + "version": "1.2.0", "type": "module", "description": "A proxy that lets Claude Agent SDK use domestic Chinese LLMs (DeepSeek, Qwen, GLM, MiniMax) as backend", "license": "MIT", @@ -23,7 +23,7 @@ ".env.example" ], "engines": { - "node": ">=18" + "node": ">=20.12" }, "publishConfig": { "access": "public" @@ -55,20 +55,17 @@ "prepack": "npm run build", "prepublishOnly": "npm run test:proxy-local", "test": "vitest run", - "test:coverage": "vitest run tests/integration/proxy-local.test.ts --coverage --coverage.reporter=lcov --coverage.reporter=text --pool threads", + "test:coverage": "vitest run tests/integration/proxy-local.test.ts --coverage --coverage.reporter=lcov --coverage.reporter=text --pool forks", "test:proxy-local": "vitest run tests/integration/proxy-local.test.ts", "test:provider-anthropic": "node --experimental-strip-types tests/integration/provider-anthropic.ts", "test:provider-cli-e2e": "node --experimental-strip-types tests/integration/provider-cli-e2e.ts", "report:provider-cli-e2e": "node --experimental-strip-types tests/integration/report-provider-cli-e2e.ts" }, "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.5", "express": "^4.21.0" }, "devDependencies": { "@vitest/coverage-v8": "^3.2.4", - "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^22.0.0", "tsx": "^4.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 080b9a5..1e2ec6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,19 +8,10 @@ importers: .: dependencies: - cors: - specifier: ^2.8.5 - version: 2.8.6 - dotenv: - specifier: ^16.4.5 - version: 16.6.1 express: specifier: ^4.21.0 version: 4.22.1 devDependencies: - '@types/cors': - specifier: ^2.8.17 - version: 2.8.19 '@types/express': specifier: ^4.17.21 version: 4.17.25 @@ -395,9 +386,6 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/cors@2.8.19': - resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -566,10 +554,6 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -603,10 +587,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -851,10 +831,6 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -1380,10 +1356,6 @@ snapshots: dependencies: '@types/node': 22.19.17 - '@types/cors@2.8.19': - dependencies: - '@types/node': 22.19.17 - '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -1584,11 +1556,6 @@ snapshots: cookie@0.7.2: {} - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1609,8 +1576,6 @@ snapshots: destroy@1.2.0: {} - dotenv@16.6.1: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1888,8 +1853,6 @@ snapshots: negotiator@0.6.3: {} - object-assign@4.1.1: {} - object-inspect@1.13.4: {} on-finished@2.4.1: diff --git a/src/proxy.ts b/src/proxy.ts index 63c1a80..218db77 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -10,13 +10,14 @@ * export ANTHROPIC_API_KEY=any-key-works */ -import "dotenv/config"; -import cors from "cors"; import express from "express"; -import { realpathSync } from "node:fs"; +import { existsSync, realpathSync } from "node:fs"; +import { loadEnvFile } from "node:process"; import { Readable } from "node:stream"; import { fileURLToPath } from "node:url"; +if (existsSync(".env")) loadEnvFile(".env"); + const DEFAULT_ANTHROPIC_VERSION = "2023-06-01"; const HOP_BY_HOP_RESPONSE_HEADERS = new Set([ "connection", @@ -401,7 +402,6 @@ export function createApp(): express.Express { const app = express(); - app.use(cors()); app.use(express.json({ limit: "50mb" })); app.get("/", (_req, res) => { diff --git a/tests/integration/lib/anthropic-compat.ts b/tests/integration/lib/anthropic-compat.ts index da20167..82a4783 100644 --- a/tests/integration/lib/anthropic-compat.ts +++ b/tests/integration/lib/anthropic-compat.ts @@ -1,5 +1,3 @@ -import "dotenv/config"; - export type TestMode = "non-stream" | "stream"; export interface AnthropicCompatibilityCase { diff --git a/tests/integration/lib/providers.ts b/tests/integration/lib/providers.ts index 6a699ea..ec0167d 100644 --- a/tests/integration/lib/providers.ts +++ b/tests/integration/lib/providers.ts @@ -1,4 +1,7 @@ -import "dotenv/config"; +import { existsSync } from "node:fs"; +import { loadEnvFile } from "node:process"; + +if (existsSync(".env")) loadEnvFile(".env"); export type ProviderKey = "deepseek" | "qwen" | "glm" | "minimax" | "kimi"; diff --git a/tests/integration/provider-cli-e2e.ts b/tests/integration/provider-cli-e2e.ts index 85f138d..a4797ca 100644 --- a/tests/integration/provider-cli-e2e.ts +++ b/tests/integration/provider-cli-e2e.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { spawn } from "node:child_process"; diff --git a/tests/integration/proxy-local.test.ts b/tests/integration/proxy-local.test.ts index 938ac87..d1e2dcc 100644 --- a/tests/integration/proxy-local.test.ts +++ b/tests/integration/proxy-local.test.ts @@ -4,6 +4,9 @@ import http, { type ServerResponse, } from "node:http"; import { once } from "node:events"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { tmpdir } from "node:os"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; interface RecordedRequest { @@ -361,8 +364,10 @@ async function createHarness(envOverrides: EnvOverrides = {}): Promise { }); }); + it("does not enable CORS for browser origins by default", async () => { + const response = await fetch(`${harness.proxyBaseUrl}/health`, { + headers: { origin: "https://example.com" }, + }); + + expect(response.status).toBe(200); + expect(response.headers.get("access-control-allow-origin")).toBeNull(); + }); + it.each([ { provider: undefined, label: "missing" }, { provider: "not-a-provider", label: "invalid" }, @@ -541,6 +555,98 @@ describe.sequential("proxy local integration", () => { }); }); + it("loads .env from the current working directory without overriding existing env", async () => { + await cleanupHarness?.close(); + cleanupHarness = undefined; + + const originalCwd = process.cwd(); + const tempDir = await mkdtemp(path.join(tmpdir(), "claude-proxy-env-")); + const envBackup = Object.fromEntries( + testEnvKeys.map((key) => [key, process.env[key]]) + ) as Record; + const recordedRequests: RecordedRequest[] = []; + let proxy: Server | undefined; + let upstream: Server | undefined; + + try { + upstream = http.createServer(async (req, res) => { + const body = await readJsonBody(req); + recordedRequests.push({ headers: req.headers, body }); + writeJson(res, 200, { + id: "msg_upstream", + type: "message", + role: "assistant", + model: body.model, + content: [{ type: "text", text: "OK" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + }); + }); + const upstreamPort = await startServer(upstream); + + for (const key of testEnvKeys) { + setEnv(key, undefined); + } + process.env.DEEPSEEK_API_KEY = "existing-secret"; + + await writeFile( + path.join(tempDir, ".env"), + [ + "PROVIDER=deepseek", + "DEEPSEEK_API_KEY=file-secret", + "DEEPSEEK_MODEL=env-file-model", + `DEEPSEEK_ANTHROPIC_BASE_URL=http://127.0.0.1:${upstreamPort}`, + "", + ].join("\n"), + "utf8" + ); + + process.chdir(tempDir); + vi.resetModules(); + const { createApp } = await import("../../src/proxy.ts"); + const startedProxy = await startProxyServer(createApp); + proxy = startedProxy.proxy; + + const providerResponse = await fetch( + `${startedProxy.proxyBaseUrl}/api/provider` + ); + expect(providerResponse.status).toBe(200); + await expect(providerResponse.json()).resolves.toEqual({ + provider: "deepseek", + model: "env-file-model", + baseUrl: `http://127.0.0.1:${upstreamPort}`, + availableProviders, + }); + + const messageResponse = await fetch( + `${startedProxy.proxyBaseUrl}/v1/messages`, + { + method: "POST", + headers: { + "content-type": "application/json", + "x-api-key": "client-placeholder", + }, + body: JSON.stringify(buildRequestPayload()), + } + ); + + expect(messageResponse.status).toBe(200); + expect(recordedRequests.at(-1)?.headers["x-api-key"]).toBe( + "existing-secret" + ); + } finally { + if (proxy) await closeServer(proxy); + if (upstream) await closeServer(upstream); + process.chdir(originalCwd); + for (const key of testEnvKeys) { + setEnv(key, envBackup[key]); + } + await rm(tempDir, { recursive: true, force: true }); + vi.resetModules(); + } + }); + it("imports as a library when the process entrypoint is missing or invalid", async () => { await cleanupHarness?.close(); cleanupHarness = undefined;