feat: init a3e32071
Steve · 2026-01-12 21:09 28 file(s) · +1813 −0
.gitignore (added) +30 −0
1 +
# Dependencies
2 +
node_modules/
3 +
bun.lockb
4 +
5 +
# Build outputs
6 +
dist/
7 +
*.tsbuildinfo
8 +
9 +
# Environment files
10 +
.env
11 +
.env.local
12 +
.dev.vars
13 +
14 +
# Wrangler
15 +
.wrangler/
16 +
17 +
# IDE
18 +
.vscode/
19 +
.idea/
20 +
*.swp
21 +
*.swo
22 +
*~
23 +
24 +
# OS
25 +
.DS_Store
26 +
Thumbs.db
27 +
28 +
# Logs
29 +
*.log
30 +
npm-debug.log*
README.md (added) +211 −0
1 +
# AT Feeds
2 +
3 +
A monorepo for indexing and displaying [Standard.site](https://standard.site) documents from the AT Protocol, powered by Cloudflare Workers, D1, and Queues.
4 +
5 +
## Architecture
6 +
7 +
```
8 +
┌─────────────────────────────────────────────────────────────┐
9 +
│                     Cloudflare                               │
10 +
├─────────────────────────────────────────────────────────────┤
11 +
│                                                              │
12 +
│  ┌──────────────┐     ┌──────────────┐     ┌─────────────┐  │
13 +
│  │    Pages     │────▶│   Worker     │────▶│     D1      │  │
14 +
│  │   (Client)   │     │   (API)      │     │  (Database) │  │
15 +
│  └──────────────┘     └──────────────┘     └─────────────┘  │
16 +
│                              ▲                    ▲          │
17 +
│                              │                    │          │
18 +
│                       ┌──────┴───────┐    ┌──────┴───────┐  │
19 +
│                       │    Queue     │    │     Cron     │  │
20 +
│                       │  (Resolver)  │    │  (Refresh)   │  │
21 +
│                       └──────┬───────┘    └──────────────┘  │
22 +
│                              │                              │
23 +
└──────────────────────────────┼──────────────────────────────┘
24 +
                               │ POST /webhook/tap
25 +
                    ┌──────────┴───────────┐
26 +
                    │   Tap Instance       │
27 +
                    │   (External VPS)     │
28 +
                    └──────────────────────┘
29 +
```
30 +
31 +
**Components:**
32 +
33 +
1. **Tap Indexer** (External) - Subscribes to the AT Protocol firehose and sends webhook events
34 +
2. **Server** (`packages/server`) - Cloudflare Worker with Hono API, D1 database, and Queue consumer
35 +
3. **Client** (`packages/client`) - Vite + React app deployed to Cloudflare Pages
36 +
37 +
## Quick Start
38 +
39 +
### Prerequisites
40 +
41 +
- [Bun](https://bun.sh) installed
42 +
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) installed and authenticated
43 +
- A tap instance running somewhere (VPS, Fly.io, etc.)
44 +
45 +
### Setup
46 +
47 +
1. Install dependencies:
48 +
49 +
```bash
50 +
bun install
51 +
```
52 +
53 +
2. Create the D1 database:
54 +
55 +
```bash
56 +
bun run db:create
57 +
```
58 +
59 +
Copy the database ID and update `packages/server/wrangler.toml`.
60 +
61 +
3. Create the queue:
62 +
63 +
```bash
64 +
wrangler queues create document-resolution
65 +
```
66 +
67 +
4. Run database migrations:
68 +
69 +
```bash
70 +
# Local development
71 +
bun run db:migrate
72 +
73 +
# Production
74 +
bun run db:migrate:prod
75 +
```
76 +
77 +
5. (Optional) Set webhook secret:
78 +
79 +
```bash
80 +
bun run secret:set
81 +
```
82 +
83 +
6. Deploy the worker:
84 +
85 +
```bash
86 +
bun run deploy
87 +
```
88 +
89 +
7. Configure your tap instance:
90 +
91 +
```bash
92 +
TAP_WEBHOOK_URL=https://your-worker.workers.dev/webhook/tap
93 +
TAP_SIGNAL_COLLECTION=site.standard.document
94 +
TAP_COLLECTION_FILTERS=site.standard.document
95 +
```
96 +
97 +
8. Trigger initial resolution of existing records:
98 +
99 +
```bash
100 +
curl -X POST https://your-worker.workers.dev/admin/resolve-all
101 +
```
102 +
103 +
## Local Development
104 +
105 +
1. Start the worker locally:
106 +
107 +
```bash
108 +
bun run dev:server
109 +
```
110 +
111 +
The API will run on `http://localhost:8787`.
112 +
113 +
2. Start the client (in a separate terminal):
114 +
115 +
```bash
116 +
bun run dev:client
117 +
```
118 +
119 +
The client will run on `http://localhost:5173`.
120 +
121 +
## API Endpoints
122 +
123 +
### Health & Stats
124 +
125 +
| Endpoint | Method | Description |
126 +
|----------|--------|-------------|
127 +
| `/health` | GET | Health check |
128 +
| `/stats` | GET | Database statistics |
129 +
130 +
### Feed Endpoints
131 +
132 +
| Endpoint | Method | Description |
133 +
|----------|--------|-------------|
134 +
| `/feed` | GET | Pre-resolved documents (fast) |
135 +
| `/feed-raw` | GET | Raw record references (for client-side resolution) |
136 +
| `/records/:did` | GET | Records by DID |
137 +
138 +
### Webhook
139 +
140 +
| Endpoint | Method | Description |
141 +
|----------|--------|-------------|
142 +
| `/webhook/tap` | POST | Receives events from tap |
143 +
| `/webhook/tap/debug` | POST | Debug endpoint (echoes payload) |
144 +
145 +
### Admin
146 +
147 +
| Endpoint | Method | Description |
148 +
|----------|--------|-------------|
149 +
| `/admin/resolve-all` | POST | Queue unresolved records for processing |
150 +
151 +
## How It Works
152 +
153 +
1. **Tap** subscribes to the AT Protocol firehose and filters for `site.standard.document` records
154 +
2. **Webhook** receives events and stores record references in D1, then pushes to the resolution queue
155 +
3. **Queue consumer** resolves each document (PDS lookup → record fetch → publication URL) and stores in `resolved_documents`
156 +
4. **Cron job** (every 15 min) refreshes stale documents and processes any missed records
157 +
5. **`/feed` endpoint** reads directly from `resolved_documents` for instant responses
158 +
159 +
## Project Structure
160 +
161 +
```
162 +
.
163 +
├── package.json            # Root workspace config
164 +
└── packages/
165 +
    ├── server/             # Cloudflare Worker
166 +
    │   ├── wrangler.toml   # Worker configuration
167 +
    │   ├── schema.sql      # D1 database schema
168 +
    │   ├── package.json
169 +
    │   └── src/
170 +
    │       └── index.ts    # API + Queue consumer + Cron handler
171 +
    └── client/             # Vite + React app
172 +
        ├── package.json
173 +
        ├── vite.config.ts
174 +
        └── src/
175 +
            ├── main.tsx
176 +
            └── App.tsx
177 +
```
178 +
179 +
## Scripts
180 +
181 +
```bash
182 +
# Development
183 +
bun run dev              # Run all packages in dev mode
184 +
bun run dev:server       # Run worker locally
185 +
bun run dev:client       # Run client locally
186 +
187 +
# Deployment
188 +
bun run deploy           # Deploy worker to Cloudflare
189 +
bun run deploy:client    # Deploy client to Cloudflare Pages
190 +
191 +
# Database
192 +
bun run db:create        # Create D1 database
193 +
bun run db:migrate       # Run migrations (local)
194 +
bun run db:migrate:prod  # Run migrations (production)
195 +
196 +
# Secrets
197 +
bun run secret:set       # Set TAP_WEBHOOK_SECRET
198 +
```
199 +
200 +
## Resources
201 +
202 +
- [tap Documentation](https://github.com/bluesky-social/indigo/tree/main/cmd/tap)
203 +
- [AT Protocol Specs](https://atproto.com/)
204 +
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
205 +
- [Cloudflare D1](https://developers.cloudflare.com/d1/)
206 +
- [Cloudflare Queues](https://developers.cloudflare.com/queues/)
207 +
- [Hono Documentation](https://hono.dev/)
208 +
209 +
## License
210 +
211 +
MIT
bun.lock (added) +494 −0
1 +
{
2 +
  "lockfileVersion": 1,
3 +
  "configVersion": 1,
4 +
  "workspaces": {
5 +
    "": {
6 +
      "name": "atfeeds",
7 +
    },
8 +
    "packages/client": {
9 +
      "name": "@atfeeds/client",
10 +
      "version": "1.0.0",
11 +
      "dependencies": {
12 +
        "react": "^18.2.0",
13 +
        "react-dom": "^18.2.0",
14 +
      },
15 +
      "devDependencies": {
16 +
        "@types/react": "^18.2.0",
17 +
        "@types/react-dom": "^18.2.0",
18 +
        "@vitejs/plugin-react": "^4.0.0",
19 +
        "typescript": "^5.0.0",
20 +
        "vite": "^5.0.0",
21 +
      },
22 +
    },
23 +
    "packages/cloudflare": {
24 +
      "name": "atfeeds-cloudflare",
25 +
      "version": "1.0.0",
26 +
      "dependencies": {
27 +
        "hono": "^4.0.0",
28 +
      },
29 +
      "devDependencies": {
30 +
        "@cloudflare/workers-types": "^4.20240117.0",
31 +
        "wrangler": "^3.0.0",
32 +
      },
33 +
    },
34 +
    "packages/server": {
35 +
      "name": "@atfeeds/server",
36 +
      "version": "1.0.0",
37 +
      "dependencies": {
38 +
        "hono": "^4.0.0",
39 +
      },
40 +
      "devDependencies": {
41 +
        "@types/bun": "^1.0.0",
42 +
      },
43 +
    },
44 +
  },
45 +
  "packages": {
46 +
    "@atfeeds/client": ["@atfeeds/client@workspace:packages/client"],
47 +
48 +
    "@atfeeds/server": ["@atfeeds/server@workspace:packages/server"],
49 +
50 +
    "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
51 +
52 +
    "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="],
53 +
54 +
    "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="],
55 +
56 +
    "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="],
57 +
58 +
    "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
59 +
60 +
    "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
61 +
62 +
    "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
63 +
64 +
    "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
65 +
66 +
    "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
67 +
68 +
    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
69 +
70 +
    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
71 +
72 +
    "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
73 +
74 +
    "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
75 +
76 +
    "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
77 +
78 +
    "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
79 +
80 +
    "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
81 +
82 +
    "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
83 +
84 +
    "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
85 +
86 +
    "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
87 +
88 +
    "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="],
89 +
90 +
    "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.0.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.14", "workerd": "^1.20250124.0" }, "optionalPeers": ["workerd"] }, "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg=="],
91 +
92 +
    "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250718.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g=="],
93 +
94 +
    "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250718.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q=="],
95 +
96 +
    "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250718.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg=="],
97 +
98 +
    "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250718.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog=="],
99 +
100 +
    "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250718.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg=="],
101 +
102 +
    "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260111.0", "", {}, "sha512-NFA2U+AqEWHkAmw6oRzNWJyc14rIvBlF/OlK3lixokunRKwyziuON07nWUZ0w0kKWlW4fJ/muA09tEUaQY07tA=="],
103 +
104 +
    "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
105 +
106 +
    "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
107 +
108 +
    "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="],
109 +
110 +
    "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="],
111 +
112 +
    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
113 +
114 +
    "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
115 +
116 +
    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
117 +
118 +
    "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
119 +
120 +
    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
121 +
122 +
    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
123 +
124 +
    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
125 +
126 +
    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
127 +
128 +
    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
129 +
130 +
    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
131 +
132 +
    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
133 +
134 +
    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
135 +
136 +
    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
137 +
138 +
    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
139 +
140 +
    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
141 +
142 +
    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
143 +
144 +
    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
145 +
146 +
    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
147 +
148 +
    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
149 +
150 +
    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
151 +
152 +
    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
153 +
154 +
    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
155 +
156 +
    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
157 +
158 +
    "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
159 +
160 +
    "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
161 +
162 +
    "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
163 +
164 +
    "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
165 +
166 +
    "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
167 +
168 +
    "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
169 +
170 +
    "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
171 +
172 +
    "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="],
173 +
174 +
    "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
175 +
176 +
    "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
177 +
178 +
    "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
179 +
180 +
    "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
181 +
182 +
    "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
183 +
184 +
    "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="],
185 +
186 +
    "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
187 +
188 +
    "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
189 +
190 +
    "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
191 +
192 +
    "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="],
193 +
194 +
    "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="],
195 +
196 +
    "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
197 +
198 +
    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
199 +
200 +
    "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
201 +
202 +
    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
203 +
204 +
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
205 +
206 +
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
207 +
208 +
    "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
209 +
210 +
    "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="],
211 +
212 +
    "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="],
213 +
214 +
    "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="],
215 +
216 +
    "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="],
217 +
218 +
    "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="],
219 +
220 +
    "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="],
221 +
222 +
    "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="],
223 +
224 +
    "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="],
225 +
226 +
    "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="],
227 +
228 +
    "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="],
229 +
230 +
    "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="],
231 +
232 +
    "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="],
233 +
234 +
    "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="],
235 +
236 +
    "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="],
237 +
238 +
    "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="],
239 +
240 +
    "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="],
241 +
242 +
    "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="],
243 +
244 +
    "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="],
245 +
246 +
    "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="],
247 +
248 +
    "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="],
249 +
250 +
    "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="],
251 +
252 +
    "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="],
253 +
254 +
    "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="],
255 +
256 +
    "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="],
257 +
258 +
    "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="],
259 +
260 +
    "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
261 +
262 +
    "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
263 +
264 +
    "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
265 +
266 +
    "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
267 +
268 +
    "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
269 +
270 +
    "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
271 +
272 +
    "@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="],
273 +
274 +
    "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
275 +
276 +
    "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="],
277 +
278 +
    "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
279 +
280 +
    "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
281 +
282 +
    "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
283 +
284 +
    "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
285 +
286 +
    "as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="],
287 +
288 +
    "atfeeds-cloudflare": ["atfeeds-cloudflare@workspace:packages/cloudflare"],
289 +
290 +
    "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="],
291 +
292 +
    "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
293 +
294 +
    "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
295 +
296 +
    "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
297 +
298 +
    "caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="],
299 +
300 +
    "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
301 +
302 +
    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
303 +
304 +
    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
305 +
306 +
    "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
307 +
308 +
    "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
309 +
310 +
    "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
311 +
312 +
    "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
313 +
314 +
    "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
315 +
316 +
    "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
317 +
318 +
    "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
319 +
320 +
    "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
321 +
322 +
    "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
323 +
324 +
    "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
325 +
326 +
    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
327 +
328 +
    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
329 +
330 +
    "estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
331 +
332 +
    "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
333 +
334 +
    "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
335 +
336 +
    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
337 +
338 +
    "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
339 +
340 +
    "get-source": ["get-source@2.0.12", "", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="],
341 +
342 +
    "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
343 +
344 +
    "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="],
345 +
346 +
    "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
347 +
348 +
    "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
349 +
350 +
    "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
351 +
352 +
    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
353 +
354 +
    "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
355 +
356 +
    "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
357 +
358 +
    "magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
359 +
360 +
    "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
361 +
362 +
    "miniflare": ["miniflare@3.20250718.3", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="],
363 +
364 +
    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
365 +
366 +
    "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
367 +
368 +
    "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
369 +
370 +
    "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
371 +
372 +
    "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
373 +
374 +
    "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
375 +
376 +
    "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
377 +
378 +
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
379 +
380 +
    "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
381 +
382 +
    "printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="],
383 +
384 +
    "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
385 +
386 +
    "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
387 +
388 +
    "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
389 +
390 +
    "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
391 +
392 +
    "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="],
393 +
394 +
    "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="],
395 +
396 +
    "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="],
397 +
398 +
    "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
399 +
400 +
    "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
401 +
402 +
    "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
403 +
404 +
    "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
405 +
406 +
    "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
407 +
408 +
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
409 +
410 +
    "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="],
411 +
412 +
    "stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
413 +
414 +
    "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
415 +
416 +
    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
417 +
418 +
    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
419 +
420 +
    "ufo": ["ufo@1.6.2", "", {}, "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q=="],
421 +
422 +
    "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
423 +
424 +
    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
425 +
426 +
    "unenv": ["unenv@2.0.0-rc.14", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", "ohash": "^2.0.10", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q=="],
427 +
428 +
    "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
429 +
430 +
    "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
431 +
432 +
    "workerd": ["workerd@1.20250718.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250718.0", "@cloudflare/workerd-darwin-arm64": "1.20250718.0", "@cloudflare/workerd-linux-64": "1.20250718.0", "@cloudflare/workerd-linux-arm64": "1.20250718.0", "@cloudflare/workerd-windows-64": "1.20250718.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg=="],
433 +
434 +
    "wrangler": ["wrangler@3.114.16", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/unenv-preset": "2.0.2", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250718.3", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.14", "workerd": "1.20250718.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250408.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-ve/ULRjrquu5BHNJ+1T0ipJJlJ6pD7qLmhwRkk0BsUIxatNe4HP4odX/R4Mq/RHG6LOnVAFs7SMeSHlz/1mNlQ=="],
435 +
436 +
    "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
437 +
438 +
    "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
439 +
440 +
    "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
441 +
442 +
    "zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
443 +
444 +
    "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
445 +
446 +
    "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
447 +
448 +
    "wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="],
449 +
450 +
    "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="],
451 +
452 +
    "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="],
453 +
454 +
    "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="],
455 +
456 +
    "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="],
457 +
458 +
    "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="],
459 +
460 +
    "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="],
461 +
462 +
    "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="],
463 +
464 +
    "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="],
465 +
466 +
    "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="],
467 +
468 +
    "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="],
469 +
470 +
    "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="],
471 +
472 +
    "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="],
473 +
474 +
    "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="],
475 +
476 +
    "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="],
477 +
478 +
    "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="],
479 +
480 +
    "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="],
481 +
482 +
    "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="],
483 +
484 +
    "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="],
485 +
486 +
    "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="],
487 +
488 +
    "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="],
489 +
490 +
    "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="],
491 +
492 +
    "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="],
493 +
  }
494 +
}
package.json (added) +19 −0
1 +
{
2 +
	"name": "atfeeds",
3 +
	"version": "1.0.0",
4 +
	"private": true,
5 +
	"workspaces": [
6 +
		"packages/*"
7 +
	],
8 +
	"scripts": {
9 +
		"dev": "bun run --filter '*' dev",
10 +
		"dev:server": "cd packages/server && npm run dev",
11 +
		"dev:client": "cd packages/client && npm run dev",
12 +
		"deploy": "cd packages/server && npm run deploy",
13 +
		"deploy:client": "cd packages/client && npm run pages:deploy",
14 +
		"db:create": "cd packages/server && npm run db:create",
15 +
		"db:migrate": "cd packages/server && npm run db:migrate",
16 +
		"db:migrate:prod": "cd packages/server && npm run db:migrate:prod",
17 +
		"secret:set": "cd packages/server && npm run secret:set"
18 +
	}
19 +
}
packages/client/index.html (added) +12 −0
1 +
<!DOCTYPE html>
2 +
<html lang="en">
3 +
  <head>
4 +
    <meta charset="UTF-8" />
5 +
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 +
    <title>AT Feeds - Standard.site Documents</title>
7 +
  </head>
8 +
  <body>
9 +
    <div id="root"></div>
10 +
    <script type="module" src="/src/main.tsx"></script>
11 +
  </body>
12 +
</html>
packages/client/package.json (added) +22 −0
1 +
{
2 +
  "name": "@atfeeds/client",
3 +
  "version": "1.0.0",
4 +
  "type": "module",
5 +
  "scripts": {
6 +
    "dev": "vite",
7 +
    "build": "tsc && vite build",
8 +
    "preview": "vite preview",
9 +
    "pages:deploy": "vite build && wrangler pages deploy dist --project-name=atfeeds"
10 +
  },
11 +
  "dependencies": {
12 +
    "react": "^18.2.0",
13 +
    "react-dom": "^18.2.0"
14 +
  },
15 +
  "devDependencies": {
16 +
    "@types/react": "^18.2.0",
17 +
    "@types/react-dom": "^18.2.0",
18 +
    "@vitejs/plugin-react": "^4.0.0",
19 +
    "typescript": "^5.0.0",
20 +
    "vite": "^5.0.0"
21 +
  }
22 +
}
packages/client/src/App.tsx (added) +128 −0
1 +
import { useEffect, useState } from "react";
2 +
3 +
// API base URL - empty for same-origin (local dev), or set via env var for production
4 +
const API_URL = "https://atfeeds-api.stevedsimkins.workers.dev";
5 +
6 +
interface Document {
7 +
	uri: string;
8 +
	did: string;
9 +
	rkey: string;
10 +
	title: string;
11 +
	path: string | null;
12 +
	site: string | null;
13 +
	content: {
14 +
		$type: string;
15 +
		markdown?: string;
16 +
	} | null;
17 +
	textContent: string | null;
18 +
	publishedAt: string | null;
19 +
	viewUrl: string | null;
20 +
}
21 +
22 +
interface FeedResponse {
23 +
	count: number;
24 +
	limit: number;
25 +
	offset: number;
26 +
	documents: Document[];
27 +
}
28 +
29 +
function App() {
30 +
	const [documents, setDocuments] = useState<Document[]>([]);
31 +
	const [loading, setLoading] = useState(true);
32 +
	const [error, setError] = useState<string | null>(null);
33 +
34 +
	useEffect(() => {
35 +
		async function fetchFeed() {
36 +
			try {
37 +
				const response = await fetch(`${API_URL}/feed`);
38 +
				if (!response.ok) {
39 +
					throw new Error("Failed to fetch feed");
40 +
				}
41 +
				const data: FeedResponse = await response.json();
42 +
				setDocuments(data.documents);
43 +
			} catch (err) {
44 +
				setError(err instanceof Error ? err.message : "Unknown error");
45 +
			} finally {
46 +
				setLoading(false);
47 +
			}
48 +
		}
49 +
50 +
		fetchFeed();
51 +
	}, []);
52 +
53 +
	const formatDate = (dateString: string | null) => {
54 +
		if (!dateString) return "Unknown date";
55 +
		return new Date(dateString).toLocaleDateString("en-US", {
56 +
			year: "numeric",
57 +
			month: "long",
58 +
			day: "numeric",
59 +
		});
60 +
	};
61 +
62 +
	const truncateText = (text: string | null, maxLength: number = 200) => {
63 +
		if (!text) return "";
64 +
		if (text.length <= maxLength) return text;
65 +
		return text.slice(0, maxLength) + "...";
66 +
	};
67 +
68 +
	if (loading) {
69 +
		return (
70 +
			<div className="container">
71 +
				<h1>Standard.site Documents</h1>
72 +
				<p className="loading">Loading documents...</p>
73 +
			</div>
74 +
		);
75 +
	}
76 +
77 +
	if (error) {
78 +
		return (
79 +
			<div className="container">
80 +
				<h1>Standard.site Documents</h1>
81 +
				<p className="error">Error: {error}</p>
82 +
			</div>
83 +
		);
84 +
	}
85 +
86 +
	return (
87 +
		<div className="container">
88 +
			<h1>Standard.site Documents</h1>
89 +
			<p className="subtitle">{documents.length} documents found</p>
90 +
91 +
			<div className="feed">
92 +
				{documents.map((doc) => (
93 +
					<article key={doc.uri} className="document-card">
94 +
						<h2>
95 +
							{doc.viewUrl ? (
96 +
								<a href={doc.viewUrl} target="_blank" rel="noopener noreferrer">
97 +
									{doc.title}
98 +
								</a>
99 +
							) : (
100 +
								doc.title
101 +
							)}
102 +
						</h2>
103 +
						<time dateTime={doc.publishedAt || undefined}>
104 +
							{formatDate(doc.publishedAt)}
105 +
						</time>
106 +
						{doc.textContent && (
107 +
							<p className="excerpt">{truncateText(doc.textContent)}</p>
108 +
						)}
109 +
						{doc.viewUrl && (
110 +
							<a
111 +
								href={doc.viewUrl}
112 +
								target="_blank"
113 +
								rel="noopener noreferrer"
114 +
								className="read-more"
115 +
							>
116 +
								Read on author's site
117 +
							</a>
118 +
						)}
119 +
					</article>
120 +
				))}
121 +
			</div>
122 +
123 +
			{documents.length === 0 && <p className="empty">No documents found.</p>}
124 +
		</div>
125 +
	);
126 +
}
127 +
128 +
export default App;
packages/client/src/index.css (added) +92 −0
1 +
* {
2 +
  box-sizing: border-box;
3 +
  margin: 0;
4 +
  padding: 0;
5 +
}
6 +
7 +
body {
8 +
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
9 +
    Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
10 +
  line-height: 1.6;
11 +
  color: #333;
12 +
  background: #f5f5f5;
13 +
}
14 +
15 +
.container {
16 +
  max-width: 800px;
17 +
  margin: 0 auto;
18 +
  padding: 2rem 1rem;
19 +
}
20 +
21 +
h1 {
22 +
  font-size: 2rem;
23 +
  margin-bottom: 0.5rem;
24 +
}
25 +
26 +
.subtitle {
27 +
  color: #666;
28 +
  margin-bottom: 2rem;
29 +
}
30 +
31 +
.loading,
32 +
.error,
33 +
.empty {
34 +
  text-align: center;
35 +
  padding: 2rem;
36 +
  color: #666;
37 +
}
38 +
39 +
.error {
40 +
  color: #c00;
41 +
}
42 +
43 +
.feed {
44 +
  display: flex;
45 +
  flex-direction: column;
46 +
  gap: 1.5rem;
47 +
}
48 +
49 +
.document-card {
50 +
  background: white;
51 +
  border-radius: 8px;
52 +
  padding: 1.5rem;
53 +
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
54 +
}
55 +
56 +
.document-card h2 {
57 +
  font-size: 1.25rem;
58 +
  margin-bottom: 0.5rem;
59 +
}
60 +
61 +
.document-card h2 a {
62 +
  color: #1a1a1a;
63 +
  text-decoration: none;
64 +
}
65 +
66 +
.document-card h2 a:hover {
67 +
  color: #0066cc;
68 +
  text-decoration: underline;
69 +
}
70 +
71 +
.document-card time {
72 +
  display: block;
73 +
  font-size: 0.875rem;
74 +
  color: #666;
75 +
  margin-bottom: 0.75rem;
76 +
}
77 +
78 +
.document-card .excerpt {
79 +
  color: #444;
80 +
  margin-bottom: 1rem;
81 +
}
82 +
83 +
.document-card .read-more {
84 +
  display: inline-block;
85 +
  color: #0066cc;
86 +
  text-decoration: none;
87 +
  font-size: 0.875rem;
88 +
}
89 +
90 +
.document-card .read-more:hover {
91 +
  text-decoration: underline;
92 +
}
packages/client/src/main.tsx (added) +10 −0
1 +
import React from "react";
2 +
import ReactDOM from "react-dom/client";
3 +
import App from "./App";
4 +
import "./index.css";
5 +
6 +
ReactDOM.createRoot(document.getElementById("root")!).render(
7 +
	<React.StrictMode>
8 +
		<App />
9 +
	</React.StrictMode>,
10 +
);
packages/client/src/vite-env.d.ts (added) +9 −0
1 +
/// <reference types="vite/client" />
2 +
3 +
interface ImportMetaEnv {
4 +
  readonly VITE_API_URL: string
5 +
}
6 +
7 +
interface ImportMeta {
8 +
  readonly env: ImportMetaEnv
9 +
}
packages/client/tsconfig.json (added) +21 −0
1 +
{
2 +
  "compilerOptions": {
3 +
    "target": "ES2020",
4 +
    "useDefineForClassFields": true,
5 +
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 +
    "module": "ESNext",
7 +
    "skipLibCheck": true,
8 +
    "moduleResolution": "bundler",
9 +
    "allowImportingTsExtensions": true,
10 +
    "resolveJsonModule": true,
11 +
    "isolatedModules": true,
12 +
    "noEmit": true,
13 +
    "jsx": "react-jsx",
14 +
    "strict": true,
15 +
    "noUnusedLocals": true,
16 +
    "noUnusedParameters": true,
17 +
    "noFallthroughCasesInSwitch": true
18 +
  },
19 +
  "include": ["src"],
20 +
  "references": [{ "path": "./tsconfig.node.json" }]
21 +
}
packages/client/tsconfig.node.json (added) +11 −0
1 +
{
2 +
  "compilerOptions": {
3 +
    "composite": true,
4 +
    "skipLibCheck": true,
5 +
    "module": "ESNext",
6 +
    "moduleResolution": "bundler",
7 +
    "allowSyntheticDefaultImports": true,
8 +
    "strict": true
9 +
  },
10 +
  "include": ["vite.config.ts"]
11 +
}
packages/client/vite.config.ts (added) +22 −0
1 +
import { defineConfig } from 'vite'
2 +
import react from '@vitejs/plugin-react'
3 +
4 +
export default defineConfig({
5 +
  plugins: [react()],
6 +
  // Define env variables for the client
7 +
  define: {
8 +
    'import.meta.env.VITE_API_URL': JSON.stringify(process.env.VITE_API_URL || ''),
9 +
  },
10 +
  server: {
11 +
    port: 5173,
12 +
    proxy: {
13 +
      '/feed-raw': 'http://localhost:3000',
14 +
      '/feed': 'http://localhost:3000',
15 +
      '/health': 'http://localhost:3000',
16 +
    },
17 +
  },
18 +
  build: {
19 +
    outDir: 'dist',
20 +
    sourcemap: true,
21 +
  },
22 +
})
packages/server/package.json (added) +20 −0
1 +
{
2 +
  "name": "@atfeeds/server",
3 +
  "version": "1.0.0",
4 +
  "private": true,
5 +
  "scripts": {
6 +
    "dev": "wrangler dev",
7 +
    "deploy": "wrangler deploy",
8 +
    "db:create": "wrangler d1 create atfeeds-db",
9 +
    "db:migrate": "wrangler d1 execute atfeeds-db --local --file=./schema.sql",
10 +
    "db:migrate:prod": "wrangler d1 execute atfeeds-db --remote --file=./schema.sql",
11 +
    "secret:set": "wrangler secret put TAP_WEBHOOK_SECRET"
12 +
  },
13 +
  "dependencies": {
14 +
    "hono": "^4.0.0"
15 +
  },
16 +
  "devDependencies": {
17 +
    "@cloudflare/workers-types": "^4.20240117.0",
18 +
    "wrangler": "^3.0.0"
19 +
  }
20 +
}
packages/server/schema.sql (added) +47 −0
1 +
-- Records synced from external tap instance
2 +
CREATE TABLE IF NOT EXISTS repo_records (
3 +
  id INTEGER PRIMARY KEY AUTOINCREMENT,
4 +
  did TEXT NOT NULL,
5 +
  rkey TEXT NOT NULL,
6 +
  collection TEXT NOT NULL,
7 +
  cid TEXT,
8 +
  synced_at TEXT DEFAULT (datetime('now')),
9 +
  UNIQUE(did, collection, rkey)
10 +
);
11 +
12 +
CREATE INDEX IF NOT EXISTS idx_repo_records_collection ON repo_records(collection);
13 +
CREATE INDEX IF NOT EXISTS idx_repo_records_did ON repo_records(did);
14 +
CREATE INDEX IF NOT EXISTS idx_repo_records_rkey ON repo_records(rkey DESC);
15 +
16 +
-- Cache for resolved PDS endpoints
17 +
CREATE TABLE IF NOT EXISTS pds_cache (
18 +
  did TEXT PRIMARY KEY,
19 +
  pds_endpoint TEXT NOT NULL,
20 +
  cached_at TEXT DEFAULT (datetime('now'))
21 +
);
22 +
23 +
-- Sync metadata to track last sync
24 +
CREATE TABLE IF NOT EXISTS sync_metadata (
25 +
  key TEXT PRIMARY KEY,
26 +
  value TEXT NOT NULL,
27 +
  updated_at TEXT DEFAULT (datetime('now'))
28 +
);
29 +
30 +
-- Pre-resolved documents for fast feed serving
31 +
CREATE TABLE IF NOT EXISTS resolved_documents (
32 +
  uri TEXT PRIMARY KEY,
33 +
  did TEXT NOT NULL,
34 +
  rkey TEXT NOT NULL,
35 +
  title TEXT,
36 +
  path TEXT,
37 +
  site TEXT,
38 +
  content TEXT,  -- JSON blob
39 +
  text_content TEXT,
40 +
  published_at TEXT,
41 +
  view_url TEXT,
42 +
  resolved_at TEXT DEFAULT (datetime('now')),
43 +
  stale_at TEXT  -- When this record should be re-resolved
44 +
);
45 +
46 +
CREATE INDEX IF NOT EXISTS idx_resolved_documents_rkey ON resolved_documents(rkey DESC);
47 +
CREATE INDEX IF NOT EXISTS idx_resolved_documents_stale ON resolved_documents(stale_at);
packages/server/src/index.ts (added) +50 −0
1 +
import { Hono } from "hono";
2 +
import { cors } from "hono/cors";
3 +
import type { Bindings } from "./types";
4 +
import { health, webhook, feed, stats, records } from "./routes";
5 +
6 +
const app = new Hono<{ Bindings: Bindings }>();
7 +
8 +
// Middleware
9 +
app.use("*", cors());
10 +
11 +
// Mount routes
12 +
app.route("/health", health);
13 +
app.route("/webhook", webhook);
14 +
app.route("/feed", feed);
15 +
app.route("/stats", stats);
16 +
app.route("/records", records);
17 +
18 +
// Legacy alias: /feed-raw -> /feed/raw
19 +
app.get("/feed-raw", async (c) => {
20 +
	const db = c.env.DB;
21 +
	const limit = Math.min(Number(c.req.query("limit")) || 15, 15);
22 +
	const offset = Number(c.req.query("offset")) || 0;
23 +
24 +
	const { results } = await db
25 +
		.prepare(
26 +
			`SELECT did, rkey FROM repo_records
27 +
       WHERE collection = 'site.standard.document'
28 +
       ORDER BY rkey DESC
29 +
       LIMIT ? OFFSET ?`,
30 +
		)
31 +
		.bind(limit, offset)
32 +
		.all<{ did: string; rkey: string }>();
33 +
34 +
	return c.json({
35 +
		count: results?.length || 0,
36 +
		limit,
37 +
		offset,
38 +
		records: results || [],
39 +
	});
40 +
});
41 +
42 +
// 404 handler
43 +
app.notFound((c) => {
44 +
	return c.json({ error: "Not found" }, 404);
45 +
});
46 +
47 +
// Export for Cloudflare Workers
48 +
export default {
49 +
	fetch: app.fetch,
50 +
};
packages/server/src/routes/feed.ts (added) +93 −0
1 +
import { Hono } from "hono";
2 +
import type { Bindings } from "../types";
3 +
4 +
const feed = new Hono<{ Bindings: Bindings }>();
5 +
6 +
// Get raw feed data (for client-side fetching)
7 +
// Accessible at both /feed/raw and /feed-raw (via alias in index.ts)
8 +
feed.get("/raw", async (c) => {
9 +
  try {
10 +
    const db = c.env.DB;
11 +
    const limit = Math.min(Number(c.req.query("limit")) || 15, 15);
12 +
    const offset = Number(c.req.query("offset")) || 0;
13 +
14 +
    const { results } = await db
15 +
      .prepare(
16 +
        `SELECT did, rkey FROM repo_records
17 +
         WHERE collection = 'site.standard.document'
18 +
         ORDER BY rkey DESC
19 +
         LIMIT ? OFFSET ?`
20 +
      )
21 +
      .bind(limit, offset)
22 +
      .all<{ did: string; rkey: string }>();
23 +
24 +
    return c.json({
25 +
      count: results?.length || 0,
26 +
      limit,
27 +
      offset,
28 +
      records: results || [],
29 +
    });
30 +
  } catch (error) {
31 +
    return c.json(
32 +
      { error: "Failed to fetch feed", details: String(error) },
33 +
      500
34 +
    );
35 +
  }
36 +
});
37 +
38 +
// Get feed of documents with resolved URLs (server-side resolution)
39 +
feed.get("/", async (c) => {
40 +
  try {
41 +
    const db = c.env.DB;
42 +
    const limit = Number(c.req.query("limit")) || 50;
43 +
    const offset = Number(c.req.query("offset")) || 0;
44 +
45 +
    const { results } = await db
46 +
      .prepare(
47 +
        `SELECT uri, did, rkey, title, path, site, content, text_content, published_at, view_url
48 +
         FROM resolved_documents
49 +
         ORDER BY rkey DESC
50 +
         LIMIT ? OFFSET ?`
51 +
      )
52 +
      .bind(limit, offset)
53 +
      .all<{
54 +
        uri: string;
55 +
        did: string;
56 +
        rkey: string;
57 +
        title: string | null;
58 +
        path: string | null;
59 +
        site: string | null;
60 +
        content: string | null;
61 +
        text_content: string | null;
62 +
        published_at: string | null;
63 +
        view_url: string | null;
64 +
      }>();
65 +
66 +
    const documents = (results || []).map((doc) => ({
67 +
      uri: doc.uri,
68 +
      did: doc.did,
69 +
      rkey: doc.rkey,
70 +
      title: doc.title || "Untitled",
71 +
      path: doc.path,
72 +
      site: doc.site,
73 +
      content: doc.content ? JSON.parse(doc.content) : null,
74 +
      textContent: doc.text_content,
75 +
      publishedAt: doc.published_at,
76 +
      viewUrl: doc.view_url,
77 +
    }));
78 +
79 +
    return c.json({
80 +
      count: documents.length,
81 +
      limit,
82 +
      offset,
83 +
      documents,
84 +
    });
85 +
  } catch (error) {
86 +
    return c.json(
87 +
      { error: "Failed to fetch feed", details: String(error) },
88 +
      500
89 +
    );
90 +
  }
91 +
});
92 +
93 +
export default feed;
packages/server/src/routes/health.ts (added) +10 −0
1 +
import { Hono } from "hono";
2 +
import type { Bindings } from "../types";
3 +
4 +
const health = new Hono<{ Bindings: Bindings }>();
5 +
6 +
health.get("/", (c) => {
7 +
  return c.json({ status: "ok", timestamp: new Date().toISOString() });
8 +
});
9 +
10 +
export default health;
packages/server/src/routes/index.ts (added) +5 −0
1 +
export { default as health } from "./health";
2 +
export { default as webhook } from "./webhook";
3 +
export { default as feed } from "./feed";
4 +
export { default as stats } from "./stats";
5 +
export { default as records } from "./records";
packages/server/src/routes/records.ts (added) +38 −0
1 +
import { Hono } from "hono";
2 +
import type { Bindings } from "../types";
3 +
4 +
const records = new Hono<{ Bindings: Bindings }>();
5 +
6 +
records.get("/:did", async (c) => {
7 +
  try {
8 +
    const db = c.env.DB;
9 +
    const did = c.req.param("did");
10 +
    const limit = Number(c.req.query("limit")) || 20;
11 +
    const offset = Number(c.req.query("offset")) || 0;
12 +
13 +
    const { results } = await db
14 +
      .prepare(
15 +
        `SELECT * FROM repo_records
16 +
         WHERE did = ? AND collection = 'site.standard.document'
17 +
         ORDER BY rkey DESC
18 +
         LIMIT ? OFFSET ?`
19 +
      )
20 +
      .bind(did, limit, offset)
21 +
      .all();
22 +
23 +
    return c.json({
24 +
      did,
25 +
      count: results?.length || 0,
26 +
      limit,
27 +
      offset,
28 +
      records: results || [],
29 +
    });
30 +
  } catch (error) {
31 +
    return c.json(
32 +
      { error: "Failed to fetch records", details: String(error) },
33 +
      500
34 +
    );
35 +
  }
36 +
});
37 +
38 +
export default records;
packages/server/src/routes/stats.ts (added) +38 −0
1 +
import { Hono } from "hono";
2 +
import type { Bindings } from "../types";
3 +
4 +
const stats = new Hono<{ Bindings: Bindings }>();
5 +
6 +
stats.get("/", async (c) => {
7 +
  try {
8 +
    const db = c.env.DB;
9 +
    const [records, pdsCache, recordCache, pubCache] = await Promise.all([
10 +
      db
11 +
        .prepare("SELECT COUNT(*) as count FROM repo_records")
12 +
        .first<{ count: number }>(),
13 +
      db
14 +
        .prepare("SELECT COUNT(*) as count FROM pds_cache")
15 +
        .first<{ count: number }>(),
16 +
      db
17 +
        .prepare("SELECT COUNT(*) as count FROM record_cache")
18 +
        .first<{ count: number }>(),
19 +
      db
20 +
        .prepare("SELECT COUNT(*) as count FROM publication_cache")
21 +
        .first<{ count: number }>(),
22 +
    ]);
23 +
24 +
    return c.json({
25 +
      repo_records: records?.count || 0,
26 +
      pds_cache: pdsCache?.count || 0,
27 +
      record_cache: recordCache?.count || 0,
28 +
      publication_cache: pubCache?.count || 0,
29 +
    });
30 +
  } catch (error) {
31 +
    return c.json(
32 +
      { error: "Failed to fetch stats", details: String(error) },
33 +
      500
34 +
    );
35 +
  }
36 +
});
37 +
38 +
export default stats;
packages/server/src/routes/webhook.ts (added) +272 −0
1 +
import { Hono } from "hono";
2 +
import type { Bindings, TapEvent } from "../types";
3 +
import { resolvePds, parseAtUri } from "../utils";
4 +
5 +
const webhook = new Hono<{ Bindings: Bindings }>();
6 +
7 +
async function resolveViewUrl(
8 +
  db: D1Database,
9 +
  siteUri: string,
10 +
  path: string
11 +
): Promise<string | null> {
12 +
  const parsed = parseAtUri(siteUri);
13 +
  if (!parsed) return null;
14 +
15 +
  try {
16 +
    const pds = await resolvePds(db, parsed.did);
17 +
    if (!pds) return null;
18 +
19 +
    const url = `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(parsed.did)}&collection=${encodeURIComponent(parsed.collection)}&rkey=${encodeURIComponent(parsed.rkey)}`;
20 +
    const response = await fetch(url);
21 +
    if (!response.ok) return null;
22 +
23 +
    const data = (await response.json()) as {
24 +
      value?: { url?: string; domain?: string };
25 +
    };
26 +
    const siteUrl = data.value?.url || data.value?.domain;
27 +
    if (!siteUrl) return null;
28 +
29 +
    const baseUrl = siteUrl.startsWith("http") ? siteUrl : `https://${siteUrl}`;
30 +
    return `${baseUrl}${path}`;
31 +
  } catch {
32 +
    return null;
33 +
  }
34 +
}
35 +
36 +
webhook.post("/tap", async (c) => {
37 +
  try {
38 +
    const db = c.env.DB;
39 +
40 +
    const secret = c.env.TAP_WEBHOOK_SECRET;
41 +
    if (secret) {
42 +
      const auth = c.req.header("Authorization");
43 +
      if (auth !== `Bearer ${secret}`) {
44 +
        return c.json({ error: "Unauthorized" }, 401);
45 +
      }
46 +
    }
47 +
48 +
    const event = (await c.req.json()) as TapEvent;
49 +
50 +
    if (event.type === "record") {
51 +
      const { record } = event;
52 +
53 +
      if (record.collection === "site.standard.document") {
54 +
        if (record.action === "create" || record.action === "update") {
55 +
          await db
56 +
            .prepare(
57 +
              `INSERT INTO repo_records (did, rkey, collection, cid, synced_at)
58 +
               VALUES (?, ?, ?, ?, datetime('now'))
59 +
               ON CONFLICT(did, collection, rkey) DO UPDATE SET
60 +
                 cid = ?,
61 +
                 synced_at = datetime('now')`
62 +
            )
63 +
            .bind(
64 +
              record.did,
65 +
              record.rkey,
66 +
              record.collection,
67 +
              record.cid || null,
68 +
              record.cid || null
69 +
            )
70 +
            .run();
71 +
72 +
          if (record.record) {
73 +
            const uri = `at://${record.did}/${record.collection}/${record.rkey}`;
74 +
            const doc = record.record as {
75 +
              title?: string;
76 +
              path?: string;
77 +
              site?: string;
78 +
              content?: unknown;
79 +
              textContent?: string;
80 +
              publishedAt?: string;
81 +
            };
82 +
83 +
            let viewUrl: string | null = null;
84 +
            if (doc.site && doc.path) {
85 +
              viewUrl = await resolveViewUrl(db, doc.site, doc.path);
86 +
            }
87 +
88 +
            await db
89 +
              .prepare(
90 +
                `INSERT INTO resolved_documents (uri, did, rkey, title, path, site, content, text_content, published_at, view_url, resolved_at)
91 +
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
92 +
                 ON CONFLICT(uri) DO UPDATE SET
93 +
                   title = ?, path = ?, site = ?, content = ?, text_content = ?, published_at = ?, view_url = ?, resolved_at = datetime('now')`
94 +
              )
95 +
              .bind(
96 +
                uri,
97 +
                record.did,
98 +
                record.rkey,
99 +
                doc.title || null,
100 +
                doc.path || null,
101 +
                doc.site || null,
102 +
                doc.content ? JSON.stringify(doc.content) : null,
103 +
                doc.textContent || null,
104 +
                doc.publishedAt || null,
105 +
                viewUrl,
106 +
                doc.title || null,
107 +
                doc.path || null,
108 +
                doc.site || null,
109 +
                doc.content ? JSON.stringify(doc.content) : null,
110 +
                doc.textContent || null,
111 +
                doc.publishedAt || null,
112 +
                viewUrl
113 +
              )
114 +
              .run();
115 +
          }
116 +
        } else if (record.action === "delete") {
117 +
          await db
118 +
            .prepare(
119 +
              "DELETE FROM repo_records WHERE did = ? AND collection = ? AND rkey = ?"
120 +
            )
121 +
            .bind(record.did, record.collection, record.rkey)
122 +
            .run();
123 +
124 +
          const uri = `at://${record.did}/${record.collection}/${record.rkey}`;
125 +
          await db
126 +
            .prepare("DELETE FROM resolved_documents WHERE uri = ?")
127 +
            .bind(uri)
128 +
            .run();
129 +
        }
130 +
      }
131 +
    }
132 +
133 +
    return c.json({ ok: true });
134 +
  } catch (error) {
135 +
    console.error("Webhook error:", error);
136 +
    return c.json(
137 +
      { error: "Failed to process webhook", details: String(error) },
138 +
      500
139 +
    );
140 +
  }
141 +
});
142 +
143 +
webhook.post("/tap/batch", async (c) => {
144 +
  try {
145 +
    const db = c.env.DB;
146 +
147 +
    const secret = c.env.TAP_WEBHOOK_SECRET;
148 +
    if (secret) {
149 +
      const auth = c.req.header("Authorization");
150 +
      if (auth !== `Bearer ${secret}`) {
151 +
        return c.json({ error: "Unauthorized" }, 401);
152 +
      }
153 +
    }
154 +
155 +
    const events = (await c.req.json()) as Array<{
156 +
      type: string;
157 +
      did: string;
158 +
      collection?: string;
159 +
      rkey?: string;
160 +
      cid?: string;
161 +
      record?: Record<string, unknown>;
162 +
    }>;
163 +
164 +
    let processed = 0;
165 +
    let errors = 0;
166 +
167 +
    for (const event of events) {
168 +
      try {
169 +
        if (
170 +
          (event.type === "commit" ||
171 +
            event.type === "create" ||
172 +
            event.type === "update") &&
173 +
          event.collection === "site.standard.document" &&
174 +
          event.did &&
175 +
          event.rkey
176 +
        ) {
177 +
          await db
178 +
            .prepare(
179 +
              `INSERT INTO repo_records (did, rkey, collection, cid, synced_at)
180 +
               VALUES (?, ?, ?, ?, datetime('now'))
181 +
               ON CONFLICT(did, collection, rkey) DO UPDATE SET cid = ?, synced_at = datetime('now')`
182 +
            )
183 +
            .bind(
184 +
              event.did,
185 +
              event.rkey,
186 +
              event.collection,
187 +
              event.cid || null,
188 +
              event.cid || null
189 +
            )
190 +
            .run();
191 +
192 +
          if (event.record) {
193 +
            const uri = `at://${event.did}/${event.collection}/${event.rkey}`;
194 +
            const doc = event.record as {
195 +
              title?: string;
196 +
              path?: string;
197 +
              site?: string;
198 +
              content?: unknown;
199 +
              textContent?: string;
200 +
              publishedAt?: string;
201 +
            };
202 +
203 +
            let viewUrl: string | null = null;
204 +
            if (doc.site && doc.path) {
205 +
              viewUrl = await resolveViewUrl(db, doc.site, doc.path);
206 +
            }
207 +
208 +
            await db
209 +
              .prepare(
210 +
                `INSERT INTO resolved_documents (uri, did, rkey, title, path, site, content, text_content, published_at, view_url, resolved_at)
211 +
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
212 +
                 ON CONFLICT(uri) DO UPDATE SET
213 +
                   title = ?, path = ?, site = ?, content = ?, text_content = ?, published_at = ?, view_url = ?, resolved_at = datetime('now')`
214 +
              )
215 +
              .bind(
216 +
                uri,
217 +
                event.did,
218 +
                event.rkey,
219 +
                doc.title || null,
220 +
                doc.path || null,
221 +
                doc.site || null,
222 +
                doc.content ? JSON.stringify(doc.content) : null,
223 +
                doc.textContent || null,
224 +
                doc.publishedAt || null,
225 +
                viewUrl,
226 +
                doc.title || null,
227 +
                doc.path || null,
228 +
                doc.site || null,
229 +
                doc.content ? JSON.stringify(doc.content) : null,
230 +
                doc.textContent || null,
231 +
                doc.publishedAt || null,
232 +
                viewUrl
233 +
              )
234 +
              .run();
235 +
          }
236 +
          processed++;
237 +
        } else if (
238 +
          event.type === "delete" &&
239 +
          event.collection === "site.standard.document" &&
240 +
          event.did &&
241 +
          event.rkey
242 +
        ) {
243 +
          await db
244 +
            .prepare(
245 +
              "DELETE FROM repo_records WHERE did = ? AND collection = ? AND rkey = ?"
246 +
            )
247 +
            .bind(event.did, event.collection, event.rkey)
248 +
            .run();
249 +
250 +
          const uri = `at://${event.did}/${event.collection}/${event.rkey}`;
251 +
          await db
252 +
            .prepare("DELETE FROM resolved_documents WHERE uri = ?")
253 +
            .bind(uri)
254 +
            .run();
255 +
          processed++;
256 +
        }
257 +
      } catch {
258 +
        errors++;
259 +
      }
260 +
    }
261 +
262 +
    return c.json({ ok: true, processed, errors });
263 +
  } catch (error) {
264 +
    console.error("Batch webhook error:", error);
265 +
    return c.json(
266 +
      { error: "Failed to process batch webhook", details: String(error) },
267 +
      500
268 +
    );
269 +
  }
270 +
});
271 +
272 +
export default webhook;
packages/server/src/types/index.ts (added) +45 −0
1 +
export type Bindings = {
2 +
  DB: D1Database;
3 +
  TAP_WEBHOOK_SECRET?: string;
4 +
};
5 +
6 +
export interface TapRecordEvent {
7 +
  id: number;
8 +
  type: "record";
9 +
  record: {
10 +
    live: boolean;
11 +
    rev: string;
12 +
    did: string;
13 +
    collection: string;
14 +
    rkey: string;
15 +
    action: "create" | "update" | "delete";
16 +
    cid?: string;
17 +
    record?: Record<string, unknown>;
18 +
  };
19 +
}
20 +
21 +
export interface TapIdentityEvent {
22 +
  id: number;
23 +
  type: "identity";
24 +
  identity: {
25 +
    did: string;
26 +
    handle: string;
27 +
    isActive: boolean;
28 +
    status: string;
29 +
  };
30 +
}
31 +
32 +
export type TapEvent = TapRecordEvent | TapIdentityEvent;
33 +
34 +
export interface Document {
35 +
  uri: string;
36 +
  did: string;
37 +
  rkey: string;
38 +
  title: string;
39 +
  path: string | null;
40 +
  site: string | null;
41 +
  content: unknown;
42 +
  textContent: string | null;
43 +
  publishedAt: string | null;
44 +
  viewUrl: string | null;
45 +
}
packages/server/src/utils/at-uri.ts (added) +15 −0
1 +
export interface AtUriComponents {
2 +
  did: string;
3 +
  collection: string;
4 +
  rkey: string;
5 +
}
6 +
7 +
export function parseAtUri(uri: string): AtUriComponents | null {
8 +
  const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/);
9 +
  if (!match) return null;
10 +
  return { did: match[1], collection: match[2], rkey: match[3] };
11 +
}
12 +
13 +
export function buildAtUri(did: string, collection: string, rkey: string): string {
14 +
  return `at://${did}/${collection}/${rkey}`;
15 +
}
packages/server/src/utils/index.ts (added) +2 −0
1 +
export { parseAtUri, buildAtUri, type AtUriComponents } from "./at-uri";
2 +
export { resolvePds } from "./resolver";
packages/server/src/utils/resolver.ts (added) +49 −0
1 +
// PDS cache TTL: 1 hour (PDS endpoints rarely change)
2 +
const PDS_CACHE_TTL_MS = 60 * 60 * 1000;
3 +
4 +
function isPdsCacheValid(cachedAt: string | null): boolean {
5 +
  if (!cachedAt) return false;
6 +
  const cacheTime = new Date(cachedAt).getTime();
7 +
  return Date.now() - cacheTime < PDS_CACHE_TTL_MS;
8 +
}
9 +
10 +
export async function resolvePds(
11 +
  db: D1Database,
12 +
  did: string
13 +
): Promise<string | null> {
14 +
  const cached = await db
15 +
    .prepare("SELECT pds_endpoint, cached_at FROM pds_cache WHERE did = ?")
16 +
    .bind(did)
17 +
    .first<{ pds_endpoint: string; cached_at: string }>();
18 +
19 +
  if (cached && isPdsCacheValid(cached.cached_at)) {
20 +
    return cached.pds_endpoint;
21 +
  }
22 +
23 +
  try {
24 +
    const response = await fetch(`https://plc.directory/${did}`);
25 +
    if (!response.ok) return null;
26 +
27 +
    const doc = (await response.json()) as {
28 +
      service?: Array<{ id: string; type: string; serviceEndpoint: string }>;
29 +
    };
30 +
31 +
    const pds = doc.service?.find((s) => s.id === "#atproto_pds");
32 +
    if (pds?.serviceEndpoint) {
33 +
      await db
34 +
        .prepare(
35 +
          `INSERT INTO pds_cache (did, pds_endpoint, cached_at)
36 +
           VALUES (?, ?, datetime('now'))
37 +
           ON CONFLICT(did) DO UPDATE SET pds_endpoint = ?, cached_at = datetime('now')`
38 +
        )
39 +
        .bind(did, pds.serviceEndpoint, pds.serviceEndpoint)
40 +
        .run();
41 +
42 +
      return pds.serviceEndpoint;
43 +
    }
44 +
45 +
    return null;
46 +
  } catch {
47 +
    return null;
48 +
  }
49 +
}
packages/server/tsconfig.json (added) +17 −0
1 +
{
2 +
  "compilerOptions": {
3 +
    "target": "ES2022",
4 +
    "module": "ESNext",
5 +
    "moduleResolution": "bundler",
6 +
    "strict": true,
7 +
    "skipLibCheck": true,
8 +
    "lib": ["ES2022"],
9 +
    "types": ["@cloudflare/workers-types"],
10 +
    "noEmit": true,
11 +
    "isolatedModules": true,
12 +
    "esModuleInterop": true,
13 +
    "allowSyntheticDefaultImports": true
14 +
  },
15 +
  "include": ["src/**/*"],
16 +
  "exclude": ["node_modules"]
17 +
}
packages/server/wrangler.toml (added) +31 −0
1 +
name = "atfeeds-api"
2 +
main = "src/index.ts"
3 +
compatibility_date = "2024-01-01"
4 +
5 +
# D1 Database binding
6 +
[[d1_databases]]
7 +
binding = "DB"
8 +
database_name = "atfeeds-db"
9 +
database_id = "bfbb9955-1496-47e9-9602-e32c9b1fa7b2"
10 +
11 +
# Queue for processing document resolution
12 +
# [[queues.producers]]
13 +
# queue = "document-resolution"
14 +
# binding = "RESOLUTION_QUEUE"
15 +
16 +
# [[queues.consumers]]
17 +
# queue = "document-resolution"
18 +
# max_batch_size = 10
19 +
# max_batch_timeout = 30
20 +
21 +
# Cron trigger to refresh stale documents
22 +
[triggers]
23 +
crons = ["*/15 * * * *"]  # Every 15 minutes
24 +
25 +
# Environment variables (secrets should be set via wrangler secret)
26 +
# TAP_WEBHOOK_SECRET - Optional secret for webhook authentication
27 +
# Set via: wrangler secret put TAP_WEBHOOK_SECRET
28 +
29 +
# Development settings
30 +
[dev]
31 +
port = 8787