Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.

Compare changes

Choose any two refs to compare.

+72
.env.grafana.example
··· 1 + # Grafana Cloud Configuration for wisp.place monorepo 2 + # Copy these variables to your .env file to enable Grafana integration 3 + # The observability package will automatically pick up these environment variables 4 + 5 + # ============================================================================ 6 + # Grafana Loki (for logs) 7 + # ============================================================================ 8 + # Get this from your Grafana Cloud portal under Loki โ†’ Details 9 + # Example: https://logs-prod-012.grafana.net 10 + GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net 11 + 12 + # Authentication Option 1: Bearer Token (Grafana Cloud) 13 + GRAFANA_LOKI_TOKEN=glc_xxx 14 + 15 + # Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 16 + # GRAFANA_LOKI_USERNAME=your-username 17 + # GRAFANA_LOKI_PASSWORD=your-password 18 + 19 + # ============================================================================ 20 + # Grafana Prometheus (for metrics) 21 + # ============================================================================ 22 + # Get this from your Grafana Cloud portal under Prometheus โ†’ Details 23 + # Note: You need to add /api/prom to the base URL for OTLP export 24 + # Example: https://prometheus-prod-10-prod-us-central-0.grafana.net/api/prom 25 + GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom 26 + 27 + # Authentication Option 1: Bearer Token (Grafana Cloud) 28 + GRAFANA_PROMETHEUS_TOKEN=glc_xxx 29 + 30 + # Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 31 + # GRAFANA_PROMETHEUS_USERNAME=your-username 32 + # GRAFANA_PROMETHEUS_PASSWORD=your-password 33 + 34 + # ============================================================================ 35 + # Optional Configuration 36 + # ============================================================================ 37 + # These will be used by both main-app and hosting-service if not overridden 38 + 39 + # Service metadata (optional - defaults are provided in code) 40 + # SERVICE_NAME=wisp-app 41 + # SERVICE_VERSION=1.0.0 42 + 43 + # Batching configuration (optional) 44 + # GRAFANA_BATCH_SIZE=100 # Flush after this many entries 45 + # GRAFANA_FLUSH_INTERVAL=5000 # Flush every 5 seconds 46 + 47 + # ============================================================================ 48 + # How to get these values: 49 + # ============================================================================ 50 + # 1. Sign up for Grafana Cloud at https://grafana.com/ 51 + # 2. Go to your Grafana Cloud portal 52 + # 3. For Loki: 53 + # - Navigate to "Connections" โ†’ "Loki" 54 + # - Click "Details" 55 + # - Copy the Push endpoint URL (without /loki/api/v1/push) 56 + # - Create an API token with push permissions 57 + # 4. For Prometheus: 58 + # - Navigate to "Connections" โ†’ "Prometheus" 59 + # - Click "Details" 60 + # - Copy the Remote Write endpoint (add /api/prom for OTLP) 61 + # - Create an API token with write permissions 62 + 63 + # ============================================================================ 64 + # Testing the integration: 65 + # ============================================================================ 66 + # 1. Copy this file's contents to your .env file 67 + # 2. Fill in the actual values 68 + # 3. Restart your services (main-app and hosting-service) 69 + # 4. Check your Grafana Cloud dashboard for incoming data 70 + # 5. Use Grafana Explore to query: 71 + # - Loki: {job="main-app"} or {job="hosting-service"} 72 + # - Prometheus: http_requests_total{service="main-app"}
+52 -42
.tangled/workflows/deploy-wisp.yml
··· 1 - # Deploy to Wisp.place 2 - # This workflow builds your site and deploys it to Wisp.place using the wisp-cli 1 + --- 3 2 when: 4 - - event: ['push'] 5 - branch: ['main'] 6 - - event: ['manual'] 7 - engine: 'nixery' 3 + - event: 4 + - push 5 + branch: 6 + - main 7 + - event: 8 + - manual 9 + engine: nixery 8 10 clone: 9 - skip: false 10 - depth: 1 11 - submodules: true 11 + skip: false 12 + depth: 1 13 + submodules: true 12 14 dependencies: 13 - nixpkgs: 14 - - git 15 - - gcc 16 - github:NixOS/nixpkgs/nixpkgs-unstable: 17 - - rustc 18 - - cargo 15 + nixpkgs: 16 + - git 17 + - gcc 18 + github:NixOS/nixpkgs/nixpkgs-unstable: 19 + - rustc 20 + - cargo 21 + - bun 19 22 environment: 20 - # Customize these for your project 21 - SITE_PATH: 'testDeploy' 22 - SITE_NAME: 'wispPlaceDocs' 23 + WISP_HANDLE: wisp.place 24 + SITE_PATH: docs/dist 25 + SITE_NAME: docs 23 26 steps: 24 - - name: 'Initialize submodules' 25 - command: | 26 - git submodule update --init --recursive 27 + - name: Initialize submodules 28 + command: | 29 + git submodule update --init --recursive 30 + - name: Build wisp-cli 31 + command: | 32 + cd cli 27 33 28 - - name: 'Build wisp-cli' 29 - command: | 30 - cd cli 31 - export PATH="$HOME/.nix-profile/bin:$PATH" 32 - nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs 33 - nix-channel --update 34 - nix-shell -p pkg-config openssl --run ' 35 - export PKG_CONFIG_PATH="$(pkg-config --variable pc_path pkg-config)" 36 - export OPENSSL_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.dev)" 37 - export OPENSSL_NO_VENDOR=1 38 - export OPENSSL_LIB_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.out)/lib" 39 - cargo build --release 40 - ' 41 - cd .. 34 + export PATH="$HOME/.nix-profile/bin:$PATH" 35 + 36 + nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs 37 + 38 + nix-channel --update 39 + 40 + nix-shell -p pkg-config openssl --run ' 41 + export PKG_CONFIG_PATH="$(pkg-config --variable pc_path pkg-config)" 42 + export OPENSSL_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.dev)" 43 + export OPENSSL_NO_VENDOR=1 44 + export OPENSSL_LIB_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.out)/lib" 45 + cargo build --release 46 + ' 42 47 43 - - name: 'Deploy to Wisp.place' 44 - command: | 45 - ./cli/target/release/wisp-cli \ 46 - "$WISP_HANDLE" \ 47 - --path "$SITE_PATH" \ 48 - --site "$SITE_NAME" \ 49 - --password "$WISP_APP_PASSWORD" 48 + cd .. 49 + - name: Build docs 50 + command: | 51 + cd docs 52 + bun run build 53 + - name: Deploy to Wisp.place 54 + command: | 55 + ./cli/target/release/wisp-cli \ 56 + "$WISP_HANDLE" \ 57 + --path "$SITE_PATH" \ 58 + --site "$SITE_NAME" \ 59 + --password "$WISP_APP_PASSWORD"
-1
.tangled/workflows/test.yml
··· 17 17 18 18 # have to regenerate otherwise it wont install necessary dependencies to run 19 19 rm -rf bun.lock package-lock.json 20 - bun install @oven/bun-linux-aarch64 21 20 bun install 22 21 23 22 - name: run all tests
+74
.wispignore.json
··· 1 + { 2 + "version": "1.0.0", 3 + "description": "Default ignore patterns for wisp.place uploads", 4 + "patterns": [ 5 + ".git", 6 + ".git/**", 7 + ".github", 8 + ".github/**", 9 + ".gitlab", 10 + ".gitlab/**", 11 + ".DS_Store", 12 + ".wisp.metadata.json", 13 + ".env", 14 + ".env.*", 15 + "node_modules", 16 + "node_modules/**", 17 + "Thumbs.db", 18 + "desktop.ini", 19 + "._*", 20 + ".Spotlight-V100", 21 + ".Spotlight-V100/**", 22 + ".Trashes", 23 + ".Trashes/**", 24 + ".fseventsd", 25 + ".fseventsd/**", 26 + ".cache", 27 + ".cache/**", 28 + ".temp", 29 + ".temp/**", 30 + ".tmp", 31 + ".tmp/**", 32 + "__pycache__", 33 + "__pycache__/**", 34 + "*.pyc", 35 + ".venv", 36 + ".venv/**", 37 + "venv", 38 + "venv/**", 39 + "env", 40 + "env/**", 41 + "*.swp", 42 + "*.swo", 43 + "*~", 44 + ".tangled", 45 + ".tangled/**" 46 + ], 47 + "comments": { 48 + ".git": "Version control - thousands of files", 49 + ".github": "GitHub workflows and configuration", 50 + ".gitlab": "GitLab CI/CD configuration", 51 + ".DS_Store": "macOS metadata - can leak info", 52 + ".wisp.metadata.json": "Wisp internal metadata", 53 + ".env": "Environment variables with secrets", 54 + "node_modules": "Dependency folder - can be 100,000+ files", 55 + "Thumbs.db": "Windows thumbnail cache", 56 + "desktop.ini": "Windows folder config", 57 + "._*": "macOS resource fork files", 58 + ".Spotlight-V100": "macOS Spotlight index", 59 + ".Trashes": "macOS trash folder", 60 + ".fseventsd": "macOS filesystem events", 61 + ".cache": "Cache directories", 62 + ".temp": "Temporary directories", 63 + ".tmp": "Temporary directories", 64 + "__pycache__": "Python cache", 65 + "*.pyc": "Python compiled files", 66 + ".venv": "Python virtual environments", 67 + "venv": "Python virtual environments", 68 + "env": "Python virtual environments", 69 + "*.swp": "Vim swap files", 70 + "*.swo": "Vim swap files", 71 + "*~": "Editor backup files", 72 + ".tangled": "Tangled directory" 73 + } 74 + }
+15 -58
Dockerfile
··· 1 - # Build stage 2 - FROM oven/bun:1.3 AS build 1 + # Production stage 2 + FROM oven/bun:1.3 3 3 4 4 WORKDIR /app 5 5 ··· 7 7 COPY package.json bunfig.toml tsconfig.json bun.lock* ./ 8 8 9 9 # Copy all workspace package.json files first (for dependency resolution) 10 - COPY packages ./packages 10 + COPY packages/@wisp/atproto-utils/package.json ./packages/@wisp/atproto-utils/package.json 11 + COPY packages/@wisp/constants/package.json ./packages/@wisp/constants/package.json 12 + COPY packages/@wisp/database/package.json ./packages/@wisp/database/package.json 13 + COPY packages/@wisp/fs-utils/package.json ./packages/@wisp/fs-utils/package.json 14 + COPY packages/@wisp/lexicons/package.json ./packages/@wisp/lexicons/package.json 15 + COPY packages/@wisp/observability/package.json ./packages/@wisp/observability/package.json 16 + COPY packages/@wisp/safe-fetch/package.json ./packages/@wisp/safe-fetch/package.json 11 17 COPY apps/main-app/package.json ./apps/main-app/package.json 12 18 COPY apps/hosting-service/package.json ./apps/hosting-service/package.json 13 19 14 - # Install all dependencies (including workspaces) 15 - RUN bun install --frozen-lockfile 20 + # Install dependencies 21 + RUN bun install --frozen-lockfile --production 16 22 17 - # Copy source files 18 - COPY apps/main-app ./apps/main-app 19 - 20 - # Build compiled server 21 - RUN bun build \ 22 - --compile \ 23 - --target bun \ 24 - --minify \ 25 - --outfile server \ 26 - apps/main-app/src/index.ts 27 - 28 - # Production dependencies stage 29 - FROM oven/bun:1.3 AS prod-deps 30 - 31 - WORKDIR /app 32 - 33 - COPY package.json bunfig.toml tsconfig.json bun.lock* ./ 23 + # Copy workspace source files 34 24 COPY packages ./packages 35 - COPY apps/main-app/package.json ./apps/main-app/package.json 36 - COPY apps/hosting-service/package.json ./apps/hosting-service/package.json 37 25 38 - # Install only production dependencies 39 - RUN bun install --frozen-lockfile --production 40 - 41 - # Remove unnecessary large packages (bun is already in base image, these are dev tools) 42 - RUN rm -rf /app/node_modules/bun \ 43 - /app/node_modules/@oven \ 44 - /app/node_modules/prettier \ 45 - /app/node_modules/@ts-morph 46 - 47 - # Final stage - use distroless or slim debian-based image 48 - FROM debian:bookworm-slim 49 - 50 - # Install Bun runtime 51 - COPY --from=oven/bun:1.3 /usr/local/bin/bun /usr/local/bin/bun 52 - 53 - WORKDIR /app 54 - 55 - # Copy compiled server 56 - COPY --from=build /app/server /app/server 57 - 58 - # Copy public files 59 - COPY apps/main-app/public apps/main-app/public 60 - 61 - # Copy production dependencies only 62 - COPY --from=prod-deps /app/node_modules /app/node_modules 63 - 64 - # Copy configs 65 - COPY package.json bunfig.toml tsconfig.json /app/ 66 - COPY apps/main-app/tsconfig.json /app/apps/main-app/tsconfig.json 67 - COPY apps/main-app/package.json /app/apps/main-app/package.json 68 - 69 - # Create symlink for module resolution 70 - RUN ln -s /app/node_modules /app/apps/main-app/node_modules 26 + # Copy app source and public files 27 + COPY apps/main-app ./apps/main-app 71 28 72 29 ENV PORT=8000 73 30 74 31 EXPOSE 8000 75 32 76 - CMD ["./server"] 33 + CMD ["bun", "run", "apps/main-app/src/index.ts"]
+20 -3
README.md
··· 39 39 40 40 ```bash 41 41 # Backend 42 + # bun install will install packages across the monorepo 42 43 bun install 43 - bun run src/index.ts 44 + bun run dev 44 45 45 46 # Hosting service 46 - cd hosting-service 47 - npm run start 47 + bun run hosting:dev 48 48 49 49 # CLI 50 50 cd cli ··· 52 52 ``` 53 53 54 54 ## Features 55 + 56 + ### File Filtering and `.wispignore` 57 + 58 + Wisp automatically excludes common files that shouldn't be uploaded to your site (like `.git`, `node_modules`, `.env` files, etc.). You can customize this behavior by creating a `.wispignore` file in your site root. 59 + 60 + The `.wispignore` file uses the same syntax as `.gitignore`: 61 + ``` 62 + # Custom ignore patterns 63 + *.log 64 + temp/ 65 + build/ 66 + .secret 67 + ``` 68 + 69 + Default patterns include: `.git`, `.github`, `.gitlab`, `.DS_Store`, `node_modules`, `.env`, cache directories, Python virtual environments, editor swap files, `.tangled`, and more. 70 + 71 + See [File Filtering Documentation](./docs/src/content/docs/file-filtering.md) for details. 55 72 56 73 ### URL Redirects and Rewrites 57 74
+4 -2
apps/hosting-service/package.json
··· 6 6 "dev": "tsx --env-file=.env src/index.ts", 7 7 "build": "bun run build.ts", 8 8 "start": "tsx src/index.ts", 9 + "check": "tsc --noEmit", 9 10 "backfill": "tsx src/index.ts --backfill" 10 11 }, 11 12 "dependencies": { ··· 18 19 "@wisp/safe-fetch": "workspace:*", 19 20 "@atproto/api": "^0.17.4", 20 21 "@atproto/identity": "^0.4.9", 21 - "@atproto/lexicon": "^0.5.1", 22 + "@atproto/lexicon": "^0.5.2", 22 23 "@atproto/sync": "^0.1.36", 23 24 "@atproto/xrpc": "^0.7.5", 24 25 "@hono/node-server": "^1.19.6", ··· 31 32 "@types/bun": "^1.3.1", 32 33 "@types/mime-types": "^2.1.4", 33 34 "@types/node": "^22.10.5", 34 - "tsx": "^4.19.2" 35 + "tsx": "^4.19.2", 36 + "typescript": "^5.9.3" 35 37 } 36 38 }
+15 -3
apps/hosting-service/src/index.ts
··· 1 1 import app from './server'; 2 2 import { serve } from '@hono/node-server'; 3 3 import { FirehoseWorker } from './lib/firehose'; 4 - import { createLogger } from '@wisp/observability'; 4 + import { createLogger, initializeGrafanaExporters } from '@wisp/observability'; 5 5 import { mkdirSync, existsSync } from 'fs'; 6 6 import { backfillCache } from './lib/backfill'; 7 - import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode } from './lib/db'; 7 + import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode, closeDatabase } from './lib/db'; 8 + 9 + // Initialize Grafana exporters if configured 10 + initializeGrafanaExporters({ 11 + serviceName: 'hosting-service', 12 + serviceVersion: '1.0.0' 13 + }); 8 14 9 15 const logger = createLogger('hosting-service'); 10 16 11 17 const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001; 12 18 const CACHE_DIR = process.env.CACHE_DIR || './cache/sites'; 19 + const BACKFILL_CONCURRENCY = process.env.BACKFILL_CONCURRENCY 20 + ? parseInt(process.env.BACKFILL_CONCURRENCY) 21 + : undefined; // Let backfill.ts default (10) apply 13 22 14 23 // Parse CLI arguments 15 24 const args = process.argv.slice(2); ··· 46 55 console.log('๐Ÿ”„ Backfill requested, starting cache backfill...'); 47 56 backfillCache({ 48 57 skipExisting: true, 49 - concurrency: 3, 58 + concurrency: BACKFILL_CONCURRENCY, 50 59 }).then((stats) => { 51 60 console.log('โœ… Cache backfill completed'); 52 61 }).catch((err) => { ··· 77 86 Cache: ${CACHE_DIR} 78 87 Firehose: Connected to Firehose 79 88 Cache-Only: ${CACHE_ONLY_MODE ? 'ENABLED (no DB writes)' : 'DISABLED'} 89 + Backfill: ${backfillOnStartup ? `ENABLED (concurrency: ${BACKFILL_CONCURRENCY || 10})` : 'DISABLED'} 80 90 `); 81 91 82 92 // Graceful shutdown ··· 84 94 console.log('\n๐Ÿ›‘ Shutting down...'); 85 95 firehose.stop(); 86 96 stopDomainCacheCleanup(); 97 + await closeDatabase(); 87 98 server.close(); 88 99 process.exit(0); 89 100 }); ··· 92 103 console.log('\n๐Ÿ›‘ Shutting down...'); 93 104 firehose.stop(); 94 105 stopDomainCacheCleanup(); 106 + await closeDatabase(); 95 107 server.close(); 96 108 process.exit(0); 97 109 });
+65 -57
apps/hosting-service/src/lib/backfill.ts
··· 60 60 console.log(`โš™๏ธ Limited to ${maxSites} sites for backfill`); 61 61 } 62 62 63 - // Process sites in batches 64 - const batches: typeof sites[] = []; 65 - for (let i = 0; i < sites.length; i += concurrency) { 66 - batches.push(sites.slice(i, i + concurrency)); 67 - } 68 - 63 + // Process sites with sliding window concurrency pool 64 + const executing = new Set<Promise<void>>(); 69 65 let processed = 0; 70 - for (const batch of batches) { 71 - await Promise.all( 72 - batch.map(async (site) => { 73 - try { 74 - // Check if already cached 75 - if (skipExisting && isCached(site.did, site.rkey)) { 76 - stats.skipped++; 77 - processed++; 78 - logger.debug(`Skipping already cached site`, { did: site.did, rkey: site.rkey }); 79 - console.log(`โญ๏ธ [${processed}/${sites.length}] Skipped (cached): ${site.display_name || site.rkey}`); 80 - return; 81 - } 82 66 83 - // Fetch site record 84 - const siteData = await fetchSiteRecord(site.did, site.rkey); 85 - if (!siteData) { 86 - stats.failed++; 87 - processed++; 88 - logger.error('Site record not found during backfill', null, { did: site.did, rkey: site.rkey }); 89 - console.log(`โŒ [${processed}/${sites.length}] Failed (not found): ${site.display_name || site.rkey}`); 90 - return; 91 - } 92 - 93 - // Get PDS endpoint 94 - const pdsEndpoint = await getPdsForDid(site.did); 95 - if (!pdsEndpoint) { 96 - stats.failed++; 97 - processed++; 98 - logger.error('PDS not found during backfill', null, { did: site.did }); 99 - console.log(`โŒ [${processed}/${sites.length}] Failed (no PDS): ${site.display_name || site.rkey}`); 100 - return; 101 - } 67 + for (const site of sites) { 68 + // Create task for this site 69 + const processSite = async () => { 70 + try { 71 + // Check if already cached 72 + if (skipExisting && isCached(site.did, site.rkey)) { 73 + stats.skipped++; 74 + processed++; 75 + logger.debug(`Skipping already cached site`, { did: site.did, rkey: site.rkey }); 76 + console.log(`โญ๏ธ [${processed}/${sites.length}] Skipped (cached): ${site.display_name || site.rkey}`); 77 + return; 78 + } 102 79 103 - // Mark site as being cached to prevent serving stale content during update 104 - markSiteAsBeingCached(site.did, site.rkey); 80 + // Fetch site record 81 + const siteData = await fetchSiteRecord(site.did, site.rkey); 82 + if (!siteData) { 83 + stats.failed++; 84 + processed++; 85 + logger.error('Site record not found during backfill', null, { did: site.did, rkey: site.rkey }); 86 + console.log(`โŒ [${processed}/${sites.length}] Failed (not found): ${site.display_name || site.rkey}`); 87 + return; 88 + } 105 89 106 - try { 107 - // Download and cache site 108 - await downloadAndCacheSite(site.did, site.rkey, siteData.record, pdsEndpoint, siteData.cid); 109 - // Clear redirect rules cache since the site was updated 110 - clearRedirectRulesCache(site.did, site.rkey); 111 - stats.cached++; 112 - processed++; 113 - logger.info('Successfully cached site during backfill', { did: site.did, rkey: site.rkey }); 114 - console.log(`โœ… [${processed}/${sites.length}] Cached: ${site.display_name || site.rkey}`); 115 - } finally { 116 - // Always unmark, even if caching fails 117 - unmarkSiteAsBeingCached(site.did, site.rkey); 118 - } 119 - } catch (err) { 90 + // Get PDS endpoint 91 + const pdsEndpoint = await getPdsForDid(site.did); 92 + if (!pdsEndpoint) { 120 93 stats.failed++; 121 94 processed++; 122 - logger.error('Failed to cache site during backfill', err, { did: site.did, rkey: site.rkey }); 123 - console.log(`โŒ [${processed}/${sites.length}] Failed: ${site.display_name || site.rkey}`); 95 + logger.error('PDS not found during backfill', null, { did: site.did }); 96 + console.log(`โŒ [${processed}/${sites.length}] Failed (no PDS): ${site.display_name || site.rkey}`); 97 + return; 98 + } 99 + 100 + // Mark site as being cached to prevent serving stale content during update 101 + markSiteAsBeingCached(site.did, site.rkey); 102 + 103 + try { 104 + // Download and cache site 105 + await downloadAndCacheSite(site.did, site.rkey, siteData.record, pdsEndpoint, siteData.cid); 106 + // Clear redirect rules cache since the site was updated 107 + clearRedirectRulesCache(site.did, site.rkey); 108 + stats.cached++; 109 + processed++; 110 + logger.info('Successfully cached site during backfill', { did: site.did, rkey: site.rkey }); 111 + console.log(`โœ… [${processed}/${sites.length}] Cached: ${site.display_name || site.rkey}`); 112 + } finally { 113 + // Always unmark, even if caching fails 114 + unmarkSiteAsBeingCached(site.did, site.rkey); 124 115 } 125 - }) 126 - ); 116 + } catch (err) { 117 + stats.failed++; 118 + processed++; 119 + logger.error('Failed to cache site during backfill', err, { did: site.did, rkey: site.rkey }); 120 + console.log(`โŒ [${processed}/${sites.length}] Failed: ${site.display_name || site.rkey}`); 121 + } 122 + }; 123 + 124 + // Add to executing pool and remove when done 125 + const promise = processSite().finally(() => executing.delete(promise)); 126 + executing.add(promise); 127 + 128 + // When pool is full, wait for at least one to complete 129 + if (executing.size >= concurrency) { 130 + await Promise.race(executing); 131 + } 127 132 } 133 + 134 + // Wait for all remaining tasks to complete 135 + await Promise.all(executing); 128 136 129 137 stats.duration = Date.now() - startTime; 130 138
+32 -1
apps/hosting-service/src/lib/db.ts
··· 183 183 return hashNum & 0x7FFFFFFFFFFFFFFFn; 184 184 } 185 185 186 + // Track active locks for cleanup on shutdown 187 + const activeLocks = new Set<string>(); 188 + 186 189 /** 187 190 * Acquire a distributed lock using PostgreSQL advisory locks 188 191 * Returns true if lock was acquired, false if already held by another instance ··· 193 196 194 197 try { 195 198 const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`; 196 - return result[0]?.acquired === true; 199 + const acquired = result[0]?.acquired === true; 200 + if (acquired) { 201 + activeLocks.add(key); 202 + } 203 + return acquired; 197 204 } catch (err) { 198 205 console.error('Failed to acquire lock', { key, error: err }); 199 206 return false; ··· 208 215 209 216 try { 210 217 await sql`SELECT pg_advisory_unlock(${Number(lockId)})`; 218 + activeLocks.delete(key); 211 219 } catch (err) { 212 220 console.error('Failed to release lock', { key, error: err }); 221 + // Still remove from tracking even if unlock fails 222 + activeLocks.delete(key); 223 + } 224 + } 225 + 226 + /** 227 + * Close all database connections 228 + * Call this during graceful shutdown 229 + */ 230 + export async function closeDatabase(): Promise<void> { 231 + try { 232 + // Release all active advisory locks before closing connections 233 + if (activeLocks.size > 0) { 234 + console.log(`[DB] Releasing ${activeLocks.size} active advisory locks before shutdown`); 235 + for (const key of activeLocks) { 236 + await releaseLock(key); 237 + } 238 + } 239 + 240 + await sql.end({ timeout: 5 }); 241 + console.log('[DB] Database connections closed'); 242 + } catch (err) { 243 + console.error('[DB] Error closing database connections:', err); 213 244 } 214 245 } 215 246
+473 -8
apps/hosting-service/src/lib/utils.test.ts
··· 1 1 import { describe, test, expect } from 'bun:test' 2 - import { sanitizePath, extractBlobCid } from './utils' 2 + import { sanitizePath, extractBlobCid, extractSubfsUris, expandSubfsNodes } from './utils' 3 3 import { CID } from 'multiformats' 4 + import { BlobRef } from '@atproto/lexicon' 5 + import type { 6 + Record as WispFsRecord, 7 + Directory as FsDirectory, 8 + Entry as FsEntry, 9 + File as FsFile, 10 + Subfs as FsSubfs, 11 + } from '@wisp/lexicons/types/place/wisp/fs' 12 + import type { 13 + Record as SubfsRecord, 14 + Directory as SubfsDirectory, 15 + Entry as SubfsEntry, 16 + File as SubfsFile, 17 + Subfs as SubfsSubfs, 18 + } from '@wisp/lexicons/types/place/wisp/subfs' 19 + import type { $Typed } from '@wisp/lexicons/util' 4 20 5 21 describe('sanitizePath', () => { 6 22 test('allows normal file paths', () => { ··· 31 47 32 48 test('blocks directory traversal in middle of path', () => { 33 49 expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd') 34 - // Note: sanitizePath only filters out ".." segments, doesn't resolve paths 35 50 expect(sanitizePath('a/b/../c')).toBe('a/b/c') 36 51 expect(sanitizePath('a/../b/../c')).toBe('a/b/c') 37 52 }) ··· 50 65 }) 51 66 52 67 test('blocks null bytes', () => { 53 - // Null bytes cause the entire segment to be filtered out 54 68 expect(sanitizePath('index.html\0.txt')).toBe('') 55 69 expect(sanitizePath('test\0')).toBe('') 56 - // Null byte in middle segment 57 70 expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css') 58 71 }) 59 72 ··· 89 102 90 103 describe('extractBlobCid', () => { 91 104 const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 92 - 105 + 93 106 test('extracts CID from IPLD link', () => { 94 107 const blobRef = { $link: TEST_CID } 95 108 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 103 116 }) 104 117 105 118 test('extracts CID from typed BlobRef with IPLD link', () => { 106 - const blobRef = { 119 + const blobRef = { 107 120 ref: { $link: TEST_CID } 108 121 } 109 122 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 129 142 }) 130 143 131 144 test('handles nested structures from AT Proto API', () => { 132 - // Real structure from AT Proto 133 145 const blobRef = { 134 146 $type: 'blob', 135 147 ref: CID.parse(TEST_CID), ··· 150 162 }) 151 163 152 164 test('prioritizes checking IPLD link first', () => { 153 - // Direct $link takes precedence 154 165 const directLink = { $link: TEST_CID } 155 166 expect(extractBlobCid(directLink)).toBe(TEST_CID) 156 167 }) ··· 167 178 expect(extractBlobCid(blobRef)).toBe(cidV1) 168 179 }) 169 180 }) 181 + 182 + const TEST_CID_BASE = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 183 + 184 + function createMockBlobRef(cidSuffix: string = '', size: number = 100, mimeType: string = 'text/plain'): BlobRef { 185 + const cidString = TEST_CID_BASE 186 + return new BlobRef(CID.parse(cidString), mimeType, size) 187 + } 188 + 189 + function createFsFile( 190 + name: string, 191 + options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {} 192 + ): FsEntry { 193 + const { mimeType = 'text/plain', size = 100, encoding, base64 } = options 194 + const file: $Typed<FsFile, 'place.wisp.fs#file'> = { 195 + $type: 'place.wisp.fs#file', 196 + type: 'file', 197 + blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType), 198 + ...(encoding && { encoding }), 199 + ...(mimeType && { mimeType }), 200 + ...(base64 && { base64 }), 201 + } 202 + return { name, node: file } 203 + } 204 + 205 + function createFsDirectory(name: string, entries: FsEntry[]): FsEntry { 206 + const dir: $Typed<FsDirectory, 'place.wisp.fs#directory'> = { 207 + $type: 'place.wisp.fs#directory', 208 + type: 'directory', 209 + entries, 210 + } 211 + return { name, node: dir } 212 + } 213 + 214 + function createFsSubfs(name: string, subject: string, flat: boolean = true): FsEntry { 215 + const subfs: $Typed<FsSubfs, 'place.wisp.fs#subfs'> = { 216 + $type: 'place.wisp.fs#subfs', 217 + type: 'subfs', 218 + subject, 219 + flat, 220 + } 221 + return { name, node: subfs } 222 + } 223 + 224 + function createFsRootDirectory(entries: FsEntry[]): FsDirectory { 225 + return { 226 + $type: 'place.wisp.fs#directory', 227 + type: 'directory', 228 + entries, 229 + } 230 + } 231 + 232 + function createFsRecord(site: string, entries: FsEntry[], fileCount?: number): WispFsRecord { 233 + return { 234 + $type: 'place.wisp.fs', 235 + site, 236 + root: createFsRootDirectory(entries), 237 + ...(fileCount !== undefined && { fileCount }), 238 + createdAt: new Date().toISOString(), 239 + } 240 + } 241 + 242 + function createSubfsFile( 243 + name: string, 244 + options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {} 245 + ): SubfsEntry { 246 + const { mimeType = 'text/plain', size = 100, encoding, base64 } = options 247 + const file: $Typed<SubfsFile, 'place.wisp.subfs#file'> = { 248 + $type: 'place.wisp.subfs#file', 249 + type: 'file', 250 + blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType), 251 + ...(encoding && { encoding }), 252 + ...(mimeType && { mimeType }), 253 + ...(base64 && { base64 }), 254 + } 255 + return { name, node: file } 256 + } 257 + 258 + function createSubfsDirectory(name: string, entries: SubfsEntry[]): SubfsEntry { 259 + const dir: $Typed<SubfsDirectory, 'place.wisp.subfs#directory'> = { 260 + $type: 'place.wisp.subfs#directory', 261 + type: 'directory', 262 + entries, 263 + } 264 + return { name, node: dir } 265 + } 266 + 267 + function createSubfsSubfs(name: string, subject: string): SubfsEntry { 268 + const subfs: $Typed<SubfsSubfs, 'place.wisp.subfs#subfs'> = { 269 + $type: 'place.wisp.subfs#subfs', 270 + type: 'subfs', 271 + subject, 272 + } 273 + return { name, node: subfs } 274 + } 275 + 276 + function createSubfsRootDirectory(entries: SubfsEntry[]): SubfsDirectory { 277 + return { 278 + $type: 'place.wisp.subfs#directory', 279 + type: 'directory', 280 + entries, 281 + } 282 + } 283 + 284 + function createSubfsRecord(entries: SubfsEntry[], fileCount?: number): SubfsRecord { 285 + return { 286 + $type: 'place.wisp.subfs', 287 + root: createSubfsRootDirectory(entries), 288 + ...(fileCount !== undefined && { fileCount }), 289 + createdAt: new Date().toISOString(), 290 + } 291 + } 292 + 293 + describe('extractSubfsUris', () => { 294 + test('extracts subfs URIs from flat directory structure', () => { 295 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/a' 296 + const dir = createFsRootDirectory([ 297 + createFsSubfs('a', subfsUri), 298 + createFsFile('file.txt'), 299 + ]) 300 + 301 + const uris = extractSubfsUris(dir) 302 + 303 + expect(uris).toHaveLength(1) 304 + expect(uris[0]).toEqual({ uri: subfsUri, path: 'a' }) 305 + }) 306 + 307 + test('extracts subfs URIs from nested directory structure', () => { 308 + const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 309 + const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 310 + 311 + const dir = createFsRootDirectory([ 312 + createFsSubfs('a', subfsAUri), 313 + createFsDirectory('nested', [ 314 + createFsSubfs('b', subfsBUri), 315 + createFsFile('file.txt'), 316 + ]), 317 + ]) 318 + 319 + const uris = extractSubfsUris(dir) 320 + 321 + expect(uris).toHaveLength(2) 322 + expect(uris).toContainEqual({ uri: subfsAUri, path: 'a' }) 323 + expect(uris).toContainEqual({ uri: subfsBUri, path: 'nested/b' }) 324 + }) 325 + 326 + test('returns empty array when no subfs nodes exist', () => { 327 + const dir = createFsRootDirectory([ 328 + createFsFile('file1.txt'), 329 + createFsDirectory('dir', [createFsFile('file2.txt')]), 330 + ]) 331 + 332 + const uris = extractSubfsUris(dir) 333 + expect(uris).toHaveLength(0) 334 + }) 335 + 336 + test('handles deeply nested subfs', () => { 337 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/deep' 338 + const dir = createFsRootDirectory([ 339 + createFsDirectory('a', [ 340 + createFsDirectory('b', [ 341 + createFsDirectory('c', [ 342 + createFsSubfs('deep', subfsUri), 343 + ]), 344 + ]), 345 + ]), 346 + ]) 347 + 348 + const uris = extractSubfsUris(dir) 349 + 350 + expect(uris).toHaveLength(1) 351 + expect(uris[0]).toEqual({ uri: subfsUri, path: 'a/b/c/deep' }) 352 + }) 353 + }) 354 + 355 + describe('expandSubfsNodes caching', () => { 356 + test('cache map is populated after expansion', async () => { 357 + const subfsCache = new Map<string, SubfsRecord | null>() 358 + const dir = createFsRootDirectory([createFsFile('file.txt')]) 359 + 360 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 361 + 362 + expect(subfsCache.size).toBe(0) 363 + expect(result.entries).toHaveLength(1) 364 + expect(result.entries[0]?.name).toBe('file.txt') 365 + }) 366 + 367 + test('cache is passed through recursion depths', async () => { 368 + const subfsCache = new Map<string, SubfsRecord | null>() 369 + const mockSubfsUri = 'at://did:plc:test/place.wisp.subfs/cached' 370 + const mockRecord = createSubfsRecord([createSubfsFile('cached-file.txt')]) 371 + subfsCache.set(mockSubfsUri, mockRecord) 372 + 373 + const dir = createFsRootDirectory([createFsSubfs('cached', mockSubfsUri)]) 374 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 375 + 376 + expect(subfsCache.has(mockSubfsUri)).toBe(true) 377 + expect(result.entries).toHaveLength(1) 378 + expect(result.entries[0]?.name).toBe('cached-file.txt') 379 + }) 380 + 381 + test('pre-populated cache prevents re-fetching', async () => { 382 + const subfsCache = new Map<string, SubfsRecord | null>() 383 + const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 384 + const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 385 + 386 + subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('b', subfsBUri)])) 387 + subfsCache.set(subfsBUri, createSubfsRecord([createSubfsFile('final.txt')])) 388 + 389 + const dir = createFsRootDirectory([createFsSubfs('a', subfsAUri)]) 390 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 391 + 392 + expect(result.entries).toHaveLength(1) 393 + expect(result.entries[0]?.name).toBe('final.txt') 394 + }) 395 + 396 + test('diamond dependency uses cache for shared reference', async () => { 397 + const subfsCache = new Map<string, SubfsRecord | null>() 398 + const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 399 + const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 400 + const subfsCUri = 'at://did:plc:test/place.wisp.subfs/c' 401 + 402 + subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)])) 403 + subfsCache.set(subfsBUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)])) 404 + subfsCache.set(subfsCUri, createSubfsRecord([createSubfsFile('shared.txt')])) 405 + 406 + const dir = createFsRootDirectory([ 407 + createFsSubfs('a', subfsAUri), 408 + createFsSubfs('b', subfsBUri), 409 + ]) 410 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 411 + 412 + expect(result.entries.filter(e => e.name === 'shared.txt')).toHaveLength(2) 413 + }) 414 + 415 + test('handles null records in cache gracefully', async () => { 416 + const subfsCache = new Map<string, SubfsRecord | null>() 417 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/missing' 418 + subfsCache.set(subfsUri, null) 419 + 420 + const dir = createFsRootDirectory([ 421 + createFsFile('file.txt'), 422 + createFsSubfs('missing', subfsUri), 423 + ]) 424 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 425 + 426 + expect(result.entries.some(e => e.name === 'file.txt')).toBe(true) 427 + expect(result.entries.some(e => e.name === 'missing')).toBe(true) 428 + }) 429 + 430 + test('non-flat subfs merge creates directory instead of hoisting', async () => { 431 + const subfsCache = new Map<string, SubfsRecord | null>() 432 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/nested' 433 + subfsCache.set(subfsUri, createSubfsRecord([createSubfsFile('nested-file.txt')])) 434 + 435 + const dir = createFsRootDirectory([ 436 + createFsFile('root.txt'), 437 + createFsSubfs('subdir', subfsUri, false), 438 + ]) 439 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 440 + 441 + expect(result.entries).toHaveLength(2) 442 + 443 + const rootFile = result.entries.find(e => e.name === 'root.txt') 444 + expect(rootFile).toBeDefined() 445 + 446 + const subdir = result.entries.find(e => e.name === 'subdir') 447 + expect(subdir).toBeDefined() 448 + 449 + if (subdir && 'entries' in subdir.node) { 450 + expect(subdir.node.type).toBe('directory') 451 + expect(subdir.node.entries).toHaveLength(1) 452 + expect(subdir.node.entries[0]?.name).toBe('nested-file.txt') 453 + } 454 + }) 455 + }) 456 + 457 + describe('WispFsRecord mock builders', () => { 458 + test('createFsRecord creates valid record structure', () => { 459 + const record = createFsRecord('my-site', [ 460 + createFsFile('index.html', { mimeType: 'text/html' }), 461 + createFsDirectory('assets', [ 462 + createFsFile('style.css', { mimeType: 'text/css' }), 463 + ]), 464 + ]) 465 + 466 + expect(record.$type).toBe('place.wisp.fs') 467 + expect(record.site).toBe('my-site') 468 + expect(record.root.type).toBe('directory') 469 + expect(record.root.entries).toHaveLength(2) 470 + expect(record.createdAt).toBeDefined() 471 + }) 472 + 473 + test('createFsFile creates valid file entry', () => { 474 + const entry = createFsFile('test.html', { mimeType: 'text/html', size: 500 }) 475 + 476 + expect(entry.name).toBe('test.html') 477 + 478 + const file = entry.node 479 + if ('blob' in file) { 480 + expect(file.$type).toBe('place.wisp.fs#file') 481 + expect(file.type).toBe('file') 482 + expect(file.blob).toBeDefined() 483 + expect(file.mimeType).toBe('text/html') 484 + } 485 + }) 486 + 487 + test('createFsFile with gzip encoding', () => { 488 + const entry = createFsFile('bundle.js', { mimeType: 'application/javascript', encoding: 'gzip' }) 489 + 490 + const file = entry.node 491 + if ('encoding' in file) { 492 + expect(file.encoding).toBe('gzip') 493 + } 494 + }) 495 + 496 + test('createFsFile with base64 flag', () => { 497 + const entry = createFsFile('data.bin', { base64: true }) 498 + 499 + const file = entry.node 500 + if ('base64' in file) { 501 + expect(file.base64).toBe(true) 502 + } 503 + }) 504 + 505 + test('createFsDirectory creates valid directory entry', () => { 506 + const entry = createFsDirectory('assets', [ 507 + createFsFile('file1.txt'), 508 + createFsFile('file2.txt'), 509 + ]) 510 + 511 + expect(entry.name).toBe('assets') 512 + 513 + const dir = entry.node 514 + if ('entries' in dir) { 515 + expect(dir.$type).toBe('place.wisp.fs#directory') 516 + expect(dir.type).toBe('directory') 517 + expect(dir.entries).toHaveLength(2) 518 + } 519 + }) 520 + 521 + test('createFsSubfs creates valid subfs entry with flat=true', () => { 522 + const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext') 523 + 524 + expect(entry.name).toBe('external') 525 + 526 + const subfs = entry.node 527 + if ('subject' in subfs) { 528 + expect(subfs.$type).toBe('place.wisp.fs#subfs') 529 + expect(subfs.type).toBe('subfs') 530 + expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/ext') 531 + expect(subfs.flat).toBe(true) 532 + } 533 + }) 534 + 535 + test('createFsSubfs creates valid subfs entry with flat=false', () => { 536 + const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext', false) 537 + 538 + const subfs = entry.node 539 + if ('subject' in subfs) { 540 + expect(subfs.flat).toBe(false) 541 + } 542 + }) 543 + 544 + test('createFsRecord with fileCount', () => { 545 + const record = createFsRecord('my-site', [createFsFile('index.html')], 1) 546 + expect(record.fileCount).toBe(1) 547 + }) 548 + }) 549 + 550 + describe('SubfsRecord mock builders', () => { 551 + test('createSubfsRecord creates valid record structure', () => { 552 + const record = createSubfsRecord([ 553 + createSubfsFile('file1.txt'), 554 + createSubfsDirectory('nested', [ 555 + createSubfsFile('file2.txt'), 556 + ]), 557 + ]) 558 + 559 + expect(record.$type).toBe('place.wisp.subfs') 560 + expect(record.root.type).toBe('directory') 561 + expect(record.root.entries).toHaveLength(2) 562 + expect(record.createdAt).toBeDefined() 563 + }) 564 + 565 + test('createSubfsFile creates valid file entry', () => { 566 + const entry = createSubfsFile('data.json', { mimeType: 'application/json', size: 1024 }) 567 + 568 + expect(entry.name).toBe('data.json') 569 + 570 + const file = entry.node 571 + if ('blob' in file) { 572 + expect(file.$type).toBe('place.wisp.subfs#file') 573 + expect(file.type).toBe('file') 574 + expect(file.blob).toBeDefined() 575 + expect(file.mimeType).toBe('application/json') 576 + } 577 + }) 578 + 579 + test('createSubfsDirectory creates valid directory entry', () => { 580 + const entry = createSubfsDirectory('subdir', [createSubfsFile('inner.txt')]) 581 + 582 + expect(entry.name).toBe('subdir') 583 + 584 + const dir = entry.node 585 + if ('entries' in dir) { 586 + expect(dir.$type).toBe('place.wisp.subfs#directory') 587 + expect(dir.type).toBe('directory') 588 + expect(dir.entries).toHaveLength(1) 589 + } 590 + }) 591 + 592 + test('createSubfsSubfs creates valid nested subfs entry', () => { 593 + const entry = createSubfsSubfs('deeper', 'at://did:plc:test/place.wisp.subfs/deeper') 594 + 595 + expect(entry.name).toBe('deeper') 596 + 597 + const subfs = entry.node 598 + if ('subject' in subfs) { 599 + expect(subfs.$type).toBe('place.wisp.subfs#subfs') 600 + expect(subfs.type).toBe('subfs') 601 + expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/deeper') 602 + expect('flat' in subfs).toBe(false) 603 + } 604 + }) 605 + 606 + test('createSubfsRecord with fileCount', () => { 607 + const record = createSubfsRecord([createSubfsFile('file.txt')], 1) 608 + expect(record.fileCount).toBe(1) 609 + }) 610 + }) 611 + 612 + describe('extractBlobCid with typed mock data', () => { 613 + test('extracts CID from FsFile blob', () => { 614 + const entry = createFsFile('test.txt') 615 + const file = entry.node 616 + 617 + if ('blob' in file) { 618 + const cid = extractBlobCid(file.blob) 619 + expect(cid).toBeDefined() 620 + expect(cid).toContain('bafkrei') 621 + } 622 + }) 623 + 624 + test('extracts CID from SubfsFile blob', () => { 625 + const entry = createSubfsFile('test.txt') 626 + const file = entry.node 627 + 628 + if ('blob' in file) { 629 + const cid = extractBlobCid(file.blob) 630 + expect(cid).toBeDefined() 631 + expect(cid).toContain('bafkrei') 632 + } 633 + }) 634 + })
+108 -18
apps/hosting-service/src/lib/utils.ts
··· 7 7 import { safeFetchJson, safeFetchBlob } from '@wisp/safe-fetch'; 8 8 import { CID } from 'multiformats'; 9 9 import { extractBlobCid } from '@wisp/atproto-utils'; 10 - import { sanitizePath, collectFileCidsFromEntries } from '@wisp/fs-utils'; 10 + import { sanitizePath, collectFileCidsFromEntries, countFilesInDirectory } from '@wisp/fs-utils'; 11 11 import { shouldCompressMimeType } from '@wisp/atproto-utils/compression'; 12 + import { MAX_BLOB_SIZE, MAX_FILE_COUNT, MAX_SITE_SIZE } from '@wisp/constants'; 12 13 13 14 // Re-export shared utilities for local usage and tests 14 15 export { extractBlobCid, sanitizePath }; ··· 89 90 export async function fetchSiteRecord(did: string, rkey: string): Promise<{ record: WispFsRecord; cid: string } | null> { 90 91 try { 91 92 const pdsEndpoint = await getPdsForDid(did); 92 - if (!pdsEndpoint) return null; 93 + if (!pdsEndpoint) { 94 + console.error('[hosting-service] Failed to get PDS endpoint for DID', { did, rkey }); 95 + return null; 96 + } 93 97 94 98 const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(rkey)}`; 95 99 const data = await safeFetchJson(url); ··· 99 103 cid: data.cid || '' 100 104 }; 101 105 } catch (err) { 102 - console.error('Failed to fetch site record', did, rkey, err); 106 + const errorCode = (err as any)?.code; 107 + const errorMsg = err instanceof Error ? err.message : String(err); 108 + 109 + // Better error logging to distinguish between network errors and 404s 110 + if (errorMsg.includes('HTTP 404') || errorMsg.includes('Not Found')) { 111 + console.log('[hosting-service] Site record not found', { did, rkey }); 112 + } else if (errorCode && ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR', 'ETIMEDOUT'].includes(errorCode)) { 113 + console.error('[hosting-service] Network/SSL error fetching site record (after retries)', { 114 + did, 115 + rkey, 116 + error: errorMsg, 117 + code: errorCode 118 + }); 119 + } else { 120 + console.error('[hosting-service] Failed to fetch site record', { 121 + did, 122 + rkey, 123 + error: errorMsg, 124 + code: errorCode 125 + }); 126 + } 127 + 103 128 return null; 104 129 } 105 130 } ··· 120 145 } 121 146 122 147 /** 148 + * Calculate total size of all blobs in a directory tree from manifest metadata 149 + */ 150 + function calculateTotalBlobSize(directory: Directory): number { 151 + let totalSize = 0; 152 + 153 + function sumBlobSizes(entries: Entry[]) { 154 + for (const entry of entries) { 155 + const node = entry.node; 156 + 157 + if ('type' in node && node.type === 'directory' && 'entries' in node) { 158 + // Recursively sum subdirectories 159 + sumBlobSizes(node.entries); 160 + } else if ('type' in node && node.type === 'file' && 'blob' in node) { 161 + // Add blob size from manifest 162 + const fileNode = node as File; 163 + const blobSize = (fileNode.blob as any)?.size || 0; 164 + totalSize += blobSize; 165 + } 166 + } 167 + } 168 + 169 + sumBlobSizes(directory.entries); 170 + return totalSize; 171 + } 172 + 173 + /** 123 174 * Extract all subfs URIs from a directory tree with their mount paths 124 175 */ 125 - function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { 176 + export function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { 126 177 const uris: Array<{ uri: string; path: string }> = []; 127 178 128 179 for (const entry of directory.entries) { ··· 182 233 * Replace subfs nodes in a directory tree with their actual content 183 234 * Subfs entries are "merged" - their root entries are hoisted into the parent directory 184 235 * This function is recursive - it will keep expanding until no subfs nodes remain 236 + * Uses a cache to avoid re-fetching the same subfs records across recursion depths 185 237 */ 186 - async function expandSubfsNodes(directory: Directory, pdsEndpoint: string, depth: number = 0): Promise<Directory> { 238 + export async function expandSubfsNodes( 239 + directory: Directory, 240 + pdsEndpoint: string, 241 + depth: number = 0, 242 + subfsCache: Map<string, SubfsRecord | null> = new Map() 243 + ): Promise<Directory> { 187 244 const MAX_DEPTH = 10; // Prevent infinite loops 188 245 189 246 if (depth >= MAX_DEPTH) { ··· 199 256 return directory; 200 257 } 201 258 202 - console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs records, fetching...`); 259 + // Filter to only URIs we haven't fetched yet 260 + const uncachedUris = subfsUris.filter(({ uri }) => !subfsCache.has(uri)); 203 261 204 - // Fetch all subfs records in parallel 205 - const subfsRecords = await Promise.all( 206 - subfsUris.map(async ({ uri, path }) => { 207 - const record = await fetchSubfsRecord(uri, pdsEndpoint); 208 - return { record, path }; 209 - }) 210 - ); 262 + if (uncachedUris.length > 0) { 263 + console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, fetching ${uncachedUris.length} new records (${subfsUris.length - uncachedUris.length} cached)...`); 211 264 212 - // Build a map of path -> root entries to merge 265 + // Fetch only uncached subfs records in parallel 266 + const fetchedRecords = await Promise.all( 267 + uncachedUris.map(async ({ uri }) => { 268 + const record = await fetchSubfsRecord(uri, pdsEndpoint); 269 + return { uri, record }; 270 + }) 271 + ); 272 + 273 + // Add fetched records to cache 274 + for (const { uri, record } of fetchedRecords) { 275 + subfsCache.set(uri, record); 276 + } 277 + } else { 278 + console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, all cached`); 279 + } 280 + 281 + // Build a map of path -> root entries to merge using the cache 213 282 // Note: SubFS entries are compatible with FS entries at runtime 214 283 const subfsMap = new Map<string, Entry[]>(); 215 - for (const { record, path } of subfsRecords) { 284 + for (const { uri, path } of subfsUris) { 285 + const record = subfsCache.get(uri); 216 286 if (record && record.root && record.root.entries) { 217 287 subfsMap.set(path, record.root.entries as unknown as Entry[]); 218 288 } ··· 280 350 }; 281 351 282 352 // Recursively expand any remaining subfs nodes (e.g., nested subfs inside parent subfs) 283 - return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1); 353 + // Pass the cache to avoid re-fetching records 354 + return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1, subfsCache); 284 355 } 285 356 286 357 ··· 299 370 300 371 // Expand subfs nodes before caching 301 372 const expandedRoot = await expandSubfsNodes(record.root, pdsEndpoint); 373 + 374 + // Verify all subfs nodes were expanded 375 + const remainingSubfs = extractSubfsUris(expandedRoot); 376 + if (remainingSubfs.length > 0) { 377 + console.warn(`[Cache] Warning: ${remainingSubfs.length} subfs nodes remain unexpanded after expansion`, remainingSubfs); 378 + } 379 + 380 + // Validate file count limit 381 + const fileCount = countFilesInDirectory(expandedRoot); 382 + if (fileCount > MAX_FILE_COUNT) { 383 + throw new Error(`Site exceeds file count limit: ${fileCount} files (max ${MAX_FILE_COUNT})`); 384 + } 385 + console.log(`[Cache] File count validation passed: ${fileCount} files (limit: ${MAX_FILE_COUNT})`); 386 + 387 + // Validate total size from blob metadata 388 + const totalBlobSize = calculateTotalBlobSize(expandedRoot); 389 + if (totalBlobSize > MAX_SITE_SIZE) { 390 + throw new Error(`Site exceeds size limit: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (max ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`); 391 + } 392 + console.log(`[Cache] Size validation passed: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (limit: ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`); 302 393 303 394 // Get existing cache metadata to check for incremental updates 304 395 const existingMetadata = await getCacheMetadata(did, rkey); ··· 514 605 515 606 console.log(`[Cache] Fetching blob for file: ${filePath}, CID: ${cid}`); 516 607 517 - // Allow up to 500MB per file blob, with 5 minute timeout 518 - let content = await safeFetchBlob(blobUrl, { maxSize: 500 * 1024 * 1024, timeout: 300000 }); 608 + let content = await safeFetchBlob(blobUrl, { maxSize: MAX_BLOB_SIZE, timeout: 300000 }); 519 609 520 610 // If content is base64-encoded, decode it back to raw binary (gzipped or not) 521 611 if (base64) {
+20 -15
apps/main-app/package.json
··· 7 7 "dev": "bun run --watch src/index.ts", 8 8 "start": "bun run src/index.ts", 9 9 "build": "bun run build.ts", 10 + "check": "tsc --noEmit", 10 11 "screenshot": "bun run scripts/screenshot-sites.ts" 11 12 }, 12 13 "dependencies": { 13 - "@wisp/lexicons": "workspace:*", 14 - "@wisp/constants": "workspace:*", 15 - "@wisp/observability": "workspace:*", 16 - "@wisp/atproto-utils": "workspace:*", 17 - "@wisp/database": "workspace:*", 18 - "@wisp/fs-utils": "workspace:*", 19 - "@atproto/api": "^0.17.3", 20 - "@atproto/common-web": "^0.4.5", 14 + "@atproto-labs/did-resolver": "^0.2.4", 15 + "@atproto/api": "^0.17.7", 16 + "@atproto/common-web": "^0.4.6", 21 17 "@atproto/jwk-jose": "^0.1.11", 22 - "@atproto/lex-cli": "^0.9.5", 23 - "@atproto/oauth-client-node": "^0.3.9", 24 - "@atproto/xrpc-server": "^0.9.5", 18 + "@atproto/lex-cli": "^0.9.7", 19 + "@atproto/oauth-client-node": "^0.3.12", 20 + "@atproto/xrpc-server": "^0.9.6", 25 21 "@elysiajs/cors": "^1.4.0", 26 22 "@elysiajs/eden": "^1.4.3", 27 23 "@elysiajs/openapi": "^1.4.11", ··· 34 30 "@radix-ui/react-slot": "^1.2.3", 35 31 "@radix-ui/react-tabs": "^1.1.13", 36 32 "@tanstack/react-query": "^5.90.2", 33 + "@wisp/atproto-utils": "workspace:*", 34 + "@wisp/constants": "workspace:*", 35 + "@wisp/database": "workspace:*", 36 + "@wisp/fs-utils": "workspace:*", 37 + "@wisp/lexicons": "workspace:*", 38 + "@wisp/observability": "workspace:*", 37 39 "actor-typeahead": "^0.1.1", 38 - "atproto-ui": "^0.11.3", 40 + "atproto-ui": "^0.12.0", 41 + "bun-plugin-tailwind": "^0.1.2", 39 42 "class-variance-authority": "^0.7.1", 40 43 "clsx": "^2.1.1", 41 - "elysia": "latest", 44 + "elysia": "^1.4.18", 45 + "ignore": "^7.0.5", 42 46 "iron-session": "^8.0.4", 43 47 "lucide-react": "^0.546.0", 44 48 "multiformats": "^13.4.1", ··· 48 52 "tailwind-merge": "^3.3.1", 49 53 "tailwindcss": "4", 50 54 "tw-animate-css": "^1.4.0", 51 - "zlib": "^1.0.5", 52 - "bun-plugin-tailwind": "^0.1.2" 55 + "zlib": "^1.0.5" 53 56 }, 54 57 "devDependencies": { 58 + "@atproto-labs/handle-resolver": "^0.3.4", 59 + "@atproto/did": "^0.2.3", 55 60 "@types/react": "^19.2.2", 56 61 "@types/react-dom": "^19.2.1", 57 62 "bun-types": "latest",
+4 -4
apps/main-app/public/acceptable-use/acceptable-use.tsx
··· 6 6 7 7 function AcceptableUsePage() { 8 8 return ( 9 - <div className="min-h-screen bg-background"> 9 + <div className="w-full min-h-screen bg-background flex flex-col"> 10 10 {/* Header */} 11 - <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 12 - <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 11 + <header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 12 + <div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between"> 13 13 <div className="flex items-center gap-2"> 14 14 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 15 15 <span className="text-xl font-semibold text-foreground"> ··· 326 326 </div> 327 327 328 328 {/* Footer */} 329 - <footer className="border-t border-border/40 bg-muted/20 mt-12"> 329 + <footer className="border-t border-border/40 bg-muted/20 mt-auto"> 330 330 <div className="container mx-auto px-4 py-8"> 331 331 <div className="text-center text-sm text-muted-foreground"> 332 332 <p>
+1 -1
apps/main-app/public/components/ui/checkbox.tsx
··· 12 12 <CheckboxPrimitive.Root 13 13 data-slot="checkbox" 14 14 className={cn( 15 - "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 15 + "peer border-border bg-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 16 16 className 17 17 )} 18 18 {...props}
+6 -6
apps/main-app/public/editor/editor.tsx
··· 302 302 return ( 303 303 <div className="w-full min-h-screen bg-background"> 304 304 {/* Header Skeleton */} 305 - <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 306 - <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 305 + <header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 306 + <div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between"> 307 307 <div className="flex items-center gap-2"> 308 308 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 309 309 <span className="text-xl font-semibold text-foreground"> ··· 366 366 } 367 367 368 368 return ( 369 - <div className="w-full min-h-screen bg-background"> 369 + <div className="w-full min-h-screen bg-background flex flex-col"> 370 370 {/* Header */} 371 - <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 372 - <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 371 + <header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 372 + <div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between"> 373 373 <div className="flex items-center gap-2"> 374 374 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 375 375 <span className="text-xl font-semibold text-foreground"> ··· 454 454 </div> 455 455 456 456 {/* Footer */} 457 - <footer className="border-t border-border/40 bg-muted/20 mt-12"> 457 + <footer className="border-t border-border/40 bg-muted/20 mt-auto"> 458 458 <div className="container mx-auto px-4 py-8"> 459 459 <div className="text-center text-sm text-muted-foreground"> 460 460 <p>
+74 -66
apps/main-app/public/index.tsx
··· 88 88 89 89 const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { 90 90 const navigationKeys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Enter', 'Escape'] 91 - 91 + 92 92 // Mark that we should preserve the index for navigation keys 93 93 if (navigationKeys.includes(e.key)) { 94 94 preserveIndexRef.current = true ··· 142 142 setIndex(-1) 143 143 setIsOpen(false) 144 144 onSelect?.(handle) 145 - 145 + 146 146 // Auto-submit the form if enabled 147 147 if (autoSubmit && inputRef.current) { 148 148 const form = inputRef.current.closest('form') ··· 236 236 height: 'calc(1.5rem + 12px)', 237 237 borderRadius: '4px', 238 238 cursor: 'pointer', 239 - backgroundColor: i === index ? 'hsl(var(--accent) / 0.5)' : 'transparent', 239 + backgroundColor: i === index ? 'color-mix(in oklch, var(--accent) 50%, transparent)' : 'transparent', 240 240 transition: 'background-color 0.1s' 241 241 }} 242 242 onMouseEnter={() => setIndex(i)} ··· 246 246 width: '1.5rem', 247 247 height: '1.5rem', 248 248 borderRadius: '50%', 249 - backgroundColor: 'hsl(var(--muted))', 249 + backgroundColor: 'var(--muted)', 250 250 overflow: 'hidden', 251 251 flexShrink: 0 252 252 }} ··· 255 255 <img 256 256 src={actor.avatar} 257 257 alt="" 258 + loading="lazy" 258 259 style={{ 259 260 display: 'block', 260 261 width: '100%', ··· 359 360 360 361 return ( 361 362 <> 362 - <div className="min-h-screen"> 363 + <div className="w-full min-h-screen flex flex-col"> 363 364 {/* Header */} 364 - <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 365 - <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 365 + <header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 366 + <div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between"> 366 367 <div className="flex items-center gap-2"> 367 368 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 368 - <span className="text-xl font-semibold text-foreground"> 369 + <span className="text-lg font-semibold text-foreground"> 369 370 wisp.place 370 371 </span> 371 372 </div> 372 - <div className="flex items-center gap-3"> 373 + <div className="flex items-center gap-4"> 374 + <a 375 + href="https://docs.wisp.place" 376 + target="_blank" 377 + rel="noopener noreferrer" 378 + className="text-sm text-muted-foreground hover:text-foreground transition-colors" 379 + > 380 + Read the Docs 381 + </a> 373 382 <Button 374 - variant="ghost" 383 + variant="outline" 375 384 size="sm" 385 + className="btn-hover-lift" 376 386 onClick={() => setShowForm(true)} 377 387 > 378 388 Sign In 379 389 </Button> 380 - <Button 381 - size="sm" 382 - className="bg-accent text-accent-foreground hover:bg-accent/90" 383 - asChild 384 - > 385 - <a href="https://docs.wisp.place" target="_blank" rel="noopener noreferrer"> 386 - Read the Docs 387 - </a> 388 - </Button> 389 390 </div> 390 391 </div> 391 392 </header> 392 393 393 394 {/* Hero Section */} 394 - <section className="container mx-auto px-4 py-20 md:py-32"> 395 + <section className="container mx-auto px-4 py-24 md:py-36"> 395 396 <div className="max-w-4xl mx-auto text-center"> 396 - <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 mb-8"> 397 - <span className="w-2 h-2 bg-accent rounded-full animate-pulse"></span> 398 - <span className="text-sm text-foreground"> 399 - Built on AT Protocol 400 - </span> 401 - </div> 402 - 403 - <h1 className="text-5xl md:text-7xl font-bold text-balance mb-6 leading-tight"> 404 - Your Website.Your Control. Lightning Fast. 397 + {/* Main Headline */} 398 + <h1 className="animate-fade-in-up animate-delay-100 text-5xl md:text-7xl font-bold mb-2 leading-tight tracking-tight"> 399 + Deploy Anywhere. 400 + </h1> 401 + <h1 className="animate-fade-in-up animate-delay-200 text-5xl md:text-7xl font-bold mb-8 leading-tight tracking-tight text-gradient-animate"> 402 + For Free. Forever. 405 403 </h1> 406 404 407 - <p className="text-xl md:text-2xl text-muted-foreground text-balance mb-10 leading-relaxed max-w-3xl mx-auto"> 408 - Host static sites in your AT Protocol account. You 409 - keep ownership and control. We just serve them fast 410 - through our CDN. 405 + {/* Subheadline */} 406 + <p className="animate-fade-in-up animate-delay-300 text-lg md:text-xl text-muted-foreground mb-12 leading-relaxed max-w-2xl mx-auto"> 407 + The easiest way to deploy and orchestrate static sites. 408 + Push updates instantly. Host on our infrastructure or yours. 409 + All powered by AT Protocol. 411 410 </p> 412 411 413 - <div className="max-w-md mx-auto relative"> 412 + {/* CTA Buttons */} 413 + <div className="animate-fade-in-up animate-delay-400 max-w-lg mx-auto relative"> 414 414 <div 415 - className={`transition-all duration-500 ease-in-out ${ 416 - showForm 417 - ? 'opacity-0 -translate-y-5 pointer-events-none' 418 - : 'opacity-100 translate-y-0' 419 - }`} 415 + className={`transition-all duration-500 ease-in-out ${showForm 416 + ? 'opacity-0 -translate-y-5 pointer-events-none absolute inset-0' 417 + : 'opacity-100 translate-y-0' 418 + }`} 420 419 > 421 - <Button 422 - size="lg" 423 - className="bg-primary text-primary-foreground hover:bg-primary/90 text-lg px-8 py-6 w-full" 424 - onClick={() => setShowForm(true)} 425 - > 426 - Log in with AT Proto 427 - <ArrowRight className="ml-2 w-5 h-5" /> 428 - </Button> 420 + <div className="flex flex-col sm:flex-row gap-3 justify-center"> 421 + <Button 422 + size="lg" 423 + className="bg-foreground text-background hover:bg-foreground/90 text-base px-6 py-5 btn-hover-lift" 424 + onClick={() => setShowForm(true)} 425 + > 426 + <span className="mr-2 font-bold">@</span> 427 + Deploy with AT 428 + </Button> 429 + <Button 430 + variant="outline" 431 + size="lg" 432 + className="text-base px-6 py-5 btn-hover-lift" 433 + asChild 434 + > 435 + <a href="https://docs.wisp.place/cli/" target="_blank" rel="noopener noreferrer"> 436 + <span className="font-mono mr-2 text-muted-foreground">&gt;_</span> 437 + Install wisp-cli 438 + </a> 439 + </Button> 440 + </div> 429 441 </div> 430 442 431 443 <div 432 - className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 433 - showForm 434 - ? 'opacity-100 translate-y-0' 435 - : 'opacity-0 translate-y-5 pointer-events-none' 436 - }`} 444 + className={`transition-all duration-500 ease-in-out ${showForm 445 + ? 'opacity-100 translate-y-0' 446 + : 'opacity-0 translate-y-5 pointer-events-none absolute inset-0' 447 + }`} 437 448 > 438 449 <form 439 450 onSubmit={async (e) => { ··· 494 505 </ActorTypeahead> 495 506 <button 496 507 type="submit" 497 - className="w-full bg-accent hover:bg-accent/90 text-accent-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors" 508 + className="w-full bg-foreground text-background hover:bg-foreground/90 font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors btn-hover-lift" 498 509 > 499 510 Continue 500 511 <ArrowRight className="ml-2 w-5 h-5" /> ··· 518 529 </div> 519 530 <div> 520 531 <h3 className="text-xl font-semibold mb-2"> 521 - Upload your static site 532 + Drop in your files 522 533 </h3> 523 534 <p className="text-muted-foreground"> 524 - Your HTML, CSS, and JavaScript files are 525 - stored in your AT Protocol account as 526 - gzipped blobs and a manifest record. 535 + Upload your site through our dashboard or push with the CLI. 536 + Everything gets stored directly in your AT Protocol account. 527 537 </p> 528 538 </div> 529 539 </div> ··· 533 543 </div> 534 544 <div> 535 545 <h3 className="text-xl font-semibold mb-2"> 536 - We serve it globally 546 + We handle the rest 537 547 </h3> 538 548 <p className="text-muted-foreground"> 539 - Wisp.place reads your site from your 540 - account and delivers it through our CDN 541 - for fast loading anywhere. 549 + Your site goes live instantly on our global CDN. 550 + Custom domains, HTTPS, cachingโ€”all automatic. 542 551 </p> 543 552 </div> 544 553 </div> ··· 548 557 </div> 549 558 <div> 550 559 <h3 className="text-xl font-semibold mb-2"> 551 - You stay in control 560 + Push updates instantly 552 561 </h3> 553 562 <p className="text-muted-foreground"> 554 - Update or remove your site anytime 555 - through your AT Protocol account. No 556 - lock-in, no middleman ownership. 563 + Ship changes in seconds. Update through the dashboard, 564 + run wisp-cli deploy, or wire up your CI/CD pipeline. 557 565 </p> 558 566 </div> 559 567 </div> ··· 686 694 </section> 687 695 688 696 {/* Footer */} 689 - <footer className="border-t border-border/40 bg-muted/20"> 697 + <footer className="border-t border-border/40 bg-muted/20 mt-auto"> 690 698 <div className="container mx-auto px-4 py-8"> 691 699 <div className="text-center text-sm text-muted-foreground"> 692 700 <p>
+16 -19
apps/main-app/public/onboarding/onboarding.tsx
··· 161 161 return ( 162 162 <div className="w-full min-h-screen bg-background"> 163 163 {/* Header */} 164 - <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 165 - <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 164 + <header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 165 + <div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between"> 166 166 <div className="flex items-center gap-2"> 167 167 <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> 168 168 <Globe className="w-5 h-5 text-primary-foreground" /> ··· 179 179 <div className="mb-8"> 180 180 <div className="flex items-center justify-center gap-2 mb-4"> 181 181 <div 182 - className={`w-8 h-8 rounded-full flex items-center justify-center ${ 183 - step === 'domain' 184 - ? 'bg-primary text-primary-foreground' 185 - : 'bg-green-500 text-white' 186 - }`} 182 + className={`w-8 h-8 rounded-full flex items-center justify-center ${step === 'domain' 183 + ? 'bg-primary text-primary-foreground' 184 + : 'bg-green-500 text-white' 185 + }`} 187 186 > 188 187 {step === 'domain' ? ( 189 188 '1' ··· 193 192 </div> 194 193 <div className="w-16 h-0.5 bg-border"></div> 195 194 <div 196 - className={`w-8 h-8 rounded-full flex items-center justify-center ${ 197 - step === 'upload' 198 - ? 'bg-primary text-primary-foreground' 199 - : step === 'domain' 200 - ? 'bg-muted text-muted-foreground' 201 - : 'bg-green-500 text-white' 202 - }`} 195 + className={`w-8 h-8 rounded-full flex items-center justify-center ${step === 'upload' 196 + ? 'bg-primary text-primary-foreground' 197 + : step === 'domain' 198 + ? 'bg-muted text-muted-foreground' 199 + : 'bg-green-500 text-white' 200 + }`} 203 201 > 204 202 {step === 'complete' ? ( 205 203 <CheckCircle2 className="w-5 h-5" /> ··· 258 256 {!isCheckingAvailability && 259 257 isAvailable !== null && ( 260 258 <div 261 - className={`absolute right-3 top-1/2 -translate-y-1/2 ${ 262 - isAvailable 263 - ? 'text-green-500' 264 - : 'text-red-500' 265 - }`} 259 + className={`absolute right-3 top-1/2 -translate-y-1/2 ${isAvailable 260 + ? 'text-green-500' 261 + : 'text-red-500' 262 + }`} 266 263 > 267 264 {isAvailable ? 'โœ“' : 'โœ—'} 268 265 </div>
+212 -39
apps/main-app/public/styles/global.css
··· 6 6 :root { 7 7 color-scheme: light; 8 8 9 - /* Warm beige background inspired by Sunset design #E9DDD8 */ 10 - --background: oklch(0.90 0.012 35); 11 - /* Very dark brown text for strong contrast #2A2420 */ 12 - --foreground: oklch(0.18 0.01 30); 9 + /* Warm beige background inspired by Sunset design */ 10 + --background: oklch(0.92 0.012 35); 11 + /* Very dark brown text for strong contrast */ 12 + --foreground: oklch(0.15 0.015 30); 13 13 14 - /* Slightly lighter card background */ 15 - --card: oklch(0.93 0.01 35); 16 - --card-foreground: oklch(0.18 0.01 30); 14 + /* Slightly lighter card background for elevation */ 15 + --card: oklch(0.95 0.008 35); 16 + --card-foreground: oklch(0.15 0.015 30); 17 17 18 - --popover: oklch(0.93 0.01 35); 19 - --popover-foreground: oklch(0.18 0.01 30); 18 + --popover: oklch(0.96 0.006 35); 19 + --popover-foreground: oklch(0.15 0.015 30); 20 20 21 - /* Dark brown primary inspired by #645343 */ 22 - --primary: oklch(0.35 0.02 35); 23 - --primary-foreground: oklch(0.95 0.01 35); 21 + /* Dark brown primary - darker for better contrast */ 22 + --primary: oklch(0.30 0.025 35); 23 + --primary-foreground: oklch(0.96 0.008 35); 24 24 25 - /* Bright pink accent for links #FFAAD2 */ 26 - --accent: oklch(0.78 0.15 345); 27 - --accent-foreground: oklch(0.18 0.01 30); 25 + /* Deeper pink accent for better visibility */ 26 + --accent: oklch(0.65 0.18 345); 27 + --accent-foreground: oklch(0.15 0.015 30); 28 28 29 - /* Medium taupe secondary inspired by #867D76 */ 30 - --secondary: oklch(0.52 0.015 30); 31 - --secondary-foreground: oklch(0.95 0.01 35); 29 + /* Darker taupe secondary for better contrast */ 30 + --secondary: oklch(0.85 0.012 30); 31 + --secondary-foreground: oklch(0.25 0.02 30); 32 32 33 - /* Light warm muted background */ 33 + /* Muted areas with better distinction */ 34 34 --muted: oklch(0.88 0.01 35); 35 - --muted-foreground: oklch(0.42 0.015 30); 35 + --muted-foreground: oklch(0.35 0.02 30); 36 36 37 - --border: oklch(0.75 0.015 30); 38 - --input: oklch(0.92 0.01 35); 39 - --ring: oklch(0.72 0.08 15); 37 + /* Significantly darker border for visibility */ 38 + --border: oklch(0.65 0.02 30); 39 + /* Input backgrounds lighter than cards */ 40 + --input: oklch(0.97 0.005 35); 41 + --ring: oklch(0.55 0.12 345); 40 42 41 - --destructive: oklch(0.577 0.245 27.325); 42 - --destructive-foreground: oklch(0.985 0 0); 43 + --destructive: oklch(0.50 0.20 25); 44 + --destructive-foreground: oklch(0.98 0 0); 43 45 44 - --chart-1: oklch(0.78 0.15 345); 46 + --chart-1: oklch(0.65 0.18 345); 45 47 --chart-2: oklch(0.32 0.04 285); 46 - --chart-3: oklch(0.56 0.08 220); 47 - --chart-4: oklch(0.85 0.02 130); 48 - --chart-5: oklch(0.93 0.03 85); 48 + --chart-3: oklch(0.50 0.10 220); 49 + --chart-4: oklch(0.70 0.08 130); 50 + --chart-5: oklch(0.75 0.06 85); 49 51 50 52 --radius: 0.75rem; 51 - --sidebar: oklch(0.985 0 0); 52 - --sidebar-foreground: oklch(0.145 0 0); 53 - --sidebar-primary: oklch(0.205 0 0); 54 - --sidebar-primary-foreground: oklch(0.985 0 0); 55 - --sidebar-accent: oklch(0.97 0 0); 56 - --sidebar-accent-foreground: oklch(0.205 0 0); 57 - --sidebar-border: oklch(0.922 0 0); 58 - --sidebar-ring: oklch(0.708 0 0); 53 + --sidebar: oklch(0.94 0.008 35); 54 + --sidebar-foreground: oklch(0.15 0.015 30); 55 + --sidebar-primary: oklch(0.30 0.025 35); 56 + --sidebar-primary-foreground: oklch(0.96 0.008 35); 57 + --sidebar-accent: oklch(0.90 0.01 35); 58 + --sidebar-accent-foreground: oklch(0.20 0.02 30); 59 + --sidebar-border: oklch(0.65 0.02 30); 60 + --sidebar-ring: oklch(0.55 0.12 345); 59 61 } 60 62 61 63 .dark { ··· 160 162 * { 161 163 @apply border-border outline-ring/50; 162 164 } 165 + 166 + html { 167 + scrollbar-gutter: stable; 168 + } 169 + 163 170 body { 164 171 @apply bg-background text-foreground; 165 172 } 166 173 } 167 174 168 175 @keyframes arrow-bounce { 169 - 0%, 100% { 176 + 177 + 0%, 178 + 100% { 170 179 transform: translateX(0); 171 180 } 181 + 172 182 50% { 173 183 transform: translateX(4px); 174 184 } ··· 189 199 border-radius: 0.5rem; 190 200 padding: 1rem; 191 201 overflow-x: auto; 192 - border: 1px solid hsl(var(--border)); 202 + border: 1px solid var(--border); 193 203 } 194 204 195 205 .shiki-wrapper pre { 196 206 margin: 0 !important; 197 207 padding: 0 !important; 198 208 } 209 + 210 + /* ========== Landing Page Animations ========== */ 211 + 212 + /* Animated gradient for headline text */ 213 + @keyframes gradient-shift { 214 + 215 + 0%, 216 + 100% { 217 + background-position: 0% 50%; 218 + } 219 + 220 + 50% { 221 + background-position: 100% 50%; 222 + } 223 + } 224 + 225 + .text-gradient-animate { 226 + background: linear-gradient(90deg, 227 + oklch(0.55 0.22 350), 228 + oklch(0.60 0.24 10), 229 + oklch(0.55 0.22 350)); 230 + background-size: 200% auto; 231 + -webkit-background-clip: text; 232 + background-clip: text; 233 + -webkit-text-fill-color: transparent; 234 + animation: gradient-shift 4s ease-in-out infinite; 235 + } 236 + 237 + .dark .text-gradient-animate { 238 + background: linear-gradient(90deg, 239 + oklch(0.75 0.12 295), 240 + oklch(0.85 0.10 5), 241 + oklch(0.75 0.12 295)); 242 + background-size: 200% auto; 243 + -webkit-background-clip: text; 244 + background-clip: text; 245 + -webkit-text-fill-color: transparent; 246 + } 247 + 248 + /* Floating/breathing animation for hero elements */ 249 + @keyframes float { 250 + 251 + 0%, 252 + 100% { 253 + transform: translateY(0); 254 + } 255 + 256 + 50% { 257 + transform: translateY(-8px); 258 + } 259 + } 260 + 261 + .animate-float { 262 + animation: float 3s ease-in-out infinite; 263 + } 264 + 265 + .animate-float-delayed { 266 + animation: float 3s ease-in-out infinite; 267 + animation-delay: 0.5s; 268 + } 269 + 270 + /* Staggered fade-in animation */ 271 + @keyframes fade-in-up { 272 + from { 273 + opacity: 0; 274 + transform: translateY(20px); 275 + } 276 + 277 + to { 278 + opacity: 1; 279 + transform: translateY(0); 280 + } 281 + } 282 + 283 + .animate-fade-in-up { 284 + animation: fade-in-up 0.6s ease-out forwards; 285 + opacity: 0; 286 + } 287 + 288 + .animate-delay-100 { 289 + animation-delay: 0.1s; 290 + } 291 + 292 + .animate-delay-200 { 293 + animation-delay: 0.2s; 294 + } 295 + 296 + .animate-delay-300 { 297 + animation-delay: 0.3s; 298 + } 299 + 300 + .animate-delay-400 { 301 + animation-delay: 0.4s; 302 + } 303 + 304 + .animate-delay-500 { 305 + animation-delay: 0.5s; 306 + } 307 + 308 + .animate-delay-600 { 309 + animation-delay: 0.6s; 310 + } 311 + 312 + /* Terminal cursor blink */ 313 + @keyframes cursor-blink { 314 + 315 + 0%, 316 + 50% { 317 + opacity: 1; 318 + } 319 + 320 + 51%, 321 + 100% { 322 + opacity: 0; 323 + } 324 + } 325 + 326 + .animate-cursor-blink { 327 + animation: cursor-blink 1s step-end infinite; 328 + } 329 + 330 + /* Button hover scale effect */ 331 + .btn-hover-lift { 332 + transition: all 0.2s ease-out; 333 + } 334 + 335 + .btn-hover-lift:hover { 336 + transform: translateY(-2px); 337 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 338 + } 339 + 340 + .btn-hover-lift:active { 341 + transform: translateY(0); 342 + } 343 + 344 + /* Subtle pulse for feature dots */ 345 + @keyframes dot-pulse { 346 + 347 + 0%, 348 + 100% { 349 + transform: scale(1); 350 + opacity: 1; 351 + } 352 + 353 + 50% { 354 + transform: scale(1.2); 355 + opacity: 0.8; 356 + } 357 + } 358 + 359 + .animate-dot-pulse { 360 + animation: dot-pulse 2s ease-in-out infinite; 361 + } 362 + 363 + .animate-dot-pulse-delayed-1 { 364 + animation: dot-pulse 2s ease-in-out infinite; 365 + animation-delay: 0.3s; 366 + } 367 + 368 + .animate-dot-pulse-delayed-2 { 369 + animation: dot-pulse 2s ease-in-out infinite; 370 + animation-delay: 0.6s; 371 + }
+30 -4
apps/main-app/src/index.ts
··· 12 12 cleanupExpiredSessions, 13 13 rotateKeysIfNeeded 14 14 } from './lib/oauth-client' 15 - import { getCookieSecret } from './lib/db' 15 + import { getCookieSecret, closeDatabase } from './lib/db' 16 16 import { authRoutes } from './routes/auth' 17 17 import { wispRoutes } from './routes/wisp' 18 18 import { domainRoutes } from './routes/domain' ··· 20 20 import { siteRoutes } from './routes/site' 21 21 import { csrfProtection } from './lib/csrf' 22 22 import { DNSVerificationWorker } from './lib/dns-verification-worker' 23 - import { createLogger, logCollector } from '@wisp/observability' 23 + import { createLogger, logCollector, initializeGrafanaExporters } from '@wisp/observability' 24 24 import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 25 25 import { promptAdminSetup } from './lib/admin-auth' 26 26 import { adminRoutes } from './routes/admin' 27 + 28 + // Initialize Grafana exporters if configured 29 + initializeGrafanaExporters({ 30 + serviceName: 'main-app', 31 + serviceVersion: '1.0.50' 32 + }) 27 33 28 34 const logger = createLogger('main-app') 29 35 ··· 55 61 setInterval(runMaintenance, 60 * 60 * 1000) 56 62 57 63 // Start DNS verification worker (runs every 10 minutes) 64 + // Can be disabled via DISABLE_DNS_WORKER=true environment variable 58 65 const dnsVerifier = new DNSVerificationWorker( 59 66 10 * 60 * 1000, // 10 minutes 60 67 (msg, data) => { ··· 62 69 } 63 70 ) 64 71 65 - dnsVerifier.start() 66 - logger.info('DNS Verifier Started - checking custom domains every 10 minutes') 72 + if (Bun.env.DISABLE_DNS_WORKER !== 'true') { 73 + dnsVerifier.start() 74 + logger.info('DNS Verifier Started - checking custom domains every 10 minutes') 75 + } else { 76 + logger.info('DNS Verifier disabled via DISABLE_DNS_WORKER environment variable') 77 + } 67 78 68 79 export const app = new Elysia({ 69 80 serve: { ··· 194 205 console.log( 195 206 `๐ŸฆŠ Elysia is running at ${app.server?.hostname}:${app.server?.port}` 196 207 ) 208 + 209 + // Graceful shutdown 210 + process.on('SIGINT', async () => { 211 + console.log('\n๐Ÿ›‘ Shutting down...') 212 + dnsVerifier.stop() 213 + await closeDatabase() 214 + process.exit(0) 215 + }) 216 + 217 + process.on('SIGTERM', async () => { 218 + console.log('\n๐Ÿ›‘ Shutting down...') 219 + dnsVerifier.stop() 220 + await closeDatabase() 221 + process.exit(0) 222 + })
+13
apps/main-app/src/lib/db.ts
··· 526 526 console.log('[CookieSecret] Generated new cookie signing secret'); 527 527 return secret; 528 528 }; 529 + 530 + /** 531 + * Close database connection 532 + * Call this during graceful shutdown 533 + */ 534 + export const closeDatabase = async (): Promise<void> => { 535 + try { 536 + await db.end(); 537 + console.log('[DB] Database connection closed'); 538 + } catch (err) { 539 + console.error('[DB] Error closing database connection:', err); 540 + } 541 + };
+127
apps/main-app/src/lib/ignore-patterns.ts
··· 1 + import ignore, { type Ignore } from 'ignore' 2 + import { readFileSync, existsSync } from 'fs' 3 + import { join } from 'path' 4 + 5 + interface IgnoreConfig { 6 + version: string 7 + description: string 8 + patterns: string[] 9 + } 10 + 11 + /** 12 + * Load default ignore patterns from the .wispignore.json file in the monorepo root 13 + */ 14 + function loadDefaultPatterns(): string[] { 15 + try { 16 + // Path to the default ignore patterns JSON file (monorepo root, 3 levels up from this file) 17 + const defaultJsonPath = join(__dirname, '../../../../.wispignore.json') 18 + 19 + if (!existsSync(defaultJsonPath)) { 20 + console.warn('โš ๏ธ Default .wispignore.json not found, using hardcoded patterns') 21 + return getHardcodedPatterns() 22 + } 23 + 24 + const contents = readFileSync(defaultJsonPath, 'utf-8') 25 + const config: IgnoreConfig = JSON.parse(contents) 26 + return config.patterns 27 + } catch (error) { 28 + console.error('Failed to load default ignore patterns:', error) 29 + return getHardcodedPatterns() 30 + } 31 + } 32 + 33 + /** 34 + * Hardcoded fallback patterns (same as in .wispignore.json) 35 + */ 36 + function getHardcodedPatterns(): string[] { 37 + return [ 38 + '.git', 39 + '.git/**', 40 + '.github', 41 + '.github/**', 42 + '.gitlab', 43 + '.gitlab/**', 44 + '.DS_Store', 45 + '.wisp.metadata.json', 46 + '.env', 47 + '.env.*', 48 + 'node_modules', 49 + 'node_modules/**', 50 + 'Thumbs.db', 51 + 'desktop.ini', 52 + '._*', 53 + '.Spotlight-V100', 54 + '.Spotlight-V100/**', 55 + '.Trashes', 56 + '.Trashes/**', 57 + '.fseventsd', 58 + '.fseventsd/**', 59 + '.cache', 60 + '.cache/**', 61 + '.temp', 62 + '.temp/**', 63 + '.tmp', 64 + '.tmp/**', 65 + '__pycache__', 66 + '__pycache__/**', 67 + '*.pyc', 68 + '.venv', 69 + '.venv/**', 70 + 'venv', 71 + 'venv/**', 72 + 'env', 73 + 'env/**', 74 + '*.swp', 75 + '*.swo', 76 + '*~', 77 + '.tangled', 78 + '.tangled/**', 79 + ] 80 + } 81 + 82 + /** 83 + * Load custom ignore patterns from a .wispignore file 84 + * @param wispignoreContent - Content of the .wispignore file (one pattern per line) 85 + */ 86 + function loadWispignorePatterns(wispignoreContent: string): string[] { 87 + return wispignoreContent 88 + .split('\n') 89 + .map(line => line.trim()) 90 + .filter(line => line && !line.startsWith('#')) // Skip empty lines and comments 91 + } 92 + 93 + /** 94 + * Create an ignore matcher 95 + * @param customPatterns - Optional custom patterns from a .wispignore file 96 + */ 97 + export function createIgnoreMatcher(customPatterns?: string[]): Ignore { 98 + const ig = ignore() 99 + 100 + // Add default patterns 101 + const defaultPatterns = loadDefaultPatterns() 102 + ig.add(defaultPatterns) 103 + 104 + // Add custom patterns if provided 105 + if (customPatterns && customPatterns.length > 0) { 106 + ig.add(customPatterns) 107 + console.log(`Loaded ${customPatterns.length} custom patterns from .wispignore`) 108 + } 109 + 110 + return ig 111 + } 112 + 113 + /** 114 + * Check if a file path should be ignored 115 + * @param matcher - The ignore matcher 116 + * @param filePath - The file path to check (relative to site root) 117 + */ 118 + export function shouldIgnore(matcher: Ignore, filePath: string): boolean { 119 + return matcher.ignores(filePath) 120 + } 121 + 122 + /** 123 + * Parse .wispignore content and return patterns 124 + */ 125 + export function parseWispignore(content: string): string[] { 126 + return loadWispignorePatterns(content) 127 + }
+5 -4
apps/main-app/src/lib/oauth-client.ts
··· 4 4 import { logger } from "./logger"; 5 5 import { SlingshotHandleResolver } from "./slingshot-handle-resolver"; 6 6 7 + // OAuth scope for all client types 8 + const OAUTH_SCOPE = 'atproto repo:place.wisp.fs repo:place.wisp.domain repo:place.wisp.subfs repo:place.wisp.settings blob:*/*'; 7 9 // Session timeout configuration (30 days in seconds) 8 10 const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds 9 11 // OAuth state timeout (1 hour in seconds) ··· 110 112 // Loopback client for local development 111 113 // For loopback, scopes and redirect_uri must be in client_id query string 112 114 const redirectUri = 'http://127.0.0.1:8000/api/auth/callback'; 113 - const scope = 'atproto repo:place.wisp.fs repo:place.wisp.domain repo:place.wisp.subfs repo:place.wisp.settings blob:*/* rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview'; 114 115 const params = new URLSearchParams(); 115 116 params.append('redirect_uri', redirectUri); 116 - params.append('scope', scope); 117 + params.append('scope', OAUTH_SCOPE); 117 118 118 119 return { 119 120 client_id: `http://localhost?${params.toString()}`, ··· 124 125 response_types: ['code'], 125 126 application_type: 'web', 126 127 token_endpoint_auth_method: 'none', 127 - scope: scope, 128 + scope: OAUTH_SCOPE, 128 129 dpop_bound_access_tokens: false, 129 130 subject_type: 'public', 130 131 authorization_signed_response_alg: 'ES256' ··· 145 146 application_type: 'web', 146 147 token_endpoint_auth_method: 'private_key_jwt', 147 148 token_endpoint_auth_signing_alg: "ES256", 148 - scope: "atproto repo:place.wisp.fs repo:place.wisp.domain repo:place.wisp.subfs repo:place.wisp.settings blob:*/* rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview", 149 + scope: OAUTH_SCOPE, 149 150 dpop_bound_access_tokens: true, 150 151 jwks_uri: `${config.domain}/jwks.json`, 151 152 subject_type: 'public',
+10 -12
apps/main-app/src/routes/user.ts
··· 1 1 import { Elysia, t } from 'elysia' 2 2 import { requireAuth } from '../lib/wisp-auth' 3 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 - import { Agent } from '@atproto/api' 5 4 import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db' 6 5 import { syncSitesFromPDS } from '../lib/sync-sites' 7 6 import { createLogger } from '@wisp/observability' 7 + import { createDidResolver, extractAtprotoData } from '@atproto-labs/did-resolver' 8 8 9 9 const logger = createLogger('main-app') 10 + const didResolver = createDidResolver({}) 10 11 11 12 export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => 12 13 new Elysia({ ··· 42 43 }) 43 44 .get('/info', async ({ auth }) => { 44 45 try { 45 - // Get user's handle from AT Protocol 46 - const agent = new Agent(auth.session) 47 - 48 46 let handle = 'unknown' 49 47 try { 50 - console.log('[User] Attempting to fetch profile for DID:', auth.did) 51 - const profile = await agent.getProfile({ actor: auth.did }) 52 - console.log('[User] Profile fetched successfully:', profile.data.handle) 53 - handle = profile.data.handle 48 + const didDoc = await didResolver.resolve(auth.did) 49 + const atprotoData = extractAtprotoData(didDoc) 50 + 51 + if (atprotoData.aka) { 52 + handle = atprotoData.aka 53 + } 54 54 } catch (err) { 55 - console.error('[User] Failed to fetch profile - Full error:', err) 56 - console.error('[User] Error message:', err instanceof Error ? err.message : String(err)) 57 - console.error('[User] Error stack:', err instanceof Error ? err.stack : 'No stack') 58 - logger.error('[User] Failed to fetch profile', err) 55 + 56 + logger.error('[User] Failed to resolve DID', err) 59 57 } 60 58 61 59 return {
+31 -112
apps/main-app/src/routes/wisp.ts
··· 34 34 failUploadJob, 35 35 addJobListener 36 36 } from '../lib/upload-jobs' 37 + import { createIgnoreMatcher, shouldIgnore, parseWispignore } from '../lib/ignore-patterns' 38 + import type { Ignore } from 'ignore' 37 39 38 40 const logger = createLogger('main-app') 39 41 40 - function isValidSiteName(siteName: string): boolean { 42 + export function isValidSiteName(siteName: string): boolean { 41 43 if (!siteName || typeof siteName !== 'string') return false; 42 44 43 45 // Length check (AT Protocol rkey limit) ··· 145 147 } 146 148 } 147 149 150 + // Check for .wispignore file in uploaded files 151 + let customIgnorePatterns: string[] = []; 152 + const wispignoreFile = fileArray.find(f => f && f.name && f.name.endsWith('.wispignore')); 153 + if (wispignoreFile) { 154 + try { 155 + const content = await wispignoreFile.text(); 156 + customIgnorePatterns = parseWispignore(content); 157 + console.log(`Found .wispignore file with ${customIgnorePatterns.length} custom patterns`); 158 + } catch (err) { 159 + console.warn('Failed to parse .wispignore file:', err); 160 + } 161 + } 162 + 163 + // Create ignore matcher with default and custom patterns 164 + const ignoreMatcher = createIgnoreMatcher(customIgnorePatterns); 165 + 148 166 // Convert File objects to UploadedFile format 149 167 const uploadedFiles: UploadedFile[] = []; 150 168 const skippedFiles: Array<{ name: string; reason: string }> = []; ··· 165 183 continue; 166 184 } 167 185 168 - console.log(`Processing file ${i + 1}/${fileArray.length}:`, file.name, file.size, 'bytes'); 186 + // Use webkitRelativePath when available (directory uploads), fallback to name for regular file uploads 187 + const webkitPath = 'webkitRelativePath' in file ? String(file.webkitRelativePath) : ''; 188 + const filePath = webkitPath || file.name; 189 + 169 190 updateJobProgress(jobId, { 170 191 filesProcessed: i + 1, 171 - currentFile: file.name 192 + currentFile: filePath 172 193 }); 173 194 174 - // Skip unwanted files and directories 175 - const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 176 - const fileName = normalizedPath.split('/').pop() || ''; 177 - const pathParts = normalizedPath.split('/'); 178 - 179 - // .git directory (version control - thousands of files) 180 - if (normalizedPath.startsWith('.git/') || normalizedPath === '.git') { 181 - console.log(`Skipping .git file: ${file.name}`); 182 - skippedFiles.push({ 183 - name: file.name, 184 - reason: '.git directory excluded' 185 - }); 186 - continue; 187 - } 188 - 189 - // .DS_Store (macOS metadata - can leak info) 190 - if (fileName === '.DS_Store') { 191 - console.log(`Skipping .DS_Store file: ${file.name}`); 192 - skippedFiles.push({ 193 - name: file.name, 194 - reason: '.DS_Store file excluded' 195 - }); 196 - continue; 197 - } 198 - 199 - // .env files (environment variables with secrets) 200 - if (fileName.startsWith('.env')) { 201 - console.log(`Skipping .env file: ${file.name}`); 202 - skippedFiles.push({ 203 - name: file.name, 204 - reason: 'environment files excluded for security' 205 - }); 206 - continue; 207 - } 195 + // Skip files that match ignore patterns 196 + const normalizedPath = filePath.replace(/^[^\/]*\//, ''); 208 197 209 - // node_modules (dependency folder - can be 100,000+ files) 210 - if (pathParts.includes('node_modules')) { 211 - console.log(`Skipping node_modules file: ${file.name}`); 198 + if (shouldIgnore(ignoreMatcher, normalizedPath)) { 212 199 skippedFiles.push({ 213 - name: file.name, 214 - reason: 'node_modules excluded' 215 - }); 216 - continue; 217 - } 218 - 219 - // OS metadata files 220 - if (fileName === 'Thumbs.db' || fileName === 'desktop.ini' || fileName.startsWith('._')) { 221 - console.log(`Skipping OS metadata file: ${file.name}`); 222 - skippedFiles.push({ 223 - name: file.name, 224 - reason: 'OS metadata file excluded' 225 - }); 226 - continue; 227 - } 228 - 229 - // macOS system directories 230 - if (pathParts.includes('.Spotlight-V100') || pathParts.includes('.Trashes') || pathParts.includes('.fseventsd')) { 231 - console.log(`Skipping macOS system file: ${file.name}`); 232 - skippedFiles.push({ 233 - name: file.name, 234 - reason: 'macOS system directory excluded' 235 - }); 236 - continue; 237 - } 238 - 239 - // Cache and temp directories 240 - if (pathParts.some(part => part === '.cache' || part === '.temp' || part === '.tmp')) { 241 - console.log(`Skipping cache/temp file: ${file.name}`); 242 - skippedFiles.push({ 243 - name: file.name, 244 - reason: 'cache/temp directory excluded' 245 - }); 246 - continue; 247 - } 248 - 249 - // Python cache 250 - if (pathParts.includes('__pycache__') || fileName.endsWith('.pyc')) { 251 - console.log(`Skipping Python cache file: ${file.name}`); 252 - skippedFiles.push({ 253 - name: file.name, 254 - reason: 'Python cache excluded' 255 - }); 256 - continue; 257 - } 258 - 259 - // Python virtual environments 260 - if (pathParts.some(part => part === '.venv' || part === 'venv' || part === 'env')) { 261 - console.log(`Skipping Python venv file: ${file.name}`); 262 - skippedFiles.push({ 263 - name: file.name, 264 - reason: 'Python virtual environment excluded' 265 - }); 266 - continue; 267 - } 268 - 269 - // Editor swap files 270 - if (fileName.endsWith('.swp') || fileName.endsWith('.swo') || fileName.endsWith('~')) { 271 - console.log(`Skipping editor swap file: ${file.name}`); 272 - skippedFiles.push({ 273 - name: file.name, 274 - reason: 'editor swap file excluded' 200 + name: filePath, 201 + reason: 'matched ignore pattern' 275 202 }); 276 203 continue; 277 204 } ··· 280 207 const maxSize = MAX_FILE_SIZE; 281 208 if (file.size > maxSize) { 282 209 skippedFiles.push({ 283 - name: file.name, 210 + name: filePath, 284 211 reason: `file too large (${(file.size / 1024 / 1024).toFixed(2)}MB, max 100MB)` 285 212 }); 286 213 continue; ··· 313 240 // Text files: compress AND base64 encode 314 241 finalContent = Buffer.from(compressedContent.toString('base64'), 'binary'); 315 242 base64Encoded = true; 316 - const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1); 317 - console.log(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`); 318 - logger.info(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`); 319 243 } else { 320 244 // Audio files: just compress, no base64 321 245 finalContent = compressedContent; 322 - const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1); 323 - console.log(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`); 324 - logger.info(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`); 325 246 } 326 247 } else { 327 248 // Binary files: upload directly 328 249 finalContent = originalContent; 329 - console.log(`Uploading ${file.name} directly: ${originalContent.length} bytes (no compression)`); 330 - logger.info(`Uploading ${file.name} directly: ${originalContent.length} bytes (binary)`); 331 250 } 332 251 333 252 uploadedFiles.push({ 334 - name: file.name, 253 + name: filePath, 335 254 content: finalContent, 336 255 mimeType: originalMimeType, 337 256 size: finalContent.length,
+9 -2
binaries/index.html
··· 51 51 transition: background-color 200ms ease, color 200ms ease; 52 52 font-family: system-ui, sans-serif; 53 53 line-height: 1.6; 54 + display: flex; 55 + flex-direction: column; 56 + min-height: 100vh; 54 57 } 55 58 56 59 .container { 57 60 max-width: 860px; 58 61 margin: 40px auto; 59 62 padding: 0 20px; 60 - min-height: 100vh; 63 + flex: 1; 64 + display: flex; 65 + flex-direction: column; 66 + width: 100%; 61 67 } 62 68 63 69 h1 { ··· 224 230 } 225 231 226 232 .footer { 227 - margin-top: 3rem; 233 + margin-top: auto; 228 234 padding-top: 2rem; 235 + padding-bottom: 2rem; 229 236 border-top: 1px solid var(--demo-hr); 230 237 text-align: center; 231 238 color: var(--demo-text-secondary);
+320 -163
bun.lock
··· 1 1 { 2 2 "lockfileVersion": 1, 3 - "configVersion": 0, 3 + "configVersion": 1, 4 4 "workspaces": { 5 5 "": { 6 - "name": "elysia-static", 6 + "name": "@wisp/monorepo", 7 7 "dependencies": { 8 8 "@tailwindcss/cli": "^4.1.17", 9 + "atproto-ui": "^0.12.0", 9 10 "bun-plugin-tailwind": "^0.1.2", 11 + "elysia": "^1.4.18", 10 12 "tailwindcss": "^4.1.17", 11 13 }, 14 + "devDependencies": { 15 + "@types/bun": "^1.3.5", 16 + }, 12 17 }, 13 18 "apps/hosting-service": { 14 19 "name": "wisp-hosting-service", ··· 16 21 "dependencies": { 17 22 "@atproto/api": "^0.17.4", 18 23 "@atproto/identity": "^0.4.9", 19 - "@atproto/lexicon": "^0.5.1", 24 + "@atproto/lexicon": "^0.5.2", 20 25 "@atproto/sync": "^0.1.36", 21 26 "@atproto/xrpc": "^0.7.5", 22 27 "@hono/node-server": "^1.19.6", ··· 37 42 "@types/mime-types": "^2.1.4", 38 43 "@types/node": "^22.10.5", 39 44 "tsx": "^4.19.2", 45 + "typescript": "^5.9.3", 40 46 }, 41 47 }, 42 48 "apps/main-app": { 43 49 "name": "@wisp/main-app", 44 50 "version": "1.0.50", 45 51 "dependencies": { 46 - "@atproto/api": "^0.17.3", 47 - "@atproto/common-web": "^0.4.5", 52 + "@atproto-labs/did-resolver": "^0.2.4", 53 + "@atproto/api": "^0.17.7", 54 + "@atproto/common-web": "^0.4.6", 48 55 "@atproto/jwk-jose": "^0.1.11", 49 - "@atproto/lex-cli": "^0.9.5", 50 - "@atproto/oauth-client-node": "^0.3.9", 51 - "@atproto/xrpc-server": "^0.9.5", 56 + "@atproto/lex-cli": "^0.9.7", 57 + "@atproto/oauth-client-node": "^0.3.12", 58 + "@atproto/xrpc-server": "^0.9.6", 52 59 "@elysiajs/cors": "^1.4.0", 53 60 "@elysiajs/eden": "^1.4.3", 54 61 "@elysiajs/openapi": "^1.4.11", ··· 68 75 "@wisp/lexicons": "workspace:*", 69 76 "@wisp/observability": "workspace:*", 70 77 "actor-typeahead": "^0.1.1", 71 - "atproto-ui": "^0.11.3", 78 + "atproto-ui": "^0.12.0", 72 79 "bun-plugin-tailwind": "^0.1.2", 73 80 "class-variance-authority": "^0.7.1", 74 81 "clsx": "^2.1.1", 75 - "elysia": "latest", 82 + "elysia": "^1.4.18", 83 + "ignore": "^7.0.5", 76 84 "iron-session": "^8.0.4", 77 85 "lucide-react": "^0.546.0", 78 86 "multiformats": "^13.4.1", ··· 85 93 "zlib": "^1.0.5", 86 94 }, 87 95 "devDependencies": { 96 + "@atproto-labs/handle-resolver": "^0.3.4", 97 + "@atproto/did": "^0.2.3", 88 98 "@types/react": "^19.2.2", 89 99 "@types/react-dom": "^19.2.1", 90 100 "bun-types": "latest", ··· 101 111 "@wisp/lexicons": "workspace:*", 102 112 "multiformats": "^13.3.1", 103 113 }, 114 + "devDependencies": { 115 + "@atproto/lexicon": "^0.5.2", 116 + }, 104 117 }, 105 118 "packages/@wisp/constants": { 106 119 "name": "@wisp/constants", ··· 130 143 }, 131 144 "devDependencies": { 132 145 "@atproto/lex-cli": "^0.9.5", 146 + "multiformats": "^13.4.1", 133 147 }, 134 148 }, 135 149 "packages/@wisp/observability": { 136 150 "name": "@wisp/observability", 137 151 "version": "1.0.0", 152 + "dependencies": { 153 + "@opentelemetry/api": "^1.9.0", 154 + "@opentelemetry/exporter-metrics-otlp-http": "^0.56.0", 155 + "@opentelemetry/resources": "^1.29.0", 156 + "@opentelemetry/sdk-metrics": "^1.29.0", 157 + "@opentelemetry/semantic-conventions": "^1.29.0", 158 + }, 159 + "devDependencies": { 160 + "@hono/node-server": "^1.19.6", 161 + "bun-types": "^1.3.3", 162 + "typescript": "^5.9.3", 163 + }, 138 164 "peerDependencies": { 139 - "hono": "^4.0.0", 165 + "hono": "^4.10.7", 140 166 }, 141 167 "optionalPeers": [ 142 168 "hono", ··· 148 174 }, 149 175 }, 150 176 "trustedDependencies": [ 151 - "core-js", 177 + "esbuild", 152 178 "cbor-extract", 153 179 "protobufjs", 180 + "core-js", 181 + "bun", 182 + "@parcel/watcher", 154 183 ], 155 184 "packages": { 156 185 "@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="], 157 186 158 - "@atcute/bluesky": ["@atcute/bluesky@3.2.10", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.2" } }, "sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg=="], 187 + "@atcute/bluesky": ["@atcute/bluesky@3.2.11", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.5" } }, "sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A=="], 159 188 160 - "@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="], 189 + "@atcute/client": ["@atcute/client@4.1.0", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.5" } }, "sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ=="], 161 190 162 - "@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="], 191 + "@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="], 163 192 164 193 "@atcute/identity-resolver": ["@atcute/identity-resolver@1.1.4", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@atcute/util-fetch": "^1.0.3", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA=="], 165 194 166 - "@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 195 + "@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="], 167 196 168 - "@atcute/tangled": ["@atcute/tangled@1.0.10", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA=="], 197 + "@atcute/tangled": ["@atcute/tangled@1.0.12", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.3" } }, "sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA=="], 169 198 170 - "@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="], 199 + "@atcute/util-fetch": ["@atcute/util-fetch@1.0.4", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg=="], 171 200 172 - "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.2", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "zod": "^3.23.8" } }, "sha512-ca2B7xR43tVoQ8XxBvha58DXwIH8cIyKQl6lpOKGkPUrJuFoO4iCLlDiSDi2Ueh+yE1rMDPP/qveHdajgDX3WQ=="], 201 + "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.4", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "zod": "^3.23.8" } }, "sha512-sbXxBnAJWsKv/FEGG6a/WLz7zQYUr1vA2TXvNnPwwJQJCjPwEJMOh1vM22wBr185Phy7D2GD88PcRokn7eUVyw=="], 173 202 174 203 "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], 175 204 176 205 "@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="], 177 206 178 - "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.2", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "zod": "^3.23.8" } }, "sha512-KIerCzh3qb+zZoqWbIvTlvBY0XPq0r56kwViaJY/LTe/3oPO2JaqlYKS/F4dByWBhHK6YoUOJ0sWrh6PMJl40A=="], 207 + "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "zod": "^3.23.8" } }, "sha512-wsNopfzfgO3uPvfnFDgNeXgDufXxSXhjBjp2WEiSzEiLrMy0Jodnqggw4OzD9MJKf0a4Iu2/ydd537qdy91LrQ=="], 179 208 180 - "@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.21", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.2", "@atproto/did": "0.2.1" } }, "sha512-fuJy5Px5pGF3lJX/ATdurbT8tbmaFWtf+PPxAQDFy7ot2no3t+iaAgymhyxYymrssOuWs6BwOP8tyF3VrfdwtQ=="], 209 + "@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.23", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.4", "@atproto/did": "0.2.3" } }, "sha512-tBRr2LCgzn3klk+DL0xrTFv4zg5tEszdeW6vSIFVebBYSb3MLdfhievmSqZdIQ4c9UCC4hN7YXTlZCXj8+2YmQ=="], 181 210 182 - "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.2", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/handle-resolver": "0.3.2" } }, "sha512-MYxO9pe0WsFyi5HFdKAwqIqHfiF2kBPoVhAIuH/4PYHzGr799ED47xLhNMxR3ZUYrJm5+TQzWXypGZ0Btw1Ffw=="], 211 + "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.4", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/handle-resolver": "0.3.4" } }, "sha512-HNUEFQIo2ws6iATxmgHd5D5rAsWYupgxZucgwolVHPiMjE1SY+EmxEsfbEN1wDEzM8/u9AKUg/jrxxPEwsgbew=="], 183 212 184 213 "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], 185 214 ··· 191 220 192 221 "@atproto/common": ["@atproto/common@0.4.12", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ=="], 193 222 194 - "@atproto/common-web": ["@atproto/common-web@0.4.5", "", { "dependencies": { "@atproto/lex-data": "0.0.1", "@atproto/lex-json": "0.0.1", "zod": "^3.23.8" } }, "sha512-Tx0xUafLm3vRvOQpbBl5eb9V8xlC7TaRXs6dAulHRkDG3Kb+P9qn3pkDteq+aeMshbVXbVa1rm3Ok4vFyuoyYA=="], 223 + "@atproto/common-web": ["@atproto/common-web@0.4.6", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "@atproto/lex-json": "0.0.2", "zod": "^3.23.8" } }, "sha512-+2mG/1oBcB/ZmYIU1ltrFMIiuy9aByKAkb2Fos/0eTdczcLBaH17k0KoxMGvhfsujN2r62XlanOAMzysa7lv1g=="], 195 224 196 - "@atproto/crypto": ["@atproto/crypto@0.4.4", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA=="], 225 + "@atproto/crypto": ["@atproto/crypto@0.4.5", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw=="], 197 226 198 - "@atproto/did": ["@atproto/did@0.2.1", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-1i5BTU2GnBaaeYWhxUOnuEKFVq9euT5+dQPFabHpa927BlJ54PmLGyBBaOI7/NbLmN5HWwBa18SBkMpg3jGZRA=="], 227 + "@atproto/did": ["@atproto/did@0.2.3", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-VI8JJkSizvM2cHYJa37WlbzeCm5tWpojyc1/Zy8q8OOjyoy6X4S4BEfoP941oJcpxpMTObamibQIXQDo7tnIjg=="], 199 228 200 229 "@atproto/identity": ["@atproto/identity@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4" } }, "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ=="], 201 230 ··· 205 234 206 235 "@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="], 207 236 208 - "@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.1", "", { "dependencies": { "@atproto/lex-data": "0.0.1", "multiformats": "^9.9.0", "tslib": "^2.8.1" } }, "sha512-GCgowcC041tYmsoIxalIECJq4ZRHgREk6lFa4BzNRUZarMqwz57YF/7eUlo2Q6hoaMUL7Bjr6FvXwcZFaKrhvA=="], 237 + "@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "multiformats": "^9.9.0", "tslib": "^2.8.1" } }, "sha512-sTr3UCL2SgxEoYVpzJGgWTnNl4TpngP5tMcRyaOvi21Se4m3oR4RDsoVDPz8AS6XphiteRwzwPstquN7aWWMbA=="], 209 238 210 - "@atproto/lex-cli": ["@atproto/lex-cli@0.9.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "chalk": "^4.1.2", "commander": "^9.4.0", "prettier": "^3.2.5", "ts-morph": "^24.0.0", "yesno": "^0.4.0", "zod": "^3.23.8" }, "bin": { "lex": "dist/index.js" } }, "sha512-EedEKmURoSP735YwSDHsFrLOhZ4P2it8goCHv5ApWi/R9DFpOKOpmYfIXJ9MAprK8cw+yBnjDJbzpLJy7UXlTg=="], 239 + "@atproto/lex-cli": ["@atproto/lex-cli@0.9.7", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "@atproto/syntax": "^0.4.1", "chalk": "^4.1.2", "commander": "^9.4.0", "prettier": "^3.2.5", "ts-morph": "^24.0.0", "yesno": "^0.4.0", "zod": "^3.23.8" }, "bin": { "lex": "dist/index.js" } }, "sha512-UZVf0pK0mB4qiuwbnrxmV0mC9/Vk2v7W3u9pd4wc4GFojzAyGP76MF2TiwWFya5mgzC7723/r5Jb4ADg0rtfng=="], 211 240 212 - "@atproto/lex-data": ["@atproto/lex-data@0.0.1", "", { "dependencies": { "@atproto/syntax": "0.4.1", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-DrS/8cQcQs3s5t9ELAFNtyDZ8/PdiCx47ALtFEP2GnX2uCBHZRkqWG7xmu6ehjc787nsFzZBvlnz3T/gov5fGA=="], 241 + "@atproto/lex-data": ["@atproto/lex-data@0.0.2", "", { "dependencies": { "@atproto/syntax": "0.4.2", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-euV2rDGi+coH8qvZOU+ieUOEbwPwff9ca6IiXIqjZJ76AvlIpj7vtAyIRCxHUW2BoU6h9yqyJgn9MKD2a7oIwg=="], 213 242 214 - "@atproto/lex-json": ["@atproto/lex-json@0.0.1", "", { "dependencies": { "@atproto/lex-data": "0.0.1", "tslib": "^2.8.1" } }, "sha512-ivcF7+pDRuD/P97IEKQ/9TruunXj0w58Khvwk3M6psaI5eZT6LRsRZ4cWcKaXiFX4SHnjy+x43g0f7pPtIsERg=="], 243 + "@atproto/lex-json": ["@atproto/lex-json@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "tslib": "^2.8.1" } }, "sha512-Pd72lO+l2rhOTutnf11omh9ZkoB/elbzE3HSmn2wuZlyH1mRhTYvoH8BOGokWQwbZkCE8LL3nOqMT3gHCD2l7g=="], 215 244 216 245 "@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 217 246 218 - "@atproto/oauth-client": ["@atproto/oauth-client@0.5.8", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.2", "@atproto-labs/identity-resolver": "0.3.2", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.5.0", "@atproto/xrpc": "0.7.5", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-7YEym6d97+Dd73qGdkQTXi5La8xvCQxwRUDzzlR/NVAARa9a4YP7MCmqBJVeP2anT0By+DSAPyPDLTsxcjIcCg=="], 247 + "@atproto/oauth-client": ["@atproto/oauth-client@0.5.10", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.4", "@atproto-labs/identity-resolver": "0.3.4", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.5.2", "@atproto/xrpc": "0.7.6", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-2mdJFyYbaOw3e/1KMBOQ2/J9p+MfWW8kE6FKdExWrJ7JPJpTJw2ZF2EmdGHCVeXw386dQgXbLkr+w4vbgSqfMQ=="], 219 248 220 - "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.10", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/handle-resolver-node": "0.1.21", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.8", "@atproto/oauth-types": "0.5.0" } }, "sha512-6khKlJqu1Ed5rt3rzcTD5hymB6JUjKdOHWYXwiphw4inkAIo6GxLCighI4eGOqZorYk2j8ueeTNB6KsgH0kcRw=="], 249 + "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.12", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/handle-resolver-node": "0.1.23", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.10", "@atproto/oauth-types": "0.5.2" } }, "sha512-9ejfO1H8qo3EbiAJgxKcdcR5Ay/9hgaC5OdxtTN63bcOrkIhvBN0xpVPGZYLL1iJQyNeK1T5m/LDrv4gUS1B+g=="], 221 250 222 - "@atproto/oauth-types": ["@atproto/oauth-types@0.5.0", "", { "dependencies": { "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-33xz7HcXhbl+XRqbIMVu3GE02iK1nKe2oMWENASsfZEYbCz2b9ZOarOFuwi7g4LKqpGowGp0iRKsQHFcq4SDaQ=="], 251 + "@atproto/oauth-types": ["@atproto/oauth-types@0.5.2", "", { "dependencies": { "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-9DCDvtvCanTwAaU5UakYDO0hzcOITS3RutK5zfLytE5Y9unj0REmTDdN8Xd8YCfUJl7T/9pYpf04Uyq7bFTASg=="], 223 252 224 253 "@atproto/repo": ["@atproto/repo@0.8.11", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.2", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "varint": "^6.0.0", "zod": "^3.23.8" } }, "sha512-b/WCu5ITws4ILHoXiZz0XXB5U9C08fUVzkBQDwpnme62GXv8gUaAPL/ttG61OusW09ARwMMQm4vxoP0hTFg+zA=="], 225 254 226 255 "@atproto/sync": ["@atproto/sync@0.1.38", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/identity": "^0.4.10", "@atproto/lexicon": "^0.5.2", "@atproto/repo": "^0.8.11", "@atproto/syntax": "^0.4.1", "@atproto/xrpc-server": "^0.10.0", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-2rE0SM21Nk4hWw/XcIYFnzlWO6/gBg8mrzuWbOvDhD49sA/wW4zyjaHZ5t1gvk28/SLok2VZiIR8nYBdbf7F5Q=="], 227 256 228 - "@atproto/syntax": ["@atproto/syntax@0.4.1", "", {}, "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw=="], 257 + "@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 229 258 230 - "@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="], 259 + "@atproto/ws-client": ["@atproto/ws-client@0.0.2", "", { "dependencies": { "@atproto/common": "^0.4.12", "ws": "^8.12.0" } }, "sha512-yb11WtI9cZfx/00MTgZRabB97Quf/TerMmtzIm2H2YirIq2oW++NPoufXYCuXuQGR4ep4fvCyzz0/GX95jCONQ=="], 231 260 232 261 "@atproto/xrpc": ["@atproto/xrpc@0.7.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "zod": "^3.23.8" } }, "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA=="], 233 262 234 - "@atproto/xrpc-server": ["@atproto/xrpc-server@0.9.5", "", { "dependencies": { "@atproto/common": "^0.4.12", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.1", "@atproto/xrpc": "^0.7.5", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-V0srjUgy6mQ5yf9+MSNBLs457m4qclEaWZsnqIE7RfYywvntexTAbMoo7J7ONfTNwdmA9Gw4oLak2z2cDAET4w=="], 263 + "@atproto/xrpc-server": ["@atproto/xrpc-server@0.9.6", "", { "dependencies": { "@atproto/common": "^0.4.12", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.1", "@atproto/ws-client": "^0.0.2", "@atproto/xrpc": "^0.7.5", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-N/wPK0VEk8lZLkVsfG1wlkINQnBLO2fzWT+xclOjYl5lJwDi5xgiiyEQJAyZN49d6cmbsONu0SuOVw9pa5xLCw=="], 235 264 236 265 "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 237 266 ··· 251 280 252 281 "@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="], 253 282 254 - "@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="], 283 + "@elysiajs/eden": ["@elysiajs/eden@1.4.5", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-hIOeH+S5NU/84A7+t8yB1JjxqjmzRkBF9fnLn6y+AH8EcF39KumOAnciMhIOkhhThVZvXZ3d+GsizRc+Fxoi8g=="], 255 284 256 285 "@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="], 257 286 258 - "@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.6", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-jR7t4M6ZvMnBqzzHsNTL6y3sNq9jbGi2vKxbkizi/OO5tlvlKl/rnBGyFjZUjQ1Hte7rCz+2kfmgOQMhkjk+Og=="], 287 + "@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.8", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-c9unbcdXfehExCv1GsiTCfos5SyIAyDwP7apcMeXmUMBaJZiAYMfiEH8RFFFIfIHJHC/xlNJzUPodkcUaaoJJQ=="], 259 288 260 - "@elysiajs/static": ["@elysiajs/static@1.4.6", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-cd61aY/DHOVhlnBjzTBX8E1XANIrsCH8MwEGHeLMaZzNrz0gD4Q8Qsde2dFMzu81I7ZDaaZ2Rim9blSLtUrYBg=="], 289 + "@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="], 261 290 262 291 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.26.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA=="], 263 292 ··· 311 340 312 341 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="], 313 342 314 - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="], 343 + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.2", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA=="], 315 344 316 345 "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], 317 346 ··· 341 370 342 371 "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="], 343 372 344 - "@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 373 + "@opentelemetry/core": ["@opentelemetry/core@1.29.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA=="], 345 374 346 375 "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="], 347 376 ··· 351 380 352 381 "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="], 353 382 354 - "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="], 383 + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-exporter-base": "0.56.0", "@opentelemetry/otlp-transformer": "0.56.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/sdk-metrics": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GD5QuCT6js+mDpb5OBO6OSyCH+k2Gy3xPHJV9BnjV8W6kpSuY8y2Samzs5vl23UcGMq6sHLAbs+Eq/VYsLMiVw=="], 355 384 356 385 "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="], 357 386 ··· 367 396 368 397 "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="], 369 398 370 - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 399 + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-transformer": "0.56.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw=="], 371 400 372 401 "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], 373 402 374 - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 403 + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.56.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.56.0", "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/sdk-logs": "0.56.0", "@opentelemetry/sdk-metrics": "1.29.0", "@opentelemetry/sdk-trace-base": "1.29.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ=="], 375 404 376 405 "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="], 377 406 378 407 "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="], 379 408 380 - "@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 409 + "@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="], 381 410 382 411 "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="], 383 412 384 - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 413 + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog=="], 385 414 386 415 "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="], 387 416 ··· 391 420 392 421 "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="], 393 422 394 - "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A=="], 423 + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], 395 424 396 - "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg=="], 425 + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow=="], 397 426 398 - "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww=="], 427 + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g=="], 399 428 400 - "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ=="], 429 + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw=="], 401 430 402 - "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w=="], 431 + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw=="], 403 432 404 - "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug=="], 433 + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw=="], 405 434 406 - "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg=="], 435 + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w=="], 407 436 408 - "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ=="], 437 + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w=="], 409 438 410 - "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A=="], 439 + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ=="], 411 440 412 - "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg=="], 441 + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ=="], 413 442 414 - "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-s00T99MjB+xLOWq+t+wVaVBrry+oBOZNiTJijt+bmkp/MJptYS3FGvs7a+nkjLNzoNDoWQcXgKew6AaHES37Bg=="], 443 + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA=="], 415 444 416 445 "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], 417 446 ··· 547 576 548 577 "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="], 549 578 550 - "@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="], 579 + "@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], 551 580 552 - "@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="], 581 + "@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="], 553 582 554 - "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], 583 + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], 555 584 556 585 "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], 557 586 558 587 "@ts-morph/common": ["@ts-morph/common@0.25.0", "", { "dependencies": { "minimatch": "^9.0.4", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.9" } }, "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg=="], 559 588 560 - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 589 + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], 561 590 562 591 "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], 563 592 564 593 "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], 565 594 566 - "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 595 + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], 567 596 568 - "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], 597 + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 569 598 570 599 "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], 571 600 ··· 593 622 594 623 "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], 595 624 596 - "actor-typeahead": ["actor-typeahead@0.1.1", "", {}, "sha512-ilsBwzplKwMSBiO6Tg6RdaZ5xxqgXds5jCQuHV+ib9Aq3ja9g0T7u2Y1PmihotmS7l5RxhpGI/tPm3ljoRDRwg=="], 625 + "actor-typeahead": ["actor-typeahead@0.1.2", "", {}, "sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A=="], 597 626 598 627 "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 599 628 ··· 605 634 606 635 "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], 607 636 608 - "atproto-ui": ["atproto-ui@0.11.3", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.10" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-NIBsORuo9lpCpr1SNKcKhNvqOVpsEy9IoHqFe1CM9gNTArpQL1hUcoP1Cou9a1O5qzCul9kaiu5xBHnB81I/WQ=="], 637 + "atproto-ui": ["atproto-ui@0.12.0", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.10" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-vdJmKNyuGWspuIIvySD601dL8wLJafgxfS/6NGBvbBFectoiaZ92Cua2JdDuSD/uRxUnRJ3AvMg7eL0M39DZ3Q=="], 609 638 610 639 "await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="], 611 640 ··· 613 642 614 643 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 615 644 616 - "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], 645 + "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], 617 646 618 647 "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 619 648 ··· 621 650 622 651 "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], 623 652 624 - "bun": ["bun@1.3.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.2", "@oven/bun-darwin-x64": "1.3.2", "@oven/bun-darwin-x64-baseline": "1.3.2", "@oven/bun-linux-aarch64": "1.3.2", "@oven/bun-linux-aarch64-musl": "1.3.2", "@oven/bun-linux-x64": "1.3.2", "@oven/bun-linux-x64-baseline": "1.3.2", "@oven/bun-linux-x64-musl": "1.3.2", "@oven/bun-linux-x64-musl-baseline": "1.3.2", "@oven/bun-windows-x64": "1.3.2", "@oven/bun-windows-x64-baseline": "1.3.2" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-x75mPJiEfhO1j4Tfc65+PtW6ZyrAB6yTZInydnjDZXF9u9PRAnr6OK3v0Q9dpDl0dxRHkXlYvJ8tteJxc8t4Sw=="], 653 + "bun": ["bun@1.3.3", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.3", "@oven/bun-darwin-x64": "1.3.3", "@oven/bun-darwin-x64-baseline": "1.3.3", "@oven/bun-linux-aarch64": "1.3.3", "@oven/bun-linux-aarch64-musl": "1.3.3", "@oven/bun-linux-x64": "1.3.3", "@oven/bun-linux-x64-baseline": "1.3.3", "@oven/bun-linux-x64-musl": "1.3.3", "@oven/bun-linux-x64-musl-baseline": "1.3.3", "@oven/bun-windows-x64": "1.3.3", "@oven/bun-windows-x64-baseline": "1.3.3" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-2hJ4ocTZ634/Ptph4lysvO+LbbRZq8fzRvMwX0/CqaLBxrF2UB5D1LdMB8qGcdtCer4/VR9Bx5ORub0yn+yzmw=="], 625 654 626 655 "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], 627 656 628 - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], 657 + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 629 658 630 659 "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 631 660 ··· 661 690 662 691 "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 663 692 664 - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], 693 + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 665 694 666 - "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], 695 + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], 667 696 668 - "core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="], 697 + "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], 669 698 670 - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 699 + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 671 700 672 701 "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 673 702 ··· 683 712 684 713 "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 685 714 686 - "elysia": ["elysia@1.4.16", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.3", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-KZtKN160/bdWVKg2hEgyoNXY8jRRquc+m6PboyisaLZL891I+Ufb7Ja6lDAD7vMQur8sLEWIcidZOzj5lWw9UA=="], 715 + "elysia": ["elysia@1.4.18", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "0.2.5", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-A6BhlipmSvgCy69SBgWADYZSdDIj3fT2gk8/9iMAC8iD+aGcnCr0fitziX0xr36MFDs/fsvVp8dWqxeq1VCgKg=="], 687 716 688 717 "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 689 718 ··· 713 742 714 743 "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], 715 744 716 - "exact-mirror": ["exact-mirror@0.2.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-aLdARfO0W0ntufjDyytUJQMbNXoB9g+BbA8KcgIq4XOOTYRw48yUGON/Pr64iDrYNZKcKvKbqE0MPW56FF2BXA=="], 745 + "exact-mirror": ["exact-mirror@0.2.5", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ=="], 717 746 718 - "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], 747 + "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], 719 748 720 749 "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], 721 750 ··· 723 752 724 753 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 725 754 726 - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], 727 - 728 - "file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="], 755 + "file-type": ["file-type@21.1.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg=="], 729 756 730 757 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 731 758 732 - "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], 759 + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], 733 760 734 761 "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 735 762 ··· 753 780 754 781 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 755 782 756 - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 757 - 758 783 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 759 784 760 785 "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 761 786 762 787 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 763 788 764 - "hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="], 789 + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], 765 790 766 - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], 791 + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], 767 792 768 793 "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 769 794 770 795 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 796 + 797 + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 771 798 772 799 "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], 773 800 ··· 895 922 896 923 "pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="], 897 924 898 - "playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="], 925 + "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], 899 926 900 - "playwright-core": ["playwright-core@1.56.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="], 927 + "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], 901 928 902 929 "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], 903 930 904 - "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], 931 + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], 905 932 906 933 "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], 907 934 ··· 913 940 914 941 "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 915 942 916 - "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], 943 + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], 917 944 918 945 "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], 919 946 ··· 921 948 922 949 "rate-limiter-flexible": ["rate-limiter-flexible@2.4.2", "", {}, "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="], 923 950 924 - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], 951 + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], 925 952 926 - "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], 953 + "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], 927 954 928 - "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], 955 + "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], 929 956 930 - "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], 957 + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], 931 958 932 959 "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], 933 960 ··· 953 980 954 981 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 955 982 956 - "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], 983 + "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], 957 984 958 985 "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], 959 986 ··· 975 1002 976 1003 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 977 1004 978 - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 1005 + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 979 1006 980 1007 "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 981 1008 ··· 989 1016 990 1017 "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 991 1018 992 - "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], 1019 + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], 993 1020 994 1021 "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], 995 1022 ··· 1011 1038 1012 1039 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1013 1040 1014 - "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], 1041 + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], 1015 1042 1016 1043 "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], 1017 1044 ··· 1061 1088 1062 1089 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1063 1090 1064 - "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], 1065 - 1066 - "@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 1091 + "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], 1067 1092 1068 1093 "@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.4.14", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ=="], 1069 1094 ··· 1071 1096 1072 1097 "@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1073 1098 1074 - "@atproto/common/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 1075 - 1076 1099 "@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1077 1100 1078 1101 "@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1079 1102 1080 1103 "@atproto/lex-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1081 1104 1082 - "@atproto/lex-cli/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="], 1083 - 1084 1105 "@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1085 1106 1086 1107 "@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1087 1108 1088 - "@atproto/oauth-client/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="], 1089 - 1090 1109 "@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1091 1110 1092 - "@atproto/repo/@atproto/common": ["@atproto/common@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.5", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-0S57sjzw4r9OLc5srJFi6uAz/aTKYl6btz3x36tSnGriL716m6h0x2IVtgd+FhUfIQfisevrqcqw8SfaGk8VTw=="], 1111 + "@atproto/repo/@atproto/common": ["@atproto/common@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA=="], 1093 1112 1094 1113 "@atproto/repo/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1095 1114 1096 - "@atproto/sync/@atproto/common": ["@atproto/common@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.5", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-0S57sjzw4r9OLc5srJFi6uAz/aTKYl6btz3x36tSnGriL716m6h0x2IVtgd+FhUfIQfisevrqcqw8SfaGk8VTw=="], 1115 + "@atproto/sync/@atproto/common": ["@atproto/common@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA=="], 1097 1116 1098 - "@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.1", "", { "dependencies": { "@atproto/common": "^0.5.1", "@atproto/crypto": "^0.4.4", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "@atproto/lexicon": "^0.5.2", "@atproto/ws-client": "^0.0.3", "@atproto/xrpc": "^0.7.6", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-kHXykL4inBV/49vefn5zR5zv/VM1//+BIRqk9OvB3+mbERw0jkFiHhc6PWyY/81VD4ciu7FZwUCpRy/mtQtIaA=="], 1117 + "@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.2", "", { "dependencies": { "@atproto/common": "^0.5.2", "@atproto/crypto": "^0.4.5", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "@atproto/lexicon": "^0.5.2", "@atproto/ws-client": "^0.0.3", "@atproto/xrpc": "^0.7.6", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-5AzN8xoV8K1Omn45z6qKH414+B3Z35D536rrScwF3aQGDEdpObAS+vya9UoSg+Gvm2+oOtVEbVri7riLTBW3Vg=="], 1099 1118 1100 1119 "@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1101 1120 1102 - "@atproto/ws-client/@atproto/common": ["@atproto/common@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.5", "@atproto/lex-cbor": "0.0.1", "@atproto/lex-data": "0.0.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-0S57sjzw4r9OLc5srJFi6uAz/aTKYl6btz3x36tSnGriL716m6h0x2IVtgd+FhUfIQfisevrqcqw8SfaGk8VTw=="], 1121 + "@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1122 + 1123 + "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1124 + 1125 + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1126 + 1127 + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1128 + 1129 + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1130 + 1131 + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1132 + 1133 + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1134 + 1135 + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1136 + 1137 + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1138 + 1139 + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1140 + 1141 + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1142 + 1143 + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1144 + 1145 + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1146 + 1147 + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="], 1148 + 1149 + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1150 + 1151 + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1152 + 1153 + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1154 + 1155 + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1156 + 1157 + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="], 1158 + 1159 + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="], 1160 + 1161 + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1162 + 1163 + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="], 1164 + 1165 + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1166 + 1167 + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1168 + 1169 + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1170 + 1171 + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1172 + 1173 + "@opentelemetry/exporter-prometheus/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1174 + 1175 + "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1176 + 1177 + "@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1178 + 1179 + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1180 + 1181 + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1182 + 1183 + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1184 + 1185 + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1186 + 1187 + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1188 + 1189 + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1190 + 1191 + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1192 + 1193 + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1194 + 1195 + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1196 + 1197 + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1198 + 1199 + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1200 + 1201 + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1103 1202 1104 - "@atproto/xrpc-server/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="], 1203 + "@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1105 1204 1106 - "@atproto/xrpc-server/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="], 1205 + "@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1107 1206 1108 - "@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1207 + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1208 + 1209 + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1210 + 1211 + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1212 + 1213 + "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g=="], 1214 + 1215 + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="], 1216 + 1217 + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.56.0", "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw=="], 1218 + 1219 + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="], 1220 + 1221 + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ=="], 1222 + 1223 + "@opentelemetry/propagator-b3/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1224 + 1225 + "@opentelemetry/propagator-jaeger/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1226 + 1227 + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="], 1228 + 1229 + "@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1230 + 1231 + "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1232 + 1233 + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1234 + 1235 + "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="], 1236 + 1237 + "@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1238 + 1239 + "@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="], 1240 + 1241 + "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1242 + 1243 + "@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1244 + 1245 + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1246 + 1247 + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1248 + 1249 + "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], 1109 1250 1110 1251 "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], 1111 1252 ··· 1121 1262 1122 1263 "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], 1123 1264 1124 - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], 1265 + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], 1125 1266 1126 1267 "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], 1127 1268 ··· 1129 1270 1130 1271 "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1131 1272 1132 - "@wisp/main-app/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="], 1133 - 1134 - "bun-types/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], 1273 + "@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 1135 1274 1136 - "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], 1275 + "@wisp/main-app/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="], 1137 1276 1138 - "fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1277 + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1139 1278 1140 1279 "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1141 1280 ··· 1143 1282 1144 1283 "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 1145 1284 1146 - "protobufjs/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], 1147 - 1148 1285 "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1149 1286 1150 - "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1287 + "send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], 1151 1288 1152 1289 "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1153 1290 1291 + "send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 1292 + 1293 + "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], 1294 + 1154 1295 "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1155 1296 1156 - "tsx/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 1297 + "tsx/esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="], 1157 1298 1158 1299 "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 1159 1300 ··· 1161 1302 1162 1303 "wisp-hosting-service/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="], 1163 1304 1164 - "@atproto/lex-cli/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 1305 + "wisp-hosting-service/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 1165 1306 1166 - "@atproto/lex-cli/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1307 + "@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="], 1167 1308 1168 - "@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="], 1309 + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1169 1310 1170 - "@atproto/ws-client/@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1311 + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1171 1312 1172 - "@atproto/xrpc-server/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 1313 + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1173 1314 1174 - "@atproto/xrpc-server/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1315 + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1175 1316 1176 - "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1317 + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1177 1318 1178 - "@wisp/main-app/@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 1319 + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1179 1320 1180 - "@wisp/main-app/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="], 1321 + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1181 1322 1182 - "@wisp/main-app/@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="], 1323 + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1183 1324 1184 - "@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1325 + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1185 1326 1186 - "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1327 + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], 1187 1328 1188 - "protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1329 + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], 1330 + 1331 + "@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1332 + 1333 + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1334 + 1335 + "@opentelemetry/sdk-metrics/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1336 + 1337 + "@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], 1338 + 1339 + "@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], 1340 + 1341 + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1342 + 1343 + "@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1189 1344 1190 1345 "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1191 1346 1192 - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 1347 + "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1193 1348 1194 - "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 1349 + "serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], 1195 1350 1196 - "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 1351 + "serve-static/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1197 1352 1198 - "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 1353 + "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 1199 1354 1200 - "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 1355 + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="], 1201 1356 1202 - "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 1357 + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="], 1203 1358 1204 - "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 1359 + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="], 1205 1360 1206 - "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 1361 + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="], 1207 1362 1208 - "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 1363 + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="], 1209 1364 1210 - "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 1365 + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="], 1211 1366 1212 - "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 1367 + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="], 1213 1368 1214 - "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 1369 + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="], 1215 1370 1216 - "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 1371 + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="], 1217 1372 1218 - "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 1373 + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="], 1219 1374 1220 - "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 1375 + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="], 1221 1376 1222 - "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 1377 + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="], 1223 1378 1224 - "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 1379 + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="], 1225 1380 1226 - "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 1381 + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="], 1227 1382 1228 - "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 1383 + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="], 1229 1384 1230 - "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 1385 + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw=="], 1231 1386 1232 - "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 1387 + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="], 1233 1388 1234 - "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 1389 + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="], 1235 1390 1236 - "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 1391 + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="], 1237 1392 1238 - "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 1393 + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="], 1239 1394 1240 - "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 1395 + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="], 1241 1396 1242 - "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 1397 + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="], 1243 1398 1244 - "wisp-hosting-service/@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 1399 + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="], 1400 + 1401 + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="], 1245 1402 1246 - "wisp-hosting-service/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="], 1403 + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="], 1247 1404 1248 - "wisp-hosting-service/@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.7.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "zod": "^3.23.8" } }, "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA=="], 1405 + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="], 1249 1406 1250 1407 "wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1251 1408 1252 - "@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 1409 + "wisp-hosting-service/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], 1253 1410 } 1254 1411 }
+116 -401
claude.md
··· 1 - # Wisp.place - Codebase Overview 1 + The project is wisp.place. It is a static site hoster built on top of the AT Protocol. The overall basis of the project is that users upload site assets to their PDS as blobs, and creates a manifest record listing every blob as well as site name. The hosting service then catches events relating to the site (create, read, upload, delete) and handles them appropriately. 2 2 3 - **Project URL**: https://wisp.place 3 + The lexicons look like this: 4 + ```typescript 5 + //place.wisp.fs 6 + interface Main { 7 + $type: 'place.wisp.fs' 8 + site: string 9 + root: Directory 10 + fileCount?: number 11 + createdAt: string 12 + } 4 13 5 - A decentralized static site hosting service built on the AT Protocol (Bluesky). Users can host static websites directly in their AT Protocol accounts, keeping full control and ownership while benefiting from fast CDN distribution. 14 + interface File { 15 + $type?: 'place.wisp.fs#file' 16 + type: 'file' 17 + blob: BlobRef 18 + encoding?: 'gzip' 19 + mimeType?: string 20 + base64?: boolean 21 + } 6 22 7 - --- 23 + interface Directory { 24 + $type?: 'place.wisp.fs#directory' 25 + type: 'directory' 26 + entries: Entry[] 27 + } 8 28 9 - ## ๐Ÿ—๏ธ Architecture Overview 29 + interface Entry { 30 + $type?: 'place.wisp.fs#entry' 31 + name: string 32 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 33 + } 10 34 11 - ### Multi-Part System 12 - 1. **Main Backend** (`/src`) - OAuth, site management, custom domains 13 - 2. **Hosting Service** (`/hosting-service`) - Microservice that serves cached sites 14 - 3. **CLI Tool** (`/cli`) - Rust CLI for direct site uploads to PDS 15 - 4. **Frontend** (`/public`) - React UI for onboarding, editor, admin 16 - 17 - ### Tech Stack 18 - - **Backend**: Elysia (Bun) + TypeScript + PostgreSQL 19 - - **Frontend**: React 19 + Tailwind CSS 4 + Radix UI 20 - - **CLI**: Rust with Jacquard (AT Protocol library) 21 - - **Database**: PostgreSQL for session/domain/site caching 22 - - **AT Protocol**: OAuth 2.0 + custom lexicons for storage 35 + interface Subfs { 36 + $type?: 'place.wisp.fs#subfs' 37 + type: 'subfs' 38 + subject: string // AT-URI pointing to a place.wisp.subfs record 39 + flat?: boolean 40 + } 23 41 24 - --- 42 + //place.wisp.subfs 43 + interface Main { 44 + $type: 'place.wisp.subfs' 45 + root: Directory 46 + fileCount?: number 47 + createdAt: string 48 + } 25 49 26 - ## ๐Ÿ“‚ Directory Structure 50 + interface File { 51 + $type?: 'place.wisp.subfs#file' 52 + type: 'file' 53 + blob: BlobRef 54 + encoding?: 'gzip' 55 + mimeType?: string 56 + base64?: boolean 57 + } 27 58 28 - ### `/src` - Main Backend Server 29 - **Purpose**: Core server handling OAuth, site management, custom domains, admin features 59 + interface Directory { 60 + $type?: 'place.wisp.subfs#directory' 61 + type: 'directory' 62 + entries: Entry[] 63 + } 30 64 31 - **Key Routes**: 32 - - `/api/auth/*` - OAuth signin/callback/logout/status 33 - - `/api/domain/*` - Custom domain management (BYOD) 34 - - `/wisp/*` - Site upload and management 35 - - `/api/user/*` - User info and site listing 36 - - `/api/admin/*` - Admin console (logs, metrics, DNS verification) 65 + interface Entry { 66 + $type?: 'place.wisp.subfs#entry' 67 + name: string 68 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 69 + } 37 70 38 - **Key Files**: 39 - - `index.ts` - Express-like Elysia app setup with middleware (CORS, CSP, security headers) 40 - - `lib/oauth-client.ts` - OAuth client setup with session/state persistence 41 - - `lib/db.ts` - PostgreSQL schema and queries for all tables 42 - - `lib/wisp-auth.ts` - Cookie-based authentication middleware 43 - - `lib/wisp-utils.ts` - File compression (gzip), manifest creation, blob handling 44 - - `lib/sync-sites.ts` - Syncs user's place.wisp.fs records from PDS to database cache 45 - - `lib/dns-verify.ts` - DNS verification for custom domains (TXT + CNAME) 46 - - `lib/dns-verification-worker.ts` - Background worker that checks domain verification every 10 minutes 47 - - `lib/admin-auth.ts` - Simple username/password admin authentication 48 - - `lib/observability.ts` - Logging, error tracking, metrics collection 49 - - `routes/auth.ts` - OAuth flow handlers 50 - - `routes/wisp.ts` - File upload and site creation (/wisp/upload-files) 51 - - `routes/domain.ts` - Domain claiming/verification API 52 - - `routes/user.ts` - User status/info/sites listing 53 - - `routes/site.ts` - Site metadata and file retrieval 54 - - `routes/admin.ts` - Admin dashboard API (logs, system health, manual DNS trigger) 71 + interface Subfs { 72 + $type?: 'place.wisp.subfs#subfs' 73 + type: 'subfs' 74 + subject: string // AT-URI pointing to another place.wisp.subfs record 75 + } 55 76 56 - ### `/lexicons` & `src/lexicons/` 57 - **Purpose**: AT Protocol Lexicon definitions for custom data types 77 + //place.wisp.settings 78 + interface Main { 79 + $type: 'place.wisp.settings' 80 + directoryListing: boolean 81 + spaMode?: string 82 + custom404?: string 83 + indexFiles?: string[] 84 + cleanUrls: boolean 85 + headers?: CustomHeader[] 86 + } 58 87 59 - **Key File**: `fs.json` - Defines `place.wisp.fs` record format 60 - - **structure**: Virtual filesystem manifest with tree structure 61 - - **site**: string identifier 62 - - **root**: directory object containing entries 63 - - **file**: blob reference + metadata (encoding, mimeType, base64 flag) 64 - - **directory**: array of entries (recursive) 65 - - **entry**: name + node (file or directory) 66 - 67 - **Important**: Files are gzip-compressed and base64-encoded before upload to bypass PDS content sniffing 68 - 69 - ### `/hosting-service` 70 - **Purpose**: Lightweight microservice that serves cached sites from disk 71 - 72 - **Architecture**: 73 - - Routes by domain lookup in PostgreSQL 74 - - Caches site content locally on first access or firehose event 75 - - Listens to AT Protocol firehose for new site records 76 - - Automatically downloads and caches files from PDS 77 - - SSRF-protected fetch (timeout, size limits, private IP blocking) 78 - 79 - **Routes**: 80 - 1. Custom domains (`/*`) โ†’ lookup custom_domains table 81 - 2. Wisp subdomains (`/*.wisp.place/*`) โ†’ lookup domains table 82 - 3. DNS hash routing (`/hash.dns.wisp.place/*`) โ†’ lookup custom_domains by hash 83 - 4. Direct serving (`/s.wisp.place/:identifier/:site/*`) โ†’ fetch from PDS if not cached 84 - 85 - **HTML Path Rewriting**: Absolute paths in HTML (`/style.css`) automatically rewritten to relative (`/:identifier/:site/style.css`) 86 - 87 - ### `/cli` 88 - **Purpose**: Rust CLI tool for direct site uploads using app password or OAuth 89 - 90 - **Flow**: 91 - 1. Authenticate with handle + app password or OAuth 92 - 2. Walk directory tree, compress files 93 - 3. Upload blobs to PDS via agent 94 - 4. Create place.wisp.fs record with manifest 95 - 5. Store site in database cache 96 - 97 - **Auth Methods**: 98 - - `--password` flag for app password auth 99 - - OAuth loopback server for browser-based auth 100 - - Supports both (password preferred if provided) 101 - 102 - --- 103 - 104 - ## ๐Ÿ” Key Concepts 105 - 106 - ### Custom Domains (BYOD - Bring Your Own Domain) 107 - **Process**: 108 - 1. User claims custom domain via API 109 - 2. System generates hash (SHA256(domain + secret)) 110 - 3. User adds DNS records: 111 - - TXT at `_wisp.example.com` = their DID 112 - - CNAME at `example.com` = `{hash}.dns.wisp.place` 113 - 4. Background worker checks verification every 10 minutes 114 - 5. Once verified, custom domain routes to their hosted sites 115 - 116 - **Tables**: `custom_domains` (id, domain, did, rkey, verified, last_verified_at) 117 - 118 - ### Wisp Subdomains 119 - **Process**: 120 - 1. Handle claimed on first signup (e.g., alice โ†’ alice.wisp.place) 121 - 2. Stored in `domains` table mapping domain โ†’ DID 122 - 3. Served by hosting service 123 - 124 - ### Site Storage 125 - **Locations**: 126 - - **Authoritative**: PDS (AT Protocol repo) as `place.wisp.fs` record 127 - - **Cache**: PostgreSQL `sites` table (rkey, did, site_name, created_at) 128 - - **File Cache**: Hosting service caches downloaded files on disk 129 - 130 - **Limits**: 131 - - MAX_SITE_SIZE: 300MB total 132 - - MAX_FILE_SIZE: 100MB per file 133 - - MAX_FILE_COUNT: 2000 files 134 - 135 - ### File Compression Strategy 136 - **Why**: Bypass PDS content sniffing issues (was treating HTML as images) 137 - 138 - **Process**: 139 - 1. All files gzip-compressed (level 9) 140 - 2. Compressed content base64-encoded 141 - 3. Uploaded as `application/octet-stream` MIME type 142 - 4. Blob metadata stores original MIME type + encoding flag 143 - 5. Hosting service decompresses on serve 144 - 145 - --- 146 - 147 - ## ๐Ÿ”„ Data Flow 148 - 149 - ### User Registration โ†’ Site Upload 150 - ``` 151 - 1. OAuth signin โ†’ state/session stored in DB 152 - 2. Cookie set with DID 153 - 3. Sync sites from PDS to cache DB 154 - 4. If no sites/domain โ†’ redirect to onboarding 155 - 5. User creates site โ†’ POST /wisp/upload-files 156 - 6. Files compressed, uploaded as blobs 157 - 7. place.wisp.fs record created 158 - 8. Site cached in DB 159 - 9. Hosting service notified via firehose 88 + interface CustomHeader { 89 + $type?: 'place.wisp.settings#customHeader' 90 + name: string 91 + value: string 92 + path?: string // Optional glob pattern 93 + } 160 94 ``` 161 95 162 - ### Custom Domain Setup 163 - ``` 164 - 1. User claims domain (DB check + allocation) 165 - 2. System generates hash 166 - 3. User adds DNS records (_wisp.domain TXT + CNAME) 167 - 4. Background worker verifies every 10 min 168 - 5. Hosting service routes based on verification status 169 - ``` 96 + The main differences between place.wisp.fs and place.wisp.subfs: 97 + - place.wisp.fs has a required site field 98 + - place.wisp.fs#subfs has an optional flat field that place.wisp.subfs#subfs doesn't have 170 99 171 - ### Site Access 172 - ``` 173 - Hosting Service: 174 - 1. Request arrives at custom domain or *.wisp.place 175 - 2. Domain lookup in PostgreSQL 176 - 3. Check cache for site files 177 - 4. If not cached: 178 - - Fetch from PDS using DID + rkey 179 - - Decompress files 180 - - Save to disk cache 181 - 5. Serve files (with HTML path rewriting) 182 - ``` 100 + The project is a monorepo. The package handler it uses for the typescript side is Bun. For the Rust cli, it is cargo. 183 101 184 - --- 102 + ### Typescript Bun Workspace Layout 185 103 186 - ## ๐Ÿ› ๏ธ Important Implementation Details 104 + Bun workspaces: `packages/@wisp/*`, `apps/main-app`, `apps/hosting-service` 187 105 188 - ### OAuth Implementation 189 - - **State & Session Storage**: PostgreSQL (with expiration) 190 - - **Key Rotation**: Periodic rotation + expiration cleanup (hourly) 191 - - **OAuth Flow**: Redirects to PDS, returns to /api/auth/callback 192 - - **Session Timeout**: 30 days 193 - - **State Timeout**: 1 hour 106 + There are two typescript apps 107 + **`apps/main-app`** - Main backend (Bun + Elysia) 194 108 195 - ### Security Headers 196 - - X-Frame-Options: DENY 197 - - X-Content-Type-Options: nosniff 198 - - Strict-Transport-Security: max-age=31536000 199 - - Content-Security-Policy (configured for Elysia + React) 200 - - X-XSS-Protection: 1; mode=block 201 - - Referrer-Policy: strict-origin-when-cross-origin 202 - 203 - ### Admin Authentication 204 - - Simple username/password (hashed with bcrypt) 205 - - Session-based cookie auth (24hr expiration) 206 - - Separate `admin_session` cookie 207 - - Initial setup prompted on startup 208 - 209 - ### Observability 210 - - **Logging**: Structured logging with service tags + event types 211 - - **Error Tracking**: Captures error context (message, stack, etc.) 212 - - **Metrics**: Request counts, latencies, error rates 213 - - **Log Levels**: debug, info, warn, error 214 - - **Collection**: Centralized log collector with in-memory buffer 215 - 216 - --- 217 - 218 - ## ๐Ÿ“ Database Schema 219 - 220 - ### oauth_states 221 - - key (primary key) 222 - - data (JSON) 223 - - created_at, expires_at (timestamps) 224 - 225 - ### oauth_sessions 226 - - sub (primary key - subject/DID) 227 - - data (JSON with OAuth session) 228 - - updated_at, expires_at 229 - 230 - ### oauth_keys 231 - - kid (primary key - key ID) 232 - - jwk (JSON Web Key) 233 - - created_at 234 - 235 - ### domains 236 - - domain (primary key - e.g., alice.wisp.place) 237 - - did (unique - user's DID) 238 - - rkey (optional - record key) 239 - - created_at 240 - 241 - ### custom_domains 242 - - id (primary key - UUID) 243 - - domain (unique - e.g., example.com) 244 - - did (user's DID) 245 - - rkey (optional) 246 - - verified (boolean) 247 - - last_verified_at (timestamp) 248 - - created_at 249 - 250 - ### sites 251 - - id, did, rkey, site_name 252 - - created_at, updated_at 253 - - Indexes on (did), (did, rkey), (rkey) 254 - 255 - ### admin_users 256 - - username (primary key) 257 - - password_hash (bcrypt) 258 - - created_at 259 - 260 - --- 261 - 262 - ## ๐Ÿš€ Key Workflows 263 - 264 - ### Sign In Flow 265 - 1. POST /api/auth/signin with handle 266 - 2. System generates state token 267 - 3. Redirects to PDS OAuth endpoint 268 - 4. PDS redirects back to /api/auth/callback?code=X&state=Y 269 - 5. Validate state (CSRF protection) 270 - 6. Exchange code for session 271 - 7. Store session in DB, set DID cookie 272 - 8. Sync sites from PDS 273 - 9. Redirect to /editor or /onboarding 274 - 275 - ### File Upload Flow 276 - 1. POST /wisp/upload-files with siteName + files 277 - 2. Validate site name (rkey format rules) 278 - 3. For each file: 279 - - Check size limits 280 - - Read as ArrayBuffer 281 - - Gzip compress 282 - - Base64 encode 283 - 4. Upload all blobs in parallel via agent.com.atproto.repo.uploadBlob() 284 - 5. Create manifest with all blob refs 285 - 6. putRecord() for place.wisp.fs with manifest 286 - 7. Upsert to sites table 287 - 8. Return URI + CID 288 - 289 - ### Domain Verification Flow 290 - 1. POST /api/custom-domains/claim 291 - 2. Generate hash = SHA256(domain + secret) 292 - 3. Store in custom_domains with verified=false 293 - 4. Return hash for user to configure DNS 294 - 5. Background worker periodically: 295 - - Query custom_domains where verified=false 296 - - Verify TXT record at _wisp.domain 297 - - Verify CNAME points to hash.dns.wisp.place 298 - - Update verified flag + last_verified_at 299 - 6. Hosting service routes when verified=true 300 - 301 - --- 302 - 303 - ## ๐ŸŽจ Frontend Structure 304 - 305 - ### `/public` 306 - - **index.tsx** - Landing page with sign-in form 307 - - **editor/editor.tsx** - Site editor/management UI 308 - - **admin/admin.tsx** - Admin dashboard 309 - - **components/ui/** - Reusable components (Button, Card, Dialog, etc.) 310 - - **styles/global.css** - Tailwind + custom styles 311 - 312 - ### Page Flow 313 - 1. `/` - Landing page (sign in / get started) 314 - 2. `/editor` - Main app (requires auth) 315 - 3. `/admin` - Admin console (requires admin auth) 316 - 4. `/onboarding` - First-time user setup 317 - 318 - --- 109 + - OAuth authentication and session management 110 + - Site CRUD operations via PDS 111 + - Custom domain management 112 + - Admin database view in /admin 113 + - React frontend in public/ 319 114 320 - ## ๐Ÿ” Notable Implementation Patterns 115 + **`apps/hosting-service`** - CDN static file server (Node + Hono) 321 116 322 - ### File Handling 323 - - Files stored as base64-encoded gzip in PDS blobs 324 - - Metadata preserves original MIME type 325 - - Hosting service decompresses on serve 326 - - Workaround for PDS image pipeline issues with HTML 117 + - Watches AT Protocol firehose for `place.wisp.fs` record changes 118 + - Downloads and caches site files to disk 119 + - Serves sites at `https://sites.wisp.place/{did}/{site-name}` and custom domains 120 + - Handles redirects (`_redirects` file support) and routing logic 121 + - Backfill mode for syncing existing sites 327 122 328 - ### Error Handling 329 - - Comprehensive logging with context 330 - - Graceful degradation (e.g., site sync failure doesn't break auth) 331 - - Structured error responses with details 123 + ### Shared Packages (`packages/@wisp/*`) 332 124 333 - ### Performance 334 - - Site sync: Batch fetch up to 100 records per request 335 - - Blob upload: Parallel promises for all files 336 - - DNS verification: Batched background worker (10 min intervals) 337 - - Caching: Two-tier (DB + disk in hosting service) 125 + - **`lexicons`** - AT Protocol lexicons (`place.wisp.fs`, `place.wisp.subfs`, `place.wisp.settings`) with 126 + generated TypeScript types 127 + - **`fs-utils`** - Filesystem tree building, manifest creation, subfs splitting logic 128 + - **`atproto-utils`** - AT Protocol helpers (blob upload, record operations, CID handling) 129 + - **`database`** - PostgreSQL schema and queries 130 + - **`constants`** - Shared constants (limits, file patterns, default settings) 131 + - **`observability`** - OpenTelemetry instrumentation 132 + - **`safe-fetch`** - Wrapped fetch with timeout/retry logic 338 133 339 - ### Validation 340 - - Lexicon validation on manifest creation 341 - - Record type checking 342 - - Domain format validation 343 - - Site name format validation (AT Protocol rkey rules) 344 - - File size limits enforced before upload 345 - 346 - --- 347 - 348 - ## ๐Ÿ› Known Quirks & Workarounds 349 - 350 - 1. **PDS Content Sniffing**: Files must be uploaded as `application/octet-stream` (even HTML) and base64-encoded to prevent PDS from misinterpreting content 351 - 352 - 2. **Max URL Query Size**: DNS verification worker queries in batch; may need pagination for users with many custom domains 353 - 354 - 3. **File Count Limits**: Max 500 entries per directory (Lexicon constraint); large sites split across multiple directories 355 - 356 - 4. **Blob Size Limits**: Individual blobs limited to 100MB by Lexicon; large files handled differently if needed 357 - 358 - 5. **HTML Path Rewriting**: Only in hosting service for `/s.wisp.place/:identifier/:site/*` routes; custom domains handled differently 134 + ### CLI 359 135 360 - --- 136 + **`cli/`** - Rust CLI using Jacquard (AT Protocol library) 137 + - Direct PDS uploads without interacting with main-app 138 + - Can also do the same firehose watching, caching, and serving hosting-service does, just without domain management 361 139 362 - ## ๐Ÿ“‹ Environment Variables 140 + ### Other Directories 363 141 364 - - `DOMAIN` - Base domain with protocol (default: `https://wisp.place`) 365 - - `CLIENT_NAME` - OAuth client name (default: `PDS-View`) 366 - - `DATABASE_URL` - PostgreSQL connection (default: `postgres://postgres:postgres@localhost:5432/wisp`) 367 - - `NODE_ENV` - production/development 368 - - `HOSTING_PORT` - Hosting service port (default: 3001) 369 - - `BASE_DOMAIN` - Domain for URLs (default: wisp.place) 370 - 371 - --- 372 - 373 - ## ๐Ÿง‘โ€๐Ÿ’ป Development Notes 374 - 375 - ### Adding New Features 376 - 1. **New routes**: Add to `/src/routes/*.ts`, import in index.ts 377 - 2. **DB changes**: Add migration in db.ts 378 - 3. **New lexicons**: Update `/lexicons/*.json`, regenerate types 379 - 4. **Admin features**: Add to /api/admin endpoints 380 - 381 - ### Testing 382 - - Run with `bun test` 383 - - CSRF tests in lib/csrf.test.ts 384 - - Utility tests in lib/wisp-utils.test.ts 385 - 386 - ### Debugging 387 - - Check logs via `/api/admin/logs` (requires admin auth) 388 - - DNS verification manual trigger: POST /api/admin/verify-dns 389 - - Health check: GET /api/health (includes DNS verifier status) 390 - 391 - --- 392 - 393 - ## ๐Ÿš€ Deployment Considerations 394 - 395 - 1. **Secrets**: Admin password, OAuth keys, database credentials 396 - 2. **HTTPS**: Required (HSTS header enforces it) 397 - 3. **CDN**: Custom domains require DNS configuration 398 - 4. **Scaling**: 399 - - Main server: Horizontal scaling with session DB 400 - - Hosting service: Independent scaling, disk cache per instance 401 - 5. **Backups**: PostgreSQL database critical; firehose provides recovery 402 - 403 - --- 404 - 405 - ## ๐Ÿ“š Related Technologies 406 - 407 - - **AT Protocol**: Decentralized identity, OAuth 2.0 408 - - **Jacquard**: Rust library for AT Protocol interactions 409 - - **Elysia**: Bun web framework (similar to Express/Hono) 410 - - **Lexicon**: AT Protocol's schema definition language 411 - - **Firehose**: Real-time event stream of repo changes 412 - - **PDS**: Personal Data Server (where users' data stored) 413 - 414 - --- 415 - 416 - ## ๐ŸŽฏ Project Goals 417 - 418 - โœ… Decentralized site hosting (data owned by users) 419 - โœ… Custom domain support with DNS verification 420 - โœ… Fast CDN distribution via hosting service 421 - โœ… Developer tools (CLI + API) 422 - โœ… Admin dashboard for monitoring 423 - โœ… Zero user data retention (sites in PDS, sessions in DB only) 424 - 425 - --- 426 - 427 - **Last Updated**: November 2025 428 - **Status**: Active development 142 + - **`docs/`** - Astro documentation site 143 + - **`binaries/`** - Compiled CLI binaries for distribution
+223 -11
cli/Cargo.lock
··· 162 162 ] 163 163 164 164 [[package]] 165 + name = "atomic-polyfill" 166 + version = "1.0.3" 167 + source = "registry+https://github.com/rust-lang/crates.io-index" 168 + checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 169 + dependencies = [ 170 + "critical-section", 171 + ] 172 + 173 + [[package]] 165 174 name = "atomic-waker" 166 175 version = "1.1.2" 167 176 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 360 369 ] 361 370 362 371 [[package]] 372 + name = "bstr" 373 + version = "1.12.1" 374 + source = "registry+https://github.com/rust-lang/crates.io-index" 375 + checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" 376 + dependencies = [ 377 + "memchr", 378 + "serde", 379 + ] 380 + 381 + [[package]] 363 382 name = "btree-range-map" 364 383 version = "0.7.2" 365 384 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 562 581 checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 563 582 564 583 [[package]] 584 + name = "cobs" 585 + version = "0.3.0" 586 + source = "registry+https://github.com/rust-lang/crates.io-index" 587 + checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" 588 + dependencies = [ 589 + "thiserror 2.0.17", 590 + ] 591 + 592 + [[package]] 565 593 name = "colorchoice" 566 594 version = "1.0.4" 567 595 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 595 623 checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" 596 624 597 625 [[package]] 626 + name = "console" 627 + version = "0.15.11" 628 + source = "registry+https://github.com/rust-lang/crates.io-index" 629 + checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 630 + dependencies = [ 631 + "encode_unicode", 632 + "libc", 633 + "once_cell", 634 + "unicode-width 0.2.2", 635 + "windows-sys 0.59.0", 636 + ] 637 + 638 + [[package]] 598 639 name = "const-oid" 599 640 version = "0.9.6" 600 641 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 670 711 ] 671 712 672 713 [[package]] 714 + name = "critical-section" 715 + version = "1.2.0" 716 + source = "registry+https://github.com/rust-lang/crates.io-index" 717 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 718 + 719 + [[package]] 673 720 name = "crossbeam-channel" 674 721 version = "0.5.15" 675 722 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 679 726 ] 680 727 681 728 [[package]] 729 + name = "crossbeam-deque" 730 + version = "0.8.6" 731 + source = "registry+https://github.com/rust-lang/crates.io-index" 732 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 733 + dependencies = [ 734 + "crossbeam-epoch", 735 + "crossbeam-utils", 736 + ] 737 + 738 + [[package]] 739 + name = "crossbeam-epoch" 740 + version = "0.9.18" 741 + source = "registry+https://github.com/rust-lang/crates.io-index" 742 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 743 + dependencies = [ 744 + "crossbeam-utils", 745 + ] 746 + 747 + [[package]] 682 748 name = "crossbeam-utils" 683 749 version = "0.8.21" 684 750 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 928 994 "subtle", 929 995 "zeroize", 930 996 ] 997 + 998 + [[package]] 999 + name = "embedded-io" 1000 + version = "0.4.0" 1001 + source = "registry+https://github.com/rust-lang/crates.io-index" 1002 + checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" 1003 + 1004 + [[package]] 1005 + name = "embedded-io" 1006 + version = "0.6.1" 1007 + source = "registry+https://github.com/rust-lang/crates.io-index" 1008 + checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 1009 + 1010 + [[package]] 1011 + name = "encode_unicode" 1012 + version = "1.0.0" 1013 + source = "registry+https://github.com/rust-lang/crates.io-index" 1014 + checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 931 1015 932 1016 [[package]] 933 1017 name = "encoding_rs" ··· 1209 1293 checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 1210 1294 1211 1295 [[package]] 1296 + name = "globset" 1297 + version = "0.4.18" 1298 + source = "registry+https://github.com/rust-lang/crates.io-index" 1299 + checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" 1300 + dependencies = [ 1301 + "aho-corasick", 1302 + "bstr", 1303 + "log", 1304 + "regex-automata", 1305 + "regex-syntax", 1306 + ] 1307 + 1308 + [[package]] 1212 1309 name = "gloo-storage" 1213 1310 version = "0.3.0" 1214 1311 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1287 1384 ] 1288 1385 1289 1386 [[package]] 1387 + name = "hash32" 1388 + version = "0.2.1" 1389 + source = "registry+https://github.com/rust-lang/crates.io-index" 1390 + checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 1391 + dependencies = [ 1392 + "byteorder", 1393 + ] 1394 + 1395 + [[package]] 1290 1396 name = "hashbrown" 1291 1397 version = "0.12.3" 1292 1398 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1305 1411 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 1306 1412 1307 1413 [[package]] 1414 + name = "heapless" 1415 + version = "0.7.17" 1416 + source = "registry+https://github.com/rust-lang/crates.io-index" 1417 + checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 1418 + dependencies = [ 1419 + "atomic-polyfill", 1420 + "hash32", 1421 + "rustc_version", 1422 + "serde", 1423 + "spin 0.9.8", 1424 + "stable_deref_trait", 1425 + ] 1426 + 1427 + [[package]] 1308 1428 name = "heck" 1309 1429 version = "0.4.1" 1310 1430 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1652 1772 ] 1653 1773 1654 1774 [[package]] 1775 + name = "ignore" 1776 + version = "0.4.25" 1777 + source = "registry+https://github.com/rust-lang/crates.io-index" 1778 + checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" 1779 + dependencies = [ 1780 + "crossbeam-deque", 1781 + "globset", 1782 + "log", 1783 + "memchr", 1784 + "regex-automata", 1785 + "same-file", 1786 + "walkdir", 1787 + "winapi-util", 1788 + ] 1789 + 1790 + [[package]] 1655 1791 name = "indexmap" 1656 1792 version = "1.9.3" 1657 1793 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1675 1811 ] 1676 1812 1677 1813 [[package]] 1814 + name = "indicatif" 1815 + version = "0.17.11" 1816 + source = "registry+https://github.com/rust-lang/crates.io-index" 1817 + checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 1818 + dependencies = [ 1819 + "console", 1820 + "number_prefix", 1821 + "portable-atomic", 1822 + "unicode-width 0.2.2", 1823 + "web-time", 1824 + ] 1825 + 1826 + [[package]] 1678 1827 name = "indoc" 1679 1828 version = "2.0.7" 1680 1829 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1751 1900 1752 1901 [[package]] 1753 1902 name = "jacquard" 1754 - version = "0.9.3" 1755 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1903 + version = "0.9.4" 1904 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 1756 1905 dependencies = [ 1757 1906 "bytes", 1758 1907 "getrandom 0.2.16", ··· 1768 1917 "regex", 1769 1918 "regex-lite", 1770 1919 "reqwest", 1920 + "ring", 1771 1921 "serde", 1772 1922 "serde_html_form", 1773 1923 "serde_json", ··· 1782 1932 [[package]] 1783 1933 name = "jacquard-api" 1784 1934 version = "0.9.2" 1785 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1935 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 1786 1936 dependencies = [ 1787 1937 "bon", 1788 1938 "bytes", ··· 1792 1942 "miette", 1793 1943 "rustversion", 1794 1944 "serde", 1945 + "serde_bytes", 1795 1946 "serde_ipld_dagcbor", 1796 1947 "thiserror 2.0.17", 1797 1948 "unicode-segmentation", ··· 1800 1951 [[package]] 1801 1952 name = "jacquard-common" 1802 1953 version = "0.9.2" 1803 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1954 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 1804 1955 dependencies = [ 1805 1956 "base64 0.22.1", 1806 1957 "bon", ··· 1821 1972 "n0-future 0.1.3", 1822 1973 "ouroboros", 1823 1974 "p256", 1975 + "postcard", 1824 1976 "rand 0.9.2", 1825 1977 "regex", 1826 1978 "regex-lite", 1827 1979 "reqwest", 1980 + "ring", 1828 1981 "serde", 1982 + "serde_bytes", 1829 1983 "serde_html_form", 1830 1984 "serde_ipld_dagcbor", 1831 1985 "serde_json", ··· 1841 1995 1842 1996 [[package]] 1843 1997 name = "jacquard-derive" 1844 - version = "0.9.3" 1845 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1998 + version = "0.9.4" 1999 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 1846 2000 dependencies = [ 1847 2001 "heck 0.5.0", 1848 2002 "jacquard-lexicon", ··· 1854 2008 [[package]] 1855 2009 name = "jacquard-identity" 1856 2010 version = "0.9.2" 1857 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 2011 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 1858 2012 dependencies = [ 1859 2013 "bon", 1860 2014 "bytes", ··· 1865 2019 "jacquard-lexicon", 1866 2020 "miette", 1867 2021 "mini-moka", 2022 + "n0-future 0.1.3", 1868 2023 "percent-encoding", 1869 2024 "reqwest", 2025 + "ring", 1870 2026 "serde", 1871 2027 "serde_html_form", 1872 2028 "serde_json", ··· 1880 2036 [[package]] 1881 2037 name = "jacquard-lexicon" 1882 2038 version = "0.9.2" 1883 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 2039 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 1884 2040 dependencies = [ 1885 2041 "cid", 1886 2042 "dashmap", ··· 1906 2062 [[package]] 1907 2063 name = "jacquard-oauth" 1908 2064 version = "0.9.2" 1909 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 2065 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 1910 2066 dependencies = [ 1911 2067 "base64 0.22.1", 1912 2068 "bytes", ··· 1921 2077 "miette", 1922 2078 "p256", 1923 2079 "rand 0.8.5", 2080 + "ring", 1924 2081 "rouille", 1925 2082 "serde", 1926 2083 "serde_html_form", ··· 2231 2388 [[package]] 2232 2389 name = "mini-moka" 2233 2390 version = "0.10.99" 2234 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 2391 + source = "git+https://tangled.org/nekomimi.pet/jacquard#69a1552424ffa439ea1022aa8e3f311ed77ab4df" 2235 2392 dependencies = [ 2236 2393 "crossbeam-channel", 2237 2394 "crossbeam-utils", ··· 2453 2610 dependencies = [ 2454 2611 "libc", 2455 2612 ] 2613 + 2614 + [[package]] 2615 + name = "number_prefix" 2616 + version = "0.4.0" 2617 + source = "registry+https://github.com/rust-lang/crates.io-index" 2618 + checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 2456 2619 2457 2620 [[package]] 2458 2621 name = "objc2" ··· 2700 2863 ] 2701 2864 2702 2865 [[package]] 2866 + name = "portable-atomic" 2867 + version = "1.11.1" 2868 + source = "registry+https://github.com/rust-lang/crates.io-index" 2869 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2870 + 2871 + [[package]] 2872 + name = "postcard" 2873 + version = "1.1.3" 2874 + source = "registry+https://github.com/rust-lang/crates.io-index" 2875 + checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" 2876 + dependencies = [ 2877 + "cobs", 2878 + "embedded-io 0.4.0", 2879 + "embedded-io 0.6.1", 2880 + "heapless", 2881 + "serde", 2882 + ] 2883 + 2884 + [[package]] 2703 2885 name = "potential_utf" 2704 2886 version = "0.1.4" 2705 2887 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3142 3324 checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 3143 3325 3144 3326 [[package]] 3327 + name = "rustc_version" 3328 + version = "0.4.1" 3329 + source = "registry+https://github.com/rust-lang/crates.io-index" 3330 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 3331 + dependencies = [ 3332 + "semver", 3333 + ] 3334 + 3335 + [[package]] 3145 3336 name = "rustix" 3146 3337 version = "1.1.2" 3147 3338 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3309 3500 "core-foundation-sys", 3310 3501 "libc", 3311 3502 ] 3503 + 3504 + [[package]] 3505 + name = "semver" 3506 + version = "1.0.27" 3507 + source = "registry+https://github.com/rust-lang/crates.io-index" 3508 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 3312 3509 3313 3510 [[package]] 3314 3511 name = "send_wrapper" ··· 3589 3786 version = "0.9.8" 3590 3787 source = "registry+https://github.com/rust-lang/crates.io-index" 3591 3788 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3789 + dependencies = [ 3790 + "lock_api", 3791 + ] 3592 3792 3593 3793 [[package]] 3594 3794 name = "spin" ··· 4659 4859 4660 4860 [[package]] 4661 4861 name = "windows-sys" 4862 + version = "0.59.0" 4863 + source = "registry+https://github.com/rust-lang/crates.io-index" 4864 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 4865 + dependencies = [ 4866 + "windows-targets 0.52.6", 4867 + ] 4868 + 4869 + [[package]] 4870 + name = "windows-sys" 4662 4871 version = "0.60.2" 4663 4872 source = "registry+https://github.com/rust-lang/crates.io-index" 4664 4873 checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" ··· 4939 5148 4940 5149 [[package]] 4941 5150 name = "wisp-cli" 4942 - version = "0.4.1" 5151 + version = "0.4.2" 4943 5152 dependencies = [ 4944 5153 "axum", 4945 5154 "base64 0.22.1", ··· 4948 5157 "clap", 4949 5158 "flate2", 4950 5159 "futures", 5160 + "globset", 5161 + "ignore", 5162 + "indicatif", 4951 5163 "jacquard", 4952 5164 "jacquard-api", 4953 5165 "jacquard-common",
+11 -1
cli/Cargo.toml
··· 1 1 [package] 2 2 name = "wisp-cli" 3 - version = "0.4.1" 3 + version = "0.4.2" 4 4 edition = "2024" 5 5 6 6 [features] ··· 15 15 jacquard-identity = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["dns"] } 16 16 jacquard-derive = { git = "https://tangled.org/nekomimi.pet/jacquard" } 17 17 jacquard-lexicon = { git = "https://tangled.org/nekomimi.pet/jacquard" } 18 + #jacquard = { path = "../../jacquard/crates/jacquard", features = ["loopback"] } 19 + #jacquard-oauth = { path = "../../jacquard/crates/jacquard-oauth" } 20 + #jacquard-api = { path = "../../jacquard/crates/jacquard-api", features = ["streaming"] } 21 + #jacquard-common = { path = "../../jacquard/crates/jacquard-common", features = ["websocket"] } 22 + #jacquard-identity = { path = "../../jacquard/crates/jacquard-identity", features = ["dns"] } 23 + #jacquard-derive = { path = "../../jacquard/crates/jacquard-derive" } 24 + #jacquard-lexicon = { path = "../../jacquard/crates/jacquard-lexicon" } 18 25 clap = { version = "4.5.51", features = ["derive"] } 19 26 tokio = { version = "1.48", features = ["full"] } 20 27 miette = { version = "7.6.0", features = ["fancy"] } ··· 40 47 chrono = "0.4" 41 48 url = "2.5" 42 49 regex = "1.11" 50 + ignore = "0.4" 51 + globset = "0.4" 52 + indicatif = "0.17"
+149
cli/src/ignore_patterns.rs
··· 1 + use globset::{Glob, GlobSet, GlobSetBuilder}; 2 + use serde::{Deserialize, Serialize}; 3 + use std::path::Path; 4 + use miette::IntoDiagnostic; 5 + 6 + #[derive(Debug, Deserialize, Serialize)] 7 + struct IgnoreConfig { 8 + patterns: Vec<String>, 9 + } 10 + 11 + /// Load ignore patterns from the default .wispignore.json file 12 + fn load_default_patterns() -> miette::Result<Vec<String>> { 13 + // Path to the default ignore patterns JSON file (in the monorepo root) 14 + let default_json_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../.wispignore.json"); 15 + 16 + match std::fs::read_to_string(default_json_path) { 17 + Ok(contents) => { 18 + let config: IgnoreConfig = serde_json::from_str(&contents).into_diagnostic()?; 19 + Ok(config.patterns) 20 + } 21 + Err(_) => { 22 + // If the default file doesn't exist, return hardcoded patterns as fallback 23 + eprintln!("โš ๏ธ Default .wispignore.json not found, using hardcoded patterns"); 24 + Ok(get_hardcoded_patterns()) 25 + } 26 + } 27 + } 28 + 29 + /// Hardcoded fallback patterns (same as in .wispignore.json) 30 + fn get_hardcoded_patterns() -> Vec<String> { 31 + vec![ 32 + ".git".to_string(), 33 + ".git/**".to_string(), 34 + ".github".to_string(), 35 + ".github/**".to_string(), 36 + ".gitlab".to_string(), 37 + ".gitlab/**".to_string(), 38 + ".DS_Store".to_string(), 39 + ".wisp.metadata.json".to_string(), 40 + ".env".to_string(), 41 + ".env.*".to_string(), 42 + "node_modules".to_string(), 43 + "node_modules/**".to_string(), 44 + "Thumbs.db".to_string(), 45 + "desktop.ini".to_string(), 46 + "._*".to_string(), 47 + ".Spotlight-V100".to_string(), 48 + ".Spotlight-V100/**".to_string(), 49 + ".Trashes".to_string(), 50 + ".Trashes/**".to_string(), 51 + ".fseventsd".to_string(), 52 + ".fseventsd/**".to_string(), 53 + ".cache".to_string(), 54 + ".cache/**".to_string(), 55 + ".temp".to_string(), 56 + ".temp/**".to_string(), 57 + ".tmp".to_string(), 58 + ".tmp/**".to_string(), 59 + "__pycache__".to_string(), 60 + "__pycache__/**".to_string(), 61 + "*.pyc".to_string(), 62 + ".venv".to_string(), 63 + ".venv/**".to_string(), 64 + "venv".to_string(), 65 + "venv/**".to_string(), 66 + "env".to_string(), 67 + "env/**".to_string(), 68 + "*.swp".to_string(), 69 + "*.swo".to_string(), 70 + "*~".to_string(), 71 + ".tangled".to_string(), 72 + ".tangled/**".to_string(), 73 + ] 74 + } 75 + 76 + /// Load custom ignore patterns from a .wispignore file in the given directory 77 + fn load_wispignore_file(dir_path: &Path) -> miette::Result<Vec<String>> { 78 + let wispignore_path = dir_path.join(".wispignore"); 79 + 80 + if !wispignore_path.exists() { 81 + return Ok(Vec::new()); 82 + } 83 + 84 + let contents = std::fs::read_to_string(&wispignore_path).into_diagnostic()?; 85 + 86 + // Parse gitignore-style file (one pattern per line, # for comments) 87 + let patterns: Vec<String> = contents 88 + .lines() 89 + .filter_map(|line| { 90 + let line = line.trim(); 91 + // Skip empty lines and comments 92 + if line.is_empty() || line.starts_with('#') { 93 + None 94 + } else { 95 + Some(line.to_string()) 96 + } 97 + }) 98 + .collect(); 99 + 100 + if !patterns.is_empty() { 101 + println!("Loaded {} custom patterns from .wispignore", patterns.len()); 102 + } 103 + 104 + Ok(patterns) 105 + } 106 + 107 + /// Build a GlobSet from a list of patterns 108 + fn build_globset(patterns: Vec<String>) -> miette::Result<GlobSet> { 109 + let mut builder = GlobSetBuilder::new(); 110 + 111 + for pattern in patterns { 112 + let glob = Glob::new(&pattern).into_diagnostic()?; 113 + builder.add(glob); 114 + } 115 + 116 + let globset = builder.build().into_diagnostic()?; 117 + Ok(globset) 118 + } 119 + 120 + /// IgnoreMatcher handles checking if paths should be ignored 121 + pub struct IgnoreMatcher { 122 + globset: GlobSet, 123 + } 124 + 125 + impl IgnoreMatcher { 126 + /// Create a new IgnoreMatcher for the given directory 127 + /// Loads default patterns and any custom .wispignore file 128 + pub fn new(dir_path: &Path) -> miette::Result<Self> { 129 + let mut all_patterns = load_default_patterns()?; 130 + 131 + // Load custom patterns from .wispignore 132 + let custom_patterns = load_wispignore_file(dir_path)?; 133 + all_patterns.extend(custom_patterns); 134 + 135 + let globset = build_globset(all_patterns)?; 136 + 137 + Ok(Self { globset }) 138 + } 139 + 140 + /// Check if the given path (relative to site root) should be ignored 141 + pub fn is_ignored(&self, path: &str) -> bool { 142 + self.globset.is_match(path) 143 + } 144 + 145 + /// Check if a filename should be ignored (checks just the filename, not full path) 146 + pub fn is_filename_ignored(&self, filename: &str) -> bool { 147 + self.globset.is_match(filename) 148 + } 149 + }
+173 -76
cli/src/main.rs
··· 8 8 mod serve; 9 9 mod subfs_utils; 10 10 mod redirects; 11 + mod ignore_patterns; 11 12 12 13 use clap::{Parser, Subcommand}; 13 14 use jacquard::CowStr; ··· 25 26 use std::io::Write; 26 27 use base64::Engine; 27 28 use futures::stream::{self, StreamExt}; 29 + use indicatif::{ProgressBar, ProgressStyle, MultiProgress}; 28 30 29 31 use place_wisp::fs::*; 30 32 use place_wisp::settings::*; 31 33 34 + /// Maximum number of concurrent file uploads to the PDS 35 + const MAX_CONCURRENT_UPLOADS: usize = 2; 36 + 37 + /// Limits for caching on wisp.place (from @wisp/constants) 38 + const MAX_FILE_COUNT: usize = 1000; 39 + const MAX_SITE_SIZE: usize = 300 * 1024 * 1024; // 300MB 40 + 32 41 #[derive(Parser, Debug)] 33 42 #[command(author, version, about = "wisp.place CLI tool")] 34 43 struct Args { ··· 63 72 /// Enable SPA mode (serve index.html for all routes) 64 73 #[arg(long, global = true, conflicts_with = "command")] 65 74 spa: bool, 75 + 76 + /// Skip confirmation prompts (automatically accept warnings) 77 + #[arg(short = 'y', long, global = true, conflicts_with = "command")] 78 + yes: bool, 66 79 } 67 80 68 81 #[derive(Subcommand, Debug)] ··· 95 108 /// Enable SPA mode (serve index.html for all routes) 96 109 #[arg(long)] 97 110 spa: bool, 111 + 112 + /// Skip confirmation prompts (automatically accept warnings) 113 + #[arg(short = 'y', long)] 114 + yes: bool, 98 115 }, 99 116 /// Pull a site from the PDS to a local directory 100 117 Pull { ··· 133 150 let args = Args::parse(); 134 151 135 152 let result = match args.command { 136 - Some(Commands::Deploy { input, path, site, store, password, directory, spa }) => { 153 + Some(Commands::Deploy { input, path, site, store, password, directory, spa, yes }) => { 137 154 // Dispatch to appropriate authentication method 138 155 if let Some(password) = password { 139 - run_with_app_password(input, password, path, site, directory, spa).await 156 + run_with_app_password(input, password, path, site, directory, spa, yes).await 140 157 } else { 141 - run_with_oauth(input, store, path, site, directory, spa).await 158 + run_with_oauth(input, store, path, site, directory, spa, yes).await 142 159 } 143 160 } 144 161 Some(Commands::Pull { input, site, output }) => { ··· 155 172 156 173 // Dispatch to appropriate authentication method 157 174 if let Some(password) = args.password { 158 - run_with_app_password(input, password, path, args.site, args.directory, args.spa).await 175 + run_with_app_password(input, password, path, args.site, args.directory, args.spa, args.yes).await 159 176 } else { 160 - run_with_oauth(input, store, path, args.site, args.directory, args.spa).await 177 + run_with_oauth(input, store, path, args.site, args.directory, args.spa, args.yes).await 161 178 } 162 179 } else { 163 180 // No command and no input, show help ··· 186 203 site: Option<String>, 187 204 directory: bool, 188 205 spa: bool, 206 + yes: bool, 189 207 ) -> miette::Result<()> { 190 208 let (session, auth) = 191 209 MemoryCredentialSession::authenticated(input, password, None, None).await?; 192 210 println!("Signed in as {}", auth.handle); 193 211 194 212 let agent: Agent<_> = Agent::from(session); 195 - deploy_site(&agent, path, site, directory, spa).await 213 + deploy_site(&agent, path, site, directory, spa, yes).await 196 214 } 197 215 198 216 /// Run deployment with OAuth authentication ··· 203 221 site: Option<String>, 204 222 directory: bool, 205 223 spa: bool, 224 + yes: bool, 206 225 ) -> miette::Result<()> { 207 226 use jacquard::oauth::scopes::Scope; 208 227 use jacquard::oauth::atproto::AtprotoClientMetadata; ··· 235 254 .await?; 236 255 237 256 let agent: Agent<_> = Agent::from(session); 238 - deploy_site(&agent, path, site, directory, spa).await 257 + deploy_site(&agent, path, site, directory, spa, yes).await 258 + } 259 + 260 + /// Scan directory to count files and calculate total size 261 + /// Returns (file_count, total_size_bytes) 262 + fn scan_directory_stats( 263 + dir_path: &Path, 264 + ignore_matcher: &ignore_patterns::IgnoreMatcher, 265 + current_path: String, 266 + ) -> miette::Result<(usize, u64)> { 267 + let mut file_count = 0; 268 + let mut total_size = 0u64; 269 + 270 + let dir_entries: Vec<_> = std::fs::read_dir(dir_path) 271 + .into_diagnostic()? 272 + .collect::<Result<Vec<_>, _>>() 273 + .into_diagnostic()?; 274 + 275 + for entry in dir_entries { 276 + let path = entry.path(); 277 + let name = entry.file_name(); 278 + let name_str = name.to_str() 279 + .ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))? 280 + .to_string(); 281 + 282 + let full_path = if current_path.is_empty() { 283 + name_str.clone() 284 + } else { 285 + format!("{}/{}", current_path, name_str) 286 + }; 287 + 288 + // Skip files/directories that match ignore patterns 289 + if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) { 290 + continue; 291 + } 292 + 293 + let metadata = entry.metadata().into_diagnostic()?; 294 + 295 + if metadata.is_file() { 296 + file_count += 1; 297 + total_size += metadata.len(); 298 + } else if metadata.is_dir() { 299 + let subdir_path = if current_path.is_empty() { 300 + name_str 301 + } else { 302 + format!("{}/{}", current_path, name_str) 303 + }; 304 + let (sub_count, sub_size) = scan_directory_stats(&path, ignore_matcher, subdir_path)?; 305 + file_count += sub_count; 306 + total_size += sub_size; 307 + } 308 + } 309 + 310 + Ok((file_count, total_size)) 239 311 } 240 312 241 313 /// Deploy the site using the provided agent ··· 245 317 site: Option<String>, 246 318 directory_listing: bool, 247 319 spa_mode: bool, 320 + skip_prompts: bool, 248 321 ) -> miette::Result<()> { 249 322 // Verify the path exists 250 323 if !path.exists() { ··· 262 335 263 336 println!("Deploying site '{}'...", site_name); 264 337 338 + // Scan directory to check file count and size 339 + let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?; 340 + let (file_count, total_size) = scan_directory_stats(&path, &ignore_matcher, String::new())?; 341 + 342 + let size_mb = total_size as f64 / (1024.0 * 1024.0); 343 + println!("Scanned: {} files, {:.1} MB total", file_count, size_mb); 344 + 345 + // Check if limits are exceeded 346 + let exceeds_file_count = file_count > MAX_FILE_COUNT; 347 + let exceeds_size = total_size > MAX_SITE_SIZE as u64; 348 + 349 + if exceeds_file_count || exceeds_size { 350 + println!("\nโš ๏ธ Warning: Your site exceeds wisp.place caching limits:"); 351 + 352 + if exceeds_file_count { 353 + println!(" โ€ข File count: {} (limit: {})", file_count, MAX_FILE_COUNT); 354 + } 355 + 356 + if exceeds_size { 357 + let size_mb = total_size as f64 / (1024.0 * 1024.0); 358 + let limit_mb = MAX_SITE_SIZE as f64 / (1024.0 * 1024.0); 359 + println!(" โ€ข Total size: {:.1} MB (limit: {:.0} MB)", size_mb, limit_mb); 360 + } 361 + 362 + println!("\nwisp.place will NOT serve your site if you proceed."); 363 + println!("Your site will be uploaded to your PDS, but will only be accessible via:"); 364 + println!(" โ€ข wisp-cli serve (local hosting)"); 365 + println!(" โ€ข Other hosting services with more generous limits"); 366 + 367 + if !skip_prompts { 368 + // Prompt for confirmation 369 + use std::io::{self, Write}; 370 + print!("\nDo you want to upload anyway? (y/N): "); 371 + io::stdout().flush().into_diagnostic()?; 372 + 373 + let mut input = String::new(); 374 + io::stdin().read_line(&mut input).into_diagnostic()?; 375 + let input = input.trim().to_lowercase(); 376 + 377 + if input != "y" && input != "yes" { 378 + println!("Upload cancelled."); 379 + return Ok(()); 380 + } 381 + } else { 382 + println!("\nSkipping confirmation (--yes flag set)."); 383 + } 384 + 385 + println!("\nProceeding with upload...\n"); 386 + } 387 + 265 388 // Try to fetch existing manifest for incremental updates 266 389 let (existing_blob_map, old_subfs_uris): (HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, Vec<(String, String)>) = { 267 390 use jacquard_common::types::string::AtUri; ··· 323 446 } 324 447 }; 325 448 326 - // Build directory tree 327 - let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new()).await?; 449 + // Create progress tracking (spinner style since we don't know total count upfront) 450 + let multi_progress = MultiProgress::new(); 451 + let progress = multi_progress.add(ProgressBar::new_spinner()); 452 + progress.set_style( 453 + ProgressStyle::default_spinner() 454 + .template("[{elapsed_precise}] {spinner:.cyan} {pos} files {msg}") 455 + .into_diagnostic()? 456 + .tick_chars("โ โ ‚โ „โก€โข€โ  โ โ ˆ ") 457 + ); 458 + progress.set_message("Scanning files..."); 459 + progress.enable_steady_tick(std::time::Duration::from_millis(100)); 460 + 461 + let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher, &progress).await?; 328 462 let uploaded_count = total_files - reused_count; 463 + 464 + progress.finish_with_message(format!("โœ“ {} files ({} uploaded, {} reused)", total_files, uploaded_count, reused_count)); 329 465 330 466 // Check if we need to split into subfs records 331 467 const MAX_MANIFEST_SIZE: usize = 140 * 1024; // 140KB (PDS limit is 150KB) ··· 603 739 dir_path: &'a Path, 604 740 existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 605 741 current_path: String, 742 + ignore_matcher: &'a ignore_patterns::IgnoreMatcher, 743 + progress: &'a ProgressBar, 606 744 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>> 607 745 { 608 746 Box::pin(async move { ··· 623 761 .ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))? 624 762 .to_string(); 625 763 626 - // Skip unwanted files and directories 627 - 628 - // .git directory (version control - thousands of files) 629 - if name_str == ".git" { 630 - continue; 631 - } 632 - 633 - // .DS_Store (macOS metadata - can leak info) 634 - if name_str == ".DS_Store" { 635 - continue; 636 - } 637 - 638 - // .wisp.metadata.json (wisp internal metadata - should not be uploaded) 639 - if name_str == ".wisp.metadata.json" { 640 - continue; 641 - } 642 - 643 - // .env files (environment variables with secrets) 644 - if name_str.starts_with(".env") { 645 - continue; 646 - } 647 - 648 - // node_modules (dependency folder - can be 100,000+ files) 649 - if name_str == "node_modules" { 650 - continue; 651 - } 652 - 653 - // OS metadata files 654 - if name_str == "Thumbs.db" || name_str == "desktop.ini" || name_str.starts_with("._") { 655 - continue; 656 - } 657 - 658 - // macOS system directories 659 - if name_str == ".Spotlight-V100" || name_str == ".Trashes" || name_str == ".fseventsd" { 660 - continue; 661 - } 764 + // Construct full path for ignore checking 765 + let full_path = if current_path.is_empty() { 766 + name_str.clone() 767 + } else { 768 + format!("{}/{}", current_path, name_str) 769 + }; 662 770 663 - // Cache and temp directories 664 - if name_str == ".cache" || name_str == ".temp" || name_str == ".tmp" { 665 - continue; 666 - } 667 - 668 - // Python cache 669 - if name_str == "__pycache__" || name_str.ends_with(".pyc") { 670 - continue; 671 - } 672 - 673 - // Python virtual environments 674 - if name_str == ".venv" || name_str == "venv" || name_str == "env" { 675 - continue; 676 - } 677 - 678 - // Editor swap files 679 - if name_str.ends_with(".swp") || name_str.ends_with(".swo") || name_str.ends_with("~") { 771 + // Skip files/directories that match ignore patterns 772 + if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) { 680 773 continue; 681 774 } 682 775 ··· 695 788 } 696 789 } 697 790 698 - // Process files concurrently with a limit of 5 791 + // Process files concurrently with a limit of 2 699 792 let file_results: Vec<(Entry<'static>, bool)> = stream::iter(file_tasks) 700 793 .map(|(name, path, full_path)| async move { 701 - let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs).await?; 794 + let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs, progress).await?; 795 + progress.inc(1); 702 796 let entry = Entry::new() 703 797 .name(CowStr::from(name)) 704 798 .node(EntryNode::File(Box::new(file_node))) 705 799 .build(); 706 800 Ok::<_, miette::Report>((entry, reused)) 707 801 }) 708 - .buffer_unordered(5) 802 + .buffer_unordered(MAX_CONCURRENT_UPLOADS) 709 803 .collect::<Vec<_>>() 710 804 .await 711 805 .into_iter() ··· 732 826 } else { 733 827 format!("{}/{}", current_path, name) 734 828 }; 735 - let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path).await?; 829 + let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher, progress).await?; 736 830 dir_entries.push(Entry::new() 737 831 .name(CowStr::from(name)) 738 832 .node(EntryNode::Directory(Box::new(subdir))) ··· 764 858 file_path: &Path, 765 859 file_path_key: &str, 766 860 existing_blobs: &HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 861 + progress: &ProgressBar, 767 862 ) -> miette::Result<(File<'static>, bool)> 768 863 { 769 864 // Read file ··· 803 898 if let Some((existing_blob_ref, existing_cid)) = existing_blob { 804 899 if existing_cid == &file_cid { 805 900 // CIDs match - reuse existing blob 806 - println!(" โœ“ Reusing blob for {} (CID: {})", file_path_key, file_cid); 901 + progress.set_message(format!("โœ“ Reused {}", file_path_key)); 807 902 let mut file_builder = File::new() 808 903 .r#type(CowStr::from("file")) 809 904 .blob(existing_blob_ref.clone()) ··· 817 912 } 818 913 819 914 return Ok((file_builder.build(), true)); 820 - } else { 821 - // CID mismatch - file changed 822 - println!(" โ†’ File changed: {} (old CID: {}, new CID: {})", file_path_key, existing_cid, file_cid); 823 - } 824 - } else { 825 - // File not in existing blob map 826 - if file_path_key.starts_with("imgs/") { 827 - println!(" โ†’ New file (not in blob map): {}", file_path_key); 828 915 } 829 916 } 830 917 ··· 835 922 MimeType::new_static("application/octet-stream") 836 923 }; 837 924 838 - println!(" โ†‘ Uploading {} ({} bytes, CID: {})", file_path_key, upload_bytes.len(), file_cid); 925 + // Format file size nicely 926 + let size_str = if upload_bytes.len() < 1024 { 927 + format!("{} B", upload_bytes.len()) 928 + } else if upload_bytes.len() < 1024 * 1024 { 929 + format!("{:.1} KB", upload_bytes.len() as f64 / 1024.0) 930 + } else { 931 + format!("{:.1} MB", upload_bytes.len() as f64 / (1024.0 * 1024.0)) 932 + }; 933 + 934 + progress.set_message(format!("โ†‘ Uploading {} ({})", file_path_key, size_str)); 839 935 let blob = agent.upload_blob(upload_bytes, mime_type).await?; 936 + progress.set_message(format!("โœ“ Uploaded {}", file_path_key)); 840 937 841 938 let mut file_builder = File::new() 842 939 .r#type(CowStr::from("file"))
+4 -1
docs/astro.config.mjs
··· 7 7 integrations: [ 8 8 starlight({ 9 9 title: 'Wisp.place Docs', 10 - social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/tangled-org/wisp.place' }], 10 + components: { 11 + SocialIcons: './src/components/SocialIcons.astro', 12 + }, 11 13 sidebar: [ 12 14 { 13 15 label: 'Getting Started', ··· 24 26 label: 'Guides', 25 27 items: [ 26 28 { label: 'Self-Hosting', slug: 'deployment' }, 29 + { label: 'Monitoring & Metrics', slug: 'monitoring' }, 27 30 { label: 'Redirects & Rewrites', slug: 'redirects' }, 28 31 ], 29 32 },
+9
docs/src/assets/tangled-icon.svg
··· 1 + <svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg1" width="25" height="25" viewBox="0 0 25 25" sodipodi:docname="tangled_dolly_silhouette.png"> 2 + <defs id="defs1"/> 3 + <sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1"> 4 + <inkscape:page x="0" y="0" width="25" height="25" id="page2" margin="0" bleed="0"/> 5 + </sodipodi:namedview> 6 + <g inkscape:groupmode="layer" inkscape:label="Image" id="g1"> 7 + <path style="fill:#000000;stroke-width:1.12248" d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z" id="path1"/> 8 + </g> 9 + </svg>
+26
docs/src/components/SocialIcons.astro
··· 1 + --- 2 + // Custom social icons component to use the Tangled icon 3 + --- 4 + 5 + <div class="sl-flex"> 6 + <a 7 + href="https://tangled.org/nekomimi.pet/wisp.place-monorepo" 8 + rel="me" 9 + class="sl-flex" 10 + aria-label="Tangled" 11 + > 12 + <svg 13 + xmlns="http://www.w3.org/2000/svg" 14 + viewBox="0 0 25 25" 15 + width="16" 16 + height="16" 17 + aria-hidden="true" 18 + focusable="false" 19 + > 20 + <path 21 + style="fill:currentColor;stroke-width:1.12248" 22 + d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z" 23 + ></path> 24 + </svg> 25 + </a> 26 + </div>
+2 -5
docs/src/content/docs/cli.md
··· 71 71 72 72 engine: 'nixery' 73 73 74 - clone: 75 - skip: false 76 - depth: 1 77 - submodules: false 78 - 79 74 dependencies: 80 75 nixpkgs: 81 76 - nodejs ··· 203 198 ## File Processing 204 199 205 200 The CLI handles all file processing automatically to ensure reliable storage and delivery. Files are compressed with gzip at level 9 for optimal size reduction, then base64 encoded to bypass PDS content sniffing restrictions. Everything is uploaded as `application/octet-stream` blobs while preserving the original MIME type as metadata. When serving your site, the hosting service automatically decompresses non-HTML/CSS/JS files, ensuring your content is delivered correctly to visitors. 201 + 202 + **File Filtering**: The CLI automatically excludes common files like `.git`, `node_modules`, `.env`, and other development artifacts. Customize this with a [`.wispignore` file](/file-filtering). 206 203 207 204 ## Incremental Updates 208 205
+58
docs/src/content/docs/file-filtering.md
··· 1 + --- 2 + title: File Filtering & .wispignore 3 + description: Control which files are uploaded to your Wisp site 4 + --- 5 + 6 + # File Filtering & .wispignore 7 + 8 + Wisp automatically excludes common files that shouldn't be deployed (`.git`, `node_modules`, `.env`, etc.). 9 + 10 + ## Default Exclusions 11 + 12 + - Version control: `.git`, `.github`, `.gitlab` 13 + - Dependencies: `node_modules`, `__pycache__`, `*.pyc` 14 + - Secrets: `.env`, `.env.*` 15 + - OS files: `.DS_Store`, `Thumbs.db`, `._*` 16 + - Cache: `.cache`, `.temp`, `.tmp` 17 + - Dev tools: `.vscode`, `*.swp`, `*~`, `.tangled` 18 + - Virtual envs: `.venv`, `venv`, `env` 19 + 20 + ## Custom Patterns 21 + 22 + Create a `.wispignore` file in your site root using gitignore syntax: 23 + 24 + ``` 25 + # Build outputs 26 + dist/ 27 + *.map 28 + 29 + # Logs and temp files 30 + *.log 31 + temp/ 32 + 33 + # Keep one exception 34 + !important.log 35 + ``` 36 + 37 + ### Pattern Syntax 38 + 39 + - `file.txt` - exact match 40 + - `*.log` - wildcard 41 + - `logs/` - directory 42 + - `src/**/*.test.js` - glob pattern 43 + - `!keep.txt` - exception (don't ignore) 44 + 45 + ## Usage 46 + 47 + **CLI**: Place `.wispignore` in your upload directory 48 + ```bash 49 + wisp-cli handle.bsky.social --path ./my-site --site my-site 50 + ``` 51 + 52 + **Web**: Include `.wispignore` when uploading files 53 + 54 + ## Notes 55 + 56 + - Custom patterns add to (not replace) default patterns 57 + - Works in both CLI and web uploads 58 + - The CLI logs which files are skipped
+85
docs/src/content/docs/guides/grafana-setup.md
··· 1 + --- 2 + title: Grafana Setup Example 3 + description: Quick setup for Grafana Cloud monitoring 4 + --- 5 + 6 + Example setup for monitoring Wisp.place with Grafana Cloud. 7 + 8 + ## 1. Create Grafana Cloud Account 9 + 10 + Sign up at [grafana.com](https://grafana.com) for a free tier account. 11 + 12 + ## 2. Get Credentials 13 + 14 + Navigate to your stack and find: 15 + 16 + **Loki** (Connections โ†’ Loki โ†’ Details): 17 + - Push endpoint: `https://logs-prod-XXX.grafana.net` 18 + - Create API token with write permissions 19 + 20 + **Prometheus** (Connections โ†’ Prometheus โ†’ Details): 21 + - Remote Write endpoint: `https://prometheus-prod-XXX.grafana.net/api/prom` 22 + - Create API token with write permissions 23 + 24 + ## 3. Configure Wisp.place 25 + 26 + Add to your `.env`: 27 + 28 + ```bash 29 + GRAFANA_LOKI_URL=https://logs-prod-XXX.grafana.net 30 + GRAFANA_LOKI_TOKEN=glc_eyJ... 31 + 32 + GRAFANA_PROMETHEUS_URL=https://prometheus-prod-XXX.grafana.net/api/prom 33 + GRAFANA_PROMETHEUS_TOKEN=glc_eyJ... 34 + ``` 35 + 36 + ## 4. Create Dashboard 37 + 38 + Import this dashboard JSON or build your own: 39 + 40 + ```json 41 + { 42 + "panels": [ 43 + { 44 + "title": "Request Rate", 45 + "targets": [{ 46 + "expr": "sum(rate(http_requests_total[1m])) by (service)" 47 + }] 48 + }, 49 + { 50 + "title": "P95 Latency", 51 + "targets": [{ 52 + "expr": "histogram_quantile(0.95, rate(http_request_duration_ms_bucket[5m]))" 53 + }] 54 + }, 55 + { 56 + "title": "Error Rate", 57 + "targets": [{ 58 + "expr": "sum(rate(errors_total[5m])) / sum(rate(http_requests_total[5m]))" 59 + }] 60 + } 61 + ] 62 + } 63 + ``` 64 + 65 + ## 5. Set Alerts 66 + 67 + Example alert for high error rate: 68 + 69 + ```yaml 70 + alert: HighErrorRate 71 + expr: | 72 + sum(rate(errors_total[5m])) by (service) / 73 + sum(rate(http_requests_total[5m])) by (service) > 0.05 74 + for: 5m 75 + annotations: 76 + summary: "High error rate in {{ $labels.service }}" 77 + ``` 78 + 79 + ## Verify Data Flow 80 + 81 + Check Grafana Explore: 82 + - Loki: `{job="main-app"} | json` 83 + - Prometheus: `http_requests_total` 84 + 85 + Data should appear within 30 seconds of service startup.
+156
docs/src/content/docs/monitoring.md
··· 1 + --- 2 + title: Monitoring & Metrics 3 + description: Track performance and debug issues with Grafana integration 4 + --- 5 + 6 + Wisp.place includes built-in observability with automatic Grafana integration for logs and metrics. Monitor request performance, track errors, and analyze usage patterns across both the main backend and hosting service. 7 + 8 + ## Quick Start 9 + 10 + Set environment variables to enable Grafana export: 11 + 12 + ```bash 13 + # Grafana Cloud 14 + GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net 15 + GRAFANA_LOKI_TOKEN=glc_xxx 16 + 17 + GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom 18 + GRAFANA_PROMETHEUS_TOKEN=glc_xxx 19 + 20 + # Self-hosted Grafana 21 + GRAFANA_LOKI_USERNAME=your-username 22 + GRAFANA_LOKI_PASSWORD=your-password 23 + ``` 24 + 25 + Restart services. Metrics and logs now flow to Grafana automatically. 26 + 27 + ## Metrics Collected 28 + 29 + ### HTTP Requests 30 + - `http_requests_total` - Total request count by path, method, status 31 + - `http_request_duration_ms` - Request duration histogram 32 + - `errors_total` - Error count by service 33 + 34 + ### Performance Stats 35 + - P50, P95, P99 response times 36 + - Requests per minute 37 + - Error rates 38 + - Average duration by endpoint 39 + 40 + ## Log Aggregation 41 + 42 + Logs are sent to Loki with automatic categorization: 43 + 44 + ``` 45 + {job="main-app"} |= "error" # OAuth and upload errors 46 + {job="hosting-service"} |= "cache" # Cache operations 47 + {service="hosting-service", level="warn"} # Warnings only 48 + ``` 49 + 50 + ## Service Identification 51 + 52 + Each service is tagged separately: 53 + - `main-app` - OAuth, uploads, domain management 54 + - `hosting-service` - Firehose, caching, content serving 55 + 56 + ## Configuration Options 57 + 58 + ### Environment Variables 59 + 60 + ```bash 61 + # Required 62 + GRAFANA_LOKI_URL # Loki endpoint 63 + GRAFANA_PROMETHEUS_URL # Prometheus endpoint (add /api/prom for OTLP) 64 + 65 + # Authentication (use one) 66 + GRAFANA_LOKI_TOKEN # Bearer token (Grafana Cloud) 67 + GRAFANA_LOKI_USERNAME # Basic auth (self-hosted) 68 + GRAFANA_LOKI_PASSWORD 69 + 70 + # Optional 71 + GRAFANA_BATCH_SIZE=100 # Batch size before flush 72 + GRAFANA_FLUSH_INTERVAL=5000 # Flush interval in ms 73 + ``` 74 + 75 + ### Programmatic Setup 76 + 77 + ```typescript 78 + import { initializeGrafanaExporters } from '@wisp/observability' 79 + 80 + initializeGrafanaExporters({ 81 + lokiUrl: 'https://logs.grafana.net', 82 + lokiAuth: { bearerToken: 'token' }, 83 + prometheusUrl: 'https://prometheus.grafana.net/api/prom', 84 + prometheusAuth: { bearerToken: 'token' }, 85 + serviceName: 'my-service', 86 + batchSize: 100, 87 + flushIntervalMs: 5000 88 + }) 89 + ``` 90 + 91 + ## Grafana Dashboard Queries 92 + 93 + ### Request Performance 94 + ```promql 95 + # Average response time by endpoint 96 + avg by (path) ( 97 + rate(http_request_duration_ms_sum[5m]) / 98 + rate(http_request_duration_ms_count[5m]) 99 + ) 100 + 101 + # Request rate 102 + sum(rate(http_requests_total[1m])) by (service) 103 + 104 + # Error rate 105 + sum(rate(errors_total[5m])) by (service) / 106 + sum(rate(http_requests_total[5m])) by (service) 107 + ``` 108 + 109 + ### Log Analysis 110 + ```logql 111 + # Recent errors 112 + {job="main-app"} |= "error" | json 113 + 114 + # Slow requests (>1s) 115 + {job="hosting-service"} |~ "duration.*[1-9][0-9]{3,}" 116 + 117 + # Failed OAuth attempts 118 + {job="main-app"} |= "OAuth" |= "failed" 119 + ``` 120 + 121 + ## Troubleshooting 122 + 123 + ### Logs not appearing 124 + - Check `GRAFANA_LOKI_URL` is correct (no trailing `/loki/api/v1/push`) 125 + - Verify authentication token/credentials 126 + - Look for connection errors in service logs 127 + 128 + ### Metrics missing 129 + - Ensure `GRAFANA_PROMETHEUS_URL` includes `/api/prom` suffix 130 + - Check firewall rules allow outbound HTTPS 131 + - Verify OpenTelemetry export errors in logs 132 + 133 + ### High memory usage 134 + - Reduce `GRAFANA_BATCH_SIZE` (default: 100) 135 + - Lower `GRAFANA_FLUSH_INTERVAL` to flush more frequently 136 + 137 + ## Local Development 138 + 139 + Metrics and logs are stored in-memory when Grafana isn't configured. Access them via: 140 + 141 + - `http://localhost:8000/api/observability/logs` 142 + - `http://localhost:8000/api/observability/metrics` 143 + - `http://localhost:8000/api/observability/errors` 144 + 145 + ## Testing Integration 146 + 147 + Run integration tests to verify setup: 148 + 149 + ```bash 150 + cd packages/@wisp/observability 151 + bun test src/integration-test.test.ts 152 + 153 + # Test with live Grafana 154 + GRAFANA_LOKI_URL=... GRAFANA_LOKI_USERNAME=... GRAFANA_LOKI_PASSWORD=... \ 155 + bun test src/integration-test.test.ts 156 + ```
+15 -15
docs/src/styles/custom.css
··· 5 5 /* Increase base font size by 10% */ 6 6 font-size: 110%; 7 7 8 - /* Light theme - Warm beige background from app */ 9 - --sl-color-bg: oklch(0.90 0.012 35); 10 - --sl-color-bg-sidebar: oklch(0.93 0.01 35); 11 - --sl-color-bg-nav: oklch(0.93 0.01 35); 12 - --sl-color-text: oklch(0.18 0.01 30); 13 - --sl-color-text-accent: oklch(0.78 0.15 345); 14 - --sl-color-accent: oklch(0.78 0.15 345); 15 - --sl-color-accent-low: oklch(0.95 0.03 345); 16 - --sl-color-border: oklch(0.75 0.015 30); 17 - --sl-color-gray-1: oklch(0.52 0.015 30); 18 - --sl-color-gray-2: oklch(0.42 0.015 30); 19 - --sl-color-gray-3: oklch(0.33 0.015 30); 20 - --sl-color-gray-4: oklch(0.25 0.015 30); 21 - --sl-color-gray-5: oklch(0.75 0.015 30); 8 + /* Light theme - Warm beige with improved contrast */ 9 + --sl-color-bg: oklch(0.92 0.012 35); 10 + --sl-color-bg-sidebar: oklch(0.95 0.008 35); 11 + --sl-color-bg-nav: oklch(0.95 0.008 35); 12 + --sl-color-text: oklch(0.15 0.015 30); 13 + --sl-color-text-accent: oklch(0.65 0.18 345); 14 + --sl-color-accent: oklch(0.65 0.18 345); 15 + --sl-color-accent-low: oklch(0.92 0.05 345); 16 + --sl-color-border: oklch(0.65 0.02 30); 17 + --sl-color-gray-1: oklch(0.45 0.02 30); 18 + --sl-color-gray-2: oklch(0.35 0.02 30); 19 + --sl-color-gray-3: oklch(0.28 0.02 30); 20 + --sl-color-gray-4: oklch(0.20 0.015 30); 21 + --sl-color-gray-5: oklch(0.65 0.02 30); 22 22 --sl-color-bg-accent: oklch(0.88 0.01 35); 23 23 } 24 24 ··· 70 70 /* Sidebar active/hover state text contrast fix */ 71 71 .sidebar a[aria-current="page"], 72 72 .sidebar a[aria-current="page"] span { 73 - color: oklch(0.23 0.015 285) !important; 73 + color: oklch(0.15 0.015 30) !important; 74 74 } 75 75 76 76 [data-theme="dark"] .sidebar a[aria-current="page"],
+11
package.json
··· 9 9 ], 10 10 "dependencies": { 11 11 "@tailwindcss/cli": "^4.1.17", 12 + "atproto-ui": "^0.12.0", 12 13 "bun-plugin-tailwind": "^0.1.2", 14 + "elysia": "^1.4.18", 13 15 "tailwindcss": "^4.1.17" 14 16 }, 15 17 "scripts": { ··· 19 21 "build": "cd apps/main-app && bun run build.ts", 20 22 "build:hosting": "cd apps/hosting-service && npm run build", 21 23 "build:all": "bun run build && npm run build:hosting", 24 + "check": "cd apps/main-app && npm run check && cd ../hosting-service && npm run check", 22 25 "screenshot": "bun run apps/main-app/scripts/screenshot-sites.ts", 23 26 "hosting:dev": "cd apps/hosting-service && npm run dev", 24 27 "hosting:start": "cd apps/hosting-service && npm run start" 28 + }, 29 + "trustedDependencies": [ 30 + "@parcel/watcher", 31 + "bun", 32 + "esbuild" 33 + ], 34 + "devDependencies": { 35 + "@types/bun": "^1.3.5" 25 36 } 26 37 }
+3
packages/@wisp/atproto-utils/package.json
··· 27 27 "@atproto/api": "^0.14.1", 28 28 "@wisp/lexicons": "workspace:*", 29 29 "multiformats": "^13.3.1" 30 + }, 31 + "devDependencies": { 32 + "@atproto/lexicon": "^0.5.2" 30 33 } 31 34 }
+1 -1
packages/@wisp/constants/src/index.ts
··· 14 14 15 15 // File size limits 16 16 export const MAX_SITE_SIZE = 300 * 1024 * 1024; // 300MB 17 - export const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB 17 + export const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB 18 18 export const MAX_FILE_COUNT = 1000; 19 19 20 20 // Cache configuration
+244
packages/@wisp/fs-utils/src/tree.test.ts
··· 1 + import { describe, test, expect } from 'bun:test' 2 + import { processUploadedFiles, type UploadedFile } from './tree' 3 + 4 + describe('processUploadedFiles', () => { 5 + test('should preserve nested directory structure', () => { 6 + const files: UploadedFile[] = [ 7 + { 8 + name: 'mysite/index.html', 9 + content: Buffer.from('<html>'), 10 + mimeType: 'text/html', 11 + size: 6 12 + }, 13 + { 14 + name: 'mysite/_astro/main.js', 15 + content: Buffer.from('console.log()'), 16 + mimeType: 'application/javascript', 17 + size: 13 18 + }, 19 + { 20 + name: 'mysite/_astro/styles.css', 21 + content: Buffer.from('body {}'), 22 + mimeType: 'text/css', 23 + size: 7 24 + }, 25 + { 26 + name: 'mysite/images/logo.png', 27 + content: Buffer.from([0x89, 0x50, 0x4e, 0x47]), 28 + mimeType: 'image/png', 29 + size: 4 30 + } 31 + ] 32 + 33 + const result = processUploadedFiles(files) 34 + 35 + expect(result.fileCount).toBe(4) 36 + expect(result.directory.entries).toHaveLength(3) // index.html, _astro/, images/ 37 + 38 + // Check _astro directory exists 39 + const astroEntry = result.directory.entries.find(e => e.name === '_astro') 40 + expect(astroEntry).toBeTruthy() 41 + expect('type' in astroEntry!.node && astroEntry!.node.type).toBe('directory') 42 + 43 + if ('entries' in astroEntry!.node) { 44 + const astroDir = astroEntry!.node 45 + expect(astroDir.entries).toHaveLength(2) // main.js, styles.css 46 + expect(astroDir.entries.find(e => e.name === 'main.js')).toBeTruthy() 47 + expect(astroDir.entries.find(e => e.name === 'styles.css')).toBeTruthy() 48 + } 49 + 50 + // Check images directory exists 51 + const imagesEntry = result.directory.entries.find(e => e.name === 'images') 52 + expect(imagesEntry).toBeTruthy() 53 + expect('type' in imagesEntry!.node && imagesEntry!.node.type).toBe('directory') 54 + 55 + if ('entries' in imagesEntry!.node) { 56 + const imagesDir = imagesEntry!.node 57 + expect(imagesDir.entries).toHaveLength(1) // logo.png 58 + expect(imagesDir.entries.find(e => e.name === 'logo.png')).toBeTruthy() 59 + } 60 + }) 61 + 62 + test('should handle deeply nested directories', () => { 63 + const files: UploadedFile[] = [ 64 + { 65 + name: 'site/a/b/c/d/deep.txt', 66 + content: Buffer.from('deep'), 67 + mimeType: 'text/plain', 68 + size: 4 69 + } 70 + ] 71 + 72 + const result = processUploadedFiles(files) 73 + 74 + expect(result.fileCount).toBe(1) 75 + 76 + // Navigate through nested structure 77 + const aEntry = result.directory.entries.find(e => e.name === 'a') 78 + expect(aEntry).toBeTruthy() 79 + expect('type' in aEntry!.node && aEntry!.node.type).toBe('directory') 80 + 81 + if ('entries' in aEntry!.node) { 82 + const bEntry = aEntry!.node.entries.find(e => e.name === 'b') 83 + expect(bEntry).toBeTruthy() 84 + expect('type' in bEntry!.node && bEntry!.node.type).toBe('directory') 85 + 86 + if ('entries' in bEntry!.node) { 87 + const cEntry = bEntry!.node.entries.find(e => e.name === 'c') 88 + expect(cEntry).toBeTruthy() 89 + expect('type' in cEntry!.node && cEntry!.node.type).toBe('directory') 90 + 91 + if ('entries' in cEntry!.node) { 92 + const dEntry = cEntry!.node.entries.find(e => e.name === 'd') 93 + expect(dEntry).toBeTruthy() 94 + expect('type' in dEntry!.node && dEntry!.node.type).toBe('directory') 95 + 96 + if ('entries' in dEntry!.node) { 97 + const fileEntry = dEntry!.node.entries.find(e => e.name === 'deep.txt') 98 + expect(fileEntry).toBeTruthy() 99 + expect('type' in fileEntry!.node && fileEntry!.node.type).toBe('file') 100 + } 101 + } 102 + } 103 + } 104 + }) 105 + 106 + test('should handle files at root level', () => { 107 + const files: UploadedFile[] = [ 108 + { 109 + name: 'mysite/index.html', 110 + content: Buffer.from('<html>'), 111 + mimeType: 'text/html', 112 + size: 6 113 + }, 114 + { 115 + name: 'mysite/robots.txt', 116 + content: Buffer.from('User-agent: *'), 117 + mimeType: 'text/plain', 118 + size: 13 119 + } 120 + ] 121 + 122 + const result = processUploadedFiles(files) 123 + 124 + expect(result.fileCount).toBe(2) 125 + expect(result.directory.entries).toHaveLength(2) 126 + expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 127 + expect(result.directory.entries.find(e => e.name === 'robots.txt')).toBeTruthy() 128 + }) 129 + 130 + test('should skip .git directories', () => { 131 + const files: UploadedFile[] = [ 132 + { 133 + name: 'mysite/index.html', 134 + content: Buffer.from('<html>'), 135 + mimeType: 'text/html', 136 + size: 6 137 + }, 138 + { 139 + name: 'mysite/.git/config', 140 + content: Buffer.from('[core]'), 141 + mimeType: 'text/plain', 142 + size: 6 143 + }, 144 + { 145 + name: 'mysite/.gitignore', 146 + content: Buffer.from('node_modules'), 147 + mimeType: 'text/plain', 148 + size: 12 149 + } 150 + ] 151 + 152 + const result = processUploadedFiles(files) 153 + 154 + expect(result.fileCount).toBe(2) // Only index.html and .gitignore 155 + expect(result.directory.entries).toHaveLength(2) 156 + expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 157 + expect(result.directory.entries.find(e => e.name === '.gitignore')).toBeTruthy() 158 + expect(result.directory.entries.find(e => e.name === '.git')).toBeFalsy() 159 + }) 160 + 161 + test('should handle mixed root and nested files', () => { 162 + const files: UploadedFile[] = [ 163 + { 164 + name: 'mysite/index.html', 165 + content: Buffer.from('<html>'), 166 + mimeType: 'text/html', 167 + size: 6 168 + }, 169 + { 170 + name: 'mysite/about/index.html', 171 + content: Buffer.from('<html>'), 172 + mimeType: 'text/html', 173 + size: 6 174 + }, 175 + { 176 + name: 'mysite/about/team.html', 177 + content: Buffer.from('<html>'), 178 + mimeType: 'text/html', 179 + size: 6 180 + }, 181 + { 182 + name: 'mysite/robots.txt', 183 + content: Buffer.from('User-agent: *'), 184 + mimeType: 'text/plain', 185 + size: 13 186 + } 187 + ] 188 + 189 + const result = processUploadedFiles(files) 190 + 191 + expect(result.fileCount).toBe(4) 192 + expect(result.directory.entries).toHaveLength(3) // index.html, about/, robots.txt 193 + 194 + const aboutEntry = result.directory.entries.find(e => e.name === 'about') 195 + expect(aboutEntry).toBeTruthy() 196 + expect('type' in aboutEntry!.node && aboutEntry!.node.type).toBe('directory') 197 + 198 + if ('entries' in aboutEntry!.node) { 199 + const aboutDir = aboutEntry!.node 200 + expect(aboutDir.entries).toHaveLength(2) // index.html, team.html 201 + expect(aboutDir.entries.find(e => e.name === 'index.html')).toBeTruthy() 202 + expect(aboutDir.entries.find(e => e.name === 'team.html')).toBeTruthy() 203 + } 204 + }) 205 + 206 + test('should handle empty file array', () => { 207 + const files: UploadedFile[] = [] 208 + 209 + const result = processUploadedFiles(files) 210 + 211 + expect(result.fileCount).toBe(0) 212 + expect(result.directory.entries).toHaveLength(0) 213 + }) 214 + 215 + test('should strip base folder name from paths', () => { 216 + // This tests the behavior where file.name includes the base folder 217 + // e.g., "mysite/index.html" should become "index.html" at root 218 + const files: UploadedFile[] = [ 219 + { 220 + name: 'build-output/index.html', 221 + content: Buffer.from('<html>'), 222 + mimeType: 'text/html', 223 + size: 6 224 + }, 225 + { 226 + name: 'build-output/assets/main.js', 227 + content: Buffer.from('console.log()'), 228 + mimeType: 'application/javascript', 229 + size: 13 230 + } 231 + ] 232 + 233 + const result = processUploadedFiles(files) 234 + 235 + expect(result.fileCount).toBe(2) 236 + 237 + // Should have index.html at root and assets/ directory 238 + expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 239 + expect(result.directory.entries.find(e => e.name === 'assets')).toBeTruthy() 240 + 241 + // Should NOT have 'build-output' directory 242 + expect(result.directory.entries.find(e => e.name === 'build-output')).toBeFalsy() 243 + }) 244 + })
+2 -1
packages/@wisp/lexicons/package.json
··· 39 39 "@atproto/xrpc-server": "^0.9.5" 40 40 }, 41 41 "devDependencies": { 42 - "@atproto/lex-cli": "^0.9.5" 42 + "@atproto/lex-cli": "^0.9.5", 43 + "multiformats": "^13.4.1" 43 44 } 44 45 }
+33
packages/@wisp/observability/.env.example
··· 1 + # Grafana Cloud Configuration for @wisp/observability 2 + # Copy this file to .env and fill in your actual values 3 + 4 + # ============================================================================ 5 + # Grafana Loki (for logs) 6 + # ============================================================================ 7 + GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net 8 + 9 + # Authentication Option 1: Bearer Token (Grafana Cloud) 10 + GRAFANA_LOKI_TOKEN=glc_xxx 11 + 12 + # Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 13 + # GRAFANA_LOKI_USERNAME=your-username 14 + # GRAFANA_LOKI_PASSWORD=your-password 15 + 16 + # ============================================================================ 17 + # Grafana Prometheus (for metrics) 18 + # ============================================================================ 19 + # Note: Add /api/prom to the base URL for OTLP export 20 + GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom 21 + 22 + # Authentication Option 1: Bearer Token (Grafana Cloud) 23 + GRAFANA_PROMETHEUS_TOKEN=glc_xxx 24 + 25 + # Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 26 + # GRAFANA_PROMETHEUS_USERNAME=your-username 27 + # GRAFANA_PROMETHEUS_PASSWORD=your-password 28 + 29 + # ============================================================================ 30 + # Optional: Override service metadata 31 + # ============================================================================ 32 + # SERVICE_NAME=wisp-app 33 + # SERVICE_VERSION=1.0.0
+217
packages/@wisp/observability/README.md
··· 1 + # @wisp/observability 2 + 3 + Framework-agnostic observability package with Grafana integration for logs and metrics persistence. 4 + 5 + ## Features 6 + 7 + - **In-memory storage** for local development 8 + - **Grafana Loki** integration for log persistence 9 + - **Prometheus/OTLP** integration for metrics 10 + - Framework middleware for Elysia and Hono 11 + - Automatic batching and buffering for efficient data transmission 12 + 13 + ## Installation 14 + 15 + ```bash 16 + bun add @wisp/observability 17 + ``` 18 + 19 + ## Basic Usage 20 + 21 + ### Without Grafana (In-Memory Only) 22 + 23 + ```typescript 24 + import { createLogger, metricsCollector } from '@wisp/observability' 25 + 26 + const logger = createLogger('my-service') 27 + 28 + // Log messages 29 + logger.info('Server started') 30 + logger.error('Failed to connect', new Error('Connection refused')) 31 + 32 + // Record metrics 33 + metricsCollector.recordRequest('/api/users', 'GET', 200, 45, 'my-service') 34 + ``` 35 + 36 + ### With Grafana Integration 37 + 38 + ```typescript 39 + import { initializeGrafanaExporters, createLogger } from '@wisp/observability' 40 + 41 + // Initialize at application startup 42 + initializeGrafanaExporters({ 43 + lokiUrl: 'https://logs-prod.grafana.net', 44 + lokiAuth: { 45 + bearerToken: 'your-loki-api-key' 46 + }, 47 + prometheusUrl: 'https://prometheus-prod.grafana.net', 48 + prometheusAuth: { 49 + bearerToken: 'your-prometheus-api-key' 50 + }, 51 + serviceName: 'wisp-app', 52 + serviceVersion: '1.0.0', 53 + batchSize: 100, 54 + flushIntervalMs: 5000 55 + }) 56 + 57 + // Now all logs and metrics will be sent to Grafana automatically 58 + const logger = createLogger('my-service') 59 + logger.info('This will be sent to Grafana Loki') 60 + ``` 61 + 62 + ## Configuration 63 + 64 + ### Environment Variables 65 + 66 + You can configure Grafana integration using environment variables: 67 + 68 + ```bash 69 + # Loki configuration 70 + GRAFANA_LOKI_URL=https://logs-prod.grafana.net 71 + 72 + # Authentication Option 1: Bearer Token (Grafana Cloud) 73 + GRAFANA_LOKI_TOKEN=your-loki-api-key 74 + 75 + # Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 76 + GRAFANA_LOKI_USERNAME=your-username 77 + GRAFANA_LOKI_PASSWORD=your-password 78 + 79 + # Prometheus configuration 80 + GRAFANA_PROMETHEUS_URL=https://prometheus-prod.grafana.net/api/prom 81 + 82 + # Authentication Option 1: Bearer Token (Grafana Cloud) 83 + GRAFANA_PROMETHEUS_TOKEN=your-prometheus-api-key 84 + 85 + # Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 86 + GRAFANA_PROMETHEUS_USERNAME=your-username 87 + GRAFANA_PROMETHEUS_PASSWORD=your-password 88 + ``` 89 + 90 + ### Programmatic Configuration 91 + 92 + ```typescript 93 + import { initializeGrafanaExporters } from '@wisp/observability' 94 + 95 + initializeGrafanaExporters({ 96 + // Loki configuration for logs 97 + lokiUrl: 'https://logs-prod.grafana.net', 98 + lokiAuth: { 99 + // Option 1: Bearer token (recommended for Grafana Cloud) 100 + bearerToken: 'your-api-key', 101 + 102 + // Option 2: Basic auth 103 + username: 'your-username', 104 + password: 'your-password' 105 + }, 106 + 107 + // Prometheus/OTLP configuration for metrics 108 + prometheusUrl: 'https://prometheus-prod.grafana.net', 109 + prometheusAuth: { 110 + bearerToken: 'your-api-key' 111 + }, 112 + 113 + // Service metadata 114 + serviceName: 'wisp-app', 115 + serviceVersion: '1.0.0', 116 + 117 + // Batching configuration 118 + batchSize: 100, // Flush after this many entries 119 + flushIntervalMs: 5000, // Flush every 5 seconds 120 + 121 + // Enable/disable exporters 122 + enabled: true 123 + }) 124 + ``` 125 + 126 + ## Middleware Integration 127 + 128 + ### Elysia 129 + 130 + ```typescript 131 + import { Elysia } from 'elysia' 132 + import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 133 + import { initializeGrafanaExporters } from '@wisp/observability' 134 + 135 + // Initialize Grafana exporters 136 + initializeGrafanaExporters({ 137 + lokiUrl: process.env.GRAFANA_LOKI_URL, 138 + lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN } 139 + }) 140 + 141 + const app = new Elysia() 142 + .use(observabilityMiddleware({ service: 'main-app' })) 143 + .get('/', () => 'Hello World') 144 + .listen(3000) 145 + ``` 146 + 147 + ### Hono 148 + 149 + ```typescript 150 + import { Hono } from 'hono' 151 + import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono' 152 + import { initializeGrafanaExporters } from '@wisp/observability' 153 + 154 + // Initialize Grafana exporters 155 + initializeGrafanaExporters({ 156 + lokiUrl: process.env.GRAFANA_LOKI_URL, 157 + lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN } 158 + }) 159 + 160 + const app = new Hono() 161 + app.use('*', observabilityMiddleware({ service: 'hosting-service' })) 162 + app.onError(observabilityErrorHandler({ service: 'hosting-service' })) 163 + ``` 164 + 165 + ## Grafana Cloud Setup 166 + 167 + 1. **Create a Grafana Cloud account** at https://grafana.com/ 168 + 169 + 2. **Get your Loki credentials:** 170 + - Go to your Grafana Cloud portal 171 + - Navigate to "Loki" โ†’ "Details" 172 + - Copy the Push endpoint URL and create an API key 173 + 174 + 3. **Get your Prometheus credentials:** 175 + - Navigate to "Prometheus" โ†’ "Details" 176 + - Copy the Remote Write endpoint and create an API key 177 + 178 + 4. **Configure your application:** 179 + ```typescript 180 + initializeGrafanaExporters({ 181 + lokiUrl: 'https://logs-prod-xxx.grafana.net', 182 + lokiAuth: { bearerToken: 'glc_xxx' }, 183 + prometheusUrl: 'https://prometheus-prod-xxx.grafana.net/api/prom', 184 + prometheusAuth: { bearerToken: 'glc_xxx' } 185 + }) 186 + ``` 187 + 188 + ## Data Flow 189 + 190 + 1. **Logs** โ†’ Buffered โ†’ Batched โ†’ Sent to Grafana Loki 191 + 2. **Metrics** โ†’ Aggregated โ†’ Exported via OTLP โ†’ Sent to Prometheus 192 + 3. **Errors** โ†’ Deduplicated โ†’ Sent to Loki with error tag 193 + 194 + ## Performance Considerations 195 + 196 + - Logs and metrics are batched to reduce network overhead 197 + - Default batch size: 100 entries 198 + - Default flush interval: 5 seconds 199 + - Failed exports are logged but don't block application 200 + - In-memory buffers are capped to prevent memory leaks 201 + 202 + ## Graceful Shutdown 203 + 204 + The exporters automatically register shutdown handlers: 205 + 206 + ```typescript 207 + import { shutdownGrafanaExporters } from '@wisp/observability' 208 + 209 + // Manual shutdown if needed 210 + process.on('beforeExit', async () => { 211 + await shutdownGrafanaExporters() 212 + }) 213 + ``` 214 + 215 + ## License 216 + 217 + MIT
+13 -1
packages/@wisp/observability/package.json
··· 24 24 } 25 25 }, 26 26 "peerDependencies": { 27 - "hono": "^4.0.0" 27 + "hono": "^4.10.7" 28 28 }, 29 29 "peerDependenciesMeta": { 30 30 "hono": { 31 31 "optional": true 32 32 } 33 + }, 34 + "dependencies": { 35 + "@opentelemetry/api": "^1.9.0", 36 + "@opentelemetry/sdk-metrics": "^1.29.0", 37 + "@opentelemetry/exporter-metrics-otlp-http": "^0.56.0", 38 + "@opentelemetry/resources": "^1.29.0", 39 + "@opentelemetry/semantic-conventions": "^1.29.0" 40 + }, 41 + "devDependencies": { 42 + "@hono/node-server": "^1.19.6", 43 + "bun-types": "^1.3.3", 44 + "typescript": "^5.9.3" 33 45 } 34 46 }
+12 -2
packages/@wisp/observability/src/core.ts
··· 3 3 * Framework-agnostic logging, error tracking, and metrics collection 4 4 */ 5 5 6 + import { lokiExporter, metricsExporter } from './exporters' 7 + 6 8 // ============================================================================ 7 9 // Types 8 10 // ============================================================================ ··· 128 130 logs.splice(MAX_LOGS) 129 131 } 130 132 133 + // Send to Loki exporter 134 + lokiExporter.pushLog(entry) 135 + 131 136 // Also log to console for compatibility 132 137 const contextStr = context ? ` ${JSON.stringify(context)}` : '' 133 138 const traceStr = traceId ? ` [trace:${traceId}]` : '' ··· 163 168 }, 164 169 165 170 debug(message: string, service: string, context?: Record<string, any>, traceId?: string) { 166 - const env = typeof Bun !== 'undefined' ? Bun.env.NODE_ENV : process.env.NODE_ENV; 167 - if (env !== 'production') { 171 + if (process.env.NODE_ENV !== 'production') { 168 172 this.log('debug', message, service, context, traceId) 169 173 } 170 174 }, ··· 233 237 234 238 errors.set(key, entry) 235 239 240 + // Send to Loki exporter 241 + lokiExporter.pushError(entry) 242 + 236 243 // Rotate if needed 237 244 if (errors.size > MAX_ERRORS) { 238 245 const oldest = Array.from(errors.keys())[0] ··· 284 291 } 285 292 286 293 metrics.unshift(entry) 294 + 295 + // Send to Prometheus/OTLP exporter 296 + metricsExporter.recordMetric(entry) 287 297 288 298 // Rotate if needed 289 299 if (metrics.length > MAX_METRICS) {
+433
packages/@wisp/observability/src/exporters.ts
··· 1 + /** 2 + * Grafana exporters for logs and metrics 3 + * Integrates with Grafana Loki for logs and Prometheus/OTLP for metrics 4 + */ 5 + 6 + import { LogEntry, ErrorEntry, MetricEntry } from './core' 7 + import { metrics, MeterProvider } from '@opentelemetry/api' 8 + import { MeterProvider as SdkMeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' 9 + import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http' 10 + import { Resource } from '@opentelemetry/resources' 11 + import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions' 12 + 13 + // ============================================================================ 14 + // Types 15 + // ============================================================================ 16 + 17 + export interface GrafanaConfig { 18 + lokiUrl?: string 19 + lokiAuth?: { 20 + username?: string 21 + password?: string 22 + bearerToken?: string 23 + } 24 + prometheusUrl?: string 25 + prometheusAuth?: { 26 + username?: string 27 + password?: string 28 + bearerToken?: string 29 + } 30 + serviceName?: string 31 + serviceVersion?: string 32 + batchSize?: number 33 + flushIntervalMs?: number 34 + enabled?: boolean 35 + } 36 + 37 + interface LokiStream { 38 + stream: Record<string, string> 39 + values: Array<[string, string]> 40 + } 41 + 42 + interface LokiBatch { 43 + streams: LokiStream[] 44 + } 45 + 46 + // ============================================================================ 47 + // Configuration 48 + // ============================================================================ 49 + 50 + class GrafanaExporterConfig { 51 + private config: GrafanaConfig = { 52 + enabled: false, 53 + batchSize: 100, 54 + flushIntervalMs: 5000, 55 + serviceName: 'wisp-app', 56 + serviceVersion: '1.0.0' 57 + } 58 + 59 + initialize(config: GrafanaConfig) { 60 + this.config = { ...this.config, ...config } 61 + 62 + // Load from environment variables if not provided 63 + if (!this.config.lokiUrl) { 64 + this.config.lokiUrl = process.env.GRAFANA_LOKI_URL 65 + } 66 + 67 + if (!this.config.prometheusUrl) { 68 + this.config.prometheusUrl = process.env.GRAFANA_PROMETHEUS_URL 69 + } 70 + 71 + // Load Loki authentication from environment 72 + if (!this.config.lokiAuth?.bearerToken && !this.config.lokiAuth?.username) { 73 + const token = process.env.GRAFANA_LOKI_TOKEN 74 + const username = process.env.GRAFANA_LOKI_USERNAME 75 + const password = process.env.GRAFANA_LOKI_PASSWORD 76 + 77 + if (token) { 78 + this.config.lokiAuth = { ...this.config.lokiAuth, bearerToken: token } 79 + } else if (username && password) { 80 + this.config.lokiAuth = { ...this.config.lokiAuth, username, password } 81 + } 82 + } 83 + 84 + // Load Prometheus authentication from environment 85 + if (!this.config.prometheusAuth?.bearerToken && !this.config.prometheusAuth?.username) { 86 + const token = process.env.GRAFANA_PROMETHEUS_TOKEN 87 + const username = process.env.GRAFANA_PROMETHEUS_USERNAME 88 + const password = process.env.GRAFANA_PROMETHEUS_PASSWORD 89 + 90 + if (token) { 91 + this.config.prometheusAuth = { ...this.config.prometheusAuth, bearerToken: token } 92 + } else if (username && password) { 93 + this.config.prometheusAuth = { ...this.config.prometheusAuth, username, password } 94 + } 95 + } 96 + 97 + // Enable if URLs are configured 98 + if (this.config.lokiUrl || this.config.prometheusUrl) { 99 + this.config.enabled = true 100 + } 101 + 102 + return this 103 + } 104 + 105 + getConfig(): GrafanaConfig { 106 + return { ...this.config } 107 + } 108 + 109 + isEnabled(): boolean { 110 + return this.config.enabled === true 111 + } 112 + } 113 + 114 + export const grafanaConfig = new GrafanaExporterConfig() 115 + 116 + // ============================================================================ 117 + // Loki Exporter for Logs 118 + // ============================================================================ 119 + 120 + class LokiExporter { 121 + private buffer: LogEntry[] = [] 122 + private errorBuffer: ErrorEntry[] = [] 123 + private flushTimer?: NodeJS.Timeout 124 + private config: GrafanaConfig = {} 125 + 126 + initialize(config: GrafanaConfig) { 127 + this.config = config 128 + 129 + if (this.config.enabled && this.config.lokiUrl) { 130 + this.startBatching() 131 + } 132 + } 133 + 134 + private startBatching() { 135 + const interval = this.config.flushIntervalMs || 5000 136 + 137 + this.flushTimer = setInterval(() => { 138 + this.flush() 139 + }, interval) 140 + } 141 + 142 + stop() { 143 + if (this.flushTimer) { 144 + clearInterval(this.flushTimer) 145 + this.flushTimer = undefined 146 + } 147 + // Final flush 148 + this.flush() 149 + } 150 + 151 + pushLog(entry: LogEntry) { 152 + if (!this.config.enabled || !this.config.lokiUrl) return 153 + 154 + this.buffer.push(entry) 155 + 156 + const batchSize = this.config.batchSize || 100 157 + if (this.buffer.length >= batchSize) { 158 + this.flush() 159 + } 160 + } 161 + 162 + pushError(entry: ErrorEntry) { 163 + if (!this.config.enabled || !this.config.lokiUrl) return 164 + 165 + this.errorBuffer.push(entry) 166 + 167 + const batchSize = this.config.batchSize || 100 168 + if (this.errorBuffer.length >= batchSize) { 169 + this.flush() 170 + } 171 + } 172 + 173 + private async flush() { 174 + if (!this.config.lokiUrl) return 175 + 176 + const logsToSend = [...this.buffer] 177 + const errorsToSend = [...this.errorBuffer] 178 + 179 + this.buffer = [] 180 + this.errorBuffer = [] 181 + 182 + if (logsToSend.length === 0 && errorsToSend.length === 0) return 183 + 184 + try { 185 + const batch = this.createLokiBatch(logsToSend, errorsToSend) 186 + await this.sendToLoki(batch) 187 + } catch (error) { 188 + console.error('[LokiExporter] Failed to send logs to Loki:', error) 189 + // Optionally re-queue failed logs 190 + } 191 + } 192 + 193 + private createLokiBatch(logs: LogEntry[], errors: ErrorEntry[]): LokiBatch { 194 + const streams: LokiStream[] = [] 195 + 196 + // Group logs by service and level 197 + const logGroups = new Map<string, LogEntry[]>() 198 + 199 + for (const log of logs) { 200 + const key = `${log.service}-${log.level}` 201 + const group = logGroups.get(key) || [] 202 + group.push(log) 203 + logGroups.set(key, group) 204 + } 205 + 206 + // Create streams for logs 207 + for (const [key, entries] of logGroups) { 208 + const [service, level] = key.split('-') 209 + const values: Array<[string, string]> = entries.map(entry => { 210 + const logLine = JSON.stringify({ 211 + message: entry.message, 212 + context: entry.context, 213 + traceId: entry.traceId, 214 + eventType: entry.eventType 215 + }) 216 + 217 + // Loki expects nanosecond timestamp as string 218 + const nanoTimestamp = String(entry.timestamp.getTime() * 1000000) 219 + return [nanoTimestamp, logLine] 220 + }) 221 + 222 + streams.push({ 223 + stream: { 224 + service: service || 'unknown', 225 + level: level || 'info', 226 + job: this.config.serviceName || 'wisp-app' 227 + }, 228 + values 229 + }) 230 + } 231 + 232 + // Create streams for errors 233 + if (errors.length > 0) { 234 + const errorValues: Array<[string, string]> = errors.map(entry => { 235 + const logLine = JSON.stringify({ 236 + message: entry.message, 237 + stack: entry.stack, 238 + context: entry.context, 239 + count: entry.count 240 + }) 241 + 242 + const nanoTimestamp = String(entry.timestamp.getTime() * 1000000) 243 + return [nanoTimestamp, logLine] 244 + }) 245 + 246 + streams.push({ 247 + stream: { 248 + service: errors[0]?.service || 'unknown', 249 + level: 'error', 250 + job: this.config.serviceName || 'wisp-app', 251 + type: 'aggregated_error' 252 + }, 253 + values: errorValues 254 + }) 255 + } 256 + 257 + return { streams } 258 + } 259 + 260 + private async sendToLoki(batch: LokiBatch) { 261 + if (!this.config.lokiUrl) return 262 + 263 + const headers: Record<string, string> = { 264 + 'Content-Type': 'application/json' 265 + } 266 + 267 + // Add authentication 268 + if (this.config.lokiAuth?.bearerToken) { 269 + headers['Authorization'] = `Bearer ${this.config.lokiAuth.bearerToken}` 270 + } else if (this.config.lokiAuth?.username && this.config.lokiAuth?.password) { 271 + const auth = Buffer.from(`${this.config.lokiAuth.username}:${this.config.lokiAuth.password}`).toString('base64') 272 + headers['Authorization'] = `Basic ${auth}` 273 + } 274 + 275 + const response = await fetch(`${this.config.lokiUrl}/loki/api/v1/push`, { 276 + method: 'POST', 277 + headers, 278 + body: JSON.stringify(batch) 279 + }) 280 + 281 + if (!response.ok) { 282 + const text = await response.text() 283 + throw new Error(`Loki push failed: ${response.status} - ${text}`) 284 + } 285 + } 286 + } 287 + 288 + // ============================================================================ 289 + // OpenTelemetry Metrics Exporter 290 + // ============================================================================ 291 + 292 + class MetricsExporter { 293 + private meterProvider?: MeterProvider 294 + private requestCounter?: any 295 + private requestDuration?: any 296 + private errorCounter?: any 297 + private config: GrafanaConfig = {} 298 + 299 + initialize(config: GrafanaConfig) { 300 + this.config = config 301 + 302 + if (!this.config.enabled || !this.config.prometheusUrl) return 303 + 304 + // Create OTLP exporter with Prometheus endpoint 305 + const exporter = new OTLPMetricExporter({ 306 + url: `${this.config.prometheusUrl}/v1/metrics`, 307 + headers: this.getAuthHeaders(), 308 + timeoutMillis: 10000 309 + }) 310 + 311 + // Create meter provider with periodic exporting 312 + const meterProvider = new SdkMeterProvider({ 313 + resource: new Resource({ 314 + [ATTR_SERVICE_NAME]: this.config.serviceName || 'wisp-app', 315 + [ATTR_SERVICE_VERSION]: this.config.serviceVersion || '1.0.0' 316 + }), 317 + readers: [ 318 + new PeriodicExportingMetricReader({ 319 + exporter, 320 + exportIntervalMillis: this.config.flushIntervalMs || 5000 321 + }) 322 + ] 323 + }) 324 + 325 + // Set global meter provider 326 + metrics.setGlobalMeterProvider(meterProvider) 327 + this.meterProvider = meterProvider 328 + 329 + // Create metrics instruments 330 + const meter = metrics.getMeter(this.config.serviceName || 'wisp-app') 331 + 332 + this.requestCounter = meter.createCounter('http_requests_total', { 333 + description: 'Total number of HTTP requests' 334 + }) 335 + 336 + this.requestDuration = meter.createHistogram('http_request_duration_ms', { 337 + description: 'HTTP request duration in milliseconds', 338 + unit: 'ms' 339 + }) 340 + 341 + this.errorCounter = meter.createCounter('errors_total', { 342 + description: 'Total number of errors' 343 + }) 344 + } 345 + 346 + private getAuthHeaders(): Record<string, string> { 347 + const headers: Record<string, string> = {} 348 + 349 + if (this.config.prometheusAuth?.bearerToken) { 350 + headers['Authorization'] = `Bearer ${this.config.prometheusAuth.bearerToken}` 351 + } else if (this.config.prometheusAuth?.username && this.config.prometheusAuth?.password) { 352 + const auth = Buffer.from(`${this.config.prometheusAuth.username}:${this.config.prometheusAuth.password}`).toString('base64') 353 + headers['Authorization'] = `Basic ${auth}` 354 + } 355 + 356 + return headers 357 + } 358 + 359 + recordMetric(entry: MetricEntry) { 360 + if (!this.config.enabled) return 361 + 362 + const attributes = { 363 + method: entry.method, 364 + path: entry.path, 365 + status: String(entry.statusCode), 366 + service: entry.service 367 + } 368 + 369 + // Record request count 370 + this.requestCounter?.add(1, attributes) 371 + 372 + // Record request duration 373 + this.requestDuration?.record(entry.duration, attributes) 374 + 375 + // Record errors 376 + if (entry.statusCode >= 400) { 377 + this.errorCounter?.add(1, attributes) 378 + } 379 + } 380 + 381 + async shutdown() { 382 + if (this.meterProvider && 'shutdown' in this.meterProvider) { 383 + await (this.meterProvider as SdkMeterProvider).shutdown() 384 + } 385 + } 386 + } 387 + 388 + // ============================================================================ 389 + // Singleton Instances 390 + // ============================================================================ 391 + 392 + export const lokiExporter = new LokiExporter() 393 + export const metricsExporter = new MetricsExporter() 394 + 395 + // ============================================================================ 396 + // Initialization 397 + // ============================================================================ 398 + 399 + export function initializeGrafanaExporters(config?: GrafanaConfig) { 400 + const finalConfig = grafanaConfig.initialize(config || {}).getConfig() 401 + 402 + if (finalConfig.enabled) { 403 + console.log('[Observability] Initializing Grafana exporters', { 404 + lokiEnabled: !!finalConfig.lokiUrl, 405 + prometheusEnabled: !!finalConfig.prometheusUrl, 406 + serviceName: finalConfig.serviceName 407 + }) 408 + 409 + lokiExporter.initialize(finalConfig) 410 + metricsExporter.initialize(finalConfig) 411 + } 412 + 413 + return { 414 + lokiExporter, 415 + metricsExporter, 416 + config: finalConfig 417 + } 418 + } 419 + 420 + // ============================================================================ 421 + // Cleanup 422 + // ============================================================================ 423 + 424 + export async function shutdownGrafanaExporters() { 425 + lokiExporter.stop() 426 + await metricsExporter.shutdown() 427 + } 428 + 429 + // Graceful shutdown handlers 430 + if (typeof process !== 'undefined') { 431 + process.on('SIGTERM', shutdownGrafanaExporters) 432 + process.on('SIGINT', shutdownGrafanaExporters) 433 + }
+8
packages/@wisp/observability/src/index.ts
··· 6 6 // Export everything from core 7 7 export * from './core' 8 8 9 + // Export Grafana integration 10 + export { 11 + initializeGrafanaExporters, 12 + shutdownGrafanaExporters, 13 + grafanaConfig, 14 + type GrafanaConfig 15 + } from './exporters' 16 + 9 17 // Note: Middleware should be imported from specific subpaths: 10 18 // - import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 11 19 // - import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono'
+336
packages/@wisp/observability/src/integration-test.test.ts
··· 1 + /** 2 + * Integration tests for Grafana exporters 3 + * Tests both mock server and live server connections 4 + */ 5 + 6 + import { describe, test, expect, beforeAll, afterAll } from 'bun:test' 7 + import { createLogger, metricsCollector, initializeGrafanaExporters, shutdownGrafanaExporters } from './index' 8 + import { Hono } from 'hono' 9 + import { serve } from '@hono/node-server' 10 + import type { ServerType } from '@hono/node-server' 11 + 12 + // ============================================================================ 13 + // Mock Grafana Server 14 + // ============================================================================ 15 + 16 + interface MockRequest { 17 + method: string 18 + path: string 19 + headers: Record<string, string> 20 + body: any 21 + } 22 + 23 + class MockGrafanaServer { 24 + private app: Hono 25 + private server?: ServerType 26 + private port: number 27 + public requests: MockRequest[] = [] 28 + 29 + constructor(port: number) { 30 + this.port = port 31 + this.app = new Hono() 32 + 33 + // Mock Loki endpoint 34 + this.app.post('/loki/api/v1/push', async (c) => { 35 + const body = await c.req.json() 36 + this.requests.push({ 37 + method: 'POST', 38 + path: '/loki/api/v1/push', 39 + headers: Object.fromEntries(c.req.raw.headers.entries()), 40 + body 41 + }) 42 + return c.json({ status: 'success' }) 43 + }) 44 + 45 + // Mock Prometheus/OTLP endpoint 46 + this.app.post('/v1/metrics', async (c) => { 47 + const body = await c.req.json() 48 + this.requests.push({ 49 + method: 'POST', 50 + path: '/v1/metrics', 51 + headers: Object.fromEntries(c.req.raw.headers.entries()), 52 + body 53 + }) 54 + return c.json({ status: 'success' }) 55 + }) 56 + 57 + // Health check 58 + this.app.get('/health', (c) => c.json({ status: 'ok' })) 59 + } 60 + 61 + async start() { 62 + this.server = serve({ 63 + fetch: this.app.fetch, 64 + port: this.port 65 + }) 66 + // Wait a bit for server to be ready 67 + await new Promise(resolve => setTimeout(resolve, 100)) 68 + } 69 + 70 + async stop() { 71 + if (this.server) { 72 + this.server.close() 73 + this.server = undefined 74 + } 75 + } 76 + 77 + clearRequests() { 78 + this.requests = [] 79 + } 80 + 81 + getRequestsByPath(path: string): MockRequest[] { 82 + return this.requests.filter(r => r.path === path) 83 + } 84 + 85 + async waitForRequests(count: number, timeoutMs: number = 10000): Promise<boolean> { 86 + const startTime = Date.now() 87 + while (this.requests.length < count) { 88 + if (Date.now() - startTime > timeoutMs) { 89 + return false 90 + } 91 + await new Promise(resolve => setTimeout(resolve, 100)) 92 + } 93 + return true 94 + } 95 + } 96 + 97 + // ============================================================================ 98 + // Test Suite 99 + // ============================================================================ 100 + 101 + describe('Grafana Integration', () => { 102 + const mockServer = new MockGrafanaServer(9999) 103 + const mockUrl = 'http://localhost:9999' 104 + 105 + beforeAll(async () => { 106 + await mockServer.start() 107 + }) 108 + 109 + afterAll(async () => { 110 + await mockServer.stop() 111 + await shutdownGrafanaExporters() 112 + }) 113 + 114 + test('should initialize with username/password auth', () => { 115 + const config = initializeGrafanaExporters({ 116 + lokiUrl: mockUrl, 117 + lokiAuth: { 118 + username: 'testuser', 119 + password: 'testpass' 120 + }, 121 + prometheusUrl: mockUrl, 122 + prometheusAuth: { 123 + username: 'testuser', 124 + password: 'testpass' 125 + }, 126 + serviceName: 'test-service', 127 + batchSize: 5, 128 + flushIntervalMs: 1000 129 + }) 130 + 131 + expect(config.config.enabled).toBe(true) 132 + expect(config.config.lokiUrl).toBe(mockUrl) 133 + expect(config.config.prometheusUrl).toBe(mockUrl) 134 + expect(config.config.lokiAuth?.username).toBe('testuser') 135 + expect(config.config.prometheusAuth?.username).toBe('testuser') 136 + }) 137 + 138 + test('should send logs to Loki with basic auth', async () => { 139 + mockServer.clearRequests() 140 + 141 + // Initialize with username/password 142 + initializeGrafanaExporters({ 143 + lokiUrl: mockUrl, 144 + lokiAuth: { 145 + username: 'testuser', 146 + password: 'testpass' 147 + }, 148 + serviceName: 'test-logs', 149 + batchSize: 2, 150 + flushIntervalMs: 500 151 + }) 152 + 153 + const logger = createLogger('test-logs') 154 + 155 + // Generate logs that will trigger batch flush 156 + logger.info('Test message 1') 157 + logger.warn('Test message 2') 158 + 159 + // Wait for batch to be sent 160 + const success = await mockServer.waitForRequests(1, 5000) 161 + expect(success).toBe(true) 162 + 163 + const lokiRequests = mockServer.getRequestsByPath('/loki/api/v1/push') 164 + expect(lokiRequests.length).toBeGreaterThanOrEqual(1) 165 + 166 + const lastRequest = lokiRequests[lokiRequests.length - 1]! 167 + 168 + // Verify basic auth header 169 + expect(lastRequest.headers['authorization']).toMatch(/^Basic /) 170 + 171 + // Verify Loki batch format 172 + expect(lastRequest.body).toHaveProperty('streams') 173 + expect(Array.isArray(lastRequest.body.streams)).toBe(true) 174 + expect(lastRequest.body.streams.length).toBeGreaterThan(0) 175 + 176 + const stream = lastRequest.body.streams[0]! 177 + expect(stream).toHaveProperty('stream') 178 + expect(stream).toHaveProperty('values') 179 + expect(stream.stream.job).toBe('test-logs') 180 + 181 + await shutdownGrafanaExporters() 182 + }) 183 + 184 + test('should send metrics to Prometheus with bearer token', async () => { 185 + mockServer.clearRequests() 186 + 187 + // Initialize with bearer token only for Prometheus (no Loki) 188 + initializeGrafanaExporters({ 189 + lokiUrl: undefined, // Explicitly disable Loki 190 + prometheusUrl: mockUrl, 191 + prometheusAuth: { 192 + bearerToken: 'test-token-123' 193 + }, 194 + serviceName: 'test-metrics', 195 + flushIntervalMs: 1000 196 + }) 197 + 198 + // Generate metrics 199 + for (let i = 0; i < 5; i++) { 200 + metricsCollector.recordRequest('/api/test', 'GET', 200, 100 + i, 'test-metrics') 201 + } 202 + 203 + // Wait for metrics to be exported 204 + await new Promise(resolve => setTimeout(resolve, 2000)) 205 + 206 + const prometheusRequests = mockServer.getRequestsByPath('/v1/metrics') 207 + expect(prometheusRequests.length).toBeGreaterThan(0) 208 + 209 + // Note: Due to singleton exporters, we may see auth from previous test 210 + // The key thing is that metrics are being sent 211 + const lastRequest = prometheusRequests[prometheusRequests.length - 1]! 212 + expect(lastRequest.headers['authorization']).toBeTruthy() 213 + 214 + await shutdownGrafanaExporters() 215 + }) 216 + 217 + test('should handle errors gracefully', async () => { 218 + // Initialize with invalid URL 219 + const config = initializeGrafanaExporters({ 220 + lokiUrl: 'http://localhost:9998', // Non-existent server 221 + lokiAuth: { 222 + username: 'test', 223 + password: 'test' 224 + }, 225 + serviceName: 'test-error', 226 + batchSize: 1, 227 + flushIntervalMs: 500 228 + }) 229 + 230 + expect(config.config.enabled).toBe(true) 231 + 232 + const logger = createLogger('test-error') 233 + 234 + // This should not throw even though server doesn't exist 235 + logger.info('This should not crash') 236 + 237 + // Wait for flush attempt 238 + await new Promise(resolve => setTimeout(resolve, 1000)) 239 + 240 + // If we got here, error handling worked 241 + expect(true).toBe(true) 242 + 243 + await shutdownGrafanaExporters() 244 + }) 245 + }) 246 + 247 + // ============================================================================ 248 + // Live Server Connection Tests (Optional) 249 + // ============================================================================ 250 + 251 + describe('Live Grafana Connection (Optional)', () => { 252 + const hasLiveConfig = Boolean( 253 + process.env.GRAFANA_LOKI_URL && 254 + (process.env.GRAFANA_LOKI_TOKEN || 255 + (process.env.GRAFANA_LOKI_USERNAME && process.env.GRAFANA_LOKI_PASSWORD)) 256 + ) 257 + 258 + test.skipIf(!hasLiveConfig)('should connect to live Loki server', async () => { 259 + const config = initializeGrafanaExporters({ 260 + serviceName: 'test-live-loki', 261 + serviceVersion: '1.0.0-test', 262 + batchSize: 5, 263 + flushIntervalMs: 2000 264 + }) 265 + 266 + expect(config.config.enabled).toBe(true) 267 + expect(config.config.lokiUrl).toBeTruthy() 268 + 269 + const logger = createLogger('test-live-loki') 270 + 271 + // Send test logs 272 + logger.info('Live connection test log', { test: true, timestamp: Date.now() }) 273 + logger.warn('Test warning from integration test') 274 + logger.error('Test error (ignore)', new Error('Test error'), { safe: true }) 275 + 276 + // Wait for flush 277 + await new Promise(resolve => setTimeout(resolve, 3000)) 278 + 279 + // If we got here without errors, connection worked 280 + expect(true).toBe(true) 281 + 282 + await shutdownGrafanaExporters() 283 + }) 284 + 285 + test.skipIf(!hasLiveConfig)('should connect to live Prometheus server', async () => { 286 + const hasPrometheusConfig = Boolean( 287 + process.env.GRAFANA_PROMETHEUS_URL && 288 + (process.env.GRAFANA_PROMETHEUS_TOKEN || 289 + (process.env.GRAFANA_PROMETHEUS_USERNAME && process.env.GRAFANA_PROMETHEUS_PASSWORD)) 290 + ) 291 + 292 + if (!hasPrometheusConfig) { 293 + console.log('Skipping Prometheus test - no config provided') 294 + return 295 + } 296 + 297 + const config = initializeGrafanaExporters({ 298 + serviceName: 'test-live-prometheus', 299 + serviceVersion: '1.0.0-test', 300 + flushIntervalMs: 2000 301 + }) 302 + 303 + expect(config.config.enabled).toBe(true) 304 + expect(config.config.prometheusUrl).toBeTruthy() 305 + 306 + // Generate test metrics 307 + for (let i = 0; i < 10; i++) { 308 + metricsCollector.recordRequest( 309 + '/test/endpoint', 310 + 'GET', 311 + 200, 312 + 50 + Math.random() * 200, 313 + 'test-live-prometheus' 314 + ) 315 + } 316 + 317 + // Wait for export 318 + await new Promise(resolve => setTimeout(resolve, 3000)) 319 + 320 + expect(true).toBe(true) 321 + 322 + await shutdownGrafanaExporters() 323 + }) 324 + }) 325 + 326 + // ============================================================================ 327 + // Manual Test Runner 328 + // ============================================================================ 329 + 330 + if (import.meta.main) { 331 + console.log('๐Ÿงช Running Grafana integration tests...\n') 332 + console.log('Live server tests will run if these environment variables are set:') 333 + console.log(' - GRAFANA_LOKI_URL + (GRAFANA_LOKI_TOKEN or GRAFANA_LOKI_USERNAME/PASSWORD)') 334 + console.log(' - GRAFANA_PROMETHEUS_URL + (GRAFANA_PROMETHEUS_TOKEN or GRAFANA_PROMETHEUS_USERNAME/PASSWORD)') 335 + console.log('') 336 + }
+128 -23
packages/@wisp/safe-fetch/src/index.ts
··· 28 28 const MAX_BLOB_SIZE = 500 * 1024 * 1024; // 500MB 29 29 const MAX_REDIRECTS = 10; 30 30 31 + // Retry configuration 32 + const MAX_RETRIES = 3; 33 + const INITIAL_RETRY_DELAY = 1000; // 1 second 34 + const MAX_RETRY_DELAY = 10000; // 10 seconds 35 + 31 36 function isBlockedHost(hostname: string): boolean { 32 37 const lowerHost = hostname.toLowerCase(); 33 38 ··· 44 49 return false; 45 50 } 46 51 52 + /** 53 + * Check if an error is retryable (network/SSL errors, not HTTP errors) 54 + */ 55 + function isRetryableError(err: unknown): boolean { 56 + if (!(err instanceof Error)) return false; 57 + 58 + // Network errors (ECONNRESET, ENOTFOUND, etc.) 59 + const errorCode = (err as any).code; 60 + if (errorCode) { 61 + const retryableCodes = [ 62 + 'ECONNRESET', 63 + 'ECONNREFUSED', 64 + 'ETIMEDOUT', 65 + 'ENOTFOUND', 66 + 'ENETUNREACH', 67 + 'EAI_AGAIN', 68 + 'EPIPE', 69 + 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR', // SSL/TLS handshake failures 70 + 'ERR_SSL_WRONG_VERSION_NUMBER', 71 + 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 72 + ]; 73 + if (retryableCodes.includes(errorCode)) { 74 + return true; 75 + } 76 + } 77 + 78 + // Timeout errors 79 + if (err.name === 'AbortError' || err.message.includes('timeout')) { 80 + return true; 81 + } 82 + 83 + // Fetch failures (generic network errors) 84 + if (err.message.includes('fetch failed')) { 85 + return true; 86 + } 87 + 88 + return false; 89 + } 90 + 91 + /** 92 + * Sleep for a given number of milliseconds 93 + */ 94 + function sleep(ms: number): Promise<void> { 95 + return new Promise(resolve => setTimeout(resolve, ms)); 96 + } 97 + 98 + /** 99 + * Retry a function with exponential backoff 100 + */ 101 + async function withRetry<T>( 102 + fn: () => Promise<T>, 103 + options: { maxRetries?: number; initialDelay?: number; maxDelay?: number; context?: string } = {} 104 + ): Promise<T> { 105 + const maxRetries = options.maxRetries ?? MAX_RETRIES; 106 + const initialDelay = options.initialDelay ?? INITIAL_RETRY_DELAY; 107 + const maxDelay = options.maxDelay ?? MAX_RETRY_DELAY; 108 + const context = options.context ?? 'Request'; 109 + 110 + let lastError: unknown; 111 + 112 + for (let attempt = 0; attempt <= maxRetries; attempt++) { 113 + try { 114 + return await fn(); 115 + } catch (err) { 116 + lastError = err; 117 + 118 + // Don't retry if this is the last attempt or error is not retryable 119 + if (attempt === maxRetries || !isRetryableError(err)) { 120 + throw err; 121 + } 122 + 123 + // Calculate delay with exponential backoff 124 + const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay); 125 + 126 + const errorCode = (err as any)?.code; 127 + const errorMsg = err instanceof Error ? err.message : String(err); 128 + console.warn( 129 + `${context} failed (attempt ${attempt + 1}/${maxRetries + 1}): ${errorMsg}${errorCode ? ` [${errorCode}]` : ''} - retrying in ${delay}ms` 130 + ); 131 + 132 + await sleep(delay); 133 + } 134 + } 135 + 136 + throw lastError; 137 + } 138 + 47 139 export async function safeFetch( 48 140 url: string, 49 - options?: RequestInit & { maxSize?: number; timeout?: number } 141 + options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean } 50 142 ): Promise<Response> { 143 + const shouldRetry = options?.retry !== false; // Default to true 51 144 const timeoutMs = options?.timeout ?? FETCH_TIMEOUT; 52 145 const maxSize = options?.maxSize ?? MAX_RESPONSE_SIZE; 53 146 54 - // Parse and validate URL 147 + // Parse and validate URL (done once, outside retry loop) 55 148 let parsedUrl: URL; 56 149 try { 57 150 parsedUrl = new URL(url); ··· 68 161 throw new Error(`Blocked host: ${hostname}`); 69 162 } 70 163 71 - const controller = new AbortController(); 72 - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); 164 + const fetchFn = async () => { 165 + const controller = new AbortController(); 166 + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); 167 + 168 + try { 169 + const response = await fetch(url, { 170 + ...options, 171 + signal: controller.signal, 172 + redirect: 'follow', 173 + headers: { 174 + 'User-Agent': 'wisp-place hosting-service', 175 + ...(options?.headers || {}), 176 + }, 177 + }); 73 178 74 - try { 75 - const response = await fetch(url, { 76 - ...options, 77 - signal: controller.signal, 78 - redirect: 'follow', 79 - }); 179 + const contentLength = response.headers.get('content-length'); 180 + if (contentLength && parseInt(contentLength, 10) > maxSize) { 181 + throw new Error(`Response too large: ${contentLength} bytes`); 182 + } 80 183 81 - const contentLength = response.headers.get('content-length'); 82 - if (contentLength && parseInt(contentLength, 10) > maxSize) { 83 - throw new Error(`Response too large: ${contentLength} bytes`); 184 + return response; 185 + } catch (err) { 186 + if (err instanceof Error && err.name === 'AbortError') { 187 + throw new Error(`Request timeout after ${timeoutMs}ms`); 188 + } 189 + throw err; 190 + } finally { 191 + clearTimeout(timeoutId); 84 192 } 193 + }; 85 194 86 - return response; 87 - } catch (err) { 88 - if (err instanceof Error && err.name === 'AbortError') { 89 - throw new Error(`Request timeout after ${timeoutMs}ms`); 90 - } 91 - throw err; 92 - } finally { 93 - clearTimeout(timeoutId); 195 + if (shouldRetry) { 196 + return withRetry(fetchFn, { context: `Fetch ${parsedUrl.hostname}` }); 197 + } else { 198 + return fetchFn(); 94 199 } 95 200 } 96 201 97 202 export async function safeFetchJson<T = any>( 98 203 url: string, 99 - options?: RequestInit & { maxSize?: number; timeout?: number } 204 + options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean } 100 205 ): Promise<T> { 101 206 const maxJsonSize = options?.maxSize ?? MAX_JSON_SIZE; 102 207 const response = await safeFetch(url, { ...options, maxSize: maxJsonSize }); ··· 142 247 143 248 export async function safeFetchBlob( 144 249 url: string, 145 - options?: RequestInit & { maxSize?: number; timeout?: number } 250 + options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean } 146 251 ): Promise<Uint8Array> { 147 252 const maxBlobSize = options?.maxSize ?? MAX_BLOB_SIZE; 148 253 const timeoutMs = options?.timeout ?? FETCH_TIMEOUT_BLOB;
+1 -1
tsconfig.json
··· 33 33 // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 34 // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 35 "types": [ 36 - "bun-types" 36 + "bun" 37 37 ] /* Specify type package names to be included without being referenced in a source file. */, 38 38 // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 39 // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */