copillm 0.3.0-beta.2 → 0.3.0-beta.4
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/cli/auth/runAuth.js +79 -50
- package/dist/cli/packageInfo.js +1 -1
- package/dist/server/debugInfo.js +16 -5
- package/package.json +1 -1
package/dist/cli/auth/runAuth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
1
2
|
import { clearStoredCredential, loadStoredCredential, loadStoredCredentialForAccount, registerExistingCredentialAsDefault, saveStoredCredential } from "../../auth/credentials.js";
|
|
2
3
|
import { readAccountsIndex, findAccount, assertValidAccountId, InvalidAccountIdError, UnknownAccountError } from "../../auth/accounts.js";
|
|
3
4
|
import { addAccount, listAccountsDetailed, removeAccountAndCredential, removeAllAccounts, switchDefaultAccount } from "../../auth/accountManager.js";
|
|
@@ -11,26 +12,37 @@ import { writeCommandOutput } from "../shared/output.js";
|
|
|
11
12
|
/**
|
|
12
13
|
* Derive a friendly, path-safe account id from the GitHub login behind a token.
|
|
13
14
|
* Returns null when the lookup fails or the login isn't a valid id.
|
|
15
|
+
*
|
|
16
|
+
* Multi-account routing now depends on this, so it retries a few times with a
|
|
17
|
+
* generous timeout: GitHub's `/user` can briefly throttle or lag right after a
|
|
18
|
+
* device-flow token exchange, and a single failed probe must not cause the
|
|
19
|
+
* caller to mis-identify (and overwrite) the wrong account.
|
|
14
20
|
*/
|
|
15
21
|
async function deriveAccountId(token) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
identity =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
const attempts = 3;
|
|
23
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
24
|
+
let identity = null;
|
|
25
|
+
try {
|
|
26
|
+
identity = await inspectGithubIdentity({ token, timeoutMs: 8_000 });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
identity = null;
|
|
30
|
+
}
|
|
31
|
+
const login = identity?.login;
|
|
32
|
+
if (login) {
|
|
33
|
+
try {
|
|
34
|
+
assertValidAccountId(login);
|
|
35
|
+
return login;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (attempt < attempts) {
|
|
42
|
+
await sleep(400 * attempt);
|
|
43
|
+
}
|
|
33
44
|
}
|
|
45
|
+
return null;
|
|
34
46
|
}
|
|
35
47
|
export async function runAuthLogin(opts, options) {
|
|
36
48
|
if (options.forceSession) {
|
|
@@ -72,45 +84,62 @@ export async function runAuthLogin(opts, options) {
|
|
|
72
84
|
return;
|
|
73
85
|
}
|
|
74
86
|
// ---- No name: auto-manage by the token's GitHub login -----------------
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
87
|
+
// Identify the account from its GitHub login so a second `auth login` for a
|
|
88
|
+
// DIFFERENT account is kept alongside the first instead of overwriting it.
|
|
89
|
+
// The cardinal rule: never replace an existing credential unless we have
|
|
90
|
+
// positively confirmed it's the SAME GitHub account.
|
|
78
91
|
const newLogin = await deriveAccountId(token);
|
|
92
|
+
const existing = await loadStoredCredential();
|
|
93
|
+
if (!existing) {
|
|
94
|
+
// Nothing stored yet.
|
|
95
|
+
if (index) {
|
|
96
|
+
// An index exists but its default account has no credential — restore it.
|
|
97
|
+
const targetId = newLogin ?? index.defaultAccount;
|
|
98
|
+
const result = await addAccount({ id: targetId, accountType, token, mode: saveMode });
|
|
99
|
+
emitLoginResult(opts, result, !result.isDefault);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Fresh single-account install: store without creating an index.
|
|
103
|
+
const backend = await saveStoredCredential(token, accountType, { mode: saveMode });
|
|
104
|
+
writeCommandOutput(opts, `Login succeeded. Credentials stored via ${describeBackend(backend)}.`, {
|
|
105
|
+
status: "ok",
|
|
106
|
+
action: "login",
|
|
107
|
+
credential_backend: backend
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// A credential already exists. We must know which GitHub account just signed
|
|
112
|
+
// in before we touch anything, or we risk clobbering a different account.
|
|
113
|
+
if (!newLogin) {
|
|
114
|
+
writeCommandOutput(opts, "Login failed: couldn't verify which GitHub account you signed in as (the GitHub user lookup didn't succeed). " +
|
|
115
|
+
"Your existing credentials were left untouched. Re-run `copillm auth login`, or name this account explicitly with `copillm auth login --as <name>`.", { status: "error", action: "login", error: "github_identity_unresolved" });
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
79
119
|
if (index) {
|
|
80
|
-
// Add the new login as its own account
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const wasKnown = findAccount(targetId) !== null;
|
|
84
|
-
const result = await addAccount({ id: targetId, accountType, token, mode: saveMode });
|
|
120
|
+
// Add the new login as its own account, or refresh it in place if known.
|
|
121
|
+
const wasKnown = findAccount(newLogin) !== null;
|
|
122
|
+
const result = await addAccount({ id: newLogin, accountType, token, mode: saveMode });
|
|
85
123
|
emitLoginResult(opts, result, !wasKnown && !result.isDefault);
|
|
86
124
|
return;
|
|
87
125
|
}
|
|
88
|
-
// No
|
|
89
|
-
|
|
90
|
-
if (newLogin) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
emitLoginResult(opts, result, !result.isDefault);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
// Same account → fall through to an in-place single-account refresh.
|
|
103
|
-
}
|
|
126
|
+
// No index yet. Compare against the existing single account.
|
|
127
|
+
const existingLogin = await deriveAccountId(existing.token);
|
|
128
|
+
if (existingLogin === newLogin) {
|
|
129
|
+
// Confirmed the SAME account → refresh in place, no index created.
|
|
130
|
+
const backend = await saveStoredCredential(token, accountType, { mode: saveMode });
|
|
131
|
+
writeCommandOutput(opts, `Login succeeded. Credentials stored via ${describeBackend(backend)}.`, {
|
|
132
|
+
status: "ok",
|
|
133
|
+
action: "login",
|
|
134
|
+
credential_backend: backend
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
104
137
|
}
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
status: "ok",
|
|
111
|
-
action: "login",
|
|
112
|
-
credential_backend: backend
|
|
113
|
-
});
|
|
138
|
+
// A different (or unverifiable) prior account → transition to multi-account,
|
|
139
|
+
// preserving the prior login as the default and adding the new one.
|
|
140
|
+
registerExistingCredentialAsDefault(existingLogin ?? "default", existing.accountType);
|
|
141
|
+
const result = await addAccount({ id: newLogin, accountType, token, mode: saveMode });
|
|
142
|
+
emitLoginResult(opts, result, !result.isDefault);
|
|
114
143
|
}
|
|
115
144
|
function emitLoginResult(opts, result, hintSwitch) {
|
|
116
145
|
const defaultSuffix = result.isDefault ? " (default)" : "";
|
package/dist/cli/packageInfo.js
CHANGED
package/dist/server/debugInfo.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { setTimeout as defaultSleep } from "node:timers/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { githubUserUrl } from "../config/upstream.js";
|
|
3
4
|
import { isRetryableStatus, isRetryableTransportError, retryDelayMs } from "./upstream/retryPolicy.js";
|
|
4
5
|
const CACHE_TTL_MS = 5 * 60 * 1_000;
|
|
5
6
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
6
|
-
|
|
7
|
+
// Cache keyed by a hash of the GitHub token. It MUST be per-token: different
|
|
8
|
+
// tokens identify different GitHub accounts, and a token-blind cache returns
|
|
9
|
+
// one account's identity for another's — which silently broke multi-account
|
|
10
|
+
// `auth login` (a second login appeared to be the same account and overwrote
|
|
11
|
+
// the first). Hashing avoids retaining raw tokens as map keys.
|
|
12
|
+
const cache = new Map();
|
|
13
|
+
function cacheKey(token) {
|
|
14
|
+
return createHash("sha256").update(token).digest("hex");
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* Fetch the GitHub user summary with bounded retries on transient failures.
|
|
9
18
|
*
|
|
@@ -20,8 +29,10 @@ let cached = null;
|
|
|
20
29
|
*/
|
|
21
30
|
export async function getGithubUserSummary(githubToken, options = {}) {
|
|
22
31
|
const now = Date.now();
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
const key = cacheKey(githubToken);
|
|
33
|
+
const hit = cache.get(key);
|
|
34
|
+
if (hit && now - hit.fetchedAt < CACHE_TTL_MS) {
|
|
35
|
+
return hit.summary;
|
|
25
36
|
}
|
|
26
37
|
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
27
38
|
const sleepImpl = options.sleepImpl ?? ((ms) => defaultSleep(ms));
|
|
@@ -60,7 +71,7 @@ export async function getGithubUserSummary(githubToken, options = {}) {
|
|
|
60
71
|
html_url: typeof payload.html_url === "string" ? payload.html_url : null,
|
|
61
72
|
plan_name: typeof payload.plan?.name === "string" ? payload.plan.name : null
|
|
62
73
|
};
|
|
63
|
-
|
|
74
|
+
cache.set(key, { fetchedAt: Date.now(), summary });
|
|
64
75
|
return summary;
|
|
65
76
|
}
|
|
66
77
|
// Non-OK. 401/403/404 are terminal — fast-fail. Other retryable statuses
|
|
@@ -79,7 +90,7 @@ export async function getGithubUserSummary(githubToken, options = {}) {
|
|
|
79
90
|
throw lastError ?? new Error("GitHub user lookup exhausted retries without error context.");
|
|
80
91
|
}
|
|
81
92
|
export function clearGithubUserCache() {
|
|
82
|
-
|
|
93
|
+
cache.clear();
|
|
83
94
|
}
|
|
84
95
|
export class GithubUserFetchError extends Error {
|
|
85
96
|
status;
|