@link-assistant/agent 0.6.0 → 0.6.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/package.json +1 -1
- package/src/auth/plugins.ts +217 -14
package/package.json
CHANGED
package/src/auth/plugins.ts
CHANGED
|
@@ -858,8 +858,6 @@ const GOOGLE_OAUTH_SCOPES = [
|
|
|
858
858
|
'https://www.googleapis.com/auth/cloud-platform',
|
|
859
859
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
860
860
|
'https://www.googleapis.com/auth/userinfo.profile',
|
|
861
|
-
'https://www.googleapis.com/auth/generative-language.tuning',
|
|
862
|
-
'https://www.googleapis.com/auth/generative-language.retriever',
|
|
863
861
|
];
|
|
864
862
|
|
|
865
863
|
// Google OAuth endpoints
|
|
@@ -867,30 +865,125 @@ const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
|
867
865
|
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
868
866
|
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
|
|
869
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Get an available port for the OAuth callback server.
|
|
870
|
+
* Supports configurable port via OAUTH_CALLBACK_PORT or GOOGLE_OAUTH_CALLBACK_PORT
|
|
871
|
+
* environment variable. Falls back to automatic port discovery (port 0) if not configured.
|
|
872
|
+
*
|
|
873
|
+
* Based on Gemini CLI implementation:
|
|
874
|
+
* https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
|
|
875
|
+
*/
|
|
876
|
+
async function getGoogleOAuthPort(): Promise<number> {
|
|
877
|
+
// Check for environment variable override (useful for containers/firewalls)
|
|
878
|
+
// Support both OAUTH_CALLBACK_PORT (Gemini CLI style) and GOOGLE_OAUTH_CALLBACK_PORT
|
|
879
|
+
const portStr =
|
|
880
|
+
process.env['OAUTH_CALLBACK_PORT'] ||
|
|
881
|
+
process.env['GOOGLE_OAUTH_CALLBACK_PORT'];
|
|
882
|
+
if (portStr) {
|
|
883
|
+
const port = parseInt(portStr, 10);
|
|
884
|
+
if (!isNaN(port) && port > 0 && port <= 65535) {
|
|
885
|
+
log.info(() => ({
|
|
886
|
+
message: 'using configured oauth callback port',
|
|
887
|
+
port,
|
|
888
|
+
}));
|
|
889
|
+
return port;
|
|
890
|
+
}
|
|
891
|
+
log.warn(() => ({
|
|
892
|
+
message: 'invalid OAUTH_CALLBACK_PORT, using auto discovery',
|
|
893
|
+
value: portStr,
|
|
894
|
+
}));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Discover an available port by binding to port 0
|
|
898
|
+
return new Promise((resolve, reject) => {
|
|
899
|
+
const server = net.createServer();
|
|
900
|
+
server.listen(0, () => {
|
|
901
|
+
const address = server.address() as net.AddressInfo;
|
|
902
|
+
const port = address.port;
|
|
903
|
+
server.close(() => resolve(port));
|
|
904
|
+
});
|
|
905
|
+
server.on('error', reject);
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Check if browser launch should be suppressed.
|
|
911
|
+
* When NO_BROWSER=true, use manual code entry flow instead of localhost redirect.
|
|
912
|
+
*
|
|
913
|
+
* Based on Gemini CLI's config.isBrowserLaunchSuppressed() functionality.
|
|
914
|
+
*/
|
|
915
|
+
function isBrowserSuppressed(): boolean {
|
|
916
|
+
const noBrowser = process.env['NO_BROWSER'];
|
|
917
|
+
return noBrowser === 'true' || noBrowser === '1';
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Get the OAuth callback host for server binding.
|
|
922
|
+
* Defaults to 'localhost' but can be configured via OAUTH_CALLBACK_HOST.
|
|
923
|
+
* Use '0.0.0.0' in Docker containers to allow external connections.
|
|
924
|
+
*/
|
|
925
|
+
function getOAuthCallbackHost(): string {
|
|
926
|
+
return process.env['OAUTH_CALLBACK_HOST'] || 'localhost';
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Google Code Assist redirect URI for manual code entry flow
|
|
931
|
+
* This is used when NO_BROWSER=true or in headless environments
|
|
932
|
+
* Based on Gemini CLI implementation
|
|
933
|
+
*/
|
|
934
|
+
const GOOGLE_CODEASSIST_REDIRECT_URI = 'https://codeassist.google.com/authcode';
|
|
935
|
+
|
|
870
936
|
/**
|
|
871
937
|
* Google OAuth Plugin
|
|
872
938
|
* Supports:
|
|
873
|
-
* - Google AI Pro/Ultra OAuth login
|
|
939
|
+
* - Google AI Pro/Ultra OAuth login (browser mode with localhost redirect)
|
|
940
|
+
* - Google AI Pro/Ultra OAuth login (manual code entry for NO_BROWSER mode)
|
|
874
941
|
* - Manual API key entry
|
|
875
942
|
*
|
|
876
943
|
* Note: This plugin uses OAuth 2.0 with PKCE for Google AI subscription authentication.
|
|
877
944
|
* After authenticating, you can use Gemini models with subscription benefits.
|
|
945
|
+
*
|
|
946
|
+
* The OAuth flow supports two modes:
|
|
947
|
+
* 1. Browser mode (default): Opens browser, uses localhost redirect server
|
|
948
|
+
* 2. Manual code entry (NO_BROWSER=true): Shows URL, user pastes authorization code
|
|
949
|
+
*
|
|
950
|
+
* Based on Gemini CLI implementation:
|
|
951
|
+
* https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
|
|
878
952
|
*/
|
|
879
953
|
const GooglePlugin: AuthPlugin = {
|
|
880
954
|
provider: 'google',
|
|
881
955
|
methods: [
|
|
882
956
|
{
|
|
883
|
-
label: 'Google AI Pro/Ultra (OAuth)',
|
|
957
|
+
label: 'Google AI Pro/Ultra (OAuth - Browser)',
|
|
884
958
|
type: 'oauth',
|
|
885
959
|
async authorize() {
|
|
960
|
+
// Check if browser is suppressed - if so, recommend manual method
|
|
961
|
+
if (isBrowserSuppressed()) {
|
|
962
|
+
log.info(() => ({
|
|
963
|
+
message: 'NO_BROWSER is set, use manual code entry method instead',
|
|
964
|
+
}));
|
|
965
|
+
}
|
|
966
|
+
|
|
886
967
|
const pkce = await generatePKCE();
|
|
887
968
|
const state = generateRandomString(16);
|
|
888
969
|
|
|
889
|
-
//
|
|
970
|
+
// Get an available port BEFORE starting the server
|
|
971
|
+
// This fixes the race condition where port was 0 when building redirect URI
|
|
972
|
+
const serverPort = await getGoogleOAuthPort();
|
|
973
|
+
const host = getOAuthCallbackHost();
|
|
974
|
+
// The redirect URI sent to Google must use localhost (loopback IP)
|
|
975
|
+
// even if we bind to a different host (like 0.0.0.0 in Docker)
|
|
976
|
+
const redirectUri = `http://localhost:${serverPort}/oauth/callback`;
|
|
977
|
+
|
|
978
|
+
log.info(() => ({
|
|
979
|
+
message: 'starting google oauth server',
|
|
980
|
+
port: serverPort,
|
|
981
|
+
host,
|
|
982
|
+
redirectUri,
|
|
983
|
+
}));
|
|
984
|
+
|
|
985
|
+
// Create server to handle OAuth redirect
|
|
890
986
|
const server = http.createServer();
|
|
891
|
-
let serverPort = 0;
|
|
892
|
-
let authCode: string | null = null;
|
|
893
|
-
let authState: string | null = null;
|
|
894
987
|
|
|
895
988
|
const authPromise = new Promise<{ code: string; state: string }>(
|
|
896
989
|
(resolve, reject) => {
|
|
@@ -944,12 +1037,22 @@ const GooglePlugin: AuthPlugin = {
|
|
|
944
1037
|
res.end('Missing code or state parameter');
|
|
945
1038
|
});
|
|
946
1039
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1040
|
+
// Listen on the configured host and pre-determined port
|
|
1041
|
+
server.listen(serverPort, host, () => {
|
|
1042
|
+
log.info(() => ({
|
|
1043
|
+
message: 'google oauth server listening',
|
|
1044
|
+
port: serverPort,
|
|
1045
|
+
host,
|
|
1046
|
+
}));
|
|
950
1047
|
});
|
|
951
1048
|
|
|
952
|
-
server.on('error',
|
|
1049
|
+
server.on('error', (err) => {
|
|
1050
|
+
log.error(() => ({
|
|
1051
|
+
message: 'google oauth server error',
|
|
1052
|
+
error: err,
|
|
1053
|
+
}));
|
|
1054
|
+
reject(err);
|
|
1055
|
+
});
|
|
953
1056
|
|
|
954
1057
|
// Timeout after 5 minutes
|
|
955
1058
|
setTimeout(
|
|
@@ -962,8 +1065,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
962
1065
|
}
|
|
963
1066
|
);
|
|
964
1067
|
|
|
965
|
-
// Build authorization URL with
|
|
966
|
-
const redirectUri = `http://localhost:${serverPort}/oauth/callback`;
|
|
1068
|
+
// Build authorization URL with the redirect URI
|
|
967
1069
|
const url = new URL(GOOGLE_AUTH_URL);
|
|
968
1070
|
url.searchParams.set('client_id', GOOGLE_OAUTH_CLIENT_ID);
|
|
969
1071
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
@@ -1034,6 +1136,107 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1034
1136
|
};
|
|
1035
1137
|
},
|
|
1036
1138
|
},
|
|
1139
|
+
{
|
|
1140
|
+
label: 'Google AI Pro/Ultra (OAuth - Manual Code Entry)',
|
|
1141
|
+
type: 'oauth',
|
|
1142
|
+
async authorize() {
|
|
1143
|
+
/**
|
|
1144
|
+
* Manual code entry flow for headless environments or when NO_BROWSER=true
|
|
1145
|
+
* Uses Google's Code Assist redirect URI which displays the auth code to the user
|
|
1146
|
+
*
|
|
1147
|
+
* Based on Gemini CLI's authWithUserCode function:
|
|
1148
|
+
* https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
|
|
1149
|
+
*/
|
|
1150
|
+
const pkce = await generatePKCE();
|
|
1151
|
+
const state = generateRandomString(16);
|
|
1152
|
+
const redirectUri = GOOGLE_CODEASSIST_REDIRECT_URI;
|
|
1153
|
+
|
|
1154
|
+
log.info(() => ({
|
|
1155
|
+
message: 'using manual code entry oauth flow',
|
|
1156
|
+
redirectUri,
|
|
1157
|
+
}));
|
|
1158
|
+
|
|
1159
|
+
// Build authorization URL with the Code Assist redirect URI
|
|
1160
|
+
const url = new URL(GOOGLE_AUTH_URL);
|
|
1161
|
+
url.searchParams.set('client_id', GOOGLE_OAUTH_CLIENT_ID);
|
|
1162
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
1163
|
+
url.searchParams.set('response_type', 'code');
|
|
1164
|
+
url.searchParams.set('scope', GOOGLE_OAUTH_SCOPES.join(' '));
|
|
1165
|
+
url.searchParams.set('access_type', 'offline');
|
|
1166
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
1167
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
1168
|
+
url.searchParams.set('state', state);
|
|
1169
|
+
url.searchParams.set('prompt', 'consent');
|
|
1170
|
+
|
|
1171
|
+
return {
|
|
1172
|
+
url: url.toString(),
|
|
1173
|
+
instructions:
|
|
1174
|
+
'Visit the URL above, complete authorization, then paste the authorization code here: ',
|
|
1175
|
+
method: 'code' as const,
|
|
1176
|
+
async callback(code?: string): Promise<AuthResult> {
|
|
1177
|
+
if (!code) {
|
|
1178
|
+
log.error(() => ({
|
|
1179
|
+
message: 'google oauth no code provided',
|
|
1180
|
+
}));
|
|
1181
|
+
return { type: 'failed' };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
try {
|
|
1185
|
+
// Exchange authorization code for tokens
|
|
1186
|
+
const tokenResult = await fetch(GOOGLE_TOKEN_URL, {
|
|
1187
|
+
method: 'POST',
|
|
1188
|
+
headers: {
|
|
1189
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1190
|
+
},
|
|
1191
|
+
body: new URLSearchParams({
|
|
1192
|
+
code: code.trim(),
|
|
1193
|
+
client_id: GOOGLE_OAUTH_CLIENT_ID,
|
|
1194
|
+
client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
|
|
1195
|
+
redirect_uri: redirectUri,
|
|
1196
|
+
grant_type: 'authorization_code',
|
|
1197
|
+
code_verifier: pkce.verifier,
|
|
1198
|
+
}),
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
if (!tokenResult.ok) {
|
|
1202
|
+
const errorText = await tokenResult.text();
|
|
1203
|
+
log.error(() => ({
|
|
1204
|
+
message: 'google oauth token exchange failed',
|
|
1205
|
+
status: tokenResult.status,
|
|
1206
|
+
error: errorText,
|
|
1207
|
+
}));
|
|
1208
|
+
return { type: 'failed' };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const json = await tokenResult.json();
|
|
1212
|
+
if (
|
|
1213
|
+
!json.access_token ||
|
|
1214
|
+
!json.refresh_token ||
|
|
1215
|
+
typeof json.expires_in !== 'number'
|
|
1216
|
+
) {
|
|
1217
|
+
log.error(() => ({
|
|
1218
|
+
message: 'google oauth token response missing fields',
|
|
1219
|
+
}));
|
|
1220
|
+
return { type: 'failed' };
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return {
|
|
1224
|
+
type: 'success',
|
|
1225
|
+
refresh: json.refresh_token,
|
|
1226
|
+
access: json.access_token,
|
|
1227
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
1228
|
+
};
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
log.error(() => ({
|
|
1231
|
+
message: 'google oauth manual code entry failed',
|
|
1232
|
+
error,
|
|
1233
|
+
}));
|
|
1234
|
+
return { type: 'failed' };
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1037
1240
|
{
|
|
1038
1241
|
label: 'Manually enter API Key',
|
|
1039
1242
|
type: 'api',
|