@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,133 @@
|
|
|
1
|
+
import type { ResourceGrant, ResourceVerb } from "../../contract/share.js";
|
|
2
|
+
export interface GrantActor {
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
isAdmin?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A grant row as every reader of one wants it: the subject column already aliased, so the same row
|
|
9
|
+
* shape comes back from `node_grants` and from `flow_grants`.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ The alias is what lets `mapGrant` stay one function. Two mappers would each carry the same
|
|
12
|
+
* `principal_type` switch, and the day a fourth principal shape arrives one of them gets it.
|
|
13
|
+
*/
|
|
14
|
+
export interface GrantRow {
|
|
15
|
+
id: string;
|
|
16
|
+
resource_id: string;
|
|
17
|
+
principal_type: "user" | "email" | "organization";
|
|
18
|
+
principal_id: string;
|
|
19
|
+
verb: ResourceVerb;
|
|
20
|
+
expires_at: string | null;
|
|
21
|
+
created_by: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const grantColumns: (subjectColumn: string) => string;
|
|
25
|
+
export declare function mapGrant(row: GrantRow): ResourceGrant;
|
|
26
|
+
export declare function principalColumns(principal: ResourceGrant["principal"]): {
|
|
27
|
+
type: ResourceGrant["principal"]["type"];
|
|
28
|
+
id: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Whether a grant row is in force at the moment bound after it. Takes one binding: that moment.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ The ONE place this repository decides what an expiry means, for both grant tables and for
|
|
34
|
+
* every statement that reads either — the point check, the subtree walk, and the two effective
|
|
35
|
+
* views alike. It is a fragment rather than part of `grantExists` because half the readers do not
|
|
36
|
+
* ask about a principal at all: `listEffectiveAccess` lists every grant along a path, and
|
|
37
|
+
* `callReach` asks only whether an organization-wide `execute` stands. Those had written the
|
|
38
|
+
* condition out by hand, which is exactly the second set of doors the header above warns about —
|
|
39
|
+
* a grace period, a different comparison, and the two spellings drift apart in silence.
|
|
40
|
+
*
|
|
41
|
+
* `git grep "expires_at IS NULL" -- packages/intel/src` must find this line and nothing else.
|
|
42
|
+
*/
|
|
43
|
+
export declare const grantInForce = "(grant_row.expires_at IS NULL OR grant_row.expires_at > ?)";
|
|
44
|
+
export declare const subtreeCte: string;
|
|
45
|
+
export declare function subtreeBindings(actor: GrantActor, verb: ResourceVerb, now: string): unknown[];
|
|
46
|
+
/**
|
|
47
|
+
* One named folder and everything filed beneath it, appended to a statement that already carries
|
|
48
|
+
* `subtreeCte`. It answers "where", never "whether": a statement joins `descendants` *in addition
|
|
49
|
+
* to* `allowed`, so the scope can only take rows away from an answer the actor was already entitled
|
|
50
|
+
* to. Joining it instead of `allowed` would turn a search argument into a permission, which is
|
|
51
|
+
* exactly what ADR-0004 forbids — the caller supplies the id, and a caller is not a grant.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ `UNION`, never `UNION ALL`, for the reason `nodeVerbQuery` gives: a cycle in `parent_id` can
|
|
54
|
+
* reach the table and this walks downwards into it.
|
|
55
|
+
*/
|
|
56
|
+
export declare const descendantsCte = "descendants(id) AS (\n SELECT scope.id FROM nodes scope WHERE scope.id = ?\n UNION\n SELECT child.id\n FROM nodes child\n JOIN descendants parent ON child.parent_id = parent.id\n )";
|
|
57
|
+
/**
|
|
58
|
+
* Both walks in one statement: what the actor may open, and what they may run. Its bindings are
|
|
59
|
+
* `subtreeBindings(actor, "read", now)` followed by `subtreeBindings(actor, "execute", now)`, in
|
|
60
|
+
* that order.
|
|
61
|
+
*/
|
|
62
|
+
export declare const readableOrRunnableCte: string;
|
|
63
|
+
/**
|
|
64
|
+
* A flow the actor may open *or* may run, for a statement carrying `readableOrRunnableCte`. It is
|
|
65
|
+
* the callable rule of ADR-0004 §2/§3 — `execute` without `read` is the library, a building block
|
|
66
|
+
* anyone may run and few may open — asked of a whole list of flows at once instead of one flow at a
|
|
67
|
+
* time. Its bindings follow the two CTEs' and are `flowCallableBindings(actor, now)`.
|
|
68
|
+
*
|
|
69
|
+
* ⚠️ Both direct grants, and both are needed. A flow handed out on its own for `execute` alone is
|
|
70
|
+
* the library case shrunk to one flow — runnable by people who may not open it — and leaving `read`
|
|
71
|
+
* out here would hide a flow somebody was explicitly given to look at.
|
|
72
|
+
*/
|
|
73
|
+
export declare const flowCallable: string;
|
|
74
|
+
export declare function flowCallableBindings(actor: GrantActor, now: string): unknown[];
|
|
75
|
+
/**
|
|
76
|
+
* The point check for one node: the node itself and every ancestor above it. Cheaper than
|
|
77
|
+
* the subtree walk and the same answer, because a grant reaches down and never sideways.
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ `UNION`, never `UNION ALL`. The service refuses to move a node into its own descendant, but a
|
|
80
|
+
* cycle in `parent_id` can still reach the table — a restore, a repair, a future writer. `UNION ALL`
|
|
81
|
+
* would walk such a ring without end, and this query runs on the authorization path of every
|
|
82
|
+
* request, so the damage lands in the database rather than in one tab. `UNION` folds the repeated
|
|
83
|
+
* row away and the recursion stops on its own.
|
|
84
|
+
*/
|
|
85
|
+
export declare const nodeVerbQuery: string;
|
|
86
|
+
export declare function nodeVerbBindings(nodeId: string, actor: GrantActor, verb: ResourceVerb, now: string): unknown[];
|
|
87
|
+
/**
|
|
88
|
+
* The same question for a flow. Three things reach it, and they are OR-ed rather than ranked: its
|
|
89
|
+
* owner, a grant on the folder it is filed in or any folder above that, and — since #530 — a grant
|
|
90
|
+
* that sits on the flow itself. Its owner keeps it the way a node's owner keeps theirs, otherwise a
|
|
91
|
+
* flow at the root of the tree would be unreachable by the person who created it. `UNION` for the
|
|
92
|
+
* same reason as above.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ The direct grant is asked BEFORE the ancestor walk, and only because it is cheaper: a point
|
|
95
|
+
* lookup on an indexed column against a recursive walk of the tree. It decides nothing the walk
|
|
96
|
+
* would have decided differently — both are the same OR.
|
|
97
|
+
*/
|
|
98
|
+
export declare const flowVerbQuery: string;
|
|
99
|
+
export declare function flowVerbBindings(flowId: string, actor: GrantActor, verb: ResourceVerb, now: string): unknown[];
|
|
100
|
+
/**
|
|
101
|
+
* The predicate a query over `flows` uses when it already carries `subtreeCte` for the same verb.
|
|
102
|
+
* Its bindings follow the CTE's.
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ The verb is now a binding of its own, and it has to be the SAME verb the CTE was seeded with.
|
|
105
|
+
* The walk answers "which folders", the direct grant answers "this flow" — asking them for two
|
|
106
|
+
* different verbs would produce a predicate that is neither, and it is the kind of mismatch nothing
|
|
107
|
+
* fails on: the query still runs and quietly hands out the wrong list.
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* ⚠️ The parent is a PARAMETER for exactly one caller, and giving it one was cheaper than the
|
|
111
|
+
* alternative. The feed reads events about flows that no longer exist: after `flows.purge` the row
|
|
112
|
+
* is gone, so `flow.parent_id`, `flow.owner_id` and every `flow_grants` row are gone with it, and
|
|
113
|
+
* the only anchor left is the `parentId` the purge wrote into its own metadata (#763, D73). A
|
|
114
|
+
* second, similar-looking predicate beside this one is the drift `destructive.md` warns about: two
|
|
115
|
+
* expressions with similar names are two different questions, and the one that goes stale is the
|
|
116
|
+
* copy.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* ⚠️ **The ALIAS is a parameter too, since #767, and leaving it hard-wired was a real fault rather
|
|
120
|
+
* than an inelegance.** The feed's third branch asks about a flow reached through its RUN, so the
|
|
121
|
+
* flow sits under a second join (`run_flow`) while `flow` is NULL on that row. Against the
|
|
122
|
+
* hard-wired name the owner branch and the direct-grant branch both compared NULL — so a flow
|
|
123
|
+
* somebody OWNS but that hangs at the top level, with no folder above it to carry a grant, answered
|
|
124
|
+
* "not visible" to its own owner.
|
|
125
|
+
*
|
|
126
|
+
* ⚠️ It fails in the safe direction, which is exactly what made it worth a test rather than a
|
|
127
|
+
* comment: nothing leaks, a row is only ever missing. A flow filed in a readable FOLDER answers
|
|
128
|
+
* correctly through the third branch, so every test with a folder is green either way — and the two
|
|
129
|
+
* tests that caught it are the ones that seed a flow at the root.
|
|
130
|
+
*/
|
|
131
|
+
export declare function flowInSubtreeOver(parentExpression: string, flowAlias?: string): string;
|
|
132
|
+
export declare const flowInSubtree: string;
|
|
133
|
+
export declare function flowInSubtreeBindings(actor: GrantActor, verb: ResourceVerb, now: string): unknown[];
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
export const grantColumns = (subjectColumn) => `id, ${subjectColumn} AS resource_id, principal_type, principal_id, verb, expires_at,
|
|
2
|
+
created_by, created_at`;
|
|
3
|
+
export function mapGrant(row) {
|
|
4
|
+
let principal;
|
|
5
|
+
switch (row.principal_type) {
|
|
6
|
+
case "user":
|
|
7
|
+
principal = { type: "user", id: row.principal_id };
|
|
8
|
+
break;
|
|
9
|
+
case "email":
|
|
10
|
+
principal = { type: "email", email: row.principal_id };
|
|
11
|
+
break;
|
|
12
|
+
case "organization":
|
|
13
|
+
principal = { type: "organization" };
|
|
14
|
+
break;
|
|
15
|
+
default:
|
|
16
|
+
throw new Error("Unsupported grant principal type");
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
id: row.id,
|
|
20
|
+
resourceId: row.resource_id,
|
|
21
|
+
principal,
|
|
22
|
+
verb: row.verb,
|
|
23
|
+
expiresAt: row.expires_at,
|
|
24
|
+
createdBy: row.created_by,
|
|
25
|
+
createdAt: row.created_at,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// How a principal is written into the subject columns of both grant tables. An organization is the
|
|
29
|
+
// literal `*` every reader above matches on; an email is folded to lower case on the way in so the
|
|
30
|
+
// comparison there has one shape to worry about.
|
|
31
|
+
export function principalColumns(principal) {
|
|
32
|
+
if (principal.type === "user")
|
|
33
|
+
return { type: "user", id: principal.id };
|
|
34
|
+
if (principal.type === "email")
|
|
35
|
+
return { type: "email", id: principal.email.toLowerCase() };
|
|
36
|
+
return { type: "organization", id: "*" };
|
|
37
|
+
}
|
|
38
|
+
const principalMatch = `(
|
|
39
|
+
(grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
|
|
40
|
+
OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
|
|
41
|
+
OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
|
|
42
|
+
)`;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a grant row is in force at the moment bound after it. Takes one binding: that moment.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ The ONE place this repository decides what an expiry means, for both grant tables and for
|
|
47
|
+
* every statement that reads either — the point check, the subtree walk, and the two effective
|
|
48
|
+
* views alike. It is a fragment rather than part of `grantExists` because half the readers do not
|
|
49
|
+
* ask about a principal at all: `listEffectiveAccess` lists every grant along a path, and
|
|
50
|
+
* `callReach` asks only whether an organization-wide `execute` stands. Those had written the
|
|
51
|
+
* condition out by hand, which is exactly the second set of doors the header above warns about —
|
|
52
|
+
* a grace period, a different comparison, and the two spellings drift apart in silence.
|
|
53
|
+
*
|
|
54
|
+
* `git grep "expires_at IS NULL" -- packages/intel/src` must find this line and nothing else.
|
|
55
|
+
*/
|
|
56
|
+
export const grantInForce = "(grant_row.expires_at IS NULL OR grant_row.expires_at > ?)";
|
|
57
|
+
/**
|
|
58
|
+
* One grant row that is in force, asked of one table.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ The table is a parameter and the rest is not, which is the whole reason #530 could give a flow
|
|
61
|
+
* its own grants without a second set of doors. `flow_grants` and `node_grants` differ in their
|
|
62
|
+
* subject column and in nothing else — same principal shapes, same verbs, same expiry — so a
|
|
63
|
+
* question asked of one is asked of the other by the same string. A hand-written copy for flows is
|
|
64
|
+
* how one of the two would eventually stop honouring `expires_at`.
|
|
65
|
+
*/
|
|
66
|
+
function grantExists(table, subjectColumn, targetColumn) {
|
|
67
|
+
return `EXISTS (
|
|
68
|
+
SELECT 1 FROM ${table} grant_row
|
|
69
|
+
WHERE grant_row.${subjectColumn} = ${targetColumn}
|
|
70
|
+
AND ${principalMatch}
|
|
71
|
+
AND grant_row.verb = ?
|
|
72
|
+
AND ${grantInForce}
|
|
73
|
+
)`;
|
|
74
|
+
}
|
|
75
|
+
const nodeGrantExists = (nodeColumn) => grantExists("node_grants", "node_id", nodeColumn);
|
|
76
|
+
// A grant that sits on the flow itself (#530), beside whatever its folder already passes down. It
|
|
77
|
+
// adds to the folder's reach and never replaces it, exactly as a grant on a document adds to its
|
|
78
|
+
// folder's — the two are OR-ed everywhere this appears.
|
|
79
|
+
const flowGrantExists = (flowColumn) => grantExists("flow_grants", "flow_id", flowColumn);
|
|
80
|
+
// Bindings for one `grantExists`: principal (twice), verb, and the moment expiry is measured
|
|
81
|
+
// against. Callers concatenate these in SQL order; the helpers below say which order that is.
|
|
82
|
+
function grantBindings(actor, verb, now) {
|
|
83
|
+
return [actor.id, actor.email, verb, now];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Seeds every node the actor reaches for one verb and walks downwards, which is what "a grant
|
|
87
|
+
* applies to everything beneath it" means in SQL. The name is a parameter because one statement can
|
|
88
|
+
* need two of these — asking `read` and `execute` at once is what turns "one query per callee" into
|
|
89
|
+
* one query (#30) — and a second walk written out by hand would be a second copy of the rule.
|
|
90
|
+
*/
|
|
91
|
+
function subtreeWalk(name) {
|
|
92
|
+
return `${name}(id, parent_id) AS (
|
|
93
|
+
SELECT seed.id, seed.parent_id
|
|
94
|
+
FROM nodes seed
|
|
95
|
+
WHERE ? = 1
|
|
96
|
+
OR seed.owner_id = ?
|
|
97
|
+
OR ${nodeGrantExists("seed.id")}
|
|
98
|
+
UNION
|
|
99
|
+
SELECT child.id, child.parent_id
|
|
100
|
+
FROM nodes child
|
|
101
|
+
JOIN ${name} parent ON child.parent_id = parent.id
|
|
102
|
+
)`;
|
|
103
|
+
}
|
|
104
|
+
// The walk under the name every single-verb statement already uses. Callers append their own SELECT
|
|
105
|
+
// and may append further CTEs with a comma.
|
|
106
|
+
export const subtreeCte = `WITH RECURSIVE ${subtreeWalk("allowed")}`;
|
|
107
|
+
export function subtreeBindings(actor, verb, now) {
|
|
108
|
+
return [actor.isAdmin ? 1 : 0, actor.id, ...grantBindings(actor, verb, now)];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* One named folder and everything filed beneath it, appended to a statement that already carries
|
|
112
|
+
* `subtreeCte`. It answers "where", never "whether": a statement joins `descendants` *in addition
|
|
113
|
+
* to* `allowed`, so the scope can only take rows away from an answer the actor was already entitled
|
|
114
|
+
* to. Joining it instead of `allowed` would turn a search argument into a permission, which is
|
|
115
|
+
* exactly what ADR-0004 forbids — the caller supplies the id, and a caller is not a grant.
|
|
116
|
+
*
|
|
117
|
+
* ⚠️ `UNION`, never `UNION ALL`, for the reason `nodeVerbQuery` gives: a cycle in `parent_id` can
|
|
118
|
+
* reach the table and this walks downwards into it.
|
|
119
|
+
*/
|
|
120
|
+
export const descendantsCte = `descendants(id) AS (
|
|
121
|
+
SELECT scope.id FROM nodes scope WHERE scope.id = ?
|
|
122
|
+
UNION
|
|
123
|
+
SELECT child.id
|
|
124
|
+
FROM nodes child
|
|
125
|
+
JOIN descendants parent ON child.parent_id = parent.id
|
|
126
|
+
)`;
|
|
127
|
+
/**
|
|
128
|
+
* Both walks in one statement: what the actor may open, and what they may run. Its bindings are
|
|
129
|
+
* `subtreeBindings(actor, "read", now)` followed by `subtreeBindings(actor, "execute", now)`, in
|
|
130
|
+
* that order.
|
|
131
|
+
*/
|
|
132
|
+
export const readableOrRunnableCte = `WITH RECURSIVE ${subtreeWalk("readable")},
|
|
133
|
+
${subtreeWalk("runnable")}`;
|
|
134
|
+
/**
|
|
135
|
+
* A flow the actor may open *or* may run, for a statement carrying `readableOrRunnableCte`. It is
|
|
136
|
+
* the callable rule of ADR-0004 §2/§3 — `execute` without `read` is the library, a building block
|
|
137
|
+
* anyone may run and few may open — asked of a whole list of flows at once instead of one flow at a
|
|
138
|
+
* time. Its bindings follow the two CTEs' and are `flowCallableBindings(actor, now)`.
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ Both direct grants, and both are needed. A flow handed out on its own for `execute` alone is
|
|
141
|
+
* the library case shrunk to one flow — runnable by people who may not open it — and leaving `read`
|
|
142
|
+
* out here would hide a flow somebody was explicitly given to look at.
|
|
143
|
+
*/
|
|
144
|
+
export const flowCallable = `(
|
|
145
|
+
? = 1
|
|
146
|
+
OR flow.owner_id = ?
|
|
147
|
+
OR flow.parent_id IN (SELECT id FROM readable)
|
|
148
|
+
OR flow.parent_id IN (SELECT id FROM runnable)
|
|
149
|
+
OR ${flowGrantExists("flow.id")}
|
|
150
|
+
OR ${flowGrantExists("flow.id")}
|
|
151
|
+
)`;
|
|
152
|
+
export function flowCallableBindings(actor, now) {
|
|
153
|
+
return [
|
|
154
|
+
actor.isAdmin ? 1 : 0,
|
|
155
|
+
actor.id,
|
|
156
|
+
...grantBindings(actor, "read", now),
|
|
157
|
+
...grantBindings(actor, "execute", now),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The point check for one node: the node itself and every ancestor above it. Cheaper than
|
|
162
|
+
* the subtree walk and the same answer, because a grant reaches down and never sideways.
|
|
163
|
+
*
|
|
164
|
+
* ⚠️ `UNION`, never `UNION ALL`. The service refuses to move a node into its own descendant, but a
|
|
165
|
+
* cycle in `parent_id` can still reach the table — a restore, a repair, a future writer. `UNION ALL`
|
|
166
|
+
* would walk such a ring without end, and this query runs on the authorization path of every
|
|
167
|
+
* request, so the damage lands in the database rather than in one tab. `UNION` folds the repeated
|
|
168
|
+
* row away and the recursion stops on its own.
|
|
169
|
+
*/
|
|
170
|
+
export const nodeVerbQuery = `WITH RECURSIVE ancestors(id, parent_id, owner_id) AS (
|
|
171
|
+
SELECT id, parent_id, owner_id FROM nodes WHERE id = ?
|
|
172
|
+
UNION
|
|
173
|
+
SELECT parent.id, parent.parent_id, parent.owner_id
|
|
174
|
+
FROM nodes parent
|
|
175
|
+
JOIN ancestors child ON child.parent_id = parent.id
|
|
176
|
+
)
|
|
177
|
+
SELECT 1 AS allowed
|
|
178
|
+
FROM ancestors
|
|
179
|
+
WHERE ? = 1
|
|
180
|
+
OR owner_id = ?
|
|
181
|
+
OR ${nodeGrantExists("ancestors.id")}
|
|
182
|
+
LIMIT 1`;
|
|
183
|
+
export function nodeVerbBindings(nodeId, actor, verb, now) {
|
|
184
|
+
return [nodeId, actor.isAdmin ? 1 : 0, actor.id, ...grantBindings(actor, verb, now)];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The same question for a flow. Three things reach it, and they are OR-ed rather than ranked: its
|
|
188
|
+
* owner, a grant on the folder it is filed in or any folder above that, and — since #530 — a grant
|
|
189
|
+
* that sits on the flow itself. Its owner keeps it the way a node's owner keeps theirs, otherwise a
|
|
190
|
+
* flow at the root of the tree would be unreachable by the person who created it. `UNION` for the
|
|
191
|
+
* same reason as above.
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ The direct grant is asked BEFORE the ancestor walk, and only because it is cheaper: a point
|
|
194
|
+
* lookup on an indexed column against a recursive walk of the tree. It decides nothing the walk
|
|
195
|
+
* would have decided differently — both are the same OR.
|
|
196
|
+
*/
|
|
197
|
+
export const flowVerbQuery = `WITH RECURSIVE ancestors(id, parent_id) AS (
|
|
198
|
+
SELECT folder.id, folder.parent_id
|
|
199
|
+
FROM nodes folder
|
|
200
|
+
JOIN flows flow ON flow.parent_id = folder.id
|
|
201
|
+
WHERE flow.id = ?
|
|
202
|
+
UNION
|
|
203
|
+
SELECT parent.id, parent.parent_id
|
|
204
|
+
FROM nodes parent
|
|
205
|
+
JOIN ancestors child ON child.parent_id = parent.id
|
|
206
|
+
)
|
|
207
|
+
SELECT 1 AS allowed
|
|
208
|
+
FROM flows flow
|
|
209
|
+
WHERE flow.id = ?
|
|
210
|
+
AND (
|
|
211
|
+
? = 1
|
|
212
|
+
OR flow.owner_id = ?
|
|
213
|
+
OR ${flowGrantExists("flow.id")}
|
|
214
|
+
OR EXISTS (SELECT 1 FROM ancestors WHERE ${nodeGrantExists("ancestors.id")})
|
|
215
|
+
)
|
|
216
|
+
LIMIT 1`;
|
|
217
|
+
export function flowVerbBindings(flowId, actor, verb, now) {
|
|
218
|
+
return [
|
|
219
|
+
flowId,
|
|
220
|
+
flowId,
|
|
221
|
+
actor.isAdmin ? 1 : 0,
|
|
222
|
+
actor.id,
|
|
223
|
+
...grantBindings(actor, verb, now),
|
|
224
|
+
...grantBindings(actor, verb, now),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The predicate a query over `flows` uses when it already carries `subtreeCte` for the same verb.
|
|
229
|
+
* Its bindings follow the CTE's.
|
|
230
|
+
*
|
|
231
|
+
* ⚠️ The verb is now a binding of its own, and it has to be the SAME verb the CTE was seeded with.
|
|
232
|
+
* The walk answers "which folders", the direct grant answers "this flow" — asking them for two
|
|
233
|
+
* different verbs would produce a predicate that is neither, and it is the kind of mismatch nothing
|
|
234
|
+
* fails on: the query still runs and quietly hands out the wrong list.
|
|
235
|
+
*/
|
|
236
|
+
/**
|
|
237
|
+
* ⚠️ The parent is a PARAMETER for exactly one caller, and giving it one was cheaper than the
|
|
238
|
+
* alternative. The feed reads events about flows that no longer exist: after `flows.purge` the row
|
|
239
|
+
* is gone, so `flow.parent_id`, `flow.owner_id` and every `flow_grants` row are gone with it, and
|
|
240
|
+
* the only anchor left is the `parentId` the purge wrote into its own metadata (#763, D73). A
|
|
241
|
+
* second, similar-looking predicate beside this one is the drift `destructive.md` warns about: two
|
|
242
|
+
* expressions with similar names are two different questions, and the one that goes stale is the
|
|
243
|
+
* copy.
|
|
244
|
+
*/
|
|
245
|
+
/**
|
|
246
|
+
* ⚠️ **The ALIAS is a parameter too, since #767, and leaving it hard-wired was a real fault rather
|
|
247
|
+
* than an inelegance.** The feed's third branch asks about a flow reached through its RUN, so the
|
|
248
|
+
* flow sits under a second join (`run_flow`) while `flow` is NULL on that row. Against the
|
|
249
|
+
* hard-wired name the owner branch and the direct-grant branch both compared NULL — so a flow
|
|
250
|
+
* somebody OWNS but that hangs at the top level, with no folder above it to carry a grant, answered
|
|
251
|
+
* "not visible" to its own owner.
|
|
252
|
+
*
|
|
253
|
+
* ⚠️ It fails in the safe direction, which is exactly what made it worth a test rather than a
|
|
254
|
+
* comment: nothing leaks, a row is only ever missing. A flow filed in a readable FOLDER answers
|
|
255
|
+
* correctly through the third branch, so every test with a folder is green either way — and the two
|
|
256
|
+
* tests that caught it are the ones that seed a flow at the root.
|
|
257
|
+
*/
|
|
258
|
+
export function flowInSubtreeOver(parentExpression, flowAlias = "flow") {
|
|
259
|
+
return `(
|
|
260
|
+
? = 1
|
|
261
|
+
OR ${flowAlias}.owner_id = ?
|
|
262
|
+
OR ${parentExpression} IN (SELECT id FROM allowed)
|
|
263
|
+
OR ${flowGrantExists(`${flowAlias}.id`)}
|
|
264
|
+
)`;
|
|
265
|
+
}
|
|
266
|
+
export const flowInSubtree = flowInSubtreeOver("flow.parent_id");
|
|
267
|
+
export function flowInSubtreeBindings(actor, verb, now) {
|
|
268
|
+
return [actor.isAdmin ? 1 : 0, actor.id, ...grantBindings(actor, verb, now)];
|
|
269
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// How much of a chunk is kept as the passage a searcher is shown. The same 480 characters
|
|
2
|
+
// `hydrateVisibleCitations` cuts out of the full-text row, so a citation reads the same length
|
|
3
|
+
// whether it came from a card's own vector or from the fallback beside it.
|
|
4
|
+
const maxPassageCharacters = 480;
|
|
5
|
+
/**
|
|
6
|
+
* What goes into the `content` column beside the title (#816).
|
|
7
|
+
*
|
|
8
|
+
* The description is a sentence ABOUT the node, not a name, so it belongs on the side the query
|
|
9
|
+
* weights at 1.0 and draws its snippet from — `bm25(node_fts, 0.0, 0.0, 4.0, 1.0)` and
|
|
10
|
+
* `snippet(node_fts, 3, ...)` in `searchVisible`. In `title` it would let a sentence outrank a
|
|
11
|
+
* matching name at four times the weight, and a description-only match would then produce a snippet
|
|
12
|
+
* cut from a body the searcher's word never appears in.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ Applied to every chunk, and that is deliberately NOT the rule the title follows on the vector
|
|
15
|
+
* side. There a prefix lands in the fingerprint, so putting it on every chunk would cost one
|
|
16
|
+
* embedding per chunk each time the node is renamed (anchrd/intel#301); a full-text row has neither
|
|
17
|
+
* a fingerprint nor an embedding behind it, so the reason for the restriction does not exist here.
|
|
18
|
+
* Since #390 every kind brings exactly one chunk anyway.
|
|
19
|
+
*/
|
|
20
|
+
function indexedContent(description, text) {
|
|
21
|
+
const sentence = description?.trim() ?? "";
|
|
22
|
+
if (sentence === "")
|
|
23
|
+
return text;
|
|
24
|
+
return text === "" ? sentence : `${sentence}\n\n${text}`;
|
|
25
|
+
}
|
|
26
|
+
function mapTarget(row, contentKeys) {
|
|
27
|
+
return {
|
|
28
|
+
nodeId: row.node_id,
|
|
29
|
+
versionId: row.version_id,
|
|
30
|
+
title: row.title,
|
|
31
|
+
description: row.description,
|
|
32
|
+
contentKeys,
|
|
33
|
+
mediaType: row.media_type,
|
|
34
|
+
size: row.size,
|
|
35
|
+
kind: row.kind,
|
|
36
|
+
updatedAt: row.updated_at,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function createNodeIndexRepository(db) {
|
|
40
|
+
return {
|
|
41
|
+
async getTarget(versionId) {
|
|
42
|
+
const row = await db
|
|
43
|
+
.prepare(`SELECT n.id AS node_id, v.id AS version_id, n.title, n.description, v.content_key,
|
|
44
|
+
v.media_type, v.size, n.kind, n.updated_at
|
|
45
|
+
FROM node_versions v
|
|
46
|
+
JOIN nodes n ON n.id = v.node_id
|
|
47
|
+
WHERE v.id = ? AND n.current_version_id = v.id AND n.archived_at IS NULL`)
|
|
48
|
+
.bind(versionId)
|
|
49
|
+
.first();
|
|
50
|
+
if (!row)
|
|
51
|
+
return null;
|
|
52
|
+
if (row.kind !== "table")
|
|
53
|
+
return mapTarget(row, [row.content_key]);
|
|
54
|
+
// A table's rows live in every segment since the newest snapshot, so indexing the newest one
|
|
55
|
+
// alone would make a search find only what the last append added (#40) — and indexing the
|
|
56
|
+
// segments before a snapshot would make it find rows a mutation removed (#135). The same
|
|
57
|
+
// reading rule as `listVersionContentKeys`, because the index must describe what a read sees.
|
|
58
|
+
const segments = await db
|
|
59
|
+
.prepare(`SELECT content_key FROM node_versions
|
|
60
|
+
WHERE node_id = ? AND sequence >= COALESCE((
|
|
61
|
+
SELECT MAX(sequence) FROM node_versions
|
|
62
|
+
WHERE node_id = ? AND segment = 'snapshot'
|
|
63
|
+
), 0)
|
|
64
|
+
ORDER BY sequence`)
|
|
65
|
+
.bind(row.node_id, row.node_id)
|
|
66
|
+
.all();
|
|
67
|
+
return mapTarget(row, (segments.results ?? []).map((segment) => segment.content_key));
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* Every passage of this node, replacing every passage it had.
|
|
71
|
+
*
|
|
72
|
+
* ⚠️ One row per chunk, in ONE batch with the delete in front of it. Most kinds bring exactly
|
|
73
|
+
* one chunk and this is the statement pair it always was; a board brings one per task (#285),
|
|
74
|
+
* and a search can then answer with the card that matched rather than with the whole board.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ A board with no tasks writes no row at all, and that is right: there is nothing to find in
|
|
77
|
+
* it. What must not happen is the delete landing without the inserts, which is why they share a
|
|
78
|
+
* batch — D1 runs it as one transaction.
|
|
79
|
+
*/
|
|
80
|
+
async replace(target, chunks) {
|
|
81
|
+
await db.batch([
|
|
82
|
+
db
|
|
83
|
+
.prepare(`DELETE FROM node_fts WHERE node_id = ? AND EXISTS (
|
|
84
|
+
SELECT 1 FROM nodes
|
|
85
|
+
WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
|
|
86
|
+
)`)
|
|
87
|
+
.bind(target.nodeId, target.nodeId, target.versionId),
|
|
88
|
+
...chunks.map((chunk) => db
|
|
89
|
+
.prepare(`INSERT INTO node_fts (version_id, node_id, title, content)
|
|
90
|
+
SELECT ?, ?, ?, ? WHERE EXISTS (
|
|
91
|
+
SELECT 1 FROM nodes
|
|
92
|
+
WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
|
|
93
|
+
)`)
|
|
94
|
+
.bind(target.versionId, target.nodeId, chunk.title, indexedContent(target.description, chunk.text), target.nodeId, target.versionId)),
|
|
95
|
+
]);
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* ⚠️ `archived_at IS NOT NULL` carries this on its own, and it is the whole guard
|
|
99
|
+
* (anchrd/intel#348). `getTarget` above answers null for two reasons and only one of them means
|
|
100
|
+
* "purge": the other is a queue message for a version a later save superseded, and Cloudflare
|
|
101
|
+
* Queues deliver at least once, so those arrive as a matter of course. Acting on one would empty
|
|
102
|
+
* the vector index of a live board behind the back of the save that had just filled it — and
|
|
103
|
+
* nothing would report it, because a search that finds less is not an error.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ Deliberately NOT `current_version_id = v.id` as well, although that is what would make this
|
|
106
|
+
* the mirror image of `getTarget`. Symmetry is not a reason to write a condition: once the node
|
|
107
|
+
* IS archived, every version of it says the same thing about that node's vectors, so the extra
|
|
108
|
+
* clause could only ever refuse a purge that was right. A condition no test can turn red is a
|
|
109
|
+
* condition the next reader has to guess the purpose of.
|
|
110
|
+
*/
|
|
111
|
+
async archivedNodeId(versionId) {
|
|
112
|
+
const row = await db
|
|
113
|
+
.prepare(`SELECT n.id AS node_id
|
|
114
|
+
FROM node_versions v
|
|
115
|
+
JOIN nodes n ON n.id = v.node_id
|
|
116
|
+
WHERE v.id = ? AND n.archived_at IS NOT NULL`)
|
|
117
|
+
.bind(versionId)
|
|
118
|
+
.first();
|
|
119
|
+
return row?.node_id ?? null;
|
|
120
|
+
},
|
|
121
|
+
async listVectors(nodeId) {
|
|
122
|
+
const result = await db
|
|
123
|
+
.prepare(`SELECT chunk_key, fingerprint, passage FROM node_vectors WHERE node_id = ?`)
|
|
124
|
+
.bind(nodeId)
|
|
125
|
+
.all();
|
|
126
|
+
return (result.results ?? []).map((row) => ({
|
|
127
|
+
chunkKey: row.chunk_key,
|
|
128
|
+
fingerprint: row.fingerprint,
|
|
129
|
+
passage: row.passage,
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* The record of every vector this node has, replacing the record it had.
|
|
134
|
+
*
|
|
135
|
+
* ⚠️ The same delete-then-insert in ONE batch as `replace` above, and the same guard in front of
|
|
136
|
+
* every statement: a pass for a version that is no longer current, or for a node that has been
|
|
137
|
+
* archived meanwhile, writes nothing at all rather than half a record. What must not happen is
|
|
138
|
+
* the delete landing without the inserts — the next pass would then re-embed the whole board,
|
|
139
|
+
* which is exactly the cost anchrd/intel#301 exists to avoid.
|
|
140
|
+
*/
|
|
141
|
+
async replaceVectors(target, records) {
|
|
142
|
+
await db.batch([
|
|
143
|
+
db
|
|
144
|
+
.prepare(`DELETE FROM node_vectors WHERE node_id = ? AND EXISTS (
|
|
145
|
+
SELECT 1 FROM nodes
|
|
146
|
+
WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
|
|
147
|
+
)`)
|
|
148
|
+
.bind(target.nodeId, target.nodeId, target.versionId),
|
|
149
|
+
...records.map((record) => db
|
|
150
|
+
.prepare(`INSERT INTO node_vectors (node_id, chunk_key, version_id, fingerprint, passage)
|
|
151
|
+
SELECT ?, ?, ?, ?, ? WHERE EXISTS (
|
|
152
|
+
SELECT 1 FROM nodes
|
|
153
|
+
WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
|
|
154
|
+
)`)
|
|
155
|
+
.bind(target.nodeId, record.chunkKey, target.versionId, record.fingerprint, record.passage.slice(0, maxPassageCharacters), target.nodeId, target.versionId)),
|
|
156
|
+
]);
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* The record of one node, gone — because its vectors are (anchrd/intel#348).
|
|
160
|
+
*
|
|
161
|
+
* ⚠️ No guard at all, and the absence is deliberate in both directions. There is no
|
|
162
|
+
* `archived_at IS NOT NULL`: the state this write exists to prevent is a row that outlives the
|
|
163
|
+
* vector it names, and a node restored between the deletion and this statement would keep
|
|
164
|
+
* exactly that — a record claiming vectors that are gone, which the next pass reads as "already
|
|
165
|
+
* embedded" and skips, leaving those cards silently unfindable. And there is no version
|
|
166
|
+
* condition: `archivedNodeId` has already established which node this is, from that same
|
|
167
|
+
* version, and a second reading of a moving row is a second answer rather than a safer one.
|
|
168
|
+
*/
|
|
169
|
+
async deleteVectors(nodeId) {
|
|
170
|
+
await db.prepare("DELETE FROM node_vectors WHERE node_id = ?").bind(nodeId).run();
|
|
171
|
+
},
|
|
172
|
+
async markIndexed(versionId, occurredAt) {
|
|
173
|
+
await db
|
|
174
|
+
.prepare(`UPDATE node_index_state
|
|
175
|
+
SET status = 'indexed', attempt_count = attempt_count + 1,
|
|
176
|
+
last_error = NULL, updated_at = ?
|
|
177
|
+
WHERE version_id = ? AND EXISTS (
|
|
178
|
+
SELECT 1 FROM node_versions version
|
|
179
|
+
JOIN nodes node ON node.id = version.node_id
|
|
180
|
+
WHERE version.id = ? AND node.current_version_id = version.id
|
|
181
|
+
AND node.archived_at IS NULL
|
|
182
|
+
)`)
|
|
183
|
+
.bind(occurredAt, versionId, versionId)
|
|
184
|
+
.run();
|
|
185
|
+
},
|
|
186
|
+
async markError(versionId, message, occurredAt) {
|
|
187
|
+
await db
|
|
188
|
+
.prepare(`UPDATE node_index_state
|
|
189
|
+
SET status = 'error', attempt_count = attempt_count + 1,
|
|
190
|
+
last_error = ?, updated_at = ? WHERE version_id = ? AND EXISTS (
|
|
191
|
+
SELECT 1 FROM node_versions version
|
|
192
|
+
JOIN nodes node ON node.id = version.node_id
|
|
193
|
+
WHERE version.id = ? AND node.current_version_id = version.id
|
|
194
|
+
AND node.archived_at IS NULL
|
|
195
|
+
)`)
|
|
196
|
+
.bind(message.slice(0, 500), occurredAt, versionId, versionId)
|
|
197
|
+
.run();
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function createOAuthClientStore(db) {
|
|
2
|
+
return {
|
|
3
|
+
async get(issuer, redirectUri) {
|
|
4
|
+
const row = await db
|
|
5
|
+
.prepare("SELECT client_id FROM oauth_clients WHERE issuer = ? AND redirect_uri = ?")
|
|
6
|
+
.bind(issuer, redirectUri)
|
|
7
|
+
.first();
|
|
8
|
+
return row?.client_id ?? null;
|
|
9
|
+
},
|
|
10
|
+
async put(input) {
|
|
11
|
+
await db
|
|
12
|
+
.prepare(`INSERT OR IGNORE INTO oauth_clients (
|
|
13
|
+
issuer, redirect_uri, client_id, created_at
|
|
14
|
+
) VALUES (?, ?, ?, ?)`)
|
|
15
|
+
.bind(input.issuer, input.redirectUri, input.clientId, input.createdAt)
|
|
16
|
+
.run();
|
|
17
|
+
const stored = await db
|
|
18
|
+
.prepare("SELECT client_id FROM oauth_clients WHERE issuer = ? AND redirect_uri = ?")
|
|
19
|
+
.bind(input.issuer, input.redirectUri)
|
|
20
|
+
.first();
|
|
21
|
+
if (!stored)
|
|
22
|
+
throw new Error("OAuth client registration was not stored");
|
|
23
|
+
return stored.client_id;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|