@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.
- package/dist/health-response.d.ts +41 -14
- package/dist/health-response.js +39 -15
- package/dist/proxy.js +3 -0
- package/docs/usage.md +25 -3
- package/package.json +1 -1
|
@@ -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
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
|
123
|
-
* access
|
|
124
|
-
*
|
|
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
|
-
* -
|
|
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
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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;
|
package/dist/health-response.js
CHANGED
|
@@ -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
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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
|
|
146
|
-
* access
|
|
147
|
-
*
|
|
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
|
-
* -
|
|
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
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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** —
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
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": {
|