@agent-native/core 0.164.4 → 0.164.12

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 (268) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
  3. package/corpus/templates/calendar/AGENTS.md +5 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +1 -1
  5. package/corpus/templates/calendar/actions/delete-events.ts +508 -0
  6. package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
  7. package/corpus/templates/calendar/actions/list-events.ts +53 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
  11. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
  12. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
  13. package/corpus/templates/calendar/app/i18n-data.ts +120 -0
  14. package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
  15. package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
  16. package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
  17. package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
  18. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  19. package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  20. package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  21. package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  22. package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
  23. package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
  26. package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +1 -0
  28. package/corpus/templates/clips/desktop/README.md +19 -4
  29. package/corpus/templates/clips/desktop/src/app.tsx +78 -16
  30. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
  31. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
  32. package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  33. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
  34. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  35. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
  36. package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
  37. package/corpus/templates/factory/AGENTS.md +3 -2
  38. package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
  39. package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
  40. package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
  41. package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
  43. package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
  44. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
  45. package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
  46. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
  47. package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
  48. package/corpus/templates/factory/app/i18n/en-US.ts +47 -0
  49. package/corpus/templates/factory/app/routes/factory.tsx +324 -61
  50. package/corpus/templates/factory/server/connectors/slack.ts +44 -2
  51. package/corpus/templates/factory/server/plugins/agent-chat.ts +12 -8
  52. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
  53. package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
  54. package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
  55. package/corpus/templates/mail/actions/view-screen.ts +1 -1
  56. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
  57. package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
  58. package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
  59. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
  60. package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
  61. package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
  62. package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
  63. package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
  64. package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
  65. package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
  66. package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
  67. package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
  68. package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
  69. package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  70. package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
  71. package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
  72. package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
  73. package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
  74. package/corpus/templates/slides/server/db/schema.ts +8 -0
  75. package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
  76. package/corpus/templates/slides/server/lib/emails.ts +26 -0
  77. package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
  78. package/corpus/templates/slides/server/plugins/db.ts +50 -0
  79. package/corpus/templates/slides/shared/deck-access.ts +31 -0
  80. package/dist/a2a/task-store.d.ts +1 -0
  81. package/dist/a2a/task-store.js +1 -1
  82. package/dist/agent/observational-memory/store.d.ts +1 -0
  83. package/dist/agent/observational-memory/store.js +1 -1
  84. package/dist/agent/run-store.d.ts +1 -0
  85. package/dist/agent/run-store.js +1 -1
  86. package/dist/agent/tool-approval-store.d.ts +1 -0
  87. package/dist/agent/tool-approval-store.js +1 -1
  88. package/dist/application-state/store.d.ts +1 -0
  89. package/dist/application-state/store.js +1 -1
  90. package/dist/browser-sessions/store.d.ts +1 -0
  91. package/dist/browser-sessions/store.js +1 -1
  92. package/dist/checkpoints/store.d.ts +1 -0
  93. package/dist/checkpoints/store.js +1 -1
  94. package/dist/cli/code-agent-schedules.d.ts +11 -0
  95. package/dist/cli/code-agent-schedules.js +44 -5
  96. package/dist/client/AgentPanel.d.ts +5 -1
  97. package/dist/client/AgentPanel.js +2 -2
  98. package/dist/client/AssistantChat.d.ts +5 -1
  99. package/dist/client/AssistantChat.js +40 -6
  100. package/dist/client/EnvironmentBadge.d.ts +7 -13
  101. package/dist/client/EnvironmentBadge.js +43 -92
  102. package/dist/client/agent-chat/index.d.ts +1 -0
  103. package/dist/client/agent-chat/index.js +1 -0
  104. package/dist/client/agent-chat-adapter.js +76 -50
  105. package/dist/client/app-providers.js +4 -4
  106. package/dist/client/chat/message-components.d.ts +4 -0
  107. package/dist/client/chat/message-components.js +28 -1
  108. package/dist/client/chat/run-recovery.js +26 -0
  109. package/dist/client/chat-first/apps-rail.d.ts +1 -0
  110. package/dist/client/chat-first/apps-rail.js +28 -2
  111. package/dist/client/sse-event-processor.js +13 -8
  112. package/dist/client/use-active-agent-chat-run.d.ts +2 -0
  113. package/dist/client/use-active-agent-chat-run.js +22 -0
  114. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
  115. package/dist/coding-tools/sandbox/executions-store.js +1 -1
  116. package/dist/collab/awareness-store.d.ts +1 -0
  117. package/dist/collab/awareness-store.js +1 -1
  118. package/dist/collab/routes.d.ts +1 -1
  119. package/dist/collab/storage.d.ts +1 -0
  120. package/dist/collab/storage.js +1 -1
  121. package/dist/collab/struct-routes.d.ts +1 -1
  122. package/dist/db/client.js +3 -3
  123. package/dist/db/ddl-guard.js +8 -0
  124. package/dist/db/migration-runtime.d.ts +23 -0
  125. package/dist/db/migration-runtime.js +41 -0
  126. package/dist/db/migrations.d.ts +1 -2
  127. package/dist/db/migrations.js +2 -24
  128. package/dist/email-catalog/log.d.ts +1 -0
  129. package/dist/email-catalog/log.js +1 -1
  130. package/dist/guards/index.d.ts +1 -0
  131. package/dist/guards/index.js +1 -0
  132. package/dist/guards/release-schema-complete.d.ts +22 -0
  133. package/dist/guards/release-schema-complete.js +144 -0
  134. package/dist/integrations/a2a-continuations-store.d.ts +1 -0
  135. package/dist/integrations/a2a-continuations-store.js +1 -1
  136. package/dist/integrations/awaiting-input-store.d.ts +1 -0
  137. package/dist/integrations/awaiting-input-store.js +1 -1
  138. package/dist/integrations/config-store.d.ts +1 -0
  139. package/dist/integrations/config-store.js +1 -1
  140. package/dist/integrations/controls-store.d.ts +1 -0
  141. package/dist/integrations/controls-store.js +1 -1
  142. package/dist/integrations/identity-links-store.d.ts +1 -0
  143. package/dist/integrations/identity-links-store.js +1 -1
  144. package/dist/integrations/installations-store.d.ts +1 -0
  145. package/dist/integrations/installations-store.js +1 -1
  146. package/dist/integrations/remote-commands-store.d.ts +1 -0
  147. package/dist/integrations/remote-commands-store.js +1 -1
  148. package/dist/integrations/remote-devices-store.d.ts +1 -0
  149. package/dist/integrations/remote-devices-store.js +1 -1
  150. package/dist/integrations/remote-push-store.d.ts +1 -0
  151. package/dist/integrations/remote-push-store.js +1 -1
  152. package/dist/integrations/remote-run-events-store.d.ts +1 -0
  153. package/dist/integrations/remote-run-events-store.js +1 -1
  154. package/dist/integrations/scope-store.d.ts +1 -0
  155. package/dist/integrations/scope-store.js +1 -1
  156. package/dist/integrations/thread-mapping-store.d.ts +1 -0
  157. package/dist/integrations/thread-mapping-store.js +1 -1
  158. package/dist/integrations/usage-budget-store.d.ts +1 -0
  159. package/dist/integrations/usage-budget-store.js +1 -1
  160. package/dist/jobs/background-automation-runner.js +111 -9
  161. package/dist/jobs/run-history.d.ts +1 -0
  162. package/dist/jobs/run-history.js +1 -1
  163. package/dist/jobs/scheduler-health.d.ts +1 -0
  164. package/dist/jobs/scheduler-health.js +1 -1
  165. package/dist/mcp/approval-store.d.ts +1 -0
  166. package/dist/mcp/approval-store.js +1 -1
  167. package/dist/mcp/connect-store.d.ts +1 -0
  168. package/dist/mcp/connect-store.js +1 -1
  169. package/dist/mcp/oauth-store.d.ts +1 -0
  170. package/dist/mcp/oauth-store.js +1 -1
  171. package/dist/notifications/store.d.ts +1 -0
  172. package/dist/notifications/store.js +1 -1
  173. package/dist/oauth-tokens/store.d.ts +1 -0
  174. package/dist/oauth-tokens/store.js +1 -1
  175. package/dist/observability/routes.d.ts +5 -5
  176. package/dist/progress/routes.d.ts +1 -1
  177. package/dist/progress/store.d.ts +1 -0
  178. package/dist/progress/store.js +1 -1
  179. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  180. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  181. package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
  182. package/dist/provider-api/corpus-jobs-store.js +1 -1
  183. package/dist/provider-api/custom-registry.d.ts +1 -0
  184. package/dist/provider-api/custom-registry.js +1 -1
  185. package/dist/provider-api/quota-governor.d.ts +1 -0
  186. package/dist/provider-api/quota-governor.js +1 -1
  187. package/dist/provider-api/staged-datasets-store.d.ts +1 -0
  188. package/dist/provider-api/staged-datasets-store.js +1 -1
  189. package/dist/resources/handlers.d.ts +3 -3
  190. package/dist/resources/store.d.ts +1 -0
  191. package/dist/resources/store.js +1 -1
  192. package/dist/scripts/dev/index.js +15 -1
  193. package/dist/search/index.js +3 -0
  194. package/dist/secrets/routes.d.ts +9 -9
  195. package/dist/secrets/storage.d.ts +1 -0
  196. package/dist/secrets/storage.js +1 -1
  197. package/dist/server/agent-chat-plugin.js +38 -9
  198. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  199. package/dist/server/agent-teams-run-queue.d.ts +1 -0
  200. package/dist/server/agent-teams-run-queue.js +1 -1
  201. package/dist/server/auth-marketing.js +3 -1
  202. package/dist/server/auth.d.ts +18 -1
  203. package/dist/server/auth.js +370 -154
  204. package/dist/server/embed-session.d.ts +1 -0
  205. package/dist/server/embed-session.js +1 -1
  206. package/dist/server/google-auth-plugin.js +3 -1
  207. package/dist/server/google-oauth.d.ts +10 -0
  208. package/dist/server/google-oauth.js +25 -1
  209. package/dist/server/identity-sso-store.d.ts +1 -0
  210. package/dist/server/identity-sso-store.js +1 -1
  211. package/dist/server/index.d.ts +1 -1
  212. package/dist/server/index.js +1 -1
  213. package/dist/server/onboarding-html.js +200 -31
  214. package/dist/server/poll.d.ts +2 -0
  215. package/dist/server/poll.js +19 -1
  216. package/dist/server/realtime-token.d.ts +1 -1
  217. package/dist/server/release-migrations.js +5 -0
  218. package/dist/server/release-schema.d.ts +46 -0
  219. package/dist/server/release-schema.js +307 -0
  220. package/dist/settings/store.d.ts +1 -0
  221. package/dist/settings/store.js +1 -1
  222. package/dist/shared/environment-lanes.d.ts +28 -0
  223. package/dist/shared/environment-lanes.js +61 -0
  224. package/dist/sharing/actions/share-resource.js +22 -2
  225. package/dist/styles/agent-native.css +17 -15
  226. package/dist/triggers/routes.d.ts +11 -0
  227. package/dist/triggers/routes.js +10 -0
  228. package/dist/usage/alerts-store.d.ts +1 -0
  229. package/dist/usage/alerts-store.js +1 -1
  230. package/dist/usage/store.d.ts +1 -0
  231. package/dist/usage/store.js +1 -1
  232. package/docs/content/actions-access-control.mdx +136 -2
  233. package/docs/content/actions-agent-tools.mdx +101 -0
  234. package/docs/content/actions-defining.mdx +2 -2
  235. package/docs/content/audit-log.mdx +1 -1
  236. package/docs/content/authentication.mdx +1 -1
  237. package/docs/content/cloudflare.mdx +43 -0
  238. package/docs/content/creating-templates.mdx +1 -1
  239. package/docs/content/deploy-an-app.mdx +109 -0
  240. package/docs/content/deployment-environment-variables.mdx +101 -0
  241. package/docs/content/deployment.mdx +125 -792
  242. package/docs/content/docs-components.mdx +2 -2
  243. package/docs/content/environment-variables.mdx +41 -41
  244. package/docs/content/file-uploads.mdx +74 -40
  245. package/docs/content/harness-agents.mdx +2 -2
  246. package/docs/content/internationalization.mdx +61 -65
  247. package/docs/content/key-concepts.mdx +3 -3
  248. package/docs/content/multi-app-workspace.mdx +2 -2
  249. package/docs/content/netlify.mdx +47 -0
  250. package/docs/content/node-docker.mdx +138 -0
  251. package/docs/content/organizations-teams-permissions.mdx +5 -5
  252. package/docs/content/other-platforms.mdx +27 -0
  253. package/docs/content/ssr-caching.mdx +45 -0
  254. package/docs/content/template-assets-developers.mdx +2 -2
  255. package/docs/content/template-chat-developers.mdx +2 -2
  256. package/docs/content/template-content-developers.mdx +2 -2
  257. package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
  258. package/docs/content/template-content-sync.mdx +2 -2
  259. package/docs/content/toolkit-agent-ux.mdx +1 -1
  260. package/docs/content/toolkit-resources.mdx +2 -2
  261. package/docs/content/updating-ui-in-production.mdx +35 -0
  262. package/docs/content/vercel.mdx +38 -0
  263. package/docs/content/what-is-agent-native.mdx +1 -1
  264. package/docs/content/workspace-deployment.mdx +350 -0
  265. package/package.json +2 -2
  266. package/docs/content/agents.mdx +0 -9
  267. package/docs/content/database.mdx +0 -317
  268. package/docs/content/human-approval.mdx +0 -156
@@ -2,17 +2,96 @@ import { createHash, randomUUID } from "node:crypto";
2
2
 
3
3
  import { defineAction } from "@agent-native/core";
4
4
  import { notifyWithDelivery } from "@agent-native/core/notifications";
5
- import { isEmailConfigured } from "@agent-native/core/server";
5
+ import {
6
+ isEmailConfigured,
7
+ sendEmail,
8
+ signScopedAgentAccessToken,
9
+ verifyScopedAgentAccessToken,
10
+ } from "@agent-native/core/server";
6
11
  import {
7
12
  getRequestUserEmail,
8
13
  getRequestUserName,
9
14
  } from "@agent-native/core/server/request-context";
10
15
  import { currentAccess, resolveAccess } from "@agent-native/core/sharing";
11
- import { and, eq } from "drizzle-orm";
16
+ import { and, eq, sql } from "drizzle-orm";
12
17
  import { z } from "zod";
13
18
 
14
19
  import { getDb, schema } from "../server/db/index.js";
15
- import { getDeckUrl } from "./_app-url.js";
20
+ import {
21
+ renderDeckAccessRequestEmail,
22
+ SLIDES_DECK_ACCESS_REQUEST_EMAIL_ID,
23
+ } from "../server/lib/access-request-email.js";
24
+ import {
25
+ SLIDES_ACCESS_REQUEST_FALLBACK_TOKEN_PREFIX,
26
+ SLIDES_ACCESS_REQUEST_TOKEN_PREFIX,
27
+ deckAccessApprovalPath,
28
+ SLIDES_ACCESS_APPROVAL_TOKEN_PREFIX,
29
+ SLIDES_ACCESS_APPROVAL_TOKEN_TTL_SECONDS,
30
+ } from "../shared/deck-access.js";
31
+ import { getDeckUrl, getSlidesAppUrl } from "./_app-url.js";
32
+
33
+ const ANONYMOUS_ACCESS_REQUEST_WINDOW_MS = 10 * 60 * 1000;
34
+ const ANONYMOUS_ACCESS_REQUEST_MAX = 5;
35
+
36
+ async function claimAnonymousAccessRequestSlot(
37
+ db: ReturnType<typeof getDb>,
38
+ deckId: string,
39
+ ): Promise<{ windowStartedAt: string }> {
40
+ const now = new Date();
41
+ const windowStartedAt = now.toISOString();
42
+ const resetBefore = new Date(
43
+ now.getTime() - ANONYMOUS_ACCESS_REQUEST_WINDOW_MS,
44
+ ).toISOString();
45
+ const limits = schema.deckAccessRequestLimits;
46
+ const [bucket] = await db
47
+ .insert(limits)
48
+ .values({
49
+ deckId,
50
+ windowStartedAt,
51
+ requestCount: 1,
52
+ })
53
+ .onConflictDoUpdate({
54
+ target: limits.deckId,
55
+ set: {
56
+ windowStartedAt: sql`CASE WHEN ${limits.windowStartedAt} <= ${resetBefore} THEN ${windowStartedAt} ELSE ${limits.windowStartedAt} END`,
57
+ requestCount: sql`CASE WHEN ${limits.windowStartedAt} <= ${resetBefore} THEN 1 ELSE ${limits.requestCount} + 1 END`,
58
+ },
59
+ where: sql`${limits.windowStartedAt} <= ${resetBefore} OR ${limits.requestCount} < ${ANONYMOUS_ACCESS_REQUEST_MAX}`,
60
+ })
61
+ .returning({
62
+ requestCount: limits.requestCount,
63
+ windowStartedAt: limits.windowStartedAt,
64
+ });
65
+
66
+ if (!bucket) {
67
+ throw httpError(
68
+ "Too many anonymous access requests for this deck. Try again later.",
69
+ 429,
70
+ );
71
+ }
72
+ return { windowStartedAt: bucket.windowStartedAt };
73
+ }
74
+
75
+ async function refundAnonymousAccessRequestSlot(
76
+ db: ReturnType<typeof getDb>,
77
+ deckId: string,
78
+ windowStartedAt: string,
79
+ ): Promise<void> {
80
+ const limits = schema.deckAccessRequestLimits;
81
+ await db
82
+ .update(limits)
83
+ .set({
84
+ requestCount: sql`${limits.requestCount} - 1`,
85
+ })
86
+ .where(
87
+ and(
88
+ eq(limits.deckId, deckId),
89
+ eq(limits.windowStartedAt, windowStartedAt),
90
+ sql`${limits.requestCount} > 0`,
91
+ ),
92
+ )
93
+ .returning({ deckId: limits.deckId });
94
+ }
16
95
 
17
96
  function httpError(message: string, statusCode: number): Error {
18
97
  return Object.assign(new Error(message), { statusCode });
@@ -35,8 +114,11 @@ function normalizeEmail(email: string): string {
35
114
  return email.trim().toLowerCase();
36
115
  }
37
116
 
38
- function cleanSubjectPart(value: string): string {
39
- return value.replace(/[\r\n]+/g, " ").trim();
117
+ function absoluteDeckAccessApprovalUrl(
118
+ deckId: string,
119
+ approvalToken: string,
120
+ ): string {
121
+ return `${getSlidesAppUrl().replace(/\/+$/, "")}${deckAccessApprovalPath(deckId, approvalToken)}`;
40
122
  }
41
123
 
42
124
  function accessRequestEventId(deckId: string, requesterEmail: string): string {
@@ -55,6 +137,9 @@ type AccessRequestPayload = {
55
137
  requesterEmail?: string;
56
138
  requesterName?: string;
57
139
  requestedAt?: string;
140
+ approvalTokenHash?: string;
141
+ accessGrantedAt?: string;
142
+ accessShareId?: string;
58
143
  notifiedOwner?: boolean;
59
144
  notifiedAt?: string;
60
145
  inAppNotified?: boolean;
@@ -68,6 +153,7 @@ type AccessRequestNotificationState = {
68
153
  emailNotified: boolean;
69
154
  emailRequired: boolean;
70
155
  ownerCanNotify: boolean;
156
+ approvalTokenHash?: string;
71
157
  };
72
158
 
73
159
  const NOTIFICATION_CLAIM_TTL_MS = 5 * 60 * 1000;
@@ -104,6 +190,7 @@ function notificationStateFor(
104
190
  emailNotified: payload.emailNotified ?? legacyNotified,
105
191
  emailRequired: ownerCanNotify && emailConfigured,
106
192
  ownerCanNotify,
193
+ approvalTokenHash: payload.approvalTokenHash,
107
194
  };
108
195
  }
109
196
 
@@ -145,63 +232,148 @@ async function notifyAccessRequestOwner(input: {
145
232
  const ownerEmail = input.ownerEmail;
146
233
  if (!ownerEmail || !input.state.ownerCanNotify) return input.state;
147
234
 
148
- const emailConfigured = await isEmailConfigured();
149
- const state = {
150
- ...input.state,
151
- emailRequired: input.state.ownerCanNotify && emailConfigured,
152
- };
153
- const channels: string[] = [];
154
- if (!state.inAppNotified) channels.push("inbox");
155
- if (state.emailRequired && !state.emailNotified) channels.push("email");
156
- if (channels.length === 0) return state;
235
+ let state = input.state;
236
+
237
+ if (!state.inAppNotified) {
238
+ try {
239
+ const delivery = await notifyWithDelivery(
240
+ {
241
+ severity: "info",
242
+ title: "Deck access requested",
243
+ body: `${input.requesterName} requested access to “${input.deckTitle}”.`,
244
+ channels: ["inbox"],
245
+ metadata: {
246
+ deckId: input.deckId,
247
+ requesterEmail: input.requesterEmail,
248
+ link: getDeckUrl(input.deckId),
249
+ },
250
+ },
251
+ { owner: ownerEmail },
252
+ );
253
+ state = {
254
+ ...state,
255
+ inAppNotified:
256
+ state.inAppNotified || delivery.deliveredChannels.includes("inbox"),
257
+ };
258
+ } catch (error) {
259
+ console.warn(
260
+ "[deck-access] in-app access request notification failed:",
261
+ error,
262
+ );
263
+ }
264
+ }
157
265
 
158
- try {
159
- const delivery = await notifyWithDelivery(
160
- {
161
- severity: "info",
162
- title: "Deck access requested",
163
- body: `${input.requesterName} requested access to “${input.deckTitle}”.`,
164
- channels,
165
- metadata: {
166
- deckId: input.deckId,
266
+ if (state.emailRequired && !state.emailNotified) {
267
+ try {
268
+ const approvalToken = signScopedAgentAccessToken({
269
+ resourceKind: SLIDES_ACCESS_APPROVAL_TOKEN_PREFIX,
270
+ resourceId: input.deckId,
271
+ viewerEmail: input.requesterEmail,
272
+ ttlSeconds: SLIDES_ACCESS_APPROVAL_TOKEN_TTL_SECONDS,
273
+ });
274
+ await sendEmail({
275
+ ...renderDeckAccessRequestEmail({
276
+ requesterName: input.requesterName,
167
277
  requesterEmail: input.requesterEmail,
168
- link: getDeckUrl(input.deckId),
169
- ...(channels.includes("email")
170
- ? {
171
- emailRecipients: [ownerEmail],
172
- emailSubject: `${cleanSubjectPart(input.requesterName)} requested access to "${cleanSubjectPart(input.deckTitle)}"`,
173
- }
174
- : {}),
175
- },
176
- },
177
- { owner: ownerEmail },
178
- );
179
- return {
180
- ...state,
181
- inAppNotified:
182
- state.inAppNotified || delivery.deliveredChannels.includes("inbox"),
183
- emailNotified:
184
- state.emailNotified || delivery.deliveredChannels.includes("email"),
185
- };
186
- } catch (error) {
187
- console.warn("[deck-access] access request notification failed:", error);
188
- return state;
278
+ deckTitle: input.deckTitle,
279
+ url: getDeckUrl(input.deckId),
280
+ allowAccessUrl: absoluteDeckAccessApprovalUrl(
281
+ input.deckId,
282
+ approvalToken,
283
+ ),
284
+ }),
285
+ to: ownerEmail,
286
+ replyTo: input.requesterEmail,
287
+ templateId: SLIDES_DECK_ACCESS_REQUEST_EMAIL_ID,
288
+ });
289
+ state = {
290
+ ...state,
291
+ emailNotified: true,
292
+ approvalTokenHash: createHash("sha256")
293
+ .update(approvalToken)
294
+ .digest("hex"),
295
+ };
296
+ } catch (error) {
297
+ console.warn("[deck-access] access request email failed:", error);
298
+ }
189
299
  }
300
+
301
+ return state;
190
302
  }
191
303
 
192
304
  export default defineAction({
193
305
  description:
194
- "Request access to a private Agent-Native Slides deck. Records an access-request event and notifies the owner when email is configured.",
306
+ "Request access to a private Agent-Native Slides deck. Signed-in viewers use their account email; anonymous viewers may provide an email address. Records an access-request event and notifies the owner in-app and by email when configured.",
195
307
  schema: z.object({
196
308
  deckId: z.string().min(1).describe("Deck ID to request access to."),
309
+ accessRequestToken: z
310
+ .string()
311
+ .trim()
312
+ .min(1)
313
+ .optional()
314
+ .describe("Short-lived capability from the private deck page."),
315
+ requesterEmail: z
316
+ .string()
317
+ .trim()
318
+ .email()
319
+ .optional()
320
+ .describe(
321
+ "Email address to request access for when the viewer is not signed in.",
322
+ ),
197
323
  }),
324
+ requiresAuth: false,
198
325
  agentTool: false,
199
- run: async ({ deckId }) => {
200
- const rawRequesterEmail = getRequestUserEmail();
201
- if (!rawRequesterEmail) {
202
- throw httpError("Sign in to request access to this deck.", 401);
326
+ run: async ({
327
+ deckId,
328
+ accessRequestToken,
329
+ requesterEmail: requesterEmailInput,
330
+ }) => {
331
+ const sessionEmail = getRequestUserEmail();
332
+ let requestToken = accessRequestToken
333
+ ? verifyScopedAgentAccessToken(accessRequestToken, {
334
+ resourceKind: SLIDES_ACCESS_REQUEST_TOKEN_PREFIX,
335
+ resourceId: deckId,
336
+ })
337
+ : { ok: false as const, reason: "missing" };
338
+ let accessProbeWasUnavailable = false;
339
+ if (accessRequestToken && !requestToken.ok) {
340
+ const fallbackToken = verifyScopedAgentAccessToken(accessRequestToken, {
341
+ resourceKind: SLIDES_ACCESS_REQUEST_FALLBACK_TOKEN_PREFIX,
342
+ resourceId: deckId,
343
+ });
344
+ if (fallbackToken.ok) {
345
+ requestToken = fallbackToken;
346
+ accessProbeWasUnavailable = true;
347
+ }
348
+ }
349
+ if (accessRequestToken && !requestToken.ok) {
350
+ throw httpError(`Deck ${deckId} not found`, 404);
351
+ }
352
+ if (!sessionEmail && !requestToken.ok) {
353
+ throw httpError(`Deck ${deckId} not found`, 404);
354
+ }
355
+ const normalizedRequesterEmail = sessionEmail
356
+ ? normalizeEmail(sessionEmail)
357
+ : requesterEmailInput
358
+ ? normalizeEmail(requesterEmailInput)
359
+ : null;
360
+ if (!normalizedRequesterEmail) {
361
+ throw httpError(
362
+ "Sign in or provide an email address to request access to this deck.",
363
+ 401,
364
+ );
365
+ }
366
+ const tokenViewerEmail =
367
+ requestToken.ok && requestToken.viewerEmail
368
+ ? normalizeEmail(requestToken.viewerEmail)
369
+ : null;
370
+ if (tokenViewerEmail && tokenViewerEmail !== normalizedRequesterEmail) {
371
+ throw httpError(
372
+ "This request is tied to a different email. Sign in with the email that opened the link.",
373
+ 403,
374
+ );
203
375
  }
204
- const requesterEmail = normalizeEmail(rawRequesterEmail);
376
+ const requesterEmail = normalizedRequesterEmail;
205
377
 
206
378
  const db = getDb();
207
379
  const claimNotification = async (
@@ -313,6 +485,8 @@ export default defineAction({
313
485
  inAppNotified: state.inAppNotified,
314
486
  emailNotified: state.emailNotified,
315
487
  notifiedOwner: delivered,
488
+ approvalTokenHash:
489
+ state.approvalTokenHash ?? claimedPayload.approvalTokenHash,
316
490
  notificationClaimedAt: undefined,
317
491
  notificationClaimToken: undefined,
318
492
  ...(delivered ? { notifiedAt: new Date().toISOString() } : {}),
@@ -355,7 +529,9 @@ export default defineAction({
355
529
  throw httpError(`Deck ${deckId} not found`, 404);
356
530
  }
357
531
 
358
- const access = await resolveAccess("deck", deckId, currentAccess());
532
+ const access = accessProbeWasUnavailable
533
+ ? null
534
+ : await resolveAccess("deck", deckId, currentAccess());
359
535
  if (access) {
360
536
  return {
361
537
  ok: true as const,
@@ -491,6 +667,10 @@ export default defineAction({
491
667
  };
492
668
  }
493
669
 
670
+ const anonymousSlot = sessionEmail
671
+ ? null
672
+ : await claimAnonymousAccessRequestSlot(db, deckId);
673
+
494
674
  const requestId = accessRequestEventId(deckId, requesterEmail);
495
675
  const requestedAt = new Date().toISOString();
496
676
  const initialPayload: AccessRequestPayload = {
@@ -506,21 +686,40 @@ export default defineAction({
506
686
  };
507
687
  const initialPayloadJson = JSON.stringify(initialPayload);
508
688
 
509
- const [insertedRequest] = await db
510
- .insert(schema.deckEvents)
511
- .values({
512
- id: requestId,
513
- deckId,
514
- type: "deck.access_requested",
515
- message: `${requesterEmail} requested access to this deck.`,
516
- payload: initialPayloadJson,
517
- createdBy: "human",
518
- createdAt: requestedAt,
519
- })
520
- .onConflictDoNothing()
521
- .returning({ id: schema.deckEvents.id });
689
+ let insertedRequest: { id: string } | undefined;
690
+ try {
691
+ [insertedRequest] = await db
692
+ .insert(schema.deckEvents)
693
+ .values({
694
+ id: requestId,
695
+ deckId,
696
+ type: "deck.access_requested",
697
+ message: `${requesterEmail} requested access to this deck.`,
698
+ payload: initialPayloadJson,
699
+ createdBy: "human",
700
+ createdAt: requestedAt,
701
+ })
702
+ .onConflictDoNothing()
703
+ .returning({ id: schema.deckEvents.id });
704
+ } catch (error) {
705
+ if (anonymousSlot) {
706
+ await refundAnonymousAccessRequestSlot(
707
+ db,
708
+ deckId,
709
+ anonymousSlot.windowStartedAt,
710
+ );
711
+ }
712
+ throw error;
713
+ }
522
714
 
523
715
  if (!insertedRequest) {
716
+ if (anonymousSlot) {
717
+ await refundAnonymousAccessRequestSlot(
718
+ db,
719
+ deckId,
720
+ anonymousSlot.windowStartedAt,
721
+ );
722
+ }
524
723
  return {
525
724
  ok: true as const,
526
725
  alreadyHasAccess: false,
@@ -0,0 +1,196 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import {
3
+ IconArrowLeft,
4
+ IconAt,
5
+ IconLock,
6
+ IconLoader2,
7
+ IconLogin2,
8
+ IconRefresh,
9
+ IconUserPlus,
10
+ IconUsersGroup,
11
+ } from "@tabler/icons-react";
12
+ import type { FormEvent } from "react";
13
+
14
+ import { Button } from "@/components/ui/button";
15
+ import type { DeckAccessStatusResponse } from "@/hooks/use-deck-access";
16
+
17
+ import { RequestDeckAccessDialog } from "./RequestDeckAccessDialog";
18
+
19
+ export function MissingDeckAccessPane({
20
+ accessStatus,
21
+ accessStatusError,
22
+ accessStatusLoading,
23
+ hasTeamJoinOption,
24
+ orgLoading,
25
+ orgError,
26
+ requestAccessPending,
27
+ accessRequestSent,
28
+ accessRequestNotified,
29
+ requestAccessDialogOpen,
30
+ requesterEmail,
31
+ requestAccessDialogError,
32
+ signedIn,
33
+ signInHref,
34
+ viewerEmail,
35
+ refreshing,
36
+ onRequestAccess,
37
+ onRequestAccessDialogOpenChange,
38
+ onRequesterEmailChange,
39
+ onSubmitGuestAccessRequest,
40
+ onSignIn,
41
+ onRetry,
42
+ onBack,
43
+ }: {
44
+ accessStatus: DeckAccessStatusResponse | null;
45
+ accessStatusError: boolean;
46
+ accessStatusLoading: boolean;
47
+ hasTeamJoinOption: boolean;
48
+ orgLoading: boolean;
49
+ orgError: boolean;
50
+ requestAccessPending: boolean;
51
+ accessRequestSent: boolean;
52
+ accessRequestNotified: boolean;
53
+ requestAccessDialogOpen: boolean;
54
+ requesterEmail: string;
55
+ requestAccessDialogError: string | null;
56
+ signedIn: boolean;
57
+ signInHref: string;
58
+ viewerEmail: string | null;
59
+ refreshing: boolean;
60
+ onRequestAccess: () => void;
61
+ onRequestAccessDialogOpenChange: (open: boolean) => void;
62
+ onRequesterEmailChange: (email: string) => void;
63
+ onSubmitGuestAccessRequest: (event: FormEvent<HTMLFormElement>) => void;
64
+ onSignIn: () => void;
65
+ onRetry: () => void;
66
+ onBack: () => void;
67
+ }) {
68
+ const t = useT();
69
+ const privateDeck = Boolean(
70
+ accessStatus?.exists &&
71
+ !accessStatus.hasAccess &&
72
+ accessStatus.visibility === "private",
73
+ );
74
+ const accessCheckFailed = accessStatusError || orgError;
75
+ const noAccess = privateDeck || accessCheckFailed;
76
+ const checkingAccess = !noAccess && (accessStatusLoading || orgLoading);
77
+ const Icon =
78
+ noAccess || (!hasTeamJoinOption && !checkingAccess)
79
+ ? IconLock
80
+ : IconUsersGroup;
81
+ const title = checkingAccess
82
+ ? t("deckEditor.lookingForDeck")
83
+ : noAccess
84
+ ? t("deckEditor.privateDeckTitle")
85
+ : hasTeamJoinOption
86
+ ? t("deckEditor.joinTeamToOpen")
87
+ : t("deckEditor.deckUnavailable");
88
+ const description = checkingAccess
89
+ ? t("deckEditor.checkingSharedAccess")
90
+ : noAccess
91
+ ? t("deckEditor.privateDeckDescription")
92
+ : hasTeamJoinOption
93
+ ? t("deckEditor.joinTeamDescription")
94
+ : t("deckEditor.deckUnavailableDescription");
95
+
96
+ return (
97
+ <div className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
98
+ <div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-sm">
99
+ <div className="mb-4 flex items-center gap-3">
100
+ <div className="flex size-10 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
101
+ <Icon className="size-5" />
102
+ </div>
103
+ <div className="min-w-0">
104
+ <h1 className="text-base font-semibold text-foreground">{title}</h1>
105
+ </div>
106
+ </div>
107
+ <p className="text-sm leading-6 text-muted-foreground">{description}</p>
108
+ {noAccess && viewerEmail ? (
109
+ <div className="mt-4 flex items-center gap-2 rounded-md border border-border bg-muted/35 px-3 py-2 text-sm">
110
+ <IconAt className="size-4 shrink-0 text-muted-foreground" />
111
+ <span className="min-w-0 truncate text-muted-foreground">
112
+ {t("deckEditor.signedInAs")}{" "}
113
+ <span className="font-medium text-foreground">{viewerEmail}</span>
114
+ </span>
115
+ </div>
116
+ ) : null}
117
+ {noAccess && accessRequestSent ? (
118
+ <div className="mt-3 rounded-md border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300">
119
+ {t(
120
+ accessRequestNotified
121
+ ? "deckEditor.accessRequestSentDescription"
122
+ : "deckEditor.accessRequestRecordedDescription",
123
+ )}
124
+ </div>
125
+ ) : null}
126
+ <div className="mt-5 flex flex-col gap-2">
127
+ {noAccess && !signedIn ? (
128
+ <>
129
+ <Button type="button" onClick={onSignIn}>
130
+ <IconLogin2 className="size-4" />
131
+ {t("deckEditor.signInToRequestAccess")}
132
+ </Button>
133
+ <Button
134
+ type="button"
135
+ variant="outline"
136
+ onClick={() => onRequestAccessDialogOpenChange(true)}
137
+ disabled={requestAccessPending || accessRequestSent}
138
+ >
139
+ <IconUserPlus className="size-4" />
140
+ {accessRequestSent
141
+ ? t("deckEditor.accessRequestSent")
142
+ : t("deckEditor.requestAccess")}
143
+ </Button>
144
+ </>
145
+ ) : noAccess ? (
146
+ <Button
147
+ type="button"
148
+ onClick={onRequestAccess}
149
+ disabled={requestAccessPending || accessRequestSent}
150
+ >
151
+ {requestAccessPending ? (
152
+ <IconLoader2 className="size-4 animate-spin" />
153
+ ) : (
154
+ <IconUserPlus className="size-4" />
155
+ )}
156
+ {requestAccessPending
157
+ ? t("deckEditor.requestAccessPending")
158
+ : accessRequestSent
159
+ ? t("deckEditor.accessRequestSent")
160
+ : t("deckEditor.requestAccess")}
161
+ </Button>
162
+ ) : null}
163
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
164
+ <Button type="button" variant="outline" onClick={onBack}>
165
+ <IconArrowLeft className="size-4" />
166
+ {t("deckEditor.backToDecks")}
167
+ </Button>
168
+ <Button
169
+ type="button"
170
+ variant={noAccess ? "ghost" : "default"}
171
+ onClick={onRetry}
172
+ disabled={refreshing || checkingAccess}
173
+ >
174
+ <IconRefresh
175
+ className={refreshing ? "size-4 animate-spin" : "size-4"}
176
+ />
177
+ {t("deckEditor.tryAgain")}
178
+ </Button>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ {noAccess && !signedIn ? (
183
+ <RequestDeckAccessDialog
184
+ open={requestAccessDialogOpen}
185
+ onOpenChange={onRequestAccessDialogOpenChange}
186
+ signInHref={signInHref}
187
+ email={requesterEmail}
188
+ onEmailChange={onRequesterEmailChange}
189
+ onSubmit={onSubmitGuestAccessRequest}
190
+ isSubmitting={requestAccessPending}
191
+ error={requestAccessDialogError}
192
+ />
193
+ ) : null}
194
+ </div>
195
+ );
196
+ }