@notis_ai/cli 0.2.18 → 0.2.19
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/README.md +542 -146
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_design_rules.json +320 -0
- package/config/portal-route-roots.json +45 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +42325 -18273
- package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-cli/SKILL.md +51 -280
- package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
- package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-query/SKILL.md +35 -699
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +74 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-spaces/SKILL.md +218 -0
- package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
- package/dist/base-skills/notis-spaces/references/context.md +81 -0
- package/dist/base-skills/notis-spaces/references/design.md +141 -0
- package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
- package/dist/base-skills/notis-spaces/references/memory.md +58 -0
- package/dist/base-skills/notis-spaces/references/reading.md +73 -0
- package/dist/base-skills/notis-spaces/references/release.md +120 -0
- package/dist/base-skills/notis-spaces/references/reports.md +77 -0
- package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
- package/dist/base-skills/notis-spaces/references/store.md +118 -0
- package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
- package/dist/base-skills/notis-spaces/references/views.md +229 -0
- package/{template/packages → dist}/sdk/package.json +9 -10
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +175 -0
- package/dist/sdk/src/components/Dialog.tsx +103 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
- package/dist/sdk/src/components/DocumentPage.tsx +184 -0
- package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
- package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
- package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
- package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
- package/dist/sdk/src/components/RecordComponents.tsx +111 -0
- package/dist/sdk/src/components/Skeleton.tsx +38 -0
- package/dist/sdk/src/config.ts +31 -0
- package/{template/packages → dist}/sdk/src/documents.ts +21 -6
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
- package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
- package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
- package/dist/sdk/src/hooks/useDocument.ts +51 -0
- package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
- package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
- package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
- package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
- package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
- package/dist/sdk/src/hooks/useQuery.ts +81 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
- package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
- package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
- package/dist/sdk/src/hooks/useViewParams.ts +195 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/{template/packages → dist}/sdk/src/index.ts +64 -1
- package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
- package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
- package/dist/sdk/src/nativeData.ts +39 -0
- package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +292 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +675 -0
- package/dist/sdk/src/runtimeModules.ts +33 -0
- package/dist/sdk/src/space.ts +442 -0
- package/dist/sdk/src/standardViewParams.ts +36 -0
- package/{template/packages → dist}/sdk/src/styles.css +81 -16
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
- package/dist/sdk/src/ui/engine/core.ts +392 -0
- package/dist/sdk/src/ui/engine/dom.ts +183 -0
- package/dist/sdk/src/ui/engine/hooks.ts +195 -0
- package/dist/sdk/src/ui/engine/instance.ts +63 -0
- package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
- package/dist/sdk/src/ui/engine/registry.ts +262 -0
- package/dist/sdk/src/ui/engine/types.ts +62 -0
- package/dist/sdk/src/ui/icons.tsx +91 -0
- package/dist/sdk/src/ui/util.ts +83 -0
- package/dist/sdk/src/viewPeriod.ts +26 -0
- package/dist/sdk/src/vite.ts +92 -0
- package/dist/sdk/src/words/actions.ts +295 -0
- package/dist/sdk/src/words/clashes.ts +158 -0
- package/dist/sdk/src/words/format.ts +136 -0
- package/dist/sdk/src/words/index.ts +13 -0
- package/dist/sdk/src/words/keymap.ts +72 -0
- package/dist/sdk/src/words/keys.ts +297 -0
- package/dist/sdk/src/words/shell.ts +46 -0
- package/dist/sdk/src/words/types.ts +65 -0
- package/dist/skill-sync/index.js +832 -151
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +3941 -0
- package/dist/space-harness/host.js +1460 -0
- package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
- package/dist/space-sdk/dist/index.js +15 -0
- package/dist/space-sdk/dist/ui.js +18 -0
- package/dist/space-sdk/package.json +33 -0
- package/dist/space-sdk/src/agentContext.d.ts +36 -0
- package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
- package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
- package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
- package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
- package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
- package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
- package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
- package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
- package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
- package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
- package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
- package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
- package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
- package/dist/space-sdk/src/config.ts +31 -0
- package/dist/space-sdk/src/documents.d.ts +59 -0
- package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
- package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
- package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
- package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
- package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
- package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
- package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
- package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
- package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
- package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
- package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
- package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
- package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
- package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
- package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
- package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
- package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
- package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
- package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
- package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
- package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
- package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
- package/dist/space-sdk/src/index.d.ts +98 -0
- package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
- package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
- package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
- package/dist/space-sdk/src/nativeData.d.ts +66 -0
- package/dist/space-sdk/src/provider.d.ts +17 -0
- package/dist/space-sdk/src/queryCache.d.ts +45 -0
- package/dist/space-sdk/src/queryErrors.ts +51 -0
- package/dist/space-sdk/src/runtime.d.ts +615 -0
- package/dist/space-sdk/src/runtimeModules.ts +33 -0
- package/dist/space-sdk/src/space.ts +442 -0
- package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
- package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
- package/dist/space-sdk/src/tailwind.ts +66 -0
- package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
- package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
- package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
- package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
- package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
- package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
- package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
- package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
- package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
- package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
- package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
- package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
- package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
- package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
- package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
- package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
- package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
- package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
- package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
- package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
- package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
- package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
- package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
- package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
- package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
- package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
- package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
- package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
- package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
- package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
- package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
- package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
- package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
- package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
- package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
- package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
- package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
- package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/icons.d.ts +13 -0
- package/dist/space-sdk/src/ui/index.d.ts +54 -0
- package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
- package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
- package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
- package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
- package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
- package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
- package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
- package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
- package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
- package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
- package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
- package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
- package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
- package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
- package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
- package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
- package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
- package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
- package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
- package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
- package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
- package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
- package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
- package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
- package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
- package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
- package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
- package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
- package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
- package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
- package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
- package/dist/space-sdk/src/ui/styles.d.ts +21 -0
- package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
- package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
- package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
- package/dist/space-sdk/src/vite.ts +92 -0
- package/dist/space-sdk/src/words/actions.ts +295 -0
- package/dist/space-sdk/src/words/clashes.ts +158 -0
- package/dist/space-sdk/src/words/format.ts +136 -0
- package/dist/space-sdk/src/words/index.ts +13 -0
- package/dist/space-sdk/src/words/keymap.ts +72 -0
- package/dist/space-sdk/src/words/keys.ts +297 -0
- package/dist/space-sdk/src/words/shell.ts +46 -0
- package/dist/space-sdk/src/words/types.ts +65 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +2374 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +212 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +250 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/png.js +213 -0
- package/dist/view-renderer/src/pool.js +110 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +271 -0
- package/package.json +21 -11
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
- package/skills/notis-onboarding/BRIEF.md +14 -9
- package/skills/notis-query/cli.md +1 -1
- package/skills/notis-spaces/cli.md +540 -0
- package/src/cli.js +8 -8
- package/src/command-specs/agents.js +121 -8
- package/src/command-specs/auth.js +3 -3
- package/src/command-specs/diagnostics.js +97 -20
- package/src/command-specs/documents.js +158 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +331 -79
- package/src/command-specs/helpers.js +50 -40
- package/src/command-specs/index.js +14 -5
- package/src/command-specs/meta.js +38 -9
- package/src/command-specs/onboarding.js +1 -1
- package/src/command-specs/profile.js +42 -1
- package/src/command-specs/skills.js +314 -9
- package/src/command-specs/smoke.js +16 -4
- package/src/command-specs/spaces.js +783 -0
- package/src/command-specs/tools.js +118 -21
- package/src/command-specs/version-history.js +20 -0
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +178 -267
- package/src/runtime/agent-setup.js +172 -13
- package/src/runtime/app-boundary-validator.js +338 -2
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +750 -1570
- package/src/runtime/auth-recovery.js +17 -0
- package/src/runtime/base-skills.d.ts +11 -0
- package/src/runtime/base-skills.js +100 -3
- package/src/runtime/computer-identity.d.ts +30 -0
- package/src/runtime/computer-identity.js +151 -0
- package/src/runtime/config-lock.js +135 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +5 -21
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +342 -31
- package/src/runtime/profiles.js +19 -112
- package/src/runtime/retired-sdk-files.js +24 -0
- package/src/runtime/retry.js +150 -0
- package/src/runtime/skill-sync/cloud-client.ts +33 -8
- package/src/runtime/skill-sync/index.ts +443 -106
- package/src/runtime/skill-sync/local-scanner.ts +248 -32
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +70 -0
- package/src/runtime/skill-sync/symlink-manager.ts +66 -16
- package/src/runtime/skill-sync/sync-plan.ts +34 -2
- package/src/runtime/skill-sync/types.ts +54 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +107 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +68 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +133 -0
- package/src/runtime/space-source.js +290 -0
- package/src/runtime/space-test-server.js +354 -0
- package/src/runtime/space-verification.js +94 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +368 -0
- package/src/runtime/sync-skills.d.ts +5 -1
- package/src/runtime/sync-skills.js +11 -2
- package/src/runtime/tool-confirmation.js +138 -0
- package/src/runtime/transport.js +118 -24
- package/src/runtime/upsell.js +219 -0
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +114 -0
- package/src/space-harness-host.ts +122 -0
- package/template/.harness/index.html.tmpl +9 -352
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +2 -6
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +4 -36
- package/template/package-lock.json +1800 -1867
- package/template/package.json +16 -14
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/template/vite.config.ts +2 -2
- package/dist/base-skills/notis-apps/SKILL.md +0 -613
- package/skills/notis-apps/cli.md +0 -285
- package/src/command-specs/apps.js +0 -3011
- package/src/runtime/app-changelog.js +0 -79
- package/src/runtime/app-dev-consumers.js +0 -154
- package/src/runtime/app-dev-host-lock.js +0 -80
- package/src/runtime/app-dev-process-identity.js +0 -111
- package/src/runtime/app-dev-roots.js +0 -284
- package/src/runtime/app-dev-server.js +0 -1140
- package/src/runtime/app-dev-sessions.js +0 -185
- package/src/runtime/app-registry-scaffolds.js +0 -367
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/cli-mode.generated.js +0 -5
- package/src/runtime/cli-mode.js +0 -34
- package/src/runtime/store-screenshot.js +0 -143
- package/template/CHANGELOG.md +0 -5
- package/template/app/page.tsx +0 -60
- package/template/components/ui/card.tsx +0 -56
- package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
- package/template/packages/sdk/src/config.ts +0 -259
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/template/packages/sdk/src/runtime.ts +0 -439
- package/template/packages/sdk/src/vite.ts +0 -56
- package/template/packages/sdk/tsconfig.json +0 -15
- /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
- /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/** Offline, frozen Space fixtures. No credentials or provider transport exist here. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
9
|
+
import addFormats from 'ajv-formats';
|
|
10
|
+
import { appFilesDigest } from './app-platform.js';
|
|
11
|
+
import { usageError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'../..');
|
|
14
|
+
// Credentialless worker + non-forwarding proxy own the offline boundary.
|
|
15
|
+
// Source shares the Shadow host realm and may call only this fixture origin.
|
|
16
|
+
const HOST_POLICY="default-src 'none'; script-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'self'; frame-src 'none'; frame-ancestors 'self'; form-action 'none'; base-uri 'none'";
|
|
17
|
+
|
|
18
|
+
function object(value) { return value && typeof value==='object' && !Array.isArray(value); }
|
|
19
|
+
/** The declared params a list's filter reads (server shown_list_params); `{record}` reads the first record param. */
|
|
20
|
+
function shownListParams(where, params) {
|
|
21
|
+
const declared=object(params) ? params : {};
|
|
22
|
+
const single=Object.entries(declared).find(([,item])=>object(item) && item.type==='record')?.[0] ?? null;
|
|
23
|
+
const names=new Set();
|
|
24
|
+
const walk=node=>{
|
|
25
|
+
if(!object(node)) return;
|
|
26
|
+
for(const group of ['and','or']) if(Array.isArray(node[group])) { node[group].forEach(walk); return; }
|
|
27
|
+
if(typeof node.when==='string' && Object.hasOwn(declared,node.when)) names.add(node.when);
|
|
28
|
+
for(const value of Array.isArray(node.value)?node.value:[node.value]) {
|
|
29
|
+
const match=typeof value==='string' ? /^\{([a-z][A-Za-z0-9_]{0,63})(?:\.([^{}.\s][^{}]{0,199}))?\}$/.exec(value) : null;
|
|
30
|
+
const name=match?.[1]==='record' ? single : match?.[1];
|
|
31
|
+
if(name && Object.hasOwn(declared,name)) names.add(name);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walk(where);
|
|
35
|
+
return [...names].sort();
|
|
36
|
+
}
|
|
37
|
+
/** Public runtime declarations (server declared_view_runtime): params, list names and chrome, never filters (nor a record page's rule). */
|
|
38
|
+
function viewDeclarations(manifest) {
|
|
39
|
+
const shows=Object.fromEntries(Object.entries(manifest.shows || {}).map(([name,value])=>[name,Object.hasOwn(value,'where')
|
|
40
|
+
? {database:value.database,...(value.open?{open:value.open}:{}),params:shownListParams(value.where,manifest.params)} : {about:value.about,...(value.services?.length?{services:value.services}:{})}]));
|
|
41
|
+
return {...(manifest.params && Object.keys(manifest.params).length?{params:Object.fromEntries(Object.entries(manifest.params).map(([name,{page,...value}])=>[name,structuredClone(value)]))}:{}),
|
|
42
|
+
...(Object.keys(shows).length?{shows}:{}),...(['portal','hidden'].includes(manifest.chrome)?{chrome:manifest.chrome}:{})};
|
|
43
|
+
}
|
|
44
|
+
// What a first-party record component reads, and the smallest valid fixture for it, so a missing
|
|
45
|
+
// fixture names exactly what to add.
|
|
46
|
+
const RECORD_VIEW_READS={record:'readonly note snapshot',html:'stored HTML page',report:'report source'};
|
|
47
|
+
function recordViewExample(operation,key) {
|
|
48
|
+
const result={
|
|
49
|
+
record:{record_key:key,writable:false,schema:{properties:{}},document:{record_key:key,properties:{}}},
|
|
50
|
+
html:{record_key:key,html:'<h1>Fictional page</h1>'},
|
|
51
|
+
report:{source:'export default function Report() { return null; }',export_name:'default'},
|
|
52
|
+
}[operation];
|
|
53
|
+
return JSON.stringify({operation,record_key:key,result});
|
|
54
|
+
}
|
|
55
|
+
const VIEWER_READS={list_databases:'databases',get_database:'databases',query_database:'databases',list_skills:'skills',get_skill:'skills'};
|
|
56
|
+
|
|
57
|
+
export function createSpaceFixtureEngine(release) {
|
|
58
|
+
const bodyTemplates=structuredClone(release.manifest.actions || {});
|
|
59
|
+
const bodyResources=structuredClone(release.manifest.resources || {});
|
|
60
|
+
let fixtures={actions:{}};
|
|
61
|
+
if(release.fixturePath) {
|
|
62
|
+
const encoded=release.sourceFiles[release.fixturePath];
|
|
63
|
+
if(typeof encoded!=='string') throw usageError('The selected verification fixtures are missing from the frozen source.');
|
|
64
|
+
try { fixtures=JSON.parse(Buffer.from(encoded,'base64').toString('utf8')); }
|
|
65
|
+
catch { throw usageError('Verification fixtures must be finite JSON objects.'); }
|
|
66
|
+
}
|
|
67
|
+
if(!object(fixtures) || Object.keys(fixtures).some(key=>!['actions','context','documentBodies','viewerReads','shown','recordViews'].includes(key)) || !object(fixtures.actions)
|
|
68
|
+
|| (fixtures.context!==undefined && !object(fixtures.context))) throw usageError('Verification fixtures declare actions and optional synthetic context.');
|
|
69
|
+
// Viewer reads have no grant: fixtures stand in for what a signed-in viewer could open.
|
|
70
|
+
const declaredReads=release.manifest.viewer_reads || [];
|
|
71
|
+
// V4: a declared list is served from fixtures per exact params, like a signed-in Editor's read.
|
|
72
|
+
const declaredLists=Object.fromEntries(Object.entries(release.manifest.shows || {}).filter(([,value])=>object(value) && Object.hasOwn(value,'where')));
|
|
73
|
+
const shownCases=fixtures.shown ?? {};
|
|
74
|
+
if(!object(shownCases)) throw usageError('Declare shown fixtures by list name.');
|
|
75
|
+
for(const [name,cases] of Object.entries(shownCases)) {
|
|
76
|
+
if(!Object.hasOwn(declaredLists,name) || !Array.isArray(cases) || !cases.length || cases.length>100) {
|
|
77
|
+
throw usageError(`Declare shown fixtures only for lists this view declares with a filter (${name}).`);
|
|
78
|
+
}
|
|
79
|
+
const previous=[];
|
|
80
|
+
for(const entry of cases) {
|
|
81
|
+
if(!object(entry) || Object.keys(entry).some(key=>!['params','result'].includes(key)) || !object(entry.params) || !object(entry.result)) {
|
|
82
|
+
throw usageError(`Declare each ${name} fixture as params and a result.`);
|
|
83
|
+
}
|
|
84
|
+
if(previous.some(params=>isDeepStrictEqual(params,entry.params))) throw usageError(`Ambiguous shown fixture for ${name}.`);
|
|
85
|
+
previous.push(entry.params);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const readCases=fixtures.viewerReads ?? {};
|
|
89
|
+
if(!object(readCases)) throw usageError('Declare viewerReads fixtures by read operation.');
|
|
90
|
+
for(const [operation,cases] of Object.entries(readCases)) {
|
|
91
|
+
if(!Object.hasOwn(VIEWER_READS,operation) || !declaredReads.includes(VIEWER_READS[operation]) || !Array.isArray(cases) || !cases.length || cases.length>100) {
|
|
92
|
+
throw usageError(`Declare explicit viewer read fixtures only for reads this Space declares (${operation}).`);
|
|
93
|
+
}
|
|
94
|
+
const previous=[];
|
|
95
|
+
for(const entry of cases) {
|
|
96
|
+
if(!object(entry) || Object.keys(entry).some(key=>!['input','result'].includes(key)) || !Object.hasOwn(entry,'result') || !object(entry.input)) {
|
|
97
|
+
throw usageError(`Declare each ${operation} fixture as an input object and a result.`);
|
|
98
|
+
}
|
|
99
|
+
if(previous.some(input=>isDeepStrictEqual(input,entry.input))) throw usageError(`Ambiguous viewer read fixture for ${operation}.`);
|
|
100
|
+
previous.push(entry.input);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const ajv=new Ajv2020({strict:false,strictNumbers:true,allErrors:true,coerceTypes:false,useDefaults:false,removeAdditional:false});
|
|
104
|
+
addFormats(ajv);
|
|
105
|
+
const validators=new Map();
|
|
106
|
+
for(const [name,template] of Object.entries(release.manifest.actions || {})) {
|
|
107
|
+
let validate;
|
|
108
|
+
try { validate=ajv.compile(template.inputs); }
|
|
109
|
+
catch { throw usageError(`Cannot validate the declared inputs for ${name}.`); }
|
|
110
|
+
validators.set(name,validate);
|
|
111
|
+
}
|
|
112
|
+
for(const [name,cases] of Object.entries(fixtures.actions)) {
|
|
113
|
+
const validate=validators.get(name);
|
|
114
|
+
if(!validate || !Array.isArray(cases) || !cases.length || cases.length>100) throw usageError(`Declare explicit fixture cases for the known action ${name}.`);
|
|
115
|
+
const previous=[];
|
|
116
|
+
for(const entry of cases) {
|
|
117
|
+
if(!object(entry) || Object.keys(entry).some(key=>!['inputs','result'].includes(key)) || !Object.hasOwn(entry,'result')
|
|
118
|
+
|| !object(entry.inputs) || !validate(entry.inputs)) throw usageError(`Fixture inputs do not match the declared action ${name}.`);
|
|
119
|
+
if(previous.some(inputs=>isDeepStrictEqual(inputs,entry.inputs))) throw usageError(`Ambiguous fixture inputs for ${name}.`);
|
|
120
|
+
previous.push(entry.inputs);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const bodies=fixtures.documentBodies || [];
|
|
124
|
+
if(!Array.isArray(bodies) || bodies.length>100) throw usageError('Declare bounded synthetic documentBodies fixtures.');
|
|
125
|
+
function bodyRequest(request) {
|
|
126
|
+
if(!object(request) || Object.keys(request).sort().join(',')!=='binding,operation,readAction,recordKey'
|
|
127
|
+
|| request.operation!=='read' || typeof request.recordKey!=='string' || !/^[a-f0-9-]{36}$/i.test(request.recordKey)) {
|
|
128
|
+
throw usageError('Verification supports exact synthetic document body reads only.');
|
|
129
|
+
}
|
|
130
|
+
const template=bodyTemplates[request.readAction];
|
|
131
|
+
if(bodyResources[request.binding]?.kind!=='database' || template?.tool!=='LOCAL_NOTIS_DATABASE_QUERY'
|
|
132
|
+
|| !isDeepStrictEqual(template.arguments,{database_id:{$asset:request.binding},request:{$input:'request'}})) {
|
|
133
|
+
throw usageError('Choose a declared canonical query action for this document binding.');
|
|
134
|
+
}
|
|
135
|
+
const inputs={request:{filter:{field:{column:'record_key'},op:'equals',value:request.recordKey},page_size:1,include_content:true}};
|
|
136
|
+
if(!validators.get(request.readAction)?.(inputs)) throw usageError('The body read inputs do not match this action declaration.');
|
|
137
|
+
}
|
|
138
|
+
const previousBodies=[];
|
|
139
|
+
for(const entry of bodies) {
|
|
140
|
+
if(!object(entry) || Object.keys(entry).some(key=>!['request','result'].includes(key))) throw usageError('Declare a document body request and result.');
|
|
141
|
+
bodyRequest(entry.request);
|
|
142
|
+
const result=entry.result;
|
|
143
|
+
if(!object(result) || Object.keys(result).some(key=>!['record_key','title','revision','schema_revision','content_markdown'].includes(key))
|
|
144
|
+
|| result.record_key!==entry.request.recordKey || typeof result.content_markdown!=='string'
|
|
145
|
+
|| !Number.isSafeInteger(result.revision) || result.revision<1 || !Number.isSafeInteger(result.schema_revision) || result.schema_revision<0
|
|
146
|
+
|| (result.title!==null && typeof result.title!=='string')) throw usageError('Declare a complete synthetic document body result.');
|
|
147
|
+
if(previousBodies.some(request=>isDeepStrictEqual(request,entry.request))) throw usageError('Ambiguous document body fixture.');
|
|
148
|
+
previousBodies.push(entry.request);
|
|
149
|
+
}
|
|
150
|
+
// First-party record components use explicit readonly fixtures. They never
|
|
151
|
+
// need a collaboration token, upload path, property writer or database key.
|
|
152
|
+
const viewName=JSON.stringify(release.manifest?.local_key || release.manifest?.name || 'view');
|
|
153
|
+
const fixtureWhere=`View ${viewName}${release.fixturePath ? ` (${release.fixturePath})` : ''}:`;
|
|
154
|
+
const recordViews=fixtures.recordViews ?? [];
|
|
155
|
+
if(!Array.isArray(recordViews) || recordViews.length>100) throw usageError('Declare bounded synthetic recordViews fixtures.');
|
|
156
|
+
// Live, a record component opens any row of a declared native database resource (a list row's HtmlFrame needs
|
|
157
|
+
// no record param), so a fixture needs one such resource, and every record param must name one.
|
|
158
|
+
const recordDeclarations=Object.values(release.manifest.params || {}).filter(value=>value?.type==='record');
|
|
159
|
+
const nativeDatabases=Object.values(bodyResources).some(value=>value?.kind==='database');
|
|
160
|
+
const previousViews=new Set();
|
|
161
|
+
for(const entry of recordViews) {
|
|
162
|
+
if(!object(entry) || Object.keys(entry).sort().join(',')!=='operation,record_key,result'
|
|
163
|
+
|| !['record','html','report'].includes(entry.operation) || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(entry.record_key)
|
|
164
|
+
|| !object(entry.result) || !nativeDatabases
|
|
165
|
+
|| !recordDeclarations.every(value=>bodyResources[value.database]?.kind==='database')) throw usageError('Declare readonly recordViews only for a declared native database resource.');
|
|
166
|
+
const identity=entry.operation+':'+entry.record_key;
|
|
167
|
+
if(previousViews.has(identity)) throw usageError('Ambiguous record view fixture.');
|
|
168
|
+
previousViews.add(identity);
|
|
169
|
+
const value=entry.result;
|
|
170
|
+
if(entry.operation==='record' && (value.record_key!==entry.record_key || !object(value.document) || !object(value.schema)
|
|
171
|
+
|| value.writable!==false || Object.hasOwn(value,'target') || value.document.record_key!==entry.record_key)) throw usageError('Record fixtures must be readonly snapshots without authoring targets.');
|
|
172
|
+
if(entry.operation==='html' && typeof value.html!=='string') {
|
|
173
|
+
throw usageError(`${fixtureWhere} the html recordViews fixture for record ${entry.record_key} is missing result.html, the page HTML as a string. Use ${recordViewExample('html',entry.record_key)}.`);
|
|
174
|
+
}
|
|
175
|
+
if(entry.operation==='html' && value.record_key!==entry.record_key) {
|
|
176
|
+
throw usageError(`${fixtureWhere} the html recordViews fixture for record ${entry.record_key} needs result.record_key set to that same record key. Use ${recordViewExample('html',entry.record_key)}.`);
|
|
177
|
+
}
|
|
178
|
+
if(entry.operation==='html' && value.title!==undefined && value.title!==null && typeof value.title!=='string') throw usageError('HTML fixtures contain a record key, HTML and an optional title.');
|
|
179
|
+
if(entry.operation==='report' && (typeof value.source!=='string' || typeof value.export_name!=='string')) throw usageError('Report fixtures contain an explicit readonly source and export.');
|
|
180
|
+
}
|
|
181
|
+
return { context:fixtures.context || {}, digest:createHash('sha256').update(JSON.stringify(fixtures)).digest('hex'),
|
|
182
|
+
executeRecordView(operation,key) {
|
|
183
|
+
const match=recordViews.find(value=>value.operation===operation && value.record_key===key);
|
|
184
|
+
if(!match) {
|
|
185
|
+
const where=release.fixturePath ? `Add it to recordViews in ${release.fixturePath}`
|
|
186
|
+
: 'This Space declares no verificationFixtures file: add one to its Space definition with a recordViews entry';
|
|
187
|
+
throw usageError(`View ${viewName} reads the ${RECORD_VIEW_READS[operation] || operation} of record ${key}, but its fixtures have no recordViews entry with operation "${operation}" and that record_key. ${where}, for example ${recordViewExample(operation,key)}.`);
|
|
188
|
+
}
|
|
189
|
+
return structuredClone(match.result);
|
|
190
|
+
},
|
|
191
|
+
executeBody(request) {
|
|
192
|
+
bodyRequest(request);
|
|
193
|
+
const matched=bodies.find(entry=>isDeepStrictEqual(entry.request,request));
|
|
194
|
+
if(!matched) throw usageError('Add an explicit synthetic document body fixture.');
|
|
195
|
+
return structuredClone(matched.result);
|
|
196
|
+
},
|
|
197
|
+
executeShown(name,params) {
|
|
198
|
+
if(!Object.hasOwn(declaredLists,name)) throw usageError('This view does not declare that list.');
|
|
199
|
+
if(!object(params)) throw usageError('List params must be an object.');
|
|
200
|
+
const matched=shownCases[name]?.find(entry=>isDeepStrictEqual(entry.params,params));
|
|
201
|
+
if(!matched) throw usageError(`Add an explicit synthetic shown fixture for ${name} and these params.`);
|
|
202
|
+
return structuredClone(matched.result);
|
|
203
|
+
},
|
|
204
|
+
executeViewerRead(operation,input) {
|
|
205
|
+
if(!Object.hasOwn(VIEWER_READS,operation) || !declaredReads.includes(VIEWER_READS[operation])) throw usageError('This Space does not declare that viewer read.');
|
|
206
|
+
if(!object(input)) throw usageError('Viewer read inputs must be an object.');
|
|
207
|
+
const matched=readCases[operation]?.find(entry=>isDeepStrictEqual(entry.input,input));
|
|
208
|
+
if(!matched) throw usageError(`Add an explicit synthetic viewer read fixture for ${operation} and this input.`);
|
|
209
|
+
return structuredClone(matched.result);
|
|
210
|
+
},
|
|
211
|
+
execute(name,inputs) {
|
|
212
|
+
const validate=validators.get(name);
|
|
213
|
+
if(!validate || !object(inputs) || !validate(inputs)) throw usageError('The action inputs do not match this Space declaration.');
|
|
214
|
+
const matched=fixtures.actions[name]?.find(entry=>isDeepStrictEqual(entry.inputs,inputs));
|
|
215
|
+
if(!matched) throw usageError(`Add an explicit synthetic fixture for action ${name} and these inputs.`);
|
|
216
|
+
return structuredClone(matched.result);
|
|
217
|
+
} };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function spaceHarnessSnapshot(release,capabilities,engine,{authorizationMode='authorize_missing',unavailableActions=[]}={}) {
|
|
221
|
+
if(release.manifest.kind!=='presentation') throw usageError('Containers do not have an executable preview.');
|
|
222
|
+
const manifest=release.manifest;
|
|
223
|
+
const names=Object.keys(manifest.actions || {}).sort();
|
|
224
|
+
if(!object(capabilities?.actions) || !object(capabilities?.bindings)
|
|
225
|
+
|| !['authorize_missing','reuse_only'].includes(authorizationMode) || !Array.isArray(unavailableActions)
|
|
226
|
+
|| unavailableActions.some(name=>typeof name!=='string') || new Set(unavailableActions).size!==unavailableActions.length) {
|
|
227
|
+
throw usageError('Verify current action authorizations before rendering this Space.');
|
|
228
|
+
}
|
|
229
|
+
const available=Object.keys(capabilities.actions).sort();
|
|
230
|
+
const missing=names.filter(name=>!Object.hasOwn(capabilities.actions,name));
|
|
231
|
+
if(available.some(name=>!names.includes(name))
|
|
232
|
+
|| (authorizationMode==='authorize_missing' && (missing.length || unavailableActions.length))
|
|
233
|
+
|| (authorizationMode==='reuse_only' && !isDeepStrictEqual([...unavailableActions].sort(),missing))) {
|
|
234
|
+
throw usageError(authorizationMode==='authorize_missing'
|
|
235
|
+
? 'Verify current action authorizations before rendering this Space.'
|
|
236
|
+
: 'Verify the exact unavailable action declarations before rendering this Space.');
|
|
237
|
+
}
|
|
238
|
+
const actions=Object.fromEntries(available.map(name=>{
|
|
239
|
+
const descriptor=capabilities.actions[name];
|
|
240
|
+
if(typeof descriptor.readOnly!=='boolean' || !isDeepStrictEqual(descriptor.inputSchema,manifest.actions[name].inputs)) {
|
|
241
|
+
throw usageError('The verified action descriptor changed. Verify this build again.');
|
|
242
|
+
}
|
|
243
|
+
return [name,{id:name,inputSchema:descriptor.inputSchema,readOnly:descriptor.readOnly}];
|
|
244
|
+
}));
|
|
245
|
+
const bindings=Object.fromEntries(Object.entries(capabilities.bindings).map(([name,value])=>
|
|
246
|
+
[name,{kind:value.kind,...(value.operations?{operations:value.operations}:{})}]));
|
|
247
|
+
if(authorizationMode==='reuse_only' && Object.values(bindings).some(binding=>
|
|
248
|
+
Object.values(binding.operations || {}).some(name=>!Object.hasOwn(actions,name)))) {
|
|
249
|
+
throw usageError('An unavailable action cannot become a fixture binding operation.');
|
|
250
|
+
}
|
|
251
|
+
const navigation=Object.keys(manifest.navigation || {}).sort();
|
|
252
|
+
if(!isDeepStrictEqual([...(capabilities.navigation || [])].sort(),navigation)) throw usageError('The verified navigation declaration changed. Verify this build again.');
|
|
253
|
+
const viewerReads=[...(manifest.viewer_reads || [])];
|
|
254
|
+
if(!isDeepStrictEqual([...(capabilities.viewerReads || [])],viewerReads)) throw usageError('The verified viewer reads changed. Verify this build again.');
|
|
255
|
+
const identity=`verification:${manifest.local_key}`;
|
|
256
|
+
const artifact=appFilesDigest(release.files);
|
|
257
|
+
const js=manifest.bundle?.js,css=manifest.bundle?.css;
|
|
258
|
+
if(!js || !release.files[js] || (css && typeof release.files[css]!=='string')) throw usageError('The selected presentation bundle or stylesheet is missing.');
|
|
259
|
+
return { source:Buffer.from(release.files[js],'base64').toString('utf8'),
|
|
260
|
+
css:css?Buffer.from(release.files[css],'base64').toString('utf8'):'',
|
|
261
|
+
cacheKey:`${artifact}:${engine.digest}`,exportCandidates:['SpaceView'],
|
|
262
|
+
descriptor:{resource:{kind:'space',id:identity,revision:1},
|
|
263
|
+
space:{id:identity,revision:1,cacheScope:`stub:${artifact}:${engine.digest}`,actions,bindings,...(navigation.length?{navigation}:{}),
|
|
264
|
+
...(viewerReads.length?{viewerReads}:{}),...viewDeclarations(manifest)},
|
|
265
|
+
app:{id:identity,name:manifest.name},route:{slug:'view',path:'/',name:manifest.name},context:engine.context} };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Existing CLI theme only; the legacy executable/JWT HTML is never served. */
|
|
269
|
+
function themeCss() {
|
|
270
|
+
const template=readFileSync(join(ROOT,'template/.harness/index.html.tmpl'),'utf8');
|
|
271
|
+
const css=template.match(/<style>([\s\S]*?)<\/style>/)?.[1];
|
|
272
|
+
if(!css) throw usageError('The verification theme is missing. Rebuild the CLI package.');
|
|
273
|
+
return css;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function startSpaceTestServer({release,capabilities,port=0,authorizationMode='authorize_missing',unavailableActions=[]}) {
|
|
277
|
+
const engine=createSpaceFixtureEngine(release),snapshot=spaceHarnessSnapshot(release,capabilities,engine,{authorizationMode,unavailableActions});
|
|
278
|
+
const assets={host:readFileSync(join(ROOT,'dist/space-harness/host.js')),css:themeCss()};
|
|
279
|
+
const server=createServer(async(req,res)=>{
|
|
280
|
+
const response=(status,body,type='application/json')=>{
|
|
281
|
+
res.writeHead(status,{'Content-Type':type,'Cache-Control':'no-store','X-Content-Type-Options':'nosniff',
|
|
282
|
+
'Referrer-Policy':'no-referrer','Content-Security-Policy':HOST_POLICY,
|
|
283
|
+
'Cross-Origin-Resource-Policy':'same-origin'});
|
|
284
|
+
res.end(req.method==='HEAD'?undefined:body);
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
const origin=`http://127.0.0.1:${server.address().port}`;
|
|
288
|
+
const target=new URL(req.url || '/',origin);
|
|
289
|
+
// Also serve as the verification browser's non-forwarding proxy. An
|
|
290
|
+
// executable cannot escape to another localhost port by navigating.
|
|
291
|
+
if(target.origin!==origin || req.headers.host!==new URL(origin).host) return response(403,'{}');
|
|
292
|
+
const path=target.pathname;
|
|
293
|
+
if(req.headers.origin && req.headers.origin!==origin) return response(403,'{}');
|
|
294
|
+
if(req.method==='GET' || req.method==='HEAD') {
|
|
295
|
+
if(path==='/') return response(200,'<!doctype html><meta charset="utf-8"><title>Space verification</title><link rel="stylesheet" href="/theme.css"><script defer src="/host.js"></script>','text/html; charset=utf-8');
|
|
296
|
+
if(path==='/host.js') return response(200,assets.host,'text/javascript');
|
|
297
|
+
if(path==='/theme.css') return response(200,assets.css,'text/css');
|
|
298
|
+
if(path==='/snapshot') return response(200,JSON.stringify(snapshot));
|
|
299
|
+
}
|
|
300
|
+
if(req.method==='POST' && path==='/fixture' && req.headers['content-type']?.split(';')[0]==='application/json') {
|
|
301
|
+
const chunks=[];let length=0;
|
|
302
|
+
for await(const chunk of req) {
|
|
303
|
+
length+=chunk.length;
|
|
304
|
+
if(length>1_000_000) {response(413,'{}');req.destroy();return;}
|
|
305
|
+
chunks.push(chunk);
|
|
306
|
+
}
|
|
307
|
+
const input=JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
308
|
+
if(object(input) && Object.keys(input).length===1 && Object.hasOwn(input,'shown')) {
|
|
309
|
+
const list=input.shown;
|
|
310
|
+
if(!object(list) || Object.keys(list).sort().join(',')!=='name,params') return response(400,'{}');
|
|
311
|
+
try {return response(200,JSON.stringify({result:engine.executeShown(list.name,list.params)}));}
|
|
312
|
+
catch(error) {return response(400,JSON.stringify({message:error.message}));}
|
|
313
|
+
}
|
|
314
|
+
if(object(input) && Object.keys(input).length===1 && Object.hasOwn(input,'viewer_read')) {
|
|
315
|
+
const read=input.viewer_read;
|
|
316
|
+
if(!object(read) || Object.keys(read).sort().join(',')!=='input,operation') return response(400,'{}');
|
|
317
|
+
try {return response(200,JSON.stringify({result:engine.executeViewerRead(read.operation,read.input)}));}
|
|
318
|
+
catch(error) {return response(400,JSON.stringify({message:error.message}));}
|
|
319
|
+
}
|
|
320
|
+
if(object(input) && Object.keys(input).length===1 && Object.hasOwn(input,'document_body')) {
|
|
321
|
+
if(snapshot.descriptor.space.actions[input.document_body?.readAction]?.readOnly!==true) return response(403,'{}');
|
|
322
|
+
try {return response(200,JSON.stringify({result:engine.executeBody(input.document_body)}));}
|
|
323
|
+
catch(error) {return response(400,JSON.stringify({message:error.message}));}
|
|
324
|
+
}
|
|
325
|
+
if(!object(input) || Object.keys(input).some(key=>!['action_id','inputs'].includes(key))) return response(400,'{}');
|
|
326
|
+
if(snapshot.descriptor.space.actions[input.action_id]?.readOnly!==true) return response(403,'{}');
|
|
327
|
+
try { return response(200,JSON.stringify({result:engine.execute(input.action_id,input.inputs)})); }
|
|
328
|
+
catch(error) {return response(400,JSON.stringify({message:error.message}));}
|
|
329
|
+
}
|
|
330
|
+
response(404,'{}');
|
|
331
|
+
} catch {response(400,JSON.stringify({message:'Invalid verification request.'}));}
|
|
332
|
+
});
|
|
333
|
+
// This is an asset/fixture server, never a CONNECT or WebSocket proxy.
|
|
334
|
+
// These events hand the raw socket to us, outside HTTP's usual lifecycle.
|
|
335
|
+
// A browser resetting a denied tunnel must close only that connection, not
|
|
336
|
+
// become an unhandled Socket error that kills every fixture presentation.
|
|
337
|
+
const deniedSockets=new Set();
|
|
338
|
+
const denyTunnel=(_req,socket)=>{
|
|
339
|
+
deniedSockets.add(socket);
|
|
340
|
+
socket.on('error',()=>socket.destroy());
|
|
341
|
+
socket.once('close',()=>deniedSockets.delete(socket));
|
|
342
|
+
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n',()=>socket.destroy());
|
|
343
|
+
};
|
|
344
|
+
server.on('connect',denyTunnel);
|
|
345
|
+
server.on('upgrade',denyTunnel);
|
|
346
|
+
await new Promise((accept,reject)=>{server.once('error',reject);server.listen(port,'127.0.0.1',()=>{server.off('error',reject);accept();});});
|
|
347
|
+
let closing;
|
|
348
|
+
const rendererDigest=createHash('sha256').update(assets.host).update(assets.css).digest('hex');
|
|
349
|
+
return {url:`http://127.0.0.1:${server.address().port}`,fixtureDigest:engine.digest,rendererDigest,
|
|
350
|
+
close(){return closing ||= new Promise(accept=>{
|
|
351
|
+
server.close(accept);server.closeAllConnections?.();
|
|
352
|
+
for(const socket of deniedSockets) socket.destroy();
|
|
353
|
+
});}};
|
|
354
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Frozen offline fixtures rendered by the same read-only library as CLI/service. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { appFilesDigest } from './app-platform.js';
|
|
6
|
+
import { createSpaceFixtureBroker } from './space-fixture-broker.js';
|
|
7
|
+
|
|
8
|
+
// Kept for the standalone legacy worker's lifecycle tests while that diagnostic
|
|
9
|
+
// entrypoint is retired. The primary verifier below never starts that worker.
|
|
10
|
+
export function spaceVerificationEnvironment(environment, { configPath }) {
|
|
11
|
+
const names = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'SystemRoot', 'WINDIR', 'DISPLAY', 'XDG_RUNTIME_DIR'];
|
|
12
|
+
return { ...Object.fromEntries(names.filter(name => typeof environment[name] === 'string').map(name => [name, environment[name]])),
|
|
13
|
+
AGENT_BROWSER_CONFIG: configPath, AGENT_BROWSER_NO_WEBMCP: '1' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function loadSpaceVerificationRenderer() {
|
|
17
|
+
const candidates = [new URL('../../dist/view-renderer/', import.meta.url), new URL('../../../view-renderer/', import.meta.url)];
|
|
18
|
+
for (const root of candidates) {
|
|
19
|
+
const entry = new URL('src/index.js', root);
|
|
20
|
+
if (!existsSync(fileURLToPath(entry))) continue;
|
|
21
|
+
const digest = createHash('sha256');
|
|
22
|
+
for (const file of ['src/renderer.js', 'src/dom.js', 'src/pool.js', 'dist/host.js', 'dist/host.css', 'dist/theme.css']) {
|
|
23
|
+
digest.update(file); digest.update(readFileSync(new URL(file, root)));
|
|
24
|
+
}
|
|
25
|
+
return { ...(await import(entry.href)), rendererDigest: digest.digest('hex') };
|
|
26
|
+
}
|
|
27
|
+
throw new Error('The shared view renderer is missing. Rebuild or reinstall this CLI package.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// One browser per verification, two widths concurrently; no worker creates an
|
|
31
|
+
// unbounded browser fanout. Up to two invocations may run simultaneously.
|
|
32
|
+
let active = 0;
|
|
33
|
+
const waiting = [];
|
|
34
|
+
async function acquire(signal) {
|
|
35
|
+
if (signal?.aborted) throw new Error('Verification cancelled.');
|
|
36
|
+
if (active < 2) { active++; return; }
|
|
37
|
+
if (waiting.length >= 8) throw new Error('The local verification queue is full.');
|
|
38
|
+
await new Promise((accept, reject) => {
|
|
39
|
+
const entry = { accept: () => { signal?.removeEventListener('abort', abort); accept(); } };
|
|
40
|
+
const abort = () => { const index = waiting.indexOf(entry); if(index >= 0) waiting.splice(index, 1); reject(new Error('Verification cancelled.')); };
|
|
41
|
+
waiting.push(entry); signal?.addEventListener('abort', abort, { once: true });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function releaseSlot() { const next = waiting.shift(); if (next) next.accept(); else active--; }
|
|
45
|
+
|
|
46
|
+
/** Initial deployment checks the exact captured bytes at both product widths. */
|
|
47
|
+
export async function verifySpaceRelease(release, { capabilities, timeoutMs = 45_000, signal, loadRenderer = loadSpaceVerificationRenderer,
|
|
48
|
+
authorizationMode = 'authorize_missing', unavailableActions = [] } = {}) {
|
|
49
|
+
const diagnostic = { schema: 'notis-space-render/v1', local_key: release.manifest.local_key,
|
|
50
|
+
source_digest: appFilesDigest(release.sourceFiles), artifact_digest: appFilesDigest(release.files),
|
|
51
|
+
mode: 'offline_fixtures', checked_at: new Date().toISOString(),
|
|
52
|
+
...(authorizationMode === 'reuse_only' ? { authorization_mode: authorizationMode,
|
|
53
|
+
unavailable_actions: structuredClone(unavailableActions) } : {}) };
|
|
54
|
+
if (signal?.aborted) return { ...diagnostic, status: 'failed', errors: [{ phase: 'browser', message: 'Verification cancelled.' }] };
|
|
55
|
+
if (release.manifest.kind === 'container') return { ...diagnostic, status: 'not_applicable_container' };
|
|
56
|
+
let pool, held = false, rendererDigest;
|
|
57
|
+
const brokers = [390, 1440].map(width => ({ width, broker: null }));
|
|
58
|
+
try {
|
|
59
|
+
// Validate fixture authority before opening a browser or acquiring a slot.
|
|
60
|
+
for (const item of brokers) item.broker = createSpaceFixtureBroker({ release, capabilities, width: item.width,
|
|
61
|
+
authorizationMode, unavailableActions });
|
|
62
|
+
await acquire(signal); held = true;
|
|
63
|
+
const library = await loadRenderer(); rendererDigest = library.rendererDigest;
|
|
64
|
+
pool = library.createBrowserPool({ concurrency: 2, maxQueue: 2 });
|
|
65
|
+
const observations = await Promise.allSettled(brokers.map(async ({ width, broker }) => {
|
|
66
|
+
const result = await library.renderView({ broker, pool, outputs: ['markdown', 'screenshot'], width, timeoutMs, signal });
|
|
67
|
+
const blocked = result.blocked_calls || [];
|
|
68
|
+
return { width, mounted: true, status: blocked.length ? 'failed' : 'passed',
|
|
69
|
+
markdown_sha256: createHash('sha256').update(result.markdown).digest('hex'),
|
|
70
|
+
screenshot_sha256: createHash('sha256').update(Buffer.from(result.screenshot.data_base64, 'base64')).digest('hex'),
|
|
71
|
+
screenshot_width: result.screenshot.width, screenshot_height: result.screenshot.height,
|
|
72
|
+
rendered_at: result.rendered_at, blocked_calls: blocked };
|
|
73
|
+
}));
|
|
74
|
+
const renders = observations.map((value, index) => value.status === 'fulfilled' ? value.value : {
|
|
75
|
+
width: brokers[index].width, mounted: false, status: 'failed', error: value.reason?.code || 'render_failed',
|
|
76
|
+
});
|
|
77
|
+
const runtimeCalls = brokers.flatMap(({ width, broker }) => broker.calls.map(call => ({ width, ...call })));
|
|
78
|
+
const passed = renders.every(value => value.status === 'passed') && runtimeCalls.every(call => call.ok);
|
|
79
|
+
// Each distinct fixture reason once (both widths usually hit the same one), ahead of the generic line.
|
|
80
|
+
const fixtureErrors = [...new Map(runtimeCalls.filter(call => !call.ok && call.error)
|
|
81
|
+
.map(call => [call.error, { phase: 'fixture', operation: call.operation,
|
|
82
|
+
...(call.recordKey ? { record_key: call.recordKey } : {}), ...(call.actionId ? { action_id: call.actionId } : {}),
|
|
83
|
+
message: call.error }])).values()].slice(0, 10);
|
|
84
|
+
return { ...diagnostic, status: passed ? 'passed' : 'failed', mounted: renders.every(value => value.mounted),
|
|
85
|
+
fixture_digest: brokers[0].broker.fixtureDigest, renderer_digest: rendererDigest,
|
|
86
|
+
render_count: renders.length, browser_count: 1, renders, runtimeCalls,
|
|
87
|
+
errors: passed ? [] : [...fixtureErrors, { phase: 'renderer', message: 'At least one width or read-only fixture failed verification.' }] };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { ...diagnostic, status: 'failed', ...(rendererDigest ? { renderer_digest: rendererDigest } : {}),
|
|
90
|
+
errors: [{ phase: 'harness', message: error.message }] };
|
|
91
|
+
} finally {
|
|
92
|
+
try { await pool?.close(); } finally { if (held) releaseSlot(); }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Runs in a credential-free child process, never in the authenticated CLI. */
|
|
2
|
+
import { closeAgentBrowserSessionResult, isAgentBrowserAvailable, runHarnessRoute } from './agent-browser.js';
|
|
3
|
+
|
|
4
|
+
let sessionName;
|
|
5
|
+
let closing;
|
|
6
|
+
let started = false;
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
let exitCode = 0;
|
|
9
|
+
const cleanup = () => closing ||= sessionName ? closeAgentBrowserSessionResult(sessionName) : Promise.resolve({ ok: true });
|
|
10
|
+
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => {
|
|
11
|
+
exitCode = signal === 'SIGINT' ? 130 : 143;
|
|
12
|
+
controller.abort();
|
|
13
|
+
if (!started) process.exit(exitCode);
|
|
14
|
+
});
|
|
15
|
+
process.on('disconnect', () => { exitCode = 1; controller.abort(); if (!started) process.exit(1); });
|
|
16
|
+
|
|
17
|
+
process.once('message', async input => {
|
|
18
|
+
started = true;
|
|
19
|
+
let result;
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(input.url);
|
|
22
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port
|
|
23
|
+
|| !/^notis-space-[a-f0-9-]+$/.test(input.sessionName)) throw new Error('Invalid offline verification session.');
|
|
24
|
+
sessionName = input.sessionName;
|
|
25
|
+
if (!isAgentBrowserAvailable()) throw new Error('Install agent-browser and its Chromium browser to verify a presentation.');
|
|
26
|
+
const observed = await runHarnessRoute({ url: url.href, sessionName, timeoutMs: input.timeoutMs,
|
|
27
|
+
designViewports: [], waitForRuntime: true, signal: controller.signal });
|
|
28
|
+
// Never retain raw eval/stdout or action inputs/results in a diagnostic receipt.
|
|
29
|
+
result = {
|
|
30
|
+
mounted: observed.mounted,
|
|
31
|
+
renderStarted: observed.renderStarted,
|
|
32
|
+
timed_out: Boolean(observed.timed_out),
|
|
33
|
+
errors: (observed.errors || []).map(error => ({ phase: String(error.phase).slice(0, 100), message: String(error.message).slice(0, 2000) })),
|
|
34
|
+
runtimeCalls: (observed.runtimeCalls || []).map(call => ({ actionId: call.actionId, ok: call.ok,
|
|
35
|
+
...(call.error ? { error: String(call.error).slice(0, 2000) } : {}) })),
|
|
36
|
+
...(observed.tool_error ? { tool_error: { phase: observed.tool_error.phase,
|
|
37
|
+
message: String(observed.tool_error.stderr || observed.tool_error.message
|
|
38
|
+
|| 'The isolated verification browser could not complete this operation.').slice(0, 2000) } } : {}),
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
result = { mounted: false, errors: [{ phase: 'browser', message: error.message }], runtimeCalls: [] };
|
|
42
|
+
} finally {
|
|
43
|
+
if (process.connected) {
|
|
44
|
+
try { process.send({ phase: 'cleanup' }, () => {}); } catch { /* Cleanup still owns the browser after IPC loss. */ }
|
|
45
|
+
}
|
|
46
|
+
const closed = await cleanup();
|
|
47
|
+
result = { ...result, cleanup: closed, ...(!closed.ok ? { cleanup_failed: true } : {}) };
|
|
48
|
+
}
|
|
49
|
+
if (process.connected) process.send(result, () => process.exit(exitCode));
|
|
50
|
+
else process.exit(exitCode);
|
|
51
|
+
});
|