@anchrd/intel 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +496 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.js +416 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +128 -0
- package/dist/adapters/content/content.types.d.ts +38 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-audit.d.ts +62 -0
- package/dist/adapters/db/db-audit.js +150 -0
- package/dist/adapters/db/db-boards.d.ts +6 -0
- package/dist/adapters/db/db-boards.js +324 -0
- package/dist/adapters/db/db-feed.d.ts +30 -0
- package/dist/adapters/db/db-feed.js +276 -0
- package/dist/adapters/db/db-flows.d.ts +42 -0
- package/dist/adapters/db/db-flows.js +1311 -0
- package/dist/adapters/db/db-grants.d.ts +133 -0
- package/dist/adapters/db/db-grants.js +269 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +200 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-prompts.d.ts +46 -0
- package/dist/adapters/db/db-prompts.js +144 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +1687 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +26 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +77 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +301 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +129 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +139 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +87 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/audit/audit.d.ts +2 -0
- package/dist/audit/audit.js +87 -0
- package/dist/audit/audit.types.d.ts +23 -0
- package/dist/audit/audit.types.js +1 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +284 -0
- package/dist/auth/auth.types.d.ts +108 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/boards/boards.d.ts +44 -0
- package/dist/boards/boards.js +447 -0
- package/dist/boards/boards.types.d.ts +117 -0
- package/dist/boards/boards.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +192 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1194 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +262 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/contract/audit.d.ts +50 -0
- package/dist/contract/audit.js +72 -0
- package/dist/contract/board.d.ts +150 -0
- package/dist/contract/board.js +262 -0
- package/dist/contract/bundle.d.ts +147 -0
- package/dist/contract/bundle.js +139 -0
- package/dist/contract/contract.d.ts +112 -0
- package/dist/contract/contract.js +143 -0
- package/dist/contract/feed.d.ts +126 -0
- package/dist/contract/feed.js +149 -0
- package/dist/contract/flow-run.d.ts +346 -0
- package/dist/contract/flow-run.js +194 -0
- package/dist/contract/flow.d.ts +1057 -0
- package/dist/contract/flow.js +552 -0
- package/dist/contract/node.d.ts +551 -0
- package/dist/contract/node.js +456 -0
- package/dist/contract/share.d.ts +216 -0
- package/dist/contract/share.js +134 -0
- package/dist/contract/table.d.ts +171 -0
- package/dist/contract/table.js +117 -0
- package/dist/contract/tool.d.ts +122 -0
- package/dist/contract/tool.js +172 -0
- package/dist/feed/feed.d.ts +2 -0
- package/dist/feed/feed.js +77 -0
- package/dist/feed/feed.types.d.ts +23 -0
- package/dist/feed/feed.types.js +1 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +2216 -0
- package/dist/flows/flows.types.d.ts +339 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +9 -0
- package/dist/http/http.js +874 -0
- package/dist/http/http.types.d.ts +22 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +239 -0
- package/dist/indexing/indexing.types.d.ts +11 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +194 -0
- package/dist/intel/intel.types.d.ts +24 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +1292 -0
- package/dist/mcp/mcp.types.d.ts +25 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/nodes/document-links/document-links.d.ts +20 -0
- package/dist/nodes/document-links/document-links.js +57 -0
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1667 -0
- package/dist/nodes/nodes.types.d.ts +511 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/prompts/prompts.d.ts +2 -0
- package/dist/prompts/prompts.js +65 -0
- package/dist/prompts/prompts.types.d.ts +71 -0
- package/dist/prompts/prompts.types.js +1 -0
- package/dist/shared/callers/callers.d.ts +28 -0
- package/dist/shared/callers/callers.js +30 -0
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/document-text/document-text.d.ts +21 -0
- package/dist/shared/document-text/document-text.js +31 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
- package/dist/shared/gate-authorization/gate-authorization.js +26 -0
- package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
- package/dist/shared/grant-expiry/grant-expiry.js +26 -0
- package/dist/shared/intel-error/intel-error.d.ts +22 -0
- package/dist/shared/intel-error/intel-error.js +27 -0
- package/dist/shared/plain-title/plain-title.d.ts +16 -0
- package/dist/shared/plain-title/plain-title.js +45 -0
- package/dist/shared/problem-details/problem-details.d.ts +30 -0
- package/dist/shared/problem-details/problem-details.js +18 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +2 -0
- package/dist/shared/sha256/sha256.js +14 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +177 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +207 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/migrations/0007_no_node_waits.sql +69 -0
- package/migrations/0008_three_layers.sql +130 -0
- package/migrations/0009_no_context_policy.sql +48 -0
- package/migrations/0010_no_run_waits_a_year.sql +27 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +97 -0
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/migrations/0020_cascade_purge_replay.sql +11 -0
- package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
- package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
- package/migrations/0023_a_board_and_its_tasks.sql +116 -0
- package/migrations/0024_the_archive_is_a_column.sql +33 -0
- package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
- package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
- package/migrations/0027_the_runs_of_every_flow.sql +27 -0
- package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
- package/package.json +118 -0
- package/src/contract/CLAUDE.md +152 -0
- package/src/contract/README.md +102 -0
- package/src/contract/audit.ts +85 -0
- package/src/contract/board.ts +306 -0
- package/src/contract/bundle.ts +157 -0
- package/src/contract/contract.ts +161 -0
- package/src/contract/feed.ts +165 -0
- package/src/contract/flow-run.ts +241 -0
- package/src/contract/flow.ts +675 -0
- package/src/contract/node.ts +583 -0
- package/src/contract/share.ts +196 -0
- package/src/contract/table.ts +178 -0
- package/src/contract/tool.ts +202 -0
- package/ui/README.md +110 -0
- package/ui/components.json +21 -0
- package/ui/index.html +13 -0
- package/ui/package.json +11 -0
- package/ui/scripts/lint-tokens.mjs +187 -0
- package/ui/src/access-summary/access-summary.tsx +385 -0
- package/ui/src/app/action-slot/action-slot.tsx +27 -0
- package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
- package/ui/src/app/app-tree/app-tree.tsx +1175 -0
- package/ui/src/app/app.tsx +87 -0
- package/ui/src/app/header-search/header-search.tsx +294 -0
- package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
- package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
- package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
- package/ui/src/app/tree-move/tree-move.tsx +427 -0
- package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
- package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
- package/ui/src/app/user-footer/user-footer.tsx +188 -0
- package/ui/src/app/view-menu/view-menu.tsx +115 -0
- package/ui/src/app-root/app-root.tsx +109 -0
- package/ui/src/app-root/app-root.types.ts +27 -0
- package/ui/src/archive/archive.tsx +417 -0
- package/ui/src/attachment-detail/attachment-detail.ts +52 -0
- package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
- package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
- package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
- package/ui/src/board/board-assignee/board-assignee.ts +48 -0
- package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
- package/ui/src/board/board-chip/board-chip.tsx +46 -0
- package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
- package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
- package/ui/src/board/board-data/board-data.ts +195 -0
- package/ui/src/board/board-data/board-data.types.ts +38 -0
- package/ui/src/board/board-dates/board-dates.ts +40 -0
- package/ui/src/board/board-dates/board-dates.tsx +105 -0
- package/ui/src/board/board-kanban/board-kanban.ts +162 -0
- package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
- package/ui/src/board/board-open-task/board-open-task.ts +32 -0
- package/ui/src/board/board-panel/board-panel.tsx +259 -0
- package/ui/src/board/board-settings/board-settings.tsx +226 -0
- package/ui/src/board/board-status/board-status.tsx +60 -0
- package/ui/src/board/board-stripes/board-stripes.ts +128 -0
- package/ui/src/board/board-table/board-table.ts +53 -0
- package/ui/src/board/board-table/board-table.tsx +610 -0
- package/ui/src/board/board-task/board-task.ts +108 -0
- package/ui/src/board/board-task/board-task.tsx +335 -0
- package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
- package/ui/src/branding/branding.tsx +17 -0
- package/ui/src/branding/custom-logo.ts +1 -0
- package/ui/src/branding/favicon.default.svg +5 -0
- package/ui/src/branding/favicon.svg +5 -0
- package/ui/src/components/ui/avatar.tsx +39 -0
- package/ui/src/components/ui/button.tsx +64 -0
- package/ui/src/components/ui/collapsible.tsx +20 -0
- package/ui/src/components/ui/command.tsx +160 -0
- package/ui/src/components/ui/dialog.tsx +143 -0
- package/ui/src/components/ui/dropdown-menu.tsx +187 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/popover.tsx +41 -0
- package/ui/src/components/ui/select.tsx +163 -0
- package/ui/src/components/ui/separator.tsx +26 -0
- package/ui/src/components/ui/sheet.tsx +136 -0
- package/ui/src/components/ui/sidebar.tsx +693 -0
- package/ui/src/components/ui/skeleton.tsx +13 -0
- package/ui/src/components/ui/switch.tsx +25 -0
- package/ui/src/components/ui/table.tsx +82 -0
- package/ui/src/components/ui/tabs.tsx +52 -0
- package/ui/src/components/ui/tooltip.tsx +51 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
- package/ui/src/data/query-client.ts +18 -0
- package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
- package/ui/src/data/request-refusal/request-refusal.ts +51 -0
- package/ui/src/data/sign-in/sign-in.ts +97 -0
- package/ui/src/data/sign-in/sign-in.types.ts +15 -0
- package/ui/src/document-link/document-link.tsx +114 -0
- package/ui/src/document-markdown/document-markdown.ts +195 -0
- package/ui/src/editor-schema/editor-schema.ts +23 -0
- package/ui/src/entry-picker/entry-picker.tsx +238 -0
- package/ui/src/feed/feed.tsx +344 -0
- package/ui/src/file-preview/file-preview-view.tsx +496 -0
- package/ui/src/file-preview/file-preview.tsx +28 -0
- package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
- package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
- package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
- package/ui/src/file-preview/word-file-preview.tsx +5 -0
- package/ui/src/flow-runs/flow-runs.tsx +238 -0
- package/ui/src/flows/flows.tsx +1646 -0
- package/ui/src/flows/node-icon/node-icon.ts +34 -0
- package/ui/src/flows/node-palette/node-palette.tsx +214 -0
- package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
- package/ui/src/folder-contents/folder-contents.tsx +321 -0
- package/ui/src/folder-index/folder-index.ts +55 -0
- package/ui/src/frontmatter/frontmatter.tsx +193 -0
- package/ui/src/graph-pane/graph-pane.tsx +53 -0
- package/ui/src/hooks/use-mobile.ts +19 -0
- package/ui/src/i18n/custom.json +4 -0
- package/ui/src/i18n/de.json +609 -0
- package/ui/src/i18n/en.json +609 -0
- package/ui/src/i18n/es.json +609 -0
- package/ui/src/i18n/i18n-context.tsx +65 -0
- package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/ui/src/i18n/i18n.ts +32 -0
- package/ui/src/i18n/i18n.types.ts +49 -0
- package/ui/src/kind-icon.ts +97 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +68 -0
- package/ui/src/modal/modal.tsx +53 -0
- package/ui/src/node-details/node-details.tsx +545 -0
- package/ui/src/node-editor/node-editor.tsx +304 -0
- package/ui/src/node-graph/graph-notice.tsx +21 -0
- package/ui/src/node-graph/node-graph.ts +214 -0
- package/ui/src/node-graph/node-graph.tsx +295 -0
- package/ui/src/node-import/node-import.tsx +418 -0
- package/ui/src/node-save-error.ts +46 -0
- package/ui/src/node-source/node-source.tsx +128 -0
- package/ui/src/node-table/node-table.tsx +107 -0
- package/ui/src/nodes/nodes.tsx +240 -0
- package/ui/src/rename-resource/rename-resource.ts +80 -0
- package/ui/src/resolved-names/resolved-names.tsx +67 -0
- package/ui/src/resource-error.ts +33 -0
- package/ui/src/resource-menu/resource-menu.tsx +1156 -0
- package/ui/src/router/router-context.ts +6 -0
- package/ui/src/router/router.tsx +67 -0
- package/ui/src/router/router.types.ts +8 -0
- package/ui/src/router/selection-search.ts +88 -0
- package/ui/src/save-button/save-button.tsx +94 -0
- package/ui/src/section-hint/section-hint.tsx +40 -0
- package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
- package/ui/src/styles.css +189 -0
- package/ui/src/table-columns/table-columns.tsx +379 -0
- package/ui/src/theme/custom.css +1 -0
- package/ui/src/theme/theme-context.tsx +89 -0
- package/ui/src/theme/theme.ts +70 -0
- package/ui/src/time/relative-time.tsx +41 -0
- package/ui/src/time/time-context.tsx +78 -0
- package/ui/src/time/time.ts +165 -0
- package/ui/src/title-row/title-row.tsx +185 -0
- package/ui/src/tools/tools.tsx +546 -0
- package/ui/src/user-name/user-name.ts +61 -0
- package/ui/src/verbatim/verbatim.tsx +68 -0
- package/ui/tsconfig.json +17 -0
- package/ui/vite.config.ts +129 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { AuditListRequest } from "../contract/audit.js";
|
|
4
|
+
import { BoardAssigneeSearchInput, BoardGetInput, BoardTaskCreateInput, BoardTaskUpdateInput, BoardUpdateInput, DirectoryResolveInput, } from "../contract/board.js";
|
|
5
|
+
import { FeedListRequest } from "../contract/feed.js";
|
|
6
|
+
import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, PurgeFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "../contract/flow.js";
|
|
7
|
+
import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "../contract/flow-run.js";
|
|
8
|
+
import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, MaxAttachmentBytes, NodeGraphInput, PurgeNodeInput, PurgeNodePreviewInput, ResolveNodeLinksInput, SaveAttachmentBytesInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "../contract/node.js";
|
|
9
|
+
import { RevokeFlowGrantInput, RevokeGrantInput, ShareFlowInput, ShareInput, } from "../contract/share.js";
|
|
10
|
+
import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, RedefineTableInput, UpdateTableRowsInput, } from "../contract/table.js";
|
|
11
|
+
import { ExecuteToolInput, TestToolInput } from "../contract/tool.js";
|
|
12
|
+
import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
|
|
13
|
+
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
14
|
+
import { intelProblem, problemDetails as problem, } from "../shared/problem-details/problem-details.js";
|
|
15
|
+
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
16
|
+
// A query string is a door to the outside like a body is, so what arrives through it is closed
|
|
17
|
+
// rather than tolerated: `z.strictObject` refuses an unknown field, and this is that refusal for the
|
|
18
|
+
// half of the input Zod never sees. A silently ignored parameter is how a caller believes it asked
|
|
19
|
+
// for something it did not get — a mistyped `limit=` that quietly draws everything, for instance.
|
|
20
|
+
//
|
|
21
|
+
// ⚠️ Not the same decision as `selectionSearch` in the UI, which deliberately passes unknown search
|
|
22
|
+
// parameters through so the portal's return marker survives a redirect. That is a browser location;
|
|
23
|
+
// this is an API route.
|
|
24
|
+
function requireKnownQuery(url, allowed) {
|
|
25
|
+
const unknown = [];
|
|
26
|
+
url.searchParams.forEach((_value, key) => {
|
|
27
|
+
if (!allowed.includes(key))
|
|
28
|
+
unknown.push(key);
|
|
29
|
+
});
|
|
30
|
+
if (unknown.length) {
|
|
31
|
+
throw new IntelError(400, "invalid_request", `Unknown query parameters: ${unknown.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function asActor(authorization) {
|
|
35
|
+
return {
|
|
36
|
+
id: authorization.identity.id,
|
|
37
|
+
email: authorization.identity.email,
|
|
38
|
+
isAdmin: authorization.can("intel", "admin"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// The exporting caller: the node actor plus whether flows may ride along in the bundle at all.
|
|
42
|
+
// Gate's `flows/read` is asked here once, so the bundle service never re-derives a capability.
|
|
43
|
+
function asBundleActor(authorization) {
|
|
44
|
+
return {
|
|
45
|
+
id: authorization.identity.id,
|
|
46
|
+
email: authorization.identity.email,
|
|
47
|
+
isAdmin: authorization.can("intel", "admin"),
|
|
48
|
+
canReadFlows: permits(authorization, "flows", "read"),
|
|
49
|
+
canCreateFlows: permits(authorization, "flows", "create"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// The idempotency key of an import travels as a header: the body is the zip itself, so there is no
|
|
53
|
+
// JSON envelope to put it in, and a query string would drag it into logs (#137).
|
|
54
|
+
const ImportIdempotencyKey = z.string().min(8).max(200);
|
|
55
|
+
// A bundle export streamed as a download. `content-length` is deliberately absent: the zip is
|
|
56
|
+
// written while it leaves, which is the whole point of streaming it (#136).
|
|
57
|
+
function zipResponse(bundle) {
|
|
58
|
+
return new Response(bundle.stream, {
|
|
59
|
+
headers: {
|
|
60
|
+
"cache-control": "private, no-store",
|
|
61
|
+
"content-disposition": contentDisposition(bundle.filename),
|
|
62
|
+
"content-type": "application/zip",
|
|
63
|
+
"x-content-type-options": "nosniff",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function asToolActor(authorization) {
|
|
68
|
+
return {
|
|
69
|
+
id: authorization.identity.id,
|
|
70
|
+
email: authorization.identity.email,
|
|
71
|
+
canExecute: permits(authorization, "tools", "execute"),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// RFC 5987 ext-value: encodeURIComponent leaves ' * ! ( ) unescaped, and a bare apostrophe is a
|
|
75
|
+
// delimiter that would make the header ambiguous. The ASCII filename stays as a fallback.
|
|
76
|
+
function contentDisposition(title) {
|
|
77
|
+
const encoded = encodeURIComponent(title).replace(/['()*!]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
78
|
+
const ascii = title.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
|
|
79
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`;
|
|
80
|
+
}
|
|
81
|
+
function asFlowActor(authorization) {
|
|
82
|
+
return {
|
|
83
|
+
id: authorization.identity.id,
|
|
84
|
+
email: authorization.identity.email,
|
|
85
|
+
canRun: permits(authorization, "flows", "run"),
|
|
86
|
+
isAdmin: authorization.can("intel", "admin"),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function createHttp(deps) {
|
|
90
|
+
const app = new Hono();
|
|
91
|
+
async function authenticated(headers) {
|
|
92
|
+
const token = bearer(headers) ?? (await deps.auth?.resolve(headers))?.bearer ?? null;
|
|
93
|
+
if (!token)
|
|
94
|
+
return null;
|
|
95
|
+
const authorization = await authorizeBearer(deps.gate, token, deps.resource);
|
|
96
|
+
return authorization ? { authorization, token } : null;
|
|
97
|
+
}
|
|
98
|
+
app.onError((error, context) => {
|
|
99
|
+
if (error instanceof IntelError) {
|
|
100
|
+
return context.json(intelProblem(error), error.status);
|
|
101
|
+
}
|
|
102
|
+
if (error instanceof z.ZodError) {
|
|
103
|
+
return context.json(problem(400, "invalid_request", "Request validation failed", z.prettifyError(error)), 400);
|
|
104
|
+
}
|
|
105
|
+
// Same rule as the outer app: an expected refusal explains itself, an unknown exception must
|
|
106
|
+
// leave a trace — otherwise the 500 is a fact without a reason anywhere (#93).
|
|
107
|
+
reportUnexpectedError(error);
|
|
108
|
+
return context.json(problem(500, "internal_error", "Internal server error"), 500);
|
|
109
|
+
});
|
|
110
|
+
app.use("*", async (context, next) => {
|
|
111
|
+
const session = await authenticated(context.req.raw.headers);
|
|
112
|
+
if (!session) {
|
|
113
|
+
context.header("WWW-Authenticate", `Bearer resource_metadata="${deps.resourceMetadataUrl}"`);
|
|
114
|
+
return context.json(problem(401, "authentication_required", "Authentication required"), 401);
|
|
115
|
+
}
|
|
116
|
+
context.set("authorization", session.authorization);
|
|
117
|
+
context.set("token", session.token);
|
|
118
|
+
await next();
|
|
119
|
+
});
|
|
120
|
+
function requireCapability(context, handle, fn) {
|
|
121
|
+
const authorization = context.get("authorization");
|
|
122
|
+
if (!permits(authorization, handle, fn)) {
|
|
123
|
+
throw new IntelError(403, "permission_required", "Permission required");
|
|
124
|
+
}
|
|
125
|
+
return authorization;
|
|
126
|
+
}
|
|
127
|
+
// The caller's own identity, so the shell can name who is signed in. It needs no capability —
|
|
128
|
+
// the answer is the token's own subject — and it returns nothing but id, email, and name.
|
|
129
|
+
app.get("/session", (context) => {
|
|
130
|
+
const authorization = context.get("authorization");
|
|
131
|
+
const { identity } = authorization;
|
|
132
|
+
return context.json({
|
|
133
|
+
id: identity.id,
|
|
134
|
+
email: identity.email,
|
|
135
|
+
name: identity.name ?? null,
|
|
136
|
+
// ⚠️ What the shell may DRAW, never what it may do (#416). Every admin route below still asks
|
|
137
|
+
// `requireCapability` for itself, and a browser that lied here would gain nothing but a
|
|
138
|
+
// button that answers 403.
|
|
139
|
+
isAdmin: authorization.can("intel", "admin"),
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
app.get("/nodes", async (context) => {
|
|
143
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
144
|
+
const url = new URL(context.req.url);
|
|
145
|
+
const unknownQuery = [];
|
|
146
|
+
url.searchParams.forEach((_value, key) => {
|
|
147
|
+
if (key !== "parentId" && key !== "includeArchived" && key !== "archivedOnly") {
|
|
148
|
+
unknownQuery.push(key);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if (unknownQuery.length) {
|
|
152
|
+
throw new IntelError(400, "invalid_request", `Unknown query parameters: ${unknownQuery.join(", ")}`);
|
|
153
|
+
}
|
|
154
|
+
const input = ListNodesInput.parse({
|
|
155
|
+
parentId: url.searchParams.get("parentId"),
|
|
156
|
+
includeArchived: url.searchParams.get("includeArchived") === "true",
|
|
157
|
+
...(url.searchParams.get("archivedOnly") === "true" ? { archivedOnly: true } : {}),
|
|
158
|
+
});
|
|
159
|
+
return context.json(await deps.nodes.list(asActor(auth), input));
|
|
160
|
+
});
|
|
161
|
+
app.get("/nodes/graph", async (context) => {
|
|
162
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
163
|
+
const url = new URL(context.req.url);
|
|
164
|
+
const input = NodeGraphInput.parse({
|
|
165
|
+
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
|
|
166
|
+
});
|
|
167
|
+
return context.json(await deps.nodes.graph(asActor(auth), input));
|
|
168
|
+
});
|
|
169
|
+
// ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
|
|
170
|
+
// node to address, so the whole installation — as this caller may read it — exports here (#136).
|
|
171
|
+
app.get("/nodes/export", async (context) => {
|
|
172
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
173
|
+
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), null));
|
|
174
|
+
});
|
|
175
|
+
// The reverse door (#137): a zip bundle — or a naked zipped folder — lands as a new subtree.
|
|
176
|
+
// The body IS the zip, streamed to the service rather than read whole here; `knowledge/create`
|
|
177
|
+
// like every other way of making nodes, and the resource-level write check is the service's.
|
|
178
|
+
async function importBundle(context, targetNodeId) {
|
|
179
|
+
const auth = requireCapability(context, "nodes", "create");
|
|
180
|
+
const idempotencyKey = ImportIdempotencyKey.parse(context.req.header("idempotency-key"));
|
|
181
|
+
const body = context.req.raw.body;
|
|
182
|
+
if (body === null) {
|
|
183
|
+
throw new IntelError(400, "invalid_request", "A zip request body is required");
|
|
184
|
+
}
|
|
185
|
+
const result = await deps.bundle.importBundle(asBundleActor(auth), {
|
|
186
|
+
targetNodeId,
|
|
187
|
+
zip: body,
|
|
188
|
+
idempotencyKey,
|
|
189
|
+
});
|
|
190
|
+
// A replay answers 200: nothing was created by THIS request, and the summary is the earlier
|
|
191
|
+
// import's. 201 stays the word for rows that exist because of this call.
|
|
192
|
+
return context.json(result, result.replayed ? 200 : 201);
|
|
193
|
+
}
|
|
194
|
+
// ⚠️ Before `/nodes/:nodeId`, or the router would read "import" as a node ID.
|
|
195
|
+
app.post("/nodes/import", async (context) => await importBundle(context, null));
|
|
196
|
+
app.get("/nodes/:nodeId", async (context) => {
|
|
197
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
198
|
+
return context.json(await deps.nodes.get(asActor(auth), context.req.param("nodeId")));
|
|
199
|
+
});
|
|
200
|
+
app.post("/nodes/:nodeId/import", async (context) => {
|
|
201
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
202
|
+
return await importBundle(context, input.nodeId);
|
|
203
|
+
});
|
|
204
|
+
// One subtree — a folder with everything beneath it, or a single node — as a zip bundle (#136).
|
|
205
|
+
app.get("/nodes/:nodeId/export", async (context) => {
|
|
206
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
207
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
208
|
+
return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
|
|
209
|
+
});
|
|
210
|
+
// The change journal, read forward from a stable position (#620).
|
|
211
|
+
//
|
|
212
|
+
// ⚠️ **The capability follows the KIND asked for** (#774). An audit row says the same thing its
|
|
213
|
+
// resource says, so the journal asks what reading that resource asks: `nodes/read` for `node`,
|
|
214
|
+
// `flows/read` for `flow` and `flow-run` — which is what every other flow-delivering route in
|
|
215
|
+
// this file requires. Until the enum was widened one capability covered the one kind there was;
|
|
216
|
+
// leaving it at `nodes/read` afterwards would have dropped the first of the three checks for
|
|
217
|
+
// exactly the two kinds this ticket opened, and left the Intel ACL as the only filter.
|
|
218
|
+
//
|
|
219
|
+
// ⚠️ The parse therefore runs BEFORE the capability check, which is the one place in this file
|
|
220
|
+
// where it does. That is not a loosening of "capability first, so a refusal costs no storage
|
|
221
|
+
// read": parsing touches the query string and nothing else, and the kind cannot be known any
|
|
222
|
+
// earlier than this. What it costs is that a malformed `resourceType` answers 400 rather than
|
|
223
|
+
// 403 — a sentence about the caller's own parameter, which names nothing of Intel's.
|
|
224
|
+
//
|
|
225
|
+
// ⚠️ GET with the cursor in the query string, because the verb is one of the four in the grammar
|
|
226
|
+
// and the method carries it. `limit` arrives as a string and is coerced here — the contract
|
|
227
|
+
// wants a number, and a query string has none.
|
|
228
|
+
app.get("/audit", async (context) => {
|
|
229
|
+
const limit = context.req.query("limit");
|
|
230
|
+
const after = context.req.query("after");
|
|
231
|
+
const input = AuditListRequest.parse({
|
|
232
|
+
resourceType: context.req.query("resourceType"),
|
|
233
|
+
...(after === undefined ? {} : { after }),
|
|
234
|
+
...(limit === undefined ? {} : { limit: Number(limit) }),
|
|
235
|
+
});
|
|
236
|
+
const auth = requireCapability(context, input.resourceType === "node" ? "nodes" : "flows", "read");
|
|
237
|
+
return context.json(await deps.audit.list(asActor(auth), input));
|
|
238
|
+
});
|
|
239
|
+
// The same journal read the other way round: newest first, for a person rather than for a
|
|
240
|
+
// consumer catching up (#740).
|
|
241
|
+
//
|
|
242
|
+
// ⚠️ `nodes/read` for the whole answer, and here that CANNOT follow the kind the way `/audit`
|
|
243
|
+
// above does: one page mixes node and flow cards on purpose, so there is no single kind to pick a
|
|
244
|
+
// capability from. It has been that way since flow cards existed and #767 only added two actions
|
|
245
|
+
// to them, so nothing about it is new — but it does mean somebody holding `nodes/read` without
|
|
246
|
+
// `flows/read` still sees flow activity here, filtered by the Intel ACL alone. Narrowing it means
|
|
247
|
+
// filtering cards by capability inside the feed service, which is a change to the answer rather
|
|
248
|
+
// than to the door: #790.
|
|
249
|
+
//
|
|
250
|
+
// ⚠️ A SECOND door and not an option on `/audit`. That one is the contract `anchrd/signals` will
|
|
251
|
+
// build on, cursor and direction included; a shared signature would put both readers on one
|
|
252
|
+
// shape, and the next change to it would reach a consumer nobody had in mind.
|
|
253
|
+
app.get("/feed", async (context) => {
|
|
254
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
255
|
+
const limit = context.req.query("limit");
|
|
256
|
+
const before = context.req.query("before");
|
|
257
|
+
const actor = context.req.query("actor");
|
|
258
|
+
const input = FeedListRequest.parse({
|
|
259
|
+
...(actor === undefined ? {} : { actor }),
|
|
260
|
+
...(before === undefined ? {} : { before }),
|
|
261
|
+
...(limit === undefined ? {} : { limit: Number(limit) }),
|
|
262
|
+
});
|
|
263
|
+
return context.json(await deps.feed.list(asActor(auth), input));
|
|
264
|
+
});
|
|
265
|
+
// The whole board in one answer (#648). `nodes/read` and nothing new: a board is a node, and a
|
|
266
|
+
// card is a node — a second capability would be a second answer to one question.
|
|
267
|
+
//
|
|
268
|
+
// ⚠️ Every filter is a query parameter and every one of them reaches the WHERE. Filtering after
|
|
269
|
+
// the read would have carried other people's cards over the wire first.
|
|
270
|
+
app.get("/boards/:boardId", async (context) => {
|
|
271
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
272
|
+
const query = context.req.query();
|
|
273
|
+
const input = BoardGetInput.parse({
|
|
274
|
+
boardId: context.req.param("boardId"),
|
|
275
|
+
...(query.status === undefined ? {} : { status: query.status }),
|
|
276
|
+
...(query.assigneeId === undefined ? {} : { assigneeId: query.assigneeId }),
|
|
277
|
+
...(query.dueBefore === undefined ? {} : { dueBefore: query.dueBefore }),
|
|
278
|
+
...(query.dependsOn === undefined ? {} : { dependsOn: query.dependsOn }),
|
|
279
|
+
...(query.includeArchived === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: { includeArchived: query.includeArchived === "true" }),
|
|
282
|
+
});
|
|
283
|
+
return context.json(await deps.boards.get(asActor(auth), input));
|
|
284
|
+
});
|
|
285
|
+
/**
|
|
286
|
+
* Who this card may be given to (#700, D70).
|
|
287
|
+
*
|
|
288
|
+
* ⚠️ `nodes/read`, not a capability of its own and certainly not `nodes/write`. Offering an
|
|
289
|
+
* assignee is something everyone who can open the board does; a separate permission would be one
|
|
290
|
+
* more thing to grant before a picker works, and nothing would be safer for it — the answer is
|
|
291
|
+
* already narrower than what gate hands any signed-in person at its own door.
|
|
292
|
+
*/
|
|
293
|
+
app.get("/boards/:boardId/assignees", async (context) => {
|
|
294
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
295
|
+
const input = BoardAssigneeSearchInput.parse({
|
|
296
|
+
boardId: context.req.param("boardId"),
|
|
297
|
+
query: context.req.query("q") ?? "",
|
|
298
|
+
});
|
|
299
|
+
return context.json(await deps.boards.searchAssignees(asActor(auth), input));
|
|
300
|
+
});
|
|
301
|
+
/**
|
|
302
|
+
* What the people already on cards are called (#258, #700).
|
|
303
|
+
*
|
|
304
|
+
* ⚠️ A POST although it reads, and for the same reason gate's own door is one: the ids travel in
|
|
305
|
+
* the body. A list of a hundred identifiers in a query string lands in proxy logs and in browser
|
|
306
|
+
* history, and it would hit the length limit long before the hundredth.
|
|
307
|
+
*/
|
|
308
|
+
app.post("/directory/resolve", async (context) => {
|
|
309
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
310
|
+
const input = DirectoryResolveInput.parse(await context.req.json().catch(() => null));
|
|
311
|
+
return context.json(await deps.boards.resolveNames(asActor(auth), input));
|
|
312
|
+
});
|
|
313
|
+
app.patch("/boards/:boardId", async (context) => {
|
|
314
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
315
|
+
const input = BoardUpdateInput.parse(await context.req.json().catch(() => null));
|
|
316
|
+
if (input.boardId !== context.req.param("boardId")) {
|
|
317
|
+
throw new IntelError(400, "board_id_mismatch", "Path and body board IDs differ");
|
|
318
|
+
}
|
|
319
|
+
return context.json(await deps.boards.update(asActor(auth), input));
|
|
320
|
+
});
|
|
321
|
+
app.post("/boards/:boardId/tasks", async (context) => {
|
|
322
|
+
const auth = requireCapability(context, "nodes", "create");
|
|
323
|
+
const input = BoardTaskCreateInput.parse(await context.req.json().catch(() => null));
|
|
324
|
+
if (input.boardId !== context.req.param("boardId")) {
|
|
325
|
+
throw new IntelError(400, "board_id_mismatch", "Path and body board IDs differ");
|
|
326
|
+
}
|
|
327
|
+
return context.json(await deps.boards.createTask(asActor(auth), input), 201);
|
|
328
|
+
});
|
|
329
|
+
// ⚠️ `PATCH` on the task, not `POST …/move`. Moving a card IS an update of `status` and
|
|
330
|
+
// `position`; a second address for it would be a second way to write one row.
|
|
331
|
+
app.patch("/boards/:boardId/tasks/:taskId", async (context) => {
|
|
332
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
333
|
+
const input = BoardTaskUpdateInput.parse(await context.req.json().catch(() => null));
|
|
334
|
+
if (input.taskId !== context.req.param("taskId")) {
|
|
335
|
+
throw new IntelError(400, "task_id_mismatch", "Path and body task IDs differ");
|
|
336
|
+
}
|
|
337
|
+
return context.json(await deps.boards.updateTask(asActor(auth), input));
|
|
338
|
+
});
|
|
339
|
+
app.get("/nodes/:nodeId/versions", async (context) => {
|
|
340
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
341
|
+
return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
|
|
342
|
+
});
|
|
343
|
+
// The content behind one row of the list above (#147): what a version-pinned citation points at.
|
|
344
|
+
// Read capability like every other node read; the resource ACL and the node/version pairing are
|
|
345
|
+
// the service's to refuse.
|
|
346
|
+
app.get("/nodes/:nodeId/versions/:versionId/content", async (context) => {
|
|
347
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
348
|
+
const input = GetNodeVersionInput.parse({
|
|
349
|
+
nodeId: context.req.param("nodeId"),
|
|
350
|
+
versionId: context.req.param("versionId"),
|
|
351
|
+
});
|
|
352
|
+
return context.json(await deps.nodes.getVersion(asActor(auth), input.nodeId, input.versionId));
|
|
353
|
+
});
|
|
354
|
+
// The metadata half of an attachment (#148): what `node_attachment_get` answers an agent, for
|
|
355
|
+
// HTTP callers who so far could only stream the bytes. Without the `resourceUri` — an MCP
|
|
356
|
+
// resource address says nothing to an HTTP client, whose address for the bytes is the sibling
|
|
357
|
+
// route below.
|
|
358
|
+
app.get("/nodes/:nodeId/attachment/meta", async (context) => {
|
|
359
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
360
|
+
const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
|
|
361
|
+
const { node, version } = await deps.nodes.getAttachment(asActor(auth), input.nodeId);
|
|
362
|
+
return context.json({ node, version });
|
|
363
|
+
});
|
|
364
|
+
app.get("/nodes/:nodeId/attachment", async (context) => {
|
|
365
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
366
|
+
const { attachment, body } = await deps.nodes.readAttachment(asActor(auth), context.req.param("nodeId"));
|
|
367
|
+
return new Response(body, {
|
|
368
|
+
headers: {
|
|
369
|
+
"cache-control": "private, no-store",
|
|
370
|
+
"content-disposition": contentDisposition(attachment.node.title),
|
|
371
|
+
"content-type": attachment.version.mediaType,
|
|
372
|
+
"x-content-type-options": "nosniff",
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
app.get("/nodes/:nodeId/table", async (context) => {
|
|
377
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
378
|
+
return context.json(await deps.nodes.getTable(asActor(auth), context.req.param("nodeId")));
|
|
379
|
+
});
|
|
380
|
+
// Defining the header and appending rows are two routes because they are two decisions: the
|
|
381
|
+
// header is written once and is the contract, an append is the everyday write (#40).
|
|
382
|
+
app.post("/nodes/:nodeId/table", async (context) => {
|
|
383
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
384
|
+
const input = DefineTableInput.parse(await context.req.json().catch(() => null));
|
|
385
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
386
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
387
|
+
}
|
|
388
|
+
return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
|
|
389
|
+
});
|
|
390
|
+
app.post("/nodes/:nodeId/table/rows", async (context) => {
|
|
391
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
392
|
+
const input = AppendTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
393
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
394
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
395
|
+
}
|
|
396
|
+
return context.json(await deps.nodes.appendTableRows(asActor(auth), input), 201);
|
|
397
|
+
});
|
|
398
|
+
// The three mutations beside the append (#135). Action-style POSTs like the archive and revoke
|
|
399
|
+
// routes, because a row has no address of its own to PUT to: positions live in the body, next to
|
|
400
|
+
// the `baseVersionId` that says which state they mean. Each writes one snapshot version, so the
|
|
401
|
+
// 201 is the same statement the append's 201 makes.
|
|
402
|
+
app.post("/nodes/:nodeId/table/rows/update", async (context) => {
|
|
403
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
404
|
+
const input = UpdateTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
405
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
406
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
407
|
+
}
|
|
408
|
+
return context.json(await deps.nodes.updateTableRows(asActor(auth), input), 201);
|
|
409
|
+
});
|
|
410
|
+
app.post("/nodes/:nodeId/table/rows/delete", async (context) => {
|
|
411
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
412
|
+
const input = DeleteTableRowsInput.parse(await context.req.json().catch(() => null));
|
|
413
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
414
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
415
|
+
}
|
|
416
|
+
return context.json(await deps.nodes.deleteTableRows(asActor(auth), input), 201);
|
|
417
|
+
});
|
|
418
|
+
app.patch("/nodes/:nodeId/table", async (context) => {
|
|
419
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
420
|
+
const input = RedefineTableInput.parse(await context.req.json().catch(() => null));
|
|
421
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
422
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
423
|
+
}
|
|
424
|
+
return context.json(await deps.nodes.redefineTable(asActor(auth), input), 201);
|
|
425
|
+
});
|
|
426
|
+
// ⚠️ There is deliberately no route for the sixth board operation. `board_repair_task_ids`
|
|
427
|
+
// (anchrd/intel#341) is MCP-only until a screen calls it: the routes here are what the browser
|
|
428
|
+
// reaches, nothing in `intel-data-provider` asks for it, and a route with no caller is the thing
|
|
429
|
+
// YAGNI is about. The screen is anchrd/intel#358, and it brings this route with it.
|
|
430
|
+
app.get("/nodes/:nodeId/links", async (context) => {
|
|
431
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
432
|
+
return context.json(await deps.nodes.listLinks(asActor(auth), context.req.param("nodeId")));
|
|
433
|
+
});
|
|
434
|
+
// A document link's name, for whoever is reading the text (#41). A POST because the list of IDs
|
|
435
|
+
// is the request body — putting them in a query string would put the tree's identifiers into
|
|
436
|
+
// logs and referrers. The `knowledge/read` capability and nothing more: it answers with what this reader may
|
|
437
|
+
// already see, and stays silent about the rest rather than saying that there is a rest.
|
|
438
|
+
//
|
|
439
|
+
// There is no route to create or delete a link any more. A relationship is written where it is
|
|
440
|
+
// meant, in the text, and `POST /nodes/:nodeId/versions` is what records it.
|
|
441
|
+
app.post("/nodes/links/resolve", async (context) => {
|
|
442
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
443
|
+
const input = ResolveNodeLinksInput.parse(await context.req.json().catch(() => null));
|
|
444
|
+
return context.json(await deps.nodes.resolveLinks(asActor(auth), input));
|
|
445
|
+
});
|
|
446
|
+
app.post("/nodes/search", async (context) => {
|
|
447
|
+
const auth = requireCapability(context, "nodes", "read");
|
|
448
|
+
const input = SearchInput.parse(await context.req.json().catch(() => null));
|
|
449
|
+
return context.json(await deps.nodes.search(asActor(auth), input));
|
|
450
|
+
});
|
|
451
|
+
app.post("/nodes/reindex", async (context) => {
|
|
452
|
+
const auth = requireCapability(context, "intel", "admin");
|
|
453
|
+
z.strictObject({}).parse(await context.req.json().catch(() => null));
|
|
454
|
+
return context.json(await deps.nodes.reindex(asActor(auth)), 202);
|
|
455
|
+
});
|
|
456
|
+
app.post("/nodes", async (context) => {
|
|
457
|
+
const auth = requireCapability(context, "nodes", "create");
|
|
458
|
+
const input = CreateNodeInput.parse(await context.req.json().catch(() => null));
|
|
459
|
+
return context.json(await deps.nodes.create(asActor(auth), input), 201);
|
|
460
|
+
});
|
|
461
|
+
app.post("/nodes/:nodeId/versions", async (context) => {
|
|
462
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
463
|
+
const input = SaveNodeVersionInput.parse(await context.req.json().catch(() => null));
|
|
464
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
465
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
466
|
+
}
|
|
467
|
+
return context.json(await deps.nodes.save(asActor(auth), input), 201);
|
|
468
|
+
});
|
|
469
|
+
/**
|
|
470
|
+
* The streaming door for a file (#821): the body IS the bytes, and everything about them travels
|
|
471
|
+
* in the headers the request already has.
|
|
472
|
+
*
|
|
473
|
+
* ⚠️ **It has no MCP twin on purpose**, and it is the one capability in this file that does not.
|
|
474
|
+
* MCP carries JSON, so a tool cannot hand over a body; `node_attachment_create` remains the MCP
|
|
475
|
+
* form of the same act, with the base64 size limit that door brings with it. What must not happen
|
|
476
|
+
* is a second set of RULES here — the checks are the service's, and both doors call the same one.
|
|
477
|
+
*
|
|
478
|
+
* ⚠️ `intel-base-version-id` carries what `baseVersionId` carries in the JSON door, and the word
|
|
479
|
+
* `none` is how a first upload says "there is no version yet". An absent header is refused rather
|
|
480
|
+
* than read as `none`: a caller who merely forgot it would otherwise overwrite the newest version
|
|
481
|
+
* instead of being told that somebody else wrote first — which is the whole point of the field.
|
|
482
|
+
*
|
|
483
|
+
* ⚠️ The length is refused BEFORE the body is read, from `content-length`. A file over the limit
|
|
484
|
+
* has to end in a sentence the reader gets, and a request that has already been read to its end
|
|
485
|
+
* cannot be answered with one on every client.
|
|
486
|
+
*/
|
|
487
|
+
app.post("/nodes/:nodeId/attachment/bytes", async (context) => {
|
|
488
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
489
|
+
const baseVersionHeader = context.req.header("intel-base-version-id");
|
|
490
|
+
if (baseVersionHeader === undefined) {
|
|
491
|
+
throw new IntelError(400, "invalid_request", 'intel-base-version-id takes the current version id, or "none" for the first upload');
|
|
492
|
+
}
|
|
493
|
+
const declared = Number(context.req.header("content-length"));
|
|
494
|
+
if (!Number.isSafeInteger(declared) || declared <= 0) {
|
|
495
|
+
throw new IntelError(411, "length_required", "An upload has to announce its length in content-length");
|
|
496
|
+
}
|
|
497
|
+
if (declared > MaxAttachmentBytes) {
|
|
498
|
+
throw new IntelError(413, "attachment_too_large", `An attachment may carry at most ${MaxAttachmentBytes} bytes`);
|
|
499
|
+
}
|
|
500
|
+
const input = SaveAttachmentBytesInput.parse({
|
|
501
|
+
nodeId: context.req.param("nodeId"),
|
|
502
|
+
baseVersionId: baseVersionHeader === "none" ? null : baseVersionHeader,
|
|
503
|
+
mediaType: context.req.header("content-type") ?? "",
|
|
504
|
+
size: declared,
|
|
505
|
+
idempotencyKey: ImportIdempotencyKey.parse(context.req.header("idempotency-key")),
|
|
506
|
+
});
|
|
507
|
+
const body = context.req.raw.body;
|
|
508
|
+
if (body === null) {
|
|
509
|
+
throw new IntelError(400, "invalid_request", "A request body is required");
|
|
510
|
+
}
|
|
511
|
+
return context.json(await deps.nodes.saveAttachmentBytes(asActor(auth), input, body), 201);
|
|
512
|
+
});
|
|
513
|
+
app.post("/nodes/:nodeId/attachment", async (context) => {
|
|
514
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
515
|
+
const input = SaveAttachmentInput.parse(await context.req.json().catch(() => null));
|
|
516
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
517
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
518
|
+
}
|
|
519
|
+
return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
|
|
520
|
+
});
|
|
521
|
+
app.patch("/nodes/:nodeId", async (context) => {
|
|
522
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
523
|
+
const input = UpdateNodeInput.parse(await context.req.json().catch(() => null));
|
|
524
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
525
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
526
|
+
}
|
|
527
|
+
return context.json(await deps.nodes.update(asActor(auth), input));
|
|
528
|
+
});
|
|
529
|
+
app.post("/nodes/:nodeId/archive", async (context) => {
|
|
530
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
531
|
+
const input = ArchiveNodeInput.parse(await context.req.json().catch(() => null));
|
|
532
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
533
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
534
|
+
}
|
|
535
|
+
// The caller's own bearer, for the one thing archiving may have to do in Gate: switch off the
|
|
536
|
+
// Application behind an agent (#182). Every other kind never reaches it.
|
|
537
|
+
return context.json(await deps.nodes.archive(asActor(auth), input));
|
|
538
|
+
});
|
|
539
|
+
// ⚠️ DELETE, and it means it (#457). Every other "delete" in this surface is `archived_at`; this
|
|
540
|
+
// one leaves nothing behind. The same capability as archiving — `nodes.write` — because the
|
|
541
|
+
// structural guard is elsewhere: only an archived node can be purged, so the destructive way has
|
|
542
|
+
// two steps and the first one is reversible.
|
|
543
|
+
app.delete("/nodes/:nodeId", async (context) => {
|
|
544
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
545
|
+
const input = PurgeNodeInput.parse(await context.req.json().catch(() => null));
|
|
546
|
+
if (input.nodeId !== context.req.param("nodeId")) {
|
|
547
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
|
|
548
|
+
}
|
|
549
|
+
return context.json(await deps.nodes.purge(asActor(auth), input));
|
|
550
|
+
});
|
|
551
|
+
app.get("/nodes/:nodeId/purge-preview", async (context) => {
|
|
552
|
+
const auth = requireCapability(context, "nodes", "write");
|
|
553
|
+
const input = PurgeNodePreviewInput.parse({ nodeId: context.req.param("nodeId") });
|
|
554
|
+
return context.json(await deps.nodes.purgePreview(asActor(auth), input));
|
|
555
|
+
});
|
|
556
|
+
app.get("/nodes/:nodeId/grants", async (context) => {
|
|
557
|
+
const auth = requireCapability(context, "nodes", "share");
|
|
558
|
+
return context.json(await deps.nodes.listGrants(asActor(auth), context.req.param("nodeId")));
|
|
559
|
+
});
|
|
560
|
+
app.get("/nodes/:nodeId/effective-access", async (context) => {
|
|
561
|
+
const auth = requireCapability(context, "nodes", "share");
|
|
562
|
+
return context.json(await deps.nodes.listEffectiveAccess(asActor(auth), context.req.param("nodeId")));
|
|
563
|
+
});
|
|
564
|
+
app.post("/nodes/:nodeId/grants", async (context) => {
|
|
565
|
+
const auth = requireCapability(context, "nodes", "share");
|
|
566
|
+
const input = ShareInput.parse(await context.req.json().catch(() => null));
|
|
567
|
+
if (input.resourceId !== context.req.param("nodeId")) {
|
|
568
|
+
throw new IntelError(400, "node_id_mismatch", "Path and body resource IDs differ");
|
|
569
|
+
}
|
|
570
|
+
return context.json(await deps.nodes.share(asActor(auth), input), 201);
|
|
571
|
+
});
|
|
572
|
+
app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
|
|
573
|
+
const auth = requireCapability(context, "nodes", "share");
|
|
574
|
+
const input = RevokeGrantInput.parse(await context.req.json().catch(() => null));
|
|
575
|
+
if (input.resourceId !== context.req.param("nodeId") ||
|
|
576
|
+
input.grantId !== context.req.param("grantId")) {
|
|
577
|
+
throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
|
|
578
|
+
}
|
|
579
|
+
return context.json(await deps.nodes.revokeGrant(asActor(auth), input));
|
|
580
|
+
});
|
|
581
|
+
app.get("/flows", async (context) => {
|
|
582
|
+
const auth = requireCapability(context, "flows", "read");
|
|
583
|
+
const url = new URL(context.req.url);
|
|
584
|
+
requireKnownQuery(url, ["parentId", "includeArchived", "archivedOnly"]);
|
|
585
|
+
// Absent means "every flow I may see"; present but empty means the root of the shared tree.
|
|
586
|
+
// Without that distinction a tree level and a full list would be the same request.
|
|
587
|
+
const parentId = url.searchParams.get("parentId");
|
|
588
|
+
// Same rule for the archive: absent means "without it". Spelling the default out would put a
|
|
589
|
+
// field into every call that asks nothing about the archive.
|
|
590
|
+
const input = ListFlowsInput.parse({
|
|
591
|
+
...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
|
|
592
|
+
...(url.searchParams.get("includeArchived") === "true" ? { includeArchived: true } : {}),
|
|
593
|
+
...(url.searchParams.get("archivedOnly") === "true" ? { archivedOnly: true } : {}),
|
|
594
|
+
});
|
|
595
|
+
return context.json(await deps.flows.list(asFlowActor(auth), input));
|
|
596
|
+
});
|
|
597
|
+
// ⚠️ Before `/flows/:flowId`, or the router would read "graph" as a flow ID. Same order and same
|
|
598
|
+
// reason as `/nodes/graph`.
|
|
599
|
+
app.get("/flows/graph", async (context) => {
|
|
600
|
+
const auth = requireCapability(context, "flows", "read");
|
|
601
|
+
const url = new URL(context.req.url);
|
|
602
|
+
requireKnownQuery(url, ["of", "folderId", "flowId", "limit"]);
|
|
603
|
+
const of = url.searchParams.get("of");
|
|
604
|
+
const input = RelationGraphInput.parse({
|
|
605
|
+
scope: of === "flow"
|
|
606
|
+
? { of: "flow", flowId: url.searchParams.get("flowId") ?? "" }
|
|
607
|
+
: { of: "folder", folderId: url.searchParams.get("folderId") || null },
|
|
608
|
+
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
|
|
609
|
+
});
|
|
610
|
+
return context.json(await deps.flows.relationGraph(asFlowActor(auth), input));
|
|
611
|
+
});
|
|
612
|
+
// A path variable is a door like any other, so it is parsed rather than trusted: `GetFlowInput`
|
|
613
|
+
// is the same schema the body-carrying routes and the MCP tools use for this identifier.
|
|
614
|
+
app.get("/flows/:flowId", async (context) => {
|
|
615
|
+
const auth = requireCapability(context, "flows", "read");
|
|
616
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
617
|
+
return context.json(await deps.flows.get(asFlowActor(auth), input.flowId));
|
|
618
|
+
});
|
|
619
|
+
/**
|
|
620
|
+
* A single flow as the same bundle shape a node export makes: one entry plus the manifest
|
|
621
|
+
* (#136). `flows/read` like every other read of the graph.
|
|
622
|
+
*
|
|
623
|
+
* ⚠️ Deliberately no `flow_export` tool beside it, and the reason is NOT the one `node_export`
|
|
624
|
+
* carries (#436). There the memory argument decides — a subtree's zip would have to fit in Worker
|
|
625
|
+
* memory twice — and a flow bundle is one entry plus the manifest, so that argument does not
|
|
626
|
+
* reach here.
|
|
627
|
+
*
|
|
628
|
+
* What decides here is that the manifest of ONE entry answers nothing MCP does not already
|
|
629
|
+
* answer. `node_export` is worth a tool because its manifest is the PLAN of a subtree: every
|
|
630
|
+
* readable entry with its path, kind and media type, which is the map for fetching each one. For
|
|
631
|
+
* a single flow that map has one row, whose id the caller already holds — and the substance, the
|
|
632
|
+
* graph, is `flow_version_get` and `flow_graph`, with the record itself in `flow_get`. A tool
|
|
633
|
+
* whose whole answer is two other tools' answers costs bytes in every `tools/list` and makes the
|
|
634
|
+
* other names less reliable as a guide, which `packages/intel/CLAUDE.md` treats as the cost it is.
|
|
635
|
+
*
|
|
636
|
+
* ⚠️ This is not the case `.claude/rules/destructive.md` warns about — "a way only the surface
|
|
637
|
+
* has is a way an agent works around". Nothing about a flow is unreachable over MCP; only the zip
|
|
638
|
+
* CONTAINER is HTTP-only, exactly as it is for nodes. `mcp.e2e.ts` pins that this exception is
|
|
639
|
+
* named rather than merely absent.
|
|
640
|
+
*/
|
|
641
|
+
app.get("/flows/:flowId/export", async (context) => {
|
|
642
|
+
const auth = requireCapability(context, "flows", "read");
|
|
643
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
644
|
+
return zipResponse(await deps.bundle.exportFlow(asBundleActor(auth), input.flowId));
|
|
645
|
+
});
|
|
646
|
+
// What this flow calls, read out of its graph. The tree lists where a flow is filed; this lists
|
|
647
|
+
// what it runs, and a shared subflow answers both questions at once (ADR-0004 §3).
|
|
648
|
+
app.get("/flows/:flowId/calls", async (context) => {
|
|
649
|
+
const auth = requireCapability(context, "flows", "read");
|
|
650
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
651
|
+
return context.json(await deps.flows.listCalls(asFlowActor(auth), input.flowId));
|
|
652
|
+
});
|
|
653
|
+
// Would this flow start, for whoever is asking, right now (#72). `flows/read` and nothing more:
|
|
654
|
+
// the answer somebody without `execute` needs most is that they are missing `execute`, and it
|
|
655
|
+
// names nothing they could not read out of the graph. A GET because it changes nothing — no run,
|
|
656
|
+
// no key, no audit event.
|
|
657
|
+
app.get("/flows/:flowId/validation", async (context) => {
|
|
658
|
+
const auth = requireCapability(context, "flows", "read");
|
|
659
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
660
|
+
return context.json(await deps.flows.validate(asFlowActor(auth), input.flowId));
|
|
661
|
+
});
|
|
662
|
+
// What this flow needs: the documents and tools its graph names. `flows/read` and nothing more —
|
|
663
|
+
// it describes a flow the caller may already open, and names only what they may already see.
|
|
664
|
+
app.get("/flows/:flowId/requirements", async (context) => {
|
|
665
|
+
const auth = requireCapability(context, "flows", "read");
|
|
666
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
667
|
+
return context.json(await deps.flows.listRequirements(asFlowActor(auth), input.flowId));
|
|
668
|
+
});
|
|
669
|
+
// The flow's own sharing (#530), segment for segment the node routes one screen up. `flows/share`
|
|
670
|
+
// was declared to Gate from the beginning and had no consumer until now; these four are it.
|
|
671
|
+
app.get("/flows/:flowId/grants", async (context) => {
|
|
672
|
+
const auth = requireCapability(context, "flows", "share");
|
|
673
|
+
return context.json(await deps.flows.listGrants(asFlowActor(auth), context.req.param("flowId")));
|
|
674
|
+
});
|
|
675
|
+
app.get("/flows/:flowId/effective-access", async (context) => {
|
|
676
|
+
const auth = requireCapability(context, "flows", "share");
|
|
677
|
+
return context.json(await deps.flows.listEffectiveAccess(asFlowActor(auth), context.req.param("flowId")));
|
|
678
|
+
});
|
|
679
|
+
app.post("/flows/:flowId/grants", async (context) => {
|
|
680
|
+
const auth = requireCapability(context, "flows", "share");
|
|
681
|
+
const input = ShareFlowInput.parse(await context.req.json().catch(() => null));
|
|
682
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
683
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
684
|
+
}
|
|
685
|
+
return context.json(await deps.flows.share(asFlowActor(auth), input), 201);
|
|
686
|
+
});
|
|
687
|
+
app.post("/flows/:flowId/grants/:grantId/revoke", async (context) => {
|
|
688
|
+
const auth = requireCapability(context, "flows", "share");
|
|
689
|
+
const input = RevokeFlowGrantInput.parse(await context.req.json().catch(() => null));
|
|
690
|
+
if (input.flowId !== context.req.param("flowId") ||
|
|
691
|
+
input.grantId !== context.req.param("grantId")) {
|
|
692
|
+
throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
|
|
693
|
+
}
|
|
694
|
+
return context.json(await deps.flows.revokeGrant(asFlowActor(auth), input));
|
|
695
|
+
});
|
|
696
|
+
app.post("/flows", async (context) => {
|
|
697
|
+
const auth = requireCapability(context, "flows", "create");
|
|
698
|
+
const input = CreateFlowInput.parse(await context.req.json().catch(() => null));
|
|
699
|
+
return context.json(await deps.flows.create(asFlowActor(auth), input), 201);
|
|
700
|
+
});
|
|
701
|
+
app.patch("/flows/:flowId", async (context) => {
|
|
702
|
+
const auth = requireCapability(context, "flows", "write");
|
|
703
|
+
const input = UpdateFlowInput.parse(await context.req.json().catch(() => null));
|
|
704
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
705
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
706
|
+
}
|
|
707
|
+
return context.json(await deps.flows.update(asFlowActor(auth), input));
|
|
708
|
+
});
|
|
709
|
+
// `flows/write`, the same capability renaming and moving need: archiving is organization, not a
|
|
710
|
+
// change to what the flow does.
|
|
711
|
+
app.post("/flows/:flowId/archive", async (context) => {
|
|
712
|
+
const auth = requireCapability(context, "flows", "write");
|
|
713
|
+
const input = ArchiveFlowInput.parse(await context.req.json().catch(() => null));
|
|
714
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
715
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
716
|
+
}
|
|
717
|
+
return context.json(await deps.flows.archive(asFlowActor(auth), input));
|
|
718
|
+
});
|
|
719
|
+
// ⚠️ DELETE, and it means it (#457) — the flow's versions and the record of every run it had.
|
|
720
|
+
app.delete("/flows/:flowId", async (context) => {
|
|
721
|
+
const auth = requireCapability(context, "flows", "write");
|
|
722
|
+
const input = PurgeFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
723
|
+
return context.json(await deps.flows.purge(asFlowActor(auth), input));
|
|
724
|
+
});
|
|
725
|
+
app.post("/flows/:flowId/versions", async (context) => {
|
|
726
|
+
const auth = requireCapability(context, "flows", "write");
|
|
727
|
+
const input = SaveFlowVersionInput.parse(await context.req.json().catch(() => null));
|
|
728
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
729
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
730
|
+
}
|
|
731
|
+
return context.json(await deps.flows.save(asFlowActor(auth), input), 201);
|
|
732
|
+
});
|
|
733
|
+
// The GET beside `POST /flows/:flowId/versions`: the same collection, read instead of appended
|
|
734
|
+
// to (#144). `flows/read` like `GET /flows/:flowId` — the history describes a flow the caller
|
|
735
|
+
// may already open.
|
|
736
|
+
app.get("/flows/:flowId/versions", async (context) => {
|
|
737
|
+
const auth = requireCapability(context, "flows", "read");
|
|
738
|
+
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
739
|
+
return context.json(await deps.flows.listVersions(asFlowActor(auth), input.flowId));
|
|
740
|
+
});
|
|
741
|
+
app.get("/flows/:flowId/versions/:versionId", async (context) => {
|
|
742
|
+
const auth = requireCapability(context, "flows", "read");
|
|
743
|
+
const input = GetFlowVersionInput.parse({
|
|
744
|
+
flowId: context.req.param("flowId"),
|
|
745
|
+
versionId: context.req.param("versionId"),
|
|
746
|
+
});
|
|
747
|
+
return context.json(await deps.flows.getVersion(asFlowActor(auth), input));
|
|
748
|
+
});
|
|
749
|
+
// What publishing would freeze, before it is published (ADR-0004 §5). A GET because it reads and
|
|
750
|
+
// changes nothing; the `publish` capability because it answers a question only a publisher has.
|
|
751
|
+
app.get("/flows/:flowId/versions/:versionId/publish-preview", async (context) => {
|
|
752
|
+
const auth = requireCapability(context, "flows", "publish");
|
|
753
|
+
const input = PreviewFlowPublishInput.parse({
|
|
754
|
+
flowId: context.req.param("flowId"),
|
|
755
|
+
versionId: context.req.param("versionId"),
|
|
756
|
+
});
|
|
757
|
+
return context.json(await deps.flows.previewPublish(asFlowActor(auth), input));
|
|
758
|
+
});
|
|
759
|
+
app.post("/flows/:flowId/publish", async (context) => {
|
|
760
|
+
const auth = requireCapability(context, "flows", "publish");
|
|
761
|
+
const input = PublishFlowInput.parse(await context.req.json().catch(() => null));
|
|
762
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
763
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
764
|
+
}
|
|
765
|
+
return context.json(await deps.flows.publish(asFlowActor(auth), input));
|
|
766
|
+
});
|
|
767
|
+
// The reverse of publish, behind the same capability: whoever may publish may withdraw (#146).
|
|
768
|
+
app.post("/flows/:flowId/unpublish", async (context) => {
|
|
769
|
+
const auth = requireCapability(context, "flows", "publish");
|
|
770
|
+
const input = UnpublishFlowInput.parse(await context.req.json().catch(() => null));
|
|
771
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
772
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
773
|
+
}
|
|
774
|
+
return context.json(await deps.flows.unpublish(asFlowActor(auth), input));
|
|
775
|
+
});
|
|
776
|
+
app.post("/flows/:flowId/runs", async (context) => {
|
|
777
|
+
const auth = requireCapability(context, "flows", "run");
|
|
778
|
+
const input = StartFlowRunInput.parse(await context.req.json().catch(() => null));
|
|
779
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
780
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
781
|
+
}
|
|
782
|
+
return context.json(await deps.flows.start(asFlowActor(auth), input), 201);
|
|
783
|
+
});
|
|
784
|
+
// What this flow has done (#35). A GET beside `POST /flows/:flowId/runs`: the same collection,
|
|
785
|
+
// read instead of appended to. `flows/read` is the capability, and which of the runs actually
|
|
786
|
+
// appear is the resource rule the service applies per row.
|
|
787
|
+
app.get("/flows/:flowId/runs", async (context) => {
|
|
788
|
+
const auth = requireCapability(context, "flows", "read");
|
|
789
|
+
const url = new URL(context.req.url);
|
|
790
|
+
requireKnownQuery(url, ["failedOnly", "limit", "cursor"]);
|
|
791
|
+
const input = ListFlowRunsInput.parse({
|
|
792
|
+
flowId: context.req.param("flowId"),
|
|
793
|
+
// Present means "only the failed ones". A query string carries no booleans, so the parameter's
|
|
794
|
+
// presence is the flag rather than a string that would have to be spelled exactly right.
|
|
795
|
+
...(url.searchParams.has("failedOnly") ? { failedOnly: true } : {}),
|
|
796
|
+
...(url.searchParams.has("limit") ? { limit: Number(url.searchParams.get("limit")) } : {}),
|
|
797
|
+
...(url.searchParams.has("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
|
|
798
|
+
});
|
|
799
|
+
return context.json(await deps.flows.listRuns(asFlowActor(auth), input));
|
|
800
|
+
});
|
|
801
|
+
/**
|
|
802
|
+
* The same list across every flow (#774) — "did anything run", which used to take one call per
|
|
803
|
+
* flow and therefore went unasked.
|
|
804
|
+
*
|
|
805
|
+
* ⚠️ It sits at the root beside the other three run routes rather than under a flow, and the
|
|
806
|
+
* reason is the one already written for them in `packages/intel/CLAUDE.md`: reading, cancelling and
|
|
807
|
+
* completing a run do not need the flow, and a run outlives it. Naming no flow is exactly that
|
|
808
|
+
* case. One tool answers both addresses — `flow_run_list` with `flowId: null` — which is the same
|
|
809
|
+
* many-to-one the attachment routes have.
|
|
810
|
+
*/
|
|
811
|
+
app.get("/flow-runs", async (context) => {
|
|
812
|
+
const auth = requireCapability(context, "flows", "read");
|
|
813
|
+
const url = new URL(context.req.url);
|
|
814
|
+
requireKnownQuery(url, ["failedOnly", "limit", "cursor"]);
|
|
815
|
+
const input = ListFlowRunsInput.parse({
|
|
816
|
+
flowId: null,
|
|
817
|
+
...(url.searchParams.has("failedOnly") ? { failedOnly: true } : {}),
|
|
818
|
+
...(url.searchParams.has("limit") ? { limit: Number(url.searchParams.get("limit")) } : {}),
|
|
819
|
+
...(url.searchParams.has("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
|
|
820
|
+
});
|
|
821
|
+
return context.json(await deps.flows.listRuns(asFlowActor(auth), input));
|
|
822
|
+
});
|
|
823
|
+
app.get("/flow-runs/:runId", async (context) => {
|
|
824
|
+
const auth = requireCapability(context, "flows", "read");
|
|
825
|
+
const input = GetFlowRunInput.parse({ runId: context.req.param("runId") });
|
|
826
|
+
return context.json(await deps.flows.getRun(asFlowActor(auth), input.runId));
|
|
827
|
+
});
|
|
828
|
+
// The drill-down behind the list: which step of this run ended it, and what it said.
|
|
829
|
+
app.get("/flow-runs/:runId/steps", async (context) => {
|
|
830
|
+
const auth = requireCapability(context, "flows", "read");
|
|
831
|
+
const input = GetFlowRunInput.parse({ runId: context.req.param("runId") });
|
|
832
|
+
return context.json(await deps.flows.listRunSteps(asFlowActor(auth), input.runId));
|
|
833
|
+
});
|
|
834
|
+
app.post("/flow-runs/:runId/steps/complete", async (context) => {
|
|
835
|
+
const auth = requireCapability(context, "flows", "run");
|
|
836
|
+
const input = CompleteFlowRunStepInput.parse(await context.req.json().catch(() => null));
|
|
837
|
+
if (input.runId !== context.req.param("runId")) {
|
|
838
|
+
throw new IntelError(400, "run_id_mismatch", "Path and body run IDs differ");
|
|
839
|
+
}
|
|
840
|
+
return context.json(await deps.flows.completeStep(asFlowActor(auth), input));
|
|
841
|
+
});
|
|
842
|
+
// `flows/run`, the same capability the start asked for: whoever may start a run may end it
|
|
843
|
+
// (#145). The resource-level `execute` is the service's question, asked against the flow.
|
|
844
|
+
app.post("/flow-runs/:runId/cancel", async (context) => {
|
|
845
|
+
const auth = requireCapability(context, "flows", "run");
|
|
846
|
+
const input = CancelFlowRunInput.parse(await context.req.json().catch(() => null));
|
|
847
|
+
if (input.runId !== context.req.param("runId")) {
|
|
848
|
+
throw new IntelError(400, "run_id_mismatch", "Path and body run IDs differ");
|
|
849
|
+
}
|
|
850
|
+
return context.json(await deps.flows.cancelRun(asFlowActor(auth), input));
|
|
851
|
+
});
|
|
852
|
+
app.get("/tools", async (context) => {
|
|
853
|
+
const auth = requireCapability(context, "tools", "read");
|
|
854
|
+
return context.json(await deps.tools.catalog(asToolActor(auth)));
|
|
855
|
+
});
|
|
856
|
+
// ⚠️ Registered before `/tools/test` and `/tools/execute` and behind `tools/read`, not a new
|
|
857
|
+
// capability: it answers strictly less than `/tools` does — the servers behind the same live
|
|
858
|
+
// list, with no schemas (D30).
|
|
859
|
+
app.get("/tools/servers", async (context) => {
|
|
860
|
+
const auth = requireCapability(context, "tools", "read");
|
|
861
|
+
return context.json(await deps.tools.servers(asToolActor(auth)));
|
|
862
|
+
});
|
|
863
|
+
app.post("/tools/test", async (context) => {
|
|
864
|
+
const auth = requireCapability(context, "tools", "test");
|
|
865
|
+
const input = TestToolInput.parse(await context.req.json().catch(() => null));
|
|
866
|
+
return context.json(await deps.tools.test(asToolActor(auth), input));
|
|
867
|
+
});
|
|
868
|
+
app.post("/tools/execute", async (context) => {
|
|
869
|
+
const auth = requireCapability(context, "tools", "execute");
|
|
870
|
+
const input = ExecuteToolInput.parse(await context.req.json().catch(() => null));
|
|
871
|
+
return context.json(await deps.tools.execute(asToolActor(auth), input));
|
|
872
|
+
});
|
|
873
|
+
return app;
|
|
874
|
+
}
|