@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
|
@@ -74,6 +74,7 @@ export interface UseChatThreadsOptions {
|
|
|
74
74
|
|
|
75
75
|
const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
|
|
76
76
|
const THREADS_UPDATED_EVENT = "agent-chat:threads-updated";
|
|
77
|
+
const THREADS_PAGE_SIZE = 50;
|
|
77
78
|
|
|
78
79
|
function emitThreadsUpdated() {
|
|
79
80
|
if (typeof window === "undefined") return;
|
|
@@ -215,6 +216,10 @@ export function useChatThreads(
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
const [threads, setThreads] = useState<ChatThreadSummary[]>([]);
|
|
219
|
+
const [hasMoreThreads, setHasMoreThreads] = useState(false);
|
|
220
|
+
const [isLoadingMoreThreads, setIsLoadingMoreThreads] = useState(false);
|
|
221
|
+
const [threadsLoadError, setThreadsLoadError] = useState<string | null>(null);
|
|
222
|
+
const nextThreadsOffsetRef = useRef(0);
|
|
218
223
|
const threadsRef = useRef<ChatThreadSummary[]>(threads);
|
|
219
224
|
threadsRef.current = threads;
|
|
220
225
|
|
|
@@ -414,66 +419,106 @@ export function useChatThreads(
|
|
|
414
419
|
threads,
|
|
415
420
|
]);
|
|
416
421
|
|
|
417
|
-
const fetchThreads = useCallback(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// Preserve any optimistic threads we've created this session that
|
|
426
|
-
// haven't shown up in the server list yet — the server only learns
|
|
427
|
-
// about a thread when the user actually sends a message and the
|
|
428
|
-
// agent run's `persistSubmittedUserMessage` writes the row.
|
|
429
|
-
const optimisticOnly = prev.filter(
|
|
430
|
-
(t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id),
|
|
422
|
+
const fetchThreads = useCallback(
|
|
423
|
+
async (options?: { append?: boolean }) => {
|
|
424
|
+
try {
|
|
425
|
+
const offset = options?.append ? nextThreadsOffsetRef.current : 0;
|
|
426
|
+
const res = await fetch(
|
|
427
|
+
offset > 0
|
|
428
|
+
? `${apiUrl}/threads?offset=${offset}`
|
|
429
|
+
: `${apiUrl}/threads`,
|
|
431
430
|
);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
// send yet, and naively replacing makes the recent-chats list
|
|
436
|
-
// visibly jump back to older timestamps right after a send.
|
|
437
|
-
const merged = loaded.map((server) => {
|
|
438
|
-
const local = prev.find((t) => t.id === server.id);
|
|
439
|
-
if (!local) return server;
|
|
440
|
-
const next = { ...server };
|
|
441
|
-
if (local.updatedAt > server.updatedAt) {
|
|
442
|
-
next.updatedAt = local.updatedAt;
|
|
443
|
-
}
|
|
444
|
-
if (userRenamedThreadIdsRef.current.has(server.id) && local.title) {
|
|
445
|
-
next.title = local.title;
|
|
446
|
-
}
|
|
447
|
-
if (pendingPinnedAtRef.current.has(server.id)) {
|
|
448
|
-
next.pinnedAt = pendingPinnedAtRef.current.get(server.id) ?? null;
|
|
449
|
-
}
|
|
450
|
-
if (pendingArchivedAtRef.current.has(server.id)) {
|
|
451
|
-
next.archivedAt =
|
|
452
|
-
pendingArchivedAtRef.current.get(server.id) ?? null;
|
|
453
|
-
}
|
|
454
|
-
if (local.messageCount > server.messageCount) {
|
|
455
|
-
next.messageCount = local.messageCount;
|
|
456
|
-
if (local.preview) next.preview = local.preview;
|
|
457
|
-
if (local.title) next.title = local.title;
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
if (!options?.append) {
|
|
433
|
+
setThreadsLoadError("Could not load chat history.");
|
|
458
434
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const data = await res.json();
|
|
438
|
+
setThreadsLoadError(null);
|
|
439
|
+
const loaded = (data.threads ?? []) as ChatThreadSummary[];
|
|
440
|
+
if (!options?.append) {
|
|
441
|
+
nextThreadsOffsetRef.current = loaded.length;
|
|
442
|
+
} else {
|
|
443
|
+
nextThreadsOffsetRef.current += loaded.length;
|
|
444
|
+
}
|
|
445
|
+
setHasMoreThreads(loaded.length >= THREADS_PAGE_SIZE);
|
|
446
|
+
setThreads((prev) => {
|
|
447
|
+
const loadedIds = new Set(loaded.map((t) => t.id));
|
|
448
|
+
// Preserve any optimistic threads we've created this session that
|
|
449
|
+
// haven't shown up in the server list yet — the server only learns
|
|
450
|
+
// about a thread when the user actually sends a message and the
|
|
451
|
+
// agent run's `persistSubmittedUserMessage` writes the row.
|
|
452
|
+
const optimisticOnly = prev.filter(
|
|
453
|
+
(t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id),
|
|
454
|
+
);
|
|
455
|
+
// Reconcile each server thread against our local copy. If the local
|
|
456
|
+
// copy has a newer updatedAt or higher messageCount, keep those
|
|
457
|
+
// fields — the server probably hasn't observed the user's latest
|
|
458
|
+
// send yet, and naively replacing makes the recent-chats list
|
|
459
|
+
// visibly jump back to older timestamps right after a send.
|
|
460
|
+
const merged = loaded.map((server) => {
|
|
461
|
+
const local = prev.find((t) => t.id === server.id);
|
|
462
|
+
if (!local) return server;
|
|
463
|
+
const next = { ...server };
|
|
464
|
+
if (local.updatedAt > server.updatedAt) {
|
|
465
|
+
next.updatedAt = local.updatedAt;
|
|
466
|
+
}
|
|
467
|
+
if (userRenamedThreadIdsRef.current.has(server.id) && local.title) {
|
|
468
|
+
next.title = local.title;
|
|
469
|
+
}
|
|
470
|
+
if (pendingPinnedAtRef.current.has(server.id)) {
|
|
471
|
+
next.pinnedAt = pendingPinnedAtRef.current.get(server.id) ?? null;
|
|
472
|
+
}
|
|
473
|
+
if (pendingArchivedAtRef.current.has(server.id)) {
|
|
474
|
+
next.archivedAt =
|
|
475
|
+
pendingArchivedAtRef.current.get(server.id) ?? null;
|
|
476
|
+
}
|
|
477
|
+
if (local.messageCount > server.messageCount) {
|
|
478
|
+
next.messageCount = local.messageCount;
|
|
479
|
+
if (local.preview) next.preview = local.preview;
|
|
480
|
+
if (local.title) next.title = local.title;
|
|
481
|
+
}
|
|
482
|
+
// Preserve optimistic scope: when the server creates the row
|
|
483
|
+
// on first message it does so without scope, and the next PUT
|
|
484
|
+
// (saveThreadData) writes the local scope back. In the brief
|
|
485
|
+
// window between those, the server list returns scope: null
|
|
486
|
+
// while the user is clearly working inside a deck — keep the
|
|
487
|
+
// local value so the tab bar doesn't blink unscoped.
|
|
488
|
+
if (local.scope && !server.scope) {
|
|
489
|
+
next.scope = local.scope;
|
|
490
|
+
}
|
|
491
|
+
return next;
|
|
492
|
+
});
|
|
493
|
+
if (options?.append) {
|
|
494
|
+
const existingIds = new Set(prev.map((t) => t.id));
|
|
495
|
+
return sortThreadSummaries([
|
|
496
|
+
...prev,
|
|
497
|
+
...merged.filter((t) => !existingIds.has(t.id)),
|
|
498
|
+
]);
|
|
467
499
|
}
|
|
468
|
-
return
|
|
500
|
+
return [...optimisticOnly, ...merged];
|
|
469
501
|
});
|
|
470
|
-
return [
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
502
|
+
return data.threads as ChatThreadSummary[];
|
|
503
|
+
} catch {
|
|
504
|
+
if (!options?.append) {
|
|
505
|
+
setThreadsLoadError("Could not load chat history.");
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
[apiUrl],
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const loadMoreThreads = useCallback(async (): Promise<void> => {
|
|
514
|
+
if (isLoadingMoreThreads || !hasMoreThreads) return;
|
|
515
|
+
setIsLoadingMoreThreads(true);
|
|
516
|
+
try {
|
|
517
|
+
await fetchThreads({ append: true });
|
|
518
|
+
} finally {
|
|
519
|
+
setIsLoadingMoreThreads(false);
|
|
475
520
|
}
|
|
476
|
-
}, [
|
|
521
|
+
}, [fetchThreads, hasMoreThreads, isLoadingMoreThreads]);
|
|
477
522
|
|
|
478
523
|
// Initial load: load threads from server, then reconcile against the
|
|
479
524
|
// saved active thread.
|
|
@@ -1158,10 +1203,14 @@ export function useChatThreads(
|
|
|
1158
1203
|
saveThreadData,
|
|
1159
1204
|
generateTitle,
|
|
1160
1205
|
searchThreads,
|
|
1206
|
+
loadMoreThreads,
|
|
1161
1207
|
getThreadShareState,
|
|
1162
1208
|
createThreadShareLink,
|
|
1163
1209
|
revokeThreadShareLink,
|
|
1164
1210
|
refreshThreads,
|
|
1211
|
+
hasMoreThreads,
|
|
1212
|
+
isLoadingMoreThreads,
|
|
1213
|
+
threadsLoadError,
|
|
1165
1214
|
isNewThread,
|
|
1166
1215
|
};
|
|
1167
1216
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Supported environment variables:
|
|
4
4
|
* - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID
|
|
5
5
|
*
|
|
6
|
+
* Netlify configuration-file env vars are build-time only for serverless
|
|
7
|
+
* functions, so the Vite plugin also bakes this public value into SSR bundles
|
|
8
|
+
* as `__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__`.
|
|
9
|
+
*
|
|
6
10
|
* Amplitude and Sentry are initialized client-side via their npm packages
|
|
7
11
|
* (see `packages/core/src/client/analytics.ts`). Only GA requires script
|
|
8
12
|
* tag injection because the gtag.js loader must be a `<script src>`.
|
|
@@ -17,16 +21,36 @@
|
|
|
17
21
|
* ```
|
|
18
22
|
*/
|
|
19
23
|
|
|
24
|
+
declare const __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: string | undefined;
|
|
25
|
+
|
|
26
|
+
function normalizeMeasurementId(value: string | undefined): string | null {
|
|
27
|
+
const trimmed = value?.trim();
|
|
28
|
+
return trimmed ? trimmed : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getGaMeasurementId(): string | null {
|
|
32
|
+
return (
|
|
33
|
+
normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||
|
|
34
|
+
normalizeMeasurementId(
|
|
35
|
+
typeof __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__ === "string"
|
|
36
|
+
? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__
|
|
37
|
+
: undefined,
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
function getGaScript(): string | null {
|
|
21
|
-
const id =
|
|
43
|
+
const id = getGaMeasurementId();
|
|
22
44
|
if (!id) return null;
|
|
45
|
+
const srcId = encodeURIComponent(id);
|
|
46
|
+
const jsId = JSON.stringify(id);
|
|
23
47
|
return (
|
|
24
|
-
`<script async src="https://www.googletagmanager.com/gtag/js?id=${
|
|
48
|
+
`<script async src="https://www.googletagmanager.com/gtag/js?id=${srcId}"></script>` +
|
|
25
49
|
`<script>` +
|
|
26
50
|
`window.dataLayer=window.dataLayer||[];` +
|
|
27
51
|
`function gtag(){dataLayer.push(arguments);}` +
|
|
28
52
|
`gtag('js',new Date());` +
|
|
29
|
-
`gtag('config'
|
|
53
|
+
`gtag('config',${jsId});` +
|
|
30
54
|
`if(typeof sessionStorage!=='undefined'&&sessionStorage.getItem('__an_signin')){` +
|
|
31
55
|
`sessionStorage.removeItem('__an_signin');` +
|
|
32
56
|
`gtag('event','sign_in');` +
|
|
@@ -71,6 +71,15 @@ export function resolveRegisteredAccessContext(
|
|
|
71
71
|
return reg?.resolveAccessContext ? reg.resolveAccessContext(ctx) : ctx;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function normalizeEmailForAccess(email: string | undefined): string | null {
|
|
75
|
+
const normalized = email?.trim().toLowerCase();
|
|
76
|
+
return normalized || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function emailColumnMatches(column: any, email: string): SQL {
|
|
80
|
+
return sql`lower(${column}) = ${email}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
/**
|
|
75
84
|
* Build a Drizzle `WHERE` clause that admits rows the current user can see.
|
|
76
85
|
* Pass the ownable resource table and its shares table; optional min role
|
|
@@ -103,15 +112,16 @@ export function accessFilter(
|
|
|
103
112
|
const reg = findRegistrationByTable(resourceTable);
|
|
104
113
|
const ctx = resolveRegisteredAccessContext(reg, rawCtx);
|
|
105
114
|
const { userEmail, orgId } = ctx;
|
|
115
|
+
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
106
116
|
const publicAllowed = reg?.allowPublic !== false;
|
|
107
117
|
const includePublic = (options.includePublic ?? false) && publicAllowed;
|
|
108
118
|
const clauses: SQL[] = [];
|
|
109
119
|
|
|
110
|
-
if (
|
|
120
|
+
if (normalizedUserEmail) {
|
|
111
121
|
clauses.push(
|
|
112
122
|
and(
|
|
113
|
-
|
|
114
|
-
ownerScopeFilter(resourceTable, ctx),
|
|
123
|
+
emailColumnMatches(resourceTable.ownerEmail, normalizedUserEmail),
|
|
124
|
+
ownerScopeFilter(reg, resourceTable, ctx),
|
|
115
125
|
)!,
|
|
116
126
|
);
|
|
117
127
|
}
|
|
@@ -128,13 +138,13 @@ export function accessFilter(
|
|
|
128
138
|
);
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
|
-
if (
|
|
141
|
+
if (normalizedUserEmail) {
|
|
132
142
|
const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);
|
|
133
143
|
clauses.push(
|
|
134
144
|
sql`exists (select 1 from ${sharesTable}
|
|
135
145
|
where ${sharesTable.resourceId} = ${resourceTable.id}
|
|
136
146
|
and ${sharesTable.principalType} = 'user'
|
|
137
|
-
and ${sharesTable.principalId} = ${
|
|
147
|
+
and lower(${sharesTable.principalId}) = ${normalizedUserEmail}
|
|
138
148
|
and ${shareScope}
|
|
139
149
|
and ${minRoleSql(minRole)})`,
|
|
140
150
|
);
|
|
@@ -154,7 +164,12 @@ export function accessFilter(
|
|
|
154
164
|
return or(...clauses) ?? sql`1=0`;
|
|
155
165
|
}
|
|
156
166
|
|
|
157
|
-
function ownerScopeFilter(
|
|
167
|
+
function ownerScopeFilter(
|
|
168
|
+
reg: ShareableResourceRegistration | undefined,
|
|
169
|
+
resourceTable: any,
|
|
170
|
+
ctx: AccessContext,
|
|
171
|
+
): SQL {
|
|
172
|
+
if (reg?.ownerAccessIgnoresOrg === true) return sql`1=1`;
|
|
158
173
|
if (ctx.orgId) {
|
|
159
174
|
// Rows created before org-scoping, or in solo mode, have no org_id. Keep
|
|
160
175
|
// them manageable by their owner after the owner joins or switches into an
|
|
@@ -167,7 +182,12 @@ function ownerScopeFilter(resourceTable: any, ctx: AccessContext): SQL {
|
|
|
167
182
|
return sql`${resourceTable.orgId} IS NULL`;
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
function ownerMatchesActiveScope(
|
|
185
|
+
function ownerMatchesActiveScope(
|
|
186
|
+
reg: ShareableResourceRegistration | undefined,
|
|
187
|
+
resource: any,
|
|
188
|
+
ctx: AccessContext,
|
|
189
|
+
): boolean {
|
|
190
|
+
if (reg?.ownerAccessIgnoresOrg === true) return true;
|
|
171
191
|
const resourceOrgId = resource?.orgId ?? null;
|
|
172
192
|
if (!resourceOrgId) return true;
|
|
173
193
|
return ctx.orgId === resourceOrgId;
|
|
@@ -233,11 +253,12 @@ export async function resolveAccess(
|
|
|
233
253
|
if (!resource) return null;
|
|
234
254
|
|
|
235
255
|
const { userEmail, orgId } = ctx;
|
|
256
|
+
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
236
257
|
|
|
237
258
|
if (
|
|
238
|
-
|
|
239
|
-
resource.ownerEmail ===
|
|
240
|
-
ownerMatchesActiveScope(resource, ctx)
|
|
259
|
+
normalizedUserEmail &&
|
|
260
|
+
normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&
|
|
261
|
+
ownerMatchesActiveScope(reg, resource, ctx)
|
|
241
262
|
) {
|
|
242
263
|
return { role: "owner", resource };
|
|
243
264
|
}
|
|
@@ -265,16 +286,17 @@ async function highestShareRole(
|
|
|
265
286
|
resource: any,
|
|
266
287
|
): Promise<ShareRole | null> {
|
|
267
288
|
const { userEmail, orgId } = ctx;
|
|
268
|
-
|
|
289
|
+
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
290
|
+
if (!normalizedUserEmail && !orgId) return null;
|
|
269
291
|
if (!explicitSharesAllowedForResource(reg, resource, ctx)) return null;
|
|
270
292
|
const db = reg.getDb() as any;
|
|
271
293
|
|
|
272
294
|
const principalClauses: ReturnType<typeof and>[] = [];
|
|
273
|
-
if (
|
|
295
|
+
if (normalizedUserEmail) {
|
|
274
296
|
principalClauses.push(
|
|
275
297
|
and(
|
|
276
298
|
eq(reg.sharesTable.principalType, "user"),
|
|
277
|
-
|
|
299
|
+
emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail),
|
|
278
300
|
),
|
|
279
301
|
);
|
|
280
302
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, eq } from "drizzle-orm";
|
|
1
|
+
import { and, eq, sql, type SQL } from "drizzle-orm";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import { defineAction } from "../../action.js";
|
|
@@ -89,6 +89,25 @@ function nanoid(size = 12): string {
|
|
|
89
89
|
return id;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function normalizePrincipalId(
|
|
93
|
+
principalType: "user" | "org",
|
|
94
|
+
principalId: string,
|
|
95
|
+
): string {
|
|
96
|
+
return principalType === "user"
|
|
97
|
+
? principalId.trim().toLowerCase()
|
|
98
|
+
: principalId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function principalIdMatches(
|
|
102
|
+
sharesTable: any,
|
|
103
|
+
principalType: "user" | "org",
|
|
104
|
+
principalId: string,
|
|
105
|
+
): SQL {
|
|
106
|
+
return principalType === "user"
|
|
107
|
+
? sql`lower(${sharesTable.principalId}) = ${principalId}`
|
|
108
|
+
: eq(sharesTable.principalId, principalId);
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
/**
|
|
93
112
|
* Returns true if the given email is either an active member of `orgId` or
|
|
94
113
|
* has a pending invitation to `orgId`. Used by resources whose registration
|
|
@@ -163,6 +182,10 @@ export default defineAction({
|
|
|
163
182
|
);
|
|
164
183
|
const actor = getRequestUserEmail();
|
|
165
184
|
if (!actor) throw new ForbiddenError("Not signed in");
|
|
185
|
+
const principalId = normalizePrincipalId(
|
|
186
|
+
args.principalType,
|
|
187
|
+
args.principalId,
|
|
188
|
+
);
|
|
166
189
|
const beforeExtensionTargets = await getExtensionShareChangeTargets(
|
|
167
190
|
args.resourceType,
|
|
168
191
|
args.resourceId,
|
|
@@ -176,10 +199,10 @@ export default defineAction({
|
|
|
176
199
|
);
|
|
177
200
|
}
|
|
178
201
|
if (args.principalType === "user") {
|
|
179
|
-
const ok = await isOrgMemberOrInvited(resourceOrgId,
|
|
202
|
+
const ok = await isOrgMemberOrInvited(resourceOrgId, principalId);
|
|
180
203
|
if (!ok) {
|
|
181
204
|
throw new ForbiddenError(
|
|
182
|
-
`${
|
|
205
|
+
`${principalId} is not in your organization. Invite them to the organization first, then share.`,
|
|
183
206
|
);
|
|
184
207
|
}
|
|
185
208
|
} else if (args.principalType === "org") {
|
|
@@ -187,7 +210,7 @@ export default defineAction({
|
|
|
187
210
|
// extension code in the viewer's auth context — the same threat
|
|
188
211
|
// model that blocks public + cross-org user shares. Pin org-
|
|
189
212
|
// principal shares to the resource's own org.
|
|
190
|
-
if (
|
|
213
|
+
if (principalId !== resourceOrgId) {
|
|
191
214
|
throw new ForbiddenError(
|
|
192
215
|
`${reg.displayName} can only be shared with its own organization, not a different one.`,
|
|
193
216
|
);
|
|
@@ -203,7 +226,7 @@ export default defineAction({
|
|
|
203
226
|
and(
|
|
204
227
|
eq(reg.sharesTable.resourceId, args.resourceId),
|
|
205
228
|
eq(reg.sharesTable.principalType, args.principalType),
|
|
206
|
-
|
|
229
|
+
principalIdMatches(reg.sharesTable, args.principalType, principalId),
|
|
207
230
|
),
|
|
208
231
|
);
|
|
209
232
|
|
|
@@ -225,7 +248,7 @@ export default defineAction({
|
|
|
225
248
|
id,
|
|
226
249
|
resourceId: args.resourceId,
|
|
227
250
|
principalType: args.principalType,
|
|
228
|
-
principalId
|
|
251
|
+
principalId,
|
|
229
252
|
role: args.role,
|
|
230
253
|
createdBy: actor,
|
|
231
254
|
createdAt: new Date().toISOString(),
|
|
@@ -240,7 +263,7 @@ export default defineAction({
|
|
|
240
263
|
args.notify !== false &&
|
|
241
264
|
args.principalType === "user" &&
|
|
242
265
|
isEmailConfigured() &&
|
|
243
|
-
!isSyntheticQaEmail(
|
|
266
|
+
!isSyntheticQaEmail(principalId)
|
|
244
267
|
) {
|
|
245
268
|
try {
|
|
246
269
|
const titleCol = reg.titleColumn ?? "title";
|
|
@@ -268,12 +291,12 @@ export default defineAction({
|
|
|
268
291
|
heading: "You've been given access",
|
|
269
292
|
paragraphs: [
|
|
270
293
|
`${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
|
|
271
|
-
`Use the button below to open it. If prompted, sign in with ${emailStrong(
|
|
294
|
+
`Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,
|
|
272
295
|
],
|
|
273
296
|
cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
|
|
274
297
|
footer: `You received this because ${actor} granted you ${args.role} access.`,
|
|
275
298
|
});
|
|
276
|
-
await sendEmail({ to:
|
|
299
|
+
await sendEmail({ to: principalId, subject, html, text });
|
|
277
300
|
} catch (err) {
|
|
278
301
|
console.error(
|
|
279
302
|
"[share-resource] failed to send share notification:",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, eq } from "drizzle-orm";
|
|
1
|
+
import { and, eq, sql, type SQL } from "drizzle-orm";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import { defineAction } from "../../action.js";
|
|
@@ -9,6 +9,25 @@ import {
|
|
|
9
9
|
notifyExtensionShareChanged,
|
|
10
10
|
} from "./extension-change.js";
|
|
11
11
|
|
|
12
|
+
function normalizePrincipalId(
|
|
13
|
+
principalType: "user" | "org",
|
|
14
|
+
principalId: string,
|
|
15
|
+
): string {
|
|
16
|
+
return principalType === "user"
|
|
17
|
+
? principalId.trim().toLowerCase()
|
|
18
|
+
: principalId;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function principalIdMatches(
|
|
22
|
+
sharesTable: any,
|
|
23
|
+
principalType: "user" | "org",
|
|
24
|
+
principalId: string,
|
|
25
|
+
): SQL {
|
|
26
|
+
return principalType === "user"
|
|
27
|
+
? sql`lower(${sharesTable.principalId}) = ${principalId}`
|
|
28
|
+
: eq(sharesTable.principalId, principalId);
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
export default defineAction({
|
|
13
32
|
description:
|
|
14
33
|
"Revoke a previously granted share. Owner or admin role required.",
|
|
@@ -28,13 +47,17 @@ export default defineAction({
|
|
|
28
47
|
args.resourceId,
|
|
29
48
|
);
|
|
30
49
|
const db = reg.getDb() as any;
|
|
50
|
+
const principalId = normalizePrincipalId(
|
|
51
|
+
args.principalType,
|
|
52
|
+
args.principalId,
|
|
53
|
+
);
|
|
31
54
|
await db
|
|
32
55
|
.delete(reg.sharesTable)
|
|
33
56
|
.where(
|
|
34
57
|
and(
|
|
35
58
|
eq(reg.sharesTable.resourceId, args.resourceId),
|
|
36
59
|
eq(reg.sharesTable.principalType, args.principalType),
|
|
37
|
-
|
|
60
|
+
principalIdMatches(reg.sharesTable, args.principalType, principalId),
|
|
38
61
|
),
|
|
39
62
|
);
|
|
40
63
|
await notifyExtensionShareChanged(
|
|
@@ -78,6 +78,15 @@ export interface ShareableResourceRegistration {
|
|
|
78
78
|
userEmail?: string;
|
|
79
79
|
orgId?: string;
|
|
80
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* When true, direct ownership is recognized by owner_email regardless of the
|
|
83
|
+
* caller's active org. Use this only for resource types where the template's
|
|
84
|
+
* own actions already treat owner_email as the cross-org authority and list
|
|
85
|
+
* views add their own org filters.
|
|
86
|
+
*
|
|
87
|
+
* Default: `false`.
|
|
88
|
+
*/
|
|
89
|
+
ownerAccessIgnoresOrg?: boolean;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
// Stash the registry on globalThis so it survives SSR bundle duplication.
|
|
@@ -1843,6 +1843,9 @@ function createAgentNativeConfig(
|
|
|
1843
1843
|
define: {
|
|
1844
1844
|
...(userConfig.define ?? {}),
|
|
1845
1845
|
...(options.define ?? {}),
|
|
1846
|
+
__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: JSON.stringify(
|
|
1847
|
+
process.env.GA_MEASUREMENT_ID?.trim() || "",
|
|
1848
|
+
),
|
|
1846
1849
|
// Framework route warmup controls how SSR `.data` routes are fetched:
|
|
1847
1850
|
// ordinary fetches keep them CDN-cacheable, while native prefetch headers
|
|
1848
1851
|
// can be refused before the CDN/origin sees the request. Keep this value
|
|
@@ -151,32 +151,43 @@ export default defineAction({
|
|
|
151
151
|
);
|
|
152
152
|
const visibleKeys = new Set(results.map((result) => result.key));
|
|
153
153
|
const providers = credentialProviderConfigs
|
|
154
|
-
.filter((provider) =>
|
|
155
|
-
provider.
|
|
156
|
-
|
|
154
|
+
.filter((provider) => {
|
|
155
|
+
const requiredMode = provider.requiredMode ?? "all";
|
|
156
|
+
return requiredMode === "any"
|
|
157
|
+
? provider.requiredKeys.some((key) => visibleKeys.has(key))
|
|
158
|
+
: provider.requiredKeys.every((key) => visibleKeys.has(key));
|
|
159
|
+
})
|
|
157
160
|
.map((provider) => {
|
|
158
161
|
const optionalKeys = provider.optionalKeys ?? [];
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
const requiredMode = provider.requiredMode ?? "all";
|
|
163
|
+
const hasRequiredCredentials =
|
|
164
|
+
requiredMode === "any"
|
|
165
|
+
? provider.requiredKeys.some((key) => configuredKeys.has(key))
|
|
166
|
+
: provider.requiredKeys.every((key) => configuredKeys.has(key));
|
|
167
|
+
const missingRequiredKeys = hasRequiredCredentials
|
|
168
|
+
? []
|
|
169
|
+
: provider.requiredKeys.filter((key) => !configuredKeys.has(key));
|
|
162
170
|
const configuredProviderKeys = [
|
|
163
171
|
...provider.requiredKeys,
|
|
164
172
|
...optionalKeys,
|
|
165
173
|
].filter((key) => configuredKeys.has(key));
|
|
174
|
+
const workspaceConnection =
|
|
175
|
+
workspaceProviderStatusById.get(provider.provider) ??
|
|
176
|
+
summarizeWorkspaceConnections(
|
|
177
|
+
provider.provider,
|
|
178
|
+
workspace.connections,
|
|
179
|
+
workspace.grants,
|
|
180
|
+
);
|
|
166
181
|
return {
|
|
167
182
|
provider: provider.provider,
|
|
168
183
|
label: provider.label,
|
|
169
|
-
configured:
|
|
184
|
+
configured:
|
|
185
|
+
hasRequiredCredentials ||
|
|
186
|
+
workspaceConnection.grantState === "connected",
|
|
170
187
|
configuredKeys: configuredProviderKeys,
|
|
171
188
|
missingRequiredKeys,
|
|
172
189
|
optionalKeys,
|
|
173
|
-
workspaceConnection
|
|
174
|
-
workspaceProviderStatusById.get(provider.provider) ??
|
|
175
|
-
summarizeWorkspaceConnections(
|
|
176
|
-
provider.provider,
|
|
177
|
-
workspace.connections,
|
|
178
|
-
workspace.grants,
|
|
179
|
-
),
|
|
190
|
+
workspaceConnection,
|
|
180
191
|
};
|
|
181
192
|
});
|
|
182
193
|
return {
|
|
@@ -60,8 +60,8 @@ export function NewAnalysisDialog() {
|
|
|
60
60
|
const status = statusData as DataSourceStatusResponse | undefined;
|
|
61
61
|
const envStatus = credentialRowsFromStatus(status);
|
|
62
62
|
const configuredSources = useMemo(
|
|
63
|
-
() => getConfiguredDataSources(envStatus),
|
|
64
|
-
[envStatus],
|
|
63
|
+
() => getConfiguredDataSources(envStatus, status),
|
|
64
|
+
[envStatus, status],
|
|
65
65
|
);
|
|
66
66
|
const configuredSourceNames = configuredSources.map((source) => source.name);
|
|
67
67
|
const statusMessage = status?.message || status?.error;
|