@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.
- package/README.md +4 -4
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +10 -0
- package/corpus/core/docs/content/authentication.md +16 -2
- package/corpus/core/docs/content/template-analytics.md +14 -9
- package/corpus/core/docs/content/template-clips.md +30 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +98 -3
- package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
- package/corpus/core/src/provider-api/index.ts +37 -17
- package/corpus/core/src/server/auth.ts +6 -8
- package/corpus/core/src/server/better-auth-instance.ts +19 -4
- package/corpus/core/src/server/core-routes-plugin.ts +157 -5
- package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/server/onboarding-html.ts +2 -1
- package/corpus/templates/analytics/app/routes/$.tsx +9 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
- package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
- package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
- package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
- package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
- package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
- package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
- package/corpus/templates/calendar/shared/api.ts +4 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
- package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
- package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
- package/corpus/templates/clips/app/lib/capture-install-options.ts +13 -0
- package/corpus/templates/clips/app/root.tsx +54 -2
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
- package/corpus/templates/clips/app/routes/download.tsx +49 -0
- package/corpus/templates/clips/app/routes/record.tsx +4 -4
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
- package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +26 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
- package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
- package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/design/app/routes/$.tsx +9 -0
- package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/mail/app/routes/$.tsx +9 -0
- package/corpus/templates/plan/app/global.css +2 -2
- package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/slides/app/routes/$.tsx +9 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
- package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +15 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +46 -3
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +11 -2
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
- package/dist/oauth-tokens/google-refresh.js +29 -17
- package/dist/oauth-tokens/google-refresh.js.map +1 -1
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -16
- package/dist/provider-api/index.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +6 -6
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +18 -4
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +5 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +107 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth-credentials.d.ts +15 -0
- package/dist/server/google-oauth-credentials.d.ts.map +1 -0
- package/dist/server/google-oauth-credentials.js +34 -0
- package/dist/server/google-oauth-credentials.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/server/onboarding-html.js.map +1 -1
- package/docs/content/authentication.md +16 -2
- package/docs/content/template-analytics.md +14 -9
- package/docs/content/template-clips.md +30 -2
- 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
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
68
|
+
lastStatusText) as string;
|
|
54
69
|
throw new Error(err);
|
|
55
70
|
}
|
|
56
|
-
|
|
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
|
|
3436
|
-
|
|
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
|
-
|
|
3443
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2466
|
-
|
|
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:
|
|
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:
|
|
2651
|
-
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
|
-
|
|
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:
|
|
818
|
-
clientSecret:
|
|
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
|
|
1409
|
-
//
|
|
1410
|
-
//
|
|
1411
|
-
//
|
|
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
|
|
34
|
+
return hasGoogleSignInCredentials();
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function getConnectionLabel(): string {
|
|
@@ -262,7 +262,17 @@ export function DayView({
|
|
|
262
262
|
onDraftDiscard={onDraftDiscard}
|
|
263
263
|
>
|
|
264
264
|
<button
|
|
265
|
-
className=
|
|
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
|
|
427
|
-
const
|
|
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={
|
|
516
|
+
inlineRsvp={canRsvpInline}
|
|
509
517
|
currentStatus={selfStatus}
|
|
510
518
|
currentNote={selfNote}
|
|
511
519
|
onResponseChange={handleSelfResponseChange}
|