@agent-native/core 0.84.56 → 0.84.58

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 (151) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -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/action.ts +11 -0
  28. package/corpus/core/src/agent/production-agent.ts +24 -46
  29. package/corpus/core/src/client/AssistantChat.tsx +69 -12
  30. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  31. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +45 -2
  32. package/corpus/core/src/coding-tools/run-code.ts +1 -0
  33. package/corpus/core/src/integrations/webhook-handler.ts +10 -9
  34. package/corpus/core/src/server/action-discovery.ts +3 -0
  35. package/corpus/core/src/server/credential-provider.ts +11 -6
  36. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +8 -0
  37. package/corpus/templates/analytics/AGENTS.md +5 -0
  38. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +31 -0
  39. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  40. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  41. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +65 -3
  42. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-can-be-copied-as-temporary-private-links-for-agents.md +6 -0
  43. package/corpus/templates/analytics/server/handlers/session-replay.ts +78 -0
  44. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +29 -0
  45. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +313 -0
  46. package/corpus/templates/analytics/server/lib/session-replay.ts +198 -3
  47. package/corpus/templates/analytics/server/plugins/agent-chat.ts +9 -1
  48. package/corpus/templates/analytics/server/routes/[...page].get.ts +104 -1
  49. package/corpus/templates/analytics/server/routes/api/session-replay/agent-context.json.get.ts +48 -0
  50. package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +63 -0
  51. package/corpus/templates/analytics/shared/session-replay-agent-access.ts +10 -0
  52. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +14 -8
  53. package/corpus/templates/clips/AGENTS.md +3 -2
  54. package/corpus/templates/clips/actions/create-recording-agent-link.ts +90 -0
  55. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -46
  56. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -3
  57. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -1
  58. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -1
  59. package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
  60. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -1
  61. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -1
  62. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -1
  63. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -1
  64. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -1
  65. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -1
  66. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -1
  67. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -1
  68. package/corpus/templates/clips/app/routes/share.$shareId.tsx +100 -46
  69. 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
  70. package/corpus/templates/clips/server/lib/public-agent-context.ts +26 -13
  71. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +2 -1
  72. package/corpus/templates/clips/server/routes/api/agent-frame.jpg.get.ts +2 -1
  73. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +2 -1
  74. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -2
  75. package/corpus/templates/clips/shared/agent-context.ts +5 -0
  76. package/corpus/templates/design/actions/list-design-extensions.ts +2 -2
  77. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +11 -89
  78. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +314 -181
  79. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +92 -134
  80. package/corpus/templates/design/app/components/design/EditPanel.tsx +93 -19
  81. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -26
  82. package/corpus/templates/design/app/components/design/TokensPanel.tsx +220 -192
  83. package/corpus/templates/design/app/components/design/code-workbench-theme.ts +150 -0
  84. package/corpus/templates/design/app/i18n/zh-TW.ts +5 -7
  85. package/corpus/templates/design/app/i18n-data.ts +61 -77
  86. package/corpus/templates/design/app/lib/design-import.ts +4 -1
  87. package/corpus/templates/design/app/pages/DesignEditor.tsx +97 -48
  88. package/corpus/templates/design/changelog/2026-07-02-design-inspector-and-canvas-controls-stay-consistent-when-se.md +6 -0
  89. package/corpus/templates/design/changelog/2026-07-02-fixed-a-crash-when-opening-the-code-editor.md +6 -0
  90. package/corpus/templates/design/changelog/2026-07-02-fixed-motion-drawer-open-transition.md +6 -0
  91. package/corpus/templates/design/changelog/2026-07-02-improved-design-assets-panel.md +6 -0
  92. package/dist/action.d.ts +8 -0
  93. package/dist/action.d.ts.map +1 -1
  94. package/dist/action.js +3 -0
  95. package/dist/action.js.map +1 -1
  96. package/dist/agent/production-agent.d.ts +10 -11
  97. package/dist/agent/production-agent.d.ts.map +1 -1
  98. package/dist/agent/production-agent.js +21 -44
  99. package/dist/agent/production-agent.js.map +1 -1
  100. package/dist/client/AssistantChat.d.ts +1 -0
  101. package/dist/client/AssistantChat.d.ts.map +1 -1
  102. package/dist/client/AssistantChat.js +61 -11
  103. package/dist/client/AssistantChat.js.map +1 -1
  104. package/dist/client/MultiTabAssistantChat.js +1 -1
  105. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  106. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  107. package/dist/client/conversation/use-near-bottom-autoscroll.js +43 -2
  108. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  109. package/dist/coding-tools/run-code.d.ts.map +1 -1
  110. package/dist/coding-tools/run-code.js +1 -0
  111. package/dist/coding-tools/run-code.js.map +1 -1
  112. package/dist/collab/routes.d.ts +2 -2
  113. package/dist/integrations/webhook-handler.d.ts +1 -0
  114. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  115. package/dist/integrations/webhook-handler.js +9 -9
  116. package/dist/integrations/webhook-handler.js.map +1 -1
  117. package/dist/notifications/routes.d.ts +3 -3
  118. package/dist/observability/routes.d.ts +3 -3
  119. package/dist/resources/handlers.d.ts +1 -1
  120. package/dist/server/action-discovery.d.ts.map +1 -1
  121. package/dist/server/action-discovery.js +3 -0
  122. package/dist/server/action-discovery.js.map +1 -1
  123. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  124. package/dist/server/credential-provider.d.ts +4 -0
  125. package/dist/server/credential-provider.d.ts.map +1 -1
  126. package/dist/server/credential-provider.js +13 -7
  127. package/dist/server/credential-provider.js.map +1 -1
  128. package/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
  129. package/docs/content/locales/ar-SA/template-clips.mdx +2 -0
  130. package/docs/content/locales/de-DE/template-analytics.mdx +2 -0
  131. package/docs/content/locales/de-DE/template-clips.mdx +2 -0
  132. package/docs/content/locales/es-ES/template-analytics.mdx +2 -0
  133. package/docs/content/locales/es-ES/template-clips.mdx +2 -0
  134. package/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
  135. package/docs/content/locales/fr-FR/template-clips.mdx +2 -0
  136. package/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
  137. package/docs/content/locales/hi-IN/template-clips.mdx +2 -0
  138. package/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
  139. package/docs/content/locales/ja-JP/template-clips.mdx +2 -0
  140. package/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
  141. package/docs/content/locales/ko-KR/template-clips.mdx +2 -0
  142. package/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
  143. package/docs/content/locales/pt-BR/template-clips.mdx +2 -0
  144. package/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
  145. package/docs/content/locales/zh-CN/template-clips.mdx +2 -0
  146. package/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
  147. package/docs/content/locales/zh-TW/template-clips.mdx +2 -0
  148. package/docs/content/template-analytics.mdx +2 -0
  149. package/docs/content/template-clips.mdx +22 -10
  150. package/docs/content/tracking.mdx +2 -0
  151. package/package.json +1 -1
@@ -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,29 @@
1
+ import type { ActionEntry } from "@agent-native/core/server";
2
+
3
+ export const PLAN_MODE_ACT_ONLY_TOOLS = new Set([
4
+ "query-agent-native-analytics",
5
+ "bigquery",
6
+ "provider-api-request",
7
+ "provider-corpus-job",
8
+ "query-staged-dataset",
9
+ "account-deep-dive",
10
+ "hubspot-deals",
11
+ "hubspot-records",
12
+ "gong-calls",
13
+ "jira-search",
14
+ "slack-messages",
15
+ "sentry",
16
+ ]);
17
+
18
+ export function applyAnalyticsPlanModePolicy(
19
+ actions: Record<string, ActionEntry>,
20
+ ): Record<string, ActionEntry> {
21
+ return Object.fromEntries(
22
+ Object.entries(actions).map(([name, entry]) => [
23
+ name,
24
+ PLAN_MODE_ACT_ONLY_TOOLS.has(name)
25
+ ? { ...entry, allowInPlanMode: false }
26
+ : entry,
27
+ ]),
28
+ );
29
+ }
@@ -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
+ }
@@ -144,6 +144,70 @@ export interface SessionRecordingSummary {
144
144
  canManage?: boolean;
145
145
  }
146
146
 
147
+ export interface AgentSessionRecordingSummary {
148
+ id: string;
149
+ clientRecordingId: string;
150
+ sessionId: string;
151
+ userId: string | null;
152
+ anonymousId: string | null;
153
+ userKey: string | null;
154
+ startedAt: string;
155
+ endedAt: string | null;
156
+ durationMs: number | null;
157
+ chunkCount: number;
158
+ eventCount: number;
159
+ totalBytes: number;
160
+ pageCount: number;
161
+ errorCount: number;
162
+ rageClickCount: number;
163
+ privacyMode: string;
164
+ firstUrl: string | null;
165
+ lastUrl: string | null;
166
+ path: string | null;
167
+ hostname: string | null;
168
+ referrer: string | null;
169
+ app: string | null;
170
+ template: string | null;
171
+ status: "active" | "completed";
172
+ createdAt: string;
173
+ updatedAt: string;
174
+ lastIngestedAt: string | null;
175
+ }
176
+
177
+ export function compactSessionRecordingSummary(
178
+ recording: SessionRecordingSummary,
179
+ ): AgentSessionRecordingSummary {
180
+ return {
181
+ id: recording.id,
182
+ clientRecordingId: recording.clientRecordingId,
183
+ sessionId: recording.sessionId,
184
+ userId: recording.userId,
185
+ anonymousId: recording.anonymousId,
186
+ userKey: recording.userKey,
187
+ startedAt: recording.startedAt,
188
+ endedAt: recording.endedAt,
189
+ durationMs: recording.durationMs,
190
+ chunkCount: recording.chunkCount,
191
+ eventCount: recording.eventCount,
192
+ totalBytes: recording.totalBytes,
193
+ pageCount: recording.pageCount,
194
+ errorCount: recording.errorCount,
195
+ rageClickCount: recording.rageClickCount,
196
+ privacyMode: recording.privacyMode,
197
+ firstUrl: recording.firstUrl,
198
+ lastUrl: recording.lastUrl,
199
+ path: recording.path,
200
+ hostname: recording.hostname,
201
+ referrer: recording.referrer,
202
+ app: recording.app,
203
+ template: recording.template,
204
+ status: recording.status,
205
+ createdAt: recording.createdAt,
206
+ updatedAt: recording.updatedAt,
207
+ lastIngestedAt: recording.lastIngestedAt,
208
+ };
209
+ }
210
+
147
211
  const MAX_REPLAY_CHUNKS_PER_REQUEST = 20;
148
212
  const MAX_REPLAY_CHUNKS_PER_RECORDING = 2_000;
149
213
  const MAX_INLINE_REPLAY_CHUNK_BYTES = 256 * 1024;
@@ -1524,6 +1588,22 @@ export async function getSessionReplaySummary(
1524
1588
  return rowToSessionRecordingSummary(access.resource, access.role);
1525
1589
  }
1526
1590
 
1591
+ export async function getSessionReplayTokenizedSummary(
1592
+ recordingId: string,
1593
+ ): Promise<SessionRecordingSummary> {
1594
+ const db = getDb() as any;
1595
+ // guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
1596
+ const [row] = await db
1597
+ .select()
1598
+ .from(schema.sessionRecordings)
1599
+ .where(eq(schema.sessionRecordings.id, recordingId))
1600
+ .limit(1);
1601
+ if (!row || !isVisibleSessionRecording(row)) {
1602
+ throw replayError("Session recording not found", 404);
1603
+ }
1604
+ return rowToSessionRecordingSummary(row, "viewer");
1605
+ }
1606
+
1527
1607
  function parseInlineReplayEvents(inlineData: string): unknown[] {
1528
1608
  try {
1529
1609
  const parsed = JSON.parse(inlineData);
@@ -1563,8 +1643,47 @@ export async function getSessionReplayManifest(
1563
1643
  }>;
1564
1644
  }> {
1565
1645
  const recording = await getSessionReplaySummary(recordingId, scope);
1646
+ return getSessionReplayManifestForRecording(recording);
1647
+ }
1648
+
1649
+ export async function getSessionReplayTokenizedManifest(
1650
+ recordingId: string,
1651
+ ): Promise<{
1652
+ recording: AgentSessionRecordingSummary;
1653
+ chunks: Array<{
1654
+ seq: number;
1655
+ checksum: string;
1656
+ byteLength: number;
1657
+ eventCount: number;
1658
+ startedAt: string | null;
1659
+ endedAt: string | null;
1660
+ bytesPath: string;
1661
+ }>;
1662
+ }> {
1663
+ const recording = await getSessionReplayTokenizedSummary(recordingId);
1664
+ const manifest = await getSessionReplayManifestForRecording(recording);
1665
+ return {
1666
+ ...manifest,
1667
+ recording: compactSessionRecordingSummary(manifest.recording),
1668
+ };
1669
+ }
1670
+
1671
+ async function getSessionReplayManifestForRecording(
1672
+ recording: SessionRecordingSummary,
1673
+ ): Promise<{
1674
+ recording: SessionRecordingSummary;
1675
+ chunks: Array<{
1676
+ seq: number;
1677
+ checksum: string;
1678
+ byteLength: number;
1679
+ eventCount: number;
1680
+ startedAt: string | null;
1681
+ endedAt: string | null;
1682
+ bytesPath: string;
1683
+ }>;
1684
+ }> {
1566
1685
  const db = getDb() as any;
1567
- // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) verifies viewer access; chunks are not directly shareable resources.
1686
+ // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) or a scoped agent token verifies access; chunks are not directly shareable resources.
1568
1687
  const rows = await db
1569
1688
  .select()
1570
1689
  .from(schema.sessionReplayChunks)
@@ -1599,8 +1718,39 @@ export async function readSessionReplayChunkBytes(
1599
1718
  json: string;
1600
1719
  }> {
1601
1720
  const recording = await getSessionReplaySummary(recordingId, scope);
1721
+ return readSessionReplayChunkBytesForRecording(recording, seq);
1722
+ }
1723
+
1724
+ export async function readSessionReplayTokenizedChunkBytes(
1725
+ recordingId: string,
1726
+ seq: number,
1727
+ ): Promise<{
1728
+ recording: AgentSessionRecordingSummary;
1729
+ seq: number;
1730
+ checksum: string;
1731
+ /** Decompressed replay-chunk JSON text (a serialized rrweb events array). */
1732
+ json: string;
1733
+ }> {
1734
+ const recording = await getSessionReplayTokenizedSummary(recordingId);
1735
+ const chunk = await readSessionReplayChunkBytesForRecording(recording, seq);
1736
+ return {
1737
+ ...chunk,
1738
+ recording: compactSessionRecordingSummary(chunk.recording),
1739
+ };
1740
+ }
1741
+
1742
+ async function readSessionReplayChunkBytesForRecording(
1743
+ recording: SessionRecordingSummary,
1744
+ seq: number,
1745
+ ): Promise<{
1746
+ recording: SessionRecordingSummary;
1747
+ seq: number;
1748
+ checksum: string;
1749
+ /** Decompressed replay-chunk JSON text (a serialized rrweb events array). */
1750
+ json: string;
1751
+ }> {
1602
1752
  const db = getDb() as any;
1603
- // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) verifies viewer access; chunks are not directly shareable resources.
1753
+ // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) or a scoped agent token verifies access; chunks are not directly shareable resources.
1604
1754
  const [row] = await db
1605
1755
  .select()
1606
1756
  .from(schema.sessionReplayChunks)
@@ -1660,6 +1810,51 @@ export async function getSessionReplayEvents(
1660
1810
  unavailableChunks: number;
1661
1811
  }> {
1662
1812
  const recording = await getSessionReplaySummary(recordingId, scope);
1813
+ return getSessionReplayEventsForRecording(recording, options);
1814
+ }
1815
+
1816
+ export async function getSessionReplayTokenizedEvents(
1817
+ recordingId: string,
1818
+ options: SessionReplayEventReadOptions = {},
1819
+ ): Promise<{
1820
+ recording: AgentSessionRecordingSummary;
1821
+ chunks: Array<{
1822
+ seq: number;
1823
+ checksum: string;
1824
+ byteLength: number;
1825
+ eventCount: number;
1826
+ events: unknown[];
1827
+ unavailable?: boolean;
1828
+ }>;
1829
+ eventCount: number;
1830
+ truncated: boolean;
1831
+ unavailableChunks: number;
1832
+ }> {
1833
+ const recording = await getSessionReplayTokenizedSummary(recordingId);
1834
+ const result = await getSessionReplayEventsForRecording(recording, options);
1835
+ return {
1836
+ ...result,
1837
+ recording: compactSessionRecordingSummary(result.recording),
1838
+ };
1839
+ }
1840
+
1841
+ async function getSessionReplayEventsForRecording(
1842
+ recording: SessionRecordingSummary,
1843
+ options: SessionReplayEventReadOptions = {},
1844
+ ): Promise<{
1845
+ recording: SessionRecordingSummary;
1846
+ chunks: Array<{
1847
+ seq: number;
1848
+ checksum: string;
1849
+ byteLength: number;
1850
+ eventCount: number;
1851
+ events: unknown[];
1852
+ unavailable?: boolean;
1853
+ }>;
1854
+ eventCount: number;
1855
+ truncated: boolean;
1856
+ unavailableChunks: number;
1857
+ }> {
1663
1858
  const maxEvents = Math.min(
1664
1859
  MAX_REPLAY_EVENTS_READ,
1665
1860
  Math.max(1, options.limit ?? MAX_REPLAY_EVENTS_READ),
@@ -1675,7 +1870,7 @@ export async function getSessionReplayEvents(
1675
1870
  }
1676
1871
 
1677
1872
  const db = getDb() as any;
1678
- // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) verifies viewer access; chunks are not directly shareable resources.
1873
+ // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) or a scoped agent token verifies access; chunks are not directly shareable resources.
1679
1874
  const rows = await db
1680
1875
  .select()
1681
1876
  .from(schema.sessionReplayChunks)