@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.
Files changed (364) hide show
  1. package/README.md +496 -0
  2. package/bin/intel.mjs +134 -0
  3. package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
  4. package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
  5. package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
  6. package/dist/adapters/cloudflare/cloudflare.js +416 -0
  7. package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
  8. package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
  9. package/dist/adapters/content/content.d.ts +3 -0
  10. package/dist/adapters/content/content.js +128 -0
  11. package/dist/adapters/content/content.types.d.ts +38 -0
  12. package/dist/adapters/content/content.types.js +1 -0
  13. package/dist/adapters/db/db-audit.d.ts +62 -0
  14. package/dist/adapters/db/db-audit.js +150 -0
  15. package/dist/adapters/db/db-boards.d.ts +6 -0
  16. package/dist/adapters/db/db-boards.js +324 -0
  17. package/dist/adapters/db/db-feed.d.ts +30 -0
  18. package/dist/adapters/db/db-feed.js +276 -0
  19. package/dist/adapters/db/db-flows.d.ts +42 -0
  20. package/dist/adapters/db/db-flows.js +1311 -0
  21. package/dist/adapters/db/db-grants.d.ts +133 -0
  22. package/dist/adapters/db/db-grants.js +269 -0
  23. package/dist/adapters/db/db-indexing.d.ts +3 -0
  24. package/dist/adapters/db/db-indexing.js +200 -0
  25. package/dist/adapters/db/db-oauth.d.ts +3 -0
  26. package/dist/adapters/db/db-oauth.js +26 -0
  27. package/dist/adapters/db/db-prompts.d.ts +46 -0
  28. package/dist/adapters/db/db-prompts.js +144 -0
  29. package/dist/adapters/db/db.d.ts +6 -0
  30. package/dist/adapters/db/db.js +1687 -0
  31. package/dist/adapters/db/db.types.d.ts +16 -0
  32. package/dist/adapters/db/db.types.js +1 -0
  33. package/dist/adapters/document-converter/document-converter.d.ts +2 -0
  34. package/dist/adapters/document-converter/document-converter.js +26 -0
  35. package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
  36. package/dist/adapters/document-converter/document-converter.types.js +1 -0
  37. package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
  38. package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
  39. package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
  40. package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
  41. package/dist/adapters/index-queue/index-queue.d.ts +10 -0
  42. package/dist/adapters/index-queue/index-queue.js +12 -0
  43. package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
  44. package/dist/adapters/index-queue/index-queue.types.js +1 -0
  45. package/dist/adapters/json-schema/json-schema.d.ts +2 -0
  46. package/dist/adapters/json-schema/json-schema.js +77 -0
  47. package/dist/adapters/openid/openid.d.ts +5 -0
  48. package/dist/adapters/openid/openid.js +301 -0
  49. package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
  50. package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
  51. package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
  52. package/dist/adapters/remote-tools/remote-tools.js +129 -0
  53. package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
  54. package/dist/adapters/semantic-index/semantic-index.js +139 -0
  55. package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
  56. package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
  57. package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
  58. package/dist/adapters/session-cookie/session-cookie.js +87 -0
  59. package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
  60. package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
  61. package/dist/audit/audit.d.ts +2 -0
  62. package/dist/audit/audit.js +87 -0
  63. package/dist/audit/audit.types.d.ts +23 -0
  64. package/dist/audit/audit.types.js +1 -0
  65. package/dist/auth/auth.d.ts +2 -0
  66. package/dist/auth/auth.js +284 -0
  67. package/dist/auth/auth.types.d.ts +108 -0
  68. package/dist/auth/auth.types.js +1 -0
  69. package/dist/boards/boards.d.ts +44 -0
  70. package/dist/boards/boards.js +447 -0
  71. package/dist/boards/boards.types.d.ts +117 -0
  72. package/dist/boards/boards.types.js +1 -0
  73. package/dist/build/build.d.ts +2 -0
  74. package/dist/build/build.js +192 -0
  75. package/dist/build/build.types.d.ts +17 -0
  76. package/dist/build/build.types.js +1 -0
  77. package/dist/bundle/bundle.d.ts +4 -0
  78. package/dist/bundle/bundle.js +1194 -0
  79. package/dist/bundle/bundle.types.d.ts +33 -0
  80. package/dist/bundle/bundle.types.js +1 -0
  81. package/dist/cli/cli.d.ts +4 -0
  82. package/dist/cli/cli.js +262 -0
  83. package/dist/cli/cli.types.d.ts +14 -0
  84. package/dist/cli/cli.types.js +1 -0
  85. package/dist/contract/audit.d.ts +50 -0
  86. package/dist/contract/audit.js +72 -0
  87. package/dist/contract/board.d.ts +150 -0
  88. package/dist/contract/board.js +262 -0
  89. package/dist/contract/bundle.d.ts +147 -0
  90. package/dist/contract/bundle.js +139 -0
  91. package/dist/contract/contract.d.ts +112 -0
  92. package/dist/contract/contract.js +143 -0
  93. package/dist/contract/feed.d.ts +126 -0
  94. package/dist/contract/feed.js +149 -0
  95. package/dist/contract/flow-run.d.ts +346 -0
  96. package/dist/contract/flow-run.js +194 -0
  97. package/dist/contract/flow.d.ts +1057 -0
  98. package/dist/contract/flow.js +552 -0
  99. package/dist/contract/node.d.ts +551 -0
  100. package/dist/contract/node.js +456 -0
  101. package/dist/contract/share.d.ts +216 -0
  102. package/dist/contract/share.js +134 -0
  103. package/dist/contract/table.d.ts +171 -0
  104. package/dist/contract/table.js +117 -0
  105. package/dist/contract/tool.d.ts +122 -0
  106. package/dist/contract/tool.js +172 -0
  107. package/dist/feed/feed.d.ts +2 -0
  108. package/dist/feed/feed.js +77 -0
  109. package/dist/feed/feed.types.d.ts +23 -0
  110. package/dist/feed/feed.types.js +1 -0
  111. package/dist/flows/flows.d.ts +41 -0
  112. package/dist/flows/flows.js +2216 -0
  113. package/dist/flows/flows.types.d.ts +339 -0
  114. package/dist/flows/flows.types.js +1 -0
  115. package/dist/http/http.d.ts +9 -0
  116. package/dist/http/http.js +874 -0
  117. package/dist/http/http.types.d.ts +22 -0
  118. package/dist/http/http.types.js +1 -0
  119. package/dist/indexing/indexing.d.ts +6 -0
  120. package/dist/indexing/indexing.js +239 -0
  121. package/dist/indexing/indexing.types.d.ts +11 -0
  122. package/dist/indexing/indexing.types.js +1 -0
  123. package/dist/intel/intel.d.ts +3 -0
  124. package/dist/intel/intel.js +194 -0
  125. package/dist/intel/intel.types.d.ts +24 -0
  126. package/dist/intel/intel.types.js +1 -0
  127. package/dist/mcp/mcp.d.ts +2 -0
  128. package/dist/mcp/mcp.js +1292 -0
  129. package/dist/mcp/mcp.types.d.ts +25 -0
  130. package/dist/mcp/mcp.types.js +1 -0
  131. package/dist/nodes/document-links/document-links.d.ts +20 -0
  132. package/dist/nodes/document-links/document-links.js +57 -0
  133. package/dist/nodes/nodes.d.ts +2 -0
  134. package/dist/nodes/nodes.js +1667 -0
  135. package/dist/nodes/nodes.types.d.ts +511 -0
  136. package/dist/nodes/nodes.types.js +1 -0
  137. package/dist/prepare/prepare.d.ts +4 -0
  138. package/dist/prepare/prepare.js +16 -0
  139. package/dist/prepare/prepare.types.d.ts +9 -0
  140. package/dist/prepare/prepare.types.js +1 -0
  141. package/dist/prompts/prompts.d.ts +2 -0
  142. package/dist/prompts/prompts.js +65 -0
  143. package/dist/prompts/prompts.types.d.ts +71 -0
  144. package/dist/prompts/prompts.types.js +1 -0
  145. package/dist/shared/callers/callers.d.ts +28 -0
  146. package/dist/shared/callers/callers.js +30 -0
  147. package/dist/shared/csv/csv.d.ts +13 -0
  148. package/dist/shared/csv/csv.js +85 -0
  149. package/dist/shared/document-text/document-text.d.ts +21 -0
  150. package/dist/shared/document-text/document-text.js +31 -0
  151. package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
  152. package/dist/shared/gate-authorization/gate-authorization.js +26 -0
  153. package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
  154. package/dist/shared/grant-expiry/grant-expiry.js +26 -0
  155. package/dist/shared/intel-error/intel-error.d.ts +22 -0
  156. package/dist/shared/intel-error/intel-error.js +27 -0
  157. package/dist/shared/plain-title/plain-title.d.ts +16 -0
  158. package/dist/shared/plain-title/plain-title.js +45 -0
  159. package/dist/shared/problem-details/problem-details.d.ts +30 -0
  160. package/dist/shared/problem-details/problem-details.js +18 -0
  161. package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
  162. package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
  163. package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
  164. package/dist/shared/safe-return-path/safe-return-path.js +11 -0
  165. package/dist/shared/sha256/sha256.d.ts +2 -0
  166. package/dist/shared/sha256/sha256.js +14 -0
  167. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  168. package/dist/tools/tool-servers/tool-servers.js +177 -0
  169. package/dist/tools/tools.d.ts +2 -0
  170. package/dist/tools/tools.js +207 -0
  171. package/dist/tools/tools.types.d.ts +56 -0
  172. package/dist/tools/tools.types.js +1 -0
  173. package/examples/branding/favicon.svg +5 -0
  174. package/examples/branding/logo.svg +5 -0
  175. package/examples/branding/theme.css +16 -0
  176. package/examples/dev.vars.example +7 -0
  177. package/examples/intel.json +9 -0
  178. package/migrations/0000_intel_foundation.sql +203 -0
  179. package/migrations/0001_portal_native_tools.sql +21 -0
  180. package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
  181. package/migrations/0003_folder_permissions.sql +211 -0
  182. package/migrations/0004_subflow_runs.sql +14 -0
  183. package/migrations/0005_flow_node_cleanup.sql +28 -0
  184. package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
  185. package/migrations/0006_links_are_written_in_the_text.sql +20 -0
  186. package/migrations/0007_no_node_waits.sql +69 -0
  187. package/migrations/0008_three_layers.sql +130 -0
  188. package/migrations/0009_no_context_policy.sql +48 -0
  189. package/migrations/0010_no_run_waits_a_year.sql +27 -0
  190. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  191. package/migrations/0012_table_snapshots.sql +29 -0
  192. package/migrations/0017_a_vector_per_card.sql +38 -0
  193. package/migrations/0018_no_context_policy_at_last.sql +97 -0
  194. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  195. package/migrations/0020_cascade_purge_replay.sql +11 -0
  196. package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
  197. package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
  198. package/migrations/0023_a_board_and_its_tasks.sql +116 -0
  199. package/migrations/0024_the_archive_is_a_column.sql +33 -0
  200. package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
  201. package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
  202. package/migrations/0027_the_runs_of_every_flow.sql +27 -0
  203. package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
  204. package/package.json +118 -0
  205. package/src/contract/CLAUDE.md +152 -0
  206. package/src/contract/README.md +102 -0
  207. package/src/contract/audit.ts +85 -0
  208. package/src/contract/board.ts +306 -0
  209. package/src/contract/bundle.ts +157 -0
  210. package/src/contract/contract.ts +161 -0
  211. package/src/contract/feed.ts +165 -0
  212. package/src/contract/flow-run.ts +241 -0
  213. package/src/contract/flow.ts +675 -0
  214. package/src/contract/node.ts +583 -0
  215. package/src/contract/share.ts +196 -0
  216. package/src/contract/table.ts +178 -0
  217. package/src/contract/tool.ts +202 -0
  218. package/ui/README.md +110 -0
  219. package/ui/components.json +21 -0
  220. package/ui/index.html +13 -0
  221. package/ui/package.json +11 -0
  222. package/ui/scripts/lint-tokens.mjs +187 -0
  223. package/ui/src/access-summary/access-summary.tsx +385 -0
  224. package/ui/src/app/action-slot/action-slot.tsx +27 -0
  225. package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
  226. package/ui/src/app/app-tree/app-tree.tsx +1175 -0
  227. package/ui/src/app/app.tsx +87 -0
  228. package/ui/src/app/header-search/header-search.tsx +294 -0
  229. package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
  230. package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
  231. package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
  232. package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
  233. package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
  234. package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
  235. package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
  236. package/ui/src/app/tree-move/tree-move.tsx +427 -0
  237. package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
  238. package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
  239. package/ui/src/app/user-footer/user-footer.tsx +188 -0
  240. package/ui/src/app/view-menu/view-menu.tsx +115 -0
  241. package/ui/src/app-root/app-root.tsx +109 -0
  242. package/ui/src/app-root/app-root.types.ts +27 -0
  243. package/ui/src/archive/archive.tsx +417 -0
  244. package/ui/src/attachment-detail/attachment-detail.ts +52 -0
  245. package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
  246. package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
  247. package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
  248. package/ui/src/board/board-assignee/board-assignee.ts +48 -0
  249. package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
  250. package/ui/src/board/board-chip/board-chip.tsx +46 -0
  251. package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
  252. package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
  253. package/ui/src/board/board-data/board-data.ts +195 -0
  254. package/ui/src/board/board-data/board-data.types.ts +38 -0
  255. package/ui/src/board/board-dates/board-dates.ts +40 -0
  256. package/ui/src/board/board-dates/board-dates.tsx +105 -0
  257. package/ui/src/board/board-kanban/board-kanban.ts +162 -0
  258. package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
  259. package/ui/src/board/board-open-task/board-open-task.ts +32 -0
  260. package/ui/src/board/board-panel/board-panel.tsx +259 -0
  261. package/ui/src/board/board-settings/board-settings.tsx +226 -0
  262. package/ui/src/board/board-status/board-status.tsx +60 -0
  263. package/ui/src/board/board-stripes/board-stripes.ts +128 -0
  264. package/ui/src/board/board-table/board-table.ts +53 -0
  265. package/ui/src/board/board-table/board-table.tsx +610 -0
  266. package/ui/src/board/board-task/board-task.ts +108 -0
  267. package/ui/src/board/board-task/board-task.tsx +335 -0
  268. package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
  269. package/ui/src/branding/branding.tsx +17 -0
  270. package/ui/src/branding/custom-logo.ts +1 -0
  271. package/ui/src/branding/favicon.default.svg +5 -0
  272. package/ui/src/branding/favicon.svg +5 -0
  273. package/ui/src/components/ui/avatar.tsx +39 -0
  274. package/ui/src/components/ui/button.tsx +64 -0
  275. package/ui/src/components/ui/collapsible.tsx +20 -0
  276. package/ui/src/components/ui/command.tsx +160 -0
  277. package/ui/src/components/ui/dialog.tsx +143 -0
  278. package/ui/src/components/ui/dropdown-menu.tsx +187 -0
  279. package/ui/src/components/ui/input.tsx +21 -0
  280. package/ui/src/components/ui/popover.tsx +41 -0
  281. package/ui/src/components/ui/select.tsx +163 -0
  282. package/ui/src/components/ui/separator.tsx +26 -0
  283. package/ui/src/components/ui/sheet.tsx +136 -0
  284. package/ui/src/components/ui/sidebar.tsx +693 -0
  285. package/ui/src/components/ui/skeleton.tsx +13 -0
  286. package/ui/src/components/ui/switch.tsx +25 -0
  287. package/ui/src/components/ui/table.tsx +82 -0
  288. package/ui/src/components/ui/tabs.tsx +52 -0
  289. package/ui/src/components/ui/tooltip.tsx +51 -0
  290. package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
  291. package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
  292. package/ui/src/data/query-client.ts +18 -0
  293. package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
  294. package/ui/src/data/request-refusal/request-refusal.ts +51 -0
  295. package/ui/src/data/sign-in/sign-in.ts +97 -0
  296. package/ui/src/data/sign-in/sign-in.types.ts +15 -0
  297. package/ui/src/document-link/document-link.tsx +114 -0
  298. package/ui/src/document-markdown/document-markdown.ts +195 -0
  299. package/ui/src/editor-schema/editor-schema.ts +23 -0
  300. package/ui/src/entry-picker/entry-picker.tsx +238 -0
  301. package/ui/src/feed/feed.tsx +344 -0
  302. package/ui/src/file-preview/file-preview-view.tsx +496 -0
  303. package/ui/src/file-preview/file-preview.tsx +28 -0
  304. package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
  305. package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
  306. package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
  307. package/ui/src/file-preview/word-file-preview.tsx +5 -0
  308. package/ui/src/flow-runs/flow-runs.tsx +238 -0
  309. package/ui/src/flows/flows.tsx +1646 -0
  310. package/ui/src/flows/node-icon/node-icon.ts +34 -0
  311. package/ui/src/flows/node-palette/node-palette.tsx +214 -0
  312. package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
  313. package/ui/src/folder-contents/folder-contents.tsx +321 -0
  314. package/ui/src/folder-index/folder-index.ts +55 -0
  315. package/ui/src/frontmatter/frontmatter.tsx +193 -0
  316. package/ui/src/graph-pane/graph-pane.tsx +53 -0
  317. package/ui/src/hooks/use-mobile.ts +19 -0
  318. package/ui/src/i18n/custom.json +4 -0
  319. package/ui/src/i18n/de.json +609 -0
  320. package/ui/src/i18n/en.json +609 -0
  321. package/ui/src/i18n/es.json +609 -0
  322. package/ui/src/i18n/i18n-context.tsx +65 -0
  323. package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
  324. package/ui/src/i18n/i18n.ts +32 -0
  325. package/ui/src/i18n/i18n.types.ts +49 -0
  326. package/ui/src/kind-icon.ts +97 -0
  327. package/ui/src/lib/utils.ts +6 -0
  328. package/ui/src/main.tsx +68 -0
  329. package/ui/src/modal/modal.tsx +53 -0
  330. package/ui/src/node-details/node-details.tsx +545 -0
  331. package/ui/src/node-editor/node-editor.tsx +304 -0
  332. package/ui/src/node-graph/graph-notice.tsx +21 -0
  333. package/ui/src/node-graph/node-graph.ts +214 -0
  334. package/ui/src/node-graph/node-graph.tsx +295 -0
  335. package/ui/src/node-import/node-import.tsx +418 -0
  336. package/ui/src/node-save-error.ts +46 -0
  337. package/ui/src/node-source/node-source.tsx +128 -0
  338. package/ui/src/node-table/node-table.tsx +107 -0
  339. package/ui/src/nodes/nodes.tsx +240 -0
  340. package/ui/src/rename-resource/rename-resource.ts +80 -0
  341. package/ui/src/resolved-names/resolved-names.tsx +67 -0
  342. package/ui/src/resource-error.ts +33 -0
  343. package/ui/src/resource-menu/resource-menu.tsx +1156 -0
  344. package/ui/src/router/router-context.ts +6 -0
  345. package/ui/src/router/router.tsx +67 -0
  346. package/ui/src/router/router.types.ts +8 -0
  347. package/ui/src/router/selection-search.ts +88 -0
  348. package/ui/src/save-button/save-button.tsx +94 -0
  349. package/ui/src/section-hint/section-hint.tsx +40 -0
  350. package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
  351. package/ui/src/styles.css +189 -0
  352. package/ui/src/table-columns/table-columns.tsx +379 -0
  353. package/ui/src/theme/custom.css +1 -0
  354. package/ui/src/theme/theme-context.tsx +89 -0
  355. package/ui/src/theme/theme.ts +70 -0
  356. package/ui/src/time/relative-time.tsx +41 -0
  357. package/ui/src/time/time-context.tsx +78 -0
  358. package/ui/src/time/time.ts +165 -0
  359. package/ui/src/title-row/title-row.tsx +185 -0
  360. package/ui/src/tools/tools.tsx +546 -0
  361. package/ui/src/user-name/user-name.ts +61 -0
  362. package/ui/src/verbatim/verbatim.tsx +68 -0
  363. package/ui/tsconfig.json +17 -0
  364. package/ui/vite.config.ts +129 -0
@@ -0,0 +1,266 @@
1
+ import type {
2
+ BoardGetInput,
3
+ BoardTaskCreateInput,
4
+ BoardTaskUpdateInput,
5
+ BoardUpdateInput,
6
+ BoardView,
7
+ DirectoryPersonList,
8
+ } from "@contract/board.ts";
9
+ import type { BundleImportResult } from "@contract/bundle.ts";
10
+ import type { SessionUser } from "@contract/contract.ts";
11
+ import type { FeedListResponse } from "@contract/feed.ts";
12
+ import type {
13
+ ArchiveFlowInput,
14
+ CreateFlowInput,
15
+ Flow,
16
+ FlowDocument,
17
+ FlowList,
18
+ FlowPublishPreview,
19
+ FlowRequirements,
20
+ FlowValidation,
21
+ FlowVersionList,
22
+ ListFlowsInput,
23
+ PreviewFlowPublishInput,
24
+ PublishFlowInput,
25
+ PurgeFlowInput,
26
+ PurgeFlowResult,
27
+ RelationGraph,
28
+ RelationGraphScope,
29
+ SaveFlowVersionInput,
30
+ UpdateFlowInput,
31
+ } from "@contract/flow.ts";
32
+ import type {
33
+ CompleteFlowRunStepInput,
34
+ FlowRunHistory,
35
+ FlowRunList,
36
+ FlowRunStep,
37
+ ListFlowRunsInput,
38
+ StartFlowRunInput,
39
+ } from "@contract/flow-run.ts";
40
+ import type {
41
+ ArchiveNodeInput,
42
+ CreateNodeInput,
43
+ ListNodesInput,
44
+ Node,
45
+ NodeDocument,
46
+ NodeGraph,
47
+ NodeKind,
48
+ NodeLinkList,
49
+ NodeList,
50
+ NodeTable,
51
+ NodeVersionList,
52
+ PurgeNodeInput,
53
+ PurgeNodePreviewInput,
54
+ PurgeNodeResult,
55
+ ReindexResult,
56
+ ResolveNodeLinksInput,
57
+ ResolveNodeLinksResult,
58
+ SaveAttachmentInput,
59
+ SaveNodeVersionInput,
60
+ SearchInput,
61
+ SearchResult,
62
+ UpdateNodeInput,
63
+ } from "@contract/node.ts";
64
+ import type {
65
+ ResourceGrantList,
66
+ RevokeFlowGrantInput,
67
+ RevokeGrantInput,
68
+ RevokeGrantResult,
69
+ ShareFlowInput,
70
+ ShareInput,
71
+ ShareResult,
72
+ } from "@contract/share.ts";
73
+ import type {
74
+ AppendTableRowsInput,
75
+ AppendTableRowsResult,
76
+ DefineTableInput,
77
+ RedefineTableInput,
78
+ } from "@contract/table.ts";
79
+ import type { ToolCatalog, ToolServerCatalog } from "@contract/tool.ts";
80
+
81
+ // One row of the shared tree. Nodes and Flows share the folder, not their nature (ADR-0004), so
82
+ // this is a union that keeps each side's record whole — never a merged "node" that is a bit of both.
83
+ // ⚠️ `openable` means "has something to expand that THIS reader may see" (#59) — not "is of a kind
84
+ // that can be expanded". The difference is the whole point: an arrow on a row with nothing beneath
85
+ // it promises content that expanding does not deliver.
86
+ export type TreeEntry =
87
+ | {
88
+ type: "node";
89
+ id: string;
90
+ title: string;
91
+ kind: NodeKind;
92
+ openable: boolean;
93
+ node: Node;
94
+ }
95
+ | { type: "flow"; id: string; title: string; kind: "flow"; openable: boolean; flow: Flow };
96
+
97
+ export interface IntelDataProvider {
98
+ getSession(): Promise<SessionUser>;
99
+ /**
100
+ * Rebuild the whole search index (`intel/admin`).
101
+ *
102
+ * ⚠️ Answers as soon as the work is QUEUED, not when it is done — `queued` counts the nodes that
103
+ * will be read again, and the index catches up behind it. A screen that said "finished" here
104
+ * would be reporting the wrong thing.
105
+ */
106
+ reindexNodes(): Promise<ReindexResult>;
107
+ /**
108
+ * One board, in one answer: its columns and every card the caller may see, without the card
109
+ * bodies (#648).
110
+ *
111
+ * ⚠️ **One call, not one per card.** A screen that fetched each card separately would look
112
+ * identical and is the reason this endpoint exists — the test that guards it counts requests
113
+ * rather than pixels.
114
+ */
115
+ getBoard(input: BoardGetInput): Promise<BoardView>;
116
+ updateBoard(input: BoardUpdateInput): Promise<BoardView>;
117
+ createBoardTask(input: BoardTaskCreateInput): Promise<BoardView>;
118
+ // Every write answers with the WHOLE board rather than the row it changed: a drag moves one card
119
+ // and shifts nothing else, but a column's contents are what the screen draws, and re-deriving
120
+ // them from a single row is where a second, quietly different truth would start.
121
+ updateBoardTask(boardId: string, input: BoardTaskUpdateInput): Promise<BoardView>;
122
+ /**
123
+ * Who a card on THIS board may be given to (#700, D70).
124
+ *
125
+ * ⚠️ Board-scoped, and the same query against two boards can give two different answers. Only
126
+ * people who can actually open this one are offered.
127
+ */
128
+ searchBoardAssignees(boardId: string, query: string): Promise<DirectoryPersonList>;
129
+ /**
130
+ * What the people already on cards are called (#258, #700).
131
+ *
132
+ * ⚠️ NOT board-scoped, unlike the search above: somebody whose access was withdrawn is still who
133
+ * the card belongs to, and a card that read as unassigned would be a worse answer than the truth.
134
+ * Ids that cannot be named are absent from the answer rather than reported.
135
+ */
136
+ resolveNames(ids: string[]): Promise<DirectoryPersonList>;
137
+ listNodes(input?: Partial<ListNodesInput>): Promise<NodeList>;
138
+ // One level of the shared tree: the documents and the flows filed in the same folder, in one
139
+ // sorted list. Per level rather than recursive, so opening a folder is what costs a request.
140
+ listTreeChildren(parentId: string | null): Promise<TreeEntry[]>;
141
+ getNode(nodeId: string): Promise<NodeDocument>;
142
+ createNodes(input: CreateNodeInput): Promise<Node>;
143
+ getNodeGraph(limit?: number): Promise<NodeGraph>;
144
+ listNodeLinks(nodeId: string): Promise<NodeLinkList>;
145
+ // The titles of the documents a text links to, for the reader. A target they may not see, or one
146
+ // that is gone, is absent from the answer — the two look the same on purpose (#41).
147
+ resolveNodeLinks(input: ResolveNodeLinksInput): Promise<ResolveNodeLinksResult>;
148
+ saveNode(input: SaveNodeVersionInput): Promise<NodeDocument>;
149
+ saveNodeAttachment(input: SaveAttachmentInput): Promise<NodeDocument>;
150
+ /**
151
+ * A file uploaded as itself, not as base64 in a JSON body (#821).
152
+ *
153
+ * ⚠️ The `File` is handed over rather than read: the browser streams it from disk, so a 50 MB
154
+ * upload never stands in memory here. `saveNodeAttachment` beside it does the opposite by
155
+ * construction, which is why it keeps the smaller limit and why this is what the tree uses.
156
+ */
157
+ saveNodeAttachmentBytes(input: {
158
+ nodeId: string;
159
+ baseVersionId: string | null;
160
+ file: File;
161
+ idempotencyKey: string;
162
+ }): Promise<NodeDocument>;
163
+ getNodeAttachment(nodeId: string): Promise<Blob>;
164
+ // One subtree — or one single node — as the zip bundle the server streams (#136). `null` is the
165
+ // root: the whole installation as this caller may read it.
166
+ exportNodeBundle(nodeId: string | null): Promise<Blob>;
167
+ // The same bundle shape for a single flow, through the flow's own export route.
168
+ exportFlowBundle(flowId: string): Promise<Blob>;
169
+ // A zip bundle — or a folder zipped on the client — as a new subtree under the target folder
170
+ // (#137). `nodeId` null files at the root. Always new nodes; the server is all-or-nothing.
171
+ importNodeBundle(input: {
172
+ nodeId: string | null;
173
+ zip: Blob;
174
+ idempotencyKey: string;
175
+ }): Promise<BundleImportResult>;
176
+ // A table as columns and rows. The CSV stays canonical — `getNode` still answers with it,
177
+ // which is what the download uses — and this is the same bytes read by the server's one reader.
178
+ getNodeTable(nodeId: string): Promise<NodeTable>;
179
+ defineTable(input: DefineTableInput): Promise<NodeTable>;
180
+ appendTableRows(input: AppendTableRowsInput): Promise<AppendTableRowsResult>;
181
+ // Changing the header of a table that already has one (#135). The mapping is explicit — every new
182
+ // column names the current one whose cells fill it, or `null` for an empty one — because a new
183
+ // header without it would reinterpret every stored row under names nobody matched to the old ones.
184
+ //
185
+ // ⚠️ `baseVersionId` is the `versionId` the mapping was written against, and the server refuses
186
+ // with `version_conflict` when the table has moved on. Not optional: a mapping onto a header
187
+ // somebody else has since changed would move the wrong cells, silently.
188
+ redefineTable(input: RedefineTableInput): Promise<NodeTable>;
189
+
190
+ // Walks the browser through the portal's OAuth flow without asking anybody anything: Gate is the
191
+ // identity provider Cloudflare Access consumes, so a signed-in person is already known there
192
+ // (#60). It navigates away — the caller renders no button for it and gets no answer back.
193
+ startPortalSignIn(returnTo?: string): void;
194
+ listNodeVersions(nodeId: string): Promise<NodeVersionList>;
195
+ updateNode(input: UpdateNodeInput): Promise<Node>;
196
+ archiveNode(input: ArchiveNodeInput): Promise<Node>;
197
+ // ⚠️ The one way across this seam after which nothing is really left (#457). The title comes back
198
+ // because nothing can look it up afterwards.
199
+ purgeNode(input: PurgeNodeInput): Promise<PurgeNodeResult>;
200
+ previewNodePurge(
201
+ input: PurgeNodePreviewInput,
202
+ ): Promise<import("@contract/node.ts").PurgeNodePreview>;
203
+ purgeFlow(input: PurgeFlowInput): Promise<PurgeFlowResult>;
204
+ searchNodes(input: SearchInput): Promise<SearchResult>;
205
+ listGrants(resourceId: string): Promise<ResourceGrantList>;
206
+ listEffectiveAccess(resourceId: string): Promise<import("@contract/share.ts").ResourceAccessList>;
207
+ // The grant, and what the grant does not cover: the documents the flows in this folder read that
208
+ // the new principal still cannot. A warning, never a refusal (ADR-0004 §4).
209
+ shareNode(input: ShareInput): Promise<ShareResult>;
210
+ revokeGrant(input: RevokeGrantInput): Promise<RevokeGrantResult>;
211
+ // The same four for one flow (#530). Separate calls rather than a widened `resourceId`, because
212
+ // the id names a different table on the other side and the routes are separate there too.
213
+ listFlowGrants(flowId: string): Promise<ResourceGrantList>;
214
+ listEffectiveFlowAccess(flowId: string): Promise<import("@contract/share.ts").ResourceAccessList>;
215
+ // ⚠️ `ShareResult.unrunnable` is only ever filled in here: a grant on one flow does not reach the
216
+ // flows it calls, and this is where that gets said.
217
+ shareFlow(input: ShareFlowInput): Promise<ShareResult>;
218
+ revokeFlowGrant(input: RevokeFlowGrantInput): Promise<RevokeGrantResult>;
219
+ listFlows(input?: ListFlowsInput): Promise<FlowList>;
220
+ getFlow(flowId: string): Promise<FlowDocument>;
221
+ // What a flow calls, read out of its graph. It answers a different question from `listTreeChildren`
222
+ // and deliberately gives a different answer: a shared sub-flow is listed under every caller.
223
+ listFlowCalls(flowId: string): Promise<FlowList>;
224
+ // Would this flow start for me, right now. A question, not an action: nothing is created (#72).
225
+ validateFlow(flowId: string): Promise<FlowValidation>;
226
+ // What accesses what, for one level of the shared tree: a folder for its contents, a flow for
227
+ // itself. ⚠️ Only nodes the signed-in user may see come back, and one they may not is absent
228
+ // altogether — never a placeholder, because the edge into one would already say that it exists.
229
+ getRelationGraph(scope: RelationGraphScope, limit?: number): Promise<RelationGraph>;
230
+ // What a flow touches: the documents and tools its graph names. Documents the signed-in user may
231
+ // not see are counted rather than named, and nothing here claims anybody may reach them.
232
+ getFlowRequirements(flowId: string): Promise<FlowRequirements>;
233
+ createFlow(input: CreateFlowInput): Promise<Flow>;
234
+ updateFlow(input: UpdateFlowInput): Promise<Flow>;
235
+ // Archive a flow or take it back out. An archived flow leaves the tree, stops being startable and
236
+ // stops resolving as another flow's callee; its versions stay untouched.
237
+ archiveFlow(input: ArchiveFlowInput): Promise<Flow>;
238
+ saveFlow(input: SaveFlowVersionInput): Promise<FlowDocument>;
239
+ listFlowVersions(flowId: string): Promise<FlowVersionList>;
240
+ // Which version each sub-flow call will take once published, and which of them publishing
241
+ // freezes. Read before publishing, so the author agrees to the pins rather than discovering them.
242
+ previewFlowPublish(input: PreviewFlowPublishInput): Promise<FlowPublishPreview>;
243
+ publishFlow(input: PublishFlowInput): Promise<Flow>;
244
+ startFlow(input: StartFlowRunInput): Promise<FlowRunStep>;
245
+ // What this flow has done, newest first, one page at a time. ⚠️ It carries what a run did and
246
+ // never what it produced: a run reads nodes with the rights of whoever started it, so its
247
+ // result is not automatically readable for everyone who may read the flow.
248
+ // The activity feed, newest first (#742). `before` is the previous answer's `nextCursor`,
249
+ // handed back unchanged; `actor` narrows it to one person or one agent.
250
+ listFeed(input: {
251
+ limit: number;
252
+ before?: string | undefined;
253
+ actor?: string | undefined;
254
+ }): Promise<FeedListResponse>;
255
+ listFlowRuns(input: ListFlowRunsInput): Promise<FlowRunList>;
256
+ getFlowRun(runId: string): Promise<FlowRunStep>;
257
+ // The drill-down behind one run: every step it took, and the call chain it belongs to.
258
+ getFlowRunSteps(runId: string): Promise<FlowRunHistory>;
259
+ completeFlowStep(input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
260
+ // Reading the catalog is the whole of the tool surface here: calling a tool belongs to a flow
261
+ // or to the Intel MCP surface, not to the screen that shows what the portal offers.
262
+ listTools(): Promise<ToolCatalog>;
263
+ listToolServers(): Promise<ToolServerCatalog>;
264
+
265
+ logout(): Promise<void>;
266
+ }
@@ -0,0 +1,18 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ import { retryRequest } from "@/data/request-refusal/request-refusal.ts";
3
+
4
+ /**
5
+ * The one query client the application runs on.
6
+ *
7
+ * ⚠️ It exists as a function rather than as the literal `new QueryClient()` that stood in `main.tsx`
8
+ * so the policy in it can be exercised by a test (#430). A default nothing renders under is a
9
+ * default nothing checks, and this one decides how long a screen stays on "Loading…".
10
+ *
11
+ * ⚠️ Flat, and without a `.unit.ts` of its own on purpose: the only thing to assert here is that the
12
+ * returned client carries `retryRequest`, and what that predicate DOES is already covered next door
13
+ * in `request-refusal.unit.ts` — twice over, because two screen tests render against this very
14
+ * client. A test that restated the wiring would be the "pure wiring" the repository rules exclude.
15
+ */
16
+ export function createIntelQueryClient(): QueryClient {
17
+ return new QueryClient({ defaultOptions: { queries: { retry: retryRequest } } });
18
+ }
@@ -0,0 +1,36 @@
1
+ import type { Refusal } from "@/data/request-refusal/request-refusal.ts";
2
+ import { useI18n } from "@/i18n/i18n-context.tsx";
3
+
4
+ /**
5
+ * What a refusal is told with, on every surface that has one.
6
+ *
7
+ * ⚠️ A refusal is not a failure, and the difference is the whole of #430. Somebody whose share was
8
+ * revoked was left in front of "Loading…" — the worst answer of all, because it says the program is
9
+ * still working. They get a sentence and no retry button: the answer was final, and a button that
10
+ * changes nothing is an invitation to keep waiting.
11
+ *
12
+ * ⚠️ One sentence for `404`, whichever of its two reasons applies. Intel answers the same status
13
+ * for "no such node" and "not for you" on purpose — `requireVisible` in
14
+ * `packages/intel/src/nodes/nodes.ts` — and a screen that told them apart would undo that from the
15
+ * other side.
16
+ *
17
+ * ⚠️ One function rather than a block per branch, because two answers to one question on one
18
+ * surface IS #445. A screen's halves — the empty level, the selected item, the view beside it — are
19
+ * asked the same thing, and copies drift.
20
+ *
21
+ * ⚠️ **Why this became shared here and not one copy earlier.** `nodes.tsx` (#445) and `flows.tsx`
22
+ * (#557) each carried an identical version, and #557 wrote its reason on the second one: this
23
+ * repository builds an abstraction after the third time it hurts, not the second. #568 is the third
24
+ * time — `?view=graph` and `?view=runs` are the third and fourth surface asking for the same
25
+ * sentence — so what would have been a third copy is this file. The rule did not bend; it was met.
26
+ */
27
+ export function RefusalNotice({ refusal }: { refusal: Refusal }) {
28
+ const i18n = useI18n();
29
+ return (
30
+ <div role="status" className="grid flex-1 place-items-center p-8 text-center text-sm">
31
+ <p className="max-w-sm text-muted-foreground">
32
+ {i18n.t(refusal === "no-permission" ? "common.noPermission" : "common.noAccess")}
33
+ </p>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,51 @@
1
+ import { IntelRequestError } from "@/data/intel-data-provider/intel-data-provider.ts";
2
+
3
+ /**
4
+ * What a failed request already settles, as opposed to what it might answer differently next time.
5
+ *
6
+ * - `not-available` is Intel's `404`. It is deliberately the same answer for "there is no such node"
7
+ * and "you may not reach this node" — `requireVisible` in `packages/intel/src/nodes/nodes.ts`, held
8
+ * in place by "answers 404 for a foreign version and for an unreadable node" in `nodes.int.ts`.
9
+ * A screen must therefore not guess between them either: one sentence covers both, and that is why
10
+ * `no-permission` is a separate value rather than prose that could be shown for a `404` by mistake.
11
+ * - `no-permission` is a `403`. The server said the word itself, so repeating it reveals nothing.
12
+ * - `null` is everything else: a timeout, a `502`, a network that came back. Those are worth trying
13
+ * again, and the screen keeps its retry button for them.
14
+ */
15
+ export type Refusal = "not-available" | "no-permission";
16
+
17
+ /** The HTTP status the data provider recorded, or `null` where the failure carried none. */
18
+ export function statusOf(error: unknown): number | null {
19
+ return error instanceof IntelRequestError ? error.status : null;
20
+ }
21
+
22
+ export function refusalOf(error: unknown): Refusal | null {
23
+ switch (statusOf(error)) {
24
+ case 403:
25
+ return "no-permission";
26
+ case 404:
27
+ return "not-available";
28
+ default:
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * The retry policy every query in Intel runs under.
35
+ *
36
+ * ⚠️ A refusal is deterministic, and repeating it is what left the screen on "Loading…" after a
37
+ * share was revoked (#430). React Query's default is three retries with a growing delay, so a `404`
38
+ * kept the reader in front of a moving loading state for seconds — and a retry is **paused while
39
+ * the tab is unfocused** (`packages/intel/ui/CLAUDE.md`, #212), so "seconds" has no upper bound whenever
40
+ * the person looked somewhere else. The answer was there the whole time.
41
+ *
42
+ * ⚠️ `408` and `429` are the two `4xx` that DO change on their own — a timeout and a rate limit both
43
+ * say "not now" rather than "not you" — so they keep the retries. Everything else in the range is
44
+ * about this request as it was made, and making it again cannot alter that.
45
+ */
46
+ export function retryRequest(failureCount: number, error: unknown): boolean {
47
+ const status = statusOf(error);
48
+ const deterministic =
49
+ status !== null && status >= 400 && status < 500 && status !== 408 && status !== 429;
50
+ return !deterministic && failureCount < 3;
51
+ }
@@ -0,0 +1,97 @@
1
+ // Sending somebody to sign in is the right answer to a 401 — but only ONCE. If the surface still
2
+ // answers 401 after a successful sign-in, redirecting again produces a page that reloads forever:
3
+ // Gate signs the person in, the interface asks, the answer is 401, and round it goes. From the
4
+ // outside it looks like a broken deployment; from the inside every single step succeeded, which is
5
+ // what makes it expensive to find (#118).
6
+ //
7
+ // It happens whenever a token is valid to Gate but not accepted by `/api/v1` — a session that
8
+ // expires between the two calls, a bearer for a different audience, a service key that no longer
9
+ // holds. The same fix landed in anchrd/agents first; this is its counterpart here.
10
+
11
+ import type { SignIn, SignInDeps } from "./sign-in.types.ts";
12
+
13
+ const Marker = "intel:last-sign-in";
14
+
15
+ // Long enough to cover Gate's redirect chain, short enough that somebody who genuinely returns
16
+ // hours later gets a fresh attempt rather than an error page.
17
+ const LoopWindowMs = 30_000;
18
+
19
+ export function createSignIn(deps: SignInDeps): SignIn {
20
+ return {
21
+ attempt() {
22
+ // ⚠️ Every unusable marker counts as "no recent attempt", never as "loop" — missing, damaged,
23
+ // and dated in the future all mean the same thing here. The last one is not hypothetical: a
24
+ // clock that jumps backwards (time zone, NTP) leaves a marker ahead of `now`, and reading a
25
+ // negative age as "just tried" would lock somebody out over the wrong wall clock. Refusing
26
+ // costs an error page; allowing costs one extra reload, so the doubt goes to allowing.
27
+ const last = Number(deps.read(Marker) ?? 0);
28
+ const age = deps.now() - last;
29
+ if (Number.isFinite(last) && last > 0 && age >= 0 && age < LoopWindowMs) return false;
30
+
31
+ deps.write(Marker, String(deps.now()));
32
+ deps.go(deps.loginPath(deps.currentPath()));
33
+ return true;
34
+ },
35
+ };
36
+ }
37
+
38
+ /**
39
+ * The same thing wired to the browser.
40
+ *
41
+ * ⚠️ `sessionStorage`, not `localStorage`: the window is about one sign-in journey, and a marker
42
+ * that outlives the tab would meet the next visit as if it were still mid-loop. Both throw rather
43
+ * than return null when storage is blocked, so both accesses are guarded — see `attempt`.
44
+ */
45
+ export function createBrowserSignIn(loginPath: (returnTo?: string) => string): SignIn {
46
+ return createSignIn({
47
+ now: () => Date.now(),
48
+ read: (key) => {
49
+ try {
50
+ return window.sessionStorage.getItem(key);
51
+ } catch {
52
+ return null;
53
+ }
54
+ },
55
+ write: (key, value) => {
56
+ try {
57
+ window.sessionStorage.setItem(key, value);
58
+ } catch {
59
+ // Nothing to do: without a marker the next 401 redirects once more. One extra reload beats
60
+ // an error page for somebody who could have been let in.
61
+ }
62
+ },
63
+ go: (url) => window.location.assign(url),
64
+ currentPath: () => `${window.location.pathname}${window.location.search}`,
65
+ loginPath,
66
+ });
67
+ }
68
+
69
+ /**
70
+ * What to do with a 401, across all of them at once (#130).
71
+ *
72
+ * ⚠️ The reason this is a guard and not a call to `attempt()` at each 401: a first load without a
73
+ * session fires several requests together — session, tree, flows — and every one of them answers
74
+ * 401. The first starts the redirect; the rest arrive while the browser is still on this page,
75
+ * find the marker the first one just wrote, and read it as "we already tried, this is a loop". It
76
+ * is not — it is this very sign-in, still in flight.
77
+ *
78
+ * Without `leaving`, the interface refuses itself before it can leave, and nobody reaches the
79
+ * login at all. That is worse than the loop this whole mechanism exists to prevent: a loop still
80
+ * eventually shows a login, a self-refusal never does.
81
+ */
82
+ export function createSignInGuard(signIn: SignIn, onRefused: () => void): () => void {
83
+ let leaving = false;
84
+ // Latches for its own reason: once the surface has turned down a freshly issued credential,
85
+ // later 401s say nothing new, and repainting for each would flicker the screen once per request
86
+ // the page had in flight.
87
+ let refused = false;
88
+ return () => {
89
+ if (leaving || refused) return;
90
+ if (signIn.attempt()) {
91
+ leaving = true;
92
+ return;
93
+ }
94
+ refused = true;
95
+ onRefused();
96
+ };
97
+ }
@@ -0,0 +1,15 @@
1
+ export interface SignInDeps {
2
+ now(): number;
3
+ read(key: string): string | null;
4
+ write(key: string, value: string): void;
5
+ go(url: string): void;
6
+ currentPath(): string;
7
+ loginPath(returnTo?: string): string;
8
+ }
9
+
10
+ export interface SignIn {
11
+ // True when it sent the person away, false when it refused to because it just did. A caller that
12
+ // gets `false` must show something rather than wait — the redirect that would have explained
13
+ // itself is exactly what is not happening.
14
+ attempt(): boolean;
15
+ }
@@ -0,0 +1,114 @@
1
+ import { createReactInlineContentSpec } from "@blocknote/react";
2
+ import { DocumentLinkInlineType } from "@contract/node.ts";
3
+ import { FileText, Link2Off } from "lucide-react";
4
+ import { createContext, useContext } from "react";
5
+
6
+ /**
7
+ * What a document link resolves to for the person reading it.
8
+ *
9
+ * ⚠️ `titles` holds only the documents this reader may see. A missing entry is the whole answer for
10
+ * both of the reasons an entry can be missing — the target is gone, or it is not theirs to reach —
11
+ * and nothing here may ever be given a way to tell those apart. `pending` exists so the editor can
12
+ * hold back the difference between "not resolved yet" and "does not resolve", which would otherwise
13
+ * flash a broken link at every reader for one render.
14
+ */
15
+ export interface DocumentLinkResolution {
16
+ titles: ReadonlyMap<string, string>;
17
+ pending: boolean;
18
+ // The one word an unresolved link says. It travels in the context because a BlockNote inline
19
+ // element is rendered by the editor rather than by a view that could reach the i18n catalog.
20
+ unresolvedLabel: string;
21
+ }
22
+
23
+ const DocumentLinkContext = createContext<DocumentLinkResolution>({
24
+ titles: new Map(),
25
+ pending: false,
26
+ unresolvedLabel: "Unavailable document",
27
+ });
28
+
29
+ export const DocumentLinkProvider = DocumentLinkContext.Provider;
30
+
31
+ export function useDocumentLinkResolution(): DocumentLinkResolution {
32
+ return useContext(DocumentLinkContext);
33
+ }
34
+
35
+ /**
36
+ * One document link inside the text (#41).
37
+ *
38
+ * ⚠️ The element stores the target's ID and nothing else. A stored title would be wrong the moment
39
+ * the target is renamed and a stored path the moment it is moved — and both would put a name into a
40
+ * document that any reader of it could then read, whether or not they may reach the target.
41
+ *
42
+ * ⚠️ An unresolved link says that it does not resolve and nothing more: no title, no path, no ID on
43
+ * screen, no hint of which of the two reasons applies. That is the whole of the leak this feature
44
+ * could have introduced, and the reason the broken form carries no data at all.
45
+ */
46
+ export function DocumentLinkText({ nodeId }: { nodeId: string }) {
47
+ const { titles, pending, unresolvedLabel } = useDocumentLinkResolution();
48
+ const title = titles.get(nodeId);
49
+ if (title === undefined) {
50
+ return (
51
+ <span
52
+ data-document-link={pending ? "pending" : "unresolved"}
53
+ className={
54
+ pending
55
+ ? "inline-flex items-center gap-1 rounded-sm px-1 text-muted-foreground"
56
+ : "inline-flex items-center gap-1 rounded-sm bg-destructive/10 px-1 text-destructive line-through"
57
+ }
58
+ >
59
+ {pending ? null : <Link2Off aria-hidden="true" className="size-3.5" />}
60
+ {pending ? "…" : unresolvedLabel}
61
+ </span>
62
+ );
63
+ }
64
+ return (
65
+ <span
66
+ data-document-link="resolved"
67
+ data-node-id={nodeId}
68
+ className="inline-flex items-center gap-1 rounded-sm bg-muted px-1 text-foreground"
69
+ >
70
+ <FileText aria-hidden="true" className="size-3.5" />
71
+ {title}
72
+ </span>
73
+ );
74
+ }
75
+
76
+ export const documentLinkSpec = createReactInlineContentSpec(
77
+ {
78
+ type: DocumentLinkInlineType,
79
+ propSchema: { nodeId: { default: "" } },
80
+ // The link has no editable text of its own: what it reads is the target's title, and a title
81
+ // somebody could type over would stop following the target the first time they did (#41).
82
+ content: "none",
83
+ },
84
+ {
85
+ render: (props) => <DocumentLinkText nodeId={String(props.inlineContent.props.nodeId)} />,
86
+ },
87
+ );
88
+
89
+ /**
90
+ * The IDs a stored BlockNote document links to.
91
+ *
92
+ * The server reads the same thing out of the same content when the document is saved; this side
93
+ * needs it too, to ask which of them it may show a title for.
94
+ */
95
+ export function documentLinkIds(blocks: unknown): string[] {
96
+ const found = new Set<string>();
97
+ const walk = (value: unknown): void => {
98
+ if (Array.isArray(value)) {
99
+ for (const entry of value) walk(entry);
100
+ return;
101
+ }
102
+ if (typeof value !== "object" || value === null) return;
103
+ const record = value as Record<string, unknown>;
104
+ if (record.type === DocumentLinkInlineType) {
105
+ const props = record.props as Record<string, unknown> | undefined;
106
+ const nodeId = props?.nodeId;
107
+ if (typeof nodeId === "string" && nodeId.length > 0) found.add(nodeId);
108
+ }
109
+ walk(record.content);
110
+ walk(record.children);
111
+ };
112
+ walk(blocks);
113
+ return [...found];
114
+ }