@askalf/dario 4.8.126 → 4.8.127

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.
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
282
282
  */
283
283
  export declare const SUPPORTED_CC_RANGE: {
284
284
  readonly min: "1.0.0";
285
- readonly maxTested: "2.1.198";
285
+ readonly maxTested: "2.1.199";
286
286
  };
287
287
  /**
288
288
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -786,7 +786,7 @@ export function _resetInstalledVersionProbeForTest() {
786
786
  */
787
787
  export const SUPPORTED_CC_RANGE = {
788
788
  min: '1.0.0',
789
- maxTested: '2.1.198',
789
+ maxTested: '2.1.199',
790
790
  };
791
791
  /**
792
792
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
package/dist/proxy.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type IncomingMessage } from 'node:http';
2
2
  import { type WriteStream } from 'node:fs';
3
+ import { getAccessToken, getStatus } from './oauth.js';
3
4
  import { type EffortValue } from './cc-template.js';
4
5
  /**
5
6
  * Resolve a Claude-side model name through the family-alias rules if it's a
@@ -454,5 +455,33 @@ export declare function upstreamAuthHeaders(upstreamApiKey: string, accessToken:
454
455
  * testing.
455
456
  */
456
457
  export declare function shouldUsePool(accountCount: number, adminEnabled: boolean): boolean;
458
+ /**
459
+ * Resolve the single-account startup auth outcome, refreshing an expired-but-
460
+ * refreshable access token before giving up.
461
+ *
462
+ * The classic single-account startup used to read getStatus() once and, on any
463
+ * un-authenticated result, log "Not authenticated" and process.exit(1) WITHOUT
464
+ * attempting a refresh. Because Docker restarts the container, that turned a
465
+ * routine access-token expiry into a CRASH LOOP whenever the container was
466
+ * (re)started while the access token was expired — even when the refresh token
467
+ * was still valid and a single refresh would have recovered cleanly (dario
468
+ * outage-hardening gap #1, 2026-07-02).
469
+ *
470
+ * getAccessToken() refreshes an expired-but-refreshable token on demand, but it
471
+ * SWALLOWS a failed refresh (returns the stale token, throwing only when there
472
+ * are no credentials at all), so its success/throw can't gate the exit. Instead:
473
+ * when the initial status is un-authenticated but merely 'expired' with a live
474
+ * refresh token, attempt the refresh via getAccessToken() and then RE-READ
475
+ * getStatus() — its `authenticated` flag is the authority on whether we
476
+ * actually recovered. A dead / invalid_grant refresh token leaves status
477
+ * un-authenticated on the re-read, so the caller still exit(1)s with the same
478
+ * message (no infinite loop). 'none' (no credentials) and 'broken' (refresh
479
+ * already known-dead) skip the doomed attempt entirely. Pure + injectable for
480
+ * unit testing.
481
+ */
482
+ export declare function resolveSingleAccountStartupStatus(deps?: {
483
+ getStatus: typeof getStatus;
484
+ getAccessToken: typeof getAccessToken;
485
+ }): Promise<Awaited<ReturnType<typeof getStatus>>>;
457
486
  export declare function startProxy(opts?: ProxyOptions): Promise<void>;
458
487
  export {};
package/dist/proxy.js CHANGED
@@ -720,6 +720,57 @@ export function upstreamAuthHeaders(upstreamApiKey, accessToken) {
720
720
  export function shouldUsePool(accountCount, adminEnabled) {
721
721
  return accountCount >= 1 || adminEnabled;
722
722
  }
723
+ /**
724
+ * Resolve the single-account startup auth outcome, refreshing an expired-but-
725
+ * refreshable access token before giving up.
726
+ *
727
+ * The classic single-account startup used to read getStatus() once and, on any
728
+ * un-authenticated result, log "Not authenticated" and process.exit(1) WITHOUT
729
+ * attempting a refresh. Because Docker restarts the container, that turned a
730
+ * routine access-token expiry into a CRASH LOOP whenever the container was
731
+ * (re)started while the access token was expired — even when the refresh token
732
+ * was still valid and a single refresh would have recovered cleanly (dario
733
+ * outage-hardening gap #1, 2026-07-02).
734
+ *
735
+ * getAccessToken() refreshes an expired-but-refreshable token on demand, but it
736
+ * SWALLOWS a failed refresh (returns the stale token, throwing only when there
737
+ * are no credentials at all), so its success/throw can't gate the exit. Instead:
738
+ * when the initial status is un-authenticated but merely 'expired' with a live
739
+ * refresh token, attempt the refresh via getAccessToken() and then RE-READ
740
+ * getStatus() — its `authenticated` flag is the authority on whether we
741
+ * actually recovered. A dead / invalid_grant refresh token leaves status
742
+ * un-authenticated on the re-read, so the caller still exit(1)s with the same
743
+ * message (no infinite loop). 'none' (no credentials) and 'broken' (refresh
744
+ * already known-dead) skip the doomed attempt entirely. Pure + injectable for
745
+ * unit testing.
746
+ */
747
+ export async function resolveSingleAccountStartupStatus(deps = {
748
+ getStatus,
749
+ getAccessToken,
750
+ }) {
751
+ let status = await deps.getStatus();
752
+ if (status.authenticated)
753
+ return status;
754
+ // Only an expired token with a live, non-broken refresh token is worth a
755
+ // refresh attempt. canRefresh already encodes "has refresh token && !broken".
756
+ if (status.status === 'expired' && status.canRefresh) {
757
+ console.error('[dario] Access token expired at startup — attempting refresh before giving up…');
758
+ try {
759
+ await deps.getAccessToken(); // refreshes an expired-but-refreshable token in place
760
+ }
761
+ catch {
762
+ // getAccessToken only throws when there are no credentials at all; a
763
+ // failed refresh is swallowed there. The re-read below is authoritative.
764
+ }
765
+ // saveCredentials() updated the on-disk tokens + in-memory cache on a
766
+ // successful refresh, so this reflects the post-refresh reality.
767
+ status = await deps.getStatus();
768
+ if (status.authenticated) {
769
+ console.error('[dario] Token refresh succeeded at startup — continuing.');
770
+ }
771
+ }
772
+ return status;
773
+ }
723
774
  export async function startProxy(opts = {}) {
724
775
  const port = opts.port ?? DEFAULT_PORT;
725
776
  const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
@@ -1002,7 +1053,11 @@ export async function startProxy(opts = {}) {
1002
1053
  // ~/.dario/accounts/ is empty. Any accounts/ entry routes through the pool
1003
1054
  // above (#618), and admin mode always does (#599) — it starts even with
1004
1055
  // zero accounts and returns a clean 503 until one is added.
1005
- status = await getStatus();
1056
+ // An expired-but-refreshable access token here is self-healing: attempt a
1057
+ // refresh before giving up so a container (re)started shortly after a normal
1058
+ // token expiry recovers instead of crash-looping on exit(1) (gap #1). Only a
1059
+ // dead refresh token or no credentials at all still exits.
1060
+ status = await resolveSingleAccountStartupStatus();
1006
1061
  if (!status.authenticated) {
1007
1062
  console.error('[dario] Not authenticated. Run `dario login` first.');
1008
1063
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.126",
3
+ "version": "4.8.127",
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": {