@agent-native/core-corpus 0.213.1 → 0.216.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 (1108) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +1 -1
  3. package/corpus/templates/analytics/CHANGELOG.md +1 -0
  4. package/corpus/templates/analytics/changelog/2026-10-21-session-replay-quotas-count-the-compressed-size-analytics-st.md +6 -0
  5. package/corpus/templates/analytics/server/lib/session-replay.ts +40 -9
  6. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +1 -1
  7. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +1 -1
  8. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +1 -1
  9. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +1 -1
  10. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +1 -1
  11. package/corpus/templates/community/qr/package.json +1 -1
  12. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +1 -1
  13. package/corpus/templates/content/.agents/skills/content-product-development/references/verification.md +36 -0
  14. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +10 -0
  15. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +26 -1
  16. package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +4 -2
  17. package/corpus/templates/content/CHANGELOG.md +51 -0
  18. package/corpus/templates/content/acceptance/_writers.ts +604 -0
  19. package/corpus/templates/content/acceptance/generate-action-registry.ts +10 -0
  20. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/create-document.json +75 -0
  21. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/duplicate-page.json +75 -0
  22. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-find-and-replace-without-a-revision.json +75 -0
  23. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-find-and-replace.json +75 -0
  24. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/edit-document-initializecontent.json +75 -0
  25. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/import-content.json +75 -0
  26. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-import-content-source-existing-page.json +75 -0
  27. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-import-content-source-new-page.json +75 -0
  28. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-sync-local-folder-source-changed-file.json +75 -0
  29. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/local-files-sync-local-folder-source-new-file.json +75 -0
  30. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/restore-document-version.json +75 -0
  31. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/set-document-property-primary-blocks-field.json +75 -0
  32. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/set-image-alt-text.json +75 -0
  33. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/undo-content-import.json +75 -0
  34. package/corpus/templates/content/acceptance/golden/gate-off-stored-bytes/update-document-browser-full-rewrite.json +75 -0
  35. package/corpus/templates/content/acceptance/known-loss.ts +69 -0
  36. package/corpus/templates/content/acceptance/round-trip-corpus.ts +1397 -0
  37. package/corpus/templates/content/actions/_blocks-field-identity.ts +35 -1
  38. package/corpus/templates/content/actions/_content-database-lifecycle.ts +11 -11
  39. package/corpus/templates/content/actions/_content-database-personal-view.ts +2 -0
  40. package/corpus/templates/content/actions/_content-favorites.ts +37 -0
  41. package/corpus/templates/content/actions/_database-membership-lock.ts +39 -8
  42. package/corpus/templates/content/actions/_database-navigation.ts +19 -21
  43. package/corpus/templates/content/actions/_database-row-batch.ts +41 -18
  44. package/corpus/templates/content/actions/_database-row-mutation.ts +17 -28
  45. package/corpus/templates/content/actions/_database-source-utils.ts +18 -1
  46. package/corpus/templates/content/actions/_database-utils.ts +3 -0
  47. package/corpus/templates/content/actions/_database-view-setup.ts +321 -11
  48. package/corpus/templates/content/actions/_document-edit-mutation.ts +33 -32
  49. package/corpus/templates/content/actions/_document-flush.ts +593 -171
  50. package/corpus/templates/content/actions/_document-save-locks.ts +54 -0
  51. package/corpus/templates/content/actions/_document-save-row.ts +133 -0
  52. package/corpus/templates/content/actions/_github-source.ts +18 -8
  53. package/corpus/templates/content/actions/_move-page-to-space.ts +189 -25
  54. package/corpus/templates/content/actions/_nfm-syntax.ts +2 -0
  55. package/corpus/templates/content/actions/_property-utils.ts +3 -0
  56. package/corpus/templates/content/actions/_row-lock-order.ts +115 -0
  57. package/corpus/templates/content/actions/create-content-database.ts +11 -0
  58. package/corpus/templates/content/actions/create-document.ts +13 -1
  59. package/corpus/templates/content/actions/delete-content-database.ts +17 -14
  60. package/corpus/templates/content/actions/delete-document-property.ts +5 -0
  61. package/corpus/templates/content/actions/delete-document.ts +270 -97
  62. package/corpus/templates/content/actions/edit-document.ts +18 -5
  63. package/corpus/templates/content/actions/export-document.ts +3 -0
  64. package/corpus/templates/content/actions/get-blocks-field-word-count.ts +22 -5
  65. package/corpus/templates/content/actions/import-content-source.ts +170 -54
  66. package/corpus/templates/content/actions/move-document.ts +191 -73
  67. package/corpus/templates/content/actions/pull-document.ts +8 -2
  68. package/corpus/templates/content/actions/remove-database-items.ts +23 -4
  69. package/corpus/templates/content/actions/restore-content-database.ts +76 -55
  70. package/corpus/templates/content/actions/restore-document-version.ts +19 -35
  71. package/corpus/templates/content/actions/restore-document.ts +65 -53
  72. package/corpus/templates/content/actions/rollback-created-slash-document.ts +11 -16
  73. package/corpus/templates/content/actions/set-document-property.ts +15 -1
  74. package/corpus/templates/content/actions/share-local-file-document.ts +18 -14
  75. package/corpus/templates/content/actions/suggest-document-edit.ts +2 -3
  76. package/corpus/templates/content/actions/sync-local-folder-source.ts +35 -1
  77. package/corpus/templates/content/actions/undo-content-import.ts +16 -12
  78. package/corpus/templates/content/actions/update-content-database-view.ts +20 -2
  79. package/corpus/templates/content/actions/update-document.ts +19 -14
  80. package/corpus/templates/content/actions/view-screen.ts +12 -0
  81. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -6
  82. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +49 -3
  83. package/corpus/templates/content/app/components/editor/ContentImportDialog.tsx +2 -0
  84. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +53 -11
  85. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +608 -353
  86. package/corpus/templates/content/app/components/editor/StaticDocumentBody.tsx +765 -0
  87. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +233 -80
  88. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +32 -3
  89. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +88 -23
  90. package/corpus/templates/content/app/components/editor/comment-anchors.ts +19 -0
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +296 -87
  92. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
  93. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
  94. package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
  95. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
  96. package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
  97. package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
  98. package/corpus/templates/content/app/components/editor/document-flush-participant.ts +253 -0
  99. package/corpus/templates/content/app/components/editor/editor-transaction-meta.ts +28 -0
  100. package/corpus/templates/content/app/components/editor/extensions/AudioBlock.tsx +2 -1
  101. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +27 -6
  102. package/corpus/templates/content/app/components/editor/extensions/LocalMdxComponentNode.tsx +0 -1
  103. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +4 -4
  104. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +24 -2
  105. package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +25 -6
  106. package/corpus/templates/content/app/components/editor/pending-document-persistence.ts +59 -1
  107. package/corpus/templates/content/app/components/editor/stored-body-images.ts +27 -0
  108. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +17 -0
  109. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -4
  110. package/corpus/templates/content/app/components/trash/TrashBrowser.tsx +4 -1
  111. package/corpus/templates/content/app/global.css +34 -0
  112. package/corpus/templates/content/app/hooks/use-content-trash.ts +8 -0
  113. package/corpus/templates/content/app/i18n-data.ts +32 -0
  114. package/corpus/templates/content/app/import-messages.ts +25 -0
  115. package/corpus/templates/content/app/trash-messages.ts +16 -0
  116. package/corpus/templates/content/changelog/2026-10-16-a-page-open-in-a-tab-that-shows-it-s-in-the-trash-reopens-wi.md +6 -0
  117. package/corpus/templates/content/changelog/2026-10-16-a-page-restored-from-trash-returns-to-its-place-in-its-colle.md +6 -0
  118. package/corpus/templates/content/changelog/2026-10-16-agent-written-markdown-keeps-nested-lists-indented-with-spac.md +6 -0
  119. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-you-can-edit-no-longer-rebuilds-its-editor-on.md +6 -0
  120. package/corpus/templates/content/changelog/2026-10-16-pages-moved-or-imported-at-the-same-time-can-no-longer-end-u.md +6 -0
  121. package/corpus/templates/content/changelog/2026-10-16-re-importing-a-markdown-export-of-a-page-no-longer-reports-t.md +6 -0
  122. package/corpus/templates/content/changelog/2026-10-16-saving-a-page-or-restoring-one-of-its-versions-no-longer-fai.md +6 -0
  123. package/corpus/templates/content/changelog/2026-10-17-agents-reading-a-page-you-have-open-no-longer-wait-or-fail-w.md +6 -0
  124. package/corpus/templates/content/changelog/2026-10-19-fresh-document-reads-require-confirmation-from-every-open-ed.md +6 -0
  125. package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
  126. package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-20-html-table-cells-preserve-a-trailing-backslash-when-opening-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-20-preserve-empty-paragraph-colors-and-refuse-unsupported-inlin.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-20-the-advanced-filter-and-each-filter-group-inside-it-have-the.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-21-blocks-fields-keep-unconfirmed-saves-visible-and-agent-reads.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-21-preserve-compact-table-row-groups-and-report-invalid-block-colors.md +6 -0
  132. package/corpus/templates/content/changelog/2026-10-21-suggested-edits-show-an-agent-s-whole-reason-open-a-group-to.md +6 -0
  133. package/corpus/templates/content/changelog/2026-10-21-suggestions-keep-refusing-unmatched-emphasis-in-stored-markd.md +6 -0
  134. package/corpus/templates/content/changelog/2026-10-22-accept-remaining-and-reject-remaining-show-the-result-for-ev.md +6 -0
  135. package/corpus/templates/content/changelog/2026-10-22-accepting-a-group-of-suggested-edits-saves-faster-and-adds-o.md +6 -0
  136. package/corpus/templates/content/changelog/2026-10-22-agents-read-saved-page-text-after-failed-icon-change.md +6 -0
  137. package/corpus/templates/content/changelog/2026-10-22-changes-made-from-a-menu-or-popover-such-as-an-image-s-alt-t.md +6 -0
  138. package/corpus/templates/content/changelog/2026-10-22-text-an-agent-writes-inside-a-callout-column-or-synced-block.md +6 -0
  139. package/corpus/templates/content/e2e/auth-files.ts +28 -0
  140. package/corpus/templates/content/e2e/convergence-summary.ts +84 -29
  141. package/corpus/templates/content/e2e/gate-arm.ts +15 -0
  142. package/corpus/templates/content/e2e/global-setup.ts +43 -75
  143. package/corpus/templates/content/e2e/global-teardown.ts +34 -0
  144. package/corpus/templates/content/e2e/helpers.ts +598 -92
  145. package/corpus/templates/content/e2e/live-body-gate.ts +188 -0
  146. package/corpus/templates/content/e2e/playwright.config.ts +12 -3
  147. package/corpus/templates/content/e2e/server-identity.ts +47 -0
  148. package/corpus/templates/content/e2e/stored-form/collab-blocks.ts +123 -0
  149. package/corpus/templates/content/package.json +1 -1
  150. package/corpus/templates/content/scripts/_stored-form-census-kinds.ts +30 -2
  151. package/corpus/templates/content/scripts/historical-icon-apply.ts +9 -1
  152. package/corpus/templates/content/scripts/trace-startup.mjs +460 -133
  153. package/corpus/templates/content/server/db/exec.ts +58 -0
  154. package/corpus/templates/content/server/db/schema.ts +1 -0
  155. package/corpus/templates/content/server/lib/content-trash-purge.ts +208 -144
  156. package/corpus/templates/content/server/lib/document-metadata-updated-at.ts +43 -1
  157. package/corpus/templates/content/server/lib/document-revision.ts +23 -0
  158. package/corpus/templates/content/server/lib/live-body-writer.ts +199 -0
  159. package/corpus/templates/content/server/lib/notion-sync.ts +63 -29
  160. package/corpus/templates/content/server/lib/notion.ts +5 -5
  161. package/corpus/templates/content/server/lib/suggested-edits.ts +357 -230
  162. package/corpus/templates/content/server/plugins/db.ts +5 -0
  163. package/corpus/templates/content/server/routes/api/documents/[id]/move.patch.ts +50 -168
  164. package/corpus/templates/content/shared/api.ts +20 -0
  165. package/corpus/templates/content/shared/blocks-fields-manifest.ts +106 -0
  166. package/corpus/templates/content/shared/database-item-layout.ts +591 -0
  167. package/corpus/templates/content/shared/database-query.ts +192 -30
  168. package/corpus/templates/content/shared/document-export.ts +12 -14
  169. package/corpus/templates/content/shared/document-flush-protocol.ts +88 -0
  170. package/corpus/templates/content/shared/editor-save-state.ts +97 -0
  171. package/corpus/templates/content/shared/feature-flags.ts +10 -0
  172. package/corpus/templates/content/shared/import/markdown.ts +91 -7
  173. package/corpus/templates/content/shared/import/types.ts +2 -0
  174. package/corpus/templates/content/shared/inline-math.ts +6 -4
  175. package/corpus/templates/content/shared/nfm-fidelity.ts +247 -0
  176. package/corpus/templates/content/shared/nfm.ts +1933 -340
  177. package/corpus/templates/content/shared/suggestion-diff.ts +13 -0
  178. package/corpus/templates/content/shared/suggestion-formatting.ts +143 -20
  179. package/corpus/templates/content/shared/suggestion-text.ts +39 -0
  180. package/corpus/templates/content/vitest.config.ts +10 -0
  181. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +1 -1
  182. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +1 -1
  183. package/corpus/templates/design/.agents/skills/native-effects/SKILL.md +9 -2
  184. package/corpus/templates/design/.generated/bridge/shader-fill-preview.generated.ts +3 -249
  185. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +5 -88
  186. package/corpus/templates/design/AGENTS.md +1 -3
  187. package/corpus/templates/design/CHANGELOG.md +32 -0
  188. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -6
  189. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -7
  190. package/corpus/templates/design/actions/_native-local-export-server.ts +44 -31
  191. package/corpus/templates/design/actions/_native-render-contexts.ts +76 -21
  192. package/corpus/templates/design/actions/claim-native-local-export.ts +5 -0
  193. package/corpus/templates/design/actions/finish-native-local-export.ts +13 -2
  194. package/corpus/templates/design/actions/get-native-shader-approvals.ts +35 -0
  195. package/corpus/templates/design/actions/get-shader.ts +134 -219
  196. package/corpus/templates/design/actions/list-design-extensions.ts +3 -58
  197. package/corpus/templates/design/actions/request-native-local-export.ts +32 -6
  198. package/corpus/templates/design/actions/run-design-extension-action.ts +0 -29
  199. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +131 -108
  200. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +0 -218
  201. package/corpus/templates/design/app/components/design/DesignThumbnail.tsx +59 -3
  202. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -47
  203. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +242 -62
  204. package/corpus/templates/design/app/components/design/NativePreviewErrorBoundary.tsx +53 -0
  205. package/corpus/templates/design/app/components/design/bridge/codegen.ts +18 -3
  206. package/corpus/templates/design/app/components/design/bridge/native-composition-clock.ts +5 -4
  207. package/corpus/templates/design/app/components/design/bridge/native-dom.ts +37 -0
  208. package/corpus/templates/design/app/components/design/bridge/native-intrinsic-image.ts +2 -1
  209. package/corpus/templates/design/app/components/design/bridge/native-mesh-grid.ts +90 -0
  210. package/corpus/templates/design/app/components/design/bridge/native-opaque-public-asset.ts +363 -0
  211. package/corpus/templates/design/app/components/design/bridge/native-opaque-worker-lease.ts +179 -0
  212. package/corpus/templates/design/app/components/design/bridge/native-preview-geometry.ts +132 -7
  213. package/corpus/templates/design/app/components/design/bridge/native-preview-policy.ts +4 -0
  214. package/corpus/templates/design/app/components/design/bridge/native-preview-viewport-transport.ts +378 -0
  215. package/corpus/templates/design/app/components/design/bridge/native-scene-body-transform.ts +62 -0
  216. package/corpus/templates/design/app/components/design/bridge/native-scene-presentation-element.ts +10 -0
  217. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +646 -167
  218. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +246 -192
  219. package/corpus/templates/design/app/components/design/bridge/native-stateful-resource.ts +2 -2
  220. package/corpus/templates/design/app/components/design/bridge/native-stateless-compute.ts +5 -2
  221. package/corpus/templates/design/app/components/design/bridge/native-svg-source.ts +11 -1
  222. package/corpus/templates/design/app/components/design/bridge/native-worker-broker.ts +6 -0
  223. package/corpus/templates/design/app/components/design/bridge/native-worker-entry.ts +761 -303
  224. package/corpus/templates/design/app/components/design/bridge/native-worker-fill.ts +16 -6
  225. package/corpus/templates/design/app/components/design/bridge/native-worker-gpu.ts +854 -91
  226. package/corpus/templates/design/app/components/design/bridge/native-worker-host.ts +15 -1
  227. package/corpus/templates/design/app/components/design/bridge/native-worker-input-assets.ts +200 -0
  228. package/corpus/templates/design/app/components/design/bridge/native-worker-protocol.ts +40 -1
  229. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +425 -126
  230. package/corpus/templates/design/app/components/design/bridge/native-worker-source-snapshot.ts +21 -0
  231. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +5 -371
  232. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +4 -146
  233. package/corpus/templates/design/app/components/design/design-canvas/interact-document.ts +5 -9
  234. package/corpus/templates/design/app/components/design/design-canvas/native-preview-surface.ts +150 -0
  235. package/corpus/templates/design/app/components/design/design-canvas/native-public-asset-preview.ts +409 -0
  236. package/corpus/templates/design/app/components/design/design-canvas/native-public-worker-preview.ts +1086 -0
  237. package/corpus/templates/design/app/components/design/design-canvas/private-replay-screenshot-preview.ts +2 -2
  238. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +15 -26
  239. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +74 -67
  240. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +7 -19
  241. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +39 -30
  242. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -0
  243. package/corpus/templates/design/app/components/design/inspector/NativeShaderColorControls.tsx +3 -7
  244. package/corpus/templates/design/app/components/design/inspector/NativeShaderPaintControls.tsx +12 -5
  245. package/corpus/templates/design/app/components/design/inspector/NativeShaderPanel.tsx +705 -190
  246. package/corpus/templates/design/app/components/design/inspector/fill-field-reading.ts +1 -2
  247. package/corpus/templates/design/app/components/design/inspector/index.ts +0 -6
  248. package/corpus/templates/design/app/components/design/inspector/native-catalog-display.ts +26 -5
  249. package/corpus/templates/design/app/components/design/inspector/native-shader-catalog-fixture.ts +42 -0
  250. package/corpus/templates/design/app/components/design/inspector/native-shader-catalog-preview.ts +51 -33
  251. package/corpus/templates/design/app/components/design/inspector/native-shader-paint.ts +1 -0
  252. package/corpus/templates/design/app/components/design/inspector/native-shader-property-draft.ts +89 -23
  253. package/corpus/templates/design/app/components/design/inspector/saved-glsl-shader.tsx +243 -0
  254. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +54 -5
  255. package/corpus/templates/design/app/components/design/shader-source-coordination.ts +66 -0
  256. package/corpus/templates/design/app/components/design/use-native-shader-approvals.ts +36 -8
  257. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +60 -3
  258. package/corpus/templates/design/app/i18n/en-US.ts +78 -2
  259. package/corpus/templates/design/app/pages/Present.tsx +36 -1
  260. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +1 -1
  261. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +1 -1
  262. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +2 -3
  263. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +1 -1
  264. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +1 -1
  265. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +1 -1
  266. package/corpus/templates/design/app/pages/design-editor/commands/paste-svg-layer.ts +1 -1
  267. package/corpus/templates/design/app/pages/design-editor/commands/prepare-content-history-replay.ts +1 -1
  268. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1 -1
  269. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +84 -41
  270. package/corpus/templates/design/app/pages/design-editor/commands/shader-source-edit.ts +1 -1
  271. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +1 -1
  272. package/corpus/templates/design/app/pages/design-editor/design-editor-view.tsx +0 -2
  273. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-content-and-components.ts +4 -33
  274. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-export-and-handoff.ts +2 -2
  275. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-history.ts +0 -11
  276. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-layer-models.ts +2 -55
  277. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +0 -3
  278. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-selection-and-styles.ts +0 -24
  279. package/corpus/templates/design/app/pages/design-editor/text-edit-utils.ts +0 -16
  280. package/corpus/templates/design/app/pages/design-editor/use-native-local-export-requests.ts +70 -11
  281. package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +0 -2
  282. package/corpus/templates/design/changelog/2026-10-20-folded-mesh-adds-an-animated-sculpted-fill-with-editable-col.md +6 -0
  283. package/corpus/templates/design/changelog/2026-10-20-read-only-design-views-render-native-shader-previews-in-nest.md +6 -0
  284. package/corpus/templates/design/changelog/2026-10-20-selected-screens-with-shader-effects-can-be-exported-togethe.md +6 -0
  285. package/corpus/templates/design/changelog/2026-10-20-shader-previews-reuse-validated-scene-data-while-panning-inc.md +6 -0
  286. package/corpus/templates/design/changelog/2026-10-20-shader-previews-stay-aligned-while-you-zoom.md +6 -0
  287. package/corpus/templates/design/changelog/2026-10-20-signed-out-sessions-no-longer-watch-for-private-png-export-r.md +6 -0
  288. package/corpus/templates/design/changelog/2026-10-21-effect-thumbnails-show-richer-previews.md +6 -0
  289. package/corpus/templates/design/changelog/2026-10-21-native-effects-render-across-previews.md +6 -0
  290. package/corpus/templates/design/changelog/2026-10-21-native-shader-previews-use-less-gpu-memory-when-many-mesh-ef.md +6 -0
  291. package/corpus/templates/design/changelog/2026-10-21-opening-design-no-longer-shows-png-export-errors-when-no-exp.md +6 -0
  292. package/corpus/templates/design/changelog/2026-10-21-pulsing-border-controls-grouped.md +6 -0
  293. package/corpus/templates/design/changelog/2026-10-21-pulsing-border-follows-element-outlines.md +6 -0
  294. package/corpus/templates/design/changelog/2026-10-21-remove-obsolete-shader-fills.md +6 -0
  295. package/corpus/templates/design/changelog/2026-10-21-shader-previews-recover-from-temporary-gpu-and-image-failure.md +6 -0
  296. package/corpus/templates/design/changelog/2026-10-22-shader-fills-and-effects-render-correctly-on-canvas-elements.md +6 -0
  297. package/corpus/templates/design/package.json +0 -1
  298. package/corpus/templates/design/playwright.config.ts +1 -1
  299. package/corpus/templates/design/scripts/runtime-budget-zoom.ts +9 -21
  300. package/corpus/templates/design/scripts/runtime-budget.ts +10 -24
  301. package/corpus/templates/design/server/plugins/agent-chat.ts +0 -2
  302. package/corpus/templates/design/shared/effect-graph.ts +46 -1
  303. package/corpus/templates/design/shared/generation-output-receipt.ts +0 -1
  304. package/corpus/templates/design/shared/native-effect-animation-capability.ts +6 -0
  305. package/corpus/templates/design/shared/native-effect-folded-mesh-v1.ts +268 -0
  306. package/corpus/templates/design/shared/native-effect-folded-mesh-v2.ts +310 -0
  307. package/corpus/templates/design/shared/native-effect-folded-mesh.ts +489 -0
  308. package/corpus/templates/design/shared/native-effect-gallery-additions.ts +2 -2
  309. package/corpus/templates/design/shared/native-effect-paint.ts +8 -0
  310. package/corpus/templates/design/shared/native-effect-parameter-constraints.ts +8 -1
  311. package/corpus/templates/design/shared/native-effect-presets.ts +18 -1
  312. package/corpus/templates/design/shared/native-effect-property-presentation.ts +81 -0
  313. package/corpus/templates/design/shared/native-effect-pulsing-border-v3.ts +281 -0
  314. package/corpus/templates/design/shared/native-effect-pulsing-border-v4.ts +284 -0
  315. package/corpus/templates/design/shared/native-effect-pulsing-border.ts +300 -0
  316. package/corpus/templates/design/shared/native-effect-trust.ts +6 -0
  317. package/corpus/templates/design/shared/native-effect-wgsl.ts +12 -0
  318. package/corpus/templates/design/shared/native-effects.ts +528 -15
  319. package/corpus/templates/design/shared/native-input-resources.ts +36 -0
  320. package/corpus/templates/design/shared/native-local-export.ts +14 -0
  321. package/corpus/templates/design/shared/native-parameter-animation.ts +1 -0
  322. package/corpus/templates/design/shared/native-pointer-input.ts +9 -11
  323. package/corpus/templates/design/shared/shader-fills.ts +0 -513
  324. package/corpus/templates/design/vitest.setup.ts +7 -1
  325. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +1 -1
  326. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +20 -18
  327. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +1 -1
  328. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +36 -80
  329. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +54 -16
  330. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +50 -73
  331. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +20 -18
  332. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +1 -1
  333. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +1 -1
  334. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +1 -1
  335. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +1 -1
  336. package/corpus/templates/slides-2/.agents/skills/actions/SKILL.md +199 -0
  337. package/corpus/templates/slides-2/.agents/skills/actions/references/action-fields.md +145 -0
  338. package/corpus/templates/slides-2/.agents/skills/actions/references/examples.md +96 -0
  339. package/corpus/templates/slides-2/.agents/skills/actions/references/provider-apis.md +95 -0
  340. package/corpus/templates/slides-2/.agents/skills/adding-a-feature/SKILL.md +235 -0
  341. package/corpus/templates/slides-2/.agents/skills/agent-native-docs/SKILL.md +161 -0
  342. package/corpus/templates/slides-2/.agents/skills/agent-native-toolkit/SKILL.md +314 -0
  343. package/corpus/templates/slides-2/.agents/skills/analytics-data-for-decks/SKILL.md +44 -0
  344. package/corpus/templates/slides-2/.agents/skills/apply-slide-comments/SKILL.md +63 -0
  345. package/corpus/templates/slides-2/.agents/skills/client-side-routing/SKILL.md +87 -0
  346. package/corpus/templates/slides-2/.agents/skills/context-awareness/SKILL.md +282 -0
  347. package/corpus/templates/slides-2/.agents/skills/create-deck/SKILL.md +263 -0
  348. package/corpus/templates/slides-2/.agents/skills/create-deck/references/reference-inputs.md +72 -0
  349. package/corpus/templates/slides-2/.agents/skills/create-deck/references/slide-templates.md +93 -0
  350. package/corpus/templates/slides-2/.agents/skills/create-deck/references/source-material.md +16 -0
  351. package/corpus/templates/slides-2/.agents/skills/creative-context/SKILL.md +134 -0
  352. package/corpus/templates/slides-2/.agents/skills/customizing-agent-native/SKILL.md +219 -0
  353. package/corpus/templates/slides-2/.agents/skills/deck-management/SKILL.md +182 -0
  354. package/corpus/templates/slides-2/.agents/skills/delegate-to-agent/SKILL.md +62 -0
  355. package/corpus/templates/slides-2/.agents/skills/design-systems/SKILL.md +233 -0
  356. package/corpus/templates/slides-2/.agents/skills/external-agents/SKILL.md +549 -0
  357. package/corpus/templates/slides-2/.agents/skills/external-agents/references/mcp-apps-embedding.md +169 -0
  358. package/corpus/templates/slides-2/.agents/skills/frontend-design/SKILL.md +102 -0
  359. package/corpus/templates/slides-2/.agents/skills/frontend-design/references/visual-direction.md +102 -0
  360. package/corpus/templates/slides-2/.agents/skills/image-generation-via-a2a/SKILL.md +134 -0
  361. package/corpus/templates/slides-2/.agents/skills/performance/SKILL.md +79 -0
  362. package/corpus/templates/slides-2/.agents/skills/portability/SKILL.md +108 -0
  363. package/corpus/templates/slides-2/.agents/skills/real-time-sync/SKILL.md +146 -0
  364. package/corpus/templates/slides-2/.agents/skills/reliable-mutations/SKILL.md +72 -0
  365. package/corpus/templates/slides-2/.agents/skills/secrets/SKILL.md +82 -0
  366. package/corpus/templates/slides-2/.agents/skills/security/SKILL.md +80 -0
  367. package/corpus/templates/slides-2/.agents/skills/self-modifying-code/SKILL.md +123 -0
  368. package/corpus/templates/slides-2/.agents/skills/shadcn-ui/SKILL.md +71 -0
  369. package/corpus/templates/slides-2/.agents/skills/sharing/SKILL.md +286 -0
  370. package/corpus/templates/slides-2/.agents/skills/slide-design/SKILL.md +144 -0
  371. package/corpus/templates/slides-2/.agents/skills/slide-editing/SKILL.md +284 -0
  372. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/animations.md +33 -0
  373. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/freeform-objects.md +22 -0
  374. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/restyling.md +69 -0
  375. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/transitions.md +26 -0
  376. package/corpus/templates/slides-2/.agents/skills/slide-editing/references/video.md +45 -0
  377. package/corpus/templates/slides-2/.agents/skills/slide-images/SKILL.md +136 -0
  378. package/corpus/templates/slides-2/.agents/skills/storing-data/SKILL.md +234 -0
  379. package/corpus/templates/slides-2/.agents/skills/turn-into-skill/SKILL.md +112 -0
  380. package/corpus/templates/slides-2/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  381. package/corpus/templates/slides-2/.agents/skills/workspace-conventions/SKILL.md +103 -0
  382. package/corpus/templates/slides-2/.builder/rules/deploy-app.mdc +22 -0
  383. package/corpus/templates/slides-2/.builder/rules/organize-ui.mdc +9 -0
  384. package/corpus/templates/slides-2/.dockerignore +61 -0
  385. package/corpus/templates/slides-2/.env.example +23 -0
  386. package/corpus/templates/slides-2/.ignore +3 -0
  387. package/corpus/templates/slides-2/.mcp.json +8 -0
  388. package/corpus/templates/slides-2/.oxfmtrc.json +8 -0
  389. package/corpus/templates/slides-2/AGENTS.md +66 -0
  390. package/corpus/templates/slides-2/CHANGELOG.md +76 -0
  391. package/corpus/templates/slides-2/DEVELOPING.md +157 -0
  392. package/corpus/templates/slides-2/README.md +58 -0
  393. package/corpus/templates/slides-2/THIRD_PARTY_NOTICES.md +33 -0
  394. package/corpus/templates/slides-2/_gitignore +38 -0
  395. package/corpus/templates/slides-2/actions/_agent-presence.ts +29 -0
  396. package/corpus/templates/slides-2/actions/_app-url.ts +37 -0
  397. package/corpus/templates/slides-2/actions/_deck-app-scope.ts +30 -0
  398. package/corpus/templates/slides-2/actions/_deck-tracking.ts +260 -0
  399. package/corpus/templates/slides-2/actions/_deck-write.ts +206 -0
  400. package/corpus/templates/slides-2/actions/_imported-slide-source.ts +162 -0
  401. package/corpus/templates/slides-2/actions/_inline-image-storage.ts +1013 -0
  402. package/corpus/templates/slides-2/actions/_linked-design-system.ts +83 -0
  403. package/corpus/templates/slides-2/actions/_presenter-notes.ts +83 -0
  404. package/corpus/templates/slides-2/actions/_render-artifacts.ts +111 -0
  405. package/corpus/templates/slides-2/actions/_slide-source.ts +344 -0
  406. package/corpus/templates/slides-2/actions/_tab-state.ts +51 -0
  407. package/corpus/templates/slides-2/actions/_test-brandfetch.ts +31 -0
  408. package/corpus/templates/slides-2/actions/_uploaded-files.ts +114 -0
  409. package/corpus/templates/slides-2/actions/add-deck.ts +123 -0
  410. package/corpus/templates/slides-2/actions/add-slide-comment.ts +267 -0
  411. package/corpus/templates/slides-2/actions/add-slide.ts +888 -0
  412. package/corpus/templates/slides-2/actions/analyze-brand-assets.ts +66 -0
  413. package/corpus/templates/slides-2/actions/apply-design-system.ts +111 -0
  414. package/corpus/templates/slides-2/actions/approve-deck-access-request.ts +287 -0
  415. package/corpus/templates/slides-2/actions/audit-contrast.ts +143 -0
  416. package/corpus/templates/slides-2/actions/check-image-gen.ts +17 -0
  417. package/corpus/templates/slides-2/actions/clone-context-slide.ts +340 -0
  418. package/corpus/templates/slides-2/actions/clone-creative-context-deck.ts +143 -0
  419. package/corpus/templates/slides-2/actions/control-deck-history.ts +186 -0
  420. package/corpus/templates/slides-2/actions/control-presenter.ts +117 -0
  421. package/corpus/templates/slides-2/actions/control-slide-editor.ts +267 -0
  422. package/corpus/templates/slides-2/actions/create-deck-from-template.ts +274 -0
  423. package/corpus/templates/slides-2/actions/create-deck.ts +1243 -0
  424. package/corpus/templates/slides-2/actions/create-design-system.ts +131 -0
  425. package/corpus/templates/slides-2/actions/create-share-link.ts +127 -0
  426. package/corpus/templates/slides-2/actions/db-health.ts +22 -0
  427. package/corpus/templates/slides-2/actions/delete-asset.ts +154 -0
  428. package/corpus/templates/slides-2/actions/delete-deck.ts +427 -0
  429. package/corpus/templates/slides-2/actions/delete-design-system.ts +167 -0
  430. package/corpus/templates/slides-2/actions/delete-slide-comment.ts +143 -0
  431. package/corpus/templates/slides-2/actions/delete-staged-dataset.ts +30 -0
  432. package/corpus/templates/slides-2/actions/duplicate-deck.ts +299 -0
  433. package/corpus/templates/slides-2/actions/export-google-slides.ts +41 -0
  434. package/corpus/templates/slides-2/actions/export-html.ts +770 -0
  435. package/corpus/templates/slides-2/actions/export-pptx.ts +2203 -0
  436. package/corpus/templates/slides-2/actions/extract-pdf.ts +49 -0
  437. package/corpus/templates/slides-2/actions/generate-image-api.ts +671 -0
  438. package/corpus/templates/slides-2/actions/generate-image.ts +492 -0
  439. package/corpus/templates/slides-2/actions/get-agent-access-deck.ts +52 -0
  440. package/corpus/templates/slides-2/actions/get-deck-access-status.ts +130 -0
  441. package/corpus/templates/slides-2/actions/get-deck-design-tokens.ts +62 -0
  442. package/corpus/templates/slides-2/actions/get-deck-reference-context.ts +232 -0
  443. package/corpus/templates/slides-2/actions/get-deck-template.ts +38 -0
  444. package/corpus/templates/slides-2/actions/get-deck-version.ts +85 -0
  445. package/corpus/templates/slides-2/actions/get-deck.ts +562 -0
  446. package/corpus/templates/slides-2/actions/get-design-system-decode-job-status.ts +57 -0
  447. package/corpus/templates/slides-2/actions/get-design-system-tier-limit.ts +12 -0
  448. package/corpus/templates/slides-2/actions/get-design-system.ts +453 -0
  449. package/corpus/templates/slides-2/actions/get-google-docs-status.ts +106 -0
  450. package/corpus/templates/slides-2/actions/get-google-picker-token.ts +84 -0
  451. package/corpus/templates/slides-2/actions/get-home-suggestions.ts +52 -0
  452. package/corpus/templates/slides-2/actions/get-layout-overflows.ts +192 -0
  453. package/corpus/templates/slides-2/actions/get-slide-render.ts +125 -0
  454. package/corpus/templates/slides-2/actions/get-slide-source.ts +115 -0
  455. package/corpus/templates/slides-2/actions/get-slide-theme-preview.ts +64 -0
  456. package/corpus/templates/slides-2/actions/get-slide-theme.ts +44 -0
  457. package/corpus/templates/slides-2/actions/get-slides-2-upload-storage-status.ts +17 -0
  458. package/corpus/templates/slides-2/actions/get-slides-notification-prefs.ts +37 -0
  459. package/corpus/templates/slides-2/actions/get-workspace-defaults.ts +51 -0
  460. package/corpus/templates/slides-2/actions/image-gen-status.ts +35 -0
  461. package/corpus/templates/slides-2/actions/import-code.ts +26 -0
  462. package/corpus/templates/slides-2/actions/import-design-project.ts +49 -0
  463. package/corpus/templates/slides-2/actions/import-document.ts +125 -0
  464. package/corpus/templates/slides-2/actions/import-docx.ts +57 -0
  465. package/corpus/templates/slides-2/actions/import-file.ts +1273 -0
  466. package/corpus/templates/slides-2/actions/import-from-url.ts +25 -0
  467. package/corpus/templates/slides-2/actions/import-google-doc.ts +208 -0
  468. package/corpus/templates/slides-2/actions/import-google-font.ts +110 -0
  469. package/corpus/templates/slides-2/actions/import-google-slides-reference.ts +667 -0
  470. package/corpus/templates/slides-2/actions/import-pptx.ts +564 -0
  471. package/corpus/templates/slides-2/actions/import-svgl-logo.ts +115 -0
  472. package/corpus/templates/slides-2/actions/index-design-system-with-builder.ts +181 -0
  473. package/corpus/templates/slides-2/actions/index-uploaded-design-system-files.ts +77 -0
  474. package/corpus/templates/slides-2/actions/list-assets.ts +286 -0
  475. package/corpus/templates/slides-2/actions/list-deck-templates.ts +83 -0
  476. package/corpus/templates/slides-2/actions/list-deck-versions.ts +185 -0
  477. package/corpus/templates/slides-2/actions/list-decks.ts +591 -0
  478. package/corpus/templates/slides-2/actions/list-design-systems.ts +180 -0
  479. package/corpus/templates/slides-2/actions/list-slide-comments.ts +128 -0
  480. package/corpus/templates/slides-2/actions/list-slide-themes.ts +17 -0
  481. package/corpus/templates/slides-2/actions/list-staged-datasets.ts +14 -0
  482. package/corpus/templates/slides-2/actions/logo-config.ts +13 -0
  483. package/corpus/templates/slides-2/actions/navigate.ts +158 -0
  484. package/corpus/templates/slides-2/actions/patch-deck.ts +2378 -0
  485. package/corpus/templates/slides-2/actions/provider-api-catalog.ts +25 -0
  486. package/corpus/templates/slides-2/actions/provider-api-docs.ts +83 -0
  487. package/corpus/templates/slides-2/actions/provider-api-request.ts +222 -0
  488. package/corpus/templates/slides-2/actions/query-staged-dataset.ts +83 -0
  489. package/corpus/templates/slides-2/actions/read-composer-source.ts +108 -0
  490. package/corpus/templates/slides-2/actions/request-deck-access.ts +803 -0
  491. package/corpus/templates/slides-2/actions/restore-deck-version.ts +203 -0
  492. package/corpus/templates/slides-2/actions/run.ts +2 -0
  493. package/corpus/templates/slides-2/actions/save-deck.ts +313 -0
  494. package/corpus/templates/slides-2/actions/search-google-fonts.ts +61 -0
  495. package/corpus/templates/slides-2/actions/search-images.ts +30 -0
  496. package/corpus/templates/slides-2/actions/search-logos.ts +23 -0
  497. package/corpus/templates/slides-2/actions/search-svgl-logos.ts +42 -0
  498. package/corpus/templates/slides-2/actions/set-default-design-system.ts +88 -0
  499. package/corpus/templates/slides-2/actions/set-workspace-defaults.ts +61 -0
  500. package/corpus/templates/slides-2/actions/start-design-system-upload.ts +80 -0
  501. package/corpus/templates/slides-2/actions/sync-design-system-with-builder.ts +84 -0
  502. package/corpus/templates/slides-2/actions/toggle-slide-comment-reaction.ts +150 -0
  503. package/corpus/templates/slides-2/actions/transform-slide-objects.ts +1528 -0
  504. package/corpus/templates/slides-2/actions/update-asset.ts +499 -0
  505. package/corpus/templates/slides-2/actions/update-deck-aspect-ratio.ts +95 -0
  506. package/corpus/templates/slides-2/actions/update-deck-design-tokens.ts +175 -0
  507. package/corpus/templates/slides-2/actions/update-design-system.ts +95 -0
  508. package/corpus/templates/slides-2/actions/update-slide-comment.ts +213 -0
  509. package/corpus/templates/slides-2/actions/update-slide-source.ts +353 -0
  510. package/corpus/templates/slides-2/actions/update-slide.ts +850 -0
  511. package/corpus/templates/slides-2/actions/update-slides-notification-prefs.ts +55 -0
  512. package/corpus/templates/slides-2/actions/view-screen.ts +1098 -0
  513. package/corpus/templates/slides-2/agent-native.app-skill.json +165 -0
  514. package/corpus/templates/slides-2/agent-native.config.ts +20 -0
  515. package/corpus/templates/slides-2/agent-native.json +18 -0
  516. package/corpus/templates/slides-2/app/components/ThemeToggle.tsx +47 -0
  517. package/corpus/templates/slides-2/app/components/comments/SlideCommentPins.tsx +781 -0
  518. package/corpus/templates/slides-2/app/components/comments/SlideCommentsPanel.tsx +1075 -0
  519. package/corpus/templates/slides-2/app/components/deck/DeckCard.tsx +304 -0
  520. package/corpus/templates/slides-2/app/components/deck/DeckFilterMenu.tsx +83 -0
  521. package/corpus/templates/slides-2/app/components/deck/ExcalidrawSlide.tsx +183 -0
  522. package/corpus/templates/slides-2/app/components/deck/MermaidRenderer.tsx +126 -0
  523. package/corpus/templates/slides-2/app/components/deck/SlideRenderer.tsx +1553 -0
  524. package/corpus/templates/slides-2/app/components/design-system/DesignSystemCard.tsx +235 -0
  525. package/corpus/templates/slides-2/app/components/design-system/DesignSystemSetup.tsx +2021 -0
  526. package/corpus/templates/slides-2/app/components/design-system/builder-design-system-upload.ts +200 -0
  527. package/corpus/templates/slides-2/app/components/design-system/builder-index-response.ts +104 -0
  528. package/corpus/templates/slides-2/app/components/editor/AddSlidePopover.tsx +461 -0
  529. package/corpus/templates/slides-2/app/components/editor/AiEditingMarker.tsx +24 -0
  530. package/corpus/templates/slides-2/app/components/editor/AlignmentGuides.tsx +82 -0
  531. package/corpus/templates/slides-2/app/components/editor/AnimationsPanel.tsx +698 -0
  532. package/corpus/templates/slides-2/app/components/editor/AppMenu.tsx +356 -0
  533. package/corpus/templates/slides-2/app/components/editor/AssetLibraryPanel.tsx +508 -0
  534. package/corpus/templates/slides-2/app/components/editor/BlockBubbleMenu.tsx +548 -0
  535. package/corpus/templates/slides-2/app/components/editor/DeckAccessDeniedPage.tsx +173 -0
  536. package/corpus/templates/slides-2/app/components/editor/DeckEditorSkeleton.tsx +35 -0
  537. package/corpus/templates/slides-2/app/components/editor/DeferredPopoverFallback.tsx +148 -0
  538. package/corpus/templates/slides-2/app/components/editor/EditorActionCluster.tsx +217 -0
  539. package/corpus/templates/slides-2/app/components/editor/EditorSidebar.tsx +1047 -0
  540. package/corpus/templates/slides-2/app/components/editor/EditorToolbar.tsx +1285 -0
  541. package/corpus/templates/slides-2/app/components/editor/ExportMenu.tsx +579 -0
  542. package/corpus/templates/slides-2/app/components/editor/GeneratingOverlay.tsx +21 -0
  543. package/corpus/templates/slides-2/app/components/editor/GeneratingSlidePreview.tsx +73 -0
  544. package/corpus/templates/slides-2/app/components/editor/GoogleDocImportHint.tsx +378 -0
  545. package/corpus/templates/slides-2/app/components/editor/GoogleDriveConnectionCta.tsx +154 -0
  546. package/corpus/templates/slides-2/app/components/editor/GoogleSlidesReferenceImport.tsx +299 -0
  547. package/corpus/templates/slides-2/app/components/editor/HistoryPanel.tsx +354 -0
  548. package/corpus/templates/slides-2/app/components/editor/HomeQuickStartDialog.tsx +202 -0
  549. package/corpus/templates/slides-2/app/components/editor/ImageCropOverlay.tsx +820 -0
  550. package/corpus/templates/slides-2/app/components/editor/ImageDropPromptPopover.tsx +280 -0
  551. package/corpus/templates/slides-2/app/components/editor/ImageGenPanel.tsx +244 -0
  552. package/corpus/templates/slides-2/app/components/editor/ImageOverlay.tsx +280 -0
  553. package/corpus/templates/slides-2/app/components/editor/ImageSearchPanel.tsx +183 -0
  554. package/corpus/templates/slides-2/app/components/editor/ImportDeckButton.tsx +247 -0
  555. package/corpus/templates/slides-2/app/components/editor/LogoSearchPanel.tsx +366 -0
  556. package/corpus/templates/slides-2/app/components/editor/MermaidToExcalidrawPanel.tsx +99 -0
  557. package/corpus/templates/slides-2/app/components/editor/MissingDeckAccessPane.tsx +219 -0
  558. package/corpus/templates/slides-2/app/components/editor/NewDeckReferenceStep.tsx +812 -0
  559. package/corpus/templates/slides-2/app/components/editor/PromptDialog.tsx +963 -0
  560. package/corpus/templates/slides-2/app/components/editor/QuestionFlow.tsx +119 -0
  561. package/corpus/templates/slides-2/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
  562. package/corpus/templates/slides-2/app/components/editor/ShareDialog.tsx +126 -0
  563. package/corpus/templates/slides-2/app/components/editor/SlideContextToolbar.tsx +1471 -0
  564. package/corpus/templates/slides-2/app/components/editor/SlideEditor.tsx +11183 -0
  565. package/corpus/templates/slides-2/app/components/editor/SlideOverflowWarning.tsx +82 -0
  566. package/corpus/templates/slides-2/app/components/editor/SlideScrollStack.tsx +307 -0
  567. package/corpus/templates/slides-2/app/components/editor/SlideSlashCommandMenu.tsx +692 -0
  568. package/corpus/templates/slides-2/app/components/editor/SlideThumbnailContextMenu.tsx +52 -0
  569. package/corpus/templates/slides-2/app/components/editor/SlideTransitionControl.tsx +184 -0
  570. package/corpus/templates/slides-2/app/components/editor/SlidesComposerContext.tsx +789 -0
  571. package/corpus/templates/slides-2/app/components/editor/SlidesComposerContextProvider.tsx +74 -0
  572. package/corpus/templates/slides-2/app/components/editor/SlidesLayersPanel.tsx +377 -0
  573. package/corpus/templates/slides-2/app/components/editor/SpeakerNotesPanel.tsx +119 -0
  574. package/corpus/templates/slides-2/app/components/editor/TweaksPanel.tsx +77 -0
  575. package/corpus/templates/slides-2/app/components/editor/UploadStorageGate.tsx +37 -0
  576. package/corpus/templates/slides-2/app/components/editor/bullet-editing.ts +880 -0
  577. package/corpus/templates/slides-2/app/components/editor/canvas-interactions/index.ts +12 -0
  578. package/corpus/templates/slides-2/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +119 -0
  579. package/corpus/templates/slides-2/app/components/editor/commit-active-edit.ts +14 -0
  580. package/corpus/templates/slides-2/app/components/editor/editor-command-model.ts +35 -0
  581. package/corpus/templates/slides-2/app/components/editor/fit-text-object.ts +31 -0
  582. package/corpus/templates/slides-2/app/components/editor/in-place-text-session.browser-cases.json +983 -0
  583. package/corpus/templates/slides-2/app/components/editor/in-place-text-session.browser-host.ts +747 -0
  584. package/corpus/templates/slides-2/app/components/editor/in-place-text-session.ts +6891 -0
  585. package/corpus/templates/slides-2/app/components/editor/inline-edit-session.ts +26 -0
  586. package/corpus/templates/slides-2/app/components/editor/list-editing.ts +501 -0
  587. package/corpus/templates/slides-2/app/components/editor/rich-text-selection.ts +975 -0
  588. package/corpus/templates/slides-2/app/components/editor/rte-test-utils.ts +265 -0
  589. package/corpus/templates/slides-2/app/components/editor/selection-overlay-measurement.ts +191 -0
  590. package/corpus/templates/slides-2/app/components/editor/slide-escape-arbiter.ts +34 -0
  591. package/corpus/templates/slides-2/app/components/editor/slide-object-interactions.ts +6362 -0
  592. package/corpus/templates/slides-2/app/components/editor/slide-object-transform.browser-host.ts +19 -0
  593. package/corpus/templates/slides-2/app/components/editor/slide-pointer-target.ts +432 -0
  594. package/corpus/templates/slides-2/app/components/editor/slide-presence.ts +13 -0
  595. package/corpus/templates/slides-2/app/components/editor/slide-style.ts +243 -0
  596. package/corpus/templates/slides-2/app/components/editor/slide-text-targets.ts +540 -0
  597. package/corpus/templates/slides-2/app/components/editor/style-clipboard.ts +102 -0
  598. package/corpus/templates/slides-2/app/components/editor/use-prompt-import.ts +159 -0
  599. package/corpus/templates/slides-2/app/components/home/SlidesHomeLibrary.tsx +79 -0
  600. package/corpus/templates/slides-2/app/components/layout/AgentWorkIndicator.tsx +193 -0
  601. package/corpus/templates/slides-2/app/components/layout/Header.tsx +110 -0
  602. package/corpus/templates/slides-2/app/components/layout/Layout.tsx +492 -0
  603. package/corpus/templates/slides-2/app/components/layout/Sidebar.tsx +109 -0
  604. package/corpus/templates/slides-2/app/components/layout/layout-route-policy.ts +28 -0
  605. package/corpus/templates/slides-2/app/components/presentation/PresentationView.tsx +1116 -0
  606. package/corpus/templates/slides-2/app/components/presentation/PresenterView.tsx +526 -0
  607. package/corpus/templates/slides-2/app/components/presentation/SlideTransitionStage.tsx +297 -0
  608. package/corpus/templates/slides-2/app/components/presentation/present-channel.ts +31 -0
  609. package/corpus/templates/slides-2/app/components/presentation/slide-build-steps.ts +231 -0
  610. package/corpus/templates/slides-2/app/components/presentation/slide-navigation.ts +273 -0
  611. package/corpus/templates/slides-2/app/components/presentation/slide-transition-motion.ts +258 -0
  612. package/corpus/templates/slides-2/app/components/presentation/voice-navigation.ts +61 -0
  613. package/corpus/templates/slides-2/app/components/settings/notification-settings.tsx +93 -0
  614. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2AssetBrowser.tsx +726 -0
  615. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2CommandMenu.tsx +453 -0
  616. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Comments.tsx +571 -0
  617. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2DesignTokens.tsx +345 -0
  618. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Editor.tsx +4804 -0
  619. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2ExportMenu.tsx +671 -0
  620. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2FontBrowser.tsx +342 -0
  621. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2ImageCropControls.tsx +487 -0
  622. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2Inspector.tsx +950 -0
  623. package/corpus/templates/slides-2/app/components/slides-2-editor/Slides2SvglLogoSearchDialog.tsx +332 -0
  624. package/corpus/templates/slides-2/app/components/slides-2-editor/cross-slide-edit-history.ts +151 -0
  625. package/corpus/templates/slides-2/app/components/slides-2-editor/design-token-presets.ts +140 -0
  626. package/corpus/templates/slides-2/app/components/slides-2-editor/editor-action-errors.ts +47 -0
  627. package/corpus/templates/slides-2/app/components/slides-2-editor/editor-export-errors.ts +60 -0
  628. package/corpus/templates/slides-2/app/components/slides-2-editor/geometry.ts +275 -0
  629. package/corpus/templates/slides-2/app/components/slides-2-editor/image-crop-style.ts +82 -0
  630. package/corpus/templates/slides-2/app/components/slides-2-editor/inline-text-editor.tsx +631 -0
  631. package/corpus/templates/slides-2/app/components/slides-2-editor/keyboard-shortcuts.ts +104 -0
  632. package/corpus/templates/slides-2/app/components/slides-2-editor/object-insertion.ts +13 -0
  633. package/corpus/templates/slides-2/app/components/slides-2-editor/preview-patches.ts +54 -0
  634. package/corpus/templates/slides-2/app/components/slides-2-editor/sandbox-render-errors.ts +23 -0
  635. package/corpus/templates/slides-2/app/components/slides-2-editor/source-structure.ts +634 -0
  636. package/corpus/templates/slides-2/app/components/slides-2-editor/types.ts +85 -0
  637. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2BlackoutOverlay.tsx +13 -0
  638. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2ControlBar.tsx +285 -0
  639. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2HelpDialog.tsx +72 -0
  640. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2JumpInput.tsx +38 -0
  641. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2LaserPointer.tsx +67 -0
  642. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2Overview.tsx +161 -0
  643. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2Player.tsx +839 -0
  644. package/corpus/templates/slides-2/app/components/slides-2-presenter/Slides2ProgressBar.tsx +23 -0
  645. package/corpus/templates/slides-2/app/components/slides-2-presenter/keyboard.ts +36 -0
  646. package/corpus/templates/slides-2/app/components/slides-2-presenter/presenter-channel.ts +131 -0
  647. package/corpus/templates/slides-2/app/components/slides-2-presenter/use-presenter-action-bridge.ts +296 -0
  648. package/corpus/templates/slides-2/app/components/slides-2-presenter/useSlides2TouchSwipe.ts +80 -0
  649. package/corpus/templates/slides-2/app/components/slides-2-runtime/SandboxedSlide.tsx +724 -0
  650. package/corpus/templates/slides-2/app/components/slides-2-runtime/SlideContentRenderer.tsx +137 -0
  651. package/corpus/templates/slides-2/app/components/slides-2-runtime/Slides2Canvas.tsx +99 -0
  652. package/corpus/templates/slides-2/app/components/slides-2-runtime/constants.ts +4 -0
  653. package/corpus/templates/slides-2/app/components/slides-2-runtime/protocol.ts +1400 -0
  654. package/corpus/templates/slides-2/app/components/slides-2-runtime/transition-frame.ts +439 -0
  655. package/corpus/templates/slides-2/app/components/templates/DeckTemplateLibrary.tsx +314 -0
  656. package/corpus/templates/slides-2/app/components/templates/DeckTemplatePreview.tsx +60 -0
  657. package/corpus/templates/slides-2/app/components/templates/DeckTemplateStage.tsx +45 -0
  658. package/corpus/templates/slides-2/app/components/ui/alert-dialog.tsx +1 -0
  659. package/corpus/templates/slides-2/app/components/ui/avatar.tsx +1 -0
  660. package/corpus/templates/slides-2/app/components/ui/button-group.tsx +1 -0
  661. package/corpus/templates/slides-2/app/components/ui/button.tsx +1 -0
  662. package/corpus/templates/slides-2/app/components/ui/card.tsx +1 -0
  663. package/corpus/templates/slides-2/app/components/ui/command.tsx +1 -0
  664. package/corpus/templates/slides-2/app/components/ui/context-menu.tsx +32 -0
  665. package/corpus/templates/slides-2/app/components/ui/dialog.tsx +1 -0
  666. package/corpus/templates/slides-2/app/components/ui/dropdown-menu.tsx +1 -0
  667. package/corpus/templates/slides-2/app/components/ui/filter-trigger.tsx +1 -0
  668. package/corpus/templates/slides-2/app/components/ui/input.tsx +1 -0
  669. package/corpus/templates/slides-2/app/components/ui/label.tsx +1 -0
  670. package/corpus/templates/slides-2/app/components/ui/popover.tsx +1 -0
  671. package/corpus/templates/slides-2/app/components/ui/progress.tsx +1 -0
  672. package/corpus/templates/slides-2/app/components/ui/scroll-area.tsx +1 -0
  673. package/corpus/templates/slides-2/app/components/ui/select.tsx +1 -0
  674. package/corpus/templates/slides-2/app/components/ui/separator.tsx +1 -0
  675. package/corpus/templates/slides-2/app/components/ui/sheet.tsx +1 -0
  676. package/corpus/templates/slides-2/app/components/ui/skeleton.tsx +1 -0
  677. package/corpus/templates/slides-2/app/components/ui/slider.tsx +1 -0
  678. package/corpus/templates/slides-2/app/components/ui/spinner.tsx +1 -0
  679. package/corpus/templates/slides-2/app/components/ui/switch.tsx +1 -0
  680. package/corpus/templates/slides-2/app/components/ui/tabs.tsx +59 -0
  681. package/corpus/templates/slides-2/app/components/ui/textarea.tsx +1 -0
  682. package/corpus/templates/slides-2/app/components/ui/toggle-group.tsx +1 -0
  683. package/corpus/templates/slides-2/app/components/ui/toolkit-provider.tsx +17 -0
  684. package/corpus/templates/slides-2/app/components/ui/tooltip.tsx +1 -0
  685. package/corpus/templates/slides-2/app/components/visual-editor/CanvasCommentPins.tsx +46 -0
  686. package/corpus/templates/slides-2/app/components/visual-editor/DrawOverlay.tsx +27 -0
  687. package/corpus/templates/slides-2/app/components/visual-editor/MultiSelectChip.tsx +97 -0
  688. package/corpus/templates/slides-2/app/components/visual-editor/SaveStatusIndicator.tsx +273 -0
  689. package/corpus/templates/slides-2/app/components/visual-editor/index.ts +4 -0
  690. package/corpus/templates/slides-2/app/context/DeckContext.tsx +7486 -0
  691. package/corpus/templates/slides-2/app/design-system.ts +3 -0
  692. package/corpus/templates/slides-2/app/entry.client.tsx +10 -0
  693. package/corpus/templates/slides-2/app/entry.server.tsx +10 -0
  694. package/corpus/templates/slides-2/app/global.css +1626 -0
  695. package/corpus/templates/slides-2/app/hooks/use-agent-generating.ts +517 -0
  696. package/corpus/templates/slides-2/app/hooks/use-compact-editor-layout.ts +26 -0
  697. package/corpus/templates/slides-2/app/hooks/use-contrast-audit-bridge.ts +49 -0
  698. package/corpus/templates/slides-2/app/hooks/use-deck-access-reload.ts +70 -0
  699. package/corpus/templates/slides-2/app/hooks/use-deck-access.ts +50 -0
  700. package/corpus/templates/slides-2/app/hooks/use-deck-design-system.ts +146 -0
  701. package/corpus/templates/slides-2/app/hooks/use-deck-history-action-bridge.ts +266 -0
  702. package/corpus/templates/slides-2/app/hooks/use-deck-presence.ts +89 -0
  703. package/corpus/templates/slides-2/app/hooks/use-deck-role.ts +46 -0
  704. package/corpus/templates/slides-2/app/hooks/use-deck-versions.ts +43 -0
  705. package/corpus/templates/slides-2/app/hooks/use-design-system-workflows.ts +11 -0
  706. package/corpus/templates/slides-2/app/hooks/use-design-systems.ts +31 -0
  707. package/corpus/templates/slides-2/app/hooks/use-generating-slide-preview.ts +115 -0
  708. package/corpus/templates/slides-2/app/hooks/use-navigation-state.ts +229 -0
  709. package/corpus/templates/slides-2/app/hooks/use-new-deck-generation-signal.ts +46 -0
  710. package/corpus/templates/slides-2/app/hooks/use-new-deck-generation.ts +710 -0
  711. package/corpus/templates/slides-2/app/hooks/use-sidebar-collapsed.ts +95 -0
  712. package/corpus/templates/slides-2/app/hooks/use-slide-comments.ts +202 -0
  713. package/corpus/templates/slides-2/app/hooks/use-slide-file-storage-status.ts +55 -0
  714. package/corpus/templates/slides-2/app/hooks/use-undo-selection.ts +66 -0
  715. package/corpus/templates/slides-2/app/hooks/use-workspace-defaults.ts +31 -0
  716. package/corpus/templates/slides-2/app/i18n/en-US.ts +1507 -0
  717. package/corpus/templates/slides-2/app/i18n/index.ts +19 -0
  718. package/corpus/templates/slides-2/app/lib/agent-visible-message.ts +21 -0
  719. package/corpus/templates/slides-2/app/lib/aspect-ratios.ts +7 -0
  720. package/corpus/templates/slides-2/app/lib/asset-error-copy.ts +59 -0
  721. package/corpus/templates/slides-2/app/lib/canvas-zoom.ts +47 -0
  722. package/corpus/templates/slides-2/app/lib/composer-context.ts +152 -0
  723. package/corpus/templates/slides-2/app/lib/composer-draft.ts +34 -0
  724. package/corpus/templates/slides-2/app/lib/contrast-audit.ts +390 -0
  725. package/corpus/templates/slides-2/app/lib/contrast-resolve.ts +240 -0
  726. package/corpus/templates/slides-2/app/lib/create-deck-generation.ts +938 -0
  727. package/corpus/templates/slides-2/app/lib/deck-access-lost.ts +4 -0
  728. package/corpus/templates/slides-2/app/lib/deck-backup.ts +186 -0
  729. package/corpus/templates/slides-2/app/lib/deck-editor-loading.ts +91 -0
  730. package/corpus/templates/slides-2/app/lib/deck-export-file-client.ts +77 -0
  731. package/corpus/templates/slides-2/app/lib/deck-filter.ts +52 -0
  732. package/corpus/templates/slides-2/app/lib/deck-list-loading.ts +16 -0
  733. package/corpus/templates/slides-2/app/lib/deck-preview-frame.ts +15 -0
  734. package/corpus/templates/slides-2/app/lib/deck-share-links.ts +9 -0
  735. package/corpus/templates/slides-2/app/lib/deck-sorting.ts +19 -0
  736. package/corpus/templates/slides-2/app/lib/deferred-editor-surfaces.ts +24 -0
  737. package/corpus/templates/slides-2/app/lib/design-system-selection.ts +11 -0
  738. package/corpus/templates/slides-2/app/lib/design-systems.ts +231 -0
  739. package/corpus/templates/slides-2/app/lib/dynamic-import.ts +50 -0
  740. package/corpus/templates/slides-2/app/lib/editor-agent-handoff.ts +92 -0
  741. package/corpus/templates/slides-2/app/lib/editor-shortcuts.ts +153 -0
  742. package/corpus/templates/slides-2/app/lib/export-google-slides-client.ts +168 -0
  743. package/corpus/templates/slides-2/app/lib/export-pdf-client.ts +684 -0
  744. package/corpus/templates/slides-2/app/lib/export-pptx-client.ts +2420 -0
  745. package/corpus/templates/slides-2/app/lib/generation-lifecycle.ts +24 -0
  746. package/corpus/templates/slides-2/app/lib/generation-state.ts +247 -0
  747. package/corpus/templates/slides-2/app/lib/google-docs-client.ts +110 -0
  748. package/corpus/templates/slides-2/app/lib/google-slides-export-availability-client.ts +53 -0
  749. package/corpus/templates/slides-2/app/lib/google-slides-reference-source.ts +18 -0
  750. package/corpus/templates/slides-2/app/lib/image-download.ts +85 -0
  751. package/corpus/templates/slides-2/app/lib/image-drop-to-agent.ts +210 -0
  752. package/corpus/templates/slides-2/app/lib/import-uploaded-deck.ts +132 -0
  753. package/corpus/templates/slides-2/app/lib/inline-edit-remote.ts +55 -0
  754. package/corpus/templates/slides-2/app/lib/localize-slides-changelog.ts +47 -0
  755. package/corpus/templates/slides-2/app/lib/mermaid-blocks.ts +18 -0
  756. package/corpus/templates/slides-2/app/lib/new-deck-reference-selection.ts +174 -0
  757. package/corpus/templates/slides-2/app/lib/normalize-slide-padding.ts +74 -0
  758. package/corpus/templates/slides-2/app/lib/pending-deck-changes.ts +33 -0
  759. package/corpus/templates/slides-2/app/lib/portaled-layer.ts +7 -0
  760. package/corpus/templates/slides-2/app/lib/pptx-google-slides.ts +204 -0
  761. package/corpus/templates/slides-2/app/lib/prompt-attachment-adapter.ts +37 -0
  762. package/corpus/templates/slides-2/app/lib/prompt-file-uploads.ts +557 -0
  763. package/corpus/templates/slides-2/app/lib/recent-references.ts +106 -0
  764. package/corpus/templates/slides-2/app/lib/reference-document-hydration.ts +406 -0
  765. package/corpus/templates/slides-2/app/lib/register-chat-renderers.tsx +645 -0
  766. package/corpus/templates/slides-2/app/lib/reveal-after-delete.ts +55 -0
  767. package/corpus/templates/slides-2/app/lib/sanitize-slide-html.ts +752 -0
  768. package/corpus/templates/slides-2/app/lib/slide-agent-context.ts +148 -0
  769. package/corpus/templates/slides-2/app/lib/slide-animation-elements.ts +900 -0
  770. package/corpus/templates/slides-2/app/lib/slide-clipboard.ts +534 -0
  771. package/corpus/templates/slides-2/app/lib/slide-comment-anchor.ts +286 -0
  772. package/corpus/templates/slides-2/app/lib/slide-content-merge.ts +295 -0
  773. package/corpus/templates/slides-2/app/lib/slide-image-replacement.ts +1919 -0
  774. package/corpus/templates/slides-2/app/lib/slide-live-remote.ts +215 -0
  775. package/corpus/templates/slides-2/app/lib/slide-scroll.ts +56 -0
  776. package/corpus/templates/slides-2/app/lib/slide-source-map.ts +1099 -0
  777. package/corpus/templates/slides-2/app/lib/slide-video.ts +390 -0
  778. package/corpus/templates/slides-2/app/lib/slides-relay-tracking.ts +114 -0
  779. package/corpus/templates/slides-2/app/lib/streaming-slide-html.ts +77 -0
  780. package/corpus/templates/slides-2/app/lib/tab-id.ts +3 -0
  781. package/corpus/templates/slides-2/app/lib/template-navigation.ts +15 -0
  782. package/corpus/templates/slides-2/app/lib/text-tool-shortcut.ts +75 -0
  783. package/corpus/templates/slides-2/app/lib/undo-reveal.ts +332 -0
  784. package/corpus/templates/slides-2/app/lib/upload-response.ts +107 -0
  785. package/corpus/templates/slides-2/app/lib/utils.ts +38 -0
  786. package/corpus/templates/slides-2/app/lib/validate-native-slide-html.ts +177 -0
  787. package/corpus/templates/slides-2/app/lib/video-upload.ts +354 -0
  788. package/corpus/templates/slides-2/app/pages/DeckEditor.tsx +6158 -0
  789. package/corpus/templates/slides-2/app/pages/DesignSystems.tsx +363 -0
  790. package/corpus/templates/slides-2/app/pages/Index.tsx +3254 -0
  791. package/corpus/templates/slides-2/app/pages/NotFound.tsx +33 -0
  792. package/corpus/templates/slides-2/app/pages/Presentation.tsx +120 -0
  793. package/corpus/templates/slides-2/app/pages/SharedPresentation.tsx +174 -0
  794. package/corpus/templates/slides-2/app/pages/SlideThemeDetail.tsx +227 -0
  795. package/corpus/templates/slides-2/app/pages/SlideThemes.tsx +155 -0
  796. package/corpus/templates/slides-2/app/pages/Slides2EditorPage.tsx +1618 -0
  797. package/corpus/templates/slides-2/app/pages/Slides2PresenterConsolePage.tsx +937 -0
  798. package/corpus/templates/slides-2/app/pages/Slides2PresenterPage.tsx +979 -0
  799. package/corpus/templates/slides-2/app/pages/Templates.tsx +32 -0
  800. package/corpus/templates/slides-2/app/root.tsx +391 -0
  801. package/corpus/templates/slides-2/app/routes/$.tsx +10 -0
  802. package/corpus/templates/slides-2/app/routes/_index.tsx +1 -0
  803. package/corpus/templates/slides-2/app/routes/access-request.approve.tsx +276 -0
  804. package/corpus/templates/slides-2/app/routes/agent.tsx +19 -0
  805. package/corpus/templates/slides-2/app/routes/chat.$threadId.tsx +1 -0
  806. package/corpus/templates/slides-2/app/routes/chat.tsx +190 -0
  807. package/corpus/templates/slides-2/app/routes/deck.$id.tsx +10 -0
  808. package/corpus/templates/slides-2/app/routes/deck.$id_.present.tsx +10 -0
  809. package/corpus/templates/slides-2/app/routes/design-systems.tsx +10 -0
  810. package/corpus/templates/slides-2/app/routes/extensions.$id.$slug.tsx +2 -0
  811. package/corpus/templates/slides-2/app/routes/extensions.$id.tsx +5 -0
  812. package/corpus/templates/slides-2/app/routes/extensions._index.tsx +5 -0
  813. package/corpus/templates/slides-2/app/routes/extensions.tsx +5 -0
  814. package/corpus/templates/slides-2/app/routes/home-layout.tsx +31 -0
  815. package/corpus/templates/slides-2/app/routes/home.tsx +22 -0
  816. package/corpus/templates/slides-2/app/routes/p.$id.tsx +195 -0
  817. package/corpus/templates/slides-2/app/routes/settings.$.tsx +1 -0
  818. package/corpus/templates/slides-2/app/routes/settings.tsx +131 -0
  819. package/corpus/templates/slides-2/app/routes/share.$token.tsx +137 -0
  820. package/corpus/templates/slides-2/app/routes/slide.tsx +209 -0
  821. package/corpus/templates/slides-2/app/routes/team.tsx +11 -0
  822. package/corpus/templates/slides-2/app/routes/templates.tsx +1 -0
  823. package/corpus/templates/slides-2/app/routes/themes.$id.tsx +1 -0
  824. package/corpus/templates/slides-2/app/routes/themes.tsx +1 -0
  825. package/corpus/templates/slides-2/app/routes.ts +24 -0
  826. package/corpus/templates/slides-2/app/vite-env.d.ts +24 -0
  827. package/corpus/templates/slides-2/changelog/2026-10-17-find-and-switch-decks-from-the-presenter-console.md +6 -0
  828. package/corpus/templates/slides-2/changelog/2026-10-17-meet-slides-2-create-and-refine-editable-slide-decks-with-an.md +6 -0
  829. package/corpus/templates/slides-2/changelog/2026-10-17-pptx-image-export.md +6 -0
  830. package/corpus/templates/slides-2/changelog/2026-10-17-visual-editor.md +6 -0
  831. package/corpus/templates/slides-2/changelog/2026-10-18-apply-an-accessible-design-system-to-an-existing-deck-before.md +6 -0
  832. package/corpus/templates/slides-2/changelog/2026-10-18-choose-a-template-or-start-a-new-presentation-from-the-slide.md +6 -0
  833. package/corpus/templates/slides-2/changelog/2026-10-18-generate-decks-from-a-prompt-then-edit-slide-elements-direct.md +6 -0
  834. package/corpus/templates/slides-2/changelog/2026-10-18-generate-presentation-decks-from-a-prompt-and-edit-every-sli.md +6 -0
  835. package/corpus/templates/slides-2/changelog/2026-10-18-new-presentation-drafts-save-before-generation-starts-so-you.md +6 -0
  836. package/corpus/templates/slides-2/changelog/2026-10-18-the-assistant-can-undo-redo-and-save-pending-edits-in-the-vi.md +6 -0
  837. package/corpus/templates/slides-2/changelog/2026-10-19-agents-can-insert-duplicate-and-delete-canvas-objects-in-sou.md +6 -0
  838. package/corpus/templates/slides-2/changelog/2026-10-19-browse-slide-themes.md +6 -0
  839. package/corpus/templates/slides-2/changelog/2026-10-19-clicking-a-slide-preview-in-overview-selects-that-slide.md +6 -0
  840. package/corpus/templates/slides-2/changelog/2026-10-19-create-decks-from-a-prompt-and-edit-slides-directly-on-the-c.md +6 -0
  841. package/corpus/templates/slides-2/changelog/2026-10-19-creating-and-editing-decks-now-starts-with-the-tools-most-re.md +6 -0
  842. package/corpus/templates/slides-2/changelog/2026-10-19-deleting-decks-revokes-public-links.md +7 -0
  843. package/corpus/templates/slides-2/changelog/2026-10-19-escape-cancels-an-in-progress-slide-drag-without-losing-earl.md +6 -0
  844. package/corpus/templates/slides-2/changelog/2026-10-19-import-failures-now-show-clear-localized-descriptions-instea.md +6 -0
  845. package/corpus/templates/slides-2/changelog/2026-10-19-presenter-controls-stay-responsive-when-moving-over-the-slid.md +6 -0
  846. package/corpus/templates/slides-2/changelog/2026-10-19-presenter-notes-stay-private-from-viewers-and-commenters.md +6 -0
  847. package/corpus/templates/slides-2/changelog/2026-10-19-preview-generated-slides-in-chat.md +6 -0
  848. package/corpus/templates/slides-2/changelog/2026-10-19-remote-image-previews-accept-supported-raster-formats.md +6 -0
  849. package/corpus/templates/slides-2/changelog/2026-10-19-search-the-svgl-catalog-and-add-svg-logos-to-a-deck-or-the-g.md +6 -0
  850. package/corpus/templates/slides-2/changelog/2026-10-19-slide-generation-explains-when-react-effect-hooks-cannot-run.md +6 -0
  851. package/corpus/templates/slides-2/changelog/2026-10-19-the-editor-highlights-hovered-objects-and-keeps-image-replac.md +6 -0
  852. package/corpus/templates/slides-2/changelog/2026-10-19-the-slide-editor-uses-the-shared-agent-native-controls.md +6 -0
  853. package/corpus/templates/slides-2/changelog/2026-10-20-deck-editing-controls-match-the-workspace.md +6 -0
  854. package/corpus/templates/slides-2/changelog/2026-10-20-google-slides-imports-now-report-missing-image-data-instead-.md +6 -0
  855. package/corpus/templates/slides-2/changelog/2026-10-20-imports-and-previews-handle-large-content-safely.md +6 -0
  856. package/corpus/templates/slides-2/changelog/2026-10-20-public-deck-pages-can-export-source-backed-slides-to-pdf.md +6 -0
  857. package/corpus/templates/slides-2/changelog/2026-10-20-saved-slide-images-stay-available.md +6 -0
  858. package/corpus/templates/slides-2/changelog/2026-10-20-scope-global-media-picks-to-assets-the-current-deck-can-reso.md +6 -0
  859. package/corpus/templates/slides-2/components.json +20 -0
  860. package/corpus/templates/slides-2/drizzle.config.ts +3 -0
  861. package/corpus/templates/slides-2/learnings.defaults.md +5 -0
  862. package/corpus/templates/slides-2/netlify.toml +24 -0
  863. package/corpus/templates/slides-2/package.json +147 -0
  864. package/corpus/templates/slides-2/public/agent-native-icon-dark.svg +10 -0
  865. package/corpus/templates/slides-2/public/agent-native-icon-light.svg +10 -0
  866. package/corpus/templates/slides-2/public/agent-native-logo-dark.svg +21 -0
  867. package/corpus/templates/slides-2/public/agent-native-logo-light.svg +21 -0
  868. package/corpus/templates/slides-2/public/assets/builder-logo-white.svg +15 -0
  869. package/corpus/templates/slides-2/public/assets/generated/preview.html +61 -0
  870. package/corpus/templates/slides-2/public/assets/intuit-logo-white.svg +6 -0
  871. package/corpus/templates/slides-2/public/assets/walmart-logo-white.svg +6 -0
  872. package/corpus/templates/slides-2/public/chatgpt-plugin-icon.svg +7 -0
  873. package/corpus/templates/slides-2/public/favicon.svg +1 -0
  874. package/corpus/templates/slides-2/public/icon-180.svg +1 -0
  875. package/corpus/templates/slides-2/public/icon-192.svg +1 -0
  876. package/corpus/templates/slides-2/public/icon-512.svg +1 -0
  877. package/corpus/templates/slides-2/public/manifest.json +20 -0
  878. package/corpus/templates/slides-2/public/placeholder.svg +1 -0
  879. package/corpus/templates/slides-2/public/robots.txt +14 -0
  880. package/corpus/templates/slides-2/public/visual-edit-structure-proof.html +99 -0
  881. package/corpus/templates/slides-2/react-router.config.ts +7 -0
  882. package/corpus/templates/slides-2/scripts/edit-fidelity/README.md +499 -0
  883. package/corpus/templates/slides-2/scripts/edit-fidelity/authoring-fuzz.ts +9517 -0
  884. package/corpus/templates/slides-2/scripts/edit-fidelity/baseline.json +16427 -0
  885. package/corpus/templates/slides-2/scripts/edit-fidelity/dev-server-url.ts +10 -0
  886. package/corpus/templates/slides-2/scripts/edit-fidelity/lib/in-page.ts +1975 -0
  887. package/corpus/templates/slides-2/scripts/edit-fidelity/lib/metrics.ts +1401 -0
  888. package/corpus/templates/slides-2/scripts/edit-fidelity/retry-infra.ts +7 -0
  889. package/corpus/templates/slides-2/scripts/edit-fidelity/run-options.ts +21 -0
  890. package/corpus/templates/slides-2/scripts/edit-fidelity/run-outcomes.ts +127 -0
  891. package/corpus/templates/slides-2/scripts/edit-fidelity/run.ts +8451 -0
  892. package/corpus/templates/slides-2/scripts/export-fidelity/README.md +116 -0
  893. package/corpus/templates/slides-2/scripts/export-fidelity/compare-layout-lib.ts +241 -0
  894. package/corpus/templates/slides-2/scripts/export-fidelity/compare-layout.ts +30 -0
  895. package/corpus/templates/slides-2/scripts/export-fidelity/diff.ts +222 -0
  896. package/corpus/templates/slides-2/scripts/export-fidelity/export.ts +765 -0
  897. package/corpus/templates/slides-2/scripts/export-fidelity/fixtures/agent-dark-deck.json +48 -0
  898. package/corpus/templates/slides-2/scripts/export-fidelity/generate-calibration.ts +765 -0
  899. package/corpus/templates/slides-2/scripts/export-fidelity/google-export.ts +137 -0
  900. package/corpus/templates/slides-2/scripts/export-fidelity/google-layout.ts +82 -0
  901. package/corpus/templates/slides-2/scripts/export-fidelity/resolve-pkg.ts +29 -0
  902. package/corpus/templates/slides-2/scripts/migrate-production.ts +22 -0
  903. package/corpus/templates/slides-2/server/db/index.ts +73 -0
  904. package/corpus/templates/slides-2/server/db/schema.ts +189 -0
  905. package/corpus/templates/slides-2/server/handlers/assets-url.ts +13 -0
  906. package/corpus/templates/slides-2/server/handlers/assets.ts +3217 -0
  907. package/corpus/templates/slides-2/server/handlers/decks.ts +185 -0
  908. package/corpus/templates/slides-2/server/handlers/demo.ts +9 -0
  909. package/corpus/templates/slides-2/server/handlers/design-systems.ts +219 -0
  910. package/corpus/templates/slides-2/server/handlers/google-docs-auth.ts +165 -0
  911. package/corpus/templates/slides-2/server/handlers/image-gen.ts +153 -0
  912. package/corpus/templates/slides-2/server/handlers/image-providers/gemini.ts +209 -0
  913. package/corpus/templates/slides-2/server/handlers/image-providers/index.ts +61 -0
  914. package/corpus/templates/slides-2/server/handlers/image-providers/openai.ts +288 -0
  915. package/corpus/templates/slides-2/server/handlers/image-providers/types.ts +35 -0
  916. package/corpus/templates/slides-2/server/handlers/import/docx-parser.ts +31 -0
  917. package/corpus/templates/slides-2/server/handlers/import/html-converter.ts +1534 -0
  918. package/corpus/templates/slides-2/server/handlers/import/pdf-fidelity-parser.ts +1039 -0
  919. package/corpus/templates/slides-2/server/handlers/import/pdf-sidecar-reader.ts +81 -0
  920. package/corpus/templates/slides-2/server/handlers/import/pdf-style-digest.ts +187 -0
  921. package/corpus/templates/slides-2/server/handlers/import/pptx-assets.ts +155 -0
  922. package/corpus/templates/slides-2/server/handlers/import/pptx-parser.ts +57 -0
  923. package/corpus/templates/slides-2/server/handlers/request-auth-context.ts +108 -0
  924. package/corpus/templates/slides-2/server/handlers/share.ts +52 -0
  925. package/corpus/templates/slides-2/server/handlers/uploads-chunked.ts +1066 -0
  926. package/corpus/templates/slides-2/server/handlers/uploads.ts +457 -0
  927. package/corpus/templates/slides-2/server/lib/access-request-email.ts +66 -0
  928. package/corpus/templates/slides-2/server/lib/assets-image-delegation.ts +331 -0
  929. package/corpus/templates/slides-2/server/lib/bounded-multipart.ts +49 -0
  930. package/corpus/templates/slides-2/server/lib/builder-design-system-proxy.ts +122 -0
  931. package/corpus/templates/slides-2/server/lib/chat-attachments.ts +425 -0
  932. package/corpus/templates/slides-2/server/lib/chatgpt-directory-tools.ts +246 -0
  933. package/corpus/templates/slides-2/server/lib/chunked-upload-session.ts +195 -0
  934. package/corpus/templates/slides-2/server/lib/comment-notifications.ts +184 -0
  935. package/corpus/templates/slides-2/server/lib/deck-access-requests.ts +102 -0
  936. package/corpus/templates/slides-2/server/lib/deck-asset-scope.ts +150 -0
  937. package/corpus/templates/slides-2/server/lib/deck-export-tracking.ts +119 -0
  938. package/corpus/templates/slides-2/server/lib/deck-templates/a-case-study.ts +154 -0
  939. package/corpus/templates/slides-2/server/lib/deck-templates/a-company.ts +160 -0
  940. package/corpus/templates/slides-2/server/lib/deck-templates/a-pitch.ts +151 -0
  941. package/corpus/templates/slides-2/server/lib/deck-templates/a-quarterly.ts +264 -0
  942. package/corpus/templates/slides-2/server/lib/deck-templates/a-shared.ts +35 -0
  943. package/corpus/templates/slides-2/server/lib/deck-templates/a-update.ts +188 -0
  944. package/corpus/templates/slides-2/server/lib/deck-templates/a-workshop.ts +196 -0
  945. package/corpus/templates/slides-2/server/lib/deck-templates/b-keynote.ts +131 -0
  946. package/corpus/templates/slides-2/server/lib/deck-templates/b-lesson.ts +209 -0
  947. package/corpus/templates/slides-2/server/lib/deck-templates/b-manifesto.ts +149 -0
  948. package/corpus/templates/slides-2/server/lib/deck-templates/b-portfolio.ts +144 -0
  949. package/corpus/templates/slides-2/server/lib/deck-templates/b-research.ts +221 -0
  950. package/corpus/templates/slides-2/server/lib/deck-templates/b-roadmap.ts +264 -0
  951. package/corpus/templates/slides-2/server/lib/deck-templates/b-shared.ts +44 -0
  952. package/corpus/templates/slides-2/server/lib/deck-templates/group-a.ts +16 -0
  953. package/corpus/templates/slides-2/server/lib/deck-templates/group-b.ts +16 -0
  954. package/corpus/templates/slides-2/server/lib/deck-templates/legacy.ts +567 -0
  955. package/corpus/templates/slides-2/server/lib/deck-templates.ts +60 -0
  956. package/corpus/templates/slides-2/server/lib/deck-versions.ts +233 -0
  957. package/corpus/templates/slides-2/server/lib/design-system-workflows.ts +22 -0
  958. package/corpus/templates/slides-2/server/lib/emails.ts +84 -0
  959. package/corpus/templates/slides-2/server/lib/env-config.ts +15 -0
  960. package/corpus/templates/slides-2/server/lib/fetch-remote-image.ts +245 -0
  961. package/corpus/templates/slides-2/server/lib/generation-completion.ts +278 -0
  962. package/corpus/templates/slides-2/server/lib/google-docs-access.ts +51 -0
  963. package/corpus/templates/slides-2/server/lib/google-docs-error.ts +54 -0
  964. package/corpus/templates/slides-2/server/lib/google-docs-oauth.ts +422 -0
  965. package/corpus/templates/slides-2/server/lib/google-fonts.ts +509 -0
  966. package/corpus/templates/slides-2/server/lib/google-oauth-preflight.ts +119 -0
  967. package/corpus/templates/slides-2/server/lib/google-slides-export-availability.ts +55 -0
  968. package/corpus/templates/slides-2/server/lib/image-proxy-response.ts +16 -0
  969. package/corpus/templates/slides-2/server/lib/image-proxy-url.ts +155 -0
  970. package/corpus/templates/slides-2/server/lib/import-asset-storage.ts +214 -0
  971. package/corpus/templates/slides-2/server/lib/imported-asset-batch.ts +218 -0
  972. package/corpus/templates/slides-2/server/lib/legacy-slide-source.ts +692 -0
  973. package/corpus/templates/slides-2/server/lib/media-search-providers.ts +216 -0
  974. package/corpus/templates/slides-2/server/lib/native-creative-context.ts +295 -0
  975. package/corpus/templates/slides-2/server/lib/pdf-parse-setup.ts +115 -0
  976. package/corpus/templates/slides-2/server/lib/provider-api.ts +57 -0
  977. package/corpus/templates/slides-2/server/lib/request-wait-until.ts +30 -0
  978. package/corpus/templates/slides-2/server/lib/shared-deck.ts +29 -0
  979. package/corpus/templates/slides-2/server/lib/slide-asset-references.ts +309 -0
  980. package/corpus/templates/slides-2/server/lib/slide-content-patch.ts +1004 -0
  981. package/corpus/templates/slides-2/server/lib/slide-hygiene.ts +731 -0
  982. package/corpus/templates/slides-2/server/lib/slide-image-insertion.ts +136 -0
  983. package/corpus/templates/slides-2/server/lib/slide-reveal-issues.ts +41 -0
  984. package/corpus/templates/slides-2/server/lib/slide-source-editing/babel-walk.ts +117 -0
  985. package/corpus/templates/slides-2/server/lib/slide-source-editing/batch-edit.ts +152 -0
  986. package/corpus/templates/slides-2/server/lib/slide-source-editing/edit-ops.ts +1478 -0
  987. package/corpus/templates/slides-2/server/lib/slide-source-editing/image-target.ts +128 -0
  988. package/corpus/templates/slides-2/server/lib/slide-source-editing/text-diff.ts +23 -0
  989. package/corpus/templates/slides-2/server/lib/slide-source-limits.ts +9 -0
  990. package/corpus/templates/slides-2/server/lib/slide-source.ts +1786 -0
  991. package/corpus/templates/slides-2/server/lib/slide-theme-demo-sources.ts +2106 -0
  992. package/corpus/templates/slides-2/server/lib/slide-themes.ts +2174 -0
  993. package/corpus/templates/slides-2/server/lib/slide-worker-runtime.ts +1995 -0
  994. package/corpus/templates/slides-2/server/lib/slides-tracking.ts +73 -0
  995. package/corpus/templates/slides-2/server/lib/source-import.ts +213 -0
  996. package/corpus/templates/slides-2/server/lib/svgl.ts +364 -0
  997. package/corpus/templates/slides-2/server/lib/tenant-files.ts +179 -0
  998. package/corpus/templates/slides-2/server/lib/upload-storage-status.ts +9 -0
  999. package/corpus/templates/slides-2/server/lib/uploaded-asset-cleanup-outbox.ts +521 -0
  1000. package/corpus/templates/slides-2/server/lib/uploaded-asset-object-lifecycle.ts +404 -0
  1001. package/corpus/templates/slides-2/server/lib/uploaded-fonts.ts +696 -0
  1002. package/corpus/templates/slides-2/server/lib/uploaded-reference-storage.ts +77 -0
  1003. package/corpus/templates/slides-2/server/lib/validate-slide-animations.ts +341 -0
  1004. package/corpus/templates/slides-2/server/middleware/auth.ts +6 -0
  1005. package/corpus/templates/slides-2/server/plugins/_dev-upload-stub.ts +6 -0
  1006. package/corpus/templates/slides-2/server/plugins/agent-chat.ts +402 -0
  1007. package/corpus/templates/slides-2/server/plugins/auth.ts +31 -0
  1008. package/corpus/templates/slides-2/server/plugins/collab.ts +30 -0
  1009. package/corpus/templates/slides-2/server/plugins/config.ts +6 -0
  1010. package/corpus/templates/slides-2/server/plugins/core-routes.ts +30 -0
  1011. package/corpus/templates/slides-2/server/plugins/creative-context.ts +187 -0
  1012. package/corpus/templates/slides-2/server/plugins/db.ts +462 -0
  1013. package/corpus/templates/slides-2/server/plugins/feature-flags.ts +5 -0
  1014. package/corpus/templates/slides-2/server/plugins/labs.ts +8 -0
  1015. package/corpus/templates/slides-2/server/plugins/orphaned-asset-cleanup.ts +17 -0
  1016. package/corpus/templates/slides-2/server/plugins/otel.ts +6 -0
  1017. package/corpus/templates/slides-2/server/plugins/transactional-emails.ts +5 -0
  1018. package/corpus/templates/slides-2/server/register-secrets.ts +209 -0
  1019. package/corpus/templates/slides-2/server/routes/[...page].get.ts +5 -0
  1020. package/corpus/templates/slides-2/server/routes/[...page].head.ts +1 -0
  1021. package/corpus/templates/slides-2/server/routes/[base]/_agent-native/google-docs/auth-url.get.ts +1 -0
  1022. package/corpus/templates/slides-2/server/routes/[base]/_agent-native/google-docs/callback.get.ts +1 -0
  1023. package/corpus/templates/slides-2/server/routes/_agent-native/google-docs/auth-url.get.ts +1 -0
  1024. package/corpus/templates/slides-2/server/routes/_agent-native/google-docs/callback.get.ts +1 -0
  1025. package/corpus/templates/slides-2/server/routes/api/assets/upload-video.post.ts +1 -0
  1026. package/corpus/templates/slides-2/server/routes/api/assets/upload.post.ts +1 -0
  1027. package/corpus/templates/slides-2/server/routes/api/assets/video-uploads.delete.ts +1 -0
  1028. package/corpus/templates/slides-2/server/routes/api/deck-agent-context.json.get.ts +105 -0
  1029. package/corpus/templates/slides-2/server/routes/api/decks/events.get.ts +1 -0
  1030. package/corpus/templates/slides-2/server/routes/api/demo.get.ts +1 -0
  1031. package/corpus/templates/slides-2/server/routes/api/exports/[filename].get.ts +221 -0
  1032. package/corpus/templates/slides-2/server/routes/api/exports/google-slides.post.ts +240 -0
  1033. package/corpus/templates/slides-2/server/routes/api/exports/html.post.ts +67 -0
  1034. package/corpus/templates/slides-2/server/routes/api/exports/pptx.post.ts +100 -0
  1035. package/corpus/templates/slides-2/server/routes/api/gen-preview/[...].get.ts +24 -0
  1036. package/corpus/templates/slides-2/server/routes/api/generated/[...].get.ts +24 -0
  1037. package/corpus/templates/slides-2/server/routes/api/image-proxy.get.ts +518 -0
  1038. package/corpus/templates/slides-2/server/routes/api/import-assets/[token].get.ts +70 -0
  1039. package/corpus/templates/slides-2/server/routes/api/preview-images.get.ts +20 -0
  1040. package/corpus/templates/slides-2/server/routes/api/share/[token].get.ts +1 -0
  1041. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId]/chunk.post.ts +1 -0
  1042. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId]/status.get.ts +1 -0
  1043. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/[sessionId].delete.ts +1 -0
  1044. package/corpus/templates/slides-2/server/routes/api/uploads-chunked/start.post.ts +1 -0
  1045. package/corpus/templates/slides-2/server/routes/api/uploads.delete.ts +1 -0
  1046. package/corpus/templates/slides-2/server/routes/api/uploads.post.ts +1 -0
  1047. package/corpus/templates/slides-2/server/workspace-defaults.ts +196 -0
  1048. package/corpus/templates/slides-2/shared/action-ui.ts +147 -0
  1049. package/corpus/templates/slides-2/shared/agent-readable.ts +2 -0
  1050. package/corpus/templates/slides-2/shared/api.ts +406 -0
  1051. package/corpus/templates/slides-2/shared/app-state-tabs.ts +30 -0
  1052. package/corpus/templates/slides-2/shared/app-storage.ts +9 -0
  1053. package/corpus/templates/slides-2/shared/aspect-ratios.ts +49 -0
  1054. package/corpus/templates/slides-2/shared/blank-slide.ts +179 -0
  1055. package/corpus/templates/slides-2/shared/client-compatibility.ts +2 -0
  1056. package/corpus/templates/slides-2/shared/contrast-audit.ts +255 -0
  1057. package/corpus/templates/slides-2/shared/deck-access.ts +26 -0
  1058. package/corpus/templates/slides-2/shared/deck-app-scope.ts +14 -0
  1059. package/corpus/templates/slides-2/shared/deck-content.ts +37 -0
  1060. package/corpus/templates/slides-2/shared/deck-history-control.ts +79 -0
  1061. package/corpus/templates/slides-2/shared/deck-scope.ts +5 -0
  1062. package/corpus/templates/slides-2/shared/deck-title.ts +199 -0
  1063. package/corpus/templates/slides-2/shared/deck-write.ts +61 -0
  1064. package/corpus/templates/slides-2/shared/design-flags.ts +7 -0
  1065. package/corpus/templates/slides-2/shared/design-system-validation.ts +88 -0
  1066. package/corpus/templates/slides-2/shared/editor-control.ts +120 -0
  1067. package/corpus/templates/slides-2/shared/generation-timing.ts +19 -0
  1068. package/corpus/templates/slides-2/shared/google-docs.ts +69 -0
  1069. package/corpus/templates/slides-2/shared/labs.ts +10 -0
  1070. package/corpus/templates/slides-2/shared/overview-navigation.ts +34 -0
  1071. package/corpus/templates/slides-2/shared/ownership.ts +6 -0
  1072. package/corpus/templates/slides-2/shared/pdf-sidecar.ts +130 -0
  1073. package/corpus/templates/slides-2/shared/presenter-control.ts +111 -0
  1074. package/corpus/templates/slides-2/shared/representative-slide.ts +89 -0
  1075. package/corpus/templates/slides-2/shared/slide-background.ts +30 -0
  1076. package/corpus/templates/slides-2/shared/slide-comment-anchor.ts +117 -0
  1077. package/corpus/templates/slides-2/shared/slide-comment-reactions.ts +47 -0
  1078. package/corpus/templates/slides-2/shared/slide-design-tokens.ts +92 -0
  1079. package/corpus/templates/slides-2/shared/slide-fit.ts +78 -0
  1080. package/corpus/templates/slides-2/shared/slide-ids.ts +133 -0
  1081. package/corpus/templates/slides-2/shared/slide-number.ts +124 -0
  1082. package/corpus/templates/slides-2/shared/slide-object-structure.ts +790 -0
  1083. package/corpus/templates/slides-2/shared/slide-text-range-selection.ts +50 -0
  1084. package/corpus/templates/slides-2/shared/slide-transitions.ts +70 -0
  1085. package/corpus/templates/slides-2/shared/slides-2-transition.ts +194 -0
  1086. package/corpus/templates/slides-2/shared/slides-user-prefs.ts +22 -0
  1087. package/corpus/templates/slides-2/shared/source-import-errors.ts +12 -0
  1088. package/corpus/templates/slides-2/shared/source-runtime-export.ts +76 -0
  1089. package/corpus/templates/slides-2/shared/upload-types.ts +41 -0
  1090. package/corpus/templates/slides-2/ssr-entry.ts +11 -0
  1091. package/corpus/templates/slides-2/tsconfig.json +27 -0
  1092. package/corpus/templates/slides-2/vite.config.ts +77 -0
  1093. package/corpus/templates/slides-2/vitest.config.ts +28 -0
  1094. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +1 -1
  1095. package/package.json +1 -1
  1096. package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +0 -182
  1097. package/corpus/templates/design/actions/apply-shader-fill.ts +0 -450
  1098. package/corpus/templates/design/actions/apply-shader.ts +0 -257
  1099. package/corpus/templates/design/actions/preview-shader-fill.ts +0 -157
  1100. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +0 -1144
  1101. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +0 -450
  1102. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +0 -71
  1103. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +0 -555
  1104. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +0 -105
  1105. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +0 -433
  1106. package/corpus/templates/design/app/components/design/inspector/color-picker-shader-pane.tsx +0 -648
  1107. package/corpus/templates/design/shared/shader-fill.ts +0 -174
  1108. package/corpus/templates/design/shared/shader-presets.ts +0 -1523
@@ -0,0 +1,3217 @@
1
+ import path from "path";
2
+
3
+ import { deleteUploadedFile, uploadFile } from "@agent-native/core/file-upload";
4
+ import {
5
+ getRequestOrgId,
6
+ runWithRequestContext,
7
+ type RecurringSweepContext,
8
+ } from "@agent-native/core/server";
9
+ import { parseBase64DataUrl } from "@agent-native/core/shared";
10
+ import { and, eq, isNull } from "drizzle-orm";
11
+ import {
12
+ assertBodySize,
13
+ defineEventHandler,
14
+ getQuery,
15
+ setResponseStatus,
16
+ } from "h3";
17
+ import { nanoid } from "nanoid";
18
+
19
+ import { getDb, schema } from "../db/index.js";
20
+ import { readBoundedMultipartFormData } from "../lib/bounded-multipart.js";
21
+ import {
22
+ assertDeckAssetUploadScope,
23
+ parseOptionalDeckId,
24
+ registerDeckUploadedAsset,
25
+ } from "../lib/deck-asset-scope.js";
26
+ import { findDeckReferencingAsset } from "../lib/slide-asset-references.js";
27
+ import {
28
+ cancelUploadedAssetRegistration,
29
+ claimDueUploadedAssetCleanup,
30
+ claimFailedUploadedAssetRegistration,
31
+ clearRegisteredUploadedAsset,
32
+ completeUploadedAssetRegistration,
33
+ finishOwnedUploadedAssetCleanup,
34
+ listDueUploadedAssetCleanups,
35
+ listRegisteredUploadedAssetCleanups,
36
+ parkUnresolvedUploadedAssetRegistration,
37
+ recordUploadedAssetCleanupObject,
38
+ reserveUploadedAssetRegistration,
39
+ retryOwnedUploadedAssetCleanup,
40
+ scheduleUploadedAssetDeletion,
41
+ type UploadedAssetCleanupOutboxRow,
42
+ type UploadedAssetCleanupOutboxInput,
43
+ type UploadedAssetCleanupScope,
44
+ type SlidesTransaction,
45
+ } from "../lib/uploaded-asset-cleanup-outbox.js";
46
+ import {
47
+ deleteUploadedAssetObject,
48
+ registerUploadedAssetObjectIdentities,
49
+ type UploadedAssetObjectIdentity,
50
+ } from "../lib/uploaded-asset-object-lifecycle.js";
51
+ import {
52
+ resolveSlidesRequestAuth,
53
+ type SlidesRequestAuthContext,
54
+ } from "./request-auth-context.js";
55
+
56
+ type AuthedSlidesSession = SlidesRequestAuthContext & { email: string };
57
+
58
+ export const MAX_ASSET_FILE_SIZE = 10 * 1024 * 1024;
59
+ export const MAX_VIDEO_ASSET_FILE_SIZE = 50 * 1024 * 1024;
60
+ const MAX_MULTIPART_OVERHEAD_BYTES = 1024 * 1024;
61
+ const MAX_ORPHANED_ASSET_CLEANUPS_PER_UPLOAD = 5;
62
+ export const MAX_ASSET_REQUEST_SIZE =
63
+ MAX_ASSET_FILE_SIZE + MAX_MULTIPART_OVERHEAD_BYTES;
64
+ export const MAX_VIDEO_ASSET_REQUEST_SIZE =
65
+ MAX_VIDEO_ASSET_FILE_SIZE + MAX_MULTIPART_OVERHEAD_BYTES;
66
+
67
+ function multipartUploadError(
68
+ event: Parameters<typeof assertBodySize>[0],
69
+ error: unknown,
70
+ fallbackMessage: string,
71
+ operation?: "image" | "video",
72
+ ) {
73
+ const statusCode = (error as { statusCode?: unknown })?.statusCode;
74
+ const status =
75
+ typeof statusCode === "number" && statusCode >= 400 && statusCode < 600
76
+ ? statusCode
77
+ : 500;
78
+ setResponseStatus(
79
+ event as unknown as Parameters<typeof setResponseStatus>[0],
80
+ status,
81
+ );
82
+ if (operation === "image") {
83
+ const tooLarge = status === 413;
84
+ return {
85
+ error: tooLarge
86
+ ? "Image files must be 10 MB or smaller."
87
+ : "The upload request could not be read.",
88
+ errorCode: tooLarge
89
+ ? "image_upload_too_large"
90
+ : "image_upload_request_invalid",
91
+ };
92
+ }
93
+ if (operation === "video") {
94
+ const tooLarge = status === 413;
95
+ return {
96
+ error: tooLarge
97
+ ? "Video files must be 50 MB or smaller."
98
+ : "The video upload request could not be read.",
99
+ errorCode: tooLarge
100
+ ? "video_upload_too_large"
101
+ : "video_upload_request_invalid",
102
+ };
103
+ }
104
+ return {
105
+ error:
106
+ status === 413 && error instanceof Error
107
+ ? error.message
108
+ : fallbackMessage,
109
+ };
110
+ }
111
+
112
+ export interface UploadedAsset {
113
+ url: string;
114
+ filename: string;
115
+ type: string;
116
+ size: number;
117
+ provider?: string;
118
+ }
119
+
120
+ export interface RegisteredUploadedAsset extends UploadedAsset {
121
+ id: string;
122
+ }
123
+
124
+ export interface UploadedVideoAsset extends UploadedAsset {
125
+ id: string;
126
+ }
127
+
128
+ async function registerUploadedAsset(args: {
129
+ row: typeof schema.uploadedAssets.$inferInsert;
130
+ deckId?: string;
131
+ email: string;
132
+ orgId?: string | null;
133
+ cleanupToken: string;
134
+ cleanupInput: UploadedAssetCleanupOutboxInput;
135
+ }): Promise<void> {
136
+ const onRegistered = (transaction: SlidesTransaction) =>
137
+ completeUploadedAssetRegistration(
138
+ transaction,
139
+ args.cleanupInput,
140
+ args.cleanupToken,
141
+ );
142
+ if (args.deckId) {
143
+ await registerDeckUploadedAsset({
144
+ row: args.row,
145
+ deckId: args.deckId,
146
+ email: args.email,
147
+ orgId: args.orgId,
148
+ onRegistered,
149
+ });
150
+ return;
151
+ }
152
+ await getDb().transaction(async (transaction) => {
153
+ await registerUploadedAssetObjectIdentities(transaction, [
154
+ {
155
+ url: args.row.url,
156
+ provider: args.row.provider ?? null,
157
+ providerObjectId: args.row.providerObjectId ?? null,
158
+ },
159
+ ]);
160
+ await transaction.insert(schema.uploadedAssets).values(args.row);
161
+ await onRegistered(transaction);
162
+ });
163
+ }
164
+
165
+ async function deleteFailedUploadUsingReservation(args: {
166
+ input: {
167
+ assetId: string;
168
+ ownerEmail: string;
169
+ orgId: string | null;
170
+ provider: string;
171
+ providerObjectId: string | null;
172
+ url: string;
173
+ };
174
+ token: string;
175
+ requestContext: { userEmail: string; orgId?: string };
176
+ mediaType: "image" | "video";
177
+ }): Promise<boolean> {
178
+ const scope = {
179
+ assetId: args.input.assetId,
180
+ ownerEmail: args.input.ownerEmail,
181
+ orgId: args.input.orgId,
182
+ };
183
+ try {
184
+ const identity: UploadedAssetObjectIdentity = {
185
+ url: args.input.url,
186
+ provider: args.input.provider,
187
+ providerObjectId: args.input.providerObjectId,
188
+ };
189
+ const finishCleanup = async (transaction: SlidesTransaction) => {
190
+ const removed = await finishOwnedUploadedAssetCleanup(
191
+ transaction,
192
+ scope,
193
+ args.token,
194
+ );
195
+ if (!removed) {
196
+ throw new Error("Uploaded asset cleanup lease was no longer current.");
197
+ }
198
+ };
199
+ const result = await runWithRequestContext(args.requestContext, () =>
200
+ deleteUploadedAssetObject({
201
+ identity,
202
+ excludeAssetId: args.input.assetId,
203
+ lease: { ...scope, token: args.token },
204
+ deleteObject: () =>
205
+ deleteUploadedFile(args.input.provider, {
206
+ id: args.input.providerObjectId ?? undefined,
207
+ url: args.input.url,
208
+ }),
209
+ onShared: finishCleanup,
210
+ onAlreadyDeleted: finishCleanup,
211
+ onDeleted: finishCleanup,
212
+ }),
213
+ );
214
+ if (result.status === "stale") return false;
215
+ if (result.status === "failed") {
216
+ await runWithRequestContext(args.requestContext, () =>
217
+ retryOwnedUploadedAssetCleanup(scope, args.token, 1),
218
+ );
219
+ console.warn(
220
+ `[slides-upload] orphaned ${args.mediaType} remains queued`,
221
+ {
222
+ assetId: args.input.assetId,
223
+ },
224
+ );
225
+ return false;
226
+ }
227
+ return true;
228
+ } catch (error) {
229
+ try {
230
+ await runWithRequestContext(args.requestContext, () =>
231
+ retryOwnedUploadedAssetCleanup(scope, args.token, 1),
232
+ );
233
+ } catch (retryError) {
234
+ console.warn("[slides-upload] failed cleanup lease will expire", {
235
+ assetId: args.input.assetId,
236
+ error:
237
+ retryError instanceof Error ? retryError.message : String(retryError),
238
+ });
239
+ }
240
+ console.warn(
241
+ `[slides-upload] could not delete orphaned ${args.mediaType}`,
242
+ {
243
+ assetId: args.input.assetId,
244
+ error: error instanceof Error ? error.message : String(error),
245
+ },
246
+ );
247
+ return false;
248
+ }
249
+ }
250
+
251
+ async function requireSession(
252
+ event: Parameters<typeof resolveSlidesRequestAuth>[0],
253
+ ): Promise<{
254
+ session: AuthedSlidesSession | null;
255
+ error: string | null;
256
+ statusCode?: number;
257
+ }> {
258
+ const auth = await resolveSlidesRequestAuth(event);
259
+ if (!auth.ok) {
260
+ setResponseStatus(event, auth.statusCode);
261
+ return { session: null, error: auth.error, statusCode: auth.statusCode };
262
+ }
263
+ if (!auth.context.email) {
264
+ setResponseStatus(event, 401);
265
+ return { session: null, error: "Unauthorized", statusCode: 401 };
266
+ }
267
+ return { session: auth.context as AuthedSlidesSession, error: null };
268
+ }
269
+
270
+ const imageUploadFailures = {
271
+ asset_deck_scope_forbidden: {
272
+ statusCode: 403,
273
+ message: "You don't have permission to add images to this presentation.",
274
+ },
275
+ deck_not_found: {
276
+ statusCode: 404,
277
+ message: "This presentation could not be found.",
278
+ },
279
+ image_asset_registration_failed: {
280
+ statusCode: 500,
281
+ message: "The uploaded image couldn't be saved. Try again.",
282
+ },
283
+ image_upload_auth_required: {
284
+ statusCode: 401,
285
+ message: "Sign in to upload images.",
286
+ },
287
+ image_upload_failed: {
288
+ statusCode: 500,
289
+ message: "Image upload failed. Try again.",
290
+ },
291
+ image_upload_file_missing: {
292
+ statusCode: 400,
293
+ message: "Choose an image to upload.",
294
+ },
295
+ image_upload_invalid_image: {
296
+ statusCode: 400,
297
+ message: "This image file couldn't be validated.",
298
+ },
299
+ image_upload_request_invalid: {
300
+ statusCode: 400,
301
+ message: "The upload request could not be read.",
302
+ },
303
+ image_upload_scope_check_failed: {
304
+ statusCode: 500,
305
+ message: "Could not verify access to this presentation.",
306
+ },
307
+ image_upload_session_unavailable: {
308
+ statusCode: 503,
309
+ message: "The upload service is temporarily unavailable. Try again.",
310
+ },
311
+ image_upload_storage_failed: {
312
+ statusCode: 502,
313
+ message: "Image storage is temporarily unavailable. Try again.",
314
+ },
315
+ image_upload_storage_unavailable: {
316
+ statusCode: 503,
317
+ message: "Connect file storage before uploading images.",
318
+ },
319
+ image_upload_too_large: {
320
+ statusCode: 413,
321
+ message: "Image files must be 10 MB or smaller.",
322
+ },
323
+ image_upload_unsupported_type: {
324
+ statusCode: 400,
325
+ message: "This image file type isn't supported.",
326
+ },
327
+ image_upload_unsafe_svg: {
328
+ statusCode: 400,
329
+ message: "This SVG contains unsupported content.",
330
+ },
331
+ invalid_deck_data: {
332
+ statusCode: 500,
333
+ message: "This presentation's data is unavailable.",
334
+ },
335
+ invalid_deck_id: {
336
+ statusCode: 400,
337
+ message: "The presentation selection is invalid.",
338
+ },
339
+ } as const;
340
+
341
+ type ImageUploadFailureCode = keyof typeof imageUploadFailures;
342
+
343
+ function imageUploadFailure(code: ImageUploadFailureCode): Error & {
344
+ errorCode: ImageUploadFailureCode;
345
+ statusCode: number;
346
+ } {
347
+ const failure = imageUploadFailures[code];
348
+ return Object.assign(new Error(failure.message), {
349
+ errorCode: code,
350
+ statusCode: failure.statusCode,
351
+ });
352
+ }
353
+
354
+ function imageUploadFailureResponse(
355
+ event: Parameters<typeof setResponseStatus>[0],
356
+ error: unknown,
357
+ ): { error: string; errorCode: ImageUploadFailureCode } {
358
+ const candidate = (error as { errorCode?: unknown })?.errorCode;
359
+ const code =
360
+ typeof candidate === "string" &&
361
+ Object.hasOwn(imageUploadFailures, candidate)
362
+ ? (candidate as ImageUploadFailureCode)
363
+ : "image_upload_failed";
364
+ const failure = imageUploadFailures[code];
365
+ setResponseStatus(event, failure.statusCode);
366
+ return { error: failure.message, errorCode: code };
367
+ }
368
+
369
+ function isImageAssetExtension(ext: string): boolean {
370
+ return new Set([
371
+ ".jpg",
372
+ ".jpeg",
373
+ ".png",
374
+ ".gif",
375
+ ".webp",
376
+ ".avif",
377
+ ".ico",
378
+ ".svg",
379
+ ]).has(ext);
380
+ }
381
+
382
+ function ascii(data: Uint8Array, start: number, end: number): string {
383
+ return Buffer.from(data.subarray(start, end)).toString("ascii");
384
+ }
385
+
386
+ const W3C_SVG_11_DOCTYPE =
387
+ /^<!DOCTYPE\s+svg\s+PUBLIC\s+(["'])-\/\/W3C\/\/DTD SVG 1\.1\/\/EN\1\s+(["'])https?:\/\/www\.w3\.org\/Graphics\/SVG\/1\.1\/DTD\/svg11\.dtd\2\s*>$/i;
388
+
389
+ export function stripSafeSvgDoctype(data: Uint8Array): Uint8Array | null {
390
+ let source: string;
391
+ try {
392
+ source = new TextDecoder("utf-8", { fatal: true }).decode(data);
393
+ } catch {
394
+ // coercion-ok: malformed UTF-8 must reject SVG validation.
395
+ return null;
396
+ }
397
+
398
+ const declarations = [...source.matchAll(/<!DOCTYPE\b[^>]*>/gi)];
399
+ if (declarations.length === 0) return data;
400
+ if (
401
+ declarations.length !== 1 ||
402
+ !W3C_SVG_11_DOCTYPE.test(declarations[0][0])
403
+ ) {
404
+ return null;
405
+ }
406
+
407
+ return new TextEncoder().encode(source.replace(declarations[0][0], ""));
408
+ }
409
+
410
+ interface IsoBox {
411
+ type: string;
412
+ start: number;
413
+ payloadStart: number;
414
+ end: number;
415
+ }
416
+
417
+ interface EbmlElement {
418
+ id: number;
419
+ payloadStart: number;
420
+ end: number;
421
+ }
422
+
423
+ const MAX_MEDIA_CONTAINER_ELEMENTS = 100_000;
424
+ const MAX_MEDIA_SAMPLE_ENTRIES = 1_000_000;
425
+ const EBML_SEGMENT_ID = 0x18538067;
426
+ const EBML_CLUSTER_ID = 0x1f43b675;
427
+ const EBML_SEGMENT_LEVEL_IDS = new Set([
428
+ 0x114d9b74,
429
+ 0x1549a966,
430
+ 0x1654ae6b,
431
+ EBML_CLUSTER_ID,
432
+ 0x1c53bb6b,
433
+ 0x1941a469,
434
+ 0x1043a770,
435
+ 0x1254c367,
436
+ ]);
437
+ const EBML_NESTED_MASTER_IDS = new Set([
438
+ 0xa0, 0x5854, 0x75a1, 0xa6, 0x8e, 0xe8, 0xc8,
439
+ ]);
440
+
441
+ function uint32(data: Uint8Array, offset: number): number {
442
+ return (
443
+ data[offset]! * 0x1000000 +
444
+ (data[offset + 1]! << 16) +
445
+ (data[offset + 2]! << 8) +
446
+ data[offset + 3]!
447
+ );
448
+ }
449
+
450
+ function readIsoBoxes(
451
+ data: Uint8Array,
452
+ start: number,
453
+ end: number,
454
+ ): IsoBox[] | null {
455
+ const boxes: IsoBox[] = [];
456
+ let offset = start;
457
+ while (offset < end) {
458
+ if (boxes.length >= MAX_MEDIA_CONTAINER_ELEMENTS || end - offset < 8)
459
+ return null;
460
+ const size32 = uint32(data, offset);
461
+ const type = ascii(data, offset + 4, offset + 8);
462
+ let headerSize = 8;
463
+ let size = size32;
464
+ if (size32 === 1) {
465
+ if (end - offset < 16 || uint32(data, offset + 8) !== 0) return null;
466
+ size = uint32(data, offset + 12);
467
+ headerSize = 16;
468
+ } else if (size32 === 0) {
469
+ size = end - offset;
470
+ }
471
+ if (size < headerSize || size > end - offset) return null;
472
+ boxes.push({
473
+ type,
474
+ start: offset,
475
+ payloadStart: offset + headerSize,
476
+ end: offset + size,
477
+ });
478
+ offset += size;
479
+ }
480
+ return offset === end ? boxes : null;
481
+ }
482
+
483
+ function singleIsoBox(boxes: IsoBox[], type: string): IsoBox | null {
484
+ const matches = boxes.filter((box) => box.type === type);
485
+ return matches.length === 1 ? matches[0]! : null;
486
+ }
487
+
488
+ interface Mp4SampleSizes {
489
+ count: number;
490
+ at(index: number): number | null;
491
+ }
492
+
493
+ function readMp4SampleSizes(
494
+ data: Uint8Array,
495
+ boxes: IsoBox[],
496
+ ): Mp4SampleSizes | null {
497
+ const sizeBoxes = boxes.filter(
498
+ (box) => box.type === "stsz" || box.type === "stz2",
499
+ );
500
+ if (sizeBoxes.length !== 1) return null;
501
+ const box = sizeBoxes[0]!;
502
+ const payloadSize = box.end - box.payloadStart;
503
+ if (payloadSize < 12) return null;
504
+ const count = uint32(data, box.payloadStart + 8);
505
+ if (count === 0 || count > MAX_MEDIA_SAMPLE_ENTRIES) return null;
506
+
507
+ if (box.type === "stsz") {
508
+ const sampleSize = uint32(data, box.payloadStart + 4);
509
+ if (sampleSize > 0) {
510
+ return payloadSize === 12 ? { count, at: () => sampleSize } : null;
511
+ }
512
+ if (payloadSize !== 12 + count * 4) return null;
513
+ return {
514
+ count,
515
+ at: (index) =>
516
+ index >= 0 && index < count
517
+ ? uint32(data, box.payloadStart + 12 + index * 4)
518
+ : null,
519
+ };
520
+ }
521
+
522
+ const fieldSize = data[box.payloadStart + 7]!;
523
+ const tableStart = box.payloadStart + 12;
524
+ const tableSize = Math.ceil((count * fieldSize) / 8);
525
+ if (
526
+ (fieldSize !== 4 && fieldSize !== 8 && fieldSize !== 16) ||
527
+ payloadSize !== 12 + tableSize
528
+ )
529
+ return null;
530
+ return {
531
+ count,
532
+ at: (index) => {
533
+ if (index < 0 || index >= count) return null;
534
+ if (fieldSize === 4) {
535
+ const packed = data[tableStart + Math.floor(index / 2)]!;
536
+ return index % 2 === 0 ? packed >> 4 : packed & 0x0f;
537
+ }
538
+ if (fieldSize === 8) return data[tableStart + index]!;
539
+ const offset = tableStart + index * 2;
540
+ return (data[offset]! << 8) + data[offset + 1]!;
541
+ },
542
+ };
543
+ }
544
+
545
+ function readMp4ChunkOffsets(
546
+ data: Uint8Array,
547
+ boxes: IsoBox[],
548
+ ): number[] | null {
549
+ const offsetBoxes = boxes.filter(
550
+ (box) => box.type === "stco" || box.type === "co64",
551
+ );
552
+ if (offsetBoxes.length !== 1) return null;
553
+ const box = offsetBoxes[0]!;
554
+ const payloadSize = box.end - box.payloadStart;
555
+ if (payloadSize < 8) return null;
556
+ const count = uint32(data, box.payloadStart + 4);
557
+ const entrySize = box.type === "stco" ? 4 : 8;
558
+ if (
559
+ count === 0 ||
560
+ count > MAX_MEDIA_CONTAINER_ELEMENTS ||
561
+ payloadSize !== 8 + count * entrySize
562
+ )
563
+ return null;
564
+ const offsets: number[] = [];
565
+ for (let index = 0; index < count; index++) {
566
+ const offset = box.payloadStart + 8 + index * entrySize;
567
+ const value =
568
+ entrySize === 4
569
+ ? uint32(data, offset)
570
+ : uint32(data, offset) * 0x100000000 + uint32(data, offset + 4);
571
+ if (!Number.isSafeInteger(value)) return null;
572
+ offsets.push(value);
573
+ }
574
+ return offsets;
575
+ }
576
+
577
+ function isMediaDataRange(
578
+ offset: number,
579
+ size: number,
580
+ mediaDataBoxes: IsoBox[],
581
+ ): boolean {
582
+ const end = offset + size;
583
+ if (!Number.isSafeInteger(end)) return false;
584
+ let low = 0;
585
+ let high = mediaDataBoxes.length;
586
+ while (low < high) {
587
+ const middle = Math.floor((low + high) / 2);
588
+ if (mediaDataBoxes[middle]!.end <= offset) low = middle + 1;
589
+ else high = middle;
590
+ }
591
+ const box = mediaDataBoxes[low];
592
+ return Boolean(box && offset >= box.payloadStart && end <= box.end);
593
+ }
594
+
595
+ function readMp4TrackId(data: Uint8Array, trackHeader: IsoBox): number | null {
596
+ const version = data[trackHeader.payloadStart];
597
+ const offset = version === 0 ? 12 : version === 1 ? 20 : null;
598
+ const minimumSize = version === 0 ? 84 : version === 1 ? 96 : null;
599
+ if (
600
+ offset === null ||
601
+ minimumSize === null ||
602
+ trackHeader.end - trackHeader.payloadStart < minimumSize
603
+ )
604
+ return null;
605
+ const trackId = uint32(data, trackHeader.payloadStart + offset);
606
+ return trackId > 0 ? trackId : null;
607
+ }
608
+
609
+ function hasValidAvcConfiguration(
610
+ data: Uint8Array,
611
+ sampleDescription: IsoBox,
612
+ ): boolean {
613
+ if (sampleDescription.type !== "avc1" && sampleDescription.type !== "avc3")
614
+ return true;
615
+ const fixedSampleEntryEnd = sampleDescription.payloadStart + 78;
616
+ if (fixedSampleEntryEnd > sampleDescription.end) return false;
617
+ const childBoxes = readIsoBoxes(
618
+ data,
619
+ fixedSampleEntryEnd,
620
+ sampleDescription.end,
621
+ );
622
+ const configurations = childBoxes?.filter((box) => box.type === "avcC") ?? [];
623
+ if (configurations.length !== 1) return false;
624
+ const configuration = configurations[0]!;
625
+ let offset = configuration.payloadStart;
626
+ if (
627
+ configuration.end - offset < 7 ||
628
+ data[offset] !== 1 ||
629
+ (data[offset + 4]! & 0xfc) !== 0xfc ||
630
+ (data[offset + 4]! & 0x03) === 0x02 ||
631
+ (data[offset + 5]! & 0xe0) !== 0xe0
632
+ )
633
+ return false;
634
+
635
+ const sequenceParameterSetCount = data[offset + 5]! & 0x1f;
636
+ if (sequenceParameterSetCount === 0) return false;
637
+ offset += 6;
638
+ for (let index = 0; index < sequenceParameterSetCount; index++) {
639
+ if (offset + 2 > configuration.end) return false;
640
+ const length = (data[offset]! << 8) | data[offset + 1]!;
641
+ offset += 2;
642
+ if (
643
+ length === 0 ||
644
+ offset + length > configuration.end ||
645
+ (data[offset]! & 0x1f) !== 7
646
+ )
647
+ return false;
648
+ offset += length;
649
+ }
650
+ if (offset >= configuration.end) return false;
651
+ const pictureParameterSetCount = data[offset++]!;
652
+ if (pictureParameterSetCount === 0) return false;
653
+ for (let index = 0; index < pictureParameterSetCount; index++) {
654
+ if (offset + 2 > configuration.end) return false;
655
+ const length = (data[offset]! << 8) | data[offset + 1]!;
656
+ offset += 2;
657
+ if (
658
+ length === 0 ||
659
+ offset + length > configuration.end ||
660
+ (data[offset]! & 0x1f) !== 8
661
+ )
662
+ return false;
663
+ offset += length;
664
+ }
665
+ if (offset === configuration.end) return true;
666
+
667
+ if (
668
+ configuration.end - offset < 4 ||
669
+ (data[offset]! & 0xfc) !== 0xfc ||
670
+ (data[offset + 1]! & 0xf8) !== 0xf8 ||
671
+ (data[offset + 2]! & 0xf8) !== 0xf8
672
+ )
673
+ return false;
674
+ const sequenceParameterSetExtCount = data[offset + 3]!;
675
+ offset += 4;
676
+ for (let index = 0; index < sequenceParameterSetExtCount; index++) {
677
+ if (offset + 2 > configuration.end) return false;
678
+ const length = (data[offset]! << 8) | data[offset + 1]!;
679
+ offset += 2;
680
+ if (
681
+ length === 0 ||
682
+ offset + length > configuration.end ||
683
+ (data[offset]! & 0x1f) !== 13
684
+ )
685
+ return false;
686
+ offset += length;
687
+ }
688
+ return offset === configuration.end;
689
+ }
690
+
691
+ interface Mp4TrackMetadata {
692
+ trackId: number;
693
+ isVideo: boolean;
694
+ sampleDescriptionCount: number;
695
+ sampleTableBoxes: IsoBox[];
696
+ }
697
+
698
+ function readMp4TrackMetadata(
699
+ data: Uint8Array,
700
+ track: IsoBox,
701
+ ): Mp4TrackMetadata | null {
702
+ const trackBoxes = readIsoBoxes(data, track.payloadStart, track.end);
703
+ if (!trackBoxes) return null;
704
+ const trackHeader = singleIsoBox(trackBoxes, "tkhd");
705
+ const media = singleIsoBox(trackBoxes, "mdia");
706
+ if (!trackHeader || !media) return null;
707
+ if (fullBoxFlags(data, trackHeader) === null) return null;
708
+ const trackId = readMp4TrackId(data, trackHeader);
709
+ if (!trackId) return null;
710
+
711
+ const mediaBoxes = readIsoBoxes(data, media.payloadStart, media.end);
712
+ if (!mediaBoxes) return null;
713
+ const mediaHeader = singleIsoBox(mediaBoxes, "mdhd");
714
+ const handler = singleIsoBox(mediaBoxes, "hdlr");
715
+ const mediaInfo = singleIsoBox(mediaBoxes, "minf");
716
+ if (
717
+ !mediaHeader ||
718
+ (data[mediaHeader.payloadStart] === 0 &&
719
+ mediaHeader.end - mediaHeader.payloadStart < 24) ||
720
+ (data[mediaHeader.payloadStart] === 1 &&
721
+ mediaHeader.end - mediaHeader.payloadStart < 36) ||
722
+ (data[mediaHeader.payloadStart] !== 0 &&
723
+ data[mediaHeader.payloadStart] !== 1) ||
724
+ fullBoxFlags(data, mediaHeader) === null ||
725
+ !handler ||
726
+ handler.end - handler.payloadStart < 24 ||
727
+ !mediaInfo
728
+ )
729
+ return null;
730
+ const isVideo =
731
+ ascii(data, handler.payloadStart + 8, handler.payloadStart + 12) === "vide";
732
+
733
+ const mediaInfoBoxes = readIsoBoxes(
734
+ data,
735
+ mediaInfo.payloadStart,
736
+ mediaInfo.end,
737
+ );
738
+ const sampleTable = mediaInfoBoxes
739
+ ? singleIsoBox(mediaInfoBoxes, "stbl")
740
+ : null;
741
+ if (!mediaInfoBoxes || !sampleTable) return null;
742
+ const sampleTableBoxes = readIsoBoxes(
743
+ data,
744
+ sampleTable.payloadStart,
745
+ sampleTable.end,
746
+ );
747
+ const sampleDescription = sampleTableBoxes
748
+ ? singleIsoBox(sampleTableBoxes, "stsd")
749
+ : null;
750
+ if (
751
+ !sampleTableBoxes ||
752
+ !sampleDescription ||
753
+ sampleDescription.end - sampleDescription.payloadStart < 8 ||
754
+ data[sampleDescription.payloadStart] !== 0 ||
755
+ fullBoxFlags(data, sampleDescription) !== 0
756
+ )
757
+ return null;
758
+ const sampleDescriptionCount = uint32(
759
+ data,
760
+ sampleDescription.payloadStart + 4,
761
+ );
762
+ if (
763
+ sampleDescriptionCount === 0 ||
764
+ sampleDescriptionCount > MAX_MEDIA_CONTAINER_ELEMENTS
765
+ )
766
+ return null;
767
+ const sampleDescriptions = readIsoBoxes(
768
+ data,
769
+ sampleDescription.payloadStart + 8,
770
+ sampleDescription.end,
771
+ );
772
+ if (
773
+ !sampleDescriptions ||
774
+ sampleDescriptions.length !== sampleDescriptionCount ||
775
+ sampleDescriptions.some(
776
+ (entry) =>
777
+ entry.end - entry.start < (isVideo ? 86 : 8) ||
778
+ (isVideo && !hasValidAvcConfiguration(data, entry)),
779
+ )
780
+ )
781
+ return null;
782
+
783
+ return { trackId, isVideo, sampleDescriptionCount, sampleTableBoxes };
784
+ }
785
+
786
+ function hasEmptyMp4SampleTables(data: Uint8Array, boxes: IsoBox[]): boolean {
787
+ const timing = singleIsoBox(boxes, "stts");
788
+ const sampleToChunk = singleIsoBox(boxes, "stsc");
789
+ const sizeBoxes = boxes.filter(
790
+ (box) => box.type === "stsz" || box.type === "stz2",
791
+ );
792
+ const offsetBoxes = boxes.filter(
793
+ (box) => box.type === "stco" || box.type === "co64",
794
+ );
795
+ if (
796
+ !timing ||
797
+ !sampleToChunk ||
798
+ sizeBoxes.length !== 1 ||
799
+ offsetBoxes.length !== 1
800
+ )
801
+ return false;
802
+
803
+ const timingSize = timing.end - timing.payloadStart;
804
+ const mappingSize = sampleToChunk.end - sampleToChunk.payloadStart;
805
+ if (
806
+ timingSize !== 8 ||
807
+ mappingSize !== 8 ||
808
+ data[timing.payloadStart] !== 0 ||
809
+ data[sampleToChunk.payloadStart] !== 0 ||
810
+ fullBoxFlags(data, timing) !== 0 ||
811
+ fullBoxFlags(data, sampleToChunk) !== 0 ||
812
+ uint32(data, timing.payloadStart + 4) !== 0 ||
813
+ uint32(data, sampleToChunk.payloadStart + 4) !== 0
814
+ )
815
+ return false;
816
+
817
+ const sampleSizes = sizeBoxes[0]!;
818
+ const sizePayload = sampleSizes.end - sampleSizes.payloadStart;
819
+ if (
820
+ sizePayload !== 12 ||
821
+ data[sampleSizes.payloadStart] !== 0 ||
822
+ fullBoxFlags(data, sampleSizes) !== 0 ||
823
+ uint32(data, sampleSizes.payloadStart + 8) !== 0
824
+ )
825
+ return false;
826
+ if (sampleSizes.type === "stsz") {
827
+ if (uint32(data, sampleSizes.payloadStart + 4) !== 0) return false;
828
+ } else {
829
+ const fieldSize = data[sampleSizes.payloadStart + 7]!;
830
+ if (fieldSize !== 4 && fieldSize !== 8 && fieldSize !== 16) return false;
831
+ }
832
+
833
+ const offsets = offsetBoxes[0]!;
834
+ const offsetPayload = offsets.end - offsets.payloadStart;
835
+ return (
836
+ offsetPayload === 8 &&
837
+ data[offsets.payloadStart] === 0 &&
838
+ fullBoxFlags(data, offsets) === 0 &&
839
+ uint32(data, offsets.payloadStart + 4) === 0
840
+ );
841
+ }
842
+
843
+ function hasMp4PlayableSamples(
844
+ data: Uint8Array,
845
+ boxes: IsoBox[],
846
+ sampleDescriptionCount: number,
847
+ mediaDataBoxes: IsoBox[],
848
+ ): boolean {
849
+ const sampleSizes = readMp4SampleSizes(data, boxes);
850
+ const timing = singleIsoBox(boxes, "stts");
851
+ const sampleToChunk = singleIsoBox(boxes, "stsc");
852
+ const chunkOffsets = readMp4ChunkOffsets(data, boxes);
853
+ if (!sampleSizes || !timing || !sampleToChunk || !chunkOffsets) return false;
854
+
855
+ const timingPayloadSize = timing.end - timing.payloadStart;
856
+ if (timingPayloadSize < 8) return false;
857
+ const timingEntryCount = uint32(data, timing.payloadStart + 4);
858
+ if (
859
+ timingEntryCount === 0 ||
860
+ timingEntryCount > MAX_MEDIA_CONTAINER_ELEMENTS ||
861
+ timingPayloadSize !== 8 + timingEntryCount * 8
862
+ )
863
+ return false;
864
+ let timedSampleCount = 0;
865
+ for (let index = 0; index < timingEntryCount; index++) {
866
+ const entryOffset = timing.payloadStart + 8 + index * 8;
867
+ const count = uint32(data, entryOffset);
868
+ const delta = uint32(data, entryOffset + 4);
869
+ if (count === 0 || delta === 0) return false;
870
+ timedSampleCount += count;
871
+ if (
872
+ !Number.isSafeInteger(timedSampleCount) ||
873
+ timedSampleCount > sampleSizes.count
874
+ )
875
+ return false;
876
+ }
877
+ if (timedSampleCount !== sampleSizes.count) return false;
878
+
879
+ const mappingPayloadSize = sampleToChunk.end - sampleToChunk.payloadStart;
880
+ if (mappingPayloadSize < 8) return false;
881
+ const mappingEntryCount = uint32(data, sampleToChunk.payloadStart + 4);
882
+ if (
883
+ mappingEntryCount === 0 ||
884
+ mappingEntryCount > MAX_MEDIA_CONTAINER_ELEMENTS ||
885
+ mappingPayloadSize !== 8 + mappingEntryCount * 12
886
+ )
887
+ return false;
888
+ const mappings: Array<{
889
+ firstChunk: number;
890
+ samplesPerChunk: number;
891
+ }> = [];
892
+ for (let index = 0; index < mappingEntryCount; index++) {
893
+ const entryOffset = sampleToChunk.payloadStart + 8 + index * 12;
894
+ const firstChunk = uint32(data, entryOffset);
895
+ const samplesPerChunk = uint32(data, entryOffset + 4);
896
+ const descriptionIndex = uint32(data, entryOffset + 8);
897
+ if (
898
+ firstChunk === 0 ||
899
+ (index === 0
900
+ ? firstChunk !== 1
901
+ : firstChunk <= mappings[index - 1]!.firstChunk) ||
902
+ firstChunk > chunkOffsets.length ||
903
+ samplesPerChunk === 0 ||
904
+ descriptionIndex === 0 ||
905
+ descriptionIndex > sampleDescriptionCount
906
+ )
907
+ return false;
908
+ mappings.push({ firstChunk, samplesPerChunk });
909
+ }
910
+
911
+ let sampleIndex = 0;
912
+ let mappingIndex = 0;
913
+ for (let chunkIndex = 1; chunkIndex <= chunkOffsets.length; chunkIndex++) {
914
+ while (
915
+ mappingIndex + 1 < mappings.length &&
916
+ mappings[mappingIndex + 1]!.firstChunk <= chunkIndex
917
+ ) {
918
+ mappingIndex++;
919
+ }
920
+ const mapping = mappings[mappingIndex]!;
921
+ const nextSampleIndex = sampleIndex + mapping.samplesPerChunk;
922
+ if (nextSampleIndex > sampleSizes.count) return false;
923
+ let chunkSize = 0;
924
+ for (; sampleIndex < nextSampleIndex; sampleIndex++) {
925
+ const sampleSize = sampleSizes.at(sampleIndex);
926
+ if (sampleSize === null || sampleSize === 0) return false;
927
+ chunkSize += sampleSize;
928
+ if (!Number.isSafeInteger(chunkSize)) return false;
929
+ }
930
+ const chunkOffset = chunkOffsets[chunkIndex - 1]!;
931
+ if (!isMediaDataRange(chunkOffset, chunkSize, mediaDataBoxes)) return false;
932
+ }
933
+ return sampleIndex === sampleSizes.count;
934
+ }
935
+
936
+ function hasMp4VideoTrack(
937
+ data: Uint8Array,
938
+ moov: IsoBox,
939
+ mediaDataBoxes: IsoBox[],
940
+ ): boolean {
941
+ const movieBoxes = readIsoBoxes(data, moov.payloadStart, moov.end);
942
+ if (!movieBoxes) return false;
943
+ const movieHeader = movieBoxes.find((box) => box.type === "mvhd");
944
+ if (!movieHeader || movieHeader.end - movieHeader.payloadStart < 20)
945
+ return false;
946
+
947
+ return movieBoxes.some((track) => {
948
+ if (track.type !== "trak") return false;
949
+ const trackBoxes = readIsoBoxes(data, track.payloadStart, track.end);
950
+ if (!trackBoxes) return false;
951
+ const trackHeader = trackBoxes.find((box) => box.type === "tkhd");
952
+ const media = trackBoxes.find((box) => box.type === "mdia");
953
+ if (
954
+ !trackHeader ||
955
+ trackHeader.end - trackHeader.payloadStart < 24 ||
956
+ !media
957
+ )
958
+ return false;
959
+
960
+ const mediaBoxes = readIsoBoxes(data, media.payloadStart, media.end);
961
+ if (!mediaBoxes) return false;
962
+ const mediaHeader = mediaBoxes.find((box) => box.type === "mdhd");
963
+ const handler = mediaBoxes.find((box) => box.type === "hdlr");
964
+ const mediaInfo = mediaBoxes.find((box) => box.type === "minf");
965
+ if (
966
+ !mediaHeader ||
967
+ mediaHeader.end - mediaHeader.payloadStart < 20 ||
968
+ !handler ||
969
+ handler.end - handler.payloadStart < 12 ||
970
+ ascii(data, handler.payloadStart + 8, handler.payloadStart + 12) !==
971
+ "vide" ||
972
+ !mediaInfo
973
+ )
974
+ return false;
975
+
976
+ const mediaInfoBoxes = readIsoBoxes(
977
+ data,
978
+ mediaInfo.payloadStart,
979
+ mediaInfo.end,
980
+ );
981
+ const sampleTable = mediaInfoBoxes?.find((box) => box.type === "stbl");
982
+ if (!sampleTable) return false;
983
+ const sampleTableBoxes = readIsoBoxes(
984
+ data,
985
+ sampleTable.payloadStart,
986
+ sampleTable.end,
987
+ );
988
+ const sampleDescription = sampleTableBoxes
989
+ ? singleIsoBox(sampleTableBoxes, "stsd")
990
+ : null;
991
+ if (
992
+ !sampleDescription ||
993
+ sampleDescription.end - sampleDescription.payloadStart < 8
994
+ )
995
+ return false;
996
+ const entryCount = uint32(data, sampleDescription.payloadStart + 4);
997
+ if (entryCount === 0 || entryCount > MAX_MEDIA_CONTAINER_ELEMENTS)
998
+ return false;
999
+ const entries = readIsoBoxes(
1000
+ data,
1001
+ sampleDescription.payloadStart + 8,
1002
+ sampleDescription.end,
1003
+ );
1004
+ return Boolean(
1005
+ entries?.length === entryCount &&
1006
+ entries.every(
1007
+ (entry) =>
1008
+ entry.end - entry.start >= 86 &&
1009
+ hasValidAvcConfiguration(data, entry),
1010
+ ) &&
1011
+ sampleTableBoxes &&
1012
+ hasMp4PlayableSamples(data, sampleTableBoxes, entryCount, mediaDataBoxes),
1013
+ );
1014
+ });
1015
+ }
1016
+
1017
+ function hasValidMp4Video(data: Uint8Array): boolean {
1018
+ if (data.length < 16) return false;
1019
+ const boxes = readIsoBoxes(data, 0, data.length);
1020
+ if (!boxes) return false;
1021
+ const fileType = boxes.find((box) => box.type === "ftyp");
1022
+ const movie = boxes.find((box) => box.type === "moov");
1023
+ const mediaDataBoxes = boxes.filter(
1024
+ (box) => box.type === "mdat" && box.end > box.payloadStart,
1025
+ );
1026
+ if (
1027
+ !fileType ||
1028
+ fileType.end - fileType.start < 16 ||
1029
+ (fileType.end - fileType.start - 16) % 4 !== 0 ||
1030
+ !movie ||
1031
+ mediaDataBoxes.length === 0
1032
+ )
1033
+ return false;
1034
+ if (boxes.some((box) => box.type === "moof")) {
1035
+ return hasValidFragmentedMp4Video(data, boxes, movie, mediaDataBoxes);
1036
+ }
1037
+ return hasMp4VideoTrack(data, movie, mediaDataBoxes);
1038
+ }
1039
+
1040
+ interface Mp4TrexDefaults {
1041
+ sampleDescriptionIndex: number;
1042
+ sampleDuration: number;
1043
+ sampleSize: number;
1044
+ }
1045
+
1046
+ interface Mp4Tfhd {
1047
+ trackId: number;
1048
+ baseDataOffset: number | null;
1049
+ sampleDescriptionIndex: number;
1050
+ defaultSampleDuration: number;
1051
+ defaultSampleSize: number;
1052
+ durationIsEmpty: boolean;
1053
+ }
1054
+
1055
+ interface Mp4Trun {
1056
+ sampleCount: number;
1057
+ dataOffset: number | null;
1058
+ sampleBytes: number;
1059
+ }
1060
+
1061
+ function fullBoxFlags(data: Uint8Array, box: IsoBox): number | null {
1062
+ if (box.end - box.payloadStart < 4) return null;
1063
+ return (
1064
+ (data[box.payloadStart + 1]! << 16) |
1065
+ (data[box.payloadStart + 2]! << 8) |
1066
+ data[box.payloadStart + 3]!
1067
+ );
1068
+ }
1069
+
1070
+ function readMp4Uint64(data: Uint8Array, offset: number): number | null {
1071
+ const value = uint32(data, offset) * 0x100000000 + uint32(data, offset + 4);
1072
+ return Number.isSafeInteger(value) ? value : null;
1073
+ }
1074
+
1075
+ function readMp4TrexDefaults(
1076
+ data: Uint8Array,
1077
+ movieExtends: IsoBox,
1078
+ tracks: Map<number, Mp4TrackMetadata>,
1079
+ ): Map<number, Mp4TrexDefaults> | null {
1080
+ const boxes = readIsoBoxes(data, movieExtends.payloadStart, movieExtends.end);
1081
+ if (!boxes) return null;
1082
+ const trexBoxes = boxes.filter((box) => box.type === "trex");
1083
+ if (trexBoxes.length !== tracks.size) return null;
1084
+ const defaults = new Map<number, Mp4TrexDefaults>();
1085
+ for (const box of trexBoxes) {
1086
+ if (
1087
+ box.end - box.payloadStart !== 24 ||
1088
+ data[box.payloadStart] !== 0 ||
1089
+ fullBoxFlags(data, box) !== 0
1090
+ )
1091
+ return null;
1092
+ const trackId = uint32(data, box.payloadStart + 4);
1093
+ const sampleDescriptionIndex = uint32(data, box.payloadStart + 8);
1094
+ const track = tracks.get(trackId);
1095
+ if (
1096
+ !track ||
1097
+ defaults.has(trackId) ||
1098
+ sampleDescriptionIndex === 0 ||
1099
+ sampleDescriptionIndex > track.sampleDescriptionCount
1100
+ )
1101
+ return null;
1102
+ defaults.set(trackId, {
1103
+ sampleDescriptionIndex,
1104
+ sampleDuration: uint32(data, box.payloadStart + 12),
1105
+ sampleSize: uint32(data, box.payloadStart + 16),
1106
+ });
1107
+ }
1108
+ return defaults.size === tracks.size ? defaults : null;
1109
+ }
1110
+
1111
+ function readMp4Tfhd(
1112
+ data: Uint8Array,
1113
+ box: IsoBox,
1114
+ moofStart: number,
1115
+ defaults: Mp4TrexDefaults,
1116
+ track: Mp4TrackMetadata,
1117
+ ): Mp4Tfhd | null {
1118
+ const payloadSize = box.end - box.payloadStart;
1119
+ const flags = fullBoxFlags(data, box);
1120
+ if (payloadSize < 8 || data[box.payloadStart] !== 0 || flags === null)
1121
+ return null;
1122
+ const allowedFlags =
1123
+ 0x000001 | 0x000002 | 0x000008 | 0x000010 | 0x000020 | 0x010000 | 0x020000;
1124
+ if (
1125
+ (flags & ~allowedFlags) !== 0 ||
1126
+ ((flags & 0x000001) !== 0 && (flags & 0x020000) !== 0)
1127
+ )
1128
+ return null;
1129
+
1130
+ const trackId = uint32(data, box.payloadStart + 4);
1131
+ if (trackId !== track.trackId) return null;
1132
+ let offset = box.payloadStart + 8;
1133
+ let baseDataOffset: number | null = null;
1134
+ let sampleDescriptionIndex = defaults.sampleDescriptionIndex;
1135
+ let defaultSampleDuration = defaults.sampleDuration;
1136
+ let defaultSampleSize = defaults.sampleSize;
1137
+ if ((flags & 0x000001) !== 0) {
1138
+ if (offset + 8 > box.end) return null;
1139
+ baseDataOffset = readMp4Uint64(data, offset);
1140
+ if (baseDataOffset === null) return null;
1141
+ offset += 8;
1142
+ } else if ((flags & 0x020000) !== 0) {
1143
+ baseDataOffset = moofStart;
1144
+ }
1145
+ if ((flags & 0x000002) !== 0) {
1146
+ if (offset + 4 > box.end) return null;
1147
+ sampleDescriptionIndex = uint32(data, offset);
1148
+ offset += 4;
1149
+ }
1150
+ if ((flags & 0x000008) !== 0) {
1151
+ if (offset + 4 > box.end) return null;
1152
+ defaultSampleDuration = uint32(data, offset);
1153
+ offset += 4;
1154
+ }
1155
+ if ((flags & 0x000010) !== 0) {
1156
+ if (offset + 4 > box.end) return null;
1157
+ defaultSampleSize = uint32(data, offset);
1158
+ offset += 4;
1159
+ }
1160
+ if ((flags & 0x000020) !== 0) {
1161
+ if (offset + 4 > box.end) return null;
1162
+ offset += 4;
1163
+ }
1164
+ if (
1165
+ offset !== box.end ||
1166
+ sampleDescriptionIndex === 0 ||
1167
+ sampleDescriptionIndex > track.sampleDescriptionCount
1168
+ )
1169
+ return null;
1170
+ return {
1171
+ trackId,
1172
+ baseDataOffset,
1173
+ sampleDescriptionIndex,
1174
+ defaultSampleDuration,
1175
+ defaultSampleSize,
1176
+ durationIsEmpty: (flags & 0x010000) !== 0,
1177
+ };
1178
+ }
1179
+
1180
+ function readMp4Trun(
1181
+ data: Uint8Array,
1182
+ box: IsoBox,
1183
+ tfhd: Mp4Tfhd,
1184
+ maxSampleCount: number,
1185
+ ): Mp4Trun | null {
1186
+ const payloadSize = box.end - box.payloadStart;
1187
+ const version = data[box.payloadStart];
1188
+ const flags = fullBoxFlags(data, box);
1189
+ if (payloadSize < 8 || (version !== 0 && version !== 1) || flags === null)
1190
+ return null;
1191
+ const allowedFlags =
1192
+ 0x000001 | 0x000004 | 0x000100 | 0x000200 | 0x000400 | 0x000800;
1193
+ if (
1194
+ (flags & ~allowedFlags) !== 0 ||
1195
+ ((flags & 0x000004) !== 0 && (flags & 0x000400) !== 0)
1196
+ )
1197
+ return null;
1198
+
1199
+ const sampleCount = uint32(data, box.payloadStart + 4);
1200
+ if (sampleCount > maxSampleCount) return null;
1201
+ let offset = box.payloadStart + 8;
1202
+ let dataOffset: number | null = null;
1203
+ if ((flags & 0x000001) !== 0) {
1204
+ if (offset + 4 > box.end) return null;
1205
+ const unsignedOffset = uint32(data, offset);
1206
+ dataOffset =
1207
+ unsignedOffset >= 0x80000000
1208
+ ? unsignedOffset - 0x100000000
1209
+ : unsignedOffset;
1210
+ offset += 4;
1211
+ }
1212
+ if ((flags & 0x000004) !== 0) {
1213
+ if (offset + 4 > box.end) return null;
1214
+ offset += 4;
1215
+ }
1216
+
1217
+ const hasDuration = (flags & 0x000100) !== 0;
1218
+ const hasSize = (flags & 0x000200) !== 0;
1219
+ const hasSampleFlags = (flags & 0x000400) !== 0;
1220
+ const hasCompositionOffset = (flags & 0x000800) !== 0;
1221
+ const perSampleWidth =
1222
+ (hasDuration ? 4 : 0) +
1223
+ (hasSize ? 4 : 0) +
1224
+ (hasSampleFlags ? 4 : 0) +
1225
+ (hasCompositionOffset ? 4 : 0);
1226
+ if (offset + sampleCount * perSampleWidth !== box.end) return null;
1227
+
1228
+ let sampleBytes = 0;
1229
+ for (let index = 0; index < sampleCount; index++) {
1230
+ const duration = hasDuration
1231
+ ? uint32(data, offset)
1232
+ : tfhd.defaultSampleDuration;
1233
+ if (hasDuration) offset += 4;
1234
+ const size = hasSize ? uint32(data, offset) : tfhd.defaultSampleSize;
1235
+ if (hasSize) offset += 4;
1236
+ if (duration === 0 || size === 0) return null;
1237
+ sampleBytes += size;
1238
+ if (!Number.isSafeInteger(sampleBytes)) return null;
1239
+ if (hasSampleFlags) offset += 4;
1240
+ if (hasCompositionOffset) offset += 4;
1241
+ }
1242
+ return { sampleCount, dataOffset, sampleBytes };
1243
+ }
1244
+
1245
+ function hasValidMp4Tfdt(data: Uint8Array, box: IsoBox): boolean {
1246
+ const payloadSize = box.end - box.payloadStart;
1247
+ const version = data[box.payloadStart];
1248
+ if (fullBoxFlags(data, box) !== 0) return false;
1249
+ if (version === 0) return payloadSize === 8;
1250
+ if (version === 1)
1251
+ return (
1252
+ payloadSize === 12 && readMp4Uint64(data, box.payloadStart + 4) !== null
1253
+ );
1254
+ return false;
1255
+ }
1256
+
1257
+ function hasValidFragmentedMp4Video(
1258
+ data: Uint8Array,
1259
+ topLevelBoxes: IsoBox[],
1260
+ movie: IsoBox,
1261
+ mediaDataBoxes: IsoBox[],
1262
+ ): boolean {
1263
+ const movieBoxes = readIsoBoxes(data, movie.payloadStart, movie.end);
1264
+ const movieHeader = movieBoxes ? singleIsoBox(movieBoxes, "mvhd") : null;
1265
+ if (!movieBoxes || !movieHeader) return false;
1266
+ const movieHeaderVersion = data[movieHeader.payloadStart];
1267
+ if (
1268
+ (movieHeaderVersion === 0 &&
1269
+ movieHeader.end - movieHeader.payloadStart < 100) ||
1270
+ (movieHeaderVersion === 1 &&
1271
+ movieHeader.end - movieHeader.payloadStart < 112) ||
1272
+ (movieHeaderVersion !== 0 && movieHeaderVersion !== 1) ||
1273
+ fullBoxFlags(data, movieHeader) === null
1274
+ )
1275
+ return false;
1276
+ const movieExtends = singleIsoBox(movieBoxes, "mvex");
1277
+ if (!movieExtends) return false;
1278
+
1279
+ const tracks = new Map<number, Mp4TrackMetadata>();
1280
+ for (const trackBox of movieBoxes.filter((box) => box.type === "trak")) {
1281
+ const track = readMp4TrackMetadata(data, trackBox);
1282
+ if (
1283
+ !track ||
1284
+ tracks.has(track.trackId) ||
1285
+ !hasEmptyMp4SampleTables(data, track.sampleTableBoxes)
1286
+ )
1287
+ return false;
1288
+ tracks.set(track.trackId, track);
1289
+ }
1290
+ if (![...tracks.values()].some((track) => track.isVideo)) return false;
1291
+ const trexDefaults = readMp4TrexDefaults(data, movieExtends, tracks);
1292
+ if (!trexDefaults) return false;
1293
+
1294
+ const movieFragments = topLevelBoxes.filter((box) => box.type === "moof");
1295
+ if (movieFragments.length === 0) return false;
1296
+ let totalSamples = 0;
1297
+ let hasVideoSamples = false;
1298
+ for (const moof of movieFragments) {
1299
+ const moofBoxes = readIsoBoxes(data, moof.payloadStart, moof.end);
1300
+ const movieFragmentHeader = moofBoxes
1301
+ ? singleIsoBox(moofBoxes, "mfhd")
1302
+ : null;
1303
+ const trafBoxes = moofBoxes?.filter((box) => box.type === "traf") ?? [];
1304
+ if (
1305
+ !moofBoxes ||
1306
+ !movieFragmentHeader ||
1307
+ movieFragmentHeader.end - movieFragmentHeader.payloadStart !== 8 ||
1308
+ data[movieFragmentHeader.payloadStart] !== 0 ||
1309
+ fullBoxFlags(data, movieFragmentHeader) !== 0 ||
1310
+ uint32(data, movieFragmentHeader.payloadStart + 4) === 0 ||
1311
+ trafBoxes.length === 0
1312
+ )
1313
+ return false;
1314
+
1315
+ let previousTrafEnd: number | null = moof.start;
1316
+ for (let trafIndex = 0; trafIndex < trafBoxes.length; trafIndex++) {
1317
+ const traf = trafBoxes[trafIndex]!;
1318
+ const trafChildren = readIsoBoxes(data, traf.payloadStart, traf.end);
1319
+ const tfhdBox = trafChildren ? singleIsoBox(trafChildren, "tfhd") : null;
1320
+ const tfdtBoxes =
1321
+ trafChildren?.filter((box) => box.type === "tfdt") ?? [];
1322
+ const trunBoxes =
1323
+ trafChildren?.filter((box) => box.type === "trun") ?? [];
1324
+ if (
1325
+ !trafChildren ||
1326
+ !tfhdBox ||
1327
+ tfdtBoxes.length > 1 ||
1328
+ (tfdtBoxes[0] && !hasValidMp4Tfdt(data, tfdtBoxes[0])) ||
1329
+ trunBoxes.length === 0
1330
+ )
1331
+ return false;
1332
+
1333
+ const trackId = uint32(data, tfhdBox.payloadStart + 4);
1334
+ const track = tracks.get(trackId);
1335
+ const defaults = trexDefaults.get(trackId);
1336
+ if (!track || !defaults) return false;
1337
+ const tfhd = readMp4Tfhd(data, tfhdBox, moof.start, defaults, track);
1338
+ if (!tfhd) return false;
1339
+ const baseDataOffset = tfhd.baseDataOffset ?? previousTrafEnd;
1340
+ if (baseDataOffset === null) return false;
1341
+
1342
+ let previousRunEnd: number | null = null;
1343
+ let trafDataEnd: number | null = null;
1344
+ let trafSampleCount = 0;
1345
+ for (const trunBox of trunBoxes) {
1346
+ const trun = readMp4Trun(
1347
+ data,
1348
+ trunBox,
1349
+ tfhd,
1350
+ MAX_MEDIA_SAMPLE_ENTRIES - totalSamples,
1351
+ );
1352
+ if (!trun) return false;
1353
+ totalSamples += trun.sampleCount;
1354
+ trafSampleCount += trun.sampleCount;
1355
+ if (
1356
+ !Number.isSafeInteger(totalSamples) ||
1357
+ totalSamples > MAX_MEDIA_SAMPLE_ENTRIES
1358
+ )
1359
+ return false;
1360
+ if (trun.sampleCount === 0) continue;
1361
+ if (tfhd.durationIsEmpty) return false;
1362
+
1363
+ const runStart: number =
1364
+ trun.dataOffset === null
1365
+ ? (previousRunEnd ?? baseDataOffset)
1366
+ : baseDataOffset + trun.dataOffset;
1367
+ const runEnd: number = runStart + trun.sampleBytes;
1368
+ if (
1369
+ !Number.isSafeInteger(runStart) ||
1370
+ !Number.isSafeInteger(runEnd) ||
1371
+ !isMediaDataRange(runStart, trun.sampleBytes, mediaDataBoxes)
1372
+ )
1373
+ return false;
1374
+ previousRunEnd = runEnd;
1375
+ trafDataEnd =
1376
+ trafDataEnd === null ? runEnd : Math.max(trafDataEnd, runEnd);
1377
+ }
1378
+ if (tfhd.durationIsEmpty && trafSampleCount !== 0) return false;
1379
+ if (trafDataEnd !== null) previousTrafEnd = trafDataEnd;
1380
+ if (track.isVideo && trafSampleCount > 0) hasVideoSamples = true;
1381
+ }
1382
+ }
1383
+ return hasVideoSamples;
1384
+ }
1385
+
1386
+ function readEbmlVint(
1387
+ data: Uint8Array,
1388
+ offset: number,
1389
+ maxWidth: number,
1390
+ keepMarker: boolean,
1391
+ ): { value: number; width: number; unknown: boolean } | null {
1392
+ const first = data[offset];
1393
+ if (first === undefined || first === 0) return null;
1394
+ let marker = 0x80;
1395
+ let width = 1;
1396
+ while ((first & marker) === 0 && width <= maxWidth) {
1397
+ marker >>= 1;
1398
+ width++;
1399
+ }
1400
+ if (width > maxWidth || offset + width > data.length) return null;
1401
+ const firstValue = keepMarker ? first : first & (marker - 1);
1402
+ const unknown =
1403
+ !keepMarker &&
1404
+ firstValue === marker - 1 &&
1405
+ data.subarray(offset + 1, offset + width).every((byte) => byte === 0xff);
1406
+ if (unknown) return { value: 0, width, unknown: true };
1407
+ let value = firstValue;
1408
+ for (let index = 1; index < width; index++) {
1409
+ value = value * 256 + data[offset + index]!;
1410
+ if (!Number.isSafeInteger(value)) return null;
1411
+ }
1412
+ return { value, width, unknown };
1413
+ }
1414
+
1415
+ function readEbmlElements(
1416
+ data: Uint8Array,
1417
+ start: number,
1418
+ end: number,
1419
+ unknownSizeIds: ReadonlySet<number> = new Set(),
1420
+ unknownSizeSiblingIds: ReadonlySet<number> = new Set(),
1421
+ ): EbmlElement[] | null {
1422
+ const elements: EbmlElement[] = [];
1423
+ let offset = start;
1424
+ while (offset < end) {
1425
+ if (elements.length >= MAX_MEDIA_CONTAINER_ELEMENTS) return null;
1426
+ const id = readEbmlVint(data, offset, 4, true);
1427
+ if (!id) return null;
1428
+ const size = readEbmlVint(data, offset + id.width, 8, false);
1429
+ if (!size) return null;
1430
+ const payloadStart = offset + id.width + size.width;
1431
+ if (payloadStart > end) return null;
1432
+ if (size.unknown && !unknownSizeIds.has(id.value)) return null;
1433
+ const elementEnd = size.unknown
1434
+ ? id.value === EBML_CLUSTER_ID
1435
+ ? findUnknownSizeClusterEnd(
1436
+ data,
1437
+ payloadStart,
1438
+ end,
1439
+ unknownSizeSiblingIds,
1440
+ )
1441
+ : end
1442
+ : payloadStart + size.value;
1443
+ if (elementEnd === null) return null;
1444
+ if (elementEnd > end || elementEnd < payloadStart) return null;
1445
+ elements.push({ id: id.value, payloadStart, end: elementEnd });
1446
+ offset = elementEnd;
1447
+ }
1448
+ return offset === end ? elements : null;
1449
+ }
1450
+
1451
+ function findUnknownSizeClusterEnd(
1452
+ data: Uint8Array,
1453
+ start: number,
1454
+ end: number,
1455
+ siblingIds: ReadonlySet<number>,
1456
+ ): number | null {
1457
+ let offset = start;
1458
+ let count = 0;
1459
+ while (offset < end) {
1460
+ if (count++ >= MAX_MEDIA_CONTAINER_ELEMENTS) return null;
1461
+ const id = readEbmlVint(data, offset, 4, true);
1462
+ if (!id) return null;
1463
+ if (siblingIds.has(id.value)) return offset;
1464
+ const size = readEbmlVint(data, offset + id.width, 8, false);
1465
+ if (!size || size.unknown) return null;
1466
+ const payloadStart = offset + id.width + size.width;
1467
+ if (payloadStart > end) return null;
1468
+ const childEnd = payloadStart + size.value;
1469
+ if (childEnd > end || childEnd < payloadStart) return null;
1470
+ offset = childEnd;
1471
+ }
1472
+ return offset === end ? end : null;
1473
+ }
1474
+
1475
+ function hasNoNestedSegmentLevelWebmElements(
1476
+ data: Uint8Array,
1477
+ elements: EbmlElement[],
1478
+ ): boolean {
1479
+ const pending = [...elements];
1480
+ let visited = 0;
1481
+ while (pending.length > 0) {
1482
+ if (visited++ >= MAX_MEDIA_CONTAINER_ELEMENTS) return false;
1483
+ const element = pending.pop()!;
1484
+ if (EBML_SEGMENT_LEVEL_IDS.has(element.id)) return false;
1485
+ if (!EBML_NESTED_MASTER_IDS.has(element.id)) continue;
1486
+ const children = readEbmlElements(data, element.payloadStart, element.end);
1487
+ if (!children) return false;
1488
+ for (const child of children) pending.push(child);
1489
+ }
1490
+ return true;
1491
+ }
1492
+
1493
+ function ebmlUnsigned(data: Uint8Array, element: EbmlElement): number | null {
1494
+ const size = element.end - element.payloadStart;
1495
+ if (size < 1 || size > 8) return null;
1496
+ let value = 0;
1497
+ for (let offset = element.payloadStart; offset < element.end; offset++) {
1498
+ value = value * 256 + data[offset]!;
1499
+ if (!Number.isSafeInteger(value)) return null;
1500
+ }
1501
+ return value;
1502
+ }
1503
+
1504
+ function hasWebmVideoTrack(
1505
+ data: Uint8Array,
1506
+ tracks: EbmlElement,
1507
+ ): Set<number> | null {
1508
+ const entries = readEbmlElements(data, tracks.payloadStart, tracks.end);
1509
+ if (!entries) return null;
1510
+ const videoTracks = new Set<number>();
1511
+ for (const entry of entries) {
1512
+ if (entry.id !== 0xae) continue;
1513
+ const fields = readEbmlElements(data, entry.payloadStart, entry.end);
1514
+ if (!fields) return null;
1515
+ const numberField = fields.find((field) => field.id === 0xd7);
1516
+ const typeField = fields.find((field) => field.id === 0x83);
1517
+ const codecField = fields.find((field) => field.id === 0x86);
1518
+ if (!numberField || !typeField || !codecField) continue;
1519
+ const number = ebmlUnsigned(data, numberField);
1520
+ const type = ebmlUnsigned(data, typeField);
1521
+ const codec = Buffer.from(
1522
+ data.subarray(codecField.payloadStart, codecField.end),
1523
+ ).toString("utf8");
1524
+ if (number && type === 1 && codec.startsWith("V_")) videoTracks.add(number);
1525
+ }
1526
+ return videoTracks.size > 0 ? videoTracks : null;
1527
+ }
1528
+
1529
+ function ebmlBlockTrackNumber(
1530
+ data: Uint8Array,
1531
+ block: EbmlElement,
1532
+ videoTracks: Set<number>,
1533
+ ): boolean {
1534
+ const track = readEbmlVint(data, block.payloadStart, 8, false);
1535
+ return (
1536
+ !!track &&
1537
+ !track.unknown &&
1538
+ videoTracks.has(track.value) &&
1539
+ block.end - (block.payloadStart + track.width) >= 4
1540
+ );
1541
+ }
1542
+
1543
+ function hasWebmVideoData(
1544
+ data: Uint8Array,
1545
+ children: EbmlElement[],
1546
+ videoTracks: Set<number>,
1547
+ ): boolean {
1548
+ const timecode = children.find((child) => child.id === 0xe7);
1549
+ if (!timecode || ebmlUnsigned(data, timecode) === null) return false;
1550
+ return children.some((child) => {
1551
+ if (child.id === 0xa3)
1552
+ return ebmlBlockTrackNumber(data, child, videoTracks);
1553
+ if (child.id !== 0xa0) return false;
1554
+ const group = readEbmlElements(data, child.payloadStart, child.end);
1555
+ const block = group?.find((item) => item.id === 0xa1);
1556
+ return !!block && ebmlBlockTrackNumber(data, block, videoTracks);
1557
+ });
1558
+ }
1559
+
1560
+ function hasValidWebmVideo(data: Uint8Array): boolean {
1561
+ const root = readEbmlElements(
1562
+ data,
1563
+ 0,
1564
+ data.length,
1565
+ new Set([EBML_SEGMENT_ID]),
1566
+ );
1567
+ if (!root || root[0]?.id !== 0x1a45dfa3) return false;
1568
+ const header = readEbmlElements(data, root[0].payloadStart, root[0].end);
1569
+ const docType = header?.find((element) => element.id === 0x4282);
1570
+ if (
1571
+ !docType ||
1572
+ Buffer.from(data.subarray(docType.payloadStart, docType.end)).toString(
1573
+ "utf8",
1574
+ ) !== "webm"
1575
+ )
1576
+ return false;
1577
+ const segment = root.find((element) => element.id === 0x18538067);
1578
+ if (!segment) return false;
1579
+ const segmentChildren = readEbmlElements(
1580
+ data,
1581
+ segment.payloadStart,
1582
+ segment.end,
1583
+ new Set([EBML_CLUSTER_ID]),
1584
+ EBML_SEGMENT_LEVEL_IDS,
1585
+ );
1586
+ if (!segmentChildren) return false;
1587
+ const infos = segmentChildren.filter((element) => element.id === 0x1549a966);
1588
+ const tracks = segmentChildren.filter((element) => element.id === 0x1654ae6b);
1589
+ const info = infos[0];
1590
+ const tracksElement = tracks[0];
1591
+ if (infos.length !== 1 || tracks.length !== 1 || !info || !tracksElement)
1592
+ return false;
1593
+ if (!readEbmlElements(data, info.payloadStart, info.end)) return false;
1594
+ const videoTracks = hasWebmVideoTrack(data, tracksElement);
1595
+ if (!videoTracks) return false;
1596
+ let hasVideoData = false;
1597
+ for (const cluster of segmentChildren.filter(
1598
+ (element) => element.id === EBML_CLUSTER_ID,
1599
+ )) {
1600
+ const children = readEbmlElements(data, cluster.payloadStart, cluster.end);
1601
+ if (!children || !hasNoNestedSegmentLevelWebmElements(data, children))
1602
+ return false;
1603
+ if (hasWebmVideoData(data, children, videoTracks)) hasVideoData = true;
1604
+ }
1605
+ return hasVideoData;
1606
+ }
1607
+
1608
+ export function hasExpectedSvgSignature(data: Uint8Array): boolean {
1609
+ const normalizedData = stripSafeSvgDoctype(data);
1610
+ if (!normalizedData) return false;
1611
+ const head = Buffer.from(
1612
+ normalizedData.subarray(0, Math.min(normalizedData.length, 8192)),
1613
+ ).toString("utf8");
1614
+ const normalized = head.replace(/^\uFEFF/, "").trimStart();
1615
+ return /^(?:(?:\s|<!--[\s\S]*?-->|<\?xml\b[\s\S]*?\?>))*<svg(?:\s|\/?>)/i.test(
1616
+ normalized,
1617
+ );
1618
+ }
1619
+
1620
+ function hasExpectedImageSignature(ext: string, data: Uint8Array): boolean {
1621
+ if (ext === ".png") {
1622
+ return (
1623
+ data[0] === 0x89 &&
1624
+ data[1] === 0x50 &&
1625
+ data[2] === 0x4e &&
1626
+ data[3] === 0x47
1627
+ );
1628
+ }
1629
+ if (ext === ".jpg" || ext === ".jpeg") {
1630
+ return data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff;
1631
+ }
1632
+ if (ext === ".gif") {
1633
+ const header = ascii(data, 0, 6);
1634
+ return header === "GIF87a" || header === "GIF89a";
1635
+ }
1636
+ if (ext === ".webp") {
1637
+ return ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 12) === "WEBP";
1638
+ }
1639
+ if (ext === ".ico") {
1640
+ return (
1641
+ data[0] === 0x00 &&
1642
+ data[1] === 0x00 &&
1643
+ data[2] === 0x01 &&
1644
+ data[3] === 0x00
1645
+ );
1646
+ }
1647
+ if (ext === ".avif") {
1648
+ return ascii(data, 4, 12).includes("ftyp");
1649
+ }
1650
+ if (ext === ".svg") {
1651
+ return hasExpectedSvgSignature(data);
1652
+ }
1653
+ return false;
1654
+ }
1655
+
1656
+ function decodeXmlReferences(source: string): string {
1657
+ const namedReferences: Record<string, string> = {
1658
+ amp: "&",
1659
+ apos: "'",
1660
+ gt: ">",
1661
+ lt: "<",
1662
+ quot: '"',
1663
+ };
1664
+ return source.replace(
1665
+ /&#x([0-9a-f]+);|&#([0-9]+);|&([a-z]+);/gi,
1666
+ (
1667
+ match,
1668
+ hex: string | undefined,
1669
+ decimal: string | undefined,
1670
+ named: string | undefined,
1671
+ ) => {
1672
+ const codePoint = hex
1673
+ ? Number.parseInt(hex, 16)
1674
+ : decimal
1675
+ ? Number.parseInt(decimal, 10)
1676
+ : undefined;
1677
+ if (
1678
+ codePoint !== undefined &&
1679
+ codePoint <= 0x10ffff &&
1680
+ !(codePoint >= 0xd800 && codePoint <= 0xdfff)
1681
+ ) {
1682
+ return String.fromCodePoint(codePoint);
1683
+ }
1684
+ return named ? (namedReferences[named.toLowerCase()] ?? match) : match;
1685
+ },
1686
+ );
1687
+ }
1688
+
1689
+ function decodeCssEscapes(source: string): string {
1690
+ return source
1691
+ .replace(/\\(?:\r\n|[\r\n\f])/g, "")
1692
+ .replace(
1693
+ /\\([0-9a-f]{1,6})(?:[ \t\r\n\f])?|\\([^\r\n])/gi,
1694
+ (match, hex: string | undefined, character: string | undefined) => {
1695
+ if (!hex) return character ?? match;
1696
+ const codePoint = Number.parseInt(hex, 16);
1697
+ if (
1698
+ codePoint > 0x10ffff ||
1699
+ (codePoint >= 0xd800 && codePoint <= 0xdfff)
1700
+ ) {
1701
+ return match;
1702
+ }
1703
+ return String.fromCodePoint(codePoint);
1704
+ },
1705
+ );
1706
+ }
1707
+
1708
+ export function isSafeSvg(data: Uint8Array): boolean {
1709
+ const normalizedData = stripSafeSvgDoctype(data);
1710
+ if (!normalizedData) return false;
1711
+ let source: string;
1712
+ try {
1713
+ source = new TextDecoder("utf-8", { fatal: true }).decode(normalizedData);
1714
+ } catch {
1715
+ // coercion-ok: malformed UTF-8 must reject SVG validation.
1716
+ return false;
1717
+ }
1718
+ source = source.replace(/^\uFEFF/, "").trim();
1719
+ const normalizedSource = decodeCssEscapes(
1720
+ decodeXmlReferences(source),
1721
+ ).replace(/\/\*[\s\S]*?\*\//g, "");
1722
+ const forbidden = [
1723
+ /<\s*(?:script|foreignObject|iframe|object|embed|link|audio|video|animate(?:Transform|Motion|Color)?|set|discard)\b/i,
1724
+ /<\s*\/?[a-z_][\w.-]*:[a-z_][\w.-]*\b/i,
1725
+ /<!\s*(?:DOCTYPE|ENTITY)\b/i,
1726
+ /<\?xml-stylesheet\b/i,
1727
+ /\bxml:base\s*=/i,
1728
+ /\son[a-z][a-z0-9:_-]*\s*=/i,
1729
+ /\b(?:javascript|vbscript)\s*:/i,
1730
+ /\b(?:expression|behavior|-moz-binding)\s*\(/i,
1731
+ /@import\b/i,
1732
+ /\b(?:image|(?:-webkit-)?image-set)\s*\(/i,
1733
+ ];
1734
+ if (forbidden.some((pattern) => pattern.test(normalizedSource))) return false;
1735
+
1736
+ for (const match of normalizedSource.matchAll(
1737
+ /(?:href|xlink:href)\s*=\s*(?:(['"])(.*?)\1|([^\s>]+))/gi,
1738
+ )) {
1739
+ const target = (match[2] ?? match[3] ?? match[4] ?? "").trim();
1740
+ if (target && !target.startsWith("#") && !isAllowedInlineSvgAsset(target)) {
1741
+ return false;
1742
+ }
1743
+ }
1744
+ for (const match of normalizedSource.matchAll(
1745
+ /url\(\s*(["']?)(.*?)\1\s*\)/gi,
1746
+ )) {
1747
+ const target = match[2]?.trim() ?? "";
1748
+ if (target && !target.startsWith("#") && !isAllowedInlineSvgAsset(target)) {
1749
+ return false;
1750
+ }
1751
+ }
1752
+ return true;
1753
+ }
1754
+
1755
+ function isAllowedInlineSvgAsset(target: string): boolean {
1756
+ const dataUrl = parseBase64DataUrl(target);
1757
+ return Boolean(
1758
+ dataUrl &&
1759
+ ["image/png", "image/jpeg", "image/gif", "image/webp"].includes(
1760
+ dataUrl.mediaType,
1761
+ ),
1762
+ );
1763
+ }
1764
+
1765
+ export function canSaveAsUploadedAsset(args: {
1766
+ originalName: string;
1767
+ data: Uint8Array;
1768
+ }): boolean {
1769
+ const ext = path.extname(args.originalName).toLowerCase();
1770
+ return (
1771
+ args.data.length <= MAX_ASSET_FILE_SIZE &&
1772
+ isImageAssetExtension(ext) &&
1773
+ (ext !== ".svg" ||
1774
+ (hasExpectedImageSignature(ext, args.data) && isSafeSvg(args.data)))
1775
+ );
1776
+ }
1777
+
1778
+ export async function uploadImageAsset(args: {
1779
+ email: string;
1780
+ orgId?: string | null;
1781
+ deckId?: string;
1782
+ originalName: string;
1783
+ data: Uint8Array;
1784
+ type?: string;
1785
+ }): Promise<RegisteredUploadedAsset> {
1786
+ if (args.data.length > MAX_ASSET_FILE_SIZE) {
1787
+ throw imageUploadFailure("image_upload_too_large");
1788
+ }
1789
+
1790
+ const ext = path.extname(args.originalName).toLowerCase();
1791
+ if (!isImageAssetExtension(ext)) {
1792
+ throw imageUploadFailure("image_upload_unsupported_type");
1793
+ }
1794
+ if (!hasExpectedImageSignature(ext, args.data)) {
1795
+ throw imageUploadFailure("image_upload_invalid_image");
1796
+ }
1797
+
1798
+ try {
1799
+ await assertDeckAssetUploadScope({
1800
+ deckId: args.deckId,
1801
+ email: args.email,
1802
+ orgId: args.orgId,
1803
+ });
1804
+ } catch (error) {
1805
+ const errorCode = (error as { errorCode?: unknown })?.errorCode;
1806
+ if (
1807
+ typeof errorCode === "string" &&
1808
+ Object.hasOwn(imageUploadFailures, errorCode)
1809
+ ) {
1810
+ throw error;
1811
+ }
1812
+ if ((error as { statusCode?: unknown })?.statusCode === 403) {
1813
+ throw imageUploadFailure("asset_deck_scope_forbidden");
1814
+ }
1815
+ throw imageUploadFailure("image_upload_scope_check_failed");
1816
+ }
1817
+
1818
+ let data = args.data;
1819
+ if (ext === ".svg") {
1820
+ const normalizedSvg = stripSafeSvgDoctype(data);
1821
+ if (!normalizedSvg || !isSafeSvg(normalizedSvg)) {
1822
+ throw imageUploadFailure("image_upload_unsafe_svg");
1823
+ }
1824
+ data = normalizedSvg;
1825
+ }
1826
+
1827
+ const mimeType = ext === ".svg" ? "image/svg+xml" : args.type;
1828
+
1829
+ const orgId =
1830
+ args.orgId === undefined ? getRequestOrgId() : (args.orgId ?? undefined);
1831
+ await reapOrphanedUploadedAssetObjects(args.email, orgId, {
1832
+ limit: MAX_ORPHANED_ASSET_CLEANUPS_PER_UPLOAD,
1833
+ });
1834
+ const id = nanoid();
1835
+ const cleanupScope: UploadedAssetCleanupScope = {
1836
+ assetId: id,
1837
+ ownerEmail: args.email,
1838
+ orgId: orgId ?? null,
1839
+ };
1840
+ let cleanupToken: string;
1841
+ try {
1842
+ cleanupToken = await reserveUploadedAssetRegistration(cleanupScope);
1843
+ } catch (error) {
1844
+ console.warn("[slides-upload] could not reserve image cleanup", {
1845
+ assetId: id,
1846
+ error: error instanceof Error ? error.message : String(error),
1847
+ });
1848
+ throw imageUploadFailure("image_asset_registration_failed");
1849
+ }
1850
+
1851
+ let result: Awaited<ReturnType<typeof uploadFile>>;
1852
+ try {
1853
+ result = await runWithRequestContext(
1854
+ { userEmail: args.email, ...(orgId === undefined ? {} : { orgId }) },
1855
+ () =>
1856
+ uploadFile({
1857
+ data,
1858
+ filename: args.originalName,
1859
+ mimeType,
1860
+ ownerEmail: args.email,
1861
+ }),
1862
+ );
1863
+ } catch {
1864
+ try {
1865
+ await parkUnresolvedUploadedAssetRegistration(cleanupScope, cleanupToken);
1866
+ } catch (error) {
1867
+ console.warn(
1868
+ "[slides-upload] unresolved image reservation could not be parked",
1869
+ {
1870
+ assetId: id,
1871
+ error: error instanceof Error ? error.message : String(error),
1872
+ },
1873
+ );
1874
+ }
1875
+ throw imageUploadFailure("image_upload_storage_failed");
1876
+ }
1877
+
1878
+ if (!result) {
1879
+ await cancelUploadedAssetRegistration(cleanupScope, cleanupToken);
1880
+ throw imageUploadFailure("image_upload_storage_unavailable");
1881
+ }
1882
+
1883
+ const asset: RegisteredUploadedAsset = {
1884
+ id,
1885
+ url: result.url,
1886
+ filename: args.originalName,
1887
+ type: mimeType || "application/octet-stream",
1888
+ size: data.length,
1889
+ provider: result.provider,
1890
+ };
1891
+ const cleanupInput: UploadedAssetCleanupOutboxInput = {
1892
+ assetId: asset.id,
1893
+ ownerEmail: args.email,
1894
+ orgId: orgId ?? null,
1895
+ provider: result.provider,
1896
+ providerObjectId: result.id ?? null,
1897
+ url: result.url,
1898
+ intent: "registration",
1899
+ };
1900
+ try {
1901
+ const recorded = await recordUploadedAssetCleanupObject(
1902
+ cleanupInput,
1903
+ cleanupToken,
1904
+ );
1905
+ if (!recorded) {
1906
+ throw new Error("Uploaded asset cleanup reservation was claimed.");
1907
+ }
1908
+ } catch (error) {
1909
+ try {
1910
+ const requestContext = {
1911
+ userEmail: args.email,
1912
+ ...(orgId === undefined ? {} : { orgId }),
1913
+ };
1914
+ const deletionToken = await claimFailedUploadedAssetRegistration(
1915
+ cleanupInput,
1916
+ cleanupToken,
1917
+ );
1918
+ if (deletionToken) {
1919
+ await deleteFailedUploadUsingReservation({
1920
+ input: cleanupInput,
1921
+ token: deletionToken,
1922
+ requestContext,
1923
+ mediaType: "image",
1924
+ });
1925
+ } else {
1926
+ await parkUnresolvedUploadedAssetRegistration(
1927
+ cleanupScope,
1928
+ cleanupToken,
1929
+ );
1930
+ }
1931
+ } catch (queueError) {
1932
+ console.warn("[slides-upload] image cleanup could not be queued", {
1933
+ assetId: asset.id,
1934
+ error:
1935
+ queueError instanceof Error ? queueError.message : String(queueError),
1936
+ });
1937
+ }
1938
+ console.warn("[slides-upload] could not record image cleanup metadata", {
1939
+ assetId: asset.id,
1940
+ error: error instanceof Error ? error.message : String(error),
1941
+ });
1942
+ throw imageUploadFailure("image_asset_registration_failed");
1943
+ }
1944
+
1945
+ try {
1946
+ await registerUploadedAsset({
1947
+ row: {
1948
+ id: asset.id,
1949
+ filename: asset.filename,
1950
+ url: asset.url,
1951
+ type: asset.type,
1952
+ size: asset.size,
1953
+ provider: asset.provider ?? null,
1954
+ providerObjectId: result.id ?? null,
1955
+ deckId: args.deckId ?? null,
1956
+ orgId: orgId ?? null,
1957
+ ownerEmail: args.email,
1958
+ createdAt: new Date().toISOString(),
1959
+ },
1960
+ deckId: args.deckId,
1961
+ email: args.email,
1962
+ orgId,
1963
+ cleanupToken,
1964
+ cleanupInput,
1965
+ });
1966
+ } catch (registrationError) {
1967
+ let committedAsset: RegisteredUploadedAsset | null = null;
1968
+ let assetLookupFailed = false;
1969
+ try {
1970
+ committedAsset = await findUploadedAssetById(
1971
+ args.email,
1972
+ asset.id,
1973
+ orgId ?? null,
1974
+ );
1975
+ } catch {
1976
+ assetLookupFailed = true;
1977
+ }
1978
+ if (committedAsset) {
1979
+ try {
1980
+ await clearRegisteredUploadedAsset(cleanupInput, cleanupToken);
1981
+ } catch (error) {
1982
+ console.warn("[slides-upload] registered image cleanup remains", {
1983
+ assetId: asset.id,
1984
+ error: error instanceof Error ? error.message : String(error),
1985
+ });
1986
+ }
1987
+ return committedAsset;
1988
+ }
1989
+ if (assetLookupFailed) {
1990
+ console.warn("[slides-upload] image registration outcome is uncertain", {
1991
+ assetId: asset.id,
1992
+ });
1993
+ } else {
1994
+ try {
1995
+ const deletionToken = await claimFailedUploadedAssetRegistration(
1996
+ cleanupInput,
1997
+ cleanupToken,
1998
+ );
1999
+ if (deletionToken) {
2000
+ await deleteFailedUploadUsingReservation({
2001
+ input: cleanupInput,
2002
+ token: deletionToken,
2003
+ requestContext: {
2004
+ userEmail: args.email,
2005
+ ...(orgId === undefined ? {} : { orgId }),
2006
+ },
2007
+ mediaType: "image",
2008
+ });
2009
+ }
2010
+ } catch (error) {
2011
+ console.warn("[slides-upload] image cleanup remains queued", {
2012
+ assetId: asset.id,
2013
+ error: error instanceof Error ? error.message : String(error),
2014
+ });
2015
+ }
2016
+ }
2017
+ const candidate = (registrationError as { errorCode?: unknown })?.errorCode;
2018
+ throw imageUploadFailure(
2019
+ typeof candidate === "string" &&
2020
+ Object.hasOwn(imageUploadFailures, candidate)
2021
+ ? (candidate as ImageUploadFailureCode)
2022
+ : "image_asset_registration_failed",
2023
+ );
2024
+ }
2025
+
2026
+ try {
2027
+ await clearRegisteredUploadedAsset(cleanupInput, cleanupToken);
2028
+ } catch (error) {
2029
+ console.warn("[slides-upload] registered image cleanup remains", {
2030
+ assetId: asset.id,
2031
+ error: error instanceof Error ? error.message : String(error),
2032
+ });
2033
+ }
2034
+ return asset;
2035
+ }
2036
+
2037
+ export function canSaveAsUploadedVideoAsset(args: {
2038
+ originalName: string;
2039
+ data: Uint8Array;
2040
+ }): boolean {
2041
+ const ext = path.extname(args.originalName).toLowerCase();
2042
+ if (args.data.length > MAX_VIDEO_ASSET_FILE_SIZE) return false;
2043
+ if (ext === ".mp4") return hasValidMp4Video(args.data);
2044
+ return ext === ".webm" && hasValidWebmVideo(args.data);
2045
+ }
2046
+
2047
+ const videoUploadFailures = {
2048
+ asset_deck_scope_forbidden: {
2049
+ statusCode: 403,
2050
+ message: "You don't have permission to add video to this presentation.",
2051
+ },
2052
+ deck_not_found: {
2053
+ statusCode: 404,
2054
+ message: "This presentation could not be found.",
2055
+ },
2056
+ invalid_deck_data: {
2057
+ statusCode: 500,
2058
+ message: "This presentation's data is unavailable.",
2059
+ },
2060
+ invalid_deck_id: {
2061
+ statusCode: 400,
2062
+ message: "The presentation selection is invalid.",
2063
+ },
2064
+ video_asset_registration_failed: {
2065
+ statusCode: 500,
2066
+ message: "The uploaded video couldn't be saved. Try again.",
2067
+ },
2068
+ video_upload_auth_required: {
2069
+ statusCode: 401,
2070
+ message: "Sign in to upload videos.",
2071
+ },
2072
+ video_upload_failed: {
2073
+ statusCode: 500,
2074
+ message: "Video upload failed. Try again.",
2075
+ },
2076
+ video_upload_file_missing: {
2077
+ statusCode: 400,
2078
+ message: "Choose a video to upload.",
2079
+ },
2080
+ video_upload_invalid_format: {
2081
+ statusCode: 400,
2082
+ message: "Only valid MP4 and WebM videos are allowed.",
2083
+ },
2084
+ video_upload_request_invalid: {
2085
+ statusCode: 400,
2086
+ message: "The video upload request could not be read.",
2087
+ },
2088
+ video_upload_scope_check_failed: {
2089
+ statusCode: 500,
2090
+ message: "Could not verify access to this presentation.",
2091
+ },
2092
+ video_upload_session_unavailable: {
2093
+ statusCode: 503,
2094
+ message: "The upload service is temporarily unavailable. Try again.",
2095
+ },
2096
+ video_upload_storage_failed: {
2097
+ statusCode: 502,
2098
+ message: "Video storage is temporarily unavailable. Try again.",
2099
+ },
2100
+ video_upload_storage_unavailable: {
2101
+ statusCode: 503,
2102
+ message:
2103
+ "No object storage is connected. Connect file storage before uploading videos.",
2104
+ },
2105
+ video_upload_too_large: {
2106
+ statusCode: 413,
2107
+ message: "Video files must be 50 MB or smaller.",
2108
+ },
2109
+ } as const;
2110
+
2111
+ type VideoUploadFailureCode = keyof typeof videoUploadFailures;
2112
+
2113
+ function uploadedVideoError(code: VideoUploadFailureCode): Error & {
2114
+ errorCode: VideoUploadFailureCode;
2115
+ statusCode: number;
2116
+ } {
2117
+ const failure = videoUploadFailures[code];
2118
+ return Object.assign(new Error(failure.message), {
2119
+ errorCode: code,
2120
+ statusCode: failure.statusCode,
2121
+ });
2122
+ }
2123
+
2124
+ export function videoUploadFailureResponse(
2125
+ event: Parameters<typeof setResponseStatus>[0],
2126
+ error: unknown,
2127
+ ): { error: string; errorCode: VideoUploadFailureCode } {
2128
+ const candidate = (error as { errorCode?: unknown })?.errorCode;
2129
+ const code =
2130
+ typeof candidate === "string" &&
2131
+ Object.hasOwn(videoUploadFailures, candidate)
2132
+ ? (candidate as VideoUploadFailureCode)
2133
+ : "video_upload_failed";
2134
+ const failure = videoUploadFailures[code];
2135
+ setResponseStatus(event, failure.statusCode);
2136
+ return { error: failure.message, errorCode: code };
2137
+ }
2138
+
2139
+ async function deleteUploadedAssetRow(
2140
+ transaction: SlidesTransaction,
2141
+ asset: {
2142
+ id: string;
2143
+ url: string;
2144
+ provider: string;
2145
+ providerObjectId: string | null;
2146
+ orgId: string | null;
2147
+ },
2148
+ ownerEmail: string,
2149
+ ): Promise<void> {
2150
+ await transaction
2151
+ .delete(schema.uploadedAssets)
2152
+ .where(
2153
+ and(
2154
+ eq(schema.uploadedAssets.id, asset.id),
2155
+ eq(schema.uploadedAssets.ownerEmail, ownerEmail),
2156
+ asset.orgId
2157
+ ? eq(schema.uploadedAssets.orgId, asset.orgId)
2158
+ : isNull(schema.uploadedAssets.orgId),
2159
+ eq(schema.uploadedAssets.url, asset.url),
2160
+ eq(schema.uploadedAssets.provider, asset.provider),
2161
+ asset.providerObjectId
2162
+ ? eq(schema.uploadedAssets.providerObjectId, asset.providerObjectId)
2163
+ : isNull(schema.uploadedAssets.providerObjectId),
2164
+ isNull(schema.uploadedAssets.deckId),
2165
+ ),
2166
+ );
2167
+ }
2168
+
2169
+ async function findUploadedVideoAssetById(
2170
+ email: string,
2171
+ assetId: string,
2172
+ requestedOrgId: string | null | undefined,
2173
+ ): Promise<UploadedVideoAsset | null> {
2174
+ const orgId = requestedOrgId ?? undefined;
2175
+ const [asset] = await getDb()
2176
+ .select({
2177
+ id: schema.uploadedAssets.id,
2178
+ filename: schema.uploadedAssets.filename,
2179
+ url: schema.uploadedAssets.url,
2180
+ type: schema.uploadedAssets.type,
2181
+ size: schema.uploadedAssets.size,
2182
+ provider: schema.uploadedAssets.provider,
2183
+ })
2184
+ .from(schema.uploadedAssets)
2185
+ .where(
2186
+ and(
2187
+ eq(schema.uploadedAssets.id, assetId),
2188
+ eq(schema.uploadedAssets.ownerEmail, email),
2189
+ orgId
2190
+ ? eq(schema.uploadedAssets.orgId, orgId)
2191
+ : isNull(schema.uploadedAssets.orgId),
2192
+ ),
2193
+ )
2194
+ .limit(1);
2195
+ if (!asset || !asset.type.startsWith("video/")) return null;
2196
+ return { ...asset, provider: asset.provider ?? undefined };
2197
+ }
2198
+
2199
+ async function findUploadedAssetById(
2200
+ email: string,
2201
+ assetId: string,
2202
+ requestedOrgId: string | null | undefined,
2203
+ ): Promise<RegisteredUploadedAsset | null> {
2204
+ const orgId = requestedOrgId ?? undefined;
2205
+ const [asset] = await getDb()
2206
+ .select({
2207
+ id: schema.uploadedAssets.id,
2208
+ filename: schema.uploadedAssets.filename,
2209
+ url: schema.uploadedAssets.url,
2210
+ type: schema.uploadedAssets.type,
2211
+ size: schema.uploadedAssets.size,
2212
+ provider: schema.uploadedAssets.provider,
2213
+ })
2214
+ .from(schema.uploadedAssets)
2215
+ .where(
2216
+ and(
2217
+ eq(schema.uploadedAssets.id, assetId),
2218
+ eq(schema.uploadedAssets.ownerEmail, email),
2219
+ orgId
2220
+ ? eq(schema.uploadedAssets.orgId, orgId)
2221
+ : isNull(schema.uploadedAssets.orgId),
2222
+ ),
2223
+ )
2224
+ .limit(1);
2225
+ return asset ? { ...asset, provider: asset.provider ?? undefined } : null;
2226
+ }
2227
+
2228
+ interface UploadedAssetCleanupRecord {
2229
+ id: string;
2230
+ url: string;
2231
+ provider: string | null;
2232
+ providerObjectId: string | null;
2233
+ deckId: string | null;
2234
+ }
2235
+
2236
+ async function findUploadedAssetForCleanup(
2237
+ ownerEmail: string,
2238
+ assetId: string,
2239
+ orgId: string | null | undefined,
2240
+ ): Promise<UploadedAssetCleanupRecord | null> {
2241
+ const [asset] = await getDb()
2242
+ .select({
2243
+ id: schema.uploadedAssets.id,
2244
+ url: schema.uploadedAssets.url,
2245
+ provider: schema.uploadedAssets.provider,
2246
+ providerObjectId: schema.uploadedAssets.providerObjectId,
2247
+ deckId: schema.uploadedAssets.deckId,
2248
+ })
2249
+ .from(schema.uploadedAssets)
2250
+ .where(
2251
+ and(
2252
+ eq(schema.uploadedAssets.id, assetId),
2253
+ eq(schema.uploadedAssets.ownerEmail, ownerEmail),
2254
+ orgId
2255
+ ? eq(schema.uploadedAssets.orgId, orgId)
2256
+ : isNull(schema.uploadedAssets.orgId),
2257
+ ),
2258
+ )
2259
+ .limit(1);
2260
+ return asset ?? null;
2261
+ }
2262
+
2263
+ function uploadedAssetCleanupScope(
2264
+ row: Pick<UploadedAssetCleanupOutboxRow, "id" | "ownerEmail" | "orgId">,
2265
+ ) {
2266
+ return and(
2267
+ eq(schema.uploadedAssets.id, row.id),
2268
+ eq(schema.uploadedAssets.ownerEmail, row.ownerEmail),
2269
+ row.orgId
2270
+ ? eq(schema.uploadedAssets.orgId, row.orgId)
2271
+ : isNull(schema.uploadedAssets.orgId),
2272
+ );
2273
+ }
2274
+
2275
+ async function deleteUploadedAssetRowForCleanup(
2276
+ transaction: SlidesTransaction,
2277
+ row: Omit<UploadedAssetCleanupOutboxRow, "provider" | "url"> & {
2278
+ provider: string;
2279
+ url: string;
2280
+ },
2281
+ ): Promise<void> {
2282
+ await transaction
2283
+ .delete(schema.uploadedAssets)
2284
+ .where(
2285
+ and(
2286
+ uploadedAssetCleanupScope(row),
2287
+ eq(schema.uploadedAssets.url, row.url),
2288
+ eq(schema.uploadedAssets.provider, row.provider),
2289
+ row.providerObjectId
2290
+ ? eq(schema.uploadedAssets.providerObjectId, row.providerObjectId)
2291
+ : isNull(schema.uploadedAssets.providerObjectId),
2292
+ isNull(schema.uploadedAssets.deckId),
2293
+ ),
2294
+ );
2295
+ }
2296
+
2297
+ function assertCleanupRunActive(options?: {
2298
+ deadlineAt?: number;
2299
+ signal?: AbortSignal;
2300
+ }): void {
2301
+ if (options?.signal?.aborted) {
2302
+ throw new Error(
2303
+ "Slides asset cleanup sweep was aborted before completion.",
2304
+ );
2305
+ }
2306
+ if (options?.deadlineAt !== undefined && Date.now() >= options.deadlineAt) {
2307
+ throw new Error("Slides asset cleanup sweep exceeded its deadline.");
2308
+ }
2309
+ }
2310
+
2311
+ export async function reapOrphanedUploadedAssetObjects(
2312
+ ownerEmail: string | undefined,
2313
+ orgId: string | null | undefined,
2314
+ options?: {
2315
+ limit?: number;
2316
+ deadlineAt?: number;
2317
+ signal?: AbortSignal;
2318
+ concurrency?: number;
2319
+ },
2320
+ ): Promise<{ failedAttempts: number }> {
2321
+ const summary = { failedAttempts: 0 };
2322
+ const requestedLimit =
2323
+ options?.limit ?? MAX_ORPHANED_ASSET_CLEANUPS_PER_UPLOAD;
2324
+ if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 0) {
2325
+ throw new RangeError("Asset cleanup limit must be a non-negative integer.");
2326
+ }
2327
+ const limit = Math.min(
2328
+ requestedLimit,
2329
+ MAX_ORPHANED_ASSET_CLEANUPS_PER_UPLOAD,
2330
+ );
2331
+ if (limit === 0) return summary;
2332
+
2333
+ let registered: UploadedAssetCleanupOutboxRow[];
2334
+ let candidates: UploadedAssetCleanupOutboxRow[];
2335
+ try {
2336
+ const scope = ownerEmail ? { ownerEmail, orgId: orgId ?? null } : {};
2337
+ [registered, candidates] = await Promise.all([
2338
+ listRegisteredUploadedAssetCleanups({ ...scope, limit }),
2339
+ listDueUploadedAssetCleanups({ ...scope, limit }),
2340
+ ]);
2341
+ } catch (error) {
2342
+ console.warn("[slides-upload] could not read cleanup outbox", {
2343
+ error: error instanceof Error ? error.message : String(error),
2344
+ });
2345
+ return { failedAttempts: 1 };
2346
+ }
2347
+
2348
+ const concurrency = Math.max(1, Math.min(options?.concurrency ?? 4, 8));
2349
+ for (let offset = 0; offset < registered.length; offset += concurrency) {
2350
+ assertCleanupRunActive(options);
2351
+ await Promise.all(
2352
+ registered.slice(offset, offset + concurrency).map(async (row) => {
2353
+ const requestContext = {
2354
+ userEmail: row.ownerEmail,
2355
+ ...(row.orgId ? { orgId: row.orgId } : {}),
2356
+ };
2357
+ try {
2358
+ const asset = await runWithRequestContext(requestContext, () =>
2359
+ findUploadedAssetForCleanup(row.ownerEmail, row.id, row.orgId),
2360
+ );
2361
+ if (!asset || !row.leaseToken) {
2362
+ summary.failedAttempts += 1;
2363
+ return;
2364
+ }
2365
+ if (
2366
+ asset.provider !== row.provider ||
2367
+ asset.url !== row.url ||
2368
+ asset.providerObjectId !== row.providerObjectId
2369
+ ) {
2370
+ summary.failedAttempts += 1;
2371
+ console.warn(
2372
+ "[slides-upload] registered cleanup metadata no longer matches asset",
2373
+ { assetId: row.id },
2374
+ );
2375
+ return;
2376
+ }
2377
+ const removed = await runWithRequestContext(requestContext, () =>
2378
+ clearRegisteredUploadedAsset(
2379
+ { assetId: row.id, ownerEmail: row.ownerEmail, orgId: row.orgId },
2380
+ row.leaseToken!,
2381
+ ),
2382
+ );
2383
+ if (!removed) summary.failedAttempts += 1;
2384
+ } catch (error) {
2385
+ summary.failedAttempts += 1;
2386
+ console.warn("[slides-upload] could not clear registered cleanup", {
2387
+ assetId: row.id,
2388
+ error: error instanceof Error ? error.message : String(error),
2389
+ });
2390
+ }
2391
+ }),
2392
+ );
2393
+ }
2394
+
2395
+ for (let offset = 0; offset < candidates.length; offset += concurrency) {
2396
+ assertCleanupRunActive(options);
2397
+ await Promise.all(
2398
+ candidates.slice(offset, offset + concurrency).map(async (candidate) => {
2399
+ const requestContext = {
2400
+ userEmail: candidate.ownerEmail,
2401
+ ...(candidate.orgId ? { orgId: candidate.orgId } : {}),
2402
+ };
2403
+ let claimed: UploadedAssetCleanupOutboxRow | null;
2404
+ try {
2405
+ claimed = await runWithRequestContext(requestContext, () =>
2406
+ claimDueUploadedAssetCleanup(candidate),
2407
+ );
2408
+ } catch (error) {
2409
+ summary.failedAttempts += 1;
2410
+ console.warn("[slides-upload] could not claim cleanup outbox row", {
2411
+ assetId: candidate.id,
2412
+ error: error instanceof Error ? error.message : String(error),
2413
+ });
2414
+ return;
2415
+ }
2416
+ if (!claimed) return;
2417
+ const leaseToken = claimed.leaseToken;
2418
+ if (!leaseToken) {
2419
+ summary.failedAttempts += 1;
2420
+ console.warn("[slides-upload] claimed cleanup has no lease token", {
2421
+ assetId: claimed.id,
2422
+ });
2423
+ return;
2424
+ }
2425
+
2426
+ const cleanupScope = {
2427
+ assetId: claimed.id,
2428
+ ownerEmail: claimed.ownerEmail,
2429
+ orgId: claimed.orgId,
2430
+ };
2431
+ const retry = async () => {
2432
+ try {
2433
+ const retried = await runWithRequestContext(requestContext, () =>
2434
+ retryOwnedUploadedAssetCleanup(
2435
+ cleanupScope,
2436
+ leaseToken,
2437
+ claimed.attemptCount,
2438
+ ),
2439
+ );
2440
+ if (!retried) summary.failedAttempts += 1;
2441
+ } catch (error) {
2442
+ summary.failedAttempts += 1;
2443
+ console.warn("[slides-upload] could not reschedule cleanup", {
2444
+ assetId: claimed.id,
2445
+ error: error instanceof Error ? error.message : String(error),
2446
+ });
2447
+ }
2448
+ };
2449
+
2450
+ try {
2451
+ assertCleanupRunActive(options);
2452
+ const provider = claimed.provider;
2453
+ const url = claimed.url;
2454
+ if (
2455
+ typeof provider !== "string" ||
2456
+ typeof url !== "string" ||
2457
+ !provider.trim() ||
2458
+ !url.trim()
2459
+ ) {
2460
+ summary.failedAttempts += 1;
2461
+ await retry();
2462
+ return;
2463
+ }
2464
+ const cleanupClaim = { ...claimed, leaseToken, provider, url };
2465
+
2466
+ let asset: UploadedAssetCleanupRecord | null;
2467
+ try {
2468
+ asset = await runWithRequestContext(requestContext, () =>
2469
+ findUploadedAssetForCleanup(
2470
+ cleanupClaim.ownerEmail,
2471
+ cleanupClaim.id,
2472
+ cleanupClaim.orgId,
2473
+ ),
2474
+ );
2475
+ } catch (error) {
2476
+ summary.failedAttempts += 1;
2477
+ console.warn("[slides-upload] could not verify cleanup asset", {
2478
+ assetId: claimed.id,
2479
+ error: error instanceof Error ? error.message : String(error),
2480
+ });
2481
+ await retry();
2482
+ return;
2483
+ }
2484
+
2485
+ if (claimed.intent === "registration" && asset) {
2486
+ const [registeredRow] = await runWithRequestContext(
2487
+ requestContext,
2488
+ () =>
2489
+ getDb()
2490
+ .update(schema.uploadedAssetCleanupOutbox)
2491
+ .set({
2492
+ state: "registered",
2493
+ leaseExpiresAt: null,
2494
+ nextAttemptAt: null,
2495
+ })
2496
+ .where(
2497
+ and(
2498
+ eq(schema.uploadedAssetCleanupOutbox.id, cleanupClaim.id),
2499
+ eq(
2500
+ schema.uploadedAssetCleanupOutbox.ownerEmail,
2501
+ cleanupClaim.ownerEmail,
2502
+ ),
2503
+ cleanupClaim.orgId
2504
+ ? eq(
2505
+ schema.uploadedAssetCleanupOutbox.orgId,
2506
+ cleanupClaim.orgId,
2507
+ )
2508
+ : isNull(schema.uploadedAssetCleanupOutbox.orgId),
2509
+ eq(schema.uploadedAssetCleanupOutbox.state, "claimed"),
2510
+ eq(
2511
+ schema.uploadedAssetCleanupOutbox.leaseToken,
2512
+ leaseToken,
2513
+ ),
2514
+ ),
2515
+ )
2516
+ .returning({ id: schema.uploadedAssetCleanupOutbox.id }),
2517
+ );
2518
+ if (!registeredRow) {
2519
+ summary.failedAttempts += 1;
2520
+ return;
2521
+ }
2522
+ const removed = await runWithRequestContext(requestContext, () =>
2523
+ clearRegisteredUploadedAsset(cleanupScope, leaseToken),
2524
+ );
2525
+ if (!removed) summary.failedAttempts += 1;
2526
+ return;
2527
+ }
2528
+
2529
+ if (
2530
+ cleanupClaim.intent === "delete" &&
2531
+ asset &&
2532
+ asset.url === cleanupClaim.url &&
2533
+ asset.provider === cleanupClaim.provider &&
2534
+ asset.providerObjectId === cleanupClaim.providerObjectId &&
2535
+ asset.deckId
2536
+ ) {
2537
+ summary.failedAttempts += 1;
2538
+ await retry();
2539
+ return;
2540
+ }
2541
+
2542
+ assertCleanupRunActive(options);
2543
+ const finishCleanup = async (transaction: SlidesTransaction) => {
2544
+ await deleteUploadedAssetRowForCleanup(transaction, cleanupClaim);
2545
+ const removed = await finishOwnedUploadedAssetCleanup(
2546
+ transaction,
2547
+ cleanupScope,
2548
+ leaseToken,
2549
+ );
2550
+ if (!removed) {
2551
+ throw new Error("Cleanup outbox claim was no longer current.");
2552
+ }
2553
+ };
2554
+ const deletion = await runWithRequestContext(requestContext, () =>
2555
+ deleteUploadedAssetObject({
2556
+ identity: {
2557
+ url: cleanupClaim.url,
2558
+ provider: cleanupClaim.provider,
2559
+ providerObjectId: cleanupClaim.providerObjectId,
2560
+ },
2561
+ excludeAssetId: cleanupClaim.id,
2562
+ lease: { ...cleanupScope, token: leaseToken },
2563
+ deleteObject: () => {
2564
+ assertCleanupRunActive(options);
2565
+ return deleteUploadedFile(cleanupClaim.provider, {
2566
+ id: cleanupClaim.providerObjectId ?? undefined,
2567
+ url: cleanupClaim.url,
2568
+ });
2569
+ },
2570
+ onShared: finishCleanup,
2571
+ onAlreadyDeleted: finishCleanup,
2572
+ onDeleted: finishCleanup,
2573
+ }),
2574
+ );
2575
+ if (deletion.status === "stale") return;
2576
+ if (deletion.status === "failed") {
2577
+ summary.failedAttempts += 1;
2578
+ await retry();
2579
+ return;
2580
+ }
2581
+ } catch (error) {
2582
+ summary.failedAttempts += 1;
2583
+ console.warn("[slides-upload] asset cleanup retry failed", {
2584
+ assetId: claimed.id,
2585
+ error: error instanceof Error ? error.message : String(error),
2586
+ });
2587
+ await retry();
2588
+ }
2589
+ }),
2590
+ );
2591
+ }
2592
+
2593
+ assertCleanupRunActive(options);
2594
+ return summary;
2595
+ }
2596
+
2597
+ const MAX_ORPHANED_ASSET_CLEANUPS_PER_SWEEP = 50;
2598
+
2599
+ export async function sweepOrphanedUploadedAssetObjects({
2600
+ deadlineAt,
2601
+ signal,
2602
+ }: RecurringSweepContext): Promise<void> {
2603
+ assertCleanupRunActive({ deadlineAt, signal });
2604
+ const summary = await reapOrphanedUploadedAssetObjects(undefined, undefined, {
2605
+ limit: MAX_ORPHANED_ASSET_CLEANUPS_PER_SWEEP,
2606
+ deadlineAt,
2607
+ signal,
2608
+ concurrency: 4,
2609
+ });
2610
+ assertCleanupRunActive({ deadlineAt, signal });
2611
+ if (summary.failedAttempts > 0) {
2612
+ throw new Error(
2613
+ `${summary.failedAttempts} uploaded asset cleanup attempt(s) failed; outbox records remain available for retry.`,
2614
+ );
2615
+ }
2616
+ }
2617
+
2618
+ export const reapOrphanedVideoAssetObjects = reapOrphanedUploadedAssetObjects;
2619
+
2620
+ export async function uploadVideoAsset(args: {
2621
+ email: string;
2622
+ orgId?: string | null;
2623
+ deckId?: string;
2624
+ originalName: string;
2625
+ data: Uint8Array;
2626
+ uploadSessionId?: string;
2627
+ }): Promise<UploadedVideoAsset> {
2628
+ if (args.data.length > MAX_VIDEO_ASSET_FILE_SIZE) {
2629
+ throw uploadedVideoError("video_upload_too_large");
2630
+ }
2631
+ if (!canSaveAsUploadedVideoAsset(args)) {
2632
+ throw uploadedVideoError("video_upload_invalid_format");
2633
+ }
2634
+
2635
+ try {
2636
+ await assertDeckAssetUploadScope({
2637
+ deckId: args.deckId,
2638
+ email: args.email,
2639
+ orgId: args.orgId,
2640
+ });
2641
+ } catch (error) {
2642
+ const errorCode = (error as { errorCode?: unknown })?.errorCode;
2643
+ if (
2644
+ typeof errorCode === "string" &&
2645
+ Object.hasOwn(videoUploadFailures, errorCode)
2646
+ ) {
2647
+ throw error;
2648
+ }
2649
+ if ((error as { statusCode?: unknown })?.statusCode === 403) {
2650
+ throw uploadedVideoError("asset_deck_scope_forbidden");
2651
+ }
2652
+ throw uploadedVideoError("video_upload_scope_check_failed");
2653
+ }
2654
+
2655
+ const orgId =
2656
+ args.orgId === undefined ? getRequestOrgId() : (args.orgId ?? undefined);
2657
+ const requestContext = {
2658
+ userEmail: args.email,
2659
+ ...(orgId === undefined ? {} : { orgId }),
2660
+ };
2661
+ await reapOrphanedUploadedAssetObjects(args.email, orgId, {
2662
+ limit: MAX_ORPHANED_ASSET_CLEANUPS_PER_UPLOAD,
2663
+ });
2664
+
2665
+ if (args.uploadSessionId) {
2666
+ const completed = await findUploadedVideoAssetForSession(
2667
+ args.email,
2668
+ args.uploadSessionId,
2669
+ orgId ?? null,
2670
+ );
2671
+ if (completed) return completed;
2672
+ }
2673
+
2674
+ const id = nanoid();
2675
+ const cleanupScope: UploadedAssetCleanupScope = {
2676
+ assetId: id,
2677
+ ownerEmail: args.email,
2678
+ orgId: orgId ?? null,
2679
+ };
2680
+ let cleanupToken: string;
2681
+ try {
2682
+ cleanupToken = await reserveUploadedAssetRegistration(cleanupScope);
2683
+ } catch (error) {
2684
+ console.warn("[slides-upload] could not reserve video cleanup", {
2685
+ assetId: id,
2686
+ error: error instanceof Error ? error.message : String(error),
2687
+ });
2688
+ throw uploadedVideoError("video_asset_registration_failed");
2689
+ }
2690
+
2691
+ const ext = path.extname(args.originalName).toLowerCase();
2692
+ const mimeType = ext === ".mp4" ? "video/mp4" : "video/webm";
2693
+ let result: Awaited<ReturnType<typeof uploadFile>>;
2694
+ try {
2695
+ result = await runWithRequestContext(requestContext, () =>
2696
+ uploadFile({
2697
+ data: args.data,
2698
+ filename: args.originalName,
2699
+ mimeType,
2700
+ ownerEmail: args.email,
2701
+ }),
2702
+ );
2703
+ } catch {
2704
+ try {
2705
+ await parkUnresolvedUploadedAssetRegistration(cleanupScope, cleanupToken);
2706
+ } catch (error) {
2707
+ console.warn(
2708
+ "[slides-upload] unresolved video reservation could not be parked",
2709
+ {
2710
+ assetId: id,
2711
+ error: error instanceof Error ? error.message : String(error),
2712
+ },
2713
+ );
2714
+ }
2715
+ throw uploadedVideoError("video_upload_storage_failed");
2716
+ }
2717
+
2718
+ if (!result) {
2719
+ try {
2720
+ await cancelUploadedAssetRegistration(cleanupScope, cleanupToken);
2721
+ } catch (error) {
2722
+ console.warn(
2723
+ "[slides-upload] empty video reservation could not be cleared",
2724
+ {
2725
+ assetId: id,
2726
+ error: error instanceof Error ? error.message : String(error),
2727
+ },
2728
+ );
2729
+ }
2730
+ throw uploadedVideoError("video_upload_storage_unavailable");
2731
+ }
2732
+
2733
+ const asset: UploadedVideoAsset = {
2734
+ id,
2735
+ url: result.url,
2736
+ filename: args.originalName,
2737
+ type: mimeType,
2738
+ size: args.data.length,
2739
+ provider: result.provider,
2740
+ };
2741
+ const cleanupInput: UploadedAssetCleanupOutboxInput = {
2742
+ assetId: asset.id,
2743
+ ownerEmail: args.email,
2744
+ orgId: orgId ?? null,
2745
+ provider: result.provider,
2746
+ providerObjectId: result.id ?? null,
2747
+ url: result.url,
2748
+ intent: "registration",
2749
+ };
2750
+ try {
2751
+ const recorded = await recordUploadedAssetCleanupObject(
2752
+ cleanupInput,
2753
+ cleanupToken,
2754
+ );
2755
+ if (!recorded) {
2756
+ throw new Error("Uploaded asset cleanup reservation was claimed.");
2757
+ }
2758
+ } catch (error) {
2759
+ try {
2760
+ const deletionToken = await claimFailedUploadedAssetRegistration(
2761
+ cleanupInput,
2762
+ cleanupToken,
2763
+ );
2764
+ if (deletionToken) {
2765
+ await deleteFailedUploadUsingReservation({
2766
+ input: cleanupInput,
2767
+ token: deletionToken,
2768
+ requestContext,
2769
+ mediaType: "video",
2770
+ });
2771
+ } else {
2772
+ await parkUnresolvedUploadedAssetRegistration(
2773
+ cleanupScope,
2774
+ cleanupToken,
2775
+ );
2776
+ }
2777
+ } catch (queueError) {
2778
+ console.warn("[slides-upload] video cleanup could not be queued", {
2779
+ assetId: asset.id,
2780
+ error:
2781
+ queueError instanceof Error ? queueError.message : String(queueError),
2782
+ });
2783
+ }
2784
+ console.warn("[slides-upload] could not record video cleanup metadata", {
2785
+ assetId: asset.id,
2786
+ error: error instanceof Error ? error.message : String(error),
2787
+ });
2788
+ throw uploadedVideoError("video_asset_registration_failed");
2789
+ }
2790
+
2791
+ try {
2792
+ await registerUploadedAsset({
2793
+ row: {
2794
+ id: asset.id,
2795
+ filename: asset.filename,
2796
+ url: asset.url,
2797
+ type: asset.type,
2798
+ size: asset.size,
2799
+ provider: asset.provider ?? null,
2800
+ providerObjectId: result.id ?? null,
2801
+ uploadSessionId: args.uploadSessionId ?? null,
2802
+ deckId: args.deckId ?? null,
2803
+ orgId: orgId ?? null,
2804
+ ownerEmail: args.email,
2805
+ createdAt: new Date().toISOString(),
2806
+ },
2807
+ deckId: args.deckId,
2808
+ email: args.email,
2809
+ orgId,
2810
+ cleanupToken,
2811
+ cleanupInput,
2812
+ });
2813
+ } catch (registrationError) {
2814
+ let committedAsset: UploadedVideoAsset | null = null;
2815
+ let assetLookupFailed = false;
2816
+ try {
2817
+ committedAsset = await findUploadedVideoAssetById(
2818
+ args.email,
2819
+ asset.id,
2820
+ orgId ?? null,
2821
+ );
2822
+ } catch {
2823
+ assetLookupFailed = true;
2824
+ }
2825
+ if (committedAsset) {
2826
+ try {
2827
+ await clearRegisteredUploadedAsset(cleanupInput, cleanupToken);
2828
+ } catch (error) {
2829
+ console.warn("[slides-upload] registered video cleanup remains", {
2830
+ assetId: asset.id,
2831
+ error: error instanceof Error ? error.message : String(error),
2832
+ });
2833
+ }
2834
+ return committedAsset;
2835
+ }
2836
+
2837
+ let completed: UploadedVideoAsset | null = null;
2838
+ let sessionLookupFailed = false;
2839
+ if (args.uploadSessionId) {
2840
+ try {
2841
+ completed = await findUploadedVideoAssetForSession(
2842
+ args.email,
2843
+ args.uploadSessionId,
2844
+ orgId ?? null,
2845
+ );
2846
+ } catch {
2847
+ sessionLookupFailed = true;
2848
+ }
2849
+ }
2850
+
2851
+ if (completed?.id === asset.id) {
2852
+ try {
2853
+ await clearRegisteredUploadedAsset(cleanupInput, cleanupToken);
2854
+ } catch (error) {
2855
+ console.warn("[slides-upload] registered video cleanup remains", {
2856
+ assetId: asset.id,
2857
+ error: error instanceof Error ? error.message : String(error),
2858
+ });
2859
+ }
2860
+ return completed;
2861
+ }
2862
+ const candidate = (registrationError as { errorCode?: unknown })?.errorCode;
2863
+ const failureCode =
2864
+ typeof candidate === "string" &&
2865
+ Object.hasOwn(videoUploadFailures, candidate)
2866
+ ? (candidate as VideoUploadFailureCode)
2867
+ : "video_asset_registration_failed";
2868
+
2869
+ if (assetLookupFailed || sessionLookupFailed) {
2870
+ console.warn("[slides-upload] video registration outcome is uncertain", {
2871
+ assetId: asset.id,
2872
+ uploadSessionId: args.uploadSessionId,
2873
+ });
2874
+ throw uploadedVideoError(failureCode);
2875
+ }
2876
+
2877
+ try {
2878
+ const deletionToken = await claimFailedUploadedAssetRegistration(
2879
+ cleanupInput,
2880
+ cleanupToken,
2881
+ );
2882
+ if (deletionToken) {
2883
+ await deleteFailedUploadUsingReservation({
2884
+ input: cleanupInput,
2885
+ token: deletionToken,
2886
+ requestContext,
2887
+ mediaType: "video",
2888
+ });
2889
+ }
2890
+ } catch (error) {
2891
+ console.warn("[slides-upload] video cleanup remains queued", {
2892
+ assetId: asset.id,
2893
+ error: error instanceof Error ? error.message : String(error),
2894
+ });
2895
+ }
2896
+
2897
+ if (completed) return completed;
2898
+ throw uploadedVideoError(failureCode);
2899
+ }
2900
+
2901
+ try {
2902
+ await clearRegisteredUploadedAsset(cleanupInput, cleanupToken);
2903
+ } catch (error) {
2904
+ console.warn("[slides-upload] registered video cleanup remains", {
2905
+ assetId: asset.id,
2906
+ error: error instanceof Error ? error.message : String(error),
2907
+ });
2908
+ }
2909
+ return asset;
2910
+ }
2911
+
2912
+ export async function findUploadedVideoAssetForSession(
2913
+ email: string,
2914
+ uploadSessionId: string,
2915
+ requestedOrgId?: string | null,
2916
+ ): Promise<UploadedVideoAsset | null> {
2917
+ const orgId =
2918
+ requestedOrgId === undefined
2919
+ ? getRequestOrgId()
2920
+ : (requestedOrgId ?? undefined);
2921
+ const [asset] = await getDb()
2922
+ .select({
2923
+ id: schema.uploadedAssets.id,
2924
+ filename: schema.uploadedAssets.filename,
2925
+ url: schema.uploadedAssets.url,
2926
+ type: schema.uploadedAssets.type,
2927
+ size: schema.uploadedAssets.size,
2928
+ provider: schema.uploadedAssets.provider,
2929
+ })
2930
+ .from(schema.uploadedAssets)
2931
+ .where(
2932
+ and(
2933
+ eq(schema.uploadedAssets.ownerEmail, email),
2934
+ eq(schema.uploadedAssets.uploadSessionId, uploadSessionId),
2935
+ orgId
2936
+ ? eq(schema.uploadedAssets.orgId, orgId)
2937
+ : isNull(schema.uploadedAssets.orgId),
2938
+ ),
2939
+ )
2940
+ .limit(1);
2941
+
2942
+ if (!asset || !asset.type.startsWith("video/")) return null;
2943
+ return {
2944
+ id: asset.id,
2945
+ filename: asset.filename,
2946
+ url: asset.url,
2947
+ type: asset.type,
2948
+ size: asset.size,
2949
+ provider: asset.provider ?? undefined,
2950
+ };
2951
+ }
2952
+
2953
+ export const uploadVideoAssetHandler = defineEventHandler(async (event) => {
2954
+ const { session, statusCode } = await requireSession(event);
2955
+ if (!session) {
2956
+ const code =
2957
+ statusCode && statusCode >= 500
2958
+ ? "video_upload_session_unavailable"
2959
+ : "video_upload_auth_required";
2960
+ return videoUploadFailureResponse(event, uploadedVideoError(code));
2961
+ }
2962
+
2963
+ let parts;
2964
+ try {
2965
+ parts = await readBoundedMultipartFormData(
2966
+ event,
2967
+ MAX_VIDEO_ASSET_REQUEST_SIZE,
2968
+ );
2969
+ } catch (error) {
2970
+ return multipartUploadError(event, error, "Video upload failed", "video");
2971
+ }
2972
+ const filePart = parts?.find((part) => part.name === "file");
2973
+ if (!filePart?.data) {
2974
+ return videoUploadFailureResponse(
2975
+ event,
2976
+ uploadedVideoError("video_upload_file_missing"),
2977
+ );
2978
+ }
2979
+ if (filePart.data.length > MAX_VIDEO_ASSET_FILE_SIZE) {
2980
+ return videoUploadFailureResponse(
2981
+ event,
2982
+ uploadedVideoError("video_upload_too_large"),
2983
+ );
2984
+ }
2985
+
2986
+ try {
2987
+ const deckId = parseOptionalDeckId(parts ?? []);
2988
+ return await uploadVideoAsset({
2989
+ email: session.email,
2990
+ orgId: session.orgId,
2991
+ deckId,
2992
+ originalName: filePart.filename || "video",
2993
+ data: filePart.data,
2994
+ });
2995
+ } catch (error) {
2996
+ return videoUploadFailureResponse(event, error);
2997
+ }
2998
+ });
2999
+
3000
+ export const discardUploadedVideoAsset = defineEventHandler(async (event) => {
3001
+ const { session, error } = await requireSession(event);
3002
+ if (!session) return { error };
3003
+
3004
+ const rawId = getQuery(event).id;
3005
+ const id = typeof rawId === "string" ? rawId.trim() : "";
3006
+ if (!id) {
3007
+ setResponseStatus(event, 400);
3008
+ return { error: "Video asset id is required" };
3009
+ }
3010
+
3011
+ const db = getDb();
3012
+ const requestContext = {
3013
+ userEmail: session.email,
3014
+ ...(session.orgId ? { orgId: session.orgId } : {}),
3015
+ };
3016
+ let result: {
3017
+ asset: {
3018
+ id: string;
3019
+ url: string;
3020
+ provider: string | null;
3021
+ providerObjectId: string | null;
3022
+ type: string;
3023
+ orgId: string | null;
3024
+ deckId: string | null;
3025
+ };
3026
+ token: string;
3027
+ } | null;
3028
+ try {
3029
+ result = await db.transaction(async (transaction) => {
3030
+ const [asset] = await transaction
3031
+ .select({
3032
+ id: schema.uploadedAssets.id,
3033
+ url: schema.uploadedAssets.url,
3034
+ provider: schema.uploadedAssets.provider,
3035
+ providerObjectId: schema.uploadedAssets.providerObjectId,
3036
+ type: schema.uploadedAssets.type,
3037
+ orgId: schema.uploadedAssets.orgId,
3038
+ deckId: schema.uploadedAssets.deckId,
3039
+ })
3040
+ .from(schema.uploadedAssets)
3041
+ .where(
3042
+ and(
3043
+ eq(schema.uploadedAssets.id, id),
3044
+ eq(schema.uploadedAssets.ownerEmail, session.email),
3045
+ session.orgId
3046
+ ? eq(schema.uploadedAssets.orgId, session.orgId)
3047
+ : isNull(schema.uploadedAssets.orgId),
3048
+ ),
3049
+ )
3050
+ .for("update");
3051
+ if (!asset) return null;
3052
+ if (!asset.type.startsWith("video/") || !asset.provider) {
3053
+ return { asset, token: "" };
3054
+ }
3055
+ if (asset.deckId) return { asset, token: "attached" };
3056
+ const referencingDeck = await findDeckReferencingAsset(transaction, {
3057
+ id: asset.id,
3058
+ ownerEmail: session.email,
3059
+ orgId: asset.orgId,
3060
+ });
3061
+ if (referencingDeck) return { asset, token: "referenced" };
3062
+ const token = await scheduleUploadedAssetDeletion(transaction, {
3063
+ assetId: asset.id,
3064
+ ownerEmail: session.email,
3065
+ orgId: asset.orgId,
3066
+ provider: asset.provider,
3067
+ providerObjectId: asset.providerObjectId,
3068
+ url: asset.url,
3069
+ });
3070
+ return { asset, token };
3071
+ });
3072
+ } catch (error) {
3073
+ console.warn("[slides-upload] could not queue discarded video cleanup", {
3074
+ assetId: id,
3075
+ error: error instanceof Error ? error.message : String(error),
3076
+ });
3077
+ setResponseStatus(event, 503);
3078
+ return { error: "Could not discard uploaded video" };
3079
+ }
3080
+
3081
+ if (!result) return { success: true };
3082
+ const { asset, token } = result;
3083
+ if (!asset.type.startsWith("video/") || !asset.provider) {
3084
+ setResponseStatus(event, 404);
3085
+ return { error: "Uploaded video asset was not found" };
3086
+ }
3087
+ const provider = asset.provider;
3088
+ if (token === "attached") {
3089
+ setResponseStatus(event, 409);
3090
+ return { error: "Video asset is attached to a presentation" };
3091
+ }
3092
+ if (token === "referenced") {
3093
+ setResponseStatus(event, 409);
3094
+ return { error: "Video asset is used by a saved slide" };
3095
+ }
3096
+
3097
+ const cleanupScope = {
3098
+ assetId: asset.id,
3099
+ ownerEmail: session.email,
3100
+ orgId: asset.orgId,
3101
+ };
3102
+ try {
3103
+ const finishCleanup = async (transaction: SlidesTransaction) => {
3104
+ await deleteUploadedAssetRow(
3105
+ transaction,
3106
+ { ...asset, provider },
3107
+ session.email,
3108
+ );
3109
+ const removed = await finishOwnedUploadedAssetCleanup(
3110
+ transaction,
3111
+ cleanupScope,
3112
+ token,
3113
+ );
3114
+ if (!removed) {
3115
+ throw new Error("Video cleanup lease was no longer current.");
3116
+ }
3117
+ };
3118
+ const deletion = await runWithRequestContext(requestContext, () =>
3119
+ deleteUploadedAssetObject({
3120
+ identity: {
3121
+ url: asset.url,
3122
+ provider,
3123
+ providerObjectId: asset.providerObjectId,
3124
+ },
3125
+ excludeAssetId: asset.id,
3126
+ lease: { ...cleanupScope, token },
3127
+ deleteObject: () =>
3128
+ deleteUploadedFile(provider, {
3129
+ id: asset.providerObjectId ?? undefined,
3130
+ url: asset.url,
3131
+ }),
3132
+ onShared: finishCleanup,
3133
+ onAlreadyDeleted: finishCleanup,
3134
+ onDeleted: finishCleanup,
3135
+ }),
3136
+ );
3137
+ if (deletion.status === "stale") {
3138
+ setResponseStatus(event, 503);
3139
+ return { error: "Could not discard uploaded video" };
3140
+ }
3141
+ if (deletion.status === "failed") throw deletion.error;
3142
+ } catch (error) {
3143
+ try {
3144
+ await runWithRequestContext(requestContext, () =>
3145
+ retryOwnedUploadedAssetCleanup(cleanupScope, token, 1),
3146
+ );
3147
+ } catch (retryError) {
3148
+ console.warn("[slides-upload] video cleanup lease will expire", {
3149
+ assetId: asset.id,
3150
+ error:
3151
+ retryError instanceof Error ? retryError.message : String(retryError),
3152
+ });
3153
+ }
3154
+ console.warn("[slides-upload] discarded video cleanup remains queued", {
3155
+ assetId: asset.id,
3156
+ error: error instanceof Error ? error.message : String(error),
3157
+ });
3158
+ setResponseStatus(event, 503);
3159
+ return { error: "Could not discard uploaded video" };
3160
+ }
3161
+
3162
+ return { success: true };
3163
+ });
3164
+
3165
+ export const uploadAsset = defineEventHandler(async (event) => {
3166
+ const { session, statusCode } = await requireSession(event);
3167
+ if (!session) {
3168
+ const code =
3169
+ statusCode && statusCode >= 500
3170
+ ? "image_upload_session_unavailable"
3171
+ : "image_upload_auth_required";
3172
+ return imageUploadFailureResponse(event, imageUploadFailure(code));
3173
+ }
3174
+
3175
+ let parts;
3176
+ try {
3177
+ parts = await readBoundedMultipartFormData(event, MAX_ASSET_REQUEST_SIZE);
3178
+ } catch (error) {
3179
+ return multipartUploadError(event, error, "Image upload failed", "image");
3180
+ }
3181
+ const filePart = parts?.find((p) => p.name === "file");
3182
+ if (!filePart || !filePart.data) {
3183
+ return imageUploadFailureResponse(
3184
+ event,
3185
+ imageUploadFailure("image_upload_file_missing"),
3186
+ );
3187
+ }
3188
+
3189
+ if (filePart.data.length > MAX_ASSET_FILE_SIZE) {
3190
+ return imageUploadFailureResponse(
3191
+ event,
3192
+ imageUploadFailure("image_upload_too_large"),
3193
+ );
3194
+ }
3195
+
3196
+ try {
3197
+ const deckId = parseOptionalDeckId(parts ?? []);
3198
+ const asset = await uploadImageAsset({
3199
+ email: session.email,
3200
+ orgId: session.orgId,
3201
+ deckId,
3202
+ originalName: filePart.filename || "upload",
3203
+ data: filePart.data,
3204
+ type: filePart.type,
3205
+ });
3206
+ return {
3207
+ id: asset.id,
3208
+ url: asset.url,
3209
+ filename: asset.filename,
3210
+ type: asset.type,
3211
+ size: asset.size,
3212
+ provider: asset.provider,
3213
+ };
3214
+ } catch (error) {
3215
+ return imageUploadFailureResponse(event, error);
3216
+ }
3217
+ });