@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,213 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { createDeflate, createInflate, crc32, inflateSync } from 'node:zlib';
|
|
3
|
+
import { ViewRenderError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stitch full-page screenshot slices into one PNG without ever holding the
|
|
7
|
+
* whole page as raw pixels. Chromium rasterizes a full-page capture in one go,
|
|
8
|
+
* so its memory grows with the page area; slices keep that peak flat.
|
|
9
|
+
*/
|
|
10
|
+
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
11
|
+
const CHANNELS = { 0: 1, 2: 3, 4: 2, 6: 4 };
|
|
12
|
+
// Chunks that describe the image as a whole, never its pixels or its size.
|
|
13
|
+
const KEEP = new Set(['sRGB', 'gAMA', 'cHRM', 'iCCP', 'pHYs']);
|
|
14
|
+
|
|
15
|
+
function invalid() { return new ViewRenderError('render_failed', 'The view could not be rendered under its current access.', 500); }
|
|
16
|
+
|
|
17
|
+
export function parsePng(buffer) {
|
|
18
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 33 || !buffer.subarray(0, 8).equals(SIGNATURE)) throw invalid();
|
|
19
|
+
let offset = 8, header, ended = false;
|
|
20
|
+
const idat = [], ancillary = [];
|
|
21
|
+
while (offset + 12 <= buffer.length) {
|
|
22
|
+
const length = buffer.readUInt32BE(offset);
|
|
23
|
+
const type = buffer.toString('latin1', offset + 4, offset + 8);
|
|
24
|
+
const end = offset + 12 + length;
|
|
25
|
+
if (end > buffer.length) throw invalid();
|
|
26
|
+
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
|
27
|
+
if (type === 'IHDR') {
|
|
28
|
+
header = { width: data.readUInt32BE(0), height: data.readUInt32BE(4), bitDepth: data[8], colorType: data[9], interlace: data[12] };
|
|
29
|
+
} else if (type === 'IDAT') idat.push(data);
|
|
30
|
+
else if (type === 'IEND') { ended = true; break; }
|
|
31
|
+
else if (!idat.length && KEEP.has(type)) ancillary.push(buffer.subarray(offset, end));
|
|
32
|
+
offset = end;
|
|
33
|
+
}
|
|
34
|
+
if (!header || !ended || !idat.length || header.bitDepth !== 8 || !(header.colorType in CHANNELS) || header.interlace !== 0) throw invalid();
|
|
35
|
+
return { ...header, channels: CHANNELS[header.colorType], idat: idat.length === 1 ? idat[0] : Buffer.concat(idat), ancillary };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function paeth(a, b, c) {
|
|
39
|
+
const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
|
40
|
+
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Undo one scanline's filter in place, given the previous unfiltered row (or zeros). */
|
|
44
|
+
function unfilterRow(filter, row, prior, bpp) {
|
|
45
|
+
for (let i = 0; i < row.length; i++) {
|
|
46
|
+
const a = i >= bpp ? row[i - bpp] : 0, b = prior ? prior[i] : 0, c = prior && i >= bpp ? prior[i - bpp] : 0;
|
|
47
|
+
switch (filter) {
|
|
48
|
+
case 0: break;
|
|
49
|
+
case 1: row[i] = (row[i] + a) & 0xff; break;
|
|
50
|
+
case 2: row[i] = (row[i] + b) & 0xff; break;
|
|
51
|
+
case 3: row[i] = (row[i] + ((a + b) >> 1)) & 0xff; break;
|
|
52
|
+
case 4: row[i] = (row[i] + paeth(a, b, c)) & 0xff; break;
|
|
53
|
+
default: throw invalid();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rawRows(png) {
|
|
59
|
+
const stride = png.width * png.channels;
|
|
60
|
+
const raw = inflateSync(png.idat);
|
|
61
|
+
if (raw.length !== png.height * (stride + 1)) throw invalid();
|
|
62
|
+
return { raw, stride };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Decode to 8-bit RGBA; used for slices whose format differs and in tests. */
|
|
66
|
+
export function decodePng(buffer) {
|
|
67
|
+
const png = parsePng(buffer);
|
|
68
|
+
const { raw, stride } = rawRows(png);
|
|
69
|
+
const pixels = Buffer.alloc(png.width * png.height * 4);
|
|
70
|
+
let prior;
|
|
71
|
+
for (let y = 0; y < png.height; y++) {
|
|
72
|
+
const start = y * (stride + 1);
|
|
73
|
+
const row = raw.subarray(start + 1, start + 1 + stride);
|
|
74
|
+
unfilterRow(raw[start], row, prior, png.channels);
|
|
75
|
+
prior = row;
|
|
76
|
+
toRgba(row, png.colorType, png.width, pixels, y * png.width * 4);
|
|
77
|
+
}
|
|
78
|
+
return { width: png.width, height: png.height, pixels };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One unfiltered row of the given colour type, written as RGBA at offset. */
|
|
82
|
+
function toRgba(row, colorType, width, out, offset) {
|
|
83
|
+
const channels = CHANNELS[colorType];
|
|
84
|
+
for (let x = 0; x < width; x++) {
|
|
85
|
+
const source = x * channels, target = offset + x * 4;
|
|
86
|
+
if (colorType === 6) row.copy(out, target, source, source + 4);
|
|
87
|
+
else if (colorType === 2) { row.copy(out, target, source, source + 3); out[target + 3] = 255; }
|
|
88
|
+
else if (colorType === 4) { out.fill(row[source], target, target + 3); out[target + 3] = row[source + 1]; }
|
|
89
|
+
else { out.fill(row[source], target, target + 3); out[target + 3] = 255; }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function chunk(type, data) {
|
|
94
|
+
const head = Buffer.alloc(8);
|
|
95
|
+
head.writeUInt32BE(data.length, 0); head.write(type, 4, 'latin1');
|
|
96
|
+
const tail = Buffer.alloc(4);
|
|
97
|
+
tail.writeUInt32BE(crc32(data, crc32(head.subarray(4))) >>> 0, 0);
|
|
98
|
+
return Buffer.concat([head, data, tail]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Rows of one slice in the output's format, each with its filter byte. Only the
|
|
103
|
+
* first row changes: its filter read a zero row above it, so it is unfiltered.
|
|
104
|
+
*/
|
|
105
|
+
function sliceRows(png, target) {
|
|
106
|
+
if (png.colorType === target.colorType) {
|
|
107
|
+
const { raw, stride } = rawRows(png);
|
|
108
|
+
const first = raw.subarray(1, 1 + stride);
|
|
109
|
+
unfilterRow(raw[0], first, undefined, png.channels);
|
|
110
|
+
raw[0] = 0;
|
|
111
|
+
return raw;
|
|
112
|
+
}
|
|
113
|
+
const decoded = decodePng(Buffer.concat([SIGNATURE, chunk('IHDR', ihdr(png)), chunk('IDAT', png.idat), chunk('IEND', Buffer.alloc(0))]));
|
|
114
|
+
const out = Buffer.alloc(png.height * (png.width * 4 + 1));
|
|
115
|
+
for (let y = 0; y < png.height; y++) decoded.pixels.copy(out, y * (png.width * 4 + 1) + 1, y * png.width * 4, (y + 1) * png.width * 4);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ihdr({ width, height, colorType }) {
|
|
120
|
+
const data = Buffer.alloc(13);
|
|
121
|
+
data.writeUInt32BE(width, 0); data.writeUInt32BE(height, 4);
|
|
122
|
+
data[8] = 8; data[9] = colorType; // compression, filter and interlace stay 0
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Stitch PNG slices, top to bottom, of one equal width, as they arrive: each
|
|
128
|
+
* slice is compressed into the output when it is added and is not kept, so the
|
|
129
|
+
* whole page is never held as pixels. Slices that share the first one's colour
|
|
130
|
+
* type keep it; any mix is written as RGBA. Stitching adds no size limit of its
|
|
131
|
+
* own: the output is refused once its compressed bytes pass maxBytes, by the add
|
|
132
|
+
* that produces them or, for bytes the compressor still holds, by finish.
|
|
133
|
+
*/
|
|
134
|
+
export function createPngStitcher({ maxBytes = Infinity, level = 6 } = {}) {
|
|
135
|
+
const tooLarge = () => new ViewRenderError('render_too_large', 'The PNG exceeds the render transport limit; nothing was cropped.', 413);
|
|
136
|
+
// overhead: every byte of the PNG besides the compressed pixels, so the limit is on the whole file.
|
|
137
|
+
let first, colorType, height = 0, out, overhead = 0;
|
|
138
|
+
const over = () => out.size + overhead > maxBytes;
|
|
139
|
+
function encoder() {
|
|
140
|
+
const state = { compressed: [], size: 0, crc: crc32('IDAT'), deflate: createDeflate({ level }) };
|
|
141
|
+
state.deflate.on('data', data => { state.size += data.length; state.crc = crc32(data, state.crc); state.compressed.push(data); });
|
|
142
|
+
state.finished = new Promise((resolve, reject) => { state.deflate.once('end', resolve); state.deflate.once('error', reject); });
|
|
143
|
+
state.finished.catch(() => {});
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
async function write(rows) {
|
|
147
|
+
await new Promise((resolve, reject) => out.deflate.write(rows, error => (error ? reject(error) : resolve())));
|
|
148
|
+
if (over()) { out.deflate.destroy(); throw tooLarge(); }
|
|
149
|
+
}
|
|
150
|
+
/** Re-encode what is written so far as RGBA, one row at a time, never the whole page. */
|
|
151
|
+
async function upgradeToRgba() {
|
|
152
|
+
const old = out;
|
|
153
|
+
old.deflate.end();
|
|
154
|
+
await old.finished;
|
|
155
|
+
out = encoder();
|
|
156
|
+
const stride = first.width * CHANNELS[colorType];
|
|
157
|
+
let carry = Buffer.alloc(0), prior;
|
|
158
|
+
for await (const data of Readable.from(old.compressed).pipe(createInflate())) {
|
|
159
|
+
carry = carry.length ? Buffer.concat([carry, data]) : data;
|
|
160
|
+
const rows = Math.floor(carry.length / (stride + 1));
|
|
161
|
+
if (!rows) continue;
|
|
162
|
+
const batch = Buffer.alloc(rows * (first.width * 4 + 1));
|
|
163
|
+
for (let y = 0; y < rows; y++) {
|
|
164
|
+
const row = Buffer.from(carry.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1)));
|
|
165
|
+
unfilterRow(carry[y * (stride + 1)], row, prior, CHANNELS[colorType]);
|
|
166
|
+
toRgba(row, colorType, first.width, batch, y * (first.width * 4 + 1) + 1);
|
|
167
|
+
prior = row;
|
|
168
|
+
}
|
|
169
|
+
carry = carry.subarray(rows * (stride + 1));
|
|
170
|
+
await write(batch);
|
|
171
|
+
}
|
|
172
|
+
if (carry.length) throw invalid();
|
|
173
|
+
old.compressed.length = 0;
|
|
174
|
+
colorType = 6;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
async add(slice) {
|
|
178
|
+
const png = parsePng(slice);
|
|
179
|
+
if (png.height < 1 || (first && png.width !== first.width)) throw invalid();
|
|
180
|
+
if (!first) {
|
|
181
|
+
// Keep only the width and copies of the kept chunks, never the first slice itself.
|
|
182
|
+
first = { width: png.width, ancillary: png.ancillary.map(part => Buffer.from(part)) };
|
|
183
|
+
colorType = png.colorType; out = encoder();
|
|
184
|
+
// Signature, IHDR, the kept chunks, the IDAT head and CRC, and IEND.
|
|
185
|
+
overhead = 8 + 25 + first.ancillary.reduce((total, part) => total + part.length, 0) + 12 + 12;
|
|
186
|
+
}
|
|
187
|
+
else if (png.colorType !== colorType && colorType !== 6) await upgradeToRgba();
|
|
188
|
+
// Raw rows of one slice at a time: never the whole page.
|
|
189
|
+
await write(sliceRows(png, { colorType }));
|
|
190
|
+
height += png.height;
|
|
191
|
+
},
|
|
192
|
+
async finish() {
|
|
193
|
+
if (!first) throw invalid();
|
|
194
|
+
out.deflate.end();
|
|
195
|
+
await out.finished;
|
|
196
|
+
if (over()) throw tooLarge();
|
|
197
|
+
const head = Buffer.alloc(8), tail = Buffer.alloc(4);
|
|
198
|
+
head.writeUInt32BE(out.size, 0); head.write('IDAT', 4, 'latin1'); tail.writeUInt32BE(out.crc >>> 0, 0);
|
|
199
|
+
const png = Buffer.concat([SIGNATURE, chunk('IHDR', ihdr({ width: first.width, height, colorType })), ...first.ancillary,
|
|
200
|
+
head, ...out.compressed, tail, chunk('IEND', Buffer.alloc(0))]);
|
|
201
|
+
if (png.length !== out.size + overhead) throw invalid();
|
|
202
|
+
return png;
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Stitch every slice at once; the same output as adding them one by one. */
|
|
208
|
+
export async function stitchPng(slices, options) {
|
|
209
|
+
if (!Array.isArray(slices) || !slices.length) throw invalid();
|
|
210
|
+
const stitcher = createPngStitcher(options);
|
|
211
|
+
for (const slice of slices) await stitcher.add(slice);
|
|
212
|
+
return stitcher.finish();
|
|
213
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ViewRenderError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export function browserEnvironment(environment = process.env) {
|
|
4
|
+
const names = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'SystemRoot', 'WINDIR', 'DISPLAY', 'XDG_RUNTIME_DIR'];
|
|
5
|
+
return Object.fromEntries(names.filter(name => typeof environment[name] === 'string').map(name => [name, environment[name]]));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A warm process, never a shared profile, cookie jar, localStorage or context.
|
|
10
|
+
* recycleAfter closes the browser once it has served that many renders and
|
|
11
|
+
* nothing is running or waiting, so a small host returns to its idle memory;
|
|
12
|
+
* the next render starts a fresh browser.
|
|
13
|
+
*/
|
|
14
|
+
export function createBrowserPool({ concurrency = 2, maxQueue = 8, recycleAfter = 0, launch = {}, chromium } = {}) {
|
|
15
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4 || !Number.isInteger(maxQueue) || maxQueue < 0 || maxQueue > 32
|
|
16
|
+
|| !Number.isInteger(recycleAfter) || recycleAfter < 0 || recycleAfter > 10000) throw new TypeError('Choose a bounded render pool.');
|
|
17
|
+
let browser, starting, retiring, closing = false, inUse = 0, served = 0, renders = 0;
|
|
18
|
+
const queue = new Set();
|
|
19
|
+
const contexts = new Set();
|
|
20
|
+
async function acquire(signal) {
|
|
21
|
+
if (closing || signal?.aborted) throw new ViewRenderError('render_cancelled', 'The render was cancelled.', 499);
|
|
22
|
+
if (inUse < concurrency) { inUse++; return; }
|
|
23
|
+
if (queue.size >= maxQueue) throw new ViewRenderError('render_busy', 'The renderer is busy. Try again shortly.', 503);
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
const entry = { resolve: () => { signal?.removeEventListener('abort', abort); resolve(); }, reject };
|
|
26
|
+
const abort = () => { queue.delete(entry); reject(new ViewRenderError('render_cancelled', 'The render was cancelled.', 499)); };
|
|
27
|
+
queue.add(entry); signal?.addEventListener('abort', abort, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function release() {
|
|
31
|
+
const next = queue.values().next().value;
|
|
32
|
+
if (next) { queue.delete(next); next.resolve(); return; }
|
|
33
|
+
inUse--;
|
|
34
|
+
if (recycleAfter && served >= recycleAfter) retire();
|
|
35
|
+
}
|
|
36
|
+
function retire() {
|
|
37
|
+
if (inUse !== 0 || !browser || closing) return;
|
|
38
|
+
const old = browser;
|
|
39
|
+
browser = undefined; served = 0;
|
|
40
|
+
const done = old.close().catch(() => {}).then(() => { if (retiring === done) retiring = undefined; });
|
|
41
|
+
retiring = done;
|
|
42
|
+
}
|
|
43
|
+
async function getBrowser() {
|
|
44
|
+
if (browser?.isConnected()) return browser;
|
|
45
|
+
if (!starting) starting = (async () => {
|
|
46
|
+
// A retired browser has released its memory before the next one starts.
|
|
47
|
+
if (retiring) await retiring;
|
|
48
|
+
// Importing CLI command metadata never loads Playwright or starts Chrome.
|
|
49
|
+
const engine = chromium || (await import('playwright-core')).chromium;
|
|
50
|
+
const allowed = Object.fromEntries(['executablePath', 'channel', 'timeout'].filter(key => key in launch).map(key => [key, launch[key]]));
|
|
51
|
+
try {
|
|
52
|
+
browser = await engine.launch({ ...allowed, headless: true, chromiumSandbox: true,
|
|
53
|
+
env: browserEnvironment(), args: ['--force-webrtc-ip-handling-policy=disable_non_proxied_udp', '--disable-background-networking',
|
|
54
|
+
// A container's small /dev/shm cannot hold a tall page's shared memory.
|
|
55
|
+
'--disable-dev-shm-usage'] });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (/Executable doesn't exist/.test(String(error?.message))) throw new ViewRenderError('render_browser_missing', 'Install the pinned Chromium browser with npx playwright-core@1.63.0 install chromium.', 503);
|
|
58
|
+
throw new ViewRenderError('render_browser_unavailable', 'Sandboxed Chromium could not start.', 503);
|
|
59
|
+
}
|
|
60
|
+
return browser;
|
|
61
|
+
})().finally(() => { starting = undefined; });
|
|
62
|
+
return starting;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
warm: () => getBrowser(),
|
|
66
|
+
/**
|
|
67
|
+
* Prove Chromium can start, failing like a render would when it cannot. A
|
|
68
|
+
* recycling pool closes the browser again at once, so an idle host keeps
|
|
69
|
+
* no browser; otherwise it stays warm for the first render.
|
|
70
|
+
*/
|
|
71
|
+
async check() {
|
|
72
|
+
await getBrowser();
|
|
73
|
+
if (recycleAfter) retire();
|
|
74
|
+
if (retiring) await retiring;
|
|
75
|
+
},
|
|
76
|
+
async use(operation, { signal, width = 1440, locale = 'en', timezone = 'UTC' } = {}) {
|
|
77
|
+
await acquire(signal);
|
|
78
|
+
let context, launched = false;
|
|
79
|
+
const abort = () => { void context?.close(); };
|
|
80
|
+
try {
|
|
81
|
+
const browser = await getBrowser();
|
|
82
|
+
launched = true;
|
|
83
|
+
if (closing || signal?.aborted) throw new ViewRenderError('render_cancelled', 'The render was cancelled.', 499);
|
|
84
|
+
context = await browser.newContext({ viewport: { width, height: 900 }, deviceScaleFactor: 1,
|
|
85
|
+
locale, timezoneId: timezone, colorScheme: 'light', serviceWorkers: 'block', acceptDownloads: false, permissions: [] });
|
|
86
|
+
contexts.add(context); signal?.addEventListener('abort', abort, { once: true });
|
|
87
|
+
if (signal?.aborted) throw new ViewRenderError('render_cancelled', 'The render was cancelled.', 499);
|
|
88
|
+
return await operation(context);
|
|
89
|
+
} finally {
|
|
90
|
+
signal?.removeEventListener('abort', abort);
|
|
91
|
+
if (context) { contexts.delete(context); served++; renders++; await context.close().catch(() => {}); }
|
|
92
|
+
// A browser that failed to open a context still counts, so recycling closes it too.
|
|
93
|
+
else if (launched) served++;
|
|
94
|
+
release();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async close() {
|
|
98
|
+
closing = true;
|
|
99
|
+
for (const entry of queue) entry.reject(new ViewRenderError('render_cancelled', 'The renderer is stopping.', 503));
|
|
100
|
+
queue.clear();
|
|
101
|
+
await Promise.allSettled([...contexts].map(context => context.close()));
|
|
102
|
+
if (starting) await starting.catch(() => {});
|
|
103
|
+
await browser?.close();
|
|
104
|
+
if (retiring) await retiring;
|
|
105
|
+
},
|
|
106
|
+
// chromium: a browser may hold memory, from its launch until its close has finished.
|
|
107
|
+
stats: () => ({ active: inUse, queued: queue.size, contexts: contexts.size, warm: Boolean(browser?.isConnected()),
|
|
108
|
+
chromium: Boolean(starting || browser || retiring), renders }),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ViewRenderError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
/** The purpose credential never enters request URLs, browser headers or source. */
|
|
4
|
+
export function createRemoteRenderBroker({ apiBase, credential, fetcher = fetch }) {
|
|
5
|
+
const base = new URL(apiBase);
|
|
6
|
+
if (!['https:', 'http:'].includes(base.protocol) || (base.protocol === 'http:' && !['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname))
|
|
7
|
+
|| base.username || base.password || base.search || base.hash || typeof credential !== 'string' || !credential) throw new TypeError('Use a configured render API and its purpose credential.');
|
|
8
|
+
let claims;
|
|
9
|
+
try { claims = JSON.parse(Buffer.from(credential.split('.')[1], 'base64url').toString('utf8')); } catch {}
|
|
10
|
+
// This is rejection/identity checking only. Python performs cryptographic
|
|
11
|
+
// authentication and live authorization before returning any data.
|
|
12
|
+
if (claims?.type !== 'notis_view_render' || claims?.aud !== 'notis-view-render' || claims?.read_only !== true
|
|
13
|
+
|| !claims?.jti || !claims?.target || typeof claims.url !== 'string') throw new ViewRenderError('render_credential_invalid', 'Use a dedicated read-only render credential, never a user JWT.', 401);
|
|
14
|
+
if (!Array.isArray(claims.outputs) || !claims.outputs.length || claims.outputs.some(value => !['markdown', 'screenshot'].includes(value))
|
|
15
|
+
|| new Set(claims.outputs).size !== claims.outputs.length || ![390, 1440].includes(claims.width)) {
|
|
16
|
+
throw new ViewRenderError('render_credential_invalid', 'This credential does not pin render outputs.', 401);
|
|
17
|
+
}
|
|
18
|
+
const stable = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
19
|
+
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
|
|
20
|
+
const endpoint = path => new URL(`portal_view_render/${path}`, base.href.replace(/\/?$/, '/')).href;
|
|
21
|
+
async function request(operation, body, { signal } = {}) {
|
|
22
|
+
const response = await fetcher(endpoint(operation), { method: 'POST', redirect: 'error', credentials: 'omit', signal,
|
|
23
|
+
headers: { Authorization: `Bearer ${credential}`, 'Content-Type': 'application/json', 'X-Notis-Spaces-Protocol': '2' },
|
|
24
|
+
body: JSON.stringify(body) });
|
|
25
|
+
let result;
|
|
26
|
+
try { result = await response.json(); } catch { throw new ViewRenderError('render_broker_unavailable', 'The render read could not be confirmed.', 503); }
|
|
27
|
+
if (!response.ok) throw new ViewRenderError(typeof result?.error === 'string' ? result.error : 'render_unavailable', 'This view is unavailable for rendering.', response.status);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
snapshot: async options => {
|
|
32
|
+
const value = await request('read', { operation: 'manifest' }, options);
|
|
33
|
+
if (value?.url !== claims.url || value?.view?.space_id !== claims.target.space_id || value.view.revision !== claims.target.revision
|
|
34
|
+
|| (value.record_key || null) !== (claims.target.record_key || null) || stable(value.params) !== stable(claims.target.params)
|
|
35
|
+
|| value.descriptor?.space?.id !== claims.target.space_id || value.descriptor.space.revision !== claims.target.revision
|
|
36
|
+
|| value.descriptor?.resource?.id !== claims.target.space_id || value.descriptor.resource.revision !== claims.target.revision
|
|
37
|
+
|| stable(value.render_options) !== stable({ outputs: claims.outputs, width: claims.width })) {
|
|
38
|
+
throw new ViewRenderError('render_manifest_invalid', 'The render manifest belongs to another view.', 502);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
},
|
|
42
|
+
read: (payload, options) => request('read', payload, options),
|
|
43
|
+
finish: async options => {
|
|
44
|
+
const value = await request('finish', {}, options);
|
|
45
|
+
if (value?.valid !== true || value.render_id !== claims.jti || value.url !== claims.url) {
|
|
46
|
+
throw new ViewRenderError('render_access_changed', 'The final render authority is not confirmed.', 403);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
},
|
|
50
|
+
complete: async (rendered, options) => {
|
|
51
|
+
const value = await request('complete', { rendered }, options);
|
|
52
|
+
if (value?.valid !== true || value.render_id !== claims.jti || value.url !== claims.url) {
|
|
53
|
+
throw new ViewRenderError('render_capture_unconfirmed', 'The completed capture could not be recorded.', 409);
|
|
54
|
+
}
|
|
55
|
+
// Attestation/receipts remain backend-only even if an endpoint regresses.
|
|
56
|
+
return { valid: true, render_id: value.render_id, url: value.url };
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|