@askalf/dario 5.5.0 → 5.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.
@@ -110,18 +110,21 @@ export declare function probeRequested(url: string | undefined): boolean;
110
110
  * Deliberately stricter than shouldDiscloseHealthInternals, because this is
111
111
  * not a disclosure decision — it spends the operator's money.
112
112
  *
113
- * The disclosure gate treats "authenticated" as sufficient, and
114
- * `authenticateRequest` returns TRUE when no DARIO_API_KEY is configured at
115
- * all. That is a reasonable convenience for the common loopback setup, and
116
- * harmless for a read-only field. It is not harmless here: an unkeyed dario
117
- * published through a Cloudflare tunnel would otherwise expose `?probe=1` as a
118
- * button any anonymous caller could press to bill the operator, once per TTL,
119
- * forever.
113
+ * The disclosure gate grants access to a caller that proved a configured
114
+ * DARIO_API_KEY, wherever it came from. That is the right answer for reading a
115
+ * field. It is not the right answer for an action that bills per call: a
116
+ * leaked or shared key becomes a metered spend endpoint reachable from the
117
+ * public internet, and the probe's own cache means an attacker needs only one
118
+ * request per TTL to keep it running indefinitely.
120
119
  *
121
120
  * So the probe additionally refuses anything that arrived through the tunnel,
122
- * whatever `authenticated` says. This only ever DENIES — it cannot widen
123
- * access — and it makes the spend path independent of whether an API key
124
- * happens to be configured.
121
+ * whatever the disclosure gate concluded. This only ever DENIES — it cannot
122
+ * widen access.
123
+ *
124
+ * (An unkeyed proxy is handled a layer up: shouldDiscloseHealthInternals now
125
+ * requires `keyConfigured`, so vacuous authentication no longer reaches here
126
+ * at all. This gate does not depend on that fix — it would refuse the tunnel
127
+ * caller either way — but the two are the same defence at different depths.)
125
128
  *
126
129
  * Accepted trade-off: an operator who authenticates THROUGH the tunnel is also
127
130
  * refused, and has to probe from beside the proxy instead. For a flag whose
@@ -137,16 +140,40 @@ export declare function shouldRunServingProbe(opts: {
137
140
  *
138
141
  * /health is intentionally auth-free (docker healthchecks need it before a
139
142
  * key is configured), so we cannot simply gate on the API key. Trust model:
140
- * - authenticated (valid DARIO_API_KEY) -> internal (an internal caller)
143
+ * - PROVED a configured DARIO_API_KEY -> internal (an internal caller)
141
144
  * - came via the Cloudflare tunnel (cf-ray) -> public (world-reachable)
142
145
  * - otherwise bare loopback -> internal (docker HC / doctor)
143
146
  * - otherwise (LAN, other container, WAN) -> public
144
- * The cf-ray check is now only ever used to DENY (force public), never to
145
- * grant, so spoofing it cannot widen disclosure — the previous fail-open
146
- * direction is closed.
147
+ * The cf-ray check is only ever used to DENY (force public), never to grant,
148
+ * so spoofing it cannot widen disclosure.
149
+ *
150
+ * `keyConfigured` is load-bearing and is why `authenticated` alone is not
151
+ * enough. `authenticateRequest()` short-circuits to TRUE when no
152
+ * DARIO_API_KEY is set — a deliberate convenience, since the common setup is
153
+ * loopback-only and requiring a key there would break `dario doctor` and every
154
+ * docker healthcheck. But it means "authenticated" is VACUOUS on an unkeyed
155
+ * proxy: every caller satisfies it, the first branch returns before cf-ray is
156
+ * ever consulted, and an unkeyed dario published through a Cloudflare tunnel
157
+ * hands its OAuth countdown, request volume and refresh-failure count to
158
+ * anyone who asks. That is the #642 fail-open re-entering through a side door
159
+ * — #642 closed the spoofable-header direction, not this one.
160
+ *
161
+ * Requiring both means the auth branch can only be taken by a caller that
162
+ * actually presented the operator's secret. Unkeyed proxies fall through to
163
+ * the transport rules, where loopback is still trusted (healthchecks and
164
+ * doctor keep working, unchanged) and the tunnel is not.
165
+ *
166
+ * `keyConfigured` is a REQUIRED field rather than an optional with a default:
167
+ * for a security predicate, every call site should be forced to state it.
168
+ *
169
+ * The HTTP status (200/503) is unaffected either way, so uptime monitoring
170
+ * that keys on the status code sees no change from this.
147
171
  */
148
172
  export declare function shouldDiscloseHealthInternals(opts: {
173
+ /** Passed authenticateRequest — which is vacuously true when unkeyed. */
149
174
  authenticated: boolean;
175
+ /** Whether a DARIO_API_KEY exists at all, i.e. whether `authenticated` means anything. */
176
+ keyConfigured: boolean;
150
177
  loopback: boolean;
151
178
  viaCfRay: boolean;
152
179
  }): boolean;
@@ -133,18 +133,21 @@ export function probeRequested(url) {
133
133
  * Deliberately stricter than shouldDiscloseHealthInternals, because this is
134
134
  * not a disclosure decision — it spends the operator's money.
135
135
  *
136
- * The disclosure gate treats "authenticated" as sufficient, and
137
- * `authenticateRequest` returns TRUE when no DARIO_API_KEY is configured at
138
- * all. That is a reasonable convenience for the common loopback setup, and
139
- * harmless for a read-only field. It is not harmless here: an unkeyed dario
140
- * published through a Cloudflare tunnel would otherwise expose `?probe=1` as a
141
- * button any anonymous caller could press to bill the operator, once per TTL,
142
- * forever.
136
+ * The disclosure gate grants access to a caller that proved a configured
137
+ * DARIO_API_KEY, wherever it came from. That is the right answer for reading a
138
+ * field. It is not the right answer for an action that bills per call: a
139
+ * leaked or shared key becomes a metered spend endpoint reachable from the
140
+ * public internet, and the probe's own cache means an attacker needs only one
141
+ * request per TTL to keep it running indefinitely.
143
142
  *
144
143
  * So the probe additionally refuses anything that arrived through the tunnel,
145
- * whatever `authenticated` says. This only ever DENIES — it cannot widen
146
- * access — and it makes the spend path independent of whether an API key
147
- * happens to be configured.
144
+ * whatever the disclosure gate concluded. This only ever DENIES — it cannot
145
+ * widen access.
146
+ *
147
+ * (An unkeyed proxy is handled a layer up: shouldDiscloseHealthInternals now
148
+ * requires `keyConfigured`, so vacuous authentication no longer reaches here
149
+ * at all. This gate does not depend on that fix — it would refuse the tunnel
150
+ * caller either way — but the two are the same defence at different depths.)
148
151
  *
149
152
  * Accepted trade-off: an operator who authenticates THROUGH the tunnel is also
150
153
  * refused, and has to probe from beside the proxy instead. For a flag whose
@@ -162,16 +165,37 @@ export function shouldRunServingProbe(opts) {
162
165
  *
163
166
  * /health is intentionally auth-free (docker healthchecks need it before a
164
167
  * key is configured), so we cannot simply gate on the API key. Trust model:
165
- * - authenticated (valid DARIO_API_KEY) -> internal (an internal caller)
168
+ * - PROVED a configured DARIO_API_KEY -> internal (an internal caller)
166
169
  * - came via the Cloudflare tunnel (cf-ray) -> public (world-reachable)
167
170
  * - otherwise bare loopback -> internal (docker HC / doctor)
168
171
  * - otherwise (LAN, other container, WAN) -> public
169
- * The cf-ray check is now only ever used to DENY (force public), never to
170
- * grant, so spoofing it cannot widen disclosure — the previous fail-open
171
- * direction is closed.
172
+ * The cf-ray check is only ever used to DENY (force public), never to grant,
173
+ * so spoofing it cannot widen disclosure.
174
+ *
175
+ * `keyConfigured` is load-bearing and is why `authenticated` alone is not
176
+ * enough. `authenticateRequest()` short-circuits to TRUE when no
177
+ * DARIO_API_KEY is set — a deliberate convenience, since the common setup is
178
+ * loopback-only and requiring a key there would break `dario doctor` and every
179
+ * docker healthcheck. But it means "authenticated" is VACUOUS on an unkeyed
180
+ * proxy: every caller satisfies it, the first branch returns before cf-ray is
181
+ * ever consulted, and an unkeyed dario published through a Cloudflare tunnel
182
+ * hands its OAuth countdown, request volume and refresh-failure count to
183
+ * anyone who asks. That is the #642 fail-open re-entering through a side door
184
+ * — #642 closed the spoofable-header direction, not this one.
185
+ *
186
+ * Requiring both means the auth branch can only be taken by a caller that
187
+ * actually presented the operator's secret. Unkeyed proxies fall through to
188
+ * the transport rules, where loopback is still trusted (healthchecks and
189
+ * doctor keep working, unchanged) and the tunnel is not.
190
+ *
191
+ * `keyConfigured` is a REQUIRED field rather than an optional with a default:
192
+ * for a security predicate, every call site should be forced to state it.
193
+ *
194
+ * The HTTP status (200/503) is unaffected either way, so uptime monitoring
195
+ * that keys on the status code sees no change from this.
172
196
  */
173
197
  export function shouldDiscloseHealthInternals(opts) {
174
- if (opts.authenticated)
198
+ if (opts.authenticated && opts.keyConfigured)
175
199
  return true;
176
200
  if (opts.viaCfRay)
177
201
  return false;
package/dist/proxy.js CHANGED
@@ -1701,6 +1701,9 @@ export async function startProxy(opts = {}) {
1701
1701
  const viaCfRay = req.headers['cf-ray'] !== undefined;
1702
1702
  const includeInternal = shouldDiscloseHealthInternals({
1703
1703
  authenticated: authenticateRequest(req.headers, apiKeyBuf),
1704
+ // Without this, `authenticated` is vacuously true on an unkeyed proxy
1705
+ // and the tunnel check below is never reached — see the gate's docs.
1706
+ keyConfigured: apiKeyBuf !== null,
1704
1707
  loopback: isLoopbackAddr(req.socket?.remoteAddress),
1705
1708
  viaCfRay,
1706
1709
  });
package/docs/usage.md CHANGED
@@ -150,9 +150,10 @@ Notes that matter in production:
150
150
 
151
151
  - **The probe is opt-in and never runs on a plain `/health`.** Existing docker
152
152
  healthchecks and uptime monitors keep costing nothing.
153
- - **Only trusted callers can trigger it** — authenticated, or loopback that did
154
- not arrive through a Cloudflare tunnel (the same gate that governs the OAuth
155
- internals). A world-readable `/health` is not a button for spending tokens.
153
+ - **Only trusted callers can trigger it** — and never a caller that arrived
154
+ through a Cloudflare tunnel, even an authenticated one. A `/health` reachable
155
+ from the internet is not a button for spending tokens, and the probe's own
156
+ cache means one request per TTL would be enough to keep it running.
156
157
  - **Results are cached and single-flighted** (`DARIO_PROBE_TTL_MS`, default
157
158
  60000), so polling every second still costs at most one probe per minute.
158
159
  - **A rate-limited or overloaded upstream is not an outage.** 429 and 529 keep
@@ -164,6 +165,27 @@ Notes that matter in production:
164
165
  failure mode in dario#905 was slots that stopped turning over entirely. Any
165
166
  release resets the stall clock, so sustained load never trips it.
166
167
 
168
+ ### Who sees what
169
+
170
+ `/health` is auth-free by design — a docker healthcheck has to work before any
171
+ key is configured. The response body is therefore split two ways, while the HTTP
172
+ status (200/503) is identical for everyone, so uptime checks are unaffected:
173
+
174
+ | Caller | Gets |
175
+ |---|---|
176
+ | Presented a configured `DARIO_API_KEY` | full detail |
177
+ | Bare loopback (docker healthcheck, `dario doctor`) | full detail |
178
+ | Arrived through a Cloudflare tunnel (`cf-ray`) | `{"status": "ok"}` only |
179
+ | Anything else (LAN, another container, WAN) | `{"status": "ok"}` only |
180
+
181
+ > **Changed in 5.5.1.** "Presented a configured key" previously read as "passed
182
+ > the API-key check" — which every caller passes when **no** `DARIO_API_KEY` is
183
+ > set. On an unkeyed proxy published through a tunnel, that disclosed the OAuth
184
+ > countdown, request volume and refresh-failure count to anyone who asked. If
185
+ > you monitor `/health` through a tunnel with no key configured, you now get the
186
+ > liveness verdict only; set `DARIO_API_KEY` and send it, or query from
187
+ > loopback, to keep the detail.
188
+
167
189
  A watchdog wants the probe; a container healthcheck usually does not:
168
190
 
169
191
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.0",
3
+ "version": "5.5.1",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {