@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
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { listOAuthAccounts, saveOAuthTokens } from "./store.js";
15
+ import { resolveGoogleProviderCredentialCandidates } from "../server/google-oauth-credentials.js";
15
16
 
16
17
  const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
17
18
 
@@ -31,29 +32,43 @@ interface RefreshResponse {
31
32
  }
32
33
 
33
34
  async function refreshOne(refreshToken: string): Promise<RefreshResponse> {
34
- const clientId = process.env.GOOGLE_CLIENT_ID;
35
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
36
- if (!clientId || !clientSecret) {
35
+ const credentialCandidates = resolveGoogleProviderCredentialCandidates();
36
+ if (!credentialCandidates.length) {
37
37
  throw new Error("GOOGLE_CLIENT_ID/SECRET not set");
38
38
  }
39
- const res = await fetch(GOOGLE_TOKEN_URL, {
40
- method: "POST",
41
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
42
- body: new URLSearchParams({
43
- refresh_token: refreshToken,
44
- client_id: clientId,
45
- client_secret: clientSecret,
46
- grant_type: "refresh_token",
47
- }),
48
- });
49
- const data = (await res.json()) as Record<string, unknown>;
50
- if (!res.ok) {
39
+
40
+ let data: Record<string, unknown> | null = null;
41
+ let lastStatusText = "refresh failed";
42
+ for (const credentials of credentialCandidates) {
43
+ const res = await fetch(GOOGLE_TOKEN_URL, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
46
+ body: new URLSearchParams({
47
+ refresh_token: refreshToken,
48
+ client_id: credentials.clientId,
49
+ client_secret: credentials.clientSecret,
50
+ grant_type: "refresh_token",
51
+ }),
52
+ });
53
+ lastStatusText = res.statusText;
54
+ data = (await res.json()) as Record<string, unknown>;
55
+ if (res.ok) return data as unknown as RefreshResponse;
56
+ if (
57
+ data.error !== "invalid_grant" &&
58
+ data.error !== "unauthorized_client" &&
59
+ data.error !== "invalid_client"
60
+ ) {
61
+ break;
62
+ }
63
+ }
64
+
65
+ if (data) {
51
66
  const err = (data.error_description ||
52
67
  data.error ||
53
- res.statusText) as string;
68
+ lastStatusText) as string;
54
69
  throw new Error(err);
55
70
  }
56
- return data as unknown as RefreshResponse;
71
+ throw new Error(lastStatusText);
57
72
  }
58
73
 
59
74
  /**
@@ -19,6 +19,7 @@ import {
19
19
  saveOAuthTokens,
20
20
  } from "../oauth-tokens/index.js";
21
21
  import { getCredentialContext } from "../server/request-context.js";
22
+ import { resolveGoogleProviderCredentialCandidates } from "../server/google-oauth-credentials.js";
22
23
  import { resolveWorkspaceConnectionCredentialForApp } from "../workspace-connections/credentials.js";
23
24
  import type { WorkspaceConnectionTemplateUse } from "../connections/catalog.js";
24
25
  import type {
@@ -3432,36 +3433,55 @@ async function refreshGoogleOAuthToken(
3432
3433
  },
3433
3434
  refreshToken: string,
3434
3435
  ): Promise<string> {
3435
- const clientId = process.env.GOOGLE_CLIENT_ID;
3436
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
3437
- if (!clientId || !clientSecret) {
3436
+ const credentialCandidates = resolveGoogleProviderCredentialCandidates();
3437
+ if (!credentialCandidates.length) {
3438
3438
  throw new Error(
3439
3439
  "GOOGLE_CLIENT_ID/SECRET not set for Google OAuth refresh.",
3440
3440
  );
3441
3441
  }
3442
- const res = await fetch("https://oauth2.googleapis.com/token", {
3443
- method: "POST",
3444
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
3445
- body: new URLSearchParams({
3446
- refresh_token: refreshToken,
3447
- client_id: clientId,
3448
- client_secret: clientSecret,
3449
- grant_type: "refresh_token",
3450
- }),
3451
- });
3452
- const data = (await res.json()) as {
3442
+
3443
+ let data: {
3453
3444
  access_token?: string;
3454
3445
  expires_in?: number;
3455
3446
  token_type?: string;
3456
3447
  scope?: string;
3457
3448
  error?: string;
3458
3449
  error_description?: string;
3459
- };
3460
- if (!res.ok || !data.access_token) {
3450
+ } | null = null;
3451
+ let lastStatusText = "refresh failed";
3452
+ for (const credentials of credentialCandidates) {
3453
+ const res = await fetch("https://oauth2.googleapis.com/token", {
3454
+ method: "POST",
3455
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3456
+ body: new URLSearchParams({
3457
+ refresh_token: refreshToken,
3458
+ client_id: credentials.clientId,
3459
+ client_secret: credentials.clientSecret,
3460
+ grant_type: "refresh_token",
3461
+ }),
3462
+ });
3463
+ lastStatusText = res.statusText;
3464
+ data = (await res.json()) as {
3465
+ access_token?: string;
3466
+ expires_in?: number;
3467
+ token_type?: string;
3468
+ scope?: string;
3469
+ error?: string;
3470
+ error_description?: string;
3471
+ };
3472
+ if (res.ok && data.access_token) break;
3461
3473
  if (data.error && PERMANENT_GOOGLE_OAUTH_REFRESH_ERRORS.has(data.error)) {
3474
+ continue;
3475
+ }
3476
+ const detail = data.error_description ?? data.error ?? lastStatusText;
3477
+ throw new Error(`Google OAuth refresh failed: ${detail}`);
3478
+ }
3479
+
3480
+ if (!data?.access_token) {
3481
+ if (data?.error && PERMANENT_GOOGLE_OAUTH_REFRESH_ERRORS.has(data.error)) {
3462
3482
  await deleteOAuthTokens(options.oauthProvider, options.accountId);
3463
3483
  }
3464
- const detail = data.error_description ?? data.error ?? res.statusText;
3484
+ const detail = data?.error_description ?? data?.error ?? lastStatusText;
3465
3485
  throw new Error(`Google OAuth refresh failed: ${detail}`);
3466
3486
  }
3467
3487
  const merged: OAuthTokens = {
@@ -72,6 +72,7 @@ import {
72
72
  } from "../db/client.js";
73
73
  import { getBetterAuth, getBetterAuthSync } from "./better-auth-instance.js";
74
74
  import type { BetterAuthConfig } from "./better-auth-instance.js";
75
+ import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
75
76
  import {
76
77
  getAllowedCorsOrigin,
77
78
  readCorsAllowedOrigins,
@@ -2462,11 +2463,8 @@ async function mountBetterAuthRoutes(
2462
2463
  // Auto-add Google OAuth routes when credentials are configured. Templates
2463
2464
  // that need broader product scopes (mail/calendar) opt out and provide
2464
2465
  // their own Nitro routes at these paths.
2465
- if (
2466
- process.env.GOOGLE_CLIENT_ID &&
2467
- process.env.GOOGLE_CLIENT_SECRET &&
2468
- options.mountGoogleOAuthRoutes !== false
2469
- ) {
2466
+ const googleSignInCredentials = resolveGoogleSignInCredentials();
2467
+ if (googleSignInCredentials && options.mountGoogleOAuthRoutes !== false) {
2470
2468
  setGenericGoogleOAuthRoutesEnabled(app, true);
2471
2469
  for (const gp of [
2472
2470
  "/_agent-native/google/callback",
@@ -2535,7 +2533,7 @@ async function mountBetterAuthRoutes(
2535
2533
  process.env.VITE_AGENT_NATIVE_WORKSPACE === "1",
2536
2534
  });
2537
2535
  const params = new URLSearchParams({
2538
- client_id: process.env.GOOGLE_CLIENT_ID!,
2536
+ client_id: googleSignInCredentials.clientId,
2539
2537
  redirect_uri: redirectUri,
2540
2538
  response_type: "code",
2541
2539
  scope: googleScopes,
@@ -2647,8 +2645,8 @@ async function mountBetterAuthRoutes(
2647
2645
  },
2648
2646
  body: new URLSearchParams({
2649
2647
  code,
2650
- client_id: process.env.GOOGLE_CLIENT_ID!,
2651
- client_secret: process.env.GOOGLE_CLIENT_SECRET!,
2648
+ client_id: googleSignInCredentials.clientId,
2649
+ client_secret: googleSignInCredentials.clientSecret,
2652
2650
  redirect_uri: redirectUri,
2653
2651
  grant_type: "authorization_code",
2654
2652
  }),
@@ -26,6 +26,7 @@ import { flushTracking, identify, track } from "../tracking/index.js";
26
26
  import { TEMPLATES } from "../cli/templates-meta.js";
27
27
  import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
28
28
  import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
29
+ import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
29
30
  import {
30
31
  getDialect,
31
32
  getDatabaseUrl,
@@ -802,7 +803,17 @@ async function createBetterAuthInstance(
802
803
  ...config?.socialProviders,
803
804
  };
804
805
 
805
- if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
806
+ const extraScopes = config?.googleScopes ?? [];
807
+ const googleCredentials =
808
+ extraScopes.length > 0
809
+ ? process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
810
+ ? {
811
+ clientId: process.env.GOOGLE_CLIENT_ID,
812
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
813
+ }
814
+ : null
815
+ : resolveGoogleSignInCredentials();
816
+ if (googleCredentials) {
806
817
  // When the template requests broader scopes (Gmail, Calendar, etc.)
807
818
  // ask for them on the primary sign-in flow so a separate "Connect
808
819
  // Google" round-trip isn't needed. `accessType: "offline"` plus
@@ -810,12 +821,11 @@ async function createBetterAuthInstance(
810
821
  // Google only re-issues a refresh token on consent, so re-signing in
811
822
  // (e.g. after switching machines) would otherwise leave us with an
812
823
  // access token that can't be refreshed.
813
- const extraScopes = config?.googleScopes ?? [];
814
824
  const baseScopes = ["openid", "email", "profile"];
815
825
  const mergedScopes = Array.from(new Set([...baseScopes, ...extraScopes]));
816
826
  socialProviders.google = {
817
- clientId: process.env.GOOGLE_CLIENT_ID,
818
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
827
+ clientId: googleCredentials.clientId,
828
+ clientSecret: googleCredentials.clientSecret,
819
829
  ...(extraScopes.length > 0
820
830
  ? {
821
831
  scope: mergedScopes,
@@ -843,6 +853,9 @@ async function createBetterAuthInstance(
843
853
  const requireEmailVerification =
844
854
  isEmailConfigured() && !shouldSkipEmailVerification();
845
855
 
856
+ const shouldMirrorGoogleAccountTokens =
857
+ (config?.googleScopes?.length ?? 0) > 0;
858
+
846
859
  const auth = betterAuth({
847
860
  basePath,
848
861
  baseURL: appUrl,
@@ -969,6 +982,7 @@ async function createBetterAuthInstance(
969
982
  // mirroring work; failures never block sign-in.
970
983
  create: {
971
984
  after: async (account: any) => {
985
+ if (!shouldMirrorGoogleAccountTokens) return;
972
986
  await mirrorGoogleAccountToOAuthTokens(account).catch((err) => {
973
987
  console.error(
974
988
  "[auth] failed to mirror Google account tokens to oauth_tokens (create)",
@@ -979,6 +993,7 @@ async function createBetterAuthInstance(
979
993
  },
980
994
  update: {
981
995
  after: async (account: any) => {
996
+ if (!shouldMirrorGoogleAccountTokens) return;
982
997
  await mirrorGoogleAccountToOAuthTokens(account).catch((err) => {
983
998
  console.error(
984
999
  "[auth] failed to mirror Google account tokens to oauth_tokens (update)",
@@ -158,6 +158,24 @@ import {
158
158
  export const FRAMEWORK_ROUTE_PREFIX = "/_agent-native";
159
159
  export const FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/events`;
160
160
  export const LEGACY_FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/poll-events`;
161
+ const DEFAULT_BUILDER_WAITLIST_FORM_ID = "DYTHuM0jlV";
162
+ const DEFAULT_BUILDER_WAITLIST_FORMS_ORIGIN = "https://forms.agent-native.com";
163
+ const BUILDER_WAITLIST_FORM_SOURCE = "connect_builder_card";
164
+ const BUILDER_WAITLIST_FORM_TIMEOUT_MS = 8000;
165
+ const BUILDER_WAITLIST_TEXT_LIMIT = 4000;
166
+
167
+ interface BuilderWaitlistFormTarget {
168
+ formId: string;
169
+ formsOrigin: string;
170
+ }
171
+
172
+ interface BuilderWaitlistBody {
173
+ prompt?: unknown;
174
+ orgName?: unknown;
175
+ appUrl?: unknown;
176
+ pageUrl?: unknown;
177
+ source?: unknown;
178
+ }
161
179
 
162
180
  export function resolveFrameworkSseRoutes(sseRoute?: string): string[] {
163
181
  return Array.from(
@@ -171,6 +189,112 @@ export function resolveFrameworkSseRoutes(sseRoute?: string): string[] {
171
189
 
172
190
  registerBuiltinEngines();
173
191
 
192
+ function cleanBuilderWaitlistText(
193
+ value: unknown,
194
+ maxLength = BUILDER_WAITLIST_TEXT_LIMIT,
195
+ ): string | undefined {
196
+ if (typeof value !== "string") return undefined;
197
+ const trimmed = value.trim();
198
+ if (!trimmed) return undefined;
199
+ return trimmed.slice(0, maxLength);
200
+ }
201
+
202
+ function normalizeHttpOrigin(value: string): string | null {
203
+ try {
204
+ const url = new URL(value);
205
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
206
+ return url.origin;
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+
212
+ function isAgentNativeHostedRequest(event: H3Event): boolean {
213
+ const hostname = getRequestURL(event).hostname.toLowerCase();
214
+ return (
215
+ hostname === "agent-native.com" || hostname.endsWith(".agent-native.com")
216
+ );
217
+ }
218
+
219
+ export function resolveBuilderWaitlistFormTargetForRequest(
220
+ event: H3Event,
221
+ ): BuilderWaitlistFormTarget | null {
222
+ if (process.env.AGENT_NATIVE_DISABLE_BUILDER_WAITLIST_FORM === "1") {
223
+ return null;
224
+ }
225
+
226
+ const envFormId = process.env.AGENT_NATIVE_BUILDER_WAITLIST_FORM_ID?.trim();
227
+ const envFormsOrigin =
228
+ process.env.AGENT_NATIVE_BUILDER_WAITLIST_FORMS_ORIGIN?.trim();
229
+ const hasExplicitTarget = Boolean(envFormId || envFormsOrigin);
230
+ if (!hasExplicitTarget && !isAgentNativeHostedRequest(event)) {
231
+ return null;
232
+ }
233
+
234
+ const formId = envFormId || DEFAULT_BUILDER_WAITLIST_FORM_ID;
235
+ const formsOrigin = normalizeHttpOrigin(
236
+ envFormsOrigin || DEFAULT_BUILDER_WAITLIST_FORMS_ORIGIN,
237
+ );
238
+ if (!formsOrigin) {
239
+ throw new Error("Invalid Builder waitlist Forms origin");
240
+ }
241
+
242
+ return { formId, formsOrigin };
243
+ }
244
+
245
+ async function submitBuilderWaitlistForm(
246
+ event: H3Event,
247
+ sessionEmail: string,
248
+ body: BuilderWaitlistBody,
249
+ ): Promise<{ submitted: boolean; formId?: string }> {
250
+ const target = resolveBuilderWaitlistFormTargetForRequest(event);
251
+ if (!target) return { submitted: false };
252
+
253
+ const appUrl =
254
+ cleanBuilderWaitlistText(body.pageUrl ?? body.appUrl, 2000) ??
255
+ cleanBuilderWaitlistText(getHeader(event, "referer"), 2000) ??
256
+ getOrigin(event);
257
+ const source =
258
+ cleanBuilderWaitlistText(body.source, 100) ?? BUILDER_WAITLIST_FORM_SOURCE;
259
+
260
+ const controller = new AbortController();
261
+ const timeout = setTimeout(
262
+ () => controller.abort(),
263
+ BUILDER_WAITLIST_FORM_TIMEOUT_MS,
264
+ );
265
+
266
+ try {
267
+ const res = await fetch(
268
+ `${target.formsOrigin}/api/submit/${encodeURIComponent(target.formId)}`,
269
+ {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify({
273
+ data: {
274
+ email: sessionEmail,
275
+ orgName: cleanBuilderWaitlistText(body.orgName, 500),
276
+ appUrl,
277
+ prompt: cleanBuilderWaitlistText(body.prompt),
278
+ source,
279
+ },
280
+ _hp: "",
281
+ _meta: {
282
+ submitterEmail: sessionEmail,
283
+ pageUrl: appUrl,
284
+ },
285
+ }),
286
+ signal: controller.signal,
287
+ },
288
+ );
289
+ if (!res.ok) {
290
+ throw new Error(`Forms waitlist submission failed (${res.status})`);
291
+ }
292
+ return { submitted: true, formId: target.formId };
293
+ } finally {
294
+ clearTimeout(timeout);
295
+ }
296
+ }
297
+
174
298
  function parseBuilderCallbackBoolean(
175
299
  value: string | null | undefined,
176
300
  ): boolean | null {
@@ -1405,10 +1529,10 @@ export function createCoreRoutesPlugin(
1405
1529
  );
1406
1530
 
1407
1531
  // Branch-creation waitlist signup. Used by ConnectBuilderCard when the
1408
- // current request has no Builder branch project configured — instead of
1409
- // the raw 403 from /builder/run, the card surfaces a waitlist CTA that
1410
- // POSTs here. Recorded as a tracking event so PostHog/Mixpanel/etc.
1411
- // capture demand without us standing up new storage.
1532
+ // current request has no Builder branch project configured. Hosted
1533
+ // Agent Native deployments submit into the Builder-org Forms waitlist;
1534
+ // local/self-hosted deployments keep the analytics signal without
1535
+ // sending private workspace data to Agent Native.
1412
1536
  getH3App(nitroApp).use(
1413
1537
  `${P}/builder/branch-waitlist`,
1414
1538
  defineEventHandler(async (event: H3Event) => {
@@ -1421,15 +1545,43 @@ export function createCoreRoutesPlugin(
1421
1545
  setResponseStatus(event, 401);
1422
1546
  return { error: "Authentication required" };
1423
1547
  }
1548
+ const body = ((await readBody(event).catch(() => ({}))) ??
1549
+ {}) as BuilderWaitlistBody;
1550
+ let formSubmission: { submitted: boolean; formId?: string };
1551
+ try {
1552
+ formSubmission = await submitBuilderWaitlistForm(
1553
+ event,
1554
+ session.email,
1555
+ body,
1556
+ );
1557
+ } catch (err) {
1558
+ await trackBuilderLifecycle(
1559
+ event,
1560
+ "builder branch waitlist form failed",
1561
+ session.email,
1562
+ {
1563
+ reason:
1564
+ err instanceof Error ? err.message : "unknown_waitlist_error",
1565
+ stage: "waitlist",
1566
+ },
1567
+ );
1568
+ setResponseStatus(event, 502);
1569
+ return {
1570
+ error:
1571
+ "Couldn't join the waitlist. Please try again in a moment.",
1572
+ };
1573
+ }
1424
1574
  await trackBuilderLifecycle(
1425
1575
  event,
1426
1576
  "builder branch waitlist joined",
1427
1577
  session.email,
1428
1578
  {
1579
+ formId: formSubmission.formId ?? null,
1580
+ formSubmitted: formSubmission.submitted,
1429
1581
  stage: "waitlist",
1430
1582
  },
1431
1583
  );
1432
- return { ok: true };
1584
+ return { ok: true, formSubmitted: formSubmission.submitted };
1433
1585
  }),
1434
1586
  );
1435
1587
 
@@ -0,0 +1,51 @@
1
+ export interface GoogleOAuthCredentials {
2
+ clientId: string;
3
+ clientSecret: string;
4
+ }
5
+
6
+ function readCredentialPair(
7
+ clientIdKey: string,
8
+ clientSecretKey: string,
9
+ ): GoogleOAuthCredentials | null {
10
+ const clientId = process.env[clientIdKey];
11
+ const clientSecret = process.env[clientSecretKey];
12
+ if (!clientId || !clientSecret) return null;
13
+ return { clientId, clientSecret };
14
+ }
15
+
16
+ /**
17
+ * Credentials for identity-only Google sign-in. Deploys that also use Google
18
+ * product APIs can set these separately from GOOGLE_CLIENT_ID/SECRET, which
19
+ * remain the backwards-compatible provider OAuth credentials.
20
+ */
21
+ export function resolveGoogleSignInCredentials(): GoogleOAuthCredentials | null {
22
+ return (
23
+ readCredentialPair(
24
+ "GOOGLE_SIGN_IN_CLIENT_ID",
25
+ "GOOGLE_SIGN_IN_CLIENT_SECRET",
26
+ ) ?? readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET")
27
+ );
28
+ }
29
+
30
+ export function hasGoogleSignInCredentials(): boolean {
31
+ return resolveGoogleSignInCredentials() !== null;
32
+ }
33
+
34
+ export function resolveGoogleProviderCredentials(): GoogleOAuthCredentials | null {
35
+ return readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET");
36
+ }
37
+
38
+ export function resolveGoogleLegacyProviderCredentials(): GoogleOAuthCredentials | null {
39
+ return readCredentialPair(
40
+ "GOOGLE_LEGACY_CLIENT_ID",
41
+ "GOOGLE_LEGACY_CLIENT_SECRET",
42
+ );
43
+ }
44
+
45
+ export function resolveGoogleProviderCredentialCandidates(): GoogleOAuthCredentials[] {
46
+ const primary = resolveGoogleProviderCredentials();
47
+ const legacy = resolveGoogleLegacyProviderCredentials();
48
+ if (!primary) return legacy ? [legacy] : [];
49
+ if (!legacy || legacy.clientId === primary.clientId) return [primary];
50
+ return [primary, legacy];
51
+ }
@@ -264,6 +264,14 @@ export {
264
264
  type SpawnTaskOptions,
265
265
  } from "./agent-teams.js";
266
266
  export { isOAuthConnected, getOAuthAccounts } from "./oauth-helpers.js";
267
+ export {
268
+ hasGoogleSignInCredentials,
269
+ resolveGoogleLegacyProviderCredentials,
270
+ resolveGoogleProviderCredentialCandidates,
271
+ resolveGoogleProviderCredentials,
272
+ resolveGoogleSignInCredentials,
273
+ type GoogleOAuthCredentials,
274
+ } from "./google-oauth-credentials.js";
267
275
  export { wrapWithAnalytics } from "./analytics.js";
268
276
  export {
269
277
  getH3App,
@@ -28,9 +28,10 @@ import {
28
28
  withAgentNativeSocialImageCacheBuster,
29
29
  } from "../shared/social-meta.js";
30
30
  import { normalizeAppBasePath } from "./app-base-path.js";
31
+ import { hasGoogleSignInCredentials } from "./google-oauth-credentials.js";
31
32
 
32
33
  function hasGoogleOAuth(): boolean {
33
- return !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET);
34
+ return hasGoogleSignInCredentials();
34
35
  }
35
36
 
36
37
  function getConnectionLabel(): string {
@@ -0,0 +1,9 @@
1
+ import NotFound from "@/pages/NotFound";
2
+
3
+ export function meta() {
4
+ return [{ title: "Not Found - Analytics" }];
5
+ }
6
+
7
+ export default function CatchAllRoute() {
8
+ return <NotFound />;
9
+ }
@@ -262,7 +262,17 @@ export function DayView({
262
262
  onDraftDiscard={onDraftDiscard}
263
263
  >
264
264
  <button
265
- className="flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-sm font-medium text-foreground transition-all hover:brightness-110"
265
+ className={cn(
266
+ "relative flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-sm font-medium text-foreground transition-all hover:brightness-110",
267
+ event.ownerColor && "pr-5",
268
+ )}
269
+ aria-label={
270
+ event.ownerName || event.overlayEmail
271
+ ? `${event.title}, ${
272
+ event.ownerName || event.overlayEmail
273
+ }'s calendar`
274
+ : event.title
275
+ }
266
276
  style={
267
277
  color
268
278
  ? {
@@ -283,6 +293,13 @@ export function DayView({
283
293
  )}
284
294
  <EventStatusIcon event={event} className="shrink-0" />
285
295
  <span className="truncate">{event.title}</span>
296
+ {event.ownerColor && (
297
+ <span
298
+ aria-hidden="true"
299
+ className="absolute right-2 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
300
+ style={{ backgroundColor: event.ownerColor }}
301
+ />
302
+ )}
286
303
  </button>
287
304
  </EventDetailPopover>
288
305
  );
@@ -458,7 +475,15 @@ export function DayView({
458
475
  isBeingDragged && isDragging && "ring-2 ring-primary/40",
459
476
  canDrag && isStart && "cursor-grab",
460
477
  isBeingDragged && isDragging && "cursor-grabbing",
478
+ event.ownerColor && "pr-4",
461
479
  )}
480
+ aria-label={
481
+ event.ownerName || event.overlayEmail
482
+ ? `${event.title}, ${
483
+ event.ownerName || event.overlayEmail
484
+ }'s calendar`
485
+ : event.title
486
+ }
462
487
  style={{
463
488
  ...posStyle,
464
489
  left: `${li.left}px`,
@@ -483,6 +508,13 @@ export function DayView({
483
508
  opacity: isBeingDragged && isDragging ? 0.9 : undefined,
484
509
  }}
485
510
  >
511
+ {event.ownerColor && (
512
+ <span
513
+ aria-hidden="true"
514
+ className="absolute right-1.5 top-1.5 size-1.5 rounded-full ring-1 ring-background/70"
515
+ style={{ backgroundColor: event.ownerColor }}
516
+ />
517
+ )}
486
518
  {durationMin <= 30 ? (
487
519
  <div className="flex items-baseline gap-1.5 truncate">
488
520
  {allOthersOut && (
@@ -16,6 +16,7 @@ import { useAttendeePhotos } from "@/hooks/use-attendee-photos";
16
16
  import { useRsvpEvent } from "@/hooks/use-events";
17
17
  import { cn } from "@/lib/utils";
18
18
  import {
19
+ canInlineRsvp,
19
20
  getRsvpStatusLabel,
20
21
  RsvpStatusIcon,
21
22
  type RsvpStatus,
@@ -406,6 +407,7 @@ export function EventAttendeesSection({
406
407
  | "id"
407
408
  | "accountEmail"
408
409
  | "attendees"
410
+ | "overlayEmail"
409
411
  | "responseStatus"
410
412
  | "source"
411
413
  | "recurringEventId"
@@ -423,8 +425,14 @@ export function EventAttendeesSection({
423
425
  const { data: photos } = useAttendeePhotos(emails);
424
426
 
425
427
  const sorted = useMemo(() => sortAttendees(attendees), [attendees]);
426
- const selfAttendee = sorted.find((attendee) => attendee.self);
427
- const others = sorted.filter((attendee) => !attendee.self);
428
+ const canRsvpInline = canInlineRsvp(event);
429
+ const selfAttendee = canRsvpInline
430
+ ? sorted.find((attendee) => attendee.self)
431
+ : undefined;
432
+ const others =
433
+ canRsvpInline && selfAttendee
434
+ ? sorted.filter((attendee) => !attendee.self)
435
+ : sorted;
428
436
 
429
437
  useEffect(() => {
430
438
  setSelfStatus(event.responseStatus || "needsAction");
@@ -505,7 +513,7 @@ export function EventAttendeesSection({
505
513
  attendee={selfAttendee}
506
514
  event={event}
507
515
  photoUrl={photos?.[selfAttendee.email.toLowerCase()]}
508
- inlineRsvp={event.source === "google"}
516
+ inlineRsvp={canRsvpInline}
509
517
  currentStatus={selfStatus}
510
518
  currentNote={selfNote}
511
519
  onResponseChange={handleSelfResponseChange}