🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@askalf/dario

Package Overview
Dependencies
Maintainers
1
Versions
379
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@askalf/dario - npm Package Compare versions

Comparing version
5.2.6
to
5.2.7
+17
dist/durable-write.d.ts
/**
* Atomically and durably write `data` to `targetPath`.
*
* Writes to a pid/random-qualified temp file in the same directory (so the
* rename is same-filesystem and therefore atomic), fsyncs the temp file,
* renames it over the target, then fsyncs the parent directory so the rename
* survives power loss / SIGKILL.
*
* `mode` sets the temp file permissions (0o600 for credential files).
*
* On platforms/filesystems where a directory fsync isn't supported (some
* Windows and network filesystems throw EINVAL/EPERM/ENOTSUP on fsync of a
* directory handle), the dir-fsync failure is swallowed: the data fsync +
* atomic rename already covers the common Linux container case this targets,
* and a hard failure here would be worse than a best-effort flush.
*/
export declare function durableWriteFile(targetPath: string, data: string, mode?: number): Promise<void>;
/**
* Durable atomic file writes for credential persistence.
*
* Why this exists (dario#790): the pool refresh loop already writes rotated
* tokens back to `~/.dario/accounts/<alias>.json` and `credentials.json` via
* writeFile(tmp) + rename. But a plain rename only guarantees atomicity of the
* *directory entry*, not that the file's data blocks — or the rename itself —
* have reached stable storage. On an abrupt container recreate (`docker rm -f`
* → SIGKILL, the autodeploy path), the page cache is discarded before the
* kernel flushes, so a bind-mounted `~/.dario` reverts to the last *durably*
* persisted content: the mint-time file. That is the observed "credentials
* frozen at the mint ms stamp after 25h of successful in-memory refreshes"
* failure — every recreate after >8h loads a rotated-away refresh token and
* every request 401s.
*
* The fix: fsync the temp file's data before the rename, then fsync the parent
* directory after the rename so the rename itself is durable. This is the
* standard write-temp → fsync(file) → rename → fsync(dir) sequence.
*/
import { open, rename, unlink } from 'node:fs/promises';
import { dirname } from 'node:path';
/**
* Atomically and durably write `data` to `targetPath`.
*
* Writes to a pid/random-qualified temp file in the same directory (so the
* rename is same-filesystem and therefore atomic), fsyncs the temp file,
* renames it over the target, then fsyncs the parent directory so the rename
* survives power loss / SIGKILL.
*
* `mode` sets the temp file permissions (0o600 for credential files).
*
* On platforms/filesystems where a directory fsync isn't supported (some
* Windows and network filesystems throw EINVAL/EPERM/ENOTSUP on fsync of a
* directory handle), the dir-fsync failure is swallowed: the data fsync +
* atomic rename already covers the common Linux container case this targets,
* and a hard failure here would be worse than a best-effort flush.
*/
export async function durableWriteFile(targetPath, data, mode = 0o600) {
const dir = dirname(targetPath);
const tmp = `${targetPath}.tmp.${process.pid}.${Date.now()}`;
// Write + fsync the temp file's contents to stable storage.
const fh = await open(tmp, 'w', mode);
try {
await fh.writeFile(data);
await fh.sync();
}
finally {
await fh.close();
}
try {
await rename(tmp, targetPath);
}
catch (err) {
// Windows can fail a rename over a busy file. Fall back to a direct
// (still-fsynced) overwrite so we never leave the caller without a write.
try {
const direct = await open(targetPath, 'w', mode);
try {
await direct.writeFile(data);
await direct.sync();
}
finally {
await direct.close();
}
try {
await unlink(tmp);
}
catch { /* best effort */ }
return;
}
catch {
// Surface the original rename error — the fallback couldn't recover.
try {
await unlink(tmp);
}
catch { /* best effort */ }
throw err;
}
}
// fsync the parent directory so the rename (the new dirent) is durable.
try {
const dh = await open(dir, 'r');
try {
await dh.sync();
}
finally {
await dh.close();
}
}
catch {
// Directory fsync unsupported on this fs/platform — data fsync + atomic
// rename above is the meaningful guarantee for the Linux container case.
}
}
/**
* Provider routing seam.
*
* dario's "which backend owns this request" decision used to live inline in the
* request handler as a set of interleaved conditions (path class, provider
* prefix, GPT-family model test, the openai-backend reroute guard, the pool
* fallback guard). This module consolidates that decision into one place: a
* small set of `ProviderAdapter`s and a `route()` function that returns the
* primary provider plus any exhaustion fallback.
*
* Scope is deliberately the DECISION, not the request lifecycle. The Claude
* path's pool/template/cch/session/overage machinery stays shared below this
* seam — it's infrastructure that happens to serve one provider, not per-
* provider behaviour, so pushing it behind an adapter interface would make the
* Claude adapter the whole proxy and the OpenAI adapter nearly empty. The seam
* that pays for itself is routing + request-shaping; the rest is shared.
*
* The adapters reuse the same primitive proxy.ts uses (`isOpenAIModel`), so this
* is a consolidation of the existing decision, not a re-derivation of it.
*/
export type ProviderId = 'claude' | 'openai';
/** Inputs the routing decision needs, computed once per request. */
export interface RouteContext {
/** urlPath === '/v1/chat/completions' (OpenAI chat shape). */
isOpenAIPath: boolean;
/** Model name after provider-prefix stripping (e.g. 'gpt-4o', 'claude-opus-4-8'). */
model: string;
/** Forced provider from a `<provider>:` prefix or `--model` override; null if unforced. */
forcedProvider: ProviderId | null;
/** An openai-compat backend is configured (`dario backend add …`). */
hasOpenAIBackend: boolean;
/** `--pool-fallback=<model>` value, or null when disabled. */
poolFallbackModel: string | null;
/** Live pool account count. */
poolSize: number;
}
export interface RouteDecision {
/** Primary handler for the request. */
provider: ProviderId;
/** Provider to fall to on primary exhaustion; only claude→openai exists today. */
fallback: ProviderId | null;
/** Human-readable trace for `--verbose` and tests. */
reason: string;
}
export interface ProviderAdapter {
id: ProviderId;
/** Higher priority is offered the request first. */
priority: number;
/** True if this adapter should PRIMARILY handle the request. */
claimsPrimary(ctx: RouteContext): boolean;
}
/**
* OpenAI-compat backend adapter. Claims a request under exactly the condition
* the request handler reroutes on: a configured backend, an OpenAI-shape
* request, not force-routed to Claude, and either force-routed to openai or a
* recognized GPT-family model.
*/
export declare const openaiAdapter: ProviderAdapter;
/**
* Claude adapter — the default owner. Claims anything the openai adapter
* doesn't, matching the request handler's fall-through to the template path
* (including OpenAI-shape requests with Claude models, which the Claude path
* serves via openai→anthropic translation).
*/
export declare const claudeAdapter: ProviderAdapter;
export declare const DEFAULT_ADAPTERS: readonly ProviderAdapter[];
/**
* Resolve the routing decision. Offers the request to adapters in priority
* order and takes the first primary claim; the Claude adapter always claims, so
* the result is total. The claude→openai pool fallback is layered on top
* because it's a cross-adapter relationship (a Claude-primary request that
* spills to openai on pool exhaustion), not a primary claim by either side.
*/
export declare function route(ctx: RouteContext, adapters?: readonly ProviderAdapter[]): RouteDecision;
/**
* Provider routing seam.
*
* dario's "which backend owns this request" decision used to live inline in the
* request handler as a set of interleaved conditions (path class, provider
* prefix, GPT-family model test, the openai-backend reroute guard, the pool
* fallback guard). This module consolidates that decision into one place: a
* small set of `ProviderAdapter`s and a `route()` function that returns the
* primary provider plus any exhaustion fallback.
*
* Scope is deliberately the DECISION, not the request lifecycle. The Claude
* path's pool/template/cch/session/overage machinery stays shared below this
* seam — it's infrastructure that happens to serve one provider, not per-
* provider behaviour, so pushing it behind an adapter interface would make the
* Claude adapter the whole proxy and the OpenAI adapter nearly empty. The seam
* that pays for itself is routing + request-shaping; the rest is shared.
*
* The adapters reuse the same primitive proxy.ts uses (`isOpenAIModel`), so this
* is a consolidation of the existing decision, not a re-derivation of it.
*/
import { isOpenAIModel } from './openai-backend.js';
/**
* OpenAI-compat backend adapter. Claims a request under exactly the condition
* the request handler reroutes on: a configured backend, an OpenAI-shape
* request, not force-routed to Claude, and either force-routed to openai or a
* recognized GPT-family model.
*/
export const openaiAdapter = {
id: 'openai',
priority: 100,
claimsPrimary(ctx) {
if (!ctx.hasOpenAIBackend)
return false;
if (!ctx.isOpenAIPath)
return false;
if (ctx.forcedProvider === 'claude')
return false;
return ctx.forcedProvider === 'openai' || isOpenAIModel(ctx.model);
},
};
/**
* Claude adapter — the default owner. Claims anything the openai adapter
* doesn't, matching the request handler's fall-through to the template path
* (including OpenAI-shape requests with Claude models, which the Claude path
* serves via openai→anthropic translation).
*/
export const claudeAdapter = {
id: 'claude',
priority: 0,
claimsPrimary() {
return true;
},
};
export const DEFAULT_ADAPTERS = [openaiAdapter, claudeAdapter];
/**
* Resolve the routing decision. Offers the request to adapters in priority
* order and takes the first primary claim; the Claude adapter always claims, so
* the result is total. The claude→openai pool fallback is layered on top
* because it's a cross-adapter relationship (a Claude-primary request that
* spills to openai on pool exhaustion), not a primary claim by either side.
*/
export function route(ctx, adapters = DEFAULT_ADAPTERS) {
const ordered = [...adapters].sort((a, b) => b.priority - a.priority);
const primary = ordered.find((a) => a.claimsPrimary(ctx)) ?? claudeAdapter;
let fallback = null;
let reason = `${primary.id} primary`;
if (primary.id === 'claude' &&
ctx.poolFallbackModel !== null &&
ctx.hasOpenAIBackend &&
ctx.isOpenAIPath &&
ctx.poolSize > 0) {
fallback = 'openai';
reason = 'claude primary, openai fallback on pool-exhaustion';
}
return { provider: primary.id, fallback, reason };
}
+8
-14

@@ -20,3 +20,3 @@ /**

*/
import { readFile, writeFile, mkdir, readdir, unlink, rename } from 'node:fs/promises';
import { readFile, mkdir, readdir, unlink } from 'node:fs/promises';
import { join, basename } from 'node:path';

@@ -30,2 +30,3 @@ import { homedir } from 'node:os';

import { redactSecrets } from './redact.js';
import { durableWriteFile } from './durable-write.js';
const MANUAL_REDIRECT_URI = 'https://platform.claude.com/oauth/code/callback';

@@ -87,15 +88,8 @@ const DARIO_DIR = join(homedir(), '.dario');

await ensureDir();
const tmp = `${path}.tmp.${randomBytes(4).toString('hex')}`;
await writeFile(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
try {
await rename(tmp, path);
}
catch {
// Windows can fail renames on busy files — fall back to direct write
await writeFile(path, JSON.stringify(creds, null, 2), { mode: 0o600 });
try {
await unlink(tmp);
}
catch { /* ignore */ }
}
// Durable write (dario#790): fsync the temp file + parent dir so a rotated
// refresh token survives an abrupt container recreate. A plain rename left
// the data in the page cache; `docker rm -f` (SIGKILL) discarded it and the
// bind-mounted file reverted to the mint content, stranding every recreate
// after >8h on a rotated-away refresh token.
await durableWriteFile(path, JSON.stringify(creds, null, 2), 0o600);
}

@@ -102,0 +96,0 @@ export async function removeAccount(alias) {

@@ -9,3 +9,3 @@ /**

import { existsSync, readFileSync } from 'node:fs';
import { readFile, writeFile, mkdir, rename, unlink } from 'node:fs/promises';
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
import { execFile } from 'node:child_process';

@@ -16,2 +16,3 @@ import { dirname, join } from 'node:path';

import { redactSecrets } from './redact.js';
import { durableWriteFile } from './durable-write.js';
// Manual-flow redirect URI. Anthropic's authorize endpoint special-cases

@@ -417,6 +418,9 @@ // this value (also baked into CC as MANUAL_REDIRECT_URL) to render the

await mkdir(dirname(path), { recursive: true });
// Write atomically: write to temp file, then rename
const tmpPath = `${path}.tmp.${Date.now()}`;
await writeFile(tmpPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
await rename(tmpPath, path);
// Durable atomic write (dario#790): fsync the temp file + parent dir so a
// refreshed token isn't lost from the page cache on an abrupt container
// recreate (SIGKILL). Previously a plain rename left the new tokens
// unflushed; a `docker rm -f` reverted the bind-mounted credentials.json to
// its last durable (mint-time) content, so every recreate after >8h loaded a
// rotated-away refresh token and 401'd until a manual re-login.
await durableWriteFile(path, JSON.stringify(creds, null, 2), 0o600);
// Invalidate cache so next read picks up the new tokens

@@ -423,0 +427,0 @@ credentialsCache = creds;

{
"name": "@askalf/dario",
"version": "5.2.6",
"version": "5.2.7",
"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.",

@@ -25,3 +25,3 @@ "type": "module",

"test": "node --test --test-concurrency=8 test/all.test.mjs",
"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/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.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",
"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/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",
"audit": "npm audit --production --audit-level=high",

@@ -28,0 +28,0 @@ "prepublishOnly": "npm run build",

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display