anyray-connect 0.3.0 → 0.5.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.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Post-apply smoke test — confirm the connection actually works, with the same
3
+ * shape of request the configured tool will send, and turn any failure into one
4
+ * specific next step instead of a cryptic 4xx on the dev's first real request.
5
+ *
6
+ * Two paths, because the two modes authenticate differently:
7
+ * - org key (placeholder): the gateway key is the bearer; we probe the
8
+ * OpenAI-compatible `/v1/chat/completions`. A 2xx means the gateway accepted
9
+ * the key and routed upstream end-to-end.
10
+ * - seat (subscription): the seat OAuth token lives in the *tool*, not connect,
11
+ * so we DON'T need (or want) the secret. We send a sentinel bearer with the
12
+ * pass-through headers to `/v1/messages` and read where it lands: if the
13
+ * gateway forwards it to Anthropic (which rejects the sentinel as an invalid
14
+ * bearer), pass-through is wired and the dev's real seat will work; if it
15
+ * comes back a Bedrock/org-provider error, pass-through ISN'T honored. This
16
+ * verifies the part connect owns — the gateway routing — without a keychain
17
+ * prompt and without ever false-alarming on a stale token (the tool itself
18
+ * tells the dev to re-`/login` if their seat lapsed).
19
+ *
20
+ * PRIVACY: probe bodies are fixed synthetic "ping"s — no user content; the
21
+ * sentinel is a literal non-secret string. Responses are read only to classify
22
+ * status + error type, never logged as content.
23
+ */
24
+ import { effectiveSubscription, metadataHeaderValue } from '../types.js';
25
+ const VERIFY_TIMEOUT_MS = 12_000;
26
+ /** A deliberately-invalid bearer for the seat-wiring probe. Not a secret; its
27
+ * only job is to be rejected by whatever upstream the gateway forwards it to,
28
+ * so we can tell Anthropic-passthrough from org-provider routing. */
29
+ const WIRING_SENTINEL = 'anyray-wiring-probe-not-a-real-token';
30
+ /** Smallest valid OpenAI-style body; `anyray-default` lets gateway routing pick
31
+ * the real model/provider. `max_tokens: 1` keeps the probe a fraction of a cent. */
32
+ const PLACEHOLDER_PROBE_BODY = {
33
+ model: 'anyray-default',
34
+ messages: [{ role: 'user', content: 'ping' }],
35
+ max_tokens: 1,
36
+ };
37
+ /** Smallest valid Anthropic-style body for the seat-wiring probe. */
38
+ const WIRING_PROBE_BODY = {
39
+ model: 'claude-3-5-haiku-20241022',
40
+ max_tokens: 1,
41
+ messages: [{ role: 'user', content: 'ping' }],
42
+ };
43
+ /** A short, content-free snippet of an error body for the status line. */
44
+ const snippet = (bodyText) => {
45
+ const oneLine = bodyText.replace(/\s+/g, ' ').trim();
46
+ return oneLine.length > 160 ? `${oneLine.slice(0, 157)}…` : oneLine;
47
+ };
48
+ /** True when the body looks like the org's Bedrock path answered instead of the
49
+ * intended provider — the tell that pass-through didn't happen. */
50
+ const looksLikeOrgProvider = (bodyText) => {
51
+ const lower = bodyText.toLowerCase();
52
+ return lower.includes('bedrock') || lower.includes('unsupported model');
53
+ };
54
+ /** Classify the org-key probe (OpenAI-compatible completion). */
55
+ export const classifyPlaceholder = (status, bodyText) => {
56
+ if (status >= 200 && status < 300) {
57
+ return {
58
+ kind: 'ok',
59
+ detail: 'gateway served a completion with the org key — traffic flows.',
60
+ remedy: [],
61
+ };
62
+ }
63
+ if (status === 401 || status === 403) {
64
+ if (looksLikeOrgProvider(bodyText)) {
65
+ return {
66
+ kind: 'routed-elsewhere',
67
+ detail: `gateway routed to the org provider and it rejected the request: ${snippet(bodyText)}`,
68
+ remedy: [
69
+ 'The org provider rejected the request — check the gateway’s provider',
70
+ 'key/config (e.g. the model is enabled for that account).',
71
+ ],
72
+ };
73
+ }
74
+ return {
75
+ kind: 'key-rejected',
76
+ detail: `gateway rejected the key (HTTP ${status}).`,
77
+ remedy: [
78
+ 'The key was refused — the setup link may be expired; ask your admin for a fresh one.',
79
+ ],
80
+ };
81
+ }
82
+ return {
83
+ kind: 'unknown',
84
+ detail: `gateway responded HTTP ${status}: ${snippet(bodyText)}`,
85
+ remedy: [],
86
+ };
87
+ };
88
+ /** Classify the seat-wiring probe (sentinel bearer through the gateway). */
89
+ export const classifySeatWiring = (status, bodyText) => {
90
+ if (looksLikeOrgProvider(bodyText)) {
91
+ return {
92
+ kind: 'routed-elsewhere',
93
+ detail: 'gateway sent the request to the org provider, not your subscription.',
94
+ remedy: [
95
+ 'Pass-through isn’t being honored for this gateway — your seat token would',
96
+ 'not be used. Check the gateway is current (subscription pass-through support).',
97
+ ],
98
+ };
99
+ }
100
+ const lower = bodyText.toLowerCase();
101
+ // The sentinel reaching Anthropic and being rejected as a bad bearer is the
102
+ // SUCCESS signal: it proves the gateway forwards seat auth to Anthropic, so
103
+ // the dev's real (valid) seat token will be honored.
104
+ if (status === 401 ||
105
+ lower.includes('authentication_error') ||
106
+ lower.includes('invalid bearer') ||
107
+ lower.includes('"provider":"anthropic"')) {
108
+ return {
109
+ kind: 'ok',
110
+ detail: 'gateway passes your sign-in through to Anthropic — your Claude seat will be used.',
111
+ remedy: [],
112
+ };
113
+ }
114
+ if (status >= 200 && status < 300) {
115
+ return {
116
+ kind: 'ok',
117
+ detail: 'gateway pass-through reached the provider — your seat will be used.',
118
+ remedy: [],
119
+ };
120
+ }
121
+ return {
122
+ kind: 'unknown',
123
+ detail: `seat-wiring probe got HTTP ${status}: ${snippet(bodyText)}`,
124
+ remedy: [],
125
+ };
126
+ };
127
+ const probe = async (url, headers, body, timeoutMs) => {
128
+ const controller = new AbortController();
129
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
130
+ try {
131
+ const res = await fetch(url, {
132
+ method: 'POST',
133
+ headers: { 'content-type': 'application/json', ...headers },
134
+ body: JSON.stringify(body),
135
+ signal: controller.signal,
136
+ });
137
+ return { status: res.status, bodyText: await res.text() };
138
+ }
139
+ finally {
140
+ clearTimeout(timer);
141
+ }
142
+ };
143
+ const unreachable = (timeoutMs, error) => {
144
+ const reason = error instanceof Error && error.name === 'AbortError'
145
+ ? `no response within ${timeoutMs}ms`
146
+ : error instanceof Error
147
+ ? error.message
148
+ : String(error);
149
+ return {
150
+ kind: 'unreachable',
151
+ detail: `could not reach the gateway to verify (${reason}).`,
152
+ remedy: [
153
+ 'Your config was still written — re-run the curl below once the gateway is reachable.',
154
+ ],
155
+ };
156
+ };
157
+ /**
158
+ * Probe the gateway with the auth the Anthropic-family tool will use and return
159
+ * a classified verdict. Never throws — a transport failure becomes `unreachable`
160
+ * (the config was still written; the dev can re-test by hand). Anthropic is the
161
+ * representative family: its org path and its seat-passthrough path are exactly
162
+ * what this smoke test distinguishes.
163
+ */
164
+ export const verifyConnection = async (target, opts = {}) => {
165
+ const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
166
+ const meta = metadataHeaderValue(target.metadata);
167
+ try {
168
+ if (effectiveSubscription(target, 'anthropic')) {
169
+ const headers = {
170
+ Authorization: `Bearer ${WIRING_SENTINEL}`,
171
+ 'anthropic-version': '2023-06-01',
172
+ 'anthropic-beta': 'oauth-2025-04-20',
173
+ 'x-anyray-auth-mode': 'passthrough',
174
+ 'x-anyray-provider': 'anthropic',
175
+ };
176
+ if (meta)
177
+ headers['x-anyray-metadata'] = meta;
178
+ const { status, bodyText } = await probe(`${target.anthropicBaseUrl}/v1/messages`, headers, WIRING_PROBE_BODY, timeoutMs);
179
+ return classifySeatWiring(status, bodyText);
180
+ }
181
+ const key = target.clientKey ?? target.placeholderKey;
182
+ const headers = { Authorization: `Bearer ${key}` };
183
+ if (meta)
184
+ headers['x-anyray-metadata'] = meta;
185
+ const { status, bodyText } = await probe(`${target.openaiBaseUrl}/chat/completions`, headers, PLACEHOLDER_PROBE_BODY, timeoutMs);
186
+ return classifyPlaceholder(status, bodyText);
187
+ }
188
+ catch (error) {
189
+ return unreachable(timeoutMs, error);
190
+ }
191
+ };
192
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/util/verify.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyray-connect",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Anyray connect — points local coding tools (Claude Code, Cursor, Windsurf, SDKs) at the Anyray gateway by writing their base URL + a placeholder key. The gateway stays the brain; this is just the on-ramp.",
5
5
  "type": "module",
6
6
  "bin": {