From 19b864d6e774568d712cf37afba1af4034ebc316 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 11 Aug 2025 18:39:41 +0200 Subject: [PATCH] switch llms.txt generation plugin for docs and remove legacy content --- docs/docs/legacy/README.md | 18 - docs/docs/tutorial-basics/_category_.json | 8 - docs/docs/tutorial-basics/congratulations.md | 23 -- .../tutorial-basics/create-a-blog-post.md | 34 -- .../docs/tutorial-basics/create-a-document.md | 57 --- docs/docs/tutorial-basics/create-a-page.md | 43 --- docs/docs/tutorial-basics/deploy-your-site.md | 31 -- .../tutorial-basics/markdown-features.mdx | 152 -------- docs/docs/tutorial-extras/_category_.json | 7 - .../img/docsVersionDropdown.png | Bin 25427 -> 0 bytes .../tutorial-extras/img/localeDropdown.png | Bin 27841 -> 0 bytes .../tutorial-extras/manage-docs-versions.md | 55 --- .../tutorial-extras/translate-your-site.md | 88 ----- docs/docusaurus.config.js | 24 +- .../archived/alternative-grc-20-spec.md | 207 ----------- .../archived/data-structure-and-sync.md | 117 ------- docs/legacy-files/archived/identity.md | 96 ----- docs/legacy-files/archived/invitations.md | 17 - .../archived/key-agreement-for-spaces.md | 38 -- docs/legacy-files/archived/lessons-learned.md | 42 --- .../archived/open-framework-questions.md | 92 ----- .../archived/recap-and-future-strategy.md | 274 --------------- .../archived/schema-graph-based.md | 331 ------------------ .../archived/schema-relational.md | 278 --------------- docs/legacy-files/framework.md | 207 ----------- docs/legacy-files/namespaces/utils.md | 66 ---- docs/legacy-files/public-graph-integration.md | 59 ---- docs/legacy-files/technology-stack.md | 18 - docs/package.json | 4 +- docs/sidebars.js | 1 - pnpm-lock.yaml | 19 +- 31 files changed, 34 insertions(+), 2372 deletions(-) delete mode 100644 docs/docs/legacy/README.md delete mode 100644 docs/docs/tutorial-basics/_category_.json delete mode 100644 docs/docs/tutorial-basics/congratulations.md delete mode 100644 docs/docs/tutorial-basics/create-a-blog-post.md delete mode 100644 docs/docs/tutorial-basics/create-a-document.md delete mode 100644 docs/docs/tutorial-basics/create-a-page.md delete mode 100644 docs/docs/tutorial-basics/deploy-your-site.md delete mode 100644 docs/docs/tutorial-basics/markdown-features.mdx delete mode 100644 docs/docs/tutorial-extras/_category_.json delete mode 100644 docs/docs/tutorial-extras/img/docsVersionDropdown.png delete mode 100644 docs/docs/tutorial-extras/img/localeDropdown.png delete mode 100644 docs/docs/tutorial-extras/manage-docs-versions.md delete mode 100644 docs/docs/tutorial-extras/translate-your-site.md delete mode 100644 docs/legacy-files/archived/alternative-grc-20-spec.md delete mode 100644 docs/legacy-files/archived/data-structure-and-sync.md delete mode 100644 docs/legacy-files/archived/identity.md delete mode 100644 docs/legacy-files/archived/invitations.md delete mode 100644 docs/legacy-files/archived/key-agreement-for-spaces.md delete mode 100644 docs/legacy-files/archived/lessons-learned.md delete mode 100644 docs/legacy-files/archived/open-framework-questions.md delete mode 100644 docs/legacy-files/archived/recap-and-future-strategy.md delete mode 100644 docs/legacy-files/archived/schema-graph-based.md delete mode 100644 docs/legacy-files/archived/schema-relational.md delete mode 100644 docs/legacy-files/framework.md delete mode 100644 docs/legacy-files/namespaces/utils.md delete mode 100644 docs/legacy-files/public-graph-integration.md delete mode 100644 docs/legacy-files/technology-stack.md diff --git a/docs/docs/legacy/README.md b/docs/docs/legacy/README.md deleted file mode 100644 index 328e5524..00000000 --- a/docs/docs/legacy/README.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Legacy Docs Overview -description: Snapshot of the original Hypergraph documentation prior to the 2025 rewrite. -version: 0.0.1 -tags: [legacy] ---- - -# ๐Ÿ—ƒ๏ธ Legacy Documentation - -The documents below were written before the current Docusaurus overhaul. They remain available for historical context and migration reference. - -* [Framework design](https://github.com/graphprotocol/hypergraph/blob/main/docs/legacy-files/framework.md) -* [Public graph integration](https://github.com/graphprotocol/hypergraph/blob/main/docs/legacy-files/public-graph-integration.md) -* [Technology stack](https://github.com/graphprotocol/hypergraph/blob/main/docs/legacy-files/technology-stack.md) -* **Namespaces** โ€” see [`docs/legacy-files/namespaces/`](https://github.com/graphprotocol/hypergraph/tree/main/docs/legacy-files/namespaces) -* **Archived material** โ€” see [`docs/legacy-files/archived/`](https://github.com/graphprotocol/hypergraph/tree/main/docs/legacy-files/archived) - -> โ„น๏ธ These pages may describe outdated APIs or naming conventions. Prefer the up-to-date docs unless you're maintaining legacy code. \ No newline at end of file diff --git a/docs/docs/tutorial-basics/_category_.json b/docs/docs/tutorial-basics/_category_.json deleted file mode 100644 index 2e6db55b..00000000 --- a/docs/docs/tutorial-basics/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Tutorial - Basics", - "position": 2, - "link": { - "type": "generated-index", - "description": "5 minutes to learn the most important Docusaurus concepts." - } -} diff --git a/docs/docs/tutorial-basics/congratulations.md b/docs/docs/tutorial-basics/congratulations.md deleted file mode 100644 index 04771a00..00000000 --- a/docs/docs/tutorial-basics/congratulations.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Congratulations! - -You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. - -Docusaurus has **much more to offer**! - -Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. - -Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) - -## What's next? - -- Read the [official documentation](https://docusaurus.io/) -- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) -- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) -- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) -- Add a [search bar](https://docusaurus.io/docs/search) -- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) -- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) diff --git a/docs/docs/tutorial-basics/create-a-blog-post.md b/docs/docs/tutorial-basics/create-a-blog-post.md deleted file mode 100644 index 550ae17e..00000000 --- a/docs/docs/tutorial-basics/create-a-blog-post.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Create a Blog Post - -Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... - -## Create your first Post - -Create a file at `blog/2021-02-28-greetings.md`: - -```md title="blog/2021-02-28-greetings.md" ---- -slug: greetings -title: Greetings! -authors: - - name: Joel Marcey - title: Co-creator of Docusaurus 1 - url: https://github.com/JoelMarcey - image_url: https://github.com/JoelMarcey.png - - name: Sรฉbastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png -tags: [greetings] ---- - -Congratulations, you have made your first post! - -Feel free to play around and edit this post as much as you like. -``` - -A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). diff --git a/docs/docs/tutorial-basics/create-a-document.md b/docs/docs/tutorial-basics/create-a-document.md deleted file mode 100644 index c22fe294..00000000 --- a/docs/docs/tutorial-basics/create-a-document.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Create a Document - -Documents are **groups of pages** connected through: - -- a **sidebar** -- **previous/next navigation** -- **versioning** - -## Create your first Doc - -Create a Markdown file at `docs/hello.md`: - -```md title="docs/hello.md" -# Hello - -This is my **first Docusaurus document**! -``` - -A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). - -## Configure the Sidebar - -Docusaurus automatically **creates a sidebar** from the `docs` folder. - -Add metadata to customize the sidebar label and position: - -```md title="docs/hello.md" {1-4} ---- -sidebar_label: 'Hi!' -sidebar_position: 3 ---- - -# Hello - -This is my **first Docusaurus document**! -``` - -It is also possible to create your sidebar explicitly in `sidebars.js`: - -```js title="sidebars.js" -export default { - tutorialSidebar: [ - 'intro', - // highlight-next-line - 'hello', - { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], - }, - ], -}; -``` diff --git a/docs/docs/tutorial-basics/create-a-page.md b/docs/docs/tutorial-basics/create-a-page.md deleted file mode 100644 index 20e2ac30..00000000 --- a/docs/docs/tutorial-basics/create-a-page.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create a Page - -Add **Markdown or React** files to `src/pages` to create a **standalone page**: - -- `src/pages/index.js` โ†’ `localhost:3000/` -- `src/pages/foo.md` โ†’ `localhost:3000/foo` -- `src/pages/foo/bar.js` โ†’ `localhost:3000/foo/bar` - -## Create your first React Page - -Create a file at `src/pages/my-react-page.js`: - -```jsx title="src/pages/my-react-page.js" -import React from 'react'; -import Layout from '@theme/Layout'; - -export default function MyReactPage() { - return ( - -

My React page

-

This is a React page

-
- ); -} -``` - -A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). - -## Create your first Markdown Page - -Create a file at `src/pages/my-markdown-page.md`: - -```mdx title="src/pages/my-markdown-page.md" -# My Markdown page - -This is a Markdown page -``` - -A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). diff --git a/docs/docs/tutorial-basics/deploy-your-site.md b/docs/docs/tutorial-basics/deploy-your-site.md deleted file mode 100644 index 1c50ee06..00000000 --- a/docs/docs/tutorial-basics/deploy-your-site.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Deploy your site - -Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). - -It builds your site as simple **static HTML, JavaScript and CSS files**. - -## Build your site - -Build your site **for production**: - -```bash -npm run build -``` - -The static files are generated in the `build` folder. - -## Deploy your site - -Test your production build locally: - -```bash -npm run serve -``` - -The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). - -You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). diff --git a/docs/docs/tutorial-basics/markdown-features.mdx b/docs/docs/tutorial-basics/markdown-features.mdx deleted file mode 100644 index 35e00825..00000000 --- a/docs/docs/tutorial-basics/markdown-features.mdx +++ /dev/null @@ -1,152 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Markdown Features - -Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. - -## Front Matter - -Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): - -```text title="my-doc.md" -// highlight-start ---- -id: my-doc-id -title: My document title -description: My document description -slug: /my-custom-url ---- -// highlight-end - -## Markdown heading - -Markdown text with [links](./hello.md) -``` - -## Links - -Regular Markdown links are supported, using url paths or relative file paths. - -```md -Let's see how to [Create a page](/create-a-page). -``` - -```md -Let's see how to [Create a page](./create-a-page.md). -``` - -**Result:** Let's see how to [Create a page](./create-a-page.md). - -## Images - -Regular Markdown images are supported. - -You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`): - -```md -![Docusaurus logo](/img/docusaurus.png) -``` - -![Docusaurus logo](/img/docusaurus.png) - -You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them: - -```md -![Docusaurus logo](./img/docusaurus.png) -``` - -## Code Blocks - -Markdown code blocks are supported with Syntax highlighting. - -````md -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

Hello, Docusaurus!

; -} -``` -```` - -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

Hello, Docusaurus!

; -} -``` - -## Admonitions - -Docusaurus has a special syntax to create admonitions and callouts: - -```md -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: -``` - -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: - -## MDX and React Components - -[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**: - -```jsx -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`) - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! -``` - -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`); - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! diff --git a/docs/docs/tutorial-extras/_category_.json b/docs/docs/tutorial-extras/_category_.json deleted file mode 100644 index a8ffcc19..00000000 --- a/docs/docs/tutorial-extras/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Tutorial - Extras", - "position": 3, - "link": { - "type": "generated-index" - } -} diff --git a/docs/docs/tutorial-extras/img/docsVersionDropdown.png b/docs/docs/tutorial-extras/img/docsVersionDropdown.png deleted file mode 100644 index 97e4164618b5f8beda34cfa699720aba0ad2e342..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25427 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 diff --git a/docs/docs/tutorial-extras/img/localeDropdown.png b/docs/docs/tutorial-extras/img/localeDropdown.png deleted file mode 100644 index e257edc1f932985396bf59584c7ccfaddf955779..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27841 zcmXt9WmFtZ(*=S%B)EHUciG??+-=biEVw%f7J?HT77G@f5ZpbB1Pku&vgoqxemw6v z-;X&{JzZV*cFmohnLgcd+M3FE*p%2vNJx09Dhj$tNXVWq2M^|}mn)^e9a~;bs1CC4 zWs#5?l5k+wXfI`CFI{Chq}oa9BP66(NZK0uiU1Kwn&3K0m`=xIMoxdVZ#+ zp?hKSLSSimjhdEzWp#6Tbpr;2A08YY9vwczVR!d;r)Q^kw|6h$pbtRyO;c2US2)Ho=#3q?{4m1GWOCI`k&9;zl9YDhH|l{oVck{{HdF$xGeh(%RX@ITa1V-QE4arPZ_3^N0KUo15FS^Rt74gNyU?f6HsD z>zmu#+n1LY=NIRf7Z*oIN2_aF7nc`%dwaXPyVf>#Q`56+>svGPi|1!&J3Bj8*0u|a zE61nDOKTge8(T{&>(jIU{?5$PF)%N#t}iaHQc%;Ky=4F7L{Hzy*Vp$Mj`%zGZ+7k< zCpRC^+V1HYCi6}{?rS`Ew80CL%d5-LF)(<1lJAQ_QE}I< z?$m+XE%JR|)Y|g5*Z=3YjLfXkvht|tSaC_|$oh1*A78S&%grr-Q|oi0ai*n%^?I3Z zz4Ifn)p1zW0ShuJU zjT*W!;4n~Y)3m5E=4m0n9;cN(k*j`y5!~j2)ij4x1#tx zB&it>z`(yY6BF>DU9?)rvOb2G!4AbPa`$!ju_}{}N=X3%ljy@XN?Dz5W~L8#vn;(% zS0y`!_FK8bT{5iuza9iPzyFntcC0hEUgCyxwZgrs_lXv54ZHujy!d4_U`~v!&Xq6w z_%CfMkDLt!D3SDYg>XEZ!YJH*s~-dg$LmS&Mt_;Y7X9a!>IDr+ded%2&q%}2^ODhk zoJMHe1;<*D7+WnelW=pb#;#*9m22_D0Uy+B;{x z(r=4T(e9>b$HL=1ZhtTnMZ8m?T*4WlE1nANJoY~M+S`a~oAzPxq?IY|K;|faC(Qf6 z6st=g2Oa&+>GJF*AU5<{Q1pIIjk9IOz}i1XThs0R)dBg}u}I!L^(JejuqE{$Bx0WH zK_L%2hekVKCo%({=C&4>8XPbm?HVjtj7;pR;Nl%bO7u_%gfl5w5S;(8b>qCb9KY=2 zcH1B8#T*pZQMR+_zF|mDvyu5p%arE^>?K|9F#FDuJCyu6$KPjjPBMq7j0f$|h@y!QXH+UdeH3iv*9ArYX^V-S2rxolaBRROkUH4!AxVghY-$mqUuOg%w5X}J1K z3LIKED&GtI+|Bu|l2OgJXS@ z##5m-UU-??q5BVBs3e%jt&;*!MXilSO_r%{gmW&qj$2WWx8M1Us?Tzp=Of?r=^y=m zDDr>5Z2+yUUf9O3Kqm?KxT9VJX#G6EP&E+e7EkxJF5QqcBPy@TsIFiD!!LWKz2ftR za<|^DinsXw>aBe|0DWOEi#5cV&B>!$i8?+vTr3ZDMK}XFeg)Ime5=*V++LLjj6sSf>5d+I|6V|cU`LfQPC z;p|(TN|j&~8CO`*qIi-79281;uL=cj-kt$ zx5MwWh>2LRlqjdUEGgk)P@$`Rs3-3sSlqxdxpG@!K`;a)V2m#wvau8$FIZuT9T00v znI8L>LHCkAZsu+5PUedUKs5fY2Ehv7Lqr}Ue$h;p6jBeeweEDUn2p#fwkvxk%Z<-6 zlgcD$>a-9H1#>^}Ku>>wLa`FkP^$V?ys$YQ&1L$o#0R}|{e?+I{K?~0CPz_*Bh#mo zh#!|PeV|ebfXa=JD#~>$?!*)i)b@eZZ`$qTk#-n$b{Cnhx2wH9N;PkqOwfS5FPe4A z!^5G+7=f|QUkN8gZmRRF-gxA&%`!7|FLGzf?uPu9E>P4d zrO@YSB$ z8Q{^@GSty5G&7xHSPy#pErSb3Yym^l5+QhvVlc)ItslUVgKOTQyYw8QX+2%`A%uhb zCJ{CE9{zUB(&-v8uRN|49S2Np{L4XRjFWz9R?)%ikl#d@WJtzM$=odVE^A1_CR5$l zs~b7y&?qM}RqSq1_-7&^wqiGh$yZuM2alHG{5LL=^QiF^u2prn!rcZ9%AF_!mJaxS9)8?8ha{9;`m^(Fx7`o(9*^- zI+OEv7<`;JEbKrNAh#EhBOA3x9E1Hr;lS)5pbY@p_LBMGn<&!Nxl41i9>dX%V}P+N zR;}+{G5WqCjnW#@f9ZNd^d5R<+ViQpx-L3$P}Nkiph3->K~K9)Sw$@INj*8YJLj@f z*+Rh+naB!_+NtSnzwWfLhq1;bmSozM80Xik(oGSLM*c)>iC_Wvd=JP|df1=roC3iU zoG&xR@$6d-6s0^VR}3V5OFQndgqfbboOay9Tf7RQmygGWgZ+DD(=|p9Aw+)O_j8?HRA#~+mIn^!H zQ6fcNW1FIjQ#SN_nK%EQV_F{VV77VfT5B(ea{vC|K#&-RTdcH#OR%(Mr#R1?jLzzq zSC-hN{(b^Ik^Q{uB|gq70;JUnM+#nmHCHA@PxC-sYqdnHZfEu1VHP*(8?jf)TsXH7 z`d(w{qU>V+81-UywGHL+AD7SV`|6-5PENL9RC02nnu15q_;*RRA_g8|!M(z88r&2? zCYs;1K=%c4QceJr-h+O=+K2tbY%HGQfyO1=9--HP5(yo2@2ad|TVK+$67(dBRpKI9 zcTvYDh?n^D9&qCvQhZoHb7DSvql}UJ8B+>~m5-ISatyypAR9WnfzbiDmXq*ctR3Xu z(~YwCAKYipx{EI8!HwsIlC6i`0rhcb>6<%+Cp)h@mK*_1d8_q6dg4>n}&ihP)NGiUvb81U?bXk&I< zbcqui@YB^CK-jFfu@*XpEERc^Mh(aJ)LBA@| ze4m|#Gs|Rc+0u4VvgE2s^$ ztYjCc@_u6&>iu~fe+ed*pr>hTdj(LcVf&SE`t2uXleZ(mhZd7kd|U$5HrJHPQ@IZ7 zz1w#&@Hi?VMVg$?DV~d{6LYoL8SFlWmuiYZxE8-M?^q32JSt7GoOVzZ8#I13;Ax`h zy=DXkH>H2B>%O@Ual0AO#Lh>Z`q=%r{iaZi3fZKcmBtmff&=e!GF%sO1~^L| z<3g?B>etUeZ?Suv6A<@bH;i=|KtG0mk@t4!qPRX4+^*osf+?77qg=U_OjVUxbTvh% z8DC!P=LlXRVFEd#m0i*Ka(b7e+3E&CC^Yv2#TgpoU(C>Wsp4))0%aRYtPxSr1x zO6uJUAMROWMj1L@;~jX6gRh(+e1ZqC_CTY4s&GfB-E;b?6+vEb;^bSE6j9xTFW;oq z9(1ndc$4}qdAB6ta4BN@p|T{**jB2P48}=Ya*Jc5#3mv|J&XRD;~yH>^DLwT>bp@)BbsVm+*3t=;598_Aj{ zF(?v`d_@ky*e%9dvu#A7+LtE~P$5VDCRJz{ZCt3Qh5aQ==>mF~k7bTCZxZg$!jnP8he7?WmJYT*1>c{*tJR|Ie+ScEevd4@gG>!gnL_ZL0 zKC)4$4wIXHIG~yE4+vZ~gh~Du9&92xJVUy91zt6P+$SZ9%)_wNU7KW~uGu2PF`KM6 z)UjHJQr%bRkMmIKABTD;BRcKhrdAbU;gFURvdg`TDW)T{)k8(vFbmtSAMueO{E8RHEQz-$F2C0;smk?8Q*e=qM%6O z6aGCJV;h1Tf3qvPEYi~fsz?&nlrg71v(eKqA!&F7d&p(^Xy#{`bl-!6%zc6pwsB;^ z+s#(uj7tu(L!ti&l1T51?Zuxg`16)sS-XNZm6tV-9#MfVeX#M39*XRuyFiJrxU@lO zA94#H%u0U~Ea9b26Qf{o;FeeG*!6uF*bYv#%%B^zN~9gqX{FS&&Ba|4AuSA${f^sf z7tg9}O%6m})g#&j5f%_eXA&}AZI!vQtzb=^sQxVZi~_}R^pgdM?5WD3%5Gx)%~qaP zgb4y1pEi3Ut}qG#QQ8SxhEkYe1Iy%QMz~|VS zKNsn5WGa%en;uc#7;LpDxYo4^@zL&dT*?Movr0f}Fry~2?+=LVy&$9SKV5+@SE-{M z4E!tmqebqFV%O~LO=L7??~zNUu90ECkq2Dut+Q$C#QJ*uQ33)=L?sH^oM|)e*HvE5J+C=qp79zhoRrLcNRA%1 zo?(m~(so82vOoC7`kQMWO5~^(`_b!C)8yq_VgnO5blD*sV`=DhQ}{$VtHxJJ@hixJ@hcZ z!Y6lPxZ6KphBnMJ)Ki2qFXY=iKs$GnX#1@Z7~hW~TuZju?)u=y?>z5W?Gv0-coA#k zCeo>mYl2HbT(xw!L&23l5KXaDk)yq}eBc&oPdWOPI`+f_o2cgW5QeU+)?Z2SHRplP z^{WM#a*z=ndtAjrTjbW0xE@*Ir~X+Bi-n#;6t1um9|^H4v%4b8X{_t71*TeupTOxB zM!=Yir}l!cM!GzQSnjS?@tOr){-JXhj8oH5p=g?cX47@jYyLLVq#|_Nsv3>>?X=ey zqHoKr;KTdI-GBAo?{+YUsVsacvsXS>8d?dLdU_)>MB*glDaE}%bBrd^98i+k4NQ8s zc0?8Fbqr&)Wq3Wd=YVyyUH$oZkbSRGYQQj1NofbRth{_t5aE##Z zRgYXbJ@On89x{nXLRlW`84WcfoXw=cPcZZH9T^b zcb#iuU7-qyv~G@U`}AkosbCYozUSeB3Hxyoirpqhcbvd|soGDf8>z48$4OE>XaW4E zM`Bd>uV&vA8~mC0n0*yWn z!;O|1HnCN1ghEB898BR#@4Bo&&oP9!4dcdtLZ@`un@&0 zzvF-GJhEY|FLF{hrM=dB7|h@3bEZZVJc3@GCJk0{ONwS8^g2F0`roJtV2uvN1O)|| zIfYh)=}lZzT`5BbTHcM6zo=WwB7-gyvx+Cm)a}&MT+1M^^h@h5kMVlZF*~3?Y5n)L zG9~s#<;5)1%>+_Ny*GZHAebop+bfp3&+eUH&4)I7Bc%5<40;DxP0G8{l|7Ufj)b!u zw?zWRNHyLJzYlCQj^pLwN#g~68@bp>+KA=l8QJkW-|B;3+XPeez-@9TIs${Q*6_9g zgZY+gF6*%)arn3AJUkn5bhfZ9zut{n6VIK=XKt|=rtOVmc&6zImd8%#b}Bw)vQ<=y zZ*)E`F>yPlf=T61Cm%u&Swgy**c63kVp0V|yM7_vkz7jkw+1H3?_NcbXa2QR`&1S! z+&YBgY5aZe3Oz3Y&y0-J_SoE$OJ?^Y5E^umyENba+t#hf=fjWb@y_QD-S_*?k6rg& zYCqi76Dk6v!l>?hqKLvuFrKkCcX`eYORriHtB{LekCARf*i6xO%HyN*j5mwg%*8!T z_-nF5R#R3`E%JC%un?Z*bLKZbmC(`y?h5hS4~y5*hgyC*ji|t|>+*|`-dcqG*G|Tt zEST8(?OF|TW>rp<0OymrGE9zAlwD*|y}VO>>~H8Z91s2Imik`Rq+^-6$BW;-O~_dA z!0~$@ir)8VZEok*1Z^bx^25FUR#w|5ZBYL3o!iz3!TIR!4dM0kJ3M$Uu6oT8;CKYy50-UD6m_X=r8s9+5$+sA0zy6pqH_&Z@W^+??+HTsDpji* zpJYPs-t|l<_3g9}ngwho*oRGjLvmgR^?mB%vOAB;nrI30-@eap3v)1iCsy6LJHpO1J< zyJZ4Wh4TL8e$;A)3J{xrvG(WSc=))?Jb7Ude7PQzrs^QKFUs80=y)usVamepIs@|w z`Iz`#mm;4!p8c?~+N=@YBv*C$SE3I503HJZ0R|PT!IyVtgvYdpEy__RjV?qXKeZS8 zQn;w-0EHEP$J1*7n@+9+ndkivReVrStsXO#HIyz74ueJ3uc5Y(sVEe}?RntR{lQiH z`Z!qQ;Og%AD&~>mulH;=Kz}3H2_E@LZb@~4srs2{vY?%@)Kl!Nap4D79D{9}Z!`{& z?#?MOm>og((zofbkjOl>6O9@pvqoooVcjc^C-#xV?L|D3rXAR!rX4PzRkgx;H70*D zI_Pqi!x-h~CVp;&e0Ji8#XXONI@+S1=SSfqMQ>WVhhw!ZpqKaFLfG@O*E!;9JweoR z?{TX1XS6B@-~)hQV+wZL_soD`{+?KKnJh{Y4z>ugj&n-b6_}jBe(jSLX6P z&9H{W>AHrLNjvzbPKRmV@tT%0mYUCuBT1kvP^GO=`ICpra+8UwYXrd(pWPuzm_4{& zWk{u~y0Zv8Qlt(vtPO(#zX5n?`VDW3Ct(plTSM;$<*Wqlw`Z7-AN6CITh2!btkaDu zrf!`e&u14f%tSP&(Dnr<9bp(XcXW%tYO*s963nBWA=#0746gunNA6vAeP1s zh3fwN_Xo-D)nJ}kr8L9iLhlp8zQQ{nY4Q$@E9VtETvY3caFqEe?wB~cpWg4cy=Whdd?Z? zXPs;EKDvGsP6*bHo;Asedj+UOAyPE`Cwl8av`E7KMRPx4{M5Nm)na^3~o1fyYQucv~N{FBO$#$%a?f> z_2b|tKXBB$5)5npHFNe?Zy-grTI8sM+$}L__i>e2nemkwx%9r!i}lDhBEL!$_8+d6 z#LJ6vr&OO=-?Wf@W*)yvCLByyX|NQV|ecCy7=VAOB)9BI*Nhl6$m2&;G5gX z7X%M-WD-iH8(`K^IByV*KC4pkE;Q%d_{*#4?^g1OlJz4do+x=4js7@ z4A1i5J{^EH#kWeooG$|j7@#2|@kwpNNOp2q5tS?TUv|0sCwg@^U#G?D|NVyEHk3@4 zh9QWPx@!?z6UooVSfd6QY0LCJiII2vLNZ0~Jqnz~Z^l-ou^A;QU;}AhM{s6oqmA>R zx?|OM=&u!W1Uio$0m&-Ry7O|=MSkJHZ2nMCm3cd2v986rcYhXj>{)~`rp~In^`jTf zFrXGkn7tKYRu$h+~JfC4LO`D=-Is- z`O52#2dQHUn`kg1yFQXPBn)1doD3>%Z#Qc1db!Om^YRfrJIQst z-;fRaT=uTy2I$-qS|{FdP~V|NDf7ik?ZkYCef!_RSVV*5*a4(SshTJnq8S~a`-xao zsx;}%hcFK5ULvK;gHS_-z^^qx#frvEWpEI~{rtfbuS8wSnx+wfU>o`2dC=x3`D zBhoCot?)M$PTo$u&5L;JYCKUEb(v4VM%h4az4C?X?!Y6cb3KdhwS}?e9dC7;HdnO7P%wI_DM;;s)@@Z%bXbtAz>;d_JUlP#%eF{9 z&G?mfv!)Kp4BGm-`S$V!e>YW%_7wOu6Y@dH03UOV54u#?t3zN87%+2DV4y8UA)tjRAF;L2r0P4{}i zS>CSrwAQsVg`0^P+-P9(t8Inr_eUS#5t?4*HluhdNj63cJr5&s250OW1_Y*Veacuo z)0zW>;IdzS14@>TV9}D^5NujBuLsVE+*^zGaRsMzd40GW&lUtN9c}wb{~oH-rn5i@ z8}x~^(V56NJ>0RjWulsd{#z*g#MP3;$Kift?|Xb^>Pq7n-uera3;fa&%Kqq+sTISU z>9I?T5p%nzkJI+%EB3-pvu^_`-K4BPitQJr=<|A1pF^2$^d||Im4!Lx+DZc#;0d%Z zU}NxmZU|4p(!59eAHdzA{rqw6Ka=ssc2YVTy@Kr%TweSx7~PHI0$Ux(MH2xP>83k; zbDo^brmW`!))Eo*!~#*~(W4nwS!=Y1;yzh_{9+ERu~TOO)jk9Zv~B;)rYQX6mHFEK z$FpwAYy(lY1r9y+I7I{>9?geW)UF1iXT09htM#|*5w)gCZMKyi*_Ji;8TO`jkr6_D z6d^;@Cn2~1@1t9zQh@LC&YnCIm}xot2eOM8;p8qUQN8+;{_dBN&^VM~s_~5G#LV6m z_E3xKqtq!foUe8JYAMWpG6L66c?}#MBe-snYIx34#${6zQ+joY8Si;6OdZ&ke9RI9 zhJVE8S27lRcxM1to&zo06ulR~=)s2%EoSb-}Kq8vZm%56`3bWG&{95m-EEyf%f3 zH>Hp1P(-{>oBt2RmrZ0^^02K|$)u`-lkn!CnYo`C98s@Jf)-Nt3YGS7qu+WJ#ig-Q zFrQrF(9BS8SkgJ;+Ad7Nb-pL%EFha^nT1{-?E>u#tIcaiqZ19=37#rTd8pgB7g#`{ z3R`W-FmER}xBCpl>6-zNKPtsGV+;sy5|;j2PzH**0v8xbiA$I)z;nGF=f0kD;9o80 zk9RY17@+hFh@PzHbGN#U;3$|?cr@7<-4>(%aAapZ`iHIwt+VtBy0LH(1}{C)3kg3a z$axD|Iyt-X`@2lAY5noiw7Ges2e_Qy#ZG7g7!r}~R1hs0kXTsZV6s<#V!mFs#>11$)A=<$Kuz z!efePeRv291X1dfQaDLD&pz&rySTeJ)gM_}RHN4$p39$|V&}Hy&}+?dW^|({y!MySY<7Jzg!O zf^s9Ppls*TLgM-SI9c;jdIIB_?_E}SC2dbL5<#e@~e!>h*T}3V7Qjuwb}kpd$k{i8yIhNxcWp5 zmhr}|T%BZqGQI3rUBDr76MVryhwI4_s>U>$O&%JFqpibpT73JynWfVyP9vAd8#TkF z@b21lX~Xp&JvEw!njH%gzR#bLZ(HQc-x>V%ncNiNZVJK&R)GfUJ{=r%@BYj|e?tAE z^QvUXJVicpo4=Ku(9&oBMNT}AFs6q4)YmcNKs}&Yl3qAPrANKvAX)cQ0-_JnGLH^% zib2!LEZ+!2?9Xjt;Vsr#lw0vn26t$134ju@;-k>6A|D<1f9{NA&6lpAq^(bHU;73`4+N|^gyuiqNV6V>4tiHuh2}gS>rpliJMYF> z8oV`hL{!l3Cr!jFuS`U(PLYOcg;mf+q*tapy-Rrq73i4^Zr_D8w5!nj+I0u!FF(jA zaa|Fie9MYyVD zY+|f$aJ?0^#q(7Bv(_Rf>!-!26{dkm`vv5_{yhqlfE=-JnrnR3CE&==9oG^BPJ~kT zwR#L%pm6XWo_o>~-xFwsnFCS-K3SEG*9n3OmOIw$y|;&`Jh_54%d_jy$;Tc2Y_spR zsaIH2IH@qw%s;q1T8%_~*JZ&ytt);Fy%vh>g z0w_CsOn#JW{R5GsH?OEs1xr47FZzM7B-{&lNe2bAnJ#CYkWk}CK065tB0jzXv_Ue+ z&!kU}(r(0*6z9AtXe^RO8lX0D<%I!#-wUlmC}2X3R^;0)cuXyXl#01U9aAYGBNq07 zQ0C`^>CvlIsr|X$a@#JlI=!B?psUQx$bJ$^?{z*pe0X~bm^`c#V&s{0MlZ2T-y>}F z;qPquk(Pkc+@>~ButddAyRL%Hp<*0=QjboBwPSW-PHOEB-@Y}(p8aa|yNnqY5iwd} zMW09Non<@D_S6*Yt^2H1H_*KaVR?1$sYP$fe%28z_TYR*uvmX_{;5wg$t{cwp()qhVL2-qx3)1wM*a1-Qko7WOS|m_n5#TglB_)$&TDF_|oOK~F z5`+$vb~~{DgX@<_1p#;oVwb#0EZ3TI6$r55L4sS>BE@dTA#G0aD>84pQZg}wEWXX` zi!o|(wQ#4Y+7TC_zH2&(JiwOOYq`B)ZMOS$()lGjP?Re|ONa!QYMvwZxST#y zqxy;V%ft%25Xi@T@m(kD!pOvW$-@7ISP-Y%N|Ru>0)+_1!Xqh6yx_LcFNm{O`PE!f z1~@)qX~N_wIEb^f5u-?lm)di~;Jr!!^i2p381+NQa^Cc41Q-KE0Pi#aTB>o!<@$c% z*Q&0@cBXHDTZ2s@7*To0m*BYhWJwxEsgU+sx@6~uz6~lY%RS;a{p~AC-LG>IUop{T zr=uIPav^B@XZ77ba;qQ)w|Dxt$Q-fY!I+bh=a*g~Nhdb4cY<~1N)F-&Ui>SR1l(Zm@ zU~{AX%FoF4u=?X-SNV(5k>HE$9dJyNJ1i`5o7!u7exC)~47YqFkDvB6Qvg#`GnW$m zy^C0qY~lL3`HdJoR6L$C-K(+><84eipiDHzaN)Qv$Lvk($43+H>IVoTphDA%<1OV7 zN*wIOIb>eQ)`8RyzvwEjennj>vn!@tYo7b3bB?40+SdR)E#yrS^OTn6TmN05HqK%l zP)ZuCwf1Dqt9nt}M75{7)xl28WCdmP&nv%F5L&v^Csh6lR4+6qW$%QBQl1y9g2m&zLQodlxDQe5t ze74A-pBpIlCOSp+vzs<1{?Jh<5)t`U7lpH47Ax0o_SFnzt-ale`H{M8h&qB)qshbx7Ad#HNB$| zo={%npyBI&{m}+3+ngQmW@l~dYovp+my{i|_PyEoYucnl>EfHm=~;&)!6SYGXW9S; zu#fmK+2v+_G46lfe~J+}-wMrzj+?*^#t`G>E$l*-E7%bPB)Ef578L#cU|%dTi4@hk zp;+bBv%g-&D%NlYIGgkRvGc3A&8QgDxkHez9M?flQx3A$cKc(&?EFW$uDMSdb(QMw9odi zQA?zO%QwiY&D&*2_|La;le8f+v*;YqftP=UX(~GO>fBxRS{^y4gbh*RyJXj3%v!%! zELfdXKw~e(B^eo_RBX;Th4TrEi|2p2@Hg*5bt%Y7ZIk$P-}GUj)gwz0gIBAGiFNn8 zU4&Na+V|69<~TqZyxqSPaeGkw<_`ynX{4vBxwIX_Ypq#9SqSJ=W^R4opKAeSa3L{m z&lHRtdQy{5Ggy~SFu34>`lJ%Zqqg`)p0E)ulwxhQ-;}L>tXPKb-xTPBQs}1)CSM*$ z)G0-&fr8_TI{4boZwExp&4Rt|u<&mI1_Iy+`yv2(?Zm>&!E#z5*xWy{v=^H#tjEA3 z;?O-=$gFu6kw*5=S@@t1PtJM?AR~Jb<+?`D@ni^f9@rf(6M@{G_~V?Cy-fQf^8)n? zQMliUqyBPjXiOCQo#z#uU#^qooR+z_tHzkiIsIG6rn#gWN}koO1iCdnJ2E?}15?Vb zHv1jpiRE-A-RvipUQ>D1lRSvmj z7W3Og%mVd(!g)KZzdxx03y^c4IMqbhs;z8!D&FY;i56b*oQ6$WJxRAsvOKW!wE>ua zD0mc=bW>_*_Ph03EUervAR2#dSHw8J{!GR_N!df0ZL;vK+=3WRYyZ#GgT>l0+k}~1qIqt zS6WmMZM)!rz7z_m`fK9CHVM8F$z&G%jWzFH!hm|FYpam-1QF?Z)lPOHi8}0f1o9EZ zDHf!)*@a?vnvbdJDr!`&Cqj=g-f;y=uFs7+Jzk$Lqc5IOB(A-BqFIgF5T*Qh4dUC& z&KPT!3?JZJ?!2FGI-p$Yz1pL2ZT@|G!_!$1J@*9lY>pk*)lpl#C(!j;vJ^FY@2K3n z2bIo|a*SE!HzHgWM{6~I(^a*s15DV0tUv$zES9Amg!xeS8?y}$1Z}K#^z*n0>1~He8ZPz~6(W>wyBjvX_I$UA!VL?CFEa)<61QoPZ6E_lJpjc$tmFIQ8ZC{iPDf zO2-9y&-i(=bBR|;{%~gM8=O_tg<9F|DLGA&TZU$Dmt&g50M3#7f)z&Uh;BRwc9Fuz z-1wDw3C{{c-~!Wkhp>&;jVmvmxQJZfG-RppOg1^@pFD4B;*!n~lLSmHhRBGUZW=wL zrq<~HsA?@Fl|25*Z_6NPzj7X+}j+I5Z=nZ2_bWFC7 zTuxY^a9H;EY7yk(wd>FO+r1&Q=A6pE#dPEy^vWSAqgg}SUq@acOCxOw#+d|Qm9XIz zRGFSu)D?W`_1iH$=?m+!uJ;FT$Ox9sW_Mi@heywtUNevsjY|GZ+9y&g$4FCA5uwfk% zf*2q%_Xk{=xlxR0V-lrZ<8c^ny0kflt5f{jx54mj|S>kwam*Tak1b3;( z5uPT_RKvI3-JN1xNUUV?slZ3MO>r6QL6oc6t-jxIO{GxTrzD(yK)QDPpLm+v`7|p} z2gy(VZGC&YNw^Sa`UGiI9uXm!9PVra7Ew3o^o&h~XSGDkY zs;^`*cxA6xHK0$Wic0L>UEZ->|DkX6j1#<+RIHQm=vtR9K&^UG7kBp zohssHdJ&9qvGa3a$c)-8t8?K+cH6&N!v~A?-<*cwix;^Kx->T5?74h9@7rrK!RqW( zo2vJoGt#1rN>*x0wCL^Iy~m|a9o+HOx%%|#GJ$IR^@H56PS~Nk&64x4VbME}59a@h zAqcjHo2qUpv4ru+gtljF5cq0UfGkddYadJBa9qH5nTqNu$*6Eyt0)uW)o4o zI;X)D{>#dI8(%wELz1GF@W7BU?iTh#pd^;0(7A|qgmkyuW5DgLce~io- ziyf8;ON`-an0(auAd<+A^E&OM70amakbMh9ou51y1A4-pKz;ftECew{C|lR<2EG2V zc_YNUU-=dDwpU#60DATW|2Y$&LhL{Md zgU?Q#<3)i(y#qZ1bzpAfA$a(p99$lv#>L?Q)GTy zvV36GhERupL#v>^msU5ZmKGe6Pb0Y50Z_*r_EQ}YYljZ+66G=_SknIB zZ29q((LiBZotu{WaHM14bGk|AaDkw7pRRF+J)Lu6k|cfbwnXs?-X|W_s!|@*zFqbI zKH(l_gt(*O6YGy(ey6N?m_zU{`f$GyG}a%6%QeTyYV_*9CTC!O*p|m9#!SnxQYjCr zx0?Pz4pbv$bbm($)?Vpu@0tzWHsS2>)v#t> z@)vmMMS@d6sl1*mp^|5P{sVa2Ydr|^bT4x;;m;G%!7jv|MnM$?)5Ax-e8U)PJP1|j zw%heI;oCzyygq;2y=EfJqsY192X~vsQkXUXIO-m*UbQ!I#`v`?SW-Wg`74otU4C1v*?+r{tKmsUFh+cJOFn%ei*x1dOd6 zFdTHO)IfMfuFw1>5}qFUpQ-y^y)mXc>I%0whfG<;p=IXi5i)%>S(gUE5DNjBWKBzr z_#Wcq8RL0%$M(|1pAfjAhgbM^y%{*VI1Cxpv0wt>7i8%;SsQ+%*i3Mo@%ohOIdc9n_pG$ewjs26kJ$SwQbo^Sk8@-{F@9Fe^jtAAGY004(QP$Jw zW%MMJ!r8%+p2x)wEYW>%pS&FodEgu=HP#p6`0Pp&o4ydp&i>(Z~^F0082|Xag}ZxCR2>ZQ5t; z>A|WQnDS?znrt%Ye7if=pzl|H131>3+~^IjMyPz5ZIm@Fg=5~D$N*x02W!5TwV`kb z5cs|uy{8RXJNs9M*y;%C*|n%;`^I*cHg&PuVYA{FO+N1V#OU2-1R1gU@ug@Xa?q>b ze*(Sl%OV@%(h7UJ-Bu0-x!o!4QqeLO#F)tNvHiyS;USp!I+M=xg@Z(rv47_0_;K4l zshut-0EL`c=&=BxhuXPiRDTm2%{M?W6#9@tfK~EMaZ8WoQZWLcVe@du#-RsW4+z}g zO%&Y$Psw`fY1m|z2k?BkJbNCMBPap;?iM?k=FSWB*Y9pWRVL?x;LPus(N-8_gAb^2 zM!(Sv0At)38Cm$o>ww`vVSsgov{ zCdYVS8Njokqj9l98H3CsY7CH3qo`^|-M;Kkwb$*2&=wdc*1-MVk+~=0au2!?|GVoi zlb*^0KS?Cd6dOGkZxX~LQMUMnNLwVqKjApVqAuG@J2V4|Fd>bG08(u4#?aCTUfwsl z{TWl42|bHA2xHp6o%d%^K-JUV6R+VEJtB_j^juRPb}G3*dpx1g1>G$4D|Q=s2G}3F z;M%u%O4iu*46HuCLsus<$^K?YHU&?^`|2hfnKp0+1Y(JBc(8|T9J{KMB=@c(b3ro2 zd}F1=?F9afZ~ia~4`SjA>gbccd%Z9QB@zWr+A5TT>sE|}xp#hA#&LC`+{fA1q~Mmx z+3>dUL=K{Nck=f3=8SQ@%l>15p%Xoytnks;MkrQJ`6T31H;fuO#pNAfE-KSZmMP3@ zdV?m2M1M4Ni5x`?cm$`5?d(F2Rn)Mc246oiYT~1vAZvcRa4>RjEnY z8NB%znB~)cz7NJ}j%6vQisQW~_;r>G41dCv^mugKaMV#j1*e|WaXQam%?@nx(d*kR z@V)Bo;iEq2(L+y3>yNCS^$`W~tUB=5o*d2ik0YLVGl&)hCY;~+g$9;+2nOIL&ClSa zTuN#y(f|?&^pdT#|Ez4cA^jTq_=Y?0|BCwVa5kW}eTrH&O080>)LunxYP43(*4|X@ zy@`aP_O8aBMb+LrYL6iH9yKCnjTi~R=Y7B5`2U<|Ki74x^W5h?g}(n)O**8@D0X7% zVv1o98ti#psHl7+4G@z!_b)r-6_a96mysLGA`sTw(Ba-7OH=r)+EA&MQ`L_4tX0x^ zh97RKX4$v-B12RoBIkh@0H=2|>nW{0opXR%ix!QX23G=kLL=*dp`Khm?uTVT%=5qU zl4gELxb+XDu+fPBS<+5c=0N?{hS8o(nA9d9b3JdK`8G~5DcxJQ00$!y=d99=`xY)w zp-=NHMv)Qjt9j(z87hEilFo(355}q1@Z61JoxzK+smK_6!asIS7%bE2S{&+M-m`xqaH!!UdGuQ{MHaAnI2l0j<#hiPzCyfQYWoGe0;pPvFm9 zT-J;f{>>*8e=-gaW$IrStoFN!%a~L;Qa~w)fv1KAARO8J#5#Sm8Z{j z#VBuH3O4+H@pkC~JCMTsw_Q%vgPKQz$H#I*U>;hwTpuL-h7cqpS2-lF(*F7RD~i67 zB&2SfG7B>msr15LAdW>s7Alqm5I~DQGk<7+a$^#JgrrLh9s~7$Xle9d(Mgo*vsD77 z{XEUQAQbTUUiSPIpf#1~#b0Qe-(P5Lc5fhIUulw)PBL~)2q*Ap5kw1*lb26_XnqN}@H)z34&U z?4Hgp4HD1g^PpCA;OR=)fDO?6y6cAq?_jC(#}EdCh`QU>IwX)KN;^qF`M~?}m)5JT zP`Yj~INK=K`7hKcie~x|80v(_XO498{ z%^s9ZU(A!qoHI=zrty!fwL9+QM|?owwFzMRf6~AS2FK|Vrouv>ZbLV&|7K8fNZY)u z_sZaM(dD5>N()A^cp|44v_qzt)7Vu!$_hUiHdi!+Gsi3aMT~4UHg=v|7Nr$)@50{9 z>sQQ{(kob4m;|9pD;r0~k%Nr~Vsm~KY04(B>;tCiYDmM}oAtAst`I3MB8-^1o2*4y zg=}#5@v$pYJIkkeVAjPefCS@EAtJ8tvw2n~bX5N#2M1`#1Ca#)q+jL=(#NqNRit|l zV;QlZ#8SMO5qsok2-sFZGbtrhPJ{>uIw=e`rw!G+gd*hp>*aCy>? zvFOe+_1UcHYR?BD$%7t)pjqZN4t<aVv#X#4^luROO`zvzKdla_cXG4rX=K-zCu|J>K`0jQkZn&>rh- z>q*zkKe)=0ROa|p#N4B4M6USBET+lU%s<_26PUl6swgZeP}E@(*;cNu1~k7XyBjLZ z`HpJ}_F3G%AAjI!fpx$zz!qTGfrip=ZgX!>06=%A<7x8awY>DVcI!75wXO&#Uzb9A zHpP!eJ}**?zDle*Ov-CgAC3N^=C%f#m_;69M2Pse-+jVicE?|p7pHyz$4(J<~(i=wYOGLEU<%oiQ19w`jb~5lv3X_mQZu-QAF5j zyURDVYTRjBr8W-84N##WY~6PKt5@Up{EN%>@?_At1##d*91dmXm79_9O;V`0J-&J- zpK)+*(;)3(T5-M#g*qaET^f{}zKnLz!3M-K{r>y{M~!|6dK$UU0{mKS1)jh089wp^ zYd{j+YOQw%d+yQ?e0FVr=dgLi!3zTw+BkM`_el7$gU;YJ$1KNg&gTayx7TlO%4d!M zt?uykNvryn@^{l4w$F`sbSjz%J*O15cln`|JisON88##nfPU9$(VI2@VJ)y4#^{%M z6js!13fnZP*!`ln;HMR^%EyNq@W#*DCvh1TYB6&#vZSlKwm19H~JQ6?WU;JO# z5kR7Ld^&MB&Ca1I>0t!MCA?GexWe&E#x3p=}c>M%Vwn0Sj)w5+(Zh1v781%P3 z*?dm@r{9L5rIzX@KJW$=;>v3tbcad25&#QagCiBE75^)48;W>{K&Dj_?+f*XXBZ!F zR_V>eQ`v_Q#P&x7ry?n1VXlqKT`eXnzX*Ztign-ZO&3fsm%QACV)MCjOiNwT=Rf@? zyE>F^p~Y9X(2UW~pQF3J5l>#Y@4~0|SZ<;CC`X;(%hUO7L*CnkziIFKcH-Xvw5TOh z`hM3OpEVQYrK*@}CPu^F?*}utYCbXE)Y)67QZjfd%Vop$A`N=Hdo30DIIr^(gHF1G zvq(BMeUX^Ne34-3H7~e>%PNPbHFdm}aWQ!^X#P(YL}d5S-T0_|l4n;p!5Gm?U+7fP z!jB{4W`p$yzKYNU-Cx{?4&c<=Xpg`J$C=E?Pll3-8jyKO;5-)-tLhVDbw&n{oQEfp zof$G!Uf&fSJbY-BLUn8LXFT7c=|_TU%MEA`XW4~ncv(2+JJ8ZUq^W_ev5BP!uL%Av z=w6fluf(qR<`3BpQd!vW)pW8Y%HvP2CAg_7n2!jK^-iTP%`tGDw?^{a6(7LAxz1Rv z3)Vtc$M>Et-r$@L&XwlS{{#* z%?2{~t{;8&ntME~&j1RJ1vVdO;f_^L8v1izz0`GA82%;8E0G;Q!Jbk=Rk*Q9ykP{9 zwvb)l!HhkuHYv7Ct~*nRc}1w4!c$`~1^wOja3=&Y)f{t1-=17-oH(8FS!4=SyXujR zcIH(75Xghz3@T(Jzoi37k;X zrbjpVDeqg4O?>>{{~ew0*i0`}sgF>o_H#p@!M32sD=a(I5fiV}V0=RFX)h@kwli7; z{v~k=mD0CJ@X^Ot(aifPRR8Z|g=rE&)N^HKn|fz(F`b91J~!2` zpdH(30GLb5bz4^RmU)Qg7O?xh9x>9j);4v{eWiVeBtoCjmo1|`ldGQ<_GkYnREV0? zsed4$`tejon3!}p!kRPMC4qh3`uXcD?cG!Wnq;f%-WdXr5n&=$7Hf3o7kgRFmrzTP za(2#kiBiBUD&q6^jT@>qc~U25YJpM&x~wo)d1K&e6S9=jH+B`JWUvQAqO;(17FZBK zcx^2vQ;a>m^3e;)2OBOjk*fw3<-QOGF4nJh-Fe7D@)QHwu-olV&mk**>sJ#6D_-mi z1iuSrns!P{xpKoTmeFUY_g+8@<#l$B09pU8vjyc5#dh9+T8)M76ckFg{#yX@SDV~_ z(eN_~_V>2%zB;6U?-2mK>NM_WQG4enWns>yR_=e-!J)2Xsl~^w{mOUq`;0#r6oN5}O5)y#~?c?S*h_@upl zQSy^#c-Szn|MpDkzu#dd+?fu+QO0NO2y=9U~R?6EJ(#tAM3y9Y}Pi`s}tCNwwa2 zq;(h27Sf=*EPTSC>bujBTN7ViPPcB#Ecj15jlExHvqY+ehUaeG>K1x~-ZQ!Nl=-kn zbP)|!kLykq(9nektRqYaa2aJ4Y+HX~@SiSv>0jRh`im5=!Js~^^?mSxJKTMHjY?v8 zVIE67<#Il@C2JLsypu8oPFN?4$Q&t=oadNY1q>5`q0I*^QX6R zD4HPWPxKb^tRKjS|8J1^U8ka6>G!fSg0%b(KS1{x<2i#afYzM<)w5L?N~eI>r8^bS zwB=5inr;qxZGSPSOpxdJUgs4XN6ekD1eco*;qL{MrcO!6N!%)#{81Sf_ZdZ0`s`&5J~>IzYFU(_%TMg&eCB69q)8it?8MkVAL;BV zxo%KgVZB&PE1{6*vo?tl;p6&BEidXAq~a!gR4^!UgbY4PvXoo}g@|oO-m(Et2NS!F zkxPjdsj0BVqIu_(Px80y`06F@sNN1iwwb6x_Vg18aeQURHJ&uTdSTCpvrO)&fEYq6 z3kicA_FqElr+57>tMvTaU`FZ;BtE3n-*3WeS*+rcB3msBs|q#%!*V=^&TH|tO#lug zbPPScgFy-h)yjm{HnbHr;gvzdYz}3F9Hr66nP~TxkIrmX8^Z`nJ)!Zys*x~i5yyiA zFG+l@ZEzN{bPSEKyJWqYPfKh0%D~e4Nnf9$+>x0>>jaPv0B}yxMjKK9dN#INB!6n$ z#~M#K9cC)sbjALErQN{AgfN~}r#G-nd^BSA!%)DPSJ#9DdyI8_|DY6uymG~$2jpi$ zQ>-1y;*M|Wxt4FZ0VYXZ%}P5%g)eAZQA2i3lr@%Rh9>Gi;cZ+?2|6M>ll z>J}}1wB{2?<>u6mTRIXu8b_BX{J-6><*dVT$eTBT8J{L&!+3C;BD1rvuYuhHF;8{8 zQ)^BjmNlgbTkeqPm6b2sPbI>@NHly0`qJ%m4~6m$k2 zIZ(#DZ)glNu@M>{^c+DeTglVV*KE3 zz`=sp7EzVg64RmB#$|Cuymg-H0)A)kf%y1%`aw98n5=6hg=p&P? z9q7RG#bI#wICqbtjv;#y(GF+nK1a}HbB-7tdu9GF$2Pgu_4T~DPkel(q8XK3CJq(1 zAC&RiyOk-5UhcMTr#5%4ji@2Unq*H7_EX#ugj1x}^sm_IViJ>6VtXUE;R+luu`SxS zid2!9y_hO<`fuf*arD<-?Ha_lOOseuPzM8$bU4?A*sC9cZMMek1n--73oL!8@)pjyO^GmWJ17DxbFwwZ?>PB5AxD)L!t0M6y6OJ=5Dsw^k3~)39Ki*1MN7*Gu^uS zcn2ap+}(4ZHAsif2>)KEH>p06lgOv6=0G_2N5}_XW_dM9l$k0lJwQQXB6!9yMal|@ zbXo@n?{+f2J1Zi(fb&EZvlPlPkN^fu8K=Oj}FISvK!kkR6w62xmiS0Lm;_ZMs)w*hs^uk@r zi!K5FkcuzOzxd}}b#6y?Y{2IK?54LDxNG%A1Hq!38nzu+3^^G z<9OWrZhVDE;@Z)L7>Oi}<6d6_9`57qhu@MG<&LdMm}#<#QEi@u&Rwx*`77q-=GEcA z5F^+3wRv~92WIm^XWqu4T34W-bOy5BHI>DC-7&le9XJIc-9a6loj73@iXV;nNy(qJ z_}?B;Rr^s#lI0NVq)>6Gt&Yoi$uQ7-F1?^sOvJTP^G;16O92yqCD%ml3T*6hMT^cD zRhluHrmM&l%HA}1HO(I6d}*G`{Da!T;rmwPC#YHqvN=t^<_i>b>q;Ga&Zq?e7X9hi z^?Kf3tyT`bv}nw;|Liab90mNtt3>fU=4x!t!~U%^>pt;8zx2nV9QVoSvRJMyNuDV4 zv5Vj@Ls|1FBE98xkWy@yx@M=zr+cT&=69&P=^Oe9ecMjl?YCGkkH3tAX6!->L<26a z-Kg!x>&h_wj#OmYG;#eU#N4-U&PK*y#A8;EmkrSyt!&*P^jcaJE-URVhK(k7!I#}7 zc=cQy|EzTJo#&*)%~(VeI)E)Fhz_~56ulIyB(s=2bG$Zhg}O%hcQ48ZpVFc$ty_g! z4u*znqi}Gr_df07jntKq-7VeVMQ z)(4M;)lp~vVqfa%Obd9n-rQ>an>tT`U`AzYOGZSDWm!PYkg=p9;0|orKEhTn=sgt0 zhEQj=P+%$H{P0mS#W^G^8rz;o_v)Z*!`XJw>E^K0rOCb_mN4MOJoyKdyMC7uIc9qs zcSVNQ;d+48Hzg}l)fE*^wjps=YV?!StX^Q@=F8I-e<4F+{+B)Oc60S=0(*9F(Hart!5pnRV_aE_nI zmVuGYkmwOX`_Pu(_Iy=PLlpa;@!Cpv8tCA_a?yVJ`_lSP840FezVboo0}!P7RvJ_R z%{uS@n$mvYl=vgv5%DPIfOfiRRw~*9b@9XND9E9zK|!HOJx+0-$jkGj_(bsap={g} zQgi#dC#hM3c>CmNhb(dN^QiHh$UML0pU2DRz+b5=D+ zsWOWdnM5vx4IeU1IiE;bL5t6G0A|xb+X}sS=8pMK%zk{f4%bmba?HMRt}ek7-rEj< z#fvb0@~Yr8mUaE@v77VUg8ua)b|$=-eH(N0^zd8^ZAeN-cw2_QKw=y(qF13Q6{n|f z|M!)oB>&Kr5_DKHr=^+*rB_gt7sZaMNyJ}&uajMfm8{TL@{0JBCfq;$D#C+yezLb; zd|T_|=f&VkKRy^BFvXaF=-a-5{Z`eS_5AaebP?Q=PG&*LD`(%8Pp%pH^}ee7-`+;_ zFL-A9o*_P$zCSMt-D2j$k$5#MG<@eFcOUf4^oNC|Q?dlH2houFlWYcmg=05|%bh7? zeM~}MtKI5_4Fr&Wj2)r15)|}*x_nSwq*UyI@@N`xST2oVpT5N!XHi{}D^t3LW z)QWYzln?}cv`F-@tpJ-bx;2s|w(^WsB^_*bQKh+#fV_AwFOu0j+L zhwf}0{96B>DmmoSin7%d_O_O{J?}3_-K{!xpZ7NQ_1O(piGa>BCsb~N8fz(%;B5`S z><96Y71j{(#eq3vk|K+edR73!{2M5dH}c1Qy|cIIhJzvK@RXPKN|HlJ7Jc}YZ)x@R z=6GiB+z>kK;_-@eC`_D*ELPO!BWtwUb{4TlSlBi^{-ZU3lRqhQOT4Oj1Jq$=W>0VM z+{dD6A_66!;&N;G?v>?NJnBa*+$P)Xf=(NM%N(uPBV1I>u+xMQdzMejPXd3a z9q)SU?37-g=>@v+(O*b`k6cy3-Gpik&WnP&pu)H1!R2pc?@srJhOS1qYmqM9$E}w4 z(b&5mLotm9<t93*u}%_?&I@<({Y~xI@y}YYbBk;1;BMyD z;^O|%)9HzryP2v{H^`S(=iy}m#Zv?v-Rx5NHb-kYv%5T}@YGaUER3yRC;>xehpD!es1gMDY)rLAZ4`DY_hw!C7jR>u(TKM-eB8GtSm3a zstZT$5maSzy-rWzwtu?^K)ymZW95bGe{|MtH1A7e^2Jj zh&aEAV%iw0dSO6u2A+JGRA_OB+bc^SPqbZ!3Txk_Z=2>rQN z=Vock1nN#SB$^R)M-Sle9ulB-9$_v3b(duYR-=9@OfkQ`+}vu!_ReUIg6erUr9` z7^=Hgn6q0LrwQ1a{$~BSfVntOrqCTWDg;%v-waLrPIGb1|1^KhHvi0K29+EG$LGB| zUTFD@uEmy}4Gw1v9*w+?J$S?KW>^EXx)N2+TC zhONu}Nda!+B~dT04W+#&CLTBJcxA6 zPcr?5?VaFqQp3@hM6^I-40PiJ{kS5$gGlOXz$JK?u_l-{sk z^&S$X))sE=9Q3;%q{FW@Czd1#hf#5VtC(ppQgOw7E`vkrTc^}|fQ-3!v_JhmiKM|HrA2=Bl&?)2e)`;lG^#ZViDV4_R$p6~Js? ztK4U6+^#q|xg*yn)6VP}v(xi9#8;AAr`&=Zn~=W#0?9ANmZ)LzXh=a~C+wtPXUDyM z6h@*TXZ5@<{^5>Hy!mSll$Etg)A9XMn_4$PVj>{!fBQm>(Uu>GWFg-A1U3%q- zIW{nU5#n6K@#^b}C`pGruWVi~g0^OSuGJqe-QckH;(U>ljsE?j&C@rLrKlj?dw~zF zSm$QbZSRUF!86E4BvL`}S%M4Jt+2-qE~L|xS~P;Wva@JQTSLutv&NZLtoo~^Vt0tb zmjFzeDM|3wz>BmVNP=3eCmeQOYTx*7sZ1kyw%Bu;z85%+ zq@9l@iwHik5aU-k`WKtEIk@&K@n2U<)!}T5MvHm-%|$QF;vQ0)G6^N?rpU-HIrwZR z;|I7qQ_QvKy}ZrK1%N&Zke^v|DL2$UYEX<&c;LkykuJR<52H7suV3J^j*J6JKh0PN z#Oy6qY&&6Fk5bo94sA$KmQvJsD9MwS`}qFif2tL-SS$0dpI?Zc(v;*oAHxCD4|MA- z4F(8{p5fONvZqT8@lF=nGL{2+4*D_s$B(k5}$UmeZ7|j zD(=(@Hiu`Ke7^e^)z#Ito@z{&pknX+4Hje$XR;()V40J6`k3|ScoU!Pabun5@9%mP zmE0H)8ujqF3@j`{ssH>D@QaMH5^8TCZ^LDO{!!%PNEn6MW7YyC+i#)^Ow8An7w4hu zJ@(nP%+vtDo!CBc0r?3jw%d0#ygUU24b7gQ#AL4HJ^wT?jFCKsgZ06I)s3?0qQi$N zB1!(9M3$G;5+Nl%L^iTl=&#ok5~E5*pOeBWrLW$koe8@$Zw6)W)1O4YY46?P5(SAV zQT%^;4ds0^Zq*?DWKH2F&`MIl^ zWEn%ensMHAjJ3`FI1qZl*{@K`N&MXJDJ!0e+qa*e+GM{4^Tk)bR+MV8-stG&VK7`i zKAqZPTO9O+%>d^;IPwo^(&- z+FY-X4}F7=lL%`%MHaXyLv>oz)~+?>bxYyv?uV!4Q$xcnTb0^<-wehR<%%U;Jo>Og9FXpA z7+m9CzO^|~+=lCrvnjn1kK-e#&g&3sd&NfXGTJ0kul{Ll{gzl81UqJ8_%IE*41!RmC`9Gbpt%HjA}7%@P?8(&foUCm1E*2&oP zA?!^}75N2RqeGh;addDgdKQg0I&z5<894GRqif|!!3NMzWJqa_F-WrD_LYmrp1Hn| z-7Lagf`8mNvVumy?6;R;ff`k9|FlT-ilx{F(5Q|&)E(*xCmJ>xaZjpw`2yF}9d;*_1R z_t7&i=K$3fV-{5>8-EF-Ja#@rS&T{rkI-8f{%WI`b)?cK3Er*wIuc1Bfos##&3)2p zP)wC7<6gKp`E7wy8J?h-et+SU-WxMo1qIc0l;u17=TaMHv%A&z!NcLz_iUq}^ALcRQGp zO3#doE5|#DE|A17N&RrT%=+<_Q}UAjR}>vMemq*pZZSq4keZc7wkj?Tyw0KDeUqAX zGZq}z9c5m3xA==aFv2W4<~sN*{{4?ULGuufMXW;sxyI+iSm?i7hO@%9UYV(+`Q>Nos%vF8g!Usd2P z;4~-_8`!v6@(tpz_4Q(RM26{pkU|)UyNr=ihw-ukPHw<UpU+AXw!RaEXpRZ`!! zYg8dc?5IoMJQ2hB>hz-+?AEJm77QYbCtHtF_p0^ms1x@`UMtAF;}i{5AxiVl9DDpj zl)*5)Ng<4^TDD4i$KlbhQ-E&f_bUF+KzD6OX^sBayL(UNNV{|$loE2{yD|2UlLV?J z@Ig(y`w&7yeCv-`?uUV^&4RXrHsy&k@i}adNm;XgZ!a@xnvjG)yI_LjRiUqV%gYIh zTK1D&S;x6J%jL!y86wNhlMbcxK=q;CDA?OTEGBAUdVZ$JYB=ElyA%2HUEC_MuhHw9 zfP)~1CR0x8cHDC6+A8>NSYxQ2z$vA2UJn>pzZdq@C^#Xoh zdqe|=^fm{HmPOP#EjbbH25nT$CZP%K7azkF(mG$3cnFnvV!sc|V%0fVJ$l8KpsRTu zO8L$dH*_-Z+K;9`{p&$Rca2+turcwk=8~cyK0rNk55^Im*gM#q=U-^i{<0)$3uHRn zH_J=aK6A*?VLE!3Hi&0;r$KN%3v1#-jxKH%pl+cXKmYXX5gm8@@y1#xCav0t9od(z z48bdZip}mIsrXig{8+&@W$YEwRGTr);Lw|2E0DvqPPPlK%Q*y-eRpGMtZQa*dHiOB zm&!{b3*PxxlCIhz1he8Qe_ituN*=VlqosmzZgl~c62oxde$5Fm7!q248t=D%7jc(T&EAIMN0uPq5-R!nvG8HJu)x# z2l7Bbq!k*ScO@_{>}1p$JUt%!O}$q309mlnN$TVTn`5E)<0cDkchxB5N9ij>^1C4R z#OSfF27Mj!AhRy0lnNE`7ddO(RS@~@s9$AV72Rat8_}SIGlyS`bO`b4OLVX-@+it2;l!x9Kc))(Q=DJL~4JFw^ z(QdVI!ny}MfWXZX+W7j09)ZfAZ3qAKqN*1(7zzgC2SM1%t1q&GJt^ZKz5~NjeW$5Z JrC|B>e*nH7H{}2T diff --git a/docs/docs/tutorial-extras/manage-docs-versions.md b/docs/docs/tutorial-extras/manage-docs-versions.md deleted file mode 100644 index ccda0b90..00000000 --- a/docs/docs/tutorial-extras/manage-docs-versions.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Manage Docs Versions - -Docusaurus can manage multiple versions of your docs. - -## Create a docs version - -Release a version 1.0 of your project: - -```bash -npm run docusaurus docs:version 1.0 -``` - -The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. - -Your docs now have 2 versions: - -- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs -- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs** - -## Add a Version Dropdown - -To navigate seamlessly across versions, add a version dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -export default { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: 'docsVersionDropdown', - }, - // highlight-end - ], - }, - }, -}; -``` - -The docs version dropdown appears in your navbar: - -![Docs Version Dropdown](./img/docsVersionDropdown.png) - -## Update an existing version - -It is possible to edit versioned docs in their respective folder: - -- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello` -- `docs/hello.md` updates `http://localhost:3000/docs/next/hello` diff --git a/docs/docs/tutorial-extras/translate-your-site.md b/docs/docs/tutorial-extras/translate-your-site.md deleted file mode 100644 index b5a644ab..00000000 --- a/docs/docs/tutorial-extras/translate-your-site.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Translate your site - -Let's translate `docs/intro.md` to French. - -## Configure i18n - -Modify `docusaurus.config.js` to add support for the `fr` locale: - -```js title="docusaurus.config.js" -export default { - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - }, -}; -``` - -## Translate a doc - -Copy the `docs/intro.md` file to the `i18n/fr` folder: - -```bash -mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ - -cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md -``` - -Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. - -## Start your localized site - -Start your site on the French locale: - -```bash -npm run start -- --locale fr -``` - -Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. - -:::caution - -In development, you can only use one locale at a time. - -::: - -## Add a Locale Dropdown - -To navigate seamlessly across languages, add a locale dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -export default { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: 'localeDropdown', - }, - // highlight-end - ], - }, - }, -}; -``` - -The locale dropdown now appears in your navbar: - -![Locale Dropdown](./img/localeDropdown.png) - -## Build your localized site - -Build your site for a specific locale: - -```bash -npm run build -- --locale fr -``` - -Or build your site to include all the locales at once: - -```bash -npm run build -``` diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 9515b91c..2e9bf73f 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -65,9 +65,29 @@ const config = { plugins: [ [ - 'docusaurus-plugin-generate-llms-txt', + 'docusaurus-plugin-llms', { - outputFile: 'llms.txt', // defaults to llms.txt if not specified + includeOrder: [ + 'quickstart', + 'key-features', + 'core-concepts', + 'typesync', + 'providers', + 'authentication', + 'spaces', + 'schema', + 'writing-private-data', + 'query-private-data', + 'mapping', + 'publishing-public-data', + 'query-public-data', + 'filtering-query-results', + 'space-invitations', + 'inboxes', + 'troubleshooting', + 'faq', + 'advanced/*', + ], }, ], ], diff --git a/docs/legacy-files/archived/alternative-grc-20-spec.md b/docs/legacy-files/archived/alternative-grc-20-spec.md deleted file mode 100644 index 6c9e4887..00000000 --- a/docs/legacy-files/archived/alternative-grc-20-spec.md +++ /dev/null @@ -1,207 +0,0 @@ -# Goals for the specification - -- The underlying data structure should be a triple store -- Data must be stored in a way that is easy to read and write -- Linkability between data must be possible -- Data can have metadata like a format or unit -- No nesting of data. Nested data must always be normalized and referenced by an ID -- Data can be versioned -- Structures/Types should support usability - -## Introduction - -We introduce a handful of concepts to achieve these goals. - -- ID -- Attribute Type -- Attribute -- Entity -- Entity Type -- Relation -- Metadata - -## Concepts - -### ID - -Attributes, entities, entity types and relations are all identified by a globally unique ID. - -All IDs must be globally unique and 32 bytes. They're created using the UUID4 standard and stripping out the dashes (bringing the length from 36 bytes to 32 bytes). If an entity is coming from a system that already has globally unique IDs of arbitrary length, they can be deterministically converted into valid globally unique 32 byte IDs by taking an sha2 hash of the string, seeding that into a UUID4 generator and stripping the dashes. - -Example ID: 8108bb24a07449dc8553620d882e5636 - -### Attribute Type - -A value is a single piece of data and be of the following types that are hardcoded in the specification. It can be: - -- text (UTF-8 encoded) -- number (64-bit floating point) -- boolean (true or false) -- point (tuple of two numbers) -- time (ISO 8601 including duration support) - -All values are nullable. There is no distinction between a value that is not present and a value that is null. In fact if a value is set to null it should be removed. - -Note: We want to avoid nested structures so that we can reference each entity. - -### Attributes - -It consists out of an id, name and value. - -- id (generated from the hash of the name and value -> uuid4(sha2(hash({ name: name, value: value }))) -- name (string) -- value type (string, number, boolean, point, time) - -It's very important that the id is generated from the name and value. This way we can ensure that the same attribute is always represented by the same id! Indexer and clients must validate the ID. This makes attribute IDs deterministic and unique. This also makes attributes immutable. - -Note: It's important to not just concatenate the name and value, but rather canonicalize both in a structure and then hash the structure to avoid collision attacks of the value type space ever gets expanded e.g. - -- "descriptionb" + "string" -- "description" + "bstring" - -must result in a different hash. - -### Entity Type - -An entity type has: - -- id (is a hash of the name and the attribute Ids and since these are the hashes of the name and value) -- name (string) -- a collection of attributes to define the schema (to represent it in a triple DB this could be EntityType and EntityTypeAttribute) - -An entity type is a collection of attributes that are common to all entities of that type. It's a way to define a schema for entities. - -- id (of the entity type) -- scoped name - for UX/DX we can give a different name e.g. when you have document you might want to call it title while it actually maps to `name (string)`. Of course it also can simply be set to the name of the attribute. -- attribute id - reference to an attribute - -Since the ID is deterministic on the structure itself we automatically can find identical structures that have the same name. - -#### Further explanation - -Why can't we just rely on the structure? Because if you have a Person with name and age and an Animal with name and age they are not the same. They both might be living beings, but an Animal is not a Person and vice versa. So we need to have a way to distinguish between them. The name + structure probably covers all cases, but maybe I'm missing something. - -The benefit is that we can match identical structures with the same name. Downside is that evolution of the structure is not possible. In case you want to evolve the structure you need to create a new entity type. In a distributed system it's not realistic to migrate all the data, but personally I think it's better to have a deterministic and immutable type system. - -That said we can handle evolution on the developer tooling side. For example: - -Let's say you have a `Event` entity with: - -- name -- description - which results in the ID `abc`. - -Now you want to add a `date` attribute. You can create a new entity type `Event` with: - -- name -- description -- date - which results in the ID `def`. - -Your tooling should allow to you to map the old `Event` to the new `Event` entity type together e.g. - -``` -{ - "Event: ["abc", "def"], // entity type Ids -} -``` - -and you can still query `useQuery({ types: "Event" })`. - -This way you can evolve the schema without having to migrate the data, but still have a deterministic and immutable type system. One benefit for example is that if you changed an attribute `age` from `string` to `number` the resulting type can be `string | number`. - -### Entity - -An entity is a set of attributes (and entity types) that define the entity. - -- id (string) - must be the globally unique string for the entity -- attribute id - reference to an attribute -- value - actual value based on the attribute type - -When a new triple is created with the same id and attribute id it will overwrite the old value. - -In addition to attributes an entity can have a reference to an entity type. - -- id (string) - must be the globally unique string for the entity -- "type" -- entity type id - -Since an entity can have multiple types it's allowed to have multiple type triples. - -#### Recommended Attributes for all entities - -- `name` (string) - standard human-readable identifier -- `description` (string) - human-readable description -- `cover` (string) - base64 encoded image - -### Relation Type - -An entity type has: - -- id (is a hash of the name and the attribute Ids and since these are the hashes of the name and value) -- name (string) -- a collection of attributes to define the schema (to represent it in a triple DB this could be RelationType and RelationTypeAttribute, but might be a good use of collection in graph databases?) - -There are three attributes that are required for a relation type: - -- `from` (string of 32 bytes and they map to an entity type) -- `to` (string of 32 bytes and they map to an entity type) -- `cardinality` (string) - one-to-one, one-to-many, many-to-one, many-to-many // based on the definition a relation the indexer will resolve multiple relations differently e.g. in case of a one-to-one it will use the latest relation as the current one - -Evolving follows the same rules as for entity types. I'm not sure the evolution strategy of entity types is applicable to relation types e.g. changing the cardinality from many-to-many to one-to-one. This might need some exploration. - -### Relation - -Relation is a special entity that links two entities together. - -#### Attributes - -It has four special attributes: - -- `from` (string of 32 bytes) -- `to` (string of 32 bytes) -- `index` (string) - -The `from` and `to` attributes are references to entities and are required. The `index` attribute is optional and can have a string the allows to determine the position using a lexicographical order. - -In addition a relation can have any number of attributes. - -#### Relation Type Reference - -In addition to attributes a a relation can have a reference to a relation type. - -Triple for relation type: - -- id (string) - must be the globally unique string for the relation -- "relation-type" -- relation type id - -Since an relation can have multiple relation types it's allowed to have multiple triple with the same id. - -### Metadata - -Metadata are attributes that can be attached to attributes for a specific entity or entity type. They can be used to store additional information like the format or unit of a value. It behaves like an attribute to an entity. - -- reference entity id + attribute id (string) - the attribute id that the metadata is attached to -- attribute id - the actual attribute id -- value - actual value based on the attribute type - -In terms of DX you probably can define in the query if you want to include the metadata or not and then the structure will be more nested. - -## Summary of the most notable changes - -- The attribute types are not referenced by ID, but rather hardcoded by the specification. This is to avoid the need to have a separate entity for each attribute types. I might be missing something, but I don't see any value in having Ids for attribute types. - -- The IDs for attributes, entity types and relation types are generated from their content. This is to ensure determinism and immutability to make it easier to build an interoperable system. - -- The type definitions are separated from attributes using a different triple structure. - -- Relations are not defined on the entity type, but rather independently. - -## Opinions - -I believe the ID generations for attributes is a good idea. The ID generation for entity types and relation types is more controversial and I would rather explore the idea a bit before setting it in stone. - -I believe having a clear distinction between attributes and types is a good idea. One is to define actual data and the other is to define the schema. - -I believe the disconnecting relation types as attributes is a good idea to avoid confusing or inconsistent definitions. diff --git a/docs/legacy-files/archived/data-structure-and-sync.md b/docs/legacy-files/archived/data-structure-and-sync.md deleted file mode 100644 index 8e3adb76..00000000 --- a/docs/legacy-files/archived/data-structure-and-sync.md +++ /dev/null @@ -1,117 +0,0 @@ -# Data structure & Sync - -## One CRDT Document per Space - -- Can be fully decentralized -- No schema solution (especially no evolving schema existing yet except for research) -- In case there are lots of change the CRDT document might become very large even with compaction (already supported by Yjs) -- Separation between read/write is not built in an would be exploratory to build on top -- Easiest to ship it early (using Secsync) - -## Sync multiple CRDT document - -- Can be fully decentralized -- No schema solution (especially no evolving schema existing yet except for research) - tricky since you are always document based -- Separation between read/write is not built in an would be exploratory to build on top -- Needs expanding Secsync or a new solution which probably would be Beehive & Beelay (timeline is a concern here?) - -## Livestore with one DB per Space - -- Requires a central sync server to ensure the order of the events -- Strong and established schema -- It's a lot of exploration -- Can connect to multiple Event histories (but can't have dependencies between them). Think of isolated data in one database. -- You can have multiple LiveStore instances in one app: Real use-case is a dashboard over multiple organizations (each being their own DB). -- Audit trail -- Allows for more custom conflict resolution -- Works well for AI (tool calling) - sqlite-vec -- Syncing timelines is a concern here - -Rules to apply to avoid rebase-conflicts: - -- On conflict resolution change you need to reapply the whole history with the new rules -- Only soft-deletes are allowed (content can be deleted though) -- Foreign-keys must be nullable to avoid conflicts when rebasing - -## Data per Space - -Each space has multiple `Nodes`. Each `Node` can either be an `Entity` or a `Triple` in indicating a relationship between two `Entities`. - -### Proposal - -Each `Node` is a small CRDT document by itself and the content of the node is derived from the Yjs document. - -This would allow to setup an event log per space. There can be 3 kinds of operations: - -- set -- delete - -### Operations - -#### Set - -```json -{ - "spaceId": "abc", - "nodeId": "xyz", - "type": "set", - "content": "base64 encoded yjs update" -} -``` - -#### Delete - -```json -{ - "spaceId": "abc", - "nodeId": "xyz", - "type": "delete" -} -``` - -### Rules - -- `delete` always wins over `set` -- `set` updates must contain a commutative operation (CRDT update) - -Therefor no conflicts can occur. - -### Syncing - -With a single sync server we could rely on an ordered event log per space managed by the server. - -To make sure we can sync even when switching sync server we should create a structure that allows to sync in a decentralized fashion. Merkel-search-trees seem to be a good fit for this. - -#### Merkel-search-trees - -- https://inria.hal.science/hal-02303490/document -- https://github.com/domodwyer/merkle-search-tree - -#### End-to-End Encryption - -The actual content should be encrypted. Therefor we want a structure like this: - -```json -{ - "spaceId": "abc", - "nodeId": "xyz", - "ciphertext": "base64", // contains the type and content in case it's a `set` - "nonce": "base64", - "commitment": "base64", // needed? - "signature": "base64" -} -``` - -For the Merke-search-tree we probably would use the hash of the whole structure above. - -### Downsides - -- Currently doesn't support any kind of access control inside a space -- Doesn't allow to sync only a part of a space - -### Open Questions - -- What if an Entity is deleted? Should we delete all Triples that reference it? When does this happen? -- Do we need some form of access control inside a space? read/write access? -- What algorithm to use for key agreement? DCGKA? -- How to manage removing members and changes happened by them after their removal? diff --git a/docs/legacy-files/archived/identity.md b/docs/legacy-files/archived/identity.md deleted file mode 100644 index 6e62c71e..00000000 --- a/docs/legacy-files/archived/identity.md +++ /dev/null @@ -1,96 +0,0 @@ -# Identity - -## Goals - -- Allow all sorts of wallet providers or wallets integrations -- Avoid passwords for encryptions keys - -## Proposal A: Wallet to sign locally stored keys - -### Sign up - -- [UI] Click sign up -- During signup a key pair is generated that is stored locally -- [UI] Sign with your wallet -- The key pair is signed by the wallet and uploaded to the relay server -- [UI] Modal shown with recovery phrase -- The key pair is encrypted with a backup key (recovery phrase) and stored in the relay server - -### Sign in with another browser/device - -- [UI] Click sign in -- A new key pair is generated and stored locally -- [UI] Sign with your wallet -- The key pair needs to be signed by the wallet and uploaded to the relay server - -### Account recovery - -- [UI] Click recover account -- The user can enter the recovery key -- [UI] Enter recovery phrase in a form -- The key pair is fetched from the server, decrypted and stored locally - -### Device management - -- [UI] See a list of devices -- From the server you can fetch a list of devices that are signed by the wallet -- [UI] Click on revoke device and sign with your wallet -- You can revoke a device by signing with the wallet that the device should be revoked - -### Remarks - -If we go down that route we probably still want some form of user keypair that can be unlocked with these device keys. The reason here is that every browser sign in would be a new device and this would mean you need if I would want to send you the key to a workspace I would need to encrypt it possibly for hundreds of devices instead of one. In addition it's quite privacy concern because you basically can track logins from other users. -Key rotation of the user-key pair is relatively easy since this can be done with signing with the wallet. - -One open question for me is if the current user public key should be stored on chain or if it's fine to trust the server to be honest. Due the wallet the server can never inject their own public key, but it could withhold the information of a removed device. Maybe that's up an option and up to the developer using the SDK? - -This is the most wallet oriented approach, but feels unnecessary complex in the Web3 context. For non-web3 context this makes a lot of sense and is something I thinking a lot about here: https://github.com/serenity-kit/identity - -## Proposal B: Wallet and additionally Passkey - -### Sign up - -- [UI] Click the sign up button and need to log in with a passkey -- A passkey keypair is created -- [UI] Sign with your wallet -- The public key of your passkey is signed with your wallet (this actually could be completely optional and is only relevant in the Web3 context). - -### Sign in with another browser/device - -- [UI] Click sign in and use your passkey - -### Remarks - -I think the easiest would be to require a passkey that is synced by the user and a user can have only one active passkey per application. Otherwise you run into the same complexity with device and user keys as in proposal A. - -Same as in Proposal A we need to decide if the public key of the passkey should be stored on chain or if it's fine to trust the server to be honest. Or could be up to the developer using the SDK. - -Not sure how solid it is, but the Passkey part has been built before: - -- https://github.com/mylofi/webauthn-local-client -- https://github.com/mylofi/local-data-lock (not sure if it's 100% secure, but the future PRF extension of Passkeys will be - we could use this method temporarily) - -## Proposal C: Metamask Snaps - -While not feasible at the moment, Metamask Snaps could be a great way to handle identity. The user would use the Wallet to sign in an be done. - -### Sign up - -- [UI] Click sign up -- Key pair is generated -- [UI] Sign with your wallet -- The keypair is stored encrypted in the Wallet - -### Sign in with another browser/device - -- [UI] Click sign in -- [UI] Sign with your wallet -- The keypair is decrypted in the Wallet - -### Remarks - -This is the most user friendly approach, but would require lock-in to Metamask. - -Optionally a recovery key could be created, but probably better to leave it up to the user to manage the wallet properly. Depending on how Snaps work the encrypted key might have to be backed up by the user. - -Resource: https://docs.metamask.io/snaps/features/data-storage/ diff --git a/docs/legacy-files/archived/invitations.md b/docs/legacy-files/archived/invitations.md deleted file mode 100644 index c8e0f1d2..00000000 --- a/docs/legacy-files/archived/invitations.md +++ /dev/null @@ -1,17 +0,0 @@ -# Invitations - -2 different routes: - -## Flow 1: Adding someone by there public profile - -- Search for the person -- The profile (name - meant to be real name ENS for username) will have accounts (wallet - could be on a different chain) associated with it -- Send them invite (syncserver url, space id, lockbox for the spacekey with encrypted for the public key of the account, account id which must match the public key, account public key) - -Note: You might set a minimum price to send you an invite to a space (celebrities). - -## Flow 2: URI-based invitation - -- Create an invite URI (contains a secret) -- Share the URI with the person (must contain a hash segment for the secret so the server can't read it) -- The person opens the URI and can add themselves to the space diff --git a/docs/legacy-files/archived/key-agreement-for-spaces.md b/docs/legacy-files/archived/key-agreement-for-spaces.md deleted file mode 100644 index 246e9bca..00000000 --- a/docs/legacy-files/archived/key-agreement-for-spaces.md +++ /dev/null @@ -1,38 +0,0 @@ -# Key Agreement for Spaces - -It's easy to create an initial key with a group of people and then share it. Once it leaked or if someone is removed from the group it's best practice to rotate the key. - -## Proposal A: MLS - -MLS is a relatively new standard that has been designed over years and is not used in dozens of projects (Cisco, Discord https://discord.com/blog/meet-dave-e2ee-for-audio-video). It's a protocol that is designed to be secure and scalable. It's a bit more complex than the other options but it's very secure and can even be post-quantum secure when picking the right algorithms. - -Downside is that it requires a system to order the events of when a new device/user got added to a group. Usually this is done by a server. We could do so as well. Alternatively we could implement this in a blockchain. - -One other downside is that there is no out of the Box JavaScript version: - -- OpenMLS builds to WASM, but has no JS bindings: - - https://github.com/openmls/openmls - - https://github.com/openmls/openmls/issues/487 -- AWS implementation has wasm support, but didn't see JS bindings: https://github.com/awslabs/mls-rs -- XMTP implementation: https://github.com/xmtp/libxmtp/tree/main/bindings_wasm/src - -Implementations: https://github.com/mlswg/mls-implementations/blob/main/implementation_list.md -Spec: https://datatracker.ietf.org/doc/rfc9420/ - -## Proposal B: Implement DCGKA - -DCGKA is fully decentralized key agreement algorithm: https://eprint.iacr.org/2020/1281. Currently multiple teams implementing it, but so far no one has every deployed it. - -It was developed by Martin Kleppmann, Matthew Weidner and others - -## Proposal C: Implement a custom solution - -This can be relatively simple: - -- Create a new key -- Encrypt the key for ever user public key that should have access -- Store all of them on the server - -This would require a central server again to trust telling everyone that a key rotation happened and ideally block syncs that happened with an old key. We also could use a blockchain to store the information that a new key rotation took place. That would be more secure since the server can't betray the users. - -This is what I did in Serenity since OpenMLS was far away from being standardized when I started. Instead of a blockchain I used a signature chain to ensure the integrity over time. diff --git a/docs/legacy-files/archived/lessons-learned.md b/docs/legacy-files/archived/lessons-learned.md deleted file mode 100644 index e17fb0fb..00000000 --- a/docs/legacy-files/archived/lessons-learned.md +++ /dev/null @@ -1,42 +0,0 @@ -# Lessons Learned - -## Tinybase/Yjs Integration - -- For Tinybase Yjs Persister you need to first to invoke `save` or `startAutoSave` before the first `load` or `startAutoLoad` call. Otherwise an error will be thrown since the whole Tinybase nested Map structure does not exist yet in the Yjs document. - - Actually the above suggestion leads to data loss. If filed a bug report: https://github.com/tinyplex/tinybase/issues/186 -- Do not modify the Yjs document directly in the same place since this will mess with the persister e.g. `const map = newYDoc.getMap("space"); map.set("id", spaceId);`. - -## XMTP - -- XMTP v3 will support MLS for group conversations. They yet have not managed to add Web support due and seem to struggle with it due WebAssembly limitations: https://docs.xmtp.org/groups/build-group-chat#web-support-for-group-chat -- XMTP manages to create a stable private key from the Wallet using the signature (when running the enable Identity part). See https://github.com/xmtp/xmtp-js/blob/003d770c63e17904650974c8d913c191d69b5040/packages/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L136-L140 - -## XMTP stable keys - -I could verify that signatures are stable (which surprised me). - -The background probably is this work: https://datatracker.ietf.org/doc/html/rfc6979 - -So what they are doing is generate a random keypair for encryption which they want to store on the server encrypted. - -To store it on the server they create anoterh random value which is shared with the server. And the wallet signature of this random value is the secret to encrypt and decrypt the encryption keypair. Since the server does not have the wallet private key it can't create the signature. - -In general this is not a good practice. Signature have not been designed to be used as input for an encryption key. - -## Process - -how the XMTP library works: - -Sign-up: - -- Generate a random `encryptionPrivateKey` for encryption (and derive a `encryptionPublicKey` from it) -- Sign the `encryptionPublicKey` with your Wallet and send the `encryptionPublicKey` and `encryptionPublicKeySignature` to XMTP. So anyone can verify that this `encryptionPublicKey` really belongs to your Wallet. -- Then the `encryptionPrivateKey` backup starts: - - Generate a random `prevKey`. Sign it with your wallet to get a `prevKeySignature`. Encrypt the `encryptionPrivateKey` with the `prevKeySignature` as key which results in `keyBackup`. This is the very unusual part in terms of cryptography. - - Send `keyBackup` and `prevKey` to the server. - -Sign in and restore keys: - -- Get the `keyBackup` and `prevKey` from the server -- create `prevKeySignature` from `prevKey` with your Wallet (Enable Identity) -- With `prevKeySignature` you can decrypt `keyBackup` and get out the `encryptionPrivateKey`. Tadaaa, you used your Wallet to get back your one private key to have an end-to-end encryption identity. diff --git a/docs/legacy-files/archived/open-framework-questions.md b/docs/legacy-files/archived/open-framework-questions.md deleted file mode 100644 index 3052237d..00000000 --- a/docs/legacy-files/archived/open-framework-questions.md +++ /dev/null @@ -1,92 +0,0 @@ -# Open Questions - -- What kind of query language do we want? - -## LiveStore discussion - -- How to organize spaces? In the current thinking each store is one space. Since you can load multiple stores this is nice, because you can sync them separately. Can livestore load multiple sqlite databases? -- Can we map Tables, Rows and Cells to a Yjs sync structure? -- Investigate undo/redo of Tinybase - -### Benefits of LiveStore - -- reactive queries -- more powerful datatypes than Tinybase (blob which would be great for Yjs) -- high performances data access - -### How to sync events - -#### ActualBudget / Evolu model - -- Hybrid Logical Clock timestamps -- Last write wins model for conflicts -- No support for rich-text (in this case last write wins needs/can be replaced with Yjs merge) -- You only can have set/deletions of attributes (no or only nullable foreign keys) -- No transactions - -#### Sorting the events by logic - -- The idea would be to resort the events to a valid chain in a deterministic way. No idea how to approach it or if there are unsolvable cases. Gut feeling: maybe with certain conditions, but not if transactions are allowed. - -An example where it would be interesting to see how you'd solve it: What happens if I have two series of mutations that are conflicting? - -Participants: - -- Client A -- Client B -- Server - -The server has two events with foreign keys: - -- Event X references person P1 -- Event Y references person P2 - -Client 1 makes the following offline mutations: - -1. Set the foreign key of Event X to person P2 -2. Delete person P1 - -Client 2 makes the following offline mutations: - -1. Set the foreign key of Event Y to person P1 -2. Delete person P2 - -Then: - -- Client 1 syncs with the server (this works) -- Client 2 syncs with the server (but can't apply the change because P1 no longer exists) - -Ideally, the changes should be applied in the following order: - -1. Set the foreign key of Event X to P2 -2. Set the foreign key of Event Y to P1 -3. Delete person P1 -4. Delete person P2 - -And this might still be manageable, but the question is how complex these cases can become and whether there are circular cases that are unsolvable. Should this perhaps be formalized in some way? - -The solution here are "soft deletes". What's important in terms of privacy is thought that the data values of a soft delete are actually removed. In and end-to-end encrypted environment this need to be combined with key-rotation and compaction to work properly. - -## Tinybase - -- How to integrate rich-text (Yjs types) editing with Tinybase? - -Something like this would be great: - -```js -field: { - name: "description", - type: schema.Uint8Array, - onMutation: (currentField, mutation) => { - // currentField ist ein yDoc (uint8array) - // mutation.value ist ein Yjs update (uint8array) - yjs.applyV2Changes(currentField, mutation.value) - } -} -``` - -- Altering a schema can lead to data loss. This is super scary! https://tinybase.org/guides/schemas/using-schemas/ - -## How to send a wallet an invite? - -- XMTP? https://docs.xmtp.org/ diff --git a/docs/legacy-files/archived/recap-and-future-strategy.md b/docs/legacy-files/archived/recap-and-future-strategy.md deleted file mode 100644 index 9d32f82b..00000000 --- a/docs/legacy-files/archived/recap-and-future-strategy.md +++ /dev/null @@ -1,274 +0,0 @@ -## Introduction - -The goal of this document is to structure my thoughts and opinions in a concise way so we can make informed decisions on the next steps. - -## Recap of the last couple weeks - -1. Authentication - we investigated how to establish encryption keys with Wallets. XMTP uses a technique where they use the signature of a nonce value stored on the server to recover a stable encryption key for message encryption. We can use XMTP directly or use the same technique to establish encryption keys for end-to-end encrypted data sync. -2. Demo showing end-to-end encrypted data sync on top of Yjs & Automerge that is leveraging a defined Schema - we have a working prototype that shows how to sync data between two clients using Yjs and Automerge. The data is end-to-end encrypted and the schema is defined in a way that it's easy to read and write. We experimented with LiveStore and Tinybase which we believe are both not good fits. Both could be used as stores, but sync would require a rebase technique with manual conflict resolution or a lax schema and again using a CRDT engine on top to sync the data. -3. Schema design and implementation including relations - based on the GRC-20 spec we created a library on top of Automerge that allows to define a schema with multiple types per entity as well as relations. -4. GRC-20 spec feedback and alternative design - several discussion in the chat, call and the alternative spec in the docs folder - -## Current state of the art - -In order to make an informed decision I want to summarize the current state of the art in local-first development and give a brief overview of the tools and frameworks that are available. - -For we need to talk about the different key factors that differs from existing Web2 apps: - -- Local-first -- Decentralized/Centralized -- End-to-end encrypted - -Building local-first apps that are centralized and not end-to-end encrypted is possible today. There are companies like Electric or Zero how are building frameworks. The UX is/will be great. The main downside is that you need to trust a provider for sync and securing your data. Migrations are are much harder in this setting compared to DB as single-source-of-truth Web2 apps. - -Building local-first apps that are centralized and end-to-end encrypted is possible today. Afaik Jazz is doing it and the app Serenity that I build was in that direction. The main downside is that you need to trust a single service with the sync. If the service allows an export you can move to another service. Due end-to-end encryption the service can't read your data which limits a lot of features: email notifications, server based search, etc. Migrations are even trickier. - -Building local-first apps that are decentralized and end-to-end encrypted is very hard. Ink&Switch is doing it with Beehive. Evolu has announced to release a new sync soon that works fully decentralized. NextGraph is also working on it. All of them are not ready or have quite some limitations compared to existing UX. A very tough problem is what to do with data that was synced from another client, you built upon it and later on your client realizes that the other client was already removed. Do you keep the data? Do you indicate it as deleted? Do you remove it? - -## Main areas to consider when building a local-first app - -- Sync -- Key management (Key rotation when members are leaving the group is the hard part) -- Invitation -- Permissions (affects Key Management) -- Identity - -## Sync - -For unencrypted data in a document structure there are established solutions. - -When it comes to encrypted data there is Secsync relying on a central server. Jazz afaik as and both rely on finding the missing changes. Other solutions try to be fully decentralized e.g. Nextgraph. The upcoming Automerge sync also wants to go in this direction. - -The hard part is not only syncing one entities/documents, but to identify which ones you need to update. Personally I'm wondering about the data storage overhead. Further problems that need to be considered are: - -- prioritized sync (loading one particular document first) -- progress indication -- handling errors (can you abort and retry if state is lost) -- compaction for of history to heavily reduce the data that needs to be synced -- shallow document/entity loading to retract history - -When considering the GRC-20 spec we need to think how to sync data back from a public graph. Ideally the indexer have similar capabilities as the native CRDT sync engine. We can do naive approaches, but long term we probably want a solid approach and this is not trivial. Note: One idea is to accompany every change with a CRDT that is published publicly as well. - -### Key Management - -Managing encryption keys for a a group is quite tricky problem. In general it's easy to establish a key for a group, but rotating the key is the hard part. - -Example of a key rotation problem: - -- Who do you allow to rotate the keys? e.g. you have commenter that informs you that his devices was stolen. can they initiate a new key? probably not. can any member in a group do so or only the admins? -- How do you recover from failed key rotations? (ignore them and move on?) - -The easy way and what most do: just trust everyone with access in the group, but this model might now work well for all use-cases. - -### Permissions - -In general it's straight forward to do read/write permissions based on space roles. This could be made more granular per entity with a custom permission system, but probably out of scope. - -There were some attempts like UCAN to make permissions very granular, but I haven't seen it in action yet. - -### Invitation - -The most common way to handle this is invitation links (Jazz, DXOS, Serenity). There are a couple of fine details that need to be considered. The main issue is: you can't just invite someone via Email directly to a space without them having an encryption key that you know beforehand or you send a secret (which then can't go through a server). - -### Identity - -The easy answer would be to use a wallet and XMTP for a stable encryption key. Account recovery on top is not that crazy either, but when you try to come up with key rotation or a multi-device setup things become quite tricky. Even more so in case you want to allow decentralization. - -## Overview of Local-first frameworks and tools - -### Evolu - -https://evolu.com/ - -### Yjs - -https://github.com/yjs/yjs - -### Jazz - -https://jazz.io/ - -### Zero - -https://zerosync.dev/ - -### Electric - -https://electric-sql.com/ - -PGlite (Postgres compiled to WASM) on the client + a custom concept "Shapes" to define which data to sync locally. The data is not end-to-end encrypted. - -### Automerge - -- No ReactNative support atm -- Requires WASM - -### Beehive - -https://www.inkandswitch.com/beehive/notebook/ - -### NextGraph - -https://nextgraph.com/ - -### DXOS - -Works on top of Automerge - -Interesting vision -https://docs.dxos.org/guide/ - -### Livestore - -Strongly typed and doesn't use a CRDT. - -The sync system will probably be rebasing with manual conflict resolution. - -### Tinybase - -A database with a CRDT build in (MergableStores) that works for unencrypted data. - -Nice UX, wipes data locally if it doesn't match the schema. - -## Strategies for the future - -The first question we should answer is: what is the direction we want to go? - -> Do we want enable local-first development that integrates into the GRC-20 spec or do we want to build a great local-first framework and have a GRC-20 integration on top of it? - -Of course we probably want to have both, but in reality I believe the outcome looks very different. - -One we have clarity I see multiple possible scenarios: - -- Build something completely from ground up (CRDTs, Sync, Key Management, etc.) -- Leverage existing CRDTs and built on top of those (Yjs, Automerge - they are all document based) -- Leverage existing CRDTs and further Layers (e.g. Sync & Key Management via Beehive, NextGraph) -- Integrate into multiple existing tools (Zero, Electric, Jazz, Evolu, NextGraph) and provide the missing parts for a Web3 integration e.g. a Wallet auth plugin for Jazz - -## Evaluation of Web3 - -In general local-first can work completely without Web3. There are things that Web3 offers that can be beneficial, but by requiring it we probably limit possible adoption. I think it can be quite useful, but it requires a deeper analysis. e.g. I could imagine that a wallet could be encrypted by the user email/password and it's secure due the OPAQUE protocol, but for people who prefer the Wallet as primary auth method they can also use it. - -### Benefits of Web3 - -- Blockchains allow us to give us an immutable ordered log of events. This is a massive issue in distributed systems. -- IPFS protocol is great for storing data. -- ENS is great for naming and finding data. -- Wallets are great for managing keys. -- Smart contracts are great for managing access control. - -### Downsides of Web3 - -- Wallets are not established and developers and users often rather want alternatives (Passkeys, Passwords, Email-login) -- Retrieving data in IPFS can be slower compared to S3 depending on the setup -- IPFS has no access-control built in - can be built on top of it, but it's not there by default -- Smart contracts are require special expertise - -## Other considerations not mentioned - -One thing almost nobody has explored is having only only a subset of the data end-to-end encrypted. For example when building a patient-information system the emails and appointments could be not end-to-end encrypted, but the medical records and chats between doctor and patient are. - -This has some benefits, but personally I haven't explored the direction. - -## Personal conclusion - -What to do heavily depends on the direction we want to go. Personally I'm find triple stores and the GRC-20 spec very fascinating, but my gut-feeling tells me that it's not helpful to establish a local-first framework. - -If the goal is to establish GRC-20, then building an end-to-end encrypted local-first framework is a good way to support it. - -Based on this we can probably can sit together and evaluate a path forward. Personally I would take a lot of short-cuts to get as fast as possible to a state where internal/external devs can build things with it e.g. once per month a 1-2 day Hackathon using it and giving feedback. - -I tried to think through how I would go about these two different directions: - -## Local-first focused framework - -### Schema and CRDT - -In general I like schemas and therefor I would define a schema that defines a clear structure that looks like a SQL-DB with certain limitations. It probably would also allow nested structures like any other NoSQL store or SQL-Store with JSON support. In addition you can be very flexible in terms of types (as long as Automerge supports it). - -For rich-text editing you would probably use the automerge-prosemirror integration. - -While Automerge is document based I would try to built this as a layer on top of Automerge (we already have this). What we should add is the ability to define in the schema what stays in the main document and what data is stored in a sub-document. When writing and querying data this mostly should be abstracted. The overhead for sub-documents should be minimal, but what's currently missing in SecSync and only limited in the existing Automerge sync is the ability to do prioritized sync. This is important for large data sets. Automerge is working on it. - -Why Automerge and not Yjs? Yjs is the best for real-time collaboration at the moment and I'm a big fan, but Automerge is actively working towards local-first supporting full decentralization and end-to-end encryption. They have more resources and a clear vision. So in terms of long-term plans Automerge seems to be the better choice. - -In terms of migrations I have an idea: Schema-changes are defined via an API and this one requires you to implement migration functions that fit the types. - -### Sync - -SecSync does the job for now, but long-term we probably just want to use the sync Automerge is working on. - -### Key Management - -Ink&Switch is working on Beehive, but it's not ready yet and there are a lot of unknowns in terms of UX. - -This is where blockchains could really shine. We could use the chain as a way to store active members of a group and store the encrypted private/public keys on the chain. - -We could start with something simple that relies on a server managing the latest state. We can experiment and verify what works for users and what doesn't. In parallel we can develop the blockchain part or we just delay it and move to the blockchain later once we are sure due smart contracts being expensive to develop. - -Alternatively we might just like Beehive and be happy with it. - -### Identity - -This highly depends on key management. If we use a key management relying not relying on a blockchain we could make wallets completely optional. This would open it up to a larger audience that doesn't want to integrate wallets or use Privy. - -If we rely on blockchains the embedded wallets from Privy look promising, but I haven't looked into the details yet. I worked on and with the OPAQUE protocol which would allow to build embedded wallets so people don't have to use Privy. - -If we require wallets we could even abstract them away and the build a auth layer where developers and users don't have to know about wallets. - -### Invitation - -Existing systems like Keybase require the users, but there is a way to get to a good experience to have link invitations. -https://github.com/serenity-kit/invitation/ - -XMTP could be used to directly add users or at least send them an invite. Not sure how active it's used in the eco-system. Personally I would stick to link-invitations and explore adding users directly later on. - -### Permissions - -Beehive afaik is only focusing on read/write permissions at the time being. We could focus on that and give developers the options to add more where needed. - -### GRC-20 integration - -This probably would be a separate library that allows you to specify where your schema maps to GRC-20 attributes, types and so on. - -In addition it feels like a very natural extension to the framework to allow you to pull in data from public graphs, showing the different states and diffs from local data compared to public data. Probably we want to store the last known public data that matches the entries in the local data. Ideally the indexers offer a event log based API to allow syncing updates in without aggressively querying the indexer or downloading the entire space. - -We can give guides for people who want to integrate into the GRC-20 spec. - -## GRC-20 focused framework - -### Schema and CRDT - -The schema probably is where we see the difference to the other approach. It allows entities to have multiple types and is limited to the GRC-20 base types and relations. The beautiful aspect of this is that we can much clearly communicate the framework focus in terms of that it provides you amazing tooling for building GRC-20 based apps. The downside is that the group of developers interested in local-first, but not GRC-20 will probably focus on other tools. I believe for example that a lot local-first apps probably never will have public data and the whole story with public spaces and publishing data makes little sense to their use-case. - -In order to use rich-text editing it's probably recommended to use our editor with GRC-20 blocks. - -The flexible design of the GRC-20 requires a different architecture on how to store data in one or multiple Automerge documents, which is less performant. With the other design I can store entities in maps per type. Here I need to store them in one entities map. We would need to explore at what amount of entities this starts to become an issue and what kind of caching/indexing we can do to counter the issues. - -Migrations could be implemented similar to what I described above. - -### Sync - -This I see identical to the other version. - -### Key Management - -This I see identical to the other version. - -### Identity - -Here integrating with wallets probably makes the most sense. You anyway need a wallet to make any kind of change on a public space. - -### Invitation - -This I see identical to the other version. - -### Permissions - -I assume there is an existing permission model defined (which I'm not aware of) of who is a member with certain permissions for a space and so on. By default we probably want this exact model. This might influence key management as well. - -### GRC-20 integration - -Since data is shaped on the same spec syncing from and to the public space is a lot easier and can be a lot more automated. - -The main thing I'm still missing conceptional is what kind of indexers can offer to allow syncing updates in without aggressively querying the indexer or downloading the entire space. diff --git a/docs/legacy-files/archived/schema-graph-based.md b/docs/legacy-files/archived/schema-graph-based.md deleted file mode 100644 index df6b9c31..00000000 --- a/docs/legacy-files/archived/schema-graph-based.md +++ /dev/null @@ -1,331 +0,0 @@ -# Schema - -## Schema definition - -### Data types - -- `text` // string -- `number` -- `checkbox` // boolean -- `time` // date and/or time -- `uri` -- `collection` -- `relation` - -All fields can be optional, required or nullable. By default all fields are required. - -Special attributes: - -- `id` - always mandatory and must a string with 32bytes (hex) -- `name` - text -- `description` - text -- `cover` - uri - -A schema example (one to one mapping to Effect): - -```ts -const schema: Schema({ - entities: { - user: { - id: t.Text, // always mandatory to set and can only be a string - name: t.Text, - }, - artist: { - id: t.Text, // always mandatory to set and can only be a string - name: t.Text, - description: t.Text, - }, - event: { - id: t.Text, // always mandatory to set and can only be a string - title: t.Text, - description: t.Text, - attendees: t.Number, - active: t.Boolean, - }, - }, - // TODO how to generate or find the attributeId by name - relationAttributes: { - Host: "okd5f77200db45c6ae96193aaef81fe3", - }, - entityIds: { - event: "1085f77200db45c6ae96193aaef81fe3", - user: "abc5f77200db45c6ae96193aaef81fe3", - } -}); -``` - -Relation example: - -```ts -const schema: Schema = { - entities: { - user: { - id: t.Text, - name: t.Text, - }, - event: { - id: t.Text, - hosts: t.Relation({ - type: "user", - cardinality: "many", // one or many - attributeId: "1085f77200db45c6ae96193aaef81fe3", - }), - }, - }, -}; -``` - -## Space Provider - -```tsx -function SpaceDetail() { - const { spaceId } = useLocationParams(); - return ( - - - - ); -} - -function Events() { - const events = useQuery({ type: "event" }); - return ( -
- {events.map((event) => ( - - ))} -
- ); -} -``` - -## Queries - -- `useQuery` to get multiple entities -- `useGetById` to get a single entity by ID - -### useQuery - -The `entity` is always mandatory. - -Optional parameters: - -- `where` to filter the entities -- `orderBy` to get a single entity by ID -- `include` to include related entities - -```ts -// all events in the local db -const events = useQuery({ type: "event" }); -// all events with attendees greater than 0 -const events = useQuery({ - type: "event", - where: { attendees: { ">": 0 } }, -}); -// all events that include the substring "foo" in the title -const events = useQuery({ - type: "event", - where: { title: { includes: "foo" } }, -}); -// all events that include the substring "foo" in the title and attendees greater than 0 -const events = useQuery({ - type: "event", - where: { title: { startsWith: "foo" }, attendees: { ">": 0 } }, -}); -// all events ordered alphabetically by title -const events = useQuery({ type: "event", orderBy: { title: "asc" } }); -// all events ordered by attendees in descending order -const events = useQuery({ type: "event", orderBy: { attendees: "desc" } }); -// all events ordered by attendees in descending order and include the hosts -const events = useQuery({ - type: "event", - orderBy: { attendees: "desc" }, - select: { - title: true, - }, -}); -// all events including the hosts and their friends -const events = useQuery({ - type: "event", - orderBy: { attendees: "desc" }, - select: { - title: true, - hosts: { - // NOTE we could allow a where clause or orderBy here as well, but probably not for the first version e.g. - where: { name: { includes: "foo" } }, - select: { friends: true }, - }, - }, -}); -``` - -### useQueryById - -The `entity` and `id` are always mandatory. - -Optional parameters: - -- `include` to include related entities - -```ts -// get a single event by id -const event = useGetById({ - type: "event", - id: "abc", - select: { hosts: true }, -}); -// get a single event by id and include the hosts -const event = useGetById({ - type: "event", - id: "abc", - select: { hosts: true }, -}); -``` - -## Mutations - -```ts -// helper function to create an ID -const id = generateId(); - -// id is optional for create, but be provided in case you want to reference it in the same operation -const create = useCreateMutation(); -const event = create({ - type: "event", - title: "My event", - description: "My description", - hosts: { - connect: ["abc", "def"], - create: [{ name: "Alice" }, { name: "Bob" }], - }, -}); -const results = create([ - { - type: "event", - name: "My event", - description: "My description", - }, - { - types: ["user", "artist"], - username: "Anna", - description: "My description", - }, -]); -``` - -```ts -const update = useUpdateMutation(); -update({ - types: ["event"], // I can add or remove types -}); -update([{}, {}]); // should it accept different types or only one type? - -const delete = useDeleteMutation(); -delete({ id: "abc" }); -delete([{ id: "abc" }, { id: "abc" }]); -``` - -## Migrations (early idea) - -Introduce a way to add a migration to a schema. This could be relative on how to extend a type and then there must be a function that does the migration from one version to the other and comply to the input/output of the schema. - -```tsx -const schemaV1: Schema({ - entities: { - user: { - id: t.Text, // always mandatory to set and can only be a string - name: t.Text, - }, - event: { - id: t.Text, // always mandatory to set and can only be a string - title: t.Text, - description: t.Text, - attendees: t.Number, - }, - }, -}); - -const { schema: schemaV2, migrate: migrateV2 } : Migration({ - original: schemaV1, - changes: { - entities: { - event: { - // add a new field - active: migration.add({ type: t.Boolean, default: false }), - // remove a field - description: migration.remove(), - // change a field - footer: migration.change({ type: t.Number, function: (title) => { return parseInt(title)} }), - }, - }, - }, -}); -``` - -## Open Questions - -- What about collections? -- Can an entity have only attributes defined in a type or also additional ones? - -## New version - -Types: - -```tsx -import * as S from "effect/Schema"; - -export const type = { - Text: S.String, - Number: S.Number, - Boolean: S.Boolean, -}; -``` - -Schema example: - -```tsx -export const schema: Schema = { - attributes: { - name: type.Text, - age: type.Number, - isActive: type.Boolean, - email: type.Text, - }, - types: { - Person: ["name", "age"], - User: ["name", "email"], - Event: ["name"], - }, -}; -``` - -Hooks: - -```tsx -const createEntity = useCreateEntity(); -createEntity(["Person", "User"], { - // create entity with type Person and User - name: "John", - age: 30, - email: "john@example.com", -}); -``` - -```tsx -// query all entities of type Person and User where name is John -const entities = useQuery({ - where: { - type: ["Person", "User"], // must be both Person and User - name: { - equals: "John", - }, - }, -}); - -// query all entities of type Person -const entities = useQuery({ - where: { - type: ["Person"], - }, -}); -``` diff --git a/docs/legacy-files/archived/schema-relational.md b/docs/legacy-files/archived/schema-relational.md deleted file mode 100644 index 06c8d511..00000000 --- a/docs/legacy-files/archived/schema-relational.md +++ /dev/null @@ -1,278 +0,0 @@ -# Schema - -## Schema definition - -### Data types - -- `string` -- `number` -- `boolean` -- `text` // rich text -- `object` (initially can NOT contain relationship or graphRelationship) -- `list` (initially can NOT contain relationship or graphRelationship) -- `relationship` -- `graphRelationship` - -All fields can be optional, required or nullable. By default all fields are required. - -A schema example (one to one mapping to Effect): - -```ts -const schema: Schema({ - entities: { - user: { - id: t.String, // always mandatory to set and can only be a string - name: t.String, - }, - event: { - id: t.String, // always mandatory to set and can only be a string - title: t.String, - description: t.Text, - attendees: t.Number, - active: t.Boolean, - }, - }, -}); -``` - -Nullable/Undefined/Optional example (one to one mapping to Effect): - -```ts -const schema: Schema = { - entities: { - event: { - id: t.String, - title: t.NullOr(t.String), - description: t.UndefinedOr(t.String), - notes: t.optionalElement(t.String), - }, - }, -}; -``` - -Object example: - -```ts -const schema: Schema = { - entities: { - event: { - id: t.String, - location: t.Object({ - name: t.String, - address: t.String, - }), - }, - }, -}; -``` - -List example: - -```ts -const schema: Schema = { - entities: { - event: { - id: t.String, - tags: t.List(t.String), - participants: t.List( - t.Object({ - name: t.String, - email: t.String, - phone: t.String, - }) - ), - }, - }, -}; -``` - -Space internal Relationship example: - -```ts -const schema: Schema = { - entities: { - event: { - id: t.String, - hosts: t.Relationship({ - type: "user", - field: "id", - relationship: "hosted by", - }), - }, - }, -}; -``` - -Public Graph Relation example: - -```ts -const schema: Schema = { - entities: { - event: { - id: t.String, - hosts: t.GraphRelationship({ - type: "user", - field: "id", - relationship: "hosted by", - }), - }, - }, -}; -``` - -## Space Provider - -```tsx -function SpaceDetail() { - const { spaceId } = useLocationParams(); - return ( - - - - ); -} - -function Events() { - const { events } = useQuery({ type: "event" }); - return ( -
- {events.map((event) => ( - - ))} -
- ); -} -``` - -## Queries - -- `useQuery` to get multiple entities -- `useQueryById` to get a single entity by ID - -### useQuery - -The `entity` is always mandatory. - -Optional parameters: - -- `where` to filter the entities -- `orderBy` to get a single entity by ID -- `include` to include related entities - -```ts -// all events in the local db -const events = useQuery({ type: "event" }); -// all events with attendees greater than 0 -const events = useQuery({ type: "event", where: { attendees: { gt: 0 } } }); -// all events that include the substring "foo" in the title -const events = useQuery({ - type: "event", - where: { title: { includes: "foo" } }, -}); -// all events that include the substring "foo" in the title and attendees greater than 0 -const events = useQuery({ - type: "event", - where: { title: { includes: "foo" }, attendees: { gt: 0 } }, -}); -// all events ordered alphabetically by title -const events = useQuery({ type: "event", orderBy: { title: "asc" } }); -// all events ordered by attendees in descending order -const events = useQuery({ type: "event", orderBy: { attendees: "desc" } }); -// all events ordered by attendees in descending order and include the hosts -const events = useQuery({ - type: "event", - orderBy: { attendees: "desc" }, - include: { hosts: true }, -}); -// all events including the hosts and their friends -const events = useQuery({ - type: "event", - orderBy: { attendees: "desc" }, - include: { - hosts: { - // NOTE we could allow a where clause or orderBy here as well, but probably not for the first version e.g. - // where: { name: { includes: "foo" } }, - include: { friends: true }, - }, - }, -}); -``` - -### useQueryById - -The `entity` and `id` are always mandatory. - -Optional parameters: - -- `include` to include related entities - -```ts -// get a single event by id -const event = useQueryById({ - type: "event", - id: "abc", - include: { hosts: true }, -}); -// get a single event by id and include the hosts -const event = useQueryById({ - type: "event", - id: "abc", - include: { hosts: true }, -}); -``` - -## Mutations - -```ts -const createEvent = useCreateMutation({ type: "event" }); -createEvent({ id: "abc", title: "My event", description: "My description" }); - -const updateEvent = useUpdateMutation({ type: "event" }); -updateEvent({ id: "abc", description: "My description" }); - -const deleteEvent = useDeleteMutation({ type: "event" }); -deleteEvent({ id: "abc" }); -``` - -## Migrations (early idea) - -Introduce a way to add a migration to a schema. This could be relative on how to extend a type and then there must be a function that does the migration from one version to the other and comply to the input/output of the schema. - -```tsx -const schemaV1: Schema({ - entities: { - user: { - id: t.String, // always mandatory to set and can only be a string - name: t.String, - }, - event: { - id: t.String, // always mandatory to set and can only be a string - title: t.String, - description: t.Text, - attendees: t.Number, - }, - }, -}); - -const { schema: schemaV2, migrateSpace: migrateSpaceV2 } : Migration({ - original: schemaV1, - changes: { - entities: { - event: { - // add a new field - active: migration.add({ type: t.Boolean, default: false }), - // remove a field - description: migration.remove(), - // change a field - title: migration.change({ type: t.Number, function: (title) => { return parseInt(title)} }), - }, - }, - }, -}); -``` - -## Open Questions - -- Not sure if and how to best distinguish internal and external relationships. I think we want to make a difference between them since the local ones can load in a sync manner, while the external ones need to be fetched async. -- In the schema definition we define `entities`, but in `userQuery` we use `type`. Should we align on one of them? -- I have barely worked with GraphDBs, so I'm not sure this is a good way to define relationships. This feels more like a relational DB (the intuitive API for me). diff --git a/docs/legacy-files/framework.md b/docs/legacy-files/framework.md deleted file mode 100644 index db570f34..00000000 --- a/docs/legacy-files/framework.md +++ /dev/null @@ -1,207 +0,0 @@ -## High level overview - -A space is a series of events. Separate from that we have the actual space content which can be a snapshot and updates. - -We map one Automerge document to one space. This simplifies the managing the data, but comes at the cost that a space with lot's of updates might get very big. In the future we can look into splitting the space into multiple documents. This could be defined as part of the schema or some other configuration. - -Updates are encrypted and signed by the author and then sent to the server. The server verifies the signature and then sends the update to all the members of the space. Once we use smart contracts the server will also send the update to the blockchain and return the verifiable confirmation to the client. - -## Interactions - -In order to create, manage and delete spaces we need the following space events: - -- createSpace -- deleteSpace -- createInvite -- acceptInvite -- removeInvite -- updateMember -- removeMember - -Separate from these we need to discover and sync the spaces the user is part of: - -- listSpaces -- getSpace -- sendUpdate -- sendCompactedUpdate - -Auth - -- createIdentity -- restoreIdentity - -I believe these actions should be independent from the transport layer, but initially would built them on top of Websocket. This way we can easily have real-time updates without a lot of retry logic. - -Some of the events have a lastKnownSpaceEventId. This is to ensure that the client has the latest state of the space. If the client doesn't have the latest state, the server should return an error or if the client is behind it should discard the event and retry syncing. - -### createSpace - -Params: - -- eventId: string -- spaceId: string -- ciphertext: string -- nonce: string -- memberSignaturePublicKey: string -- memberEncryptionPublicKey: string -- keyBox: { ciphertext: string, recipientPublicKey: string, authorPublicKey: string } -- signature of the combined data: string - -### deleteSpace - -Params: - -- eventId: string -- spaceId: string -- signature of the combined data: string - -### createInvite - -Params: - -- eventId: string -- spaceId: string -- invitationKeysCiphertext: string -- invitationKeysNonce: string -- invitationSignaturePublicKey: string -- invitationEncryptionPublicKey: string -- lastKnownSpaceEventId: string -- signature of the combined data: string - -### acceptInvite - -Params: - -- eventId: string -- spaceId: string -- memberSignaturePublicKey: string -- memberEncryptionPublicKey: string -- signature of the combined data using the invitationSignaturePublicKey: string -- signature of the combined data using the member public key: string - -### removeInvite - -This should trigger a key rotation for the group and therefor a new spaceKey is created which should be used for all further encryptions. - -Params: - -- eventId: string (automatically becomes the spaceKeyId) -- spaceId: string -- inviteId: string -- lastKnownSpaceEventId: string -- keyBox: { ciphertext: string, recipientPublicKey: string, authorPublicKey: string } -- signature of the combined data: string - -### updateMember - -Can only be done by space admins. - -Params: - -- eventId: string -- spaceId: string -- memberSignaturePublicKey: string -- role: "editor" | "admin" - -### removeMember - -Params: - -- eventId: string (automatically becomes the spaceKeyId) -- spaceId: string -- memberSignaturePublicKey: string -- lastKnownSpaceEventId: string -- keyBoxes: { ciphertext: string, recipientPublicKey: string, authorPublicKey: string }[] -- signature of the combined data: string - -### listSpaces - -No Params -Returns a list of space Ids and the remove event for the ones that have been removed. The client then can connect get each space separately. - -### getSpace - -When using a Websocket this is the first thing that should automatically happen on connect. - -This might be useful to have as a HTTP endpoint as well. - -### sendUpdate - -Params: - -- eventId: string -- ciphertext: string -- nonce: string -- spaceId: string -- spaceKeyId: string -- signature of the combined data: string - -### sendCompactedUpdate - -SecSync has the capability to create "Snapshots" of a space. I believe we should go with a simpler approach and allow clients to compact these events for faster loading of a space. Can be a simple algorithm that ever 200 events a client creates a snapshot for all the past events. In the future we can even optimize this by splitting them into smaller chunks so clients don't have to download all events or the snapshot and can get smaller chunks of the data - -Params: - -- eventId: string -- snapshotUntilEventId: string -- ciphertext: string -- nonce: string -- spaceId: string -- spaceKeyId: string -- signature of the combined data: string - -## Authentication and Authorization - -Authentication is handled through Privy cookies. Authorization for the actions is handled by verifying the signature of the event. The server should only accept events that are signed by the user that is part of the space. - -## API - -### Authentication and Authorization - -```tsx -const { authenticated } = usePrivy(); -``` - -```tsx -import { - createSpace, - acceptInvite, - createSpace, - listSpaces, -} from "@graphprotocol/hypergraph"; - -export const { - SpaceProvider, - // schema - useCreateEntity, - useDeleteEntity, - useSpaceId, - createDocumentId, - useQuery, - // space utils - createInvite, - removeInvite, - updateMember, - removeMember, - deleteSpace, - listSpaces, - getSpace, -} = createFunctions(schema, { - endpoint: "http://localhost:3000/sync", -}); - -// automatically connects to the server -; - -listSpaces({ account }); -getSpace({ account, spaceId }); -acceptInvite({ account, spaceId, inviteKey }); -``` - -## Questions - -- Do we want to use a blockchain to manage space/invite/member events? -- Can we ensure a linear order of events using an L2 chain or can multiple events end up in a block? If so should we then sort them or can we argue only one is valid? -- When submitting an event to the chain. How long do we usually need to wait on an L2 chain before there is enough certainty that the event is part of the chain? -- Can we aggregate data on the server, send it down and verify that it's correct? Or would we need all the events and how can we verify that the server didn't leave out some? -- Do we want to leverage IPFS for storing data? diff --git a/docs/legacy-files/namespaces/utils.md b/docs/legacy-files/namespaces/utils.md deleted file mode 100644 index bbfa647f..00000000 --- a/docs/legacy-files/namespaces/utils.md +++ /dev/null @@ -1,66 +0,0 @@ -# Utils - -Provides common utilities for the Graph Framework. - -_All utilities must be runnable on: Browser, NodeJS server, ReactNative._ - -## API - -- `generateId()` - generates a base58 encoded ID from a generated v4 UUID. - -```ts -import { generateId } from "@graph-framework/utils"; - -const id = generateId(); -console.log(id); // Gw9uTVTnJdhtczyuzBkL3X -``` - -### Base58 utils - -- `encodeBase58` - encodes a given string (like the hyphen-stripped UUID) to base 58 - -```ts -import { v4 } from "uuid"; -import { encodeBase58 } from "@graph-framework/utils"; - -const uuid = v4(); // 92539817-7989-4083-ab80-e9c2b2b66669 -const stripped = uuid.replaceAll(/-/g, ""); // 9253981779894083ab80e9c2b2b66669 -const encoded = encodeBase58(dashesRemoved); -console.log(encoded); // K51CbDqxW35osbjPo5ZF77 -``` - -- `decodeBase58ToUUID` - decodes the given base58 encoded UUID back to its original UUID value - -```ts -import { v4 } from "uuid"; -import { decodeBase58ToUUID, encodeBase58 } from "@graph-framework/utils"; - -const uuid = v4(); // 92539817-7989-4083-ab80-e9c2b2b66669 -const stripped = uuid.replaceAll(/-/g, ""); // 9253981779894083ab80e9c2b2b66669 -const encoded = encodeBase58(dashesRemoved); // K51CbDqxW35osbjPo5ZF77 -const decoded = decodeBase58ToUUID(encoded); - -expect(encoded).toHaveLength(22); -expect(decoded).toEqual(uuid); -``` - -### JSC utils - -- `canonicalize` - JSON canonicalize function. Creates crypto safe predictable canocalization of JSON as defined by RFC8785. - -```ts -import { canonicalize } from '@graph-framework/utils' - -console.log(canonicalize(null)) // 'null' -console.log(canonicalize(1)) // '1' -console.log(canonicalize("test")) // "string" -console.log(canonicalize(true)) // 'true' -const json = { - from_account: '543 232 625-3', - to_account: '321 567 636-4', - amount: 500, - currency: 'USD', -}; -console.log(canonicalize(json)) // '{"amount":500,"currency":"USD","from_account":"543 232 625-3","to_account":"321 567 636-4"}' -console.log(canonicalize([1, 'text', null, true, false])) // '[1,"text",null,true,false]' -``` \ No newline at end of file diff --git a/docs/legacy-files/public-graph-integration.md b/docs/legacy-files/public-graph-integration.md deleted file mode 100644 index dbffd647..00000000 --- a/docs/legacy-files/public-graph-integration.md +++ /dev/null @@ -1,59 +0,0 @@ -# Public Graph Integration - -## API flavours - -- Query only private data -- Query only public data -- Query both combined where based on options you can choose how they are combined e.g. - - private data first (when you editing something at the moment) - - latest update - -## Public Graph API - -Ideally the public graph API should match the local one. On the other hand it has different behavior - -```ts -const { isPending, isError, error, data, isFetching } = useQuery(Todo) -``` - -### GraphQL Endpoint - -- GraphiQL: https://kg.thegraph.com/graphiql - -## Mappings file - -```ts -const mappings = { - Person: { // matches the local type name - typeIds: ['xyz'], // matches the public type ID - spaceId: 'abc', // matches the public space ID - properties: { - name: 'gfd', - email: 'asd', - isAttending: { id: 'opi', reverseId: 'opi2', to: 'Event' }, - } - }, - Event: { - typeIds: ['xyz'], // matches the public type ID - spaceId: 'abc', // matches the public space ID - properties: { - name: 'asd', - attendees: { id: 'opi2', reverseId: 'opi1', to: 'Person' }, - }, - }, -}; -``` - -```tsx - - - -``` - -useQuery(Todo) would automatically use the mapping based on the name - -It then constructs a query based on the mappings and queries the public graph API. - -## Alternative approach - -- Use a GraphQL client e.g. urql or Apollo diff --git a/docs/legacy-files/technology-stack.md b/docs/legacy-files/technology-stack.md deleted file mode 100644 index fa571258..00000000 --- a/docs/legacy-files/technology-stack.md +++ /dev/null @@ -1,18 +0,0 @@ -# Technology Stack - -- Frontend - - React - - Shadcn - - Vite - - TypeScript -- Testing - - Vitest -- Backend - - Database: Sqlite + Prisma (explore Neo4j as well) - - API: Websocket - - Effect -- Other - - CRDT: Automerge - - Cryptography: Nobel - - Auth: Privy - - ESM diff --git a/docs/package.json b/docs/package.json index 977ef03a..041af602 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,14 +18,14 @@ "@docusaurus/preset-classic": "3.8.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", - "docusaurus-plugin-generate-llms-txt": "^0.0.1", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/types": "3.8.1" + "@docusaurus/types": "3.8.1", + "docusaurus-plugin-llms": "^0.1.5" }, "browserslist": { "production": [ diff --git a/docs/sidebars.js b/docs/sidebars.js index 2cb5952d..15bfe984 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -37,7 +37,6 @@ const sidebars = { // { type: 'doc', id: 'api-reference', label: '๐Ÿ“š API Reference' }, { type: 'doc', id: 'troubleshooting', label: '๐Ÿ› ๏ธ Troubleshooting' }, { type: 'doc', id: 'faq', label: 'โ“ FAQ' }, - // { type: 'doc', id: 'legacy/README', label: '๐Ÿ—ƒ๏ธ Legacy Documentation' }, { type: 'category', label: 'Advanced', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11d73ae2..f34f4ee5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -579,9 +579,6 @@ importers: clsx: specifier: ^2.0.0 version: 2.1.1 - docusaurus-plugin-generate-llms-txt: - specifier: ^0.0.1 - version: 0.0.1(@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.9)(react@19.1.0))(acorn@8.15.0)(bufferutil@4.0.9)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)) prism-react-renderer: specifier: ^2.3.0 version: 2.4.1(react@19.1.0) @@ -598,6 +595,9 @@ importers: '@docusaurus/types': specifier: 3.8.1 version: 3.8.1(acorn@8.15.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + docusaurus-plugin-llms: + specifier: ^0.1.5 + version: 0.1.5(@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.9)(react@19.1.0))(acorn@8.15.0)(bufferutil@4.0.9)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)) packages/hypergraph: dependencies: @@ -6885,11 +6885,11 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - docusaurus-plugin-generate-llms-txt@0.0.1: - resolution: {integrity: sha512-XlUeQ23anYc85I1k9pO61fWcMWcUs8Tx7n3zqIus/1wjZ1mjezm0lOX9jymu9Y2MAi3/YQbDxsfo4kZrMf5q6A==} - engines: {node: '>=18'} + docusaurus-plugin-llms@0.1.5: + resolution: {integrity: sha512-TKcHQG6MyTTleDdOFJL+5OpmW9Le6XGiagClgSX05GXhxD//4PQlY2Iq9HVETLBIsHVETVp8AxjZQ6e3E/fr/Q==} + engines: {node: '>=18.0'} peerDependencies: - '@docusaurus/core': '>=2.0.0' + '@docusaurus/core': ^3.0.0 dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -20523,10 +20523,11 @@ snapshots: dependencies: esutils: 2.0.3 - docusaurus-plugin-generate-llms-txt@0.0.1(@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.9)(react@19.1.0))(acorn@8.15.0)(bufferutil@4.0.9)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)): + docusaurus-plugin-llms@0.1.5(@docusaurus/core@3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.9)(react@19.1.0))(acorn@8.15.0)(bufferutil@4.0.9)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)): dependencies: '@docusaurus/core': 3.8.1(@mdx-js/react@3.1.0(@types/react@19.1.9)(react@19.1.0))(acorn@8.15.0)(bufferutil@4.0.9)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10) - js-yaml: 4.1.0 + gray-matter: 4.0.3 + minimatch: 9.0.5 dom-accessibility-api@0.5.16: {}