@gnahz77/opencode-copilot-multi-auth 0.1.2 → 0.1.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/README.md +36 -14
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +56 -12
- package/dist/constants.d.ts +10 -2
- package/dist/constants.js +10 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -6
- package/dist/models.js +6 -5
- package/dist/pool.d.ts +3 -0
- package/dist/pool.js +327 -27
- package/dist/types.d.ts +28 -0
- package/dist/usage.js +1 -1
- package/index.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Add the plugin to your `opencode` config:
|
|
|
12
12
|
{
|
|
13
13
|
"$schema": "https://opencode.ai/config.json",
|
|
14
14
|
"plugin": [
|
|
15
|
-
"@gnahz77/opencode-copilot-multi-auth@0.1.
|
|
15
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.4"
|
|
16
16
|
]
|
|
17
17
|
}
|
|
18
18
|
```
|
|
@@ -25,13 +25,16 @@ If you also want the TUI command support provided by this package, add the same
|
|
|
25
25
|
{
|
|
26
26
|
"$schema": "https://opencode.ai/tui.json",
|
|
27
27
|
"plugin": [
|
|
28
|
-
"@gnahz77/opencode-copilot-multi-auth@0.1.
|
|
28
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.4"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
After restarting OpenCode TUI, you can open the command palette and run `copilot-usage`, or type `/copilot-usage`.
|
|
34
|
-
The plugin will open a popup dialog showing all accounts currently
|
|
34
|
+
The plugin will open a popup dialog showing all accounts currently loaded from split local storage:
|
|
35
|
+
|
|
36
|
+
- OAuth/account data: `~/.local/share/opencode/copilot-auth.json`
|
|
37
|
+
- Per-account routing policy: `~/.config/opencode/copilot-auth.json`
|
|
35
38
|
|
|
36
39
|
For local development before publishing, you can load the file directly:
|
|
37
40
|
|
|
@@ -66,7 +69,9 @@ This package now includes a TUI command named `copilot-usage`.
|
|
|
66
69
|
|
|
67
70
|
What it does:
|
|
68
71
|
|
|
69
|
-
- Reads every Copilot account
|
|
72
|
+
- Reads every Copilot account from the merged local pool assembled from:
|
|
73
|
+
- `~/.local/share/opencode/copilot-auth.json` (OAuth/account data)
|
|
74
|
+
- `~/.config/opencode/copilot-auth.json` (routing policy)
|
|
70
75
|
- Calls the GitHub Copilot entitlement endpoint `/copilot_internal/user` for each account using that account's OAuth token
|
|
71
76
|
- Opens a popup dialog showing, for each account:
|
|
72
77
|
- account name
|
|
@@ -147,10 +152,16 @@ The plugin intentionally does not try to change the `opencode` core UI. So the v
|
|
|
147
152
|
|
|
148
153
|
## Multi-Account Support
|
|
149
154
|
|
|
150
|
-
This fork
|
|
155
|
+
This fork keeps Copilot account state in split local files:
|
|
156
|
+
|
|
157
|
+
- OAuth-required account data in `~/.local/share/opencode/copilot-auth.json`
|
|
158
|
+
- Per-account routing policy in `~/.config/opencode/copilot-auth.json`
|
|
159
|
+
|
|
160
|
+
On startup, the plugin automatically migrates legacy single-file storage into this two-file layout.
|
|
161
|
+
|
|
151
162
|
Each successful OAuth login updates that pool automatically: logging in with a new deployment-scoped account appends a new record, and logging in again with the same GitHub identity on the same deployment updates the existing record instead of creating a duplicate.
|
|
152
163
|
|
|
153
|
-
|
|
164
|
+
At runtime, the plugin merges both files into the same in-memory `version: 2` pool shape (`accounts`) used for routing.
|
|
154
165
|
|
|
155
166
|
| Field | Meaning |
|
|
156
167
|
| --- | --- |
|
|
@@ -165,20 +176,14 @@ Automatic routing works on the raw Copilot model IDs that `opencode` already use
|
|
|
165
176
|
|
|
166
177
|
Wildcard matching is case-sensitive and currently supports `*` for zero or more characters anywhere in the pattern. For example, `claude-*` matches `claude-sonnet-4.6` and `claude-opus-4.6`.
|
|
167
178
|
|
|
168
|
-
Example
|
|
179
|
+
Example auth file (`~/.local/share/opencode/copilot-auth.json`):
|
|
169
180
|
|
|
170
181
|
```json
|
|
171
182
|
{
|
|
172
|
-
"version":
|
|
183
|
+
"version": 2,
|
|
173
184
|
"accounts": [
|
|
174
185
|
{
|
|
175
186
|
"key": "github.com:12345678",
|
|
176
|
-
"id": "work",
|
|
177
|
-
"name": "Work",
|
|
178
|
-
"enabled": true,
|
|
179
|
-
"priority": 100,
|
|
180
|
-
"allowlist": ["claude-*"],
|
|
181
|
-
"blocklist": [],
|
|
182
187
|
"deployment": "github.com",
|
|
183
188
|
"domain": "github.com",
|
|
184
189
|
"baseUrl": "https://api.githubcopilot.com",
|
|
@@ -196,3 +201,20 @@ Example pool file:
|
|
|
196
201
|
]
|
|
197
202
|
}
|
|
198
203
|
```
|
|
204
|
+
|
|
205
|
+
Example policy file (`~/.config/opencode/copilot-auth.json`):
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"version": 2,
|
|
210
|
+
"accounts": [
|
|
211
|
+
{
|
|
212
|
+
"key": "github.com:12345678",
|
|
213
|
+
"enabled": true,
|
|
214
|
+
"priority": 100,
|
|
215
|
+
"allowlist": ["claude-*"],
|
|
216
|
+
"blocklist": []
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
```
|
package/dist/auth.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare function getUrls(domain: string): {
|
|
|
3
3
|
DEVICE_CODE_URL: string;
|
|
4
4
|
ACCESS_TOKEN_URL: string;
|
|
5
5
|
COPILOT_ENTITLEMENT_URL: string;
|
|
6
|
+
COPILOT_TOKEN_URL: string;
|
|
6
7
|
};
|
|
7
8
|
export declare function lookupGitHubIdentity(accessToken: string, enterpriseUrl?: string): Promise<{
|
|
8
9
|
login: any;
|
|
@@ -10,7 +11,8 @@ export declare function lookupGitHubIdentity(accessToken: string, enterpriseUrl?
|
|
|
10
11
|
}>;
|
|
11
12
|
export declare function fetchEntitlement(info: AuthInput): Promise<any>;
|
|
12
13
|
export declare function getBaseURL(info: AuthInput): Promise<any>;
|
|
13
|
-
export declare function
|
|
14
|
+
export declare function getCopilotToken(info: AuthInput, forceRefresh?: boolean): Promise<any>;
|
|
15
|
+
export declare function buildHeaders(init: RequestInit | undefined, copilotToken: string, isVision: boolean, isAgent: boolean): {
|
|
14
16
|
[k: string]: string;
|
|
15
17
|
};
|
|
16
18
|
export declare function resolveSelectedPoolAccount(pool: AccountPool, accountKey?: string, requestedRawModelId?: string): PoolAccount | null;
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { API_VERSION } from "./constants.js";
|
|
1
|
+
import { API_VERSION, COPILOT_TOKEN_EXPIRY_BUFFER_MS, DEFAULT_COPILOT_BASE_URL, VSCODE_HEADERS, } from "./constants.js";
|
|
2
2
|
import { getSelectedAccountExpiredError, getSelectedAccountMissingError } from "./errors.js";
|
|
3
|
-
import { readPool, resolveWinnerAccount,
|
|
3
|
+
import { readPool, resolveWinnerAccount, writePoolAuthData } from "./pool.js";
|
|
4
4
|
import { stripRoutingHeaders } from "./routing.js";
|
|
5
5
|
import { applyBaseURLToRequestInput, getConversationMetadata, isValidBaseURL, normalizeDomain, normalizeHeaderObject, } from "./utils.js";
|
|
6
6
|
export function getUrls(domain) {
|
|
@@ -9,8 +9,17 @@ export function getUrls(domain) {
|
|
|
9
9
|
DEVICE_CODE_URL: `https://${domain}/login/device/code`,
|
|
10
10
|
ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
|
|
11
11
|
COPILOT_ENTITLEMENT_URL: `https://${apiDomain}/copilot_internal/user`,
|
|
12
|
+
COPILOT_TOKEN_URL: `https://${apiDomain}/copilot_internal/v2/token`,
|
|
12
13
|
};
|
|
13
14
|
}
|
|
15
|
+
const cachedCopilotTokens = new Map();
|
|
16
|
+
function getCopilotTokenCacheKey(info) {
|
|
17
|
+
const domain = info.enterpriseUrl ? normalizeDomain(info.enterpriseUrl) : "github.com";
|
|
18
|
+
return `${domain}:${info.refresh}`;
|
|
19
|
+
}
|
|
20
|
+
function isReusableCopilotToken(token, expiresAt, now) {
|
|
21
|
+
return Boolean(token && expiresAt && expiresAt - COPILOT_TOKEN_EXPIRY_BUFFER_MS > now);
|
|
22
|
+
}
|
|
14
23
|
export async function lookupGitHubIdentity(accessToken, enterpriseUrl) {
|
|
15
24
|
const deployment = enterpriseUrl ? normalizeDomain(enterpriseUrl) : "github.com";
|
|
16
25
|
const apiDomain = deployment === "github.com" ? "api.github.com" : `api.${deployment}`;
|
|
@@ -42,7 +51,7 @@ export async function fetchEntitlement(info) {
|
|
|
42
51
|
headers: {
|
|
43
52
|
Accept: "application/json",
|
|
44
53
|
Authorization: `Bearer ${info.refresh}`,
|
|
45
|
-
|
|
54
|
+
...VSCODE_HEADERS,
|
|
46
55
|
},
|
|
47
56
|
});
|
|
48
57
|
if (!response.ok) {
|
|
@@ -54,7 +63,41 @@ export async function getBaseURL(info) {
|
|
|
54
63
|
if (info.baseUrl)
|
|
55
64
|
return info.baseUrl;
|
|
56
65
|
const entitlement = await fetchEntitlement(info);
|
|
57
|
-
return entitlement?.endpoints?.api;
|
|
66
|
+
return entitlement?.endpoints?.api ?? DEFAULT_COPILOT_BASE_URL;
|
|
67
|
+
}
|
|
68
|
+
export async function getCopilotToken(info, forceRefresh = false) {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
if (!forceRefresh && isReusableCopilotToken(info.access, info.expires, now)) {
|
|
71
|
+
return info.access;
|
|
72
|
+
}
|
|
73
|
+
const cacheKey = getCopilotTokenCacheKey(info);
|
|
74
|
+
const cachedToken = cachedCopilotTokens.get(cacheKey);
|
|
75
|
+
if (!forceRefresh && cachedToken && isReusableCopilotToken(cachedToken.token, cachedToken.expiresAt, now)) {
|
|
76
|
+
return cachedToken.token;
|
|
77
|
+
}
|
|
78
|
+
const domain = info.enterpriseUrl ? normalizeDomain(info.enterpriseUrl) : "github.com";
|
|
79
|
+
const urls = getUrls(domain);
|
|
80
|
+
const response = await fetch(urls.COPILOT_TOKEN_URL, {
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: "application/json",
|
|
83
|
+
Authorization: `Bearer ${info.refresh}`,
|
|
84
|
+
...VSCODE_HEADERS,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`[opencode-copilot-cli-auth] Copilot token exchange failed: ${response.status}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
const token = typeof data?.token === "string" ? data.token : "";
|
|
92
|
+
const expiresAt = typeof data?.expires_at === "number" ? data.expires_at * 1000 : 0;
|
|
93
|
+
if (!token || !expiresAt) {
|
|
94
|
+
throw new Error("[opencode-copilot-cli-auth] Copilot token exchange returned an invalid payload.");
|
|
95
|
+
}
|
|
96
|
+
cachedCopilotTokens.set(cacheKey, {
|
|
97
|
+
token,
|
|
98
|
+
expiresAt,
|
|
99
|
+
});
|
|
100
|
+
return token;
|
|
58
101
|
}
|
|
59
102
|
function getHeader(headers, name) {
|
|
60
103
|
if (!headers)
|
|
@@ -74,14 +117,13 @@ function getHeader(headers, name) {
|
|
|
74
117
|
}
|
|
75
118
|
return undefined;
|
|
76
119
|
}
|
|
77
|
-
export function buildHeaders(init,
|
|
120
|
+
export function buildHeaders(init, copilotToken, isVision, isAgent) {
|
|
78
121
|
const explicitInitiator = getHeader(init?.headers, "x-initiator");
|
|
79
122
|
const headers = {
|
|
80
123
|
...normalizeHeaderObject(init?.headers),
|
|
81
|
-
Authorization: `Bearer ${
|
|
82
|
-
|
|
124
|
+
Authorization: `Bearer ${copilotToken}`,
|
|
125
|
+
...VSCODE_HEADERS,
|
|
83
126
|
"Openai-Intent": "conversation-agent",
|
|
84
|
-
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
85
127
|
"X-GitHub-Api-Version": API_VERSION,
|
|
86
128
|
"X-Initiator": explicitInitiator ?? (isAgent ? "agent" : "user"),
|
|
87
129
|
"X-Interaction-Id": crypto.randomUUID(),
|
|
@@ -93,6 +135,7 @@ export function buildHeaders(init, info, isVision, isAgent) {
|
|
|
93
135
|
}
|
|
94
136
|
delete headers["x-api-key"];
|
|
95
137
|
delete headers.authorization;
|
|
138
|
+
delete headers["authorization"];
|
|
96
139
|
delete headers["x-initiator"];
|
|
97
140
|
return stripRoutingHeaders(headers);
|
|
98
141
|
}
|
|
@@ -165,13 +208,14 @@ export async function refreshSelectedAccountBaseURL(accountKey) {
|
|
|
165
208
|
}
|
|
166
209
|
: account),
|
|
167
210
|
};
|
|
168
|
-
|
|
211
|
+
writePoolAuthData(updatedPool);
|
|
169
212
|
return updatedPool.accounts[accountIndex];
|
|
170
213
|
}
|
|
171
214
|
export async function fetchWithSelectedAccount(input, init, selectedAccount) {
|
|
172
215
|
const { isVision, isAgent } = getConversationMetadata(init);
|
|
173
|
-
const dispatch = async (account) => {
|
|
174
|
-
const
|
|
216
|
+
const dispatch = async (account, forceRefreshToken = false) => {
|
|
217
|
+
const copilotToken = await getCopilotToken(account.auth, forceRefreshToken);
|
|
218
|
+
const headers = buildHeaders(init, copilotToken, isVision, isAgent);
|
|
175
219
|
const requestInput = applyBaseURLToRequestInput(input, account.baseUrl);
|
|
176
220
|
return fetch(requestInput, {
|
|
177
221
|
...init,
|
|
@@ -187,7 +231,7 @@ export async function fetchWithSelectedAccount(input, init, selectedAccount) {
|
|
|
187
231
|
return response;
|
|
188
232
|
}
|
|
189
233
|
const refreshedAccount = await refreshSelectedAccountBaseURL(activeAccount.key);
|
|
190
|
-
const retryResponse = await dispatch(refreshedAccount);
|
|
234
|
+
const retryResponse = await dispatch(refreshedAccount, true);
|
|
191
235
|
if ([401, 403].includes(retryResponse.status)) {
|
|
192
236
|
throw getSelectedAccountExpiredError(refreshedAccount.key);
|
|
193
237
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
export declare const ACCOUNT_POOL_SCHEMA_VERSION =
|
|
1
|
+
export declare const ACCOUNT_POOL_SCHEMA_VERSION = 2;
|
|
2
2
|
export declare const ROUTING_ACCOUNT_KEY_HEADER = "x-opencode-copilot-account-key";
|
|
3
3
|
export declare const ROUTING_SOURCE_HEADER = "x-opencode-copilot-route-source";
|
|
4
4
|
export declare const INTERNAL_ROUTING_HEADERS: Set<string>;
|
|
5
|
-
export declare const CLIENT_ID = "
|
|
5
|
+
export declare const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
6
6
|
export declare const API_VERSION = "2025-05-01";
|
|
7
7
|
export declare const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
8
8
|
export declare const OAUTH_SCOPES = "read:user read:org repo gist";
|
|
9
|
+
export declare const DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
10
|
+
export declare const COPILOT_TOKEN_EXPIRY_BUFFER_MS: number;
|
|
11
|
+
export declare const VSCODE_HEADERS: {
|
|
12
|
+
readonly "User-Agent": "GitHubCopilotChat/0.38.0";
|
|
13
|
+
readonly "Editor-Version": "vscode/1.110.1";
|
|
14
|
+
readonly "Editor-Plugin-Version": "copilot-chat/0.38.0";
|
|
15
|
+
readonly "Copilot-Integration-Id": "vscode-chat";
|
|
16
|
+
};
|
|
9
17
|
export declare const RESPONSES_API_ALTERNATE_INPUT_TYPES: readonly ["file_search_call", "computer_call", "computer_call_output", "web_search_call", "function_call", "function_call_output", "image_generation_call", "code_interpreter_call", "local_shell_call", "local_shell_call_output", "mcp_list_tools", "mcp_approval_request", "mcp_approval_response", "mcp_call", "reasoning"];
|
package/dist/constants.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
export const ACCOUNT_POOL_SCHEMA_VERSION =
|
|
1
|
+
export const ACCOUNT_POOL_SCHEMA_VERSION = 2;
|
|
2
2
|
export const ROUTING_ACCOUNT_KEY_HEADER = "x-opencode-copilot-account-key";
|
|
3
3
|
export const ROUTING_SOURCE_HEADER = "x-opencode-copilot-route-source";
|
|
4
4
|
export const INTERNAL_ROUTING_HEADERS = new Set([
|
|
5
5
|
ROUTING_ACCOUNT_KEY_HEADER,
|
|
6
6
|
ROUTING_SOURCE_HEADER,
|
|
7
7
|
]);
|
|
8
|
-
export const CLIENT_ID = "
|
|
8
|
+
export const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
9
9
|
export const API_VERSION = "2025-05-01";
|
|
10
10
|
export const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
11
11
|
export const OAUTH_SCOPES = "read:user read:org repo gist";
|
|
12
|
+
export const DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
13
|
+
export const COPILOT_TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
14
|
+
export const VSCODE_HEADERS = {
|
|
15
|
+
"User-Agent": "GitHubCopilotChat/0.38.0",
|
|
16
|
+
"Editor-Version": "vscode/1.110.1",
|
|
17
|
+
"Editor-Plugin-Version": "copilot-chat/0.38.0",
|
|
18
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
19
|
+
};
|
|
12
20
|
export const RESPONSES_API_ALTERNATE_INPUT_TYPES = [
|
|
13
21
|
"file_search_call",
|
|
14
22
|
"computer_call",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { lookupGitHubIdentity } from "./auth.js";
|
|
3
|
-
import { deriveAccountKey, getPoolPath, readPool, resolveWinnerAccount, upsertAccount, writePool } from "./pool.js";
|
|
3
|
+
import { deriveAccountKey, getPolicyPath, getPoolPath, migrateLegacyPoolStorageIfNeeded, readPool, resolveWinnerAccount, upsertAccount, writePool } from "./pool.js";
|
|
4
4
|
import { injectRoutingHeaders, stripRoutingHeaders } from "./routing.js";
|
|
5
|
-
export { getPoolPath, readPool, writePool, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount };
|
|
5
|
+
export { getPoolPath, getPolicyPath, readPool, writePool, migrateLegacyPoolStorageIfNeeded, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount, };
|
|
6
6
|
export { injectRoutingHeaders, stripRoutingHeaders };
|
|
7
7
|
export declare const CopilotAuthPlugin: Plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { CLIENT_ID, OAUTH_POLLING_SAFETY_MARGIN_MS, OAUTH_SCOPES, ROUTING_ACCOUNT_KEY_HEADER, ROUTING_SOURCE_HEADER, } from "./constants.js";
|
|
2
|
-
import { buildHeaders, fetchEntitlement, fetchWithSelectedAccount, getBaseURL, getUrls, lookupGitHubIdentity, resolveSelectedPoolAccount, } from "./auth.js";
|
|
2
|
+
import { buildHeaders, fetchEntitlement, fetchWithSelectedAccount, getBaseURL, getCopilotToken, getUrls, lookupGitHubIdentity, resolveSelectedPoolAccount, } from "./auth.js";
|
|
3
3
|
import { buildPoolBackedModels, normalizeExistingModels, resolveProviderModels } from "./models.js";
|
|
4
|
-
import { deriveAccountKey, getPoolPath, readPool, resolveWinnerAccount, upsertAccount, writePool, } from "./pool.js";
|
|
4
|
+
import { deriveAccountKey, getPolicyPath, getPoolPath, migrateLegacyPoolStorageIfNeeded, readPool, resolveWinnerAccount, upsertAccount, writePool, } from "./pool.js";
|
|
5
5
|
import { injectRoutingHeaders, stripRoutingHeaders } from "./routing.js";
|
|
6
|
-
import { getHeader, getConversationMetadata, getRequestedRawModelId, normalizeDomain, resolveClaudeThinkingBudget, } from "./utils.js";
|
|
7
|
-
export { getPoolPath, readPool, writePool, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount };
|
|
6
|
+
import { applyBaseURLToRequestInput, getHeader, getConversationMetadata, getRequestedRawModelId, normalizeDomain, resolveClaudeThinkingBudget, } from "./utils.js";
|
|
7
|
+
export { getPoolPath, getPolicyPath, readPool, writePool, migrateLegacyPoolStorageIfNeeded, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount, };
|
|
8
8
|
export { injectRoutingHeaders, stripRoutingHeaders };
|
|
9
9
|
export const CopilotAuthPlugin = async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
migrateLegacyPoolStorageIfNeeded();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.warn("[opencode-copilot-cli-auth] Failed to run pool storage migration:", error instanceof Error ? error.message : String(error));
|
|
15
|
+
}
|
|
10
16
|
return {
|
|
11
17
|
provider: {
|
|
12
18
|
id: "github-copilot",
|
|
@@ -69,8 +75,10 @@ export const CopilotAuthPlugin = async (input) => {
|
|
|
69
75
|
return fetch(inputRequest, init);
|
|
70
76
|
}
|
|
71
77
|
const { isVision, isAgent } = getConversationMetadata(init);
|
|
72
|
-
const
|
|
73
|
-
|
|
78
|
+
const copilotToken = await getCopilotToken(auth);
|
|
79
|
+
const headers = buildHeaders(init, copilotToken, isVision, isAgent);
|
|
80
|
+
const requestInput = applyBaseURLToRequestInput(inputRequest, baseURL);
|
|
81
|
+
return fetch(requestInput, {
|
|
74
82
|
...init,
|
|
75
83
|
headers,
|
|
76
84
|
});
|
package/dist/models.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { getBaseURL } from "./auth.js";
|
|
1
|
+
import { getBaseURL, getCopilotToken } from "./auth.js";
|
|
2
|
+
import { API_VERSION, VSCODE_HEADERS } from "./constants.js";
|
|
2
3
|
import { resolveWinnerAccount } from "./pool.js";
|
|
3
4
|
import { getReleaseDate, isPickerModel, zeroCost } from "./utils.js";
|
|
4
5
|
export async function fetchModels(info, baseURL) {
|
|
6
|
+
const copilotToken = await getCopilotToken(info);
|
|
5
7
|
const response = await fetch(`${baseURL}/models`, {
|
|
6
8
|
headers: {
|
|
7
|
-
Authorization: `Bearer ${
|
|
8
|
-
|
|
9
|
+
Authorization: `Bearer ${copilotToken}`,
|
|
10
|
+
...VSCODE_HEADERS,
|
|
9
11
|
"Openai-Intent": "model-access",
|
|
10
|
-
"
|
|
11
|
-
"X-GitHub-Api-Version": "2025-05-01",
|
|
12
|
+
"X-GitHub-Api-Version": API_VERSION,
|
|
12
13
|
"X-Interaction-Type": "model-access",
|
|
13
14
|
"X-Request-Id": crypto.randomUUID(),
|
|
14
15
|
},
|
package/dist/pool.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { AccountPool, PoolAccount, UpsertAccountData } from "./types.js";
|
|
2
2
|
export declare function getPoolPath(): string;
|
|
3
|
+
export declare function getPolicyPath(): string;
|
|
3
4
|
export declare function validatePoolSchema(pool: unknown, context: string): AccountPool;
|
|
4
5
|
export declare function readPool(): AccountPool;
|
|
5
6
|
export declare function writePool(pool: AccountPool): void;
|
|
7
|
+
export declare function writePoolAuthData(pool: AccountPool): void;
|
|
8
|
+
export declare function migrateLegacyPoolStorageIfNeeded(): void;
|
|
6
9
|
export declare function deriveAccountKey(deployment: string, userId: number): string;
|
|
7
10
|
export declare function upsertAccount(pool: AccountPool, accountData: UpsertAccountData): AccountPool;
|
|
8
11
|
export declare function resolveWinnerAccount(rawModelId: string, pool: AccountPool): PoolAccount;
|
package/dist/pool.js
CHANGED
|
@@ -6,44 +6,344 @@ import { matchesAnyModelIdPattern, normalizeDomain, normalizeIdSource, normalize
|
|
|
6
6
|
export function getPoolPath() {
|
|
7
7
|
return `${homedir()}/.local/share/opencode/copilot-auth.json`;
|
|
8
8
|
}
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
export function getPolicyPath() {
|
|
10
|
+
return `${homedir()}/.config/opencode/copilot-auth.json`;
|
|
11
|
+
}
|
|
12
|
+
function assertObject(value, context) {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected object.`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function assertString(value, context) {
|
|
19
|
+
if (typeof value !== "string") {
|
|
20
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected string.`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function assertNumber(value, context) {
|
|
25
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
26
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected finite number.`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function assertBoolean(value, context) {
|
|
31
|
+
if (typeof value !== "boolean") {
|
|
32
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected boolean.`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function assertNullableString(value, context) {
|
|
37
|
+
if (value === null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return assertString(value, context);
|
|
41
|
+
}
|
|
42
|
+
function assertStringArray(value, context) {
|
|
43
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
44
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected string[].`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function parsePoolIdentity(value, context) {
|
|
49
|
+
const identityObject = assertObject(value, context);
|
|
50
|
+
return {
|
|
51
|
+
login: assertString(identityObject.login, `${context}.login`),
|
|
52
|
+
userId: assertNumber(identityObject.userId, `${context}.userId`),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parseOAuthAuth(value, context) {
|
|
56
|
+
const authObject = assertObject(value, context);
|
|
57
|
+
if (authObject.type !== "oauth") {
|
|
58
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}.type: expected \"oauth\".`);
|
|
59
|
+
}
|
|
60
|
+
const parsed = {
|
|
61
|
+
type: "oauth",
|
|
62
|
+
refresh: assertString(authObject.refresh, `${context}.refresh`),
|
|
63
|
+
};
|
|
64
|
+
if (typeof authObject.access === "string") {
|
|
65
|
+
parsed.access = authObject.access;
|
|
66
|
+
}
|
|
67
|
+
if (typeof authObject.expires === "number" && Number.isFinite(authObject.expires)) {
|
|
68
|
+
parsed.expires = authObject.expires;
|
|
69
|
+
}
|
|
70
|
+
if (authObject.baseUrl === null || typeof authObject.baseUrl === "string") {
|
|
71
|
+
parsed.baseUrl = authObject.baseUrl;
|
|
72
|
+
}
|
|
73
|
+
if (typeof authObject.provider === "string") {
|
|
74
|
+
parsed.provider = authObject.provider;
|
|
75
|
+
}
|
|
76
|
+
if (typeof authObject.enterpriseUrl === "string") {
|
|
77
|
+
parsed.enterpriseUrl = authObject.enterpriseUrl;
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
function parseAuthPoolAccount(value, context) {
|
|
82
|
+
const account = assertObject(value, context);
|
|
83
|
+
return {
|
|
84
|
+
key: assertString(account.key, `${context}.key`),
|
|
85
|
+
deployment: assertString(account.deployment, `${context}.deployment`),
|
|
86
|
+
domain: assertString(account.domain, `${context}.domain`),
|
|
87
|
+
identity: parsePoolIdentity(account.identity, `${context}.identity`),
|
|
88
|
+
enterpriseUrl: assertNullableString(account.enterpriseUrl, `${context}.enterpriseUrl`),
|
|
89
|
+
baseUrl: assertNullableString(account.baseUrl, `${context}.baseUrl`),
|
|
90
|
+
auth: parseOAuthAuth(account.auth, `${context}.auth`),
|
|
91
|
+
createdAt: assertString(account.createdAt, `${context}.createdAt`),
|
|
92
|
+
updatedAt: assertString(account.updatedAt, `${context}.updatedAt`),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parsePolicyPoolAccount(value, context) {
|
|
96
|
+
const account = assertObject(value, context);
|
|
97
|
+
return {
|
|
98
|
+
key: assertString(account.key, `${context}.key`),
|
|
99
|
+
enabled: assertBoolean(account.enabled, `${context}.enabled`),
|
|
100
|
+
priority: assertNumber(account.priority, `${context}.priority`),
|
|
101
|
+
allowlist: assertStringArray(account.allowlist, `${context}.allowlist`),
|
|
102
|
+
blocklist: assertStringArray(account.blocklist, `${context}.blocklist`),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function parseVersionedAccountsDocument(value, context) {
|
|
106
|
+
const document = assertObject(value, context);
|
|
107
|
+
if (document.version !== ACCOUNT_POOL_SCHEMA_VERSION || !Array.isArray(document.accounts)) {
|
|
14
108
|
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected { version: ${ACCOUNT_POOL_SCHEMA_VERSION}, accounts: [] } schema.`);
|
|
15
109
|
}
|
|
16
|
-
return
|
|
110
|
+
return {
|
|
111
|
+
version: document.version,
|
|
112
|
+
accounts: document.accounts,
|
|
113
|
+
};
|
|
17
114
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
23
|
-
accounts: [],
|
|
24
|
-
};
|
|
25
|
-
writePool(defaultPool);
|
|
26
|
-
return defaultPool;
|
|
115
|
+
function parseLegacyVersionedAccountsDocument(value, context) {
|
|
116
|
+
const document = assertObject(value, context);
|
|
117
|
+
if (document.version !== 1 || !Array.isArray(document.accounts)) {
|
|
118
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected legacy { version: 1, accounts: [] } schema.`);
|
|
27
119
|
}
|
|
28
|
-
|
|
29
|
-
|
|
120
|
+
return {
|
|
121
|
+
version: document.version,
|
|
122
|
+
accounts: document.accounts,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function validateAuthPoolSchema(pool, context) {
|
|
126
|
+
const parsed = parseVersionedAccountsDocument(pool, context);
|
|
127
|
+
return {
|
|
128
|
+
version: parsed.version,
|
|
129
|
+
accounts: parsed.accounts.map((account, index) => parseAuthPoolAccount(account, `${context}.accounts[${index}]`)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function validatePolicyPoolSchema(pool, context) {
|
|
133
|
+
const parsed = parseVersionedAccountsDocument(pool, context);
|
|
134
|
+
return {
|
|
135
|
+
version: parsed.version,
|
|
136
|
+
accounts: parsed.accounts.map((account, index) => parsePolicyPoolAccount(account, `${context}.accounts[${index}]`)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function validateLegacyPoolSchema(pool, context) {
|
|
140
|
+
const parsed = parseLegacyVersionedAccountsDocument(pool, context);
|
|
141
|
+
return {
|
|
142
|
+
version: parsed.version,
|
|
143
|
+
accounts: parsed.accounts.map((account, index) => {
|
|
144
|
+
const accountObject = assertObject(account, `${context}.accounts[${index}]`);
|
|
145
|
+
return {
|
|
146
|
+
key: assertString(accountObject.key, `${context}.accounts[${index}].key`),
|
|
147
|
+
id: assertString(accountObject.id, `${context}.accounts[${index}].id`),
|
|
148
|
+
name: assertString(accountObject.name, `${context}.accounts[${index}].name`),
|
|
149
|
+
enabled: assertBoolean(accountObject.enabled, `${context}.accounts[${index}].enabled`),
|
|
150
|
+
priority: assertNumber(accountObject.priority, `${context}.accounts[${index}].priority`),
|
|
151
|
+
deployment: assertString(accountObject.deployment, `${context}.accounts[${index}].deployment`),
|
|
152
|
+
domain: assertString(accountObject.domain, `${context}.accounts[${index}].domain`),
|
|
153
|
+
identity: parsePoolIdentity(accountObject.identity, `${context}.accounts[${index}].identity`),
|
|
154
|
+
enterpriseUrl: assertNullableString(accountObject.enterpriseUrl, `${context}.accounts[${index}].enterpriseUrl`),
|
|
155
|
+
baseUrl: assertNullableString(accountObject.baseUrl, `${context}.accounts[${index}].baseUrl`),
|
|
156
|
+
allowlist: assertStringArray(accountObject.allowlist, `${context}.accounts[${index}].allowlist`),
|
|
157
|
+
blocklist: assertStringArray(accountObject.blocklist, `${context}.accounts[${index}].blocklist`),
|
|
158
|
+
auth: parseOAuthAuth(accountObject.auth, `${context}.accounts[${index}].auth`),
|
|
159
|
+
createdAt: assertString(accountObject.createdAt, `${context}.accounts[${index}].createdAt`),
|
|
160
|
+
updatedAt: assertString(accountObject.updatedAt, `${context}.accounts[${index}].updatedAt`),
|
|
161
|
+
};
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function toDefaultPolicyAccount(account) {
|
|
166
|
+
return {
|
|
167
|
+
key: account.key,
|
|
168
|
+
enabled: true,
|
|
169
|
+
priority: 0,
|
|
170
|
+
allowlist: [],
|
|
171
|
+
blocklist: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function parseJsonFile(filePath, context) {
|
|
175
|
+
const raw = readFileSync(filePath, "utf8");
|
|
30
176
|
try {
|
|
31
|
-
|
|
177
|
+
return JSON.parse(raw);
|
|
32
178
|
}
|
|
33
179
|
catch (error) {
|
|
34
|
-
throw new Error(`[opencode-copilot-cli-auth] Malformed JSON in
|
|
180
|
+
throw new Error(`[opencode-copilot-cli-auth] Malformed JSON in ${context} at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function writeJsonAtomic(filePath, payload) {
|
|
184
|
+
const directoryPath = dirname(filePath);
|
|
185
|
+
const tmpPath = `${filePath}.tmp`;
|
|
186
|
+
mkdirSync(directoryPath, { recursive: true });
|
|
187
|
+
writeFileSync(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
188
|
+
renameSync(tmpPath, filePath);
|
|
189
|
+
chmodSync(filePath, 0o600);
|
|
190
|
+
}
|
|
191
|
+
function buildAuthPoolDocument(pool) {
|
|
192
|
+
return {
|
|
193
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
194
|
+
accounts: pool.accounts.map((account) => ({
|
|
195
|
+
key: account.key,
|
|
196
|
+
deployment: account.deployment,
|
|
197
|
+
domain: account.domain,
|
|
198
|
+
identity: account.identity,
|
|
199
|
+
enterpriseUrl: account.enterpriseUrl,
|
|
200
|
+
baseUrl: account.baseUrl,
|
|
201
|
+
auth: account.auth,
|
|
202
|
+
createdAt: account.createdAt,
|
|
203
|
+
updatedAt: account.updatedAt,
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function buildPolicyPoolDocument(pool) {
|
|
208
|
+
return {
|
|
209
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
210
|
+
accounts: pool.accounts.map((account) => ({
|
|
211
|
+
key: account.key,
|
|
212
|
+
enabled: account.enabled,
|
|
213
|
+
priority: account.priority,
|
|
214
|
+
allowlist: account.allowlist,
|
|
215
|
+
blocklist: account.blocklist,
|
|
216
|
+
})),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function mergePoolDocuments(authPool, policyPool) {
|
|
220
|
+
const policyByKey = new Map(policyPool.accounts.map((account) => [account.key, account]));
|
|
221
|
+
const merged = {
|
|
222
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
223
|
+
accounts: authPool.accounts.map((authAccount) => {
|
|
224
|
+
const policy = policyByKey.get(authAccount.key) ?? toDefaultPolicyAccount(authAccount);
|
|
225
|
+
const userIdText = String(authAccount.identity.userId);
|
|
226
|
+
const fallbackId = normalizeIdSource(authAccount.identity.login || userIdText) || userIdText;
|
|
227
|
+
const fallbackName = authAccount.identity.login || userIdText;
|
|
228
|
+
return {
|
|
229
|
+
key: authAccount.key,
|
|
230
|
+
id: fallbackId,
|
|
231
|
+
name: fallbackName,
|
|
232
|
+
enabled: policy.enabled,
|
|
233
|
+
priority: policy.priority,
|
|
234
|
+
deployment: authAccount.deployment,
|
|
235
|
+
domain: authAccount.domain,
|
|
236
|
+
identity: authAccount.identity,
|
|
237
|
+
enterpriseUrl: authAccount.enterpriseUrl,
|
|
238
|
+
baseUrl: authAccount.baseUrl,
|
|
239
|
+
allowlist: policy.allowlist,
|
|
240
|
+
blocklist: policy.blocklist,
|
|
241
|
+
auth: authAccount.auth,
|
|
242
|
+
createdAt: authAccount.createdAt,
|
|
243
|
+
updatedAt: authAccount.updatedAt,
|
|
244
|
+
};
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
return validatePoolSchema(merged, "merged account pool");
|
|
248
|
+
}
|
|
249
|
+
export function validatePoolSchema(pool, context) {
|
|
250
|
+
const parsed = parseVersionedAccountsDocument(pool, context);
|
|
251
|
+
return {
|
|
252
|
+
version: parsed.version,
|
|
253
|
+
accounts: parsed.accounts.map((account, index) => {
|
|
254
|
+
const accountObject = assertObject(account, `${context}.accounts[${index}]`);
|
|
255
|
+
return {
|
|
256
|
+
key: assertString(accountObject.key, `${context}.accounts[${index}].key`),
|
|
257
|
+
id: assertString(accountObject.id, `${context}.accounts[${index}].id`),
|
|
258
|
+
name: assertString(accountObject.name, `${context}.accounts[${index}].name`),
|
|
259
|
+
enabled: assertBoolean(accountObject.enabled, `${context}.accounts[${index}].enabled`),
|
|
260
|
+
priority: assertNumber(accountObject.priority, `${context}.accounts[${index}].priority`),
|
|
261
|
+
deployment: assertString(accountObject.deployment, `${context}.accounts[${index}].deployment`),
|
|
262
|
+
domain: assertString(accountObject.domain, `${context}.accounts[${index}].domain`),
|
|
263
|
+
identity: parsePoolIdentity(accountObject.identity, `${context}.accounts[${index}].identity`),
|
|
264
|
+
enterpriseUrl: assertNullableString(accountObject.enterpriseUrl, `${context}.accounts[${index}].enterpriseUrl`),
|
|
265
|
+
baseUrl: assertNullableString(accountObject.baseUrl, `${context}.accounts[${index}].baseUrl`),
|
|
266
|
+
allowlist: assertStringArray(accountObject.allowlist, `${context}.accounts[${index}].allowlist`),
|
|
267
|
+
blocklist: assertStringArray(accountObject.blocklist, `${context}.accounts[${index}].blocklist`),
|
|
268
|
+
auth: parseOAuthAuth(accountObject.auth, `${context}.accounts[${index}].auth`),
|
|
269
|
+
createdAt: assertString(accountObject.createdAt, `${context}.accounts[${index}].createdAt`),
|
|
270
|
+
updatedAt: assertString(accountObject.updatedAt, `${context}.accounts[${index}].updatedAt`),
|
|
271
|
+
};
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function readPool() {
|
|
276
|
+
const authPath = getPoolPath();
|
|
277
|
+
const policyPath = getPolicyPath();
|
|
278
|
+
const defaultDocument = {
|
|
279
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
280
|
+
accounts: [],
|
|
281
|
+
};
|
|
282
|
+
if (!existsSync(authPath)) {
|
|
283
|
+
writeJsonAtomic(authPath, defaultDocument);
|
|
35
284
|
}
|
|
36
|
-
|
|
285
|
+
if (!existsSync(policyPath)) {
|
|
286
|
+
writeJsonAtomic(policyPath, defaultDocument);
|
|
287
|
+
}
|
|
288
|
+
const parsedAuth = parseJsonFile(authPath, "Copilot auth file");
|
|
289
|
+
const parsedPolicy = parseJsonFile(policyPath, "Copilot routing policy file");
|
|
290
|
+
const authPool = validateAuthPoolSchema(parsedAuth, `auth pool file at ${authPath}`);
|
|
291
|
+
const policyPool = validatePolicyPoolSchema(parsedPolicy, `policy pool file at ${policyPath}`);
|
|
292
|
+
return mergePoolDocuments(authPool, policyPool);
|
|
37
293
|
}
|
|
38
294
|
export function writePool(pool) {
|
|
39
295
|
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
296
|
+
const authDocument = validateAuthPoolSchema(buildAuthPoolDocument(validatedPool), "auth pool payload");
|
|
297
|
+
const policyDocument = validatePolicyPoolSchema(buildPolicyPoolDocument(validatedPool), "policy pool payload");
|
|
298
|
+
writeJsonAtomic(getPolicyPath(), policyDocument);
|
|
299
|
+
writeJsonAtomic(getPoolPath(), authDocument);
|
|
300
|
+
}
|
|
301
|
+
export function writePoolAuthData(pool) {
|
|
302
|
+
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
303
|
+
const authDocument = validateAuthPoolSchema(buildAuthPoolDocument(validatedPool), "auth pool payload");
|
|
304
|
+
writeJsonAtomic(getPoolPath(), authDocument);
|
|
305
|
+
}
|
|
306
|
+
export function migrateLegacyPoolStorageIfNeeded() {
|
|
307
|
+
const authPath = getPoolPath();
|
|
308
|
+
const policyPath = getPolicyPath();
|
|
309
|
+
if (!existsSync(authPath)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const parsed = parseJsonFile(authPath, "Copilot account pool file");
|
|
313
|
+
try {
|
|
314
|
+
const legacyPool = validateLegacyPoolSchema(parsed, `legacy account pool file at ${authPath}`);
|
|
315
|
+
const existingPolicy = existsSync(policyPath)
|
|
316
|
+
? validatePolicyPoolSchema(parseJsonFile(policyPath, "Copilot routing policy file"), `policy pool file at ${policyPath}`)
|
|
317
|
+
: null;
|
|
318
|
+
const migratedPolicy = {
|
|
319
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
320
|
+
accounts: legacyPool.accounts.map((account) => {
|
|
321
|
+
const existingPolicyAccount = existingPolicy?.accounts.find((candidate) => candidate.key === account.key);
|
|
322
|
+
return existingPolicyAccount ?? {
|
|
323
|
+
key: account.key,
|
|
324
|
+
enabled: account.enabled,
|
|
325
|
+
priority: account.priority,
|
|
326
|
+
allowlist: account.allowlist,
|
|
327
|
+
blocklist: account.blocklist,
|
|
328
|
+
};
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
writeJsonAtomic(policyPath, validatePolicyPoolSchema(migratedPolicy, "migrated policy pool payload"));
|
|
332
|
+
writeJsonAtomic(authPath, validateAuthPoolSchema(buildAuthPoolDocument(legacyPool), "migrated auth pool payload"));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Intentionally continue: file may already be auth-only format or a non-legacy document.
|
|
337
|
+
}
|
|
338
|
+
if (existsSync(policyPath)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const authPool = validateAuthPoolSchema(parsed, `auth pool file at ${authPath}`);
|
|
342
|
+
const defaultPolicyPool = {
|
|
343
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
344
|
+
accounts: authPool.accounts.map((account) => toDefaultPolicyAccount(account)),
|
|
345
|
+
};
|
|
346
|
+
writeJsonAtomic(policyPath, validatePolicyPoolSchema(defaultPolicyPool, "default policy pool payload"));
|
|
47
347
|
}
|
|
48
348
|
function deriveDefaultAccountId(accounts, key, identity) {
|
|
49
349
|
const userIdText = String(identity.userId);
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,24 @@ export interface PoolAccount {
|
|
|
28
28
|
createdAt: string;
|
|
29
29
|
updatedAt: string;
|
|
30
30
|
}
|
|
31
|
+
export interface AuthPoolAccount {
|
|
32
|
+
key: string;
|
|
33
|
+
deployment: string;
|
|
34
|
+
domain: string;
|
|
35
|
+
identity: PoolIdentity;
|
|
36
|
+
enterpriseUrl: string | null;
|
|
37
|
+
baseUrl: string | null;
|
|
38
|
+
auth: OAuthAuth;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PolicyPoolAccount {
|
|
43
|
+
key: string;
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
priority: number;
|
|
46
|
+
allowlist: string[];
|
|
47
|
+
blocklist: string[];
|
|
48
|
+
}
|
|
31
49
|
export interface UpsertAccountData {
|
|
32
50
|
key: string;
|
|
33
51
|
deployment?: string;
|
|
@@ -42,6 +60,14 @@ export interface AccountPool {
|
|
|
42
60
|
version: number;
|
|
43
61
|
accounts: PoolAccount[];
|
|
44
62
|
}
|
|
63
|
+
export interface AuthPoolDocument {
|
|
64
|
+
version: number;
|
|
65
|
+
accounts: AuthPoolAccount[];
|
|
66
|
+
}
|
|
67
|
+
export interface PolicyPoolDocument {
|
|
68
|
+
version: number;
|
|
69
|
+
accounts: PolicyPoolAccount[];
|
|
70
|
+
}
|
|
45
71
|
export interface LiveModel {
|
|
46
72
|
id: string;
|
|
47
73
|
name?: string;
|
|
@@ -111,6 +137,8 @@ export interface ProviderModel {
|
|
|
111
137
|
export type HeaderObject = Headers | Array<[string, string]> | Record<string, string> | undefined;
|
|
112
138
|
export interface AuthInput {
|
|
113
139
|
refresh: string;
|
|
140
|
+
access?: string;
|
|
141
|
+
expires?: number;
|
|
114
142
|
enterpriseUrl?: string | null;
|
|
115
143
|
baseUrl?: string | null;
|
|
116
144
|
type?: string;
|
package/dist/usage.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function getCopilotUsageDialogMessage() {
|
|
|
106
106
|
const pool = readPool();
|
|
107
107
|
const accounts = [...pool.accounts].sort((left, right) => left.key.localeCompare(right.key));
|
|
108
108
|
if (accounts.length === 0) {
|
|
109
|
-
throw new Error("No Copilot accounts found in the account pool. Log in again to populate ~/.local/share/opencode/copilot-auth.json.");
|
|
109
|
+
throw new Error("No Copilot accounts found in the account pool. Log in again to populate ~/.local/share/opencode/copilot-auth.json (auth) and ~/.config/opencode/copilot-auth.json (policy).");
|
|
110
110
|
}
|
|
111
111
|
const sections = await Promise.all(accounts.map((account) => getCopilotUsageSummary(account)));
|
|
112
112
|
return sections.join("\n\n");
|
package/index.mjs
CHANGED