codebase-cli 2.0.0-pre.40 → 2.0.0-pre.41

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.
@@ -1,22 +1,34 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import * as lockfile from "proper-lockfile";
1
4
  import { refreshAccessToken } from "./flow.js";
2
5
  /**
3
6
  * Read-through, refresh-aware accessor for the OAuth access token.
4
7
  *
5
- * Pi-mono's `getApiKey` runs on every API call, so we have to be fast in
6
- * the common case (token still valid). Slow path: refresh + persist before
7
- * returning the new token. A single in-flight promise (`pending`) prevents
8
- * a burst of concurrent calls from firing parallel refreshes at the same
9
- * moment they all await the one refresh that's already running.
8
+ * Two coordination layers:
9
+ * 1. In-memory single-flight (`pending`) multiple awaits within ONE
10
+ * process collapse into one refresh round-trip.
11
+ * 2. Filesystem lockfile on the credentials directory multiple
12
+ * `codebase` processes on the same machine coordinate so only one
13
+ * refreshes at a time. The others wait, re-read the rotated token,
14
+ * and skip their own refresh. Necessary because refresh tokens are
15
+ * often one-time-use: two parallel refreshes burn the shared refresh
16
+ * token and one process gets logged out.
17
+ *
18
+ * Pi-mono's `getApiKey` runs on every API call, so the cached-token fast
19
+ * path stays branch-free; the slow path only fires near expiry.
10
20
  */
11
21
  export class TokenManager {
12
22
  store;
13
23
  oauthConfig;
14
24
  refreshSkewMs;
25
+ lockTimeoutMs;
15
26
  pending = null;
16
27
  constructor(options) {
17
28
  this.store = options.store;
18
29
  this.oauthConfig = options.oauthConfig;
19
- this.refreshSkewMs = options.refreshSkewMs ?? 60_000;
30
+ this.refreshSkewMs = options.refreshSkewMs ?? 5 * 60_000;
31
+ this.lockTimeoutMs = options.lockTimeoutMs ?? 30_000;
20
32
  }
21
33
  /**
22
34
  * Return a valid access token, refreshing if the stored one is within
@@ -33,35 +45,19 @@ export class TokenManager {
33
45
  if (!creds.refreshToken) {
34
46
  throw new Error("access token expired and no refresh token saved — run `codebase auth login`");
35
47
  }
36
- return this.refresh(creds.refreshToken);
48
+ return this.refresh();
37
49
  }
38
50
  needsRefresh(creds) {
39
51
  if (!creds.expiresAt)
40
52
  return false;
41
53
  return creds.expiresAt - this.refreshSkewMs <= Date.now();
42
54
  }
43
- refresh(refreshToken) {
55
+ refresh() {
44
56
  if (this.pending)
45
57
  return this.pending;
46
58
  this.pending = (async () => {
47
59
  try {
48
- const next = await refreshAccessToken(this.oauthConfig, refreshToken);
49
- // Preserve fields the refresh response doesn't echo back (source,
50
- // email, userId) so they survive every rotation. The refresh
51
- // response is authoritative for tokens + expiry; we layer the
52
- // stable metadata back on top.
53
- const existing = this.store.load();
54
- this.store.save({
55
- accessToken: next.accessToken,
56
- refreshToken: next.refreshToken ?? existing?.refreshToken ?? refreshToken,
57
- expiresAt: next.expiresAt,
58
- scopes: next.scopes,
59
- source: existing?.source ?? next.source,
60
- userId: existing?.userId ?? next.userId,
61
- email: existing?.email ?? next.email,
62
- provider: existing?.provider ?? next.provider,
63
- });
64
- return next.accessToken;
60
+ return await this.refreshWithLock();
65
61
  }
66
62
  finally {
67
63
  this.pending = null;
@@ -69,5 +65,58 @@ export class TokenManager {
69
65
  })();
70
66
  return this.pending;
71
67
  }
68
+ /**
69
+ * Acquire a cross-process lock, then re-check (another process may have
70
+ * already refreshed by the time we get the lock), then refresh + save.
71
+ * The double-check is the whole point of taking the lock — it converts
72
+ * N parallel refreshes into 1 refresh + N-1 reads of the fresh token.
73
+ */
74
+ async refreshWithLock() {
75
+ const lockDir = dirname(this.store.filePath);
76
+ mkdirSync(lockDir, { recursive: true });
77
+ const release = await lockfile.lock(lockDir, {
78
+ retries: {
79
+ retries: Math.max(1, Math.ceil(this.lockTimeoutMs / 500)),
80
+ minTimeout: 250,
81
+ maxTimeout: 1000,
82
+ factor: 1.5,
83
+ randomize: true,
84
+ },
85
+ // 30s — stale-lock window. Survives a normal refresh; releases
86
+ // quickly enough that a crashed peer doesn't wedge us for long.
87
+ stale: 30_000,
88
+ });
89
+ try {
90
+ const reread = this.store.load();
91
+ if (reread && !this.needsRefresh(reread))
92
+ return reread.accessToken;
93
+ if (!reread?.refreshToken) {
94
+ throw new Error("access token expired and no refresh token saved — run `codebase auth login`");
95
+ }
96
+ const next = await refreshAccessToken(this.oauthConfig, reread.refreshToken);
97
+ // Preserve fields the refresh response doesn't echo back (source,
98
+ // email, userId) so they survive every rotation. The refresh
99
+ // response is authoritative for tokens + expiry; we layer the
100
+ // stable metadata back on top.
101
+ this.store.save({
102
+ accessToken: next.accessToken,
103
+ refreshToken: next.refreshToken ?? reread.refreshToken,
104
+ expiresAt: next.expiresAt,
105
+ scopes: next.scopes,
106
+ source: reread.source ?? next.source,
107
+ userId: reread.userId ?? next.userId,
108
+ email: reread.email ?? next.email,
109
+ provider: reread.provider ?? next.provider,
110
+ });
111
+ return next.accessToken;
112
+ }
113
+ finally {
114
+ await release().catch(() => {
115
+ // Release can fail if the lock was stale-released by another
116
+ // process while we held it. The credentials are already saved
117
+ // at that point — there's nothing useful to do here.
118
+ });
119
+ }
120
+ }
72
121
  }
73
122
  //# sourceMappingURL=token-manager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAcjE;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;IACP,KAAK,CAAmB;IACxB,WAAW,CAAc;IACzB,aAAa,CAAS;IAC/B,OAAO,GAA2B,IAAI,CAAC;IAE/C,YAAY,OAA4B;QACvC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QACvC,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,MAAM,CAAC;IACtD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,cAAc;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QACzE,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,WAAW,CAAC;QACxD,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;QAChG,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACzC,CAAC;IAEO,YAAY,CAAC,KAAkB;QACtC,IAAI,CAAC,KAAK,CAAC,SAAS;YAAE,OAAO,KAAK,CAAC;QACnC,OAAO,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAC3D,CAAC;IAEO,OAAO,CAAC,YAAoB;QACnC,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACtC,IAAI,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,IAAI,CAAC;gBACJ,MAAM,IAAI,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;gBACtE,kEAAkE;gBAClE,6DAA6D;gBAC7D,8DAA8D;gBAC9D,+BAA+B;gBAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;oBACf,WAAW,EAAE,IAAI,CAAC,WAAW;oBAC7B,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,QAAQ,EAAE,YAAY,IAAI,YAAY;oBACzE,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,IAAI,CAAC,MAAM;oBACvC,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,IAAI,CAAC,MAAM;oBACvC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,IAAI,CAAC,KAAK;oBACpC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,IAAI,IAAI,CAAC,QAAQ;iBAC7C,CAAC,CAAC;gBACH,OAAO,IAAI,CAAC,WAAW,CAAC;YACzB,CAAC;oBAAS,CAAC;gBACV,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACrB,CAAC;QACF,CAAC,CAAC,EAAE,CAAC;QACL,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;CACD"}
1
+ {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAoB,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAqBjE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,YAAY;IACP,KAAK,CAAmB;IACxB,WAAW,CAAc;IACzB,aAAa,CAAS;IACtB,aAAa,CAAS;IAC/B,OAAO,GAA2B,IAAI,CAAC;IAE/C,YAAY,OAA4B;QACvC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QACvC,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,CAAC,GAAG,MAAM,CAAC;QACzD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,MAAM,CAAC;IACtD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,cAAc;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QACzE,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,WAAW,CAAC;QACxD,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;QAChG,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IAEO,YAAY,CAAC,KAAkB;QACtC,IAAI,CAAC,KAAK,CAAC,SAAS;YAAE,OAAO,KAAK,CAAC;QACnC,OAAO,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAC3D,CAAC;IAEO,OAAO;QACd,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACtC,IAAI,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,IAAI,CAAC;gBACJ,OAAO,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;YACrC,CAAC;oBAAS,CAAC;gBACV,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACrB,CAAC;QACF,CAAC,CAAC,EAAE,CAAC;QACL,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,eAAe;QAC5B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7C,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE;YAC5C,OAAO,EAAE;gBACR,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,GAAG,GAAG,CAAC,CAAC;gBACzD,UAAU,EAAE,GAAG;gBACf,UAAU,EAAE,IAAI;gBAChB,MAAM,EAAE,GAAG;gBACX,SAAS,EAAE,IAAI;aACf;YACD,+DAA+D;YAC/D,gEAAgE;YAChE,KAAK,EAAE,MAAM;SACb,CAAC,CAAC;QACH,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACjC,IAAI,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;gBAAE,OAAO,MAAM,CAAC,WAAW,CAAC;YACpE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;YAChG,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;YAC7E,kEAAkE;YAClE,6DAA6D;YAC7D,8DAA8D;YAC9D,+BAA+B;YAC/B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBACf,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,YAAY;gBACtD,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM;gBACpC,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM;gBACpC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK;gBACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;aAC1C,CAAC,CAAC;YACH,OAAO,IAAI,CAAC,WAAW,CAAC;QACzB,CAAC;gBAAS,CAAC;YACV,MAAM,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;gBAC1B,6DAA6D;gBAC7D,8DAA8D;gBAC9D,qDAAqD;YACtD,CAAC,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;CACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebase-cli",
3
- "version": "2.0.0-pre.40",
3
+ "version": "2.0.0-pre.41",
4
4
  "description": "Codebase CLI — a TypeScript coding agent on the pi-mono runtime. OAuth-aware, any LLM provider, single install.",
5
5
  "keywords": [
6
6
  "ai",
@@ -62,10 +62,12 @@
62
62
  "@earendil-works/pi-agent-core": "0.74.0",
63
63
  "@earendil-works/pi-ai": "0.74.0",
64
64
  "@types/diff": "^7.0.2",
65
+ "@types/proper-lockfile": "^4.1.4",
65
66
  "diff": "^9.0.0",
66
67
  "glob": "^13.0.1",
67
68
  "ignore": "^7.0.0",
68
69
  "ink": "^5.2.1",
70
+ "proper-lockfile": "^4.1.2",
69
71
  "react": "^18.3.1",
70
72
  "typebox": "^1.1.24"
71
73
  },