@agent-native/core 0.96.0 → 0.97.0

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 (450) hide show
  1. package/README.md +6 -0
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +39 -0
  4. package/corpus/core/docs/content/audit-log.mdx +2 -1
  5. package/corpus/core/docs/content/automations.mdx +7 -7
  6. package/corpus/core/docs/content/creating-templates.mdx +1 -1
  7. package/corpus/core/docs/content/internationalization.mdx +2 -1
  8. package/corpus/core/docs/content/locales/ar-SA/audit-log.mdx +2 -1
  9. package/corpus/core/docs/content/locales/ar-SA/automations.mdx +7 -7
  10. package/corpus/core/docs/content/locales/ar-SA/getting-started.mdx +7 -1
  11. package/corpus/core/docs/content/locales/ar-SA/internationalization.mdx +1 -1
  12. package/corpus/core/docs/content/locales/ar-SA/package-lifecycle.mdx +19 -0
  13. package/corpus/core/docs/content/locales/ar-SA/sandbox-adapters.mdx +18 -7
  14. package/corpus/core/docs/content/locales/ar-SA/security.mdx +8 -0
  15. package/corpus/core/docs/content/locales/de-DE/audit-log.mdx +2 -1
  16. package/corpus/core/docs/content/locales/de-DE/automations.mdx +7 -7
  17. package/corpus/core/docs/content/locales/de-DE/getting-started.mdx +7 -1
  18. package/corpus/core/docs/content/locales/de-DE/internationalization.mdx +1 -1
  19. package/corpus/core/docs/content/locales/de-DE/package-lifecycle.mdx +22 -0
  20. package/corpus/core/docs/content/locales/de-DE/sandbox-adapters.mdx +18 -7
  21. package/corpus/core/docs/content/locales/de-DE/security.mdx +8 -0
  22. package/corpus/core/docs/content/locales/es-ES/audit-log.mdx +2 -1
  23. package/corpus/core/docs/content/locales/es-ES/automations.mdx +7 -7
  24. package/corpus/core/docs/content/locales/es-ES/getting-started.mdx +7 -1
  25. package/corpus/core/docs/content/locales/es-ES/internationalization.mdx +1 -1
  26. package/corpus/core/docs/content/locales/es-ES/package-lifecycle.mdx +22 -0
  27. package/corpus/core/docs/content/locales/es-ES/sandbox-adapters.mdx +18 -7
  28. package/corpus/core/docs/content/locales/es-ES/security.mdx +8 -0
  29. package/corpus/core/docs/content/locales/fr-FR/audit-log.mdx +2 -1
  30. package/corpus/core/docs/content/locales/fr-FR/automations.mdx +7 -7
  31. package/corpus/core/docs/content/locales/fr-FR/getting-started.mdx +7 -1
  32. package/corpus/core/docs/content/locales/fr-FR/internationalization.mdx +1 -1
  33. package/corpus/core/docs/content/locales/fr-FR/package-lifecycle.mdx +21 -0
  34. package/corpus/core/docs/content/locales/fr-FR/sandbox-adapters.mdx +19 -8
  35. package/corpus/core/docs/content/locales/fr-FR/security.mdx +8 -0
  36. package/corpus/core/docs/content/locales/hi-IN/audit-log.mdx +2 -1
  37. package/corpus/core/docs/content/locales/hi-IN/automations.mdx +7 -7
  38. package/corpus/core/docs/content/locales/hi-IN/getting-started.mdx +7 -1
  39. package/corpus/core/docs/content/locales/hi-IN/internationalization.mdx +1 -1
  40. package/corpus/core/docs/content/locales/hi-IN/package-lifecycle.mdx +20 -0
  41. package/corpus/core/docs/content/locales/hi-IN/sandbox-adapters.mdx +18 -7
  42. package/corpus/core/docs/content/locales/hi-IN/security.mdx +8 -0
  43. package/corpus/core/docs/content/locales/ja-JP/audit-log.mdx +2 -1
  44. package/corpus/core/docs/content/locales/ja-JP/automations.mdx +7 -7
  45. package/corpus/core/docs/content/locales/ja-JP/getting-started.mdx +7 -1
  46. package/corpus/core/docs/content/locales/ja-JP/internationalization.mdx +1 -1
  47. package/corpus/core/docs/content/locales/ja-JP/package-lifecycle.mdx +21 -0
  48. package/corpus/core/docs/content/locales/ja-JP/sandbox-adapters.mdx +17 -6
  49. package/corpus/core/docs/content/locales/ja-JP/security.mdx +8 -0
  50. package/corpus/core/docs/content/locales/ko-KR/audit-log.mdx +2 -1
  51. package/corpus/core/docs/content/locales/ko-KR/automations.mdx +7 -7
  52. package/corpus/core/docs/content/locales/ko-KR/getting-started.mdx +7 -1
  53. package/corpus/core/docs/content/locales/ko-KR/internationalization.mdx +1 -1
  54. package/corpus/core/docs/content/locales/ko-KR/package-lifecycle.mdx +20 -0
  55. package/corpus/core/docs/content/locales/ko-KR/sandbox-adapters.mdx +17 -6
  56. package/corpus/core/docs/content/locales/ko-KR/security.mdx +8 -0
  57. package/corpus/core/docs/content/locales/pt-BR/audit-log.mdx +2 -1
  58. package/corpus/core/docs/content/locales/pt-BR/automations.mdx +7 -7
  59. package/corpus/core/docs/content/locales/pt-BR/getting-started.mdx +7 -1
  60. package/corpus/core/docs/content/locales/pt-BR/internationalization.mdx +1 -1
  61. package/corpus/core/docs/content/locales/pt-BR/package-lifecycle.mdx +21 -0
  62. package/corpus/core/docs/content/locales/pt-BR/sandbox-adapters.mdx +18 -7
  63. package/corpus/core/docs/content/locales/pt-BR/security.mdx +8 -0
  64. package/corpus/core/docs/content/locales/zh-CN/audit-log.mdx +2 -1
  65. package/corpus/core/docs/content/locales/zh-CN/automations.mdx +7 -7
  66. package/corpus/core/docs/content/locales/zh-CN/getting-started.mdx +6 -4
  67. package/corpus/core/docs/content/locales/zh-CN/internationalization.mdx +1 -1
  68. package/corpus/core/docs/content/locales/zh-CN/package-lifecycle.mdx +19 -0
  69. package/corpus/core/docs/content/locales/zh-CN/sandbox-adapters.mdx +17 -6
  70. package/corpus/core/docs/content/locales/zh-CN/security.mdx +8 -0
  71. package/corpus/core/docs/content/locales/zh-TW/audit-log.mdx +2 -1
  72. package/corpus/core/docs/content/locales/zh-TW/automations.mdx +7 -7
  73. package/corpus/core/docs/content/locales/zh-TW/getting-started.mdx +6 -4
  74. package/corpus/core/docs/content/locales/zh-TW/internationalization.mdx +1 -1
  75. package/corpus/core/docs/content/locales/zh-TW/package-lifecycle.mdx +19 -0
  76. package/corpus/core/docs/content/locales/zh-TW/sandbox-adapters.mdx +18 -7
  77. package/corpus/core/docs/content/locales/zh-TW/security.mdx +8 -0
  78. package/corpus/core/docs/content/package-lifecycle.mdx +51 -0
  79. package/corpus/core/docs/content/sandbox-adapters.mdx +16 -5
  80. package/corpus/core/package.json +9 -2
  81. package/corpus/core/src/a2a/auth-policy.ts +33 -0
  82. package/corpus/core/src/agent/durable-background.ts +15 -7
  83. package/corpus/core/src/agent/run-store.ts +13 -4
  84. package/corpus/core/src/audit/actions/export-audit-events.ts +162 -0
  85. package/corpus/core/src/audit/store.ts +8 -3
  86. package/corpus/core/src/audit/types.ts +3 -0
  87. package/corpus/core/src/cli/doctor.ts +396 -0
  88. package/corpus/core/src/cli/index.ts +56 -0
  89. package/corpus/core/src/cli/info.ts +3 -3
  90. package/corpus/core/src/cli/package-lifecycle.ts +821 -0
  91. package/corpus/core/src/client/agent-chat-adapter.ts +9 -9
  92. package/corpus/core/src/guards/db-tool-scoping.ts +129 -0
  93. package/corpus/core/src/guards/index.ts +23 -0
  94. package/corpus/core/src/guards/no-drizzle-push.ts +116 -0
  95. package/corpus/core/src/guards/no-env-credentials.ts +229 -0
  96. package/corpus/core/src/guards/no-env-mutation.ts +114 -0
  97. package/corpus/core/src/guards/no-localhost-fallback.ts +124 -0
  98. package/corpus/core/src/guards/no-unscoped-credentials.ts +227 -0
  99. package/corpus/core/src/guards/no-unscoped-queries.ts +834 -0
  100. package/corpus/core/src/guards/scan-utils.ts +105 -0
  101. package/corpus/core/src/guards/types.ts +30 -0
  102. package/corpus/core/src/org/accept-pending.ts +9 -1
  103. package/corpus/core/src/org/migrations.ts +41 -0
  104. package/corpus/core/src/package-lifecycle/index.ts +1 -0
  105. package/corpus/core/src/package-lifecycle/manifest.ts +120 -0
  106. package/corpus/core/src/provider-api/custom-registry.ts +56 -0
  107. package/corpus/core/src/provider-api/index.ts +2 -0
  108. package/corpus/core/src/secrets/storage.ts +47 -27
  109. package/corpus/core/src/server/action-discovery.ts +4 -0
  110. package/corpus/core/src/server/agent-chat-plugin.ts +54 -12
  111. package/corpus/core/src/server/core-routes-plugin.ts +30 -14
  112. package/corpus/core/src/server/csrf.ts +24 -5
  113. package/corpus/core/src/server/framework-request-handler.ts +34 -0
  114. package/corpus/core/src/server/google-realtime-session.ts +1 -36
  115. package/corpus/core/src/server/poll.ts +24 -2
  116. package/corpus/core/src/server/realtime-voice.ts +9 -0
  117. package/corpus/core/src/server/request-origin.ts +63 -0
  118. package/corpus/core/src/server/transcribe-voice.ts +1 -50
  119. package/corpus/core/src/templates/default/package.json +2 -2
  120. package/corpus/core/src/templates/headless/package.json +0 -1
  121. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +3 -0
  122. package/corpus/core/src/templates/workspace-core/.agents/skills/automations/SKILL.md +1 -1
  123. package/corpus/core/src/templates/workspace-core/package.json +0 -1
  124. package/corpus/core/src/templates/workspace-root/package.json +0 -1
  125. package/corpus/core/src/triggers/actions.ts +11 -3
  126. package/corpus/core/src/triggers/types.ts +7 -1
  127. package/corpus/templates/analytics/actions/compose-dashboard.ts +65 -37
  128. package/corpus/templates/analytics/actions/mutate-dashboard.ts +73 -24
  129. package/corpus/templates/analytics/actions/reorder-dashboard-panels.ts +16 -13
  130. package/corpus/templates/analytics/actions/update-dashboard.ts +53 -42
  131. package/corpus/templates/analytics/changelog/2026-07-10-dashboard-edits-from-the-agent-and-the-ui-at-the-same-time-n.md +6 -0
  132. package/corpus/templates/analytics/changelog/2026-07-10-timed-out-bigquery-jobs-are-now-cancelled-so-they-stop-consu.md +6 -0
  133. package/corpus/templates/analytics/package.json +1 -2
  134. package/corpus/templates/analytics/server/lib/bigquery.ts +3 -2
  135. package/corpus/templates/analytics/server/lib/dashboards-store.ts +154 -11
  136. package/corpus/templates/assets/package.json +1 -2
  137. package/corpus/templates/brain/package.json +1 -2
  138. package/corpus/templates/calendar/package.json +1 -2
  139. package/corpus/templates/chat/package.json +1 -2
  140. package/corpus/templates/clips/changelog/2026-07-10-atomic-view-tracking.md +6 -0
  141. package/corpus/templates/clips/chrome-extension/package.json +3 -2
  142. package/corpus/templates/clips/desktop/package.json +6 -4
  143. package/corpus/templates/clips/desktop/src/lib/recorder.ts +26 -13
  144. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +4 -1
  145. package/corpus/templates/clips/package.json +1 -2
  146. package/corpus/templates/clips/server/db/schema.ts +29 -17
  147. package/corpus/templates/clips/server/plugins/db.ts +8 -0
  148. package/corpus/templates/clips/server/routes/api/view-event.post.ts +260 -105
  149. package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +16 -0
  150. package/corpus/templates/content/AGENTS.md +72 -62
  151. package/corpus/templates/content/actions/_content-database-source-adapters.ts +84 -0
  152. package/corpus/templates/content/actions/_database-source-utils.ts +206 -96
  153. package/corpus/templates/content/actions/_notion-database-source-adapter.ts +216 -0
  154. package/corpus/templates/content/actions/_position-utils.ts +76 -0
  155. package/corpus/templates/content/actions/_property-utils.ts +31 -20
  156. package/corpus/templates/content/actions/add-database-item.ts +52 -39
  157. package/corpus/templates/content/actions/attach-content-database-source.ts +95 -11
  158. package/corpus/templates/content/actions/change-content-database-source-role.ts +24 -1
  159. package/corpus/templates/content/actions/configure-document-property.ts +36 -27
  160. package/corpus/templates/content/actions/create-content-database.ts +40 -30
  161. package/corpus/templates/content/actions/create-document.ts +39 -32
  162. package/corpus/templates/content/actions/duplicate-document-property.ts +37 -28
  163. package/corpus/templates/content/actions/list-notion-database-sources.ts +80 -0
  164. package/corpus/templates/content/actions/move-document.ts +64 -48
  165. package/corpus/templates/content/actions/refresh-content-database-source.ts +42 -3
  166. package/corpus/templates/content/actions/share-local-file-document.ts +36 -27
  167. package/corpus/templates/content/actions/suggest-source-join-key.ts +39 -16
  168. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +89 -23
  169. package/corpus/templates/content/app/components/editor/database/settings.tsx +1 -1
  170. package/corpus/templates/content/app/hooks/use-content-database.ts +18 -1
  171. package/corpus/templates/content/app/i18n/zh-TW.ts +4 -0
  172. package/corpus/templates/content/app/i18n-data.ts +40 -0
  173. package/corpus/templates/content/changelog/2026-07-10-failed-row-creation-and-source-attach-now-show-an-error-inst.md +6 -0
  174. package/corpus/templates/content/changelog/2026-07-10-items-added-at-the-same-time-no-longer-end-up-with-identical.md +6 -0
  175. package/corpus/templates/content/changelog/2026-07-10-notion-databases-can-add-read-only-details-to-content-tables.md +6 -0
  176. package/corpus/templates/content/package.json +1 -2
  177. package/corpus/templates/content/parity/matrix.md +25 -25
  178. package/corpus/templates/content/parity/matrix.ts +1 -0
  179. package/corpus/templates/content/server/routes/api/documents/[id]/move.patch.ts +43 -26
  180. package/corpus/templates/content/shared/api.ts +16 -1
  181. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +8 -4
  182. package/corpus/templates/design/AGENTS.md +12 -2
  183. package/corpus/templates/design/DEVELOPING.md +10 -6
  184. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -6
  185. package/corpus/templates/design/actions/duplicate-design.ts +31 -25
  186. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +5 -1
  187. package/corpus/templates/design/changelog/2026-07-10-duplicating-a-design-can-no-longer-produce-a-copy-with-missi.md +6 -0
  188. package/corpus/templates/design/changelog/2026-07-10-local-visual-edits-can-now-save-safe-leaf-text-class-and.md +6 -0
  189. package/corpus/templates/design/package.json +1 -1
  190. package/corpus/templates/design/shared/local-jsx-visual-edit.ts +388 -0
  191. package/corpus/templates/dispatch/package.json +1 -2
  192. package/corpus/templates/forms/actions/patch-form-fields.ts +82 -44
  193. package/corpus/templates/forms/changelog/2026-07-10-simultaneous-edits-to-different-form-fields-no-longer-overwr.md +6 -0
  194. package/corpus/templates/forms/package.json +1 -2
  195. package/corpus/templates/macros/package.json +1 -2
  196. package/corpus/templates/mail/app/components/email/EmailThread.tsx +3 -1
  197. package/corpus/templates/mail/changelog/2026-07-10-fixed-calendar-rsvp-buttons-in-emails-firing-duplicate-respo.md +6 -0
  198. package/corpus/templates/mail/package.json +1 -2
  199. package/corpus/templates/plan/actions/restore-plan-version.ts +126 -96
  200. package/corpus/templates/plan/changelog/2026-07-10-restoring-a-plan-version-can-no-longer-leave-a-plan-half-res.md +6 -0
  201. package/corpus/templates/plan/package.json +1 -2
  202. package/corpus/templates/plan/server/plans.ts +9 -2
  203. package/corpus/templates/slides/actions/list-decks.ts +28 -0
  204. package/corpus/templates/slides/app/components/deck/MermaidRenderer.tsx +41 -26
  205. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +4 -8
  206. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +2 -4
  207. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -8
  208. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +5 -12
  209. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +3 -6
  210. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +3 -6
  211. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +2 -5
  212. package/corpus/templates/slides/app/context/DeckContext.tsx +47 -4
  213. package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -30
  214. package/corpus/templates/slides/app/pages/Index.tsx +2 -3
  215. package/corpus/templates/slides/app/root.tsx +0 -5
  216. package/corpus/templates/slides/changelog/2026-07-10-deck-lists-load-much-faster-and-no-longer-re-download-slide-.md +6 -0
  217. package/corpus/templates/slides/package.json +1 -2
  218. package/dist/a2a/auth-policy.d.ts +19 -0
  219. package/dist/a2a/auth-policy.d.ts.map +1 -1
  220. package/dist/a2a/auth-policy.js +32 -0
  221. package/dist/a2a/auth-policy.js.map +1 -1
  222. package/dist/agent/durable-background.d.ts +8 -5
  223. package/dist/agent/durable-background.d.ts.map +1 -1
  224. package/dist/agent/durable-background.js +15 -8
  225. package/dist/agent/durable-background.js.map +1 -1
  226. package/dist/agent/run-store.d.ts.map +1 -1
  227. package/dist/agent/run-store.js +9 -4
  228. package/dist/agent/run-store.js.map +1 -1
  229. package/dist/audit/actions/export-audit-events.d.ts +27 -0
  230. package/dist/audit/actions/export-audit-events.d.ts.map +1 -0
  231. package/dist/audit/actions/export-audit-events.js +144 -0
  232. package/dist/audit/actions/export-audit-events.js.map +1 -0
  233. package/dist/audit/store.d.ts +1 -0
  234. package/dist/audit/store.d.ts.map +1 -1
  235. package/dist/audit/store.js +8 -3
  236. package/dist/audit/store.js.map +1 -1
  237. package/dist/audit/types.d.ts +3 -0
  238. package/dist/audit/types.d.ts.map +1 -1
  239. package/dist/audit/types.js.map +1 -1
  240. package/dist/cli/doctor.d.ts +83 -0
  241. package/dist/cli/doctor.d.ts.map +1 -0
  242. package/dist/cli/doctor.js +277 -0
  243. package/dist/cli/doctor.js.map +1 -0
  244. package/dist/cli/index.js +50 -0
  245. package/dist/cli/index.js.map +1 -1
  246. package/dist/cli/info.js +3 -3
  247. package/dist/cli/info.js.map +1 -1
  248. package/dist/cli/package-lifecycle.d.ts +50 -0
  249. package/dist/cli/package-lifecycle.d.ts.map +1 -0
  250. package/dist/cli/package-lifecycle.js +560 -0
  251. package/dist/cli/package-lifecycle.js.map +1 -0
  252. package/dist/client/agent-chat-adapter.js +7 -3
  253. package/dist/client/agent-chat-adapter.js.map +1 -1
  254. package/dist/collab/struct-routes.d.ts +1 -1
  255. package/dist/guards/db-tool-scoping.d.ts +38 -0
  256. package/dist/guards/db-tool-scoping.d.ts.map +1 -0
  257. package/dist/guards/db-tool-scoping.js +114 -0
  258. package/dist/guards/db-tool-scoping.js.map +1 -0
  259. package/dist/guards/index.d.ts +23 -0
  260. package/dist/guards/index.d.ts.map +1 -0
  261. package/dist/guards/index.js +19 -0
  262. package/dist/guards/index.js.map +1 -0
  263. package/dist/guards/no-drizzle-push.d.ts +14 -0
  264. package/dist/guards/no-drizzle-push.d.ts.map +1 -0
  265. package/dist/guards/no-drizzle-push.js +97 -0
  266. package/dist/guards/no-drizzle-push.js.map +1 -0
  267. package/dist/guards/no-env-credentials.d.ts +36 -0
  268. package/dist/guards/no-env-credentials.d.ts.map +1 -0
  269. package/dist/guards/no-env-credentials.js +204 -0
  270. package/dist/guards/no-env-credentials.js.map +1 -0
  271. package/dist/guards/no-env-mutation.d.ts +23 -0
  272. package/dist/guards/no-env-mutation.d.ts.map +1 -0
  273. package/dist/guards/no-env-mutation.js +90 -0
  274. package/dist/guards/no-env-mutation.js.map +1 -0
  275. package/dist/guards/no-localhost-fallback.d.ts +30 -0
  276. package/dist/guards/no-localhost-fallback.d.ts.map +1 -0
  277. package/dist/guards/no-localhost-fallback.js +105 -0
  278. package/dist/guards/no-localhost-fallback.js.map +1 -0
  279. package/dist/guards/no-unscoped-credentials.d.ts +28 -0
  280. package/dist/guards/no-unscoped-credentials.d.ts.map +1 -0
  281. package/dist/guards/no-unscoped-credentials.js +199 -0
  282. package/dist/guards/no-unscoped-credentials.js.map +1 -0
  283. package/dist/guards/no-unscoped-queries.d.ts +32 -0
  284. package/dist/guards/no-unscoped-queries.d.ts.map +1 -0
  285. package/dist/guards/no-unscoped-queries.js +720 -0
  286. package/dist/guards/no-unscoped-queries.js.map +1 -0
  287. package/dist/guards/scan-utils.d.ts +28 -0
  288. package/dist/guards/scan-utils.d.ts.map +1 -0
  289. package/dist/guards/scan-utils.js +92 -0
  290. package/dist/guards/scan-utils.js.map +1 -0
  291. package/dist/guards/types.d.ts +28 -0
  292. package/dist/guards/types.d.ts.map +1 -0
  293. package/dist/guards/types.js +11 -0
  294. package/dist/guards/types.js.map +1 -0
  295. package/dist/org/accept-pending.d.ts.map +1 -1
  296. package/dist/org/accept-pending.js +9 -1
  297. package/dist/org/accept-pending.js.map +1 -1
  298. package/dist/org/migrations.d.ts +7 -2
  299. package/dist/org/migrations.d.ts.map +1 -1
  300. package/dist/org/migrations.js +41 -0
  301. package/dist/org/migrations.js.map +1 -1
  302. package/dist/package-lifecycle/index.d.ts +2 -0
  303. package/dist/package-lifecycle/index.d.ts.map +1 -0
  304. package/dist/package-lifecycle/index.js +2 -0
  305. package/dist/package-lifecycle/index.js.map +1 -0
  306. package/dist/package-lifecycle/manifest.d.ts +24 -0
  307. package/dist/package-lifecycle/manifest.d.ts.map +1 -0
  308. package/dist/package-lifecycle/manifest.js +81 -0
  309. package/dist/package-lifecycle/manifest.js.map +1 -0
  310. package/dist/progress/routes.d.ts +1 -1
  311. package/dist/provider-api/custom-registry.d.ts +32 -1
  312. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  313. package/dist/provider-api/custom-registry.js +41 -1
  314. package/dist/provider-api/custom-registry.js.map +1 -1
  315. package/dist/provider-api/index.d.ts +1 -1
  316. package/dist/provider-api/index.d.ts.map +1 -1
  317. package/dist/provider-api/index.js +1 -1
  318. package/dist/provider-api/index.js.map +1 -1
  319. package/dist/secrets/storage.d.ts.map +1 -1
  320. package/dist/secrets/storage.js +45 -27
  321. package/dist/secrets/storage.js.map +1 -1
  322. package/dist/server/action-discovery.d.ts.map +1 -1
  323. package/dist/server/action-discovery.js +4 -0
  324. package/dist/server/action-discovery.js.map +1 -1
  325. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  326. package/dist/server/agent-chat-plugin.js +47 -13
  327. package/dist/server/agent-chat-plugin.js.map +1 -1
  328. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  329. package/dist/server/core-routes-plugin.js +23 -12
  330. package/dist/server/core-routes-plugin.js.map +1 -1
  331. package/dist/server/csrf.d.ts.map +1 -1
  332. package/dist/server/csrf.js +18 -5
  333. package/dist/server/csrf.js.map +1 -1
  334. package/dist/server/framework-request-handler.d.ts.map +1 -1
  335. package/dist/server/framework-request-handler.js +34 -0
  336. package/dist/server/framework-request-handler.js.map +1 -1
  337. package/dist/server/google-realtime-session.d.ts.map +1 -1
  338. package/dist/server/google-realtime-session.js +2 -34
  339. package/dist/server/google-realtime-session.js.map +1 -1
  340. package/dist/server/poll.d.ts.map +1 -1
  341. package/dist/server/poll.js +24 -2
  342. package/dist/server/poll.js.map +1 -1
  343. package/dist/server/realtime-voice.d.ts.map +1 -1
  344. package/dist/server/realtime-voice.js +9 -0
  345. package/dist/server/realtime-voice.js.map +1 -1
  346. package/dist/server/request-origin.d.ts +10 -0
  347. package/dist/server/request-origin.d.ts.map +1 -0
  348. package/dist/server/request-origin.js +60 -0
  349. package/dist/server/request-origin.js.map +1 -0
  350. package/dist/server/transcribe-voice.d.ts.map +1 -1
  351. package/dist/server/transcribe-voice.js +2 -48
  352. package/dist/server/transcribe-voice.js.map +1 -1
  353. package/dist/templates/default/package.json +2 -2
  354. package/dist/templates/headless/package.json +0 -1
  355. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +3 -0
  356. package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +1 -1
  357. package/dist/templates/workspace-core/package.json +0 -1
  358. package/dist/templates/workspace-root/package.json +0 -1
  359. package/dist/triggers/actions.d.ts.map +1 -1
  360. package/dist/triggers/actions.js +8 -3
  361. package/dist/triggers/actions.js.map +1 -1
  362. package/dist/triggers/types.d.ts +7 -1
  363. package/dist/triggers/types.d.ts.map +1 -1
  364. package/dist/triggers/types.js.map +1 -1
  365. package/docs/content/audit-log.mdx +2 -1
  366. package/docs/content/automations.mdx +7 -7
  367. package/docs/content/creating-templates.mdx +1 -1
  368. package/docs/content/internationalization.mdx +2 -1
  369. package/docs/content/locales/ar-SA/audit-log.mdx +2 -1
  370. package/docs/content/locales/ar-SA/automations.mdx +7 -7
  371. package/docs/content/locales/ar-SA/getting-started.mdx +7 -1
  372. package/docs/content/locales/ar-SA/internationalization.mdx +1 -1
  373. package/docs/content/locales/ar-SA/package-lifecycle.mdx +19 -0
  374. package/docs/content/locales/ar-SA/sandbox-adapters.mdx +18 -7
  375. package/docs/content/locales/ar-SA/security.mdx +8 -0
  376. package/docs/content/locales/de-DE/audit-log.mdx +2 -1
  377. package/docs/content/locales/de-DE/automations.mdx +7 -7
  378. package/docs/content/locales/de-DE/getting-started.mdx +7 -1
  379. package/docs/content/locales/de-DE/internationalization.mdx +1 -1
  380. package/docs/content/locales/de-DE/package-lifecycle.mdx +22 -0
  381. package/docs/content/locales/de-DE/sandbox-adapters.mdx +18 -7
  382. package/docs/content/locales/de-DE/security.mdx +8 -0
  383. package/docs/content/locales/es-ES/audit-log.mdx +2 -1
  384. package/docs/content/locales/es-ES/automations.mdx +7 -7
  385. package/docs/content/locales/es-ES/getting-started.mdx +7 -1
  386. package/docs/content/locales/es-ES/internationalization.mdx +1 -1
  387. package/docs/content/locales/es-ES/package-lifecycle.mdx +22 -0
  388. package/docs/content/locales/es-ES/sandbox-adapters.mdx +18 -7
  389. package/docs/content/locales/es-ES/security.mdx +8 -0
  390. package/docs/content/locales/fr-FR/audit-log.mdx +2 -1
  391. package/docs/content/locales/fr-FR/automations.mdx +7 -7
  392. package/docs/content/locales/fr-FR/getting-started.mdx +7 -1
  393. package/docs/content/locales/fr-FR/internationalization.mdx +1 -1
  394. package/docs/content/locales/fr-FR/package-lifecycle.mdx +21 -0
  395. package/docs/content/locales/fr-FR/sandbox-adapters.mdx +19 -8
  396. package/docs/content/locales/fr-FR/security.mdx +8 -0
  397. package/docs/content/locales/hi-IN/audit-log.mdx +2 -1
  398. package/docs/content/locales/hi-IN/automations.mdx +7 -7
  399. package/docs/content/locales/hi-IN/getting-started.mdx +7 -1
  400. package/docs/content/locales/hi-IN/internationalization.mdx +1 -1
  401. package/docs/content/locales/hi-IN/package-lifecycle.mdx +20 -0
  402. package/docs/content/locales/hi-IN/sandbox-adapters.mdx +18 -7
  403. package/docs/content/locales/hi-IN/security.mdx +8 -0
  404. package/docs/content/locales/ja-JP/audit-log.mdx +2 -1
  405. package/docs/content/locales/ja-JP/automations.mdx +7 -7
  406. package/docs/content/locales/ja-JP/getting-started.mdx +7 -1
  407. package/docs/content/locales/ja-JP/internationalization.mdx +1 -1
  408. package/docs/content/locales/ja-JP/package-lifecycle.mdx +21 -0
  409. package/docs/content/locales/ja-JP/sandbox-adapters.mdx +17 -6
  410. package/docs/content/locales/ja-JP/security.mdx +8 -0
  411. package/docs/content/locales/ko-KR/audit-log.mdx +2 -1
  412. package/docs/content/locales/ko-KR/automations.mdx +7 -7
  413. package/docs/content/locales/ko-KR/getting-started.mdx +7 -1
  414. package/docs/content/locales/ko-KR/internationalization.mdx +1 -1
  415. package/docs/content/locales/ko-KR/package-lifecycle.mdx +20 -0
  416. package/docs/content/locales/ko-KR/sandbox-adapters.mdx +17 -6
  417. package/docs/content/locales/ko-KR/security.mdx +8 -0
  418. package/docs/content/locales/pt-BR/audit-log.mdx +2 -1
  419. package/docs/content/locales/pt-BR/automations.mdx +7 -7
  420. package/docs/content/locales/pt-BR/getting-started.mdx +7 -1
  421. package/docs/content/locales/pt-BR/internationalization.mdx +1 -1
  422. package/docs/content/locales/pt-BR/package-lifecycle.mdx +21 -0
  423. package/docs/content/locales/pt-BR/sandbox-adapters.mdx +18 -7
  424. package/docs/content/locales/pt-BR/security.mdx +8 -0
  425. package/docs/content/locales/zh-CN/audit-log.mdx +2 -1
  426. package/docs/content/locales/zh-CN/automations.mdx +7 -7
  427. package/docs/content/locales/zh-CN/getting-started.mdx +6 -4
  428. package/docs/content/locales/zh-CN/internationalization.mdx +1 -1
  429. package/docs/content/locales/zh-CN/package-lifecycle.mdx +19 -0
  430. package/docs/content/locales/zh-CN/sandbox-adapters.mdx +17 -6
  431. package/docs/content/locales/zh-CN/security.mdx +8 -0
  432. package/docs/content/locales/zh-TW/audit-log.mdx +2 -1
  433. package/docs/content/locales/zh-TW/automations.mdx +7 -7
  434. package/docs/content/locales/zh-TW/getting-started.mdx +6 -4
  435. package/docs/content/locales/zh-TW/internationalization.mdx +1 -1
  436. package/docs/content/locales/zh-TW/package-lifecycle.mdx +19 -0
  437. package/docs/content/locales/zh-TW/sandbox-adapters.mdx +18 -7
  438. package/docs/content/locales/zh-TW/security.mdx +8 -0
  439. package/docs/content/package-lifecycle.mdx +51 -0
  440. package/docs/content/sandbox-adapters.mdx +16 -5
  441. package/package.json +10 -3
  442. package/src/templates/default/package.json +2 -2
  443. package/src/templates/headless/package.json +0 -1
  444. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +3 -0
  445. package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +1 -1
  446. package/src/templates/workspace-core/package.json +0 -1
  447. package/src/templates/workspace-root/package.json +0 -1
  448. package/corpus/templates/slides/app/components/ui/toast.tsx +0 -1
  449. package/corpus/templates/slides/app/components/ui/toaster.tsx +0 -1
  450. package/corpus/templates/slides/app/hooks/use-toast.ts +0 -1
@@ -11,7 +11,7 @@
11
11
  * | "cta-click" | "reaction",
12
12
  * timestampMs?: number,
13
13
  * payload?: object,
14
- * viewerEmail?: string, // server falls back to session when present
14
+ * viewerEmail?: string, // ignored; authenticated session is authoritative
15
15
  * viewerName?: string,
16
16
  * sessionId: string, // anonymous-viewer key (persisted in browser)
17
17
  * viewSessionId?: string, // per-player-open key for counted visits
@@ -27,10 +27,14 @@
27
27
 
28
28
  import { writeAppState } from "@agent-native/core/application-state";
29
29
  import { emit } from "@agent-native/core/event-bus";
30
- import { getSession, runWithRequestContext } from "@agent-native/core/server";
30
+ import {
31
+ getSession,
32
+ readBodyWithSizeLimit,
33
+ runWithRequestContext,
34
+ } from "@agent-native/core/server";
31
35
  import { resolveAccess } from "@agent-native/core/sharing";
32
- import { and, eq } from "drizzle-orm";
33
- import { defineEventHandler, readBody, setResponseStatus } from "h3";
36
+ import { and, asc, eq, isNull, sql } from "drizzle-orm";
37
+ import { defineEventHandler, getRequestIP, setResponseStatus } from "h3";
34
38
 
35
39
  import { getDb, schema } from "../../db/index.js";
36
40
  import { nanoid, shouldCountView } from "../../lib/recordings.js";
@@ -69,12 +73,29 @@ const ALLOWED_KINDS = new Set([
69
73
  // Simple in-memory rate limiter — per IP per 10s window.
70
74
  const RATE_LIMIT_WINDOW_MS = 10_000;
71
75
  const RATE_LIMIT_MAX = 60;
76
+ const RATE_LIMIT_MAX_BUCKETS = 5000;
77
+ const MAX_BODY_BYTES = 16 * 1024;
78
+ const MAX_ID_CHARS = 256;
79
+ const MAX_VIEWER_NAME_CHARS = 200;
80
+ const MAX_EVENT_TIME_MS = 7 * 24 * 60 * 60 * 1000;
81
+ const MAX_PAYLOAD_BYTES = 8 * 1024;
72
82
  const rateBuckets = new Map<string, { count: number; reset: number }>();
73
83
 
84
+ function pruneExpiredRateBuckets(now: number): void {
85
+ for (const [key, bucket] of rateBuckets) {
86
+ if (bucket.reset < now) rateBuckets.delete(key);
87
+ }
88
+ }
89
+
74
90
  function rateLimit(key: string): boolean {
75
91
  const now = Date.now();
76
92
  const existing = rateBuckets.get(key);
77
93
  if (!existing || existing.reset < now) {
94
+ if (existing) rateBuckets.delete(key);
95
+ if (rateBuckets.size >= RATE_LIMIT_MAX_BUCKETS) {
96
+ pruneExpiredRateBuckets(now);
97
+ if (rateBuckets.size >= RATE_LIMIT_MAX_BUCKETS) return false;
98
+ }
78
99
  rateBuckets.set(key, { count: 1, reset: now + RATE_LIMIT_WINDOW_MS });
79
100
  return true;
80
101
  }
@@ -83,47 +104,126 @@ function rateLimit(key: string): boolean {
83
104
  return true;
84
105
  }
85
106
 
107
+ export function __resetViewEventRateLimitForTests(): void {
108
+ rateBuckets.clear();
109
+ }
110
+
111
+ function boundedString(value: unknown, maxChars: number): string | null {
112
+ if (typeof value !== "string") return null;
113
+ const trimmed = value.trim();
114
+ return trimmed && trimmed.length <= maxChars ? trimmed : null;
115
+ }
116
+
117
+ function boundedNumber(
118
+ value: unknown,
119
+ defaultValue: number,
120
+ max: number,
121
+ ): number | null {
122
+ const resolved = value === undefined ? defaultValue : value;
123
+ return typeof resolved === "number" &&
124
+ Number.isFinite(resolved) &&
125
+ resolved >= 0 &&
126
+ resolved <= max
127
+ ? resolved
128
+ : null;
129
+ }
130
+
131
+ function serializedPayload(value: unknown): string | null {
132
+ if (
133
+ value === null ||
134
+ typeof value !== "object" ||
135
+ Array.isArray(value) ||
136
+ (Object.getPrototypeOf(value) !== Object.prototype &&
137
+ Object.getPrototypeOf(value) !== null)
138
+ ) {
139
+ return null;
140
+ }
141
+ try {
142
+ const serialized = JSON.stringify(value);
143
+ return new TextEncoder().encode(serialized).byteLength <= MAX_PAYLOAD_BYTES
144
+ ? serialized
145
+ : null;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
86
151
  export default defineEventHandler(async (event) => {
87
- const body = (await readBody(event).catch(
88
- () => null,
89
- )) as ViewEventBody | null;
152
+ let body: ViewEventBody | null;
153
+ try {
154
+ body = await readBodyWithSizeLimit<ViewEventBody>(event, MAX_BODY_BYTES);
155
+ } catch (error) {
156
+ const statusCode = (error as { statusCode?: number })?.statusCode;
157
+ setResponseStatus(event, statusCode === 413 ? 413 : 400);
158
+ return {
159
+ error:
160
+ statusCode === 413 ? "Request body too large" : "Invalid request body",
161
+ };
162
+ }
90
163
  if (!body || typeof body !== "object") {
91
164
  setResponseStatus(event, 400);
92
165
  return { error: "Invalid body" };
93
166
  }
94
167
 
95
- const {
96
- recordingId,
97
- kind,
98
- timestampMs = 0,
99
- payload = {},
100
- sessionId,
101
- viewSessionId,
102
- totalWatchMs = 0,
103
- completedPct = 0,
104
- scrubbedToEnd = false,
105
- } = body;
168
+ const recordingId = boundedString(body.recordingId, MAX_ID_CHARS);
169
+ const sessionId = boundedString(body.sessionId, MAX_ID_CHARS);
170
+ const viewSessionId =
171
+ body.viewSessionId === undefined
172
+ ? null
173
+ : boundedString(body.viewSessionId, MAX_ID_CHARS);
174
+ const viewerName =
175
+ body.viewerName === undefined
176
+ ? null
177
+ : boundedString(body.viewerName, MAX_VIEWER_NAME_CHARS);
178
+ const timestampMs = boundedNumber(body.timestampMs, 0, MAX_EVENT_TIME_MS);
179
+ const totalWatchMs = boundedNumber(body.totalWatchMs, 0, MAX_EVENT_TIME_MS);
180
+ const completedPct = boundedNumber(body.completedPct, 0, 100);
181
+ const payload = serializedPayload(body.payload ?? {});
106
182
 
107
- if (!recordingId || typeof recordingId !== "string") {
183
+ if (!recordingId) {
108
184
  setResponseStatus(event, 400);
109
- return { error: "recordingId is required" };
185
+ return {
186
+ error: "recordingId is required and must be at most 256 characters",
187
+ };
110
188
  }
111
- if (!kind || !ALLOWED_KINDS.has(kind)) {
189
+ if (!body.kind || !ALLOWED_KINDS.has(body.kind)) {
112
190
  setResponseStatus(event, 400);
113
- return { error: `Invalid kind: ${kind}` };
191
+ return { error: `Invalid kind: ${body.kind}` };
114
192
  }
115
- if (!sessionId || typeof sessionId !== "string") {
193
+ if (!sessionId) {
116
194
  setResponseStatus(event, 400);
117
- return { error: "sessionId is required" };
195
+ return {
196
+ error: "sessionId is required and must be at most 256 characters",
197
+ };
198
+ }
199
+ if (body.viewSessionId !== undefined && !viewSessionId) {
200
+ setResponseStatus(event, 400);
201
+ return { error: "viewSessionId must be at most 256 characters" };
202
+ }
203
+ if (body.viewerName !== undefined && !viewerName) {
204
+ setResponseStatus(event, 400);
205
+ return { error: "viewerName must be at most 200 characters" };
206
+ }
207
+ if (timestampMs === null || totalWatchMs === null || completedPct === null) {
208
+ setResponseStatus(event, 400);
209
+ return { error: "Invalid view metrics" };
210
+ }
211
+ if (
212
+ body.scrubbedToEnd !== undefined &&
213
+ typeof body.scrubbedToEnd !== "boolean"
214
+ ) {
215
+ setResponseStatus(event, 400);
216
+ return { error: "scrubbedToEnd must be a boolean" };
217
+ }
218
+ if (payload === null) {
219
+ setResponseStatus(event, 400);
220
+ return { error: "payload must be a plain object no larger than 8 KiB" };
118
221
  }
119
222
 
120
223
  // Rate limit by IP + sessionId.
121
- const ip =
122
- (event.node?.req?.headers["x-forwarded-for"] as string | undefined)
123
- ?.split(",")[0]
124
- ?.trim() ||
125
- event.node?.req?.socket?.remoteAddress ||
126
- "unknown";
224
+ // Deliberately do not opt into x-forwarded-for parsing: only the hosting
225
+ // adapter's resolved peer address is trusted for this process-local guard.
226
+ const ip = getRequestIP(event) || "unknown";
127
227
  if (!rateLimit(`${ip}:${sessionId}`)) {
128
228
  setResponseStatus(event, 429);
129
229
  return { error: "Rate limit exceeded" };
@@ -132,8 +232,10 @@ export default defineEventHandler(async (event) => {
132
232
  const session = await getSession(event).catch(() => null);
133
233
  const sessionEmail = session?.email;
134
234
  const viewerEmail = sessionEmail ?? null;
135
- const viewerName = body.viewerName ?? sessionEmail?.split("@")[0] ?? null;
235
+ const resolvedViewerName = viewerName ?? sessionEmail?.split("@")[0] ?? null;
136
236
  const now = new Date().toISOString();
237
+ const kind = body.kind;
238
+ const scrubbedToEnd = body.scrubbedToEnd ?? false;
137
239
 
138
240
  return runWithRequestContext(
139
241
  { userEmail: sessionEmail, orgId: session?.orgId },
@@ -152,106 +254,159 @@ export default defineEventHandler(async (event) => {
152
254
  // present) else sessionId. We store the session id in the viewer_name
153
255
  // column as a best-effort fallback so anon sessions don't conflate.
154
256
  const viewerKey = viewerEmail ?? `anon:${sessionId}`;
155
- const countedViewSessionId =
156
- typeof viewSessionId === "string" && viewSessionId.trim()
157
- ? viewSessionId.trim()
158
- : `legacy:${sessionId}`;
257
+ const countedViewSessionId = viewSessionId ?? `legacy:${sessionId}`;
159
258
 
160
- // Try to find an existing row for this viewer.
161
- // (Using a simple scan is acceptable here — indexes are per-DB-dialect.)
162
- const existingRows = await db
163
- .select()
164
- .from(schema.recordingViewers)
165
- .where(eq(schema.recordingViewers.recordingId, recordingId));
166
- const existing = existingRows.find((r) => {
167
- if (viewerEmail) return r.viewerEmail === viewerEmail;
168
- return r.viewerEmail === null && r.viewerName === viewerKey;
169
- });
259
+ const selectViewerByKey = () =>
260
+ db
261
+ .select({
262
+ id: schema.recordingViewers.id,
263
+ totalWatchMs: schema.recordingViewers.totalWatchMs,
264
+ completedPct: schema.recordingViewers.completedPct,
265
+ countedView: schema.recordingViewers.countedView,
266
+ ctaClicked: schema.recordingViewers.ctaClicked,
267
+ })
268
+ .from(schema.recordingViewers)
269
+ .where(
270
+ and(
271
+ eq(schema.recordingViewers.recordingId, recordingId),
272
+ eq(schema.recordingViewers.viewerKey, viewerKey),
273
+ ),
274
+ )
275
+ .limit(1);
276
+
277
+ let [existing] = await selectViewerByKey();
278
+ if (!existing) {
279
+ const legacyIdentity = viewerEmail
280
+ ? eq(schema.recordingViewers.viewerEmail, viewerEmail)
281
+ : and(
282
+ isNull(schema.recordingViewers.viewerEmail),
283
+ eq(schema.recordingViewers.viewerName, viewerKey),
284
+ );
285
+ const [legacy] = await db
286
+ .select({ id: schema.recordingViewers.id })
287
+ .from(schema.recordingViewers)
288
+ .where(
289
+ and(
290
+ eq(schema.recordingViewers.recordingId, recordingId),
291
+ isNull(schema.recordingViewers.viewerKey),
292
+ legacyIdentity,
293
+ ),
294
+ )
295
+ .orderBy(
296
+ asc(schema.recordingViewers.firstViewedAt),
297
+ asc(schema.recordingViewers.id),
298
+ )
299
+ .limit(1);
300
+
301
+ if (legacy) {
302
+ await db
303
+ .update(schema.recordingViewers)
304
+ .set({ viewerKey })
305
+ .where(
306
+ and(
307
+ eq(schema.recordingViewers.id, legacy.id),
308
+ eq(schema.recordingViewers.recordingId, recordingId),
309
+ isNull(schema.recordingViewers.viewerKey),
310
+ ),
311
+ );
312
+ } else {
313
+ await db
314
+ .insert(schema.recordingViewers)
315
+ .values({
316
+ id: nanoid(),
317
+ recordingId,
318
+ viewerKey,
319
+ viewerEmail,
320
+ viewerName: viewerEmail ? resolvedViewerName : viewerKey,
321
+ firstViewedAt: now,
322
+ lastViewedAt: now,
323
+ totalWatchMs: 0,
324
+ completedPct: 0,
325
+ countedView: false,
326
+ ctaClicked: false,
327
+ })
328
+ .onConflictDoNothing();
329
+ }
330
+ [existing] = await selectViewerByKey();
331
+ }
332
+
333
+ if (!existing) {
334
+ throw new Error("Failed to resolve canonical recording viewer");
335
+ }
170
336
 
171
- let viewerId: string;
172
- const wasCountedBefore = existing?.countedView ?? false;
173
- let countedView = wasCountedBefore;
337
+ const viewerId = existing.id;
174
338
  const newTotalWatchMs = Math.max(
175
- existing?.totalWatchMs ?? 0,
339
+ existing.totalWatchMs,
176
340
  Math.floor(totalWatchMs),
177
341
  );
178
342
  const newCompletedPct = Math.max(
179
- existing?.completedPct ?? 0,
343
+ existing.completedPct,
180
344
  Math.floor(completedPct),
181
345
  );
182
-
183
346
  const meetsThreshold = shouldCountView(
184
347
  newTotalWatchMs,
185
348
  newCompletedPct,
186
- Boolean(scrubbedToEnd),
349
+ scrubbedToEnd,
187
350
  );
188
- if (meetsThreshold) countedView = true;
189
-
190
- const ctaClicked =
191
- kind === "cta-click" ? true : (existing?.ctaClicked ?? false);
192
351
 
193
- if (existing) {
194
- viewerId = existing.id;
195
- await db
352
+ const persisted = await db.transaction(async (tx) => {
353
+ await tx
196
354
  .update(schema.recordingViewers)
197
355
  .set({
198
356
  lastViewedAt: now,
199
- totalWatchMs: newTotalWatchMs,
200
- completedPct: newCompletedPct,
201
- countedView,
202
- ctaClicked,
357
+ totalWatchMs: sql`CASE WHEN ${schema.recordingViewers.totalWatchMs} > ${Math.floor(totalWatchMs)} THEN ${schema.recordingViewers.totalWatchMs} ELSE ${Math.floor(totalWatchMs)} END`,
358
+ completedPct: sql`CASE WHEN ${schema.recordingViewers.completedPct} > ${Math.floor(completedPct)} THEN ${schema.recordingViewers.completedPct} ELSE ${Math.floor(completedPct)} END`,
359
+ ...(meetsThreshold ? { countedView: true } : {}),
360
+ ...(kind === "cta-click" ? { ctaClicked: true } : {}),
203
361
  })
204
362
  .where(
205
363
  and(
206
- eq(schema.recordingViewers.id, existing.id),
207
364
  eq(schema.recordingViewers.recordingId, recordingId),
365
+ eq(schema.recordingViewers.viewerKey, viewerKey),
208
366
  ),
209
367
  );
210
- } else {
211
- viewerId = nanoid();
212
- await db.insert(schema.recordingViewers).values({
213
- id: viewerId,
368
+
369
+ await tx.insert(schema.recordingEvents).values({
370
+ id: nanoid(),
214
371
  recordingId,
215
- viewerEmail,
216
- viewerName: viewerEmail ? viewerName : viewerKey,
217
- firstViewedAt: now,
218
- lastViewedAt: now,
219
- totalWatchMs: newTotalWatchMs,
220
- completedPct: newCompletedPct,
221
- countedView,
222
- ctaClicked,
372
+ viewerId,
373
+ kind,
374
+ timestampMs: Math.floor(timestampMs),
375
+ payload,
376
+ createdAt: now,
223
377
  });
224
- }
225
378
 
226
- await db.insert(schema.recordingEvents).values({
227
- id: nanoid(),
228
- recordingId,
229
- viewerId,
230
- kind,
231
- timestampMs: Math.max(0, Math.floor(timestampMs)),
232
- payload: JSON.stringify(payload ?? {}),
233
- createdAt: now,
234
- });
379
+ if (meetsThreshold) {
380
+ await tx
381
+ .insert(schema.recordingViews)
382
+ .values({
383
+ id: nanoid(),
384
+ recordingId,
385
+ viewerId,
386
+ viewerKey,
387
+ viewSessionId: countedViewSessionId,
388
+ viewerEmail,
389
+ viewerName: viewerEmail ? resolvedViewerName : viewerKey,
390
+ viewedAt: now,
391
+ })
392
+ .onConflictDoNothing();
393
+ }
235
394
 
236
- // Record a per-open counted view when this request itself satisfies the
237
- // count threshold. Returning viewers can therefore appear again in the
238
- // owner-facing timeline, while repeated threshold/progress posts from the
239
- // same player-open session collapse through the unique index.
240
- if (meetsThreshold) {
241
- await db
242
- .insert(schema.recordingViews)
243
- .values({
244
- id: nanoid(),
245
- recordingId,
246
- viewerId,
247
- viewerKey,
248
- viewSessionId: countedViewSessionId,
249
- viewerEmail,
250
- viewerName: viewerEmail ? viewerName : viewerKey,
251
- viewedAt: now,
395
+ const [updated] = await tx
396
+ .select({
397
+ countedView: schema.recordingViewers.countedView,
252
398
  })
253
- .onConflictDoNothing();
254
- }
399
+ .from(schema.recordingViewers)
400
+ .where(
401
+ and(
402
+ eq(schema.recordingViewers.recordingId, recordingId),
403
+ eq(schema.recordingViewers.viewerKey, viewerKey),
404
+ ),
405
+ )
406
+ .limit(1);
407
+ if (!updated) throw new Error("Canonical recording viewer disappeared");
408
+ return updated;
409
+ });
255
410
 
256
411
  // Only broadcast a refresh signal on "meaningful" events to avoid
257
412
  // spamming the polling clients every 2s with watch-progress
@@ -279,7 +434,7 @@ export default defineEventHandler(async (event) => {
279
434
  }
280
435
  }
281
436
 
282
- return { ok: true, viewerId, countedView };
437
+ return { ok: true, viewerId, countedView: persisted.countedView };
283
438
  },
284
439
  );
285
440
  });
@@ -114,6 +114,22 @@ find a page to link to).
114
114
  pnpm action search-notion-pages --query "meeting notes"
115
115
  ```
116
116
 
117
+ ### list-notion-database-sources
118
+
119
+ List Notion data sources visible to the current user's OAuth connection before
120
+ attaching one to a Content database:
121
+
122
+ ```bash
123
+ pnpm action list-notion-database-sources --query "projects"
124
+ ```
125
+
126
+ The database-source pilot is read-only and uses the same per-user OAuth
127
+ connection as page sync. Choose a returned data-source ID, run
128
+ `suggest-source-join-key`, then attach it with
129
+ `attach-content-database-source --sourceType notion-database
130
+ --relationshipMode details`. Use `refresh-content-database-source` to pull a
131
+ new bounded snapshot. Never use a pasted token or claim Notion write-back.
132
+
117
133
  ### disconnect-notion
118
134
 
119
135
  Disconnect the current user's Notion OAuth connection.