@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,129 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
|
|
4
|
+
import { ToolTestResult } from "../../contract/tool.js";
|
|
5
|
+
function responseWithLimit(response, maxBytes) {
|
|
6
|
+
const length = Number(response.headers.get("content-length"));
|
|
7
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
8
|
+
void response.body?.cancel();
|
|
9
|
+
throw new Error(`Remote MCP response exceeded ${maxBytes} bytes`);
|
|
10
|
+
}
|
|
11
|
+
if (!response.body)
|
|
12
|
+
return response;
|
|
13
|
+
let received = 0;
|
|
14
|
+
const bounded = response.body.pipeThrough(new TransformStream({
|
|
15
|
+
transform(chunk, controller) {
|
|
16
|
+
received += chunk.byteLength;
|
|
17
|
+
if (received > maxBytes) {
|
|
18
|
+
controller.error(new Error(`Remote MCP response exceeded ${maxBytes} bytes`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
controller.enqueue(chunk);
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
return new Response(bounded, {
|
|
25
|
+
status: response.status,
|
|
26
|
+
statusText: response.statusText,
|
|
27
|
+
headers: response.headers,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function createRemoteTools(deps) {
|
|
31
|
+
async function connected(url, accessToken, signal, maxBytes, work) {
|
|
32
|
+
/**
|
|
33
|
+
* ⚠️ The SDK's DEFAULT validator is Ajv, and Ajv cannot run in a Worker.
|
|
34
|
+
*
|
|
35
|
+
* `Client.cacheToolMetadata` compiles a validator for every tool that carries an `outputSchema`,
|
|
36
|
+
* Ajv builds that with `new Function`, and `workerd` answers `EvalError: Code generation from
|
|
37
|
+
* strings disallowed for this context`. The throw takes the WHOLE `tools/list` with it, so a
|
|
38
|
+
* single such tool empties the catalog of every other server (#308).
|
|
39
|
+
*
|
|
40
|
+
* Nothing announces this. Tools without an `outputSchema` never reach the line, so a portal can
|
|
41
|
+
* work for months and break the moment somebody adds a server whose tools declare one — which
|
|
42
|
+
* is exactly how it was found: three servers fine, the fourth took the screen down. And no test
|
|
43
|
+
* can catch it either: `@cloudflare/vitest-pool-workers` replaces `globalThis.Function` with a
|
|
44
|
+
* proxy onto an unsafe-eval binding, so code generation WORKS in every test and fails in every
|
|
45
|
+
* deployment.
|
|
46
|
+
*
|
|
47
|
+
* This is #229 one layer down — there Ajv sat in Intel's own schema adapter, here it sits in a
|
|
48
|
+
* dependency. The SDK offers the way out itself: `CfWorkerJsonSchemaValidator` interprets the
|
|
49
|
+
* schema instead of compiling it, and it is the same `@cfworker/json-schema` #229 settled on.
|
|
50
|
+
*/
|
|
51
|
+
const client = new Client({ name: "intel-tools", version: "0.1.0" }, { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() });
|
|
52
|
+
const transport = new StreamableHTTPClientTransport(new URL(url), {
|
|
53
|
+
fetch: async (input, init) => responseWithLimit(await deps.fetch(input, {
|
|
54
|
+
...init,
|
|
55
|
+
signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal,
|
|
56
|
+
}), maxBytes),
|
|
57
|
+
requestInit: { headers: { authorization: `Bearer ${accessToken}` }, signal },
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
// The SDK's transport and client currently disagree under exactOptionalPropertyTypes.
|
|
61
|
+
await client.connect(transport);
|
|
62
|
+
return await work(client);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await client.close().catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
async list(url, accessToken, signal) {
|
|
70
|
+
return await connected(url, accessToken, signal, 5_000_000, async (client) => {
|
|
71
|
+
const tools = [];
|
|
72
|
+
let cursor;
|
|
73
|
+
do {
|
|
74
|
+
const page = await client.listTools(cursor ? { cursor } : undefined);
|
|
75
|
+
for (const tool of page.tools) {
|
|
76
|
+
tools.push({
|
|
77
|
+
name: tool.name,
|
|
78
|
+
title: tool.title ?? tool.annotations?.title ?? null,
|
|
79
|
+
description: tool.description ?? null,
|
|
80
|
+
inputSchema: tool.inputSchema,
|
|
81
|
+
outputSchema: tool.outputSchema ?? null,
|
|
82
|
+
annotations: {
|
|
83
|
+
...(tool.annotations?.title ? { title: tool.annotations.title } : {}),
|
|
84
|
+
...(tool.annotations?.readOnlyHint !== undefined
|
|
85
|
+
? { readOnlyHint: tool.annotations.readOnlyHint }
|
|
86
|
+
: {}),
|
|
87
|
+
...(tool.annotations?.destructiveHint !== undefined
|
|
88
|
+
? { destructiveHint: tool.annotations.destructiveHint }
|
|
89
|
+
: {}),
|
|
90
|
+
...(tool.annotations?.idempotentHint !== undefined
|
|
91
|
+
? { idempotentHint: tool.annotations.idempotentHint }
|
|
92
|
+
: {}),
|
|
93
|
+
...(tool.annotations?.openWorldHint !== undefined
|
|
94
|
+
? { openWorldHint: tool.annotations.openWorldHint }
|
|
95
|
+
: {}),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
if (tools.length > 1_000)
|
|
99
|
+
return tools;
|
|
100
|
+
}
|
|
101
|
+
cursor = page.nextCursor;
|
|
102
|
+
} while (cursor);
|
|
103
|
+
return tools;
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
async call(input) {
|
|
107
|
+
return await connected(input.url, input.accessToken, input.signal, 1_100_000, async (client) => {
|
|
108
|
+
const result = await client.callTool({
|
|
109
|
+
name: input.name,
|
|
110
|
+
arguments: input.arguments,
|
|
111
|
+
});
|
|
112
|
+
if (!("content" in result)) {
|
|
113
|
+
return ToolTestResult.parse({
|
|
114
|
+
isError: false,
|
|
115
|
+
content: [],
|
|
116
|
+
structuredContent: result.toolResult,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return ToolTestResult.parse({
|
|
120
|
+
isError: result.isError ?? false,
|
|
121
|
+
content: result.content,
|
|
122
|
+
...(result.structuredContent !== undefined
|
|
123
|
+
? { structuredContent: result.structuredContent }
|
|
124
|
+
: {}),
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const defaultModel = "@cf/baai/bge-m3";
|
|
2
|
+
const maxEmbeddingCharacters = 24_000;
|
|
3
|
+
// How much of one embedding request this adapter is willing to be: at most this many texts, and at
|
|
4
|
+
// most this many characters across them. Both bounds are ours rather than a number Cloudflare
|
|
5
|
+
// publishes — what the model documents is the shape of `text`, not a batch size — so they are set
|
|
6
|
+
// well inside anything plausible. The point is that a board with three hundred cards costs a
|
|
7
|
+
// handful of requests on its first pass instead of three hundred round trips.
|
|
8
|
+
const maxBatchTexts = 25;
|
|
9
|
+
const maxBatchCharacters = 96_000;
|
|
10
|
+
// Vectorize takes at most 1000 vectors in one call from a Worker (platform limits). A board may
|
|
11
|
+
// hold 5000 tasks, so the write is cut rather than sent whole and refused.
|
|
12
|
+
const maxVectorsPerCall = 1000;
|
|
13
|
+
/**
|
|
14
|
+
* How a chunk is named in the vector index (anchrd/intel#301).
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ `""` keeps the bare node id, which is what every vector written before this was called. That
|
|
17
|
+
* is the whole reason the separator sits on the SUFFIX side: nothing an installation already holds
|
|
18
|
+
* changes its name, so this ships without re-embedding a tree.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ Reading it back splits at the FIRST separator, and that direction matters. A node id is minted
|
|
21
|
+
* here (`ulid`) and cannot contain a `#`; a task id can, because a bundle import carries the task
|
|
22
|
+
* ids written in the file. Splitting at the first `#` therefore always recovers the node id exactly
|
|
23
|
+
* and leaves the rest to the key — and even a key that never matched a card costs a hit that
|
|
24
|
+
* hydrates to nothing, never a citation for a node the searcher did not match.
|
|
25
|
+
*/
|
|
26
|
+
const chunkSeparator = "#";
|
|
27
|
+
function vectorId(nodeId, key) {
|
|
28
|
+
return key === "" ? nodeId : `${nodeId}${chunkSeparator}${key}`;
|
|
29
|
+
}
|
|
30
|
+
function embeddings(result, expected) {
|
|
31
|
+
if (typeof result !== "object" ||
|
|
32
|
+
result === null ||
|
|
33
|
+
!("data" in result) ||
|
|
34
|
+
!Array.isArray(result.data) ||
|
|
35
|
+
// ⚠️ The count is part of the check. The rows come back in the order the texts went out and
|
|
36
|
+
// nothing else identifies them, so a short answer would silently pair every embedding after the
|
|
37
|
+
// gap with the wrong card — a search that answers confidently with the neighbouring task.
|
|
38
|
+
result.data.length !== expected ||
|
|
39
|
+
// ⚠️ And every row has to hold something. An empty one passes "is an array of numbers" and would
|
|
40
|
+
// be upserted under a real card's name as a vector that matches nothing — indexed, recorded as
|
|
41
|
+
// fingerprinted, and never looked at again until somebody edits that card.
|
|
42
|
+
!result.data.every((row) => Array.isArray(row) && row.length > 0 && row.every((value) => typeof value === "number"))) {
|
|
43
|
+
throw new Error("The embedding provider returned an invalid response");
|
|
44
|
+
}
|
|
45
|
+
return result.data;
|
|
46
|
+
}
|
|
47
|
+
function matches(result) {
|
|
48
|
+
if (typeof result !== "object" ||
|
|
49
|
+
result === null ||
|
|
50
|
+
!("matches" in result) ||
|
|
51
|
+
!Array.isArray(result.matches)) {
|
|
52
|
+
throw new Error("The semantic index returned an invalid response");
|
|
53
|
+
}
|
|
54
|
+
return result.matches.flatMap((match) => {
|
|
55
|
+
if (typeof match !== "object" ||
|
|
56
|
+
match === null ||
|
|
57
|
+
!("id" in match) ||
|
|
58
|
+
typeof match.id !== "string" ||
|
|
59
|
+
!("score" in match) ||
|
|
60
|
+
typeof match.score !== "number") {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const separator = match.id.indexOf(chunkSeparator);
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
nodeId: separator < 0 ? match.id : match.id.slice(0, separator),
|
|
67
|
+
chunkKey: separator < 0 ? "" : match.id.slice(separator + 1),
|
|
68
|
+
score: Math.max(0, Math.min(1, match.score)),
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function batched(chunks) {
|
|
74
|
+
const batches = [];
|
|
75
|
+
let current = [];
|
|
76
|
+
let characters = 0;
|
|
77
|
+
for (const chunk of chunks) {
|
|
78
|
+
if (current.length > 0 &&
|
|
79
|
+
(current.length >= maxBatchTexts || characters >= maxBatchCharacters)) {
|
|
80
|
+
batches.push(current);
|
|
81
|
+
current = [];
|
|
82
|
+
characters = 0;
|
|
83
|
+
}
|
|
84
|
+
current.push(chunk);
|
|
85
|
+
characters += chunk.text.length;
|
|
86
|
+
}
|
|
87
|
+
if (current.length > 0)
|
|
88
|
+
batches.push(current);
|
|
89
|
+
return batches;
|
|
90
|
+
}
|
|
91
|
+
export function createSemanticIndex(deps) {
|
|
92
|
+
const model = deps.model ?? defaultModel;
|
|
93
|
+
// Every embedding request carries it, both the write side and the search side (anchrd/intel#800).
|
|
94
|
+
const gateway = { id: deps.gateway };
|
|
95
|
+
async function embed(texts) {
|
|
96
|
+
return embeddings(await deps.ai.run(model, { text: texts }, { gateway }), texts.length);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
async upsert(target, chunks) {
|
|
100
|
+
const vectors = [];
|
|
101
|
+
for (const batch of batched(chunks)) {
|
|
102
|
+
const values = await embed(batch.map((chunk) => chunk.text.slice(0, maxEmbeddingCharacters)));
|
|
103
|
+
batch.forEach((chunk, index) => {
|
|
104
|
+
const embedded = values[index];
|
|
105
|
+
// The index is what makes this reachable at all — `embeddings` has already refused a short
|
|
106
|
+
// answer and an empty row, so this is the type system's question rather than the
|
|
107
|
+
// provider's. It throws anyway rather than skipping: the alternative to a missing card is
|
|
108
|
+
// never a shorter list, it is a card recorded as fingerprinted with no vector behind it.
|
|
109
|
+
if (!embedded)
|
|
110
|
+
throw new Error("The embedding provider returned an invalid response");
|
|
111
|
+
vectors.push({
|
|
112
|
+
id: vectorId(target.nodeId, chunk.key),
|
|
113
|
+
values: embedded,
|
|
114
|
+
metadata: { versionId: target.versionId },
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (let offset = 0; offset < vectors.length; offset += maxVectorsPerCall) {
|
|
119
|
+
await deps.index.upsert(vectors.slice(offset, offset + maxVectorsPerCall));
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
async remove(nodeId, keys) {
|
|
123
|
+
const ids = keys.map((key) => vectorId(nodeId, key));
|
|
124
|
+
for (let offset = 0; offset < ids.length; offset += maxVectorsPerCall) {
|
|
125
|
+
await deps.index.deleteByIds(ids.slice(offset, offset + maxVectorsPerCall));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async search(query, limit) {
|
|
129
|
+
const [vector] = await embed([query]);
|
|
130
|
+
if (!vector)
|
|
131
|
+
throw new Error("The embedding provider returned an invalid response");
|
|
132
|
+
const result = await deps.index.query(vector, {
|
|
133
|
+
topK: Math.max(1, Math.min(100, limit)),
|
|
134
|
+
returnMetadata: "none",
|
|
135
|
+
});
|
|
136
|
+
return matches(result);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { NodeIndexTarget } from "../../nodes/nodes.types.js";
|
|
2
|
+
export interface WorkersAiBinding {
|
|
3
|
+
run(model: string, input: {
|
|
4
|
+
text: string[];
|
|
5
|
+
}, options: {
|
|
6
|
+
gateway: {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
9
|
+
}): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface VectorizeBinding {
|
|
12
|
+
upsert(vectors: Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
values: number[];
|
|
15
|
+
metadata: {
|
|
16
|
+
versionId: string;
|
|
17
|
+
};
|
|
18
|
+
}>): Promise<unknown>;
|
|
19
|
+
deleteByIds(ids: string[]): Promise<unknown>;
|
|
20
|
+
query(vector: number[], options: {
|
|
21
|
+
topK: number;
|
|
22
|
+
returnMetadata: "none";
|
|
23
|
+
}): Promise<unknown>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One vector's worth of a node (anchrd/intel#301).
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ `key` is `""` for a node that carries exactly one vector — every kind but `board` — and the
|
|
29
|
+
* vector is then named by the bare node id it has always been named by. A board card carries its
|
|
30
|
+
* task id, and its vector is `<node id>#<task id>`.
|
|
31
|
+
*
|
|
32
|
+
* `text` is embedded verbatim. Composing it — the node's title in front of a document's body, the
|
|
33
|
+
* card's own title in front of a card — belongs to the indexing pass rather than here, because the
|
|
34
|
+
* same string has to be fingerprinted there to decide whether this vector needs making at all. A
|
|
35
|
+
* title prefixed on this side would sit outside that fingerprint and change nothing when it changed.
|
|
36
|
+
*/
|
|
37
|
+
export interface SemanticChunk {
|
|
38
|
+
key: string;
|
|
39
|
+
text: string;
|
|
40
|
+
}
|
|
41
|
+
export interface SemanticHit {
|
|
42
|
+
nodeId: string;
|
|
43
|
+
chunkKey: string;
|
|
44
|
+
score: number;
|
|
45
|
+
}
|
|
46
|
+
export interface SemanticIndex {
|
|
47
|
+
upsert(target: NodeIndexTarget, chunks: SemanticChunk[]): Promise<void>;
|
|
48
|
+
remove(nodeId: string, keys: string[]): Promise<void>;
|
|
49
|
+
search(query: string, limit: number): Promise<SemanticHit[]>;
|
|
50
|
+
}
|
|
51
|
+
export interface SemanticIndexDeps {
|
|
52
|
+
ai: WorkersAiBinding;
|
|
53
|
+
index: VectorizeBinding;
|
|
54
|
+
gateway: string;
|
|
55
|
+
model?: string;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SafeReturnPath } from "../../shared/safe-return-path/safe-return-path.js";
|
|
3
|
+
const PendingSession = z.strictObject({
|
|
4
|
+
version: z.literal(1),
|
|
5
|
+
kind: z.literal("pending"),
|
|
6
|
+
clientId: z.string().min(1),
|
|
7
|
+
codeVerifier: z.string().min(43),
|
|
8
|
+
state: z.string().min(16),
|
|
9
|
+
returnTo: SafeReturnPath,
|
|
10
|
+
expiresAt: z.number().int().positive(),
|
|
11
|
+
});
|
|
12
|
+
const UserSession = z.strictObject({
|
|
13
|
+
version: z.literal(1),
|
|
14
|
+
kind: z.literal("user"),
|
|
15
|
+
clientId: z.string().min(1),
|
|
16
|
+
accessToken: z.string().min(1),
|
|
17
|
+
expiresAt: z.number().int().positive(),
|
|
18
|
+
});
|
|
19
|
+
const ConnectionPendingSession = z.strictObject({
|
|
20
|
+
version: z.literal(1),
|
|
21
|
+
kind: z.literal("connection-pending"),
|
|
22
|
+
clientId: z.string().min(1),
|
|
23
|
+
codeVerifier: z.string().min(43),
|
|
24
|
+
state: z.string().min(16),
|
|
25
|
+
returnTo: SafeReturnPath,
|
|
26
|
+
issuer: z.url(),
|
|
27
|
+
resource: z.url(),
|
|
28
|
+
userId: z.string().min(1),
|
|
29
|
+
user: UserSession,
|
|
30
|
+
// Older cookies carry no `silent`, and one in flight across a deploy must not become an invalid
|
|
31
|
+
// session: absent means the visible flow, which is what those attempts were.
|
|
32
|
+
silent: z.boolean().default(false),
|
|
33
|
+
// Same for `resumed`: absent means a first attempt, so a handoff written before the deploy still
|
|
34
|
+
// gets its one restart rather than dying at the parser.
|
|
35
|
+
resumed: z.boolean().default(false),
|
|
36
|
+
expiresAt: z.number().int().positive(),
|
|
37
|
+
});
|
|
38
|
+
const Session = z.discriminatedUnion("kind", [
|
|
39
|
+
PendingSession,
|
|
40
|
+
ConnectionPendingSession,
|
|
41
|
+
UserSession,
|
|
42
|
+
]);
|
|
43
|
+
const additionalData = new TextEncoder().encode("anchrd-intel-session-v1");
|
|
44
|
+
function encode(bytes) {
|
|
45
|
+
let value = "";
|
|
46
|
+
for (const byte of bytes)
|
|
47
|
+
value += String.fromCharCode(byte);
|
|
48
|
+
return btoa(value).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
49
|
+
}
|
|
50
|
+
function decode(value) {
|
|
51
|
+
const padded = value
|
|
52
|
+
.replaceAll("-", "+")
|
|
53
|
+
.replaceAll("_", "/")
|
|
54
|
+
.padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
55
|
+
return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
56
|
+
}
|
|
57
|
+
export function createSessionCodec(deps) {
|
|
58
|
+
if (new TextEncoder().encode(deps.secret).byteLength < 32) {
|
|
59
|
+
throw new Error("INTEL_SESSION_SECRET must contain at least 32 bytes");
|
|
60
|
+
}
|
|
61
|
+
const key = deps.crypto.subtle
|
|
62
|
+
.digest("SHA-256", new TextEncoder().encode(deps.secret))
|
|
63
|
+
.then((hash) => deps.crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]));
|
|
64
|
+
return {
|
|
65
|
+
async seal(value) {
|
|
66
|
+
const session = Session.parse(value);
|
|
67
|
+
const iv = deps.crypto.getRandomValues(new Uint8Array(12));
|
|
68
|
+
const encrypted = new Uint8Array(await deps.crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, await key, new TextEncoder().encode(JSON.stringify(session))));
|
|
69
|
+
const output = new Uint8Array(iv.byteLength + encrypted.byteLength);
|
|
70
|
+
output.set(iv);
|
|
71
|
+
output.set(encrypted, iv.byteLength);
|
|
72
|
+
return encode(output);
|
|
73
|
+
},
|
|
74
|
+
async open(value) {
|
|
75
|
+
try {
|
|
76
|
+
const input = decode(value);
|
|
77
|
+
if (input.byteLength <= 28)
|
|
78
|
+
return null;
|
|
79
|
+
const decrypted = await deps.crypto.subtle.decrypt({ name: "AES-GCM", iv: input.slice(0, 12), additionalData }, await key, input.slice(12));
|
|
80
|
+
return Session.parse(JSON.parse(new TextDecoder().decode(decrypted)));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createToolSourcePolicy(configuredOrigins: string | undefined): (sourceUrl: string) => boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createToolSourcePolicy(configuredOrigins) {
|
|
2
|
+
const origins = new Set((configuredOrigins ?? "")
|
|
3
|
+
.split(",")
|
|
4
|
+
.map((value) => value.trim())
|
|
5
|
+
.filter(Boolean)
|
|
6
|
+
.map((value) => {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.username || url.password || (url.pathname !== "/" && url.pathname !== "")) {
|
|
9
|
+
throw new Error("TOOL_SOURCE_ORIGINS must contain exact origins without credentials or paths");
|
|
10
|
+
}
|
|
11
|
+
return url.origin;
|
|
12
|
+
}));
|
|
13
|
+
// A malformed URL is not an allowed origin, so the policy answers false instead of throwing.
|
|
14
|
+
return (sourceUrl) => {
|
|
15
|
+
try {
|
|
16
|
+
return origins.has(new URL(sourceUrl).origin);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
2
|
+
// The separator inside the encoded cursor. `\0` cannot occur in either half: an ISO timestamp
|
|
3
|
+
// is digits and punctuation, an Intel id is Crockford base32. Splitting on the FIRST occurrence
|
|
4
|
+
// would still be safe, but there is no occurrence to split on.
|
|
5
|
+
const SEPARATOR = "\0";
|
|
6
|
+
/**
|
|
7
|
+
* The pair, made into one string a reader cannot take apart by accident.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ This is not encryption and does not pretend to be. Anyone determined can decode base64url and
|
|
10
|
+
* read a timestamp. The point is the accident, not the attacker: a cursor that LOOKS like a
|
|
11
|
+
* timestamp invites a caller to send a timestamp, and a timestamp alone loses every second event
|
|
12
|
+
* written in the same millisecond. Making the shape unusable by hand is what keeps the pair whole.
|
|
13
|
+
*/
|
|
14
|
+
function encodeCursor(position) {
|
|
15
|
+
const raw = `${position.occurredAt}${SEPARATOR}${position.id}`;
|
|
16
|
+
return btoa(String.fromCharCode(...new TextEncoder().encode(raw)))
|
|
17
|
+
.replaceAll("+", "-")
|
|
18
|
+
.replaceAll("/", "_")
|
|
19
|
+
.replaceAll("=", "");
|
|
20
|
+
}
|
|
21
|
+
function decodeCursor(cursor) {
|
|
22
|
+
// A cursor comes back from a consumer that stored it, possibly weeks ago, possibly truncated by
|
|
23
|
+
// whatever stored it. Every failure below is the same answer: this is not a cursor this service
|
|
24
|
+
// handed out. Guessing a position from a damaged one would silently move the reader — forward
|
|
25
|
+
// past unread events, or backward into duplicates.
|
|
26
|
+
const refuse = () => new IntelError(400, "invalid_cursor", "The cursor is not one this service issued. Pass back the nextCursor from a previous response unchanged, or omit it to start from the beginning.");
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
const padded = cursor.replaceAll("-", "+").replaceAll("_", "/");
|
|
30
|
+
const binary = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
|
|
31
|
+
raw = new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw refuse();
|
|
35
|
+
}
|
|
36
|
+
const separator = raw.indexOf(SEPARATOR);
|
|
37
|
+
if (separator <= 0 || separator === raw.length - 1)
|
|
38
|
+
throw refuse();
|
|
39
|
+
const occurredAt = raw.slice(0, separator);
|
|
40
|
+
const id = raw.slice(separator + 1);
|
|
41
|
+
// ⚠️ Both halves are checked, not just the split. A cursor whose timestamp decoded to an empty
|
|
42
|
+
// string would compare as "before everything" and quietly replay the whole journal.
|
|
43
|
+
if (occurredAt.length === 0 || id.length === 0)
|
|
44
|
+
throw refuse();
|
|
45
|
+
return { occurredAt, id };
|
|
46
|
+
}
|
|
47
|
+
export function createAudit(deps) {
|
|
48
|
+
return {
|
|
49
|
+
async list(actor, input) {
|
|
50
|
+
const after = input.after === undefined ? null : decodeCursor(input.after);
|
|
51
|
+
// ⚠️ The refusal that stood here until #774 is GONE, and it was doing real work: it named
|
|
52
|
+
// `node` explicitly so that widening the contract enum could not silently route flow events
|
|
53
|
+
// through the node visibility walk. What replaces it is not trust but a repository that takes
|
|
54
|
+
// the kind as an argument and picks a statement per kind — each with its own walk, none able
|
|
55
|
+
// to answer for another. Adding a fourth kind to the enum is now a type error there rather
|
|
56
|
+
// than a wrong answer here, which is the stronger version of the same guard.
|
|
57
|
+
//
|
|
58
|
+
// One more row than asked for, so "is there another page" is answered by the same authorized
|
|
59
|
+
// query instead of a second one that could disagree with it.
|
|
60
|
+
const page = await deps.audit.listEvents(actor, {
|
|
61
|
+
after,
|
|
62
|
+
resourceType: input.resourceType,
|
|
63
|
+
limit: input.limit + 1,
|
|
64
|
+
});
|
|
65
|
+
const hasMore = page.events.length > input.limit;
|
|
66
|
+
const events = hasMore ? page.events.slice(0, input.limit) : page.events;
|
|
67
|
+
const last = events.at(-1);
|
|
68
|
+
return {
|
|
69
|
+
events,
|
|
70
|
+
/**
|
|
71
|
+
* ⚠️ The cursor is the LAST DELIVERED row, never the probe row that was dropped. Taking it
|
|
72
|
+
* from the probe would skip exactly one event per page — the classic off-by-one that shows
|
|
73
|
+
* up as a lead nobody contacted, not as an error.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ **And it is returned whenever rows were delivered, not only when more follows.** It
|
|
76
|
+
* used to carry `hasMore &&`, which meant a reader catching up received fifty events and no
|
|
77
|
+
* position: it started over on its next run and would have delivered the same fifty for
|
|
78
|
+
* ever. A reader paging through never sees it, because it always asks for a full page —
|
|
79
|
+
* which is why this survived until `anchrd/signals` read the journal for real (`#793`).
|
|
80
|
+
*/
|
|
81
|
+
nextCursor: last === undefined ? null : encodeCursor(last),
|
|
82
|
+
// Whether another page may follow. Its own field, because it is its own question.
|
|
83
|
+
hasMore,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AuditEvent, AuditListRequest, AuditListResponse, AuditResourceType } from "../contract/audit.js";
|
|
2
|
+
import type { Actor } from "../nodes/nodes.types.js";
|
|
3
|
+
export interface AuditPosition {
|
|
4
|
+
occurredAt: string;
|
|
5
|
+
id: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AuditQuery {
|
|
8
|
+
after: AuditPosition | null;
|
|
9
|
+
resourceType: AuditResourceType;
|
|
10
|
+
limit: number;
|
|
11
|
+
}
|
|
12
|
+
export interface AuditPage {
|
|
13
|
+
events: AuditEvent[];
|
|
14
|
+
}
|
|
15
|
+
export interface AuditRepository {
|
|
16
|
+
listEvents(actor: Actor, query: AuditQuery): Promise<AuditPage>;
|
|
17
|
+
}
|
|
18
|
+
export interface AuditDeps {
|
|
19
|
+
audit: AuditRepository;
|
|
20
|
+
}
|
|
21
|
+
export interface AuditService {
|
|
22
|
+
list(actor: Actor, input: AuditListRequest): Promise<AuditListResponse>;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|