@agent-native/core 0.77.6 → 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 +12 -0
- package/corpus/core/package.json +3 -3
- 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/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/lib/browser-diagnostics-capture.ts +5 -1
- 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/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/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/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/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 +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +2 -2
- 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/server/transcribe-voice.d.ts +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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/browser";
|
|
2
|
+
|
|
3
|
+
declare const __CLIPS_SENTRY_DSN__: string;
|
|
4
|
+
declare const __CLIPS_SENTRY_ENVIRONMENT__: string;
|
|
5
|
+
|
|
6
|
+
type CaptureContext = {
|
|
7
|
+
tags?: Record<string, string | undefined>;
|
|
8
|
+
extra?: Record<string, unknown>;
|
|
9
|
+
contexts?: Record<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SENSITIVE_QUERY_PARAM_RE =
|
|
13
|
+
/(?:token|secret|password|passwd|pwd|key|api[-_]?key|code|state|session|auth|credential)/i;
|
|
14
|
+
|
|
15
|
+
let initialized = false;
|
|
16
|
+
let globalErrorCaptureInstalled = false;
|
|
17
|
+
|
|
18
|
+
function configuredDsn(): string {
|
|
19
|
+
return typeof __CLIPS_SENTRY_DSN__ === "string"
|
|
20
|
+
? __CLIPS_SENTRY_DSN__.trim()
|
|
21
|
+
: "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extensionVersion(): string {
|
|
25
|
+
try {
|
|
26
|
+
return chrome.runtime.getManifest().version || "unknown";
|
|
27
|
+
} catch {
|
|
28
|
+
return "unknown";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function scrubUrl(
|
|
33
|
+
value: string,
|
|
34
|
+
options: { redactAllQueryValues?: boolean } = {},
|
|
35
|
+
): string {
|
|
36
|
+
try {
|
|
37
|
+
const url = new URL(value);
|
|
38
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
39
|
+
if (options.redactAllQueryValues || SENSITIVE_QUERY_PARAM_RE.test(key)) {
|
|
40
|
+
url.searchParams.set(key, "<redacted>");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return url.toString();
|
|
44
|
+
} catch {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scrubEvent<T extends Sentry.Event>(event: T): T {
|
|
50
|
+
if (event.request?.url) {
|
|
51
|
+
event.request.url = scrubUrl(event.request.url);
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(event.breadcrumbs)) {
|
|
54
|
+
for (const breadcrumb of event.breadcrumbs) {
|
|
55
|
+
const data = breadcrumb.data as Record<string, unknown> | undefined;
|
|
56
|
+
if (!data) continue;
|
|
57
|
+
for (const key of ["url", "from", "to"]) {
|
|
58
|
+
if (typeof data[key] === "string") {
|
|
59
|
+
data[key] = scrubUrl(data[key]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (event.user) {
|
|
65
|
+
delete (event.user as Record<string, unknown>).ip_address;
|
|
66
|
+
}
|
|
67
|
+
return event;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scrubScopeString(value: string): string {
|
|
71
|
+
const scrubbed = scrubUrl(value, { redactAllQueryValues: true });
|
|
72
|
+
if (scrubbed !== value) return scrubbed;
|
|
73
|
+
return value.replace(/https?:\/\/[^\s"'<>]+/g, (url) =>
|
|
74
|
+
scrubUrl(url, { redactAllQueryValues: true }),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scrubScopeValue(
|
|
79
|
+
value: unknown,
|
|
80
|
+
seen = new WeakSet<object>(),
|
|
81
|
+
): unknown {
|
|
82
|
+
if (typeof value === "string") return scrubScopeString(value);
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.map((item) => scrubScopeValue(item, seen));
|
|
85
|
+
}
|
|
86
|
+
if (!value || typeof value !== "object") return value;
|
|
87
|
+
if (seen.has(value)) return "[Circular]";
|
|
88
|
+
if (Object.prototype.toString.call(value) !== "[object Object]") {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
seen.add(value);
|
|
92
|
+
return Object.fromEntries(
|
|
93
|
+
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
|
|
94
|
+
key,
|
|
95
|
+
scrubScopeValue(entry, seen),
|
|
96
|
+
]),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function installGlobalErrorCapture(surface: string): void {
|
|
101
|
+
if (globalErrorCaptureInstalled) return;
|
|
102
|
+
globalErrorCaptureInstalled = true;
|
|
103
|
+
const target = globalThis as typeof globalThis & {
|
|
104
|
+
addEventListener?: (
|
|
105
|
+
type: string,
|
|
106
|
+
listener: EventListenerOrEventListenerObject,
|
|
107
|
+
) => void;
|
|
108
|
+
};
|
|
109
|
+
if (typeof target.addEventListener !== "function") return;
|
|
110
|
+
|
|
111
|
+
target.addEventListener("error", (event) => {
|
|
112
|
+
const errorEvent = event as ErrorEvent;
|
|
113
|
+
const error =
|
|
114
|
+
errorEvent.error instanceof Error
|
|
115
|
+
? errorEvent.error
|
|
116
|
+
: new Error(errorEvent.message || "Unhandled extension error");
|
|
117
|
+
captureExtensionError(error, {
|
|
118
|
+
tags: { surface, mechanism: "global-error" },
|
|
119
|
+
extra: {
|
|
120
|
+
filename: errorEvent.filename,
|
|
121
|
+
lineno: errorEvent.lineno,
|
|
122
|
+
colno: errorEvent.colno,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
target.addEventListener("unhandledrejection", (event) => {
|
|
128
|
+
const rejection = event as PromiseRejectionEvent;
|
|
129
|
+
const error =
|
|
130
|
+
rejection.reason instanceof Error
|
|
131
|
+
? rejection.reason
|
|
132
|
+
: new Error(String(rejection.reason ?? "Unhandled promise rejection"));
|
|
133
|
+
captureExtensionError(error, {
|
|
134
|
+
tags: { surface, mechanism: "unhandled-rejection" },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function initExtensionSentry(surface: string): void {
|
|
140
|
+
if (initialized) return;
|
|
141
|
+
const dsn = configuredDsn();
|
|
142
|
+
if (!dsn) return;
|
|
143
|
+
initialized = true;
|
|
144
|
+
|
|
145
|
+
Sentry.init({
|
|
146
|
+
dsn,
|
|
147
|
+
environment:
|
|
148
|
+
typeof __CLIPS_SENTRY_ENVIRONMENT__ === "string" &&
|
|
149
|
+
__CLIPS_SENTRY_ENVIRONMENT__
|
|
150
|
+
? __CLIPS_SENTRY_ENVIRONMENT__
|
|
151
|
+
: "production",
|
|
152
|
+
release: `clips-chrome-extension@${extensionVersion()}`,
|
|
153
|
+
sendDefaultPii: false,
|
|
154
|
+
skipBrowserExtensionCheck: true,
|
|
155
|
+
beforeSend(event) {
|
|
156
|
+
event.tags = {
|
|
157
|
+
...event.tags,
|
|
158
|
+
app: "agent-native-clips",
|
|
159
|
+
template: "clips",
|
|
160
|
+
runtime: "chrome-extension",
|
|
161
|
+
surface,
|
|
162
|
+
};
|
|
163
|
+
return scrubEvent(event);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
Sentry.setTag("app", "agent-native-clips");
|
|
168
|
+
Sentry.setTag("template", "clips");
|
|
169
|
+
Sentry.setTag("runtime", "chrome-extension");
|
|
170
|
+
Sentry.setTag("surface", surface);
|
|
171
|
+
installGlobalErrorCapture(surface);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function captureExtensionError(
|
|
175
|
+
error: unknown,
|
|
176
|
+
context: CaptureContext = {},
|
|
177
|
+
): string | undefined {
|
|
178
|
+
if (!initialized) return undefined;
|
|
179
|
+
try {
|
|
180
|
+
return Sentry.withScope((scope) => {
|
|
181
|
+
if (context.tags) {
|
|
182
|
+
for (const [key, value] of Object.entries(context.tags)) {
|
|
183
|
+
if (typeof value === "string") {
|
|
184
|
+
scope.setTag(key, scrubScopeString(value));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (context.extra) {
|
|
189
|
+
for (const [key, value] of Object.entries(context.extra)) {
|
|
190
|
+
if (value !== undefined) scope.setExtra(key, scrubScopeValue(value));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (context.contexts) {
|
|
194
|
+
for (const [key, value] of Object.entries(context.contexts)) {
|
|
195
|
+
scope.setContext(
|
|
196
|
+
key,
|
|
197
|
+
scrubScopeValue(value) as Record<string, unknown>,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return Sentry.captureException(error);
|
|
202
|
+
});
|
|
203
|
+
} catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -5,10 +5,57 @@ import { defineConfig } from "vite";
|
|
|
5
5
|
|
|
6
6
|
const root = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
|
|
8
|
+
function firstNonEmpty(...values: Array<string | undefined>): string {
|
|
9
|
+
for (const value of values) {
|
|
10
|
+
const trimmed = value?.trim();
|
|
11
|
+
if (trimmed) return trimmed;
|
|
12
|
+
}
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveSentryDsn(): string {
|
|
17
|
+
const direct = firstNonEmpty(
|
|
18
|
+
process.env.SENTRY_CLIENT_DSN,
|
|
19
|
+
process.env.VITE_SENTRY_CLIENT_DSN,
|
|
20
|
+
process.env.VITE_SENTRY_DSN,
|
|
21
|
+
process.env.SENTRY_DSN,
|
|
22
|
+
);
|
|
23
|
+
if (direct) return direct;
|
|
24
|
+
|
|
25
|
+
const key = firstNonEmpty(
|
|
26
|
+
process.env.SENTRY_CLIENT_KEY,
|
|
27
|
+
process.env.VITE_SENTRY_CLIENT_KEY,
|
|
28
|
+
);
|
|
29
|
+
const projectId = firstNonEmpty(
|
|
30
|
+
process.env.SENTRY_PROJECT_ID,
|
|
31
|
+
process.env.VITE_SENTRY_PROJECT_ID,
|
|
32
|
+
);
|
|
33
|
+
const host = firstNonEmpty(
|
|
34
|
+
process.env.SENTRY_INGEST_HOST,
|
|
35
|
+
process.env.VITE_SENTRY_INGEST_HOST,
|
|
36
|
+
);
|
|
37
|
+
return key && projectId && host ? `https://${key}@${host}/${projectId}` : "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveSentryEnvironment(): string {
|
|
41
|
+
return (
|
|
42
|
+
firstNonEmpty(
|
|
43
|
+
process.env.SENTRY_ENVIRONMENT,
|
|
44
|
+
process.env.NETLIFY_CONTEXT,
|
|
45
|
+
process.env.VERCEL_ENV,
|
|
46
|
+
process.env.NODE_ENV,
|
|
47
|
+
) || "production"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
8
51
|
export default defineConfig({
|
|
9
52
|
root,
|
|
10
53
|
base: "./",
|
|
11
54
|
publicDir: "public",
|
|
55
|
+
define: {
|
|
56
|
+
__CLIPS_SENTRY_DSN__: JSON.stringify(resolveSentryDsn()),
|
|
57
|
+
__CLIPS_SENTRY_ENVIRONMENT__: JSON.stringify(resolveSentryEnvironment()),
|
|
58
|
+
},
|
|
12
59
|
// Share recording primitives with the web app recorder (templates/clips/shared)
|
|
13
60
|
// — matches the "@shared/*" tsconfig path so imports resolve in both builds.
|
|
14
61
|
resolve: {
|
|
@@ -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 =
|