@oevortex/opencode-qwen-auth 0.1.0 → 0.1.2
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/index.cjs +1600 -0
- package/dist/index.d.cts +445 -0
- package/dist/index.d.ts +445 -0
- package/dist/index.js +1534 -0
- package/package.json +14 -6
- package/index.ts +0 -82
- package/src/constants.ts +0 -43
- package/src/global.d.ts +0 -257
- package/src/models.ts +0 -148
- package/src/plugin/auth.ts +0 -151
- package/src/plugin/browser.ts +0 -126
- package/src/plugin/fetch-wrapper.ts +0 -460
- package/src/plugin/logger.ts +0 -111
- package/src/plugin/server.ts +0 -364
- package/src/plugin/token.ts +0 -225
- package/src/plugin.ts +0 -444
- package/src/qwen/oauth.ts +0 -271
- package/src/qwen/thinking-parser.ts +0 -190
- package/src/types.ts +0 -292
package/src/plugin/auth.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth utilities for the Qwen OpenCode plugin
|
|
3
|
-
* Based on opencode-google-antigravity-auth/src/plugin/auth.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AuthDetails, OAuthAuthDetails, RefreshParts } from "../types";
|
|
7
|
-
|
|
8
|
-
/** Buffer time before token expiry to trigger refresh (60 seconds) */
|
|
9
|
-
const ACCESS_TOKEN_EXPIRY_BUFFER_MS = 60 * 1000;
|
|
10
|
-
|
|
11
|
-
/** Separator for multi-part refresh tokens */
|
|
12
|
-
const PARTS_SEPARATOR = "|";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Type guard to check if auth details are OAuth type.
|
|
16
|
-
*/
|
|
17
|
-
export function isOAuthAuth(auth: AuthDetails): auth is OAuthAuthDetails {
|
|
18
|
-
return auth.type === "oauth";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Type guard to check if auth details are API key type.
|
|
23
|
-
*/
|
|
24
|
-
export function isApiAuth(auth: AuthDetails): auth is { type: "api"; apiKey: string } {
|
|
25
|
-
return auth.type === "api";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Parse refresh token string into its constituent parts.
|
|
30
|
-
* Format: "refreshToken|resourceUrl"
|
|
31
|
-
*/
|
|
32
|
-
export function parseRefreshParts(refresh: string): RefreshParts {
|
|
33
|
-
const [refreshToken = "", resourceUrl = ""] = (refresh ?? "").split(PARTS_SEPARATOR);
|
|
34
|
-
return {
|
|
35
|
-
refreshToken,
|
|
36
|
-
resourceUrl: resourceUrl || undefined,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Serialize refresh parts back into stored string format.
|
|
42
|
-
*/
|
|
43
|
-
export function formatRefreshParts(parts: RefreshParts): string {
|
|
44
|
-
const base = parts.refreshToken;
|
|
45
|
-
return parts.resourceUrl ? `${base}${PARTS_SEPARATOR}${parts.resourceUrl}` : base;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Check if an access token is expired or missing.
|
|
50
|
-
* Includes a buffer for clock skew to trigger refresh slightly early.
|
|
51
|
-
*/
|
|
52
|
-
export function accessTokenExpired(auth: OAuthAuthDetails): boolean {
|
|
53
|
-
if (!auth.access || typeof auth.expires !== "number") {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Convert plugin auth details to Qwen OAuth credentials format.
|
|
61
|
-
*/
|
|
62
|
-
export function authToCredentials(auth: OAuthAuthDetails): {
|
|
63
|
-
access_token: string;
|
|
64
|
-
refresh_token: string;
|
|
65
|
-
expiry_date: number;
|
|
66
|
-
resource_url?: string;
|
|
67
|
-
} {
|
|
68
|
-
const parts = parseRefreshParts(auth.refresh);
|
|
69
|
-
return {
|
|
70
|
-
access_token: auth.access || "",
|
|
71
|
-
refresh_token: parts.refreshToken,
|
|
72
|
-
expiry_date: auth.expires || 0,
|
|
73
|
-
resource_url: parts.resourceUrl,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Convert Qwen OAuth credentials to plugin auth details format.
|
|
79
|
-
*/
|
|
80
|
-
export function credentialsToAuth(credentials: {
|
|
81
|
-
access_token: string;
|
|
82
|
-
refresh_token: string;
|
|
83
|
-
expiry_date: number;
|
|
84
|
-
resource_url?: string;
|
|
85
|
-
}): OAuthAuthDetails {
|
|
86
|
-
const parts: RefreshParts = {
|
|
87
|
-
refreshToken: credentials.refresh_token,
|
|
88
|
-
resourceUrl: credentials.resource_url,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
type: "oauth",
|
|
93
|
-
access: credentials.access_token,
|
|
94
|
-
refresh: formatRefreshParts(parts),
|
|
95
|
-
expires: credentials.expiry_date,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Create auth details object for storage.
|
|
101
|
-
*/
|
|
102
|
-
export function createAuthDetails(
|
|
103
|
-
accessToken: string,
|
|
104
|
-
refreshToken: string,
|
|
105
|
-
expiresAt: number,
|
|
106
|
-
resourceUrl?: string
|
|
107
|
-
): OAuthAuthDetails {
|
|
108
|
-
const parts: RefreshParts = {
|
|
109
|
-
refreshToken,
|
|
110
|
-
resourceUrl,
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
type: "oauth",
|
|
115
|
-
access: accessToken,
|
|
116
|
-
refresh: formatRefreshParts(parts),
|
|
117
|
-
expires: expiresAt,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Validate that auth details have required fields.
|
|
123
|
-
*/
|
|
124
|
-
export function isValidAuth(auth: AuthDetails): boolean {
|
|
125
|
-
if (isOAuthAuth(auth)) {
|
|
126
|
-
return Boolean(auth.refresh);
|
|
127
|
-
}
|
|
128
|
-
if (isApiAuth(auth)) {
|
|
129
|
-
return Boolean(auth.apiKey);
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Get a human-readable description of auth status.
|
|
136
|
-
*/
|
|
137
|
-
export function getAuthStatusDescription(auth: AuthDetails): string {
|
|
138
|
-
if (isOAuthAuth(auth)) {
|
|
139
|
-
if (!auth.access) {
|
|
140
|
-
return "OAuth configured but no access token";
|
|
141
|
-
}
|
|
142
|
-
if (accessTokenExpired(auth)) {
|
|
143
|
-
return "OAuth access token expired (will refresh)";
|
|
144
|
-
}
|
|
145
|
-
return "OAuth authenticated";
|
|
146
|
-
}
|
|
147
|
-
if (isApiAuth(auth)) {
|
|
148
|
-
return "API key configured";
|
|
149
|
-
}
|
|
150
|
-
return "Unknown auth type";
|
|
151
|
-
}
|
package/src/plugin/browser.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser utility for opening URLs
|
|
3
|
-
* Opens OAuth URLs in the user's default browser
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createLogger } from "./logger";
|
|
7
|
-
|
|
8
|
-
const log = createLogger("browser");
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Open a URL in the user's default browser.
|
|
12
|
-
*
|
|
13
|
-
* Uses the 'open' npm package which handles cross-platform browser opening.
|
|
14
|
-
*
|
|
15
|
-
* @param url - URL to open
|
|
16
|
-
* @returns Promise that resolves when the browser is launched
|
|
17
|
-
*/
|
|
18
|
-
export async function openBrowser(url: string): Promise<void> {
|
|
19
|
-
log.debug("Opening browser", { url: url.slice(0, 100) + "..." });
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
// Dynamic import of 'open' package
|
|
23
|
-
const open = await import("open");
|
|
24
|
-
await open.default(url);
|
|
25
|
-
log.info("Browser opened successfully");
|
|
26
|
-
} catch (error) {
|
|
27
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
28
|
-
log.error("Failed to open browser", { error: errorMessage });
|
|
29
|
-
throw new Error(`Failed to open browser: ${errorMessage}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Check if we're running in a headless environment where browser opening won't work.
|
|
35
|
-
*
|
|
36
|
-
* Detects SSH sessions, containers, CI environments, etc.
|
|
37
|
-
*
|
|
38
|
-
* @returns True if running in headless mode
|
|
39
|
-
*/
|
|
40
|
-
export function isHeadless(): boolean {
|
|
41
|
-
// Check for SSH session
|
|
42
|
-
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) {
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Check for explicit headless flag
|
|
47
|
-
if (process.env.OPENCODE_HEADLESS === "1" || process.env.OPENCODE_HEADLESS === "true") {
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check for CI environments
|
|
52
|
-
if (
|
|
53
|
-
process.env.CI === "true" ||
|
|
54
|
-
process.env.GITHUB_ACTIONS === "true" ||
|
|
55
|
-
process.env.GITLAB_CI === "true" ||
|
|
56
|
-
process.env.JENKINS_URL ||
|
|
57
|
-
process.env.TRAVIS === "true" ||
|
|
58
|
-
process.env.CIRCLECI === "true"
|
|
59
|
-
) {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Check for Docker/container environments
|
|
64
|
-
if (process.env.DOCKER_CONTAINER || process.env.container) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check for no display (Linux)
|
|
69
|
-
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Try to open browser, with fallback to manual URL entry.
|
|
78
|
-
*
|
|
79
|
-
* @param url - URL to open
|
|
80
|
-
* @returns True if browser was opened, false if user needs to manually navigate
|
|
81
|
-
*/
|
|
82
|
-
export async function tryOpenBrowser(url: string): Promise<boolean> {
|
|
83
|
-
// Check for headless environment first
|
|
84
|
-
if (isHeadless()) {
|
|
85
|
-
log.info("Running in headless mode, skipping browser open");
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
await openBrowser(url);
|
|
91
|
-
return true;
|
|
92
|
-
} catch {
|
|
93
|
-
log.warn("Could not open browser automatically");
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Format a URL for display in the terminal.
|
|
100
|
-
* Truncates very long URLs for readability.
|
|
101
|
-
*
|
|
102
|
-
* @param url - URL to format
|
|
103
|
-
* @param maxLength - Maximum length before truncation
|
|
104
|
-
* @returns Formatted URL string
|
|
105
|
-
*/
|
|
106
|
-
export function formatUrlForDisplay(url: string, maxLength: number = 100): string {
|
|
107
|
-
if (url.length <= maxLength) {
|
|
108
|
-
return url;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Try to preserve the domain and beginning of path
|
|
112
|
-
try {
|
|
113
|
-
const parsed = new URL(url);
|
|
114
|
-
const base = `${parsed.protocol}//${parsed.host}`;
|
|
115
|
-
const remaining = maxLength - base.length - 3; // 3 for "..."
|
|
116
|
-
|
|
117
|
-
if (remaining > 20) {
|
|
118
|
-
const path = parsed.pathname + parsed.search;
|
|
119
|
-
return `${base}${path.slice(0, remaining)}...`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return url.slice(0, maxLength - 3) + "...";
|
|
123
|
-
} catch {
|
|
124
|
-
return url.slice(0, maxLength - 3) + "...";
|
|
125
|
-
}
|
|
126
|
-
}
|