@miller-tech/uap 1.170.0 → 1.170.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.170.0",
3
+ "version": "1.170.1",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -161,8 +161,9 @@ PROXY_PORT = int(os.environ.get("PROXY_PORT", "4000"))
161
161
  # PROXY_AUTH_TOKEN so the exposure is credential-gated.
162
162
  PROXY_HOST = os.environ.get("PROXY_HOST", "127.0.0.1")
163
163
  # Optional shared secret. When set, every request to a model route must present
164
- # it as `Authorization: Bearer <token>` or `X-Uap-Proxy-Token: <token>`. Unset
165
- # (default) = no check, which is safe only because the default bind is loopback.
164
+ # it as `Authorization: Bearer <token>`, `X-Uap-Proxy-Token: <token>`, or
165
+ # `X-Api-Key: <token>`. Unset (default) = no check, which is safe only because
166
+ # the default bind is loopback.
166
167
  # The health probe stays open so liveness checks don't need the secret.
167
168
  PROXY_AUTH_TOKEN = os.environ.get("PROXY_AUTH_TOKEN", "").strip()
168
169
  PROXY_LOG_LEVEL = os.environ.get("PROXY_LOG_LEVEL", "INFO").upper()
@@ -3584,8 +3585,9 @@ async def _shared_secret_auth(request: Request, call_next):
3584
3585
  No-op when the token is unset (the default; safe only because the default
3585
3586
  bind is loopback). When set — the intended posture for a shared LAN service
3586
3587
  (PROXY_HOST=0.0.0.0) — a request must present the token as
3587
- `Authorization: Bearer <token>` or `X-Uap-Proxy-Token: <token>`; otherwise
3588
- 401. Uses a constant-time compare to avoid a timing oracle.
3588
+ `Authorization: Bearer <token>`, `X-Uap-Proxy-Token: <token>`, or
3589
+ `X-Api-Key: <token>`; otherwise 401. Uses a constant-time compare to avoid a
3590
+ timing oracle.
3589
3591
  """
3590
3592
  if PROXY_AUTH_TOKEN and request.url.path not in _PROXY_AUTH_OPEN_PATHS and request.method != "OPTIONS":
3591
3593
  provided = request.headers.get("x-uap-proxy-token", "")
@@ -3593,6 +3595,13 @@ async def _shared_secret_auth(request: Request, call_next):
3593
3595
  auth = request.headers.get("authorization", "")
3594
3596
  if auth.lower().startswith("bearer "):
3595
3597
  provided = auth[7:].strip()
3598
+ if not provided:
3599
+ # This proxy emulates the Anthropic Messages API, whose SDK clients
3600
+ # (opencode's @ai-sdk/anthropic, claude-code, factory, …) send their
3601
+ # credential in the x-api-key header rather than Authorization/
3602
+ # X-Uap-Proxy-Token. Accept it as an equivalent token source so any
3603
+ # Anthropic-native client can authenticate without custom headers.
3604
+ provided = request.headers.get("x-api-key", "").strip()
3596
3605
  import hmac as _hmac
3597
3606
 
3598
3607
  if not (provided and _hmac.compare_digest(provided, PROXY_AUTH_TOKEN)):
@@ -3602,7 +3611,7 @@ async def _shared_secret_auth(request: Request, call_next):
3602
3611
  "type": "error",
3603
3612
  "error": {
3604
3613
  "type": "authentication_error",
3605
- "message": "missing or invalid proxy token (set X-Uap-Proxy-Token or Authorization: Bearer)",
3614
+ "message": "missing or invalid proxy token (set X-Uap-Proxy-Token, Authorization: Bearer, or X-Api-Key)",
3606
3615
  },
3607
3616
  }
3608
3617
  ),
@@ -0,0 +1,82 @@
1
+ """Tests for the shared-secret auth middleware header sources.
2
+
3
+ The proxy emulates the Anthropic Messages API. Its clients authenticate in one
4
+ of three ways depending on the SDK: `X-Uap-Proxy-Token`, `Authorization: Bearer`,
5
+ or — for Anthropic-native SDKs like opencode's @ai-sdk/anthropic and claude-code
6
+ — `x-api-key`. All three must be accepted; a wrong or missing token must 401.
7
+ """
8
+ import asyncio
9
+ import importlib.util
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+
14
+ def _load_proxy_module():
15
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
16
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
17
+ module = importlib.util.module_from_spec(spec)
18
+ spec.loader.exec_module(module)
19
+ return module
20
+
21
+
22
+ proxy = _load_proxy_module()
23
+
24
+ _TOKEN = "proxy-secret-token"
25
+ _SENTINEL = object()
26
+
27
+
28
+ class _FakeURL:
29
+ def __init__(self, path):
30
+ self.path = path
31
+
32
+
33
+ class _FakeReq:
34
+ def __init__(self, headers, path="/v1/messages", method="POST"):
35
+ self.headers = headers
36
+ self.url = _FakeURL(path)
37
+ self.method = method
38
+
39
+
40
+ async def _call_next(_request):
41
+ return _SENTINEL
42
+
43
+
44
+ def _run_auth(headers):
45
+ """Drive the async middleware; return _SENTINEL when auth passes, else the 401 Response."""
46
+ req = _FakeReq(headers)
47
+ return asyncio.run(proxy._shared_secret_auth(req, _call_next))
48
+
49
+
50
+ class ProxyAuthHeaderTest(unittest.TestCase):
51
+ def setUp(self):
52
+ self._prev = proxy.PROXY_AUTH_TOKEN
53
+ proxy.PROXY_AUTH_TOKEN = _TOKEN
54
+
55
+ def tearDown(self):
56
+ proxy.PROXY_AUTH_TOKEN = self._prev
57
+
58
+ def test_x_api_key_accepted(self):
59
+ # NEW behavior: Anthropic-native SDKs send the token as x-api-key.
60
+ self.assertIs(_run_auth({"x-api-key": _TOKEN}), _SENTINEL)
61
+
62
+ def test_x_api_key_wrong_token_rejected(self):
63
+ resp = _run_auth({"x-api-key": "not-the-token"})
64
+ self.assertEqual(getattr(resp, "status_code", None), 401)
65
+
66
+ def test_x_uap_proxy_token_still_accepted(self):
67
+ self.assertIs(_run_auth({"x-uap-proxy-token": _TOKEN}), _SENTINEL)
68
+
69
+ def test_authorization_bearer_still_accepted(self):
70
+ self.assertIs(_run_auth({"authorization": f"Bearer {_TOKEN}"}), _SENTINEL)
71
+
72
+ def test_no_credentials_rejected(self):
73
+ resp = _run_auth({})
74
+ self.assertEqual(getattr(resp, "status_code", None), 401)
75
+
76
+ def test_no_op_when_token_unset(self):
77
+ proxy.PROXY_AUTH_TOKEN = ""
78
+ self.assertIs(_run_auth({}), _SENTINEL)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ unittest.main()