@agent-native/core 0.68.1 → 0.68.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.
Files changed (111) hide show
  1. package/README.md +4 -4
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +10 -0
  4. package/corpus/core/docs/content/authentication.md +16 -2
  5. package/corpus/core/docs/content/template-analytics.md +14 -9
  6. package/corpus/core/docs/content/template-clips.md +30 -2
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  9. package/corpus/core/src/client/CommandMenu.tsx +98 -3
  10. package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
  11. package/corpus/core/src/client/index.ts +2 -0
  12. package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
  13. package/corpus/core/src/provider-api/index.ts +37 -17
  14. package/corpus/core/src/server/auth.ts +6 -8
  15. package/corpus/core/src/server/better-auth-instance.ts +19 -4
  16. package/corpus/core/src/server/core-routes-plugin.ts +157 -5
  17. package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
  18. package/corpus/core/src/server/index.ts +8 -0
  19. package/corpus/core/src/server/onboarding-html.ts +2 -1
  20. package/corpus/templates/analytics/app/routes/$.tsx +9 -0
  21. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
  22. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
  23. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
  24. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
  26. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
  27. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
  28. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
  29. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
  30. package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
  31. package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
  32. package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
  33. package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
  34. package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
  35. package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
  36. package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
  37. package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
  38. package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
  39. package/corpus/templates/calendar/shared/api.ts +4 -0
  40. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
  41. package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
  42. package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
  43. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
  44. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
  45. package/corpus/templates/clips/app/lib/capture-install-options.ts +13 -0
  46. package/corpus/templates/clips/app/root.tsx +54 -2
  47. package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
  48. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
  49. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
  50. package/corpus/templates/clips/app/routes/download.tsx +49 -0
  51. package/corpus/templates/clips/app/routes/record.tsx +4 -4
  52. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
  53. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +26 -0
  54. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
  55. package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
  56. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
  57. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
  58. package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
  59. package/corpus/templates/design/app/routes/$.tsx +9 -0
  60. package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
  61. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  62. package/corpus/templates/plan/app/global.css +2 -2
  63. package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
  64. package/corpus/templates/slides/app/routes/$.tsx +9 -0
  65. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
  66. package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
  67. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  68. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  69. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  70. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  71. package/dist/client/CommandMenu.d.ts +15 -2
  72. package/dist/client/CommandMenu.d.ts.map +1 -1
  73. package/dist/client/CommandMenu.js +46 -3
  74. package/dist/client/CommandMenu.js.map +1 -1
  75. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  76. package/dist/client/ConnectBuilderCard.js +11 -2
  77. package/dist/client/ConnectBuilderCard.js.map +1 -1
  78. package/dist/client/index.d.ts +1 -1
  79. package/dist/client/index.d.ts.map +1 -1
  80. package/dist/client/index.js.map +1 -1
  81. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
  82. package/dist/oauth-tokens/google-refresh.js +29 -17
  83. package/dist/oauth-tokens/google-refresh.js.map +1 -1
  84. package/dist/provider-api/index.d.ts.map +1 -1
  85. package/dist/provider-api/index.js +28 -16
  86. package/dist/provider-api/index.js.map +1 -1
  87. package/dist/server/auth.d.ts.map +1 -1
  88. package/dist/server/auth.js +6 -6
  89. package/dist/server/auth.js.map +1 -1
  90. package/dist/server/better-auth-instance.d.ts.map +1 -1
  91. package/dist/server/better-auth-instance.js +18 -4
  92. package/dist/server/better-auth-instance.js.map +1 -1
  93. package/dist/server/core-routes-plugin.d.ts +5 -0
  94. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  95. package/dist/server/core-routes-plugin.js +107 -5
  96. package/dist/server/core-routes-plugin.js.map +1 -1
  97. package/dist/server/google-oauth-credentials.d.ts +15 -0
  98. package/dist/server/google-oauth-credentials.d.ts.map +1 -0
  99. package/dist/server/google-oauth-credentials.js +34 -0
  100. package/dist/server/google-oauth-credentials.js.map +1 -0
  101. package/dist/server/index.d.ts +1 -0
  102. package/dist/server/index.d.ts.map +1 -1
  103. package/dist/server/index.js +1 -0
  104. package/dist/server/index.js.map +1 -1
  105. package/dist/server/onboarding-html.d.ts.map +1 -1
  106. package/dist/server/onboarding-html.js +2 -1
  107. package/dist/server/onboarding-html.js.map +1 -1
  108. package/docs/content/authentication.md +16 -2
  109. package/docs/content/template-analytics.md +14 -9
  110. package/docs/content/template-clips.md +30 -2
  111. package/package.json +1 -1
@@ -107,16 +107,13 @@ export function allOtherDeclined(event: CalendarEvent): boolean {
107
107
 
108
108
  /**
109
109
  * Returns a hex color for a calendar event based on its meeting type.
110
- * Respects user-set colors and overlay colors first.
110
+ * Respects user-set colors first.
111
111
  * For local (non-Google) events without a color, returns CSS var.
112
112
  */
113
113
  export function getEventAutoColor(event: CalendarEvent): string {
114
114
  // User/Google-set color takes priority
115
115
  if (event.color) return event.color;
116
116
 
117
- // Overlay events keep their overlay-assigned color
118
- if (event.overlayEmail) return EVENT_CATEGORY_COLORS.fallback;
119
-
120
117
  // Local events without a color use the theme primary
121
118
  if (event.source !== "google") return "hsl(var(--primary))";
122
119
 
@@ -27,6 +27,12 @@ export function getRsvpStatusLabel(status?: string) {
27
27
  }
28
28
  }
29
29
 
30
+ export function canInlineRsvp(
31
+ event: Pick<CalendarEvent, "source" | "overlayEmail">,
32
+ ): boolean {
33
+ return event.source === "google" && !event.overlayEmail;
34
+ }
35
+
30
36
  export function RsvpStatusIcon({
31
37
  status,
32
38
  className,
@@ -462,16 +462,21 @@ export default function CalendarView() {
462
462
  // small non-blocking spinner instead of hiding everything behind a skeleton.
463
463
  const eventsRefreshing = isFetching && !eventsLoading;
464
464
 
465
- // Apply overlay colors and filter hidden calendars
465
+ // Apply overlay ownership markers and filter hidden calendars
466
466
  const events = useMemo(() => {
467
- const colorMap = new Map(overlayPeople.map((p) => [p.email, p.color]));
467
+ const ownerMap = new Map(overlayPeople.map((p) => [p.email, p]));
468
468
  const sourceEvents = draftEvent
469
469
  ? [...rawEvents.filter((e) => e.id !== draftEvent.id), draftEvent]
470
470
  : rawEvents;
471
471
  return sourceEvents
472
472
  .map((e) => {
473
- if (e.overlayEmail && colorMap.has(e.overlayEmail)) {
474
- return { ...e, color: colorMap.get(e.overlayEmail) };
473
+ if (e.overlayEmail && ownerMap.has(e.overlayEmail)) {
474
+ const owner = ownerMap.get(e.overlayEmail);
475
+ return {
476
+ ...e,
477
+ ownerColor: owner?.color,
478
+ ownerName: owner?.name,
479
+ };
475
480
  }
476
481
  const tempId = quickEditTempIds[e.id];
477
482
  return tempId && !e._tempId ? { ...e, _tempId: tempId } : e;
@@ -10,7 +10,7 @@ export default function NotFound() {
10
10
  }, [location.pathname]);
11
11
 
12
12
  return (
13
- <div className="min-h-screen flex items-center justify-center bg-background">
13
+ <div className="flex min-h-full w-full items-center justify-center bg-background px-4 py-12">
14
14
  <div className="text-center">
15
15
  <h1 className="text-6xl font-bold text-foreground/10 mb-2">404</h1>
16
16
  <p className="text-sm text-muted-foreground mb-6">
@@ -31,6 +31,7 @@ import {
31
31
  import {
32
32
  useGoogleAuthStatus,
33
33
  useGoogleAuthUrl,
34
+ useGoogleDesktopAuth,
34
35
  useDisconnectGoogle,
35
36
  } from "@/hooks/use-google-auth";
36
37
  import {
@@ -45,6 +46,15 @@ export default function Settings() {
45
46
  const updateSettings = useUpdateSettings();
46
47
  const googleStatus = useGoogleAuthStatus();
47
48
  const disconnectGoogle = useDisconnectGoogle();
49
+ const {
50
+ isDesktopGoogleAuth,
51
+ isGoogleDesktopAuthPending,
52
+ startDesktopGoogleAuth,
53
+ } = useGoogleDesktopAuth({
54
+ onError: (issue) =>
55
+ toast.error(issue.message || issue.error || "Google sign-in failed"),
56
+ onSuccess: () => window.location.reload(),
57
+ });
48
58
  const zoomStatus = useZoomStatus();
49
59
  const connectZoom = useConnectZoom();
50
60
  const disconnectZoom = useDisconnectZoom();
@@ -81,6 +91,12 @@ export default function Settings() {
81
91
  }
82
92
 
83
93
  function handleConnect() {
94
+ if (isDesktopGoogleAuth) {
95
+ startDesktopGoogleAuth({
96
+ previousAccountCount: googleStatus.data?.accounts?.length ?? 0,
97
+ });
98
+ return;
99
+ }
84
100
  setWantAuthUrl(true);
85
101
  }
86
102
 
@@ -179,7 +195,15 @@ export default function Settings() {
179
195
  Disconnect
180
196
  </Button>
181
197
  ) : (
182
- <Button size="sm" onClick={handleConnect}>
198
+ <Button
199
+ size="sm"
200
+ onClick={handleConnect}
201
+ disabled={
202
+ authUrl.isLoading ||
203
+ authUrl.isFetching ||
204
+ isGoogleDesktopAuthPending
205
+ }
206
+ >
183
207
  <IconExternalLink className="mr-1.5 h-3.5 w-3.5" />
184
208
  Connect
185
209
  </Button>
@@ -0,0 +1,9 @@
1
+ import NotFound from "@/pages/NotFound";
2
+
3
+ export function meta() {
4
+ return [{ title: "Not Found - Calendar" }];
5
+ }
6
+
7
+ export default function AppCatchAllRoute() {
8
+ return <NotFound />;
9
+ }
@@ -9,6 +9,7 @@ import {
9
9
  getSession,
10
10
  isElectron,
11
11
  getAppUrl,
12
+ resolveGoogleSignInCredentials,
12
13
  resolveOAuthRedirectUri,
13
14
  encodeOAuthState,
14
15
  decodeOAuthState,
@@ -19,6 +20,7 @@ import {
19
20
  oauthErrorPage,
20
21
  setDesktopExchange,
21
22
  setDesktopExchangeError,
23
+ safeReturnPath,
22
24
  } from "@agent-native/core/server";
23
25
  import {
24
26
  getAuthUrl,
@@ -29,6 +31,85 @@ import {
29
31
  import { OAuthAccountOwnedByOtherUserError } from "@agent-native/core/oauth-tokens";
30
32
 
31
33
  const OAUTH_STATE_APP_ID = process.env.APP_NAME || "calendar";
34
+ const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
35
+ const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
36
+ const GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
37
+ const GOOGLE_IDENTITY_SCOPES = [
38
+ "openid",
39
+ "https://www.googleapis.com/auth/userinfo.email",
40
+ "https://www.googleapis.com/auth/userinfo.profile",
41
+ ];
42
+
43
+ function resolveCalendarOAuthCredentials() {
44
+ const clientId = process.env.GOOGLE_CLIENT_ID;
45
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
46
+ if (!clientId || !clientSecret) return null;
47
+ return { clientId, clientSecret };
48
+ }
49
+
50
+ function isCalendarConnectRequest(
51
+ query: Record<string, any>,
52
+ owner: string | undefined,
53
+ ) {
54
+ return (
55
+ !!owner ||
56
+ query.calendar === "1" ||
57
+ query.calendar === "true" ||
58
+ query.product === "calendar"
59
+ );
60
+ }
61
+
62
+ async function exchangeIdentityCode(
63
+ code: string,
64
+ redirectUri: string,
65
+ ): Promise<{
66
+ email: string;
67
+ id?: string;
68
+ name?: string;
69
+ }> {
70
+ const credentials = resolveGoogleSignInCredentials();
71
+ if (!credentials) {
72
+ throw new Error(
73
+ "Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
74
+ );
75
+ }
76
+
77
+ const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
80
+ body: new URLSearchParams({
81
+ code,
82
+ client_id: credentials.clientId,
83
+ client_secret: credentials.clientSecret,
84
+ redirect_uri: redirectUri,
85
+ grant_type: "authorization_code",
86
+ }),
87
+ });
88
+ const tokens = await tokenRes.json();
89
+ if (!tokenRes.ok) {
90
+ throw new Error(
91
+ tokens.error_description || tokens.error || "Token exchange failed",
92
+ );
93
+ }
94
+
95
+ const userRes = await fetch(GOOGLE_USERINFO_URL, {
96
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
97
+ });
98
+ const user = await userRes.json();
99
+ const email = user.email as string | undefined;
100
+ if (!email) throw new Error("Could not get email from Google");
101
+ if (user.verified_email !== true) {
102
+ throw new Error(
103
+ "Google account email is not verified. Please verify your email with Google and try again.",
104
+ );
105
+ }
106
+
107
+ return {
108
+ email,
109
+ id: typeof user.id === "string" ? user.id : undefined,
110
+ name: typeof user.name === "string" ? user.name : undefined,
111
+ };
112
+ }
32
113
 
33
114
  function oauthRedirectResponse(url: string) {
34
115
  // h3 v2 sendRedirect returns an object the framework shim can stringify as
@@ -93,14 +174,6 @@ function googleOAuthErrorResponse(
93
174
  }
94
175
 
95
176
  export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
96
- if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
97
- setResponseStatus(event, 422);
98
- return {
99
- error: "missing_credentials",
100
- message:
101
- "Google OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.",
102
- };
103
- }
104
177
  try {
105
178
  const q = getQuery(event);
106
179
  const redirectUri = resolveOAuthRedirectUri(event);
@@ -116,17 +189,55 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
116
189
  const desktop =
117
190
  isElectron(event) || q.desktop === "1" || q.desktop === "true";
118
191
  const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
192
+ const calendarConnect = isCalendarConnectRequest(q, owner);
193
+ const credentials = calendarConnect
194
+ ? resolveCalendarOAuthCredentials()
195
+ : resolveGoogleSignInCredentials();
196
+
197
+ if (!credentials) {
198
+ setResponseStatus(event, 422);
199
+ return {
200
+ error: "missing_credentials",
201
+ message: calendarConnect
202
+ ? "Google Calendar OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET."
203
+ : "Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
204
+ };
205
+ }
206
+
207
+ if (calendarConnect && !owner) {
208
+ setResponseStatus(event, 401);
209
+ return {
210
+ error: "not_authenticated",
211
+ message: "Sign in before connecting Google Calendar.",
212
+ };
213
+ }
214
+
215
+ const requestedReturn =
216
+ typeof q.return === "string" ? safeReturnPath(q.return) : "/";
217
+ const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
119
218
  // Use the named-arg overload — the positional form previously passed
120
219
  // `flowId` in the `returnUrl` slot, breaking desktop completion.
121
220
  const state = encodeOAuthState({
122
221
  redirectUri,
123
222
  owner,
124
223
  desktop,
125
- addAccount: false,
224
+ addAccount: calendarConnect,
126
225
  app: OAUTH_STATE_APP_ID,
226
+ returnUrl,
127
227
  flowId,
128
228
  });
129
- const url = getAuthUrl(undefined, redirectUri, state);
229
+
230
+ const url = calendarConnect
231
+ ? getAuthUrl(undefined, redirectUri, state)
232
+ : `${GOOGLE_AUTH_URL}?${new URLSearchParams({
233
+ client_id: credentials.clientId,
234
+ redirect_uri: redirectUri,
235
+ response_type: "code",
236
+ scope: GOOGLE_IDENTITY_SCOPES.join(" "),
237
+ access_type: "online",
238
+ prompt: "select_account",
239
+ state,
240
+ })}`;
130
241
  if (q.redirect === "1") {
131
242
  return oauthRedirectResponse(url);
132
243
  }
@@ -172,7 +283,7 @@ export const handleGoogleCallback = defineEventHandler(
172
283
  return { error: "Missing authorization code" };
173
284
  }
174
285
 
175
- const { redirectUri, owner: stateOwner, addAccount } = state;
286
+ const { redirectUri, owner: stateOwner, addAccount, returnUrl } = state;
176
287
 
177
288
  // 1. Resolve owner (needs session context, before exchangeCode)
178
289
  const { owner, hasProductionSession } = await resolveOAuthOwner(
@@ -180,7 +291,36 @@ export const handleGoogleCallback = defineEventHandler(
180
291
  stateOwner,
181
292
  );
182
293
 
183
- // 2. Exchange code with Google (template-specific)
294
+ if (!addAccount) {
295
+ const identity = await exchangeIdentityCode(code, redirectUri);
296
+ const { sessionToken } = await createOAuthSession(
297
+ event,
298
+ identity.email,
299
+ {
300
+ hasProductionSession,
301
+ desktop,
302
+ trackSignup: {
303
+ authProvider: "google",
304
+ authUserId: identity.id,
305
+ name: identity.name,
306
+ },
307
+ },
308
+ );
309
+
310
+ if (flowId && sessionToken) {
311
+ setDesktopExchange(flowId, sessionToken, identity.email);
312
+ }
313
+
314
+ return oauthCallbackResponse(event, identity.email, {
315
+ sessionToken,
316
+ desktop,
317
+ returnUrl,
318
+ flowId,
319
+ appName: "Calendar",
320
+ });
321
+ }
322
+
323
+ // 2. Exchange code with Google (template-specific Calendar connect)
184
324
  const email = await exchangeCode(code, undefined, redirectUri, owner);
185
325
 
186
326
  // 3. Create session token (after we have the email)
@@ -46,21 +46,59 @@ interface GoogleTokens {
46
46
  photoUrl?: string;
47
47
  }
48
48
 
49
+ interface GoogleOAuthCredentials {
50
+ clientId: string;
51
+ clientSecret: string;
52
+ }
53
+
49
54
  function optionalString(value: unknown): string | undefined {
50
55
  return typeof value === "string" && value.length > 0 ? value : undefined;
51
56
  }
52
57
 
53
58
  function getOAuth2Credentials() {
54
- const clientId = process.env.GOOGLE_CLIENT_ID;
55
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
56
- if (!clientId || !clientSecret) {
59
+ const credentials = resolveGoogleProviderCredentialCandidates()[0];
60
+ if (!credentials) {
57
61
  throw new Error(
58
62
  "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in environment",
59
63
  );
60
64
  }
65
+ return credentials;
66
+ }
67
+
68
+ function getOAuth2RefreshCredentials() {
69
+ const candidates = resolveGoogleProviderCredentialCandidates();
70
+ if (!candidates.length) {
71
+ throw new Error(
72
+ "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in environment",
73
+ );
74
+ }
75
+ return candidates;
76
+ }
77
+
78
+ function readCredentialPair(
79
+ clientIdKey: string,
80
+ clientSecretKey: string,
81
+ ): GoogleOAuthCredentials | null {
82
+ const clientId = process.env[clientIdKey];
83
+ const clientSecret = process.env[clientSecretKey];
84
+ if (!clientId || !clientSecret) return null;
61
85
  return { clientId, clientSecret };
62
86
  }
63
87
 
88
+ function resolveGoogleProviderCredentialCandidates(): GoogleOAuthCredentials[] {
89
+ const primary = readCredentialPair(
90
+ "GOOGLE_CLIENT_ID",
91
+ "GOOGLE_CLIENT_SECRET",
92
+ );
93
+ const legacy = readCredentialPair(
94
+ "GOOGLE_LEGACY_CLIENT_ID",
95
+ "GOOGLE_LEGACY_CLIENT_SECRET",
96
+ );
97
+ if (!primary) return legacy ? [legacy] : [];
98
+ if (!legacy || legacy.clientId === primary.clientId) return [primary];
99
+ return [primary, legacy];
100
+ }
101
+
64
102
  /**
65
103
  * Permanent OAuth refresh failures Google can return. When we hit one of
66
104
  * these, the refresh_token is dead — keeping the row around makes
@@ -316,17 +354,27 @@ async function getValidAccessToken(
316
354
  );
317
355
  }
318
356
  try {
319
- const { clientId, clientSecret } = getOAuth2Credentials();
320
- const oauth2 = createOAuth2Client(clientId, clientSecret, "");
321
- const newTokens = await oauth2.refreshToken(tokens.refresh_token);
322
- const merged = { ...tokens, ...newTokens };
323
- await saveOAuthTokens(
324
- "google",
325
- accountId,
326
- merged as unknown as Record<string, unknown>,
327
- owner ?? accountId,
328
- );
329
- return merged.access_token;
357
+ let lastRefreshError: any;
358
+ for (const { clientId, clientSecret } of getOAuth2RefreshCredentials()) {
359
+ try {
360
+ const oauth2 = createOAuth2Client(clientId, clientSecret, "");
361
+ const newTokens = await oauth2.refreshToken(tokens.refresh_token);
362
+ const merged = { ...tokens, ...newTokens };
363
+ await saveOAuthTokens(
364
+ "google",
365
+ accountId,
366
+ merged as unknown as Record<string, unknown>,
367
+ owner ?? accountId,
368
+ );
369
+ return merged.access_token;
370
+ } catch (err: any) {
371
+ lastRefreshError = err;
372
+ if (!isPermanentRefreshError(err?.message || "")) {
373
+ throw err;
374
+ }
375
+ }
376
+ }
377
+ throw lastRefreshError;
330
378
  } catch (err: any) {
331
379
  if (isPermanentRefreshError(err?.message || "")) {
332
380
  // Drop the dead row so isOAuthConnected returns false and the UI
@@ -860,6 +908,7 @@ export async function listOverlayEvents(
860
908
  eventType: event.eventType || "default",
861
909
  accountEmail: undefined,
862
910
  overlayEmail,
911
+ ...mapColor(event),
863
912
  attendees: mapAttendees(event),
864
913
  organizer: mapOrganizer(event),
865
914
  createdAt: event.created || new Date().toISOString(),
@@ -1,28 +1,12 @@
1
1
  import { createAuthPlugin } from "@agent-native/core/server";
2
2
 
3
- // Calendar requires a Google connection to read/write events, so the
4
- // onboarding page only offers "Sign in with Google" — no email/password
5
- // account creation, since that path can't be used to access the calendar.
6
- //
7
- // Calendar/Contacts/Directory scopes are requested up front during the
8
- // primary "Sign in with Google" flow. Tokens land in the framework's
9
- // `oauth_tokens` table automatically (via a Better Auth account hook)
10
- // so the existing `templates/calendar/server/lib/google-calendar.ts`
11
- // client works on first sign-in — no separate "Connect Google" page
12
- // needed. The template-specific routes under `/_agent-native/google/*`
13
- // remain available for "add another account" flows.
3
+ // Calendar keeps Google as the primary auth surface, but the first sign-in is
4
+ // identity-only. The template-owned `/_agent-native/google/*` routes request
5
+ // Calendar/Contacts/Directory scopes only after there is a signed-in owner, so
6
+ // basic login stays isolated from product API verification/blocking issues.
14
7
  export default createAuthPlugin({
15
8
  googleOnly: true,
16
9
  mountGoogleOAuthRoutes: false,
17
- googleScopes: [
18
- "https://www.googleapis.com/auth/calendar.readonly",
19
- "https://www.googleapis.com/auth/calendar.events",
20
- "https://www.googleapis.com/auth/userinfo.email",
21
- "https://www.googleapis.com/auth/userinfo.profile",
22
- "https://www.googleapis.com/auth/directory.readonly",
23
- "https://www.googleapis.com/auth/contacts.readonly",
24
- "https://www.googleapis.com/auth/contacts.other.readonly",
25
- ],
26
10
  marketing: {
27
11
  appName: "Agent-Native Calendar",
28
12
  tagline:
@@ -17,6 +17,10 @@ export interface CalendarEvent {
17
17
  accountEmail?: string;
18
18
  /** Set when this event belongs to an overlaid person's calendar */
19
19
  overlayEmail?: string;
20
+ /** Client-only marker for overlaid calendar ownership */
21
+ ownerColor?: string;
22
+ /** Client-only display name for the overlaid calendar owner */
23
+ ownerName?: string;
20
24
  color?: string;
21
25
  /** Google Calendar event color id (1-11). */
22
26
  colorId?: string;
@@ -48,6 +48,23 @@ Loom iframe and the native Clips editor is hidden for those recordings. If the
48
48
  user needs Clips-native trimming, exports, frame extraction, or upload-based
49
49
  transcription, ask them to upload the original video file instead.
50
50
 
51
+ ## Browser diagnostics and recorder install options
52
+
53
+ Browser recordings can save bounded, redacted diagnostics through
54
+ `createBrowserDiagnosticsCapture`: console messages plus fetch/XHR method, URL,
55
+ status, duration, and errors. The browser recorder only captures activity from
56
+ the recorder page itself. The Clips Chrome extension is the active-tab path for
57
+ browser logs: it launches `/record` with an extension capture session and passes
58
+ `developerLogs=1/0`, then saves diagnostics with source `extension`.
59
+
60
+ Keep the public Chrome extension option hidden until the Web Store listing is
61
+ live. UI prompts that otherwise say "Download desktop app" should use the shared
62
+ install-choice component so `VITE_CLIPS_CHROME_EXTENSION_ENABLED=1` can reveal
63
+ two options: Chrome extension for browser logs, and desktop app for the most
64
+ seamless native capture. Also set `VITE_CLIPS_CHROME_EXTENSION_URL` when the
65
+ extension URL exists; leave the boolean unset while preparing/submitting the
66
+ listing.
67
+
51
68
  ## Pause / resume
52
69
 
53
70
  `MediaRecorder.pause()` / `.resume()` are supported in all evergreen browsers. Keep a single `MediaRecorder` instance across pauses — don't tear down the stream, or the permission prompt will fire again. While paused, the upload worker keeps draining its buffer so we catch up before the user stops.
@@ -21,7 +21,8 @@ import { accessFilter } from "@agent-native/core/sharing";
21
21
  import { readAppSecret, writeAppSecret } from "@agent-native/core/secrets";
22
22
  import {
23
23
  listEvents,
24
- refreshAccessToken,
24
+ resolveGoogleOAuthCredentialCandidates,
25
+ refreshAccessTokenWithFallback,
25
26
  pickJoinUrl,
26
27
  type CalendarEvent,
27
28
  } from "../server/lib/google-calendar-client.js";
@@ -55,9 +56,8 @@ async function resolveAccessToken(args: {
55
56
  accessRef: string | null;
56
57
  refreshRef: string | null;
57
58
  }): Promise<string | null> {
58
- const clientId = process.env.GOOGLE_CLIENT_ID;
59
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
60
- if (!clientId || !clientSecret) return null;
59
+ const credentialCandidates = resolveGoogleOAuthCredentialCandidates();
60
+ if (!credentialCandidates.length) return null;
61
61
 
62
62
  let bundle: AccessTokenBundle | null = null;
63
63
  if (args.accessRef) {
@@ -93,10 +93,9 @@ async function resolveAccessToken(args: {
93
93
 
94
94
  let refreshed;
95
95
  try {
96
- refreshed = await refreshAccessToken({
96
+ refreshed = await refreshAccessTokenWithFallback({
97
97
  refreshToken: refreshSecret.value,
98
- clientId,
99
- clientSecret,
98
+ credentials: credentialCandidates,
100
99
  });
101
100
  } catch {
102
101
  return null;