@agent-native/core 0.157.3 → 0.157.5

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 (211) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  3. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +21 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +40 -1
  5. package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  6. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +21 -0
  7. package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  8. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +21 -0
  9. package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  10. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +21 -0
  11. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
  12. package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  13. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  14. package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  15. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -7
  16. package/corpus/templates/clips/actions/approve-recording-access-request.ts +147 -0
  17. package/corpus/templates/clips/actions/request-recording-access.ts +121 -23
  18. package/corpus/templates/clips/actions/view-screen.ts +1 -1
  19. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +34 -53
  20. package/corpus/templates/clips/app/components/player/request-access-dialog.tsx +104 -0
  21. package/corpus/templates/clips/app/components/player/share-dialog.tsx +36 -45
  22. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +3 -6
  23. package/corpus/templates/clips/app/i18n/en-US.ts +26 -1
  24. package/corpus/templates/clips/app/lib/public-meeting.ts +1 -1
  25. package/corpus/templates/clips/app/routes/access-request.approve.tsx +250 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -32
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +123 -37
  28. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +41 -1
  29. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +1 -0
  30. package/corpus/templates/clips/server/lib/emails.ts +40 -11
  31. package/corpus/templates/clips/shared/recording-link.ts +32 -1
  32. package/corpus/templates/clips/shared/share-attribution.ts +13 -0
  33. package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  34. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +21 -0
  35. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +22 -10
  36. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +2 -1
  37. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +245 -9
  38. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +185 -134
  39. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +61 -45
  40. package/corpus/templates/content/app/components/editor/extensions/ImageNode.ts +6 -0
  41. package/corpus/templates/content/app/components/editor/image-upload.ts +137 -5
  42. package/corpus/templates/content/app/global.css +8 -0
  43. package/corpus/templates/content/changelog/2026-08-12-image-uploads-now-confirm-the-image-can-be-displayed-before-.md +6 -0
  44. package/corpus/templates/content/changelog/2026-08-14-images-fill-the-page-width-by-default-and-remain-visible-whe.md +6 -0
  45. package/corpus/templates/content/changelog/2026-08-14-images-inserted-from-the-slash-command-stay-saved-after-uplo.md +6 -0
  46. package/corpus/templates/content/changelog/2026-08-14-svg-files-can-be-selected-reliably-from-the-image-picker.md +6 -0
  47. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  48. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +21 -0
  49. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  50. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +21 -0
  51. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  52. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +21 -0
  53. package/corpus/templates/factory/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  54. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +21 -0
  55. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  56. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +21 -0
  57. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  58. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +21 -0
  59. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  60. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +21 -0
  61. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -1
  62. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  63. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +21 -0
  64. package/corpus/templates/plan/app/pages/PlansPage.tsx +25 -30
  65. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  66. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +21 -0
  67. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +2 -1
  68. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +124 -180
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +1 -0
  70. package/corpus/templates/slides/app/global.css +99 -3
  71. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  72. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  73. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +21 -0
  74. package/dist/chat-threads/schema.d.ts +1 -1
  75. package/dist/cli/code-agent-executor.js +318 -0
  76. package/dist/client/AgentPanel.d.ts +13 -1
  77. package/dist/client/AgentPanel.js +41 -3
  78. package/dist/client/AssistantChat.d.ts +9 -1
  79. package/dist/client/AssistantChat.js +26 -13
  80. package/dist/client/MultiTabAssistantChat.js +18 -5
  81. package/dist/client/agent-chat.d.ts +8 -0
  82. package/dist/client/agent-chat.js +15 -0
  83. package/dist/client/app-chat-sidebar.d.ts +23 -0
  84. package/dist/client/app-chat-sidebar.js +53 -0
  85. package/dist/client/app-providers.js +31 -2
  86. package/dist/client/frame-protocol.d.ts +20 -2
  87. package/dist/client/hooks/index.d.ts +1 -0
  88. package/dist/client/hooks/index.js +1 -0
  89. package/dist/client/sharing/AgentShareSection.d.ts +8 -0
  90. package/dist/client/sharing/AgentShareSection.js +50 -0
  91. package/dist/client/sharing/ShareButton.js +2 -1
  92. package/dist/client/sharing/ShareDialog.d.ts +2 -2
  93. package/dist/client/sharing/ShareDialog.js +21 -29
  94. package/dist/client/sharing/index.d.ts +1 -0
  95. package/dist/client/sharing/index.js +1 -0
  96. package/dist/client/sharing/useShareButtonController.d.ts +1 -0
  97. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  98. package/dist/client/sharing/useShareDialogController.js +2 -1
  99. package/dist/client/theme.d.ts +21 -0
  100. package/dist/client/theme.js +60 -0
  101. package/dist/client/ui/index.d.ts +1 -1
  102. package/dist/client/ui/index.js +1 -1
  103. package/dist/client/use-session.js +23 -0
  104. package/dist/collab/awareness.d.ts +2 -2
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/dashboard-storage/schema.d.ts +1 -1
  107. package/dist/data-programs/schema.d.ts +1 -1
  108. package/dist/extensions/schema.d.ts +1 -1
  109. package/dist/localization/core-messages/ar-SA.js +6 -0
  110. package/dist/localization/core-messages/de-DE.js +6 -0
  111. package/dist/localization/core-messages/en-US.d.ts +6 -0
  112. package/dist/localization/core-messages/en-US.js +6 -0
  113. package/dist/localization/core-messages/es-ES.js +6 -0
  114. package/dist/localization/core-messages/fr-FR.js +6 -0
  115. package/dist/localization/core-messages/hi-IN.js +6 -0
  116. package/dist/localization/core-messages/ja-JP.js +6 -0
  117. package/dist/localization/core-messages/ko-KR.js +6 -0
  118. package/dist/localization/core-messages/pt-BR.js +6 -0
  119. package/dist/localization/core-messages/zh-CN.js +6 -0
  120. package/dist/localization/core-messages/zh-TW.js +6 -0
  121. package/dist/localization/core-messages.d.ts +6 -0
  122. package/dist/localization/default-messages.d.ts +1 -0
  123. package/dist/localization/default-messages.js +1 -0
  124. package/dist/notifications/routes.d.ts +3 -3
  125. package/dist/progress/routes.d.ts +1 -1
  126. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  127. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  128. package/dist/server/auth.js +8 -5
  129. package/dist/server/identity-sso-store.d.ts +6 -1
  130. package/dist/server/identity-sso-store.js +57 -14
  131. package/dist/server/identity-sso.d.ts +7 -8
  132. package/dist/server/identity-sso.js +15 -12
  133. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  134. package/dist/sharing/actions/list-resource-shares.js +1 -0
  135. package/dist/sharing/schema.d.ts +1 -1
  136. package/dist/sharing/schema.js +4 -1
  137. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  138. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  139. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  140. package/dist/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  141. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  142. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  143. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
  144. package/docs/content/actions-access-control.mdx +226 -0
  145. package/docs/content/actions-advanced.mdx +152 -0
  146. package/docs/content/actions-defining.mdx +275 -0
  147. package/docs/content/actions-other-surfaces.mdx +167 -0
  148. package/docs/content/actions-overview.mdx +214 -0
  149. package/docs/content/actions-run-context.mdx +180 -0
  150. package/docs/content/agent-surfaces.mdx +1 -1
  151. package/docs/content/agent-teams.mdx +1 -1
  152. package/docs/content/agent-web-surfaces.mdx +1 -1
  153. package/docs/content/audit-log.mdx +1 -1
  154. package/docs/content/automation-connectors.mdx +1 -1
  155. package/docs/content/automations.mdx +1 -1
  156. package/docs/content/blueprint-installer.mdx +1 -1
  157. package/docs/content/cli-adapters.mdx +1 -1
  158. package/docs/content/client-agent-chat.mdx +1 -1
  159. package/docs/content/client-data.mdx +2 -2
  160. package/docs/content/client-overview.mdx +2 -2
  161. package/docs/content/components.mdx +1 -1
  162. package/docs/content/cross-app-sso.mdx +24 -17
  163. package/docs/content/data-programs.mdx +2 -2
  164. package/docs/content/database.mdx +1 -1
  165. package/docs/content/deployment.mdx +1 -1
  166. package/docs/content/docs-components.mdx +14 -3
  167. package/docs/content/drop-in-agent.mdx +2 -2
  168. package/docs/content/durable-resume.mdx +1 -1
  169. package/docs/content/evals.mdx +1 -1
  170. package/docs/content/extensions.mdx +1 -1
  171. package/docs/content/external-agents-catalog.mdx +2 -2
  172. package/docs/content/external-agents.mdx +1 -1
  173. package/docs/content/generative-ui.mdx +1 -1
  174. package/docs/content/getting-started.mdx +1 -1
  175. package/docs/content/http-api.mdx +4 -4
  176. package/docs/content/human-approval.mdx +1 -1
  177. package/docs/content/key-concepts.mdx +1 -1
  178. package/docs/content/mcp-apps.mdx +1 -1
  179. package/docs/content/mcp-clients.mdx +1 -1
  180. package/docs/content/mcp-protocol.mdx +1 -1
  181. package/docs/content/native-chat-ui.mdx +1 -1
  182. package/docs/content/observability.mdx +1 -1
  183. package/docs/content/organizations-teams-permissions.mdx +1 -1
  184. package/docs/content/processors.mdx +1 -1
  185. package/docs/content/pure-agent-apps.mdx +1 -1
  186. package/docs/content/recurring-jobs.mdx +1 -1
  187. package/docs/content/server-database.mdx +1 -1
  188. package/docs/content/server-overview.mdx +3 -3
  189. package/docs/content/server-routes.mdx +2 -2
  190. package/docs/content/template-analytics-developers.mdx +1 -1
  191. package/docs/content/template-assets-developers.mdx +1 -1
  192. package/docs/content/template-brain-developers.mdx +1 -1
  193. package/docs/content/template-calendar-developers.mdx +1 -1
  194. package/docs/content/template-chat-first-edits.mdx +1 -1
  195. package/docs/content/template-chat.mdx +1 -1
  196. package/docs/content/template-content-developers.mdx +1 -1
  197. package/docs/content/template-forms-developers.mdx +1 -1
  198. package/docs/content/template-forms.mdx +1 -1
  199. package/docs/content/template-mail-developers.mdx +2 -2
  200. package/docs/content/toolkit-org-team.mdx +1 -1
  201. package/docs/content/tracking.mdx +1 -1
  202. package/docs/content/what-is-agent-native.mdx +1 -1
  203. package/package.json +2 -2
  204. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  205. package/src/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  206. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  207. package/src/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  208. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  209. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  210. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
  211. package/docs/content/actions.mdx +0 -750
@@ -8,6 +8,7 @@ import {
8
8
  isEmailConfigured,
9
9
  renderEmail,
10
10
  sendEmail,
11
+ signScopedAgentAccessToken,
11
12
  verifyScopedAgentAccessToken,
12
13
  withConfiguredAppBasePath,
13
14
  } from "@agent-native/core/server";
@@ -23,12 +24,53 @@ import { z } from "zod";
23
24
  import { getDb, schema } from "../server/db/index.js";
24
25
  import { normalizeOwnerEmail } from "../server/lib/recordings.js";
25
26
  import {
27
+ CLIPS_ACCESS_APPROVAL_TOKEN_PREFIX,
28
+ CLIPS_ACCESS_APPROVAL_TOKEN_TTL_SECONDS,
26
29
  CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
30
+ recordingAccessApprovalPath,
27
31
  recordingSharePath,
28
32
  } from "../shared/recording-link.js";
29
33
 
30
34
  export const CLIPS_ACCESS_REQUEST_EMAIL_ID = "clips.access-request";
31
35
 
36
+ const ANONYMOUS_ACCESS_REQUEST_WINDOW_MS = 10 * 60 * 1000;
37
+ const ANONYMOUS_ACCESS_REQUEST_MAX = 5;
38
+ const ANONYMOUS_ACCESS_REQUEST_MAX_BUCKETS = 5000;
39
+ const anonymousAccessRequestBuckets = new Map<
40
+ string,
41
+ { count: number; resetAt: number }
42
+ >();
43
+
44
+ export function __resetAnonymousAccessRequestRateLimitForTests(): void {
45
+ anonymousAccessRequestBuckets.clear();
46
+ }
47
+
48
+ function allowAnonymousAccessRequest(recordingId: string): boolean {
49
+ const now = Date.now();
50
+ for (const [key, bucket] of anonymousAccessRequestBuckets) {
51
+ if (bucket.resetAt <= now) anonymousAccessRequestBuckets.delete(key);
52
+ }
53
+
54
+ let bucket = anonymousAccessRequestBuckets.get(recordingId);
55
+ if (!bucket) {
56
+ if (
57
+ anonymousAccessRequestBuckets.size >= ANONYMOUS_ACCESS_REQUEST_MAX_BUCKETS
58
+ ) {
59
+ const oldestKey = anonymousAccessRequestBuckets.keys().next().value;
60
+ if (oldestKey) anonymousAccessRequestBuckets.delete(oldestKey);
61
+ }
62
+ bucket = {
63
+ count: 0,
64
+ resetAt: now + ANONYMOUS_ACCESS_REQUEST_WINDOW_MS,
65
+ };
66
+ anonymousAccessRequestBuckets.set(recordingId, bucket);
67
+ }
68
+
69
+ if (bucket.count >= ANONYMOUS_ACCESS_REQUEST_MAX) return false;
70
+ bucket.count += 1;
71
+ return true;
72
+ }
73
+
32
74
  function accessRequestEventId(
33
75
  recordingId: string,
34
76
  requesterEmail: string,
@@ -64,25 +106,38 @@ function absoluteRecordingUrl(recordingId: string): string {
64
106
  return `${withConfiguredAppBasePath(getAppProductionUrl()).replace(/\/+$/, "")}${recordingSharePath(recordingId)}`;
65
107
  }
66
108
 
109
+ function absoluteAccessApprovalUrl(
110
+ recordingId: string,
111
+ approvalToken: string,
112
+ ): string {
113
+ return `${withConfiguredAppBasePath(getAppProductionUrl()).replace(/\/+$/, "")}${recordingAccessApprovalPath(recordingId, approvalToken)}`;
114
+ }
115
+
67
116
  export function renderRecordingAccessRequestEmail(input: {
68
117
  requesterName: string;
69
118
  requesterEmail: string;
70
119
  recordingTitle: string;
71
120
  url: string;
121
+ allowAccessUrl: string;
72
122
  }) {
73
- const subject = `${input.requesterName} requested access to "${input.recordingTitle}"`;
123
+ const subject = `Access request for "${input.recordingTitle}"`;
74
124
  return {
75
125
  subject,
76
126
  ...renderEmail({
77
127
  brandName: "Clips",
78
128
  preheader: subject,
79
- heading: "Access request",
129
+ heading: "Access requested",
80
130
  paragraphs: [
81
- `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.recordingTitle)}.`,
82
- "Open the Clip and use Share to grant access if this request should be approved.",
131
+ `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested viewer access to ${emailStrong(input.recordingTitle)}.`,
132
+ "Select Allow access to add them to this Clip's standard sharing list. You can also open the Clip to review sharing first.",
83
133
  ],
84
- cta: { label: "Open Clip", url: input.url },
85
- footer: "You received this because you own this Clip.",
134
+ cta: { label: "Allow access", url: input.allowAccessUrl },
135
+ secondaryCta: { label: "Open Clip", url: input.url },
136
+ closingParagraphs: [
137
+ "This approval link expires in 7 days and requires you to be signed in as a Clip owner or admin.",
138
+ ],
139
+ footer:
140
+ "You received this because you own this Clip. If you do not recognize the requester, you can ignore this email.",
86
141
  }),
87
142
  };
88
143
  }
@@ -99,12 +154,23 @@ async function notifyOwner(input: {
99
154
  }
100
155
  if (!(await isEmailConfigured())) return false;
101
156
 
157
+ const approvalToken = signScopedAgentAccessToken({
158
+ resourceKind: CLIPS_ACCESS_APPROVAL_TOKEN_PREFIX,
159
+ resourceId: input.recordingId,
160
+ viewerEmail: input.requesterEmail,
161
+ ttlSeconds: CLIPS_ACCESS_APPROVAL_TOKEN_TTL_SECONDS,
162
+ });
163
+
102
164
  await sendEmail({
103
165
  ...renderRecordingAccessRequestEmail({
104
166
  requesterName: input.requesterName,
105
167
  requesterEmail: input.requesterEmail,
106
168
  recordingTitle: input.recordingTitle,
107
169
  url: absoluteRecordingUrl(input.recordingId),
170
+ allowAccessUrl: absoluteAccessApprovalUrl(
171
+ input.recordingId,
172
+ approvalToken,
173
+ ),
108
174
  }),
109
175
  to: input.ownerEmail,
110
176
  replyTo: input.requesterEmail,
@@ -115,7 +181,7 @@ async function notifyOwner(input: {
115
181
 
116
182
  export default defineAction({
117
183
  description:
118
- "Request access to a private Clips recording. Records the request and notifies the owner in-app and by email when configured.",
184
+ "Request access to a private Clips recording. Signed-in viewers use their account email; anonymous viewers may provide an email address. Records the request and notifies the owner in-app and by email when configured.",
119
185
  schema: z.object({
120
186
  recordingId: z
121
187
  .string()
@@ -127,18 +193,53 @@ export default defineAction({
127
193
  .min(1)
128
194
  .optional()
129
195
  .describe("Short-lived capability from the private share page."),
196
+ requesterEmail: z
197
+ .string()
198
+ .trim()
199
+ .email()
200
+ .optional()
201
+ .describe(
202
+ "Email address to request access for when the viewer is not signed in.",
203
+ ),
130
204
  }),
131
205
  agentTool: false,
132
- run: async ({ recordingId, accessRequestToken }) => {
133
- const requesterEmail = getRequestUserEmail();
134
- if (!requesterEmail) {
135
- throw httpError("Sign in to request access to this clip.", 401);
206
+ run: async ({ recordingId, accessRequestToken, requesterEmail }) => {
207
+ const sessionEmail = getRequestUserEmail();
208
+ const token = accessRequestToken
209
+ ? verifyScopedAgentAccessToken(accessRequestToken, {
210
+ resourceKind: CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
211
+ resourceId: recordingId,
212
+ })
213
+ : { ok: false as const, reason: "missing" };
214
+ if (!token.ok) {
215
+ throw httpError(`Recording ${recordingId} not found`, 404);
216
+ }
217
+
218
+ const normalizedRequesterEmail = sessionEmail
219
+ ? normalizeOwnerEmail(sessionEmail)
220
+ : requesterEmail
221
+ ? normalizeOwnerEmail(requesterEmail)
222
+ : null;
223
+ if (!normalizedRequesterEmail) {
224
+ throw httpError(
225
+ "Sign in or provide an email address to request access to this clip.",
226
+ 401,
227
+ );
228
+ }
229
+
230
+ const tokenViewerEmail = token.viewerEmail
231
+ ? normalizeOwnerEmail(token.viewerEmail)
232
+ : null;
233
+ if (tokenViewerEmail && tokenViewerEmail !== normalizedRequesterEmail) {
234
+ throw httpError(
235
+ "This request is tied to a different email. Sign in with the email that opened the link.",
236
+ 403,
237
+ );
136
238
  }
137
239
 
138
- const normalizedRequesterEmail = normalizeOwnerEmail(requesterEmail);
139
240
  const access = await resolveAccess("recording", recordingId, {
140
241
  userEmail: normalizedRequesterEmail,
141
- orgId: getRequestOrgId() ?? undefined,
242
+ orgId: sessionEmail ? (getRequestOrgId() ?? undefined) : undefined,
142
243
  });
143
244
 
144
245
  if (access) {
@@ -157,16 +258,6 @@ export default defineAction({
157
258
  };
158
259
  }
159
260
 
160
- if (
161
- !accessRequestToken ||
162
- !verifyScopedAgentAccessToken(accessRequestToken, {
163
- resourceKind: CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
164
- resourceId: recordingId,
165
- }).ok
166
- ) {
167
- throw httpError(`Recording ${recordingId} not found`, 404);
168
- }
169
-
170
261
  const db = getDb();
171
262
  const [recording] = await db
172
263
  .select({
@@ -223,6 +314,13 @@ export default defineAction({
223
314
  };
224
315
  }
225
316
 
317
+ if (!sessionEmail && !allowAnonymousAccessRequest(recordingId)) {
318
+ throw httpError(
319
+ "Too many anonymous access requests for this clip. Try again later.",
320
+ 429,
321
+ );
322
+ }
323
+
226
324
  const requesterName =
227
325
  getRequestUserName()?.trim() ||
228
326
  displayNameForEmail(normalizedRequesterEmail);
@@ -425,7 +425,7 @@ async function fetchMeetingDetail(meetingId: string) {
425
425
  source: meeting.source,
426
426
  userNotesMd: meeting.userNotesMd,
427
427
  transcriptStatus: meeting.transcriptStatus,
428
- shareTranscript: Boolean(meeting.shareTranscript),
428
+ shareTranscript: meeting.shareTranscript === true,
429
429
  summaryMd: meeting.summaryMd,
430
430
  bullets: safeJsonArray<{ text: string }>(meeting.bulletsJson),
431
431
  },
@@ -1,11 +1,10 @@
1
1
  import { appPath } from "@agent-native/core/client/api-path";
2
2
  import {
3
- useActionMutation,
4
3
  useActionQuery,
4
+ useActionMutation,
5
5
  } from "@agent-native/core/client/hooks";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
7
  import { ShareAgentsSection } from "@agent-native/toolkit/sharing";
8
- import { IconLink, IconMail } from "@tabler/icons-react";
9
8
  import {
10
9
  useCallback,
11
10
  useEffect,
@@ -34,7 +33,6 @@ import {
34
33
  PopoverTrigger,
35
34
  } from "@/components/ui/popover";
36
35
  import { Switch } from "@/components/ui/switch";
37
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
38
36
 
39
37
  export interface ShareMeetingPopoverProps {
40
38
  meetingId: string;
@@ -90,49 +88,16 @@ function ShareMeetingContent({
90
88
  const canManage = data?.role === "owner" || data?.role === "admin";
91
89
 
92
90
  return (
93
- <>
94
- <Tabs defaultValue="link" className="min-w-0 px-4 py-3">
95
- <TabsList className="grid w-full grid-cols-2">
96
- <TabsTrigger value="link" className="gap-1.5">
97
- <IconLink size={14} />
98
- {t("clipsFinalRaw.link")}
99
- </TabsTrigger>
100
- <TabsTrigger value="invite" className="gap-1.5">
101
- <IconMail size={14} />
102
- {t("clipsFinalRaw.invite")}
103
- </TabsTrigger>
104
- </TabsList>
105
-
106
- <TabsContent value="link" className="mt-3">
107
- <LinkTab
108
- meetingId={meetingId}
109
- shareUrl={shareUrl}
110
- sharesQuery={sharesQuery}
111
- canManage={canManage}
112
- shareTranscript={shareTranscript}
113
- transcriptReady={transcriptReady}
114
- />
115
- </TabsContent>
116
-
117
- <TabsContent value="invite" className="mt-3">
118
- <SharePeopleTab
119
- resourceType="meeting"
120
- resourceId={meetingId}
121
- sharesQuery={sharesQuery}
122
- canManage={canManage}
123
- onError={(err, action) =>
124
- toast.error(
125
- err instanceof Error
126
- ? err.message
127
- : action === "invite"
128
- ? t("clipsFinalRaw.inviteFailed")
129
- : t("clipsFinalRaw.removePersonFailed"),
130
- )
131
- }
132
- />
133
- </TabsContent>
134
- </Tabs>
135
- </>
91
+ <div className="min-w-0 px-4 py-3">
92
+ <LinkTab
93
+ meetingId={meetingId}
94
+ shareUrl={shareUrl}
95
+ sharesQuery={sharesQuery}
96
+ canManage={canManage}
97
+ shareTranscript={shareTranscript}
98
+ transcriptReady={transcriptReady}
99
+ />
100
+ </div>
136
101
  );
137
102
  }
138
103
 
@@ -242,6 +207,16 @@ function LinkTab({
242
207
 
243
208
  return (
244
209
  <div className="space-y-3">
210
+ <CopyField
211
+ label={
212
+ isPublic
213
+ ? t("clipsFinalRaw.shareLink")
214
+ : t("shareDialog.shareWithHumans")
215
+ }
216
+ value={shareUrl}
217
+ disabled={!sharesLoaded}
218
+ />
219
+
245
220
  <GeneralAccessSelect
246
221
  visibility={visibility}
247
222
  canManage={canManage}
@@ -277,14 +252,20 @@ function LinkTab({
277
252
  />
278
253
  </div>
279
254
 
280
- <CopyField
281
- label={
282
- isPublic
283
- ? t("clipsFinalRaw.shareLink")
284
- : t("shareDialog.shareWithHumans")
255
+ <SharePeopleTab
256
+ resourceType="meeting"
257
+ resourceId={meetingId}
258
+ sharesQuery={sharesQuery}
259
+ canManage={canManage}
260
+ onError={(err, action) =>
261
+ toast.error(
262
+ err instanceof Error
263
+ ? err.message
264
+ : action === "invite"
265
+ ? t("clipsFinalRaw.inviteFailed")
266
+ : t("clipsFinalRaw.removePersonFailed"),
267
+ )
285
268
  }
286
- value={shareUrl}
287
- disabled={!sharesLoaded}
288
269
  />
289
270
 
290
271
  {!isPublic && canManage ? (
@@ -0,0 +1,104 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { IconLogin2 } from "@tabler/icons-react";
3
+ import type { FormEvent } from "react";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import { Input } from "@/components/ui/input";
14
+ import { Label } from "@/components/ui/label";
15
+ import { Separator } from "@/components/ui/separator";
16
+
17
+ export interface RequestAccessDialogProps {
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ signInHref: string;
21
+ email: string;
22
+ onEmailChange: (email: string) => void;
23
+ onSubmit: (event: FormEvent<HTMLFormElement>) => void;
24
+ isSubmitting: boolean;
25
+ error?: string | null;
26
+ }
27
+
28
+ export function RequestAccessDialog({
29
+ open,
30
+ onOpenChange,
31
+ signInHref,
32
+ email,
33
+ onEmailChange,
34
+ onSubmit,
35
+ isSubmitting,
36
+ error,
37
+ }: RequestAccessDialogProps) {
38
+ const t = useT();
39
+
40
+ return (
41
+ <Dialog open={open} onOpenChange={onOpenChange}>
42
+ <DialogContent className="sm:max-w-md">
43
+ <DialogHeader>
44
+ <DialogTitle>{t("sharePage.requestAccessDialogTitle")}</DialogTitle>
45
+ <DialogDescription>
46
+ {t("sharePage.requestAccessDialogDescription")}
47
+ </DialogDescription>
48
+ </DialogHeader>
49
+
50
+ <div className="grid gap-4">
51
+ <Button asChild className="w-full gap-2">
52
+ <a href={signInHref}>
53
+ <IconLogin2
54
+ className="size-4 rtl:-scale-x-100"
55
+ aria-hidden="true"
56
+ />
57
+ {t("sharePage.requestAccessSignIn")}
58
+ </a>
59
+ </Button>
60
+
61
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
62
+ <Separator className="flex-1" />
63
+ <span>{t("sharePage.requestAccessOr")}</span>
64
+ <Separator className="flex-1" />
65
+ </div>
66
+
67
+ <form className="grid gap-3" onSubmit={onSubmit}>
68
+ <div className="grid gap-2">
69
+ <Label htmlFor="private-clip-request-email">
70
+ {t("sharePage.requestAccessEmailLabel")}
71
+ </Label>
72
+ <Input
73
+ id="private-clip-request-email"
74
+ type="email"
75
+ inputMode="email"
76
+ autoComplete="email"
77
+ required
78
+ value={email}
79
+ onChange={(event) => onEmailChange(event.target.value)}
80
+ placeholder={t("sharePage.requestAccessEmailPlaceholder")}
81
+ aria-invalid={Boolean(error)}
82
+ />
83
+ <p className="text-xs text-muted-foreground">
84
+ {t("sharePage.requestAccessEmailHint")}
85
+ </p>
86
+ </div>
87
+
88
+ {error ? (
89
+ <p className="text-sm text-destructive" role="alert">
90
+ {error}
91
+ </p>
92
+ ) : null}
93
+
94
+ <Button type="submit" className="w-full" disabled={isSubmitting}>
95
+ {isSubmitting
96
+ ? t("sharePage.requestingAccess")
97
+ : t("sharePage.requestAccessWithEmail")}
98
+ </Button>
99
+ </form>
100
+ </div>
101
+ </DialogContent>
102
+ </Dialog>
103
+ );
104
+ }
@@ -7,12 +7,7 @@ import {
7
7
  } from "@agent-native/core/client/hooks";
8
8
  import { useT } from "@agent-native/core/client/i18n";
9
9
  import { ShareAgentsSection } from "@agent-native/toolkit/sharing";
10
- import {
11
- IconCode,
12
- IconExternalLink,
13
- IconLink,
14
- IconMail,
15
- } from "@tabler/icons-react";
10
+ import { IconExternalLink } from "@tabler/icons-react";
16
11
  import {
17
12
  useCallback,
18
13
  useEffect,
@@ -240,7 +235,7 @@ function ShareRecordingContent({
240
235
  // editor keep it regardless of visibility since they can flip to public
241
236
  // from inside it.
242
237
  const canEmbed = canViewShares || visibility === "public";
243
- const tabCount = 1 + (canViewShares ? 1 : 0) + (canEmbed ? 1 : 0);
238
+ const tabCount = 1 + (canEmbed ? 1 : 0);
244
239
 
245
240
  // Attribution `via` must be a stable non-PII id, never an email. The only
246
241
  // owner id available client-side is the *current* session's userId, which is
@@ -266,21 +261,19 @@ function ShareRecordingContent({
266
261
  >
267
262
  {tabCount > 1 ? (
268
263
  <TabsList
269
- className={`grid w-full ${tabCount === 3 ? "grid-cols-3" : "grid-cols-2"}`}
264
+ className={`grid w-full rounded-xl bg-muted/70 p-1 ${tabCount === 3 ? "grid-cols-3" : "grid-cols-2"}`}
270
265
  >
271
- <TabsTrigger value="link" className="gap-1.5">
272
- <IconLink size={14} />
266
+ <TabsTrigger
267
+ value="link"
268
+ className="h-10 rounded-lg px-3 text-sm font-medium text-muted-foreground transition-colors data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm data-[state=active]:ring-1 data-[state=active]:ring-border"
269
+ >
273
270
  {t("shareDialog.link")}
274
271
  </TabsTrigger>
275
- {canViewShares ? (
276
- <TabsTrigger value="invite" className="gap-1.5">
277
- <IconMail size={14} />
278
- {t("shareDialog.invite")}
279
- </TabsTrigger>
280
- ) : null}
281
272
  {canEmbed ? (
282
- <TabsTrigger value="embed" className="gap-1.5">
283
- <IconCode size={14} />
273
+ <TabsTrigger
274
+ value="embed"
275
+ className="h-10 rounded-lg px-3 text-sm font-medium text-muted-foreground transition-colors data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm data-[state=active]:ring-1 data-[state=active]:ring-border"
276
+ >
284
277
  {t("shareDialog.embed")}
285
278
  </TabsTrigger>
286
279
  ) : null}
@@ -304,24 +297,6 @@ function ShareRecordingContent({
304
297
  />
305
298
  </TabsContent>
306
299
 
307
- {canViewShares ? (
308
- <TabsContent value="invite" className="mt-3">
309
- <SharePeopleTab
310
- resourceType="recording"
311
- resourceId={recordingId}
312
- resourceUrl={absoluteAppUrl(`/r/${recordingId}`)}
313
- sharesQuery={sharesQuery}
314
- canManage={canManage}
315
- roleCopy={{
316
- commenter: {
317
- label: t("shareUi.recordingCommenter.label"),
318
- description: t("shareUi.recordingCommenter.description"),
319
- },
320
- }}
321
- />
322
- </TabsContent>
323
- ) : null}
324
-
325
300
  {canEmbed ? (
326
301
  <TabsContent value="embed" className="mt-3">
327
302
  <ClipsEmbedConfigurator
@@ -523,6 +498,16 @@ function LinkTab({
523
498
 
524
499
  return (
525
500
  <div className="space-y-4">
501
+ <CopyField
502
+ label={
503
+ isPublic
504
+ ? t("shareDialog.shareLink")
505
+ : t("shareDialog.shareWithHumans")
506
+ }
507
+ value={shareUrl}
508
+ disabled={visibilityPending || !sharesLoaded}
509
+ />
510
+
526
511
  {canViewShares ? (
527
512
  visibility ? (
528
513
  <GeneralAccessSelect
@@ -541,15 +526,21 @@ function LinkTab({
541
526
  )
542
527
  ) : null}
543
528
 
544
- <CopyField
545
- label={
546
- isPublic
547
- ? t("shareDialog.shareLink")
548
- : t("shareDialog.shareWithHumans")
549
- }
550
- value={shareUrl}
551
- disabled={visibilityPending || !sharesLoaded}
552
- />
529
+ {canViewShares ? (
530
+ <SharePeopleTab
531
+ resourceType="recording"
532
+ resourceId={recordingId}
533
+ resourceUrl={absoluteAppUrl(`/r/${recordingId}`)}
534
+ sharesQuery={sharesQuery}
535
+ canManage={canManage}
536
+ roleCopy={{
537
+ commenter: {
538
+ label: t("shareUi.recordingCommenter.label"),
539
+ description: t("shareUi.recordingCommenter.description"),
540
+ },
541
+ }}
542
+ />
543
+ ) : null}
553
544
 
554
545
  <ShareAgentsSection
555
546
  label={t("shareDialog.shareWithAgents")}
@@ -81,8 +81,8 @@ export const ROLE_OPTIONS: Array<{ value: Role; label: string }> = [
81
81
  { value: "admin", label: "Admin" },
82
82
  ];
83
83
 
84
- export function copyToClipboard(value: string): void {
85
- void writeClipboardText(value);
84
+ export function copyToClipboard(value: string): Promise<boolean> {
85
+ return writeClipboardText(value);
86
86
  }
87
87
 
88
88
  // ---------------------------------------------------------------------------
@@ -288,10 +288,7 @@ export function CopyField({
288
288
  disabled={disabled}
289
289
  copyLabel={t("shareUi.copy")}
290
290
  copiedLabel={t("recordRoute.linkCopied")}
291
- onCopy={(nextValue) => {
292
- copyToClipboard(nextValue);
293
- return true;
294
- }}
291
+ onCopy={copyToClipboard}
295
292
  />
296
293
  );
297
294
  }
@@ -260,12 +260,37 @@ const messages = {
260
260
  privateClipMessage:
261
261
  "This clip is private. Request access and the owner will be notified.",
262
262
  privateClipSignedOutMessage:
263
- "This clip is private. Sign in to request access from the owner.",
263
+ "This clip is private. Sign in or enter your email to request access.",
264
264
  requestAccess: "Request access",
265
+ requestAccessDialogTitle: "Request access",
266
+ requestAccessDialogDescription:
267
+ "Sign in, or enter the email address the owner should share this clip with.",
268
+ requestAccessSignIn: "Sign in or sign up",
269
+ requestAccessOr: "or",
270
+ requestAccessEmailLabel: "Email address",
271
+ requestAccessEmailPlaceholder: "you@example.com",
272
+ requestAccessEmailHint:
273
+ "After access is granted, sign in with this email to view the clip.",
274
+ requestAccessWithEmail: "Request with email",
275
+ requestAccessEmailRequired: "Enter a valid email address.",
265
276
  requestingAccess: "Requesting access...",
266
277
  accessRequested: "Access requested",
267
278
  accessRequestSent: "The clip owner was notified.",
279
+ accessRequestSentWithEmail:
280
+ "The clip owner was asked to share this clip with {{email}}.",
268
281
  accessRequestFailed: "Couldn't request access. Try again.",
282
+ accessApprovalTitle: "Access granted",
283
+ accessApprovalAlreadyTitle: "Access already granted",
284
+ accessApprovalMessage: "{{email}} can now view this clip.",
285
+ accessApprovalAlreadyMessage: "{{email}} already has access to this clip.",
286
+ accessApprovalErrorTitle: "Couldn't grant access",
287
+ accessApprovalInvalid: "This access request is invalid or expired.",
288
+ accessApprovalSignInTitle: "Sign in to allow access",
289
+ accessApprovalSignInMessage:
290
+ "Sign in as the clip owner or admin to approve this request.",
291
+ accessApprovalOpenClip: "Open clip",
292
+ accessApprovalSignIn: "Sign in",
293
+ accessApprovalLoading: "Granting access...",
269
294
  clipUnavailable: "Clip unavailable",
270
295
  clipUnavailableMessage:
271
296
  "This recording isn't public, or the link is invalid. If it's your clip, sign in to check access.",
@@ -39,7 +39,7 @@ export interface PublicMeeting {
39
39
  actualStart: string | null;
40
40
  actualEnd: string | null;
41
41
  transcriptStatus: string | null;
42
- transcript?: PublicMeetingTranscript | null;
42
+ transcript: PublicMeetingTranscript | null;
43
43
  }
44
44
 
45
45
  export interface PublicMeetingPayload {