@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
@@ -707,6 +707,8 @@ export {
707
707
  openAgentSidebar,
708
708
  submitToAgent,
709
709
  type CommandMenuProps,
710
+ type CommandMenuDoc,
711
+ type CommandDocsGroupProps,
710
712
  type CommandGroupProps,
711
713
  type CommandItemProps,
712
714
  type CommandShortcutProps,
@@ -116,13 +116,23 @@ export async function applyExtensionContentUpdate(
116
116
  export async function formatExtensionHtml(content: string): Promise<string> {
117
117
  try {
118
118
  const prettier = await import("prettier");
119
- return await prettier.format(content, {
119
+ const formatted = await prettier.format(content, {
120
120
  parser: "html",
121
121
  htmlWhitespaceSensitivity: "ignore",
122
122
  });
123
+ return typeof formatted === "string" ? formatted : content;
123
124
  } catch (err: any) {
125
+ const message = String(err?.message ?? err);
126
+ if (
127
+ message.includes("Cannot find package 'prettier'") ||
128
+ message.includes('Cannot find package "prettier"') ||
129
+ message.includes("Cannot find module 'prettier'") ||
130
+ message.includes('Cannot find module "prettier"')
131
+ ) {
132
+ return content;
133
+ }
124
134
  throw new Error(
125
- `Unable to format extension HTML with Prettier: ${err?.message ?? err}`,
135
+ `Unable to format extension HTML with Prettier: ${message}`,
126
136
  );
127
137
  }
128
138
  }
@@ -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 {
@@ -330,7 +330,12 @@ export function renderAgentNativeOgImageSvg(
330
330
  export async function renderAgentNativeOgImagePng(
331
331
  input: AgentNativeOgImageInput = {},
332
332
  ): Promise<Uint8Array> {
333
- const { Resvg } = await import(/* @vite-ignore */ "@resvg/resvg-js");
333
+ const overridePackage =
334
+ typeof process !== "undefined"
335
+ ? process.env.AGENT_NATIVE_RESVG_PACKAGE
336
+ : undefined;
337
+ const resvgPackage = overridePackage || "@resvg/resvg-js";
338
+ const { Resvg } = await import(/* @vite-ignore */ resvgPackage);
334
339
  // Feed resvg the embedded Liberation Sans font explicitly. System fonts can't
335
340
  // be relied on: Linux serverless runtimes (Netlify/Lambda) ship neither Arial
336
341
  // nor Inter, so without a bundled font every `<text>` rendered blank.