@askalf/dario 5.4.11 → 5.4.12
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/live-fingerprint.d.ts +29 -0
- package/dist/live-fingerprint.js +51 -5
- package/package.json +2 -2
|
@@ -239,6 +239,35 @@ interface CapturedRequest {
|
|
|
239
239
|
rawHeaders: string[];
|
|
240
240
|
body: Record<string, unknown>;
|
|
241
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Does this request belong to the capture we started?
|
|
244
|
+
*
|
|
245
|
+
* The MITM used to accept the FIRST request whose URL contained
|
|
246
|
+
* `/v1/messages` with nothing tying it to the child we spawned (dario#872).
|
|
247
|
+
* The port is ephemeral, so a collision is improbable rather than
|
|
248
|
+
* impossible, and nothing downstream could tell a foreign request from the
|
|
249
|
+
* child's once it was captured.
|
|
250
|
+
*
|
|
251
|
+
* The nonce rides in the URL path because that is the one channel we fully
|
|
252
|
+
* control without touching what CC sends. Two measurements decided it over a
|
|
253
|
+
* key-borne nonce:
|
|
254
|
+
*
|
|
255
|
+
* 1. CC honours a path segment in ANTHROPIC_BASE_URL — given
|
|
256
|
+
* `http://127.0.0.1:PORT/<nonce>` it requests
|
|
257
|
+
* `/<nonce>/v1/messages?beta=true` (and probes `/<nonce>/api/hello`
|
|
258
|
+
* first, which still 404s here).
|
|
259
|
+
* 2. On a subscription install CC authenticates with
|
|
260
|
+
* `authorization: Bearer sk-ant-…`, so ANTHROPIC_API_KEY is not the auth
|
|
261
|
+
* header at all. A key-borne nonce would do nothing on OAuth installs
|
|
262
|
+
* while changing the auth path for API-key ones — and changing what the
|
|
263
|
+
* child authenticates with can change what it sends, which is the whole
|
|
264
|
+
* thing a fingerprint capture must not do.
|
|
265
|
+
*
|
|
266
|
+
* Fails closed: no nonce, no capture. A miss makes the capture return null
|
|
267
|
+
* and the bake exit non-zero, which is the correct outcome for a request we
|
|
268
|
+
* cannot attribute.
|
|
269
|
+
*/
|
|
270
|
+
export declare function isOwnCaptureRequest(url: string | undefined, nonce: string): boolean;
|
|
242
271
|
/**
|
|
243
272
|
* Run a loopback MITM server on a random port, spawn CC with
|
|
244
273
|
* ANTHROPIC_BASE_URL pointed at it, wait for one request, respond with a
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
* the right piece without re-deriving the threat model.
|
|
86
86
|
*/
|
|
87
87
|
import { spawn, execFileSync } from 'node:child_process';
|
|
88
|
+
import { randomBytes } from 'node:crypto';
|
|
88
89
|
import { createServer } from 'node:http';
|
|
89
90
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from 'node:fs';
|
|
90
91
|
import { homedir } from 'node:os';
|
|
@@ -363,6 +364,41 @@ export function _atomicWriteJsonForTest(targetPath, data) {
|
|
|
363
364
|
function writeLiveCache(data) {
|
|
364
365
|
atomicWriteJson(liveCachePath(), data);
|
|
365
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Does this request belong to the capture we started?
|
|
369
|
+
*
|
|
370
|
+
* The MITM used to accept the FIRST request whose URL contained
|
|
371
|
+
* `/v1/messages` with nothing tying it to the child we spawned (dario#872).
|
|
372
|
+
* The port is ephemeral, so a collision is improbable rather than
|
|
373
|
+
* impossible, and nothing downstream could tell a foreign request from the
|
|
374
|
+
* child's once it was captured.
|
|
375
|
+
*
|
|
376
|
+
* The nonce rides in the URL path because that is the one channel we fully
|
|
377
|
+
* control without touching what CC sends. Two measurements decided it over a
|
|
378
|
+
* key-borne nonce:
|
|
379
|
+
*
|
|
380
|
+
* 1. CC honours a path segment in ANTHROPIC_BASE_URL — given
|
|
381
|
+
* `http://127.0.0.1:PORT/<nonce>` it requests
|
|
382
|
+
* `/<nonce>/v1/messages?beta=true` (and probes `/<nonce>/api/hello`
|
|
383
|
+
* first, which still 404s here).
|
|
384
|
+
* 2. On a subscription install CC authenticates with
|
|
385
|
+
* `authorization: Bearer sk-ant-…`, so ANTHROPIC_API_KEY is not the auth
|
|
386
|
+
* header at all. A key-borne nonce would do nothing on OAuth installs
|
|
387
|
+
* while changing the auth path for API-key ones — and changing what the
|
|
388
|
+
* child authenticates with can change what it sends, which is the whole
|
|
389
|
+
* thing a fingerprint capture must not do.
|
|
390
|
+
*
|
|
391
|
+
* Fails closed: no nonce, no capture. A miss makes the capture return null
|
|
392
|
+
* and the bake exit non-zero, which is the correct outcome for a request we
|
|
393
|
+
* cannot attribute.
|
|
394
|
+
*/
|
|
395
|
+
export function isOwnCaptureRequest(url, nonce) {
|
|
396
|
+
if (!url || nonce.length === 0)
|
|
397
|
+
return false;
|
|
398
|
+
if (!url.startsWith(`/${nonce}/`))
|
|
399
|
+
return false;
|
|
400
|
+
return url.includes('/v1/messages');
|
|
401
|
+
}
|
|
366
402
|
/**
|
|
367
403
|
* Run a loopback MITM server on a random port, spawn CC with
|
|
368
404
|
* ANTHROPIC_BASE_URL pointed at it, wait for one request, respond with a
|
|
@@ -377,9 +413,11 @@ export async function captureLiveTemplateAsync(timeoutMs = 10_000) {
|
|
|
377
413
|
return extractTemplate(captured);
|
|
378
414
|
}
|
|
379
415
|
async function runCapture(timeoutMs) {
|
|
416
|
+
const nonce = `dario-capture-${randomBytes(12).toString('hex')}`;
|
|
380
417
|
return new Promise((resolve) => {
|
|
381
418
|
let captured = null;
|
|
382
419
|
let settled = false;
|
|
420
|
+
let foreign = 0;
|
|
383
421
|
const settle = (result) => {
|
|
384
422
|
if (settled)
|
|
385
423
|
return;
|
|
@@ -395,9 +433,17 @@ async function runCapture(timeoutMs) {
|
|
|
395
433
|
resolve(result);
|
|
396
434
|
};
|
|
397
435
|
const server = createServer((req, res) => {
|
|
398
|
-
// Only handle /v1/messages — everything else gets a 404 so CC
|
|
399
|
-
// accidentally think /v1/models is live
|
|
400
|
-
|
|
436
|
+
// Only handle OUR /v1/messages — everything else gets a 404 so CC
|
|
437
|
+
// doesn't accidentally think /v1/models is live, and so a request we
|
|
438
|
+
// cannot attribute to the spawned child is never captured (dario#872).
|
|
439
|
+
if (!isOwnCaptureRequest(req.url, nonce)) {
|
|
440
|
+
// A /v1/messages without our nonce is exactly the case #872 is about:
|
|
441
|
+
// something else reached this port. Say so — a silent 404 here is how
|
|
442
|
+
// a foreign capture would have gone unnoticed.
|
|
443
|
+
if (req.url?.includes('/v1/messages')) {
|
|
444
|
+
foreign++;
|
|
445
|
+
console.error(`[dario] capture: rejected a /v1/messages request that did not carry this capture's nonce (${foreign} so far) — see dario#872`);
|
|
446
|
+
}
|
|
401
447
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
402
448
|
res.end('{"type":"error","error":{"type":"not_found_error","message":"not found"}}');
|
|
403
449
|
return;
|
|
@@ -417,7 +463,7 @@ async function runCapture(timeoutMs) {
|
|
|
417
463
|
}
|
|
418
464
|
captured = {
|
|
419
465
|
method: req.method ?? 'POST',
|
|
420
|
-
path: req.url ?? '/v1/messages',
|
|
466
|
+
path: (req.url ?? '/v1/messages').replace(`/${nonce}`, ''),
|
|
421
467
|
headers,
|
|
422
468
|
rawHeaders: Array.isArray(req.rawHeaders) ? [...req.rawHeaders] : [],
|
|
423
469
|
body,
|
|
@@ -481,7 +527,7 @@ async function runCapture(timeoutMs) {
|
|
|
481
527
|
settle(null);
|
|
482
528
|
return;
|
|
483
529
|
}
|
|
484
|
-
const url = `http://127.0.0.1:${address.port}`;
|
|
530
|
+
const url = `http://127.0.0.1:${address.port}/${nonce}`;
|
|
485
531
|
// Spawn CC with ANTHROPIC_BASE_URL pointed at our MITM.
|
|
486
532
|
const claudeBin = findClaudeBinary();
|
|
487
533
|
if (!claudeBin) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.12",
|
|
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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc && cp src/cc-template-data.json dist/",
|
|
24
24
|
"test": "node --test --test-concurrency=8 test/all.test.mjs",
|
|
25
|
-
"test:serial": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/live-fingerprint.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/cc-authorize-probe-classifier.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/doctor-identity-drift.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/durable-token-persist.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs && node test/context-bleed.mjs && node test/sanitize-messages.mjs && node test/platform-tools.mjs && node test/strict-template-flags.mjs && node test/request-queue.mjs && node test/effort-flag.mjs && node test/template-invariants.mjs",
|
|
25
|
+
"test:serial": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/live-fingerprint.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/cc-authorize-probe-classifier.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/doctor-identity-drift.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/durable-token-persist.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs && node test/context-bleed.mjs && node test/capture-provenance.mjs && node test/sanitize-messages.mjs && node test/platform-tools.mjs && node test/strict-template-flags.mjs && node test/request-queue.mjs && node test/effort-flag.mjs && node test/template-invariants.mjs",
|
|
26
26
|
"audit": "npm audit --production --audit-level=high",
|
|
27
27
|
"prepublishOnly": "npm run build",
|
|
28
28
|
"start": "node dist/cli.js",
|