@fungi.computer/inkcap 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +106 -2
- package/backend/document-activity.ts +77 -0
- package/backend/documents-changed.ts +31 -0
- package/backend/gills.ts +55 -0
- package/backend/index.ts +52 -0
- package/backend/methods.ts +194 -0
- package/backend/test/cloudflare-workers.fixture.ts +1 -0
- package/backend/test/document-marker.test.ts +128 -0
- package/backend/test/gills.test.ts +156 -0
- package/backend/test/methods.test.ts +456 -0
- package/backend/tsconfig.json +14 -0
- package/backend/vitest.config.ts +12 -0
- package/dist/assets/Anton-5.3.0-latin-Byf51wtH.woff2 +0 -0
- package/dist/assets/BricolageGrotesque-VF-latin-Cre6nC2_.woff2 +0 -0
- package/dist/assets/DepartureMono-Regular-2BZob_Zz.woff2 +0 -0
- package/dist/assets/FileTab-bwd_G6y3.js +2078 -0
- package/dist/assets/Fraunces-Italic-VF-latin-D2JnYqd-.woff2 +0 -0
- package/dist/assets/Fraunces-VF-latin-CFFu7zhK.woff2 +0 -0
- package/dist/assets/GrenzeGotisch-VF-latin-BhUVw2FV.woff2 +0 -0
- package/dist/assets/InkcapFileTree-Cx3HsaWr.js +1 -0
- package/dist/assets/InstrumentSans-Italic-VF-latin-BlcgWI5J.woff2 +0 -0
- package/dist/assets/InstrumentSans-VF-latin-BbzFLZTg.woff2 +0 -0
- package/dist/assets/MapleMono-VF-CCDrizxw.woff2 +0 -0
- package/dist/assets/NothingYouCouldDo-Regular-B7VfwEBS.woff2 +0 -0
- package/dist/assets/Nunito-VF-8c6a9bb-cyrillic-BsWUdSU9.woff2 +0 -0
- package/dist/assets/Nunito-VF-8c6a9bb-cyrillic-ext-Cb1j0nGJ.woff2 +0 -0
- package/dist/assets/Nunito-VF-8c6a9bb-latin-BYs3GYNf.woff2 +0 -0
- package/dist/assets/Nunito-VF-8c6a9bb-latin-ext-0RvtKGDy.woff2 +0 -0
- package/dist/assets/Nunito-VF-8c6a9bb-vietnamese-cS2dauUA.woff2 +0 -0
- package/dist/assets/abap-CLvhMVsD.js +1 -0
- package/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/dist/assets/ada-C5qYipkI.js +1 -0
- package/dist/assets/ahk-CA9gdOdF.js +1 -0
- package/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/dist/assets/angular-html-B8mrqUL5.js +1 -0
- package/dist/assets/angular-ts-Bzix210W.js +1 -0
- package/dist/assets/apache-U0d_L8uA.js +1 -0
- package/dist/assets/apex-KFGFyKwG.js +1 -0
- package/dist/assets/apl-Dhin1L29.js +1 -0
- package/dist/assets/applescript-CCn79oCD.js +1 -0
- package/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/dist/assets/astro-B-4NNgOP.js +1 -0
- package/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/dist/assets/awk-BWXHIvNe.js +1 -0
- package/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/dist/assets/beancount-D-usSTwE.js +1 -0
- package/dist/assets/berry-DKpUyyne.js +1 -0
- package/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/dist/assets/blade-C6Gedziy.js +1 -0
- package/dist/assets/bsl-_VjlB9mv.js +1 -0
- package/dist/assets/c-iCihek07.js +1 -0
- package/dist/assets/c3-D0apG41V.js +1 -0
- package/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/dist/assets/cairo-DLTphjLi.js +1 -0
- package/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/dist/assets/chapel-DmtgEXO_.js +1 -0
- package/dist/assets/clarity-CtfBBspg.js +1 -0
- package/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/dist/assets/cobol-Cw2-KvLW.js +1 -0
- package/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/dist/assets/coffee-CiiSLhB6.js +1 -0
- package/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/dist/assets/coq-DuBRPxYg.js +1 -0
- package/dist/assets/cpp-71xkOQct.js +1 -0
- package/dist/assets/crystal-DGPAtLeY.js +1 -0
- package/dist/assets/csharp-oqKa8noW.js +1 -0
- package/dist/assets/css-CNuuLpMC.js +1 -0
- package/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/dist/assets/cue-CE9AQfxI.js +1 -0
- package/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/dist/assets/d-qD-0Kul2.js +1 -0
- package/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/dist/assets/dart-CnvKMtbv.js +1 -0
- package/dist/assets/dax-BkyTk9wS.js +1 -0
- package/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/dist/assets/dialog-CWwO4mBD.js +71 -0
- package/dist/assets/diff-woXpYk--.js +1 -0
- package/dist/assets/docker-IyjqRm3v.js +1 -0
- package/dist/assets/dotenv-_5a1GRtc.js +1 -0
- package/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/dist/assets/edge-DCY0a-ow.js +1 -0
- package/dist/assets/elixir-DP304Xio.js +1 -0
- package/dist/assets/elm-o4NJxfws.js +1 -0
- package/dist/assets/emacs-lisp-c_oH4hRZ.js +1 -0
- package/dist/assets/erb-C7MPl934.js +1 -0
- package/dist/assets/erlang-Cphh6RMH.js +1 -0
- package/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/dist/assets/fish-BJitypiv.js +1 -0
- package/dist/assets/fluent-C03EYrpw.js +1 -0
- package/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/dist/assets/fsharp-D13ZGOAj.js +1 -0
- package/dist/assets/gdresource-xMC7jr7x.js +1 -0
- package/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/dist/assets/genie-CV2tkWYe.js +1 -0
- package/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/dist/assets/git-commit-BSykSTBG.js +1 -0
- package/dist/assets/git-rebase-BDPpS8w6.js +1 -0
- package/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/dist/assets/glimmer-js-BONyzAsu.js +1 -0
- package/dist/assets/glimmer-ts-f9c0GCOe.js +1 -0
- package/dist/assets/glsl-D5MA2a_X.js +1 -0
- package/dist/assets/gn-ilITqXS6.js +1 -0
- package/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/dist/assets/go-rLFTqkRN.js +1 -0
- package/dist/assets/graphql-Ctjuxyvl.js +1 -0
- package/dist/assets/groovy-CacY0gHj.js +1 -0
- package/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/dist/assets/hack-Dh_ZLRbs.js +1 -0
- package/dist/assets/haml-CwEpbK1d.js +1 -0
- package/dist/assets/handlebars-ryMP1rBR.js +1 -0
- package/dist/assets/haskell-D8IpX4py.js +1 -0
- package/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/dist/assets/hcl-Dh228itO.js +1 -0
- package/dist/assets/hjson-CxZEssPk.js +1 -0
- package/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/dist/assets/houston-CsvMBhTu.js +1 -0
- package/dist/assets/html-XwrE-1r6.js +1 -0
- package/dist/assets/html-derivative-i4STDFnl.js +1 -0
- package/dist/assets/http-kmH8AyfE.js +1 -0
- package/dist/assets/hurl-DCZHXKU4.js +1 -0
- package/dist/assets/hxml-Dcm88srW.js +1 -0
- package/dist/assets/hy-CZbG8q4J.js +1 -0
- package/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/dist/assets/index-BNEzthyN.js +1279 -0
- package/dist/assets/index-ByFi_OY2.css +2 -0
- package/dist/assets/ini-B5eOa1yu.js +1 -0
- package/dist/assets/java-DdHNQ9hk.js +1 -0
- package/dist/assets/javascript-obGGvMPs.js +1 -0
- package/dist/assets/jinja-B-tLrNsM.js +1 -0
- package/dist/assets/jison-CBjhYtSs.js +1 -0
- package/dist/assets/json-DAgxKhMr.js +1 -0
- package/dist/assets/json5-BR5RXkoi.js +1 -0
- package/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/dist/assets/jsx-gTgmArGx.js +1 -0
- package/dist/assets/julia-CXJVy74_.js +1 -0
- package/dist/assets/just-B9Xn_Dp4.js +1 -0
- package/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/dist/assets/latex-o9DRzaEe.js +1 -0
- package/dist/assets/lean-CewbzKMR.js +1 -0
- package/dist/assets/less-DVTAwKKz.js +1 -0
- package/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/dist/assets/liquid-G0MH4h_m.js +1 -0
- package/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/dist/assets/log-BNLmms1o.js +1 -0
- package/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/dist/assets/lua-FONpmTUP.js +1 -0
- package/dist/assets/luau-C2pBCSgt.js +1 -0
- package/dist/assets/make-Dixweg8N.js +1 -0
- package/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/assets/marko-DV5GrDmo.js +1 -0
- package/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/dist/assets/mdc-BKfPFPZw.js +1 -0
- package/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/dist/assets/monokai-B_bV9PQ9.js +1 -0
- package/dist/assets/moonbit-DgjMoLWr.js +1 -0
- package/dist/assets/move-BYnGhZoM.js +1 -0
- package/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/dist/assets/nextflow-BJtWHP5T.js +1 -0
- package/dist/assets/nextflow-groovy-DJMQeKeT.js +1 -0
- package/dist/assets/nginx-BTMw9COX.js +1 -0
- package/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/dist/assets/nim-B6yJ_yaC.js +1 -0
- package/dist/assets/nix-IvuFDN5E.js +1 -0
- package/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/dist/assets/nushell-DHYfO66s.js +1 -0
- package/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/dist/assets/odin-B1RWQWA5.js +1 -0
- package/dist/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/dist/assets/org-Ddi8Gs6E.js +1 -0
- package/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/dist/assets/perl-DEgXgjuC.js +1 -0
- package/dist/assets/php-B9UPq-vz.js +1 -0
- package/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/dist/assets/po-BiJDBrnU.js +1 -0
- package/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/dist/assets/polar-C7UOKdEL.js +1 -0
- package/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/dist/assets/powershell-CCSX75yM.js +1 -0
- package/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/dist/assets/proto-DB4EqR-F.js +1 -0
- package/dist/assets/pug-DkgDHZci.js +1 -0
- package/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/dist/assets/python-gzcpVVnB.js +1 -0
- package/dist/assets/qml-_a-QhfPD.js +1 -0
- package/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/dist/assets/r-BHcO8Vjq.js +1 -0
- package/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/dist/assets/raku-B3gFvitq.js +1 -0
- package/dist/assets/razor-BMJ0eI3p.js +1 -0
- package/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/dist/assets/red-DX1BQOs8.js +1 -0
- package/dist/assets/reg-CRGYupPL.js +1 -0
- package/dist/assets/regexp-DR20uMnK.js +1 -0
- package/dist/assets/rel-BtDbiS_P.js +1 -0
- package/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/assets/ron-VUp2lXgN.js +1 -0
- package/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/dist/assets/rst-BQCbfb9V.js +1 -0
- package/dist/assets/ruby-B8JcIHv0.js +1 -0
- package/dist/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/assets/sas-CBpMWA2M.js +1 -0
- package/dist/assets/sass-DXrisJhu.js +1 -0
- package/dist/assets/scala-BiFt_7mu.js +1 -0
- package/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/dist/assets/scss-YesllqfO.js +1 -0
- package/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/dist/assets/shaderlab-TOUzSsQk.js +1 -0
- package/dist/assets/shellscript-Da78Ox-3.js +1 -0
- package/dist/assets/shellsession-CVsQGYfZ.js +1 -0
- package/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/dist/assets/solarized-dark-UdsmMDCL.js +1 -0
- package/dist/assets/solarized-light-C1IUL_tW.js +1 -0
- package/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/dist/assets/soy-DEd-qcad.js +1 -0
- package/dist/assets/sparql-D_iOobhT.js +1 -0
- package/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/dist/assets/sql-DdD7vxS4.js +1 -0
- package/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/dist/assets/stata-CMv7m7Gi.js +1 -0
- package/dist/assets/stylus-B6D30XZt.js +1 -0
- package/dist/assets/surrealql-BZoq2lXR.js +1 -0
- package/dist/assets/svelte-Bj3Hmc9F.js +1 -0
- package/dist/assets/swift-CyEgAFGc.js +1 -0
- package/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/dist/assets/systemd-BxMlprV5.js +1 -0
- package/dist/assets/talonscript-CohzipZa.js +1 -0
- package/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/dist/assets/templ-TjxbArHn.js +1 -0
- package/dist/assets/terraform-DswuEJGm.js +1 -0
- package/dist/assets/tex-BK-H2Wn4.js +1 -0
- package/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/dist/assets/toml-CcmNWLt0.js +1 -0
- package/dist/assets/ts-tags-Cc5do7zu.js +1 -0
- package/dist/assets/tsv-sltzmVWM.js +1 -0
- package/dist/assets/tsx-CUOciP0b.js +1 -0
- package/dist/assets/turtle-ByJddavk.js +1 -0
- package/dist/assets/twig-BVSnuTf-.js +1 -0
- package/dist/assets/typescript-BydbNFcO.js +1 -0
- package/dist/assets/typespec-BRdr0IET.js +1 -0
- package/dist/assets/typst-C40W_gC6.js +1 -0
- package/dist/assets/useForm-CuiUTLkb.js +1 -0
- package/dist/assets/v-DBj4K9d_.js +1 -0
- package/dist/assets/vala-zf12oZj6.js +1 -0
- package/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/dist/assets/viml-DvXPmvsu.js +1 -0
- package/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/dist/assets/vue-BNQF7r6F.js +1 -0
- package/dist/assets/vue-html-DkKkQnZL.js +1 -0
- package/dist/assets/vue-vine-DFaKYU4X.js +1 -0
- package/dist/assets/vyper-CgoNMtux.js +1 -0
- package/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/dist/assets/wit-DdvCle-K.js +1 -0
- package/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/dist/assets/xml-BlnrEAEy.js +1 -0
- package/dist/assets/xsl-BOPS8gLt.js +1 -0
- package/dist/assets/yaml-Du_6cdrH.js +1 -0
- package/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/dist/assets/zig-CMLA9XwU.js +1 -0
- package/dist/index.html +15 -0
- package/package.json +225 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jamstack Consulting LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Inkcap (`@fungi.computer/inkcap`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Inkcap edits the files on your Computer: a file explorer, tabs and a code editor
|
|
4
|
+
in the browser.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
Inkcap is one of the starter Apps of
|
|
9
|
+
[Oyster](https://github.com/fungi-computer/oyster), the open App host that runs
|
|
10
|
+
on your own Cloudflare account. On Oyster, the Computer is a Linux workspace in
|
|
11
|
+
a Cloudflare Container, and Inkcap reads and writes its `/workspace`.
|
|
12
|
+
|
|
13
|
+
## What you can do
|
|
14
|
+
|
|
15
|
+
- Browse the Computer's folders and open files in tabs.
|
|
16
|
+
- Edit text with syntax highlighting, and save with **Ctrl+S** / **⌘S**.
|
|
17
|
+
- Preview images (PNG, JPEG, GIF, WebP, SVG and AVIF) and video (MP4 and WebM).
|
|
18
|
+
- Turn on **Preview Markdown** for any `.md` file.
|
|
19
|
+
- Create files and folders, **Save file as** another path, or download a draft
|
|
20
|
+
without writing it.
|
|
21
|
+
- Resolve conflicts: when a file changed on the Computer after you opened it,
|
|
22
|
+
choose **Keep mine**, **Reload theirs** or **Compare** before anything is
|
|
23
|
+
overwritten.
|
|
24
|
+
|
|
25
|
+
Closing a tab or the App with unsaved edits asks whether to save, discard or
|
|
26
|
+
cancel. On a phone, **Open file explorer** brings the explorer over the editor.
|
|
27
|
+
|
|
28
|
+
<img alt="Inkcap on a phone in the light theme, with no file open and a New draft button" src="docs/media/new-draft-phone.png" width="300">
|
|
29
|
+
|
|
30
|
+
## Get it
|
|
31
|
+
|
|
32
|
+
Inkcap installs with the other starter Apps. On a deployed Oyster (see the
|
|
33
|
+
[Oyster quick start](https://github.com/fungi-computer/oyster#quick-start-deploy-your-own-oyster)),
|
|
34
|
+
run:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
export OYSTER_URL=https://oyster.<your-subdomain>.workers.dev
|
|
38
|
+
export OYSTER_OWNER_KEY=<your owner key>
|
|
39
|
+
npx oyster install-apps
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The installer clones this repository and the six others, seeds each on your
|
|
43
|
+
Oyster with `git push`, builds them on your Computer and installs them. Sign in
|
|
44
|
+
at `$OYSTER_URL/login` with the owner key and open Inkcap from the Tourtois
|
|
45
|
+
desktop. Run `install-apps` again to update; each App keeps its data.
|
|
46
|
+
|
|
47
|
+
## Change it
|
|
48
|
+
|
|
49
|
+
Every installed App is also a template you can fork on your own Oyster. Fork
|
|
50
|
+
Inkcap into a Bonsai repository and clone it with ordinary `git`:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
curl -X POST -H "Authorization: Bearer $OYSTER_OWNER_KEY" -H 'content-type: application/json' \
|
|
54
|
+
-d '{"appId":"app_inkcap","name":"my-inkcap"}' \
|
|
55
|
+
$OYSTER_URL/apps/bonsai/methods/apps.fork
|
|
56
|
+
git -c http.extraHeader="Authorization: Bearer $OYSTER_OWNER_KEY" \
|
|
57
|
+
clone $OYSTER_URL/git/bonsai/my-inkcap.git
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The reply names your fork's App id and repository. Push your edits, open the
|
|
61
|
+
repository in Bonsai and choose **Settings**, then **Build & install** with that
|
|
62
|
+
App id and your commit as `git:<sha>`. Install the Release at `inkcap` to
|
|
63
|
+
replace the running Inkcap, or at a new id to keep both.
|
|
64
|
+
|
|
65
|
+
To work from GitHub instead, fork this repository and point the installer at
|
|
66
|
+
your forks with `--repositories <base URL>` and `--ref <branch or tag>`. The
|
|
67
|
+
installer reads all seven starter Apps from that base, so fork them under one
|
|
68
|
+
account.
|
|
69
|
+
|
|
70
|
+
## Agent tools
|
|
71
|
+
|
|
72
|
+
Agents call Inkcap through Oyster's MCP endpoint, or at
|
|
73
|
+
`POST $OYSTER_URL/apps/inkcap/methods/<name>` with a key; see
|
|
74
|
+
[Git, keys and MCP](https://github.com/fungi-computer/oyster#git-keys-and-mcp).
|
|
75
|
+
|
|
76
|
+
| Method | What it does |
|
|
77
|
+
| ---------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
78
|
+
| `listFiles({path?, limit?, offset?})` | Reads one page of a directory, up to 256 entries. |
|
|
79
|
+
| `readFile({path, byteOffset?, byteLength?})` | Reads up to 128 KiB of a file as base64. |
|
|
80
|
+
| `writeFile({path, content, expectedContent?})` | Writes UTF-8 text, optionally only if the file still holds `expectedContent`. |
|
|
81
|
+
| `documents.recent` | Lists the documents most recently opened and edited in Inkcap. |
|
|
82
|
+
|
|
83
|
+
Paths are absolute and cannot contain `..`. A read-only attachment cannot write,
|
|
84
|
+
and the host picks the Computer: no argument selects one.
|
|
85
|
+
|
|
86
|
+
## Build and check
|
|
87
|
+
|
|
88
|
+
Inkcap is a React App built with Vite. The file tree and editor come from
|
|
89
|
+
`@pierre/trees` and `@pierre/diffs`, and the controls from
|
|
90
|
+
[Caps](https://github.com/fungi-computer/caps).
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pnpm install
|
|
94
|
+
pnpm build
|
|
95
|
+
pnpm typecheck
|
|
96
|
+
pnpm test
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
[src/InkcapApp.tsx](src/InkcapApp.tsx) owns the tabs, the selected Computer and
|
|
100
|
+
the close checks. [src/useFileDocument.ts](src/useFileDocument.ts) owns each
|
|
101
|
+
draft, its saved baseline and conflict recovery. The agent tools are in
|
|
102
|
+
[backend/methods.ts](backend/methods.ts), and [package.json](package.json)
|
|
103
|
+
declares the permissions the App asks for.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import {
|
|
3
|
+
documentActivitySchema,
|
|
4
|
+
documentIds,
|
|
5
|
+
documentsRecentSchema,
|
|
6
|
+
} from "../src/document-activity-contract.js";
|
|
7
|
+
import {
|
|
8
|
+
createReadCopies,
|
|
9
|
+
type ReadCopySql,
|
|
10
|
+
} from "@fungi.computer/agar/read-copy";
|
|
11
|
+
|
|
12
|
+
/** Bounded owner SQLite rows for each actor's recent Computer documents. */
|
|
13
|
+
export type DocumentActivityStore = ReturnType<typeof createReadCopies>;
|
|
14
|
+
export function createDocumentActivityStore(
|
|
15
|
+
sql: ReadCopySql,
|
|
16
|
+
): DocumentActivityStore {
|
|
17
|
+
return createReadCopies(sql, {
|
|
18
|
+
version: 1,
|
|
19
|
+
maxRows: 256,
|
|
20
|
+
// One row holds up to 100 entries whose paths reach 4,096 characters.
|
|
21
|
+
maxValueBytes: 4 * 1024 * 1024,
|
|
22
|
+
// Activity rows store no blobs; the store's grammar requires positive bounds.
|
|
23
|
+
maxBlobBytes: 1,
|
|
24
|
+
maxBlobTotalBytes: 1,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const stateSchema = z.strictObject({
|
|
29
|
+
sequence: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
30
|
+
rows: z.array(documentActivitySchema).max(100),
|
|
31
|
+
});
|
|
32
|
+
/** Member/Agent/key identity is supplied only by the admitting host. */
|
|
33
|
+
export function documentActivity(
|
|
34
|
+
activity: DocumentActivityStore,
|
|
35
|
+
actorId: string,
|
|
36
|
+
computerId: string,
|
|
37
|
+
) {
|
|
38
|
+
const store = activity.open("document-activity-v1", stateSchema);
|
|
39
|
+
const key = JSON.stringify([actorId, computerId]);
|
|
40
|
+
function state() {
|
|
41
|
+
const saved = store.read(key);
|
|
42
|
+
return saved.kind === "ready" ? saved.value : { sequence: 0, rows: [] };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
record(path: string, activity: "opened" | "edited") {
|
|
46
|
+
const previous = state();
|
|
47
|
+
const row = documentActivitySchema.parse({
|
|
48
|
+
id:
|
|
49
|
+
previous.rows.find((item) => item.path === path)?.id ??
|
|
50
|
+
documentIds.document.generate(),
|
|
51
|
+
computerId,
|
|
52
|
+
path,
|
|
53
|
+
activity,
|
|
54
|
+
occurredAt: Date.now(),
|
|
55
|
+
sequence: previous.sequence + 1,
|
|
56
|
+
});
|
|
57
|
+
store.write(key, null, {
|
|
58
|
+
sequence: row.sequence,
|
|
59
|
+
rows: [
|
|
60
|
+
row,
|
|
61
|
+
...previous.rows.filter((item) => item.path !== path),
|
|
62
|
+
].slice(0, 100),
|
|
63
|
+
});
|
|
64
|
+
return row.sequence;
|
|
65
|
+
},
|
|
66
|
+
recent(limit: number, after?: { before: number }) {
|
|
67
|
+
const rows = state().rows.filter(
|
|
68
|
+
(row) => after === undefined || row.sequence < after.before,
|
|
69
|
+
);
|
|
70
|
+
const page = rows.slice(0, limit);
|
|
71
|
+
return documentsRecentSchema.parse({
|
|
72
|
+
rows: page,
|
|
73
|
+
next: rows.length > limit ? { before: page.at(-1)?.sequence } : null,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as S from "effect/Schema";
|
|
2
|
+
import {
|
|
3
|
+
AppStorageError,
|
|
4
|
+
appStorageReadResultSchema,
|
|
5
|
+
appStorageMutationResultSchema,
|
|
6
|
+
type AppStorageWire,
|
|
7
|
+
} from "@fungi.computer/agar/storage";
|
|
8
|
+
import { documentsChangedKey } from "../src/document-activity-contract.js";
|
|
9
|
+
|
|
10
|
+
/** Publish a scoped invalidation only after the real activity row was saved. */
|
|
11
|
+
export async function markDocumentsChanged(
|
|
12
|
+
storage: Pick<AppStorageWire, "read" | "write">,
|
|
13
|
+
computerId: string,
|
|
14
|
+
sequence: number,
|
|
15
|
+
) {
|
|
16
|
+
const current = S.decodeUnknownSync(appStorageReadResultSchema, {
|
|
17
|
+
onExcessProperty: "error",
|
|
18
|
+
})(await storage.read(documentsChangedKey));
|
|
19
|
+
if (current.kind === "rejected") throw new AppStorageError(current.code);
|
|
20
|
+
const result = S.decodeUnknownSync(appStorageMutationResultSchema, {
|
|
21
|
+
onExcessProperty: "error",
|
|
22
|
+
})(
|
|
23
|
+
await storage.write({
|
|
24
|
+
key: documentsChangedKey,
|
|
25
|
+
scope: "member",
|
|
26
|
+
basedOn: current.value.member?.revision ?? null,
|
|
27
|
+
value: { computerId, sequence },
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
if (result.kind === "rejected") throw new AppStorageError(result.code);
|
|
31
|
+
}
|
package/backend/gills.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineAppGill, type AppBackendGill } from "@fungi.computer/agar/gills";
|
|
2
|
+
import {
|
|
3
|
+
documentActivityRefusalSchema,
|
|
4
|
+
documentsChangedKey,
|
|
5
|
+
documentsRecentInputSchema,
|
|
6
|
+
documentsRecentSchema,
|
|
7
|
+
} from "../src/document-activity-contract.js";
|
|
8
|
+
|
|
9
|
+
/** The source pages saved actor/Computer activity; rendering never probes files. */
|
|
10
|
+
export const recentFilesGill = defineAppGill({
|
|
11
|
+
id: "recent-files",
|
|
12
|
+
title: "Recent Files",
|
|
13
|
+
source: "documents.recent",
|
|
14
|
+
limit: 10,
|
|
15
|
+
sizes: ["M", "L"],
|
|
16
|
+
params: [
|
|
17
|
+
{ key: "computer", label: "Computer", kind: "computer", required: true },
|
|
18
|
+
],
|
|
19
|
+
watch: { storage: documentsChangedKey },
|
|
20
|
+
input: ({ limit, after }): ReturnType<AppBackendGill["input"]> => {
|
|
21
|
+
const cursor: unknown = after === undefined ? undefined : JSON.parse(after);
|
|
22
|
+
const input = documentsRecentInputSchema.parse(
|
|
23
|
+
cursor === undefined ? { limit } : { limit, after: cursor },
|
|
24
|
+
);
|
|
25
|
+
return input.after === undefined
|
|
26
|
+
? { limit: input.limit }
|
|
27
|
+
: { limit: input.limit, after: input.after };
|
|
28
|
+
},
|
|
29
|
+
rows: (output) => {
|
|
30
|
+
const identity = { viewId: "recent-files", title: "Recent Files" };
|
|
31
|
+
if (documentActivityRefusalSchema.safeParse(output).success)
|
|
32
|
+
return {
|
|
33
|
+
...identity,
|
|
34
|
+
state: "error",
|
|
35
|
+
rows: [],
|
|
36
|
+
error: "Choose a Computer and allow Inkcap to read its files.",
|
|
37
|
+
recovery: { kind: "configure", param: "computer" },
|
|
38
|
+
};
|
|
39
|
+
const page = documentsRecentSchema.parse(output);
|
|
40
|
+
if (page.rows.length === 0)
|
|
41
|
+
return { ...identity, state: "empty", rows: [] };
|
|
42
|
+
const rows = page.rows.map((record) => {
|
|
43
|
+
const name = record.path.split("/").at(-1);
|
|
44
|
+
return {
|
|
45
|
+
id: record.id,
|
|
46
|
+
title: name === undefined || name.length === 0 ? record.path : name,
|
|
47
|
+
detail: `${record.activity === "edited" ? "Edited" : "Opened"} · ${record.path}`,
|
|
48
|
+
open: { params: [{ key: "path", value: record.path }] },
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
return page.next === null
|
|
52
|
+
? { ...identity, state: "full", rows }
|
|
53
|
+
: { ...identity, state: "full", rows, next: JSON.stringify(page.next) };
|
|
54
|
+
},
|
|
55
|
+
});
|
package/backend/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { markDocumentsChanged } from "./documents-changed.js";
|
|
2
|
+
import * as Redacted from "effect/Redacted";
|
|
3
|
+
import { DurableObject } from "cloudflare:workers";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import {
|
|
6
|
+
appBackendActorHeader,
|
|
7
|
+
appBackendComputerHeader,
|
|
8
|
+
} from "@fungi.computer/agar/backend";
|
|
9
|
+
import { createDocumentActivityStore } from "./document-activity.js";
|
|
10
|
+
import { appBackend, type InkcapBackendContext } from "./methods.js";
|
|
11
|
+
|
|
12
|
+
const requestSchema = z.strictObject({
|
|
13
|
+
method: z.enum(["documents.opened", "documents.edited", "documents.recent"]),
|
|
14
|
+
input: z.json(),
|
|
15
|
+
});
|
|
16
|
+
class InkcapFacet extends DurableObject<InkcapBackendContext["env"]> {
|
|
17
|
+
declare public readonly env: InkcapBackendContext["env"];
|
|
18
|
+
readonly activity;
|
|
19
|
+
constructor(ctx: DurableObjectState, env: InkcapBackendContext["env"]) {
|
|
20
|
+
super(ctx, env);
|
|
21
|
+
this.activity = createDocumentActivityStore(ctx.storage.sql);
|
|
22
|
+
}
|
|
23
|
+
readonly documentsChanged = (
|
|
24
|
+
computerId: string,
|
|
25
|
+
sequence: number,
|
|
26
|
+
): Promise<void> =>
|
|
27
|
+
markDocumentsChanged(this.env.APP_STORAGE, computerId, sequence);
|
|
28
|
+
override async fetch(request: Request): Promise<Response> {
|
|
29
|
+
if (request.method !== "PUT")
|
|
30
|
+
return new Response(null, { status: 405, headers: { Allow: "PUT" } });
|
|
31
|
+
const memberId = request.headers.get(appBackendActorHeader);
|
|
32
|
+
if (!memberId)
|
|
33
|
+
return Response.json({ error: "App actor unavailable" }, { status: 403 });
|
|
34
|
+
try {
|
|
35
|
+
const input = requestSchema.parse(await request.json());
|
|
36
|
+
return Response.json(
|
|
37
|
+
await appBackend.call(input.method, input.input, this, {
|
|
38
|
+
actor: { kind: "member", memberId },
|
|
39
|
+
computerId: request.headers.get(appBackendComputerHeader),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
console.error("Inkcap action failed", Redacted.make(cause));
|
|
44
|
+
// Never reflect provider errors, remote addresses or headers into the UI.
|
|
45
|
+
return Response.json(
|
|
46
|
+
{ error: "Inkcap could not complete this action" },
|
|
47
|
+
{ status: 400 },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export const Inkcap = appBackend.implement(InkcapFacet);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { recentFilesGill } from "./gills.js";
|
|
2
|
+
import {
|
|
3
|
+
documentIdentitySchema,
|
|
4
|
+
documentsRecentInputSchema,
|
|
5
|
+
documentsRecentSchema,
|
|
6
|
+
documentRecordedSchema,
|
|
7
|
+
documentActivityRefusalSchema,
|
|
8
|
+
} from "../src/document-activity-contract.js";
|
|
9
|
+
import {
|
|
10
|
+
documentActivity,
|
|
11
|
+
type DocumentActivityStore,
|
|
12
|
+
} from "./document-activity.js";
|
|
13
|
+
import * as z from "zod";
|
|
14
|
+
import type { AppMethodCallContext } from "@fungi.computer/puffball/app-method-runtime";
|
|
15
|
+
import {
|
|
16
|
+
defineAppBackend,
|
|
17
|
+
defineAppMethod,
|
|
18
|
+
} from "@fungi.computer/agar/backend-definition";
|
|
19
|
+
import type { WhistleJsonValue } from "@fungi.computer/whistle";
|
|
20
|
+
import type { AppStorageWire } from "@fungi.computer/agar/storage";
|
|
21
|
+
import {
|
|
22
|
+
pathSchema,
|
|
23
|
+
listInputSchema,
|
|
24
|
+
listOutputSchema,
|
|
25
|
+
readBytesInputSchema,
|
|
26
|
+
writeOutputSchema,
|
|
27
|
+
} from "@fungi.computer/computer/operations";
|
|
28
|
+
export type InkcapBackendContext = {
|
|
29
|
+
activity: DocumentActivityStore;
|
|
30
|
+
documentsChanged(computerId: string, sequence: number): Promise<void>;
|
|
31
|
+
env: {
|
|
32
|
+
APP_STORAGE: Pick<AppStorageWire, "read" | "write">;
|
|
33
|
+
COMPUTER?: {
|
|
34
|
+
list?(input: z.output<typeof listInput>): Promise<WhistleJsonValue>;
|
|
35
|
+
read?(input: z.output<typeof readInput>): Promise<WhistleJsonValue>;
|
|
36
|
+
write?(input: z.output<typeof writeInput>): Promise<WhistleJsonValue>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const computerPath = pathSchema.refine(
|
|
41
|
+
(value) => !value.split("/").includes(".."),
|
|
42
|
+
"Parent path segments are not allowed",
|
|
43
|
+
);
|
|
44
|
+
const listInput = listInputSchema.extend({ path: computerPath.default("/") });
|
|
45
|
+
const readInput = readBytesInputSchema.extend({
|
|
46
|
+
path: computerPath,
|
|
47
|
+
byteOffset: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.nonnegative()
|
|
51
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
52
|
+
.default(0),
|
|
53
|
+
byteLength: z
|
|
54
|
+
.number()
|
|
55
|
+
.int()
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(128 * 1024)
|
|
58
|
+
.default(128 * 1024),
|
|
59
|
+
});
|
|
60
|
+
const text = z.string().max(48 * 1024);
|
|
61
|
+
const writeInput = z
|
|
62
|
+
.strictObject({
|
|
63
|
+
path: computerPath,
|
|
64
|
+
content: text,
|
|
65
|
+
expectedContent: text.optional(),
|
|
66
|
+
})
|
|
67
|
+
.refine(
|
|
68
|
+
(value) =>
|
|
69
|
+
new TextEncoder().encode(value.content).byteLength +
|
|
70
|
+
new TextEncoder().encode(value.expectedContent ?? "").byteLength <=
|
|
71
|
+
48 * 1024,
|
|
72
|
+
"Combined content exceeds 48 KiB",
|
|
73
|
+
);
|
|
74
|
+
const readOutput = z.strictObject({
|
|
75
|
+
path: computerPath,
|
|
76
|
+
byteOffset: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
77
|
+
content: z.base64().max(174764),
|
|
78
|
+
encoding: z.literal("base64"),
|
|
79
|
+
truncated: z.boolean(),
|
|
80
|
+
});
|
|
81
|
+
const refusal = z.union([
|
|
82
|
+
z.strictObject({
|
|
83
|
+
error: z.enum([
|
|
84
|
+
"not_granted",
|
|
85
|
+
"revoked",
|
|
86
|
+
"grantor_lost_authority",
|
|
87
|
+
"unavailable",
|
|
88
|
+
"invalid",
|
|
89
|
+
"invalid_path",
|
|
90
|
+
"too_large",
|
|
91
|
+
"conflict",
|
|
92
|
+
"not_found",
|
|
93
|
+
]),
|
|
94
|
+
}),
|
|
95
|
+
z.strictObject({
|
|
96
|
+
error: z.literal("attachment_budget_exceeded"),
|
|
97
|
+
kind: z.literal("write"),
|
|
98
|
+
retryAfterMs: z.number().int().min(1).max(86400000),
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
const listReply = z.union([listOutputSchema, refusal]);
|
|
102
|
+
const readReply = z.union([readOutput, refusal]);
|
|
103
|
+
const writeReply = z.union([writeOutputSchema, refusal]);
|
|
104
|
+
async function recordDocument(
|
|
105
|
+
input: z.output<typeof documentIdentitySchema>,
|
|
106
|
+
context: InkcapBackendContext,
|
|
107
|
+
caller: AppMethodCallContext,
|
|
108
|
+
activity: "opened" | "edited",
|
|
109
|
+
) {
|
|
110
|
+
if (caller.computerId === null || caller.computerId !== input.computerId)
|
|
111
|
+
return { error: "not_granted" as const };
|
|
112
|
+
const sequence = documentActivity(
|
|
113
|
+
context.activity,
|
|
114
|
+
JSON.stringify(caller.actor),
|
|
115
|
+
caller.computerId,
|
|
116
|
+
).record(input.path, activity);
|
|
117
|
+
await context.documentsChanged(caller.computerId, sequence);
|
|
118
|
+
return { recorded: true as const };
|
|
119
|
+
}
|
|
120
|
+
export const appBackend = defineAppBackend({
|
|
121
|
+
gills: [recentFilesGill],
|
|
122
|
+
methods: {
|
|
123
|
+
"documents.opened": defineAppMethod({
|
|
124
|
+
description: "Record a successfully opened foreground Computer document",
|
|
125
|
+
effect: "write",
|
|
126
|
+
requires: ["computer.readFile", "app.storage"],
|
|
127
|
+
input: documentIdentitySchema,
|
|
128
|
+
output: z.union([documentRecordedSchema, documentActivityRefusalSchema]),
|
|
129
|
+
handler: (input, context: InkcapBackendContext, caller) =>
|
|
130
|
+
recordDocument(input, context, caller, "opened"),
|
|
131
|
+
}),
|
|
132
|
+
"documents.edited": defineAppMethod({
|
|
133
|
+
description: "Record an accepted Computer document save",
|
|
134
|
+
effect: "write",
|
|
135
|
+
requires: ["computer.writeFile", "app.storage"],
|
|
136
|
+
input: documentIdentitySchema,
|
|
137
|
+
output: z.union([documentRecordedSchema, documentActivityRefusalSchema]),
|
|
138
|
+
handler: (input, context: InkcapBackendContext, caller) =>
|
|
139
|
+
recordDocument(input, context, caller, "edited"),
|
|
140
|
+
}),
|
|
141
|
+
"documents.recent": defineAppMethod({
|
|
142
|
+
description:
|
|
143
|
+
"Read one saved recent-documents page for the actor and admitted Computer; no provider calls",
|
|
144
|
+
effect: "read",
|
|
145
|
+
requires: ["computer.readFile"],
|
|
146
|
+
input: documentsRecentInputSchema,
|
|
147
|
+
output: z.union([documentsRecentSchema, documentActivityRefusalSchema]),
|
|
148
|
+
handler: (input, context: InkcapBackendContext, caller) =>
|
|
149
|
+
caller.computerId === null
|
|
150
|
+
? { error: "not_granted" as const }
|
|
151
|
+
: documentActivity(
|
|
152
|
+
context.activity,
|
|
153
|
+
JSON.stringify(caller.actor),
|
|
154
|
+
caller.computerId,
|
|
155
|
+
).recent(input.limit, input.after),
|
|
156
|
+
}),
|
|
157
|
+
listFiles: defineAppMethod({
|
|
158
|
+
description:
|
|
159
|
+
"List one bounded directory page on the attached Computer. Continue with nextOffset; no Computer selector is accepted.",
|
|
160
|
+
effect: "read",
|
|
161
|
+
requires: ["computer.list"],
|
|
162
|
+
input: listInput,
|
|
163
|
+
output: listReply,
|
|
164
|
+
handler: async (input, context: InkcapBackendContext, caller) =>
|
|
165
|
+
caller.computerId === null || !context.env.COMPUTER?.list
|
|
166
|
+
? refusal.parse({ error: "not_granted" })
|
|
167
|
+
: listReply.parse(await context.env.COMPUTER.list(input)),
|
|
168
|
+
}),
|
|
169
|
+
readFile: defineAppMethod({
|
|
170
|
+
description:
|
|
171
|
+
"Read up to 128 KiB of a file on the attached Computer as base64. Use byteOffset for the next chunk.",
|
|
172
|
+
effect: "read",
|
|
173
|
+
requires: ["computer.readFile"],
|
|
174
|
+
input: readInput,
|
|
175
|
+
output: readReply,
|
|
176
|
+
handler: async (input, context: InkcapBackendContext, caller) =>
|
|
177
|
+
caller.computerId === null || !context.env.COMPUTER?.read
|
|
178
|
+
? refusal.parse({ error: "not_granted" })
|
|
179
|
+
: readReply.parse(await context.env.COMPUTER.read(input)),
|
|
180
|
+
}),
|
|
181
|
+
writeFile: defineAppMethod({
|
|
182
|
+
description:
|
|
183
|
+
"Write UTF-8 text on the attached Computer. Supply expectedContent for a compare-and-swap; combined content is limited to 48 KiB. The host owns attribution and the attachment write budget.",
|
|
184
|
+
effect: "write",
|
|
185
|
+
requires: ["computer.writeFile"],
|
|
186
|
+
input: writeInput,
|
|
187
|
+
output: writeReply,
|
|
188
|
+
handler: async (input, context: InkcapBackendContext, caller) =>
|
|
189
|
+
caller.computerId === null || !context.env.COMPUTER?.write
|
|
190
|
+
? refusal.parse({ error: "not_granted" })
|
|
191
|
+
: writeReply.parse(await context.env.COMPUTER.write(input)),
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DurableObject } from "../../../../packages/shiitake/test/fixtures/cloudflare-workers.fixture.js";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { afterEach, expect, it } from "vitest";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { fromNodeSqlite } from "../../../../packages/watchdog/src/node-sqlite.js";
|
|
5
|
+
import { createAppStorage } from "../../../../packages/oyster/src/app-storage.js";
|
|
6
|
+
import {
|
|
7
|
+
AppStorageError,
|
|
8
|
+
type AppStorageResult,
|
|
9
|
+
type AppStorageWire,
|
|
10
|
+
} from "@fungi.computer/agar/storage";
|
|
11
|
+
import { markDocumentsChanged } from "../documents-changed.js";
|
|
12
|
+
import { documentsChangedKey } from "../../src/document-activity-contract.js";
|
|
13
|
+
|
|
14
|
+
const databases = new Set<DatabaseSync>();
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const database of databases) database.close();
|
|
17
|
+
databases.clear();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function storageOwner(): Pick<AppStorageWire, "read" | "write"> {
|
|
21
|
+
const database = new DatabaseSync(":memory:");
|
|
22
|
+
databases.add(database);
|
|
23
|
+
const owner = fromNodeSqlite(database);
|
|
24
|
+
const storage = createAppStorage({
|
|
25
|
+
sql: owner.sql,
|
|
26
|
+
commit: (body) =>
|
|
27
|
+
Effect.try({
|
|
28
|
+
try: () => owner.transactionSync(body),
|
|
29
|
+
catch: (cause) =>
|
|
30
|
+
cause instanceof Error
|
|
31
|
+
? cause
|
|
32
|
+
: new Error("Storage commit failed", { cause }),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
const view = storage.open(() =>
|
|
36
|
+
Effect.succeed({
|
|
37
|
+
address: "installation:inkcap",
|
|
38
|
+
appId: "app_inkcap",
|
|
39
|
+
memberId: "owner",
|
|
40
|
+
membershipVersion: 1,
|
|
41
|
+
isOwner: true,
|
|
42
|
+
keys: [{ key: documentsChangedKey, scopes: ["member"], maxBytes: 2048 }],
|
|
43
|
+
assertCurrent: () => undefined,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
const result = <A>(
|
|
47
|
+
program: Effect.Effect<A, AppStorageError>,
|
|
48
|
+
): Promise<AppStorageResult<A>> =>
|
|
49
|
+
Effect.runPromise(
|
|
50
|
+
Effect.match(program, {
|
|
51
|
+
onSuccess: (value) => ({ kind: "accepted" as const, value }),
|
|
52
|
+
onFailure: (cause) => ({ kind: "rejected" as const, code: cause.code }),
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
read: (key) => result(view.read(key)),
|
|
57
|
+
write: (input) => result(view.write(input)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
it("updates the same recent-documents marker after an open and every accepted save", async () => {
|
|
62
|
+
const storage = storageOwner();
|
|
63
|
+
await markDocumentsChanged(storage, "workspace", 1);
|
|
64
|
+
await markDocumentsChanged(storage, "workspace", 2);
|
|
65
|
+
await markDocumentsChanged(storage, "workspace", 3);
|
|
66
|
+
expect(await storage.read(documentsChangedKey)).toMatchObject({
|
|
67
|
+
kind: "accepted",
|
|
68
|
+
value: { member: { value: { computerId: "workspace", sequence: 3 } } },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("preserves a competing marker and reports its CAS conflict without retrying", async () => {
|
|
73
|
+
const owner = storageOwner();
|
|
74
|
+
await markDocumentsChanged(owner, "workspace", 1);
|
|
75
|
+
let writes = 0;
|
|
76
|
+
const storage: Pick<AppStorageWire, "read" | "write"> = {
|
|
77
|
+
read: async (key) => {
|
|
78
|
+
const snapshot = await owner.read(key);
|
|
79
|
+
if (snapshot.kind !== "accepted")
|
|
80
|
+
throw new AppStorageError(snapshot.code);
|
|
81
|
+
expect(
|
|
82
|
+
await owner.write({
|
|
83
|
+
key,
|
|
84
|
+
scope: "member",
|
|
85
|
+
basedOn: snapshot.value.member?.revision ?? null,
|
|
86
|
+
value: { computerId: "workspace", sequence: 3 },
|
|
87
|
+
}),
|
|
88
|
+
).toMatchObject({ kind: "accepted" });
|
|
89
|
+
return snapshot;
|
|
90
|
+
},
|
|
91
|
+
write: (input) => {
|
|
92
|
+
writes++;
|
|
93
|
+
return owner.write(input);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
await expect(
|
|
97
|
+
markDocumentsChanged(storage, "workspace", 2),
|
|
98
|
+
).rejects.toMatchObject({ code: "conflict" });
|
|
99
|
+
expect(writes).toBe(1);
|
|
100
|
+
expect(await owner.read(documentsChangedKey)).toMatchObject({
|
|
101
|
+
kind: "accepted",
|
|
102
|
+
value: { member: { value: { computerId: "workspace", sequence: 3 } } },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("does not attempt a marker write when its live read is refused", async () => {
|
|
107
|
+
const owner = storageOwner();
|
|
108
|
+
let writes = 0;
|
|
109
|
+
await expect(
|
|
110
|
+
markDocumentsChanged(
|
|
111
|
+
{
|
|
112
|
+
read: () =>
|
|
113
|
+
Promise.resolve({ kind: "rejected", code: "owner_required" }),
|
|
114
|
+
write: (input) => {
|
|
115
|
+
writes++;
|
|
116
|
+
return owner.write(input);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
"workspace",
|
|
120
|
+
1,
|
|
121
|
+
),
|
|
122
|
+
).rejects.toMatchObject({ code: "owner_required" });
|
|
123
|
+
expect(writes).toBe(0);
|
|
124
|
+
expect(await owner.read(documentsChangedKey)).toMatchObject({
|
|
125
|
+
kind: "accepted",
|
|
126
|
+
value: { source: "none", member: null },
|
|
127
|
+
});
|
|
128
|
+
});
|