@oevortex/opencode-qwen-auth 0.1.0 → 0.1.1

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,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
- }
@@ -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
- }