diff --git a/bun.lock b/bun.lock index 830f0d66..f9d96334 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "officer", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.41", + "@anthropic-ai/sdk": "^0.71.0", "@bitcoinerlab/secp256k1": "1.2.0", "@getalby/sdk": "^8.0.3", "@hookform/resolvers": "^5.2.2", @@ -324,6 +325,8 @@ "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.59", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-xPOUZZimZI5ChaO791olWGXqaRvCwOfj9/1micu42EL9czdcwiDm0WK1OGsqb2mZ7LSCoYWBB0ZHVKOxehemDA=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -1616,6 +1619,8 @@ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -2290,6 +2295,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], diff --git a/package.json b/package.json index 70988f50..5d6640f9 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.41", + "@anthropic-ai/sdk": "^0.71.0", "@bitcoinerlab/secp256k1": "1.2.0", "@getalby/sdk": "^8.0.3", "@hookform/resolvers": "^5.2.2", diff --git a/src/servers/anthropic.ts b/src/servers/anthropic.ts new file mode 100644 index 00000000..e2609de2 --- /dev/null +++ b/src/servers/anthropic.ts @@ -0,0 +1,50 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { ANTHROPIC_PROXY_URL } from './officer-url.mjs'; +import { readProxySecretFromDisk } from './sidecar/claude/state'; + +/** + * The sanctioned way for PLATFORM-SIDE code — including a plugin's server routes — to call Claude. + * + * ── Why this module exists ── + * + * `officer-anthropic-proxy` swaps a local secret for the owner's OAuth bearer, and that secret lives + * in `sidecar/claude-state.json` — platform-internal state whose location, format and permissions are + * this repo's to change. The first caller outside the sidecar (fofos-website's translate route) read + * the file directly, which worked and meant the sidecar could no longer move its own state without + * breaking a plugin. This module is the boundary: plugins import IT, and only platform code touches + * the file. + * + * ── Why a module and not an HTTP route ── + * + * Every caller so far is server-side code in this same process, already behind its own gate. An HTTP + * endpoint that spends the owner's subscription would be NEW surface — reachable by anything holding a + * session, needing its own registry entry and permission — bought for no caller that exists. The day + * a browser-side caller appears, the route is a thin wrapper over this plus that registry entry. + * + * ── The secret is read per call ── + * + * A fresh install writes it through a 30s debounce, so anything that cached at boot could hold an + * empty string forever. Reading late costs one small file read and removes the failure mode. + */ + +/** + * A client pointed at the proxy, or null when the secret is not on disk yet. + * + * Null rather than a throw, because "not yet" is an ordinary state on a fresh install — the caller + * says which process is missing, in its own words, with its own status code. + */ +/* + Re-exported so a caller's `instanceof` works. + + A plugin that imported '@anthropic-ai/sdk' itself could resolve a SECOND copy from the plugins' + own node_modules — and an error thrown by the platform's copy is not an instance of the other + one's classes, so `catch (e) { e instanceof RateLimitError }` silently stops matching. One import + path, one copy, one class identity. +*/ +export { default as Anthropic } from '@anthropic-ai/sdk'; + +export function anthropicViaProxy(): Anthropic | null { + const secret = readProxySecretFromDisk(); + if (!secret) return null; + return new Anthropic({ apiKey: secret, baseURL: ANTHROPIC_PROXY_URL }); +}