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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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(
|
|
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(
|
|
55
|
+
refresh() {
|
|
44
56
|
if (this.pending)
|
|
45
57
|
return this.pending;
|
|
46
58
|
this.pending = (async () => {
|
|
47
59
|
try {
|
|
48
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebase-cli",
|
|
3
|
-
"version": "2.0.0-pre.
|
|
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
|
},
|