@link-assistant/agent 0.5.3 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -68,6 +68,8 @@
68
68
  "@opentui/solid": "^0.1.46",
69
69
  "@parcel/watcher": "^2.5.1",
70
70
  "@solid-primitives/event-bus": "^1.1.2",
71
+ "@standard-community/standard-json": "^0.3.5",
72
+ "@standard-community/standard-openapi": "^0.2.9",
71
73
  "@zip.js/zip.js": "^2.8.10",
72
74
  "ai": "6.0.0-beta.99",
73
75
  "chokidar": "^4.0.3",
@@ -83,9 +85,8 @@
83
85
  "ignore": "^7.0.5",
84
86
  "jsonc-parser": "^3.3.1",
85
87
  "lino-objects-codec": "^0.1.1",
88
+ "log-lazy": "^1.0.4",
86
89
  "minimatch": "^10.1.1",
87
- "open": "^11.0.0",
88
- "partial-json": "^0.1.7",
89
90
  "remeda": "^2.32.0",
90
91
  "solid-js": "^1.9.10",
91
92
  "strip-ansi": "^7.1.2",
@@ -147,9 +147,10 @@ export namespace ClaudeOAuth {
147
147
  */
148
148
  export async function saveState(state: OAuthState): Promise<void> {
149
149
  await Bun.write(statePath, JSON.stringify(state, null, 2));
150
- log.info('saved oauth state', {
150
+ log.info(() => ({
151
+ message: 'saved oauth state',
151
152
  expiresAt: new Date(state.expiresAt).toISOString(),
152
- });
153
+ }));
153
154
  }
154
155
 
155
156
  /**
@@ -165,14 +166,14 @@ export namespace ClaudeOAuth {
165
166
  const parsed = OAuthState.parse(content);
166
167
 
167
168
  if (parsed.expiresAt < Date.now()) {
168
- log.warn('oauth state expired');
169
+ log.warn(() => ({ message: 'oauth state expired' }));
169
170
  await clearState();
170
171
  return undefined;
171
172
  }
172
173
 
173
174
  return parsed;
174
175
  } catch (error) {
175
- log.error('failed to load oauth state', { error });
176
+ log.error(() => ({ message: 'failed to load oauth state', error }));
176
177
  return undefined;
177
178
  }
178
179
  }
@@ -190,7 +191,7 @@ export namespace ClaudeOAuth {
190
191
  await fs.unlink(statePath).catch(() => {});
191
192
  }
192
193
  } catch (error) {
193
- log.error('failed to clear oauth state', { error });
194
+ log.error(() => ({ message: 'failed to clear oauth state', error }));
194
195
  }
195
196
  }
196
197
 
@@ -213,7 +214,9 @@ export namespace ClaudeOAuth {
213
214
  code_verifier: codeVerifier,
214
215
  });
215
216
 
216
- log.info('exchanging authorization code for tokens');
217
+ log.info(() => ({
218
+ message: 'exchanging authorization code for tokens',
219
+ }));
217
220
 
218
221
  const response = await fetch(Config.tokenUrl, {
219
222
  method: 'POST',
@@ -225,7 +228,11 @@ export namespace ClaudeOAuth {
225
228
 
226
229
  if (!response.ok) {
227
230
  const error = await response.text();
228
- log.error('token exchange failed', { status: response.status, error });
231
+ log.error(() => ({
232
+ message: 'token exchange failed',
233
+ status: response.status,
234
+ error,
235
+ }));
229
236
  throw new Error(`Token exchange failed: ${response.status} ${error}`);
230
237
  }
231
238
 
@@ -267,9 +274,10 @@ export namespace ClaudeOAuth {
267
274
  };
268
275
 
269
276
  await Bun.write(credentialsPath, JSON.stringify(credentials, null, 2));
270
- log.info('saved credentials', {
277
+ log.info(() => ({
278
+ message: 'saved credentials',
271
279
  expiresAt: new Date(credentials.claudeAiOauth!.expiresAt).toISOString(),
272
- });
280
+ }));
273
281
  }
274
282
 
275
283
  /**
@@ -283,7 +291,10 @@ export namespace ClaudeOAuth {
283
291
  try {
284
292
  const file = Bun.file(credentialsPath);
285
293
  if (!(await file.exists())) {
286
- log.info('credentials file not found', { path: credentialsPath });
294
+ log.info(() => ({
295
+ message: 'credentials file not found',
296
+ path: credentialsPath,
297
+ }));
287
298
  return undefined;
288
299
  }
289
300
 
@@ -291,27 +302,31 @@ export namespace ClaudeOAuth {
291
302
  const parsed = Credentials.parse(content);
292
303
 
293
304
  if (!parsed.claudeAiOauth) {
294
- log.info('no claudeAiOauth credentials found');
305
+ log.info(() => ({
306
+ message: 'no claudeAiOauth credentials found',
307
+ }));
295
308
  return undefined;
296
309
  }
297
310
 
298
311
  // Check if token is expired
299
312
  if (parsed.claudeAiOauth.expiresAt < Date.now()) {
300
- log.warn('token expired', {
313
+ log.warn(() => ({
314
+ message: 'token expired',
301
315
  expiresAt: new Date(parsed.claudeAiOauth.expiresAt).toISOString(),
302
- });
316
+ }));
303
317
  // TODO: Implement token refresh using refreshToken
304
318
  // For now, user needs to re-authenticate
305
319
  }
306
320
 
307
- log.info('loaded oauth credentials', {
321
+ log.info(() => ({
322
+ message: 'loaded oauth credentials',
308
323
  subscriptionType: parsed.claudeAiOauth.subscriptionType,
309
324
  scopes: parsed.claudeAiOauth.scopes,
310
- });
325
+ }));
311
326
 
312
327
  return parsed.claudeAiOauth;
313
328
  } catch (error) {
314
- log.error('failed to read credentials', { error });
329
+ log.error(() => ({ message: 'failed to read credentials', error }));
315
330
  return undefined;
316
331
  }
317
332
  }
@@ -344,7 +359,9 @@ export namespace ClaudeOAuth {
344
359
  export async function completeAuth(code: string): Promise<boolean> {
345
360
  const state = await loadState();
346
361
  if (!state) {
347
- log.error('no oauth state found - please start login flow first');
362
+ log.error(() => ({
363
+ message: 'no oauth state found - please start login flow first',
364
+ }));
348
365
  return false;
349
366
  }
350
367
 
@@ -352,10 +369,15 @@ export namespace ClaudeOAuth {
352
369
  const tokens = await exchangeCode(code, state.codeVerifier);
353
370
  await saveCredentials(tokens);
354
371
  await clearState();
355
- log.info('authentication completed successfully');
372
+ log.info(() => ({
373
+ message: 'authentication completed successfully',
374
+ }));
356
375
  return true;
357
376
  } catch (error) {
358
- log.error('failed to complete authentication', { error });
377
+ log.error(() => ({
378
+ message: 'failed to complete authentication',
379
+ error,
380
+ }));
359
381
  await clearState();
360
382
  return false;
361
383
  }
@@ -367,7 +389,7 @@ export namespace ClaudeOAuth {
367
389
  export async function refreshToken(): Promise<boolean> {
368
390
  const creds = await getCredentials();
369
391
  if (!creds?.refreshToken) {
370
- log.error('no refresh token available');
392
+ log.error(() => ({ message: 'no refresh token available' }));
371
393
  return false;
372
394
  }
373
395
 
@@ -377,7 +399,7 @@ export namespace ClaudeOAuth {
377
399
  refresh_token: creds.refreshToken,
378
400
  });
379
401
 
380
- log.info('refreshing access token');
402
+ log.info(() => ({ message: 'refreshing access token' }));
381
403
 
382
404
  try {
383
405
  const response = await fetch(Config.tokenUrl, {
@@ -390,16 +412,20 @@ export namespace ClaudeOAuth {
390
412
 
391
413
  if (!response.ok) {
392
414
  const error = await response.text();
393
- log.error('token refresh failed', { status: response.status, error });
415
+ log.error(() => ({
416
+ message: 'token refresh failed',
417
+ status: response.status,
418
+ error,
419
+ }));
394
420
  return false;
395
421
  }
396
422
 
397
423
  const tokens = TokenResponse.parse(await response.json());
398
424
  await saveCredentials(tokens);
399
- log.info('token refreshed successfully');
425
+ log.info(() => ({ message: 'token refreshed successfully' }));
400
426
  return true;
401
427
  } catch (error) {
402
- log.error('failed to refresh token', { error });
428
+ log.error(() => ({ message: 'failed to refresh token', error }));
403
429
  return false;
404
430
  }
405
431
  }
@@ -162,9 +162,10 @@ const AnthropicPlugin: AuthPlugin = {
162
162
  );
163
163
 
164
164
  if (!result.ok) {
165
- log.error('anthropic oauth token exchange failed', {
165
+ log.error(() => ({
166
+ message: 'anthropic oauth token exchange failed',
166
167
  status: result.status,
167
- });
168
+ }));
168
169
  return { type: 'failed' };
169
170
  }
170
171
 
@@ -229,9 +230,10 @@ const AnthropicPlugin: AuthPlugin = {
229
230
  );
230
231
 
231
232
  if (!tokenResult.ok) {
232
- log.error('anthropic oauth token exchange failed', {
233
+ log.error(() => ({
234
+ message: 'anthropic oauth token exchange failed',
233
235
  status: tokenResult.status,
234
- });
236
+ }));
235
237
  return { type: 'failed' };
236
238
  }
237
239
 
@@ -286,7 +288,9 @@ const AnthropicPlugin: AuthPlugin = {
286
288
 
287
289
  // Refresh token if expired
288
290
  if (!currentAuth.access || currentAuth.expires < Date.now()) {
289
- log.info('refreshing anthropic oauth token');
291
+ log.info(() => ({
292
+ message: 'refreshing anthropic oauth token',
293
+ }));
290
294
  const response = await fetch(
291
295
  'https://console.anthropic.com/v1/oauth/token',
292
296
  {
@@ -566,7 +570,7 @@ const GitHubCopilotPlugin: AuthPlugin = {
566
570
  : 'github.com';
567
571
  const urls = getCopilotUrls(domain);
568
572
 
569
- log.info('refreshing github copilot token');
573
+ log.info(() => ({ message: 'refreshing github copilot token' }));
570
574
  const response = await fetch(urls.COPILOT_API_KEY_URL, {
571
575
  headers: {
572
576
  Accept: 'application/json',
@@ -720,7 +724,9 @@ const OpenAIPlugin: AuthPlugin = {
720
724
  }
721
725
 
722
726
  if (!code) {
723
- log.error('openai oauth no code provided');
727
+ log.error(() => ({
728
+ message: 'openai oauth no code provided',
729
+ }));
724
730
  return { type: 'failed' };
725
731
  }
726
732
 
@@ -740,9 +746,10 @@ const OpenAIPlugin: AuthPlugin = {
740
746
  });
741
747
 
742
748
  if (!tokenResult.ok) {
743
- log.error('openai oauth token exchange failed', {
749
+ log.error(() => ({
750
+ message: 'openai oauth token exchange failed',
744
751
  status: tokenResult.status,
745
- });
752
+ }));
746
753
  return { type: 'failed' };
747
754
  }
748
755
 
@@ -752,7 +759,9 @@ const OpenAIPlugin: AuthPlugin = {
752
759
  !json.refresh_token ||
753
760
  typeof json.expires_in !== 'number'
754
761
  ) {
755
- log.error('openai oauth token response missing fields');
762
+ log.error(() => ({
763
+ message: 'openai oauth token response missing fields',
764
+ }));
756
765
  return { type: 'failed' };
757
766
  }
758
767
 
@@ -787,7 +796,7 @@ const OpenAIPlugin: AuthPlugin = {
787
796
 
788
797
  // Refresh token if expired
789
798
  if (!currentAuth.access || currentAuth.expires < Date.now()) {
790
- log.info('refreshing openai oauth token');
799
+ log.info(() => ({ message: 'refreshing openai oauth token' }));
791
800
  const response = await fetch(OPENAI_TOKEN_URL, {
792
801
  method: 'POST',
793
802
  headers: {
@@ -858,30 +867,125 @@ const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
858
867
  const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
859
868
  const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
860
869
 
870
+ /**
871
+ * Get an available port for the OAuth callback server.
872
+ * Supports configurable port via OAUTH_CALLBACK_PORT or GOOGLE_OAUTH_CALLBACK_PORT
873
+ * environment variable. Falls back to automatic port discovery (port 0) if not configured.
874
+ *
875
+ * Based on Gemini CLI implementation:
876
+ * https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
877
+ */
878
+ async function getGoogleOAuthPort(): Promise<number> {
879
+ // Check for environment variable override (useful for containers/firewalls)
880
+ // Support both OAUTH_CALLBACK_PORT (Gemini CLI style) and GOOGLE_OAUTH_CALLBACK_PORT
881
+ const portStr =
882
+ process.env['OAUTH_CALLBACK_PORT'] ||
883
+ process.env['GOOGLE_OAUTH_CALLBACK_PORT'];
884
+ if (portStr) {
885
+ const port = parseInt(portStr, 10);
886
+ if (!isNaN(port) && port > 0 && port <= 65535) {
887
+ log.info(() => ({
888
+ message: 'using configured oauth callback port',
889
+ port,
890
+ }));
891
+ return port;
892
+ }
893
+ log.warn(() => ({
894
+ message: 'invalid OAUTH_CALLBACK_PORT, using auto discovery',
895
+ value: portStr,
896
+ }));
897
+ }
898
+
899
+ // Discover an available port by binding to port 0
900
+ return new Promise((resolve, reject) => {
901
+ const server = net.createServer();
902
+ server.listen(0, () => {
903
+ const address = server.address() as net.AddressInfo;
904
+ const port = address.port;
905
+ server.close(() => resolve(port));
906
+ });
907
+ server.on('error', reject);
908
+ });
909
+ }
910
+
911
+ /**
912
+ * Check if browser launch should be suppressed.
913
+ * When NO_BROWSER=true, use manual code entry flow instead of localhost redirect.
914
+ *
915
+ * Based on Gemini CLI's config.isBrowserLaunchSuppressed() functionality.
916
+ */
917
+ function isBrowserSuppressed(): boolean {
918
+ const noBrowser = process.env['NO_BROWSER'];
919
+ return noBrowser === 'true' || noBrowser === '1';
920
+ }
921
+
922
+ /**
923
+ * Get the OAuth callback host for server binding.
924
+ * Defaults to 'localhost' but can be configured via OAUTH_CALLBACK_HOST.
925
+ * Use '0.0.0.0' in Docker containers to allow external connections.
926
+ */
927
+ function getOAuthCallbackHost(): string {
928
+ return process.env['OAUTH_CALLBACK_HOST'] || 'localhost';
929
+ }
930
+
931
+ /**
932
+ * Google Code Assist redirect URI for manual code entry flow
933
+ * This is used when NO_BROWSER=true or in headless environments
934
+ * Based on Gemini CLI implementation
935
+ */
936
+ const GOOGLE_CODEASSIST_REDIRECT_URI = 'https://codeassist.google.com/authcode';
937
+
861
938
  /**
862
939
  * Google OAuth Plugin
863
940
  * Supports:
864
- * - Google AI Pro/Ultra OAuth login
941
+ * - Google AI Pro/Ultra OAuth login (browser mode with localhost redirect)
942
+ * - Google AI Pro/Ultra OAuth login (manual code entry for NO_BROWSER mode)
865
943
  * - Manual API key entry
866
944
  *
867
945
  * Note: This plugin uses OAuth 2.0 with PKCE for Google AI subscription authentication.
868
946
  * After authenticating, you can use Gemini models with subscription benefits.
947
+ *
948
+ * The OAuth flow supports two modes:
949
+ * 1. Browser mode (default): Opens browser, uses localhost redirect server
950
+ * 2. Manual code entry (NO_BROWSER=true): Shows URL, user pastes authorization code
951
+ *
952
+ * Based on Gemini CLI implementation:
953
+ * https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
869
954
  */
870
955
  const GooglePlugin: AuthPlugin = {
871
956
  provider: 'google',
872
957
  methods: [
873
958
  {
874
- label: 'Google AI Pro/Ultra (OAuth)',
959
+ label: 'Google AI Pro/Ultra (OAuth - Browser)',
875
960
  type: 'oauth',
876
961
  async authorize() {
962
+ // Check if browser is suppressed - if so, recommend manual method
963
+ if (isBrowserSuppressed()) {
964
+ log.info(() => ({
965
+ message: 'NO_BROWSER is set, use manual code entry method instead',
966
+ }));
967
+ }
968
+
877
969
  const pkce = await generatePKCE();
878
970
  const state = generateRandomString(16);
879
971
 
880
- // Start local server to handle OAuth redirect
972
+ // Get an available port BEFORE starting the server
973
+ // This fixes the race condition where port was 0 when building redirect URI
974
+ const serverPort = await getGoogleOAuthPort();
975
+ const host = getOAuthCallbackHost();
976
+ // The redirect URI sent to Google must use localhost (loopback IP)
977
+ // even if we bind to a different host (like 0.0.0.0 in Docker)
978
+ const redirectUri = `http://localhost:${serverPort}/oauth/callback`;
979
+
980
+ log.info(() => ({
981
+ message: 'starting google oauth server',
982
+ port: serverPort,
983
+ host,
984
+ redirectUri,
985
+ }));
986
+
987
+ // Create server to handle OAuth redirect
881
988
  const server = http.createServer();
882
- let serverPort = 0;
883
- let authCode: string | null = null;
884
- let authState: string | null = null;
885
989
 
886
990
  const authPromise = new Promise<{ code: string; state: string }>(
887
991
  (resolve, reject) => {
@@ -935,12 +1039,22 @@ const GooglePlugin: AuthPlugin = {
935
1039
  res.end('Missing code or state parameter');
936
1040
  });
937
1041
 
938
- server.listen(0, () => {
939
- const address = server.address() as net.AddressInfo;
940
- serverPort = address.port;
1042
+ // Listen on the configured host and pre-determined port
1043
+ server.listen(serverPort, host, () => {
1044
+ log.info(() => ({
1045
+ message: 'google oauth server listening',
1046
+ port: serverPort,
1047
+ host,
1048
+ }));
941
1049
  });
942
1050
 
943
- server.on('error', reject);
1051
+ server.on('error', (err) => {
1052
+ log.error(() => ({
1053
+ message: 'google oauth server error',
1054
+ error: err,
1055
+ }));
1056
+ reject(err);
1057
+ });
944
1058
 
945
1059
  // Timeout after 5 minutes
946
1060
  setTimeout(
@@ -953,8 +1067,7 @@ const GooglePlugin: AuthPlugin = {
953
1067
  }
954
1068
  );
955
1069
 
956
- // Build authorization URL with local redirect URI
957
- const redirectUri = `http://localhost:${serverPort}/oauth/callback`;
1070
+ // Build authorization URL with the redirect URI
958
1071
  const url = new URL(GOOGLE_AUTH_URL);
959
1072
  url.searchParams.set('client_id', GOOGLE_OAUTH_CLIENT_ID);
960
1073
  url.searchParams.set('redirect_uri', redirectUri);
@@ -992,9 +1105,108 @@ const GooglePlugin: AuthPlugin = {
992
1105
  });
993
1106
 
994
1107
  if (!tokenResult.ok) {
995
- log.error('google oauth token exchange failed', {
1108
+ log.error(() => ({
1109
+ message: 'google oauth token exchange failed',
1110
+ status: tokenResult.status,
1111
+ }));
1112
+ return { type: 'failed' };
1113
+ }
1114
+
1115
+ const json = await tokenResult.json();
1116
+ if (
1117
+ !json.access_token ||
1118
+ !json.refresh_token ||
1119
+ typeof json.expires_in !== 'number'
1120
+ ) {
1121
+ log.error(() => ({
1122
+ message: 'google oauth token response missing fields',
1123
+ }));
1124
+ return { type: 'failed' };
1125
+ }
1126
+
1127
+ return {
1128
+ type: 'success',
1129
+ refresh: json.refresh_token,
1130
+ access: json.access_token,
1131
+ expires: Date.now() + json.expires_in * 1000,
1132
+ };
1133
+ } catch (error) {
1134
+ log.error(() => ({ message: 'google oauth failed', error }));
1135
+ return { type: 'failed' };
1136
+ }
1137
+ },
1138
+ };
1139
+ },
1140
+ },
1141
+ {
1142
+ label: 'Google AI Pro/Ultra (OAuth - Manual Code Entry)',
1143
+ type: 'oauth',
1144
+ async authorize() {
1145
+ /**
1146
+ * Manual code entry flow for headless environments or when NO_BROWSER=true
1147
+ * Uses Google's Code Assist redirect URI which displays the auth code to the user
1148
+ *
1149
+ * Based on Gemini CLI's authWithUserCode function:
1150
+ * https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
1151
+ */
1152
+ const pkce = await generatePKCE();
1153
+ const state = generateRandomString(16);
1154
+ const redirectUri = GOOGLE_CODEASSIST_REDIRECT_URI;
1155
+
1156
+ log.info(() => ({
1157
+ message: 'using manual code entry oauth flow',
1158
+ redirectUri,
1159
+ }));
1160
+
1161
+ // Build authorization URL with the Code Assist redirect URI
1162
+ const url = new URL(GOOGLE_AUTH_URL);
1163
+ url.searchParams.set('client_id', GOOGLE_OAUTH_CLIENT_ID);
1164
+ url.searchParams.set('redirect_uri', redirectUri);
1165
+ url.searchParams.set('response_type', 'code');
1166
+ url.searchParams.set('scope', GOOGLE_OAUTH_SCOPES.join(' '));
1167
+ url.searchParams.set('access_type', 'offline');
1168
+ url.searchParams.set('code_challenge', pkce.challenge);
1169
+ url.searchParams.set('code_challenge_method', 'S256');
1170
+ url.searchParams.set('state', state);
1171
+ url.searchParams.set('prompt', 'consent');
1172
+
1173
+ return {
1174
+ url: url.toString(),
1175
+ instructions:
1176
+ 'Visit the URL above, complete authorization, then paste the authorization code here: ',
1177
+ method: 'code' as const,
1178
+ async callback(code?: string): Promise<AuthResult> {
1179
+ if (!code) {
1180
+ log.error(() => ({
1181
+ message: 'google oauth no code provided',
1182
+ }));
1183
+ return { type: 'failed' };
1184
+ }
1185
+
1186
+ try {
1187
+ // Exchange authorization code for tokens
1188
+ const tokenResult = await fetch(GOOGLE_TOKEN_URL, {
1189
+ method: 'POST',
1190
+ headers: {
1191
+ 'Content-Type': 'application/x-www-form-urlencoded',
1192
+ },
1193
+ body: new URLSearchParams({
1194
+ code: code.trim(),
1195
+ client_id: GOOGLE_OAUTH_CLIENT_ID,
1196
+ client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
1197
+ redirect_uri: redirectUri,
1198
+ grant_type: 'authorization_code',
1199
+ code_verifier: pkce.verifier,
1200
+ }),
1201
+ });
1202
+
1203
+ if (!tokenResult.ok) {
1204
+ const errorText = await tokenResult.text();
1205
+ log.error(() => ({
1206
+ message: 'google oauth token exchange failed',
996
1207
  status: tokenResult.status,
997
- });
1208
+ error: errorText,
1209
+ }));
998
1210
  return { type: 'failed' };
999
1211
  }
1000
1212
 
@@ -1004,7 +1216,9 @@ const GooglePlugin: AuthPlugin = {
1004
1216
  !json.refresh_token ||
1005
1217
  typeof json.expires_in !== 'number'
1006
1218
  ) {
1007
- log.error('google oauth token response missing fields');
1219
+ log.error(() => ({
1220
+ message: 'google oauth token response missing fields',
1221
+ }));
1008
1222
  return { type: 'failed' };
1009
1223
  }
1010
1224
 
@@ -1015,7 +1229,10 @@ const GooglePlugin: AuthPlugin = {
1015
1229
  expires: Date.now() + json.expires_in * 1000,
1016
1230
  };
1017
1231
  } catch (error) {
1018
- log.error('google oauth failed', { error });
1232
+ log.error(() => ({
1233
+ message: 'google oauth manual code entry failed',
1234
+ error,
1235
+ }));
1019
1236
  return { type: 'failed' };
1020
1237
  }
1021
1238
  },
@@ -1058,7 +1275,7 @@ const GooglePlugin: AuthPlugin = {
1058
1275
  !currentAuth.access ||
1059
1276
  currentAuth.expires < Date.now() + FIVE_MIN_MS
1060
1277
  ) {
1061
- log.info('refreshing google oauth token');
1278
+ log.info(() => ({ message: 'refreshing google oauth token' }));
1062
1279
  const response = await fetch(GOOGLE_TOKEN_URL, {
1063
1280
  method: 'POST',
1064
1281
  headers: {