@askalf/dario 5.5.67 → 5.5.68

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/accounts.js CHANGED
@@ -23,7 +23,7 @@ import { homedir } from 'node:os';
23
23
  import { randomUUID, randomBytes, createHash } from 'node:crypto';
24
24
  import { createServer } from 'node:http';
25
25
  import { detectCCOAuthConfig } from './cc-oauth-detect.js';
26
- import { loadCredentials, saveCredentialsTokens, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials } from './oauth.js';
26
+ import { loadCredentials, saveCredentialsTokens, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials, tokenRefreshDisabled, refreshDisabledError } from './oauth.js';
27
27
  import { openBrowser } from './open-browser.js';
28
28
  import { redactSecrets } from './redact.js';
29
29
  import { durableWriteFile } from './durable-write.js';
@@ -137,6 +137,12 @@ export async function detectClaudeIdentity() {
137
137
  const accountRefreshesInFlight = new Map();
138
138
  /** Refresh an account's OAuth token using dario's auto-detected CC OAuth config. */
139
139
  export async function refreshAccountToken(creds) {
140
+ // Borrow-don't-rotate: the pool path needs the same guard as the legacy one
141
+ // in oauth.ts, and for the same reason — a rotation here logs out every
142
+ // other holder of this account. Guarding only one of the two paths would
143
+ // leave the hole open for whichever store the process happens to load.
144
+ if (tokenRefreshDisabled())
145
+ throw refreshDisabledError();
140
146
  const inFlight = accountRefreshesInFlight.get(creds.alias);
141
147
  if (inFlight)
142
148
  return inFlight;
package/dist/oauth.d.ts CHANGED
@@ -188,6 +188,31 @@ export declare function readLineFromStdin(prompt: string): Promise<string>;
188
188
  * doomed retries and masking as healthy.
189
189
  */
190
190
  export declare function isTerminalRefreshFailure(status: number, body: string): boolean;
191
+ /**
192
+ * Borrow-don't-rotate mode. `DARIO_NO_TOKEN_REFRESH=1` makes every refresh
193
+ * path throw instead of rotating.
194
+ *
195
+ * WHY THIS EXISTS. Anthropic invalidates the previous refresh_token on every
196
+ * refresh (see the distributed-lock note in accounts.ts). So a process that
197
+ * merely READS a shared credential store is harmless, but the moment it
198
+ * refreshes, whatever else holds that token — a production proxy, another
199
+ * box — is silently logged out. Copying the file first does not help: the
200
+ * rotation happens upstream, so the copy invalidates the original too.
201
+ *
202
+ * That is not hypothetical. On 2026-08-25 a CI job on the self-hosted runner
203
+ * started a proxy against the shared store, refreshed it, and took the fleet
204
+ * down for roughly three hours with `invalid_grant`. The runner shares a box
205
+ * with prod and `/root/.dario` is a symlink into prod's store, so "just point
206
+ * HOME somewhere else" only helps when the job needs no real auth at all.
207
+ *
208
+ * Jobs that genuinely need subscription OAuth (the billing-classifier canary
209
+ * cannot use an API key — an API key IS the other billing bucket, so it would
210
+ * test the wrong thing) can now borrow the credential read-only: requests work
211
+ * for as long as the access token is valid, and an expiry fails LOUDLY here
212
+ * rather than quietly rotating a token that production is holding.
213
+ */
214
+ export declare function tokenRefreshDisabled(env?: NodeJS.ProcessEnv): boolean;
215
+ export declare function refreshDisabledError(): Error;
191
216
  export declare function refreshTokens(): Promise<OAuthTokens>;
192
217
  /**
193
218
  * Get a valid access token, refreshing if needed.
package/dist/oauth.js CHANGED
@@ -769,7 +769,41 @@ export async function readLineFromStdin(prompt) {
769
769
  export function isTerminalRefreshFailure(status, body) {
770
770
  return status === 401 || status === 403 || /invalid_grant/i.test(body);
771
771
  }
772
+ /**
773
+ * Borrow-don't-rotate mode. `DARIO_NO_TOKEN_REFRESH=1` makes every refresh
774
+ * path throw instead of rotating.
775
+ *
776
+ * WHY THIS EXISTS. Anthropic invalidates the previous refresh_token on every
777
+ * refresh (see the distributed-lock note in accounts.ts). So a process that
778
+ * merely READS a shared credential store is harmless, but the moment it
779
+ * refreshes, whatever else holds that token — a production proxy, another
780
+ * box — is silently logged out. Copying the file first does not help: the
781
+ * rotation happens upstream, so the copy invalidates the original too.
782
+ *
783
+ * That is not hypothetical. On 2026-08-25 a CI job on the self-hosted runner
784
+ * started a proxy against the shared store, refreshed it, and took the fleet
785
+ * down for roughly three hours with `invalid_grant`. The runner shares a box
786
+ * with prod and `/root/.dario` is a symlink into prod's store, so "just point
787
+ * HOME somewhere else" only helps when the job needs no real auth at all.
788
+ *
789
+ * Jobs that genuinely need subscription OAuth (the billing-classifier canary
790
+ * cannot use an API key — an API key IS the other billing bucket, so it would
791
+ * test the wrong thing) can now borrow the credential read-only: requests work
792
+ * for as long as the access token is valid, and an expiry fails LOUDLY here
793
+ * rather than quietly rotating a token that production is holding.
794
+ */
795
+ export function tokenRefreshDisabled(env = process.env) {
796
+ return env.DARIO_NO_TOKEN_REFRESH === '1';
797
+ }
798
+ export function refreshDisabledError() {
799
+ return new Error('token refresh is disabled (DARIO_NO_TOKEN_REFRESH=1) and the access token needs renewing. ' +
800
+ 'This process is borrowing a shared credential read-only: refreshing would rotate the token ' +
801
+ 'out from under whoever else holds it. Re-run once the credential owner has refreshed, or ' +
802
+ 'unset DARIO_NO_TOKEN_REFRESH if this process is the owner.');
803
+ }
772
804
  export async function refreshTokens() {
805
+ if (tokenRefreshDisabled())
806
+ throw refreshDisabledError();
773
807
  // Prevent concurrent refreshes — if one is already in progress, wait for it
774
808
  if (refreshInProgress)
775
809
  return refreshInProgress;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.67",
3
+ "version": "5.5.68",
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": {