@agent-native/core 0.77.4 → 0.77.7
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +31 -0
- package/corpus/core/package.json +3 -3
- package/corpus/core/src/agent/production-agent.ts +35 -0
- package/corpus/core/src/agent/run-store.ts +2 -0
- package/corpus/core/src/agent/thread-data-builder.ts +21 -2
- package/corpus/core/src/cli/create.ts +35 -1
- package/corpus/core/src/cli/skills.ts +8 -0
- package/corpus/core/src/client/AgentPanel.tsx +57 -25
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -1
- package/corpus/core/src/client/analytics.ts +24 -2
- package/corpus/core/src/client/changelog/Changelog.tsx +22 -10
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +405 -35
- package/corpus/core/src/client/use-agent-engine-configured.ts +29 -15
- package/corpus/core/src/client/use-chat-threads.ts +104 -55
- package/corpus/core/src/server/analytics.ts +27 -3
- package/corpus/core/src/sharing/access.ts +35 -13
- package/corpus/core/src/sharing/actions/share-resource.ts +32 -9
- package/corpus/core/src/sharing/actions/unshare-resource.ts +25 -2
- package/corpus/core/src/sharing/registry.ts +9 -0
- package/corpus/core/src/vite/client.ts +3 -0
- package/corpus/templates/analytics/actions/data-source-status.ts +25 -14
- package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +2 -2
- package/corpus/templates/analytics/app/lib/data-source-status.ts +47 -6
- package/corpus/templates/analytics/app/lib/data-sources.ts +14 -3
- package/corpus/templates/analytics/app/pages/DataSources.tsx +24 -5
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +15 -43
- package/corpus/templates/analytics/changelog/2026-06-25-fixed-hubspot-data-source-status-so-private-app-tokens-legac.md +6 -0
- package/corpus/templates/analytics/netlify.toml +1 -0
- package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -1
- package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +12 -3
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +5 -2
- package/corpus/templates/assets/changelog/2026-06-25-create-now-returns-from-agent-settings-to-the-asset-chat-eve.md +6 -0
- package/corpus/templates/assets/netlify.toml +1 -0
- package/corpus/templates/brain/netlify.toml +1 -0
- package/corpus/templates/calendar/app/i18n-data.ts +12 -0
- package/corpus/templates/calendar/app/lib/clipboard.ts +58 -0
- package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +15 -9
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +6 -2
- package/corpus/templates/calendar/changelog/2026-06-25-booking-link-copy-buttons-no-longer-fail-when-clipboard-perm.md +6 -0
- package/corpus/templates/calendar/netlify.toml +1 -0
- package/corpus/templates/chat/netlify.toml +1 -0
- package/corpus/templates/clips/AGENTS.md +9 -7
- package/corpus/templates/clips/actions/add-vocabulary-term.ts +2 -2
- package/corpus/templates/clips/actions/archive-recording.ts +5 -2
- package/corpus/templates/clips/actions/clear-edits.ts +5 -2
- package/corpus/templates/clips/actions/create-folder.ts +7 -2
- package/corpus/templates/clips/actions/delete-folder.ts +7 -4
- package/corpus/templates/clips/actions/delete-recording-permanent.ts +5 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +5 -2
- package/corpus/templates/clips/actions/import-loom-recording.ts +2 -1
- package/corpus/templates/clips/actions/list-notifications.ts +9 -5
- package/corpus/templates/clips/actions/list-organization-state.ts +2 -1
- package/corpus/templates/clips/actions/list-recordings.ts +4 -1
- package/corpus/templates/clips/actions/move-recording.ts +5 -1
- package/corpus/templates/clips/actions/rename-folder.ts +6 -3
- package/corpus/templates/clips/actions/request-transcript.ts +5 -2
- package/corpus/templates/clips/actions/restore-recording.ts +5 -2
- package/corpus/templates/clips/actions/save-browser-diagnostics.ts +21 -8
- package/corpus/templates/clips/actions/set-chapters.ts +5 -2
- package/corpus/templates/clips/actions/set-thumbnail.ts +5 -2
- package/corpus/templates/clips/actions/split-recording.ts +5 -2
- package/corpus/templates/clips/actions/stitch-recordings.ts +6 -2
- package/corpus/templates/clips/actions/trash-recording.ts +5 -2
- package/corpus/templates/clips/actions/undo-edit.ts +5 -2
- package/corpus/templates/clips/actions/view-screen.ts +2 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +24 -7
- package/corpus/templates/clips/app/i18n/ar-SA.ts +14 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +14 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +14 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +14 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +14 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +14 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +14 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +14 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +14 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +14 -0
- package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +5 -1
- package/corpus/templates/clips/app/root.tsx +2 -2
- package/corpus/templates/clips/app/routes/_app.extensions.$id.tsx +4 -1
- package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +4 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +12 -5
- package/corpus/templates/clips/changelog/2026-06-25-chrome-extension-recordings-now-upload-large-capture-bursts.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-console-diagnostics-keep-more-useful-non-secret-details-when.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-fixed-owner-recognition-so-clips-created-from-desktop-and-we.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-localized-clips-settings-changelog-and-extension-sidebar-lab.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +3 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +85 -9
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +55 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +79 -3
- package/corpus/templates/clips/chrome-extension/src/options.ts +10 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +6 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +8 -1
- package/corpus/templates/clips/chrome-extension/src/popup.ts +65 -39
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +206 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +47 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +4 -2
- package/corpus/templates/clips/desktop/src/styles.css +15 -1
- package/corpus/templates/clips/netlify.toml +1 -0
- package/corpus/templates/clips/server/db/index.ts +1 -0
- package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +35 -23
- package/corpus/templates/clips/server/lib/public-agent-context.ts +8 -1
- package/corpus/templates/clips/server/lib/recordings.ts +18 -2
- package/corpus/templates/clips/server/lib/slack-oauth.ts +9 -6
- package/corpus/templates/clips/server/plugins/onboarding-calendar.ts +6 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +2 -2
- package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +5 -2
- package/corpus/templates/clips/shared/browser-diagnostics.ts +14 -4
- package/corpus/templates/content/netlify.toml +1 -0
- package/corpus/templates/design/netlify.toml +1 -0
- package/corpus/templates/dispatch/netlify.toml +1 -0
- package/corpus/templates/forms/netlify.toml +1 -0
- package/corpus/templates/macros/netlify.toml +1 -0
- package/corpus/templates/mail/app/components/email/EmailList.tsx +7 -3
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +4 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +10 -2
- package/corpus/templates/mail/app/hooks/use-compose-state.ts +15 -5
- package/corpus/templates/mail/app/hooks/use-google-auth.ts +47 -9
- package/corpus/templates/mail/changelog/2026-06-25-account-avatars-stay-stable-while-mail-refreshes-google-acco.md +6 -0
- package/corpus/templates/mail/changelog/2026-06-25-mail-background-draft-saves-and-thread-prefetches-no-longer.md +6 -0
- package/corpus/templates/mail/netlify.toml +1 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +8 -0
- package/corpus/templates/plan/netlify.toml +1 -0
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/videos/netlify.toml +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +31 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +2 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +2 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +23 -2
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +28 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +8 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +8 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +29 -15
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +4 -4
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +22 -2
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/changelog/Changelog.d.ts +7 -2
- package/dist/client/changelog/Changelog.d.ts.map +1 -1
- package/dist/client/changelog/Changelog.js +9 -7
- package/dist/client/changelog/Changelog.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +339 -11
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +23 -13
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +4 -0
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +49 -4
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/analytics.d.ts +4 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +19 -3
- package/dist/server/analytics.js.map +1 -1
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +26 -12
- package/dist/sharing/access.js.map +1 -1
- package/dist/sharing/actions/share-resource.js +20 -9
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/actions/unshare-resource.js +13 -2
- package/dist/sharing/actions/unshare-resource.js.map +1 -1
- package/dist/sharing/registry.d.ts +9 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +3 -3
|
@@ -13,7 +13,11 @@ import { getQuery, type H3Event } from "h3";
|
|
|
13
13
|
|
|
14
14
|
import { getDb, schema } from "../db/index.js";
|
|
15
15
|
import { exchangeCode, getUserInfo } from "./google-calendar-client.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getActiveOrganizationId,
|
|
18
|
+
normalizeOwnerEmail,
|
|
19
|
+
ownerEmailMatches,
|
|
20
|
+
} from "./recordings.js";
|
|
17
21
|
|
|
18
22
|
export const CLIPS_GOOGLE_OAUTH_APP_ID = process.env.APP_NAME || "clips";
|
|
19
23
|
|
|
@@ -54,6 +58,7 @@ export async function handleGoogleCalendarCallback(
|
|
|
54
58
|
"Your session expired during the OAuth flow. Sign in again and retry.",
|
|
55
59
|
);
|
|
56
60
|
}
|
|
61
|
+
const ownerEmail = normalizeOwnerEmail(userEmail);
|
|
57
62
|
|
|
58
63
|
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
59
64
|
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
@@ -63,7 +68,7 @@ export async function handleGoogleCalendarCallback(
|
|
|
63
68
|
);
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
return runWithRequestContext({ userEmail }, async () => {
|
|
71
|
+
return runWithRequestContext({ userEmail: ownerEmail }, async () => {
|
|
67
72
|
const { redirectUri, returnUrl } = state;
|
|
68
73
|
|
|
69
74
|
// 1. Exchange code -> tokens.
|
|
@@ -82,8 +87,29 @@ export async function handleGoogleCalendarCallback(
|
|
|
82
87
|
const externalAccountId = profile.id;
|
|
83
88
|
const accountEmail = profile.email;
|
|
84
89
|
|
|
85
|
-
// 3.
|
|
86
|
-
//
|
|
90
|
+
// 3. Find an existing account case-insensitively so email-casing changes
|
|
91
|
+
// don't create duplicate calendar connections.
|
|
92
|
+
const db = getDb();
|
|
93
|
+
const orgId = await getActiveOrganizationId().catch(() => undefined);
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
const [existing] = await db
|
|
96
|
+
.select({
|
|
97
|
+
id: schema.calendarAccounts.id,
|
|
98
|
+
ownerEmail: schema.calendarAccounts.ownerEmail,
|
|
99
|
+
})
|
|
100
|
+
.from(schema.calendarAccounts)
|
|
101
|
+
.where(
|
|
102
|
+
and(
|
|
103
|
+
eq(schema.calendarAccounts.provider, "google"),
|
|
104
|
+
eq(schema.calendarAccounts.externalAccountId, externalAccountId),
|
|
105
|
+
ownerEmailMatches(schema.calendarAccounts.ownerEmail, ownerEmail),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 4. Persist tokens in app_secrets (encrypted at rest). NEVER write
|
|
110
|
+
// tokens onto the calendar_accounts row. Existing rows may have stored
|
|
111
|
+
// mixed-case owner emails, so keep their secret scope stable.
|
|
112
|
+
const secretScopeEmail = existing?.ownerEmail ?? ownerEmail;
|
|
87
113
|
const accessKey = calendarSecretKey("google", externalAccountId, "access");
|
|
88
114
|
const refreshKey = calendarSecretKey(
|
|
89
115
|
"google",
|
|
@@ -101,7 +127,7 @@ export async function handleGoogleCalendarCallback(
|
|
|
101
127
|
scope: tokens.scope,
|
|
102
128
|
}),
|
|
103
129
|
scope: "user",
|
|
104
|
-
scopeId:
|
|
130
|
+
scopeId: secretScopeEmail,
|
|
105
131
|
description: `Google Calendar access token for ${accountEmail}`,
|
|
106
132
|
});
|
|
107
133
|
if (tokens.refresh_token) {
|
|
@@ -109,26 +135,12 @@ export async function handleGoogleCalendarCallback(
|
|
|
109
135
|
key: refreshKey,
|
|
110
136
|
value: tokens.refresh_token,
|
|
111
137
|
scope: "user",
|
|
112
|
-
scopeId:
|
|
138
|
+
scopeId: secretScopeEmail,
|
|
113
139
|
description: `Google Calendar refresh token for ${accountEmail}`,
|
|
114
140
|
});
|
|
115
141
|
}
|
|
116
142
|
|
|
117
|
-
//
|
|
118
|
-
const db = getDb();
|
|
119
|
-
const orgId = await getActiveOrganizationId().catch(() => undefined);
|
|
120
|
-
const now = new Date().toISOString();
|
|
121
|
-
const [existing] = await db
|
|
122
|
-
.select({ id: schema.calendarAccounts.id })
|
|
123
|
-
.from(schema.calendarAccounts)
|
|
124
|
-
.where(
|
|
125
|
-
and(
|
|
126
|
-
eq(schema.calendarAccounts.provider, "google"),
|
|
127
|
-
eq(schema.calendarAccounts.externalAccountId, externalAccountId),
|
|
128
|
-
eq(schema.calendarAccounts.ownerEmail, userEmail),
|
|
129
|
-
),
|
|
130
|
-
);
|
|
131
|
-
|
|
143
|
+
// 5. Upsert the calendar_accounts row.
|
|
132
144
|
if (existing) {
|
|
133
145
|
await db
|
|
134
146
|
.update(schema.calendarAccounts)
|
|
@@ -160,13 +172,13 @@ export async function handleGoogleCalendarCallback(
|
|
|
160
172
|
lastSyncError: null,
|
|
161
173
|
createdAt: now,
|
|
162
174
|
updatedAt: now,
|
|
163
|
-
ownerEmail
|
|
175
|
+
ownerEmail,
|
|
164
176
|
orgId: orgId ?? null,
|
|
165
177
|
visibility: "private",
|
|
166
178
|
} as any);
|
|
167
179
|
}
|
|
168
180
|
|
|
169
|
-
return oauthCallbackResponse(event, accountEmail ||
|
|
181
|
+
return oauthCallbackResponse(event, accountEmail || ownerEmail, {
|
|
170
182
|
desktop,
|
|
171
183
|
addAccount: true, // close-tab page; never switch the active session
|
|
172
184
|
returnUrl,
|
|
@@ -51,6 +51,13 @@ export type PublicAgentAccessResult =
|
|
|
51
51
|
|
|
52
52
|
const DEFAULT_MAX_AGENT_FRAME_MEDIA_BYTES = 200 * 1024 * 1024;
|
|
53
53
|
|
|
54
|
+
function sameOwnerEmail(
|
|
55
|
+
a: string | null | undefined,
|
|
56
|
+
b: string | null | undefined,
|
|
57
|
+
): boolean {
|
|
58
|
+
return !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
export class RecordingMediaFetchError extends Error {
|
|
55
62
|
constructor(
|
|
56
63
|
message: string,
|
|
@@ -195,7 +202,7 @@ export async function loadPublicAgentAccess(
|
|
|
195
202
|
|
|
196
203
|
const session = await getSession(event).catch(() => null);
|
|
197
204
|
const viewerIsOwner = Boolean(
|
|
198
|
-
session?.email && session.email
|
|
205
|
+
session?.email && sameOwnerEmail(session.email, recording.ownerEmail),
|
|
199
206
|
);
|
|
200
207
|
|
|
201
208
|
if (recording.visibility !== "public" && !viewerIsOwner) {
|
|
@@ -13,7 +13,23 @@ import { getDb, schema } from "../db/index.js";
|
|
|
13
13
|
export function getCurrentOwnerEmail(): string {
|
|
14
14
|
const email = getRequestUserEmail();
|
|
15
15
|
if (!email) throw new Error("no authenticated user");
|
|
16
|
-
return email;
|
|
16
|
+
return normalizeOwnerEmail(email);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeOwnerEmail(email: string): string {
|
|
20
|
+
return email.trim().toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ownerEmailMatches(column: unknown, email: string) {
|
|
24
|
+
return sql`lower(${column as any}) = ${normalizeOwnerEmail(email)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sameOwnerEmail(
|
|
28
|
+
a: string | null | undefined,
|
|
29
|
+
b: string | null | undefined,
|
|
30
|
+
): boolean {
|
|
31
|
+
if (!a || !b) return false;
|
|
32
|
+
return normalizeOwnerEmail(a) === normalizeOwnerEmail(b);
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
export async function getEventOwnerContext(event: H3Event): Promise<{
|
|
@@ -258,7 +274,7 @@ export async function getRecordingOrThrow(id: string): Promise<RecordingRow> {
|
|
|
258
274
|
eq(schema.recordings.id, id),
|
|
259
275
|
// visibility check happens at the action layer via the framework
|
|
260
276
|
// sharing helpers; this is just the ownership-or-visible fallback.
|
|
261
|
-
|
|
277
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
262
278
|
),
|
|
263
279
|
);
|
|
264
280
|
if (!row) throw new Error(`Recording not found: ${id}`);
|
|
@@ -25,6 +25,8 @@ import { getDb, schema } from "../db/index.js";
|
|
|
25
25
|
import {
|
|
26
26
|
getActiveOrganizationId,
|
|
27
27
|
getOrganizationRoleForEmail,
|
|
28
|
+
ownerEmailMatches,
|
|
29
|
+
sameOwnerEmail,
|
|
28
30
|
} from "./recordings.js";
|
|
29
31
|
import type { SlackLinkSharedPayload } from "./slack-unfurls.js";
|
|
30
32
|
|
|
@@ -388,12 +390,13 @@ export async function listVisibleSlackInstallations(options: {
|
|
|
388
390
|
userEmail: string;
|
|
389
391
|
orgId?: string | null;
|
|
390
392
|
}): Promise<SlackInstallationListItem[]> {
|
|
393
|
+
const ownerWhere = ownerEmailMatches(
|
|
394
|
+
schema.slackInstallations.ownerEmail,
|
|
395
|
+
options.userEmail,
|
|
396
|
+
);
|
|
391
397
|
const scopeWhere = options.orgId
|
|
392
|
-
? or(
|
|
393
|
-
|
|
394
|
-
eq(schema.slackInstallations.ownerEmail, options.userEmail),
|
|
395
|
-
)
|
|
396
|
-
: eq(schema.slackInstallations.ownerEmail, options.userEmail);
|
|
398
|
+
? or(eq(schema.slackInstallations.orgId, options.orgId), ownerWhere)
|
|
399
|
+
: ownerWhere;
|
|
397
400
|
|
|
398
401
|
return getDb()
|
|
399
402
|
.select({
|
|
@@ -484,7 +487,7 @@ export async function disconnectSlackInstallation(options: {
|
|
|
484
487
|
.limit(1);
|
|
485
488
|
if (!row) return null;
|
|
486
489
|
|
|
487
|
-
const canDisconnectOwn = row.ownerEmail
|
|
490
|
+
const canDisconnectOwn = sameOwnerEmail(row.ownerEmail, options.userEmail);
|
|
488
491
|
let canDisconnectOrg = false;
|
|
489
492
|
if (row.orgId) {
|
|
490
493
|
const role = await getOrganizationRoleForEmail(
|
|
@@ -79,11 +79,15 @@ export default async (): Promise<void> => {
|
|
|
79
79
|
() => ({ db: null as any, schema: null as any }),
|
|
80
80
|
);
|
|
81
81
|
if (!db || !schema?.calendarAccounts) return false;
|
|
82
|
-
const {
|
|
82
|
+
const { ownerEmailMatches } = await import(
|
|
83
|
+
"../lib/recordings.js" as string
|
|
84
|
+
);
|
|
83
85
|
const rows = await db
|
|
84
86
|
.select({ id: schema.calendarAccounts.id })
|
|
85
87
|
.from(schema.calendarAccounts)
|
|
86
|
-
.where(
|
|
88
|
+
.where(
|
|
89
|
+
ownerEmailMatches(schema.calendarAccounts.ownerEmail, userEmail),
|
|
90
|
+
)
|
|
87
91
|
.limit(1);
|
|
88
92
|
return rows.length > 0;
|
|
89
93
|
} catch {
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "../../../shared/transcript-segments.js";
|
|
34
34
|
import { getDb, schema } from "../../db/index.js";
|
|
35
35
|
import { resolvePlayerVideoUrl } from "../../lib/player-video-url.js";
|
|
36
|
-
import { parseSpaceIds } from "../../lib/recordings.js";
|
|
36
|
+
import { parseSpaceIds, sameOwnerEmail } from "../../lib/recordings.js";
|
|
37
37
|
import { verifySharePassword } from "../../lib/share-password.js";
|
|
38
38
|
|
|
39
39
|
function appPath(path: string): string {
|
|
@@ -142,7 +142,7 @@ export default defineEventHandler(async (event) => {
|
|
|
142
142
|
|
|
143
143
|
const session = await getSession(event).catch(() => null);
|
|
144
144
|
const viewerIsOwner = Boolean(
|
|
145
|
-
session?.email && session.email
|
|
145
|
+
session?.email && sameOwnerEmail(session.email, rec.ownerEmail),
|
|
146
146
|
);
|
|
147
147
|
|
|
148
148
|
if (rec.visibility !== "public" && !viewerIsOwner) {
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
|
|
25
25
|
import { parseEdits } from "../../../../../app/lib/timestamp-mapping.js";
|
|
26
26
|
import { getDb, schema } from "../../../../db/index.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
getEventOwnerContext,
|
|
29
|
+
ownerEmailMatches,
|
|
30
|
+
} from "../../../../lib/recordings.js";
|
|
28
31
|
|
|
29
32
|
const MAX_THUMBNAIL_BYTES = 2 * 1024 * 1024;
|
|
30
33
|
|
|
@@ -85,7 +88,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
85
88
|
.where(
|
|
86
89
|
and(
|
|
87
90
|
eq(schema.recordings.id, recordingId),
|
|
88
|
-
|
|
91
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
89
92
|
),
|
|
90
93
|
);
|
|
91
94
|
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
} from "h3";
|
|
21
21
|
|
|
22
22
|
import { getDb, schema } from "../../../../db/index.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
getEventOwnerContext,
|
|
25
|
+
ownerEmailMatches,
|
|
26
|
+
} from "../../../../lib/recordings.js";
|
|
24
27
|
|
|
25
28
|
export default defineEventHandler(async (event: H3Event) => {
|
|
26
29
|
const recordingId = getRouterParam(event, "recordingId");
|
|
@@ -47,7 +50,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
47
50
|
.where(
|
|
48
51
|
and(
|
|
49
52
|
eq(schema.recordings.id, recordingId),
|
|
50
|
-
|
|
53
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
51
54
|
),
|
|
52
55
|
);
|
|
53
56
|
|
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
import finalizeRecording from "../../../../../actions/finalize-recording.js";
|
|
36
36
|
import { getDb, schema } from "../../../../db/index.js";
|
|
37
37
|
import { debugLog } from "../../../../lib/debug.js";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
getEventOwnerContext,
|
|
40
|
+
ownerEmailMatches,
|
|
41
|
+
} from "../../../../lib/recordings.js";
|
|
39
42
|
import {
|
|
40
43
|
shouldRejectVideoUploadWithoutStorage,
|
|
41
44
|
STORAGE_SETUP_REQUIRED_REASON,
|
|
@@ -156,7 +159,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
156
159
|
.where(
|
|
157
160
|
and(
|
|
158
161
|
eq(schema.recordings.id, recordingId),
|
|
159
|
-
|
|
162
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
160
163
|
),
|
|
161
164
|
);
|
|
162
165
|
|
|
@@ -40,7 +40,10 @@ import {
|
|
|
40
40
|
} from "h3";
|
|
41
41
|
|
|
42
42
|
import { getDb, schema } from "../../../../db/index.js";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
getEventOwnerContext,
|
|
45
|
+
ownerEmailMatches,
|
|
46
|
+
} from "../../../../lib/recordings.js";
|
|
44
47
|
|
|
45
48
|
interface CompressionMeta {
|
|
46
49
|
originalBytes?: number;
|
|
@@ -97,7 +100,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
97
100
|
.where(
|
|
98
101
|
and(
|
|
99
102
|
eq(schema.recordings.id, recordingId),
|
|
100
|
-
|
|
103
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
101
104
|
),
|
|
102
105
|
);
|
|
103
106
|
|
|
@@ -20,14 +20,24 @@ const UNQUOTED_SECRET_VALUE_RE = new RegExp(
|
|
|
20
20
|
"gi",
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
export
|
|
24
|
-
|
|
23
|
+
export interface RedactBrowserDiagnosticStringOptions {
|
|
24
|
+
/** Redact every query value. Use for URL fields; leave off for console text. */
|
|
25
|
+
redactQueryValues?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function redactBrowserDiagnosticString(
|
|
29
|
+
value: string,
|
|
30
|
+
options: RedactBrowserDiagnosticStringOptions = {},
|
|
31
|
+
): string {
|
|
32
|
+
const redacted = value
|
|
25
33
|
.replace(AUTHORIZATION_SCHEME_RE, "$1$2<redacted>")
|
|
26
34
|
.replace(/\b(bearer|basic)\s+[a-z0-9._~+/-]+=*/gi, "$1 <redacted>")
|
|
27
35
|
.replace(DOUBLE_QUOTED_SECRET_VALUE_RE, '$1$2$1$3"<redacted>"')
|
|
28
36
|
.replace(SINGLE_QUOTED_SECRET_VALUE_RE, "$1$2$1$3'<redacted>'")
|
|
29
|
-
.replace(UNQUOTED_SECRET_VALUE_RE, "$1$2$1$3<redacted>")
|
|
30
|
-
|
|
37
|
+
.replace(UNQUOTED_SECRET_VALUE_RE, "$1$2$1$3<redacted>");
|
|
38
|
+
return options.redactQueryValues
|
|
39
|
+
? redacted.replace(/([?&][^=\s&?#]+)=([^&\s#]+)/g, "$1=<redacted>")
|
|
40
|
+
: redacted;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export type BrowserDiagnosticConsoleLevel =
|
|
@@ -569,7 +569,9 @@ export function EmailList({
|
|
|
569
569
|
// Enter on a single focused row is a single-thread action — clear any
|
|
570
570
|
// in-progress multi-selection so shortcuts in detail view start fresh.
|
|
571
571
|
setSelectedIds(new Set());
|
|
572
|
-
void ensureThread(targetThreadId, thread.latestMessage.accountEmail)
|
|
572
|
+
void ensureThread(targetThreadId, thread.latestMessage.accountEmail).catch(
|
|
573
|
+
() => {},
|
|
574
|
+
);
|
|
573
575
|
onNavigateThread?.(targetThreadId);
|
|
574
576
|
navigate(`/${view}/${targetThreadId}${routeSearchSuffix}`);
|
|
575
577
|
if (thread.hasUnread) {
|
|
@@ -618,7 +620,9 @@ export function EmailList({
|
|
|
618
620
|
// instant down the list.
|
|
619
621
|
const nextTid =
|
|
620
622
|
nextThread.latestMessage.threadId || nextThread.latestMessage.id;
|
|
621
|
-
void ensureThread(nextTid, nextThread.latestMessage.accountEmail)
|
|
623
|
+
void ensureThread(nextTid, nextThread.latestMessage.accountEmail).catch(
|
|
624
|
+
() => {},
|
|
625
|
+
);
|
|
622
626
|
} else {
|
|
623
627
|
setFocusedId(null);
|
|
624
628
|
}
|
|
@@ -1065,7 +1069,7 @@ export function EmailList({
|
|
|
1065
1069
|
onDraftOpen(email);
|
|
1066
1070
|
return;
|
|
1067
1071
|
}
|
|
1068
|
-
void ensureThread(targetThreadId, email.accountEmail);
|
|
1072
|
+
void ensureThread(targetThreadId, email.accountEmail).catch(() => {});
|
|
1069
1073
|
onNavigateThread?.(targetThreadId);
|
|
1070
1074
|
navigate(`/${view}/${targetThreadId}${routeSearchSuffix}`);
|
|
1071
1075
|
if (thread.hasUnread) {
|
|
@@ -408,7 +408,10 @@ export function EmailThread({
|
|
|
408
408
|
(t.latestMessage.threadId || t.latestMessage.id) === nextThreadId,
|
|
409
409
|
);
|
|
410
410
|
setSelectedIds?.(new Set());
|
|
411
|
-
void ensureThread(
|
|
411
|
+
void ensureThread(
|
|
412
|
+
nextThreadId,
|
|
413
|
+
nextThread?.latestMessage.accountEmail,
|
|
414
|
+
).catch(() => {});
|
|
412
415
|
onNavigateThread?.(nextThreadId);
|
|
413
416
|
navigate(`/${view}/${nextThreadId}${routeSearchSuffix}`);
|
|
414
417
|
},
|
|
@@ -99,13 +99,21 @@ function AccountAvatar({
|
|
|
99
99
|
fallbackClassName: string;
|
|
100
100
|
}) {
|
|
101
101
|
const [imageFailed, setImageFailed] = useState(false);
|
|
102
|
+
const [stablePhotoUrl, setStablePhotoUrl] = useState(photoUrl ?? null);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!photoUrl || photoUrl === stablePhotoUrl) return;
|
|
106
|
+
setStablePhotoUrl(photoUrl);
|
|
107
|
+
setImageFailed(false);
|
|
108
|
+
}, [photoUrl, stablePhotoUrl]);
|
|
109
|
+
|
|
102
110
|
const shouldLoadRemoteAvatar =
|
|
103
|
-
!!
|
|
111
|
+
!!stablePhotoUrl && !isMcpEmbedSurface() && !imageFailed;
|
|
104
112
|
|
|
105
113
|
if (shouldLoadRemoteAvatar) {
|
|
106
114
|
return (
|
|
107
115
|
<img
|
|
108
|
-
src={
|
|
116
|
+
src={stablePhotoUrl}
|
|
109
117
|
alt=""
|
|
110
118
|
className={imageClassName}
|
|
111
119
|
referrerPolicy="no-referrer"
|
|
@@ -80,6 +80,16 @@ async function saveDraftToEmails(
|
|
|
80
80
|
return result?.draftId;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export async function saveDraftToEmailsBestEffort(
|
|
84
|
+
draft: ComposeState,
|
|
85
|
+
): Promise<string | undefined> {
|
|
86
|
+
try {
|
|
87
|
+
return await saveDraftToEmails(draft);
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
export function useComposeState() {
|
|
84
94
|
const qc = useQueryClient();
|
|
85
95
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
@@ -225,7 +235,7 @@ export function useComposeState() {
|
|
|
225
235
|
).find((d) => d.id === id);
|
|
226
236
|
if (!current || !hasDraftContent(current)) return;
|
|
227
237
|
|
|
228
|
-
|
|
238
|
+
void saveDraftToEmailsBestEffort(current).then((draftId) => {
|
|
229
239
|
if (draftId && draftId !== current.savedDraftId) {
|
|
230
240
|
// Store the Gmail draft ID back so subsequent saves update rather than create
|
|
231
241
|
qc.setQueryData<ComposeState[]>(["compose-drafts"], (old) =>
|
|
@@ -306,8 +316,8 @@ export function useComposeState() {
|
|
|
306
316
|
|
|
307
317
|
// Auto-save to persistent drafts if there's any content
|
|
308
318
|
if (draft && hasDraftContent(draft)) {
|
|
309
|
-
|
|
310
|
-
qc.invalidateQueries({ queryKey: ["emails"] });
|
|
319
|
+
void saveDraftToEmailsBestEffort(draft).then((draftId) => {
|
|
320
|
+
if (draftId) qc.invalidateQueries({ queryKey: ["emails"] });
|
|
311
321
|
});
|
|
312
322
|
}
|
|
313
323
|
|
|
@@ -377,8 +387,8 @@ export function useComposeState() {
|
|
|
377
387
|
// Save all drafts with content
|
|
378
388
|
for (const draft of currentDrafts) {
|
|
379
389
|
if (hasDraftContent(draft)) {
|
|
380
|
-
|
|
381
|
-
qc.invalidateQueries({ queryKey: ["emails"] });
|
|
390
|
+
void saveDraftToEmailsBestEffort(draft).then((draftId) => {
|
|
391
|
+
if (draftId) qc.invalidateQueries({ queryKey: ["emails"] });
|
|
382
392
|
});
|
|
383
393
|
}
|
|
384
394
|
}
|
|
@@ -2,14 +2,50 @@ import { agentNativePath, oauthRedirectUri } from "@agent-native/core/client";
|
|
|
2
2
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { useEffect } from "react";
|
|
4
4
|
|
|
5
|
-
interface
|
|
5
|
+
export interface GoogleAuthAccount {
|
|
6
|
+
email: string;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
expiresAt?: string;
|
|
9
|
+
photoUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GoogleAuthStatus {
|
|
6
13
|
connected: boolean;
|
|
7
|
-
accounts:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
accounts: GoogleAuthAccount[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stablePhotoUrls = new Map<string, string>();
|
|
18
|
+
|
|
19
|
+
export function mergeStableGoogleAuthStatus(
|
|
20
|
+
status: GoogleAuthStatus,
|
|
21
|
+
photoCache: Map<string, string> = stablePhotoUrls,
|
|
22
|
+
): GoogleAuthStatus {
|
|
23
|
+
if (!status.connected || status.accounts.length === 0) {
|
|
24
|
+
photoCache.clear();
|
|
25
|
+
return status;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const accountEmails = new Set(
|
|
29
|
+
status.accounts.map((account) => account.email),
|
|
30
|
+
);
|
|
31
|
+
for (const email of photoCache.keys()) {
|
|
32
|
+
if (!accountEmails.has(email)) photoCache.delete(email);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let changed = false;
|
|
36
|
+
const accounts = status.accounts.map((account) => {
|
|
37
|
+
if (account.photoUrl) {
|
|
38
|
+
photoCache.set(account.email, account.photoUrl);
|
|
39
|
+
return account;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cachedPhotoUrl = photoCache.get(account.email);
|
|
43
|
+
if (!cachedPhotoUrl) return account;
|
|
44
|
+
changed = true;
|
|
45
|
+
return { ...account, photoUrl: cachedPhotoUrl };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return changed ? { ...status, accounts } : status;
|
|
13
49
|
}
|
|
14
50
|
|
|
15
51
|
/**
|
|
@@ -86,8 +122,10 @@ export function useGoogleAuthStatus() {
|
|
|
86
122
|
return useQuery<GoogleAuthStatus>({
|
|
87
123
|
queryKey: ["google-status"],
|
|
88
124
|
queryFn: async () => {
|
|
89
|
-
return
|
|
90
|
-
|
|
125
|
+
return mergeStableGoogleAuthStatus(
|
|
126
|
+
await fetchJson<GoogleAuthStatus>(
|
|
127
|
+
agentNativePath("/_agent-native/google/status"),
|
|
128
|
+
),
|
|
91
129
|
);
|
|
92
130
|
},
|
|
93
131
|
});
|
|
@@ -26,6 +26,14 @@ or cross either artboard, remove the label and explain the transition with a
|
|
|
26
26
|
nearby annotation instead. Before handoff, inspect the top canvas at default zoom
|
|
27
27
|
and move any frame whose label, connector, or annotation crosses another frame.
|
|
28
28
|
|
|
29
|
+
**Board-unit spacing defaults.** The canvas coordinate system uses approximately 2 board units per screen pixel. `browser` frames occupy roughly 700 × 600 board units; `desktop` frames roughly 900 × 700 board units. Apply these minimum x/y gaps when placing frames explicitly — any less and frames will touch or overlap:
|
|
30
|
+
|
|
31
|
+
- x-gap between `browser` frames: **≥ 1100** (700-unit frame + 400-unit gutter)
|
|
32
|
+
- x-gap between `desktop` frames: **≥ 1300** (900-unit frame + 400-unit gutter)
|
|
33
|
+
- y-gap between rows of any surface: **≥ 1400** (includes frame height + section header + buffer)
|
|
34
|
+
|
|
35
|
+
When in doubt, use larger values — the canvas auto-zooms to fit everything.
|
|
36
|
+
|
|
29
37
|
**Canvas annotations are designer notes on the artboard.** When a top canvas is
|
|
30
38
|
present, sprinkle Figma-style notes near the frames they explain: a short
|
|
31
39
|
heading, supporting text, and bullets — plain text layers, never bordered or
|