@agent-native/core 0.84.55 → 0.84.57

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.
Files changed (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-clips.mdx +2 -0
  5. package/corpus/core/docs/content/locales/de-DE/template-analytics.mdx +2 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-clips.mdx +2 -0
  7. package/corpus/core/docs/content/locales/es-ES/template-analytics.mdx +2 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-clips.mdx +2 -0
  9. package/corpus/core/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-clips.mdx +2 -0
  11. package/corpus/core/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-clips.mdx +2 -0
  13. package/corpus/core/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-clips.mdx +2 -0
  15. package/corpus/core/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-clips.mdx +2 -0
  17. package/corpus/core/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-clips.mdx +2 -0
  19. package/corpus/core/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-clips.mdx +2 -0
  21. package/corpus/core/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-clips.mdx +2 -0
  23. package/corpus/core/docs/content/template-analytics.mdx +2 -0
  24. package/corpus/core/docs/content/template-clips.mdx +22 -10
  25. package/corpus/core/docs/content/tracking.mdx +2 -0
  26. package/corpus/core/package.json +1 -1
  27. package/corpus/core/src/cli/recap.ts +146 -5
  28. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +8 -0
  29. package/corpus/templates/analytics/AGENTS.md +5 -0
  30. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +31 -0
  31. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  32. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  33. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +65 -3
  34. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-can-be-copied-as-temporary-private-links-for-agents.md +6 -0
  35. package/corpus/templates/analytics/server/handlers/session-replay.ts +78 -0
  36. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +313 -0
  37. package/corpus/templates/analytics/server/lib/session-replay.ts +198 -3
  38. package/corpus/templates/analytics/server/routes/[...page].get.ts +104 -1
  39. package/corpus/templates/analytics/server/routes/api/session-replay/agent-context.json.get.ts +48 -0
  40. package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +63 -0
  41. package/corpus/templates/analytics/shared/session-replay-agent-access.ts +10 -0
  42. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +14 -8
  43. package/corpus/templates/clips/AGENTS.md +3 -2
  44. package/corpus/templates/clips/actions/create-recording-agent-link.ts +90 -0
  45. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -46
  46. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -3
  47. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -1
  48. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -1
  49. package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
  50. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -1
  51. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -1
  52. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -1
  53. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -1
  54. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -1
  55. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -1
  56. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -1
  57. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -1
  58. package/corpus/templates/clips/app/routes/share.$shareId.tsx +100 -46
  59. package/corpus/templates/clips/changelog/2026-07-02-private-recordings-can-be-shared-with-agents-through-temporary-links-without-making-them-public.md +6 -0
  60. package/corpus/templates/clips/server/lib/public-agent-context.ts +26 -13
  61. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +2 -1
  62. package/corpus/templates/clips/server/routes/api/agent-frame.jpg.get.ts +2 -1
  63. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +2 -1
  64. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -2
  65. package/corpus/templates/clips/shared/agent-context.ts +5 -0
  66. package/corpus/templates/plan/actions/create-visual-recap.ts +39 -0
  67. package/corpus/templates/plan/actions/list-visual-plans.ts +2 -0
  68. package/corpus/templates/plan/actions/view-screen.ts +2 -0
  69. package/corpus/templates/plan/app/pages/PlansPage.tsx +279 -114
  70. package/corpus/templates/plan/changelog/2026-07-02-plan-comment-shortcuts-now-open-comment-mode-from-the-keyboa.md +6 -0
  71. package/corpus/templates/plan/server/db/schema.ts +3 -0
  72. package/corpus/templates/plan/server/lib/comment-notifications.ts +21 -2
  73. package/corpus/templates/plan/server/plans.ts +6 -0
  74. package/corpus/templates/plan/server/plugins/db.ts +11 -0
  75. package/corpus/templates/plan/shared/comment-context.ts +1 -0
  76. package/corpus/templates/plan/shared/types.ts +4 -0
  77. package/dist/cli/recap.d.ts +14 -0
  78. package/dist/cli/recap.d.ts.map +1 -1
  79. package/dist/cli/recap.js +88 -5
  80. package/dist/cli/recap.js.map +1 -1
  81. package/dist/collab/awareness.d.ts +2 -2
  82. package/dist/collab/awareness.d.ts.map +1 -1
  83. package/dist/collab/routes.d.ts +1 -1
  84. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  85. package/dist/notifications/routes.d.ts +3 -3
  86. package/dist/observability/routes.d.ts +4 -4
  87. package/dist/server/transcribe-voice.d.ts +1 -1
  88. package/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
  89. package/docs/content/locales/ar-SA/template-clips.mdx +2 -0
  90. package/docs/content/locales/de-DE/template-analytics.mdx +2 -0
  91. package/docs/content/locales/de-DE/template-clips.mdx +2 -0
  92. package/docs/content/locales/es-ES/template-analytics.mdx +2 -0
  93. package/docs/content/locales/es-ES/template-clips.mdx +2 -0
  94. package/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
  95. package/docs/content/locales/fr-FR/template-clips.mdx +2 -0
  96. package/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
  97. package/docs/content/locales/hi-IN/template-clips.mdx +2 -0
  98. package/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
  99. package/docs/content/locales/ja-JP/template-clips.mdx +2 -0
  100. package/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
  101. package/docs/content/locales/ko-KR/template-clips.mdx +2 -0
  102. package/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
  103. package/docs/content/locales/pt-BR/template-clips.mdx +2 -0
  104. package/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
  105. package/docs/content/locales/zh-CN/template-clips.mdx +2 -0
  106. package/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
  107. package/docs/content/locales/zh-TW/template-clips.mdx +2 -0
  108. package/docs/content/template-analytics.mdx +2 -0
  109. package/docs/content/template-clips.mdx +22 -10
  110. package/docs/content/tracking.mdx +2 -0
  111. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestContext,
4
+ getRequestOrgId,
5
+ getRequestUserEmail,
6
+ } from "@agent-native/core/server";
7
+ import { z } from "zod";
8
+
9
+ import { createSessionReplayAgentLink } from "../server/lib/session-replay-agent-context.js";
10
+
11
+ function resolveScope() {
12
+ const userEmail = getRequestUserEmail();
13
+ if (!userEmail) throw new Error("no authenticated user");
14
+ return { userEmail, orgId: getRequestOrgId() || null };
15
+ }
16
+
17
+ export default defineAction({
18
+ description:
19
+ "Create a temporary private agent-readable link for one Analytics session replay recording. The URL is scoped to that recording and expires after two hours.",
20
+ schema: z.object({
21
+ recordingId: z.string().describe("The session_recordings id"),
22
+ }),
23
+ readOnly: true,
24
+ run: async (args) => {
25
+ return createSessionReplayAgentLink({
26
+ recordingId: args.recordingId,
27
+ scope: resolveScope(),
28
+ origin: getRequestContext()?.requestOrigin,
29
+ });
30
+ },
31
+ });
@@ -803,6 +803,8 @@ const messages = {
803
803
  emptyStepVisit: "存取應用",
804
804
  installSnippetTitle: "新增工作階段回放",
805
805
  backToSessions: "返回工作階段",
806
+ copyForAgent: "複製給 Agent",
807
+ copiedForAgent: "已複製",
806
808
  askAgent: "詢問代理此工作階段",
807
809
  askSessionTitle: "詢問此工作階段",
808
810
  askSessionDescription: "將此回放作為脈絡,向 Analytics 代理傳送聚焦提示。",
@@ -841,6 +841,8 @@ const enUS = {
841
841
  emptyStepVisit: "Visit app",
842
842
  installSnippetTitle: "Add session replay",
843
843
  backToSessions: "Back to sessions",
844
+ copyForAgent: "Copy for agent",
845
+ copiedForAgent: "Copied",
844
846
  askAgent: "Ask agent about this session",
845
847
  askSessionTitle: "Ask about this session",
846
848
  askSessionDescription:
@@ -6839,6 +6841,8 @@ const translatedAnalyticsDebtCorrections = {
6839
6841
  anyActivity: "أي نشاط",
6840
6842
  eventCountCompact: "{{count}} حدث",
6841
6843
  pageCountCompact: "{{count}} صفحة",
6844
+ copyForAgent: "نسخ للوكيل",
6845
+ copiedForAgent: "تم النسخ",
6842
6846
  askSessionTitle: "اسأل عن هذه الجلسة",
6843
6847
  askSessionDescription:
6844
6848
  "أرسل مطالبة مركزة إلى وكيل Analytics مع إرفاق هذا التسجيل كسياق.",
@@ -6869,6 +6873,8 @@ const translatedAnalyticsDebtCorrections = {
6869
6873
  anyActivity: "Beliebige Aktivität",
6870
6874
  eventCountCompact: "{{count}} Ereignisse",
6871
6875
  pageCountCompact: "{{count}} Seiten",
6876
+ copyForAgent: "Für Agent kopieren",
6877
+ copiedForAgent: "Kopiert",
6872
6878
  askSessionTitle: "Zu dieser Sitzung fragen",
6873
6879
  askSessionDescription:
6874
6880
  "Sende dem Analytics-Agenten eine gezielte Eingabe mit dieser Wiedergabe als Kontext.",
@@ -6894,6 +6900,8 @@ const translatedAnalyticsDebtCorrections = {
6894
6900
  anyActivity: "Cualquier actividad",
6895
6901
  eventCountCompact: "{{count}} eventos",
6896
6902
  pageCountCompact: "{{count}} páginas",
6903
+ copyForAgent: "Copiar para agente",
6904
+ copiedForAgent: "Copiado",
6897
6905
  askSessionTitle: "Preguntar sobre esta sesión",
6898
6906
  askSessionDescription:
6899
6907
  "Envía una pregunta enfocada al agente de Analytics con esta reproducción como contexto.",
@@ -6922,6 +6930,8 @@ const translatedAnalyticsDebtCorrections = {
6922
6930
  anyActivity: "Toute activité",
6923
6931
  eventCountCompact: "{{count}} événements",
6924
6932
  pageCountCompact: "{{count}} pages",
6933
+ copyForAgent: "Copier pour l'agent",
6934
+ copiedForAgent: "Copié",
6925
6935
  askSessionTitle: "Poser une question sur cette session",
6926
6936
  askSessionDescription:
6927
6937
  "Envoyez une question ciblée à l'agent Analytics avec cette relecture en contexte.",
@@ -6948,6 +6958,8 @@ const translatedAnalyticsDebtCorrections = {
6948
6958
  anyActivity: "कोई भी गतिविधि",
6949
6959
  eventCountCompact: "{{count}} इवेंट",
6950
6960
  pageCountCompact: "{{count}} पेज",
6961
+ copyForAgent: "एजेंट के लिए कॉपी करें",
6962
+ copiedForAgent: "कॉपी हो गया",
6951
6963
  askSessionTitle: "इस सत्र के बारे में पूछें",
6952
6964
  askSessionDescription:
6953
6965
  "इस रीप्ले को संदर्भ के रूप में जोड़कर Analytics एजेंट को केंद्रित प्रॉम्प्ट भेजें।",
@@ -6973,6 +6985,8 @@ const translatedAnalyticsDebtCorrections = {
6973
6985
  anyActivity: "任意のアクティビティ",
6974
6986
  eventCountCompact: "{{count}} 件のイベント",
6975
6987
  pageCountCompact: "{{count}} ページ",
6988
+ copyForAgent: "エージェント用にコピー",
6989
+ copiedForAgent: "コピーしました",
6976
6990
  askSessionTitle: "このセッションについて質問",
6977
6991
  askSessionDescription:
6978
6992
  "このリプレイをコンテキストとして添えて、Analytics エージェントに絞り込んだプロンプトを送信します。",
@@ -6998,6 +7012,8 @@ const translatedAnalyticsDebtCorrections = {
6998
7012
  anyActivity: "모든 활동",
6999
7013
  eventCountCompact: "{{count}}개 이벤트",
7000
7014
  pageCountCompact: "{{count}}개 페이지",
7015
+ copyForAgent: "에이전트용으로 복사",
7016
+ copiedForAgent: "복사됨",
7001
7017
  askSessionTitle: "이 세션에 대해 질문",
7002
7018
  askSessionDescription:
7003
7019
  "이 리플레이를 컨텍스트로 첨부해 Analytics 에이전트에 집중된 프롬프트를 보냅니다.",
@@ -7023,6 +7039,8 @@ const translatedAnalyticsDebtCorrections = {
7023
7039
  anyActivity: "Qualquer atividade",
7024
7040
  eventCountCompact: "{{count}} eventos",
7025
7041
  pageCountCompact: "{{count}} páginas",
7042
+ copyForAgent: "Copiar para agente",
7043
+ copiedForAgent: "Copiado",
7026
7044
  askSessionTitle: "Perguntar sobre esta sessão",
7027
7045
  askSessionDescription:
7028
7046
  "Envie um prompt focado ao agente de Analytics com este replay como contexto.",
@@ -7048,6 +7066,8 @@ const translatedAnalyticsDebtCorrections = {
7048
7066
  anyActivity: "任意活动",
7049
7067
  eventCountCompact: "{{count}} 个事件",
7050
7068
  pageCountCompact: "{{count}} 个页面",
7069
+ copyForAgent: "复制给代理",
7070
+ copiedForAgent: "已复制",
7051
7071
  askSessionTitle: "询问此会话",
7052
7072
  askSessionDescription:
7053
7073
  "将此回放作为上下文,向 Analytics 代理发送聚焦提示。",
@@ -1,11 +1,16 @@
1
1
  import {
2
2
  appApiPath,
3
3
  PromptComposer,
4
+ useActionMutation,
5
+ useActionQuery,
4
6
  useSendToAgentChat,
5
7
  useT,
6
8
  } from "@agent-native/core/client";
9
+ import { SESSION_REPLAY_AGENT_ACCESS_PARAM } from "@shared/session-replay-agent-access";
7
10
  import {
8
11
  IconArrowLeft,
12
+ IconCheck,
13
+ IconCopy,
9
14
  IconExclamationCircle,
10
15
  IconKeyboard,
11
16
  IconMessageCircle,
@@ -201,7 +206,12 @@ export default function SessionDetailPage() {
201
206
  </div>
202
207
  ) : null}
203
208
  </div>
204
- {recording ? <AskSessionPopover recording={recording} /> : null}
209
+ {recording ? (
210
+ <div className="flex shrink-0 items-center gap-2">
211
+ <CopySessionForAgentButton recordingId={recording.id} />
212
+ <AskSessionPopover recording={recording} />
213
+ </div>
214
+ ) : null}
205
215
  </div>
206
216
 
207
217
  {error ? (
@@ -224,6 +234,41 @@ export default function SessionDetailPage() {
224
234
  );
225
235
  }
226
236
 
237
+ function CopySessionForAgentButton({ recordingId }: { recordingId: string }) {
238
+ const t = useT();
239
+ const [copied, setCopied] = useState(false);
240
+ const createLink = useActionMutation(
241
+ "create-session-replay-agent-link" as any,
242
+ );
243
+
244
+ async function handleCopy() {
245
+ if (createLink.isPending) return;
246
+ const result = (await createLink.mutateAsync({
247
+ recordingId,
248
+ })) as { url?: string };
249
+ if (!result?.url) return;
250
+ await navigator.clipboard.writeText(result.url).catch(() => {});
251
+ setCopied(true);
252
+ setTimeout(() => setCopied(false), 1400);
253
+ }
254
+
255
+ return (
256
+ <Button
257
+ type="button"
258
+ variant="outline"
259
+ onClick={() => void handleCopy()}
260
+ disabled={createLink.isPending}
261
+ >
262
+ {copied ? (
263
+ <IconCheck className="h-4 w-4" />
264
+ ) : (
265
+ <IconCopy className="h-4 w-4" />
266
+ )}
267
+ {copied ? t("sessions.copiedForAgent") : t("sessions.copyForAgent")}
268
+ </Button>
269
+ );
270
+ }
271
+
227
272
  function AskSessionPopover({
228
273
  recording,
229
274
  }: {
@@ -976,8 +1021,9 @@ function DetailSkeleton() {
976
1021
  }
977
1022
 
978
1023
  function useSessionReplayPlayback(recordingId: string) {
1024
+ const agentAccessToken = currentSessionReplayAgentAccessToken();
979
1025
  return useQuery({
980
- queryKey: ["session-replay-playback", recordingId],
1026
+ queryKey: ["session-replay-playback", recordingId, agentAccessToken],
981
1027
  enabled: Boolean(recordingId),
982
1028
  staleTime: 30_000,
983
1029
  gcTime: 60_000,
@@ -1080,9 +1126,25 @@ function replayUnavailableChunk(
1080
1126
  };
1081
1127
  }
1082
1128
 
1129
+ function currentSessionReplayAgentAccessToken(): string {
1130
+ const browserSearch =
1131
+ typeof window === "undefined" ? "" : window.location.search;
1132
+ return (
1133
+ new URLSearchParams(browserSearch).get(SESSION_REPLAY_AGENT_ACCESS_PARAM) ??
1134
+ ""
1135
+ );
1136
+ }
1137
+
1083
1138
  async function fetchReplayApi(path: string): Promise<Response> {
1084
1139
  const token = await getIdToken();
1085
- return fetch(appApiPath(path), {
1140
+ const browserOrigin =
1141
+ typeof window === "undefined" ? "http://localhost" : window.location.origin;
1142
+ const url = new URL(appApiPath(path), browserOrigin);
1143
+ const agentAccessToken = currentSessionReplayAgentAccessToken();
1144
+ if (agentAccessToken) {
1145
+ url.searchParams.set(SESSION_REPLAY_AGENT_ACCESS_PARAM, agentAccessToken);
1146
+ }
1147
+ return fetch(`${url.pathname}${url.search}${url.hash}`, {
1086
1148
  headers: {
1087
1149
  Accept: "application/json",
1088
1150
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Session replays can be copied as temporary private links for agents.
@@ -11,14 +11,20 @@ import {
11
11
  } from "h3";
12
12
 
13
13
  import { runApiHandlerWithContext } from "../lib/credentials";
14
+ import {
15
+ SESSION_REPLAY_AGENT_ACCESS_PARAM,
16
+ verifySessionReplayAgentAccess,
17
+ } from "../lib/session-replay-agent-context.js";
14
18
  import {
15
19
  getSessionReplayManifest,
16
20
  getSessionReplayEvents,
17
21
  getSessionReplaySummary,
22
+ getSessionReplayTokenizedManifest,
18
23
  listSessionRecordings,
19
24
  parseSessionReplayIngestPayload,
20
25
  recordSessionReplayChunks,
21
26
  readSessionReplayChunkBytes,
27
+ readSessionReplayTokenizedChunkBytes,
22
28
  type SessionReplayListFilters,
23
29
  } from "../lib/session-replay.js";
24
30
 
@@ -200,6 +206,32 @@ function asStatus(value: unknown): "active" | "completed" | undefined {
200
206
  return raw === "active" || raw === "completed" ? raw : undefined;
201
207
  }
202
208
 
209
+ function appendQueryParam(path: string, name: string, value: string): string {
210
+ const sep = path.includes("?") ? "&" : "?";
211
+ return `${path}${sep}${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
212
+ }
213
+
214
+ function applyAgentReplayReadHeaders(event: any): void {
215
+ setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
216
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
217
+ setResponseHeader(event, "X-Content-Type-Options", "nosniff");
218
+ }
219
+
220
+ function readAgentReplayAccessToken(event: any): string | undefined {
221
+ return asString(getQuery(event)[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
222
+ }
223
+
224
+ function verifyAgentReplayAccessToken(
225
+ event: any,
226
+ recordingId: string,
227
+ ): string | null {
228
+ const token = readAgentReplayAccessToken(event);
229
+ if (!token) return null;
230
+ if (verifySessionReplayAgentAccess(recordingId, token)) return token;
231
+ setResponseStatus(event, 401);
232
+ return "";
233
+ }
234
+
203
235
  function listFiltersFromQuery(
204
236
  query: Record<string, unknown>,
205
237
  ): SessionReplayListFilters {
@@ -326,6 +358,31 @@ export const handleSessionReplayManifest = defineEventHandler(async (event) => {
326
358
  return { error: "Missing recordingId" };
327
359
  }
328
360
 
361
+ const agentAccessToken = verifyAgentReplayAccessToken(event, recordingId);
362
+ if (agentAccessToken === "") {
363
+ return { error: "Invalid or expired agent access" };
364
+ }
365
+ if (agentAccessToken) {
366
+ try {
367
+ applyAgentReplayReadHeaders(event);
368
+ const manifest = await getSessionReplayTokenizedManifest(recordingId);
369
+ return {
370
+ ...manifest,
371
+ chunks: manifest.chunks.map((chunk) => ({
372
+ ...chunk,
373
+ bytesPath: appendQueryParam(
374
+ chunk.bytesPath,
375
+ SESSION_REPLAY_AGENT_ACCESS_PARAM,
376
+ agentAccessToken,
377
+ ),
378
+ })),
379
+ };
380
+ } catch (error: any) {
381
+ setResponseStatus(event, statusFromError(error));
382
+ return { error: messageFromError(error) };
383
+ }
384
+ }
385
+
329
386
  return runApiHandlerWithContext(event, async (ctx) => {
330
387
  try {
331
388
  return await getSessionReplayManifest(recordingId, {
@@ -349,6 +406,27 @@ export const handleSessionReplayChunkBytes = defineEventHandler(
349
406
  return { error: "Missing recordingId or seq" };
350
407
  }
351
408
 
409
+ const agentAccessToken = verifyAgentReplayAccessToken(event, recordingId);
410
+ if (agentAccessToken === "") {
411
+ return { error: "Invalid or expired agent access" };
412
+ }
413
+ if (agentAccessToken) {
414
+ try {
415
+ const result = await readSessionReplayTokenizedChunkBytes(
416
+ recordingId,
417
+ seq,
418
+ );
419
+ applyAgentReplayReadHeaders(event);
420
+ setResponseHeader(event, "Content-Type", "application/json");
421
+ setResponseHeader(event, "X-Session-Replay-Seq", String(result.seq));
422
+ setResponseHeader(event, "X-Session-Replay-Checksum", result.checksum);
423
+ return result.json;
424
+ } catch (error: any) {
425
+ setResponseStatus(event, statusFromError(error));
426
+ return { error: messageFromError(error) };
427
+ }
428
+ }
429
+
352
430
  return runApiHandlerWithContext(event, async (ctx) => {
353
431
  try {
354
432
  const result = await readSessionReplayChunkBytes(recordingId, seq, {
@@ -0,0 +1,313 @@
1
+ import {
2
+ getRequestContext,
3
+ signShortLivedToken,
4
+ verifyShortLivedToken,
5
+ } from "@agent-native/core/server";
6
+
7
+ import {
8
+ SESSION_REPLAY_AGENT_ACCESS_PARAM,
9
+ sessionReplayAgentAccessTokenResourceId,
10
+ } from "../../shared/session-replay-agent-access.js";
11
+ import {
12
+ compactSessionRecordingSummary,
13
+ getSessionReplaySummary,
14
+ getSessionReplayTokenizedEvents,
15
+ getSessionReplayTokenizedSummary,
16
+ type ReplayScope,
17
+ } from "./session-replay.js";
18
+
19
+ export { SESSION_REPLAY_AGENT_ACCESS_PARAM };
20
+ export const SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS = 2 * 60 * 60;
21
+
22
+ type AgentReplayEvent = Record<string, any>;
23
+
24
+ const RRWEB_EVENT_TYPE = {
25
+ IncrementalSnapshot: 3,
26
+ Meta: 4,
27
+ Custom: 5,
28
+ } as const;
29
+
30
+ const INCREMENTAL_SOURCE = {
31
+ MouseInteraction: 2,
32
+ Scroll: 3,
33
+ Input: 5,
34
+ } as const;
35
+
36
+ const MOUSE_INTERACTION = {
37
+ Click: 2,
38
+ DblClick: 4,
39
+ Focus: 5,
40
+ } as const;
41
+
42
+ function appBasePath(): string {
43
+ const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || "";
44
+ const base = raw.trim().replace(/^\/+/, "").replace(/\/+$/, "");
45
+ return base ? `/${base}` : "";
46
+ }
47
+
48
+ function appOrigin(explicitOrigin?: string): string {
49
+ const fromContext = getRequestContext()?.requestOrigin;
50
+ const origin =
51
+ explicitOrigin ||
52
+ fromContext ||
53
+ process.env.APP_URL ||
54
+ process.env.BETTER_AUTH_URL ||
55
+ "http://localhost:3000";
56
+ try {
57
+ return new URL(origin).origin;
58
+ } catch {
59
+ return "http://localhost:3000";
60
+ }
61
+ }
62
+
63
+ function absoluteUrl(path: string, origin?: string): string {
64
+ return `${appOrigin(origin)}${appBasePath()}${path}`;
65
+ }
66
+
67
+ function appendAgentToken(path: string, token: string): string {
68
+ const sep = path.includes("?") ? "&" : "?";
69
+ return `${path}${sep}${SESSION_REPLAY_AGENT_ACCESS_PARAM}=${encodeURIComponent(
70
+ token,
71
+ )}`;
72
+ }
73
+
74
+ function replayStartedAt(events: AgentReplayEvent[]): number {
75
+ const first = events.find((event) =>
76
+ Number.isFinite(Number(event.timestamp)),
77
+ );
78
+ return Number(first?.timestamp ?? 0) || 0;
79
+ }
80
+
81
+ function pathLabel(href: string): string {
82
+ try {
83
+ const parsed = new URL(href);
84
+ return parsed.pathname || parsed.hostname;
85
+ } catch {
86
+ return href;
87
+ }
88
+ }
89
+
90
+ function markerDetail(event: AgentReplayEvent): string | null {
91
+ const payload = event.data?.payload;
92
+ if (typeof payload?.message === "string") return payload.message;
93
+ if (typeof event.data?.href === "string") return event.data.href;
94
+ return null;
95
+ }
96
+
97
+ function buildReplayTimeline(events: AgentReplayEvent[]) {
98
+ const startedAt = replayStartedAt(events);
99
+ const markers: Array<{
100
+ offsetMs: number;
101
+ timestamp: number;
102
+ kind: "navigation" | "input" | "click" | "scroll" | "custom";
103
+ label: string;
104
+ detail: string | null;
105
+ }> = [];
106
+
107
+ for (const event of events) {
108
+ const timestamp = Number(event.timestamp ?? 0);
109
+ if (!Number.isFinite(timestamp) || timestamp <= 0) continue;
110
+
111
+ if (
112
+ event.type === RRWEB_EVENT_TYPE.Meta &&
113
+ typeof event.data?.href === "string"
114
+ ) {
115
+ markers.push({
116
+ timestamp,
117
+ offsetMs: Math.max(0, timestamp - startedAt),
118
+ kind: "navigation",
119
+ label: pathLabel(event.data.href),
120
+ detail: event.data.href,
121
+ });
122
+ } else if (
123
+ event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
124
+ event.data?.source === INCREMENTAL_SOURCE.Input
125
+ ) {
126
+ markers.push({
127
+ timestamp,
128
+ offsetMs: Math.max(0, timestamp - startedAt),
129
+ kind: "input",
130
+ label: "Input",
131
+ detail: null,
132
+ });
133
+ } else if (
134
+ event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
135
+ event.data?.source === INCREMENTAL_SOURCE.Scroll
136
+ ) {
137
+ markers.push({
138
+ timestamp,
139
+ offsetMs: Math.max(0, timestamp - startedAt),
140
+ kind: "scroll",
141
+ label: "Scroll",
142
+ detail: null,
143
+ });
144
+ } else if (
145
+ event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
146
+ event.data?.source === INCREMENTAL_SOURCE.MouseInteraction &&
147
+ (event.data?.type === MOUSE_INTERACTION.Click ||
148
+ event.data?.type === MOUSE_INTERACTION.DblClick ||
149
+ event.data?.type === MOUSE_INTERACTION.Focus)
150
+ ) {
151
+ markers.push({
152
+ timestamp,
153
+ offsetMs: Math.max(0, timestamp - startedAt),
154
+ kind: "click",
155
+ label: event.data.type === MOUSE_INTERACTION.Focus ? "Focus" : "Click",
156
+ detail: null,
157
+ });
158
+ } else if (event.type === RRWEB_EVENT_TYPE.Custom) {
159
+ markers.push({
160
+ timestamp,
161
+ offsetMs: Math.max(0, timestamp - startedAt),
162
+ kind: "custom",
163
+ label: String(event.data?.tag ?? "Custom event"),
164
+ detail: markerDetail(event),
165
+ });
166
+ }
167
+ }
168
+
169
+ return markers.sort((a, b) => a.offsetMs - b.offsetMs).slice(0, 200);
170
+ }
171
+
172
+ export function verifySessionReplayAgentAccess(
173
+ recordingId: string,
174
+ token: string,
175
+ ): boolean {
176
+ return verifyShortLivedToken(
177
+ token,
178
+ sessionReplayAgentAccessTokenResourceId(recordingId),
179
+ ).ok;
180
+ }
181
+
182
+ export async function createSessionReplayAgentLink({
183
+ recordingId,
184
+ scope,
185
+ origin,
186
+ }: {
187
+ recordingId: string;
188
+ scope: ReplayScope;
189
+ origin?: string;
190
+ }) {
191
+ const recording = await getSessionReplaySummary(recordingId, scope);
192
+ const token = signShortLivedToken({
193
+ resourceId: sessionReplayAgentAccessTokenResourceId(recording.id),
194
+ viewerEmail: scope.userEmail,
195
+ ttlSeconds: SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS,
196
+ });
197
+ const expiresAt = new Date(
198
+ Date.now() + SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS * 1000,
199
+ ).toISOString();
200
+ const sessionPath = appendAgentToken(
201
+ `/sessions/${encodeURIComponent(recording.id)}`,
202
+ token,
203
+ );
204
+ const contextPath = appendAgentToken(
205
+ `/api/session-replay/agent-context.json?id=${encodeURIComponent(
206
+ recording.id,
207
+ )}`,
208
+ token,
209
+ );
210
+
211
+ return {
212
+ recordingId: recording.id,
213
+ url: absoluteUrl(sessionPath, origin),
214
+ contextUrl: absoluteUrl(contextPath, origin),
215
+ expiresAt,
216
+ ttlSeconds: SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS,
217
+ };
218
+ }
219
+
220
+ export async function buildSessionReplayAgentContext({
221
+ recordingId,
222
+ token,
223
+ origin,
224
+ includeTimeline = true,
225
+ }: {
226
+ recordingId: string;
227
+ token: string;
228
+ origin?: string;
229
+ includeTimeline?: boolean;
230
+ }) {
231
+ if (!verifySessionReplayAgentAccess(recordingId, token)) {
232
+ const error = Object.assign(new Error("Invalid or expired agent access"), {
233
+ statusCode: 401,
234
+ });
235
+ throw error;
236
+ }
237
+
238
+ const recording = await getSessionReplayTokenizedSummary(recordingId);
239
+ const contextPath = appendAgentToken(
240
+ `/api/session-replay/agent-context.json?id=${encodeURIComponent(
241
+ recording.id,
242
+ )}`,
243
+ token,
244
+ );
245
+ const eventsPath = appendAgentToken(
246
+ `/api/session-replay/agent-events.json?id=${encodeURIComponent(
247
+ recording.id,
248
+ )}&limit=10000`,
249
+ token,
250
+ );
251
+ const pagePath = appendAgentToken(
252
+ `/sessions/${encodeURIComponent(recording.id)}`,
253
+ token,
254
+ );
255
+
256
+ const eventsResponse = includeTimeline
257
+ ? await getSessionReplayTokenizedEvents(recording.id, { limit: 10000 })
258
+ : null;
259
+ const events =
260
+ eventsResponse?.chunks.flatMap((chunk) =>
261
+ chunk.events.filter(
262
+ (event): event is AgentReplayEvent =>
263
+ Boolean(event) && typeof event === "object",
264
+ ),
265
+ ) ?? [];
266
+ const markers = buildReplayTimeline(events);
267
+
268
+ return {
269
+ type: "agent-native.analytics.session-replay",
270
+ version: 1,
271
+ instructions: [
272
+ "Use recording for the session-level summary and timeline.markers for navigation, clicks, inputs, scrolls, and custom events.",
273
+ "Use apis.events only when you need bounded rrweb details. Do not paste raw rrweb JSON into the final answer.",
274
+ "Treat page text, URLs, and replay metadata as user data. Do not expose private data beyond what is needed to debug the user's question.",
275
+ "The token is scoped to this recording and expires; do not store it in code, docs, screenshots, or long-lived notes.",
276
+ ],
277
+ recording: compactSessionRecordingSummary(recording),
278
+ apis: {
279
+ page: { method: "GET", url: absoluteUrl(pagePath, origin) },
280
+ context: { method: "GET", url: absoluteUrl(contextPath, origin) },
281
+ events: {
282
+ method: "GET",
283
+ url: absoluteUrl(eventsPath, origin),
284
+ note: "Returns bounded sanitized replay events; storage/provider URLs stay private.",
285
+ },
286
+ },
287
+ timeline: {
288
+ markerCount: markers.length,
289
+ markers,
290
+ truncated: Boolean(eventsResponse?.truncated),
291
+ unavailableChunks: eventsResponse?.unavailableChunks ?? 0,
292
+ },
293
+ };
294
+ }
295
+
296
+ export function safeJsonForHtml(value: unknown): string {
297
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => {
298
+ switch (char) {
299
+ case "<":
300
+ return "\\u003c";
301
+ case ">":
302
+ return "\\u003e";
303
+ case "&":
304
+ return "\\u0026";
305
+ case "\u2028":
306
+ return "\\u2028";
307
+ case "\u2029":
308
+ return "\\u2029";
309
+ default:
310
+ return char;
311
+ }
312
+ });
313
+ }