@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,16 @@
|
|
|
1
|
+
export interface D1Result<T = unknown> {
|
|
2
|
+
results?: T[];
|
|
3
|
+
meta?: {
|
|
4
|
+
changes?: number;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export interface D1Statement {
|
|
8
|
+
bind(...values: unknown[]): D1Statement;
|
|
9
|
+
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
10
|
+
all<T = Record<string, unknown>>(): Promise<D1Result<T>>;
|
|
11
|
+
run<T = Record<string, unknown>>(): Promise<D1Result<T>>;
|
|
12
|
+
}
|
|
13
|
+
export interface D1Database {
|
|
14
|
+
prepare(query: string): D1Statement;
|
|
15
|
+
batch<T = unknown>(statements: D1Statement[]): Promise<D1Result<T>[]>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function converted(result) {
|
|
2
|
+
const item = Array.isArray(result) ? result[0] : result;
|
|
3
|
+
if (typeof item !== "object" ||
|
|
4
|
+
item === null ||
|
|
5
|
+
!("format" in item) ||
|
|
6
|
+
(item.format !== "markdown" && item.format !== "text") ||
|
|
7
|
+
!("data" in item) ||
|
|
8
|
+
typeof item.data !== "string") {
|
|
9
|
+
const detail = typeof item === "object" && item !== null && "error" in item && typeof item.error === "string"
|
|
10
|
+
? item.error
|
|
11
|
+
: "Markdown conversion returned an invalid response";
|
|
12
|
+
throw new Error(detail);
|
|
13
|
+
}
|
|
14
|
+
return item.data;
|
|
15
|
+
}
|
|
16
|
+
export function createDocumentConverter(deps) {
|
|
17
|
+
const gateway = { id: deps.gateway };
|
|
18
|
+
return {
|
|
19
|
+
async convert(name, mediaType, content) {
|
|
20
|
+
return converted(await deps.ai.toMarkdown({
|
|
21
|
+
name,
|
|
22
|
+
blob: new Blob([content], { type: mediaType }),
|
|
23
|
+
}, { gateway }));
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface MarkdownAiBinding {
|
|
2
|
+
toMarkdown(input: {
|
|
3
|
+
name: string;
|
|
4
|
+
blob: Blob;
|
|
5
|
+
}, options: {
|
|
6
|
+
gateway: {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
9
|
+
}): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface DocumentConverterDeps {
|
|
12
|
+
ai: MarkdownAiBinding;
|
|
13
|
+
gateway: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DocumentConverter {
|
|
16
|
+
convert(name: string, mediaType: string, content: ArrayBuffer): Promise<string>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createFlowRuntime(binding) {
|
|
2
|
+
return {
|
|
3
|
+
async start(runId) {
|
|
4
|
+
try {
|
|
5
|
+
await binding.create({ id: runId, params: { runId } });
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
try {
|
|
9
|
+
const existing = await binding.get(runId);
|
|
10
|
+
await existing.status();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
async signal(runId) {
|
|
18
|
+
const instance = await binding.get(runId);
|
|
19
|
+
await instance.sendEvent({ type: "advance", payload: { runId } });
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface WorkflowBinding {
|
|
2
|
+
create(options: {
|
|
3
|
+
id: string;
|
|
4
|
+
params: {
|
|
5
|
+
runId: string;
|
|
6
|
+
};
|
|
7
|
+
}): Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
}>;
|
|
10
|
+
get(id: string): Promise<{
|
|
11
|
+
status(): Promise<{
|
|
12
|
+
status: string;
|
|
13
|
+
}>;
|
|
14
|
+
sendEvent(event: {
|
|
15
|
+
type: string;
|
|
16
|
+
payload: unknown;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { QueueProducer } from "./index-queue.types.js";
|
|
3
|
+
export declare const IndexMessage: z.ZodObject<{
|
|
4
|
+
type: z.ZodLiteral<"node.version">;
|
|
5
|
+
versionId: z.ZodString;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
export type IndexMessage = z.infer<typeof IndexMessage>;
|
|
8
|
+
export declare function createIndexQueue(queue: QueueProducer<IndexMessage>): {
|
|
9
|
+
enqueue(versionId: string): Promise<void>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const IndexMessage = z.strictObject({
|
|
3
|
+
type: z.literal("node.version"),
|
|
4
|
+
versionId: z.string().min(1),
|
|
5
|
+
});
|
|
6
|
+
export function createIndexQueue(queue) {
|
|
7
|
+
return {
|
|
8
|
+
async enqueue(versionId) {
|
|
9
|
+
await queue.send({ type: "node.version", versionId });
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Validator } from "@cfworker/json-schema";
|
|
2
|
+
import { reportUnexpectedError } from "../../shared/report-unexpected-error/report-unexpected-error.js";
|
|
3
|
+
// ⚠️ This validator must not generate code, and Ajv must never come back. Ajv turns a schema into
|
|
4
|
+
// JavaScript with `new Function`, and workerd answers that with `EvalError: Code generation from
|
|
5
|
+
// strings disallowed for this context` — so `compile` threw for EVERY schema in production, the
|
|
6
|
+
// catch called it a broken schema, and each tool call died with 400 "The cached tool schema is
|
|
7
|
+
// invalid" while nothing was wrong with any schema (#229). Ajv is usable only with schemas
|
|
8
|
+
// precompiled at build time; these arrive from a remote catalog and are unknown until the call.
|
|
9
|
+
// `@cfworker/json-schema` interprets the schema instead, which is what it was written for.
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ The `workerd` test pool does not prove this on its own: it replaces `globalThis.Function` with
|
|
12
|
+
// a proxy onto an unsafe-eval binding, so Ajv compiled happily in every test while failing in every
|
|
13
|
+
// deployment. `json-schema.int.ts` therefore takes that patch away for the length of the test — it
|
|
14
|
+
// is the only thing that turns red when a code-generating validator returns.
|
|
15
|
+
// Tool schemas come from remote catalogs and are therefore untrusted: `ShortCircuit` stops at the
|
|
16
|
+
// first error so a crafted schema cannot amplify the work of one call, and the prepared validators
|
|
17
|
+
// are bounded because each one holds its dereferenced schema for the life of the isolate.
|
|
18
|
+
const MaxPreparedSchemas = 200;
|
|
19
|
+
const ShortCircuit = true;
|
|
20
|
+
const MaxDetailLength = 2_000;
|
|
21
|
+
// A tool schema that names its draft gets that draft: draft-07 ignores keywords sitting beside a
|
|
22
|
+
// `$ref` and the later drafts apply them, so guessing here would validate a foreign catalog against
|
|
23
|
+
// rules its author never wrote. Unnamed means 2020-12, the draft the MCP generators emit when they
|
|
24
|
+
// stay silent.
|
|
25
|
+
const DefaultDraft = "2020-12";
|
|
26
|
+
const DraftsByMetaSchema = [
|
|
27
|
+
["draft-04", "4"],
|
|
28
|
+
["draft-07", "7"],
|
|
29
|
+
["2019-09", "2019-09"],
|
|
30
|
+
["2020-12", "2020-12"],
|
|
31
|
+
];
|
|
32
|
+
function draftOf(schema) {
|
|
33
|
+
const declared = schema.$schema;
|
|
34
|
+
if (typeof declared !== "string")
|
|
35
|
+
return DefaultDraft;
|
|
36
|
+
return DraftsByMetaSchema.find(([marker]) => declared.includes(marker))?.[1] ?? DefaultDraft;
|
|
37
|
+
}
|
|
38
|
+
export function createJsonSchemaValidator() {
|
|
39
|
+
const prepared = new Map();
|
|
40
|
+
return (schema, value) => {
|
|
41
|
+
const key = JSON.stringify(schema);
|
|
42
|
+
let validator = prepared.get(key);
|
|
43
|
+
if (!validator) {
|
|
44
|
+
try {
|
|
45
|
+
validator = new Validator(schema, draftOf(schema), ShortCircuit);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// A schema this validator cannot even read is still refused by name — but the caller hears
|
|
49
|
+
// that it was the schema, not their arguments, and the reason itself goes to the log. The
|
|
50
|
+
// old wording claimed a broken schema for every failure, which is what hid #229 for weeks.
|
|
51
|
+
reportUnexpectedError(error);
|
|
52
|
+
return { valid: false, detail: "The tool schema could not be read; the reason was logged" };
|
|
53
|
+
}
|
|
54
|
+
if (prepared.size >= MaxPreparedSchemas)
|
|
55
|
+
prepared.clear();
|
|
56
|
+
prepared.set(key, validator);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const result = validator.validate(value);
|
|
60
|
+
if (result.valid)
|
|
61
|
+
return { valid: true };
|
|
62
|
+
return { valid: false, detail: describe(result) };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// An unresolvable `$ref` only shows up while walking the instance, so this is the same
|
|
66
|
+
// unreadable schema arriving one step later — and never a statement about the arguments.
|
|
67
|
+
reportUnexpectedError(error);
|
|
68
|
+
return { valid: false, detail: "The tool schema could not be read; the reason was logged" };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function describe(result) {
|
|
73
|
+
return result.errors
|
|
74
|
+
.map((unit) => `${unit.instanceLocation}: ${unit.error}`)
|
|
75
|
+
.join("; ")
|
|
76
|
+
.slice(0, MaxDetailLength);
|
|
77
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import * as client from "openid-client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ToolSourceUrl } from "../../contract/tool.js";
|
|
4
|
+
const ProtectedResource = z.looseObject({
|
|
5
|
+
resource: ToolSourceUrl,
|
|
6
|
+
authorization_servers: z.array(ToolSourceUrl).min(1),
|
|
7
|
+
});
|
|
8
|
+
function metadataCandidates(resourceUrl, challenge) {
|
|
9
|
+
const resource = new URL(resourceUrl);
|
|
10
|
+
const path = resource.pathname === "/" ? "" : resource.pathname.replace(/\/+$/, "");
|
|
11
|
+
let challengeUrl = null;
|
|
12
|
+
try {
|
|
13
|
+
challengeUrl = challenge ? new URL(challenge, resource) : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
challengeUrl = null;
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
...(challengeUrl?.origin === resource.origin ? [challengeUrl.href] : []),
|
|
20
|
+
`${resource.origin}/.well-known/oauth-protected-resource${path}`,
|
|
21
|
+
`${resource.origin}/.well-known/oauth-protected-resource`,
|
|
22
|
+
].filter((value, index, values) => values.indexOf(value) === index);
|
|
23
|
+
}
|
|
24
|
+
function resourceMetadataChallenge(header) {
|
|
25
|
+
return header?.match(/resource_metadata="([^"]+)"/i)?.[1] ?? null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* ⚠️ `issuer` is REQUIRED by RFC 8414 §2 and by OpenID Connect Discovery, and it is read here for one
|
|
29
|
+
* reason: it is the only thing that says WHO a metadata document describes. Two of the URLs below are
|
|
30
|
+
* bare-root guesses, and a bare root under a multi-tenant authorization server answers for a
|
|
31
|
+
* different tenant — a perfectly valid document about somebody else.
|
|
32
|
+
*/
|
|
33
|
+
const AuthorizationServerMetadata = z.looseObject({
|
|
34
|
+
issuer: z.string().nullish(),
|
|
35
|
+
scopes_supported: z.array(z.string()).nullish(),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Where an authorization server may publish its metadata, in the order worth asking.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ One candidate was never a decision, it was an omission — `metadataCandidates` above tries three
|
|
41
|
+
* and `withAlgorithmFallback` tries two. A server that publishes only OIDC discovery read as "names
|
|
42
|
+
* no scopes", which is #97 at a different server class and with the same silent symptom (#303).
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ The three spellings are not interchangeable, and the third is the one that is easy to miss.
|
|
45
|
+
* RFC 8414 §3.1 INSERTS the issuer's path between the well-known prefix and nothing else; OpenID
|
|
46
|
+
* Connect Discovery 1.0 APPENDS its suffix to the issuer instead. A realm-style issuer
|
|
47
|
+
* (`https://as.example/realms/x`) publishes at `…/realms/x/.well-known/openid-configuration` and at
|
|
48
|
+
* neither of the other two — which is exactly the form `client.discovery` asks for further down this
|
|
49
|
+
* file, so the same server was reachable for registration and invisible here.
|
|
50
|
+
*/
|
|
51
|
+
function authorizationServerCandidates(issuer) {
|
|
52
|
+
const url = new URL(issuer);
|
|
53
|
+
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
|
|
54
|
+
return [
|
|
55
|
+
// RFC 8414 with the path inserted. First, because it is the shape the MCP portals publish.
|
|
56
|
+
`${url.origin}/.well-known/oauth-authorization-server${path}`,
|
|
57
|
+
// The same document at the bare root, for an issuer WITH a path that publishes it there anyway.
|
|
58
|
+
`${url.origin}/.well-known/oauth-authorization-server`,
|
|
59
|
+
// OpenID Connect Discovery 1.0, appended to the issuer — the realm-style form.
|
|
60
|
+
`${url.origin}${path}/.well-known/openid-configuration`,
|
|
61
|
+
// The same document read the RFC 8414 way, and at the bare root.
|
|
62
|
+
`${url.origin}/.well-known/openid-configuration${path}`,
|
|
63
|
+
`${url.origin}/.well-known/openid-configuration`,
|
|
64
|
+
].filter((value, index, values) => values.indexOf(value) === index);
|
|
65
|
+
}
|
|
66
|
+
/** Two issuer identifiers are the same server, compared the way a URL says so and not as strings. */
|
|
67
|
+
function sameIssuer(published, wanted) {
|
|
68
|
+
if (!published)
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
return new URL(published).href.replace(/\/$/, "") === new URL(wanted).href.replace(/\/$/, "");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The scopes this authorization server says it understands, or `null` when it names none.
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ `null` is not "no scopes" — RFC 8414 makes `scopes_supported` OPTIONAL, and Cloudflare Access
|
|
81
|
+
* omits it. It means "this server never said", and the only safe reading of that is to ask for
|
|
82
|
+
* nothing: a scope the server does not know is at best ignored and at worst turns the whole
|
|
83
|
+
* authorization down, and neither failure names the scope as its cause.
|
|
84
|
+
*
|
|
85
|
+
* This replaces a hostname check (`issuer.endsWith(".cloudflareaccess.com")`) that decided the same
|
|
86
|
+
* question by guessing who the server was. A Cloudflare Access portal on its own domain — the
|
|
87
|
+
* normal case for anyone who cares what their portal is called — was read as some other vendor, and
|
|
88
|
+
* Intel then asked it for `offline_access`, a scope it never published (#97).
|
|
89
|
+
*/
|
|
90
|
+
async function publishedScopes(fetcher, issuer) {
|
|
91
|
+
for (const candidate of authorizationServerCandidates(issuer)) {
|
|
92
|
+
const response = await fetcher(candidate, {
|
|
93
|
+
signal: AbortSignal.timeout(RequestTimeoutMs),
|
|
94
|
+
headers: { accept: "application/json" },
|
|
95
|
+
}).catch(() => null);
|
|
96
|
+
if (!response?.ok)
|
|
97
|
+
continue;
|
|
98
|
+
const metadata = AuthorizationServerMetadata.safeParse(await response.json().catch(() => null));
|
|
99
|
+
if (!metadata.success)
|
|
100
|
+
continue;
|
|
101
|
+
// ⚠️ The document has to be ABOUT this issuer, and that check is what makes the bare-root
|
|
102
|
+
// candidates safe to ask at all. Under a multi-tenant server the root answers for a different
|
|
103
|
+
// tenant, and taking its `scopes_supported` would ask THIS server for a scope it never published
|
|
104
|
+
// — the very rule this function exists to keep, broken by the fallback added to protect it.
|
|
105
|
+
// The resource loop further down does the same thing for the same reason.
|
|
106
|
+
if (!sameIssuer(metadata.data.issuer, issuer))
|
|
107
|
+
continue;
|
|
108
|
+
// ⚠️ A document that answered ABOUT THIS SERVER and named no scopes ends the search. Reading on
|
|
109
|
+
// would let the next candidate answer a question this server has already answered — "I publish
|
|
110
|
+
// none" — and the documents of one issuer may disagree about a field RFC 8414 makes optional.
|
|
111
|
+
return metadata.data.scopes_supported ?? null;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Of the scopes Intel would like, the ones this server published — joined, or absent.
|
|
117
|
+
*
|
|
118
|
+
* `offline_access` is the one that matters: it is where a refresh token comes from on a server that
|
|
119
|
+
* gates it behind a scope. Asking for it unconditionally is what a hostname check amounted to.
|
|
120
|
+
*/
|
|
121
|
+
function requestableScope(published) {
|
|
122
|
+
if (!published)
|
|
123
|
+
return undefined;
|
|
124
|
+
const wanted = ["openid", "profile", "email", "offline_access"].filter((scope) => published.includes(scope));
|
|
125
|
+
return wanted.length > 0 ? wanted.join(" ") : undefined;
|
|
126
|
+
}
|
|
127
|
+
function tokenResponse(tokens) {
|
|
128
|
+
return {
|
|
129
|
+
accessToken: tokens.access_token,
|
|
130
|
+
expiresIn: tokens.expires_in ?? 3_600,
|
|
131
|
+
...(tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Discovery, token and probe requests run inside the login and connect paths. Without a deadline a
|
|
135
|
+
// hanging issuer would hold the request open until the platform kills it.
|
|
136
|
+
const RequestTimeoutMs = 10_000;
|
|
137
|
+
export function createOpenId(deps) {
|
|
138
|
+
const customFetch = async (url, options) => {
|
|
139
|
+
const init = options;
|
|
140
|
+
const deadline = AbortSignal.timeout(RequestTimeoutMs);
|
|
141
|
+
// The library may pass its own signal. Spreading init last would drop the deadline, spreading it
|
|
142
|
+
// first would drop the caller's cancellation, so both have to abort the request.
|
|
143
|
+
return await deps.fetch(url, {
|
|
144
|
+
...init,
|
|
145
|
+
signal: init.signal ? AbortSignal.any([init.signal, deadline]) : deadline,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
const configurations = new Map();
|
|
149
|
+
function options(issuer) {
|
|
150
|
+
const insecure = new URL(issuer).protocol === "http:";
|
|
151
|
+
if (insecure && !deps.allowInsecure) {
|
|
152
|
+
throw new Error("Insecure OAuth issuers require ALLOW_INSECURE_OAUTH=true");
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
[client.customFetch]: customFetch,
|
|
156
|
+
...(insecure ? { execute: [client.allowInsecureRequests] } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// openid-client resolves issuer metadata via OIDC discovery unless told otherwise. Gate serves
|
|
160
|
+
// that document, but a plain OAuth 2.0 authorization server — the MCP portal on its custom
|
|
161
|
+
// domain is one — publishes only RFC 8414's oauth-authorization-server path and answers the OIDC
|
|
162
|
+
// one with a 404, which surfaced as a bare 500 on /auth/connect (#93). The fallback repeats the
|
|
163
|
+
// operation once with the OAuth document; when both fail, the second error is thrown because it
|
|
164
|
+
// belongs to the attempt that got further for the issuer that needed the fallback at all.
|
|
165
|
+
async function withAlgorithmFallback(run, usable = () => true) {
|
|
166
|
+
try {
|
|
167
|
+
const first = await run();
|
|
168
|
+
// ⚠️ A THROW is not the only way the OIDC document can be the wrong one (#425). Cloudflare
|
|
169
|
+
// Access answers `/.well-known/openid-configuration` with **200** and a two-field stub —
|
|
170
|
+
// `issuer` and `jwks_uri`, nothing else — while the endpoints live only under RFC 8414's
|
|
171
|
+
// `oauth-authorization-server`. Discovery therefore SUCCEEDS, the fallback never runs, and
|
|
172
|
+
// the missing endpoint surfaces minutes later inside `buildAuthorizationUrl` as
|
|
173
|
+
// `authorization server metadata does not contain a valid "as.authorization_endpoint"` — a
|
|
174
|
+
// 500 on `/auth/connect` with nothing in it pointing at discovery.
|
|
175
|
+
//
|
|
176
|
+
// So the question is not "did it fail" but "is what came back usable". An answer that parses
|
|
177
|
+
// and carries nothing is the same situation as a 404, and it gets the same second attempt.
|
|
178
|
+
return usable(first) ? first : await run("oauth2");
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return await run("oauth2");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function discover(issuer, clientId) {
|
|
185
|
+
const key = `${issuer}#${clientId}`;
|
|
186
|
+
let configuration = configurations.get(key);
|
|
187
|
+
if (!configuration) {
|
|
188
|
+
configuration = withAlgorithmFallback((algorithm) => client.discovery(new URL(issuer), clientId, { token_endpoint_auth_method: "none" }, client.None(), { ...options(issuer), ...(algorithm ? { algorithm } : {}) }),
|
|
189
|
+
// The two endpoints every flow here needs. Asking for exactly them rather than for a long
|
|
190
|
+
// list keeps the check honest: a server that publishes these two is usable, and one that
|
|
191
|
+
// publishes neither is the stub case above, whatever else it carries.
|
|
192
|
+
(resolved) => {
|
|
193
|
+
const metadata = resolved.serverMetadata();
|
|
194
|
+
return Boolean(metadata.authorization_endpoint && metadata.token_endpoint);
|
|
195
|
+
});
|
|
196
|
+
configurations.set(key, configuration);
|
|
197
|
+
void configuration.catch(() => configurations.delete(key));
|
|
198
|
+
}
|
|
199
|
+
return await configuration;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
async discoverResource(resourceUrl) {
|
|
203
|
+
const probe = await deps
|
|
204
|
+
.fetch(resourceUrl, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
signal: AbortSignal.timeout(RequestTimeoutMs),
|
|
207
|
+
headers: {
|
|
208
|
+
accept: "application/json, text/event-stream",
|
|
209
|
+
"content-type": "application/json",
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
jsonrpc: "2.0",
|
|
213
|
+
id: "intel-oauth-discovery",
|
|
214
|
+
method: "initialize",
|
|
215
|
+
params: {
|
|
216
|
+
protocolVersion: "2025-11-25",
|
|
217
|
+
capabilities: {},
|
|
218
|
+
clientInfo: { name: "intel", version: "0.1.0" },
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
.catch(() => null);
|
|
223
|
+
const candidates = metadataCandidates(resourceUrl, resourceMetadataChallenge(probe?.headers.get("www-authenticate") ?? null));
|
|
224
|
+
for (const candidate of candidates) {
|
|
225
|
+
const response = await deps
|
|
226
|
+
.fetch(candidate, {
|
|
227
|
+
signal: AbortSignal.timeout(RequestTimeoutMs),
|
|
228
|
+
headers: { accept: "application/json" },
|
|
229
|
+
})
|
|
230
|
+
.catch(() => null);
|
|
231
|
+
if (!response?.ok)
|
|
232
|
+
continue;
|
|
233
|
+
const metadata = ProtectedResource.safeParse(await response.json().catch(() => null));
|
|
234
|
+
if (!metadata.success)
|
|
235
|
+
continue;
|
|
236
|
+
if (new URL(metadata.data.resource).href !== new URL(resourceUrl).href)
|
|
237
|
+
continue;
|
|
238
|
+
const issuer = metadata.data.authorization_servers[0];
|
|
239
|
+
if (issuer) {
|
|
240
|
+
const scope = requestableScope(await publishedScopes(deps.fetch, issuer));
|
|
241
|
+
return {
|
|
242
|
+
resource: metadata.data.resource,
|
|
243
|
+
issuer,
|
|
244
|
+
...(scope ? { scope } : {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
throw new Error("MCP resource did not publish valid OAuth metadata");
|
|
249
|
+
},
|
|
250
|
+
async register(input) {
|
|
251
|
+
const configuration = await withAlgorithmFallback((algorithm) => client.dynamicClientRegistration(new URL(input.issuer), {
|
|
252
|
+
client_name: input.clientName,
|
|
253
|
+
redirect_uris: [input.redirectUri],
|
|
254
|
+
response_types: ["code"],
|
|
255
|
+
// ⚠️ `refresh_token` belongs here, not only in the scope. RFC 7591 §2: `grant_types`
|
|
256
|
+
// declares which grants this client may USE, and a client registered without it may
|
|
257
|
+
// not refresh — whatever scope it was granted. That is why the portal connection did
|
|
258
|
+
// not survive its first access token (#97): `offline_access` was being requested and
|
|
259
|
+
// the right to act on it never was.
|
|
260
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
261
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
262
|
+
token_endpoint_auth_method: "none",
|
|
263
|
+
// ⚠️ Unconditional, and that is the protocol rather than a vendor accommodation. The
|
|
264
|
+
// MCP authorization spec: "MCP clients MUST send this parameter regardless of whether
|
|
265
|
+
// authorization servers support it" (RFC 8707 Resource Indicators). Sending it only to
|
|
266
|
+
// hosts whose name ended in `.cloudflareaccess.com` meant every portal on its own
|
|
267
|
+
// domain registered as a client that never named the resource it wanted a token for —
|
|
268
|
+
// and Cloudflare's Managed OAuth requires exactly that conformance before it will hand
|
|
269
|
+
// out a refresh token. The connection then died at the first token expiry, and the log
|
|
270
|
+
// said "no refresh token was ever issued" (#97).
|
|
271
|
+
resource: input.resource,
|
|
272
|
+
}, client.None(), { ...options(input.issuer), ...(algorithm ? { algorithm } : {}) }));
|
|
273
|
+
return configuration.clientMetadata().client_id;
|
|
274
|
+
},
|
|
275
|
+
async authorizationUrl(input) {
|
|
276
|
+
const configuration = await discover(input.issuer, input.clientId);
|
|
277
|
+
return client.buildAuthorizationUrl(configuration, {
|
|
278
|
+
response_type: "code",
|
|
279
|
+
redirect_uri: input.redirectUri,
|
|
280
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
281
|
+
resource: input.resource,
|
|
282
|
+
code_challenge: input.codeChallenge,
|
|
283
|
+
code_challenge_method: "S256",
|
|
284
|
+
state: input.state,
|
|
285
|
+
...(input.prompt ? { prompt: input.prompt } : {}),
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
async exchange(input) {
|
|
289
|
+
const configuration = await discover(input.issuer, input.clientId);
|
|
290
|
+
const tokens = await client.authorizationCodeGrant(configuration, input.callbackUrl, { pkceCodeVerifier: input.codeVerifier, expectedState: input.state }, { resource: input.resource });
|
|
291
|
+
return tokenResponse(tokens);
|
|
292
|
+
},
|
|
293
|
+
async refresh(input) {
|
|
294
|
+
const configuration = await discover(input.issuer, input.clientId);
|
|
295
|
+
const tokens = await client.refreshTokenGrant(configuration, input.refreshToken, {
|
|
296
|
+
resource: input.resource,
|
|
297
|
+
});
|
|
298
|
+
return tokenResponse(tokens);
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PortalTokenStore } from "../../tools/tools.types.js";
|
|
2
|
+
import type { D1Database } from "../db/db.types.js";
|
|
3
|
+
export declare function createPortalTokenStore(deps: {
|
|
4
|
+
db: D1Database;
|
|
5
|
+
secret: string;
|
|
6
|
+
crypto: Crypto;
|
|
7
|
+
now(): Date;
|
|
8
|
+
}): PortalTokenStore;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { reportUnexpectedError } from "../../shared/report-unexpected-error/report-unexpected-error.js";
|
|
3
|
+
const StoredToken = z.strictObject({
|
|
4
|
+
version: z.literal(1),
|
|
5
|
+
accessToken: z.string().min(1),
|
|
6
|
+
refreshToken: z.string().min(1).nullable(),
|
|
7
|
+
expiresAt: z.number().int().positive(),
|
|
8
|
+
issuer: z.url(),
|
|
9
|
+
clientId: z.string().min(1),
|
|
10
|
+
resource: z.url(),
|
|
11
|
+
});
|
|
12
|
+
// Bound to this purpose so a sealed value cannot be replayed as a session cookie and vice versa.
|
|
13
|
+
const additionalData = new TextEncoder().encode("anchrd-intel-portal-token-v1");
|
|
14
|
+
function encode(bytes) {
|
|
15
|
+
let value = "";
|
|
16
|
+
for (const byte of bytes)
|
|
17
|
+
value += String.fromCharCode(byte);
|
|
18
|
+
return btoa(value);
|
|
19
|
+
}
|
|
20
|
+
function decode(value) {
|
|
21
|
+
return Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
|
|
22
|
+
}
|
|
23
|
+
export function createPortalTokenStore(deps) {
|
|
24
|
+
if (new TextEncoder().encode(deps.secret).byteLength < 32) {
|
|
25
|
+
throw new Error("INTEL_SESSION_SECRET must contain at least 32 bytes");
|
|
26
|
+
}
|
|
27
|
+
const key = deps.crypto.subtle
|
|
28
|
+
.digest("SHA-256", new TextEncoder().encode(deps.secret))
|
|
29
|
+
.then((hash) => deps.crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]));
|
|
30
|
+
return {
|
|
31
|
+
async read(userId) {
|
|
32
|
+
const row = await deps.db
|
|
33
|
+
.prepare("SELECT sealed FROM portal_tokens WHERE user_id = ?")
|
|
34
|
+
.bind(userId)
|
|
35
|
+
.first();
|
|
36
|
+
if (!row)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
const raw = decode(row.sealed);
|
|
40
|
+
const plaintext = await deps.crypto.subtle.decrypt({ name: "AES-GCM", iv: raw.subarray(0, 12), additionalData }, await key, raw.subarray(12));
|
|
41
|
+
const parsed = StoredToken.parse(JSON.parse(new TextDecoder().decode(plaintext)));
|
|
42
|
+
const { version: _version, ...token } = parsed;
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// A row that no longer decrypts (rotated secret, tampering) is treated as absent so the
|
|
47
|
+
// user is asked to reconnect instead of hitting an opaque failure on every call.
|
|
48
|
+
// ⚠️ Absent and unreadable look identical from the outside — both end in the sign-in screen
|
|
49
|
+
// — but only one of them is a defect. Without this line the difference is unobservable, and
|
|
50
|
+
// a connection that disappears has no explanation anywhere (#97). The row is named by its
|
|
51
|
+
// length alone; the sealed value never goes near a log.
|
|
52
|
+
reportUnexpectedError(new Error(`portal token for a user did not decrypt (sealed length ${row.sealed.length})`, {
|
|
53
|
+
cause: error,
|
|
54
|
+
}));
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
async write(userId, token) {
|
|
59
|
+
const iv = deps.crypto.getRandomValues(new Uint8Array(12));
|
|
60
|
+
const encrypted = new Uint8Array(await deps.crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, await key, new TextEncoder().encode(JSON.stringify({ version: 1, ...token }))));
|
|
61
|
+
const sealed = new Uint8Array(iv.byteLength + encrypted.byteLength);
|
|
62
|
+
sealed.set(iv);
|
|
63
|
+
sealed.set(encrypted, iv.byteLength);
|
|
64
|
+
const timestamp = deps.now().toISOString();
|
|
65
|
+
await deps.db
|
|
66
|
+
.prepare(`INSERT INTO portal_tokens (user_id, sealed, expires_at, updated_at)
|
|
67
|
+
VALUES (?, ?, ?, ?)
|
|
68
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
69
|
+
sealed = excluded.sealed,
|
|
70
|
+
expires_at = excluded.expires_at,
|
|
71
|
+
updated_at = excluded.updated_at`)
|
|
72
|
+
.bind(userId, encode(sealed), new Date(token.expiresAt).toISOString(), timestamp)
|
|
73
|
+
.run();
|
|
74
|
+
},
|
|
75
|
+
async clear(userId) {
|
|
76
|
+
await deps.db.prepare("DELETE FROM portal_tokens WHERE user_id = ?").bind(userId).run();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|