opencode-deepseek-auth 1.0.3 → 2.0.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.
package/src/plugin.js ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeepSeekAuthPlugin = void 0;
4
+ const constants_1 = require("./constants");
5
+ const auth_1 = require("./deepseek/auth");
6
+ /**
7
+ * Prepares the request to DeepSeek API by setting appropriate headers and transforming the payload
8
+ */
9
+ function prepareDeepSeekRequest(input, init, accessToken) {
10
+ // Clone the original init to avoid mutating it
11
+ const transformedInit = { ...init };
12
+ // Set authorization header
13
+ transformedInit.headers = {
14
+ ...transformedInit.headers,
15
+ ...constants_1.DEEPSEEK_BASE_HEADERS,
16
+ "authorization": `Bearer ${accessToken}`
17
+ };
18
+ // Construct the request
19
+ const request = new Request(input, transformedInit);
20
+ return { request, init: transformedInit };
21
+ }
22
+ /**
23
+ * Transforms the DeepSeek API response to match expected format
24
+ */
25
+ async function transformDeepSeekResponse(response) {
26
+ // For now, just pass through the response
27
+ return response;
28
+ }
29
+ /**
30
+ * Validates if this is a DeepSeek API request
31
+ */
32
+ function isDeepSeekRequest(input) {
33
+ const urlString = typeof input === 'string' ? input :
34
+ input instanceof URL ? input.toString() :
35
+ input.url || '';
36
+ return urlString.includes('deepseek.com') || urlString.includes('/v1/chat/completions');
37
+ }
38
+ /**
39
+ * Registers the DeepSeek simple API key provider for Opencode.
40
+ * Users can configure their credentials using `opencode connect` with format: email:password or phone:password
41
+ */
42
+ const DeepSeekAuthPlugin = async ({ client }) => ({
43
+ auth: {
44
+ provider: constants_1.DEEPSEEK_PROVIDER_ID,
45
+ loader: async (getAuth, provider) => {
46
+ const auth = await getAuth();
47
+ // This plugin only handles API key authentication
48
+ if (!auth?.apiKey) {
49
+ return null;
50
+ }
51
+ // Determine if this is a DeepSeek-specific authentication by checking the auth metadata
52
+ const isDeepSeekAuth = auth.provider === 'deepseek' || auth.type === 'deepseek-email-password';
53
+ const apiKeyValue = auth.apiKey;
54
+ // If explicitly marked as DeepSeek auth, process as email:password
55
+ if (isDeepSeekAuth) {
56
+ const parts = apiKeyValue.split(':');
57
+ if (parts.length === 2) {
58
+ const [identifier, password] = parts;
59
+ // Convert credentials to DeepSeek access token
60
+ const result = await (0, auth_1.loginDeepSeek)(identifier, password);
61
+ if (result.type !== "success") {
62
+ console.error(`Failed to authenticate with provided credentials: ${result.error}`);
63
+ throw new Error(`Authentication failed: ${result.error}`);
64
+ }
65
+ // Successfully obtained access token from credentials
66
+ const accessToken = result.access;
67
+ // If models are defined in the provider, set cost to 0 to indicate free usage
68
+ if (provider.models) {
69
+ for (const model of Object.values(provider.models)) {
70
+ if (model) {
71
+ model.cost = { input: 0, output: 0 };
72
+ }
73
+ }
74
+ }
75
+ return {
76
+ apiKey: accessToken,
77
+ async fetch(input, init) {
78
+ // If this isn't a DeepSeek request, pass through normally
79
+ if (!isDeepSeekRequest(input)) {
80
+ return fetch(input, init);
81
+ }
82
+ // Prepare the request with proper headers
83
+ const { request, init: transformedInit } = prepareDeepSeekRequest(input, init, accessToken);
84
+ // Make the API call
85
+ const response = await fetch(request, transformedInit);
86
+ // Transform response if needed
87
+ return transformDeepSeekResponse(response);
88
+ },
89
+ };
90
+ }
91
+ else {
92
+ // If explicitly marked as DeepSeek auth but not in correct format, throw error
93
+ throw new Error(`DeepSeek authentication requires email:password format`);
94
+ }
95
+ }
96
+ else {
97
+ // Otherwise, check if this looks like email:password format but isn't explicitly marked
98
+ const parts = apiKeyValue.split(':');
99
+ if (parts.length === 2) {
100
+ const [potentialIdentifier, potentialPassword] = parts;
101
+ // Heuristic: Check if it looks like an email or phone number format, and password seems reasonable
102
+ const isEmailFormat = potentialIdentifier.includes('@');
103
+ const isPhoneFormat = /^[0-9+\-\s()]+$/.test(potentialIdentifier.trim());
104
+ const isPasswordReasonableLength = potentialPassword.length >= 6;
105
+ // Only attempt email:password processing if both parts seem valid
106
+ if ((isEmailFormat || isPhoneFormat) && isPasswordReasonableLength) {
107
+ try {
108
+ // Try to use as email:password
109
+ const result = await (0, auth_1.loginDeepSeek)(potentialIdentifier, potentialPassword);
110
+ if (result.type === "success") {
111
+ // Successfully authenticated with email:password
112
+ const accessToken = result.access;
113
+ // If models are defined in the provider, set cost to 0 to indicate free usage
114
+ if (provider.models) {
115
+ for (const model of Object.values(provider.models)) {
116
+ if (model) {
117
+ model.cost = { input: 0, output: 0 };
118
+ }
119
+ }
120
+ }
121
+ return {
122
+ apiKey: accessToken,
123
+ async fetch(input, init) {
124
+ // If this isn't a DeepSeek request, pass through normally
125
+ if (!isDeepSeekRequest(input)) {
126
+ return fetch(input, init);
127
+ }
128
+ // Prepare the request with proper headers
129
+ const { request, init: transformedInit } = prepareDeepSeekRequest(input, init, accessToken);
130
+ // Make the API call
131
+ const response = await fetch(request, transformedInit);
132
+ // Transform response if needed
133
+ return transformDeepSeekResponse(response);
134
+ },
135
+ };
136
+ }
137
+ }
138
+ catch (error) {
139
+ console.warn("Email:password authentication failed, treating as regular API key:", error);
140
+ // Fall through to treat as regular API key
141
+ }
142
+ }
143
+ }
144
+ // Default: treat as direct DeepSeek access token or other API key
145
+ const accessToken = apiKeyValue;
146
+ return {
147
+ apiKey: accessToken,
148
+ async fetch(input, init) {
149
+ // If this isn't a DeepSeek request, pass through normally
150
+ if (!isDeepSeekRequest(input)) {
151
+ return fetch(input, init);
152
+ }
153
+ // Prepare the request with proper headers
154
+ const { request, init: transformedInit } = prepareDeepSeekRequest(input, init, accessToken);
155
+ // Make the API call
156
+ const response = await fetch(request, transformedInit);
157
+ // Transform response if needed
158
+ return transformDeepSeekResponse(response);
159
+ },
160
+ };
161
+ }
162
+ },
163
+ methods: [], // Empty methods array since we're using config-based auth
164
+ },
165
+ });
166
+ exports.DeepSeekAuthPlugin = DeepSeekAuthPlugin;
package/src/plugin.ts CHANGED
@@ -2,8 +2,6 @@ import { spawn } from "node:child_process";
2
2
 
3
3
  import { DEEPSEEK_PROVIDER_ID, DEEPSEEK_REDIRECT_URI, DEEPSEEK_BASE_HEADERS } from "./constants";
4
4
  import {
5
- authorizeDeepSeek,
6
- exchangeDeepSeek,
7
5
  loginDeepSeek
8
6
  } from "./deepseek/auth";
9
7
  import type { DeepSeekTokenExchangeResult } from "./deepseek/auth";
@@ -13,64 +11,9 @@ import type {
13
11
  LoaderResult,
14
12
  PluginContext,
15
13
  PluginResult,
16
- Provider,
17
- FormConfig
14
+ Provider
18
15
  } from "./types";
19
16
 
20
- // Keep track of active tokens
21
- const tokenCache = new Map<string, { token: string, expires: number, email: string }>();
22
-
23
- /**
24
- * Checks if an access token has expired
25
- */
26
- function accessTokenExpired(authRecord: any): boolean {
27
- const now = Date.now();
28
- return !authRecord.expires || now >= authRecord.expires - 60000; // 1 minute before expiry
29
- }
30
-
31
- /**
32
- * Determines if the auth method is OAuth-based
33
- */
34
- function isOAuthAuth(auth: any): boolean {
35
- return auth && auth.type === "oauth";
36
- }
37
-
38
- /**
39
- * Refresh access token if expired
40
- */
41
- async function refreshAccessToken(authRecord: any, client: any): Promise<any | null> {
42
- // DeepSeek doesn't have refresh tokens, so a full re-login is required
43
- const email = authRecord.email;
44
- const password = authRecord.password;
45
-
46
- if (!email || !password) {
47
- return null;
48
- }
49
-
50
- const result = await loginDeepSeek(email, password);
51
- if (result.type === "success") {
52
- // Update the stored credentials
53
- const newAuth = {
54
- type: "oauth",
55
- access: result.access,
56
- expires: result.expires,
57
- email: result.email || email,
58
- password: password // Store password for refresh
59
- };
60
-
61
- // Update cache
62
- tokenCache.set(email, {
63
- token: result.access,
64
- expires: result.expires,
65
- email: result.email || email
66
- });
67
-
68
- return newAuth;
69
- }
70
-
71
- return null;
72
- }
73
-
74
17
  /**
75
18
  * Prepares the request to DeepSeek API by setting appropriate headers and transforming the payload
76
19
  */
@@ -102,7 +45,6 @@ async function transformDeepSeekResponse(
102
45
  response: Response
103
46
  ): Promise<Response> {
104
47
  // For now, just pass through the response
105
- // If needed, we could transform to match OpenAI format
106
48
  return response;
107
49
  }
108
50
 
@@ -117,160 +59,163 @@ function isDeepSeekRequest(input: Parameters<typeof fetch>[0]): boolean {
117
59
  }
118
60
 
119
61
  /**
120
- * Registers the DeepSeek Auth provider for Opencode, handling auth, request rewriting,
121
- * and response normalization for DeepSeek requests.
62
+ * Registers the DeepSeek simple API key provider for Opencode.
63
+ * Users can configure their credentials using `opencode connect` with format: email:password or phone:password
122
64
  */
123
65
  export const DeepSeekAuthPlugin = async (
124
66
  { client }: PluginContext,
125
67
  ): Promise<PluginResult> => ({
126
68
  auth: {
127
69
  provider: DEEPSEEK_PROVIDER_ID,
128
- loader: async (getAuth: GetAuth, provider: Provider): Promise<LoaderResult | null> => {
129
- const auth = await getAuth();
130
-
131
- // Handle both OAuth-based auth and API key formatted credentials
132
- if (!isOAuthAuth(auth) && !auth.apiKey) {
133
- return null;
134
- }
135
-
136
- // If models are defined in the provider, set cost to 0 to indicate free usage
137
- if (provider.models) {
138
- for (const model of Object.values(provider.models)) {
139
- if (model) {
140
- model.cost = { input: 0, output: 0 };
141
- }
142
- }
143
- }
144
-
145
- // Handle the case where API key contains email:password
146
- let resolvedAccessToken = "";
147
- if (auth.apiKey) {
148
- // Check if API key contains email:password or phone:password format
149
- const credentialParts = auth.apiKey.split(':');
150
- if (credentialParts.length === 2) {
151
- const [identifier, password] = credentialParts;
152
- // Try to login with these credentials
153
- const result = await loginDeepSeek(identifier, password);
154
- if (result.type === "success") {
155
- resolvedAccessToken = result.access;
156
- // Cache this for potential future use
157
- tokenCache.set(identifier, {
158
- token: result.access,
159
- expires: result.expires,
160
- email: result.email || identifier
161
- });
162
- } else {
163
- throw new Error(`Failed to authenticate with provided credentials: ${result.error}`);
164
- }
165
- } else {
166
- // If it's not in email:password format, assume it's already a valid access token
167
- resolvedAccessToken = auth.apiKey;
168
- }
169
- } else if (isOAuthAuth(auth)) {
170
- // Handle normal OAuth flow
171
- let authRecord = auth;
172
-
173
- // Check if token has expired and refresh if possible
174
- if (accessTokenExpired(authRecord)) {
175
- const refreshed = await refreshAccessToken(authRecord, client);
176
- if (!refreshed) {
177
- console.warn("Could not refresh DeepSeek access token");
178
- return null;
179
- }
180
- authRecord = refreshed;
181
- }
182
-
183
- resolvedAccessToken = authRecord.access;
184
- if (!resolvedAccessToken) {
185
- return null;
186
- }
187
- }
188
-
189
- return {
190
- apiKey: resolvedAccessToken || "",
191
- async fetch(input: Parameters<typeof fetch>[0], init?: RequestInit) {
192
- // If this isn't a DeepSeek request, pass through normally
193
- if (!isDeepSeekRequest(input)) {
194
- return fetch(input as RequestInfo, init);
195
- }
196
-
197
- // Prepare the request with proper headers
198
- const { request, init: transformedInit } = prepareDeepSeekRequest(
199
- input,
200
- init,
201
- resolvedAccessToken
202
- );
203
-
204
- // Make the API call
205
- const response = await fetch(request, transformedInit);
206
-
207
- // Transform response if needed
208
- return transformDeepSeekResponse(response);
209
- },
210
- };
211
- },
212
- methods: [
213
- {
214
- label: "Login with DeepSeek Account",
215
- type: "oauth",
216
- authorize: async () => {
217
- // Direct credential form for DeepSeek as they don't use standard OAuth
218
- const form: FormConfig = {
219
- fields: [
220
- {
221
- name: "email",
222
- label: "Email",
223
- type: "email",
224
- required: true,
225
- },
226
- {
227
- name: "password",
228
- label: "Password",
229
- type: "password",
230
- required: true,
231
- }
232
- ]
233
- };
234
-
235
- return {
236
- url: "https://chat.deepseek.com",
237
- instructions: "Enter your DeepSeek account credentials below. Your credentials will be stored securely and used for authentication.",
238
- method: "form",
239
- form: form,
240
- callback: async (formData: Record<string, string>): Promise<DeepSeekTokenExchangeResult> => {
241
- const email = formData.email;
242
- const password = formData.password;
243
-
244
- // Validate inputs
245
- if (!email || !password) {
246
- return {
247
- type: "failed",
248
- error: "Email and password are required for DeepSeek authentication"
249
- };
250
- }
70
+ loader: async (getAuth: GetAuth, provider: Provider): Promise<LoaderResult | null> => {
71
+ const auth = await getAuth();
72
+
73
+ // This plugin only handles API key authentication
74
+ if (!auth?.apiKey) {
75
+ return null;
76
+ }
77
+
78
+ // Determine if this is a DeepSeek-specific authentication by checking the auth metadata
79
+ const isDeepSeekAuth = auth.provider === 'deepseek' || auth.type === 'deepseek-email-password';
80
+ const apiKeyValue = auth.apiKey;
81
+
82
+ // If explicitly marked as DeepSeek auth, process as email:password
83
+ if (isDeepSeekAuth) {
84
+ const parts = apiKeyValue.split(':');
85
+ if (parts.length === 2) {
86
+ const [identifier, password] = parts;
87
+
88
+ // Convert credentials to DeepSeek access token
89
+ const result = await loginDeepSeek(identifier, password);
90
+ if (result.type !== "success") {
91
+ console.error(`Failed to authenticate with provided credentials: ${result.error}`);
92
+ throw new Error(`Authentication failed: ${result.error}`);
93
+ }
94
+
95
+ // Successfully obtained access token from credentials
96
+ const accessToken = result.access;
97
+
98
+ // If models are defined in the provider, set cost to 0 to indicate free usage
99
+ if (provider.models) {
100
+ for (const model of Object.values(provider.models)) {
101
+ if (model) {
102
+ model.cost = { input: 0, output: 0 };
103
+ }
104
+ }
105
+ }
106
+
107
+ return {
108
+ apiKey: accessToken,
109
+ async fetch(input: Parameters<typeof fetch>[0], init?: RequestInit) {
110
+ // If this isn't a DeepSeek request, pass through normally
111
+ if (!isDeepSeekRequest(input)) {
112
+ return fetch(input as RequestInfo, init);
113
+ }
114
+
115
+ // Prepare the request with proper headers
116
+ const { request, init: transformedInit } = prepareDeepSeekRequest(
117
+ input,
118
+ init,
119
+ accessToken
120
+ );
121
+
122
+ // Make the API call
123
+ const response = await fetch(request, transformedInit);
124
+
125
+ // Transform response if needed
126
+ return transformDeepSeekResponse(response);
127
+ },
128
+ };
129
+ } else {
130
+ // If explicitly marked as DeepSeek auth but not in correct format, throw error
131
+ throw new Error(`DeepSeek authentication requires email:password format`);
132
+ }
133
+ } else {
134
+ // Otherwise, check if this looks like email:password format but isn't explicitly marked
135
+ const parts = apiKeyValue.split(':');
136
+ if (parts.length === 2) {
137
+ const [potentialIdentifier, potentialPassword] = parts;
138
+
139
+ // Heuristic: Check if it looks like an email or phone number format, and password seems reasonable
140
+ const isEmailFormat = potentialIdentifier.includes('@');
141
+ const isPhoneFormat = /^[0-9+\-\s()]+$/.test(potentialIdentifier.trim());
142
+ const isPasswordReasonableLength = potentialPassword.length >= 6;
143
+
144
+ // Only attempt email:password processing if both parts seem valid
145
+ if ((isEmailFormat || isPhoneFormat) && isPasswordReasonableLength) {
146
+ try {
147
+ // Try to use as email:password
148
+ const result = await loginDeepSeek(potentialIdentifier, potentialPassword);
149
+ if (result.type === "success") {
150
+ // Successfully authenticated with email:password
151
+ const accessToken = result.access;
152
+
153
+ // If models are defined in the provider, set cost to 0 to indicate free usage
154
+ if (provider.models) {
155
+ for (const model of Object.values(provider.models)) {
156
+ if (model) {
157
+ model.cost = { input: 0, output: 0 };
158
+ }
159
+ }
160
+ }
161
+
162
+ return {
163
+ apiKey: accessToken,
164
+ async fetch(input: Parameters<typeof fetch>[0], init?: RequestInit) {
165
+ // If this isn't a DeepSeek request, pass through normally
166
+ if (!isDeepSeekRequest(input)) {
167
+ return fetch(input as RequestInfo, init);
168
+ }
169
+
170
+ // Prepare the request with proper headers
171
+ const { request, init: transformedInit } = prepareDeepSeekRequest(
172
+ input,
173
+ init,
174
+ accessToken
175
+ );
176
+
177
+ // Make the API call
178
+ const response = await fetch(request, transformedInit);
179
+
180
+ // Transform response if needed
181
+ return transformDeepSeekResponse(response);
182
+ },
183
+ };
184
+ }
185
+ } catch (error) {
186
+ console.warn("Email:password authentication failed, treating as regular API key:", error);
187
+ // Fall through to treat as regular API key
188
+ }
189
+ }
190
+ }
191
+
192
+ // Default: treat as direct DeepSeek access token or other API key
193
+ const accessToken = apiKeyValue;
194
+
195
+ return {
196
+ apiKey: accessToken,
197
+ async fetch(input: Parameters<typeof fetch>[0], init?: RequestInit) {
198
+ // If this isn't a DeepSeek request, pass through normally
199
+ if (!isDeepSeekRequest(input)) {
200
+ return fetch(input as RequestInfo, init);
201
+ }
202
+
203
+ // Prepare the request with proper headers
204
+ const { request, init: transformedInit } = prepareDeepSeekRequest(
205
+ input,
206
+ init,
207
+ accessToken
208
+ );
251
209
 
252
- // Attempt to log in using the provided credentials
253
- const result = await loginDeepSeek(email, password);
254
-
255
- if (result.type === "success") {
256
- // Cache the token
257
- tokenCache.set(email, {
258
- token: result.access,
259
- expires: result.expires,
260
- email: result.email || email
261
- });
262
- }
263
-
264
- return result;
265
- },
266
- };
267
- },
268
- },
269
- {
270
- provider: DEEPSEEK_PROVIDER_ID,
271
- label: "Enter credentials (email:password or phone:password)",
272
- type: "api",
273
- },
274
- ],
210
+ // Make the API call
211
+ const response = await fetch(request, transformedInit);
212
+
213
+ // Transform response if needed
214
+ return transformDeepSeekResponse(response);
215
+ },
216
+ };
217
+ }
218
+ },
219
+ methods: [], // Empty methods array since we're using config-based auth
275
220
  },
276
221
  });
package/src/types.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Types for the DeepSeek Auth plugin
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
package/src/types.ts CHANGED
@@ -26,7 +26,11 @@ export interface PluginResult {
26
26
  }
27
27
 
28
28
  export interface GetAuth {
29
- (): Promise<any>;
29
+ (): Promise<{
30
+ apiKey?: string;
31
+ type?: string; // Additional field to specify the type of authentication
32
+ provider?: string; // Additional field to specify which provider this auth is for
33
+ }>;
30
34
  }
31
35
 
32
36
  export interface LoaderResult {