@agent-native/core 0.68.0 → 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 +18 -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/agent/engine/builder-engine.ts +22 -3
- package/corpus/core/src/agent/engine/types.ts +1 -0
- package/corpus/core/src/agent/production-agent.ts +2 -0
- package/corpus/core/src/agent/types.ts +1 -0
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +45 -6
- package/corpus/core/src/client/CommandMenu.tsx +98 -3
- package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
- package/corpus/core/src/client/active-run-state.ts +16 -0
- package/corpus/core/src/client/guided-questions.tsx +73 -17
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/sse-event-processor.ts +9 -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/core/src/templates/workspace-root/.env.example +4 -0
- 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/actions/show-design-questions.ts +15 -1
- package/corpus/templates/design/app/hooks/use-agent-generating.ts +47 -8
- package/corpus/templates/design/app/pages/DesignEditor.tsx +6 -0
- 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/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +21 -3
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/agent/engine/types.d.ts +2 -0
- package/dist/agent/engine/types.d.ts.map +1 -1
- package/dist/agent/engine/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +3 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +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/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +42 -7
- package/dist/client/AssistantChat.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/active-run-state.d.ts +2 -0
- package/dist/client/active-run-state.d.ts.map +1 -1
- package/dist/client/active-run-state.js +10 -0
- package/dist/client/active-run-state.js.map +1 -1
- package/dist/client/guided-questions.d.ts +2 -0
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +52 -7
- package/dist/client/guided-questions.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/client/sse-event-processor.d.ts +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.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/dist/templates/workspace-root/.env.example +4 -0
- 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
- package/src/templates/workspace-root/.env.example +4 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getSession,
|
|
10
10
|
isElectron,
|
|
11
11
|
getAppUrl,
|
|
12
|
+
resolveGoogleSignInCredentials,
|
|
12
13
|
resolveOAuthRedirectUri,
|
|
13
14
|
encodeOAuthState,
|
|
14
15
|
decodeOAuthState,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
oauthErrorPage,
|
|
20
21
|
setDesktopExchange,
|
|
21
22
|
setDesktopExchangeError,
|
|
23
|
+
safeReturnPath,
|
|
22
24
|
} from "@agent-native/core/server";
|
|
23
25
|
import {
|
|
24
26
|
getAuthUrl,
|
|
@@ -29,6 +31,85 @@ import {
|
|
|
29
31
|
import { OAuthAccountOwnedByOtherUserError } from "@agent-native/core/oauth-tokens";
|
|
30
32
|
|
|
31
33
|
const OAUTH_STATE_APP_ID = process.env.APP_NAME || "calendar";
|
|
34
|
+
const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
35
|
+
const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
36
|
+
const GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
37
|
+
const GOOGLE_IDENTITY_SCOPES = [
|
|
38
|
+
"openid",
|
|
39
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
40
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function resolveCalendarOAuthCredentials() {
|
|
44
|
+
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
45
|
+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
46
|
+
if (!clientId || !clientSecret) return null;
|
|
47
|
+
return { clientId, clientSecret };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isCalendarConnectRequest(
|
|
51
|
+
query: Record<string, any>,
|
|
52
|
+
owner: string | undefined,
|
|
53
|
+
) {
|
|
54
|
+
return (
|
|
55
|
+
!!owner ||
|
|
56
|
+
query.calendar === "1" ||
|
|
57
|
+
query.calendar === "true" ||
|
|
58
|
+
query.product === "calendar"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function exchangeIdentityCode(
|
|
63
|
+
code: string,
|
|
64
|
+
redirectUri: string,
|
|
65
|
+
): Promise<{
|
|
66
|
+
email: string;
|
|
67
|
+
id?: string;
|
|
68
|
+
name?: string;
|
|
69
|
+
}> {
|
|
70
|
+
const credentials = resolveGoogleSignInCredentials();
|
|
71
|
+
if (!credentials) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
80
|
+
body: new URLSearchParams({
|
|
81
|
+
code,
|
|
82
|
+
client_id: credentials.clientId,
|
|
83
|
+
client_secret: credentials.clientSecret,
|
|
84
|
+
redirect_uri: redirectUri,
|
|
85
|
+
grant_type: "authorization_code",
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
const tokens = await tokenRes.json();
|
|
89
|
+
if (!tokenRes.ok) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
tokens.error_description || tokens.error || "Token exchange failed",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const userRes = await fetch(GOOGLE_USERINFO_URL, {
|
|
96
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
97
|
+
});
|
|
98
|
+
const user = await userRes.json();
|
|
99
|
+
const email = user.email as string | undefined;
|
|
100
|
+
if (!email) throw new Error("Could not get email from Google");
|
|
101
|
+
if (user.verified_email !== true) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Google account email is not verified. Please verify your email with Google and try again.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
email,
|
|
109
|
+
id: typeof user.id === "string" ? user.id : undefined,
|
|
110
|
+
name: typeof user.name === "string" ? user.name : undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
32
113
|
|
|
33
114
|
function oauthRedirectResponse(url: string) {
|
|
34
115
|
// h3 v2 sendRedirect returns an object the framework shim can stringify as
|
|
@@ -93,14 +174,6 @@ function googleOAuthErrorResponse(
|
|
|
93
174
|
}
|
|
94
175
|
|
|
95
176
|
export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
|
|
96
|
-
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
|
97
|
-
setResponseStatus(event, 422);
|
|
98
|
-
return {
|
|
99
|
-
error: "missing_credentials",
|
|
100
|
-
message:
|
|
101
|
-
"Google OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.",
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
177
|
try {
|
|
105
178
|
const q = getQuery(event);
|
|
106
179
|
const redirectUri = resolveOAuthRedirectUri(event);
|
|
@@ -116,17 +189,55 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
|
|
|
116
189
|
const desktop =
|
|
117
190
|
isElectron(event) || q.desktop === "1" || q.desktop === "true";
|
|
118
191
|
const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
|
|
192
|
+
const calendarConnect = isCalendarConnectRequest(q, owner);
|
|
193
|
+
const credentials = calendarConnect
|
|
194
|
+
? resolveCalendarOAuthCredentials()
|
|
195
|
+
: resolveGoogleSignInCredentials();
|
|
196
|
+
|
|
197
|
+
if (!credentials) {
|
|
198
|
+
setResponseStatus(event, 422);
|
|
199
|
+
return {
|
|
200
|
+
error: "missing_credentials",
|
|
201
|
+
message: calendarConnect
|
|
202
|
+
? "Google Calendar OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET."
|
|
203
|
+
: "Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (calendarConnect && !owner) {
|
|
208
|
+
setResponseStatus(event, 401);
|
|
209
|
+
return {
|
|
210
|
+
error: "not_authenticated",
|
|
211
|
+
message: "Sign in before connecting Google Calendar.",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const requestedReturn =
|
|
216
|
+
typeof q.return === "string" ? safeReturnPath(q.return) : "/";
|
|
217
|
+
const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
|
|
119
218
|
// Use the named-arg overload — the positional form previously passed
|
|
120
219
|
// `flowId` in the `returnUrl` slot, breaking desktop completion.
|
|
121
220
|
const state = encodeOAuthState({
|
|
122
221
|
redirectUri,
|
|
123
222
|
owner,
|
|
124
223
|
desktop,
|
|
125
|
-
addAccount:
|
|
224
|
+
addAccount: calendarConnect,
|
|
126
225
|
app: OAUTH_STATE_APP_ID,
|
|
226
|
+
returnUrl,
|
|
127
227
|
flowId,
|
|
128
228
|
});
|
|
129
|
-
|
|
229
|
+
|
|
230
|
+
const url = calendarConnect
|
|
231
|
+
? getAuthUrl(undefined, redirectUri, state)
|
|
232
|
+
: `${GOOGLE_AUTH_URL}?${new URLSearchParams({
|
|
233
|
+
client_id: credentials.clientId,
|
|
234
|
+
redirect_uri: redirectUri,
|
|
235
|
+
response_type: "code",
|
|
236
|
+
scope: GOOGLE_IDENTITY_SCOPES.join(" "),
|
|
237
|
+
access_type: "online",
|
|
238
|
+
prompt: "select_account",
|
|
239
|
+
state,
|
|
240
|
+
})}`;
|
|
130
241
|
if (q.redirect === "1") {
|
|
131
242
|
return oauthRedirectResponse(url);
|
|
132
243
|
}
|
|
@@ -172,7 +283,7 @@ export const handleGoogleCallback = defineEventHandler(
|
|
|
172
283
|
return { error: "Missing authorization code" };
|
|
173
284
|
}
|
|
174
285
|
|
|
175
|
-
const { redirectUri, owner: stateOwner, addAccount } = state;
|
|
286
|
+
const { redirectUri, owner: stateOwner, addAccount, returnUrl } = state;
|
|
176
287
|
|
|
177
288
|
// 1. Resolve owner (needs session context, before exchangeCode)
|
|
178
289
|
const { owner, hasProductionSession } = await resolveOAuthOwner(
|
|
@@ -180,7 +291,36 @@ export const handleGoogleCallback = defineEventHandler(
|
|
|
180
291
|
stateOwner,
|
|
181
292
|
);
|
|
182
293
|
|
|
183
|
-
|
|
294
|
+
if (!addAccount) {
|
|
295
|
+
const identity = await exchangeIdentityCode(code, redirectUri);
|
|
296
|
+
const { sessionToken } = await createOAuthSession(
|
|
297
|
+
event,
|
|
298
|
+
identity.email,
|
|
299
|
+
{
|
|
300
|
+
hasProductionSession,
|
|
301
|
+
desktop,
|
|
302
|
+
trackSignup: {
|
|
303
|
+
authProvider: "google",
|
|
304
|
+
authUserId: identity.id,
|
|
305
|
+
name: identity.name,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (flowId && sessionToken) {
|
|
311
|
+
setDesktopExchange(flowId, sessionToken, identity.email);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return oauthCallbackResponse(event, identity.email, {
|
|
315
|
+
sessionToken,
|
|
316
|
+
desktop,
|
|
317
|
+
returnUrl,
|
|
318
|
+
flowId,
|
|
319
|
+
appName: "Calendar",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 2. Exchange code with Google (template-specific Calendar connect)
|
|
184
324
|
const email = await exchangeCode(code, undefined, redirectUri, owner);
|
|
185
325
|
|
|
186
326
|
// 3. Create session token (after we have the email)
|
|
@@ -46,21 +46,59 @@ interface GoogleTokens {
|
|
|
46
46
|
photoUrl?: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
interface GoogleOAuthCredentials {
|
|
50
|
+
clientId: string;
|
|
51
|
+
clientSecret: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
function optionalString(value: unknown): string | undefined {
|
|
50
55
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function getOAuth2Credentials() {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
if (!clientId || !clientSecret) {
|
|
59
|
+
const credentials = resolveGoogleProviderCredentialCandidates()[0];
|
|
60
|
+
if (!credentials) {
|
|
57
61
|
throw new Error(
|
|
58
62
|
"GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in environment",
|
|
59
63
|
);
|
|
60
64
|
}
|
|
65
|
+
return credentials;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getOAuth2RefreshCredentials() {
|
|
69
|
+
const candidates = resolveGoogleProviderCredentialCandidates();
|
|
70
|
+
if (!candidates.length) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in environment",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return candidates;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readCredentialPair(
|
|
79
|
+
clientIdKey: string,
|
|
80
|
+
clientSecretKey: string,
|
|
81
|
+
): GoogleOAuthCredentials | null {
|
|
82
|
+
const clientId = process.env[clientIdKey];
|
|
83
|
+
const clientSecret = process.env[clientSecretKey];
|
|
84
|
+
if (!clientId || !clientSecret) return null;
|
|
61
85
|
return { clientId, clientSecret };
|
|
62
86
|
}
|
|
63
87
|
|
|
88
|
+
function resolveGoogleProviderCredentialCandidates(): GoogleOAuthCredentials[] {
|
|
89
|
+
const primary = readCredentialPair(
|
|
90
|
+
"GOOGLE_CLIENT_ID",
|
|
91
|
+
"GOOGLE_CLIENT_SECRET",
|
|
92
|
+
);
|
|
93
|
+
const legacy = readCredentialPair(
|
|
94
|
+
"GOOGLE_LEGACY_CLIENT_ID",
|
|
95
|
+
"GOOGLE_LEGACY_CLIENT_SECRET",
|
|
96
|
+
);
|
|
97
|
+
if (!primary) return legacy ? [legacy] : [];
|
|
98
|
+
if (!legacy || legacy.clientId === primary.clientId) return [primary];
|
|
99
|
+
return [primary, legacy];
|
|
100
|
+
}
|
|
101
|
+
|
|
64
102
|
/**
|
|
65
103
|
* Permanent OAuth refresh failures Google can return. When we hit one of
|
|
66
104
|
* these, the refresh_token is dead — keeping the row around makes
|
|
@@ -316,17 +354,27 @@ async function getValidAccessToken(
|
|
|
316
354
|
);
|
|
317
355
|
}
|
|
318
356
|
try {
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
357
|
+
let lastRefreshError: any;
|
|
358
|
+
for (const { clientId, clientSecret } of getOAuth2RefreshCredentials()) {
|
|
359
|
+
try {
|
|
360
|
+
const oauth2 = createOAuth2Client(clientId, clientSecret, "");
|
|
361
|
+
const newTokens = await oauth2.refreshToken(tokens.refresh_token);
|
|
362
|
+
const merged = { ...tokens, ...newTokens };
|
|
363
|
+
await saveOAuthTokens(
|
|
364
|
+
"google",
|
|
365
|
+
accountId,
|
|
366
|
+
merged as unknown as Record<string, unknown>,
|
|
367
|
+
owner ?? accountId,
|
|
368
|
+
);
|
|
369
|
+
return merged.access_token;
|
|
370
|
+
} catch (err: any) {
|
|
371
|
+
lastRefreshError = err;
|
|
372
|
+
if (!isPermanentRefreshError(err?.message || "")) {
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
throw lastRefreshError;
|
|
330
378
|
} catch (err: any) {
|
|
331
379
|
if (isPermanentRefreshError(err?.message || "")) {
|
|
332
380
|
// Drop the dead row so isOAuthConnected returns false and the UI
|
|
@@ -860,6 +908,7 @@ export async function listOverlayEvents(
|
|
|
860
908
|
eventType: event.eventType || "default",
|
|
861
909
|
accountEmail: undefined,
|
|
862
910
|
overlayEmail,
|
|
911
|
+
...mapColor(event),
|
|
863
912
|
attendees: mapAttendees(event),
|
|
864
913
|
organizer: mapOrganizer(event),
|
|
865
914
|
createdAt: event.created || new Date().toISOString(),
|
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
import { createAuthPlugin } from "@agent-native/core/server";
|
|
2
2
|
|
|
3
|
-
// Calendar
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Calendar/Contacts/Directory scopes are requested up front during the
|
|
8
|
-
// primary "Sign in with Google" flow. Tokens land in the framework's
|
|
9
|
-
// `oauth_tokens` table automatically (via a Better Auth account hook)
|
|
10
|
-
// so the existing `templates/calendar/server/lib/google-calendar.ts`
|
|
11
|
-
// client works on first sign-in — no separate "Connect Google" page
|
|
12
|
-
// needed. The template-specific routes under `/_agent-native/google/*`
|
|
13
|
-
// remain available for "add another account" flows.
|
|
3
|
+
// Calendar keeps Google as the primary auth surface, but the first sign-in is
|
|
4
|
+
// identity-only. The template-owned `/_agent-native/google/*` routes request
|
|
5
|
+
// Calendar/Contacts/Directory scopes only after there is a signed-in owner, so
|
|
6
|
+
// basic login stays isolated from product API verification/blocking issues.
|
|
14
7
|
export default createAuthPlugin({
|
|
15
8
|
googleOnly: true,
|
|
16
9
|
mountGoogleOAuthRoutes: false,
|
|
17
|
-
googleScopes: [
|
|
18
|
-
"https://www.googleapis.com/auth/calendar.readonly",
|
|
19
|
-
"https://www.googleapis.com/auth/calendar.events",
|
|
20
|
-
"https://www.googleapis.com/auth/userinfo.email",
|
|
21
|
-
"https://www.googleapis.com/auth/userinfo.profile",
|
|
22
|
-
"https://www.googleapis.com/auth/directory.readonly",
|
|
23
|
-
"https://www.googleapis.com/auth/contacts.readonly",
|
|
24
|
-
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
25
|
-
],
|
|
26
10
|
marketing: {
|
|
27
11
|
appName: "Agent-Native Calendar",
|
|
28
12
|
tagline:
|
|
@@ -17,6 +17,10 @@ export interface CalendarEvent {
|
|
|
17
17
|
accountEmail?: string;
|
|
18
18
|
/** Set when this event belongs to an overlaid person's calendar */
|
|
19
19
|
overlayEmail?: string;
|
|
20
|
+
/** Client-only marker for overlaid calendar ownership */
|
|
21
|
+
ownerColor?: string;
|
|
22
|
+
/** Client-only display name for the overlaid calendar owner */
|
|
23
|
+
ownerName?: string;
|
|
20
24
|
color?: string;
|
|
21
25
|
/** Google Calendar event color id (1-11). */
|
|
22
26
|
colorId?: string;
|
|
@@ -48,6 +48,23 @@ Loom iframe and the native Clips editor is hidden for those recordings. If the
|
|
|
48
48
|
user needs Clips-native trimming, exports, frame extraction, or upload-based
|
|
49
49
|
transcription, ask them to upload the original video file instead.
|
|
50
50
|
|
|
51
|
+
## Browser diagnostics and recorder install options
|
|
52
|
+
|
|
53
|
+
Browser recordings can save bounded, redacted diagnostics through
|
|
54
|
+
`createBrowserDiagnosticsCapture`: console messages plus fetch/XHR method, URL,
|
|
55
|
+
status, duration, and errors. The browser recorder only captures activity from
|
|
56
|
+
the recorder page itself. The Clips Chrome extension is the active-tab path for
|
|
57
|
+
browser logs: it launches `/record` with an extension capture session and passes
|
|
58
|
+
`developerLogs=1/0`, then saves diagnostics with source `extension`.
|
|
59
|
+
|
|
60
|
+
Keep the public Chrome extension option hidden until the Web Store listing is
|
|
61
|
+
live. UI prompts that otherwise say "Download desktop app" should use the shared
|
|
62
|
+
install-choice component so `VITE_CLIPS_CHROME_EXTENSION_ENABLED=1` can reveal
|
|
63
|
+
two options: Chrome extension for browser logs, and desktop app for the most
|
|
64
|
+
seamless native capture. Also set `VITE_CLIPS_CHROME_EXTENSION_URL` when the
|
|
65
|
+
extension URL exists; leave the boolean unset while preparing/submitting the
|
|
66
|
+
listing.
|
|
67
|
+
|
|
51
68
|
## Pause / resume
|
|
52
69
|
|
|
53
70
|
`MediaRecorder.pause()` / `.resume()` are supported in all evergreen browsers. Keep a single `MediaRecorder` instance across pauses — don't tear down the stream, or the permission prompt will fire again. While paused, the upload worker keeps draining its buffer so we catch up before the user stops.
|
|
@@ -21,7 +21,8 @@ import { accessFilter } from "@agent-native/core/sharing";
|
|
|
21
21
|
import { readAppSecret, writeAppSecret } from "@agent-native/core/secrets";
|
|
22
22
|
import {
|
|
23
23
|
listEvents,
|
|
24
|
-
|
|
24
|
+
resolveGoogleOAuthCredentialCandidates,
|
|
25
|
+
refreshAccessTokenWithFallback,
|
|
25
26
|
pickJoinUrl,
|
|
26
27
|
type CalendarEvent,
|
|
27
28
|
} from "../server/lib/google-calendar-client.js";
|
|
@@ -55,9 +56,8 @@ async function resolveAccessToken(args: {
|
|
|
55
56
|
accessRef: string | null;
|
|
56
57
|
refreshRef: string | null;
|
|
57
58
|
}): Promise<string | null> {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (!clientId || !clientSecret) return null;
|
|
59
|
+
const credentialCandidates = resolveGoogleOAuthCredentialCandidates();
|
|
60
|
+
if (!credentialCandidates.length) return null;
|
|
61
61
|
|
|
62
62
|
let bundle: AccessTokenBundle | null = null;
|
|
63
63
|
if (args.accessRef) {
|
|
@@ -93,10 +93,9 @@ async function resolveAccessToken(args: {
|
|
|
93
93
|
|
|
94
94
|
let refreshed;
|
|
95
95
|
try {
|
|
96
|
-
refreshed = await
|
|
96
|
+
refreshed = await refreshAccessTokenWithFallback({
|
|
97
97
|
refreshToken: refreshSecret.value,
|
|
98
|
-
|
|
99
|
-
clientSecret,
|
|
98
|
+
credentials: credentialCandidates,
|
|
100
99
|
});
|
|
101
100
|
} catch {
|
|
102
101
|
return null;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import {
|
|
4
|
+
IconBrowser,
|
|
5
|
+
IconChevronDown,
|
|
6
|
+
IconDeviceDesktop,
|
|
7
|
+
IconExternalLink,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
import { Button, type ButtonProps } from "@/components/ui/button";
|
|
10
|
+
import {
|
|
11
|
+
Popover,
|
|
12
|
+
PopoverContent,
|
|
13
|
+
PopoverTrigger,
|
|
14
|
+
} from "@/components/ui/popover";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
import {
|
|
17
|
+
clipsChromeExtensionEnabled,
|
|
18
|
+
clipsChromeExtensionUrl,
|
|
19
|
+
} from "@/lib/capture-install-options";
|
|
20
|
+
|
|
21
|
+
type PopoverPlacement = {
|
|
22
|
+
align?: "start" | "center" | "end";
|
|
23
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CaptureInstallButtonProps = Omit<ButtonProps, "asChild"> &
|
|
27
|
+
PopoverPlacement & {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
desktopHref?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type CaptureInstallInlineLinkProps = PopoverPlacement & {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
desktopHref?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function InstallOptionsContent({ desktopHref = "/download" }) {
|
|
39
|
+
const chromeAvailable = Boolean(clipsChromeExtensionUrl);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="grid gap-2">
|
|
43
|
+
<div className="px-1 pb-1">
|
|
44
|
+
<div className="text-sm font-medium text-popover-foreground">
|
|
45
|
+
Choose your recorder
|
|
46
|
+
</div>
|
|
47
|
+
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
48
|
+
Use Chrome when you need browser logs, or desktop for the smoothest
|
|
49
|
+
everyday capture.
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{chromeAvailable ? (
|
|
54
|
+
<a
|
|
55
|
+
href={clipsChromeExtensionUrl ?? undefined}
|
|
56
|
+
target="_blank"
|
|
57
|
+
rel="noreferrer"
|
|
58
|
+
className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
|
|
59
|
+
>
|
|
60
|
+
<IconBrowser className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
|
61
|
+
<span className="min-w-0 flex-1">
|
|
62
|
+
<span className="block text-sm font-medium">Chrome extension</span>
|
|
63
|
+
<span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
|
|
64
|
+
Best when you want redacted console and network diagnostics from
|
|
65
|
+
the browser tab.
|
|
66
|
+
</span>
|
|
67
|
+
</span>
|
|
68
|
+
<IconExternalLink className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
69
|
+
</a>
|
|
70
|
+
) : (
|
|
71
|
+
<div className="flex items-start gap-3 rounded-md border border-dashed border-border p-3 text-left opacity-70">
|
|
72
|
+
<IconBrowser className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
73
|
+
<span className="min-w-0 flex-1">
|
|
74
|
+
<span className="block text-sm font-medium">Chrome extension</span>
|
|
75
|
+
<span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
|
|
76
|
+
Browser logs option is ready, pending the Chrome Web Store URL.
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<Link
|
|
83
|
+
to={desktopHref}
|
|
84
|
+
className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
|
|
85
|
+
>
|
|
86
|
+
<IconDeviceDesktop className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
|
87
|
+
<span className="min-w-0 flex-1">
|
|
88
|
+
<span className="block text-sm font-medium">Desktop app</span>
|
|
89
|
+
<span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
|
|
90
|
+
Most seamless for global shortcuts, menu-bar recording, meetings,
|
|
91
|
+
and repeat captures.
|
|
92
|
+
</span>
|
|
93
|
+
</span>
|
|
94
|
+
</Link>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function CaptureInstallButton({
|
|
100
|
+
children,
|
|
101
|
+
className,
|
|
102
|
+
desktopHref = "/download",
|
|
103
|
+
align = "end",
|
|
104
|
+
side = "bottom",
|
|
105
|
+
...buttonProps
|
|
106
|
+
}: CaptureInstallButtonProps) {
|
|
107
|
+
if (!clipsChromeExtensionEnabled) {
|
|
108
|
+
return (
|
|
109
|
+
<Button asChild className={className} {...buttonProps}>
|
|
110
|
+
<Link to={desktopHref}>{children}</Link>
|
|
111
|
+
</Button>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Popover>
|
|
117
|
+
<PopoverTrigger asChild>
|
|
118
|
+
<Button className={className} {...buttonProps}>
|
|
119
|
+
{children}
|
|
120
|
+
<IconChevronDown className="h-3.5 w-3.5" />
|
|
121
|
+
</Button>
|
|
122
|
+
</PopoverTrigger>
|
|
123
|
+
<PopoverContent align={align} side={side} className="w-80 p-3">
|
|
124
|
+
<InstallOptionsContent desktopHref={desktopHref} />
|
|
125
|
+
</PopoverContent>
|
|
126
|
+
</Popover>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function CaptureInstallInlineLink({
|
|
131
|
+
children,
|
|
132
|
+
className,
|
|
133
|
+
desktopHref = "/download",
|
|
134
|
+
align = "start",
|
|
135
|
+
side = "bottom",
|
|
136
|
+
}: CaptureInstallInlineLinkProps) {
|
|
137
|
+
if (!clipsChromeExtensionEnabled) {
|
|
138
|
+
return (
|
|
139
|
+
<Link to={desktopHref} className={className}>
|
|
140
|
+
{children}
|
|
141
|
+
</Link>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Popover>
|
|
147
|
+
<PopoverTrigger asChild>
|
|
148
|
+
<button type="button" className={cn("cursor-pointer", className)}>
|
|
149
|
+
{children}
|
|
150
|
+
</button>
|
|
151
|
+
</PopoverTrigger>
|
|
152
|
+
<PopoverContent align={align} side={side} className="w-80 p-3">
|
|
153
|
+
<InstallOptionsContent desktopHref={desktopHref} />
|
|
154
|
+
</PopoverContent>
|
|
155
|
+
</Popover>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -53,6 +53,10 @@ import { FolderTree, type FolderNode } from "./folder-tree";
|
|
|
53
53
|
import { SearchBar } from "./search-bar";
|
|
54
54
|
import { PageHeaderSlotProvider } from "./page-header";
|
|
55
55
|
import { CreateSpaceDialog } from "./create-space-dialog";
|
|
56
|
+
import {
|
|
57
|
+
CaptureInstallButton,
|
|
58
|
+
CaptureInstallInlineLink,
|
|
59
|
+
} from "@/components/capture-install-options";
|
|
56
60
|
import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
|
|
57
61
|
import { toast } from "sonner";
|
|
58
62
|
import {
|
|
@@ -477,13 +481,10 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
477
481
|
<>
|
|
478
482
|
<div className="shrink-0 space-y-1.5 border-t border-border px-2 py-1.5">
|
|
479
483
|
{shouldShowSidebarLink && (
|
|
480
|
-
<
|
|
481
|
-
to="/download"
|
|
482
|
-
className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-foreground hover:bg-accent/60"
|
|
483
|
-
>
|
|
484
|
+
<CaptureInstallInlineLink className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-foreground hover:bg-accent/60">
|
|
484
485
|
<IconAppWindow className="h-4 w-4" />
|
|
485
486
|
Get desktop app
|
|
486
|
-
</
|
|
487
|
+
</CaptureInstallInlineLink>
|
|
487
488
|
)}
|
|
488
489
|
<SearchBar />
|
|
489
490
|
</div>
|
|
@@ -532,9 +533,9 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
532
533
|
Record from the menu bar, global shortcut, auto-updates.
|
|
533
534
|
</span>
|
|
534
535
|
</div>
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
</
|
|
536
|
+
<CaptureInstallButton size="sm" className="shrink-0">
|
|
537
|
+
Download
|
|
538
|
+
</CaptureInstallButton>
|
|
538
539
|
<Tooltip>
|
|
539
540
|
<TooltipTrigger asChild>
|
|
540
541
|
<Button
|
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
IconUpload,
|
|
19
19
|
IconVideo,
|
|
20
20
|
} from "@tabler/icons-react";
|
|
21
|
-
import { agentNativePath
|
|
21
|
+
import { agentNativePath } from "@agent-native/core/client";
|
|
22
22
|
import { Button } from "@/components/ui/button";
|
|
23
23
|
import { Input } from "@/components/ui/input";
|
|
24
|
+
import { CaptureInstallInlineLink } from "@/components/capture-install-options";
|
|
24
25
|
import {
|
|
25
26
|
Popover,
|
|
26
27
|
PopoverContent,
|
|
@@ -724,12 +725,9 @@ export function PreRecordPanel({
|
|
|
724
725
|
{micAccessError ? (
|
|
725
726
|
<p className="text-[11px] leading-snug text-muted-foreground">
|
|
726
727
|
{micAccessError}{" "}
|
|
727
|
-
<
|
|
728
|
-
href={appPath("/download")}
|
|
729
|
-
className="text-foreground underline-offset-4 hover:underline"
|
|
730
|
-
>
|
|
728
|
+
<CaptureInstallInlineLink className="text-foreground underline-offset-4 hover:underline">
|
|
731
729
|
Try Clips Desktop.
|
|
732
|
-
</
|
|
730
|
+
</CaptureInstallInlineLink>
|
|
733
731
|
</p>
|
|
734
732
|
) : null}
|
|
735
733
|
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function isTruthy(value: unknown): boolean {
|
|
2
|
+
if (typeof value !== "string") return false;
|
|
3
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const chromeExtensionUrl =
|
|
7
|
+
import.meta.env.VITE_CLIPS_CHROME_EXTENSION_URL?.trim() ?? "";
|
|
8
|
+
|
|
9
|
+
export const clipsChromeExtensionEnabled = isTruthy(
|
|
10
|
+
import.meta.env.VITE_CLIPS_CHROME_EXTENSION_ENABLED,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export const clipsChromeExtensionUrl = chromeExtensionUrl || null;
|