A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: ALL the docs

Trezy a50d3507 a791aab0

+4217 -860
+23 -5
.env.example
··· 1 - HOST=0.0.0.0 1 + # Postgres 2 + POSTGRES_USER=happyview 3 + POSTGRES_PASSWORD=happyview 4 + POSTGRES_DB=happyview 5 + 6 + # Tap 7 + TAP_DATABASE_URL=postgres://tap:tap@postgres/tap 8 + TAP_RELAY_URL=https://relay1.us-east.bsky.network 9 + TAP_PLC_URL=https://plc.directory 10 + TAP_ADMIN_PASSWORD=your-secret-here 11 + TAP_COLLECTION_FILTERS= 12 + TAP_SIGNAL_COLLECTIONS= 13 + 14 + # HappyView 15 + DATABASE_URL=postgres://happyview:happyview@postgres/happyview 16 + AIP_URL=https://aip.gamesgamesgamesgames.games 17 + TAP_URL=http://tap:2480 18 + RELAY_URL=https://relay1.us-east.bsky.network 2 19 PORT=3000 3 - DATABASE_URL=postgres://localhost:5432/happyview 4 - AIP_URL=http://localhost:8080 5 - JETSTREAM_URL=wss://jetstream2.us-west.bsky.network/subscribe 6 - RUST_LOG=happyview=debug,tower_http=debug 20 + 21 + # Web dashboard 22 + WEB_HOSTNAME=0.0.0.0 23 + API_URL=http://happyview:3000 24 + AIP_PROXY_URL=https://aip.gamesgamesgamesgames.games
design/logotype.black.png

This is a binary file and will not be displayed.

+160
design/logotype.black.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + width="1023.6287mm" 6 + height="660.30609mm" 7 + viewBox="0 0 1023.6287 660.30609" 8 + version="1.1" 9 + id="svg1" 10 + inkscape:version="1.4.3 (0d15f75, 2025-12-25)" 11 + sodipodi:docname="logotype.black.svg" 12 + inkscape:export-filename="logotype.white.png" 13 + inkscape:export-xdpi="96" 14 + inkscape:export-ydpi="96" 15 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 17 + xmlns="http://www.w3.org/2000/svg" 18 + xmlns:svg="http://www.w3.org/2000/svg"> 19 + <sodipodi:namedview 20 + id="namedview1" 21 + pagecolor="#ffffff" 22 + bordercolor="#000000" 23 + borderopacity="0.25" 24 + inkscape:showpageshadow="2" 25 + inkscape:pageopacity="0.0" 26 + inkscape:pagecheckerboard="0" 27 + inkscape:deskcolor="#d1d1d1" 28 + inkscape:document-units="mm" 29 + inkscape:zoom="0.13590834" 30 + inkscape:cx="2291.9858" 31 + inkscape:cy="1559.8748" 32 + inkscape:window-width="1512" 33 + inkscape:window-height="921" 34 + inkscape:window-x="0" 35 + inkscape:window-y="33" 36 + inkscape:window-maximized="0" 37 + inkscape:current-layer="layer1" /> 38 + <defs 39 + id="defs1" /> 40 + <g 41 + inkscape:label="Layer 1" 42 + inkscape:groupmode="layer" 43 + id="layer1" 44 + transform="translate(-33.602081,181.70683)"> 45 + <g 46 + id="g13" 47 + transform="matrix(1.8968141,0,0,1.8968141,-57.531102,-664.30544)" 48 + style="fill:#000000"> 49 + <g 50 + id="g8" 51 + inkscape:label="happy" 52 + transform="rotate(-27.434563,289.39227,269.0186)" 53 + style="fill:#000000"> 54 + <text 55 + xml:space="preserve" 56 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:221.005px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 57 + x="-150.65446" 58 + y="458.86594" 59 + id="text4" 60 + transform="rotate(-23.564568)"><tspan 61 + sodipodi:role="line" 62 + id="tspan4" 63 + x="-150.65446" 64 + y="458.86594" 65 + style="fill:#000000;stroke-width:0">H</tspan></text> 66 + <text 67 + xml:space="preserve" 68 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 69 + x="62.749569" 70 + y="446.52225" 71 + id="text5" 72 + transform="rotate(-10.154403)"><tspan 73 + sodipodi:role="line" 74 + id="tspan5" 75 + x="62.749569" 76 + y="446.52225">a</tspan></text> 77 + <text 78 + xml:space="preserve" 79 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 80 + x="246.01134" 81 + y="374.17978" 82 + id="text6" 83 + transform="rotate(4.7758099)"><tspan 84 + sodipodi:role="line" 85 + id="tspan6" 86 + x="246.01134" 87 + y="374.17978">p</tspan></text> 88 + <text 89 + xml:space="preserve" 90 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 91 + x="399.94565" 92 + y="268.97476" 93 + id="text7" 94 + transform="rotate(20.795498)"><tspan 95 + sodipodi:role="line" 96 + id="tspan7" 97 + x="399.94565" 98 + y="268.97476">p</tspan></text> 99 + <text 100 + xml:space="preserve" 101 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 102 + x="504.13312" 103 + y="266.52905" 104 + id="text8" 105 + transform="rotate(25.591901)"><tspan 106 + sodipodi:role="line" 107 + id="tspan8" 108 + x="504.13312" 109 + y="266.52905">y</tspan></text> 110 + </g> 111 + <g 112 + id="g12" 113 + style="fill:#000000"> 114 + <text 115 + xml:space="preserve" 116 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 117 + x="236.74713" 118 + y="603.30707" 119 + id="text9"><tspan 120 + sodipodi:role="line" 121 + id="tspan9" 122 + x="236.74713" 123 + y="603.30707">V</tspan></text> 124 + <text 125 + xml:space="preserve" 126 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 127 + x="300.23373" 128 + y="622.72784" 129 + id="text10" 130 + transform="rotate(-3.9284119)"><tspan 131 + sodipodi:role="line" 132 + id="tspan10" 133 + x="300.23373" 134 + y="622.72784">i</tspan></text> 135 + <text 136 + xml:space="preserve" 137 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 138 + x="251.78416" 139 + y="664.49115" 140 + id="text11" 141 + transform="rotate(-13.020145)"><tspan 142 + sodipodi:role="line" 143 + id="tspan11" 144 + x="251.78416" 145 + y="664.49115">e</tspan></text> 146 + <text 147 + xml:space="preserve" 148 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#000000;stroke:#000000;stroke-width:0;stroke-dasharray:none" 149 + x="92.779106" 150 + y="758.57172" 151 + id="text12" 152 + transform="rotate(-33.677515)"><tspan 153 + sodipodi:role="line" 154 + id="tspan12" 155 + x="92.779106" 156 + y="758.57172">w</tspan></text> 157 + </g> 158 + </g> 159 + </g> 160 + </svg>
design/logotype.white.png

This is a binary file and will not be displayed.

+160
design/logotype.white.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + width="1023.6287mm" 6 + height="660.30609mm" 7 + viewBox="0 0 1023.6287 660.30609" 8 + version="1.1" 9 + id="svg1" 10 + inkscape:version="1.4.3 (0d15f75, 2025-12-25)" 11 + sodipodi:docname="logotype.white.svg" 12 + inkscape:export-filename="logotype.png" 13 + inkscape:export-xdpi="96" 14 + inkscape:export-ydpi="96" 15 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 17 + xmlns="http://www.w3.org/2000/svg" 18 + xmlns:svg="http://www.w3.org/2000/svg"> 19 + <sodipodi:namedview 20 + id="namedview1" 21 + pagecolor="#ffffff" 22 + bordercolor="#000000" 23 + borderopacity="0.25" 24 + inkscape:showpageshadow="2" 25 + inkscape:pageopacity="0.0" 26 + inkscape:pagecheckerboard="0" 27 + inkscape:deskcolor="#d1d1d1" 28 + inkscape:document-units="mm" 29 + inkscape:zoom="0.13590834" 30 + inkscape:cx="2291.9858" 31 + inkscape:cy="1559.8748" 32 + inkscape:window-width="1512" 33 + inkscape:window-height="921" 34 + inkscape:window-x="0" 35 + inkscape:window-y="33" 36 + inkscape:window-maximized="0" 37 + inkscape:current-layer="layer1" /> 38 + <defs 39 + id="defs1" /> 40 + <g 41 + inkscape:label="Layer 1" 42 + inkscape:groupmode="layer" 43 + id="layer1" 44 + transform="translate(-33.602081,181.70683)"> 45 + <g 46 + id="g13" 47 + transform="matrix(1.8968141,0,0,1.8968141,-57.531102,-664.30544)" 48 + style="fill:#ffffff"> 49 + <g 50 + id="g8" 51 + inkscape:label="happy" 52 + transform="rotate(-27.434563,289.39227,269.0186)" 53 + style="fill:#ffffff"> 54 + <text 55 + xml:space="preserve" 56 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:221.005px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 57 + x="-150.65446" 58 + y="458.86594" 59 + id="text4" 60 + transform="rotate(-23.564568)"><tspan 61 + sodipodi:role="line" 62 + id="tspan4" 63 + x="-150.65446" 64 + y="458.86594" 65 + style="fill:#ffffff;stroke-width:0">H</tspan></text> 66 + <text 67 + xml:space="preserve" 68 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 69 + x="62.749569" 70 + y="446.52225" 71 + id="text5" 72 + transform="rotate(-10.154403)"><tspan 73 + sodipodi:role="line" 74 + id="tspan5" 75 + x="62.749569" 76 + y="446.52225">a</tspan></text> 77 + <text 78 + xml:space="preserve" 79 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 80 + x="246.01134" 81 + y="374.17978" 82 + id="text6" 83 + transform="rotate(4.7758099)"><tspan 84 + sodipodi:role="line" 85 + id="tspan6" 86 + x="246.01134" 87 + y="374.17978">p</tspan></text> 88 + <text 89 + xml:space="preserve" 90 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 91 + x="399.94565" 92 + y="268.97476" 93 + id="text7" 94 + transform="rotate(20.795498)"><tspan 95 + sodipodi:role="line" 96 + id="tspan7" 97 + x="399.94565" 98 + y="268.97476">p</tspan></text> 99 + <text 100 + xml:space="preserve" 101 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 102 + x="504.13312" 103 + y="266.52905" 104 + id="text8" 105 + transform="rotate(25.591901)"><tspan 106 + sodipodi:role="line" 107 + id="tspan8" 108 + x="504.13312" 109 + y="266.52905">y</tspan></text> 110 + </g> 111 + <g 112 + id="g12" 113 + style="fill:#ffffff"> 114 + <text 115 + xml:space="preserve" 116 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 117 + x="236.74713" 118 + y="603.30707" 119 + id="text9"><tspan 120 + sodipodi:role="line" 121 + id="tspan9" 122 + x="236.74713" 123 + y="603.30707">V</tspan></text> 124 + <text 125 + xml:space="preserve" 126 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 127 + x="300.23373" 128 + y="622.72784" 129 + id="text10" 130 + transform="rotate(-3.9284119)"><tspan 131 + sodipodi:role="line" 132 + id="tspan10" 133 + x="300.23373" 134 + y="622.72784">i</tspan></text> 135 + <text 136 + xml:space="preserve" 137 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 138 + x="251.78416" 139 + y="664.49115" 140 + id="text11" 141 + transform="rotate(-13.020145)"><tspan 142 + sodipodi:role="line" 143 + id="tspan11" 144 + x="251.78416" 145 + y="664.49115">e</tspan></text> 146 + <text 147 + xml:space="preserve" 148 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;line-height:0px;font-family:Heyam;-inkscape-font-specification:Heyam;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:0;stroke-dasharray:none" 149 + x="92.779106" 150 + y="758.57172" 151 + id="text12" 152 + transform="rotate(-33.677515)"><tspan 153 + sodipodi:role="line" 154 + id="tspan12" 155 + x="92.779106" 156 + y="758.57172">w</tspan></text> 157 + </g> 158 + </g> 159 + </g> 160 + </svg>
+18 -22
docker-compose.yml
··· 2 2 postgres: 3 3 image: postgres:17 4 4 environment: 5 - POSTGRES_USER: happyview 6 - POSTGRES_PASSWORD: happyview 7 - POSTGRES_DB: happyview 5 + POSTGRES_USER: ${POSTGRES_USER} 6 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 7 + POSTGRES_DB: ${POSTGRES_DB} 8 8 ports: 9 9 - "5432:5432" 10 10 volumes: 11 11 - pgdata:/var/lib/postgresql/data 12 12 - ./docker/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh 13 13 healthcheck: 14 - test: ["CMD-SHELL", "pg_isready -U happyview"] 14 + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] 15 15 interval: 5s 16 16 timeout: 3s 17 17 retries: 5 18 18 19 19 tap: 20 - build: 21 - context: ../indigo 22 - dockerfile: cmd/tap/Dockerfile 23 - # image: ghcr.io/bluesky-social/indigo/tap:latest 20 + image: ghcr.io/bluesky-social/indigo/tap:latest 24 21 ports: 25 22 - "2480:2480" 26 23 environment: 27 - TAP_DATABASE_URL: postgres://tap:tap@postgres/tap 28 - TAP_RELAY_URL: https://relay1.us-east.bsky.network 29 - TAP_PLC_URL: https://plc.directory 24 + TAP_DATABASE_URL: ${TAP_DATABASE_URL} 25 + TAP_RELAY_URL: ${TAP_RELAY_URL} 26 + TAP_PLC_URL: ${TAP_PLC_URL} 30 27 TAP_ADMIN_PASSWORD: ${TAP_ADMIN_PASSWORD} 31 - TAP_COLLECTION_FILTERS: "" 32 - TAP_SIGNAL_COLLECTIONS: "" 28 + TAP_COLLECTION_FILTERS: ${TAP_COLLECTION_FILTERS} 29 + TAP_SIGNAL_COLLECTIONS: ${TAP_SIGNAL_COLLECTIONS} 33 30 depends_on: 34 31 postgres: 35 32 condition: service_healthy ··· 46 43 - cargo-git:/usr/local/cargo/git 47 44 - cargo-target:/app/target 48 45 environment: 49 - DATABASE_URL: postgres://happyview:happyview@postgres/happyview 50 - AIP_URL: https://aip.gamesgamesgamesgames.games 51 - TAP_URL: http://tap:2480 46 + DATABASE_URL: ${DATABASE_URL} 47 + AIP_URL: ${AIP_URL} 48 + TAP_URL: ${TAP_URL} 52 49 TAP_ADMIN_PASSWORD: ${TAP_ADMIN_PASSWORD} 53 - RELAY_URL: https://relay1.us-east.bsky.network 54 - PORT: 3000 50 + RELAY_URL: ${RELAY_URL} 51 + PORT: ${PORT} 55 52 depends_on: 56 53 postgres: 57 54 condition: service_healthy ··· 68 65 - ./web:/app 69 66 - web-node-modules:/app/node_modules 70 67 environment: 71 - HOSTNAME: 0.0.0.0 72 - API_URL: http://happyview:3000 73 - AIP_PROXY_URL: https://aip.gamesgamesgamesgames.games 74 - # NEXT_PUBLIC_AIP_URL: http://localhost:8080 68 + HOSTNAME: ${WEB_HOSTNAME} 69 + API_URL: ${API_URL} 70 + AIP_PROXY_URL: ${AIP_PROXY_URL} 75 71 76 72 volumes: 77 73 pgdata:
+25 -24
docs/README.md
··· 1 1 # HappyView 2 2 3 - HappyView is a lexicon-driven ATProto AppView. Upload lexicon definitions at runtime and HappyView dynamically generates XRPC query and procedure endpoints, indexes records from the network via Jetstream, and proxies writes to users' PDSes --- no restart required. 3 + HappyView is the best way to build an [AppView](https://atproto.com/guides/glossary#app-view) for the [AT Protocol](https://atproto.com). Upload your [lexicon](reference/glossary#at-protocol-terms) schemas and get a fully functional AppView, complete with [XRPC](reference/glossary#at-protocol-terms) endpoints, OAuth, real-time network sync, and historical [backfill](guides/backfill), without writing a single line of server code. 4 + 5 + Building an AppView from scratch means wiring up firehose connections, record storage, XRPC routing, OAuth flows, and PDS write proxying before you can even think about your application. HappyView handles all of that. Define your data model with lexicons, add custom logic with Lua scripts when you need it, and ship your app. 6 + 7 + ## Features 8 + 9 + - 📜 **Lexicon-Driven**: Upload your lexicon schemas and HappyView generates fully functional XRPC query and procedure endpoints automatically, no code required 10 + - 🔄 **Real-Time Sync**: Records stream in from the AT Protocol network in real-time via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap), with cryptographic verification and backfill via the admin API 11 + - 🔐 **OAuth Built In**: [AIP](https://github.com/graze-social/aip) handles authentication, and writes are proxied back to the user's PDS, so there's no session management needed 12 + - 🌙 **Lua Scripting**: Add custom query and procedure logic with Lua scripts that have full access to the record database 13 + - 🗄️ **Automatic Indexing**: HappyView indexes relevant records into PostgreSQL as they arrive, ready to query 14 + - 🌐 **Network Lexicons**: Fetch lexicon schemas directly from the AT Protocol network via DNS authority resolution 15 + - ⚡ **Hot Reloading**: Upload or update lexicons at runtime, and new endpoints are available immediately with no restart 16 + - 🛠️ **Admin Dashboard**: Manage lexicons, monitor record stats, and run backfill jobs through a built-in admin API 4 17 5 - ## How it fits together 18 + ## Design Principles 6 19 7 - ``` 8 - Jetstream ──> HappyView ──> PostgreSQL 9 - 10 - ┌───────────┼───────────┐ 11 - │ │ │ 12 - Clients AIP PDSes 13 - (OAuth) (user repos) 14 - ``` 20 + - **Schema-first**: Your Lexicons are the source of truth. Upload a schema and HappyView derives endpoints, indexing rules, and network sync from it. You describe _what_ your data looks like; HappyView figures out the rest. 21 + 22 + - **Zero boilerplate**: HappyView handles AppView infrastructure (firehose, backfill, OAuth, PDS proxying) for you. You should be writing application logic from minute one, not plumbing. 23 + 24 + - **Runtime-configurable**: Lexicons can be added, updated, and removed without restarting the server. New endpoints and sync rules take effect immediately, so you can iterate on your data model in real time. 15 25 16 - - **Jetstream** pushes real-time record events. HappyView subscribes to the collections defined by uploaded lexicons and indexes records into Postgres. 17 - - **AIP** (ATProto Identity Provider) handles OAuth 2.1 with PKCE. HappyView validates tokens by calling AIP's `/oauth/userinfo` endpoint. 18 - - **PDSes** store user data. HappyView proxies writes and blob uploads to each user's PDS using DPoP-authenticated requests. 19 - - **Clients** talk to HappyView's XRPC and admin APIs. Any ATProto-compatible client can connect. 26 + - **Protocol-native**: HappyView works with _any_ PDS, resolves DIDs through the directory, and follows AT Protocol conventions. It's a first-class citizen of the network, not a wrapper around it. 20 27 21 - ## Docs 28 + ## Next Steps 22 29 23 - - [Quickstart](quickstart.md) - get a local instance running 24 - - [Configuration](configuration.md) - environment variables reference 25 - - [Deployment](deployment.md) - Docker, production, TLS 26 - - [Lexicons](lexicons.md) - uploading and managing lexicon definitions 27 - - [Network Lexicons](network-lexicons.md) - loading lexicons from the ATProto network 28 - - [Backfill](backfill.md) - bulk-indexing historical records 29 - - [XRPC API](xrpc-api.md) - query and procedure endpoints 30 - - [Admin API](admin-api.md) - manage lexicons, backfills, and admins 31 - - [Architecture](architecture.md) - internals for contributors 30 + - [Quickstart](getting-started/deployment/railway): Deploy HappyView on Railway or run it locally 31 + - [Lexicons](guides/lexicons): Upload lexicon schemas and start indexing records 32 + - [Lua Scripting](guides/scripting): Write custom query and procedure logic
+61 -24
docs/admin-api.md docs/reference/admin-api.md
··· 1 1 # Admin API 2 2 3 - All admin endpoints live under `/admin` and require an AIP-issued Bearer token from a DID that exists in the `admins` table. 3 + The admin API lets you manage lexicons, monitor records, run backfill jobs, and control admin access. All endpoints live under `/admin` and require an [AIP](https://github.com/graze-social/aip)-issued Bearer token from a DID that exists in the `admins` table. You can also manage all of this through the [web dashboard](../getting-started/dashboard). 4 4 5 5 ## Auth 6 6 7 - Admin auth works the same as user auth — the Bearer token is validated against AIP's `/oauth/userinfo` endpoint to retrieve the caller's DID. That DID is then checked against the `admins` table. 7 + Admin auth works the same as user auth: the Bearer token is validated against AIP's `/oauth/userinfo` endpoint to retrieve the caller's DID. That DID is then checked against the `admins` table. 8 8 9 9 **Auto-bootstrap**: If the `admins` table is empty, the first authenticated request automatically inserts the caller as the initial admin. 10 10 11 11 Non-admin DIDs receive a `403 Forbidden` response. 12 12 13 + All error responses return JSON with an `error` field: 14 + 15 + ```json 16 + { 17 + "error": "description of what went wrong" 18 + } 19 + ``` 20 + 21 + | Status | Meaning | 22 + |--------|---------| 23 + | `400 Bad Request` | Invalid input (missing required fields, malformed lexicon JSON) | 24 + | `401 Unauthorized` | Missing or invalid Bearer token. See [AIP documentation](https://github.com/graze-social/aip) for token issues | 25 + | `403 Forbidden` | Authenticated DID is not in the admins table | 26 + | `404 Not Found` | Lexicon, admin, or backfill job not found | 27 + 13 28 ```sh 14 29 # All examples assume $TOKEN is an AIP-issued access token for an admin DID 15 30 AUTH="Authorization: Bearer $TOKEN" 16 31 ``` 17 - 18 - --- 19 32 20 33 ## Lexicons 21 34 ··· 30 43 -H "$AUTH" \ 31 44 -H "Content-Type: application/json" \ 32 45 -d '{ 33 - "lexicon_json": { "lexicon": 1, "id": "example.record", "defs": { "main": { "type": "record", "key": "tid", "record": { "type": "object", "properties": { "title": { "type": "string" } } } } } }, 46 + "lexicon_json": { "lexicon": 1, "id": "xyz.statusphere.status", "defs": { "main": { "type": "record", "key": "tid", "record": { "type": "object", "required": ["status", "createdAt"], "properties": { "status": { "type": "string", "maxGraphemes": 1 }, "createdAt": { "type": "string", "format": "datetime" } } } } } }, 34 47 "backfill": true, 35 48 "target_collection": null 36 49 }' ··· 46 59 47 60 ```json 48 61 { 49 - "id": "example.record", 62 + "id": "xyz.statusphere.status", 50 63 "revision": 1 51 64 } 52 65 ``` ··· 66 79 ```json 67 80 [ 68 81 { 69 - "id": "example.record", 82 + "id": "xyz.statusphere.status", 70 83 "revision": 1, 71 84 "lexicon_type": "record", 72 85 "backfill": true, ··· 83 96 ``` 84 97 85 98 ```sh 86 - curl http://localhost:3000/admin/lexicons/example.record -H "$AUTH" 99 + curl http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH" 87 100 ``` 88 101 89 102 **Response**: `200 OK` with full lexicon details including raw JSON. ··· 95 108 ``` 96 109 97 110 ```sh 98 - curl -X DELETE http://localhost:3000/admin/lexicons/example.record -H "$AUTH" 111 + curl -X DELETE http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH" 99 112 ``` 100 113 101 114 **Response**: `204 No Content` 102 - 103 - --- 104 115 105 116 ## Network Lexicons 106 117 107 - Network lexicons are fetched from the ATProto network via DNS TXT resolution and kept updated via Jetstream. See [Network Lexicons](network-lexicons.md) for background. 118 + Network lexicons are fetched from the AT Protocol network via DNS TXT resolution and kept updated via Tap. See [Lexicons - Network lexicons](../guides/lexicons#network-lexicons) for background. 108 119 109 120 ### Add a network lexicon 110 121 ··· 117 128 -H "$AUTH" \ 118 129 -H "Content-Type: application/json" \ 119 130 -d '{ 120 - "nsid": "games.gamesgamesgamesgames.game", 131 + "nsid": "xyz.statusphere.status", 121 132 "target_collection": null 122 133 }' 123 134 ``` ··· 133 144 134 145 ```json 135 146 { 136 - "nsid": "games.gamesgamesgamesgames.game", 147 + "nsid": "xyz.statusphere.status", 137 148 "authority_did": "did:plc:authority", 138 149 "revision": 1 139 150 } ··· 154 165 ```json 155 166 [ 156 167 { 157 - "nsid": "games.gamesgamesgamesgames.game", 168 + "nsid": "xyz.statusphere.status", 158 169 "authority_did": "did:plc:authority", 159 170 "target_collection": null, 160 171 "last_fetched_at": "2025-01-01T00:00:00Z", ··· 170 181 ``` 171 182 172 183 ```sh 173 - curl -X DELETE http://localhost:3000/admin/network-lexicons/games.gamesgamesgamesgames.game \ 184 + curl -X DELETE http://localhost:3000/admin/network-lexicons/xyz.statusphere.status \ 174 185 -H "$AUTH" 175 186 ``` 176 187 ··· 178 189 179 190 **Response**: `204 No Content` 180 191 181 - --- 182 - 183 192 ## Stats 184 193 185 194 ### Record counts ··· 197 206 ```json 198 207 { 199 208 "total_records": 12345, 200 - "collections": [{ "collection": "example.record", "count": 500 }] 209 + "collections": [{ "collection": "xyz.statusphere.status", "count": 500 }] 201 210 } 202 211 ``` 203 212 204 - --- 213 + ## Tap Stats 214 + 215 + Aggregate stats from the [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) instance. Useful for monitoring backfill progress. See [Backfill - Job lifecycle](../guides/backfill#job-lifecycle) for context. 216 + 217 + ### Get Tap stats 218 + 219 + ``` 220 + GET /admin/tap/stats 221 + ``` 222 + 223 + ```sh 224 + curl http://localhost:3000/admin/tap/stats -H "$AUTH" 225 + ``` 226 + 227 + **Response**: `200 OK` 228 + 229 + ```json 230 + { 231 + "repo_count": 5234, 232 + "record_count": 1048576, 233 + "outbox_buffer": 42 234 + } 235 + ``` 236 + 237 + | Field | Type | Description | 238 + | -------------- | ------ | -------------------------------------------------------- | 239 + | `repo_count` | number | Total repos Tap is tracking | 240 + | `record_count` | number | Total records Tap has indexed | 241 + | `outbox_buffer`| number | Pending events awaiting delivery (high = Tap is busy) | 242 + 243 + Returns `502 Bad Gateway` if Tap is unreachable. 205 244 206 245 ## Backfill 207 246 ··· 215 254 curl -X POST http://localhost:3000/admin/backfill \ 216 255 -H "$AUTH" \ 217 256 -H "Content-Type: application/json" \ 218 - -d '{ "collection": "example.record" }' 257 + -d '{ "collection": "xyz.statusphere.status" }' 219 258 ``` 220 259 221 260 | Field | Type | Required | Description | ··· 248 287 [ 249 288 { 250 289 "id": "550e8400-e29b-41d4-a716-446655440000", 251 - "collection": "example.record", 290 + "collection": "xyz.statusphere.status", 252 291 "did": null, 253 292 "status": "completed", 254 293 "total_repos": 42, ··· 261 300 } 262 301 ] 263 302 ``` 264 - 265 - --- 266 303 267 304 ## Admin management 268 305
-170
docs/architecture.md
··· 1 - # Architecture 2 - 3 - Guide for contributors working on HappyView itself. 4 - 5 - ## Module overview 6 - 7 - ``` 8 - src/ 9 - main.rs Startup: config, DB, migrations, spawn workers, start server 10 - lib.rs AppState struct, module declarations 11 - config.rs Environment variable loading 12 - error.rs AppError enum (Auth, BadRequest, Forbidden, Internal, NotFound, PdsError) 13 - server.rs Axum router: fixed routes + admin nest + XRPC catch-all 14 - lexicon.rs ParsedLexicon, LexiconRegistry (Arc<RwLock<HashMap>>) 15 - profile.rs DID document resolution, PDS discovery, profile fetching 16 - jetstream.rs Jetstream WebSocket listener, record indexing 17 - backfill.rs Background worker for bulk record ingestion 18 - auth/ 19 - middleware.rs Claims extractor (validates Bearer token via AIP /oauth/userinfo) 20 - admin/ 21 - mod.rs Admin route definitions 22 - auth.rs AdminAuth extractor (Claims + DID lookup + auto-bootstrap) 23 - admins.rs Admin CRUD handlers 24 - lexicons.rs Lexicon CRUD handlers 25 - stats.rs Record count stats 26 - backfill.rs Backfill job creation and status 27 - types.rs Request/response structs for admin endpoints 28 - repo/ 29 - mod.rs Re-exports 30 - dpop.rs DPoP JWT proof generation (ES256/P-256) 31 - pds.rs PDS proxy helpers (JSON POST, blob POST, response forwarding) 32 - session.rs ATP session fetching from AIP 33 - upload_blob.rs Blob upload handler 34 - media.rs Media blob URL enrichment 35 - at_uri.rs AT URI parsing 36 - xrpc/ 37 - mod.rs Re-exports 38 - query.rs Dynamic GET handler (single record + list with pagination) 39 - procedure.rs Dynamic POST handler (create vs put auto-detection) 40 - ``` 41 - 42 - ## Request flow 43 - 44 - ### Reads (queries) 45 - 46 - ``` 47 - Client GET /xrpc/{method}?params 48 - -> xrpc::xrpc_get() 49 - -> LexiconRegistry lookup (must be Query type) 50 - -> SQL query on records table (collection from target_collection) 51 - -> Media blob URL enrichment 52 - -> JSON response 53 - ``` 54 - 55 - ### Writes (procedures) 56 - 57 - ``` 58 - Client POST /xrpc/{method} + Bearer token 59 - -> Claims extractor validates token via AIP /oauth/userinfo 60 - -> xrpc::xrpc_post() 61 - -> LexiconRegistry lookup (must be Procedure type) 62 - -> Fetch ATP session from AIP /api/atprotocol/session 63 - -> Generate DPoP proof (ES256) 64 - -> Proxy to user's PDS (createRecord or putRecord) 65 - -> Upsert record locally 66 - -> Forward PDS response 67 - ``` 68 - 69 - ### Admin endpoints 70 - 71 - ``` 72 - Client request + Bearer token 73 - -> AdminAuth extractor: 74 - 1. Claims validation via AIP 75 - 2. DID lookup in admins table (auto-bootstrap if empty) 76 - 3. 403 if not admin 77 - -> Admin handler 78 - -> JSON response 79 - ``` 80 - 81 - ## Data flow 82 - 83 - ### Real-time indexing 84 - 85 - ``` 86 - Jetstream WebSocket 87 - -> Filter by wantedCollections (from record-type lexicons) 88 - -> commit events: 89 - create/update -> UPSERT into records table 90 - delete -> DELETE from records table 91 - -> Cursor tracked in AtomicI64 (rewinds 5s on reconnect) 92 - ``` 93 - 94 - ### Backfill 95 - 96 - ``` 97 - backfill_jobs table (status = pending) 98 - -> Worker picks up job 99 - -> Relay listReposByCollection -> list of DIDs 100 - -> For each DID (8 concurrent): 101 - PLC directory -> PDS endpoint 102 - PDS listRecords -> UPSERT into records table 103 - -> Update job status and counters 104 - ``` 105 - 106 - ## Database schema 107 - 108 - ### `records` 109 - 110 - | Column | Type | Description | 111 - |--------|------|-------------| 112 - | `uri` | text (PK) | AT URI (`at://did/collection/rkey`) | 113 - | `did` | text | Author DID | 114 - | `collection` | text | Lexicon NSID | 115 - | `rkey` | text | Record key | 116 - | `record` | jsonb | Record value | 117 - | `cid` | text | Content identifier | 118 - | `indexed_at` | timestamptz | When HappyView indexed this record | 119 - 120 - ### `lexicons` 121 - 122 - | Column | Type | Description | 123 - |--------|------|-------------| 124 - | `id` | text (PK) | Lexicon NSID | 125 - | `revision` | integer | Incremented on upsert | 126 - | `lexicon_json` | jsonb | Raw lexicon definition | 127 - | `lexicon_type` | text | record, query, procedure, definitions | 128 - | `backfill` | boolean | Whether to backfill on upload | 129 - | `target_collection` | text | For queries/procedures: which record collection | 130 - | `created_at` | timestamptz | | 131 - | `updated_at` | timestamptz | | 132 - 133 - ### `admins` 134 - 135 - | Column | Type | Description | 136 - |--------|------|-------------| 137 - | `id` | uuid (PK) | | 138 - | `did` | text (unique) | Admin's ATProto DID | 139 - | `created_at` | timestamptz | | 140 - | `last_used_at` | timestamptz | Updated on each authenticated request | 141 - 142 - ### `backfill_jobs` 143 - 144 - | Column | Type | Description | 145 - |--------|------|-------------| 146 - | `id` | uuid (PK) | | 147 - | `collection` | text | Target collection (null = all) | 148 - | `did` | text | Target DID (null = all) | 149 - | `status` | text | pending, running, completed, failed | 150 - | `total_repos` | integer | | 151 - | `processed_repos` | integer | | 152 - | `total_records` | integer | | 153 - | `error` | text | Error message if failed | 154 - | `started_at` | timestamptz | | 155 - | `completed_at` | timestamptz | | 156 - | `created_at` | timestamptz | | 157 - 158 - ## Testing 159 - 160 - ```sh 161 - # Unit tests (no database needed) 162 - cargo test --lib 163 - 164 - # All tests including e2e (requires Postgres) 165 - docker compose -f docker-compose.test.yml up -d 166 - TEST_DATABASE_URL=postgres://happyview:happyview@localhost:5433/happyview_test cargo test 167 - docker compose -f docker-compose.test.yml down 168 - ``` 169 - 170 - E2e tests use `wiremock` to mock external services (AIP, PLC directory, PDSes) and a real Postgres database for full integration coverage.
-78
docs/backfill.md
··· 1 - # Backfill 2 - 3 - Backfill bulk-indexes existing records from the ATProto network into HappyView's database. It discovers repos via the relay and fetches records directly from each user's PDS. 4 - 5 - ## When backfill runs 6 - 7 - - **Automatically** when a record-type lexicon is uploaded with `backfill: true` (the default) 8 - - **Manually** via `POST /admin/backfill` 9 - 10 - ## How it works 11 - 12 - 1. **Determine target collections**: uses the specified collection, or all record lexicons with `backfill: true` 13 - 2. **Discover DIDs**: calls the relay's `com.atproto.sync.listReposByCollection` to find repos that contain records for each collection (paginated, 1000 per page) 14 - 3. **Fetch records**: for each DID, resolves the PDS endpoint via PLC directory, then calls `com.atproto.repo.listRecords` on the PDS (paginated, 100 per page) 15 - 4. **Upsert**: inserts or updates records in Postgres 16 - 17 - ## Creating a backfill job 18 - 19 - ### Backfill all collections 20 - 21 - ```sh 22 - curl -X POST http://localhost:3000/admin/backfill \ 23 - -H "Authorization: Bearer $TOKEN" \ 24 - -H "Content-Type: application/json" \ 25 - -d '{}' 26 - ``` 27 - 28 - ### Backfill a specific collection 29 - 30 - ```sh 31 - curl -X POST http://localhost:3000/admin/backfill \ 32 - -H "Authorization: Bearer $TOKEN" \ 33 - -H "Content-Type: application/json" \ 34 - -d '{ "collection": "games.gamesgamesgamesgames.game" }' 35 - ``` 36 - 37 - ### Backfill a specific DID 38 - 39 - ```sh 40 - curl -X POST http://localhost:3000/admin/backfill \ 41 - -H "Authorization: Bearer $TOKEN" \ 42 - -H "Content-Type: application/json" \ 43 - -d '{ 44 - "collection": "games.gamesgamesgamesgames.game", 45 - "did": "did:plc:abc123" 46 - }' 47 - ``` 48 - 49 - ## Monitoring progress 50 - 51 - ```sh 52 - curl http://localhost:3000/admin/backfill/status \ 53 - -H "Authorization: Bearer $TOKEN" 54 - ``` 55 - 56 - ```json 57 - [ 58 - { 59 - "id": "550e8400-e29b-41d4-a716-446655440000", 60 - "collection": "games.gamesgamesgamesgames.game", 61 - "did": null, 62 - "status": "running", 63 - "total_repos": 42, 64 - "processed_repos": 15, 65 - "total_records": 350, 66 - "error": null, 67 - "started_at": "2025-01-01T00:01:00Z", 68 - "completed_at": null, 69 - "created_at": "2025-01-01T00:00:00Z" 70 - } 71 - ] 72 - ``` 73 - 74 - Job statuses: `pending` -> `running` -> `completed` or `failed`. 75 - 76 - ## Concurrency 77 - 78 - The backfill worker processes one job at a time, polling for pending jobs every 5 seconds. Within a job, up to 8 PDSes are fetched concurrently.
-31
docs/configuration.md
··· 1 - # Configuration 2 - 3 - HappyView is configured via environment variables. A `.env` file in the project root is loaded automatically on startup. 4 - 5 - ## Environment variables 6 - 7 - | Variable | Required | Default | Description | 8 - |----------|----------|---------|-------------| 9 - | `DATABASE_URL` | yes | --- | Postgres connection string | 10 - | `AIP_URL` | yes | --- | AIP instance URL for OAuth token validation | 11 - | `HOST` | no | `0.0.0.0` | Bind host | 12 - | `PORT` | no | `3000` | Bind port | 13 - | `JETSTREAM_URL` | no | `wss://jetstream2.us-west.bsky.network/subscribe` | Jetstream WebSocket URL | 14 - | `RELAY_URL` | no | `https://bsky.network` | Relay URL for backfill repo discovery | 15 - | `PLC_URL` | no | `https://plc.directory` | PLC directory URL for DID resolution | 16 - | `RUST_LOG` | no | `happyview=debug,tower_http=debug` | Log filter (uses `tracing_subscriber::EnvFilter`) | 17 - 18 - ## Example `.env` 19 - 20 - ```sh 21 - DATABASE_URL=postgres://happyview:happyview@localhost/happyview 22 - AIP_URL=http://localhost:8080 23 - 24 - # Optional overrides 25 - # HOST=0.0.0.0 26 - # PORT=3000 27 - # JETSTREAM_URL=wss://jetstream2.us-west.bsky.network/subscribe 28 - # RELAY_URL=https://bsky.network 29 - # PLC_URL=https://plc.directory 30 - # RUST_LOG=happyview=debug,tower_http=debug 31 - ```
-61
docs/deployment.md
··· 1 - # Deployment 2 - 3 - ## Docker 4 - 5 - Build the image: 6 - 7 - ```sh 8 - docker build -t happyview . 9 - ``` 10 - 11 - ### Local development with Docker Compose 12 - 13 - ```sh 14 - docker compose up 15 - ``` 16 - 17 - This starts Postgres, AIP, and HappyView together. See `docker-compose.yml` for the full configuration. 18 - 19 - ### Production Compose example 20 - 21 - ```yaml 22 - services: 23 - postgres: 24 - image: postgres:17 25 - environment: 26 - POSTGRES_USER: happyview 27 - POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" 28 - POSTGRES_DB: happyview 29 - volumes: 30 - - pgdata:/var/lib/postgresql/data 31 - 32 - happyview: 33 - image: happyview:latest 34 - ports: 35 - - "3000:3000" 36 - environment: 37 - DATABASE_URL: "postgres://happyview:${POSTGRES_PASSWORD}@postgres/happyview" 38 - AIP_URL: "https://aip.example.com" 39 - depends_on: 40 - postgres: 41 - condition: service_healthy 42 - 43 - volumes: 44 - pgdata: 45 - ``` 46 - 47 - ## Railway / Fly.io / generic 48 - 49 - 1. Provision a Postgres database 50 - 2. Set `DATABASE_URL` and `AIP_URL` environment variables 51 - 3. Deploy the Docker image or build from source 52 - 4. HappyView listens on `PORT` (default `3000`) 53 - 5. Health check: `GET /health` returns `ok` 54 - 55 - ## Database 56 - 57 - Migrations run automatically on startup via `sqlx::migrate!()`. No manual migration step is needed. 58 - 59 - ## TLS 60 - 61 - HappyView does not terminate TLS. Put it behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel, etc.) for HTTPS.
+65
docs/getting-started/authentication.md
··· 1 + # Authentication 2 + 3 + HappyView uses [AT Protocol OAuth](https://atproto.com/specs/oauth) for authentication, handled by an external [AIP](https://github.com/graze-social/aip) instance. HappyView does not store credentials or issue tokens: all OAuth is delegated to AIP. 4 + 5 + ## Which endpoints require auth? 6 + 7 + | Endpoint type | Auth required? | 8 + |---------------|---------------| 9 + | Queries (`GET /xrpc/{method}`) | No | 10 + | Procedures (`POST /xrpc/{method}`) | Yes | 11 + | Admin API (`/admin/*`) | Yes (must be an admin) | 12 + | Health check (`GET /health`) | No | 13 + 14 + Authenticated requests must include an `Authorization` header with a token issued by AIP: 15 + 16 + ``` 17 + Authorization: Bearer <token> 18 + ``` 19 + 20 + ## Getting a token from the dashboard 21 + 22 + The easiest way to get a token for CLI or curl usage is through the [web dashboard](dashboard): 23 + 24 + 1. Open the dashboard and log in with your AT Protocol identity 25 + 2. Open your browser's developer tools (F12 or Cmd+Shift+I) 26 + 3. Go to **Application** (Chrome) or **Storage** (Firefox) > **Session Storage** 27 + 4. Find the entry for your dashboard's URL 28 + 5. Copy the value of the `session` key: this contains your access token 29 + 30 + You can then use it in curl: 31 + 32 + ```sh 33 + export TOKEN="your-token-here" 34 + curl http://localhost:3000/admin/lexicons \ 35 + -H "Authorization: Bearer $TOKEN" 36 + ``` 37 + 38 + Tokens expire based on AIP's configuration. When a token expires, log in again through the dashboard to get a new one. 39 + 40 + ## Programmatic access 41 + 42 + For scripts or applications that need to authenticate programmatically, you'll need to implement the AT Protocol OAuth flow against your AIP instance. This involves: 43 + 44 + 1. Registering an OAuth client with AIP 45 + 2. Redirecting the user to AIP's authorization endpoint 46 + 3. Exchanging the authorization code for an access token 47 + 4. Using that token with HappyView 48 + 49 + See the [AIP documentation](https://github.com/graze-social/aip) for endpoint details and the [ATProto OAuth spec](https://atproto.com/specs/oauth) for the full protocol. 50 + 51 + ## How token validation works 52 + 53 + When HappyView receives an authenticated request, it forwards the token to AIP's `/oauth/userinfo` endpoint. AIP responds with the user's DID, which HappyView uses to: 54 + 55 + - Identify who is making the request 56 + - Proxy writes to the correct PDS 57 + - Check admin permissions (for admin endpoints) 58 + 59 + Token validation happens on every request; there is no local token caching. 60 + 61 + ## Admin access 62 + 63 + Admin endpoints require the authenticated user's DID to exist in the `admins` table. If the table is empty (fresh deployment), the first authenticated request to any admin endpoint auto-bootstraps that user as the initial admin. 64 + 65 + To add more admins, use `POST /admin/admins` or the [dashboard](dashboard). See [Admin API](../reference/admin-api#admin-management) for details.
+33
docs/getting-started/configuration.md
··· 1 + # Configuration 2 + 3 + HappyView is configured via environment variables. A `.env` file in the project root is loaded automatically on startup. See [Deployment](deployment/docker) for local setup or [Production Deployment](../reference/production-deployment) for production setup. 4 + 5 + ## Environment variables 6 + 7 + | Variable | Required | Default | Description | 8 + |----------|----------|---------|-------------| 9 + | `DATABASE_URL` | yes | --- | Postgres connection string | 10 + | `AIP_URL` | yes | --- | [AIP](https://github.com/graze-social/aip) instance URL for OAuth token validation | 11 + | `HOST` | no | `0.0.0.0` | Bind host | 12 + | `PORT` | no | `3000` | Bind port | 13 + | `TAP_URL` | no | `http://localhost:2480` | [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) instance URL for real-time record streaming and backfill | 14 + | `TAP_ADMIN_PASSWORD` | no | --- | Shared secret for authenticating with Tap's admin endpoints | 15 + | `RELAY_URL` | no | `https://bsky.network` | Relay URL for [backfill](../guides/backfill) repo discovery | 16 + | `PLC_URL` | no | `https://plc.directory` | [PLC directory](https://github.com/did-method-plc/did-method-plc) URL for DID resolution | 17 + | `RUST_LOG` | no | `happyview=debug,tower_http=debug` | Log filter (uses `tracing_subscriber::EnvFilter`) | 18 + 19 + ## Example `.env` 20 + 21 + ```sh 22 + DATABASE_URL=postgres://happyview:happyview@localhost/happyview 23 + AIP_URL=http://localhost:8080 24 + 25 + # Optional overrides 26 + # HOST=0.0.0.0 27 + # PORT=3000 28 + # TAP_URL=http://localhost:2480 29 + # TAP_ADMIN_PASSWORD=your-secret-here 30 + # RELAY_URL=https://bsky.network 31 + # PLC_URL=https://plc.directory 32 + # RUST_LOG=happyview=debug,tower_http=debug 33 + ```
+38
docs/getting-started/dashboard.md
··· 1 + # Dashboard 2 + 3 + HappyView ships with a web dashboard that provides a visual interface for everything the [admin API](../reference/admin-api) offers: managing lexicons, viewing indexed records, and monitoring backfill jobs. It runs as a separate Next.js application alongside the Rust backend. 4 + 5 + ## Logging in for the first time 6 + 7 + The dashboard uses AT Protocol OAuth via AIP. If no admins exist in the database yet, the first authenticated request to any admin endpoint automatically bootstraps that user as an admin. 8 + 9 + ## Adding a lexicon 10 + 11 + Navigate to **Lexicons > Add Lexicon** and choose **Local** or **Network**. 12 + 13 + **Local** lexicons are defined by you. The editor shows two side-by-side panels (stacked on mobile): 14 + 15 + - **Lexicon JSON** (left): define your lexicon schema 16 + - **Lua Script** (right): write the handler for query/procedure types 17 + 18 + The Lua panel only appears when the lexicon's `defs.main.type` is `query` or `procedure`. For record-type lexicons, only the JSON panel is shown. 19 + 20 + A default Lua script is auto-generated when you first set the type to query or procedure. The template updates automatically when the type changes, but once you manually edit the script your changes are preserved. 21 + 22 + Toggle **Enable backfill** to index historical records when uploading a record-type lexicon. 23 + 24 + **Network** lexicons are fetched from the AT Protocol network. Enter an NSID (e.g. `xyz.statusphere.status`) and HappyView resolves the schema automatically. If found, the lexicon JSON is displayed in a read-only editor. Click **Add** to track it. Network lexicons are kept up to date via Tap. See [Lexicons - Network lexicons](../guides/lexicons#network-lexicons) for how resolution works. 25 + 26 + ### JSON editor 27 + 28 + The JSON editor provides real-time validation against the AT Protocol Lexicon v1 schema: 29 + 30 + - Validation for Lexicon format 31 + - Auto-complete for definition types (`record`, `query`, `procedure`, `subscription`), property types (`string`, `integer`, `boolean`, `ref`, `union`, `blob`, `cid-link`, etc.), and schema structure (`defs`, `main`, `properties`, `required`) 32 + - Enforces the required top-level shape: `lexicon`, `id`, and `defs.main` 33 + 34 + ### Lua editor 35 + 36 + The Lua editor provides context-aware code completions, including suggestions for the `Record`, `db`, `input`, and `params` APIs as well as Lua keywords, builtins, and standard library functions. It also offers snippet templates for common constructs like `if`, `for`, and `function`. 37 + 38 + See [Lua Scripting](../guides/scripting) for the full runtime reference and examples.
+49
docs/getting-started/deployment/docker.md
··· 1 + # Local Development with Docker 2 + 3 + This guide runs the full HappyView stack locally using Docker Compose: Postgres, [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap), HappyView, and the web dashboard. 4 + 5 + ## Prerequisites 6 + 7 + - [Docker](https://docs.docker.com/get-docker/) and Docker Compose 8 + - An [AIP](https://github.com/graze-social/aip) instance for OAuth. The Docker Compose config points at the public AIP instance at `aip.gamesgamesgamesgames.games` by default. 9 + 10 + :::warning 11 + This public AIP instance is provided for development convenience only. Production deployments should run their own AIP instance or risk being blocked. See the [AIP documentation](https://github.com/graze-social/aip) for setup. 12 + ::: 13 + 14 + ## 1. Clone and configure 15 + 16 + ```sh 17 + git clone https://github.com/graze-social/happyview.git 18 + cd happyview 19 + cp .env.example .env 20 + ``` 21 + 22 + Set `TAP_ADMIN_PASSWORD` in your `.env`. This shared secret is used by both Tap and HappyView: 23 + 24 + ```sh 25 + TAP_ADMIN_PASSWORD=your-secret-here 26 + ``` 27 + 28 + The `docker-compose.yml` configures everything else (database URLs, service connections) automatically. 29 + 30 + ## 2. Start the stack 31 + 32 + ```sh 33 + docker compose up 34 + ``` 35 + 36 + This starts: 37 + 38 + | Service | Port | Description | 39 + | ------------- | ---- | ---------------------------------------------------- | 40 + | **postgres** | 5432 | PostgreSQL 17 (databases for both HappyView and Tap) | 41 + | **tap** | 2480 | Firehose consumer, backfill worker | 42 + | **happyview** | 3000 | HappyView API server | 43 + | **web** | 3001 | Next.js dashboard | 44 + 45 + HappyView runs migrations automatically on startup. The first build will take a few minutes while Rust compiles. 46 + 47 + ## Next steps 48 + 49 + Your HappyView stack is running. Follow the [Statusphere tutorial](../../tutorials/statusphere) to upload lexicons, add custom query logic, and start indexing records from the network.
+55
docs/getting-started/deployment/other.md
··· 1 + # Local Development from Source 2 + 3 + This guide runs HappyView directly with `cargo run`, with you managing Postgres, AIP, and Tap separately. If you'd rather use Docker Compose to run everything together, see [Local Development with Docker](docker). 4 + 5 + ## Prerequisites 6 + 7 + - Rust (stable) 8 + - PostgreSQL 17+ 9 + - A running [AIP](https://github.com/graze-social/aip) instance (handles OAuth). See the [AIP documentation](https://github.com/graze-social/aip) for setup. 10 + - A running [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) instance (delivers real-time records and handles backfill). See the [Tap documentation](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) for setup. 11 + 12 + ## 1. Clone and configure 13 + 14 + ```sh 15 + git clone https://github.com/graze-social/happyview.git 16 + cd happyview 17 + cp .env.example .env 18 + ``` 19 + 20 + Edit `.env` to point at your running services: 21 + 22 + ```sh 23 + DATABASE_URL=postgres://happyview:happyview@localhost/happyview 24 + AIP_URL=http://localhost:8080 25 + TAP_URL=http://localhost:2480 26 + TAP_ADMIN_PASSWORD=your-secret-here 27 + ``` 28 + 29 + See [Configuration](../configuration) for all available variables. 30 + 31 + ## 2. Create the database 32 + 33 + ```sh 34 + createdb happyview 35 + ``` 36 + 37 + Or if using a Postgres user with a password: 38 + 39 + ```sh 40 + psql -c "CREATE DATABASE happyview;" -U postgres 41 + ``` 42 + 43 + HappyView runs migrations automatically on startup, so no manual migration step is needed. 44 + 45 + ## 3. Start HappyView 46 + 47 + ```sh 48 + cargo run 49 + ``` 50 + 51 + HappyView starts on port 3000 (configurable via the `PORT` environment variable). 52 + 53 + ## Next steps 54 + 55 + Your HappyView instance is running. Follow the [Statusphere tutorial](../../tutorials/statusphere) to upload lexicons, add custom query logic, and start indexing records from the network.
+20
docs/getting-started/deployment/railway.md
··· 1 + # Deploy on Railway 2 + 3 + The fastest way to get HappyView running is with Railway. This template deploys HappyView, [AIP](https://github.com/graze-social/aip) (OAuth provider), [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) (real-time data and backfill), and Postgres with a single click: 4 + 5 + [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/I1jvZl?referralCode=0QOgj_) 6 + 7 + ## Required configuration 8 + 9 + After deploying the template, you'll need to configure a few things before the stack works properly: 10 + 11 + 1. **Set your admin DID.** In the AIP service variables, set `ADMIN_DIDS` to your AT Protocol DID (e.g. `did:plc:abc123...`). You can find your DID by looking up your handle on [Internect](https://internect.info/). 12 + 13 + 2. **Generate AIP signing keys.** The `OAUTH_SIGNING_KEYS` and `ATPROTO_OAUTH_SIGNING_KEYS` variables require multibase-encoded P-256 private keys. See the [AIP Signing Keys documentation](https://github.com/graze-social/aip/blob/main/CONFIGURATION.md#signing-keys) for generation instructions. 14 + 15 + 3. **Assign public domains.** In the Railway dashboard, add a public domain to both the HappyView and AIP services. The services need publicly accessible URLs to handle OAuth callbacks and XRPC requests. 16 + :::note 17 + Your instances can use custom domains or Railway's generated URLs with no additional configuration. The domains are injected automatically to the containers. 18 + ::: 19 + 20 + 4. Access your HappyView dashboard at the instance's public URL.
+62
docs/getting-started/quickstart.md
··· 1 + # Quickstart 2 + 3 + This page walks you through the fastest path to a working HappyView instance. By the end, you'll have an AppView that indexes records from the AT Protocol network and serves XRPC endpoints. 4 + 5 + ## 1. Deploy HappyView 6 + 7 + Pick whichever option fits your situation: 8 + 9 + | Option | Best for | 10 + |--------|----------| 11 + | [**Railway**](deployment/railway) | Fastest path — one-click deploy of the full stack (HappyView + AIP + Tap + Postgres) | 12 + | [**Docker Compose**](deployment/docker) | Local development with the full stack running in containers | 13 + | [**From source**](deployment/other) | Running HappyView with `cargo run` and managing dependencies yourself | 14 + 15 + If you're just trying HappyView for the first time, start with Railway. 16 + 17 + ## 2. Log in to the dashboard 18 + 19 + Open your HappyView instance in a browser. The built-in [dashboard](dashboard) is served at the root URL. 20 + 21 + Click **Log in** and authenticate with your AT Protocol identity. On a fresh deployment with no admins configured, the first authenticated request to any admin endpoint automatically bootstraps that user as an admin. 22 + 23 + ## 3. Add your first lexicon 24 + 25 + Lexicons tell HappyView what data to index and what endpoints to serve. The quickest way to get started is to add one from the network: 26 + 27 + 1. In the dashboard, go to **Lexicons > Add Lexicon > Network** 28 + 2. Enter an NSID (e.g. `xyz.statusphere.status`) 29 + 3. HappyView resolves the schema from the AT Protocol network and shows a preview 30 + 4. Click **Add** 31 + 32 + HappyView immediately starts indexing records for that collection. A backfill job is created to fetch historical records, and new records stream in via Tap in real time. 33 + 34 + You can also upload lexicons manually via the dashboard or the [admin API](../reference/admin-api). See [Lexicons](../guides/lexicons) for the full details. 35 + 36 + ## 4. Verify records are being indexed 37 + 38 + Go to the **Dashboard** home page. The stat cards show the total record count and a breakdown by collection. You can also browse indexed records on the **Records** page. 39 + 40 + To check backfill progress, go to the **Backfill** page. The Tap stats cards show how many repos and records Tap has processed. 41 + 42 + ## 5. Query your data 43 + 44 + Once you have a record lexicon indexed, add a query lexicon to expose a read endpoint. Go to **Lexicons > Add Lexicon > Local** and create a query lexicon with `target_collection` set to your record collection's NSID. 45 + 46 + Without a Lua script, HappyView generates a default query endpoint that supports `limit`, `cursor`, `did`, and `uri` parameters: 47 + 48 + ``` 49 + GET /xrpc/xyz.statusphere.listStatuses?limit=5 50 + ``` 51 + 52 + For custom query logic, attach a [Lua script](../guides/scripting). 53 + 54 + ## Next steps 55 + 56 + You now have a working AppView. Here's where to go from here: 57 + 58 + - [**Statusphere tutorial**](../tutorials/statusphere): end-to-end walkthrough building a complete AppView with record, query, and procedure lexicons 59 + - [**Lexicons guide**](../guides/lexicons): target collections, backfill flag, network lexicons 60 + - [**Lua Scripting**](../guides/scripting): custom query and procedure logic 61 + - [**Configuration**](configuration): environment variables and tuning 62 + - [**Authentication**](authentication): how OAuth works and how to get API tokens
+33
docs/guides/backfill.md
··· 1 + # Backfill 2 + 3 + When you add a new record-type lexicon, HappyView starts indexing new records from that moment via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap). But what about records that already exist on the network? That's what backfill does: HappyView discovers repos via the relay and delegates the actual record fetching to Tap. 4 + 5 + ## When backfill runs 6 + 7 + - **Automatically** when a record-type lexicon is uploaded with `backfill: true` (the default). See [Lexicons - Backfill flag](lexicons#backfill-flag). 8 + - **Manually** via `POST /admin/backfill` or the [dashboard](../getting-started/dashboard). You can scope a manual backfill to a specific collection, a specific DID, or both. 9 + 10 + See the [admin API](../reference/admin-api#backfill) for endpoint details. 11 + 12 + ## How it works 13 + 14 + 1. **Determine target collections**: uses the specified collection, or all record lexicons with `backfill: true` 15 + 2. **Discover DIDs**: HappyView calls the relay's `com.atproto.sync.listReposByCollection` to find repos that contain records for each target collection (paginated, 1000 per page) 16 + 3. **Delegate to Tap**: HappyView sends discovered DIDs to Tap in batches of 1000 via its `/repos/add` endpoint 17 + 4. **Tap fetches records**: Tap handles the actual record fetching from each user's PDS and delivers them to HappyView via the WebSocket channel 18 + 19 + ## Job lifecycle 20 + 21 + HappyView marks a backfill job as "completed" once it finishes discovering repos and handing DIDs off to Tap (steps 1-3). This does **not** mean Tap has finished processing all the records. Tap works through them asynchronously after the handoff. 22 + 23 + To see whether Tap is still working through the backlog, check the Tap stats on the dashboard's Backfill page or via `GET /admin/tap/stats`. The **outbox buffer** indicates how many events are still queued for delivery; a high number means Tap is actively processing. 24 + 25 + ## Re-running backfills 26 + 27 + Backfill jobs are idempotent. Running a backfill for a collection that's already been backfilled will re-discover repos and send them to Tap again. Tap deduplicates repos it already knows about, so re-running a backfill is safe and useful for catching repos that were added to the network since the last run. 28 + 29 + ## Next steps 30 + 31 + - [Lexicons](lexicons#backfill-flag): Control whether lexicons trigger backfill on upload 32 + - [Admin API](../reference/admin-api#backfill): Full reference for backfill endpoints 33 + - [Admin API - Tap Stats](../reference/admin-api#tap-stats): Monitor Tap's processing progress
+82
docs/guides/lexicons.md
··· 1 + # Lexicons 2 + 3 + Lexicons are the core building block of HappyView. They're [AT Protocol schema definitions](https://atproto.com/specs/lexicon) that describe your data model, and HappyView uses them to decide which records to index from the network and what XRPC endpoints to serve. 4 + 5 + You don't write route handlers or database queries; you upload a lexicon and HappyView generates the infrastructure from it. There are two ways to add lexicons: uploading them via the [admin API](../reference/admin-api#lexicons) or [dashboard](../getting-started/dashboard), or fetching them directly from the AT Protocol network via [DNS authority resolution](#network-lexicons). 6 + 7 + ## Supported lexicon types 8 + 9 + | Type | Effect | 10 + | ------------- | ------------------------------------------------------------------------------ | 11 + | `record` | Syncs the collection filter to Tap and indexes records into Postgres | 12 + | `query` | Registers a `GET /xrpc/{nsid}` endpoint that queries indexed records | 13 + | `procedure` | Registers a `POST /xrpc/{nsid}` endpoint that proxies writes to the user's PDS | 14 + | `definitions` | Stored but does not generate routes or subscriptions | 15 + 16 + A typical setup has three lexicons working together: a **record** lexicon that defines the data and triggers indexing, a **query** lexicon that exposes a read endpoint, and a **procedure** lexicon that exposes a write endpoint. The [Statusphere tutorial](../tutorials/statusphere) walks through this pattern end-to-end. 17 + 18 + ## Target collection 19 + 20 + Query and procedure lexicons don't store data themselves. They operate on records stored by a record-type lexicon. The `target_collection` field tells HappyView which record collection to read from or write to. Without it, default queries and procedures won't know which DB records to operate on. 21 + 22 + For example, a query lexicon `xyz.statusphere.listStatuses` would set `target_collection` to `xyz.statusphere.status` to read from that record collection. 23 + 24 + See the [admin API](../reference/admin-api#upload--upsert-a-lexicon) for how to set `target_collection` when uploading. 25 + 26 + :::note 27 + The `target_collection` is available in Lua scripts as the `collection` global, but it is not required if your endpoint uses a Lua script. 28 + ::: 29 + 30 + ## Backfill flag 31 + 32 + When uploading a record-type lexicon, HappyView automatically creates a backfill job to discover existing records. If you only want to index new records going forward, you can set `backfill` to `false`. 33 + 34 + ## Tap collection filters 35 + 36 + When record-type lexicons change (uploaded or deleted), HappyView automatically syncs the updated collection filter to Tap. HappyView always includes `com.atproto.lexicon.schema` in the filter to track network lexicon updates. 37 + 38 + ## Network lexicons 39 + 40 + If a lexicon has already been published, you don't need to upload the JSON manually. Point HappyView at the NSID and it fetches the lexicon directly from the network. Network lexicons are kept updated automatically via Tap. If the publisher updates their schema, your instance will pick up the change. 41 + 42 + ### NSID authority resolution 43 + 44 + Lexicons are stored as records themselves with the `com.atproto.lexicon.schema` NSID and the rkey set to the lexicon's NSID. To find which repo holds a lexicon, HappyView resolves the NSID's authority: 45 + 46 + 1. Extract the authority from the NSID (all segments except the last). For example, `xyz.statusphere.status` has authority `xyz.statusphere`. 47 + 2. Reverse the authority segments to form a domain: `statusphere.xyz`. 48 + 3. Look up the DNS TXT record at `_lexicon.{domain}` (e.g. `_lexicon.statusphere.xyz`). 49 + 4. Parse the TXT record for a `did=<DID>` value. 50 + 5. Resolve the DID to a PDS endpoint via the PLC directory. 51 + 52 + :::note 53 + The spec states that resolution must be **non-hierarchical**. Each authority requires its own explicit TXT record. If you have multiple levels of authority (e.g. `xyz.statusphere.status` and `xyz.statusphere.actor.profile`), each level must have an explicit TXT record. 54 + ::: 55 + 56 + ### Fetching 57 + 58 + Once the authority DID and PDS endpoint are known, HappyView calls `com.atproto.repo.getRecord` with: 59 + 60 + - `repo` = the authority DID 61 + - `collection` = `com.atproto.lexicon.schema` 62 + - `rkey` = the NSID 63 + 64 + The `value` field of the response is the raw lexicon JSON. 65 + 66 + ### Live updates via Tap 67 + 68 + Tap always subscribes to `com.atproto.lexicon.schema` alongside the dynamic record collections. When a record event arrives: 69 + 70 + - **create/update**: If the event's DID and rkey match a tracked network lexicon (`authority_did` and `nsid`), the lexicon is parsed, upserted into the `lexicons` table and in-memory registry, and collection filters are updated if it's a record type. 71 + - **delete**: The lexicon is removed from the `lexicons` table and registry. 72 + 73 + ### Startup re-fetch 74 + 75 + On every startup, HappyView re-fetches all network lexicons from their respective PDSes. This ensures consistency even if events were missed while offline. Failures are logged as warnings but don't block startup. 76 + 77 + ## Next steps 78 + 79 + - [Lua Scripting](scripting): Add custom query and procedure logic to your endpoints 80 + - [XRPC API](../reference/xrpc-api): Understand how the generated endpoints behave 81 + - [Backfill](backfill): Learn how historical records are indexed 82 + - [Admin API](../reference/admin-api): Full reference for lexicon management endpoints
+314
docs/guides/scripting.md
··· 1 + # Lua Scripting 2 + 3 + Without Lua scripts, HappyView's query endpoints return raw records and procedure endpoints proxy simple creates and updates. Lua scripts let you go much further: 4 + 5 + - Add filtering logic 6 + - Transform responses 7 + - Validate input 8 + - Compose multi-record operations 9 + - Build entirely custom behavior 10 + 11 + Scripts are attached to query and procedure lexicons and run in a sandboxed Lua VM with access to the [Record API](#record-api), a [read-only database API](#database-api), and a set of [context globals](#context-globals). 12 + 13 + ## Script structure 14 + 15 + Every script must define a `handle()` function. HappyView calls it when the XRPC endpoint is hit and returns its result as JSON to the client. 16 + 17 + ```lua 18 + function handle() 19 + -- your logic here 20 + return { key = "value" } 21 + end 22 + ``` 23 + 24 + You can define helper functions and variables outside `handle()`. They're evaluated once when the script loads, then `handle()` is called per request. 25 + 26 + ## Sandbox 27 + 28 + Scripts run in a restricted environment. The following standard Lua modules are **removed** and unavailable: 29 + 30 + `os`, `io`, `debug`, `package`, `require`, `dofile`, `loadfile`, `load`, `collectgarbage` 31 + 32 + An instruction limit of 1,000,000 prevents infinite loops. Exceeding it terminates the script with an error. 33 + 34 + ## Context globals 35 + 36 + These globals are set automatically before `handle()` is called. 37 + 38 + ### Procedure globals 39 + 40 + | Global | Type | Description | 41 + | ------------ | ------ | ------------------------------------------------------- | 42 + | `method` | string | The XRPC method name (e.g. `xyz.statusphere.setStatus`) | 43 + | `input` | table | Parsed JSON request body | 44 + | `caller_did` | string | DID of the authenticated user | 45 + | `collection` | string | Target collection NSID | 46 + 47 + ### Query globals 48 + 49 + | Global | Type | Description | 50 + | ------------ | ------ | ------------------------------------------------ | 51 + | `method` | string | The XRPC method name | 52 + | `params` | table | Query string parameters (all values are strings) | 53 + | `collection` | string | Target collection NSID | 54 + 55 + Queries are unauthenticated: there is no `caller_did` or `input`. 56 + 57 + ## Utility globals 58 + 59 + Available in both queries and procedures: 60 + 61 + | Function | Returns | Description | 62 + | -------------- | ------- | ------------------------------------------------------------------- | 63 + | `now()` | string | Current UTC timestamp in ISO 8601 format | 64 + | `log(message)` | — | Log a message (appears in server logs at debug level) | 65 + | `TID()` | string | Generate a fresh AT Protocol TID (13-character sortable identifier) | 66 + 67 + ## Record API 68 + 69 + The `Record` API is only available in **procedure** scripts. It handles creating, updating, loading, and deleting AT Protocol records. Writes are proxied to the caller's PDS and indexed locally. 70 + 71 + ### Constructor 72 + 73 + ```lua 74 + local r = Record("xyz.statusphere.status", { status = "\ud83d\ude0a", createdAt = now() }) 75 + ``` 76 + 77 + Creates a new record instance for the given collection. The optional second argument sets initial field values. The record's `_key_type` is automatically set from the lexicon's `key` definition. Default values from the schema are populated for any missing fields. 78 + 79 + ### Static methods 80 + 81 + ```lua 82 + -- Save multiple records in parallel 83 + Record.save_all({ record1, record2, record3 }) 84 + 85 + -- Load a record from the local database by AT URI 86 + local r = Record.load("at://did:plc:abc/xyz.statusphere.status/abc123") 87 + -- Returns nil if not found 88 + 89 + -- Load multiple records in parallel 90 + local records = Record.load_all({ uri1, uri2 }) 91 + -- Returns nil entries for URIs not found 92 + ``` 93 + 94 + ### Instance methods 95 + 96 + ```lua 97 + -- Save (creates or updates depending on whether _uri is set) 98 + r:save() 99 + 100 + -- Delete from PDS and local database 101 + r:delete() 102 + 103 + -- Set the record key type (tid, any, nsid, or literal:*) 104 + r:set_key_type("tid") 105 + 106 + -- Set a specific record key 107 + r:set_rkey("my-key") 108 + 109 + -- Auto-generate a record key based on _key_type 110 + local key = r:generate_rkey() 111 + ``` 112 + 113 + **Key type behavior for `generate_rkey()`:** 114 + 115 + | Key type | Generated rkey | 116 + | --------------- | --------------------------------- | 117 + | `tid` | Sortable timestamp-based ID | 118 + | `any` | Same as `tid` | 119 + | `literal:value` | The literal value after the colon | 120 + | `nsid` | Error — use `set_rkey()` instead | 121 + 122 + ### Instance fields 123 + 124 + These fields are set automatically and are read-only (writes raise an error): 125 + 126 + | Field | Type | Description | 127 + | ------------- | ------- | ----------------------------------------------------------- | 128 + | `_uri` | string? | AT URI — set after `save()`, cleared after `delete()` | 129 + | `_cid` | string? | Content hash — set after `save()`, cleared after `delete()` | 130 + | `_key_type` | string? | Record key type from the lexicon definition | 131 + | `_rkey` | string? | Record key — set via `set_rkey()` or `generate_rkey()` | 132 + | `_collection` | string | Collection NSID (always set) | 133 + | `_schema` | table? | Schema definition from the lexicon (used for validation) | 134 + 135 + ### Schema validation 136 + 137 + When a record has a schema (loaded from the lexicon): 138 + 139 + - **On save:** required fields are checked, and missing required fields raise an error 140 + - **On construction:** default values from schema properties are auto-populated 141 + - **On save:** only fields defined in the schema's `properties` are sent to the PDS 142 + 143 + ### Save behavior 144 + 145 + `r:save()` auto-detects create vs update: 146 + 147 + - If `_uri` is nil → calls `createRecord` on the PDS 148 + - If `_uri` is set → calls `putRecord` on the PDS 149 + 150 + After a successful save, `_uri` and `_cid` are updated on the record instance. 151 + 152 + ## Database API 153 + 154 + The `db` table provides read-only access to indexed records. Available in both queries and procedures. 155 + 156 + ### db.query 157 + 158 + ```lua 159 + local result = db.query({ 160 + collection = "xyz.statusphere.status", -- required 161 + did = "did:plc:abc", -- optional: filter by DID 162 + limit = 20, -- optional: max 100, default 20 163 + offset = 0, -- optional: for pagination 164 + }) 165 + 166 + -- result.records — array of record tables (each includes a "uri" field) 167 + -- result.cursor — present when more records exist 168 + ``` 169 + 170 + ### db.get 171 + 172 + ```lua 173 + local record = db.get("at://did:plc:abc/xyz.statusphere.status/abc123") 174 + -- Returns the record table or nil 175 + -- The returned table includes a "uri" field 176 + ``` 177 + 178 + ### db.count 179 + 180 + ```lua 181 + local n = db.count("xyz.statusphere.status") 182 + local n = db.count("xyz.statusphere.status", "did:plc:abc") -- filter by DID 183 + ``` 184 + 185 + ## Standard libraries 186 + 187 + The following Lua 5.4 standard library modules are available: 188 + 189 + <details> 190 + <summary> 191 + `string` 192 + </summary> 193 + - [`byte`](https://lua.org/manual/5.4/manual.html#pdf-string.byte) 194 + - [`char`](https://lua.org/manual/5.4/manual.html#pdf-string.char) 195 + - [`find`](https://lua.org/manual/5.4/manual.html#pdf-string.find) 196 + - [`format`](https://lua.org/manual/5.4/manual.html#pdf-string.format) 197 + - [`gmatch`](https://lua.org/manual/5.4/manual.html#pdf-string.gmatch) 198 + - [`gsub`](https://lua.org/manual/5.4/manual.html#pdf-string.gsub) 199 + - [`len`](https://lua.org/manual/5.4/manual.html#pdf-string.len) 200 + - [`lower`](https://lua.org/manual/5.4/manual.html#pdf-string.lower) 201 + - [`match`](https://lua.org/manual/5.4/manual.html#pdf-string.match) 202 + - [`rep`](https://lua.org/manual/5.4/manual.html#pdf-string.rep) 203 + - [`reverse`](https://lua.org/manual/5.4/manual.html#pdf-string.reverse) 204 + - [`sub`](https://lua.org/manual/5.4/manual.html#pdf-string.sub) 205 + - [`upper`](https://lua.org/manual/5.4/manual.html#pdf-string.upper) 206 + </details> 207 + 208 + <details> 209 + <summary> 210 + `table` 211 + </summary> 212 + - [`concat`](https://lua.org/manual/5.4/manual.html#pdf-table.concat) 213 + - [`insert`](https://lua.org/manual/5.4/manual.html#pdf-table.insert) 214 + - [`remove`](https://lua.org/manual/5.4/manual.html#pdf-table.remove) 215 + - [`sort`](https://lua.org/manual/5.4/manual.html#pdf-table.sort) 216 + - [`unpack`](https://lua.org/manual/5.4/manual.html#pdf-table.unpack) 217 + </details> 218 + 219 + <details> 220 + <summary> 221 + `math` 222 + </summary> 223 + - [`abs`](https://lua.org/manual/5.4/manual.html#pdf-math.abs) 224 + - [`ceil`](https://lua.org/manual/5.4/manual.html#pdf-math.ceil) 225 + - [`floor`](https://lua.org/manual/5.4/manual.html#pdf-math.floor) 226 + - [`max`](https://lua.org/manual/5.4/manual.html#pdf-math.max) 227 + - [`min`](https://lua.org/manual/5.4/manual.html#pdf-math.min) 228 + - [`random`](https://lua.org/manual/5.4/manual.html#pdf-math.random) 229 + - [`sqrt`](https://lua.org/manual/5.4/manual.html#pdf-math.sqrt) 230 + - [`huge`](https://lua.org/manual/5.4/manual.html#pdf-math.huge) 231 + - [`pi`](https://lua.org/manual/5.4/manual.html#pdf-math.pi) 232 + </details> 233 + 234 + <details> 235 + <summary> 236 + Standard builtins 237 + </summary> 238 + - [`print`](https://lua.org/manual/5.4/manual.html#pdf-print) 239 + - [`tostring`](https://lua.org/manual/5.4/manual.html#pdf-tostring) 240 + - [`tonumber`](https://lua.org/manual/5.4/manual.html#pdf-tonumber) 241 + - [`type`](https://lua.org/manual/5.4/manual.html#pdf-type) 242 + - [`pairs`](https://lua.org/manual/5.4/manual.html#pdf-pairs) 243 + - [`ipairs`](https://lua.org/manual/5.4/manual.html#pdf-ipairs) 244 + - [`next`](https://lua.org/manual/5.4/manual.html#pdf-next) 245 + - [`select`](https://lua.org/manual/5.4/manual.html#pdf-select) 246 + - [`unpack`](https://lua.org/manual/5.4/manual.html#pdf-table.unpack) 247 + - [`error`](https://lua.org/manual/5.4/manual.html#pdf-error) 248 + - [`pcall`](https://lua.org/manual/5.4/manual.html#pdf-pcall) 249 + - [`xpcall`](https://lua.org/manual/5.4/manual.html#pdf-xpcall) 250 + - [`assert`](https://lua.org/manual/5.4/manual.html#pdf-assert) 251 + - [`setmetatable`](https://lua.org/manual/5.4/manual.html#pdf-setmetatable) 252 + - [`getmetatable`](https://lua.org/manual/5.4/manual.html#pdf-getmetatable) 253 + - [`rawget`](https://lua.org/manual/5.4/manual.html#pdf-rawget) 254 + - [`rawset`](https://lua.org/manual/5.4/manual.html#pdf-rawset) 255 + - [`rawequal`](https://lua.org/manual/5.4/manual.html#pdf-rawequal) 256 + </details> 257 + 258 + ## Debugging 259 + 260 + ### Logging 261 + 262 + Use `log()` to trace script execution. Output appears in the server logs at **debug** level with the field `lua_log`: 263 + 264 + ```lua 265 + function handle() 266 + log("handle called with params: " .. tostring(params.limit)) 267 + local result = db.query({ collection = collection, limit = params.limit }) 268 + log("query returned " .. #result.records .. " records") 269 + return result 270 + end 271 + ``` 272 + 273 + To see log output, make sure your `RUST_LOG` environment variable includes debug level for HappyView (the default `happyview=debug` works). See [Configuration](../getting-started/configuration). 274 + 275 + ### Error messages 276 + 277 + When a script fails, the client receives a generic `500` response: 278 + 279 + - `{"error": "script execution failed"}`: covers syntax errors, runtime errors, missing `handle()` function, and errors raised with `error()` 280 + - `{"error": "script exceeded execution time limit"}`: the script hit the 1,000,000 instruction limit 281 + 282 + The **full error message** is logged server-side at error level. Check the server logs to see the actual Lua error, including line numbers and stack traces. 283 + 284 + ### Common mistakes 285 + 286 + - **Missing `handle()` function**: Every script must define a global `handle()` function. If it's missing or misspelled, the script fails silently with "script execution failed". 287 + - **Calling `error()` for expected conditions**: Lua's `error()` triggers a 500 response. For expected conditions like "record not found", return a structured error response instead: `return { error = "not found" }`. 288 + - **Infinite loops**: The sandbox enforces a 1,000,000 instruction limit. If your script processes large data sets, paginate with `db.query()` limits instead of loading everything at once. 289 + - **Forgetting `params` values are strings**: All query string parameters arrive as strings. Use `tonumber(params.limit)` if you need a number. 290 + 291 + ## Example scripts 292 + 293 + See the example script references for complete, ready-to-use scripts: 294 + 295 + **Queries:** 296 + - [Get a record](../reference/scripts/get-record) — fetch a single record by AT URI 297 + - [Paginated list](../reference/scripts/paginated-list) — list records with cursor-based pagination and DID filtering 298 + - [List or fetch](../reference/scripts/list-or-fetch) — combined single-record lookup and paginated listing 299 + - [Expanded query](../reference/scripts/expanded-query) — list statuses with user profiles in a single response 300 + 301 + **Procedures:** 302 + - [Create a record](../reference/scripts/create-record) — simple write that saves input as a record 303 + - [Upsert a record](../reference/scripts/upsert-record) — create or update using a deterministic rkey 304 + - [Update or delete](../reference/scripts/update-or-delete) — single endpoint handling create, update, and delete 305 + - [Batch save](../reference/scripts/batch-save) — create multiple records in parallel with `Record.save_all()` 306 + - [Sidecar records](../reference/scripts/sidecar-records) — create linked records across collections with a shared rkey 307 + - [Cascading delete](../reference/scripts/cascading-delete) — delete a record and all related records 308 + - [Complex mutations](../reference/scripts/complex-mutations) — load, transform, and save a record with multiple field changes 309 + 310 + ## Next steps 311 + 312 + - [Lexicons](lexicons): Understand how record, query, and procedure lexicons work together 313 + - [XRPC API](../reference/xrpc-api): See how endpoints behave with and without Lua scripts 314 + - [Dashboard](../getting-started/dashboard#lua-editor): Use the web editor with context-aware completions
-115
docs/lexicons.md
··· 1 - # Lexicons 2 - 3 - Lexicons are ATProto schema definitions that tell HappyView which records to index and what XRPC endpoints to serve. HappyView supports uploading lexicons at runtime via the admin API. 4 - 5 - ## Supported lexicon types 6 - 7 - | Type | Effect | 8 - |------|--------| 9 - | `record` | Subscribes to Jetstream for that collection and indexes records into Postgres | 10 - | `query` | Registers a `GET /xrpc/{nsid}` endpoint that queries indexed records | 11 - | `procedure` | Registers a `POST /xrpc/{nsid}` endpoint that proxies writes to the user's PDS | 12 - | `definitions` | Stored but does not generate routes or subscriptions | 13 - 14 - ## Uploading a lexicon 15 - 16 - ```sh 17 - curl -X POST http://localhost:3000/admin/lexicons \ 18 - -H "Authorization: Bearer $TOKEN" \ 19 - -H "Content-Type: application/json" \ 20 - -d '{ 21 - "lexicon_json": { 22 - "lexicon": 1, 23 - "id": "games.gamesgamesgamesgames.game", 24 - "defs": { 25 - "main": { 26 - "type": "record", 27 - "key": "tid", 28 - "record": { 29 - "type": "object", 30 - "properties": { 31 - "title": { "type": "string" } 32 - } 33 - } 34 - } 35 - } 36 - }, 37 - "backfill": true 38 - }' 39 - ``` 40 - 41 - Re-uploading the same lexicon ID increments its revision number. 42 - 43 - ## The `target_collection` field 44 - 45 - Query and procedure lexicons need to know which record collection they operate on. Set `target_collection` to the NSID of the record lexicon: 46 - 47 - ```sh 48 - curl -X POST http://localhost:3000/admin/lexicons \ 49 - -H "Authorization: Bearer $TOKEN" \ 50 - -H "Content-Type: application/json" \ 51 - -d '{ 52 - "lexicon_json": { 53 - "lexicon": 1, 54 - "id": "games.gamesgamesgamesgames.listGames", 55 - "defs": { 56 - "main": { 57 - "type": "query", 58 - "parameters": { 59 - "type": "params", 60 - "properties": { 61 - "limit": { "type": "integer" } 62 - } 63 - }, 64 - "output": { "encoding": "application/json" } 65 - } 66 - } 67 - }, 68 - "target_collection": "games.gamesgamesgamesgames.game" 69 - }' 70 - ``` 71 - 72 - Without `target_collection`, queries and procedures won't know which DB records to read from. 73 - 74 - ## The `backfill` flag 75 - 76 - When `backfill` is `true` (the default), uploading a record-type lexicon triggers a backfill job that discovers existing repos via the relay and fetches historical records from their PDSes. 77 - 78 - Set `backfill: false` if you only want to index new records going forward. 79 - 80 - ## Jetstream collection filters 81 - 82 - When record-type lexicons change (uploaded or deleted), HappyView automatically reconnects to Jetstream with an updated collection filter. If no record lexicons exist, the Jetstream listener idles without connecting. 83 - 84 - ## Example: full setup 85 - 86 - Upload the record lexicon, then a query and a procedure that target it: 87 - 88 - ```sh 89 - # 1. Record lexicon (triggers Jetstream subscription + backfill) 90 - curl -X POST http://localhost:3000/admin/lexicons \ 91 - -H "Authorization: Bearer $TOKEN" \ 92 - -H "Content-Type: application/json" \ 93 - -d '{ 94 - "lexicon_json": { "lexicon": 1, "id": "games.gamesgamesgamesgames.game", "defs": { "main": { "type": "record", "key": "tid", "record": { "type": "object", "properties": { "title": { "type": "string" } } } } } }, 95 - "backfill": true 96 - }' 97 - 98 - # 2. Query lexicon 99 - curl -X POST http://localhost:3000/admin/lexicons \ 100 - -H "Authorization: Bearer $TOKEN" \ 101 - -H "Content-Type: application/json" \ 102 - -d '{ 103 - "lexicon_json": { "lexicon": 1, "id": "games.gamesgamesgamesgames.listGames", "defs": { "main": { "type": "query", "output": { "encoding": "application/json" } } } }, 104 - "target_collection": "games.gamesgamesgamesgames.game" 105 - }' 106 - 107 - # 3. Procedure lexicon 108 - curl -X POST http://localhost:3000/admin/lexicons \ 109 - -H "Authorization: Bearer $TOKEN" \ 110 - -H "Content-Type: application/json" \ 111 - -d '{ 112 - "lexicon_json": { "lexicon": 1, "id": "games.gamesgamesgamesgames.createGame", "defs": { "main": { "type": "procedure", "input": { "encoding": "application/json" }, "output": { "encoding": "application/json" } } } }, 113 - "target_collection": "games.gamesgamesgamesgames.game" 114 - }' 115 - ```
-57
docs/network-lexicons.md
··· 1 - # Network Lexicons 2 - 3 - Network lexicons are lexicon definitions that HappyView fetches directly from the ATProto network rather than being uploaded manually via the admin API. An admin specifies an NSID, HappyView resolves the authority's repo, fetches the lexicon record, and keeps it updated via Jetstream. 4 - 5 - ## How it works 6 - 7 - ### NSID authority resolution 8 - 9 - Lexicon records live in repos as `com.atproto.lexicon.schema` with the rkey set to the NSID. To find which repo holds a lexicon, HappyView resolves the NSID's authority: 10 - 11 - 1. Extract the authority from the NSID (all segments except the last). For example, `games.gamesgamesgamesgames.game` has authority `games.gamesgamesgamesgames`. 12 - 2. Reverse the authority segments to form a domain: `gamesgamesgamesgames.games`. 13 - 3. Look up the DNS TXT record at `_lexicon.{domain}` (e.g. `_lexicon.gamesgamesgamesgames.games`). 14 - 4. Parse the TXT record for a `did=<DID>` value. 15 - 5. Resolve the DID to a PDS endpoint via the PLC directory. 16 - 17 - Resolution is **non-hierarchical** --- each authority requires its own explicit TXT record. 18 - 19 - ### Fetching 20 - 21 - Once the authority DID and PDS endpoint are known, HappyView calls `com.atproto.repo.getRecord` with: 22 - - `repo` = the authority DID 23 - - `collection` = `com.atproto.lexicon.schema` 24 - - `rkey` = the NSID 25 - 26 - The `value` field of the response is the raw lexicon JSON. 27 - 28 - ### Live updates via Jetstream 29 - 30 - Jetstream always subscribes to `com.atproto.lexicon.schema` alongside the dynamic record collections. When a commit event arrives: 31 - 32 - - **create/update**: If the event's DID and rkey match a tracked network lexicon (`authority_did` and `nsid`), the lexicon is parsed, upserted into the `lexicons` table and in-memory registry, and Jetstream is notified if it's a record type. 33 - - **delete**: The lexicon is removed from the `lexicons` table and registry. 34 - 35 - ### Startup re-fetch 36 - 37 - On every startup, HappyView re-fetches all network lexicons from their respective PDSes. This ensures consistency even if Jetstream events were missed while offline. Failures are logged as warnings but don't block startup. 38 - 39 - ## Admin API 40 - 41 - See [Admin API - Network Lexicons](admin-api.md#network-lexicons) for endpoint details. 42 - 43 - ### Quick reference 44 - 45 - ```sh 46 - # Add a network lexicon 47 - curl -X POST http://localhost:3000/admin/network-lexicons \ 48 - -H "$AUTH" -H "Content-Type: application/json" \ 49 - -d '{ "nsid": "games.gamesgamesgamesgames.game" }' 50 - 51 - # List tracked network lexicons 52 - curl http://localhost:3000/admin/network-lexicons -H "$AUTH" 53 - 54 - # Remove a network lexicon 55 - curl -X DELETE http://localhost:3000/admin/network-lexicons/games.gamesgamesgamesgames.game \ 56 - -H "$AUTH" 57 - ```
-90
docs/quickstart.md
··· 1 - # Quickstart 2 - 3 - ## Deploy on Railway 4 - 5 - The fastest way to get HappyView running is with Railway. This template deploys HappyView, AIP, Tap, and Postgres with a single click: 6 - 7 - [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/I1jvZl?referralCode=0QOgj_) 8 - 9 - ### Required configuration 10 - 11 - After deploying the template, you'll need to configure a few things before the stack works properly: 12 - 13 - 1. **Set your admin DID.** In the AIP service variables, set `ADMIN_DIDS` to your AT Protocol DID (e.g. `did:plc:abc123...`). You can find your DID by looking up your handle on [Internect](https://internect.info/). 14 - 15 - 2. **Generate AIP signing keys.** The `OAUTH_SIGNING_KEYS` and `ATPROTO_OAUTH_SIGNING_KEYS` variables require multibase-encoded P-256 private keys. See the [AIP Signing Keys documentation](https://github.com/graze-social/aip/blob/main/CONFIGURATION.md#signing-keys) for generation instructions. 16 - 17 - 3. **Generate public URLs.** The services won't work until HappyView and AIP have public domains assigned in Railway. 18 - 19 - ## Local development 20 - 21 - ### Prerequisites 22 - 23 - - Rust (stable) 24 - - PostgreSQL 17+ 25 - - A running [AIP](https://github.com/graze-social/aip) instance 26 - 27 - ### 1. Clone and configure 28 - 29 - ```sh 30 - git clone https://github.com/graze-social/happyview.git 31 - cd happyview 32 - cp .env.example .env 33 - ``` 34 - 35 - Edit `.env`: 36 - 37 - ```sh 38 - DATABASE_URL=postgres://happyview:happyview@localhost/happyview 39 - AIP_URL=http://localhost:8080 40 - ``` 41 - 42 - See [Configuration](configuration.md) for all available variables. 43 - 44 - ### 2. Start Postgres and run migrations 45 - 46 - ```sh 47 - docker compose up -d postgres 48 - cargo run 49 - ``` 50 - 51 - Migrations run automatically on startup. 52 - 53 - ### 3. Upload a lexicon 54 - 55 - The first authenticated request to an admin endpoint auto-creates you as the initial admin. Authenticate with an AIP-issued Bearer token: 56 - 57 - ```sh 58 - curl -X POST http://localhost:3000/admin/lexicons \ 59 - -H "Authorization: Bearer $TOKEN" \ 60 - -H "Content-Type: application/json" \ 61 - -d '{ 62 - "lexicon_json": { 63 - "lexicon": 1, 64 - "id": "games.gamesgamesgamesgames.game", 65 - "defs": { 66 - "main": { 67 - "type": "record", 68 - "key": "tid", 69 - "record": { 70 - "type": "object", 71 - "properties": { 72 - "title": { "type": "string" } 73 - } 74 - } 75 - } 76 - } 77 - }, 78 - "backfill": true 79 - }' 80 - ``` 81 - 82 - HappyView now subscribes to `games.gamesgamesgamesgames.game` on Jetstream and starts indexing records. 83 - 84 - ### 4. Query records 85 - 86 - ```sh 87 - curl http://localhost:3000/xrpc/games.gamesgamesgamesgames.listGames?limit=10 88 - ``` 89 - 90 - See [XRPC API](xrpc-api.md) for query and procedure details.
+211
docs/reference/architecture.md
··· 1 + # Architecture 2 + 3 + Guide for contributors working on HappyView itself. For a user-facing overview, see the [Introduction](/). 4 + 5 + ## System overview 6 + 7 + ```mermaid 8 + graph LR 9 + Application 10 + 11 + Application -->|"GET /xrpc/{method}"| Query 12 + Application -->|"POST /xrpc/{method}"| Procedure 13 + 14 + subgraph HappyView 15 + Query["Query Handler<br/><small>Lua Script (Optional)</small>"] 16 + Procedure["Procedure Handler<br/><small>Lua Script (Optional)</small>"] 17 + end 18 + 19 + Procedure --> DB 20 + Query --> DB 21 + 22 + Procedure -->|proxy write| PDS["User PDS"] 23 + 24 + DB[("PostgreSQL<br/><small>records · lexicons</small>")] 25 + 26 + Tap["Tap<br/><small>WebSocket</small>"] -->|record events| DB 27 + Relay["Relay<br/><small>Firehose</small>"] --> Tap 28 + ``` 29 + 30 + Reads flow top-down through the query handler to Postgres. Writes flow through the procedure handler to the user's PDS, then HappyView indexes the record locally. All record data enters the system through Tap, which handles both real-time firehose events and historical backfill. HappyView syncs collection filters to Tap and discovers repos via the relay for backfill, but Tap performs all record fetching. 31 + 32 + ## Module overview 33 + 34 + ``` 35 + src/ 36 + main.rs Startup: config, DB, migrations, spawn Tap worker, start server 37 + lib.rs AppState struct, module declarations 38 + config.rs Environment variable loading 39 + error.rs AppError enum (Auth, BadRequest, Forbidden, Internal, NotFound, PdsError) 40 + server.rs Axum router: fixed routes + admin nest + XRPC catch-all + static files 41 + lexicon.rs ParsedLexicon, LexiconRegistry (Arc<RwLock<HashMap>>) 42 + profile.rs DID document resolution, PDS discovery, profile fetching 43 + tap.rs Tap WebSocket listener, collection filter sync, backfill delegation 44 + aip.rs AIP reverse proxy 45 + resolve.rs NSID authority resolution (DNS TXT → DID → PDS) 46 + auth/ 47 + mod.rs Re-exports 48 + middleware.rs Claims extractor (validates Bearer token via AIP /oauth/userinfo) 49 + jwks.rs JWKS key fetching 50 + admin/ 51 + mod.rs Admin route definitions 52 + auth.rs AdminAuth extractor (Claims + DID lookup + auto-bootstrap) 53 + admins.rs Admin CRUD handlers 54 + lexicons.rs Lexicon CRUD handlers 55 + network_lexicons.rs Network lexicon tracking (add, list, remove) 56 + records.rs Record listing handler 57 + stats.rs Record count stats 58 + backfill.rs Backfill job creation (relay discovery + Tap delegation) 59 + types.rs Request/response structs for admin endpoints 60 + lua/ 61 + mod.rs Re-exports 62 + context.rs Lua context globals (method, params, input, caller_did, collection) 63 + db_api.rs Lua database API (db.query, db.get, db.count) 64 + execute.rs Script execution and sandbox setup 65 + record.rs Lua Record API (constructor, save, delete, load) 66 + sandbox.rs Restricted Lua environment (removed modules, instruction limit) 67 + tid.rs TID generation for Lua scripts 68 + repo/ 69 + mod.rs Re-exports 70 + dpop.rs DPoP JWT proof generation (ES256/P-256) 71 + pds.rs PDS proxy helpers (JSON POST, blob POST, response forwarding) 72 + session.rs ATP session fetching from AIP 73 + upload_blob.rs Blob upload handler 74 + xrpc/ 75 + mod.rs Re-exports 76 + query.rs Dynamic GET handler (Lua script or default: single record + list) 77 + procedure.rs Dynamic POST handler (Lua script or default: create vs put) 78 + ``` 79 + 80 + ## Request flow 81 + 82 + ### Reads (queries) 83 + 84 + ``` 85 + Client GET /xrpc/{method}?params 86 + -> xrpc::xrpc_get() 87 + -> LexiconRegistry lookup (must be Query type) 88 + -> If Lua script attached: execute script (has access to db API) 89 + -> Else: default SQL query on records table (collection from target_collection) 90 + -> JSON response 91 + ``` 92 + 93 + ### Writes (procedures) 94 + 95 + ``` 96 + Client POST /xrpc/{method} + Bearer token 97 + -> Claims extractor validates token via AIP /oauth/userinfo 98 + -> xrpc::xrpc_post() 99 + -> LexiconRegistry lookup (must be Procedure type) 100 + -> If Lua script attached: execute script (has access to Record API) 101 + -> Else: default create/update (auto-detect based on uri field) 102 + -> Fetch ATP session from AIP /api/atprotocol/session 103 + -> Generate DPoP proof (ES256) 104 + -> Proxy to user's PDS (createRecord or putRecord) 105 + -> Upsert record locally 106 + -> Forward PDS response 107 + ``` 108 + 109 + ### Admin endpoints 110 + 111 + ``` 112 + Client request + Bearer token 113 + -> AdminAuth extractor: 114 + 1. Claims validation via AIP 115 + 2. DID lookup in admins table (auto-bootstrap if empty) 116 + 3. 403 if not admin 117 + -> Admin handler 118 + -> JSON response 119 + ``` 120 + 121 + ## Data flow 122 + 123 + ### Real-time indexing 124 + 125 + ``` 126 + Tap WebSocket connection (tap::spawn) 127 + -> Collection filters synced to Tap on startup and lexicon changes 128 + -> Record events: 129 + create/update -> UPSERT into records table 130 + delete -> DELETE from records table 131 + -> Lexicon schema events (com.atproto.lexicon.schema): 132 + -> Update tracked network lexicons in DB and registry 133 + -> Reconnects automatically on errors or collection filter changes 134 + ``` 135 + 136 + ### Backfill 137 + 138 + ``` 139 + POST /admin/backfill 140 + -> Create backfill_jobs record (status = running) 141 + -> Relay listReposByCollection -> list of DIDs 142 + -> Send DIDs to Tap in batches of 1000 (POST /repos/add) 143 + -> Mark job as completed 144 + -> Tap fetches records asynchronously and delivers via WebSocket 145 + ``` 146 + 147 + ## Database schema 148 + 149 + ### `records` 150 + 151 + | Column | Type | Description | 152 + | ------------ | ----------- | ----------------------------------- | 153 + | `uri` | text (PK) | AT URI (`at://did/collection/rkey`) | 154 + | `did` | text | Author DID | 155 + | `collection` | text | Lexicon NSID | 156 + | `rkey` | text | Record key | 157 + | `record` | jsonb | Record value | 158 + | `cid` | text | Content identifier | 159 + | `indexed_at` | timestamptz | When HappyView indexed this record | 160 + 161 + ### `lexicons` 162 + 163 + | Column | Type | Description | 164 + | ------------------- | ----------- | ----------------------------------------------- | 165 + | `id` | text (PK) | Lexicon NSID | 166 + | `revision` | integer | Incremented on upsert | 167 + | `lexicon_json` | jsonb | Raw lexicon definition | 168 + | `lexicon_type` | text | record, query, procedure, definitions | 169 + | `backfill` | boolean | Whether to backfill on upload | 170 + | `target_collection` | text | For queries/procedures: which record collection | 171 + | `created_at` | timestamptz | | 172 + | `updated_at` | timestamptz | | 173 + 174 + ### `admins` 175 + 176 + | Column | Type | Description | 177 + | -------------- | ------------- | ------------------------------------- | 178 + | `id` | uuid (PK) | | 179 + | `did` | text (unique) | Admin's AT Protocol DID | 180 + | `created_at` | timestamptz | | 181 + | `last_used_at` | timestamptz | Updated on each authenticated request | 182 + 183 + ### `backfill_jobs` 184 + 185 + | Column | Type | Description | 186 + | ----------------- | ----------- | ----------------------------------- | 187 + | `id` | uuid (PK) | | 188 + | `collection` | text | Target collection (null = all) | 189 + | `did` | text | Target DID (null = all) | 190 + | `status` | text | pending, running, completed, failed | 191 + | `total_repos` | integer | | 192 + | `processed_repos` | integer | | 193 + | `total_records` | integer | | 194 + | `error` | text | Error message if failed | 195 + | `started_at` | timestamptz | | 196 + | `completed_at` | timestamptz | | 197 + | `created_at` | timestamptz | | 198 + 199 + ## Testing 200 + 201 + ```sh 202 + # Unit tests (no database needed) 203 + cargo test --lib 204 + 205 + # All tests including end-to-end (requires Postgres) 206 + docker compose -f docker-compose.test.yml up -d 207 + TEST_DATABASE_URL=postgres://happyview:happyview@localhost:5433/happyview_test cargo test 208 + docker compose -f docker-compose.test.yml down 209 + ``` 210 + 211 + End-to-end tests use `wiremock` to mock external services (AIP, PLC directory, PDSes) and a real Postgres database for full integration coverage.
+43
docs/reference/glossary.md
··· 1 + # Glossary 2 + 3 + Key terms used throughout the HappyView documentation. For a broader introduction to the AT Protocol, see the [official ATProto glossary](https://atproto.com/guides/glossary). 4 + 5 + ## AT Protocol terms 6 + 7 + **AppView** — A backend service that indexes AT Protocol records and serves them through an API. HappyView is an AppView. See the [ATProto docs](https://atproto.com/guides/glossary#app-view) for more. 8 + 9 + **DID** (Decentralized Identifier) — A persistent, globally unique identifier for an account (e.g. `did:plc:abc123`). 10 + 11 + **Firehose** — A real-time stream of all record events (creates, updates, deletes) across the AT Protocol network. HappyView consumes this via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap). 12 + 13 + **Handle** — A human-readable name for an account (e.g. `user.bsky.social`). Handles resolve to a DID via DNS or the PLC directory. 14 + 15 + **Lexicon** — A schema definition for AT Protocol data types and API methods. Lexicons define what records look like, what endpoints exist, and what parameters they accept. See [Lexicons](../guides/lexicons). 16 + 17 + **NSID** (Namespaced Identifier) — A reverse-DNS identifier for a lexicon (e.g. `xyz.statusphere.status`). The authority is everything except the last segment. 18 + 19 + **PDS** (Personal Data Server) — The server that hosts a user's data. Users can be on any PDS — there's no single server. HappyView proxies writes back to each user's PDS. 20 + 21 + **PLC directory** — A public service (e.g. `plc.directory`) that maps DIDs to their DID documents, which contain the user's PDS endpoint and other metadata. 22 + 23 + **Record** — A single piece of data in an AT Protocol repository, identified by an AT URI (e.g. `at://did:plc:abc/xyz.statusphere.status/abc123`). 24 + 25 + **Relay** — A network service that aggregates repository data from many PDSes. HappyView queries the relay during [backfill](../guides/backfill) to discover which repos contain records for a given collection, then delegates the actual record fetching to Tap. 26 + 27 + **rkey** (Record Key) — The unique key for a record within a collection and repo. These are most commonly TIDs (timestamp-based) or NSIDs. 28 + 29 + **TID** (Timestamp Identifier) — A 13-character sortable identifier used as a record key. Generated from the current timestamp. 30 + 31 + **XRPC** — The HTTP-based RPC protocol used by the AT Protocol. Query methods map to GET requests, procedure methods map to POST requests. See [XRPC API](xrpc-api). 32 + 33 + ## HappyView-specific terms 34 + 35 + **AIP** — [Authentication and Identity Provider](https://github.com/graze-social/aip). An external service that handles AT Protocol OAuth for HappyView. Issues Bearer tokens used for authentication. 36 + 37 + **Backfill** — The process of bulk-indexing existing records from the network. HappyView discovers repos via the relay and delegates record fetching to Tap. Runs when a new record-type lexicon is uploaded or triggered manually. See [Backfill](../guides/backfill). 38 + 39 + **Network lexicon** — A lexicon fetched directly from the AT Protocol network via DNS authority resolution, rather than uploaded manually. See [Lexicons - Network lexicons](../guides/lexicons#network-lexicons). 40 + 41 + **Tap** — A [firehose consumer and backfill worker](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) that handles real-time record streaming, cryptographic verification, and historical record fetching. HappyView connects to Tap via WebSocket to receive record events, and delegates backfill work to Tap via its HTTP API. 42 + 43 + **Target collection** — The record collection that a query or procedure lexicon operates on. Set via the `target_collection` field when uploading a lexicon.
+70
docs/reference/production-deployment.md
··· 1 + # Deployment 2 + 3 + HappyView requires a Postgres database and an [AIP](https://github.com/graze-social/aip) instance for OAuth. The [Quickstart](../getting-started/deployment/railway) covers the fastest path with Railway. This page covers other deployment options. 4 + 5 + ## Docker 6 + 7 + Build the image: 8 + 9 + ```sh 10 + docker build -t happyview . 11 + ``` 12 + 13 + For local development, see [Docker deployment](../getting-started/deployment/docker). 14 + 15 + ### Production Compose example 16 + 17 + :::note 18 + This example omits [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap), which is required for real-time record streaming and backfill. See the full `docker-compose.yml` in the repository for a complete configuration including Tap. 19 + ::: 20 + 21 + ```yaml 22 + services: 23 + postgres: 24 + image: postgres:17 25 + environment: 26 + POSTGRES_USER: happyview 27 + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" 28 + POSTGRES_DB: happyview 29 + volumes: 30 + - pgdata:/var/lib/postgresql/data 31 + 32 + happyview: 33 + image: happyview:latest 34 + ports: 35 + - "3000:3000" 36 + environment: 37 + DATABASE_URL: "postgres://happyview:${POSTGRES_PASSWORD}@postgres/happyview" 38 + AIP_URL: "https://aip.example.com" 39 + depends_on: 40 + postgres: 41 + condition: service_healthy 42 + 43 + volumes: 44 + pgdata: 45 + ``` 46 + 47 + ## Railway / Fly.io / other platforms 48 + 49 + The general process for any hosting platform: 50 + 51 + 1. Provision a Postgres 17+ database 52 + 2. Deploy an [AIP](https://github.com/graze-social/aip) instance (handles OAuth for your AppView) 53 + 3. Set `DATABASE_URL` and `AIP_URL` environment variables (see [Configuration](../getting-started/configuration) for all options) 54 + 4. Deploy the Docker image or build from source 55 + 5. HappyView listens on `PORT` (default `3000`) 56 + 6. Health check: `GET /health` returns `ok` 57 + 58 + For Railway specifically, the [Quickstart](../getting-started/deployment/railway) template handles all of this with a single click. 59 + 60 + ## Database 61 + 62 + Migrations run automatically on startup via `sqlx::migrate!()`. No manual migration step is needed. 63 + 64 + ## TLS 65 + 66 + HappyView does not terminate TLS. Put it behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel, etc.) for HTTPS. 67 + 68 + ## Logging 69 + 70 + HappyView uses the `RUST_LOG` environment variable to control log output. The default (`happyview=debug,tower_http=debug`) logs all HappyView activity and HTTP requests. For production, consider `happyview=info,tower_http=info` to reduce noise. See [Configuration](../getting-started/configuration) for details.
+46
docs/reference/scripts/batch-save.md
··· 1 + # Procedure: Batch Save 2 + 3 + Use `Record.save_all()` to create multiple records in parallel. 4 + 5 + **Lexicon type:** procedure 6 + 7 + ```lua 8 + function handle() 9 + local records = {} 10 + for _, item in ipairs(input.items) do 11 + local r = Record(collection, item) 12 + records[#records + 1] = r 13 + end 14 + Record.save_all(records) 15 + 16 + local uris = {} 17 + for _, r in ipairs(records) do 18 + uris[#uris + 1] = r._uri 19 + end 20 + return { uris = uris } 21 + end 22 + ``` 23 + 24 + ## How it works 25 + 26 + 1. Iterate over `input.items` and create a [`Record`](../../guides/scripting#record-api) instance for each item. 27 + 2. Call [`Record.save_all()`](../../guides/scripting#static-methods) to save all records in parallel, rather than one at a time. 28 + 3. Collect the resulting AT URIs and return them. 29 + 30 + ## Usage 31 + 32 + ```sh 33 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.batchCreate \ 34 + -H "Authorization: Bearer $TOKEN" \ 35 + -H "Content-Type: application/json" \ 36 + -d '{ 37 + "items": [ 38 + { "text": "First", "createdAt": "2025-01-01T00:00:00Z" }, 39 + { "text": "Second", "createdAt": "2025-01-01T00:01:00Z" } 40 + ] 41 + }' 42 + ``` 43 + 44 + ## Use case 45 + 46 + Batch saving is useful when a single user action should create multiple records (e.g. importing data, multi-step forms). `save_all` is significantly faster than calling `r:save()` in a loop because the PDS writes happen concurrently.
+74
docs/reference/scripts/cascading-delete.md
··· 1 + # Procedure: Cascading Delete 2 + 3 + Delete a record and all related records across collections. 4 + 5 + **Lexicon type:** procedure 6 + 7 + ```lua 8 + function handle() 9 + if not input.uri then 10 + return { error = "uri is required" } 11 + end 12 + 13 + -- Load the primary record 14 + local primary = Record.load(input.uri) 15 + if not primary then 16 + return { error = "not found" } 17 + end 18 + 19 + -- Find related records that reference this URI 20 + local comments = db.query({ 21 + collection = "xyz.statusphere.comment", 22 + did = caller_did, 23 + limit = 100, 24 + }) 25 + 26 + -- Collect records to delete 27 + local to_delete = { primary } 28 + for _, comment in ipairs(comments.records) do 29 + if comment.postUri == input.uri then 30 + local r = Record.load(comment.uri) 31 + if r then 32 + to_delete[#to_delete + 1] = r 33 + end 34 + end 35 + end 36 + 37 + -- Delete all matched records 38 + for _, r in ipairs(to_delete) do 39 + r:delete() 40 + end 41 + 42 + return { 43 + deleted = #to_delete, 44 + } 45 + end 46 + ``` 47 + 48 + ## How it works 49 + 50 + 1. Load the primary record by URI. Return early if it doesn't exist. 51 + 2. Query for related records, in this example comments by the same user that reference the primary record's URI. 52 + 3. Load each related record with [`Record.load`](../../guides/scripting#static-methods) to get a deletable `Record` instance. 53 + 4. Delete everything. Each `r:delete()` removes the record from the user's PDS and the local index. 54 + 55 + ## Usage 56 + 57 + ```sh 58 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.deletePost \ 59 + -H "Authorization: Bearer $TOKEN" \ 60 + -H "Content-Type: application/json" \ 61 + -d '{ "uri": "at://did:plc:abc/xyz.statusphere.post/abc123" }' 62 + ``` 63 + 64 + ```json 65 + { 66 + "deleted": 4 67 + } 68 + ``` 69 + 70 + ## Use case 71 + 72 + Cascading deletes are useful when your data model has parent-child relationships across collections. For example, deleting a post should also clean up its comments, reactions, or metadata records. This keeps the user's repo and the local index consistent. 73 + 74 + Note that this only deletes records owned by `caller_did`. AT Protocol records can only be deleted by their owner. If the related records could have more than 100 matches, paginate through all of them before deleting.
+84
docs/reference/scripts/complex-mutations.md
··· 1 + # Procedure: Complex Mutations 2 + 3 + Load an existing record, apply multiple transformations, and save it back. 4 + 5 + **Lexicon type:** procedure 6 + 7 + ```lua 8 + function handle() 9 + if not input.uri then 10 + return { error = "uri is required" } 11 + end 12 + 13 + local r = Record.load(input.uri) 14 + if not r then 15 + return { error = "not found" } 16 + end 17 + 18 + -- Increment a counter 19 + r.likeCount = (r.likeCount or 0) + 1 20 + 21 + -- Merge tags, deduplicating and capping at 10 22 + r.tags = r.tags or {} 23 + if input.tags then 24 + for _, tag in ipairs(input.tags) do 25 + local found = false 26 + for _, t in ipairs(r.tags) do 27 + if t == tag then 28 + found = true 29 + break 30 + end 31 + end 32 + if not found then 33 + r.tags[#r.tags + 1] = tag 34 + end 35 + end 36 + -- Keep only the last 10 37 + while #r.tags > 10 do 38 + table.remove(r.tags, 1) 39 + end 40 + end 41 + 42 + -- Normalize a string field 43 + if input.title then 44 + r.title = string.gsub(input.title, "^%s+", "") 45 + r.title = string.gsub(r.title, "%s+$", "") 46 + end 47 + 48 + -- Set a computed field 49 + r.updatedAt = now() 50 + 51 + r:save() 52 + 53 + return { uri = r._uri, cid = r._cid } 54 + end 55 + ``` 56 + 57 + ## How it works 58 + 59 + 1. Load the existing record with [`Record.load`](../../guides/scripting#static-methods). This gives you a mutable `Record` instance with all the current field values. 60 + 2. Apply transformations directly on the record's fields: 61 + - **Increment a counter**: use `or 0` to handle the field being `nil` on first access. 62 + - **Merge tags**: iterate over `input.tags`, skip duplicates already in `r.tags`, append new ones, then trim the list to 10. 63 + - **Normalize a string**: use `string.gsub` to trim whitespace. 64 + - **Set a timestamp**: use [`now()`](../../guides/scripting#utility-globals) for UTC ISO 8601. 65 + 3. Call `r:save()`. Since `_uri` is set (from the load), this calls `putRecord` to update the record on the user's PDS. 66 + 67 + ## Usage 68 + 69 + ```sh 70 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.updatePost \ 71 + -H "Authorization: Bearer $TOKEN" \ 72 + -H "Content-Type: application/json" \ 73 + -d '{ 74 + "uri": "at://did:plc:abc/xyz.statusphere.post/abc123", 75 + "tags": ["tutorial", "atproto"], 76 + "title": " My Post Title " 77 + }' 78 + ``` 79 + 80 + ## Use case 81 + 82 + This pattern is useful when updates involve more than simple field replacement: counters, bounded lists, string normalization, or computed fields. All mutations happen in memory before the single `r:save()` call, so there's no partial save: either all changes are written or none are. 83 + 84 + If the record has a schema, HappyView only sends fields defined in the schema's `properties` to the PDS on save. Extra fields you set on the record instance are ignored.
+32
docs/reference/scripts/create-record.md
··· 1 + # Procedure: Create a Record 2 + 3 + The simplest write: take the request body, save it as a record, and return the URI. 4 + 5 + **Lexicon type:** procedure 6 + 7 + ```lua 8 + function handle() 9 + local r = Record(collection, input) 10 + r:save() 11 + return { uri = r._uri, cid = r._cid } 12 + end 13 + ``` 14 + 15 + ## How it works 16 + 17 + 1. Create a new [`Record`](../../guides/scripting#record-api) instance from the target collection, populated with the fields from the request body. 18 + 2. Call `r:save()`, which creates the record on the caller's PDS and indexes it locally. 19 + 3. Return the AT URI and CID of the newly created record. 20 + 21 + ## Usage 22 + 23 + ```sh 24 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.createRecord \ 25 + -H "Authorization: Bearer $TOKEN" \ 26 + -H "Content-Type: application/json" \ 27 + -d '{ "text": "Hello world", "createdAt": "2025-01-01T00:00:00Z" }' 28 + ``` 29 + 30 + ## Use case 31 + 32 + This is the simplest possible write procedure. It works well when the client is responsible for populating all record fields and no server-side validation or transformation is needed.
+83
docs/reference/scripts/expanded-query.md
··· 1 + # Query: Expanded Query with Profiles 2 + 3 + List statuses and include the profile of each user who created one. 4 + 5 + **Lexicon type:** query 6 + 7 + ```lua 8 + function handle() 9 + local limit = tonumber(params.limit) or 20 10 + if limit > 100 then limit = 100 end 11 + 12 + local result = db.query({ 13 + collection = "xyz.statusphere.status", 14 + did = params.did, 15 + limit = limit, 16 + offset = tonumber(params.cursor) or 0, 17 + }) 18 + 19 + -- Collect unique DIDs from the statuses 20 + local seen = {} 21 + local profile_uris = {} 22 + for _, status in ipairs(result.records) do 23 + local did = string.match(status.uri, "at://([^/]+)/") 24 + if did and not seen[did] then 25 + seen[did] = true 26 + profile_uris[#profile_uris + 1] = "at://" .. did .. "/app.bsky.actor.profile/self" 27 + end 28 + end 29 + 30 + -- Load all profiles in parallel 31 + local profiles = {} 32 + if #profile_uris > 0 then 33 + local loaded = Record.load_all(profile_uris) 34 + for i, profile in ipairs(loaded) do 35 + if profile then 36 + profiles[#profiles + 1] = profile 37 + end 38 + end 39 + end 40 + 41 + return { 42 + statuses = result.records, 43 + profiles = profiles, 44 + cursor = result.cursor, 45 + } 46 + end 47 + ``` 48 + 49 + ## How it works 50 + 51 + 1. Query statuses from the target collection with pagination, same as a normal list query. 52 + 2. Extract the unique DIDs from the returned status URIs using `string.match`. 53 + 3. Build an AT URI for each DID's `app.bsky.actor.profile/self` record (this is where Bluesky profiles live). 54 + 4. Load all profiles in parallel with [`Record.load_all`](../../guides/scripting#static-methods). Profiles that aren't indexed locally return `nil` and are skipped. 55 + 5. Return statuses and profiles as separate keys, with the cursor from the status query. 56 + 57 + ## Usage 58 + 59 + ``` 60 + GET /xrpc/xyz.statusphere.listStatusesWithProfiles?limit=10 61 + GET /xrpc/xyz.statusphere.listStatusesWithProfiles?did=did:plc:abc 62 + GET /xrpc/xyz.statusphere.listStatusesWithProfiles?cursor=20&limit=20 63 + ``` 64 + 65 + ```json 66 + { 67 + "statuses": [ 68 + { "uri": "at://did:plc:abc/xyz.statusphere.status/3abc123", "status": "😊", "createdAt": "..." }, 69 + { "uri": "at://did:plc:def/xyz.statusphere.status/3def456", "status": "🌟", "createdAt": "..." } 70 + ], 71 + "profiles": [ 72 + { "uri": "at://did:plc:abc/app.bsky.actor.profile/self", "displayName": "Alice", "avatar": "..." }, 73 + { "uri": "at://did:plc:def/app.bsky.actor.profile/self", "displayName": "Bob", "avatar": "..." } 74 + ], 75 + "cursor": "10" 76 + } 77 + ``` 78 + 79 + ## Use case 80 + 81 + This pattern avoids N+1 queries (fetching each author's profile individually) on the client side. Instead of fetching statuses and then making a separate request for each user's profile, the client gets everything in one call. The deduplication step ensures each profile is loaded only once even if multiple statuses are from the same user. 82 + 83 + Note that `Record.load_all` reads from HappyView's local index. Profiles only appear if `app.bsky.actor.profile` is also being indexed. If a profile hasn't been indexed yet, it's silently omitted from the response.
+36
docs/reference/scripts/get-record.md
··· 1 + # Query: Get a Single Record 2 + 3 + Fetch a single record by its AT URI. 4 + 5 + **Lexicon type:** query 6 + 7 + ```lua 8 + function handle() 9 + if not params.uri then 10 + return { error = "uri parameter is required" } 11 + end 12 + 13 + local record = db.get(params.uri) 14 + if not record then 15 + return { error = "not found" } 16 + end 17 + 18 + return { record = record } 19 + end 20 + ``` 21 + 22 + ## How it works 23 + 24 + 1. Check that the `uri` query parameter is present. Return a structured error if missing. 25 + 2. Look up the record with [`db.get`](../../guides/scripting#dbget), which returns the record table or `nil`. 26 + 3. Return the record wrapped in an object. 27 + 28 + ## Usage 29 + 30 + ``` 31 + GET /xrpc/xyz.statusphere.getRecord?uri=at://did:plc:abc/xyz.statusphere.record/abc123 32 + ``` 33 + 34 + ## Use case 35 + 36 + A focused read endpoint for detail views or record verification. Returns structured error responses instead of calling `error()`, so the client gets a 200 with an error field it can handle gracefully rather than a 500.
+41
docs/reference/scripts/list-or-fetch.md
··· 1 + # Query: List or Fetch Records 2 + 3 + This query handles both single-record lookups (when a `uri` param is provided) and paginated listing. 4 + 5 + **Lexicon type:** query 6 + 7 + ```lua 8 + function handle() 9 + if params.uri then 10 + local record = db.get(params.uri) 11 + if not record then 12 + return { error = "record not found" } 13 + end 14 + return { record = record } 15 + end 16 + 17 + return db.query({ 18 + collection = collection, 19 + did = params.did, 20 + limit = tonumber(params.limit) or 20, 21 + offset = tonumber(params.cursor) or 0, 22 + }) 23 + end 24 + ``` 25 + 26 + ## How it works 27 + 28 + 1. If a `uri` query parameter is provided, fetch that single record with [`db.get`](../../guides/scripting#dbget) and return it. If it doesn't exist, return a structured error (using `error()` would trigger a 500 response). 29 + 2. Otherwise, list records from the target collection using [`db.query`](../../guides/scripting#dbquery), with optional filtering by `did` and pagination via `limit`/`offset`. Since query parameters arrive as strings, `tonumber()` converts them to numbers. 30 + 31 + ## Usage 32 + 33 + ``` 34 + GET /xrpc/xyz.statusphere.listRecords?limit=10 35 + GET /xrpc/xyz.statusphere.listRecords?did=did:plc:abc 36 + GET /xrpc/xyz.statusphere.listRecords?uri=at://did:plc:abc/xyz.statusphere.record/abc123 37 + ``` 38 + 39 + ## Use case 40 + 41 + This is a good default query script when you want a single endpoint that serves double duty: list browsing for feeds/timelines and direct record fetching for detail views.
+40
docs/reference/scripts/paginated-list.md
··· 1 + # Query: Paginated List 2 + 3 + List records from a collection with cursor-based pagination and an optional DID filter. 4 + 5 + **Lexicon type:** query 6 + 7 + ```lua 8 + function handle() 9 + local limit = tonumber(params.limit) or 20 10 + if limit > 100 then limit = 100 end 11 + 12 + local result = db.query({ 13 + collection = collection, 14 + did = params.did, 15 + limit = limit, 16 + offset = tonumber(params.cursor) or 0, 17 + }) 18 + 19 + return result 20 + end 21 + ``` 22 + 23 + ## How it works 24 + 25 + 1. Parse `limit` from the query string, defaulting to 20 and capping at 100. 26 + 2. Call [`db.query`](../../guides/scripting#dbquery) with the target collection, optional DID filter, and offset-based pagination. 27 + 3. Return the result directly. `db.query` returns `{ records = [...], cursor = "..." }` where `cursor` is present when more records exist. 28 + 29 + ## Usage 30 + 31 + ``` 32 + GET /xrpc/xyz.statusphere.listStatuses 33 + GET /xrpc/xyz.statusphere.listStatuses?limit=50 34 + GET /xrpc/xyz.statusphere.listStatuses?did=did:plc:abc&limit=10 35 + GET /xrpc/xyz.statusphere.listStatuses?cursor=20&limit=20 36 + ``` 37 + 38 + ## Use case 39 + 40 + A straightforward list endpoint for feeds, timelines, or browsing records by collection. The `cursor` value returned by `db.query` is an offset. Clients pass it back as the `cursor` parameter to fetch the next page. Since all query parameters arrive as strings, use `tonumber()` to convert `limit` and `cursor` to numbers.
+74
docs/reference/scripts/sidecar-records.md
··· 1 + # Procedure: Create Sidecar Records 2 + 3 + Create two records with different collection NSIDs but the same rkey, linking them together by key. 4 + 5 + **Lexicon type:** procedure 6 + 7 + ```lua 8 + function handle() 9 + local rkey = TID() 10 + 11 + local post = Record("xyz.statusphere.post", { 12 + text = input.text, 13 + createdAt = now(), 14 + }) 15 + post:set_rkey(rkey) 16 + 17 + local metadata = Record("xyz.statusphere.postMetadata", { 18 + lang = input.lang or "en", 19 + source = input.source or "web", 20 + createdAt = now(), 21 + }) 22 + metadata:set_rkey(rkey) 23 + 24 + Record.save_all({ post, metadata }) 25 + 26 + return { 27 + post = { uri = post._uri, cid = post._cid }, 28 + metadata = { uri = metadata._uri, cid = metadata._cid }, 29 + } 30 + end 31 + ``` 32 + 33 + ## How it works 34 + 35 + 1. Generate a single [`TID()`](../../guides/scripting#utility-globals) to use as the rkey for both records. 36 + 2. Create a `Record` for each collection and call `r:set_rkey()` with the shared rkey. 37 + 3. Save both records in parallel with [`Record.save_all()`](../../guides/scripting#static-methods). 38 + 4. Return both URIs so the client knows the identity of each record. 39 + 40 + ## Usage 41 + 42 + ```sh 43 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.createPost \ 44 + -H "Authorization: Bearer $TOKEN" \ 45 + -H "Content-Type: application/json" \ 46 + -d '{ "text": "Hello world", "lang": "en", "source": "web" }' 47 + ``` 48 + 49 + The response includes URIs for both the post and its metadata: 50 + 51 + ```json 52 + { 53 + "post": { 54 + "uri": "at://did:plc:abc/xyz.statusphere.post/3abc123", 55 + "cid": "bafyrei..." 56 + }, 57 + "metadata": { 58 + "uri": "at://did:plc:abc/xyz.statusphere.postMetadata/3abc123", 59 + "cid": "bafyrei..." 60 + } 61 + } 62 + ``` 63 + 64 + ## Use case 65 + 66 + Sidecar records are useful when you want to associate related data across collections without embedding everything in a single record. Because they share an rkey, you can derive one URI from the other: 67 + 68 + ``` 69 + at:// did:plc:abc /xyz.statusphere.post /3abc123 70 + at:// did:plc:abc /xyz.statusphere.postMetadata /3abc123 71 + ^^^^^^^ same rkey 72 + ``` 73 + 74 + This is a common AT Protocol pattern for keeping a primary record lean while storing auxiliary data (metadata, reactions, settings) in a companion collection.
+61
docs/reference/scripts/update-or-delete.md
··· 1 + # Procedure: Update or Delete 2 + 3 + A single endpoint that handles create, update, and delete based on the input fields. 4 + 5 + **Lexicon type:** procedure 6 + 7 + ```lua 8 + function handle() 9 + if input.delete and input.uri then 10 + local r = Record.load(input.uri) 11 + if r then r:delete() end 12 + return { success = true } 13 + end 14 + 15 + if input.uri then 16 + -- Update existing 17 + local r = Record.load(input.uri) 18 + if not r then error("not found") end 19 + r.status = input.status 20 + r:save() 21 + return { uri = r._uri, cid = r._cid } 22 + end 23 + 24 + -- Create new 25 + local r = Record(collection, input) 26 + r:save() 27 + return { uri = r._uri, cid = r._cid } 28 + end 29 + ``` 30 + 31 + ## How it works 32 + 33 + 1. If `input.delete` is truthy and `input.uri` is provided, load the record with [`Record.load`](../../guides/scripting#static-methods) and delete it. 34 + 2. If only `input.uri` is provided, load the existing record with [`Record.load`](../../guides/scripting#static-methods), update its fields, and save it back. Since `_uri` is already set, `r:save()` calls `putRecord` instead of `createRecord`. 35 + 3. If neither condition matches, create a new record from the input. 36 + 37 + ## Usage 38 + 39 + ```sh 40 + # Create 41 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setRecord \ 42 + -H "Authorization: Bearer $TOKEN" \ 43 + -H "Content-Type: application/json" \ 44 + -d '{ "status": "hello" }' 45 + 46 + # Update 47 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setRecord \ 48 + -H "Authorization: Bearer $TOKEN" \ 49 + -H "Content-Type: application/json" \ 50 + -d '{ "uri": "at://did:plc:abc/xyz.statusphere.record/abc123", "status": "updated" }' 51 + 52 + # Delete 53 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setRecord \ 54 + -H "Authorization: Bearer $TOKEN" \ 55 + -H "Content-Type: application/json" \ 56 + -d '{ "uri": "at://did:plc:abc/xyz.statusphere.record/abc123", "delete": true }' 57 + ``` 58 + 59 + ## Use case 60 + 61 + This pattern reduces the number of endpoints your app needs by multiplexing create, update, and delete through a single procedure. The presence of `uri` and `delete` fields in the input determines the action.
+60
docs/reference/scripts/upsert-record.md
··· 1 + # Procedure: Upsert a Record 2 + 3 + Create a new record, or update an existing one if the client provides its rkey. 4 + 5 + **Lexicon type:** procedure 6 + 7 + ```lua 8 + function handle() 9 + local rkey = input.rkey or TID() 10 + local uri = "at://" .. caller_did .. "/" .. collection .. "/" .. rkey 11 + 12 + local r = Record.load(uri) 13 + if r then 14 + -- Update existing record 15 + r.status = input.status 16 + r.updatedAt = now() 17 + r:save() 18 + else 19 + -- Create new record 20 + r = Record(collection, { 21 + status = input.status, 22 + createdAt = now(), 23 + updatedAt = now(), 24 + }) 25 + r:set_rkey(rkey) 26 + r:save() 27 + end 28 + 29 + return { uri = r._uri, cid = r._cid } 30 + end 31 + ``` 32 + 33 + ## How it works 34 + 35 + 1. Use the client-provided `input.rkey` if present, otherwise generate a new [`TID()`](../../guides/scripting#utility-globals). This means omitting `rkey` always creates, while providing one enables updates. 36 + 2. Build the AT URI from the caller's DID, the target collection, and the rkey, then try to load it with [`Record.load`](../../guides/scripting#static-methods). 37 + 3. If the record exists, update its fields and save. Since `_uri` is already set, `r:save()` calls `putRecord`. 38 + 4. If it doesn't exist, create a new record, set the rkey explicitly with `r:set_rkey()`, and save. This calls `createRecord` with the specified rkey. 39 + 40 + ## Usage 41 + 42 + ```sh 43 + # Create: no rkey, so a new TID is generated 44 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setStatus \ 45 + -H "Authorization: Bearer $TOKEN" \ 46 + -H "Content-Type: application/json" \ 47 + -d '{ "status": "hello" }' 48 + # → { "uri": "at://did:plc:abc/xyz.statusphere.status/3abc123", "cid": "bafyrei..." } 49 + 50 + # Update: pass the rkey back to update the same record 51 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setStatus \ 52 + -H "Authorization: Bearer $TOKEN" \ 53 + -H "Content-Type: application/json" \ 54 + -d '{ "rkey": "3abc123", "status": "updated" }' 55 + # → { "uri": "at://did:plc:abc/xyz.statusphere.status/3abc123", "cid": "bafyrei..." } 56 + ``` 57 + 58 + ## Use case 59 + 60 + This is useful when the client knows whether it's creating or editing, but you want a single endpoint for both. The client omits `rkey` for new records and includes it when editing an existing one. The rkey from the initial create response acts as the record's stable identifier for future updates.
+100
docs/reference/troubleshooting.md
··· 1 + # Troubleshooting 2 + 3 + Common issues and how to resolve them. 4 + 5 + ## XRPC endpoint returns 404 6 + 7 + **Symptom**: `GET /xrpc/your.method.name` returns `{"error": "method not found"}`. 8 + 9 + **Causes**: 10 + 11 + - The lexicon hasn't been uploaded yet. Check with `GET /admin/lexicons` or the [dashboard](../getting-started/dashboard). 12 + - The lexicon's `defs.main.type` doesn't match the HTTP method. Queries are `GET`, procedures are `POST`. 13 + - The NSID in the URL doesn't match the `id` field in the uploaded lexicon JSON. 14 + 15 + ## Queries return empty results 16 + 17 + **Symptom**: The XRPC query endpoint returns `{"records": []}` even though records should exist. 18 + 19 + **Causes**: 20 + 21 + - The query lexicon is missing a `target_collection`. Without it, the query doesn't know which records to read. See [Lexicons - target_collection](../guides/lexicons#target-collection). 22 + - The record-type lexicon hasn't finished backfilling. Check backfill status with `GET /admin/backfill/status` or the dashboard. 23 + - Records exist on the network but HappyView hasn't indexed them yet. Tap only picks up new events from when the collection filter was added. Use [backfill](../guides/backfill) for historical records. 24 + 25 + ## Procedure returns 401 Unauthorized 26 + 27 + **Symptom**: `POST /xrpc/your.method.name` returns `{"error": "..."}` with status 401. 28 + 29 + **Causes**: 30 + 31 + - The `Authorization: Bearer <token>` header is missing or malformed. 32 + - The token has expired or is invalid. Tokens are validated against AIP's `/oauth/userinfo` endpoint. 33 + - AIP is unreachable. Check that `AIP_URL` is set correctly and the AIP service is running. 34 + 35 + For AIP-specific issues, see the [AIP documentation](https://github.com/graze-social/aip). 36 + 37 + ## Admin endpoints return 403 Forbidden 38 + 39 + **Symptom**: Admin API calls return `{"error": "forbidden"}`. 40 + 41 + **Causes**: 42 + 43 + - Your DID is not in the admins table. Ask an existing admin to add you via `POST /admin/admins`. 44 + - If this is a fresh deployment with no admins, the first authenticated request to any admin endpoint automatically bootstraps you as admin. Make sure you're sending a valid Bearer token. 45 + 46 + ## Lua script errors 47 + 48 + **Symptom**: An XRPC endpoint returns `{"error": "script execution failed"}` or `{"error": "script exceeded execution time limit"}`. 49 + 50 + **What to do**: 51 + 52 + 1. Check the server logs: the full error message is logged at error level but not exposed to the client. 53 + 2. Use `log("message")` in your script to trace execution. Output appears in server logs at debug level (requires `RUST_LOG` to include debug). 54 + 3. If you hit the execution limit, your script likely has an infinite loop or is processing too much data. See [Lua Scripting - Sandbox](../guides/scripting#sandbox). 55 + 56 + See [Lua Scripting - Debugging](../guides/scripting#debugging) for more. 57 + 58 + ## Backfill job stuck in "pending" or "running" 59 + 60 + **Symptom**: A backfill job doesn't progress or stays in `pending`. 61 + 62 + **Causes**: 63 + 64 + - The backfill worker processes one job at a time. If another job is running, yours will wait. 65 + - The relay (`RELAY_URL`) may be unreachable or slow to respond. Check connectivity. 66 + - Individual PDS fetches can fail silently. The worker logs warnings and continues. Check server logs for details. 67 + 68 + See [Backfill](../guides/backfill) for how the process works. 69 + 70 + ## Records not appearing in real time 71 + 72 + **Symptom**: New records created on the network don't show up in queries. 73 + 74 + **Causes**: 75 + 76 + - HappyView receives real-time events via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap). Make sure Tap is running and connected to HappyView. See the [Tap documentation](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) for configuration. 77 + - No record-type lexicon exists for the collection. HappyView only indexes collections that have a corresponding record-type lexicon. 78 + - The Tap connection hasn't synced the new collection filter after a lexicon change. This should happen automatically. Check server logs for connection errors. 79 + 80 + ## OAuth or login issues 81 + 82 + OAuth is handled entirely by [AIP](https://github.com/graze-social/aip). If users can't log in or tokens aren't working: 83 + 84 + 1. Verify AIP is running and reachable at the configured `AIP_URL`. 85 + 2. Check that AIP has valid signing keys configured (`OAUTH_SIGNING_KEYS`). 86 + 3. Check that both HappyView and AIP have public URLs assigned (required for OAuth callbacks). 87 + 88 + See the [AIP documentation](https://github.com/graze-social/aip) for setup and debugging. 89 + 90 + ## Database connection errors 91 + 92 + **Symptom**: HappyView fails to start or returns 500 errors. 93 + 94 + **Causes**: 95 + 96 + - `DATABASE_URL` is not set or points to an unreachable Postgres instance. 97 + - The database user doesn't have sufficient permissions. HappyView needs to create tables (migrations run automatically on startup). 98 + - Postgres version is too old. HappyView requires Postgres 17+. 99 + 100 + See [Configuration](../getting-started/configuration) for environment variable details.
+208
docs/reference/xrpc-api.md
··· 1 + # XRPC API 2 + 3 + [XRPC](https://atproto.com/specs/xrpc) is the HTTP-based RPC protocol used by the AT Protocol. HappyView dynamically registers XRPC endpoints based on your uploaded [lexicons](../guides/lexicons): query lexicons become `GET /xrpc/{nsid}` routes, procedure lexicons become `POST /xrpc/{nsid}` routes. 4 + 5 + If a query or procedure lexicon has a [Lua script](../guides/scripting) attached, the script handles the request. Otherwise, HappyView uses built-in default behavior (described below). 6 + 7 + ## Auth 8 + 9 + - **Queries** (`GET /xrpc/{method}`): unauthenticated 10 + - **Procedures** (`POST /xrpc/{method}`): require an AIP-issued `Authorization: Bearer <token>` header 11 + - **getProfile**: requires auth 12 + - **uploadBlob**: requires auth 13 + 14 + ## Fixed endpoints 15 + 16 + These endpoints are always available regardless of which lexicons are loaded. 17 + 18 + ### Health check 19 + 20 + ``` 21 + GET /health 22 + ``` 23 + 24 + ```sh 25 + curl http://localhost:3000/health 26 + ``` 27 + 28 + **Response**: `200 OK` with body `ok` 29 + 30 + ### Get profile 31 + 32 + ``` 33 + GET /xrpc/app.bsky.actor.getProfile 34 + ``` 35 + 36 + Returns the authenticated user's profile, resolved from their PDS via PLC directory lookup. 37 + 38 + ```sh 39 + curl http://localhost:3000/xrpc/app.bsky.actor.getProfile \ 40 + -H "Authorization: Bearer $TOKEN" 41 + ``` 42 + 43 + **Response**: `200 OK` 44 + 45 + ```json 46 + { 47 + "did": "did:plc:abc123", 48 + "handle": "user.bsky.social", 49 + "displayName": "User Name", 50 + "description": "Bio text", 51 + "avatarURL": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=did:plc:abc123&cid=bafyabc" 52 + } 53 + ``` 54 + 55 + ### Upload blob 56 + 57 + ``` 58 + POST /xrpc/com.atproto.repo.uploadBlob 59 + ``` 60 + 61 + Proxies a blob upload to the authenticated user's PDS. Maximum size: 50MB. 62 + 63 + ```sh 64 + curl -X POST http://localhost:3000/xrpc/com.atproto.repo.uploadBlob \ 65 + -H "Authorization: Bearer $TOKEN" \ 66 + -H "Content-Type: image/png" \ 67 + --data-binary @image.png 68 + ``` 69 + 70 + **Response**: proxied from the user's PDS. 71 + 72 + ## Dynamic query endpoints 73 + 74 + Query endpoints are generated from lexicons with `type: "query"`. Without a [Lua script](../guides/scripting), they support two built-in modes depending on whether a `uri` parameter is provided. 75 + 76 + ### Single record 77 + 78 + ``` 79 + GET /xrpc/{method}?uri={at-uri} 80 + ``` 81 + 82 + ```sh 83 + curl "http://localhost:3000/xrpc/xyz.statusphere.listStatuses?uri=at%3A%2F%2Fdid%3Aplc%3Aabc%2Fxyz.statusphere.status%2Fabc123" 84 + ``` 85 + 86 + **Response**: `200 OK` 87 + 88 + ```json 89 + { 90 + "record": { 91 + "uri": "at://did:plc:abc/xyz.statusphere.status/abc123", 92 + "$type": "xyz.statusphere.status", 93 + "status": "\ud83d\ude0a", 94 + "createdAt": "2025-01-01T12:00:00Z" 95 + } 96 + } 97 + ``` 98 + 99 + Media blobs are automatically enriched with a `url` field pointing to the user's PDS. 100 + 101 + ### List records 102 + 103 + ``` 104 + GET /xrpc/{method}?limit=20&cursor=0&did=optional 105 + ``` 106 + 107 + | Param | Type | Default | Description | 108 + |-------|------|---------|-------------| 109 + | `limit` | integer | 20 | Max records to return (max 100) | 110 + | `cursor` | string | `0` | Pagination cursor (opaque, pass from previous response) | 111 + | `did` | string | --- | Filter records by DID | 112 + 113 + ```sh 114 + curl "http://localhost:3000/xrpc/xyz.statusphere.listStatuses?limit=10&did=did:plc:abc" 115 + ``` 116 + 117 + **Response**: `200 OK` 118 + 119 + ```json 120 + { 121 + "records": [ 122 + { 123 + "uri": "at://did:plc:abc/xyz.statusphere.status/abc123", 124 + "status": "\ud83d\ude0a", 125 + "createdAt": "2025-01-01T12:00:00Z" 126 + } 127 + ], 128 + "cursor": "10" 129 + } 130 + ``` 131 + 132 + The `cursor` field is present only when more records exist. 133 + 134 + ## Dynamic procedure endpoints 135 + 136 + Procedure endpoints are generated from lexicons with `type: "procedure"`. Without a [Lua script](../guides/scripting), HappyView auto-detects create vs update based on whether the request body contains a `uri` field. 137 + 138 + ### Create a record 139 + 140 + ``` 141 + POST /xrpc/{method} 142 + ``` 143 + 144 + When the body does **not** contain a `uri` field, a new record is created. 145 + 146 + ```sh 147 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setStatus \ 148 + -H "Authorization: Bearer $TOKEN" \ 149 + -H "Content-Type: application/json" \ 150 + -d '{ "status": "\ud83d\ude0a", "createdAt": "2025-01-01T12:00:00Z" }' 151 + ``` 152 + 153 + HappyView proxies this to the user's PDS as `com.atproto.repo.createRecord`, then indexes the created record locally. 154 + 155 + ### Update a record 156 + 157 + When the body **contains** a `uri` field, the existing record is updated. 158 + 159 + ```sh 160 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setStatus \ 161 + -H "Authorization: Bearer $TOKEN" \ 162 + -H "Content-Type: application/json" \ 163 + -d '{ 164 + "uri": "at://did:plc:abc/xyz.statusphere.status/abc123", 165 + "status": "\ud83c\udf1f", 166 + "createdAt": "2025-01-01T13:00:00Z" 167 + }' 168 + ``` 169 + 170 + HappyView proxies this to the user's PDS as `com.atproto.repo.putRecord`, then upserts the record locally. 171 + 172 + **Response** for both: proxied from the user's PDS. 173 + 174 + ## Errors 175 + 176 + All error responses return JSON with an `error` field: 177 + 178 + ```json 179 + { 180 + "error": "description of what went wrong" 181 + } 182 + ``` 183 + 184 + | Status | Meaning | Common causes | 185 + |--------|---------|---------------| 186 + | `400 Bad Request` | Invalid input | Missing required fields, malformed JSON, invalid AT URI | 187 + | `401 Unauthorized` | Authentication failed | Missing or invalid Bearer token. See [AIP documentation](https://github.com/graze-social/aip) for token issues | 188 + | `404 Not Found` | Method or record not found | XRPC method has no matching lexicon, or the requested record doesn't exist | 189 + | `500 Internal Server Error` | Server-side failure | Lua script error, database error, or upstream PDS failure | 190 + 191 + ### Lua script errors 192 + 193 + When a Lua script fails, the response is `500` with one of: 194 + 195 + - `{"error": "script execution failed"}`: syntax error, runtime error, or missing `handle()` function 196 + - `{"error": "script exceeded execution time limit"}`: the script hit the 1,000,000 instruction limit 197 + 198 + The full error details are logged server-side but not exposed to the client. See [Lua Scripting - Debugging](../guides/scripting#debugging) for how to diagnose script issues. 199 + 200 + ### PDS errors 201 + 202 + When a procedure proxies a write to the user's PDS and the PDS returns an error, HappyView forwards the PDS response status code and body directly to the client. 203 + 204 + ## Next steps 205 + 206 + - [Lua Scripting](../guides/scripting): Override the default query and procedure behavior with custom logic 207 + - [Lexicons](../guides/lexicons): Understand how lexicons generate these endpoints 208 + - [Admin API](admin-api): Manage lexicons and monitor your instance
+305
docs/tutorials/statusphere.md
··· 1 + # Tutorial: Statusphere with HappyView 2 + 3 + [Statusphere](https://github.com/bluesky-social/statusphere-example-app) is an example AT Protocol application where users set their current status as a single emoji. It's a great way to learn how HappyView works because the data model is simple but the queries are interesting. 4 + 5 + In this tutorial, you'll set up HappyView to act as the AppView for Statusphere. By the end, you'll have automatically indexed records and automatically generated XPRC endpoints. 6 + 7 + :::tip 8 + This tutorial assumes you have a running HappyView instance. If you don't, start with the [Quickstart](../getting-started/deployment/railway) or one of the local development guides ([Docker](../getting-started/deployment/docker), [from source](../getting-started/deployment/other)). 9 + ::: 10 + 11 + ## The Statusphere lexicon 12 + 13 + Statusphere uses a single record type, `xyz.statusphere.status`. Each record has two fields: 14 + 15 + - `status`: a single emoji 16 + - `createdAt`: a timestamp 17 + 18 + Users can set their status as many times as they want. Each status is a new record in their repository, keyed by a TID (timestamp-based identifier). The most recent record is their "current" status. 19 + 20 + For more background on how the app works, see the [ATProto Statusphere guide](https://atproto.com/guides/applications). 21 + 22 + ## Step 1: Upload the record lexicon 23 + 24 + First, upload the `xyz.statusphere.status` lexicon to HappyView. This tells HappyView to start indexing Statusphere records from across the network as they're created, updated, or deleted. 25 + 26 + The examples below use `$TOKEN` as a placeholder for an AIP-issued access token. See [Authentication](../getting-started/authentication) for how to get one. 27 + 28 + ```sh 29 + curl -X POST http://localhost:3000/admin/lexicons \ 30 + -H "Authorization: Bearer $TOKEN" \ 31 + -H "Content-Type: application/json" \ 32 + -d '{ 33 + "lexicon_json": { 34 + "lexicon": 1, 35 + "id": "xyz.statusphere.status", 36 + "defs": { 37 + "main": { 38 + "type": "record", 39 + "key": "tid", 40 + "record": { 41 + "type": "object", 42 + "required": ["status", "createdAt"], 43 + "properties": { 44 + "status": { "type": "string", "maxGraphemes": 1 }, 45 + "createdAt": { "type": "string", "format": "datetime" } 46 + } 47 + } 48 + } 49 + } 50 + }, 51 + "backfill": true 52 + }' 53 + ``` 54 + 55 + HappyView now subscribes to `xyz.statusphere.status` via Tap. The `backfill` flag tells HappyView to also index existing status records from the network. You can monitor progress with `GET /admin/backfill/status` or the [dashboard](../getting-started/dashboard). 56 + 57 + :::tip 58 + Since the `xyz.statusphere.status` lexicon is [published on the AT Protocol network](../guides/lexicons#network-lexicons), you can also add it as a network lexicon instead of uploading the JSON manually: 59 + 60 + ```sh 61 + curl -X POST http://localhost:3000/admin/network-lexicons \ 62 + -H "Authorization: Bearer $TOKEN" \ 63 + -H "Content-Type: application/json" \ 64 + -d '{ "nsid": "xyz.statusphere.status" }' 65 + ``` 66 + 67 + ::: 68 + 69 + ## Step 2: Verify records are being indexed 70 + 71 + Once the backfill starts processing, you should see records appearing. Check the stats: 72 + 73 + ```sh 74 + curl http://localhost:3000/admin/stats \ 75 + -H "Authorization: Bearer $TOKEN" 76 + ``` 77 + 78 + ```json 79 + { 80 + "total_records": 1234, 81 + "collections": [{ "collection": "xyz.statusphere.status", "count": 1234 }] 82 + } 83 + ``` 84 + 85 + ## Step 3: Add a query lexicon for listing statuses 86 + 87 + Now add a query endpoint to read the indexed data. Upload a query lexicon with `target_collection` pointing at the record collection from Step 1: 88 + 89 + ```sh 90 + curl -X POST http://localhost:3000/admin/lexicons \ 91 + -H "Authorization: Bearer $TOKEN" \ 92 + -H "Content-Type: application/json" \ 93 + -d '{ 94 + "lexicon_json": { 95 + "lexicon": 1, 96 + "id": "xyz.statusphere.listStatuses", 97 + "defs": { 98 + "main": { 99 + "type": "query", 100 + "output": { "encoding": "application/json" } 101 + } 102 + } 103 + }, 104 + "target_collection": "xyz.statusphere.status" 105 + }' 106 + ``` 107 + 108 + This creates a `GET /xrpc/xyz.statusphere.listStatuses` endpoint. Without a Lua script, it uses HappyView's built-in default behavior: listing records with `limit`, `cursor`, and `did` parameters, or fetching a single record by `uri`. Try it: 109 + 110 + ```sh 111 + curl "http://localhost:3000/xrpc/xyz.statusphere.listStatuses?limit=5" 112 + ``` 113 + 114 + ```json 115 + { 116 + "records": [ 117 + { 118 + "uri": "at://did:plc:abc/xyz.statusphere.status/3abc123", 119 + "status": "\ud83d\ude0a", 120 + "createdAt": "2025-01-01T12:00:00Z" 121 + }, 122 + { 123 + "uri": "at://did:plc:def/xyz.statusphere.status/3def456", 124 + "status": "\ud83c\udf1f", 125 + "createdAt": "2025-01-01T11:30:00Z" 126 + } 127 + ], 128 + "cursor": "5" 129 + } 130 + ``` 131 + 132 + See [XRPC API](../reference/xrpc-api) for the full default query behavior. 133 + 134 + ## Step 4: Enhance the query with a Lua script 135 + 136 + The default query behavior works, but let's customize it with a [Lua script](../guides/scripting). Here's a script that handles single-record lookups by URI and paginated listing with an optional DID filter: 137 + 138 + ```lua 139 + function handle() 140 + if params.uri then 141 + local record = db.get(params.uri) 142 + if not record then 143 + return { error = "not found" } 144 + end 145 + return { record = record } 146 + end 147 + 148 + return db.query({ 149 + collection = collection, 150 + did = params.did, 151 + limit = tonumber(params.limit) or 20, 152 + offset = tonumber(params.cursor) or 0, 153 + }) 154 + end 155 + ``` 156 + 157 + Re-upload the lexicon with parameters defined in the schema and the script attached: 158 + 159 + ```sh 160 + LEXICON='{ 161 + "lexicon": 1, 162 + "id": "xyz.statusphere.listStatuses", 163 + "defs": { 164 + "main": { 165 + "type": "query", 166 + "parameters": { 167 + "type": "params", 168 + "properties": { 169 + "uri": { "type": "string" }, 170 + "did": { "type": "string" }, 171 + "limit": { "type": "integer" }, 172 + "cursor": { "type": "string" } 173 + } 174 + }, 175 + "output": { "encoding": "application/json" } 176 + } 177 + } 178 + }' 179 + 180 + SCRIPT='function handle() 181 + if params.uri then 182 + local record = db.get(params.uri) 183 + if not record then 184 + return { error = "not found" } 185 + end 186 + return { record = record } 187 + end 188 + 189 + return db.query({ 190 + collection = collection, 191 + did = params.did, 192 + limit = tonumber(params.limit) or 20, 193 + offset = tonumber(params.cursor) or 0, 194 + }) 195 + end' 196 + 197 + curl -X POST http://localhost:3000/admin/lexicons \ 198 + -H "Authorization: Bearer $TOKEN" \ 199 + -H "Content-Type: application/json" \ 200 + -d "{ 201 + \"lexicon_json\": $LEXICON, 202 + \"target_collection\": \"xyz.statusphere.status\", 203 + \"script\": \"$SCRIPT\" 204 + }" 205 + ``` 206 + 207 + The endpoint now uses your custom logic. Filter by a specific user: 208 + 209 + ```sh 210 + curl "http://localhost:3000/xrpc/xyz.statusphere.listStatuses?did=did:plc:abc&limit=1" 211 + ``` 212 + 213 + Fetch a single record by URI: 214 + 215 + ```sh 216 + curl "http://localhost:3000/xrpc/xyz.statusphere.listStatuses?uri=at://did:plc:abc/xyz.statusphere.status/3abc123" 217 + ``` 218 + 219 + ## Step 5: Add a procedure lexicon for setting status 220 + 221 + Add a write endpoint so users can set their status through your AppView. This creates a `POST /xrpc/xyz.statusphere.setStatus` endpoint that proxies writes to the user's PDS. 222 + 223 + The Lua script auto-fills `createdAt` and uses the authenticated user's DID: 224 + 225 + ```lua 226 + function handle() 227 + local r = Record(collection, { 228 + status = input.status, 229 + createdAt = now(), 230 + }) 231 + r:save() 232 + return { uri = r._uri, cid = r._cid } 233 + end 234 + ``` 235 + 236 + Upload the procedure lexicon with this script: 237 + 238 + ```sh 239 + LEXICON='{ 240 + "lexicon": 1, 241 + "id": "xyz.statusphere.setStatus", 242 + "defs": { 243 + "main": { 244 + "type": "procedure", 245 + "input": { "encoding": "application/json" }, 246 + "output": { "encoding": "application/json" } 247 + } 248 + } 249 + }' 250 + 251 + SCRIPT='function handle() 252 + local r = Record(collection, { 253 + status = input.status, 254 + createdAt = now(), 255 + }) 256 + r:save() 257 + return { uri = r._uri, cid = r._cid } 258 + end' 259 + 260 + curl -X POST http://localhost:3000/admin/lexicons \ 261 + -H "Authorization: Bearer $TOKEN" \ 262 + -H "Content-Type: application/json" \ 263 + -d "{ 264 + \"lexicon_json\": $LEXICON, 265 + \"target_collection\": \"xyz.statusphere.status\", 266 + \"script\": \"$SCRIPT\" 267 + }" 268 + ``` 269 + 270 + ## Step 6: Test the procedure endpoint 271 + 272 + Set a status (requires authentication): 273 + 274 + ```sh 275 + curl -X POST http://localhost:3000/xrpc/xyz.statusphere.setStatus \ 276 + -H "Authorization: Bearer $TOKEN" \ 277 + -H "Content-Type: application/json" \ 278 + -d '{ "status": "\ud83d\ude80" }' 279 + ``` 280 + 281 + ```json 282 + { 283 + "uri": "at://did:plc:yourDID/xyz.statusphere.status/3xyz789", 284 + "cid": "bafyreiabc123..." 285 + } 286 + ``` 287 + 288 + The record is created on your PDS and immediately indexed by HappyView. 289 + 290 + ## What you've built 291 + 292 + With three lexicon uploads and a few lines of Lua, you have a complete Statusphere AppView: 293 + 294 + - **Real-time indexing** of `xyz.statusphere.status` records from the entire AT Protocol network 295 + - **Historical backfill** of existing status records 296 + - **A query endpoint** (`xyz.statusphere.listStatuses`) with filtering, pagination, and single-record lookups 297 + - **A write endpoint** (`xyz.statusphere.setStatus`) that creates records on the user's PDS and indexes them locally 298 + 299 + ## Next steps 300 + 301 + - [Lua Scripting](../guides/scripting): Explore the full Record and database APIs to build more complex queries 302 + - [Lexicons](../guides/lexicons): Learn about network lexicons, the backfill flag, and target collections 303 + - [XRPC API](../reference/xrpc-api): Understand how the generated endpoints behave 304 + - [Statusphere example app](https://github.com/bluesky-social/statusphere-example-app): See the full Statusphere frontend 305 + - [ATProto Statusphere guide](https://atproto.com/guides/applications): Deep dive into how the app works at the protocol level
-171
docs/xrpc-api.md
··· 1 - # XRPC API 2 - 3 - HappyView dynamically registers XRPC endpoints based on uploaded lexicons. Query lexicons become `GET /xrpc/{nsid}` routes, procedure lexicons become `POST /xrpc/{nsid}` routes. 4 - 5 - ## Auth 6 - 7 - - **Queries** (`GET /xrpc/{method}`): unauthenticated 8 - - **Procedures** (`POST /xrpc/{method}`): require an AIP-issued `Authorization: Bearer <token>` header 9 - - **getProfile**: requires auth 10 - - **uploadBlob**: requires auth 11 - 12 - --- 13 - 14 - ## Fixed endpoints 15 - 16 - ### Health check 17 - 18 - ``` 19 - GET /health 20 - ``` 21 - 22 - ```sh 23 - curl http://localhost:3000/health 24 - ``` 25 - 26 - **Response**: `200 OK` with body `ok` 27 - 28 - ### Get profile 29 - 30 - ``` 31 - GET /xrpc/app.bsky.actor.getProfile 32 - ``` 33 - 34 - Returns the authenticated user's profile, resolved from their PDS via PLC directory lookup. 35 - 36 - ```sh 37 - curl http://localhost:3000/xrpc/app.bsky.actor.getProfile \ 38 - -H "Authorization: Bearer $TOKEN" 39 - ``` 40 - 41 - **Response**: `200 OK` 42 - 43 - ```json 44 - { 45 - "did": "did:plc:abc123", 46 - "handle": "user.bsky.social", 47 - "displayName": "User Name", 48 - "description": "Bio text", 49 - "avatarURL": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=did:plc:abc123&cid=bafyabc" 50 - } 51 - ``` 52 - 53 - ### Upload blob 54 - 55 - ``` 56 - POST /xrpc/com.atproto.repo.uploadBlob 57 - ``` 58 - 59 - Proxies a blob upload to the authenticated user's PDS. Maximum size: 50MB. 60 - 61 - ```sh 62 - curl -X POST http://localhost:3000/xrpc/com.atproto.repo.uploadBlob \ 63 - -H "Authorization: Bearer $TOKEN" \ 64 - -H "Content-Type: image/png" \ 65 - --data-binary @image.png 66 - ``` 67 - 68 - **Response**: proxied from the user's PDS. 69 - 70 - --- 71 - 72 - ## Dynamic query endpoints 73 - 74 - Query endpoints are generated from lexicons with `type: "query"`. They support two modes depending on whether a `uri` parameter is provided. 75 - 76 - ### Single record 77 - 78 - ``` 79 - GET /xrpc/{method}?uri={at-uri} 80 - ``` 81 - 82 - ```sh 83 - curl "http://localhost:3000/xrpc/games.gamesgamesgamesgames.listGames?uri=at%3A%2F%2Fdid%3Aplc%3Aabc%2Fgames.gamesgamesgamesgames.game%2Fabc123" 84 - ``` 85 - 86 - **Response**: `200 OK` 87 - 88 - ```json 89 - { 90 - "record": { 91 - "uri": "at://did:plc:abc/games.gamesgamesgamesgames.game/abc123", 92 - "$type": "games.gamesgamesgamesgames.game", 93 - "title": "My Game" 94 - } 95 - } 96 - ``` 97 - 98 - Media blobs are automatically enriched with a `url` field pointing to the user's PDS. 99 - 100 - ### List records 101 - 102 - ``` 103 - GET /xrpc/{method}?limit=20&cursor=0&did=optional 104 - ``` 105 - 106 - | Param | Type | Default | Description | 107 - |-------|------|---------|-------------| 108 - | `limit` | integer | 20 | Max records to return (max 100) | 109 - | `cursor` | string | `0` | Pagination cursor (opaque, pass from previous response) | 110 - | `did` | string | --- | Filter records by DID | 111 - 112 - ```sh 113 - curl "http://localhost:3000/xrpc/games.gamesgamesgamesgames.listGames?limit=10&did=did:plc:abc" 114 - ``` 115 - 116 - **Response**: `200 OK` 117 - 118 - ```json 119 - { 120 - "records": [ 121 - { 122 - "uri": "at://did:plc:abc/games.gamesgamesgamesgames.game/abc123", 123 - "title": "My Game" 124 - } 125 - ], 126 - "cursor": "10" 127 - } 128 - ``` 129 - 130 - The `cursor` field is present only when more records exist. 131 - 132 - --- 133 - 134 - ## Dynamic procedure endpoints 135 - 136 - Procedure endpoints are generated from lexicons with `type: "procedure"`. HappyView auto-detects create vs update based on whether the request body contains a `uri` field. 137 - 138 - ### Create a record 139 - 140 - ``` 141 - POST /xrpc/{method} 142 - ``` 143 - 144 - When the body does **not** contain a `uri` field, a new record is created. 145 - 146 - ```sh 147 - curl -X POST http://localhost:3000/xrpc/games.gamesgamesgamesgames.createGame \ 148 - -H "Authorization: Bearer $TOKEN" \ 149 - -H "Content-Type: application/json" \ 150 - -d '{ "title": "My Game" }' 151 - ``` 152 - 153 - HappyView proxies this to the user's PDS as `com.atproto.repo.createRecord`, then indexes the created record locally. 154 - 155 - ### Update a record 156 - 157 - When the body **contains** a `uri` field, the existing record is updated. 158 - 159 - ```sh 160 - curl -X POST http://localhost:3000/xrpc/games.gamesgamesgamesgames.createGame \ 161 - -H "Authorization: Bearer $TOKEN" \ 162 - -H "Content-Type: application/json" \ 163 - -d '{ 164 - "uri": "at://did:plc:abc/games.gamesgamesgamesgames.game/abc123", 165 - "title": "Updated Title" 166 - }' 167 - ``` 168 - 169 - HappyView proxies this to the user's PDS as `com.atproto.repo.putRecord`, then upserts the record locally. 170 - 171 - **Response** for both: proxied from the user's PDS.
+13 -2
docusaurus.config.ts
··· 6 6 tagline: "Lexicon-driven ATProto AppView", 7 7 url: "https://happyview.dev", 8 8 baseUrl: "/", 9 + favicon: "img/favicon.png", 9 10 onBrokenLinks: "throw", 11 + markdown: { 12 + mermaid: true, 13 + }, 14 + themes: ["@docusaurus/theme-mermaid"], 10 15 11 16 i18n: { 12 17 defaultLocale: "en", ··· 33 38 themeConfig: { 34 39 navbar: { 35 40 title: "HappyView", 41 + logo: { 42 + alt: "HappyView Logo", 43 + src: "img/logo.png", 44 + }, 36 45 items: [ 37 46 { 38 47 type: "docSidebar", ··· 47 56 }, 48 57 ], 49 58 }, 59 + prism: { 60 + additionalLanguages: ["lua"], 61 + }, 50 62 footer: { 51 - style: "dark", 52 - copyright: `Copyright \u00a9 ${new Date().getFullYear()} HappyView.`, 63 + copyright: `Copyright \u00a9 ${new Date().getFullYear()} [Birbhouse Games](https://birb.house).`, 53 64 }, 54 65 } satisfies Preset.ThemeConfig, 55 66 };
+1255
package-lock.json
··· 10 10 "dependencies": { 11 11 "@docusaurus/core": "^3.7.0", 12 12 "@docusaurus/preset-classic": "^3.7.0", 13 + "@docusaurus/theme-mermaid": "^3.9.2", 14 + "prismjs": "^1.30.0", 13 15 "react": "^19.0.0", 14 16 "react-dom": "^19.0.0" 15 17 }, ··· 250 252 }, 251 253 "engines": { 252 254 "node": ">= 14.0.0" 255 + } 256 + }, 257 + "node_modules/@antfu/install-pkg": { 258 + "version": "1.1.0", 259 + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", 260 + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", 261 + "license": "MIT", 262 + "dependencies": { 263 + "package-manager-detector": "^1.3.0", 264 + "tinyexec": "^1.0.1" 265 + }, 266 + "funding": { 267 + "url": "https://github.com/sponsors/antfu" 253 268 } 254 269 }, 255 270 "node_modules/@babel/code-frame": { ··· 1966 1981 "node": ">=6.9.0" 1967 1982 } 1968 1983 }, 1984 + "node_modules/@braintree/sanitize-url": { 1985 + "version": "7.1.2", 1986 + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", 1987 + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", 1988 + "license": "MIT" 1989 + }, 1990 + "node_modules/@chevrotain/cst-dts-gen": { 1991 + "version": "11.1.1", 1992 + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz", 1993 + "integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==", 1994 + "license": "Apache-2.0", 1995 + "dependencies": { 1996 + "@chevrotain/gast": "11.1.1", 1997 + "@chevrotain/types": "11.1.1", 1998 + "lodash-es": "4.17.23" 1999 + } 2000 + }, 2001 + "node_modules/@chevrotain/gast": { 2002 + "version": "11.1.1", 2003 + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz", 2004 + "integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==", 2005 + "license": "Apache-2.0", 2006 + "dependencies": { 2007 + "@chevrotain/types": "11.1.1", 2008 + "lodash-es": "4.17.23" 2009 + } 2010 + }, 2011 + "node_modules/@chevrotain/regexp-to-ast": { 2012 + "version": "11.1.1", 2013 + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz", 2014 + "integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==", 2015 + "license": "Apache-2.0" 2016 + }, 2017 + "node_modules/@chevrotain/types": { 2018 + "version": "11.1.1", 2019 + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz", 2020 + "integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==", 2021 + "license": "Apache-2.0" 2022 + }, 2023 + "node_modules/@chevrotain/utils": { 2024 + "version": "11.1.1", 2025 + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz", 2026 + "integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==", 2027 + "license": "Apache-2.0" 2028 + }, 1969 2029 "node_modules/@colors/colors": { 1970 2030 "version": "1.5.0", 1971 2031 "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", ··· 3911 3971 "react-dom": "^18.0.0 || ^19.0.0" 3912 3972 } 3913 3973 }, 3974 + "node_modules/@docusaurus/theme-mermaid": { 3975 + "version": "3.9.2", 3976 + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", 3977 + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", 3978 + "license": "MIT", 3979 + "dependencies": { 3980 + "@docusaurus/core": "3.9.2", 3981 + "@docusaurus/module-type-aliases": "3.9.2", 3982 + "@docusaurus/theme-common": "3.9.2", 3983 + "@docusaurus/types": "3.9.2", 3984 + "@docusaurus/utils-validation": "3.9.2", 3985 + "mermaid": ">=11.6.0", 3986 + "tslib": "^2.6.0" 3987 + }, 3988 + "engines": { 3989 + "node": ">=20.0" 3990 + }, 3991 + "peerDependencies": { 3992 + "@mermaid-js/layout-elk": "^0.1.9", 3993 + "react": "^18.0.0 || ^19.0.0", 3994 + "react-dom": "^18.0.0 || ^19.0.0" 3995 + }, 3996 + "peerDependenciesMeta": { 3997 + "@mermaid-js/layout-elk": { 3998 + "optional": true 3999 + } 4000 + } 4001 + }, 3914 4002 "node_modules/@docusaurus/theme-search-algolia": { 3915 4003 "version": "3.9.2", 3916 4004 "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", ··· 4077 4165 "@hapi/hoek": "^9.0.0" 4078 4166 } 4079 4167 }, 4168 + "node_modules/@iconify/types": { 4169 + "version": "2.0.0", 4170 + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", 4171 + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", 4172 + "license": "MIT" 4173 + }, 4174 + "node_modules/@iconify/utils": { 4175 + "version": "3.1.0", 4176 + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", 4177 + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", 4178 + "license": "MIT", 4179 + "dependencies": { 4180 + "@antfu/install-pkg": "^1.1.0", 4181 + "@iconify/types": "^2.0.0", 4182 + "mlly": "^1.8.0" 4183 + } 4184 + }, 4080 4185 "node_modules/@jest/schemas": { 4081 4186 "version": "29.6.3", 4082 4187 "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", ··· 4629 4734 "peerDependencies": { 4630 4735 "@types/react": ">=16", 4631 4736 "react": ">=16" 4737 + } 4738 + }, 4739 + "node_modules/@mermaid-js/parser": { 4740 + "version": "1.0.0", 4741 + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz", 4742 + "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==", 4743 + "license": "MIT", 4744 + "dependencies": { 4745 + "langium": "^4.0.0" 4632 4746 } 4633 4747 }, 4634 4748 "node_modules/@noble/hashes": { ··· 5240 5354 "@types/node": "*" 5241 5355 } 5242 5356 }, 5357 + "node_modules/@types/d3": { 5358 + "version": "7.4.3", 5359 + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", 5360 + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", 5361 + "license": "MIT", 5362 + "dependencies": { 5363 + "@types/d3-array": "*", 5364 + "@types/d3-axis": "*", 5365 + "@types/d3-brush": "*", 5366 + "@types/d3-chord": "*", 5367 + "@types/d3-color": "*", 5368 + "@types/d3-contour": "*", 5369 + "@types/d3-delaunay": "*", 5370 + "@types/d3-dispatch": "*", 5371 + "@types/d3-drag": "*", 5372 + "@types/d3-dsv": "*", 5373 + "@types/d3-ease": "*", 5374 + "@types/d3-fetch": "*", 5375 + "@types/d3-force": "*", 5376 + "@types/d3-format": "*", 5377 + "@types/d3-geo": "*", 5378 + "@types/d3-hierarchy": "*", 5379 + "@types/d3-interpolate": "*", 5380 + "@types/d3-path": "*", 5381 + "@types/d3-polygon": "*", 5382 + "@types/d3-quadtree": "*", 5383 + "@types/d3-random": "*", 5384 + "@types/d3-scale": "*", 5385 + "@types/d3-scale-chromatic": "*", 5386 + "@types/d3-selection": "*", 5387 + "@types/d3-shape": "*", 5388 + "@types/d3-time": "*", 5389 + "@types/d3-time-format": "*", 5390 + "@types/d3-timer": "*", 5391 + "@types/d3-transition": "*", 5392 + "@types/d3-zoom": "*" 5393 + } 5394 + }, 5395 + "node_modules/@types/d3-array": { 5396 + "version": "3.2.2", 5397 + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", 5398 + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", 5399 + "license": "MIT" 5400 + }, 5401 + "node_modules/@types/d3-axis": { 5402 + "version": "3.0.6", 5403 + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", 5404 + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", 5405 + "license": "MIT", 5406 + "dependencies": { 5407 + "@types/d3-selection": "*" 5408 + } 5409 + }, 5410 + "node_modules/@types/d3-brush": { 5411 + "version": "3.0.6", 5412 + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", 5413 + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", 5414 + "license": "MIT", 5415 + "dependencies": { 5416 + "@types/d3-selection": "*" 5417 + } 5418 + }, 5419 + "node_modules/@types/d3-chord": { 5420 + "version": "3.0.6", 5421 + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", 5422 + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", 5423 + "license": "MIT" 5424 + }, 5425 + "node_modules/@types/d3-color": { 5426 + "version": "3.1.3", 5427 + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", 5428 + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", 5429 + "license": "MIT" 5430 + }, 5431 + "node_modules/@types/d3-contour": { 5432 + "version": "3.0.6", 5433 + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", 5434 + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", 5435 + "license": "MIT", 5436 + "dependencies": { 5437 + "@types/d3-array": "*", 5438 + "@types/geojson": "*" 5439 + } 5440 + }, 5441 + "node_modules/@types/d3-delaunay": { 5442 + "version": "6.0.4", 5443 + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", 5444 + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", 5445 + "license": "MIT" 5446 + }, 5447 + "node_modules/@types/d3-dispatch": { 5448 + "version": "3.0.7", 5449 + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", 5450 + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", 5451 + "license": "MIT" 5452 + }, 5453 + "node_modules/@types/d3-drag": { 5454 + "version": "3.0.7", 5455 + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", 5456 + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", 5457 + "license": "MIT", 5458 + "dependencies": { 5459 + "@types/d3-selection": "*" 5460 + } 5461 + }, 5462 + "node_modules/@types/d3-dsv": { 5463 + "version": "3.0.7", 5464 + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", 5465 + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", 5466 + "license": "MIT" 5467 + }, 5468 + "node_modules/@types/d3-ease": { 5469 + "version": "3.0.2", 5470 + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", 5471 + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", 5472 + "license": "MIT" 5473 + }, 5474 + "node_modules/@types/d3-fetch": { 5475 + "version": "3.0.7", 5476 + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", 5477 + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", 5478 + "license": "MIT", 5479 + "dependencies": { 5480 + "@types/d3-dsv": "*" 5481 + } 5482 + }, 5483 + "node_modules/@types/d3-force": { 5484 + "version": "3.0.10", 5485 + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", 5486 + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", 5487 + "license": "MIT" 5488 + }, 5489 + "node_modules/@types/d3-format": { 5490 + "version": "3.0.4", 5491 + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", 5492 + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", 5493 + "license": "MIT" 5494 + }, 5495 + "node_modules/@types/d3-geo": { 5496 + "version": "3.1.0", 5497 + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", 5498 + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", 5499 + "license": "MIT", 5500 + "dependencies": { 5501 + "@types/geojson": "*" 5502 + } 5503 + }, 5504 + "node_modules/@types/d3-hierarchy": { 5505 + "version": "3.1.7", 5506 + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", 5507 + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", 5508 + "license": "MIT" 5509 + }, 5510 + "node_modules/@types/d3-interpolate": { 5511 + "version": "3.0.4", 5512 + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", 5513 + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", 5514 + "license": "MIT", 5515 + "dependencies": { 5516 + "@types/d3-color": "*" 5517 + } 5518 + }, 5519 + "node_modules/@types/d3-path": { 5520 + "version": "3.1.1", 5521 + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", 5522 + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", 5523 + "license": "MIT" 5524 + }, 5525 + "node_modules/@types/d3-polygon": { 5526 + "version": "3.0.2", 5527 + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", 5528 + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", 5529 + "license": "MIT" 5530 + }, 5531 + "node_modules/@types/d3-quadtree": { 5532 + "version": "3.0.6", 5533 + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", 5534 + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", 5535 + "license": "MIT" 5536 + }, 5537 + "node_modules/@types/d3-random": { 5538 + "version": "3.0.3", 5539 + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", 5540 + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", 5541 + "license": "MIT" 5542 + }, 5543 + "node_modules/@types/d3-scale": { 5544 + "version": "4.0.9", 5545 + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", 5546 + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", 5547 + "license": "MIT", 5548 + "dependencies": { 5549 + "@types/d3-time": "*" 5550 + } 5551 + }, 5552 + "node_modules/@types/d3-scale-chromatic": { 5553 + "version": "3.1.0", 5554 + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", 5555 + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", 5556 + "license": "MIT" 5557 + }, 5558 + "node_modules/@types/d3-selection": { 5559 + "version": "3.0.11", 5560 + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", 5561 + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", 5562 + "license": "MIT" 5563 + }, 5564 + "node_modules/@types/d3-shape": { 5565 + "version": "3.1.8", 5566 + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", 5567 + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", 5568 + "license": "MIT", 5569 + "dependencies": { 5570 + "@types/d3-path": "*" 5571 + } 5572 + }, 5573 + "node_modules/@types/d3-time": { 5574 + "version": "3.0.4", 5575 + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", 5576 + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", 5577 + "license": "MIT" 5578 + }, 5579 + "node_modules/@types/d3-time-format": { 5580 + "version": "4.0.3", 5581 + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", 5582 + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", 5583 + "license": "MIT" 5584 + }, 5585 + "node_modules/@types/d3-timer": { 5586 + "version": "3.0.2", 5587 + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", 5588 + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", 5589 + "license": "MIT" 5590 + }, 5591 + "node_modules/@types/d3-transition": { 5592 + "version": "3.0.9", 5593 + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", 5594 + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", 5595 + "license": "MIT", 5596 + "dependencies": { 5597 + "@types/d3-selection": "*" 5598 + } 5599 + }, 5600 + "node_modules/@types/d3-zoom": { 5601 + "version": "3.0.8", 5602 + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", 5603 + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", 5604 + "license": "MIT", 5605 + "dependencies": { 5606 + "@types/d3-interpolate": "*", 5607 + "@types/d3-selection": "*" 5608 + } 5609 + }, 5243 5610 "node_modules/@types/debug": { 5244 5611 "version": "4.1.12", 5245 5612 "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", ··· 5307 5674 "@types/range-parser": "*", 5308 5675 "@types/send": "*" 5309 5676 } 5677 + }, 5678 + "node_modules/@types/geojson": { 5679 + "version": "7946.0.16", 5680 + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", 5681 + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", 5682 + "license": "MIT" 5310 5683 }, 5311 5684 "node_modules/@types/gtag.js": { 5312 5685 "version": "0.0.12", ··· 5544 5917 "dependencies": { 5545 5918 "@types/node": "*" 5546 5919 } 5920 + }, 5921 + "node_modules/@types/trusted-types": { 5922 + "version": "2.0.7", 5923 + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", 5924 + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", 5925 + "license": "MIT", 5926 + "optional": true 5547 5927 }, 5548 5928 "node_modules/@types/unist": { 5549 5929 "version": "3.0.3", ··· 6682 7062 "url": "https://github.com/sponsors/fb55" 6683 7063 } 6684 7064 }, 7065 + "node_modules/chevrotain": { 7066 + "version": "11.1.1", 7067 + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", 7068 + "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", 7069 + "license": "Apache-2.0", 7070 + "peer": true, 7071 + "dependencies": { 7072 + "@chevrotain/cst-dts-gen": "11.1.1", 7073 + "@chevrotain/gast": "11.1.1", 7074 + "@chevrotain/regexp-to-ast": "11.1.1", 7075 + "@chevrotain/types": "11.1.1", 7076 + "@chevrotain/utils": "11.1.1", 7077 + "lodash-es": "4.17.23" 7078 + } 7079 + }, 7080 + "node_modules/chevrotain-allstar": { 7081 + "version": "0.3.1", 7082 + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", 7083 + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", 7084 + "license": "MIT", 7085 + "dependencies": { 7086 + "lodash-es": "^4.17.21" 7087 + }, 7088 + "peerDependencies": { 7089 + "chevrotain": "^11.0.0" 7090 + } 7091 + }, 6685 7092 "node_modules/chokidar": { 6686 7093 "version": "3.6.0", 6687 7094 "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", ··· 6971 7378 "version": "0.0.1", 6972 7379 "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 6973 7380 "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 7381 + "license": "MIT" 7382 + }, 7383 + "node_modules/confbox": { 7384 + "version": "0.1.8", 7385 + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", 7386 + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", 6974 7387 "license": "MIT" 6975 7388 }, 6976 7389 "node_modules/config-chain": { ··· 7173 7586 "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", 7174 7587 "license": "MIT" 7175 7588 }, 7589 + "node_modules/cose-base": { 7590 + "version": "1.0.3", 7591 + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", 7592 + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", 7593 + "license": "MIT", 7594 + "dependencies": { 7595 + "layout-base": "^1.0.0" 7596 + } 7597 + }, 7176 7598 "node_modules/cosmiconfig": { 7177 7599 "version": "8.3.6", 7178 7600 "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", ··· 7659 8081 "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", 7660 8082 "license": "MIT" 7661 8083 }, 8084 + "node_modules/cytoscape": { 8085 + "version": "3.33.1", 8086 + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", 8087 + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", 8088 + "license": "MIT", 8089 + "peer": true, 8090 + "engines": { 8091 + "node": ">=0.10" 8092 + } 8093 + }, 8094 + "node_modules/cytoscape-cose-bilkent": { 8095 + "version": "4.1.0", 8096 + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", 8097 + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", 8098 + "license": "MIT", 8099 + "dependencies": { 8100 + "cose-base": "^1.0.0" 8101 + }, 8102 + "peerDependencies": { 8103 + "cytoscape": "^3.2.0" 8104 + } 8105 + }, 8106 + "node_modules/cytoscape-fcose": { 8107 + "version": "2.2.0", 8108 + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", 8109 + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", 8110 + "license": "MIT", 8111 + "dependencies": { 8112 + "cose-base": "^2.2.0" 8113 + }, 8114 + "peerDependencies": { 8115 + "cytoscape": "^3.2.0" 8116 + } 8117 + }, 8118 + "node_modules/cytoscape-fcose/node_modules/cose-base": { 8119 + "version": "2.2.0", 8120 + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", 8121 + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", 8122 + "license": "MIT", 8123 + "dependencies": { 8124 + "layout-base": "^2.0.0" 8125 + } 8126 + }, 8127 + "node_modules/cytoscape-fcose/node_modules/layout-base": { 8128 + "version": "2.0.1", 8129 + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", 8130 + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", 8131 + "license": "MIT" 8132 + }, 8133 + "node_modules/d3": { 8134 + "version": "7.9.0", 8135 + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", 8136 + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", 8137 + "license": "ISC", 8138 + "dependencies": { 8139 + "d3-array": "3", 8140 + "d3-axis": "3", 8141 + "d3-brush": "3", 8142 + "d3-chord": "3", 8143 + "d3-color": "3", 8144 + "d3-contour": "4", 8145 + "d3-delaunay": "6", 8146 + "d3-dispatch": "3", 8147 + "d3-drag": "3", 8148 + "d3-dsv": "3", 8149 + "d3-ease": "3", 8150 + "d3-fetch": "3", 8151 + "d3-force": "3", 8152 + "d3-format": "3", 8153 + "d3-geo": "3", 8154 + "d3-hierarchy": "3", 8155 + "d3-interpolate": "3", 8156 + "d3-path": "3", 8157 + "d3-polygon": "3", 8158 + "d3-quadtree": "3", 8159 + "d3-random": "3", 8160 + "d3-scale": "4", 8161 + "d3-scale-chromatic": "3", 8162 + "d3-selection": "3", 8163 + "d3-shape": "3", 8164 + "d3-time": "3", 8165 + "d3-time-format": "4", 8166 + "d3-timer": "3", 8167 + "d3-transition": "3", 8168 + "d3-zoom": "3" 8169 + }, 8170 + "engines": { 8171 + "node": ">=12" 8172 + } 8173 + }, 8174 + "node_modules/d3-array": { 8175 + "version": "3.2.4", 8176 + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", 8177 + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", 8178 + "license": "ISC", 8179 + "dependencies": { 8180 + "internmap": "1 - 2" 8181 + }, 8182 + "engines": { 8183 + "node": ">=12" 8184 + } 8185 + }, 8186 + "node_modules/d3-axis": { 8187 + "version": "3.0.0", 8188 + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", 8189 + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", 8190 + "license": "ISC", 8191 + "engines": { 8192 + "node": ">=12" 8193 + } 8194 + }, 8195 + "node_modules/d3-brush": { 8196 + "version": "3.0.0", 8197 + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", 8198 + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", 8199 + "license": "ISC", 8200 + "dependencies": { 8201 + "d3-dispatch": "1 - 3", 8202 + "d3-drag": "2 - 3", 8203 + "d3-interpolate": "1 - 3", 8204 + "d3-selection": "3", 8205 + "d3-transition": "3" 8206 + }, 8207 + "engines": { 8208 + "node": ">=12" 8209 + } 8210 + }, 8211 + "node_modules/d3-chord": { 8212 + "version": "3.0.1", 8213 + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", 8214 + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", 8215 + "license": "ISC", 8216 + "dependencies": { 8217 + "d3-path": "1 - 3" 8218 + }, 8219 + "engines": { 8220 + "node": ">=12" 8221 + } 8222 + }, 8223 + "node_modules/d3-color": { 8224 + "version": "3.1.0", 8225 + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", 8226 + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", 8227 + "license": "ISC", 8228 + "engines": { 8229 + "node": ">=12" 8230 + } 8231 + }, 8232 + "node_modules/d3-contour": { 8233 + "version": "4.0.2", 8234 + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", 8235 + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", 8236 + "license": "ISC", 8237 + "dependencies": { 8238 + "d3-array": "^3.2.0" 8239 + }, 8240 + "engines": { 8241 + "node": ">=12" 8242 + } 8243 + }, 8244 + "node_modules/d3-delaunay": { 8245 + "version": "6.0.4", 8246 + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", 8247 + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", 8248 + "license": "ISC", 8249 + "dependencies": { 8250 + "delaunator": "5" 8251 + }, 8252 + "engines": { 8253 + "node": ">=12" 8254 + } 8255 + }, 8256 + "node_modules/d3-dispatch": { 8257 + "version": "3.0.1", 8258 + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", 8259 + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", 8260 + "license": "ISC", 8261 + "engines": { 8262 + "node": ">=12" 8263 + } 8264 + }, 8265 + "node_modules/d3-drag": { 8266 + "version": "3.0.0", 8267 + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", 8268 + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", 8269 + "license": "ISC", 8270 + "dependencies": { 8271 + "d3-dispatch": "1 - 3", 8272 + "d3-selection": "3" 8273 + }, 8274 + "engines": { 8275 + "node": ">=12" 8276 + } 8277 + }, 8278 + "node_modules/d3-dsv": { 8279 + "version": "3.0.1", 8280 + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", 8281 + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", 8282 + "license": "ISC", 8283 + "dependencies": { 8284 + "commander": "7", 8285 + "iconv-lite": "0.6", 8286 + "rw": "1" 8287 + }, 8288 + "bin": { 8289 + "csv2json": "bin/dsv2json.js", 8290 + "csv2tsv": "bin/dsv2dsv.js", 8291 + "dsv2dsv": "bin/dsv2dsv.js", 8292 + "dsv2json": "bin/dsv2json.js", 8293 + "json2csv": "bin/json2dsv.js", 8294 + "json2dsv": "bin/json2dsv.js", 8295 + "json2tsv": "bin/json2dsv.js", 8296 + "tsv2csv": "bin/dsv2dsv.js", 8297 + "tsv2json": "bin/dsv2json.js" 8298 + }, 8299 + "engines": { 8300 + "node": ">=12" 8301 + } 8302 + }, 8303 + "node_modules/d3-dsv/node_modules/commander": { 8304 + "version": "7.2.0", 8305 + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 8306 + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", 8307 + "license": "MIT", 8308 + "engines": { 8309 + "node": ">= 10" 8310 + } 8311 + }, 8312 + "node_modules/d3-dsv/node_modules/iconv-lite": { 8313 + "version": "0.6.3", 8314 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 8315 + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 8316 + "license": "MIT", 8317 + "dependencies": { 8318 + "safer-buffer": ">= 2.1.2 < 3.0.0" 8319 + }, 8320 + "engines": { 8321 + "node": ">=0.10.0" 8322 + } 8323 + }, 8324 + "node_modules/d3-ease": { 8325 + "version": "3.0.1", 8326 + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", 8327 + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", 8328 + "license": "BSD-3-Clause", 8329 + "engines": { 8330 + "node": ">=12" 8331 + } 8332 + }, 8333 + "node_modules/d3-fetch": { 8334 + "version": "3.0.1", 8335 + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", 8336 + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", 8337 + "license": "ISC", 8338 + "dependencies": { 8339 + "d3-dsv": "1 - 3" 8340 + }, 8341 + "engines": { 8342 + "node": ">=12" 8343 + } 8344 + }, 8345 + "node_modules/d3-force": { 8346 + "version": "3.0.0", 8347 + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", 8348 + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", 8349 + "license": "ISC", 8350 + "dependencies": { 8351 + "d3-dispatch": "1 - 3", 8352 + "d3-quadtree": "1 - 3", 8353 + "d3-timer": "1 - 3" 8354 + }, 8355 + "engines": { 8356 + "node": ">=12" 8357 + } 8358 + }, 8359 + "node_modules/d3-format": { 8360 + "version": "3.1.2", 8361 + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", 8362 + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", 8363 + "license": "ISC", 8364 + "engines": { 8365 + "node": ">=12" 8366 + } 8367 + }, 8368 + "node_modules/d3-geo": { 8369 + "version": "3.1.1", 8370 + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", 8371 + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", 8372 + "license": "ISC", 8373 + "dependencies": { 8374 + "d3-array": "2.5.0 - 3" 8375 + }, 8376 + "engines": { 8377 + "node": ">=12" 8378 + } 8379 + }, 8380 + "node_modules/d3-hierarchy": { 8381 + "version": "3.1.2", 8382 + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", 8383 + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", 8384 + "license": "ISC", 8385 + "engines": { 8386 + "node": ">=12" 8387 + } 8388 + }, 8389 + "node_modules/d3-interpolate": { 8390 + "version": "3.0.1", 8391 + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", 8392 + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", 8393 + "license": "ISC", 8394 + "dependencies": { 8395 + "d3-color": "1 - 3" 8396 + }, 8397 + "engines": { 8398 + "node": ">=12" 8399 + } 8400 + }, 8401 + "node_modules/d3-path": { 8402 + "version": "3.1.0", 8403 + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", 8404 + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", 8405 + "license": "ISC", 8406 + "engines": { 8407 + "node": ">=12" 8408 + } 8409 + }, 8410 + "node_modules/d3-polygon": { 8411 + "version": "3.0.1", 8412 + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", 8413 + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", 8414 + "license": "ISC", 8415 + "engines": { 8416 + "node": ">=12" 8417 + } 8418 + }, 8419 + "node_modules/d3-quadtree": { 8420 + "version": "3.0.1", 8421 + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", 8422 + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", 8423 + "license": "ISC", 8424 + "engines": { 8425 + "node": ">=12" 8426 + } 8427 + }, 8428 + "node_modules/d3-random": { 8429 + "version": "3.0.1", 8430 + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", 8431 + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", 8432 + "license": "ISC", 8433 + "engines": { 8434 + "node": ">=12" 8435 + } 8436 + }, 8437 + "node_modules/d3-sankey": { 8438 + "version": "0.12.3", 8439 + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", 8440 + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", 8441 + "license": "BSD-3-Clause", 8442 + "dependencies": { 8443 + "d3-array": "1 - 2", 8444 + "d3-shape": "^1.2.0" 8445 + } 8446 + }, 8447 + "node_modules/d3-sankey/node_modules/d3-array": { 8448 + "version": "2.12.1", 8449 + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", 8450 + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", 8451 + "license": "BSD-3-Clause", 8452 + "dependencies": { 8453 + "internmap": "^1.0.0" 8454 + } 8455 + }, 8456 + "node_modules/d3-sankey/node_modules/d3-path": { 8457 + "version": "1.0.9", 8458 + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", 8459 + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", 8460 + "license": "BSD-3-Clause" 8461 + }, 8462 + "node_modules/d3-sankey/node_modules/d3-shape": { 8463 + "version": "1.3.7", 8464 + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", 8465 + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", 8466 + "license": "BSD-3-Clause", 8467 + "dependencies": { 8468 + "d3-path": "1" 8469 + } 8470 + }, 8471 + "node_modules/d3-sankey/node_modules/internmap": { 8472 + "version": "1.0.1", 8473 + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", 8474 + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", 8475 + "license": "ISC" 8476 + }, 8477 + "node_modules/d3-scale": { 8478 + "version": "4.0.2", 8479 + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", 8480 + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", 8481 + "license": "ISC", 8482 + "dependencies": { 8483 + "d3-array": "2.10.0 - 3", 8484 + "d3-format": "1 - 3", 8485 + "d3-interpolate": "1.2.0 - 3", 8486 + "d3-time": "2.1.1 - 3", 8487 + "d3-time-format": "2 - 4" 8488 + }, 8489 + "engines": { 8490 + "node": ">=12" 8491 + } 8492 + }, 8493 + "node_modules/d3-scale-chromatic": { 8494 + "version": "3.1.0", 8495 + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", 8496 + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", 8497 + "license": "ISC", 8498 + "dependencies": { 8499 + "d3-color": "1 - 3", 8500 + "d3-interpolate": "1 - 3" 8501 + }, 8502 + "engines": { 8503 + "node": ">=12" 8504 + } 8505 + }, 8506 + "node_modules/d3-selection": { 8507 + "version": "3.0.0", 8508 + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", 8509 + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", 8510 + "license": "ISC", 8511 + "peer": true, 8512 + "engines": { 8513 + "node": ">=12" 8514 + } 8515 + }, 8516 + "node_modules/d3-shape": { 8517 + "version": "3.2.0", 8518 + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", 8519 + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", 8520 + "license": "ISC", 8521 + "dependencies": { 8522 + "d3-path": "^3.1.0" 8523 + }, 8524 + "engines": { 8525 + "node": ">=12" 8526 + } 8527 + }, 8528 + "node_modules/d3-time": { 8529 + "version": "3.1.0", 8530 + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", 8531 + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", 8532 + "license": "ISC", 8533 + "dependencies": { 8534 + "d3-array": "2 - 3" 8535 + }, 8536 + "engines": { 8537 + "node": ">=12" 8538 + } 8539 + }, 8540 + "node_modules/d3-time-format": { 8541 + "version": "4.1.0", 8542 + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", 8543 + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", 8544 + "license": "ISC", 8545 + "dependencies": { 8546 + "d3-time": "1 - 3" 8547 + }, 8548 + "engines": { 8549 + "node": ">=12" 8550 + } 8551 + }, 8552 + "node_modules/d3-timer": { 8553 + "version": "3.0.1", 8554 + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", 8555 + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", 8556 + "license": "ISC", 8557 + "engines": { 8558 + "node": ">=12" 8559 + } 8560 + }, 8561 + "node_modules/d3-transition": { 8562 + "version": "3.0.1", 8563 + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", 8564 + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", 8565 + "license": "ISC", 8566 + "dependencies": { 8567 + "d3-color": "1 - 3", 8568 + "d3-dispatch": "1 - 3", 8569 + "d3-ease": "1 - 3", 8570 + "d3-interpolate": "1 - 3", 8571 + "d3-timer": "1 - 3" 8572 + }, 8573 + "engines": { 8574 + "node": ">=12" 8575 + }, 8576 + "peerDependencies": { 8577 + "d3-selection": "2 - 3" 8578 + } 8579 + }, 8580 + "node_modules/d3-zoom": { 8581 + "version": "3.0.0", 8582 + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", 8583 + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", 8584 + "license": "ISC", 8585 + "dependencies": { 8586 + "d3-dispatch": "1 - 3", 8587 + "d3-drag": "2 - 3", 8588 + "d3-interpolate": "1 - 3", 8589 + "d3-selection": "2 - 3", 8590 + "d3-transition": "2 - 3" 8591 + }, 8592 + "engines": { 8593 + "node": ">=12" 8594 + } 8595 + }, 8596 + "node_modules/dagre-d3-es": { 8597 + "version": "7.0.13", 8598 + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", 8599 + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", 8600 + "license": "MIT", 8601 + "dependencies": { 8602 + "d3": "^7.9.0", 8603 + "lodash-es": "^4.17.21" 8604 + } 8605 + }, 8606 + "node_modules/dayjs": { 8607 + "version": "1.11.19", 8608 + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", 8609 + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", 8610 + "license": "MIT" 8611 + }, 7662 8612 "node_modules/debounce": { 7663 8613 "version": "1.2.1", 7664 8614 "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", ··· 7820 8770 "url": "https://github.com/sponsors/ljharb" 7821 8771 } 7822 8772 }, 8773 + "node_modules/delaunator": { 8774 + "version": "5.0.1", 8775 + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", 8776 + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", 8777 + "license": "ISC", 8778 + "dependencies": { 8779 + "robust-predicates": "^3.0.2" 8780 + } 8781 + }, 7823 8782 "node_modules/depd": { 7824 8783 "version": "2.0.0", 7825 8784 "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", ··· 7956 8915 }, 7957 8916 "funding": { 7958 8917 "url": "https://github.com/fb55/domhandler?sponsor=1" 8918 + } 8919 + }, 8920 + "node_modules/dompurify": { 8921 + "version": "3.3.1", 8922 + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", 8923 + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", 8924 + "license": "(MPL-2.0 OR Apache-2.0)", 8925 + "optionalDependencies": { 8926 + "@types/trusted-types": "^2.0.7" 7959 8927 } 7960 8928 }, 7961 8929 "node_modules/domutils": { ··· 9191 10159 "url": "https://github.com/sponsors/sindresorhus" 9192 10160 } 9193 10161 }, 10162 + "node_modules/hachure-fill": { 10163 + "version": "0.5.2", 10164 + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", 10165 + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", 10166 + "license": "MIT" 10167 + }, 9194 10168 "node_modules/handle-thing": { 9195 10169 "version": "2.0.1", 9196 10170 "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", ··· 9872 10846 "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", 9873 10847 "license": "MIT" 9874 10848 }, 10849 + "node_modules/internmap": { 10850 + "version": "2.0.3", 10851 + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", 10852 + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", 10853 + "license": "ISC", 10854 + "engines": { 10855 + "node": ">=12" 10856 + } 10857 + }, 9875 10858 "node_modules/invariant": { 9876 10859 "version": "2.2.4", 9877 10860 "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", ··· 10367 11350 "graceful-fs": "^4.1.6" 10368 11351 } 10369 11352 }, 11353 + "node_modules/katex": { 11354 + "version": "0.16.33", 11355 + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.33.tgz", 11356 + "integrity": "sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==", 11357 + "funding": [ 11358 + "https://opencollective.com/katex", 11359 + "https://github.com/sponsors/katex" 11360 + ], 11361 + "license": "MIT", 11362 + "dependencies": { 11363 + "commander": "^8.3.0" 11364 + }, 11365 + "bin": { 11366 + "katex": "cli.js" 11367 + } 11368 + }, 11369 + "node_modules/katex/node_modules/commander": { 11370 + "version": "8.3.0", 11371 + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", 11372 + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", 11373 + "license": "MIT", 11374 + "engines": { 11375 + "node": ">= 12" 11376 + } 11377 + }, 10370 11378 "node_modules/keyv": { 10371 11379 "version": "4.5.4", 10372 11380 "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", ··· 10376 11384 "json-buffer": "3.0.1" 10377 11385 } 10378 11386 }, 11387 + "node_modules/khroma": { 11388 + "version": "2.1.0", 11389 + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", 11390 + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" 11391 + }, 10379 11392 "node_modules/kind-of": { 10380 11393 "version": "6.0.3", 10381 11394 "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", ··· 10394 11407 "node": ">=6" 10395 11408 } 10396 11409 }, 11410 + "node_modules/langium": { 11411 + "version": "4.2.1", 11412 + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", 11413 + "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", 11414 + "license": "MIT", 11415 + "dependencies": { 11416 + "chevrotain": "~11.1.1", 11417 + "chevrotain-allstar": "~0.3.1", 11418 + "vscode-languageserver": "~9.0.1", 11419 + "vscode-languageserver-textdocument": "~1.0.11", 11420 + "vscode-uri": "~3.1.0" 11421 + }, 11422 + "engines": { 11423 + "node": ">=20.10.0", 11424 + "npm": ">=10.2.3" 11425 + } 11426 + }, 10397 11427 "node_modules/latest-version": { 10398 11428 "version": "7.0.0", 10399 11429 "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", ··· 10418 11448 "picocolors": "^1.1.1", 10419 11449 "shell-quote": "^1.8.3" 10420 11450 } 11451 + }, 11452 + "node_modules/layout-base": { 11453 + "version": "1.0.2", 11454 + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", 11455 + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", 11456 + "license": "MIT" 10421 11457 }, 10422 11458 "node_modules/leven": { 10423 11459 "version": "3.1.0", ··· 10494 11530 "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", 10495 11531 "license": "MIT" 10496 11532 }, 11533 + "node_modules/lodash-es": { 11534 + "version": "4.17.23", 11535 + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", 11536 + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", 11537 + "license": "MIT" 11538 + }, 10497 11539 "node_modules/lodash.debounce": { 10498 11540 "version": "4.0.8", 10499 11541 "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", ··· 10584 11626 "funding": { 10585 11627 "type": "github", 10586 11628 "url": "https://github.com/sponsors/wooorm" 11629 + } 11630 + }, 11631 + "node_modules/marked": { 11632 + "version": "16.4.2", 11633 + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", 11634 + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", 11635 + "license": "MIT", 11636 + "bin": { 11637 + "marked": "bin/marked.js" 11638 + }, 11639 + "engines": { 11640 + "node": ">= 20" 10587 11641 } 10588 11642 }, 10589 11643 "node_modules/math-intrinsics": { ··· 11063 12117 "license": "MIT", 11064 12118 "engines": { 11065 12119 "node": ">= 8" 12120 + } 12121 + }, 12122 + "node_modules/mermaid": { 12123 + "version": "11.12.3", 12124 + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", 12125 + "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", 12126 + "license": "MIT", 12127 + "dependencies": { 12128 + "@braintree/sanitize-url": "^7.1.1", 12129 + "@iconify/utils": "^3.0.1", 12130 + "@mermaid-js/parser": "^1.0.0", 12131 + "@types/d3": "^7.4.3", 12132 + "cytoscape": "^3.29.3", 12133 + "cytoscape-cose-bilkent": "^4.1.0", 12134 + "cytoscape-fcose": "^2.2.0", 12135 + "d3": "^7.9.0", 12136 + "d3-sankey": "^0.12.3", 12137 + "dagre-d3-es": "7.0.13", 12138 + "dayjs": "^1.11.18", 12139 + "dompurify": "^3.2.5", 12140 + "katex": "^0.16.22", 12141 + "khroma": "^2.1.0", 12142 + "lodash-es": "^4.17.23", 12143 + "marked": "^16.2.1", 12144 + "roughjs": "^4.6.6", 12145 + "stylis": "^4.3.6", 12146 + "ts-dedent": "^2.2.0", 12147 + "uuid": "^11.1.0" 12148 + } 12149 + }, 12150 + "node_modules/mermaid/node_modules/uuid": { 12151 + "version": "11.1.0", 12152 + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", 12153 + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 12154 + "funding": [ 12155 + "https://github.com/sponsors/broofa", 12156 + "https://github.com/sponsors/ctavan" 12157 + ], 12158 + "license": "MIT", 12159 + "bin": { 12160 + "uuid": "dist/esm/bin/uuid" 11066 12161 } 11067 12162 }, 11068 12163 "node_modules/methods": { ··· 12972 14067 "url": "https://github.com/sponsors/ljharb" 12973 14068 } 12974 14069 }, 14070 + "node_modules/mlly": { 14071 + "version": "1.8.0", 14072 + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", 14073 + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", 14074 + "license": "MIT", 14075 + "dependencies": { 14076 + "acorn": "^8.15.0", 14077 + "pathe": "^2.0.3", 14078 + "pkg-types": "^1.3.1", 14079 + "ufo": "^1.6.1" 14080 + } 14081 + }, 12975 14082 "node_modules/mrmime": { 12976 14083 "version": "2.0.1", 12977 14084 "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", ··· 13429 14536 "url": "https://github.com/sponsors/sindresorhus" 13430 14537 } 13431 14538 }, 14539 + "node_modules/package-manager-detector": { 14540 + "version": "1.6.0", 14541 + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", 14542 + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", 14543 + "license": "MIT" 14544 + }, 13432 14545 "node_modules/param-case": { 13433 14546 "version": "3.0.4", 13434 14547 "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", ··· 13556 14669 "tslib": "^2.0.3" 13557 14670 } 13558 14671 }, 14672 + "node_modules/path-data-parser": { 14673 + "version": "0.1.0", 14674 + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", 14675 + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", 14676 + "license": "MIT" 14677 + }, 13559 14678 "node_modules/path-exists": { 13560 14679 "version": "5.0.0", 13561 14680 "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", ··· 13604 14723 "node": ">=8" 13605 14724 } 13606 14725 }, 14726 + "node_modules/pathe": { 14727 + "version": "2.0.3", 14728 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 14729 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 14730 + "license": "MIT" 14731 + }, 13607 14732 "node_modules/picocolors": { 13608 14733 "version": "1.1.1", 13609 14734 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", ··· 13637 14762 "url": "https://github.com/sponsors/sindresorhus" 13638 14763 } 13639 14764 }, 14765 + "node_modules/pkg-types": { 14766 + "version": "1.3.1", 14767 + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", 14768 + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", 14769 + "license": "MIT", 14770 + "dependencies": { 14771 + "confbox": "^0.1.8", 14772 + "mlly": "^1.7.4", 14773 + "pathe": "^2.0.1" 14774 + } 14775 + }, 13640 14776 "node_modules/pkijs": { 13641 14777 "version": "3.3.3", 13642 14778 "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", ··· 13652 14788 }, 13653 14789 "engines": { 13654 14790 "node": ">=16.0.0" 14791 + } 14792 + }, 14793 + "node_modules/points-on-curve": { 14794 + "version": "0.2.0", 14795 + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", 14796 + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", 14797 + "license": "MIT" 14798 + }, 14799 + "node_modules/points-on-path": { 14800 + "version": "0.2.1", 14801 + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", 14802 + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", 14803 + "license": "MIT", 14804 + "dependencies": { 14805 + "path-data-parser": "0.1.0", 14806 + "points-on-curve": "0.2.0" 13655 14807 } 13656 14808 }, 13657 14809 "node_modules/postcss": { ··· 16089 17241 "node": ">=0.10.0" 16090 17242 } 16091 17243 }, 17244 + "node_modules/robust-predicates": { 17245 + "version": "3.0.2", 17246 + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", 17247 + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", 17248 + "license": "Unlicense" 17249 + }, 17250 + "node_modules/roughjs": { 17251 + "version": "4.6.6", 17252 + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", 17253 + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", 17254 + "license": "MIT", 17255 + "dependencies": { 17256 + "hachure-fill": "^0.5.2", 17257 + "path-data-parser": "^0.1.0", 17258 + "points-on-curve": "^0.2.0", 17259 + "points-on-path": "^0.2.1" 17260 + } 17261 + }, 16092 17262 "node_modules/rtlcss": { 16093 17263 "version": "4.3.0", 16094 17264 "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", ··· 16141 17311 "dependencies": { 16142 17312 "queue-microtask": "^1.2.2" 16143 17313 } 17314 + }, 17315 + "node_modules/rw": { 17316 + "version": "1.3.3", 17317 + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", 17318 + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", 17319 + "license": "BSD-3-Clause" 16144 17320 }, 16145 17321 "node_modules/safe-buffer": { 16146 17322 "version": "5.2.1", ··· 16974 18150 "postcss": "^8.4.31" 16975 18151 } 16976 18152 }, 18153 + "node_modules/stylis": { 18154 + "version": "4.3.6", 18155 + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", 18156 + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", 18157 + "license": "MIT" 18158 + }, 16977 18159 "node_modules/supports-color": { 16978 18160 "version": "7.2.0", 16979 18161 "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", ··· 17172 18354 "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", 17173 18355 "license": "MIT" 17174 18356 }, 18357 + "node_modules/tinyexec": { 18358 + "version": "1.0.2", 18359 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", 18360 + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", 18361 + "license": "MIT", 18362 + "engines": { 18363 + "node": ">=18" 18364 + } 18365 + }, 17175 18366 "node_modules/tinypool": { 17176 18367 "version": "1.1.1", 17177 18368 "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", ··· 17247 18438 "url": "https://github.com/sponsors/wooorm" 17248 18439 } 17249 18440 }, 18441 + "node_modules/ts-dedent": { 18442 + "version": "2.2.0", 18443 + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", 18444 + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", 18445 + "license": "MIT", 18446 + "engines": { 18447 + "node": ">=6.10" 18448 + } 18449 + }, 17250 18450 "node_modules/tslib": { 17251 18451 "version": "2.8.1", 17252 18452 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", ··· 17341 18541 "engines": { 17342 18542 "node": ">=14.17" 17343 18543 } 18544 + }, 18545 + "node_modules/ufo": { 18546 + "version": "1.6.3", 18547 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", 18548 + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", 18549 + "license": "MIT" 17344 18550 }, 17345 18551 "node_modules/undici-types": { 17346 18552 "version": "7.16.0", ··· 17836 19042 "type": "opencollective", 17837 19043 "url": "https://opencollective.com/unified" 17838 19044 } 19045 + }, 19046 + "node_modules/vscode-jsonrpc": { 19047 + "version": "8.2.0", 19048 + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", 19049 + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", 19050 + "license": "MIT", 19051 + "engines": { 19052 + "node": ">=14.0.0" 19053 + } 19054 + }, 19055 + "node_modules/vscode-languageserver": { 19056 + "version": "9.0.1", 19057 + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", 19058 + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", 19059 + "license": "MIT", 19060 + "dependencies": { 19061 + "vscode-languageserver-protocol": "3.17.5" 19062 + }, 19063 + "bin": { 19064 + "installServerIntoExtension": "bin/installServerIntoExtension" 19065 + } 19066 + }, 19067 + "node_modules/vscode-languageserver-protocol": { 19068 + "version": "3.17.5", 19069 + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", 19070 + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", 19071 + "license": "MIT", 19072 + "dependencies": { 19073 + "vscode-jsonrpc": "8.2.0", 19074 + "vscode-languageserver-types": "3.17.5" 19075 + } 19076 + }, 19077 + "node_modules/vscode-languageserver-textdocument": { 19078 + "version": "1.0.12", 19079 + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", 19080 + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", 19081 + "license": "MIT" 19082 + }, 19083 + "node_modules/vscode-languageserver-types": { 19084 + "version": "3.17.5", 19085 + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", 19086 + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", 19087 + "license": "MIT" 19088 + }, 19089 + "node_modules/vscode-uri": { 19090 + "version": "3.1.0", 19091 + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", 19092 + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", 19093 + "license": "MIT" 17839 19094 }, 17840 19095 "node_modules/watchpack": { 17841 19096 "version": "2.5.1",
+2
package.json
··· 10 10 "dependencies": { 11 11 "@docusaurus/core": "^3.7.0", 12 12 "@docusaurus/preset-classic": "^3.7.0", 13 + "@docusaurus/theme-mermaid": "^3.9.2", 14 + "prismjs": "^1.30.0", 13 15 "react": "^19.0.0", 14 16 "react-dom": "^19.0.0" 15 17 },
+181 -10
sidebars.ts
··· 2 2 3 3 const sidebars: SidebarsConfig = { 4 4 docs: [ 5 - "README", 6 - "quickstart", 7 - "configuration", 8 - "deployment", 9 - "lexicons", 10 - "network-lexicons", 11 - "backfill", 12 - "xrpc-api", 13 - "admin-api", 14 - "architecture", 5 + { 6 + type: "doc", 7 + id: "README", 8 + label: "Introduction", 9 + }, 10 + { 11 + type: "category", 12 + label: "Getting Started", 13 + items: [ 14 + { 15 + type: "doc", 16 + id: "getting-started/quickstart", 17 + label: "Quickstart", 18 + }, 19 + { 20 + type: "category", 21 + label: "Deployment", 22 + items: [ 23 + { 24 + type: "doc", 25 + id: "getting-started/deployment/railway", 26 + label: "Railway", 27 + }, 28 + { 29 + type: "doc", 30 + id: "getting-started/deployment/docker", 31 + label: "Docker", 32 + }, 33 + { 34 + type: "doc", 35 + id: "getting-started/deployment/other", 36 + label: "Other", 37 + }, 38 + ], 39 + }, 40 + { 41 + type: "doc", 42 + id: "getting-started/authentication", 43 + label: "Authentication", 44 + }, 45 + { 46 + type: "doc", 47 + id: "getting-started/configuration", 48 + label: "Configuration", 49 + }, 50 + { 51 + type: "doc", 52 + id: "getting-started/dashboard", 53 + label: "Dashboard", 54 + }, 55 + ], 56 + }, 57 + { 58 + type: "category", 59 + label: "Tutorials", 60 + items: [ 61 + { 62 + type: "doc", 63 + id: "tutorials/statusphere", 64 + label: "Statusphere", 65 + }, 66 + ], 67 + }, 68 + { 69 + type: "category", 70 + label: "Guides", 71 + items: [ 72 + { 73 + type: "doc", 74 + id: "guides/lexicons", 75 + label: "Lexicons", 76 + }, 77 + { 78 + type: "doc", 79 + id: "guides/scripting", 80 + label: "Lua Scripting", 81 + }, 82 + { 83 + type: "doc", 84 + id: "guides/backfill", 85 + label: "Backfill", 86 + }, 87 + ], 88 + }, 89 + { 90 + type: "category", 91 + label: "Reference", 92 + items: [ 93 + { 94 + type: "doc", 95 + id: "reference/xrpc-api", 96 + label: "XRPC API", 97 + }, 98 + { 99 + type: "doc", 100 + id: "reference/admin-api", 101 + label: "Admin API", 102 + }, 103 + { 104 + type: "category", 105 + label: "Script Examples", 106 + items: [ 107 + { 108 + type: "doc", 109 + id: "reference/scripts/get-record", 110 + label: "Get a Record", 111 + }, 112 + { 113 + type: "doc", 114 + id: "reference/scripts/create-record", 115 + label: "Create Record", 116 + }, 117 + { 118 + type: "doc", 119 + id: "reference/scripts/upsert-record", 120 + label: "Upsert Record", 121 + }, 122 + { 123 + type: "doc", 124 + id: "reference/scripts/paginated-list", 125 + label: "Paginated List", 126 + }, 127 + { 128 + type: "doc", 129 + id: "reference/scripts/list-or-fetch", 130 + label: "List or Fetch", 131 + }, 132 + { 133 + type: "doc", 134 + id: "reference/scripts/expanded-query", 135 + label: "Expanded Query", 136 + }, 137 + { 138 + type: "doc", 139 + id: "reference/scripts/update-or-delete", 140 + label: "Update or Delete", 141 + }, 142 + { 143 + type: "doc", 144 + id: "reference/scripts/batch-save", 145 + label: "Batch Save", 146 + }, 147 + { 148 + type: "doc", 149 + id: "reference/scripts/sidecar-records", 150 + label: "Sidecar Records", 151 + }, 152 + { 153 + type: "doc", 154 + id: "reference/scripts/cascading-delete", 155 + label: "Cascading Delete", 156 + }, 157 + { 158 + type: "doc", 159 + id: "reference/scripts/complex-mutations", 160 + label: "Complex Mutations", 161 + }, 162 + ], 163 + }, 164 + { 165 + type: "doc", 166 + id: "reference/glossary", 167 + label: "Glossary", 168 + }, 169 + { 170 + type: "doc", 171 + id: "reference/architecture", 172 + label: "Architecture", 173 + }, 174 + { 175 + type: "doc", 176 + id: "reference/troubleshooting", 177 + label: "Troubleshooting", 178 + }, 179 + { 180 + type: "doc", 181 + id: "reference/production-deployment", 182 + label: "Production", 183 + }, 184 + ], 185 + }, 15 186 ], 16 187 }; 17 188
static/img/favicon.png

This is a binary file and will not be displayed.

static/img/logo.png

This is a binary file and will not be displayed.