@elytracms/studio 0.0.92 → 0.0.94
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.
- package/convex/analytics-cost.test.ts +817 -0
- package/convex/asset-alt-migration.test.ts +127 -0
- package/convex/asset-replace.test.ts +168 -0
- package/convex/asset-revalidate.test.ts +6 -6
- package/convex/assets.ts +41 -5
- package/convex/auth.ts +5 -2
- package/convex/authority.ts +35 -11
- package/convex/cms.ts +25 -11
- package/convex/contract-parity.test.ts +25 -3
- package/convex/delivery.ts +12 -9
- package/convex/document-revalidate.test.ts +10 -12
- package/convex/guard.ts +9 -7
- package/convex/http-routes.test.ts +18 -8
- package/convex/http.ts +23 -17
- package/convex/ingest.test.ts +326 -0
- package/convex/ingest.ts +281 -0
- package/convex/invites.ts +13 -5
- package/convex/members.ts +10 -4
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/rail-write-parity.test.ts +11 -8
- package/convex/record-functions.ts +325 -0
- package/convex/records.test.ts +156 -0
- package/convex/redirect-migration.test.ts +205 -0
- package/convex/redirect-revalidate.test.ts +5 -1
- package/convex/redirects.ts +120 -79
- package/convex/role-policy.test.ts +315 -0
- package/convex/roles.ts +83 -0
- package/convex/schema.ts +36 -0
- package/convex/triggers.ts +21 -1
- package/dist/addon.d.ts +71 -0
- package/dist/addon.js +67 -0
- package/dist/addon.js.map +1 -0
- package/dist/components/account/account-settings.js +2 -2
- package/dist/components/account/account-settings.js.map +1 -1
- package/dist/components/account/credential-panel.js +2 -2
- package/dist/components/account/credential-panel.js.map +1 -1
- package/dist/components/account/self-account-schema.d.ts +6 -15
- package/dist/components/account/self-account-schema.js +52 -34
- package/dist/components/account/self-account-schema.js.map +1 -1
- package/dist/components/account/self-account-view-source.d.ts +1 -1
- package/dist/components/account/self-account-view-source.js +6 -4
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +6 -6
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/ai-cms-panel.js +2 -2
- package/dist/components/ai/ai-cms-panel.js.map +1 -1
- package/dist/components/ai/ai-panel-toggle.js +4 -4
- package/dist/components/ai/ai-panel-toggle.js.map +1 -1
- package/dist/components/ai/chat-ui.js +2 -2
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +2 -2
- package/dist/components/ai/proposal-card.js +4 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.js +2 -2
- package/dist/components/ai/proposal-issues.js.map +1 -1
- package/dist/components/ai/translate-document-action.js +2 -2
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.js +10 -8
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -0
- package/dist/components/ai/translate-skipped.js +46 -0
- package/dist/components/ai/translate-skipped.js.map +1 -0
- package/dist/components/app-views/default-modules.d.ts +50 -2
- package/dist/components/app-views/default-modules.js +65 -20
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +53 -3
- package/dist/components/app-views/module.js +14 -7
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.js +1 -1
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +11 -2
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-detail.js +20 -10
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-grid.js +2 -2
- package/dist/components/assets/asset-grid.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +15 -4
- package/dist/components/assets/asset-metadata.js +15 -2
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +4 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +0 -8
- package/dist/components/assets/asset-selection.js +4 -23
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +6 -5
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +5 -5
- package/dist/components/assets/assets-collection-view-source.js +9 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-module.js +1 -1
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +16 -2
- package/dist/components/assets/assets-store.js +28 -6
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.js +1 -1
- package/dist/components/assets/assets-view-preference.js.map +1 -1
- package/dist/components/assets/replace-file.d.ts +22 -0
- package/dist/components/assets/replace-file.js +59 -0
- package/dist/components/assets/replace-file.js.map +1 -0
- package/dist/components/auth-context.d.ts +1 -1
- package/dist/components/auth-context.js +6 -6
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +15 -27
- package/dist/components/builder/blocks-model.js +38 -44
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +8 -4
- package/dist/components/builder/blocks-panel.js +10 -9
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +104 -15
- package/dist/components/builder/builder-store.js +157 -32
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-body.d.ts +30 -0
- package/dist/components/builder/canvas-body.js +38 -0
- package/dist/components/builder/canvas-body.js.map +1 -0
- package/dist/components/builder/canvas-breakpoints.js +1 -1
- package/dist/components/builder/canvas-breakpoints.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +7 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +9 -9
- package/dist/components/builder/canvas-dnd.js +6 -6
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +28 -20
- package/dist/components/builder/canvas-render-context.js +46 -47
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-runs.d.ts +45 -0
- package/dist/components/builder/canvas-runs.js +51 -0
- package/dist/components/builder/canvas-runs.js.map +1 -0
- package/dist/components/builder/canvas-theme-notice.js +2 -2
- package/dist/components/builder/canvas-theme-notice.js.map +1 -1
- package/dist/components/builder/canvas.js +35 -8
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +16 -1
- package/dist/components/builder/clipboard-actions.js +48 -11
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +4 -16
- package/dist/components/builder/composition-field-editor.js +32 -43
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/delete-key.d.ts +5 -0
- package/dist/components/builder/delete-key.js +4 -0
- package/dist/components/builder/delete-key.js.map +1 -1
- package/dist/components/builder/drag-session.d.ts +1 -1
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.js +1 -1
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +18 -26
- package/dist/components/builder/insert-context.js +47 -94
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.js +1 -1
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-title.js +1 -1
- package/dist/components/builder/node-title.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +4 -4
- package/dist/components/builder/page-tree.js +8 -8
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.d.ts +18 -0
- package/dist/components/builder/placement-refusal.js +46 -0
- package/dist/components/builder/placement-refusal.js.map +1 -0
- package/dist/components/builder/property-inspector.js +8 -4
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +68 -30
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -1
- package/dist/components/builder/reuse.js +70 -3
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/rich-text-toolbar.js +1 -1
- package/dist/components/builder/rich-text-toolbar.js.map +1 -1
- package/dist/components/builder/run-inspector.d.ts +11 -0
- package/dist/components/builder/run-inspector.js +33 -0
- package/dist/components/builder/run-inspector.js.map +1 -0
- package/dist/components/builder/selection-overlay.js +3 -3
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +2 -2
- package/dist/components/builder/spacing-handles.js +3 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +184 -60
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +16 -0
- package/dist/components/cms/asset-picker-core.js +20 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +14 -20
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +4 -20
- package/dist/components/cms/bulk-actions.js +5 -26
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +2 -2
- package/dist/components/cms/bulk-bar.js +2 -2
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +1 -1
- package/dist/components/cms/cell-edit.js +1 -1
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.d.ts +1 -1
- package/dist/components/cms/cell.js +3 -3
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-index.js +2 -2
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/cms/cms-labels.js +2 -2
- package/dist/components/cms/cms-labels.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +7 -6
- package/dist/components/cms/cms-store.js +23 -43
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -7
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.d.ts +5 -2
- package/dist/components/cms/detail-editor.js +20 -15
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.js +1 -1
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/doc-state.js +1 -1
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +17 -12
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/document-editor-context.js +1 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/document-staging.js +1 -1
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/document-url-path.js +2 -2
- package/dist/components/cms/document-url-path.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +1 -1
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/editable-cell.js +3 -3
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-anchors.d.ts +7 -4
- package/dist/components/cms/field-anchors.js +9 -7
- package/dist/components/cms/field-anchors.js.map +1 -1
- package/dist/components/cms/field-collaboration.d.ts +1 -1
- package/dist/components/cms/field-collaboration.js +1 -1
- package/dist/components/cms/field-collaboration.js.map +1 -1
- package/dist/components/cms/field-connection.d.ts +1 -1
- package/dist/components/cms/field-connection.js +2 -2
- package/dist/components/cms/field-connection.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +15 -1
- package/dist/components/cms/field-control-lib.js +23 -4
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +4 -4
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.d.ts +1 -1
- package/dist/components/cms/field-renderer.js +2 -2
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/filter-builder.d.ts +1 -1
- package/dist/components/cms/filter-builder.js +3 -3
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -32
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/import-export.js +2 -2
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/issue-copy.js +1 -1
- package/dist/components/cms/issue-copy.js.map +1 -1
- package/dist/components/cms/issues-pane.js +3 -3
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/object-array-field.js +6 -6
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +7 -2
- package/dist/components/cms/prop-fields.js +14 -7
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.d.ts +1 -1
- package/dist/components/cms/reference-confirm.js +2 -2
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/relation-picker.js +2 -2
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/rich-text/block-style.js +1 -1
- package/dist/components/cms/rich-text/block-style.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.js +4 -4
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +3 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-picker.js +3 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.js +3 -3
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.js +1 -1
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
- package/dist/components/cms/rich-text/font-size-control.js +2 -2
- package/dist/components/cms/rich-text/font-size-control.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +20 -9
- package/dist/components/cms/rich-text/insert.js +25 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.js +1 -1
- package/dist/components/cms/rich-text/lazy.js.map +1 -1
- package/dist/components/cms/rich-text/link-control.js +2 -2
- package/dist/components/cms/rich-text/link-control.js.map +1 -1
- package/dist/components/cms/rich-text/slash-command.js +3 -3
- package/dist/components/cms/rich-text/slash-command.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.js +1 -1
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/rich-text/unknown-node.js +2 -2
- package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
- package/dist/components/cms/rich-text-preview.js +2 -2
- package/dist/components/cms/rich-text-preview.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +1 -1
- package/dist/components/cms/row-actions.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.d.ts +1 -1
- package/dist/components/cms/scalar-value-editor.js +2 -2
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/cms/use-discard-controller.d.ts +1 -1
- package/dist/components/cms/use-discard-controller.js +1 -1
- package/dist/components/cms/use-discard-controller.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +7 -1
- package/dist/components/cms/use-document-draft.js +16 -2
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-document-versions.js +1 -1
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-field-focus.d.ts +1 -1
- package/dist/components/cms/use-field-focus.js +2 -2
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +1 -1
- package/dist/components/cms/use-live-url.js +3 -3
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +2 -3
- package/dist/components/cms/use-publish-controller.js +8 -20
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +1 -1
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/use-restore-version.d.ts +1 -1
- package/dist/components/cms/use-restore-version.js +1 -1
- package/dist/components/cms/use-restore-version.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.js +1 -1
- package/dist/components/cms/version-compare-lazy.js.map +1 -1
- package/dist/components/cms/version-compare.js +1 -1
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +11 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.js +3 -3
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +5 -3
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/collection-view/collection-view-context.d.ts +1 -1
- package/dist/components/collection-view/collection-view-context.js +1 -1
- package/dist/components/collection-view/collection-view-context.js.map +1 -1
- package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
- package/dist/components/collection-view/collection-view-surface.js +18 -0
- package/dist/components/collection-view/collection-view-surface.js.map +1 -0
- package/dist/components/components/component-detail.js +10 -10
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +2 -2
- package/dist/components/components/component-preview.js +5 -5
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/component-status.d.ts +1 -1
- package/dist/components/components/component-status.js +1 -1
- package/dist/components/components/component-status.js.map +1 -1
- package/dist/components/components/components-index.js +2 -2
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/field-inspector.js +1 -1
- package/dist/components/components/field-inspector.js.map +1 -1
- package/dist/components/components/registry-browser.js +3 -3
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +1 -1
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +4 -3
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +10 -10
- package/dist/components/components/transfer.js +13 -12
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/current-user-menu.js +1 -1
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/delivery-health-banner.js +2 -2
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +3 -3
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/dev-login-badge.js +2 -2
- package/dist/components/dev-login-badge.js.map +1 -1
- package/dist/components/guards.js +1 -1
- package/dist/components/guards.js.map +1 -1
- package/dist/components/inspector/document-panes.d.ts +54 -0
- package/dist/components/inspector/document-panes.js +41 -0
- package/dist/components/inspector/document-panes.js.map +1 -0
- package/dist/components/inspector/inspector-sidebar.js +1 -1
- package/dist/components/inspector/inspector-sidebar.js.map +1 -1
- package/dist/components/nav-issues-registry.d.ts +53 -0
- package/dist/components/nav-issues-registry.js +69 -0
- package/dist/components/nav-issues-registry.js.map +1 -0
- package/dist/components/nav-issues.js +4 -4
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/operation-error.d.ts +1 -1
- package/dist/components/operation-error.js +4 -4
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/playground-badge.js +2 -2
- package/dist/components/playground-badge.js.map +1 -1
- package/dist/components/project-list.js +3 -3
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +2 -2
- package/dist/components/project-session.js +5 -5
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js +2 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.js +1 -1
- package/dist/components/rail-store-status.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +6 -5
- package/dist/components/rail-view-source.js +4 -3
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/records/record-lens.d.ts +51 -0
- package/dist/components/records/record-lens.js +156 -0
- package/dist/components/records/record-lens.js.map +1 -0
- package/dist/components/records/record-rail.d.ts +86 -0
- package/dist/components/records/record-rail.js +241 -0
- package/dist/components/records/record-rail.js.map +1 -0
- package/dist/components/redirects/redirects-schema.js +1 -1
- package/dist/components/redirects/redirects-schema.js.map +1 -1
- package/dist/components/references/references-panel.js +2 -2
- package/dist/components/references/references-panel.js.map +1 -1
- package/dist/components/section-nav.js +27 -11
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-state.js +2 -2
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/section-surface.js +2 -2
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/session-expired-overlay.js +3 -3
- package/dist/components/session-expired-overlay.js.map +1 -1
- package/dist/components/settings-form.js +1 -1
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/sign-in-needed-badge.js +1 -1
- package/dist/components/sign-in-needed-badge.js.map +1 -1
- package/dist/components/sign-in-view.d.ts +2 -2
- package/dist/components/sign-in-view.js +5 -5
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/studio-context.js +3 -3
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-project-screen.js +30 -3
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +6 -5
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/theme-context.d.ts +1 -1
- package/dist/components/theme-context.js +2 -2
- package/dist/components/theme-context.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +1 -1
- package/dist/components/use-hydration-phase.js +1 -1
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/components/users/invite-user-bar.d.ts +27 -0
- package/dist/components/users/invite-user-bar.js +25 -9
- package/dist/components/users/invite-user-bar.js.map +1 -1
- package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
- package/dist/components/users/role-policy-drift-issues.js +71 -0
- package/dist/components/users/role-policy-drift-issues.js.map +1 -0
- package/dist/components/users/users-collection-view-source.d.ts +1 -1
- package/dist/components/users/users-collection-view-source.js +4 -2
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/components/users/users-module.js +1 -1
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-schema.d.ts +10 -1
- package/dist/components/users/users-schema.js +45 -25
- package/dist/components/users/users-schema.js.map +1 -1
- package/dist/components/users/users-store.js +3 -3
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/components/workspace-projects.d.ts +1 -1
- package/dist/components/workspace-projects.js +1 -1
- package/dist/components/workspace-projects.js.map +1 -1
- package/dist/lib/ai/apply-content.js +56 -116
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +8 -0
- package/dist/lib/ai/chat-context.js +37 -6
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +6 -0
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +20 -5
- package/dist/lib/ai/constraints.js +125 -33
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +16 -3
- package/dist/lib/ai/fake-provider.js +22 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +3 -1
- package/dist/lib/ai/index.js +3 -1
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/leash.d.ts +34 -0
- package/dist/lib/ai/leash.js +28 -0
- package/dist/lib/ai/leash.js.map +1 -0
- package/dist/lib/ai/node-handler.d.ts +1 -1
- package/dist/lib/ai/node-handler.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +8 -8
- package/dist/lib/ai/rich-text-input.js +2 -1
- package/dist/lib/ai/rich-text-input.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +27 -4
- package/dist/lib/ai/rich-text-translate.js +28 -10
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +13 -13
- package/dist/lib/ai/shape.js +69 -93
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +36 -41
- package/dist/lib/ai/tools.js +83 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +32 -4
- package/dist/lib/ai/translate-document.js +145 -47
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +34 -23
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +15 -12
- package/dist/lib/ai/update-fields.js +112 -188
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +116 -0
- package/dist/lib/ai/writer-form.js +429 -0
- package/dist/lib/ai/writer-form.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +31 -2
- package/dist/lib/assets/upload.js +61 -6
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
- package/dist/lib/clipboard/os-clipboard.js +8 -4
- package/dist/lib/clipboard/os-clipboard.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.js +1 -1
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +10 -9
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +0 -10
- package/dist/lib/cms/field-patch.js +4 -30
- package/dist/lib/cms/field-patch.js.map +1 -1
- package/dist/lib/cms/published-documents.js +1 -1
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +9 -9
- package/dist/lib/cms-views/view-model.js +2 -2
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +14 -5
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.js +3 -2
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/content-session-refresh.d.ts +1 -1
- package/dist/lib/data/content-session-refresh.js.map +1 -1
- package/dist/lib/data/playground-unload-guard.d.ts +1 -1
- package/dist/lib/data/playground-unload-guard.js +1 -1
- package/dist/lib/data/playground-unload-guard.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +25 -31
- package/dist/lib/dnd/drop-rules.js +53 -59
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/i18n.js +1 -1
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +44 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +38 -2
- package/dist/lib/i18n/messages.en.js +51 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/project-runtime-config.d.ts +34 -6
- package/dist/lib/operations/project-runtime-config.js +50 -10
- package/dist/lib/operations/project-runtime-config.js.map +1 -1
- package/dist/lib/operations/publish-with-revision.d.ts +33 -1
- package/dist/lib/operations/publish-with-revision.js +32 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -1
- package/dist/lib/operations/runtime-validator.d.ts +19 -68
- package/dist/lib/operations/runtime-validator.js +12 -138
- package/dist/lib/operations/runtime-validator.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +17 -2
- package/dist/lib/operations/studio-operations.js +18 -11
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +16 -3
- package/dist/lib/persistence/convex-adapter.js +83 -47
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +14 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +4 -2
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/reference-model.js +1 -1
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +1 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +14 -11
- package/dist/lib/workspace/index.js +20 -24
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/mount.d.ts +28 -0
- package/dist/mount.js +27 -0
- package/dist/mount.js.map +1 -0
- package/dist/router/root.js +10 -10
- package/dist/router/root.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +2 -2
- package/package.json +15 -11
- package/dist/components/cms/cms-composition-validators.d.ts +0 -16
- package/dist/components/cms/cms-composition-validators.js +0 -133
- package/dist/components/cms/cms-composition-validators.js.map +0 -1
- package/dist/components/cms/publish-clean.d.ts +0 -28
- package/dist/components/cms/publish-clean.js +0 -38
- package/dist/components/cms/publish-clean.js.map +0 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +0 -53
- package/dist/components/redirects/redirects-collection-view-source.js +0 -146
- package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
- package/dist/components/redirects/redirects-module.d.ts +0 -21
- package/dist/components/redirects/redirects-module.js +0 -40
- package/dist/components/redirects/redirects-module.js.map +0 -1
- package/dist/components/redirects/redirects-store.d.ts +0 -68
- package/dist/components/redirects/redirects-store.js +0 -132
- package/dist/components/redirects/redirects-store.js.map +0 -1
- package/dist/lib/assets/asset-placements.d.ts +0 -28
- package/dist/lib/assets/asset-placements.js +0 -83
- package/dist/lib/assets/asset-placements.js.map +0 -1
package/convex/ingest.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { httpActionGeneric, makeFunctionReference } from 'convex/server'
|
|
2
|
+
import type { FunctionReference, PublicHttpAction } from 'convex/server'
|
|
3
|
+
import { v } from 'convex/values'
|
|
4
|
+
import type { GenericId } from 'convex/values'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import {
|
|
7
|
+
INGEST_MAX_BODY_BYTES,
|
|
8
|
+
INGEST_MAX_UPLOAD_BYTES,
|
|
9
|
+
INGEST_SIGNATURE_HEADER,
|
|
10
|
+
INGEST_UPLOAD_TTL_MS,
|
|
11
|
+
ingestPayloadSchema,
|
|
12
|
+
readIngestReceiverBody,
|
|
13
|
+
verifyIngestSignature,
|
|
14
|
+
} from '@elytracms/core/ingest'
|
|
15
|
+
import type { IngestHandlerResult, IngestPayload, IngestUploadClaim } from '@elytracms/core/ingest'
|
|
16
|
+
import { defineRecordKind, resolveRecordQuery } from '@elytracms/core/persistence'
|
|
17
|
+
import { createUuidIdFactory } from '@elytracms/core/determinism'
|
|
18
|
+
import type { MutationCtx, QueryCtx } from './functions'
|
|
19
|
+
import { findRecordDoc, listRecordWire, saveRecord } from './record-functions'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* EC-617 — the ONE inbound receiver of the public ingest seam (ruling EC-565 § 6c), and the
|
|
23
|
+
* helpers an addon's ingest handler is written with. A factory module: it registers no
|
|
24
|
+
* function itself — `defineStudioHttp({ auth, ingest })` (`http.ts`) mounts
|
|
25
|
+
* {@link ingestReceiver} at `INGEST_RECEIVER_PATH` (`POST /elytra/ingest`, an exact path).
|
|
26
|
+
*
|
|
27
|
+
* The receiver, per request:
|
|
28
|
+
*
|
|
29
|
+
* 1. `ELYTRA_INGEST_SECRET` unset → 401 `{ configured: false }` — closed until configured;
|
|
30
|
+
* 2. body over `INGEST_MAX_BODY_BYTES` → 413;
|
|
31
|
+
* 3. the HMAC (`x-elytra-signature: sha256=…`) verified in constant time with the
|
|
32
|
+
* deployment's secret → 401 on anything else (the site's route signs with the same
|
|
33
|
+
* value, `INGEST_SECRET`);
|
|
34
|
+
* 4. the envelope (`@elytracms/core/ingest`) → 400 with its issues;
|
|
35
|
+
* 5. the doctor's `probe` → 200 `{ ok: true, probe: true }`, nothing dispatched;
|
|
36
|
+
* 6. dispatch by kind to the handler the PROJECT registered — `event` / `submission`, and
|
|
37
|
+
* `upload` to the submission handler (uploads are submission-scoped). A kind with no
|
|
38
|
+
* handler is REFUSED (422, naming the kind) — never dropped: an `event` with no
|
|
39
|
+
* analytics addon must fail visibly (EC-618's counsel gate relies on it);
|
|
40
|
+
* 7. an accepted `upload` gets ONE short-lived storage URL (`ctx.storage.generateUploadUrl`)
|
|
41
|
+
* — the bytes go browser → storage, cross-origin by design; the submission that follows
|
|
42
|
+
* claims the stored file with {@link claimIngestUpload}, which checks it against what the
|
|
43
|
+
* upload request declared.
|
|
44
|
+
*
|
|
45
|
+
* The engine never imports an addon: the handler is an internal-mutation REFERENCE the
|
|
46
|
+
* consumer passes in its one `http.ts` line (`defineStudioHttp({ auth, ingest: {
|
|
47
|
+
* submission: formsIngest } })`), exported by the addon's `./convex` entry.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** What an ingest handler receives: the verified, parsed payload and the receipt time. */
|
|
51
|
+
export const ingestHandlerArgs = { payload: v.any(), receivedAt: v.string() }
|
|
52
|
+
|
|
53
|
+
/** An addon's ingest handler: an INTERNAL mutation (never client-callable). */
|
|
54
|
+
export type IngestHandlerRef = FunctionReference<
|
|
55
|
+
'mutation',
|
|
56
|
+
'internal',
|
|
57
|
+
{ payload: IngestPayload; receivedAt: string },
|
|
58
|
+
IngestHandlerResult
|
|
59
|
+
>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Name an addon's ingest handler for `defineStudioHttp({ ingest })` — its internal mutation by
|
|
63
|
+
* its module path (`ingestHandler('forms:ingest')`), the way every engine function is
|
|
64
|
+
* addressed (a string ref resolved against the consumer's deployment, EC-223). An addon
|
|
65
|
+
* exports the result from its `./convex` entry so the consumer writes no string.
|
|
66
|
+
*/
|
|
67
|
+
export function ingestHandler(name: `${string}:${string}`): IngestHandlerRef {
|
|
68
|
+
return makeFunctionReference<'mutation'>(name) as unknown as IngestHandlerRef
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The handlers a project registers, by payload kind (`upload` goes to `submission`). */
|
|
72
|
+
export interface IngestHandlers {
|
|
73
|
+
event?: IngestHandlerRef
|
|
74
|
+
submission?: IngestHandlerRef
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Re-validate a handler's `payload` argument (it crossed a function boundary as `v.any()`). */
|
|
78
|
+
export function parseIngestPayload(payload: unknown): IngestPayload {
|
|
79
|
+
return ingestPayloadSchema.parse(payload)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function json(status: number, body: Record<string, unknown>): Response {
|
|
83
|
+
return new Response(JSON.stringify(body), {
|
|
84
|
+
status,
|
|
85
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ingestSecret(): string | undefined {
|
|
90
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
|
|
91
|
+
const value = env['ELYTRA_INGEST_SECRET']?.trim()
|
|
92
|
+
return value ? value : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function utf8Length(text: string): number {
|
|
96
|
+
return new TextEncoder().encode(text).byteLength
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The receiver `httpAction` (see the module header). */
|
|
100
|
+
export function ingestReceiver(handlers: IngestHandlers = {}): PublicHttpAction {
|
|
101
|
+
return httpActionGeneric(async (ctx, request) => {
|
|
102
|
+
const secret = ingestSecret()
|
|
103
|
+
if (!secret) return json(401, { error: 'Ingest is not configured on this deployment.', configured: false })
|
|
104
|
+
|
|
105
|
+
const declared = Number(request.headers.get('content-length'))
|
|
106
|
+
if (Number.isFinite(declared) && declared > INGEST_MAX_BODY_BYTES) {
|
|
107
|
+
return json(413, { error: 'The payload is too large.' })
|
|
108
|
+
}
|
|
109
|
+
const raw = await request.text()
|
|
110
|
+
if (utf8Length(raw) > INGEST_MAX_BODY_BYTES) return json(413, { error: 'The payload is too large.' })
|
|
111
|
+
|
|
112
|
+
if (!(await verifyIngestSignature(raw, request.headers.get(INGEST_SIGNATURE_HEADER), secret))) {
|
|
113
|
+
return json(401, { error: 'Invalid or missing ingest signature.', configured: true })
|
|
114
|
+
}
|
|
115
|
+
const read = readIngestReceiverBody(raw)
|
|
116
|
+
if (!read.ok) return json(400, { error: read.message, ...(read.issues ? { issues: read.issues } : {}) })
|
|
117
|
+
const body = read.value
|
|
118
|
+
if (body.kind === 'probe') return json(200, { ok: true, probe: true })
|
|
119
|
+
|
|
120
|
+
const handlerKind = body.kind === 'upload' ? 'submission' : body.kind
|
|
121
|
+
const handler = handlers[handlerKind]
|
|
122
|
+
if (!handler) {
|
|
123
|
+
return json(422, {
|
|
124
|
+
error: `No handler is registered for "${body.kind}" ingest payloads on this deployment — install the addon that handles them and pass its handler to defineStudioHttp({ ingest }).`,
|
|
125
|
+
kind: body.kind,
|
|
126
|
+
handled: false,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
const result = await ctx.runMutation(handler, { payload: body, receivedAt: new Date().toISOString() })
|
|
130
|
+
if (!result.ok) {
|
|
131
|
+
return json(result.status, { error: result.message, ...(result.issues ? { issues: result.issues } : {}) })
|
|
132
|
+
}
|
|
133
|
+
if (body.kind === 'upload') {
|
|
134
|
+
if (!result.uploadId) {
|
|
135
|
+
return json(500, { error: 'The submission handler accepted the upload but recorded no upload intent (recordIngestUpload).' })
|
|
136
|
+
}
|
|
137
|
+
const uploadUrl = await ctx.storage.generateUploadUrl()
|
|
138
|
+
return json(200, { ok: true, uploadId: result.uploadId, uploadUrl, expiresInSeconds: INGEST_UPLOAD_TTL_MS / 1000 })
|
|
139
|
+
}
|
|
140
|
+
return json(200, { ok: true, ...(result.id ? { id: result.id } : {}) })
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Submission-scoped uploads -------------------------------------------------------------
|
|
145
|
+
//
|
|
146
|
+
// The receiver issues a storage URL only for an upload a handler ACCEPTED and RECORDED as an
|
|
147
|
+
// intent (`recordIngestUpload`); a submission CLAIMS it (`claimIngestUpload`), which checks the
|
|
148
|
+
// stored file against the intent. Abuse is bounded by: the route's limiter and bot gate on every
|
|
149
|
+
// URL issued, `INGEST_MAX_UPLOAD_BYTES` per file, the URL's one-hour life, the post-upload check
|
|
150
|
+
// (an over-size or mistyped file is deleted, never kept), and the unclaimed intents an addon can
|
|
151
|
+
// list (`unclaimedIngestUploads`) and surface. Convex returns the storage id to the uploader
|
|
152
|
+
// only, so the bytes of an upload no submission ever claims cannot be located by the engine —
|
|
153
|
+
// that is the documented residue, bounded by the limits above.
|
|
154
|
+
|
|
155
|
+
/** The upload intent — an engine-internal record kind (no public functions). */
|
|
156
|
+
export const ingestUploadRecordKind = defineRecordKind({
|
|
157
|
+
kind: 'ingestUpload',
|
|
158
|
+
module: 'ingest',
|
|
159
|
+
keys: ['target', 'state'],
|
|
160
|
+
time: 'issuedAt',
|
|
161
|
+
fields: {
|
|
162
|
+
target: z.string().min(1),
|
|
163
|
+
field: z.string().min(1),
|
|
164
|
+
contentType: z.string().min(1),
|
|
165
|
+
size: z.number().int().positive(),
|
|
166
|
+
state: z.enum(['issued', 'claimed', 'rejected']),
|
|
167
|
+
issuedAt: z.string().min(1),
|
|
168
|
+
storageId: z.string().optional(),
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
let mintUploadUuid: (() => string) | undefined
|
|
173
|
+
|
|
174
|
+
export interface IngestUploadPolicy {
|
|
175
|
+
/** Largest accepted file in bytes (≤ `INGEST_MAX_UPLOAD_BYTES`). */
|
|
176
|
+
maxBytes?: number
|
|
177
|
+
/** Accepted content types (exact, or `image/*`-style prefixes). Absent → any. */
|
|
178
|
+
contentTypes?: readonly string[]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function typeAllowed(contentType: string, allowed: readonly string[] | undefined): boolean {
|
|
182
|
+
if (!allowed) return true
|
|
183
|
+
return allowed.some((entry) => (entry.endsWith('/*') ? contentType.startsWith(entry.slice(0, -1)) : entry === contentType))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Accept an `upload` payload under the handler's policy and record its intent. Returns the
|
|
188
|
+
* handler result to hand back to the receiver (`{ ok: true, uploadId }`, or a 413/415
|
|
189
|
+
* refusal). Call it from the submission handler's `upload` branch.
|
|
190
|
+
*/
|
|
191
|
+
export async function recordIngestUpload(
|
|
192
|
+
ctx: MutationCtx,
|
|
193
|
+
payload: Extract<IngestPayload, { kind: 'upload' }>,
|
|
194
|
+
receivedAt: string,
|
|
195
|
+
policy: IngestUploadPolicy = {},
|
|
196
|
+
): Promise<IngestHandlerResult> {
|
|
197
|
+
const maxBytes = Math.min(policy.maxBytes ?? INGEST_MAX_UPLOAD_BYTES, INGEST_MAX_UPLOAD_BYTES)
|
|
198
|
+
if (payload.size > maxBytes) return { ok: false, status: 413, message: `The file is larger than ${maxBytes} bytes.` }
|
|
199
|
+
if (!typeAllowed(payload.contentType, policy.contentTypes)) {
|
|
200
|
+
return { ok: false, status: 415, message: `Files of type “${payload.contentType}” are not accepted here.` }
|
|
201
|
+
}
|
|
202
|
+
// EC-578: the one id factory (UUID v4 from crypto.getRandomValues) — never a clock or a counter.
|
|
203
|
+
mintUploadUuid ??= createUuidIdFactory()
|
|
204
|
+
const uploadId = `upload_${mintUploadUuid()}`
|
|
205
|
+
await saveRecord(
|
|
206
|
+
ctx,
|
|
207
|
+
ingestUploadRecordKind,
|
|
208
|
+
uploadId,
|
|
209
|
+
{ ...payloadColumns(payload), state: 'issued', issuedAt: receivedAt },
|
|
210
|
+
{ createdAt: receivedAt, updatedAt: receivedAt },
|
|
211
|
+
)
|
|
212
|
+
return { ok: true, uploadId }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function payloadColumns(payload: Extract<IngestPayload, { kind: 'upload' }>) {
|
|
216
|
+
return { target: payload.target, field: payload.field, contentType: payload.contentType, size: payload.size }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** What a claim yields: the stored file's facts, or a refusal for the handler to return. */
|
|
220
|
+
export type IngestUploadClaimResult =
|
|
221
|
+
| { ok: true; storageId: string; field: string; contentType: string; size: number }
|
|
222
|
+
| { ok: false; status: 409 | 413 | 415 | 422; message: string }
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Claim one upload for a submission to `target`: the intent must exist, be unclaimed,
|
|
226
|
+
* belong to `target`, be younger than `INGEST_UPLOAD_TTL_MS`, and the STORED file must match
|
|
227
|
+
* it (size ≤ declared, same content type). A mismatched file is deleted and the intent
|
|
228
|
+
* marked `rejected` — never kept.
|
|
229
|
+
*/
|
|
230
|
+
export async function claimIngestUpload(
|
|
231
|
+
ctx: MutationCtx,
|
|
232
|
+
claim: IngestUploadClaim,
|
|
233
|
+
target: string,
|
|
234
|
+
receivedAt: string,
|
|
235
|
+
): Promise<IngestUploadClaimResult> {
|
|
236
|
+
const intent = await findRecordDoc(ctx, ingestUploadRecordKind, claim.uploadId)
|
|
237
|
+
const columns = intent?.data as
|
|
238
|
+
| { target: string; field: string; contentType: string; size: number; state: string; issuedAt: string }
|
|
239
|
+
| undefined
|
|
240
|
+
if (!columns || columns.target !== target) return { ok: false, status: 422, message: `Unknown upload “${claim.uploadId}”.` }
|
|
241
|
+
if (columns.state !== 'issued') return { ok: false, status: 409, message: `Upload “${claim.uploadId}” was already claimed.` }
|
|
242
|
+
if (Date.parse(receivedAt) - Date.parse(columns.issuedAt) > INGEST_UPLOAD_TTL_MS) {
|
|
243
|
+
return { ok: false, status: 422, message: `Upload “${claim.uploadId}” expired.` }
|
|
244
|
+
}
|
|
245
|
+
const storageId = claim.storageId as GenericId<'_storage'>
|
|
246
|
+
const file = await ctx.db.system.get(storageId).catch(() => null)
|
|
247
|
+
const stamps = { createdAt: receivedAt, updatedAt: receivedAt }
|
|
248
|
+
if (!file) return { ok: false, status: 422, message: `No stored file “${claim.storageId}”.` }
|
|
249
|
+
const refused =
|
|
250
|
+
file.size > columns.size
|
|
251
|
+
? ({ status: 413, message: 'The stored file is larger than the upload declared.' } as const)
|
|
252
|
+
: (file.contentType ?? '') !== columns.contentType
|
|
253
|
+
? ({ status: 415, message: 'The stored file is not of the declared type.' } as const)
|
|
254
|
+
: undefined
|
|
255
|
+
if (refused) {
|
|
256
|
+
await ctx.storage.delete(storageId)
|
|
257
|
+
await saveRecord(ctx, ingestUploadRecordKind, claim.uploadId, { state: 'rejected', storageId: claim.storageId }, stamps)
|
|
258
|
+
return { ok: false, ...refused }
|
|
259
|
+
}
|
|
260
|
+
await saveRecord(ctx, ingestUploadRecordKind, claim.uploadId, { state: 'claimed', storageId: claim.storageId }, stamps)
|
|
261
|
+
return { ok: true, storageId: claim.storageId, field: columns.field, contentType: columns.contentType, size: file.size }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Upload intents of `target` still unclaimed past their life — the visible orphan finding. */
|
|
265
|
+
export async function unclaimedIngestUploads(
|
|
266
|
+
ctx: QueryCtx | MutationCtx,
|
|
267
|
+
target: string,
|
|
268
|
+
now: string,
|
|
269
|
+
): Promise<{ uploadId: string; field: string; issuedAt: string }[]> {
|
|
270
|
+
const before = new Date(Date.parse(now) - INGEST_UPLOAD_TTL_MS).toISOString()
|
|
271
|
+
const rows = await listRecordWire(
|
|
272
|
+
ctx,
|
|
273
|
+
ingestUploadRecordKind,
|
|
274
|
+
resolveRecordQuery(ingestUploadRecordKind, { where: { target, state: 'issued' }, before }),
|
|
275
|
+
)
|
|
276
|
+
return rows.map((row) => ({
|
|
277
|
+
uploadId: String(row[ingestUploadRecordKind.idKey]),
|
|
278
|
+
field: String(row['field']),
|
|
279
|
+
issuedAt: String(row['issuedAt']),
|
|
280
|
+
}))
|
|
281
|
+
}
|
package/convex/invites.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { selectOrphanCredentialPurges } from '@elytracms/core/persistence'
|
|
|
3
3
|
import { mutation, query } from './functions'
|
|
4
4
|
import type { MutationCtx } from './functions'
|
|
5
5
|
import { requireCapability } from './guard'
|
|
6
|
+
import { assertAuthorityFloor, assertDeclaredRole } from './authority'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Invite allowlist management (EC-324). An admin invites an email + role; the
|
|
@@ -11,10 +12,14 @@ import { requireCapability } from './guard'
|
|
|
11
12
|
* `requireCapability('project.administer')`, the same trust boundary as
|
|
12
13
|
* member/user management; UI gating alone is never trusted.
|
|
13
14
|
*
|
|
14
|
-
* The invited `role` is validated against the
|
|
15
|
-
* write boundary (`invites.create`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* The invited `role` is validated against the project's policy at the operations
|
|
16
|
+
* write boundary (`invites.create`) AND here, against the DEPLOYED policy (EC-605 —
|
|
17
|
+
* `assertDeclaredRole`): an invite grants its role on consumption, so a role nobody
|
|
18
|
+
* declared must never be stored. Granting it is authority, so the integrity floor's
|
|
19
|
+
* "assign only roles you hold" applies too (`assertAuthorityFloor`, no target yet).
|
|
20
|
+
* The table keeps the role a lenient string (schema-as-lens, EC-277). Keyed by
|
|
21
|
+
* normalized (lowercased) email; `create` upserts so re-inviting the same email
|
|
22
|
+
* replaces the pending role.
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
25
|
const normalizeEmail = (email: string): string => email.trim().toLowerCase()
|
|
@@ -109,7 +114,10 @@ export const create = mutation({
|
|
|
109
114
|
updatedAt: v.string(),
|
|
110
115
|
},
|
|
111
116
|
handler: async (ctx, args) => {
|
|
112
|
-
await requireCapability(ctx, 'project.administer')
|
|
117
|
+
const callerId = await requireCapability(ctx, 'project.administer')
|
|
118
|
+
// EC-605: only a declared role, and only one the inviter could grant themselves.
|
|
119
|
+
assertDeclaredRole(args.role)
|
|
120
|
+
await assertAuthorityFloor(ctx, { callerId, rolesGranted: [args.role], removesMembership: false })
|
|
113
121
|
const email = normalizeEmail(args.email)
|
|
114
122
|
// EC-358: never adopt orphan credentials — purge member-less auth rows for
|
|
115
123
|
// this email atomically with the invite, so the invitee sets their own password.
|
package/convex/members.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v } from 'convex/values'
|
|
2
2
|
import { mutation, query } from './functions'
|
|
3
3
|
import { getOptionalUserId, requireCapability, requireUserId } from './guard'
|
|
4
|
-
import { assertAuthorityFloor, recordAuthorityAudit } from './authority'
|
|
4
|
+
import { assertAuthorityFloor, assertDeclaredRole, recordAuthorityAudit } from './authority'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Deployment membership persistence (EC-150, single-tenant per EC-179). The
|
|
@@ -9,10 +9,13 @@ import { assertAuthorityFloor, recordAuthorityAudit } from './authority'
|
|
|
9
9
|
* `ConvexMembersRepository` maps these onto `@elytracms/core/persistence` records.
|
|
10
10
|
* Member *management* is admin-only, server-side — UI gating alone is never
|
|
11
11
|
* trusted. One row per user grants their role on this deployment's one project.
|
|
12
|
+
*
|
|
13
|
+
* EC-605: a role is any string the DEPLOYED role policy declares (`./roles`) — the
|
|
14
|
+
* validator accepts a string and `set` refuses an undeclared one visibly
|
|
15
|
+
* (`assertDeclaredRole`), instead of the former literal admin|editor|viewer union,
|
|
16
|
+
* which made a project's own roles unwritable.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
|
-
const roleValidator = v.union(v.literal('admin'), v.literal('editor'), v.literal('viewer'))
|
|
15
|
-
|
|
16
19
|
export const list = query({
|
|
17
20
|
args: {},
|
|
18
21
|
handler: async (ctx) => {
|
|
@@ -62,7 +65,8 @@ export const mine = query({
|
|
|
62
65
|
export const set = mutation({
|
|
63
66
|
args: {
|
|
64
67
|
userId: v.string(),
|
|
65
|
-
role
|
|
68
|
+
/** One of the deployed policy's role ids — refused visibly otherwise (EC-605). */
|
|
69
|
+
role: v.string(),
|
|
66
70
|
/**
|
|
67
71
|
* Studio-user custom fields (EC-240, 240a). Optional so the member-management
|
|
68
72
|
* path (`ConvexMembersRepository.setMember`) omits it; the users rail
|
|
@@ -75,6 +79,8 @@ export const set = mutation({
|
|
|
75
79
|
},
|
|
76
80
|
handler: async (ctx, args) => {
|
|
77
81
|
const callerId = await requireCapability(ctx, 'project.administer')
|
|
82
|
+
// EC-605: only a role the deployed policy declares can be stored.
|
|
83
|
+
assertDeclaredRole(args.role)
|
|
78
84
|
// Integrity floor (EC-240): can't demote the last admin / assign above self.
|
|
79
85
|
// Throws `unauthorized: …` on denial — a first-class, visible error (verified
|
|
80
86
|
// live). NOTE (EC-240 H-tail item 6): a denied attempt is NOT audited here. A
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import type { ConvexHttpClient } from 'convex/browser'
|
|
4
|
+
import type { FunctionReference } from 'convex/server'
|
|
5
|
+
import type { CollectionDef } from '@elytracms/core/cms-core'
|
|
6
|
+
import { createSequentialIdFactory } from '@elytracms/core/determinism'
|
|
7
|
+
import { createOperations } from '@elytracms/core/operations'
|
|
8
|
+
import { replaceAtCurrentRevision } from '@elytracms/core/persistence'
|
|
9
|
+
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
10
|
+
import { loadHydratedCms } from '../src/lib/operations/session-hydration'
|
|
11
|
+
import schema from './schema'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* EC-606 close-out — the orphan count on the CONNECTED path, short of a signed-in
|
|
15
|
+
* connected studio (owner-run).
|
|
16
|
+
*
|
|
17
|
+
* The count itself is mode-independent: `loadHydratedCms` runs over whatever
|
|
18
|
+
* operations client the session has, and `project-session.test.tsx` already proves
|
|
19
|
+
* the content session carries its `orphans` to the rail foot. What only the connected
|
|
20
|
+
* mode adds is the backend between them — the real `cms:listDocuments` handler and the
|
|
21
|
+
* production `ConvexPersistenceAdapter`. This runs exactly that path on convex-test's
|
|
22
|
+
* in-memory backend (the harness of `contract-parity.test.ts`: the adapter's HTTP client
|
|
23
|
+
* is a call-through double onto `t.query` / `t.mutation`): a row of a collection the
|
|
24
|
+
* config does not declare must reach the session's orphan list, with its published
|
|
25
|
+
* count, and must not reach its documents.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const modules = {
|
|
29
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
30
|
+
'./_generated/api.ts': async () => ({}),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PROJECT_ID = 'orphan-hydration'
|
|
34
|
+
const SESSION_USER_ID = 'user_admin'
|
|
35
|
+
|
|
36
|
+
const DECLARED: CollectionDef[] = [{ id: 'post', kind: 'document', fields: [{ name: 'title', type: 'text' }] }]
|
|
37
|
+
|
|
38
|
+
interface AdapterBackend {
|
|
39
|
+
query: (ref: FunctionReference<'query'>, args: Record<string, unknown>) => Promise<unknown>
|
|
40
|
+
mutation: (ref: FunctionReference<'mutation'>, args: Record<string, unknown>) => Promise<unknown>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function asAdapterClient(backend: AdapterBackend): ConvexHttpClient {
|
|
44
|
+
return {
|
|
45
|
+
query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => backend.query(ref, args ?? {}),
|
|
46
|
+
mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => backend.mutation(ref, args ?? {}),
|
|
47
|
+
} as unknown as ConvexHttpClient
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The production adapter over a fresh in-memory Convex backend, as the session admin. */
|
|
51
|
+
async function connectedAdapter() {
|
|
52
|
+
const t = convexTest(schema, modules)
|
|
53
|
+
const now = '2026-10-26T00:00:00.000Z'
|
|
54
|
+
await t.run(async (ctx) => {
|
|
55
|
+
await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
|
|
56
|
+
})
|
|
57
|
+
const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
|
|
58
|
+
return new ConvexPersistenceAdapter(asAdapterClient(asAdmin), PROJECT_ID)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The operations client a headless embedder (or the session) builds over that adapter. */
|
|
62
|
+
function operationsOver(adapter: ConvexPersistenceAdapter) {
|
|
63
|
+
return createOperations({
|
|
64
|
+
adapter,
|
|
65
|
+
collections: () => DECLARED,
|
|
66
|
+
validateDocument: () => [],
|
|
67
|
+
idFactory: createSequentialIdFactory('doc'),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('EC-606 — orphans reach a connected session through the Convex functions', () => {
|
|
72
|
+
it('a stored row of an undeclared collection is counted (published too), and kept out of the documents', async () => {
|
|
73
|
+
const adapter = await connectedAdapter()
|
|
74
|
+
await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
|
|
75
|
+
collection: 'post',
|
|
76
|
+
id: 'kept',
|
|
77
|
+
values: { title: 'Kept' },
|
|
78
|
+
})
|
|
79
|
+
await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
|
|
80
|
+
collection: 'cms-data-sources',
|
|
81
|
+
id: 'legacy-draft',
|
|
82
|
+
values: { name: 'left behind by the data-binding subsystem' },
|
|
83
|
+
})
|
|
84
|
+
const published = await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
|
|
85
|
+
collection: 'cms-data-sources',
|
|
86
|
+
id: 'legacy-live',
|
|
87
|
+
values: { name: 'and published' },
|
|
88
|
+
})
|
|
89
|
+
const pinned = await adapter.cms.publishDocument(
|
|
90
|
+
PROJECT_ID,
|
|
91
|
+
{ collection: 'cms-data-sources', id: 'legacy-live' },
|
|
92
|
+
published.revision,
|
|
93
|
+
)
|
|
94
|
+
expect(pinned.status).toBe('ok')
|
|
95
|
+
|
|
96
|
+
const result = await loadHydratedCms(operationsOver(adapter), PROJECT_ID, DECLARED)
|
|
97
|
+
expect(result.ok).toBe(true)
|
|
98
|
+
if (!result.ok || !result.value) throw new Error('hydration must answer')
|
|
99
|
+
expect(result.value.orphans).toEqual([{ collection: 'cms-data-sources', count: 2, published: 1 }])
|
|
100
|
+
expect(result.value.documents.map((document) => `${document.collection}:${document.id}`)).toEqual(['post:kept'])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('CONTROL: declared rows only → no orphans (the count is not a constant)', async () => {
|
|
104
|
+
const adapter = await connectedAdapter()
|
|
105
|
+
await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
|
|
106
|
+
collection: 'post',
|
|
107
|
+
id: 'kept',
|
|
108
|
+
values: { title: 'Kept' },
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const result = await loadHydratedCms(operationsOver(adapter), PROJECT_ID, DECLARED)
|
|
112
|
+
if (!result.ok || !result.value) throw new Error('hydration must answer')
|
|
113
|
+
expect(result.value.orphans).toEqual([])
|
|
114
|
+
expect(result.value.documents).toHaveLength(1)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -3,7 +3,7 @@ import { convexTest } from 'convex-test'
|
|
|
3
3
|
import { makeFunctionReference } from 'convex/server'
|
|
4
4
|
import type { ConvexHttpClient } from 'convex/browser'
|
|
5
5
|
import type { FunctionReference } from 'convex/server'
|
|
6
|
-
import { InMemoryPersistenceAdapter } from '@elytracms/core/persistence'
|
|
6
|
+
import { InMemoryPersistenceAdapter, assetAlt } from '@elytracms/core/persistence'
|
|
7
7
|
import type { PersistenceAdapter } from '@elytracms/core/persistence'
|
|
8
8
|
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
9
9
|
import { updateAssetMetadata } from '../src/components/assets/asset-metadata'
|
|
@@ -60,7 +60,6 @@ async function seedHero(adapter: PersistenceAdapter) {
|
|
|
60
60
|
byteSize: 10,
|
|
61
61
|
width: 4000,
|
|
62
62
|
height: 2000,
|
|
63
|
-
alt: 'Harbour',
|
|
64
63
|
title: 'Hero',
|
|
65
64
|
focalPoint: { x: 0.2, y: 0.8 },
|
|
66
65
|
thumb: THUMB,
|
|
@@ -69,17 +68,21 @@ async function seedHero(adapter: PersistenceAdapter) {
|
|
|
69
68
|
})
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* "Reset to centre", then clear alt — the two connected clears that never persisted.
|
|
73
|
+
* EC-587: the alt is cleared the way the options dialog clears it, by writing the bag
|
|
74
|
+
* without it (`meta.alt` is its one home).
|
|
75
|
+
*/
|
|
73
76
|
async function metadataClears(adapter: PersistenceAdapter) {
|
|
74
77
|
await seedHero(adapter)
|
|
75
78
|
const assets = adapter.assets
|
|
76
79
|
const reset = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { focalPoint: null })
|
|
77
|
-
const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', {
|
|
80
|
+
const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { meta: { legacyKey: 'kept' } })
|
|
78
81
|
expect([reset.ok, clearedAlt.ok]).toEqual([true, true])
|
|
79
82
|
const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_hero')
|
|
80
83
|
return {
|
|
81
84
|
focalPoint: after.focalPoint ?? null,
|
|
82
|
-
alt: after
|
|
85
|
+
alt: assetAlt(after),
|
|
83
86
|
title: after.title ?? null,
|
|
84
87
|
thumb: after.thumb ?? null,
|
|
85
88
|
lqip: after.lqip ?? null,
|
|
@@ -97,7 +100,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
|
|
|
97
100
|
title: 'Hero',
|
|
98
101
|
thumb: THUMB,
|
|
99
102
|
lqip: LQIP,
|
|
100
|
-
meta: {
|
|
103
|
+
meta: { legacyKey: 'kept' },
|
|
101
104
|
}
|
|
102
105
|
expect({ memory, convex }).toEqual({ memory: expected, convex: expected })
|
|
103
106
|
})
|
|
@@ -128,7 +131,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
|
|
|
128
131
|
async get() {
|
|
129
132
|
// An editor saves alt while the backfill is fetching this original — the
|
|
130
133
|
// backfill's write lands after it, from a listing taken before it.
|
|
131
|
-
await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { alt: 'Mid-run alt' })
|
|
134
|
+
await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { meta: { alt: 'Mid-run alt' } })
|
|
132
135
|
return new Uint8Array([1])
|
|
133
136
|
},
|
|
134
137
|
}
|
|
@@ -142,7 +145,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
|
|
|
142
145
|
)
|
|
143
146
|
expect(progress.generated).toBe(1)
|
|
144
147
|
const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_big')
|
|
145
|
-
expect([after
|
|
148
|
+
expect([assetAlt(after), after.thumb, after.lqip]).toEqual(['Mid-run alt', THUMB, LQIP])
|
|
146
149
|
}
|
|
147
150
|
})
|
|
148
151
|
})
|