@agent-native/core 0.137.7 → 0.138.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.
- package/agent-native.eject.json +2 -0
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +15 -0
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/analytics/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/analytics/AGENTS.md +2 -2
- package/corpus/templates/analytics/actions/get-first-party-analytics-health.ts +65 -0
- package/corpus/templates/analytics/actions/provider-api-catalog.ts +3 -6
- package/corpus/templates/analytics/actions/provider-api-docs.ts +3 -6
- package/corpus/templates/analytics/actions/provider-api-request.ts +3 -6
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +3 -3
- package/corpus/templates/analytics/agent-native.json +12 -0
- package/corpus/templates/analytics/app/i18n-data.ts +162 -0
- package/corpus/templates/analytics/app/pages/DataSources.tsx +126 -1
- package/corpus/templates/analytics/changelog/2026-08-05-analytics-now-flags-neon-pressure-and-guides-high-volume-tra.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-first-party-analytics-uses-compact-rollups-for-faster-usage-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +52 -0
- package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +397 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +182 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +86 -15
- package/corpus/templates/analytics/server/lib/provider-api.ts +20 -1
- package/corpus/templates/analytics/server/lib/replay-storage.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/analytics/server/plugins/db.ts +60 -0
- package/corpus/templates/assets/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/assets/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/assets/agent-native.json +12 -0
- package/corpus/templates/assets/server/lib/generation.ts +8 -8
- package/corpus/templates/assets/server/lib/storage.ts +1 -1
- package/corpus/templates/assets/server/plugins/onboarding.ts +1 -1
- package/corpus/templates/brain/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/brain/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/brain/agent-native.json +12 -0
- package/corpus/templates/calendar/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/calendar/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/calendar/agent-native.json +12 -0
- package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/chat/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/chat/AGENTS.md +3 -0
- package/corpus/templates/chat/DESIGN.md +28 -0
- package/corpus/templates/chat/agent-native.json +12 -0
- package/corpus/templates/clips/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/clips/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/clips/actions/cleanup-transcript.ts +2 -2
- package/corpus/templates/clips/actions/delete-recording-permanent.ts +3 -0
- package/corpus/templates/clips/actions/get-playback-position.ts +68 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
- package/corpus/templates/clips/actions/save-playback-position.ts +80 -0
- package/corpus/templates/clips/actions/set-thumbnail.ts +1 -1
- package/corpus/templates/clips/agent-native.json +12 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +54 -5
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +65 -18
- package/corpus/templates/clips/app/hooks/use-playback-position.ts +188 -0
- package/corpus/templates/clips/app/hooks/use-view-tracking.ts +6 -24
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/lib/playback-position.ts +99 -0
- package/corpus/templates/clips/app/lib/viewer-session.ts +29 -0
- package/corpus/templates/clips/changelog/2026-08-05-clip-buffering-now-uses-buffering-copy-after-playback.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-remembers-your-playback-position-when-you-reopen-a-cli.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +70 -6
- package/corpus/templates/clips/desktop/src/components/AlertDialog.tsx +131 -0
- package/corpus/templates/clips/desktop/src/styles.css +129 -0
- package/corpus/templates/clips/learnings.defaults.md +1 -1
- package/corpus/templates/clips/server/db/schema.ts +18 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +1 -1
- package/corpus/templates/clips/server/lib/video-storage.ts +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +15 -0
- package/corpus/templates/clips/server/routes/api/media/index.post.ts +1 -1
- package/corpus/templates/content/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +13 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/content/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_content-database-mutation-lock.ts +41 -0
- package/corpus/templates/content/actions/_content-database-row-migration.ts +569 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +82 -36
- package/corpus/templates/content/actions/_database-utils.ts +5 -0
- package/corpus/templates/content/actions/_delete-content-space.ts +25 -11
- package/corpus/templates/content/actions/add-database-item.ts +60 -42
- package/corpus/templates/content/actions/attach-content-database-source.ts +2 -0
- package/corpus/templates/content/actions/configure-document-property.ts +99 -104
- package/corpus/templates/content/actions/delete-content-database.ts +19 -7
- package/corpus/templates/content/actions/delete-document-property.ts +47 -58
- package/corpus/templates/content/actions/delete-document.ts +390 -167
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +41 -24
- package/corpus/templates/content/actions/duplicate-database-item.ts +63 -31
- package/corpus/templates/content/actions/duplicate-database-items.ts +86 -43
- package/corpus/templates/content/actions/duplicate-document-property.ts +37 -14
- package/corpus/templates/content/actions/migrate-content-database-rows.ts +830 -0
- package/corpus/templates/content/actions/move-database-item.ts +14 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +27 -6
- package/corpus/templates/content/actions/remove-database-items.ts +13 -0
- package/corpus/templates/content/actions/reorder-document-property.ts +45 -46
- package/corpus/templates/content/actions/set-document-property.ts +68 -28
- package/corpus/templates/content/actions/submit-content-database-form.ts +49 -0
- package/corpus/templates/content/actions/transcribe-media.ts +1 -1
- package/corpus/templates/content/agent-native.json +12 -0
- package/corpus/templates/content/app/components/editor/image-upload.ts +2 -2
- package/corpus/templates/content/app/i18n-data.ts +1 -1
- package/corpus/templates/content/changelog/2026-08-01-content-can-reorganize-every-row-in-an-existing-database-as-.md +6 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +4 -2
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +24 -0
- package/corpus/templates/content/server/plugins/onboarding.ts +2 -2
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/crm/agent-native.json +12 -0
- package/corpus/templates/design/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/design/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +2 -2
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +2 -2
- package/corpus/templates/design/actions/apply-design-token-edit.ts +1 -1
- package/corpus/templates/design/actions/capture-design-state.ts +1 -1
- package/corpus/templates/design/actions/connect-builder-app.ts +2 -2
- package/corpus/templates/design/actions/create-design-branch.ts +1 -1
- package/corpus/templates/design/actions/create-fusion-app.ts +3 -3
- package/corpus/templates/design/actions/deploy-design-preview.ts +1 -1
- package/corpus/templates/design/actions/generate-design.ts +3 -2
- package/corpus/templates/design/actions/get-component-details.ts +1 -1
- package/corpus/templates/design/actions/get-design-branch-diff.ts +1 -1
- package/corpus/templates/design/actions/index-components.ts +3 -3
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +1 -1
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +3 -3
- package/corpus/templates/design/actions/open-component-source.ts +1 -1
- package/corpus/templates/design/actions/update-design.ts +96 -14
- package/corpus/templates/design/agent-native.json +12 -0
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +7 -2
- package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +11 -2
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +1 -1
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +8 -1
- package/corpus/templates/design/app/i18n-data.ts +53 -22
- package/corpus/templates/design/app/lib/design-file-upload.ts +2 -3
- package/corpus/templates/design/app/lib/upload-limits.ts +4 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
- package/corpus/templates/design/changelog/2026-08-05-chat-attachments-now-warn-before-upload-when-they-exceed-the.md +6 -0
- package/corpus/templates/design/changelog/2026-08-05-figma-fig-uploads-now-show-a-clear-4-mb-limit-up-front-inste.md +6 -0
- package/corpus/templates/design/scripts/qa-figma-stress-import.ts +1 -1
- package/corpus/templates/design/server/handlers/import-design-file.ts +9 -5
- package/corpus/templates/design/server/handlers/uploads.ts +13 -9
- package/corpus/templates/design/server/lib/fig-file-limits.ts +2 -1
- package/corpus/templates/design/server/lib/figma-image-hydration.ts +1 -1
- package/corpus/templates/design/server/lib/figma-node-import.ts +1 -1
- package/corpus/templates/design/server/lib/request-body-limits.ts +11 -0
- package/corpus/templates/design/shared/canvas-frames.ts +81 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +25 -9
- package/corpus/templates/dispatch/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/dispatch/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/dispatch/agent-native.json +12 -0
- package/corpus/templates/factory/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/factory/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/factory/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/factory/agent-native.json +12 -0
- package/corpus/templates/forms/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/forms/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/forms/actions/export-responses.ts +1 -1
- package/corpus/templates/forms/agent-native.json +12 -0
- package/corpus/templates/macros/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/macros/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/macros/agent-native.json +12 -0
- package/corpus/templates/mail/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/mail/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/mail/agent-native.json +12 -0
- package/corpus/templates/mail/app/i18n/en-US.ts +1 -1
- package/corpus/templates/mail/server/lib/media-upload.ts +1 -1
- package/corpus/templates/plan/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/plan/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/plan/agent-native.json +12 -0
- package/corpus/templates/plan/server/lib/plan-assets.ts +1 -1
- package/corpus/templates/slides/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/slides/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/slides/actions/generate-image-api.ts +1 -1
- package/corpus/templates/slides/actions/generate-slides-ai.ts +1 -1
- package/corpus/templates/slides/actions/import-file.ts +2 -2
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +1 -1
- package/corpus/templates/slides/agent-native.json +12 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +2 -2
- package/corpus/templates/slides/app/lib/deck-sorting.ts +19 -0
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-08-05-decks-are-now-sorted-by-most-recently-updated.md +6 -0
- package/corpus/templates/slides/server/handlers/assets.ts +1 -1
- package/corpus/templates/slides/server/handlers/uploads.ts +1 -1
- package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +23 -2
- package/corpus/templates/tasks/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/corpus/templates/tasks/agent-native.json +12 -0
- package/dist/action.d.ts +6 -0
- package/dist/agent/engine/builder-engine.js +2 -2
- package/dist/agent/engine/credential-errors.d.ts +1 -1
- package/dist/agent/engine/credential-errors.js +2 -2
- package/dist/agent/production-agent.js +3 -2
- package/dist/brand-kit/fig/index.js +1 -1
- package/dist/cli/agent.js +45 -3
- package/dist/cli/code-agent-executor.js +2 -1
- package/dist/cli/create.d.ts +9 -1
- package/dist/cli/create.js +94 -1
- package/dist/cli/doctor.d.ts +8 -11
- package/dist/cli/doctor.js +92 -24
- package/dist/cli/index.js +6 -5
- package/dist/client/AgentAskPopover.d.ts +3 -1
- package/dist/client/AgentAskPopover.js +2 -2
- package/dist/client/AgentPanel.js +9 -3
- package/dist/client/AssistantChat.js +54 -33
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/RouteTransitionIndicator.d.ts +7 -0
- package/dist/client/RouteTransitionIndicator.js +31 -0
- package/dist/client/agent-engine-key.js +1 -1
- package/dist/client/app-providers.js +11 -2
- package/dist/client/chat/message-components.js +1 -1
- package/dist/client/chat/run-recovery.js +1 -1
- package/dist/client/chat/tool-call-display.d.ts +3 -1
- package/dist/client/chat/tool-call-display.js +4 -3
- package/dist/client/chat-model-groups.d.ts +6 -0
- package/dist/client/chat-model-groups.js +11 -0
- package/dist/client/error-format.js +1 -1
- package/dist/client/i18n.js +1 -1
- package/dist/client/onboarding/FirstRunOnboarding.d.ts +5 -1
- package/dist/client/onboarding/FirstRunOnboarding.js +29 -13
- package/dist/client/onboarding/first-run-enabled.d.ts +6 -1
- package/dist/client/onboarding/first-run-enabled.js +33 -3
- package/dist/client/onboarding/use-onboarding.js +16 -11
- package/dist/client/require-session.js +13 -4
- package/dist/client/resources/mcp-integration-catalog.js +26 -0
- package/dist/client/resources/mcp-integration-logos.js +4 -0
- package/dist/client/route-warmup.js +56 -10
- package/dist/client/settings/AccountSettingsCard.js +78 -3
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +2 -2
- package/dist/client/settings/useBuilderStatus.js +1 -1
- package/dist/client/setup-connections/useBuilderConnectCardController.js +1 -1
- package/dist/client/ui/index.d.ts +1 -0
- package/dist/client/ui/index.js +1 -0
- package/dist/client/uploads/upload-editor-image.js +1 -1
- package/dist/client/use-chat-threads.js +3 -0
- package/dist/client/use-session.d.ts +11 -0
- package/dist/client/use-session.js +34 -6
- package/dist/collab/routes.d.ts +2 -2
- package/dist/config.d.ts +48 -0
- package/dist/config.js +111 -0
- package/dist/db/migrations.js +5 -0
- package/dist/eject/provider-api-definitions.d.ts +1 -1
- package/dist/eject/provider-api-definitions.js +1 -0
- package/dist/eject/remote-mcp-presets.js +1 -0
- package/dist/extensions/web-search-tool.js +2 -2
- package/dist/file-upload/actions/upload-image.js +1 -1
- package/dist/file-upload/registry.js +1 -1
- package/dist/guards/index.d.ts +3 -0
- package/dist/guards/index.js +2 -0
- package/dist/guards/no-empty-migrations.d.ts +14 -0
- package/dist/guards/no-empty-migrations.js +126 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/localization/default-messages.d.ts +24 -0
- package/dist/localization/default-messages.js +26 -2
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/onboarding/app-profile.js +1 -1
- package/dist/onboarding/plugin.d.ts +2 -0
- package/dist/onboarding/plugin.js +29 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/index.d.ts +4 -4
- package/dist/provider-api/index.js +35 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/handlers.js +1 -1
- package/dist/scripts/docs/framework-search.d.ts +9 -0
- package/dist/scripts/docs/framework-search.js +239 -0
- package/dist/scripts/docs/index.js +1 -0
- package/dist/scripts/docs/search.d.ts +10 -0
- package/dist/scripts/docs/search.js +1 -1
- package/dist/scripts/docs/source-search.d.ts +10 -2
- package/dist/scripts/docs/source-search.js +108 -41
- package/dist/search-utils/index.d.ts +11 -0
- package/dist/search-utils/index.js +72 -0
- package/dist/server/action-change.d.ts +6 -0
- package/dist/server/action-change.js +28 -14
- package/dist/server/action-discovery.js +9 -0
- package/dist/server/action-routes.js +1 -0
- package/dist/server/agent-chat/browser-team-tools.js +1 -1
- package/dist/server/agent-chat/script-entries.js +43 -2
- package/dist/server/auth-login-mode.d.ts +8 -0
- package/dist/server/auth-login-mode.js +17 -0
- package/dist/server/auth.js +100 -15
- package/dist/server/better-auth-instance.d.ts +36 -0
- package/dist/server/better-auth-instance.js +25 -3
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/core-routes-plugin.js +17 -14
- package/dist/server/credential-provider.js +3 -3
- package/dist/server/email-templates.d.ts +7 -0
- package/dist/server/email-templates.js +24 -0
- package/dist/server/email.d.ts +17 -0
- package/dist/server/email.js +22 -1
- package/dist/server/framework-request-handler.js +6 -0
- package/dist/server/onboarding-html.d.ts +2 -0
- package/dist/server/onboarding-html.js +169 -8
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/realtime-voice.js +1 -1
- package/dist/server/sentry.d.ts +1 -1
- package/dist/server/transcribe-voice.js +3 -3
- package/dist/shared/first-run-onboarding.d.ts +1 -0
- package/dist/shared/first-run-onboarding.js +1 -0
- package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +23 -2
- package/dist/templates/chat/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/dist/templates/chat/AGENTS.md +3 -0
- package/dist/templates/chat/DESIGN.md +28 -0
- package/dist/templates/chat/agent-native.json +12 -0
- package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +23 -2
- package/dist/templates/default/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/dist/templates/default/AGENTS.md +4 -0
- package/dist/templates/default/DESIGN.md +30 -0
- package/dist/templates/default/agent-native.json +9 -0
- package/dist/templates/default/app/i18n/ar-SA.ts +2 -2
- package/dist/templates/default/app/i18n/de-DE.ts +2 -2
- package/dist/templates/default/app/i18n/en-US.ts +2 -2
- package/dist/templates/default/app/i18n/es-ES.ts +2 -2
- package/dist/templates/default/app/i18n/fr-FR.ts +2 -2
- package/dist/templates/default/app/i18n/hi-IN.ts +2 -2
- package/dist/templates/default/app/i18n/ja-JP.ts +2 -2
- package/dist/templates/default/app/i18n/ko-KR.ts +2 -2
- package/dist/templates/default/app/i18n/pt-BR.ts +2 -2
- package/dist/templates/default/app/i18n/zh-CN.ts +2 -2
- package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
- package/dist/templates/default/package.json +1 -0
- package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/dist/templates/headless/agent-native.json +9 -0
- package/dist/templates/headless/package.json +2 -0
- package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +23 -2
- package/dist/templates/workspace-core/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/dist/templates/workspace-core/AGENTS.md +3 -0
- package/dist/templates/workspace-core/DESIGN.md +35 -0
- package/dist/templates/workspace-root/AGENTS.md +3 -0
- package/dist/templates/workspace-root/DESIGN.md +35 -0
- package/dist/templates/workspace-root/agent-native.json +9 -0
- package/dist/templates/workspace-root/package.json +3 -0
- package/dist/transcription/builder-transcription.js +1 -1
- package/dist/user-profile/actions/change-password.d.ts +8 -0
- package/dist/user-profile/actions/change-password.js +24 -0
- package/dist/user-profile/actions/get-auth-methods.d.ts +6 -0
- package/dist/user-profile/actions/get-auth-methods.js +23 -0
- package/dist/user-profile/actions/set-password.d.ts +7 -0
- package/dist/user-profile/actions/set-password.js +23 -0
- package/dist/vite/client.d.ts +8 -1
- package/dist/vite/client.js +63 -12
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +1 -0
- package/docs/AGENTS.md +7 -2
- package/docs/SKILL.md +7 -5
- package/docs/content/agent-native-config.mdx +165 -0
- package/docs/content/agent-native-toolkit.mdx +1 -1
- package/docs/content/deployment.mdx +7 -2
- package/docs/content/doctor.mdx +21 -13
- package/docs/content/environment-variables.mdx +158 -0
- package/docs/content/file-uploads.mdx +1 -1
- package/docs/content/getting-started.mdx +1 -1
- package/docs/content/key-concepts.mdx +1 -1
- package/docs/content/multi-app-workspace.mdx +1 -1
- package/docs/content/template-clips-ai-and-editing.mdx +6 -6
- package/docs/content/template-clips-developers.mdx +3 -3
- package/docs/content/toolkit-settings.mdx +1 -1
- package/docs/content/voice-input.mdx +4 -4
- package/package.json +6 -2
- package/src/eject/provider-api-definitions.ts +1 -0
- package/src/eject/remote-mcp-presets.ts +1 -0
- package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +23 -2
- package/src/templates/chat/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/src/templates/chat/AGENTS.md +3 -0
- package/src/templates/chat/DESIGN.md +28 -0
- package/src/templates/chat/agent-native.json +12 -0
- package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +23 -2
- package/src/templates/default/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/src/templates/default/AGENTS.md +4 -0
- package/src/templates/default/DESIGN.md +30 -0
- package/src/templates/default/agent-native.json +9 -0
- package/src/templates/default/app/i18n/ar-SA.ts +2 -2
- package/src/templates/default/app/i18n/de-DE.ts +2 -2
- package/src/templates/default/app/i18n/en-US.ts +2 -2
- package/src/templates/default/app/i18n/es-ES.ts +2 -2
- package/src/templates/default/app/i18n/fr-FR.ts +2 -2
- package/src/templates/default/app/i18n/hi-IN.ts +2 -2
- package/src/templates/default/app/i18n/ja-JP.ts +2 -2
- package/src/templates/default/app/i18n/ko-KR.ts +2 -2
- package/src/templates/default/app/i18n/pt-BR.ts +2 -2
- package/src/templates/default/app/i18n/zh-CN.ts +2 -2
- package/src/templates/default/app/i18n/zh-TW.ts +1 -1
- package/src/templates/default/package.json +1 -0
- package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/src/templates/headless/agent-native.json +9 -0
- package/src/templates/headless/package.json +2 -0
- package/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +23 -3
- package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +16 -0
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +23 -2
- package/src/templates/workspace-core/.agents/skills/frontend-design/references/visual-direction.md +99 -0
- package/src/templates/workspace-core/AGENTS.md +3 -0
- package/src/templates/workspace-core/DESIGN.md +35 -0
- package/src/templates/workspace-root/AGENTS.md +3 -0
- package/src/templates/workspace-root/DESIGN.md +35 -0
- package/src/templates/workspace-root/agent-native.json +9 -0
- package/src/templates/workspace-root/package.json +3 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
|
+
import { isLocalDatabase, isPostgres } from "@agent-native/core/db";
|
|
4
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
5
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
9
|
+
import {
|
|
10
|
+
lockContentDatabaseMutation,
|
|
11
|
+
touchContentDatabase,
|
|
12
|
+
withContentDatabaseMutationLock,
|
|
13
|
+
} from "./_content-database-mutation-lock.js";
|
|
14
|
+
import {
|
|
15
|
+
applyMigration,
|
|
16
|
+
deterministicId,
|
|
17
|
+
digest,
|
|
18
|
+
migrationPlanSchema,
|
|
19
|
+
snapshotDigest,
|
|
20
|
+
snapshotMigration,
|
|
21
|
+
serializeMigrationValue,
|
|
22
|
+
validatePlan,
|
|
23
|
+
} from "./_content-database-row-migration.js";
|
|
24
|
+
import { lockDatabaseMemberships } from "./_database-membership-lock.js";
|
|
25
|
+
import { flushOpenDocumentEditorToSql } from "./_document-flush.js";
|
|
26
|
+
|
|
27
|
+
const operationalSchema = z.discriminatedUnion("phase", [
|
|
28
|
+
z.object({ phase: z.literal("validate"), plan: migrationPlanSchema }),
|
|
29
|
+
z.object({ phase: z.literal("apply"), plan: migrationPlanSchema }),
|
|
30
|
+
z.object({
|
|
31
|
+
phase: z.literal("verify"),
|
|
32
|
+
databaseId: z.string().min(1),
|
|
33
|
+
idempotencyKey: z.string().min(1),
|
|
34
|
+
expectedPostDigest: z.string().min(1),
|
|
35
|
+
}),
|
|
36
|
+
z.object({
|
|
37
|
+
phase: z.literal("rollback"),
|
|
38
|
+
databaseId: z.string().min(1),
|
|
39
|
+
idempotencyKey: z.string().min(1),
|
|
40
|
+
expectedPostDigest: z.string().min(1),
|
|
41
|
+
}),
|
|
42
|
+
z.object({
|
|
43
|
+
phase: z.literal("finalize"),
|
|
44
|
+
databaseId: z.string().min(1),
|
|
45
|
+
idempotencyKey: z.string().min(1),
|
|
46
|
+
expectedPostDigest: z.string().min(1),
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function parseJson(text: string) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(text) as any;
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error("Migration receipt is corrupt.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function receiptResult(receipt: any, replayed: boolean) {
|
|
58
|
+
const {
|
|
59
|
+
plan: _plan,
|
|
60
|
+
transitionExpectedPostDigest: _transitionExpectedPostDigest,
|
|
61
|
+
...result
|
|
62
|
+
} = parseJson(receipt.resultJson);
|
|
63
|
+
return {
|
|
64
|
+
...result,
|
|
65
|
+
receiptId: receipt.id,
|
|
66
|
+
state: receipt.state,
|
|
67
|
+
preDigest: receipt.preDigest,
|
|
68
|
+
postDigest: receipt.postDigest,
|
|
69
|
+
replayed,
|
|
70
|
+
verified: result.verified === true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function lockCurrentDatabaseMemberships(tx: any, databaseId: string) {
|
|
75
|
+
const memberships = await tx
|
|
76
|
+
.select({ id: schema.contentDatabaseItems.id })
|
|
77
|
+
.from(schema.contentDatabaseItems)
|
|
78
|
+
.where(eq(schema.contentDatabaseItems.databaseId, databaseId));
|
|
79
|
+
await lockDatabaseMemberships(
|
|
80
|
+
tx,
|
|
81
|
+
memberships.map((membership: { id: string }) => membership.id),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function flushMigrationDocuments(rows: Array<{ documentId: string }>) {
|
|
86
|
+
const accesses = await Promise.all(
|
|
87
|
+
rows.map((row) => assertAccess("document", row.documentId, "editor")),
|
|
88
|
+
);
|
|
89
|
+
const flushes = await Promise.allSettled(
|
|
90
|
+
rows.map((row, index) =>
|
|
91
|
+
flushOpenDocumentEditorToSql({
|
|
92
|
+
documentId: row.documentId,
|
|
93
|
+
ownerEmail: accesses[index]?.resource.ownerEmail,
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
const failed = flushes.find(
|
|
98
|
+
(flush): flush is PromiseRejectedResult => flush.status === "rejected",
|
|
99
|
+
);
|
|
100
|
+
if (failed) throw failed.reason;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default defineAction({
|
|
104
|
+
description:
|
|
105
|
+
"Atomically migrate every active row in one ordinary Content database without attached Sources: validates an exact bounded plan, snapshots bodies, writes only new safe properties, and supports guarded rollback or legacy-property finalization.",
|
|
106
|
+
schema: operationalSchema,
|
|
107
|
+
audit: {
|
|
108
|
+
recordInputs: false,
|
|
109
|
+
target: (args) => ({
|
|
110
|
+
type: "content-database",
|
|
111
|
+
id:
|
|
112
|
+
args.phase === "apply" || args.phase === "validate"
|
|
113
|
+
? args.plan.databaseId
|
|
114
|
+
: args.databaseId,
|
|
115
|
+
visibility: "private",
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
needsApproval: (args) =>
|
|
119
|
+
args.phase === "rollback" || args.phase === "finalize",
|
|
120
|
+
run: async (args) => {
|
|
121
|
+
const db = getDb();
|
|
122
|
+
const databaseId =
|
|
123
|
+
args.phase === "apply" || args.phase === "validate"
|
|
124
|
+
? args.plan.databaseId
|
|
125
|
+
: args.databaseId;
|
|
126
|
+
const localDatabase = isLocalDatabase();
|
|
127
|
+
const flushUnderDurableLock = isPostgres() && !localDatabase;
|
|
128
|
+
return withContentDatabaseMutationLock(databaseId, async () => {
|
|
129
|
+
const [database] = await db
|
|
130
|
+
.select()
|
|
131
|
+
.from(schema.contentDatabases)
|
|
132
|
+
.where(eq(schema.contentDatabases.id, databaseId));
|
|
133
|
+
if (!database) throw new Error("Database not found.");
|
|
134
|
+
await assertAccess("document", database.documentId, "admin");
|
|
135
|
+
if (args.phase === "apply") {
|
|
136
|
+
const replay = await db.transaction(async (tx) => {
|
|
137
|
+
const [existing] = await tx
|
|
138
|
+
.select()
|
|
139
|
+
.from(schema.contentDatabaseMigrationReceipts)
|
|
140
|
+
.where(
|
|
141
|
+
and(
|
|
142
|
+
eq(
|
|
143
|
+
schema.contentDatabaseMigrationReceipts.databaseId,
|
|
144
|
+
args.plan.databaseId,
|
|
145
|
+
),
|
|
146
|
+
eq(
|
|
147
|
+
schema.contentDatabaseMigrationReceipts.idempotencyKey,
|
|
148
|
+
args.plan.idempotencyKey,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
const planHash = digest(args.plan);
|
|
153
|
+
if (existing) {
|
|
154
|
+
if (existing.planHash !== planHash)
|
|
155
|
+
throw new Error(
|
|
156
|
+
"Idempotency key was already used with a different migration plan.",
|
|
157
|
+
);
|
|
158
|
+
if (existing.state !== "applied" && existing.state !== "verified")
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Migration receipt is already ${existing.state}.`,
|
|
161
|
+
);
|
|
162
|
+
if (
|
|
163
|
+
snapshotDigest(
|
|
164
|
+
await snapshotMigration(tx, args.plan.databaseId),
|
|
165
|
+
) !== existing.postDigest
|
|
166
|
+
)
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Applied migration has drifted; replay is refused.",
|
|
169
|
+
);
|
|
170
|
+
return receiptResult(existing, true);
|
|
171
|
+
}
|
|
172
|
+
validatePlan(
|
|
173
|
+
args.plan,
|
|
174
|
+
await snapshotMigration(tx, args.plan.databaseId),
|
|
175
|
+
);
|
|
176
|
+
return null;
|
|
177
|
+
});
|
|
178
|
+
if (replay) return replay;
|
|
179
|
+
if (!localDatabase && !flushUnderDurableLock) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
"Database row migration requires PostgreSQL or a local SQLite/PGlite database so live editor saves can be serialized safely.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (!flushUnderDurableLock) {
|
|
185
|
+
await flushMigrationDocuments(args.plan.rows);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (args.phase === "rollback") {
|
|
189
|
+
const preflight = await db.transaction(async (tx) => {
|
|
190
|
+
const [receipt] = await tx
|
|
191
|
+
.select()
|
|
192
|
+
.from(schema.contentDatabaseMigrationReceipts)
|
|
193
|
+
.where(
|
|
194
|
+
and(
|
|
195
|
+
eq(
|
|
196
|
+
schema.contentDatabaseMigrationReceipts.databaseId,
|
|
197
|
+
args.databaseId,
|
|
198
|
+
),
|
|
199
|
+
eq(
|
|
200
|
+
schema.contentDatabaseMigrationReceipts.idempotencyKey,
|
|
201
|
+
args.idempotencyKey,
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
if (!receipt) throw new Error("Migration receipt not found.");
|
|
206
|
+
const result = parseJson(receipt.resultJson);
|
|
207
|
+
const current = await snapshotMigration(tx, args.databaseId);
|
|
208
|
+
if (receipt.state === "rolled_back") {
|
|
209
|
+
if (
|
|
210
|
+
result.transitionExpectedPostDigest !== args.expectedPostDigest ||
|
|
211
|
+
snapshotDigest(current) !== receipt.postDigest
|
|
212
|
+
)
|
|
213
|
+
throw new Error(
|
|
214
|
+
"Terminal migration result has drifted; replay is refused.",
|
|
215
|
+
);
|
|
216
|
+
return { replay: receiptResult(receipt, true), versions: [] };
|
|
217
|
+
}
|
|
218
|
+
if (receipt.state !== "applied" && receipt.state !== "verified")
|
|
219
|
+
throw new Error(`Migration receipt is already ${receipt.state}.`);
|
|
220
|
+
if (
|
|
221
|
+
receipt.postDigest !== args.expectedPostDigest ||
|
|
222
|
+
snapshotDigest(current) !== receipt.postDigest
|
|
223
|
+
)
|
|
224
|
+
throw new Error(
|
|
225
|
+
"Migration has drifted; guarded operation is refused.",
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
replay: null,
|
|
229
|
+
versions: parseJson(receipt.rollbackJson).versions ?? [],
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
if (preflight.replay) return preflight.replay;
|
|
233
|
+
if (!localDatabase && !flushUnderDurableLock) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"Database row migration requires PostgreSQL or a local SQLite/PGlite database so live editor saves can be serialized safely.",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (!flushUnderDurableLock) {
|
|
239
|
+
await flushMigrationDocuments(preflight.versions);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (args.phase === "verify" || args.phase === "finalize") {
|
|
243
|
+
const [receipt] = await db
|
|
244
|
+
.select()
|
|
245
|
+
.from(schema.contentDatabaseMigrationReceipts)
|
|
246
|
+
.where(
|
|
247
|
+
and(
|
|
248
|
+
eq(
|
|
249
|
+
schema.contentDatabaseMigrationReceipts.databaseId,
|
|
250
|
+
args.databaseId,
|
|
251
|
+
),
|
|
252
|
+
eq(
|
|
253
|
+
schema.contentDatabaseMigrationReceipts.idempotencyKey,
|
|
254
|
+
args.idempotencyKey,
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
if (!receipt) throw new Error("Migration receipt not found.");
|
|
259
|
+
if (receipt.state === "applied" || receipt.state === "verified") {
|
|
260
|
+
const plan = parseJson(receipt.resultJson).plan;
|
|
261
|
+
if (!plan)
|
|
262
|
+
throw new Error("Migration receipt lacks its verification plan.");
|
|
263
|
+
for (const row of plan.rows)
|
|
264
|
+
await assertAccess(
|
|
265
|
+
"document",
|
|
266
|
+
row.documentId,
|
|
267
|
+
args.phase === "finalize" ? "editor" : "viewer",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (args.phase === "verify" || args.phase === "finalize") {
|
|
272
|
+
const replay = await db.transaction(async (tx) => {
|
|
273
|
+
const [receipt] = await tx
|
|
274
|
+
.select()
|
|
275
|
+
.from(schema.contentDatabaseMigrationReceipts)
|
|
276
|
+
.where(
|
|
277
|
+
and(
|
|
278
|
+
eq(
|
|
279
|
+
schema.contentDatabaseMigrationReceipts.databaseId,
|
|
280
|
+
args.databaseId,
|
|
281
|
+
),
|
|
282
|
+
eq(
|
|
283
|
+
schema.contentDatabaseMigrationReceipts.idempotencyKey,
|
|
284
|
+
args.idempotencyKey,
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
if (!receipt) throw new Error("Migration receipt not found.");
|
|
289
|
+
const storedResult = parseJson(receipt.resultJson);
|
|
290
|
+
const isTerminalReplay =
|
|
291
|
+
(args.phase === "verify" && receipt.state === "verified") ||
|
|
292
|
+
(args.phase === "finalize" && receipt.state === "finalized");
|
|
293
|
+
if (!isTerminalReplay) return null;
|
|
294
|
+
const expectedDigest =
|
|
295
|
+
args.phase === "verify"
|
|
296
|
+
? receipt.postDigest
|
|
297
|
+
: storedResult.transitionExpectedPostDigest;
|
|
298
|
+
if (expectedDigest !== args.expectedPostDigest)
|
|
299
|
+
throw new Error(
|
|
300
|
+
"Expected post-migration digest does not match receipt.",
|
|
301
|
+
);
|
|
302
|
+
if (
|
|
303
|
+
snapshotDigest(await snapshotMigration(tx, args.databaseId)) !==
|
|
304
|
+
receipt.postDigest
|
|
305
|
+
)
|
|
306
|
+
throw new Error(
|
|
307
|
+
args.phase === "verify"
|
|
308
|
+
? "Migration has drifted; verification is refused."
|
|
309
|
+
: "Terminal migration result has drifted; replay is refused.",
|
|
310
|
+
);
|
|
311
|
+
return receiptResult(receipt, true);
|
|
312
|
+
});
|
|
313
|
+
if (replay) return replay;
|
|
314
|
+
}
|
|
315
|
+
let mutated = false;
|
|
316
|
+
const result = await db.transaction(async (tx) => {
|
|
317
|
+
if (args.phase !== "validate") {
|
|
318
|
+
await lockContentDatabaseMutation(
|
|
319
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
320
|
+
databaseId,
|
|
321
|
+
);
|
|
322
|
+
await lockCurrentDatabaseMemberships(tx, databaseId);
|
|
323
|
+
}
|
|
324
|
+
if (args.phase === "validate" || args.phase === "apply") {
|
|
325
|
+
const planHash = digest(args.plan);
|
|
326
|
+
if (args.phase === "apply") {
|
|
327
|
+
const [existing] = await tx
|
|
328
|
+
.select()
|
|
329
|
+
.from(schema.contentDatabaseMigrationReceipts)
|
|
330
|
+
.where(
|
|
331
|
+
and(
|
|
332
|
+
eq(
|
|
333
|
+
schema.contentDatabaseMigrationReceipts.databaseId,
|
|
334
|
+
args.plan.databaseId,
|
|
335
|
+
),
|
|
336
|
+
eq(
|
|
337
|
+
schema.contentDatabaseMigrationReceipts.idempotencyKey,
|
|
338
|
+
args.plan.idempotencyKey,
|
|
339
|
+
),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
if (existing) {
|
|
343
|
+
if (existing.planHash !== planHash)
|
|
344
|
+
throw new Error(
|
|
345
|
+
"Idempotency key was already used with a different migration plan.",
|
|
346
|
+
);
|
|
347
|
+
if (existing.state !== "applied" && existing.state !== "verified")
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Migration receipt is already ${existing.state}.`,
|
|
350
|
+
);
|
|
351
|
+
const current = await snapshotMigration(tx, args.plan.databaseId);
|
|
352
|
+
if (snapshotDigest(current) !== existing.postDigest)
|
|
353
|
+
throw new Error(
|
|
354
|
+
"Applied migration has drifted; replay is refused.",
|
|
355
|
+
);
|
|
356
|
+
return receiptResult(existing, true);
|
|
357
|
+
}
|
|
358
|
+
// A separate server can run the same migration while an editor is
|
|
359
|
+
// saving. Keep the durable database lock across the flush so that
|
|
360
|
+
// save is part of the state reloaded and validated by this writer.
|
|
361
|
+
if (flushUnderDurableLock) {
|
|
362
|
+
validatePlan(
|
|
363
|
+
args.plan,
|
|
364
|
+
await snapshotMigration(tx, args.plan.databaseId),
|
|
365
|
+
);
|
|
366
|
+
await flushMigrationDocuments(args.plan.rows);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const snapshot = await snapshotMigration(tx, args.plan.databaseId);
|
|
370
|
+
validatePlan(args.plan, snapshot);
|
|
371
|
+
const preDigest = snapshotDigest(snapshot);
|
|
372
|
+
const orderedIds = snapshot.rows.map((row: any) => ({
|
|
373
|
+
itemId: row.item.id,
|
|
374
|
+
documentId: row.document.id,
|
|
375
|
+
}));
|
|
376
|
+
if (args.phase === "validate")
|
|
377
|
+
return {
|
|
378
|
+
phase: "validate",
|
|
379
|
+
planHash,
|
|
380
|
+
preDigest,
|
|
381
|
+
counts: {
|
|
382
|
+
rows: snapshot.rows.length,
|
|
383
|
+
properties: args.plan.propertyDefinitions.length,
|
|
384
|
+
},
|
|
385
|
+
orderedIds,
|
|
386
|
+
written: 0,
|
|
387
|
+
replayed: false,
|
|
388
|
+
verified: false,
|
|
389
|
+
};
|
|
390
|
+
const receiptId = deterministicId(
|
|
391
|
+
"migration_receipt",
|
|
392
|
+
args.plan.databaseId,
|
|
393
|
+
args.plan.idempotencyKey,
|
|
394
|
+
);
|
|
395
|
+
const now = new Date().toISOString();
|
|
396
|
+
await tx.insert(schema.contentDatabaseMigrationReceipts).values({
|
|
397
|
+
id: receiptId,
|
|
398
|
+
ownerEmail: snapshot.database.ownerEmail,
|
|
399
|
+
orgId: snapshot.database.orgId,
|
|
400
|
+
databaseId: args.plan.databaseId,
|
|
401
|
+
databaseDocumentId: args.plan.databaseDocumentId,
|
|
402
|
+
idempotencyKey: args.plan.idempotencyKey,
|
|
403
|
+
planHash,
|
|
404
|
+
state: "applying",
|
|
405
|
+
preDigest,
|
|
406
|
+
postDigest: preDigest,
|
|
407
|
+
rollbackJson: "{}",
|
|
408
|
+
resultJson: JSON.stringify({ phase: "apply", plan: args.plan }),
|
|
409
|
+
createdAt: now,
|
|
410
|
+
updatedAt: now,
|
|
411
|
+
});
|
|
412
|
+
const rollback = await applyMigration(
|
|
413
|
+
tx,
|
|
414
|
+
args.plan,
|
|
415
|
+
snapshot,
|
|
416
|
+
receiptId,
|
|
417
|
+
now,
|
|
418
|
+
);
|
|
419
|
+
const current = await snapshotMigration(tx, args.plan.databaseId);
|
|
420
|
+
const postDigest = snapshotDigest(current);
|
|
421
|
+
const resultJson = {
|
|
422
|
+
phase: "apply",
|
|
423
|
+
planHash,
|
|
424
|
+
legacyPropertyIds: args.plan.legacyPropertyIds,
|
|
425
|
+
counts: {
|
|
426
|
+
rows: args.plan.rows.length,
|
|
427
|
+
properties: args.plan.propertyDefinitions.length,
|
|
428
|
+
},
|
|
429
|
+
orderedIds,
|
|
430
|
+
written: args.plan.rows.length,
|
|
431
|
+
verified: false,
|
|
432
|
+
plan: args.plan,
|
|
433
|
+
};
|
|
434
|
+
const claimed = await tx
|
|
435
|
+
.update(schema.contentDatabaseMigrationReceipts)
|
|
436
|
+
.set({
|
|
437
|
+
state: "applied",
|
|
438
|
+
postDigest,
|
|
439
|
+
rollbackJson: JSON.stringify(rollback),
|
|
440
|
+
resultJson: JSON.stringify(resultJson),
|
|
441
|
+
updatedAt: now,
|
|
442
|
+
})
|
|
443
|
+
.where(
|
|
444
|
+
and(
|
|
445
|
+
eq(schema.contentDatabaseMigrationReceipts.id, receiptId),
|
|
446
|
+
eq(schema.contentDatabaseMigrationReceipts.state, "applying"),
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
.returning({ id: schema.contentDatabaseMigrationReceipts.id });
|
|
450
|
+
if (claimed.length !== 1)
|
|
451
|
+
throw new Error("Migration receipt claim was lost.");
|
|
452
|
+
mutated = true;
|
|
453
|
+
return {
|
|
454
|
+
phase: "apply",
|
|
455
|
+
planHash,
|
|
456
|
+
legacyPropertyIds: args.plan.legacyPropertyIds,
|
|
457
|
+
counts: resultJson.counts,
|
|
458
|
+
orderedIds,
|
|
459
|
+
written: args.plan.rows.length,
|
|
460
|
+
verified: false,
|
|
461
|
+
receiptId,
|
|
462
|
+
state: "applied",
|
|
463
|
+
preDigest,
|
|
464
|
+
postDigest,
|
|
465
|
+
replayed: false,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const [receipt] = await tx
|
|
469
|
+
.select()
|
|
470
|
+
.from(schema.contentDatabaseMigrationReceipts)
|
|
471
|
+
.where(
|
|
472
|
+
and(
|
|
473
|
+
eq(
|
|
474
|
+
schema.contentDatabaseMigrationReceipts.databaseId,
|
|
475
|
+
args.databaseId,
|
|
476
|
+
),
|
|
477
|
+
eq(
|
|
478
|
+
schema.contentDatabaseMigrationReceipts.idempotencyKey,
|
|
479
|
+
args.idempotencyKey,
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
);
|
|
483
|
+
if (!receipt) throw new Error("Migration receipt not found.");
|
|
484
|
+
const storedResult = parseJson(receipt.resultJson);
|
|
485
|
+
if (args.phase === "verify" && receipt.state === "verified") {
|
|
486
|
+
if (receipt.postDigest !== args.expectedPostDigest)
|
|
487
|
+
throw new Error(
|
|
488
|
+
"Expected post-migration digest does not match receipt.",
|
|
489
|
+
);
|
|
490
|
+
if (
|
|
491
|
+
snapshotDigest(await snapshotMigration(tx, args.databaseId)) !==
|
|
492
|
+
receipt.postDigest
|
|
493
|
+
)
|
|
494
|
+
throw new Error("Migration has drifted; verification is refused.");
|
|
495
|
+
return receiptResult(receipt, true);
|
|
496
|
+
}
|
|
497
|
+
if (
|
|
498
|
+
(args.phase === "rollback" && receipt.state === "rolled_back") ||
|
|
499
|
+
(args.phase === "finalize" && receipt.state === "finalized")
|
|
500
|
+
) {
|
|
501
|
+
if (
|
|
502
|
+
storedResult.transitionExpectedPostDigest !==
|
|
503
|
+
args.expectedPostDigest
|
|
504
|
+
)
|
|
505
|
+
throw new Error(
|
|
506
|
+
"Expected post-migration digest does not match receipt.",
|
|
507
|
+
);
|
|
508
|
+
if (
|
|
509
|
+
snapshotDigest(await snapshotMigration(tx, args.databaseId)) !==
|
|
510
|
+
receipt.postDigest
|
|
511
|
+
)
|
|
512
|
+
throw new Error(
|
|
513
|
+
"Terminal migration result has drifted; replay is refused.",
|
|
514
|
+
);
|
|
515
|
+
return receiptResult(receipt, true);
|
|
516
|
+
}
|
|
517
|
+
if (args.phase === "verify") {
|
|
518
|
+
if (receipt.state !== "applied" && receipt.state !== "verified")
|
|
519
|
+
throw new Error(`Migration receipt is already ${receipt.state}.`);
|
|
520
|
+
if (receipt.postDigest !== args.expectedPostDigest)
|
|
521
|
+
throw new Error(
|
|
522
|
+
"Expected post-migration digest does not match receipt.",
|
|
523
|
+
);
|
|
524
|
+
const current = await snapshotMigration(tx, args.databaseId);
|
|
525
|
+
if (snapshotDigest(current) !== receipt.postDigest)
|
|
526
|
+
throw new Error("Migration has drifted; verification is refused.");
|
|
527
|
+
const plan = parseJson(receipt.resultJson).plan;
|
|
528
|
+
if (!plan)
|
|
529
|
+
throw new Error("Migration receipt lacks its verification plan.");
|
|
530
|
+
const newDefinitions = new Map(
|
|
531
|
+
plan.propertyDefinitions.map((definition: any) => [
|
|
532
|
+
definition.id,
|
|
533
|
+
definition,
|
|
534
|
+
]),
|
|
535
|
+
);
|
|
536
|
+
if (
|
|
537
|
+
current.definitions.filter((definition: any) =>
|
|
538
|
+
newDefinitions.has(definition.id),
|
|
539
|
+
).length !== plan.propertyDefinitions.length
|
|
540
|
+
)
|
|
541
|
+
throw new Error(
|
|
542
|
+
"Migration verification found a property definition count mismatch.",
|
|
543
|
+
);
|
|
544
|
+
for (const definition of plan.propertyDefinitions) {
|
|
545
|
+
const actual = current.definitions.find(
|
|
546
|
+
(candidate: any) => candidate.id === definition.id,
|
|
547
|
+
);
|
|
548
|
+
const optionsJson = JSON.stringify(
|
|
549
|
+
definition.type === "multi_select"
|
|
550
|
+
? { options: definition.options }
|
|
551
|
+
: {},
|
|
552
|
+
);
|
|
553
|
+
if (
|
|
554
|
+
!actual ||
|
|
555
|
+
actual.name !== definition.name.trim() ||
|
|
556
|
+
actual.type !== definition.type ||
|
|
557
|
+
actual.visibility !== definition.visibility ||
|
|
558
|
+
actual.optionsJson !== optionsJson
|
|
559
|
+
)
|
|
560
|
+
throw new Error(
|
|
561
|
+
"Migration verification found a property definition mismatch.",
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
for (const row of plan.rows) {
|
|
565
|
+
const persisted = current.rows.find(
|
|
566
|
+
(candidate: any) =>
|
|
567
|
+
candidate.item.id === row.itemId &&
|
|
568
|
+
candidate.document.id === row.documentId,
|
|
569
|
+
);
|
|
570
|
+
if (!persisted || persisted.document.content !== row.content)
|
|
571
|
+
throw new Error(
|
|
572
|
+
"Migration verification found a row body mismatch.",
|
|
573
|
+
);
|
|
574
|
+
for (const value of row.propertyValues) {
|
|
575
|
+
const actual = current.values.find(
|
|
576
|
+
(candidate: any) =>
|
|
577
|
+
candidate.documentId === row.documentId &&
|
|
578
|
+
candidate.propertyId === value.propertyId,
|
|
579
|
+
)?.valueJson;
|
|
580
|
+
if (
|
|
581
|
+
actual !==
|
|
582
|
+
serializeMigrationValue(
|
|
583
|
+
newDefinitions.get(value.propertyId) as any,
|
|
584
|
+
value.value,
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
throw new Error(
|
|
588
|
+
"Migration verification found a new property mismatch.",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
for (const protectedValue of row.protectedPropertyValues) {
|
|
592
|
+
const actual =
|
|
593
|
+
current.values.find(
|
|
594
|
+
(candidate: any) =>
|
|
595
|
+
candidate.documentId === row.documentId &&
|
|
596
|
+
candidate.propertyId === protectedValue.propertyId,
|
|
597
|
+
)?.valueJson ?? "null";
|
|
598
|
+
if (actual !== protectedValue.valueJson)
|
|
599
|
+
throw new Error(
|
|
600
|
+
"Migration verification found a protected property mismatch.",
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const now = new Date().toISOString();
|
|
605
|
+
const resultJson = {
|
|
606
|
+
...parseJson(receipt.resultJson),
|
|
607
|
+
phase: "verify",
|
|
608
|
+
verified: true,
|
|
609
|
+
};
|
|
610
|
+
const transitioned = await tx
|
|
611
|
+
.update(schema.contentDatabaseMigrationReceipts)
|
|
612
|
+
.set({
|
|
613
|
+
state: "verified",
|
|
614
|
+
resultJson: JSON.stringify(resultJson),
|
|
615
|
+
updatedAt: now,
|
|
616
|
+
})
|
|
617
|
+
.where(
|
|
618
|
+
and(
|
|
619
|
+
eq(schema.contentDatabaseMigrationReceipts.id, receipt.id),
|
|
620
|
+
eq(schema.contentDatabaseMigrationReceipts.state, "applied"),
|
|
621
|
+
),
|
|
622
|
+
)
|
|
623
|
+
.returning({ id: schema.contentDatabaseMigrationReceipts.id });
|
|
624
|
+
if (transitioned.length !== 1)
|
|
625
|
+
throw new Error("Migration receipt state changed during verify.");
|
|
626
|
+
mutated = true;
|
|
627
|
+
return {
|
|
628
|
+
receiptId: receipt.id,
|
|
629
|
+
state: "verified",
|
|
630
|
+
preDigest: receipt.preDigest,
|
|
631
|
+
postDigest: receipt.postDigest,
|
|
632
|
+
replayed: false,
|
|
633
|
+
verified: true,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (args.phase === "finalize" && receipt.state !== "verified")
|
|
637
|
+
throw new Error(
|
|
638
|
+
"Migration receipt must be verified before finalization.",
|
|
639
|
+
);
|
|
640
|
+
if (
|
|
641
|
+
args.phase === "rollback" &&
|
|
642
|
+
receipt.state !== "applied" &&
|
|
643
|
+
receipt.state !== "verified"
|
|
644
|
+
)
|
|
645
|
+
throw new Error(`Migration receipt is already ${receipt.state}.`);
|
|
646
|
+
if (receipt.postDigest !== args.expectedPostDigest)
|
|
647
|
+
throw new Error(
|
|
648
|
+
"Expected post-migration digest does not match receipt.",
|
|
649
|
+
);
|
|
650
|
+
if (args.phase === "rollback" && flushUnderDurableLock) {
|
|
651
|
+
await flushMigrationDocuments(
|
|
652
|
+
parseJson(receipt.rollbackJson).versions ?? [],
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const current = await snapshotMigration(tx, args.databaseId);
|
|
656
|
+
if (snapshotDigest(current) !== receipt.postDigest)
|
|
657
|
+
throw new Error(
|
|
658
|
+
"Migration has drifted; guarded operation is refused.",
|
|
659
|
+
);
|
|
660
|
+
const rollback = parseJson(receipt.rollbackJson);
|
|
661
|
+
const now = new Date().toISOString();
|
|
662
|
+
if (args.phase === "rollback") {
|
|
663
|
+
for (const prior of rollback.versions ?? []) {
|
|
664
|
+
const [version] = await tx
|
|
665
|
+
.select()
|
|
666
|
+
.from(schema.documentVersions)
|
|
667
|
+
.where(eq(schema.documentVersions.id, prior.versionId));
|
|
668
|
+
if (!version) throw new Error("Rollback snapshot is missing.");
|
|
669
|
+
const restoredRows = await tx
|
|
670
|
+
.update(schema.documents)
|
|
671
|
+
.set({
|
|
672
|
+
title: version.title,
|
|
673
|
+
content: version.content,
|
|
674
|
+
updatedAt: now,
|
|
675
|
+
})
|
|
676
|
+
.where(
|
|
677
|
+
and(
|
|
678
|
+
eq(schema.documents.id, prior.documentId),
|
|
679
|
+
eq(schema.documents.updatedAt, prior.appliedUpdatedAt),
|
|
680
|
+
),
|
|
681
|
+
)
|
|
682
|
+
.returning({ id: schema.documents.id });
|
|
683
|
+
if (restoredRows.length !== 1)
|
|
684
|
+
throw new Error(
|
|
685
|
+
`Rollback row ${prior.documentId} changed concurrently.`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const ids = rollback.createdPropertyIds ?? [];
|
|
689
|
+
if (ids.length) {
|
|
690
|
+
await tx
|
|
691
|
+
.delete(schema.documentPropertyValues)
|
|
692
|
+
.where(inArray(schema.documentPropertyValues.propertyId, ids));
|
|
693
|
+
await tx
|
|
694
|
+
.delete(schema.documentPropertyDefinitions)
|
|
695
|
+
.where(inArray(schema.documentPropertyDefinitions.id, ids));
|
|
696
|
+
}
|
|
697
|
+
await tx
|
|
698
|
+
.update(schema.contentDatabases)
|
|
699
|
+
.set({ updatedAt: now })
|
|
700
|
+
.where(eq(schema.contentDatabases.id, args.databaseId));
|
|
701
|
+
const restored = await snapshotMigration(tx, args.databaseId);
|
|
702
|
+
const postDigest = snapshotDigest(restored);
|
|
703
|
+
if (postDigest !== receipt.preDigest)
|
|
704
|
+
throw new Error("Rollback verification failed.");
|
|
705
|
+
const resultJson = {
|
|
706
|
+
phase: "rollback",
|
|
707
|
+
transitionExpectedPostDigest: receipt.postDigest,
|
|
708
|
+
counts: {
|
|
709
|
+
rows: (rollback.versions ?? []).length,
|
|
710
|
+
properties: ids.length,
|
|
711
|
+
},
|
|
712
|
+
verified: true,
|
|
713
|
+
};
|
|
714
|
+
const transitioned = await tx
|
|
715
|
+
.update(schema.contentDatabaseMigrationReceipts)
|
|
716
|
+
.set({
|
|
717
|
+
state: "rolled_back",
|
|
718
|
+
postDigest,
|
|
719
|
+
resultJson: JSON.stringify(resultJson),
|
|
720
|
+
updatedAt: now,
|
|
721
|
+
})
|
|
722
|
+
.where(
|
|
723
|
+
and(
|
|
724
|
+
eq(schema.contentDatabaseMigrationReceipts.id, receipt.id),
|
|
725
|
+
inArray(schema.contentDatabaseMigrationReceipts.state, [
|
|
726
|
+
"applied",
|
|
727
|
+
"verified",
|
|
728
|
+
]),
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
.returning({ id: schema.contentDatabaseMigrationReceipts.id });
|
|
732
|
+
if (transitioned.length !== 1)
|
|
733
|
+
throw new Error("Migration receipt state changed during rollback.");
|
|
734
|
+
mutated = true;
|
|
735
|
+
return {
|
|
736
|
+
receiptId: receipt.id,
|
|
737
|
+
state: "rolled_back",
|
|
738
|
+
preDigest: receipt.preDigest,
|
|
739
|
+
postDigest,
|
|
740
|
+
counts: {
|
|
741
|
+
rows: (rollback.versions ?? []).length,
|
|
742
|
+
properties: ids.length,
|
|
743
|
+
},
|
|
744
|
+
replayed: false,
|
|
745
|
+
verified: true,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
const planResult = parseJson(receipt.resultJson);
|
|
749
|
+
// Legacy ids are deliberately copied into the receipt result only after apply validation.
|
|
750
|
+
const legacyIds: string[] = planResult.legacyPropertyIds ?? [];
|
|
751
|
+
const legacy = current.definitions.filter((definition: any) =>
|
|
752
|
+
legacyIds.includes(definition.id),
|
|
753
|
+
);
|
|
754
|
+
if (
|
|
755
|
+
legacy.length !== legacyIds.length ||
|
|
756
|
+
legacy.some(
|
|
757
|
+
(definition: any) =>
|
|
758
|
+
definition.systemRole || definition.type === "blocks",
|
|
759
|
+
)
|
|
760
|
+
)
|
|
761
|
+
throw new Error("Legacy property is missing or unsafe to finalize.");
|
|
762
|
+
if (legacyIds.length) {
|
|
763
|
+
await tx
|
|
764
|
+
.delete(schema.documentPropertyValues)
|
|
765
|
+
.where(
|
|
766
|
+
inArray(schema.documentPropertyValues.propertyId, legacyIds),
|
|
767
|
+
);
|
|
768
|
+
await tx
|
|
769
|
+
.delete(schema.documentPropertyDefinitions)
|
|
770
|
+
.where(inArray(schema.documentPropertyDefinitions.id, legacyIds));
|
|
771
|
+
}
|
|
772
|
+
await touchContentDatabase(
|
|
773
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
774
|
+
args.databaseId,
|
|
775
|
+
now,
|
|
776
|
+
);
|
|
777
|
+
const finalized = await snapshotMigration(tx, args.databaseId);
|
|
778
|
+
if (
|
|
779
|
+
finalized.definitions.some((definition: any) =>
|
|
780
|
+
legacyIds.includes(definition.id),
|
|
781
|
+
) ||
|
|
782
|
+
finalized.values.some((value: any) =>
|
|
783
|
+
legacyIds.includes(value.propertyId),
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
throw new Error("Legacy property finalization verification failed.");
|
|
787
|
+
const postDigest = snapshotDigest(finalized);
|
|
788
|
+
const resultJson = {
|
|
789
|
+
phase: "finalize",
|
|
790
|
+
transitionExpectedPostDigest: receipt.postDigest,
|
|
791
|
+
counts: { rows: 0, properties: legacyIds.length },
|
|
792
|
+
verified: true,
|
|
793
|
+
};
|
|
794
|
+
const transitioned = await tx
|
|
795
|
+
.update(schema.contentDatabaseMigrationReceipts)
|
|
796
|
+
.set({
|
|
797
|
+
state: "finalized",
|
|
798
|
+
postDigest,
|
|
799
|
+
resultJson: JSON.stringify(resultJson),
|
|
800
|
+
updatedAt: now,
|
|
801
|
+
})
|
|
802
|
+
.where(
|
|
803
|
+
and(
|
|
804
|
+
eq(schema.contentDatabaseMigrationReceipts.id, receipt.id),
|
|
805
|
+
eq(schema.contentDatabaseMigrationReceipts.state, "verified"),
|
|
806
|
+
),
|
|
807
|
+
)
|
|
808
|
+
.returning({ id: schema.contentDatabaseMigrationReceipts.id });
|
|
809
|
+
if (transitioned.length !== 1)
|
|
810
|
+
throw new Error("Migration receipt state changed during finalize.");
|
|
811
|
+
mutated = true;
|
|
812
|
+
return {
|
|
813
|
+
receiptId: receipt.id,
|
|
814
|
+
state: "finalized",
|
|
815
|
+
preDigest: receipt.preDigest,
|
|
816
|
+
postDigest,
|
|
817
|
+
counts: { rows: 0, properties: legacyIds.length },
|
|
818
|
+
replayed: false,
|
|
819
|
+
verified: true,
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
if (mutated)
|
|
823
|
+
await writeAppState("refresh-signal", { ts: Date.now() }).catch(() => {
|
|
824
|
+
// The receipt is already committed; polling reconciles this optional
|
|
825
|
+
// UI hint when a concurrent SQLite writer briefly holds the database.
|
|
826
|
+
});
|
|
827
|
+
return result;
|
|
828
|
+
});
|
|
829
|
+
},
|
|
830
|
+
});
|