@agent-native/core 0.68.1 → 0.68.3

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 (129) hide show
  1. package/README.md +4 -4
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +16 -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 +35 -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/AgentPanel.tsx +2 -4
  10. package/corpus/core/src/client/CommandMenu.tsx +98 -3
  11. package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
  12. package/corpus/core/src/client/index.ts +2 -0
  13. package/corpus/core/src/extensions/content-patch.ts +12 -2
  14. package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
  15. package/corpus/core/src/provider-api/index.ts +37 -17
  16. package/corpus/core/src/server/auth.ts +6 -8
  17. package/corpus/core/src/server/better-auth-instance.ts +19 -4
  18. package/corpus/core/src/server/core-routes-plugin.ts +157 -5
  19. package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
  20. package/corpus/core/src/server/index.ts +8 -0
  21. package/corpus/core/src/server/onboarding-html.ts +2 -1
  22. package/corpus/core/src/server/social-og-image.ts +6 -1
  23. package/corpus/core/src/vite/client.ts +83 -1
  24. package/corpus/templates/analytics/app/routes/$.tsx +9 -0
  25. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
  27. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
  28. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
  29. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
  30. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
  31. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
  32. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
  33. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
  34. package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
  35. package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
  36. package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
  37. package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
  39. package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
  40. package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
  41. package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
  42. package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
  43. package/corpus/templates/calendar/shared/api.ts +4 -0
  44. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
  45. package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
  46. package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
  47. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
  48. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
  49. package/corpus/templates/clips/app/lib/capture-install-options.ts +14 -0
  50. package/corpus/templates/clips/app/root.tsx +54 -2
  51. package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
  52. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
  53. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
  54. package/corpus/templates/clips/app/routes/download.tsx +49 -0
  55. package/corpus/templates/clips/app/routes/record.tsx +4 -4
  56. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
  57. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +29 -0
  58. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
  59. package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
  60. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
  61. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
  62. package/corpus/templates/content/vite.config.ts +37 -1
  63. package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
  64. package/corpus/templates/design/app/routes/$.tsx +9 -0
  65. package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
  66. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  67. package/corpus/templates/plan/app/global.css +2 -2
  68. package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
  69. package/corpus/templates/slides/app/routes/$.tsx +9 -0
  70. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
  71. package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
  72. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  73. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  74. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  75. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  76. package/dist/client/AgentPanel.d.ts +1 -1
  77. package/dist/client/AgentPanel.d.ts.map +1 -1
  78. package/dist/client/AgentPanel.js +1 -1
  79. package/dist/client/AgentPanel.js.map +1 -1
  80. package/dist/client/CommandMenu.d.ts +15 -2
  81. package/dist/client/CommandMenu.d.ts.map +1 -1
  82. package/dist/client/CommandMenu.js +46 -3
  83. package/dist/client/CommandMenu.js.map +1 -1
  84. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  85. package/dist/client/ConnectBuilderCard.js +11 -2
  86. package/dist/client/ConnectBuilderCard.js.map +1 -1
  87. package/dist/client/index.d.ts +1 -1
  88. package/dist/client/index.d.ts.map +1 -1
  89. package/dist/client/index.js.map +1 -1
  90. package/dist/extensions/content-patch.d.ts.map +1 -1
  91. package/dist/extensions/content-patch.js +10 -2
  92. package/dist/extensions/content-patch.js.map +1 -1
  93. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
  94. package/dist/oauth-tokens/google-refresh.js +29 -17
  95. package/dist/oauth-tokens/google-refresh.js.map +1 -1
  96. package/dist/provider-api/index.d.ts.map +1 -1
  97. package/dist/provider-api/index.js +28 -16
  98. package/dist/provider-api/index.js.map +1 -1
  99. package/dist/server/auth.d.ts.map +1 -1
  100. package/dist/server/auth.js +6 -6
  101. package/dist/server/auth.js.map +1 -1
  102. package/dist/server/better-auth-instance.d.ts.map +1 -1
  103. package/dist/server/better-auth-instance.js +18 -4
  104. package/dist/server/better-auth-instance.js.map +1 -1
  105. package/dist/server/core-routes-plugin.d.ts +5 -0
  106. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  107. package/dist/server/core-routes-plugin.js +107 -5
  108. package/dist/server/core-routes-plugin.js.map +1 -1
  109. package/dist/server/google-oauth-credentials.d.ts +15 -0
  110. package/dist/server/google-oauth-credentials.d.ts.map +1 -0
  111. package/dist/server/google-oauth-credentials.js +34 -0
  112. package/dist/server/google-oauth-credentials.js.map +1 -0
  113. package/dist/server/index.d.ts +1 -0
  114. package/dist/server/index.d.ts.map +1 -1
  115. package/dist/server/index.js +1 -0
  116. package/dist/server/index.js.map +1 -1
  117. package/dist/server/onboarding-html.d.ts.map +1 -1
  118. package/dist/server/onboarding-html.js +2 -1
  119. package/dist/server/onboarding-html.js.map +1 -1
  120. package/dist/server/social-og-image.d.ts.map +1 -1
  121. package/dist/server/social-og-image.js +5 -1
  122. package/dist/server/social-og-image.js.map +1 -1
  123. package/dist/vite/client.d.ts.map +1 -1
  124. package/dist/vite/client.js +83 -1
  125. package/dist/vite/client.js.map +1 -1
  126. package/docs/content/authentication.md +16 -2
  127. package/docs/content/template-analytics.md +14 -9
  128. package/docs/content/template-clips.md +35 -2
  129. package/package.json +1 -1
@@ -41,6 +41,7 @@ import { DeleteRecordingMenu } from "@/components/player/delete-recording-menu";
41
41
  import { usePlayerShortcuts } from "@/hooks/use-player-shortcuts";
42
42
  import { useViewTracking } from "@/hooks/use-view-tracking";
43
43
  import { Button } from "@/components/ui/button";
44
+ import { CaptureInstallButton } from "@/components/capture-install-options";
44
45
  import {
45
46
  DropdownMenu,
46
47
  DropdownMenuContent,
@@ -1004,12 +1005,10 @@ function PublicAgentEmptyState() {
1004
1005
  Loom alternative
1005
1006
  </p>
1006
1007
  <div className="mt-7 flex w-full max-w-[220px] flex-col gap-2">
1007
- <Button asChild className="w-full gap-2">
1008
- <a href={appPath("/download")}>
1009
- <IconDownload className="h-4 w-4" />
1010
- {downloadLabel}
1011
- </a>
1012
- </Button>
1008
+ <CaptureInstallButton className="w-full gap-2" align="center">
1009
+ <IconDownload className="h-4 w-4" />
1010
+ {downloadLabel}
1011
+ </CaptureInstallButton>
1013
1012
  <Button asChild variant="outline" className="w-full">
1014
1013
  <a href={appPath("/signup")}>Sign up</a>
1015
1014
  </Button>
@@ -6,6 +6,8 @@ Name: Agent-Native Clips
6
6
 
7
7
  Chrome Web Store item ID: `baoipacpchggcdigagnajakiidcgcffn`
8
8
 
9
+ Chrome Web Store URL: `https://chromewebstore.google.com/detail/baoipacpchggcdigagnajakiidcgcffn`
10
+
9
11
  Summary: Start Clips recordings from Chrome with optional redacted console and network diagnostics.
10
12
 
11
13
  Category: Productivity
@@ -39,3 +41,30 @@ Diagnostics are bounded and redacted before they are saved. Clips does not colle
39
41
  ## Review Notes
40
42
 
41
43
  The extension opens `https://clips.agent-native.com/record` by default. Local development can point the settings page to a localhost Clips instance.
44
+
45
+ ## Submission Artifact
46
+
47
+ Build the latest Chrome Web Store ZIP from the repo root:
48
+
49
+ ```bash
50
+ pnpm --filter clips-chrome-extension package
51
+ ```
52
+
53
+ Upload the generated artifact:
54
+
55
+ ```txt
56
+ templates/clips/chrome-extension/releases/clips-chrome-extension-0.1.0.zip
57
+ ```
58
+
59
+ ## Web App Rollout Gate
60
+
61
+ Keep the web app's Chrome extension UI hidden until the Web Store listing is
62
+ approved and there is a stable public URL.
63
+
64
+ - `VITE_CLIPS_CHROME_EXTENSION_ENABLED=1` reveals the Chrome option beside
65
+ Clips desktop prompts.
66
+ - `VITE_CLIPS_CHROME_EXTENSION_URL` overrides the default install URL
67
+ (`https://chromewebstore.google.com/detail/baoipacpchggcdigagnajakiidcgcffn`)
68
+ if a deployment uses a different listing.
69
+ - Leave both unset while submitting the draft so production continues to send
70
+ users directly to the desktop app.
@@ -12,7 +12,8 @@ import {
12
12
  detectPlatform,
13
13
  getEvent,
14
14
  pickJoinUrl,
15
- refreshAccessToken,
15
+ resolveGoogleOAuthCredentialCandidates,
16
+ refreshAccessTokenWithFallback,
16
17
  type CalendarEvent,
17
18
  } from "./google-calendar-client.js";
18
19
 
@@ -77,9 +78,8 @@ export function shouldMarkNeedsReauth(message: string): boolean {
77
78
  export async function resolveCalendarAccessToken(
78
79
  account: CalendarAccountForEvents,
79
80
  ): Promise<string | null> {
80
- const clientId = process.env.GOOGLE_CLIENT_ID;
81
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
82
- if (!clientId || !clientSecret || !account.ownerEmail) return null;
81
+ const credentialCandidates = resolveGoogleOAuthCredentialCandidates();
82
+ if (!credentialCandidates.length || !account.ownerEmail) return null;
83
83
 
84
84
  let bundle: AccessTokenBundle | null = null;
85
85
  if (account.accessTokenSecretRef) {
@@ -112,10 +112,9 @@ export async function resolveCalendarAccessToken(
112
112
 
113
113
  let refreshed;
114
114
  try {
115
- refreshed = await refreshAccessToken({
115
+ refreshed = await refreshAccessTokenWithFallback({
116
116
  refreshToken: refreshSecret.value,
117
- clientId,
118
- clientSecret,
117
+ credentials: credentialCandidates,
119
118
  });
120
119
  } catch {
121
120
  // TODO(needs-reauth classification): a transient refresh failure (network
@@ -82,6 +82,35 @@ export interface ExchangeCodeArgs {
82
82
  redirectUri: string;
83
83
  }
84
84
 
85
+ export interface GoogleOAuthClientCredentials {
86
+ clientId: string;
87
+ clientSecret: string;
88
+ }
89
+
90
+ function readCredentialPair(
91
+ clientIdKey: string,
92
+ clientSecretKey: string,
93
+ ): GoogleOAuthClientCredentials | null {
94
+ const clientId = process.env[clientIdKey];
95
+ const clientSecret = process.env[clientSecretKey];
96
+ if (!clientId || !clientSecret) return null;
97
+ return { clientId, clientSecret };
98
+ }
99
+
100
+ export function resolveGoogleOAuthCredentialCandidates(): GoogleOAuthClientCredentials[] {
101
+ const primary = readCredentialPair(
102
+ "GOOGLE_CLIENT_ID",
103
+ "GOOGLE_CLIENT_SECRET",
104
+ );
105
+ const legacy = readCredentialPair(
106
+ "GOOGLE_LEGACY_CLIENT_ID",
107
+ "GOOGLE_LEGACY_CLIENT_SECRET",
108
+ );
109
+ if (!primary) return legacy ? [legacy] : [];
110
+ if (!legacy || legacy.clientId === primary.clientId) return [primary];
111
+ return [primary, legacy];
112
+ }
113
+
85
114
  /** Exchange an authorization code for tokens. Throws on non-2xx. */
86
115
  export async function exchangeCode(
87
116
  args: ExchangeCodeArgs,
@@ -129,6 +158,38 @@ export async function refreshAccessToken(args: {
129
158
  return (await res.json()) as GoogleTokenResponse;
130
159
  }
131
160
 
161
+ function isPermanentRefreshFailure(error: unknown): boolean {
162
+ const message = error instanceof Error ? error.message : String(error || "");
163
+ const lower = message.toLowerCase();
164
+ return (
165
+ lower.includes("invalid_grant") ||
166
+ lower.includes("unauthorized_client") ||
167
+ lower.includes("invalid_client")
168
+ );
169
+ }
170
+
171
+ export async function refreshAccessTokenWithFallback(args: {
172
+ refreshToken: string;
173
+ credentials: GoogleOAuthClientCredentials[];
174
+ }): Promise<GoogleTokenResponse> {
175
+ let lastError: unknown;
176
+ for (const credentials of args.credentials) {
177
+ try {
178
+ return await refreshAccessToken({
179
+ refreshToken: args.refreshToken,
180
+ clientId: credentials.clientId,
181
+ clientSecret: credentials.clientSecret,
182
+ });
183
+ } catch (error) {
184
+ lastError = error;
185
+ if (!isPermanentRefreshFailure(error)) throw error;
186
+ }
187
+ }
188
+ throw lastError instanceof Error
189
+ ? lastError
190
+ : new Error("Google token refresh failed.");
191
+ }
192
+
132
193
  /** Best-effort revoke. Returns true if Google returned 2xx, false otherwise. */
133
194
  export async function revokeToken(token: string): Promise<boolean> {
134
195
  try {
@@ -8,6 +8,7 @@ import {
8
8
  encodeOAuthState,
9
9
  getSession,
10
10
  isElectron,
11
+ resolveGoogleSignInCredentials,
11
12
  resolveOAuthRedirectUri,
12
13
  safeReturnPath,
13
14
  } from "@agent-native/core/server";
@@ -34,17 +35,6 @@ function oauthRedirectResponse(url: string) {
34
35
  }
35
36
 
36
37
  export default defineEventHandler(async (event: H3Event) => {
37
- const clientId = process.env.GOOGLE_CLIENT_ID;
38
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
39
- if (!clientId || !clientSecret) {
40
- setResponseStatus(event, 422);
41
- return {
42
- error: "missing_credentials",
43
- message:
44
- "Google OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.",
45
- };
46
- }
47
-
48
38
  try {
49
39
  const q = getQuery(event);
50
40
  const redirectUri = resolveOAuthRedirectUri(event);
@@ -67,6 +57,23 @@ export default defineEventHandler(async (event: H3Event) => {
67
57
  const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
68
58
  const calendarConnect =
69
59
  q.calendar === "1" || q.calendar === "true" || q.product === "calendar";
60
+ const credentials = calendarConnect
61
+ ? process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
62
+ ? {
63
+ clientId: process.env.GOOGLE_CLIENT_ID,
64
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
65
+ }
66
+ : null
67
+ : resolveGoogleSignInCredentials();
68
+ if (!credentials) {
69
+ setResponseStatus(event, 422);
70
+ return {
71
+ error: "missing_credentials",
72
+ message: calendarConnect
73
+ ? "Google Calendar OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET."
74
+ : "Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
75
+ };
76
+ }
70
77
 
71
78
  if (calendarConnect && !owner) {
72
79
  setResponseStatus(event, 401);
@@ -87,7 +94,7 @@ export default defineEventHandler(async (event: H3Event) => {
87
94
  });
88
95
 
89
96
  const params = new URLSearchParams({
90
- client_id: clientId,
97
+ client_id: credentials.clientId,
91
98
  redirect_uri: redirectUri,
92
99
  response_type: "code",
93
100
  state,
@@ -10,6 +10,7 @@ import {
10
10
  getAppUrl,
11
11
  oauthCallbackResponse,
12
12
  oauthErrorPage,
13
+ resolveGoogleSignInCredentials,
13
14
  resolveOAuthOwner,
14
15
  setDesktopExchange,
15
16
  type OAuthStatePayload,
@@ -51,11 +52,10 @@ async function handleGoogleSignInCallback(
51
52
  return { error: "Missing authorization code" };
52
53
  }
53
54
 
54
- const clientId = process.env.GOOGLE_CLIENT_ID;
55
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
56
- if (!clientId || !clientSecret) {
55
+ const credentials = resolveGoogleSignInCredentials();
56
+ if (!credentials) {
57
57
  return oauthErrorPage(
58
- "Google OAuth is not configured (missing client id/secret).",
58
+ "Google sign-in is not configured (missing client id/secret).",
59
59
  );
60
60
  }
61
61
 
@@ -66,8 +66,8 @@ async function handleGoogleSignInCallback(
66
66
  },
67
67
  body: new URLSearchParams({
68
68
  code,
69
- client_id: clientId,
70
- client_secret: clientSecret,
69
+ client_id: credentials.clientId,
70
+ client_secret: credentials.clientSecret,
71
71
  redirect_uri: state.redirectUri,
72
72
  grant_type: "authorization_code",
73
73
  }),
@@ -346,6 +346,42 @@ const dynamicLocalComponentDirs = (() => {
346
346
  }
347
347
  })();
348
348
 
349
+ const cloudflareSsrStubs =
350
+ process.env.NITRO_PRESET === "cloudflare_pages"
351
+ ? [
352
+ "@assistant-ui/react",
353
+ "@tiptap/core",
354
+ "@tiptap/extension-blockquote",
355
+ "@tiptap/extension-code-block-lowlight",
356
+ "@tiptap/extension-collaboration",
357
+ "@tiptap/extension-collaboration-caret",
358
+ "@tiptap/extension-image",
359
+ "@tiptap/extension-link",
360
+ "@tiptap/extension-placeholder",
361
+ "@tiptap/extension-table",
362
+ "@tiptap/extension-table-cell",
363
+ "@tiptap/extension-table-header",
364
+ "@tiptap/extension-table-row",
365
+ "@tiptap/extension-task-item",
366
+ "@tiptap/extension-task-list",
367
+ "@tiptap/pm",
368
+ "@tiptap/react",
369
+ "@tiptap/starter-kit",
370
+ "@xterm/addon-fit",
371
+ "@xterm/addon-web-links",
372
+ "@xterm/xterm",
373
+ "katex",
374
+ "lowlight",
375
+ "prettier",
376
+ "react-markdown",
377
+ "remark-gfm",
378
+ "remark-mdx",
379
+ "tiptap-markdown",
380
+ "yjs",
381
+ "y-protocols",
382
+ ]
383
+ : [];
384
+
349
385
  export default defineConfig({
350
386
  plugins: [contentLocalComponentsPlugin(), reactRouter()],
351
387
  fsAllow: [
@@ -354,7 +390,7 @@ export default defineConfig({
354
390
  ],
355
391
  // shiki only runs in AssistantChat's useEffect — keep it out of the
356
392
  // CF Pages Functions bundle (25 MiB limit).
357
- ssrStubs: ["shiki"],
393
+ ssrStubs: ["shiki", ...cloudflareSsrStubs],
358
394
  optimizeDeps: {
359
395
  include: [
360
396
  "yjs",
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
4
4
 
5
5
  export default function NotFound() {
6
6
  return (
7
- <div className="min-h-screen bg-background flex flex-col items-center justify-center">
7
+ <div className="flex min-h-full w-full flex-col items-center justify-center bg-background px-4 py-12">
8
8
  <h1 className="text-6xl font-bold text-muted-foreground/60 mb-4">404</h1>
9
9
  <p className="text-sm text-muted-foreground mb-6">
10
10
  The page you are looking for does not exist.
@@ -0,0 +1,9 @@
1
+ import NotFound from "@/pages/NotFound";
2
+
3
+ export function meta() {
4
+ return [{ title: "Not Found - Design" }];
5
+ }
6
+
7
+ export default function CatchAllRoute() {
8
+ return <NotFound />;
9
+ }
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
4
4
 
5
5
  export function NotFound() {
6
6
  return (
7
- <div className="flex h-screen items-center justify-center bg-background">
7
+ <div className="flex min-h-full w-full items-center justify-center bg-background px-4 py-12">
8
8
  <div className="text-center">
9
9
  <IconMail className="h-12 w-12 text-muted-foreground/40 mx-auto mb-4" />
10
10
  <h1 className="text-2xl font-semibold text-foreground">404</h1>
@@ -0,0 +1,9 @@
1
+ import { NotFound } from "@/pages/NotFound";
2
+
3
+ export function meta() {
4
+ return [{ title: "Not Found - Mail" }];
5
+ }
6
+
7
+ export default function CatchAllRoute() {
8
+ return <NotFound />;
9
+ }
@@ -380,9 +380,9 @@
380
380
  --rail-gap: 2rem;
381
381
  }
382
382
 
383
- @media (min-width: 1024px) {
383
+ @container plan-doc (min-width: 64rem) {
384
384
  .plan-content-surface[data-plan-layout="wide"] .plan-document-shell {
385
- --plan-wide-component-width: min(1560px, calc(100vw - 5rem));
385
+ --plan-wide-component-width: min(1560px, calc(100cqw - 5rem));
386
386
  }
387
387
 
388
388
  .plan-content-surface[data-plan-layout="wide"] .plan-document-flow-stack {
@@ -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-0 w-full flex-1 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-muted-foreground/40 mb-2">
16
16
  404
@@ -0,0 +1,9 @@
1
+ import NotFound from "@/pages/NotFound";
2
+
3
+ export function meta() {
4
+ return [{ title: "Not Found - Slides" }];
5
+ }
6
+
7
+ export default function CatchAllRoute() {
8
+ return <NotFound />;
9
+ }
@@ -30,19 +30,55 @@ interface GoogleUserInfo {
30
30
  verified_email?: boolean;
31
31
  }
32
32
 
33
+ interface GoogleOAuthCredentials {
34
+ clientId: string;
35
+ clientSecret: string;
36
+ }
37
+
38
+ function readCredentialPair(
39
+ clientIdKey: string,
40
+ clientSecretKey: string,
41
+ ): GoogleOAuthCredentials | null {
42
+ const clientId = process.env[clientIdKey];
43
+ const clientSecret = process.env[clientSecretKey];
44
+ if (!clientId || !clientSecret) return null;
45
+ return { clientId, clientSecret };
46
+ }
47
+
48
+ function resolveGoogleProviderCredentialCandidates(): GoogleOAuthCredentials[] {
49
+ const primary = readCredentialPair(
50
+ "GOOGLE_CLIENT_ID",
51
+ "GOOGLE_CLIENT_SECRET",
52
+ );
53
+ const legacy = readCredentialPair(
54
+ "GOOGLE_LEGACY_CLIENT_ID",
55
+ "GOOGLE_LEGACY_CLIENT_SECRET",
56
+ );
57
+ if (!primary) return legacy ? [legacy] : [];
58
+ if (!legacy || legacy.clientId === primary.clientId) return [primary];
59
+ return [primary, legacy];
60
+ }
61
+
33
62
  function getOAuthCredentials(): { clientId: string; clientSecret: string } {
34
- const clientId = process.env.GOOGLE_CLIENT_ID;
35
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
36
- if (!clientId || !clientSecret) {
63
+ const credentials = resolveGoogleProviderCredentialCandidates()[0];
64
+ if (!credentials) {
37
65
  throw new Error(
38
66
  "Google OAuth is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.",
39
67
  );
40
68
  }
41
- return { clientId, clientSecret };
69
+ return credentials;
42
70
  }
43
71
 
44
72
  export function isGoogleDocsOAuthConfigured(): boolean {
45
- return !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET);
73
+ return resolveGoogleProviderCredentialCandidates().length > 0;
74
+ }
75
+
76
+ function isPermanentGoogleRefreshError(error: string | undefined): boolean {
77
+ return (
78
+ error === "invalid_grant" ||
79
+ error === "unauthorized_client" ||
80
+ error === "invalid_client"
81
+ );
46
82
  }
47
83
 
48
84
  export function getGooglePickerConfig(): {
@@ -98,35 +134,55 @@ async function refreshGoogleDocsToken(
98
134
  throw new Error("Google Docs connection expired. Please reconnect.");
99
135
  }
100
136
 
101
- const { clientId, clientSecret } = getOAuthCredentials();
102
- const response = await fetch(TOKEN_URL, {
103
- method: "POST",
104
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
105
- body: new URLSearchParams({
106
- refresh_token: tokens.refresh_token,
107
- client_id: clientId,
108
- client_secret: clientSecret,
109
- grant_type: "refresh_token",
110
- }),
111
- });
112
- const data = (await response.json()) as {
137
+ const credentialCandidates = resolveGoogleProviderCredentialCandidates();
138
+ let data: {
113
139
  access_token?: string;
114
140
  expires_in?: number;
115
141
  token_type?: string;
116
142
  scope?: string;
117
143
  error?: string;
118
144
  error_description?: string;
119
- };
120
- if (!response.ok || !data.access_token) {
121
- if (
122
- data.error === "invalid_grant" ||
123
- data.error === "unauthorized_client" ||
124
- data.error === "invalid_client"
125
- ) {
145
+ } | null = null;
146
+ let lastStatusText = "Could not refresh Google token.";
147
+ for (const credentials of credentialCandidates) {
148
+ const response = await fetch(TOKEN_URL, {
149
+ method: "POST",
150
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
151
+ body: new URLSearchParams({
152
+ refresh_token: tokens.refresh_token,
153
+ client_id: credentials.clientId,
154
+ client_secret: credentials.clientSecret,
155
+ grant_type: "refresh_token",
156
+ }),
157
+ });
158
+ lastStatusText = response.statusText;
159
+ data = (await response.json()) as {
160
+ access_token?: string;
161
+ expires_in?: number;
162
+ token_type?: string;
163
+ scope?: string;
164
+ error?: string;
165
+ error_description?: string;
166
+ };
167
+ if (response.ok && data.access_token) break;
168
+ if (!isPermanentGoogleRefreshError(data.error)) {
169
+ throw new Error(
170
+ data.error_description ||
171
+ data.error ||
172
+ "Could not refresh Google token.",
173
+ );
174
+ }
175
+ }
176
+
177
+ if (!data?.access_token) {
178
+ if (isPermanentGoogleRefreshError(data?.error)) {
126
179
  await deleteOAuthTokens(GOOGLE_DOCS_PROVIDER, accountId);
127
180
  }
128
181
  throw new Error(
129
- data.error_description || data.error || "Could not refresh Google token.",
182
+ data?.error_description ||
183
+ data?.error ||
184
+ lastStatusText ||
185
+ "Could not refresh Google token.",
130
186
  );
131
187
  }
132
188
 
@@ -12,7 +12,7 @@ const NotFound = () => {
12
12
  }, [location.pathname]);
13
13
 
14
14
  return (
15
- <div className="min-h-screen flex items-center justify-center bg-background">
15
+ <div className="flex min-h-full w-full items-center justify-center bg-background px-4 py-12">
16
16
  <div className="text-center">
17
17
  <h1 className="text-5xl font-bold text-muted-foreground/30 mb-3">
18
18
  404