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

Compare changes

Choose any two refs to compare.

Changed files
+9173 -6101
.tangled
apps
binaries
cli
docs
lexicons
packages
scripts
+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"}
+1
.gitignore
··· 1 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 .env 3 # dependencies
··· 1 + .research/ 2 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 .env 4 # dependencies
+54 -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 3 when: 4 - - event: ['push'] 5 - branch: ['main'] 6 - - event: ['manual'] 7 - engine: 'nixery' 8 clone: 9 - skip: false 10 - depth: 1 11 - submodules: true 12 dependencies: 13 - nixpkgs: 14 - - git 15 - - gcc 16 - github:NixOS/nixpkgs/nixpkgs-unstable: 17 - - rustc 18 - - cargo 19 environment: 20 - # Customize these for your project 21 - SITE_PATH: 'testDeploy' 22 - SITE_NAME: 'wispPlaceDocs' 23 steps: 24 - - name: 'Initialize submodules' 25 - command: | 26 - git submodule update --init --recursive 27 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 .. 42 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"
··· 1 + --- 2 when: 3 + - event: 4 + - push 5 + branch: 6 + - main 7 + - event: 8 + - manual 9 + engine: nixery 10 clone: 11 + skip: false 12 + depth: 1 13 + submodules: true 14 dependencies: 15 + nixpkgs: 16 + - git 17 + - gcc 18 + github:NixOS/nixpkgs/nixpkgs-unstable: 19 + - rustc 20 + - cargo 21 + - bun 22 environment: 23 + WISP_HANDLE: wisp.place 24 + SITE_PATH: docs/dist 25 + SITE_NAME: docs 26 steps: 27 + - name: Initialize submodules 28 + command: | 29 + git submodule update --init --recursive 30 + - name: Build wisp-cli 31 + command: | 32 + cd cli 33 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 + ' 47 + 48 + cd .. 49 + - name: Build docs 50 + command: | 51 + cd docs 52 + export PATH="$HOME/.nix-profile/bin:$PATH" 53 + bun install 54 + bun run build 55 + - name: Deploy to Wisp.place 56 + command: | 57 + ./cli/target/release/wisp-cli \ 58 + "$WISP_HANDLE" \ 59 + --path "$SITE_PATH" \ 60 + --site "$SITE_NAME" \ 61 + --password "$WISP_APP_PASSWORD"
+2 -1
.tangled/workflows/test.yml
··· 7 dependencies: 8 nixpkgs: 9 - git 10 github:NixOS/nixpkgs/nixpkgs-unstable: 11 - bun 12 ··· 16 export PATH="$HOME/.nix-profile/bin:$PATH" 17 18 # have to regenerate otherwise it wont install necessary dependencies to run 19 - rm -rf bun.lock package-lock.json 20 bun install 21 22 - name: run all tests
··· 7 dependencies: 8 nixpkgs: 9 - git 10 + - findutils 11 github:NixOS/nixpkgs/nixpkgs-unstable: 12 - bun 13 ··· 17 export PATH="$HOME/.nix-profile/bin:$PATH" 18 19 # have to regenerate otherwise it wont install necessary dependencies to run 20 + find . -type f \( -name "bun.lock" -o -name "package-lock.json" \) -delete 21 bun install 22 23 - name: run all tests
+15 -58
Dockerfile
··· 1 - # Build stage 2 - FROM oven/bun:1.3 AS build 3 4 WORKDIR /app 5 ··· 7 COPY package.json bunfig.toml tsconfig.json bun.lock* ./ 8 9 # Copy all workspace package.json files first (for dependency resolution) 10 - COPY packages ./packages 11 COPY apps/main-app/package.json ./apps/main-app/package.json 12 COPY apps/hosting-service/package.json ./apps/hosting-service/package.json 13 14 - # Install all dependencies (including workspaces) 15 - RUN bun install --frozen-lockfile 16 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* ./ 34 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 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 71 72 ENV PORT=8000 73 74 EXPOSE 8000 75 76 - CMD ["./server"]
··· 1 + # Production stage 2 + FROM oven/bun:1.3 3 4 WORKDIR /app 5 ··· 7 COPY package.json bunfig.toml tsconfig.json bun.lock* ./ 8 9 # Copy all workspace package.json files first (for dependency resolution) 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 17 COPY apps/main-app/package.json ./apps/main-app/package.json 18 COPY apps/hosting-service/package.json ./apps/hosting-service/package.json 19 20 + # Install dependencies 21 + RUN bun install --frozen-lockfile --production 22 23 + # Copy workspace source files 24 COPY packages ./packages 25 26 + # Copy app source and public files 27 + COPY apps/main-app ./apps/main-app 28 29 ENV PORT=8000 30 31 EXPOSE 8000 32 33 + CMD ["bun", "run", "apps/main-app/src/index.ts"]
+20 -3
README.md
··· 39 40 ```bash 41 # Backend 42 bun install 43 - bun run src/index.ts 44 45 # Hosting service 46 - cd hosting-service 47 - npm run start 48 49 # CLI 50 cd cli ··· 52 ``` 53 54 ## Features 55 56 ### URL Redirects and Rewrites 57
··· 39 40 ```bash 41 # Backend 42 + # bun install will install packages across the monorepo 43 bun install 44 + bun run dev 45 46 # Hosting service 47 + bun run hosting:dev 48 49 # CLI 50 cd cli ··· 52 ``` 53 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. 72 73 ### URL Redirects and Rewrites 74
+13 -10
apps/hosting-service/package.json
··· 6 "dev": "tsx --env-file=.env src/index.ts", 7 "build": "bun run build.ts", 8 "start": "tsx src/index.ts", 9 "backfill": "tsx src/index.ts --backfill" 10 }, 11 "dependencies": { 12 - "@wisp/lexicons": "workspace:*", 13 - "@wisp/constants": "workspace:*", 14 - "@wisp/observability": "workspace:*", 15 - "@wisp/atproto-utils": "workspace:*", 16 - "@wisp/database": "workspace:*", 17 - "@wisp/fs-utils": "workspace:*", 18 - "@wisp/safe-fetch": "workspace:*", 19 "@atproto/api": "^0.17.4", 20 "@atproto/identity": "^0.4.9", 21 - "@atproto/lexicon": "^0.5.1", 22 "@atproto/sync": "^0.1.36", 23 "@atproto/xrpc": "^0.7.5", 24 "@hono/node-server": "^1.19.6", 25 "hono": "^4.10.4", 26 "mime-types": "^2.1.35", 27 "multiformats": "^13.4.1", 28 - "postgres": "^3.4.5" 29 }, 30 "devDependencies": { 31 "@types/bun": "^1.3.1", 32 "@types/mime-types": "^2.1.4", 33 "@types/node": "^22.10.5", 34 - "tsx": "^4.19.2" 35 } 36 }
··· 6 "dev": "tsx --env-file=.env src/index.ts", 7 "build": "bun run build.ts", 8 "start": "tsx src/index.ts", 9 + "check": "tsc --noEmit", 10 "backfill": "tsx src/index.ts --backfill" 11 }, 12 "dependencies": { 13 "@atproto/api": "^0.17.4", 14 "@atproto/identity": "^0.4.9", 15 + "@atproto/lexicon": "^0.5.2", 16 "@atproto/sync": "^0.1.36", 17 "@atproto/xrpc": "^0.7.5", 18 "@hono/node-server": "^1.19.6", 19 + "@wisp/atproto-utils": "workspace:*", 20 + "@wisp/constants": "workspace:*", 21 + "@wisp/database": "workspace:*", 22 + "@wisp/fs-utils": "workspace:*", 23 + "@wisp/lexicons": "workspace:*", 24 + "@wisp/observability": "workspace:*", 25 + "@wisp/safe-fetch": "workspace:*", 26 "hono": "^4.10.4", 27 "mime-types": "^2.1.35", 28 "multiformats": "^13.4.1", 29 + "postgres": "^3.4.5", 30 + "tiered-storage": "1.0.3" 31 }, 32 "devDependencies": { 33 "@types/bun": "^1.3.1", 34 "@types/mime-types": "^2.1.4", 35 "@types/node": "^22.10.5", 36 + "tsx": "^4.19.2", 37 + "typescript": "^5.9.3" 38 } 39 }
+46 -6
apps/hosting-service/src/index.ts
··· 1 import app from './server'; 2 import { serve } from '@hono/node-server'; 3 import { FirehoseWorker } from './lib/firehose'; 4 - import { createLogger } from '@wisp/observability'; 5 import { mkdirSync, existsSync } from 'fs'; 6 import { backfillCache } from './lib/backfill'; 7 - import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode } from './lib/db'; 8 9 const logger = createLogger('hosting-service'); 10 11 const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001; 12 const CACHE_DIR = process.env.CACHE_DIR || './cache/sites'; 13 14 // Parse CLI arguments 15 const args = process.argv.slice(2); ··· 41 42 firehose.start(); 43 44 // Run backfill if requested 45 if (backfillOnStartup) { 46 console.log('๐Ÿ”„ Backfill requested, starting cache backfill...'); 47 backfillCache({ 48 skipExisting: true, 49 - concurrency: 3, 50 }).then((stats) => { 51 console.log('โœ… Cache backfill completed'); 52 }).catch((err) => { ··· 69 port: PORT, 70 }); 71 72 console.log(` 73 - Wisp Hosting Service 74 75 Server: http://localhost:${PORT} 76 Health: http://localhost:${PORT}/health 77 - Cache: ${CACHE_DIR} 78 - Firehose: Connected to Firehose 79 Cache-Only: ${CACHE_ONLY_MODE ? 'ENABLED (no DB writes)' : 'DISABLED'} 80 `); 81 82 // Graceful shutdown ··· 84 console.log('\n๐Ÿ›‘ Shutting down...'); 85 firehose.stop(); 86 stopDomainCacheCleanup(); 87 server.close(); 88 process.exit(0); 89 }); ··· 92 console.log('\n๐Ÿ›‘ Shutting down...'); 93 firehose.stop(); 94 stopDomainCacheCleanup(); 95 server.close(); 96 process.exit(0); 97 });
··· 1 import app from './server'; 2 import { serve } from '@hono/node-server'; 3 import { FirehoseWorker } from './lib/firehose'; 4 + import { createLogger, initializeGrafanaExporters } from '@wisp/observability'; 5 import { mkdirSync, existsSync } from 'fs'; 6 import { backfillCache } from './lib/backfill'; 7 + import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode, closeDatabase } from './lib/db'; 8 + import { storage, getStorageConfig } from './lib/storage'; 9 + 10 + // Initialize Grafana exporters if configured 11 + initializeGrafanaExporters({ 12 + serviceName: 'hosting-service', 13 + serviceVersion: '1.0.0' 14 + }); 15 16 const logger = createLogger('hosting-service'); 17 18 const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001; 19 const CACHE_DIR = process.env.CACHE_DIR || './cache/sites'; 20 + const BACKFILL_CONCURRENCY = process.env.BACKFILL_CONCURRENCY 21 + ? parseInt(process.env.BACKFILL_CONCURRENCY) 22 + : undefined; // Let backfill.ts default (10) apply 23 24 // Parse CLI arguments 25 const args = process.argv.slice(2); ··· 51 52 firehose.start(); 53 54 + // Optional: Bootstrap hot cache from warm tier on startup 55 + const BOOTSTRAP_HOT_ON_STARTUP = process.env.BOOTSTRAP_HOT_ON_STARTUP === 'true'; 56 + const BOOTSTRAP_HOT_LIMIT = process.env.BOOTSTRAP_HOT_LIMIT ? parseInt(process.env.BOOTSTRAP_HOT_LIMIT) : 100; 57 + 58 + if (BOOTSTRAP_HOT_ON_STARTUP) { 59 + console.log(`๐Ÿ”ฅ Bootstrapping hot cache (top ${BOOTSTRAP_HOT_LIMIT} items)...`); 60 + storage.bootstrapHot(BOOTSTRAP_HOT_LIMIT) 61 + .then((loaded: number) => { 62 + console.log(`โœ… Bootstrapped ${loaded} items into hot cache`); 63 + }) 64 + .catch((err: unknown) => { 65 + console.error('โŒ Hot cache bootstrap error:', err); 66 + }); 67 + } 68 + 69 // Run backfill if requested 70 if (backfillOnStartup) { 71 console.log('๐Ÿ”„ Backfill requested, starting cache backfill...'); 72 backfillCache({ 73 skipExisting: true, 74 + concurrency: BACKFILL_CONCURRENCY, 75 }).then((stats) => { 76 console.log('โœ… Cache backfill completed'); 77 }).catch((err) => { ··· 94 port: PORT, 95 }); 96 97 + // Get storage configuration for display 98 + const storageConfig = getStorageConfig(); 99 + 100 console.log(` 101 + Wisp Hosting Service with Tiered Storage 102 103 Server: http://localhost:${PORT} 104 Health: http://localhost:${PORT}/health 105 Cache-Only: ${CACHE_ONLY_MODE ? 'ENABLED (no DB writes)' : 'DISABLED'} 106 + Backfill: ${backfillOnStartup ? `ENABLED (concurrency: ${BACKFILL_CONCURRENCY || 10})` : 'DISABLED'} 107 + 108 + Tiered Storage Configuration: 109 + Hot Cache: ${storageConfig.hotCacheSize} (${storageConfig.hotCacheCount} items max) 110 + Warm Cache: ${storageConfig.warmCacheSize} (${storageConfig.warmEvictionPolicy} eviction) 111 + Cold Storage: S3 - ${storageConfig.s3Bucket} 112 + S3 Region: ${storageConfig.s3Region} 113 + S3 Endpoint: ${storageConfig.s3Endpoint} 114 + S3 Prefix: ${storageConfig.s3Prefix} 115 + Metadata Bucket: ${storageConfig.metadataBucket} 116 + 117 + Firehose: Connecting... 118 `); 119 120 // Graceful shutdown ··· 122 console.log('\n๐Ÿ›‘ Shutting down...'); 123 firehose.stop(); 124 stopDomainCacheCleanup(); 125 + await closeDatabase(); 126 server.close(); 127 process.exit(0); 128 }); ··· 131 console.log('\n๐Ÿ›‘ Shutting down...'); 132 firehose.stop(); 133 stopDomainCacheCleanup(); 134 + await closeDatabase(); 135 server.close(); 136 process.exit(0); 137 });
+65 -57
apps/hosting-service/src/lib/backfill.ts
··· 60 console.log(`โš™๏ธ Limited to ${maxSites} sites for backfill`); 61 } 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 - 69 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 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 - } 102 103 - // Mark site as being cached to prevent serving stale content during update 104 - markSiteAsBeingCached(site.did, site.rkey); 105 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) { 120 stats.failed++; 121 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}`); 124 } 125 - }) 126 - ); 127 } 128 129 stats.duration = Date.now() - startTime; 130
··· 60 console.log(`โš™๏ธ Limited to ${maxSites} sites for backfill`); 61 } 62 63 + // Process sites with sliding window concurrency pool 64 + const executing = new Set<Promise<void>>(); 65 let processed = 0; 66 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 && await 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 + } 79 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 + } 89 90 + // Get PDS endpoint 91 + const pdsEndpoint = await getPdsForDid(site.did); 92 + if (!pdsEndpoint) { 93 stats.failed++; 94 processed++; 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); 115 } 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 + } 132 } 133 + 134 + // Wait for all remaining tasks to complete 135 + await Promise.all(executing); 136 137 stats.duration = Date.now() - startTime; 138
+35 -43
apps/hosting-service/src/lib/cache.ts
··· 1 - // In-memory LRU cache for file contents and metadata 2 3 interface CacheEntry<T> { 4 value: T; 5 size: number; ··· 96 return true; 97 } 98 99 - // Invalidate all entries for a specific site 100 - invalidateSite(did: string, rkey: string): number { 101 - const prefix = `${did}:${rkey}:`; 102 - let count = 0; 103 - 104 - for (const key of Array.from(this.cache.keys())) { 105 - if (key.startsWith(prefix)) { 106 - this.delete(key); 107 - count++; 108 - } 109 - } 110 - 111 - return count; 112 - } 113 - 114 - // Get cache size 115 size(): number { 116 return this.cache.size; 117 } ··· 127 return { ...this.stats }; 128 } 129 130 - // Get cache hit rate 131 getHitRate(): number { 132 const total = this.stats.hits + this.stats.misses; 133 return total === 0 ? 0 : (this.stats.hits / total) * 100; 134 } 135 } 136 137 - // File metadata cache entry 138 - export interface FileMetadata { 139 - encoding?: 'gzip'; 140 - mimeType: string; 141 - } 142 - 143 - // Global cache instances 144 - const FILE_CACHE_SIZE = 100 * 1024 * 1024; // 100MB 145 - const FILE_CACHE_COUNT = 500; 146 - const METADATA_CACHE_COUNT = 2000; 147 - 148 - export const fileCache = new LRUCache<Buffer>(FILE_CACHE_SIZE, FILE_CACHE_COUNT); 149 - export const metadataCache = new LRUCache<FileMetadata>(1024 * 1024, METADATA_CACHE_COUNT); // 1MB for metadata 150 export const rewrittenHtmlCache = new LRUCache<Buffer>(50 * 1024 * 1024, 200); // 50MB for rewritten HTML 151 152 - // Helper to generate cache keys 153 export function getCacheKey(did: string, rkey: string, filePath: string, suffix?: string): string { 154 const base = `${did}:${rkey}:${filePath}`; 155 return suffix ? `${base}:${suffix}` : base; 156 } 157 158 - // Invalidate all caches for a site 159 - export function invalidateSiteCache(did: string, rkey: string): void { 160 - const fileCount = fileCache.invalidateSite(did, rkey); 161 - const metaCount = metadataCache.invalidateSite(did, rkey); 162 - const htmlCount = rewrittenHtmlCache.invalidateSite(did, rkey); 163 164 - console.log(`[Cache] Invalidated site ${did}:${rkey} - ${fileCount} files, ${metaCount} metadata, ${htmlCount} HTML`); 165 } 166 167 // Track sites currently being cached (to prevent serving stale cache during updates) ··· 183 } 184 185 // Get overall cache statistics 186 - export function getCacheStats() { 187 return { 188 - files: fileCache.getStats(), 189 - fileHitRate: fileCache.getHitRate(), 190 - metadata: metadataCache.getStats(), 191 - metadataHitRate: metadataCache.getHitRate(), 192 rewrittenHtml: rewrittenHtmlCache.getStats(), 193 rewrittenHtmlHitRate: rewrittenHtmlCache.getHitRate(), 194 sitesBeingCached: sitesBeingCached.size,
··· 1 + /** 2 + * Cache management for wisp-hosting-service 3 + * 4 + * With tiered storage, most caching is handled transparently. 5 + * This module tracks sites being cached and manages rewritten HTML cache. 6 + */ 7 8 + import { storage } from './storage'; 9 + 10 + // In-memory LRU cache for rewritten HTML (for path rewriting in subdomain routes) 11 interface CacheEntry<T> { 12 value: T; 13 size: number; ··· 104 return true; 105 } 106 107 size(): number { 108 return this.cache.size; 109 } ··· 119 return { ...this.stats }; 120 } 121 122 getHitRate(): number { 123 const total = this.stats.hits + this.stats.misses; 124 return total === 0 ? 0 : (this.stats.hits / total) * 100; 125 } 126 } 127 128 + // Rewritten HTML cache: stores HTML after path rewriting for subdomain routes 129 export const rewrittenHtmlCache = new LRUCache<Buffer>(50 * 1024 * 1024, 200); // 50MB for rewritten HTML 130 131 + // Helper to generate cache keys for rewritten HTML 132 export function getCacheKey(did: string, rkey: string, filePath: string, suffix?: string): string { 133 const base = `${did}:${rkey}:${filePath}`; 134 return suffix ? `${base}:${suffix}` : base; 135 } 136 137 + /** 138 + * Invalidate site cache via tiered storage 139 + * Also invalidates locally cached rewritten HTML 140 + */ 141 + export async function invalidateSiteCache(did: string, rkey: string): Promise<void> { 142 + // Invalidate in tiered storage 143 + const prefix = `${did}/${rkey}/`; 144 + const deleted = await storage.invalidate(prefix); 145 146 + // Invalidate rewritten HTML cache for this site 147 + const sitePrefix = `${did}:${rkey}:`; 148 + let htmlCount = 0; 149 + const cacheKeys = Array.from((rewrittenHtmlCache as any).cache?.keys() || []) as string[]; 150 + for (const key of cacheKeys) { 151 + if (key.startsWith(sitePrefix)) { 152 + rewrittenHtmlCache.delete(key); 153 + htmlCount++; 154 + } 155 + } 156 + 157 + console.log(`[Cache] Invalidated site ${did}:${rkey} - ${deleted} files in tiered storage, ${htmlCount} rewritten HTML`); 158 } 159 160 // Track sites currently being cached (to prevent serving stale cache during updates) ··· 176 } 177 178 // Get overall cache statistics 179 + export async function getCacheStats() { 180 + const tieredStats = await storage.getStats(); 181 + 182 return { 183 + tieredStorage: tieredStats, 184 rewrittenHtml: rewrittenHtmlCache.getStats(), 185 rewrittenHtmlHitRate: rewrittenHtmlCache.getHitRate(), 186 sitesBeingCached: sitesBeingCached.size,
+32 -1
apps/hosting-service/src/lib/db.ts
··· 183 return hashNum & 0x7FFFFFFFFFFFFFFFn; 184 } 185 186 /** 187 * Acquire a distributed lock using PostgreSQL advisory locks 188 * Returns true if lock was acquired, false if already held by another instance ··· 193 194 try { 195 const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`; 196 - return result[0]?.acquired === true; 197 } catch (err) { 198 console.error('Failed to acquire lock', { key, error: err }); 199 return false; ··· 208 209 try { 210 await sql`SELECT pg_advisory_unlock(${Number(lockId)})`; 211 } catch (err) { 212 console.error('Failed to release lock', { key, error: err }); 213 } 214 } 215
··· 183 return hashNum & 0x7FFFFFFFFFFFFFFFn; 184 } 185 186 + // Track active locks for cleanup on shutdown 187 + const activeLocks = new Set<string>(); 188 + 189 /** 190 * Acquire a distributed lock using PostgreSQL advisory locks 191 * Returns true if lock was acquired, false if already held by another instance ··· 196 197 try { 198 const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`; 199 + const acquired = result[0]?.acquired === true; 200 + if (acquired) { 201 + activeLocks.add(key); 202 + } 203 + return acquired; 204 } catch (err) { 205 console.error('Failed to acquire lock', { key, error: err }); 206 return false; ··· 215 216 try { 217 await sql`SELECT pg_advisory_unlock(${Number(lockId)})`; 218 + activeLocks.delete(key); 219 } catch (err) { 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); 244 } 245 } 246
+64 -76
apps/hosting-service/src/lib/file-serving.ts
··· 7 import { lookup } from 'mime-types'; 8 import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings'; 9 import { shouldCompressMimeType } from '@wisp/atproto-utils/compression'; 10 - import { fileCache, metadataCache, rewrittenHtmlCache, getCacheKey, isSiteBeingCached } from './cache'; 11 import { getCachedFilePath, getCachedSettings } from './utils'; 12 import { loadRedirectRules, matchRedirectRule, parseCookies, parseQueryString } from './redirects'; 13 import { rewriteHtmlPaths, isHtmlContent } from './html-rewriter'; 14 import { generate404Page, generateDirectoryListing, siteUpdatingResponse } from './page-generators'; 15 import { getIndexFiles, applyCustomHeaders, fileExists } from './request-utils'; 16 import { getRedirectRulesFromCache, setRedirectRulesInCache } from './site-cache'; 17 18 /** 19 * Helper to serve files from cache (for custom domains and subdomains) ··· 176 177 // Not a directory, try to serve as a file 178 const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html'; 179 - const cacheKey = getCacheKey(did, rkey, fileRequestPath); 180 - const cachedFile = getCachedFilePath(did, rkey, fileRequestPath); 181 182 - // Check in-memory cache first 183 - let content = fileCache.get(cacheKey); 184 - let meta = metadataCache.get(cacheKey); 185 186 - if (!content && await fileExists(cachedFile)) { 187 - // Read from disk and cache 188 - content = await readFile(cachedFile); 189 - fileCache.set(cacheKey, content, content.length); 190 191 - const metaFile = `${cachedFile}.meta`; 192 - if (await fileExists(metaFile)) { 193 - const metaJson = await readFile(metaFile, 'utf-8'); 194 - meta = JSON.parse(metaJson); 195 - metadataCache.set(cacheKey, meta!, JSON.stringify(meta).length); 196 - } 197 - } 198 - 199 - if (content) { 200 // Build headers with caching 201 - const headers: Record<string, string> = {}; 202 203 - if (meta && meta.encoding === 'gzip' && meta.mimeType) { 204 const shouldServeCompressed = shouldCompressMimeType(meta.mimeType); 205 206 if (!shouldServeCompressed) { ··· 233 } 234 235 // Non-compressed files 236 - const mimeType = lookup(cachedFile) || 'application/octet-stream'; 237 headers['Content-Type'] = mimeType; 238 headers['Cache-Control'] = mimeType.startsWith('text/html') 239 ? 'public, max-age=300' ··· 246 if (!fileRequestPath.includes('.')) { 247 for (const indexFileName of indexFiles) { 248 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 249 - const indexCacheKey = getCacheKey(did, rkey, indexPath); 250 - const indexFile = getCachedFilePath(did, rkey, indexPath); 251 252 - let indexContent = fileCache.get(indexCacheKey); 253 - let indexMeta = metadataCache.get(indexCacheKey); 254 255 - if (!indexContent && await fileExists(indexFile)) { 256 - indexContent = await readFile(indexFile); 257 - fileCache.set(indexCacheKey, indexContent, indexContent.length); 258 259 - const indexMetaFile = `${indexFile}.meta`; 260 - if (await fileExists(indexMetaFile)) { 261 - const metaJson = await readFile(indexMetaFile, 'utf-8'); 262 - indexMeta = JSON.parse(metaJson); 263 - metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length); 264 - } 265 - } 266 - 267 - if (indexContent) { 268 const headers: Record<string, string> = { 269 'Content-Type': 'text/html; charset=utf-8', 270 'Cache-Control': 'public, max-age=300', 271 }; 272 273 - if (indexMeta && indexMeta.encoding === 'gzip') { 274 headers['Content-Encoding'] = 'gzip'; 275 } 276 ··· 556 557 // Not a directory, try to serve as a file 558 const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html'; 559 - const cacheKey = getCacheKey(did, rkey, fileRequestPath); 560 - const cachedFile = getCachedFilePath(did, rkey, fileRequestPath); 561 562 // Check for rewritten HTML in cache first (if it's HTML) 563 const mimeTypeGuess = lookup(fileRequestPath) || 'application/octet-stream'; ··· 565 const rewrittenKey = getCacheKey(did, rkey, fileRequestPath, `rewritten:${basePath}`); 566 const rewrittenContent = rewrittenHtmlCache.get(rewrittenKey); 567 if (rewrittenContent) { 568 const headers: Record<string, string> = { 569 'Content-Type': 'text/html; charset=utf-8', 570 'Content-Encoding': 'gzip', 571 'Cache-Control': 'public, max-age=300', 572 }; 573 applyCustomHeaders(headers, fileRequestPath, settings); 574 return new Response(rewrittenContent, { headers }); 575 } 576 } 577 578 - // Check in-memory file cache 579 - let content = fileCache.get(cacheKey); 580 - let meta = metadataCache.get(cacheKey); 581 582 - if (!content && await fileExists(cachedFile)) { 583 - // Read from disk and cache 584 - content = await readFile(cachedFile); 585 - fileCache.set(cacheKey, content, content.length); 586 587 - const metaFile = `${cachedFile}.meta`; 588 - if (await fileExists(metaFile)) { 589 - const metaJson = await readFile(metaFile, 'utf-8'); 590 - meta = JSON.parse(metaJson); 591 - metadataCache.set(cacheKey, meta!, JSON.stringify(meta).length); 592 - } 593 - } 594 - 595 - if (content) { 596 - const mimeType = meta?.mimeType || lookup(cachedFile) || 'application/octet-stream'; 597 - const isGzipped = meta?.encoding === 'gzip'; 598 599 // Check if this is HTML content that needs rewriting 600 if (isHtmlContent(fileRequestPath, mimeType)) { 601 let htmlContent: string; 602 if (isGzipped) { 603 // Verify content is actually gzipped ··· 612 } else { 613 htmlContent = content.toString('utf-8'); 614 } 615 const rewritten = rewriteHtmlPaths(htmlContent, basePath, fileRequestPath); 616 617 // Recompress and cache the rewritten HTML 618 const { gzipSync } = await import('zlib'); 619 const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8')); ··· 625 'Content-Type': 'text/html; charset=utf-8', 626 'Content-Encoding': 'gzip', 627 'Cache-Control': 'public, max-age=300', 628 }; 629 applyCustomHeaders(htmlHeaders, fileRequestPath, settings); 630 return new Response(recompressed, { headers: htmlHeaders }); ··· 634 const headers: Record<string, string> = { 635 'Content-Type': mimeType, 636 'Cache-Control': 'public, max-age=31536000, immutable', 637 }; 638 639 if (isGzipped) { ··· 663 if (!fileRequestPath.includes('.')) { 664 for (const indexFileName of indexFiles) { 665 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 666 - const indexCacheKey = getCacheKey(did, rkey, indexPath); 667 - const indexFile = getCachedFilePath(did, rkey, indexPath); 668 669 // Check for rewritten index file in cache 670 const rewrittenKey = getCacheKey(did, rkey, indexPath, `rewritten:${basePath}`); ··· 674 'Content-Type': 'text/html; charset=utf-8', 675 'Content-Encoding': 'gzip', 676 'Cache-Control': 'public, max-age=300', 677 }; 678 applyCustomHeaders(headers, indexPath, settings); 679 return new Response(rewrittenContent, { headers }); 680 } 681 682 - let indexContent = fileCache.get(indexCacheKey); 683 - let indexMeta = metadataCache.get(indexCacheKey); 684 685 - if (!indexContent && await fileExists(indexFile)) { 686 - indexContent = await readFile(indexFile); 687 - fileCache.set(indexCacheKey, indexContent, indexContent.length); 688 - 689 - const indexMetaFile = `${indexFile}.meta`; 690 - if (await fileExists(indexMetaFile)) { 691 - const metaJson = await readFile(indexMetaFile, 'utf-8'); 692 - indexMeta = JSON.parse(metaJson); 693 - metadataCache.set(indexCacheKey, indexMeta!, JSON.stringify(indexMeta).length); 694 - } 695 - } 696 - 697 - if (indexContent) { 698 const isGzipped = indexMeta?.encoding === 'gzip'; 699 700 let htmlContent: string; ··· 722 'Content-Type': 'text/html; charset=utf-8', 723 'Content-Encoding': 'gzip', 724 'Cache-Control': 'public, max-age=300', 725 }; 726 applyCustomHeaders(headers, indexPath, settings); 727 return new Response(recompressed, { headers });
··· 7 import { lookup } from 'mime-types'; 8 import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings'; 9 import { shouldCompressMimeType } from '@wisp/atproto-utils/compression'; 10 + import { rewrittenHtmlCache, getCacheKey, isSiteBeingCached } from './cache'; 11 import { getCachedFilePath, getCachedSettings } from './utils'; 12 import { loadRedirectRules, matchRedirectRule, parseCookies, parseQueryString } from './redirects'; 13 import { rewriteHtmlPaths, isHtmlContent } from './html-rewriter'; 14 import { generate404Page, generateDirectoryListing, siteUpdatingResponse } from './page-generators'; 15 import { getIndexFiles, applyCustomHeaders, fileExists } from './request-utils'; 16 import { getRedirectRulesFromCache, setRedirectRulesInCache } from './site-cache'; 17 + import { storage } from './storage'; 18 + 19 + /** 20 + * Helper to retrieve a file with metadata from tiered storage 21 + */ 22 + async function getFileWithMetadata(did: string, rkey: string, filePath: string) { 23 + const key = `${did}/${rkey}/${filePath}`; 24 + return await storage.getWithMetadata(key); 25 + } 26 27 /** 28 * Helper to serve files from cache (for custom domains and subdomains) ··· 185 186 // Not a directory, try to serve as a file 187 const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html'; 188 189 + // Retrieve from tiered storage 190 + const result = await getFileWithMetadata(did, rkey, fileRequestPath); 191 192 + if (result) { 193 + const content = Buffer.from(result.data); 194 + const meta = result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 195 196 // Build headers with caching 197 + const headers: Record<string, string> = { 198 + 'X-Cache-Tier': result.source, 199 + }; 200 201 + if (meta?.encoding === 'gzip' && meta.mimeType) { 202 const shouldServeCompressed = shouldCompressMimeType(meta.mimeType); 203 204 if (!shouldServeCompressed) { ··· 231 } 232 233 // Non-compressed files 234 + const mimeType = meta?.mimeType || lookup(fileRequestPath) || 'application/octet-stream'; 235 headers['Content-Type'] = mimeType; 236 headers['Cache-Control'] = mimeType.startsWith('text/html') 237 ? 'public, max-age=300' ··· 244 if (!fileRequestPath.includes('.')) { 245 for (const indexFileName of indexFiles) { 246 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 247 248 + const indexResult = await getFileWithMetadata(did, rkey, indexPath); 249 250 + if (indexResult) { 251 + const indexContent = Buffer.from(indexResult.data); 252 + const indexMeta = indexResult.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 253 254 const headers: Record<string, string> = { 255 'Content-Type': 'text/html; charset=utf-8', 256 'Cache-Control': 'public, max-age=300', 257 + 'X-Cache-Tier': indexResult.source, 258 }; 259 260 + if (indexMeta?.encoding === 'gzip') { 261 headers['Content-Encoding'] = 'gzip'; 262 } 263 ··· 543 544 // Not a directory, try to serve as a file 545 const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html'; 546 547 // Check for rewritten HTML in cache first (if it's HTML) 548 const mimeTypeGuess = lookup(fileRequestPath) || 'application/octet-stream'; ··· 550 const rewrittenKey = getCacheKey(did, rkey, fileRequestPath, `rewritten:${basePath}`); 551 const rewrittenContent = rewrittenHtmlCache.get(rewrittenKey); 552 if (rewrittenContent) { 553 + console.log(`[HTML Rewrite] Serving from rewritten cache: ${rewrittenKey}`); 554 const headers: Record<string, string> = { 555 'Content-Type': 'text/html; charset=utf-8', 556 'Content-Encoding': 'gzip', 557 'Cache-Control': 'public, max-age=300', 558 + 'X-Cache-Tier': 'local', // Rewritten HTML is stored locally 559 }; 560 applyCustomHeaders(headers, fileRequestPath, settings); 561 return new Response(rewrittenContent, { headers }); 562 } 563 } 564 565 + // Retrieve from tiered storage 566 + const result = await getFileWithMetadata(did, rkey, fileRequestPath); 567 568 + if (result) { 569 + const content = Buffer.from(result.data); 570 + const meta = result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 571 + const mimeType = meta?.mimeType || lookup(fileRequestPath) || 'application/octet-stream'; 572 + const isGzipped = meta?.encoding === 'gzip'; 573 574 + console.log(`[File Serve] Serving ${fileRequestPath}, mimeType: ${mimeType}, isHTML: ${isHtmlContent(fileRequestPath, mimeType)}, basePath: ${basePath}`); 575 576 // Check if this is HTML content that needs rewriting 577 if (isHtmlContent(fileRequestPath, mimeType)) { 578 + console.log(`[HTML Rewrite] Processing ${fileRequestPath}, basePath: ${basePath}, mimeType: ${mimeType}, isGzipped: ${isGzipped}`); 579 let htmlContent: string; 580 if (isGzipped) { 581 // Verify content is actually gzipped ··· 590 } else { 591 htmlContent = content.toString('utf-8'); 592 } 593 + // Check for <base> tag which can override paths 594 + const baseTagMatch = htmlContent.match(/<base\s+[^>]*href=["'][^"']+["'][^>]*>/i); 595 + if (baseTagMatch) { 596 + console.warn(`[HTML Rewrite] WARNING: <base> tag found: ${baseTagMatch[0]} - this may override path rewrites`); 597 + } 598 + 599 + // Find src/href attributes (quoted and unquoted) to debug 600 + const allMatches = htmlContent.match(/(?:src|href)\s*=\s*["']?\/[^"'\s>]+/g); 601 + console.log(`[HTML Rewrite] Found ${allMatches ? allMatches.length : 0} local path attrs`); 602 + if (allMatches && allMatches.length > 0) { 603 + console.log(`[HTML Rewrite] Sample paths: ${allMatches.slice(0, 5).join(', ')}`); 604 + } 605 + 606 const rewritten = rewriteHtmlPaths(htmlContent, basePath, fileRequestPath); 607 608 + const rewrittenMatches = rewritten.match(/(?:src|href)\s*=\s*["']?\/[^"'\s>]+/g); 609 + console.log(`[HTML Rewrite] After rewrite, found ${rewrittenMatches ? rewrittenMatches.length : 0} local paths`); 610 + if (rewrittenMatches && rewrittenMatches.length > 0) { 611 + console.log(`[HTML Rewrite] Sample rewritten: ${rewrittenMatches.slice(0, 5).join(', ')}`); 612 + } 613 + 614 // Recompress and cache the rewritten HTML 615 const { gzipSync } = await import('zlib'); 616 const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8')); ··· 622 'Content-Type': 'text/html; charset=utf-8', 623 'Content-Encoding': 'gzip', 624 'Cache-Control': 'public, max-age=300', 625 + 'X-Cache-Tier': result.source, 626 }; 627 applyCustomHeaders(htmlHeaders, fileRequestPath, settings); 628 return new Response(recompressed, { headers: htmlHeaders }); ··· 632 const headers: Record<string, string> = { 633 'Content-Type': mimeType, 634 'Cache-Control': 'public, max-age=31536000, immutable', 635 + 'X-Cache-Tier': result.source, 636 }; 637 638 if (isGzipped) { ··· 662 if (!fileRequestPath.includes('.')) { 663 for (const indexFileName of indexFiles) { 664 const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName; 665 666 // Check for rewritten index file in cache 667 const rewrittenKey = getCacheKey(did, rkey, indexPath, `rewritten:${basePath}`); ··· 671 'Content-Type': 'text/html; charset=utf-8', 672 'Content-Encoding': 'gzip', 673 'Cache-Control': 'public, max-age=300', 674 + 'X-Cache-Tier': 'local', // Rewritten HTML is stored locally 675 }; 676 applyCustomHeaders(headers, indexPath, settings); 677 return new Response(rewrittenContent, { headers }); 678 } 679 680 + const indexResult = await getFileWithMetadata(did, rkey, indexPath); 681 682 + if (indexResult) { 683 + const indexContent = Buffer.from(indexResult.data); 684 + const indexMeta = indexResult.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 685 const isGzipped = indexMeta?.encoding === 'gzip'; 686 687 let htmlContent: string; ··· 709 'Content-Type': 'text/html; charset=utf-8', 710 'Content-Encoding': 'gzip', 711 'Cache-Control': 'public, max-age=300', 712 + 'X-Cache-Tier': indexResult.source, 713 }; 714 applyCustomHeaders(headers, indexPath, settings); 715 return new Response(recompressed, { headers });
+46 -35
apps/hosting-service/src/lib/firehose.ts
··· 1 - import { existsSync, rmSync } from 'fs' 2 import { 3 getPdsForDid, 4 downloadAndCacheSite, ··· 13 import { invalidateSiteCache, markSiteAsBeingCached, unmarkSiteAsBeingCached } from './cache' 14 import { clearRedirectRulesCache } from './site-cache' 15 16 - const CACHE_DIR = './cache/sites' 17 18 export class FirehoseWorker { 19 private firehose: Firehose | null = null 20 private idResolver: IdResolver 21 private isShuttingDown = false 22 private lastEventTime = Date.now() 23 private cacheCleanupInterval: NodeJS.Timeout | null = null 24 25 constructor( 26 private logger?: (msg: string, data?: Record<string, unknown>) => void ··· 47 48 this.log('IdResolver cache cleared') 49 }, 60 * 60 * 1000) // Every hour 50 } 51 52 start() { ··· 61 if (this.cacheCleanupInterval) { 62 clearInterval(this.cacheCleanupInterval) 63 this.cacheCleanupInterval = null 64 } 65 66 if (this.firehose) { ··· 80 filterCollections: ['place.wisp.fs', 'place.wisp.settings'], 81 handleEvent: async (evt: any) => { 82 this.lastEventTime = Date.now() 83 84 // Watch for write events 85 if (evt.event === 'create' || evt.event === 'update') { ··· 189 } 190 }) 191 192 - this.firehose.start() 193 - this.log('Firehose started') 194 } 195 196 private async handleCreateOrUpdate( ··· 250 } 251 252 // Invalidate in-memory caches before updating 253 - invalidateSiteCache(did, site) 254 255 // Mark site as being cached to prevent serving stale content during update 256 markSiteAsBeingCached(did, site) ··· 340 }) 341 } 342 343 - // Invalidate in-memory caches 344 - invalidateSiteCache(did, site) 345 - 346 - // Delete disk cache 347 - this.deleteCache(did, site) 348 349 this.log('Successfully processed delete', { did, site }) 350 } ··· 353 this.log('Processing settings change', { did, rkey }) 354 355 // Invalidate in-memory caches (includes metadata which stores settings) 356 - invalidateSiteCache(did, rkey) 357 358 // Check if site is already cached 359 const cacheDir = `${CACHE_DIR}/${did}/${rkey}` ··· 413 } 414 415 this.log('Successfully processed settings change', { did, rkey }) 416 - } 417 - 418 - private deleteCache(did: string, site: string) { 419 - const cacheDir = `${CACHE_DIR}/${did}/${site}` 420 - 421 - if (!existsSync(cacheDir)) { 422 - this.log('Cache directory does not exist, nothing to delete', { 423 - did, 424 - site 425 - }) 426 - return 427 - } 428 - 429 - try { 430 - rmSync(cacheDir, { recursive: true, force: true }) 431 - this.log('Cache deleted', { did, site, path: cacheDir }) 432 - } catch (err) { 433 - this.log('Failed to delete cache', { 434 - did, 435 - site, 436 - path: cacheDir, 437 - error: err instanceof Error ? err.message : String(err) 438 - }) 439 - } 440 } 441 442 getHealth() {
··· 1 + import { existsSync } from 'fs' 2 import { 3 getPdsForDid, 4 downloadAndCacheSite, ··· 13 import { invalidateSiteCache, markSiteAsBeingCached, unmarkSiteAsBeingCached } from './cache' 14 import { clearRedirectRulesCache } from './site-cache' 15 16 + const CACHE_DIR = process.env.CACHE_DIR || './cache/sites' 17 18 export class FirehoseWorker { 19 private firehose: Firehose | null = null 20 private idResolver: IdResolver 21 private isShuttingDown = false 22 private lastEventTime = Date.now() 23 + private eventCount = 0 24 private cacheCleanupInterval: NodeJS.Timeout | null = null 25 + private healthCheckInterval: NodeJS.Timeout | null = null 26 27 constructor( 28 private logger?: (msg: string, data?: Record<string, unknown>) => void ··· 49 50 this.log('IdResolver cache cleared') 51 }, 60 * 60 * 1000) // Every hour 52 + 53 + // Health check: log if no events received for 30 seconds 54 + this.healthCheckInterval = setInterval(() => { 55 + if (this.isShuttingDown) return 56 + 57 + const timeSinceLastEvent = Date.now() - this.lastEventTime 58 + if (timeSinceLastEvent > 30000 && this.eventCount === 0) { 59 + this.log('Warning: No firehose events received in the last 30 seconds', { 60 + timeSinceLastEvent, 61 + eventsReceived: this.eventCount 62 + }) 63 + } else if (timeSinceLastEvent > 60000) { 64 + this.log('Firehose status check', { 65 + timeSinceLastEvent, 66 + eventsReceived: this.eventCount 67 + }) 68 + } 69 + }, 30000) // Every 30 seconds 70 } 71 72 start() { ··· 81 if (this.cacheCleanupInterval) { 82 clearInterval(this.cacheCleanupInterval) 83 this.cacheCleanupInterval = null 84 + } 85 + 86 + if (this.healthCheckInterval) { 87 + clearInterval(this.healthCheckInterval) 88 + this.healthCheckInterval = null 89 } 90 91 if (this.firehose) { ··· 105 filterCollections: ['place.wisp.fs', 'place.wisp.settings'], 106 handleEvent: async (evt: any) => { 107 this.lastEventTime = Date.now() 108 + this.eventCount++ 109 + 110 + if (this.eventCount === 1) { 111 + this.log('First firehose event received - connection established', { 112 + eventType: evt.event, 113 + collection: evt.collection 114 + }) 115 + } 116 117 // Watch for write events 118 if (evt.event === 'create' || evt.event === 'update') { ··· 222 } 223 }) 224 225 + this.firehose.start().catch((err: unknown) => { 226 + this.log('Fatal firehose error', { 227 + error: err instanceof Error ? err.message : String(err) 228 + }) 229 + console.error('Fatal firehose error:', err) 230 + }) 231 + this.log('Firehose starting') 232 } 233 234 private async handleCreateOrUpdate( ··· 288 } 289 290 // Invalidate in-memory caches before updating 291 + await invalidateSiteCache(did, site) 292 293 // Mark site as being cached to prevent serving stale content during update 294 markSiteAsBeingCached(did, site) ··· 378 }) 379 } 380 381 + // Invalidate all caches (tiered storage invalidation is handled by invalidateSiteCache) 382 + await invalidateSiteCache(did, site) 383 384 this.log('Successfully processed delete', { did, site }) 385 } ··· 388 this.log('Processing settings change', { did, rkey }) 389 390 // Invalidate in-memory caches (includes metadata which stores settings) 391 + await invalidateSiteCache(did, rkey) 392 393 // Check if site is already cached 394 const cacheDir = `${CACHE_DIR}/${did}/${rkey}` ··· 448 } 449 450 this.log('Successfully processed settings change', { did, rkey }) 451 } 452 453 getHealth() {
+16
apps/hosting-service/src/lib/html-rewriter.ts
··· 189 `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}'([^']*)'`, 190 'gi' 191 ) 192 193 rewritten = rewritten.replace(doubleQuoteRegex, (match, value) => { 194 const rewrittenValue = rewritePath( ··· 206 documentPath 207 ) 208 return `${attr}='${rewrittenValue}'` 209 }) 210 } 211 }
··· 189 `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}'([^']*)'`, 190 'gi' 191 ) 192 + // Unquoted attributes (valid in HTML5 for values without spaces/special chars) 193 + // Match: attr=value where value starts immediately (no quotes) and continues until space or > 194 + // Use negative lookahead to ensure we don't match quoted attributes 195 + const unquotedRegex = new RegExp( 196 + `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}(?!["'])([^\\s>]+)`, 197 + 'gi' 198 + ) 199 200 rewritten = rewritten.replace(doubleQuoteRegex, (match, value) => { 201 const rewrittenValue = rewritePath( ··· 213 documentPath 214 ) 215 return `${attr}='${rewrittenValue}'` 216 + }) 217 + 218 + rewritten = rewritten.replace(unquotedRegex, (match, value) => { 219 + const rewrittenValue = rewritePath( 220 + value, 221 + normalizedBase, 222 + documentPath 223 + ) 224 + return `${attr}=${rewrittenValue}` 225 }) 226 } 227 }
+1 -1
apps/hosting-service/src/lib/site-cache.ts
··· 42 * Returns true if site is successfully cached, false otherwise 43 */ 44 export async function ensureSiteCached(did: string, rkey: string): Promise<boolean> { 45 - if (isCached(did, rkey)) { 46 return true; 47 } 48
··· 42 * Returns true if site is successfully cached, false otherwise 43 */ 44 export async function ensureSiteCached(did: string, rkey: string): Promise<boolean> { 45 + if (await isCached(did, rkey)) { 46 return true; 47 } 48
+270
apps/hosting-service/src/lib/storage.ts
···
··· 1 + /** 2 + * Tiered storage configuration for wisp-hosting-service 3 + * 4 + * Implements a three-tier caching strategy: 5 + * - Hot (Memory): Instant access for frequently used files (index.html, CSS, JS) 6 + * - Warm (Disk): Local cache with eviction policy 7 + * - Cold (S3/R2): Object storage as source of truth (optional) 8 + * 9 + * When S3 is not configured, falls back to disk-only mode (warm tier acts as source of truth). 10 + * In cache-only mode (non-master nodes), S3 writes are skipped even if configured. 11 + */ 12 + 13 + import { 14 + TieredStorage, 15 + MemoryStorageTier, 16 + DiskStorageTier, 17 + S3StorageTier, 18 + type StorageTier, 19 + type StorageMetadata, 20 + } from 'tiered-storage'; 21 + 22 + const CACHE_DIR = process.env.CACHE_DIR || './cache/sites'; 23 + const HOT_CACHE_SIZE = parseInt(process.env.HOT_CACHE_SIZE || '104857600', 10); // 100MB default 24 + const HOT_CACHE_COUNT = parseInt(process.env.HOT_CACHE_COUNT || '500', 10); 25 + const WARM_CACHE_SIZE = parseInt(process.env.WARM_CACHE_SIZE || '10737418240', 10); // 10GB default 26 + const WARM_EVICTION_POLICY = (process.env.WARM_EVICTION_POLICY || 'lru') as 'lru' | 'fifo' | 'size'; 27 + 28 + // Cache-only mode: skip S3 writes (non-master nodes) 29 + // This is the same flag used to skip database writes 30 + const CACHE_ONLY_MODE = process.env.CACHE_ONLY_MODE === 'true'; 31 + 32 + // S3/Cold tier configuration (optional) 33 + const S3_BUCKET = process.env.S3_BUCKET || ''; 34 + const S3_METADATA_BUCKET = process.env.S3_METADATA_BUCKET; 35 + const S3_REGION = process.env.S3_REGION || 'us-east-1'; 36 + const S3_ENDPOINT = process.env.S3_ENDPOINT; 37 + const S3_FORCE_PATH_STYLE = process.env.S3_FORCE_PATH_STYLE !== 'false'; 38 + const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; 39 + const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; 40 + const S3_PREFIX = process.env.S3_PREFIX || 'sites/'; 41 + 42 + // Identity serializers for raw binary data (no JSON transformation) 43 + // Files are stored as-is without any encoding/decoding 44 + const identitySerialize = async (data: unknown): Promise<Uint8Array> => { 45 + if (data instanceof Uint8Array) return data; 46 + if (data instanceof ArrayBuffer) return new Uint8Array(data); 47 + if (Buffer.isBuffer(data)) return new Uint8Array(data); 48 + // For other types, fall back to JSON (shouldn't happen with file storage) 49 + return new TextEncoder().encode(JSON.stringify(data)); 50 + }; 51 + 52 + const identityDeserialize = async (data: Uint8Array): Promise<unknown> => { 53 + // Return as-is for binary file storage 54 + return data; 55 + }; 56 + 57 + /** 58 + * Read-only wrapper for S3 tier in cache-only mode. 59 + * Allows reads from S3 but skips all writes (for non-master nodes). 60 + */ 61 + class ReadOnlyS3Tier implements StorageTier { 62 + private static hasLoggedWriteSkip = false; 63 + 64 + constructor(private tier: StorageTier) {} 65 + 66 + // Read operations - pass through to underlying tier 67 + async get(key: string) { 68 + return this.tier.get(key); 69 + } 70 + 71 + async getWithMetadata(key: string) { 72 + return this.tier.getWithMetadata?.(key) ?? null; 73 + } 74 + 75 + async getStream(key: string) { 76 + return this.tier.getStream?.(key) ?? null; 77 + } 78 + 79 + async exists(key: string) { 80 + return this.tier.exists(key); 81 + } 82 + 83 + async getMetadata(key: string) { 84 + return this.tier.getMetadata(key); 85 + } 86 + 87 + async *listKeys(prefix?: string) { 88 + yield* this.tier.listKeys(prefix); 89 + } 90 + 91 + async getStats() { 92 + return this.tier.getStats(); 93 + } 94 + 95 + // Write operations - no-op in cache-only mode 96 + async set(key: string, _data: Uint8Array, _metadata: StorageMetadata) { 97 + this.logWriteSkip('set', key); 98 + } 99 + 100 + async setStream(key: string, _stream: NodeJS.ReadableStream, _metadata: StorageMetadata) { 101 + this.logWriteSkip('setStream', key); 102 + } 103 + 104 + async setMetadata(key: string, _metadata: StorageMetadata) { 105 + this.logWriteSkip('setMetadata', key); 106 + } 107 + 108 + async delete(key: string) { 109 + this.logWriteSkip('delete', key); 110 + } 111 + 112 + async deleteMany(keys: string[]) { 113 + this.logWriteSkip('deleteMany', `${keys.length} keys`); 114 + } 115 + 116 + async clear() { 117 + this.logWriteSkip('clear', 'all keys'); 118 + } 119 + 120 + private logWriteSkip(operation: string, key: string) { 121 + // Only log once to avoid spam 122 + if (!ReadOnlyS3Tier.hasLoggedWriteSkip) { 123 + console.log(`[Storage] Cache-only mode: skipping S3 writes (operation: ${operation})`); 124 + ReadOnlyS3Tier.hasLoggedWriteSkip = true; 125 + } 126 + } 127 + } 128 + 129 + /** 130 + * Initialize tiered storage 131 + * Must be called before serving requests 132 + */ 133 + function initializeStorage(): TieredStorage<Uint8Array> { 134 + // Determine cold tier: S3 if configured, otherwise disk acts as cold 135 + let coldTier: StorageTier; 136 + let warmTier: StorageTier | undefined; 137 + 138 + const diskTier = new DiskStorageTier({ 139 + directory: CACHE_DIR, 140 + maxSizeBytes: WARM_CACHE_SIZE, 141 + evictionPolicy: WARM_EVICTION_POLICY, 142 + encodeColons: false, // Preserve colons for readable DID paths on Unix/macOS 143 + }); 144 + 145 + if (S3_BUCKET) { 146 + // Full three-tier setup with S3 as cold storage 147 + const s3Tier = new S3StorageTier({ 148 + bucket: S3_BUCKET, 149 + metadataBucket: S3_METADATA_BUCKET, 150 + region: S3_REGION, 151 + endpoint: S3_ENDPOINT, 152 + forcePathStyle: S3_FORCE_PATH_STYLE, 153 + credentials: 154 + AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 155 + ? { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY } 156 + : undefined, 157 + prefix: S3_PREFIX, 158 + }); 159 + 160 + // In cache-only mode, wrap S3 tier to make it read-only 161 + coldTier = CACHE_ONLY_MODE ? new ReadOnlyS3Tier(s3Tier) : s3Tier; 162 + warmTier = diskTier; 163 + 164 + if (CACHE_ONLY_MODE) { 165 + console.log('[Storage] Cache-only mode: S3 as read-only cold tier (no writes), disk as warm tier'); 166 + } else { 167 + console.log('[Storage] Using S3 as cold tier, disk as warm tier'); 168 + } 169 + } else { 170 + // Disk-only mode: disk tier acts as source of truth (cold) 171 + coldTier = diskTier; 172 + warmTier = undefined; 173 + console.log('[Storage] S3 not configured - using disk-only mode (disk as cold tier)'); 174 + } 175 + 176 + const storage = new TieredStorage<Uint8Array>({ 177 + tiers: { 178 + // Hot tier: In-memory LRU for instant serving 179 + hot: new MemoryStorageTier({ 180 + maxSizeBytes: HOT_CACHE_SIZE, 181 + maxItems: HOT_CACHE_COUNT, 182 + }), 183 + 184 + // Warm tier: Disk-based cache (only when S3 is configured) 185 + warm: warmTier, 186 + 187 + // Cold tier: S3/R2 as source of truth, or disk in disk-only mode 188 + cold: coldTier, 189 + }, 190 + 191 + // Placement rules: determine which tiers each file goes to 192 + placementRules: [ 193 + // Metadata is critical: frequently accessed for cache validity checks 194 + { 195 + pattern: '**/.metadata.json', 196 + tiers: ['hot', 'warm', 'cold'], 197 + }, 198 + 199 + // index.html is critical: write to all tiers for instant serving 200 + { 201 + pattern: '**/index.html', 202 + tiers: ['hot', 'warm', 'cold'], 203 + }, 204 + { 205 + pattern: 'index.html', 206 + tiers: ['hot', 'warm', 'cold'], 207 + }, 208 + 209 + // CSS and JS: eligible for hot tier if accessed frequently 210 + { 211 + pattern: '**/*.{css,js}', 212 + tiers: ['hot', 'warm', 'cold'], 213 + }, 214 + 215 + // Media files: never needed in memory, skip hot tier 216 + { 217 + pattern: '**/*.{jpg,jpeg,png,gif,webp,svg,ico,mp4,webm,mp3,woff,woff2,ttf,eot}', 218 + tiers: ['warm', 'cold'], 219 + }, 220 + 221 + // Default: everything else goes to warm and cold 222 + { 223 + pattern: '**', 224 + tiers: ['warm', 'cold'], 225 + }, 226 + ], 227 + 228 + // IMPORTANT: Compression is disabled at the tiered-storage level 229 + // Text files (HTML, CSS, JS, JSON) are pre-compressed with gzip at the app level 230 + // Binary files (images, video) are stored uncompressed as they're already compressed 231 + // The file's compression state is tracked in customMetadata.encoding 232 + compression: false, 233 + 234 + // TTL for cache entries (14 days) 235 + defaultTTL: 14 * 24 * 60 * 60 * 1000, 236 + 237 + // Eager promotion: promote data to upper tiers on read 238 + // This ensures frequently accessed files end up in hot tier 239 + promotionStrategy: 'eager', 240 + 241 + // Identity serialization: store raw binary without JSON transformation 242 + serialization: { 243 + serialize: identitySerialize, 244 + deserialize: identityDeserialize, 245 + }, 246 + }); 247 + 248 + return storage; 249 + } 250 + 251 + // Export singleton instance 252 + export const storage = initializeStorage(); 253 + 254 + /** 255 + * Get storage configuration summary for logging 256 + */ 257 + export function getStorageConfig() { 258 + return { 259 + cacheDir: CACHE_DIR, 260 + hotCacheSize: `${(HOT_CACHE_SIZE / 1024 / 1024).toFixed(0)}MB`, 261 + hotCacheCount: HOT_CACHE_COUNT, 262 + warmCacheSize: `${(WARM_CACHE_SIZE / 1024 / 1024 / 1024).toFixed(1)}GB`, 263 + warmEvictionPolicy: WARM_EVICTION_POLICY, 264 + s3Bucket: S3_BUCKET, 265 + s3Region: S3_REGION, 266 + s3Endpoint: S3_ENDPOINT || '(default AWS S3)', 267 + s3Prefix: S3_PREFIX, 268 + metadataBucket: S3_METADATA_BUCKET || '(embedded in data bucket)', 269 + }; 270 + }
+473 -8
apps/hosting-service/src/lib/utils.test.ts
··· 1 import { describe, test, expect } from 'bun:test' 2 - import { sanitizePath, extractBlobCid } from './utils' 3 import { CID } from 'multiformats' 4 5 describe('sanitizePath', () => { 6 test('allows normal file paths', () => { ··· 31 32 test('blocks directory traversal in middle of path', () => { 33 expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd') 34 - // Note: sanitizePath only filters out ".." segments, doesn't resolve paths 35 expect(sanitizePath('a/b/../c')).toBe('a/b/c') 36 expect(sanitizePath('a/../b/../c')).toBe('a/b/c') 37 }) ··· 50 }) 51 52 test('blocks null bytes', () => { 53 - // Null bytes cause the entire segment to be filtered out 54 expect(sanitizePath('index.html\0.txt')).toBe('') 55 expect(sanitizePath('test\0')).toBe('') 56 - // Null byte in middle segment 57 expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css') 58 }) 59 ··· 89 90 describe('extractBlobCid', () => { 91 const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 92 - 93 test('extracts CID from IPLD link', () => { 94 const blobRef = { $link: TEST_CID } 95 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 103 }) 104 105 test('extracts CID from typed BlobRef with IPLD link', () => { 106 - const blobRef = { 107 ref: { $link: TEST_CID } 108 } 109 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 129 }) 130 131 test('handles nested structures from AT Proto API', () => { 132 - // Real structure from AT Proto 133 const blobRef = { 134 $type: 'blob', 135 ref: CID.parse(TEST_CID), ··· 150 }) 151 152 test('prioritizes checking IPLD link first', () => { 153 - // Direct $link takes precedence 154 const directLink = { $link: TEST_CID } 155 expect(extractBlobCid(directLink)).toBe(TEST_CID) 156 }) ··· 167 expect(extractBlobCid(blobRef)).toBe(cidV1) 168 }) 169 })
··· 1 import { describe, test, expect } from 'bun:test' 2 + import { sanitizePath, extractBlobCid, extractSubfsUris, expandSubfsNodes } from './utils' 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' 20 21 describe('sanitizePath', () => { 22 test('allows normal file paths', () => { ··· 47 48 test('blocks directory traversal in middle of path', () => { 49 expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd') 50 expect(sanitizePath('a/b/../c')).toBe('a/b/c') 51 expect(sanitizePath('a/../b/../c')).toBe('a/b/c') 52 }) ··· 65 }) 66 67 test('blocks null bytes', () => { 68 expect(sanitizePath('index.html\0.txt')).toBe('') 69 expect(sanitizePath('test\0')).toBe('') 70 expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css') 71 }) 72 ··· 102 103 describe('extractBlobCid', () => { 104 const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 105 + 106 test('extracts CID from IPLD link', () => { 107 const blobRef = { $link: TEST_CID } 108 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 116 }) 117 118 test('extracts CID from typed BlobRef with IPLD link', () => { 119 + const blobRef = { 120 ref: { $link: TEST_CID } 121 } 122 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 142 }) 143 144 test('handles nested structures from AT Proto API', () => { 145 const blobRef = { 146 $type: 'blob', 147 ref: CID.parse(TEST_CID), ··· 162 }) 163 164 test('prioritizes checking IPLD link first', () => { 165 const directLink = { $link: TEST_CID } 166 expect(extractBlobCid(directLink)).toBe(TEST_CID) 167 }) ··· 178 expect(extractBlobCid(blobRef)).toBe(cidV1) 179 }) 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 + })
+177 -170
apps/hosting-service/src/lib/utils.ts
··· 4 import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings'; 5 import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs'; 6 import { writeFile, readFile, rename } from 'fs/promises'; 7 import { safeFetchJson, safeFetchBlob } from '@wisp/safe-fetch'; 8 import { CID } from 'multiformats'; 9 import { extractBlobCid } from '@wisp/atproto-utils'; 10 - import { sanitizePath, collectFileCidsFromEntries } from '@wisp/fs-utils'; 11 import { shouldCompressMimeType } from '@wisp/atproto-utils/compression'; 12 13 // Re-export shared utilities for local usage and tests 14 export { extractBlobCid, sanitizePath }; ··· 89 export async function fetchSiteRecord(did: string, rkey: string): Promise<{ record: WispFsRecord; cid: string } | null> { 90 try { 91 const pdsEndpoint = await getPdsForDid(did); 92 - if (!pdsEndpoint) return null; 93 94 const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(rkey)}`; 95 const data = await safeFetchJson(url); ··· 99 cid: data.cid || '' 100 }; 101 } catch (err) { 102 - console.error('Failed to fetch site record', did, rkey, err); 103 return null; 104 } 105 } ··· 120 } 121 122 /** 123 * Extract all subfs URIs from a directory tree with their mount paths 124 */ 125 - function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { 126 const uris: Array<{ uri: string; path: string }> = []; 127 128 for (const entry of directory.entries) { ··· 182 * Replace subfs nodes in a directory tree with their actual content 183 * Subfs entries are "merged" - their root entries are hoisted into the parent directory 184 * This function is recursive - it will keep expanding until no subfs nodes remain 185 */ 186 - async function expandSubfsNodes(directory: Directory, pdsEndpoint: string, depth: number = 0): Promise<Directory> { 187 const MAX_DEPTH = 10; // Prevent infinite loops 188 189 if (depth >= MAX_DEPTH) { ··· 199 return directory; 200 } 201 202 - console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs records, fetching...`); 203 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 - ); 211 212 - // Build a map of path -> root entries to merge 213 // Note: SubFS entries are compatible with FS entries at runtime 214 const subfsMap = new Map<string, Entry[]>(); 215 - for (const { record, path } of subfsRecords) { 216 if (record && record.root && record.root.entries) { 217 subfsMap.set(path, record.root.entries as unknown as Entry[]); 218 } ··· 280 }; 281 282 // Recursively expand any remaining subfs nodes (e.g., nested subfs inside parent subfs) 283 - return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1); 284 } 285 286 ··· 300 // Expand subfs nodes before caching 301 const expandedRoot = await expandSubfsNodes(record.root, pdsEndpoint); 302 303 // Get existing cache metadata to check for incremental updates 304 const existingMetadata = await getCacheMetadata(did, rkey); 305 const existingFileCids = existingMetadata?.fileCids || {}; 306 307 - // Use a temporary directory with timestamp to avoid collisions 308 - const tempSuffix = `.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; 309 - const tempDir = `${CACHE_DIR}/${did}/${rkey}${tempSuffix}`; 310 - const finalDir = `${CACHE_DIR}/${did}/${rkey}`; 311 312 - try { 313 - // Collect file CIDs from the new record (using expanded root) 314 - const newFileCids: Record<string, string> = {}; 315 - collectFileCidsFromEntries(expandedRoot.entries, '', newFileCids); 316 317 - // Fetch site settings (optional) 318 - const settings = await fetchSiteSettings(did, rkey); 319 - 320 - // Download/copy files to temporary directory (with incremental logic, using expanded root) 321 - await cacheFiles(did, rkey, expandedRoot.entries, pdsEndpoint, '', tempSuffix, existingFileCids, finalDir); 322 - await saveCacheMetadata(did, rkey, recordCid, tempSuffix, newFileCids, settings); 323 324 - // Atomically replace old cache with new cache 325 - // On POSIX systems (Linux/macOS), rename is atomic 326 - if (existsSync(finalDir)) { 327 - // Rename old directory to backup 328 - const backupDir = `${finalDir}.old-${Date.now()}`; 329 - await rename(finalDir, backupDir); 330 - 331 - try { 332 - // Rename new directory to final location 333 - await rename(tempDir, finalDir); 334 - 335 - // Clean up old backup 336 - rmSync(backupDir, { recursive: true, force: true }); 337 - } catch (err) { 338 - // If rename failed, restore backup 339 - if (existsSync(backupDir) && !existsSync(finalDir)) { 340 - await rename(backupDir, finalDir); 341 - } 342 - throw err; 343 - } 344 - } else { 345 - // No existing cache, just rename temp to final 346 - await rename(tempDir, finalDir); 347 - } 348 - 349 - console.log('Successfully cached site atomically', did, rkey); 350 - } catch (err) { 351 - // Clean up temp directory on failure 352 - if (existsSync(tempDir)) { 353 - rmSync(tempDir, { recursive: true, force: true }); 354 - } 355 - throw err; 356 - } 357 } 358 359 ··· 363 entries: Entry[], 364 pdsEndpoint: string, 365 pathPrefix: string, 366 - dirSuffix: string = '', 367 - existingFileCids: Record<string, string> = {}, 368 - existingCacheDir?: string 369 ): Promise<void> { 370 - // Collect file tasks, separating unchanged files from new/changed files 371 const downloadTasks: Array<() => Promise<void>> = []; 372 - const copyTasks: Array<() => Promise<void>> = []; 373 374 function collectFileTasks( 375 entries: Entry[], ··· 386 const cid = extractBlobCid(fileNode.blob); 387 388 // Check if file is unchanged (same CID as existing cache) 389 - if (cid && existingFileCids[currentPath] === cid && existingCacheDir) { 390 - // File unchanged - copy from existing cache instead of downloading 391 - copyTasks.push(() => copyExistingFile( 392 - did, 393 - site, 394 - currentPath, 395 - dirSuffix, 396 - existingCacheDir 397 - )); 398 } else { 399 // File new or changed - download it 400 downloadTasks.push(() => cacheFileBlob( ··· 405 pdsEndpoint, 406 fileNode.encoding, 407 fileNode.mimeType, 408 - fileNode.base64, 409 - dirSuffix 410 )); 411 } 412 } ··· 415 416 collectFileTasks(entries, pathPrefix); 417 418 - console.log(`[Incremental Update] Files to copy: ${copyTasks.length}, Files to download: ${downloadTasks.length}`); 419 - 420 - // Copy unchanged files in parallel (fast local operations) - increased limit for better performance 421 - const copyLimit = 50; 422 - for (let i = 0; i < copyTasks.length; i += copyLimit) { 423 - const batch = copyTasks.slice(i, i + copyLimit); 424 - await Promise.all(batch.map(task => task())); 425 - if (copyTasks.length > copyLimit) { 426 - console.log(`[Cache Progress] Copied ${Math.min(i + copyLimit, copyTasks.length)}/${copyTasks.length} unchanged files`); 427 - } 428 - } 429 430 - // Download new/changed files concurrently - increased from 3 to 20 for much better performance 431 const downloadLimit = 20; 432 let successCount = 0; 433 let failureCount = 0; ··· 456 } 457 } 458 459 - /** 460 - * Copy an unchanged file from existing cache to new cache location 461 - */ 462 - async function copyExistingFile( 463 - did: string, 464 - site: string, 465 - filePath: string, 466 - dirSuffix: string, 467 - existingCacheDir: string 468 - ): Promise<void> { 469 - const { copyFile } = await import('fs/promises'); 470 - 471 - const sourceFile = `${existingCacheDir}/${filePath}`; 472 - const destFile = `${CACHE_DIR}/${did}/${site}${dirSuffix}/${filePath}`; 473 - const destDir = destFile.substring(0, destFile.lastIndexOf('/')); 474 - 475 - // Create destination directory if needed 476 - if (destDir && !existsSync(destDir)) { 477 - mkdirSync(destDir, { recursive: true }); 478 - } 479 - 480 - try { 481 - // Copy the file 482 - await copyFile(sourceFile, destFile); 483 - 484 - // Copy metadata file if it exists 485 - const sourceMetaFile = `${sourceFile}.meta`; 486 - const destMetaFile = `${destFile}.meta`; 487 - if (existsSync(sourceMetaFile)) { 488 - await copyFile(sourceMetaFile, destMetaFile); 489 - } 490 - } catch (err) { 491 - console.error(`Failed to copy cached file ${filePath}, will attempt download:`, err); 492 - throw err; 493 - } 494 - } 495 - 496 async function cacheFileBlob( 497 did: string, 498 site: string, ··· 501 pdsEndpoint: string, 502 encoding?: 'gzip', 503 mimeType?: string, 504 - base64?: boolean, 505 - dirSuffix: string = '' 506 ): Promise<void> { 507 const cid = extractBlobCid(blobRef); 508 if (!cid) { ··· 514 515 console.log(`[Cache] Fetching blob for file: ${filePath}, CID: ${cid}`); 516 517 - // Allow up to 500MB per file blob, with 5 minute timeout 518 - let content = await safeFetchBlob(blobUrl, { maxSize: 500 * 1024 * 1024, timeout: 300000 }); 519 520 // If content is base64-encoded, decode it back to raw binary (gzipped or not) 521 if (base64) { ··· 526 content = Buffer.from(base64String, 'base64'); 527 } 528 529 - const cacheFile = `${CACHE_DIR}/${did}/${site}${dirSuffix}/${filePath}`; 530 - const fileDir = cacheFile.substring(0, cacheFile.lastIndexOf('/')); 531 - 532 - if (fileDir && !existsSync(fileDir)) { 533 - mkdirSync(fileDir, { recursive: true }); 534 - } 535 - 536 // Use the shared function to determine if this should remain compressed 537 const shouldStayCompressed = shouldCompressMimeType(mimeType); 538 ··· 550 } 551 } 552 553 - await writeFile(cacheFile, content); 554 555 - // Store metadata only if file is still compressed 556 if (encoding === 'gzip' && mimeType) { 557 - const metaFile = `${cacheFile}.meta`; 558 - await writeFile(metaFile, JSON.stringify({ encoding, mimeType })); 559 console.log('Cached file', filePath, content.length, 'bytes (gzipped,', mimeType + ')'); 560 } else { 561 console.log('Cached file', filePath, content.length, 'bytes'); ··· 568 return `${CACHE_DIR}/${did}/${site}/${sanitizedPath}`; 569 } 570 571 - export function isCached(did: string, site: string): boolean { 572 - return existsSync(`${CACHE_DIR}/${did}/${site}`); 573 } 574 575 - async function saveCacheMetadata(did: string, rkey: string, recordCid: string, dirSuffix: string = '', fileCids?: Record<string, string>, settings?: WispSettings | null): Promise<void> { 576 const metadata: CacheMetadata = { 577 recordCid, 578 cachedAt: Date.now(), ··· 582 settings: settings || undefined 583 }; 584 585 - const metadataPath = `${CACHE_DIR}/${did}/${rkey}${dirSuffix}/.metadata.json`; 586 - const metadataDir = metadataPath.substring(0, metadataPath.lastIndexOf('/')); 587 - 588 - if (!existsSync(metadataDir)) { 589 - mkdirSync(metadataDir, { recursive: true }); 590 - } 591 - 592 - await writeFile(metadataPath, JSON.stringify(metadata, null, 2)); 593 } 594 595 async function getCacheMetadata(did: string, rkey: string): Promise<CacheMetadata | null> { 596 try { 597 - const metadataPath = `${CACHE_DIR}/${did}/${rkey}/.metadata.json`; 598 - if (!existsSync(metadataPath)) return null; 599 600 - const content = await readFile(metadataPath, 'utf-8'); 601 - return JSON.parse(content) as CacheMetadata; 602 } catch (err) { 603 console.error('Failed to read cache metadata', err); 604 return null; ··· 632 } 633 634 export async function updateCacheMetadataSettings(did: string, rkey: string, settings: WispSettings | null): Promise<void> { 635 - const metadataPath = `${CACHE_DIR}/${did}/${rkey}/.metadata.json`; 636 637 - if (!existsSync(metadataPath)) { 638 - console.warn('Metadata file does not exist, cannot update settings', { did, rkey }); 639 - return; 640 - } 641 - 642 - try { 643 - // Read existing metadata 644 - const content = await readFile(metadataPath, 'utf-8'); 645 - const metadata = JSON.parse(content) as CacheMetadata; 646 647 // Update settings field 648 // Store null explicitly to cache "no settings" state and avoid repeated fetches 649 metadata.settings = settings ?? null; 650 651 - // Write back to disk 652 - await writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'); 653 console.log('Updated metadata settings', { did, rkey, hasSettings: !!settings }); 654 } catch (err) { 655 console.error('Failed to update metadata settings', err);
··· 4 import type { Record as WispSettings } from '@wisp/lexicons/types/place/wisp/settings'; 5 import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs'; 6 import { writeFile, readFile, rename } from 'fs/promises'; 7 + import { Readable } from 'stream'; 8 import { safeFetchJson, safeFetchBlob } from '@wisp/safe-fetch'; 9 import { CID } from 'multiformats'; 10 import { extractBlobCid } from '@wisp/atproto-utils'; 11 + import { sanitizePath, collectFileCidsFromEntries, countFilesInDirectory } from '@wisp/fs-utils'; 12 import { shouldCompressMimeType } from '@wisp/atproto-utils/compression'; 13 + import { MAX_BLOB_SIZE, MAX_FILE_COUNT, MAX_SITE_SIZE } from '@wisp/constants'; 14 + import { storage } from './storage'; 15 16 // Re-export shared utilities for local usage and tests 17 export { extractBlobCid, sanitizePath }; ··· 92 export async function fetchSiteRecord(did: string, rkey: string): Promise<{ record: WispFsRecord; cid: string } | null> { 93 try { 94 const pdsEndpoint = await getPdsForDid(did); 95 + if (!pdsEndpoint) { 96 + console.error('[hosting-service] Failed to get PDS endpoint for DID', { did, rkey }); 97 + return null; 98 + } 99 100 const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(rkey)}`; 101 const data = await safeFetchJson(url); ··· 105 cid: data.cid || '' 106 }; 107 } catch (err) { 108 + const errorCode = (err as any)?.code; 109 + const errorMsg = err instanceof Error ? err.message : String(err); 110 + 111 + // Better error logging to distinguish between network errors and 404s 112 + if (errorMsg.includes('HTTP 404') || errorMsg.includes('Not Found')) { 113 + console.log('[hosting-service] Site record not found', { did, rkey }); 114 + } else if (errorCode && ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR', 'ETIMEDOUT'].includes(errorCode)) { 115 + console.error('[hosting-service] Network/SSL error fetching site record (after retries)', { 116 + did, 117 + rkey, 118 + error: errorMsg, 119 + code: errorCode 120 + }); 121 + } else { 122 + console.error('[hosting-service] Failed to fetch site record', { 123 + did, 124 + rkey, 125 + error: errorMsg, 126 + code: errorCode 127 + }); 128 + } 129 + 130 return null; 131 } 132 } ··· 147 } 148 149 /** 150 + * Calculate total size of all blobs in a directory tree from manifest metadata 151 + */ 152 + function calculateTotalBlobSize(directory: Directory): number { 153 + let totalSize = 0; 154 + 155 + function sumBlobSizes(entries: Entry[]) { 156 + for (const entry of entries) { 157 + const node = entry.node; 158 + 159 + if ('type' in node && node.type === 'directory' && 'entries' in node) { 160 + // Recursively sum subdirectories 161 + sumBlobSizes(node.entries); 162 + } else if ('type' in node && node.type === 'file' && 'blob' in node) { 163 + // Add blob size from manifest 164 + const fileNode = node as File; 165 + const blobSize = (fileNode.blob as any)?.size || 0; 166 + totalSize += blobSize; 167 + } 168 + } 169 + } 170 + 171 + sumBlobSizes(directory.entries); 172 + return totalSize; 173 + } 174 + 175 + /** 176 * Extract all subfs URIs from a directory tree with their mount paths 177 */ 178 + export function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { 179 const uris: Array<{ uri: string; path: string }> = []; 180 181 for (const entry of directory.entries) { ··· 235 * Replace subfs nodes in a directory tree with their actual content 236 * Subfs entries are "merged" - their root entries are hoisted into the parent directory 237 * This function is recursive - it will keep expanding until no subfs nodes remain 238 + * Uses a cache to avoid re-fetching the same subfs records across recursion depths 239 */ 240 + export async function expandSubfsNodes( 241 + directory: Directory, 242 + pdsEndpoint: string, 243 + depth: number = 0, 244 + subfsCache: Map<string, SubfsRecord | null> = new Map() 245 + ): Promise<Directory> { 246 const MAX_DEPTH = 10; // Prevent infinite loops 247 248 if (depth >= MAX_DEPTH) { ··· 258 return directory; 259 } 260 261 + // Filter to only URIs we haven't fetched yet 262 + const uncachedUris = subfsUris.filter(({ uri }) => !subfsCache.has(uri)); 263 264 + if (uncachedUris.length > 0) { 265 + console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, fetching ${uncachedUris.length} new records (${subfsUris.length - uncachedUris.length} cached)...`); 266 267 + // Fetch only uncached subfs records in parallel 268 + const fetchedRecords = await Promise.all( 269 + uncachedUris.map(async ({ uri }) => { 270 + const record = await fetchSubfsRecord(uri, pdsEndpoint); 271 + return { uri, record }; 272 + }) 273 + ); 274 + 275 + // Add fetched records to cache 276 + for (const { uri, record } of fetchedRecords) { 277 + subfsCache.set(uri, record); 278 + } 279 + } else { 280 + console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, all cached`); 281 + } 282 + 283 + // Build a map of path -> root entries to merge using the cache 284 // Note: SubFS entries are compatible with FS entries at runtime 285 const subfsMap = new Map<string, Entry[]>(); 286 + for (const { uri, path } of subfsUris) { 287 + const record = subfsCache.get(uri); 288 if (record && record.root && record.root.entries) { 289 subfsMap.set(path, record.root.entries as unknown as Entry[]); 290 } ··· 352 }; 353 354 // Recursively expand any remaining subfs nodes (e.g., nested subfs inside parent subfs) 355 + // Pass the cache to avoid re-fetching records 356 + return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1, subfsCache); 357 } 358 359 ··· 373 // Expand subfs nodes before caching 374 const expandedRoot = await expandSubfsNodes(record.root, pdsEndpoint); 375 376 + // Verify all subfs nodes were expanded 377 + const remainingSubfs = extractSubfsUris(expandedRoot); 378 + if (remainingSubfs.length > 0) { 379 + console.warn(`[Cache] Warning: ${remainingSubfs.length} subfs nodes remain unexpanded after expansion`, remainingSubfs); 380 + } 381 + 382 + // Validate file count limit 383 + const fileCount = countFilesInDirectory(expandedRoot); 384 + if (fileCount > MAX_FILE_COUNT) { 385 + throw new Error(`Site exceeds file count limit: ${fileCount} files (max ${MAX_FILE_COUNT})`); 386 + } 387 + console.log(`[Cache] File count validation passed: ${fileCount} files (limit: ${MAX_FILE_COUNT})`); 388 + 389 + // Validate total size from blob metadata 390 + const totalBlobSize = calculateTotalBlobSize(expandedRoot); 391 + if (totalBlobSize > MAX_SITE_SIZE) { 392 + throw new Error(`Site exceeds size limit: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (max ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`); 393 + } 394 + console.log(`[Cache] Size validation passed: ${(totalBlobSize / 1024 / 1024).toFixed(2)}MB (limit: ${(MAX_SITE_SIZE / 1024 / 1024).toFixed(0)}MB)`); 395 + 396 // Get existing cache metadata to check for incremental updates 397 const existingMetadata = await getCacheMetadata(did, rkey); 398 const existingFileCids = existingMetadata?.fileCids || {}; 399 400 + // Collect file CIDs from the new record (using expanded root) 401 + const newFileCids: Record<string, string> = {}; 402 + collectFileCidsFromEntries(expandedRoot.entries, '', newFileCids); 403 404 + // Fetch site settings (optional) 405 + const settings = await fetchSiteSettings(did, rkey); 406 407 + // Download files directly to tiered storage (with incremental logic) 408 + await cacheFiles(did, rkey, expandedRoot.entries, pdsEndpoint, '', existingFileCids); 409 + await saveCacheMetadata(did, rkey, recordCid, newFileCids, settings); 410 411 + console.log('Successfully cached site', did, rkey); 412 } 413 414 ··· 418 entries: Entry[], 419 pdsEndpoint: string, 420 pathPrefix: string, 421 + existingFileCids: Record<string, string> = {} 422 ): Promise<void> { 423 + // Collect file download tasks (skip unchanged files) 424 const downloadTasks: Array<() => Promise<void>> = []; 425 426 function collectFileTasks( 427 entries: Entry[], ··· 438 const cid = extractBlobCid(fileNode.blob); 439 440 // Check if file is unchanged (same CID as existing cache) 441 + if (cid && existingFileCids[currentPath] === cid) { 442 + // File unchanged - skip download (already in tiered storage) 443 + console.log(`Skipping unchanged file: ${currentPath}`); 444 } else { 445 // File new or changed - download it 446 downloadTasks.push(() => cacheFileBlob( ··· 451 pdsEndpoint, 452 fileNode.encoding, 453 fileNode.mimeType, 454 + fileNode.base64 455 )); 456 } 457 } ··· 460 461 collectFileTasks(entries, pathPrefix); 462 463 + console.log(`[Incremental Update] Files to download: ${downloadTasks.length}`); 464 465 + // Download new/changed files concurrently 466 const downloadLimit = 20; 467 let successCount = 0; 468 let failureCount = 0; ··· 491 } 492 } 493 494 async function cacheFileBlob( 495 did: string, 496 site: string, ··· 499 pdsEndpoint: string, 500 encoding?: 'gzip', 501 mimeType?: string, 502 + base64?: boolean 503 ): Promise<void> { 504 const cid = extractBlobCid(blobRef); 505 if (!cid) { ··· 511 512 console.log(`[Cache] Fetching blob for file: ${filePath}, CID: ${cid}`); 513 514 + let content = await safeFetchBlob(blobUrl, { maxSize: MAX_BLOB_SIZE, timeout: 300000 }); 515 516 // If content is base64-encoded, decode it back to raw binary (gzipped or not) 517 if (base64) { ··· 522 content = Buffer.from(base64String, 'base64'); 523 } 524 525 // Use the shared function to determine if this should remain compressed 526 const shouldStayCompressed = shouldCompressMimeType(mimeType); 527 ··· 539 } 540 } 541 542 + // Write to tiered storage with metadata 543 + const stream = Readable.from([content]); 544 + const key = `${did}/${site}/${filePath}`; 545 546 + // Build metadata object, only including defined values 547 + const customMetadata: Record<string, string> = {}; 548 + if (encoding) customMetadata.encoding = encoding; 549 + if (mimeType) customMetadata.mimeType = mimeType; 550 + 551 + await storage.setStream(key, stream, { 552 + size: content.length, 553 + skipTiers: ['hot'], // Don't put in memory on ingest, only on access 554 + metadata: customMetadata, 555 + }); 556 + 557 + // Log completion 558 if (encoding === 'gzip' && mimeType) { 559 console.log('Cached file', filePath, content.length, 'bytes (gzipped,', mimeType + ')'); 560 } else { 561 console.log('Cached file', filePath, content.length, 'bytes'); ··· 568 return `${CACHE_DIR}/${did}/${site}/${sanitizedPath}`; 569 } 570 571 + /** 572 + * Check if a site exists in any tier of the cache (without checking metadata) 573 + * This is a quick existence check - for actual retrieval, use storage.get() 574 + */ 575 + export async function isCached(did: string, site: string): Promise<boolean> { 576 + // Check if any file exists for this site by checking for the index.html 577 + // If index.html exists, the site is cached 578 + const indexKey = `${did}/${site}/index.html`; 579 + return await storage.exists(indexKey); 580 } 581 582 + async function saveCacheMetadata(did: string, rkey: string, recordCid: string, fileCids?: Record<string, string>, settings?: WispSettings | null): Promise<void> { 583 const metadata: CacheMetadata = { 584 recordCid, 585 cachedAt: Date.now(), ··· 589 settings: settings || undefined 590 }; 591 592 + // Store through tiered storage for persistence to S3/cold tier 593 + const metadataKey = `${did}/${rkey}/.metadata.json`; 594 + const metadataBytes = new TextEncoder().encode(JSON.stringify(metadata, null, 2)); 595 + await storage.set(metadataKey, metadataBytes); 596 } 597 598 async function getCacheMetadata(did: string, rkey: string): Promise<CacheMetadata | null> { 599 try { 600 + // Retrieve metadata from tiered storage 601 + const metadataKey = `${did}/${rkey}/.metadata.json`; 602 + const data = await storage.get(metadataKey); 603 + 604 + if (!data) return null; 605 606 + // Deserialize from Uint8Array to JSON (storage uses identity serialization) 607 + const jsonString = new TextDecoder().decode(data as Uint8Array); 608 + return JSON.parse(jsonString) as CacheMetadata; 609 } catch (err) { 610 console.error('Failed to read cache metadata', err); 611 return null; ··· 639 } 640 641 export async function updateCacheMetadataSettings(did: string, rkey: string, settings: WispSettings | null): Promise<void> { 642 + try { 643 + // Read existing metadata from tiered storage 644 + const metadata = await getCacheMetadata(did, rkey); 645 646 + if (!metadata) { 647 + console.warn('Metadata does not exist, cannot update settings', { did, rkey }); 648 + return; 649 + } 650 651 // Update settings field 652 // Store null explicitly to cache "no settings" state and avoid repeated fetches 653 metadata.settings = settings ?? null; 654 655 + // Write back through tiered storage 656 + // Convert to Uint8Array since storage is typed for binary data 657 + const metadataKey = `${did}/${rkey}/.metadata.json`; 658 + const metadataBytes = new TextEncoder().encode(JSON.stringify(metadata, null, 2)); 659 + await storage.set(metadataKey, metadataBytes); 660 console.log('Updated metadata settings', { did, rkey, hasSettings: !!settings }); 661 } catch (err) { 662 console.error('Failed to update metadata settings', err);
+4 -1
apps/hosting-service/src/server.ts
··· 80 return c.text('Invalid identifier', 400); 81 } 82 83 // Check if site is currently being cached - return updating response early 84 if (isSiteBeingCached(did, site)) { 85 return siteUpdatingResponse(); ··· 93 94 // Serve with HTML path rewriting to handle absolute paths 95 const basePath = `/${identifier}/${site}/`; 96 const headers = extractHeaders(c.req.raw.headers); 97 return serveFromCacheWithRewrite(did, site, filePath, basePath, c.req.url, headers); 98 } ··· 227 228 app.get('/__internal__/observability/cache', async (c) => { 229 const { getCacheStats } = await import('./lib/cache'); 230 - const stats = getCacheStats(); 231 return c.json({ cache: stats }); 232 }); 233
··· 80 return c.text('Invalid identifier', 400); 81 } 82 83 + console.log(`[Server] sites.wisp.place request: identifier=${identifier}, site=${site}, filePath=${filePath}`); 84 + 85 // Check if site is currently being cached - return updating response early 86 if (isSiteBeingCached(did, site)) { 87 return siteUpdatingResponse(); ··· 95 96 // Serve with HTML path rewriting to handle absolute paths 97 const basePath = `/${identifier}/${site}/`; 98 + console.log(`[Server] Serving with basePath: ${basePath}`); 99 const headers = extractHeaders(c.req.raw.headers); 100 return serveFromCacheWithRewrite(did, site, filePath, basePath, c.req.url, headers); 101 } ··· 230 231 app.get('/__internal__/observability/cache', async (c) => { 232 const { getCacheStats } = await import('./lib/cache'); 233 + const stats = await getCacheStats(); 234 return c.json({ cache: stats }); 235 }); 236
+11 -7
apps/main-app/package.json
··· 7 "dev": "bun run --watch src/index.ts", 8 "start": "bun run src/index.ts", 9 "build": "bun run build.ts", 10 "screenshot": "bun run scripts/screenshot-sites.ts" 11 }, 12 "dependencies": { 13 - "@atproto/api": "^0.17.3", 14 - "@atproto/common-web": "^0.4.5", 15 "@atproto/jwk-jose": "^0.1.11", 16 - "@atproto/lex-cli": "^0.9.5", 17 - "@atproto/oauth-client-node": "^0.3.9", 18 - "@atproto/xrpc-server": "^0.9.5", 19 "@elysiajs/cors": "^1.4.0", 20 "@elysiajs/eden": "^1.4.3", 21 "@elysiajs/openapi": "^1.4.11", ··· 35 "@wisp/lexicons": "workspace:*", 36 "@wisp/observability": "workspace:*", 37 "actor-typeahead": "^0.1.1", 38 - "atproto-ui": "^0.11.3", 39 "bun-plugin-tailwind": "^0.1.2", 40 "class-variance-authority": "^0.7.1", 41 "clsx": "^2.1.1", 42 - "elysia": "latest", 43 "ignore": "^7.0.5", 44 "iron-session": "^8.0.4", 45 "lucide-react": "^0.546.0", ··· 53 "zlib": "^1.0.5" 54 }, 55 "devDependencies": { 56 "@types/react": "^19.2.2", 57 "@types/react-dom": "^19.2.1", 58 "bun-types": "latest",
··· 7 "dev": "bun run --watch src/index.ts", 8 "start": "bun run src/index.ts", 9 "build": "bun run build.ts", 10 + "check": "tsc --noEmit", 11 "screenshot": "bun run scripts/screenshot-sites.ts" 12 }, 13 "dependencies": { 14 + "@atproto-labs/did-resolver": "^0.2.4", 15 + "@atproto/api": "^0.17.7", 16 + "@atproto/common-web": "^0.4.6", 17 "@atproto/jwk-jose": "^0.1.11", 18 + "@atproto/lex-cli": "^0.9.7", 19 + "@atproto/oauth-client-node": "^0.3.12", 20 + "@atproto/xrpc-server": "^0.9.6", 21 "@elysiajs/cors": "^1.4.0", 22 "@elysiajs/eden": "^1.4.3", 23 "@elysiajs/openapi": "^1.4.11", ··· 37 "@wisp/lexicons": "workspace:*", 38 "@wisp/observability": "workspace:*", 39 "actor-typeahead": "^0.1.1", 40 + "atproto-ui": "^0.12.0", 41 "bun-plugin-tailwind": "^0.1.2", 42 "class-variance-authority": "^0.7.1", 43 "clsx": "^2.1.1", 44 + "elysia": "^1.4.18", 45 "ignore": "^7.0.5", 46 "iron-session": "^8.0.4", 47 "lucide-react": "^0.546.0", ··· 55 "zlib": "^1.0.5" 56 }, 57 "devDependencies": { 58 + "@atproto-labs/handle-resolver": "^0.3.4", 59 + "@atproto/did": "^0.2.3", 60 "@types/react": "^19.2.2", 61 "@types/react-dom": "^19.2.1", 62 "bun-types": "latest",
+4 -4
apps/main-app/public/acceptable-use/acceptable-use.tsx
··· 6 7 function AcceptableUsePage() { 8 return ( 9 - <div className="min-h-screen bg-background"> 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"> 13 <div className="flex items-center gap-2"> 14 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 15 <span className="text-xl font-semibold text-foreground"> ··· 326 </div> 327 328 {/* Footer */} 329 - <footer className="border-t border-border/40 bg-muted/20 mt-12"> 330 <div className="container mx-auto px-4 py-8"> 331 <div className="text-center text-sm text-muted-foreground"> 332 <p>
··· 6 7 function AcceptableUsePage() { 8 return ( 9 + <div className="w-full min-h-screen bg-background flex flex-col"> 10 {/* Header */} 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 <div className="flex items-center gap-2"> 14 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 15 <span className="text-xl font-semibold text-foreground"> ··· 326 </div> 327 328 {/* Footer */} 329 + <footer className="border-t border-border/40 bg-muted/20 mt-auto"> 330 <div className="container mx-auto px-4 py-8"> 331 <div className="text-center text-sm text-muted-foreground"> 332 <p>
+1 -1
apps/main-app/public/components/ui/checkbox.tsx
··· 12 <CheckboxPrimitive.Root 13 data-slot="checkbox" 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", 16 className 17 )} 18 {...props}
··· 12 <CheckboxPrimitive.Root 13 data-slot="checkbox" 14 className={cn( 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 className 17 )} 18 {...props}
+6 -6
apps/main-app/public/editor/editor.tsx
··· 302 return ( 303 <div className="w-full min-h-screen bg-background"> 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"> 307 <div className="flex items-center gap-2"> 308 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 309 <span className="text-xl font-semibold text-foreground"> ··· 366 } 367 368 return ( 369 - <div className="w-full min-h-screen bg-background"> 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"> 373 <div className="flex items-center gap-2"> 374 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 375 <span className="text-xl font-semibold text-foreground"> ··· 454 </div> 455 456 {/* Footer */} 457 - <footer className="border-t border-border/40 bg-muted/20 mt-12"> 458 <div className="container mx-auto px-4 py-8"> 459 <div className="text-center text-sm text-muted-foreground"> 460 <p>
··· 302 return ( 303 <div className="w-full min-h-screen bg-background"> 304 {/* Header Skeleton */} 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 <div className="flex items-center gap-2"> 308 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 309 <span className="text-xl font-semibold text-foreground"> ··· 366 } 367 368 return ( 369 + <div className="w-full min-h-screen bg-background flex flex-col"> 370 {/* Header */} 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 <div className="flex items-center gap-2"> 374 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 375 <span className="text-xl font-semibold text-foreground"> ··· 454 </div> 455 456 {/* Footer */} 457 + <footer className="border-t border-border/40 bg-muted/20 mt-auto"> 458 <div className="container mx-auto px-4 py-8"> 459 <div className="text-center text-sm text-muted-foreground"> 460 <p>
+74 -66
apps/main-app/public/index.tsx
··· 88 89 const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { 90 const navigationKeys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Enter', 'Escape'] 91 - 92 // Mark that we should preserve the index for navigation keys 93 if (navigationKeys.includes(e.key)) { 94 preserveIndexRef.current = true ··· 142 setIndex(-1) 143 setIsOpen(false) 144 onSelect?.(handle) 145 - 146 // Auto-submit the form if enabled 147 if (autoSubmit && inputRef.current) { 148 const form = inputRef.current.closest('form') ··· 236 height: 'calc(1.5rem + 12px)', 237 borderRadius: '4px', 238 cursor: 'pointer', 239 - backgroundColor: i === index ? 'hsl(var(--accent) / 0.5)' : 'transparent', 240 transition: 'background-color 0.1s' 241 }} 242 onMouseEnter={() => setIndex(i)} ··· 246 width: '1.5rem', 247 height: '1.5rem', 248 borderRadius: '50%', 249 - backgroundColor: 'hsl(var(--muted))', 250 overflow: 'hidden', 251 flexShrink: 0 252 }} ··· 255 <img 256 src={actor.avatar} 257 alt="" 258 style={{ 259 display: 'block', 260 width: '100%', ··· 359 360 return ( 361 <> 362 - <div className="min-h-screen"> 363 {/* 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"> 366 <div className="flex items-center gap-2"> 367 <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 wisp.place 370 </span> 371 </div> 372 - <div className="flex items-center gap-3"> 373 <Button 374 - variant="ghost" 375 size="sm" 376 onClick={() => setShowForm(true)} 377 > 378 Sign In 379 </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 </div> 390 </div> 391 </header> 392 393 {/* Hero Section */} 394 - <section className="container mx-auto px-4 py-20 md:py-32"> 395 <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. 405 </h1> 406 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. 411 </p> 412 413 - <div className="max-w-md mx-auto relative"> 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 - }`} 420 > 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> 429 </div> 430 431 <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 - }`} 437 > 438 <form 439 onSubmit={async (e) => { ··· 494 </ActorTypeahead> 495 <button 496 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" 498 > 499 Continue 500 <ArrowRight className="ml-2 w-5 h-5" /> ··· 518 </div> 519 <div> 520 <h3 className="text-xl font-semibold mb-2"> 521 - Upload your static site 522 </h3> 523 <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. 527 </p> 528 </div> 529 </div> ··· 533 </div> 534 <div> 535 <h3 className="text-xl font-semibold mb-2"> 536 - We serve it globally 537 </h3> 538 <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. 542 </p> 543 </div> 544 </div> ··· 548 </div> 549 <div> 550 <h3 className="text-xl font-semibold mb-2"> 551 - You stay in control 552 </h3> 553 <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. 557 </p> 558 </div> 559 </div> ··· 686 </section> 687 688 {/* Footer */} 689 - <footer className="border-t border-border/40 bg-muted/20"> 690 <div className="container mx-auto px-4 py-8"> 691 <div className="text-center text-sm text-muted-foreground"> 692 <p>
··· 88 89 const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { 90 const navigationKeys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Enter', 'Escape'] 91 + 92 // Mark that we should preserve the index for navigation keys 93 if (navigationKeys.includes(e.key)) { 94 preserveIndexRef.current = true ··· 142 setIndex(-1) 143 setIsOpen(false) 144 onSelect?.(handle) 145 + 146 // Auto-submit the form if enabled 147 if (autoSubmit && inputRef.current) { 148 const form = inputRef.current.closest('form') ··· 236 height: 'calc(1.5rem + 12px)', 237 borderRadius: '4px', 238 cursor: 'pointer', 239 + backgroundColor: i === index ? 'color-mix(in oklch, var(--accent) 50%, transparent)' : 'transparent', 240 transition: 'background-color 0.1s' 241 }} 242 onMouseEnter={() => setIndex(i)} ··· 246 width: '1.5rem', 247 height: '1.5rem', 248 borderRadius: '50%', 249 + backgroundColor: 'var(--muted)', 250 overflow: 'hidden', 251 flexShrink: 0 252 }} ··· 255 <img 256 src={actor.avatar} 257 alt="" 258 + loading="lazy" 259 style={{ 260 display: 'block', 261 width: '100%', ··· 360 361 return ( 362 <> 363 + <div className="w-full min-h-screen flex flex-col"> 364 {/* Header */} 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"> 367 <div className="flex items-center gap-2"> 368 <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 369 + <span className="text-lg font-semibold text-foreground"> 370 wisp.place 371 </span> 372 </div> 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> 382 <Button 383 + variant="outline" 384 size="sm" 385 + className="btn-hover-lift" 386 onClick={() => setShowForm(true)} 387 > 388 Sign In 389 </Button> 390 </div> 391 </div> 392 </header> 393 394 {/* Hero Section */} 395 + <section className="container mx-auto px-4 py-24 md:py-36"> 396 <div className="max-w-4xl mx-auto text-center"> 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. 403 </h1> 404 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. 410 </p> 411 412 + {/* CTA Buttons */} 413 + <div className="animate-fade-in-up animate-delay-400 max-w-lg mx-auto relative"> 414 <div 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 + }`} 419 > 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> 441 </div> 442 443 <div 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 + }`} 448 > 449 <form 450 onSubmit={async (e) => { ··· 505 </ActorTypeahead> 506 <button 507 type="submit" 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" 509 > 510 Continue 511 <ArrowRight className="ml-2 w-5 h-5" /> ··· 529 </div> 530 <div> 531 <h3 className="text-xl font-semibold mb-2"> 532 + Drop in your files 533 </h3> 534 <p className="text-muted-foreground"> 535 + Upload your site through our dashboard or push with the CLI. 536 + Everything gets stored directly in your AT Protocol account. 537 </p> 538 </div> 539 </div> ··· 543 </div> 544 <div> 545 <h3 className="text-xl font-semibold mb-2"> 546 + We handle the rest 547 </h3> 548 <p className="text-muted-foreground"> 549 + Your site goes live instantly on our global CDN. 550 + Custom domains, HTTPS, cachingโ€”all automatic. 551 </p> 552 </div> 553 </div> ··· 557 </div> 558 <div> 559 <h3 className="text-xl font-semibold mb-2"> 560 + Push updates instantly 561 </h3> 562 <p className="text-muted-foreground"> 563 + Ship changes in seconds. Update through the dashboard, 564 + run wisp-cli deploy, or wire up your CI/CD pipeline. 565 </p> 566 </div> 567 </div> ··· 694 </section> 695 696 {/* Footer */} 697 + <footer className="border-t border-border/40 bg-muted/20 mt-auto"> 698 <div className="container mx-auto px-4 py-8"> 699 <div className="text-center text-sm text-muted-foreground"> 700 <p>
+16 -19
apps/main-app/public/onboarding/onboarding.tsx
··· 161 return ( 162 <div className="w-full min-h-screen bg-background"> 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"> 166 <div className="flex items-center gap-2"> 167 <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> 168 <Globe className="w-5 h-5 text-primary-foreground" /> ··· 179 <div className="mb-8"> 180 <div className="flex items-center justify-center gap-2 mb-4"> 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 - }`} 187 > 188 {step === 'domain' ? ( 189 '1' ··· 193 </div> 194 <div className="w-16 h-0.5 bg-border"></div> 195 <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 - }`} 203 > 204 {step === 'complete' ? ( 205 <CheckCircle2 className="w-5 h-5" /> ··· 258 {!isCheckingAvailability && 259 isAvailable !== null && ( 260 <div 261 - className={`absolute right-3 top-1/2 -translate-y-1/2 ${ 262 - isAvailable 263 - ? 'text-green-500' 264 - : 'text-red-500' 265 - }`} 266 > 267 {isAvailable ? 'โœ“' : 'โœ—'} 268 </div>
··· 161 return ( 162 <div className="w-full min-h-screen bg-background"> 163 {/* Header */} 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 <div className="flex items-center gap-2"> 167 <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> 168 <Globe className="w-5 h-5 text-primary-foreground" /> ··· 179 <div className="mb-8"> 180 <div className="flex items-center justify-center gap-2 mb-4"> 181 <div 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 + }`} 186 > 187 {step === 'domain' ? ( 188 '1' ··· 192 </div> 193 <div className="w-16 h-0.5 bg-border"></div> 194 <div 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 + }`} 201 > 202 {step === 'complete' ? ( 203 <CheckCircle2 className="w-5 h-5" /> ··· 256 {!isCheckingAvailability && 257 isAvailable !== null && ( 258 <div 259 + className={`absolute right-3 top-1/2 -translate-y-1/2 ${isAvailable 260 + ? 'text-green-500' 261 + : 'text-red-500' 262 + }`} 263 > 264 {isAvailable ? 'โœ“' : 'โœ—'} 265 </div>
+212 -39
apps/main-app/public/styles/global.css
··· 6 :root { 7 color-scheme: light; 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); 13 14 - /* Slightly lighter card background */ 15 - --card: oklch(0.93 0.01 35); 16 - --card-foreground: oklch(0.18 0.01 30); 17 18 - --popover: oklch(0.93 0.01 35); 19 - --popover-foreground: oklch(0.18 0.01 30); 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); 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); 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); 32 33 - /* Light warm muted background */ 34 --muted: oklch(0.88 0.01 35); 35 - --muted-foreground: oklch(0.42 0.015 30); 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); 40 41 - --destructive: oklch(0.577 0.245 27.325); 42 - --destructive-foreground: oklch(0.985 0 0); 43 44 - --chart-1: oklch(0.78 0.15 345); 45 --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); 49 50 --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); 59 } 60 61 .dark { ··· 160 * { 161 @apply border-border outline-ring/50; 162 } 163 body { 164 @apply bg-background text-foreground; 165 } 166 } 167 168 @keyframes arrow-bounce { 169 - 0%, 100% { 170 transform: translateX(0); 171 } 172 50% { 173 transform: translateX(4px); 174 } ··· 189 border-radius: 0.5rem; 190 padding: 1rem; 191 overflow-x: auto; 192 - border: 1px solid hsl(var(--border)); 193 } 194 195 .shiki-wrapper pre { 196 margin: 0 !important; 197 padding: 0 !important; 198 }
··· 6 :root { 7 color-scheme: light; 8 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 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 18 + --popover: oklch(0.96 0.006 35); 19 + --popover-foreground: oklch(0.15 0.015 30); 20 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 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 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 33 + /* Muted areas with better distinction */ 34 --muted: oklch(0.88 0.01 35); 35 + --muted-foreground: oklch(0.35 0.02 30); 36 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); 42 43 + --destructive: oklch(0.50 0.20 25); 44 + --destructive-foreground: oklch(0.98 0 0); 45 46 + --chart-1: oklch(0.65 0.18 345); 47 --chart-2: oklch(0.32 0.04 285); 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); 51 52 --radius: 0.75rem; 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); 61 } 62 63 .dark { ··· 162 * { 163 @apply border-border outline-ring/50; 164 } 165 + 166 + html { 167 + scrollbar-gutter: stable; 168 + } 169 + 170 body { 171 @apply bg-background text-foreground; 172 } 173 } 174 175 @keyframes arrow-bounce { 176 + 177 + 0%, 178 + 100% { 179 transform: translateX(0); 180 } 181 + 182 50% { 183 transform: translateX(4px); 184 } ··· 199 border-radius: 0.5rem; 200 padding: 1rem; 201 overflow-x: auto; 202 + border: 1px solid var(--border); 203 } 204 205 .shiki-wrapper pre { 206 margin: 0 !important; 207 padding: 0 !important; 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 cleanupExpiredSessions, 13 rotateKeysIfNeeded 14 } from './lib/oauth-client' 15 - import { getCookieSecret } from './lib/db' 16 import { authRoutes } from './routes/auth' 17 import { wispRoutes } from './routes/wisp' 18 import { domainRoutes } from './routes/domain' ··· 20 import { siteRoutes } from './routes/site' 21 import { csrfProtection } from './lib/csrf' 22 import { DNSVerificationWorker } from './lib/dns-verification-worker' 23 - import { createLogger, logCollector } from '@wisp/observability' 24 import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 25 import { promptAdminSetup } from './lib/admin-auth' 26 import { adminRoutes } from './routes/admin' 27 28 const logger = createLogger('main-app') 29 ··· 55 setInterval(runMaintenance, 60 * 60 * 1000) 56 57 // Start DNS verification worker (runs every 10 minutes) 58 const dnsVerifier = new DNSVerificationWorker( 59 10 * 60 * 1000, // 10 minutes 60 (msg, data) => { ··· 62 } 63 ) 64 65 - dnsVerifier.start() 66 - logger.info('DNS Verifier Started - checking custom domains every 10 minutes') 67 68 export const app = new Elysia({ 69 serve: { ··· 194 console.log( 195 `๐ŸฆŠ Elysia is running at ${app.server?.hostname}:${app.server?.port}` 196 )
··· 12 cleanupExpiredSessions, 13 rotateKeysIfNeeded 14 } from './lib/oauth-client' 15 + import { getCookieSecret, closeDatabase } from './lib/db' 16 import { authRoutes } from './routes/auth' 17 import { wispRoutes } from './routes/wisp' 18 import { domainRoutes } from './routes/domain' ··· 20 import { siteRoutes } from './routes/site' 21 import { csrfProtection } from './lib/csrf' 22 import { DNSVerificationWorker } from './lib/dns-verification-worker' 23 + import { createLogger, logCollector, initializeGrafanaExporters } from '@wisp/observability' 24 import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 25 import { promptAdminSetup } from './lib/admin-auth' 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 + }) 33 34 const logger = createLogger('main-app') 35 ··· 61 setInterval(runMaintenance, 60 * 60 * 1000) 62 63 // Start DNS verification worker (runs every 10 minutes) 64 + // Can be disabled via DISABLE_DNS_WORKER=true environment variable 65 const dnsVerifier = new DNSVerificationWorker( 66 10 * 60 * 1000, // 10 minutes 67 (msg, data) => { ··· 69 } 70 ) 71 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 + } 78 79 export const app = new Elysia({ 80 serve: { ··· 205 console.log( 206 `๐ŸฆŠ Elysia is running at ${app.server?.hostname}:${app.server?.port}` 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 console.log('[CookieSecret] Generated new cookie signing secret'); 527 return secret; 528 };
··· 526 console.log('[CookieSecret] Generated new cookie signing secret'); 527 return secret; 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 + };
+5 -4
apps/main-app/src/lib/oauth-client.ts
··· 4 import { logger } from "./logger"; 5 import { SlingshotHandleResolver } from "./slingshot-handle-resolver"; 6 7 // Session timeout configuration (30 days in seconds) 8 const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds 9 // OAuth state timeout (1 hour in seconds) ··· 110 // Loopback client for local development 111 // For loopback, scopes and redirect_uri must be in client_id query string 112 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 const params = new URLSearchParams(); 115 params.append('redirect_uri', redirectUri); 116 - params.append('scope', scope); 117 118 return { 119 client_id: `http://localhost?${params.toString()}`, ··· 124 response_types: ['code'], 125 application_type: 'web', 126 token_endpoint_auth_method: 'none', 127 - scope: scope, 128 dpop_bound_access_tokens: false, 129 subject_type: 'public', 130 authorization_signed_response_alg: 'ES256' ··· 145 application_type: 'web', 146 token_endpoint_auth_method: 'private_key_jwt', 147 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 dpop_bound_access_tokens: true, 150 jwks_uri: `${config.domain}/jwks.json`, 151 subject_type: 'public',
··· 4 import { logger } from "./logger"; 5 import { SlingshotHandleResolver } from "./slingshot-handle-resolver"; 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:*/*'; 9 // Session timeout configuration (30 days in seconds) 10 const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds 11 // OAuth state timeout (1 hour in seconds) ··· 112 // Loopback client for local development 113 // For loopback, scopes and redirect_uri must be in client_id query string 114 const redirectUri = 'http://127.0.0.1:8000/api/auth/callback'; 115 const params = new URLSearchParams(); 116 params.append('redirect_uri', redirectUri); 117 + params.append('scope', OAUTH_SCOPE); 118 119 return { 120 client_id: `http://localhost?${params.toString()}`, ··· 125 response_types: ['code'], 126 application_type: 'web', 127 token_endpoint_auth_method: 'none', 128 + scope: OAUTH_SCOPE, 129 dpop_bound_access_tokens: false, 130 subject_type: 'public', 131 authorization_signed_response_alg: 'ES256' ··· 146 application_type: 'web', 147 token_endpoint_auth_method: 'private_key_jwt', 148 token_endpoint_auth_signing_alg: "ES256", 149 + scope: OAUTH_SCOPE, 150 dpop_bound_access_tokens: true, 151 jwks_uri: `${config.domain}/jwks.json`, 152 subject_type: 'public',
+10 -12
apps/main-app/src/routes/user.ts
··· 1 import { Elysia, t } from 'elysia' 2 import { requireAuth } from '../lib/wisp-auth' 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 - import { Agent } from '@atproto/api' 5 import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db' 6 import { syncSitesFromPDS } from '../lib/sync-sites' 7 import { createLogger } from '@wisp/observability' 8 9 const logger = createLogger('main-app') 10 11 export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => 12 new Elysia({ ··· 42 }) 43 .get('/info', async ({ auth }) => { 44 try { 45 - // Get user's handle from AT Protocol 46 - const agent = new Agent(auth.session) 47 - 48 let handle = 'unknown' 49 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 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) 59 } 60 61 return {
··· 1 import { Elysia, t } from 'elysia' 2 import { requireAuth } from '../lib/wisp-auth' 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db' 5 import { syncSitesFromPDS } from '../lib/sync-sites' 6 import { createLogger } from '@wisp/observability' 7 + import { createDidResolver, extractAtprotoData } from '@atproto-labs/did-resolver' 8 9 const logger = createLogger('main-app') 10 + const didResolver = createDidResolver({}) 11 12 export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => 13 new Elysia({ ··· 43 }) 44 .get('/info', async ({ auth }) => { 45 try { 46 let handle = 'unknown' 47 try { 48 + const didDoc = await didResolver.resolve(auth.did) 49 + const atprotoData = extractAtprotoData(didDoc) 50 + 51 + if (atprotoData.aka) { 52 + handle = atprotoData.aka 53 + } 54 } catch (err) { 55 + 56 + logger.error('[User] Failed to resolve DID', err) 57 } 58 59 return {
+10 -16
apps/main-app/src/routes/wisp.ts
··· 39 40 const logger = createLogger('main-app') 41 42 - function isValidSiteName(siteName: string): boolean { 43 if (!siteName || typeof siteName !== 'string') return false; 44 45 // Length check (AT Protocol rkey limit) ··· 183 continue; 184 } 185 186 - console.log(`Processing file ${i + 1}/${fileArray.length}:`, file.name, file.size, 'bytes'); 187 updateJobProgress(jobId, { 188 filesProcessed: i + 1, 189 - currentFile: file.name 190 }); 191 192 // Skip files that match ignore patterns 193 - const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 194 195 if (shouldIgnore(ignoreMatcher, normalizedPath)) { 196 - console.log(`Skipping ignored file: ${file.name}`); 197 skippedFiles.push({ 198 - name: file.name, 199 reason: 'matched ignore pattern' 200 }); 201 continue; ··· 205 const maxSize = MAX_FILE_SIZE; 206 if (file.size > maxSize) { 207 skippedFiles.push({ 208 - name: file.name, 209 reason: `file too large (${(file.size / 1024 / 1024).toFixed(2)}MB, max 100MB)` 210 }); 211 continue; ··· 238 // Text files: compress AND base64 encode 239 finalContent = Buffer.from(compressedContent.toString('base64'), 'binary'); 240 base64Encoded = true; 241 - const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1); 242 - console.log(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`); 243 - logger.info(`Compressing+base64 ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${finalContent.length} bytes`); 244 } else { 245 // Audio files: just compress, no base64 246 finalContent = compressedContent; 247 - const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1); 248 - console.log(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`); 249 - logger.info(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%)`); 250 } 251 } else { 252 // Binary files: upload directly 253 finalContent = originalContent; 254 - console.log(`Uploading ${file.name} directly: ${originalContent.length} bytes (no compression)`); 255 - logger.info(`Uploading ${file.name} directly: ${originalContent.length} bytes (binary)`); 256 } 257 258 uploadedFiles.push({ 259 - name: file.name, 260 content: finalContent, 261 mimeType: originalMimeType, 262 size: finalContent.length,
··· 39 40 const logger = createLogger('main-app') 41 42 + export function isValidSiteName(siteName: string): boolean { 43 if (!siteName || typeof siteName !== 'string') return false; 44 45 // Length check (AT Protocol rkey limit) ··· 183 continue; 184 } 185 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 + 190 updateJobProgress(jobId, { 191 filesProcessed: i + 1, 192 + currentFile: filePath 193 }); 194 195 // Skip files that match ignore patterns 196 + const normalizedPath = filePath.replace(/^[^\/]*\//, ''); 197 198 if (shouldIgnore(ignoreMatcher, normalizedPath)) { 199 skippedFiles.push({ 200 + name: filePath, 201 reason: 'matched ignore pattern' 202 }); 203 continue; ··· 207 const maxSize = MAX_FILE_SIZE; 208 if (file.size > maxSize) { 209 skippedFiles.push({ 210 + name: filePath, 211 reason: `file too large (${(file.size / 1024 / 1024).toFixed(2)}MB, max 100MB)` 212 }); 213 continue; ··· 240 // Text files: compress AND base64 encode 241 finalContent = Buffer.from(compressedContent.toString('base64'), 'binary'); 242 base64Encoded = true; 243 } else { 244 // Audio files: just compress, no base64 245 finalContent = compressedContent; 246 } 247 } else { 248 // Binary files: upload directly 249 finalContent = originalContent; 250 } 251 252 uploadedFiles.push({ 253 + name: filePath, 254 content: finalContent, 255 mimeType: originalMimeType, 256 size: finalContent.length,
+59
backup.nix
···
··· 1 + { 2 + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 3 + inputs.nci.url = "github:90-008/nix-cargo-integration"; 4 + inputs.nci.inputs.nixpkgs.follows = "nixpkgs"; 5 + inputs.parts.url = "github:hercules-ci/flake-parts"; 6 + inputs.parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 7 + inputs.fenix = { 8 + url = "github:nix-community/fenix"; 9 + inputs.nixpkgs.follows = "nixpkgs"; 10 + }; 11 + 12 + outputs = inputs @ { 13 + parts, 14 + nci, 15 + ... 16 + }: 17 + parts.lib.mkFlake {inherit inputs;} { 18 + systems = ["x86_64-linux" "aarch64-darwin"]; 19 + imports = [ 20 + nci.flakeModule 21 + ./crates.nix 22 + ]; 23 + perSystem = { 24 + pkgs, 25 + config, 26 + ... 27 + }: let 28 + crateOutputs = config.nci.outputs."wisp-cli"; 29 + mkRenamedPackage = name: pkg: isWindows: pkgs.runCommand name {} '' 30 + mkdir -p $out/bin 31 + if [ -f ${pkg}/bin/wisp-cli.exe ]; then 32 + cp ${pkg}/bin/wisp-cli.exe $out/bin/${name} 33 + elif [ -f ${pkg}/bin/wisp-cli ]; then 34 + cp ${pkg}/bin/wisp-cli $out/bin/${name} 35 + else 36 + echo "Error: Could not find wisp-cli binary in ${pkg}/bin/" 37 + ls -la ${pkg}/bin/ || true 38 + exit 1 39 + fi 40 + ''; 41 + in { 42 + devShells.default = crateOutputs.devShell; 43 + packages.default = crateOutputs.packages.release; 44 + packages.wisp-cli-x86_64-linux = mkRenamedPackage "wisp-cli-x86_64-linux" crateOutputs.packages.release false; 45 + packages.wisp-cli-aarch64-linux = mkRenamedPackage "wisp-cli-aarch64-linux" crateOutputs.allTargets."aarch64-unknown-linux-gnu".packages.release false; 46 + packages.wisp-cli-x86_64-windows = mkRenamedPackage "wisp-cli-x86_64-windows.exe" crateOutputs.allTargets."x86_64-pc-windows-gnu".packages.release true; 47 + packages.wisp-cli-aarch64-darwin = mkRenamedPackage "wisp-cli-aarch64-darwin" crateOutputs.allTargets."aarch64-apple-darwin".packages.release false; 48 + packages.all = pkgs.symlinkJoin { 49 + name = "wisp-cli-all"; 50 + paths = [ 51 + config.packages.wisp-cli-x86_64-linux 52 + config.packages.wisp-cli-aarch64-linux 53 + config.packages.wisp-cli-x86_64-windows 54 + config.packages.wisp-cli-aarch64-darwin 55 + ]; 56 + }; 57 + }; 58 + }; 59 + }
+9 -2
binaries/index.html
··· 51 transition: background-color 200ms ease, color 200ms ease; 52 font-family: system-ui, sans-serif; 53 line-height: 1.6; 54 } 55 56 .container { 57 max-width: 860px; 58 margin: 40px auto; 59 padding: 0 20px; 60 - min-height: 100vh; 61 } 62 63 h1 { ··· 224 } 225 226 .footer { 227 - margin-top: 3rem; 228 padding-top: 2rem; 229 border-top: 1px solid var(--demo-hr); 230 text-align: center; 231 color: var(--demo-text-secondary);
··· 51 transition: background-color 200ms ease, color 200ms ease; 52 font-family: system-ui, sans-serif; 53 line-height: 1.6; 54 + display: flex; 55 + flex-direction: column; 56 + min-height: 100vh; 57 } 58 59 .container { 60 max-width: 860px; 61 margin: 40px auto; 62 padding: 0 20px; 63 + flex: 1; 64 + display: flex; 65 + flex-direction: column; 66 + width: 100%; 67 } 68 69 h1 { ··· 230 } 231 232 .footer { 233 + margin-top: auto; 234 padding-top: 2rem; 235 + padding-bottom: 2rem; 236 border-top: 1px solid var(--demo-hr); 237 text-align: center; 238 color: var(--demo-text-secondary);
+541 -162
bun.lock
··· 1 { 2 "lockfileVersion": 1, 3 - "configVersion": 0, 4 "workspaces": { 5 "": { 6 - "name": "elysia-static", 7 "dependencies": { 8 "@tailwindcss/cli": "^4.1.17", 9 "bun-plugin-tailwind": "^0.1.2", 10 "tailwindcss": "^4.1.17", 11 }, 12 }, 13 "apps/hosting-service": { 14 "name": "wisp-hosting-service", ··· 16 "dependencies": { 17 "@atproto/api": "^0.17.4", 18 "@atproto/identity": "^0.4.9", 19 - "@atproto/lexicon": "^0.5.1", 20 "@atproto/sync": "^0.1.36", 21 "@atproto/xrpc": "^0.7.5", 22 "@hono/node-server": "^1.19.6", ··· 31 "mime-types": "^2.1.35", 32 "multiformats": "^13.4.1", 33 "postgres": "^3.4.5", 34 }, 35 "devDependencies": { 36 "@types/bun": "^1.3.1", 37 "@types/mime-types": "^2.1.4", 38 "@types/node": "^22.10.5", 39 "tsx": "^4.19.2", 40 }, 41 }, 42 "apps/main-app": { 43 "name": "@wisp/main-app", 44 "version": "1.0.50", 45 "dependencies": { 46 - "@atproto/api": "^0.17.3", 47 - "@atproto/common-web": "^0.4.5", 48 "@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", 52 "@elysiajs/cors": "^1.4.0", 53 "@elysiajs/eden": "^1.4.3", 54 "@elysiajs/openapi": "^1.4.11", ··· 68 "@wisp/lexicons": "workspace:*", 69 "@wisp/observability": "workspace:*", 70 "actor-typeahead": "^0.1.1", 71 - "atproto-ui": "^0.11.3", 72 "bun-plugin-tailwind": "^0.1.2", 73 "class-variance-authority": "^0.7.1", 74 "clsx": "^2.1.1", 75 - "elysia": "latest", 76 "ignore": "^7.0.5", 77 "iron-session": "^8.0.4", 78 "lucide-react": "^0.546.0", ··· 86 "zlib": "^1.0.5", 87 }, 88 "devDependencies": { 89 "@types/react": "^19.2.2", 90 "@types/react-dom": "^19.2.1", 91 "bun-types": "latest", ··· 101 "@atproto/api": "^0.14.1", 102 "@wisp/lexicons": "workspace:*", 103 "multiformats": "^13.3.1", 104 }, 105 }, 106 "packages/@wisp/constants": { ··· 131 }, 132 "devDependencies": { 133 "@atproto/lex-cli": "^0.9.5", 134 }, 135 }, 136 "packages/@wisp/observability": { 137 "name": "@wisp/observability", 138 "version": "1.0.0", 139 "peerDependencies": { 140 - "hono": "^4.0.0", 141 }, 142 "optionalPeers": [ 143 "hono", ··· 149 }, 150 }, 151 "trustedDependencies": [ 152 - "core-js", 153 "cbor-extract", 154 "protobufjs", 155 ], 156 "packages": { 157 "@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="], 158 159 - "@atcute/bluesky": ["@atcute/bluesky@3.2.10", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.2" } }, "sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg=="], 160 161 - "@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=="], 162 163 - "@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="], 164 165 "@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=="], 166 167 - "@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 168 169 - "@atcute/tangled": ["@atcute/tangled@1.0.10", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA=="], 170 171 - "@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="], 172 173 - "@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=="], 174 175 "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], 176 177 "@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=="], 178 179 - "@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=="], 180 181 - "@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=="], 182 183 - "@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=="], 184 185 "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], 186 ··· 192 193 "@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=="], 194 195 - "@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=="], 196 197 - "@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=="], 198 199 - "@atproto/did": ["@atproto/did@0.2.1", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-1i5BTU2GnBaaeYWhxUOnuEKFVq9euT5+dQPFabHpa927BlJ54PmLGyBBaOI7/NbLmN5HWwBa18SBkMpg3jGZRA=="], 200 201 "@atproto/identity": ["@atproto/identity@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4" } }, "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ=="], 202 ··· 206 207 "@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=="], 208 209 - "@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=="], 210 211 - "@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=="], 212 213 - "@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=="], 214 215 - "@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=="], 216 217 "@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=="], 218 219 - "@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=="], 220 221 - "@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=="], 222 223 - "@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=="], 224 225 "@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=="], 226 227 "@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=="], 228 229 - "@atproto/syntax": ["@atproto/syntax@0.4.1", "", {}, "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw=="], 230 231 - "@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=="], 232 233 "@atproto/xrpc": ["@atproto/xrpc@0.7.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "zod": "^3.23.8" } }, "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA=="], 234 235 - "@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=="], 236 237 "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 238 ··· 252 253 "@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="], 254 255 - "@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="], 256 257 "@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="], 258 259 - "@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=="], 260 261 - "@elysiajs/static": ["@elysiajs/static@1.4.6", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-cd61aY/DHOVhlnBjzTBX8E1XANIrsCH8MwEGHeLMaZzNrz0gD4Q8Qsde2dFMzu81I7ZDaaZ2Rim9blSLtUrYBg=="], 262 263 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.26.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA=="], 264 ··· 312 313 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="], 314 315 - "@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=="], 316 317 "@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=="], 318 ··· 342 343 "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="], 344 345 - "@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=="], 346 347 "@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=="], 348 ··· 352 353 "@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=="], 354 355 - "@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=="], 356 357 "@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=="], 358 ··· 368 369 "@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=="], 370 371 - "@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=="], 372 373 "@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=="], 374 375 - "@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=="], 376 377 "@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=="], 378 379 "@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=="], 380 381 - "@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=="], 382 383 "@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=="], 384 385 - "@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=="], 386 387 "@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=="], 388 ··· 392 393 "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="], 394 395 - "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A=="], 396 397 - "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg=="], 398 399 - "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww=="], 400 401 - "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ=="], 402 403 - "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w=="], 404 405 - "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug=="], 406 407 - "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg=="], 408 409 - "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ=="], 410 411 - "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A=="], 412 413 - "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg=="], 414 415 - "@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=="], 416 417 "@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=="], 418 ··· 516 517 "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], 518 519 "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 520 521 "@tailwindcss/cli": ["@tailwindcss/cli@4.1.17", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.17" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-jUIxcyUNlCC2aNPnyPEWU/L2/ik3pB4fF3auKGXr8AvN3T3OFESVctFKOBoPZQaZJIeUpPn1uCLp0MRxuek8gg=="], ··· 548 549 "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="], 550 551 - "@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="], 552 553 - "@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="], 554 555 - "@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=="], 556 557 "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], 558 559 "@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=="], 560 561 - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 562 563 "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], 564 565 "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], 566 567 - "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 568 569 - "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], 570 571 "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], 572 ··· 594 595 "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], 596 597 - "actor-typeahead": ["actor-typeahead@0.1.1", "", {}, "sha512-ilsBwzplKwMSBiO6Tg6RdaZ5xxqgXds5jCQuHV+ib9Aq3ja9g0T7u2Y1PmihotmS7l5RxhpGI/tPm3ljoRDRwg=="], 598 599 "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 600 ··· 606 607 "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], 608 609 - "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=="], 610 611 "await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="], 612 ··· 614 615 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 616 617 - "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=="], 618 619 "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 620 621 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 622 623 - "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], 624 625 - "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=="], 626 627 "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], 628 629 - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], 630 631 "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 632 ··· 662 663 "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 664 665 - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], 666 667 - "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], 668 669 - "core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="], 670 671 - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 672 673 "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 674 ··· 684 685 "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 686 687 - "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=="], 688 689 "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 690 ··· 714 715 "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], 716 717 - "exact-mirror": ["exact-mirror@0.2.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-aLdARfO0W0ntufjDyytUJQMbNXoB9g+BbA8KcgIq4XOOTYRw48yUGON/Pr64iDrYNZKcKvKbqE0MPW56FF2BXA=="], 718 719 - "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=="], 720 721 "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], 722 723 "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], 724 725 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 726 727 - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], 728 - 729 - "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=="], 730 731 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 732 733 - "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=="], 734 735 "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 736 ··· 754 755 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 756 757 - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 758 - 759 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 760 761 "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 762 763 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 764 765 - "hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="], 766 767 - "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=="], 768 769 "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 770 ··· 898 899 "pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="], 900 901 - "playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="], 902 903 - "playwright-core": ["playwright-core@1.56.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="], 904 905 "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], 906 907 - "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], 908 909 "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], 910 ··· 916 917 "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=="], 918 919 - "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], 920 921 "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], 922 ··· 924 925 "rate-limiter-flexible": ["rate-limiter-flexible@2.4.2", "", {}, "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="], 926 927 - "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=="], 928 929 - "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], 930 931 - "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], 932 933 - "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=="], 934 935 "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=="], 936 937 "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], 938 939 - "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], 940 941 "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], 942 ··· 956 957 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 958 959 - "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=="], 960 961 "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=="], 962 ··· 978 979 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 980 981 - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 982 983 "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=="], 984 985 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 986 987 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 988 989 "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], 990 ··· 992 993 "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 994 995 - "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], 996 997 "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], 998 999 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], 1000 1001 "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], 1002 1003 "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 1004 ··· 1014 1015 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1016 1017 - "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=="], 1018 1019 "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], 1020 ··· 1040 1041 "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], 1042 1043 "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], 1044 1045 "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], ··· 1064 1065 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1066 1067 - "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], 1068 - 1069 - "@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=="], 1070 1071 "@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=="], 1072 ··· 1074 1075 "@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1076 1077 - "@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=="], 1078 - 1079 "@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1080 1081 "@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1082 1083 "@atproto/lex-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1084 1085 - "@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=="], 1086 - 1087 "@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1088 1089 "@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1090 - 1091 - "@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=="], 1092 1093 "@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1094 1095 - "@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=="], 1096 1097 "@atproto/repo/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1098 1099 - "@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=="], 1100 1101 - "@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=="], 1102 1103 "@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1104 1105 - "@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=="], 1106 1107 - "@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=="], 1108 1109 - "@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=="], 1110 1111 "@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1112 1113 "@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=="], 1114 1115 "@radix-ui/react-dialog/@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=="], ··· 1124 1125 "@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=="], 1126 1127 - "@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=="], 1128 1129 "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], 1130 ··· 1132 1133 "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1134 1135 - "@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=="], 1136 1137 - "bun-types/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], 1138 1139 - "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], 1140 1141 - "fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1142 1143 "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1144 ··· 1146 1147 "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 1148 1149 - "protobufjs/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], 1150 1151 "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1152 1153 - "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1154 1155 "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1156 1157 "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1158 1159 - "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=="], 1160 1161 "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 1162 ··· 1164 1165 "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=="], 1166 1167 - "@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=="], 1168 1169 - "@atproto/lex-cli/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1170 1171 - "@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=="], 1172 1173 - "@atproto/ws-client/@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1174 1175 - "@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=="], 1176 1177 - "@atproto/xrpc-server/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1178 1179 - "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1180 1181 - "@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=="], 1182 1183 - "@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=="], 1184 1185 - "@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=="], 1186 1187 "@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1188 1189 - "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1190 - 1191 - "protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1192 1193 "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1194 1195 - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 1196 1197 - "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 1198 1199 - "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 1200 1201 - "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 1202 1203 - "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 1204 1205 - "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 1206 1207 - "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 1208 1209 - "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 1210 1211 - "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 1212 1213 - "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 1214 1215 - "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 1216 1217 - "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 1218 1219 - "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 1220 1221 - "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 1222 1223 - "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 1224 1225 - "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 1226 1227 - "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 1228 1229 - "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 1230 1231 - "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 1232 1233 - "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 1234 1235 - "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 1236 1237 - "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 1238 1239 - "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 1240 1241 - "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 1242 1243 - "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 1244 1245 - "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 1246 1247 - "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=="], 1248 1249 - "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=="], 1250 1251 - "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=="], 1252 1253 "wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1254 1255 - "@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=="], 1256 } 1257 }
··· 1 { 2 "lockfileVersion": 1, 3 + "configVersion": 1, 4 "workspaces": { 5 "": { 6 + "name": "@wisp/monorepo", 7 "dependencies": { 8 "@tailwindcss/cli": "^4.1.17", 9 + "atproto-ui": "^0.12.0", 10 "bun-plugin-tailwind": "^0.1.2", 11 + "elysia": "^1.4.18", 12 "tailwindcss": "^4.1.17", 13 }, 14 + "devDependencies": { 15 + "@types/bun": "^1.3.5", 16 + }, 17 }, 18 "apps/hosting-service": { 19 "name": "wisp-hosting-service", ··· 21 "dependencies": { 22 "@atproto/api": "^0.17.4", 23 "@atproto/identity": "^0.4.9", 24 + "@atproto/lexicon": "^0.5.2", 25 "@atproto/sync": "^0.1.36", 26 "@atproto/xrpc": "^0.7.5", 27 "@hono/node-server": "^1.19.6", ··· 36 "mime-types": "^2.1.35", 37 "multiformats": "^13.4.1", 38 "postgres": "^3.4.5", 39 + "tiered-storage": "1.0.3", 40 }, 41 "devDependencies": { 42 "@types/bun": "^1.3.1", 43 "@types/mime-types": "^2.1.4", 44 "@types/node": "^22.10.5", 45 "tsx": "^4.19.2", 46 + "typescript": "^5.9.3", 47 }, 48 }, 49 "apps/main-app": { 50 "name": "@wisp/main-app", 51 "version": "1.0.50", 52 "dependencies": { 53 + "@atproto-labs/did-resolver": "^0.2.4", 54 + "@atproto/api": "^0.17.7", 55 + "@atproto/common-web": "^0.4.6", 56 "@atproto/jwk-jose": "^0.1.11", 57 + "@atproto/lex-cli": "^0.9.7", 58 + "@atproto/oauth-client-node": "^0.3.12", 59 + "@atproto/xrpc-server": "^0.9.6", 60 "@elysiajs/cors": "^1.4.0", 61 "@elysiajs/eden": "^1.4.3", 62 "@elysiajs/openapi": "^1.4.11", ··· 76 "@wisp/lexicons": "workspace:*", 77 "@wisp/observability": "workspace:*", 78 "actor-typeahead": "^0.1.1", 79 + "atproto-ui": "^0.12.0", 80 "bun-plugin-tailwind": "^0.1.2", 81 "class-variance-authority": "^0.7.1", 82 "clsx": "^2.1.1", 83 + "elysia": "^1.4.18", 84 "ignore": "^7.0.5", 85 "iron-session": "^8.0.4", 86 "lucide-react": "^0.546.0", ··· 94 "zlib": "^1.0.5", 95 }, 96 "devDependencies": { 97 + "@atproto-labs/handle-resolver": "^0.3.4", 98 + "@atproto/did": "^0.2.3", 99 "@types/react": "^19.2.2", 100 "@types/react-dom": "^19.2.1", 101 "bun-types": "latest", ··· 111 "@atproto/api": "^0.14.1", 112 "@wisp/lexicons": "workspace:*", 113 "multiformats": "^13.3.1", 114 + }, 115 + "devDependencies": { 116 + "@atproto/lexicon": "^0.5.2", 117 }, 118 }, 119 "packages/@wisp/constants": { ··· 144 }, 145 "devDependencies": { 146 "@atproto/lex-cli": "^0.9.5", 147 + "multiformats": "^13.4.1", 148 }, 149 }, 150 "packages/@wisp/observability": { 151 "name": "@wisp/observability", 152 "version": "1.0.0", 153 + "dependencies": { 154 + "@opentelemetry/api": "^1.9.0", 155 + "@opentelemetry/exporter-metrics-otlp-http": "^0.56.0", 156 + "@opentelemetry/resources": "^1.29.0", 157 + "@opentelemetry/sdk-metrics": "^1.29.0", 158 + "@opentelemetry/semantic-conventions": "^1.29.0", 159 + }, 160 + "devDependencies": { 161 + "@hono/node-server": "^1.19.6", 162 + "bun-types": "^1.3.3", 163 + "typescript": "^5.9.3", 164 + }, 165 "peerDependencies": { 166 + "hono": "^4.10.7", 167 }, 168 "optionalPeers": [ 169 "hono", ··· 175 }, 176 }, 177 "trustedDependencies": [ 178 + "esbuild", 179 "cbor-extract", 180 "protobufjs", 181 + "core-js", 182 + "bun", 183 + "@parcel/watcher", 184 ], 185 "packages": { 186 "@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="], 187 188 + "@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=="], 189 190 + "@atcute/client": ["@atcute/client@4.1.0", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.5" } }, "sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ=="], 191 192 + "@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="], 193 194 "@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=="], 195 196 + "@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="], 197 198 + "@atcute/tangled": ["@atcute/tangled@1.0.12", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.3" } }, "sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA=="], 199 200 + "@atcute/util-fetch": ["@atcute/util-fetch@1.0.4", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg=="], 201 202 + "@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=="], 203 204 "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], 205 206 "@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=="], 207 208 + "@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=="], 209 210 + "@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=="], 211 212 + "@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=="], 213 214 "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], 215 ··· 221 222 "@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=="], 223 224 + "@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=="], 225 226 + "@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=="], 227 228 + "@atproto/did": ["@atproto/did@0.2.3", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-VI8JJkSizvM2cHYJa37WlbzeCm5tWpojyc1/Zy8q8OOjyoy6X4S4BEfoP941oJcpxpMTObamibQIXQDo7tnIjg=="], 229 230 "@atproto/identity": ["@atproto/identity@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4" } }, "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ=="], 231 ··· 235 236 "@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=="], 237 238 + "@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=="], 239 240 + "@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=="], 241 242 + "@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=="], 243 244 + "@atproto/lex-json": ["@atproto/lex-json@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "tslib": "^2.8.1" } }, "sha512-Pd72lO+l2rhOTutnf11omh9ZkoB/elbzE3HSmn2wuZlyH1mRhTYvoH8BOGokWQwbZkCE8LL3nOqMT3gHCD2l7g=="], 245 246 "@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=="], 247 248 + "@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=="], 249 250 + "@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=="], 251 252 + "@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=="], 253 254 "@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=="], 255 256 "@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=="], 257 258 + "@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 259 260 + "@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=="], 261 262 "@atproto/xrpc": ["@atproto/xrpc@0.7.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "zod": "^3.23.8" } }, "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA=="], 263 264 + "@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=="], 265 + 266 + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], 267 + 268 + "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], 269 + 270 + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], 271 + 272 + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], 273 + 274 + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], 275 + 276 + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], 277 + 278 + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], 279 + 280 + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.962.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/credential-provider-node": "3.962.0", "@aws-sdk/middleware-bucket-endpoint": "3.957.0", "@aws-sdk/middleware-expect-continue": "3.957.0", "@aws-sdk/middleware-flexible-checksums": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-location-constraint": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-sdk-s3": "3.957.0", "@aws-sdk/middleware-ssec": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/signature-v4-multi-region": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/eventstream-serde-browser": "^4.2.7", "@smithy/eventstream-serde-config-resolver": "^4.3.7", "@smithy/eventstream-serde-node": "^4.2.7", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-blob-browser": "^4.2.8", "@smithy/hash-node": "^4.2.7", "@smithy/hash-stream-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/md5-js": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-I2/1McBZCcM3PfM4ck8D6gnZR3K7+yl1fGkwTq/3ThEn9tdLjNwcdgTbPfxfX6LoecLrH9Ekoo+D9nmQ0T261w=="], 281 + 282 + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.958.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg=="], 283 + 284 + "@aws-sdk/core": ["@aws-sdk/core@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws-sdk/xml-builder": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw=="], 285 + 286 + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-qSwSfI+qBU9HDsd6/4fM9faCxYJx2yDuHtj+NVOQ6XYDWQzFab/hUdwuKZ77Pi6goLF1pBZhJ2azaC2w7LbnTA=="], 287 + 288 + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog=="], 289 + 290 + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw=="], 291 + 292 + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.962.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-login": "3.962.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-h0kVnXLW2d3nxbcrR/Pfg3W/+YoCguasWz7/3nYzVqmdKarGrpJzaFdoZtLgvDSZ8VgWUC4lWOTcsDMV0UNqUQ=="], 293 + 294 + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.962.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-kHYH6Av2UifG3mPkpPUNRh/PuX6adaAcpmsclJdHdxlixMCRdh8GNeEihq480DC0GmfqdpoSf1w2CLmLLPIS6w=="], 295 + 296 + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.962.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-ini": "3.962.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-CS78NsWRxLa+nWqeWBEYMZTLacMFIXs1C5WJuM9kD05LLiWL32ksljoPsvNN24Bc7rCSQIIMx/U3KGvkDVZMVg=="], 297 + 298 + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg=="], 299 + 300 + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.958.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.958.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/token-providers": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg=="], 301 + 302 + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA=="], 303 + 304 + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.962.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/smithy-client": "^4.10.2", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.962.0" } }, "sha512-Ai5gWRQkzsUMQ6NPoZZoiLXoQ6/yPRcR4oracIVjyWcu48TfBpsRgbqY/5zNOM55ag1wPX9TtJJGOhK3TNk45g=="], 305 + 306 + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iczcn/QRIBSpvsdAS/rbzmoBpleX1JBjXvCynMbDceVLBIcVrwT1hXECrhtIC2cjh4HaLo9ClAbiOiWuqt+6MA=="], 307 + 308 + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-AlbK3OeVNwZZil0wlClgeI/ISlOt/SPUxBsIns876IFaVu/Pj3DgImnYhpcJuFRek4r4XM51xzIaGQXM6GDHGg=="], 309 + 310 + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.957.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/crc64-nvme": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iJpeVR5V8se1hl2pt+k8bF/e9JO4KWgPCMjg8BtRspNtKIUGy7j6msYvbDixaKZaF2Veg9+HoYcOhwnZumjXSA=="], 311 + 312 + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA=="], 313 + 314 + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-y8/W7TOQpmDJg/fPYlqAhwA4+I15LrS7TwgUEoxogtkD8gfur9wFMRLT8LCyc9o4NMEcAnK50hSb4+wB0qv6tQ=="], 315 + 316 + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ=="], 317 + 318 + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA=="], 319 + 320 + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg=="], 321 + 322 + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-qwkmrK0lizdjNt5qxl4tHYfASh8DFpHXM1iDVo+qHe+zuslfMqQEGRkzxS8tJq/I+8F0c6v3IKOveKJAfIvfqQ=="], 323 + 324 + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ=="], 325 + 326 + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.958.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw=="], 327 + 328 + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A=="], 329 + 330 + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.957.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg=="], 331 + 332 + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q=="], 333 + 334 + "@aws-sdk/types": ["@aws-sdk/types@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg=="], 335 + 336 + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.957.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g=="], 337 + 338 + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", "tslib": "^2.6.2" } }, "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw=="], 339 + 340 + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.957.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw=="], 341 + 342 + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw=="], 343 + 344 + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.957.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q=="], 345 + 346 + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA=="], 347 + 348 + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="], 349 350 "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 351 ··· 365 366 "@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="], 367 368 + "@elysiajs/eden": ["@elysiajs/eden@1.4.5", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-hIOeH+S5NU/84A7+t8yB1JjxqjmzRkBF9fnLn6y+AH8EcF39KumOAnciMhIOkhhThVZvXZ3d+GsizRc+Fxoi8g=="], 369 370 "@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="], 371 372 + "@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=="], 373 374 + "@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="], 375 376 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.26.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA=="], 377 ··· 425 426 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="], 427 428 + "@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=="], 429 430 "@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=="], 431 ··· 455 456 "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="], 457 458 + "@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=="], 459 460 "@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=="], 461 ··· 465 466 "@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=="], 467 468 + "@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=="], 469 470 "@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=="], 471 ··· 481 482 "@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=="], 483 484 + "@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=="], 485 486 "@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=="], 487 488 + "@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=="], 489 490 "@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=="], 491 492 "@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=="], 493 494 + "@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=="], 495 496 "@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=="], 497 498 + "@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=="], 499 500 "@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=="], 501 ··· 505 506 "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="], 507 508 + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], 509 510 + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow=="], 511 512 + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g=="], 513 514 + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw=="], 515 516 + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw=="], 517 518 + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw=="], 519 520 + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w=="], 521 522 + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w=="], 523 524 + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ=="], 525 526 + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ=="], 527 528 + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA=="], 529 530 "@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=="], 531 ··· 629 630 "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], 631 632 + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw=="], 633 + 634 + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="], 635 + 636 + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="], 637 + 638 + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg=="], 639 + 640 + "@smithy/core": ["@smithy/core@3.20.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ=="], 641 + 642 + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA=="], 643 + 644 + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="], 645 + 646 + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g=="], 647 + 648 + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ=="], 649 + 650 + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A=="], 651 + 652 + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.7", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g=="], 653 + 654 + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg=="], 655 + 656 + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.8", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw=="], 657 + 658 + "@smithy/hash-node": ["@smithy/hash-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw=="], 659 + 660 + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ=="], 661 + 662 + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ=="], 663 + 664 + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], 665 + 666 + "@smithy/md5-js": ["@smithy/md5-js@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw=="], 667 + 668 + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg=="], 669 + 670 + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.1", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg=="], 671 + 672 + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.17", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg=="], 673 + 674 + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="], 675 + 676 + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw=="], 677 + 678 + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.7", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw=="], 679 + 680 + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ=="], 681 + 682 + "@smithy/property-provider": ["@smithy/property-provider@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA=="], 683 + 684 + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA=="], 685 + 686 + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="], 687 + 688 + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w=="], 689 + 690 + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0" } }, "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA=="], 691 + 692 + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.2", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg=="], 693 + 694 + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.7", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg=="], 695 + 696 + "@smithy/smithy-client": ["@smithy/smithy-client@4.10.2", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g=="], 697 + 698 + "@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], 699 + 700 + "@smithy/url-parser": ["@smithy/url-parser@4.2.7", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg=="], 701 + 702 + "@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], 703 + 704 + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], 705 + 706 + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], 707 + 708 + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], 709 + 710 + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], 711 + 712 + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.16", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ=="], 713 + 714 + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.19", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA=="], 715 + 716 + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg=="], 717 + 718 + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], 719 + 720 + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w=="], 721 + 722 + "@smithy/util-retry": ["@smithy/util-retry@4.2.7", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg=="], 723 + 724 + "@smithy/util-stream": ["@smithy/util-stream@4.5.8", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w=="], 725 + 726 + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], 727 + 728 + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], 729 + 730 + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="], 731 + 732 + "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], 733 + 734 "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 735 736 "@tailwindcss/cli": ["@tailwindcss/cli@4.1.17", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.17" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-jUIxcyUNlCC2aNPnyPEWU/L2/ik3pB4fF3auKGXr8AvN3T3OFESVctFKOBoPZQaZJIeUpPn1uCLp0MRxuek8gg=="], ··· 763 764 "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="], 765 766 + "@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], 767 768 + "@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="], 769 770 + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], 771 772 "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], 773 774 "@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=="], 775 776 + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], 777 778 "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], 779 780 "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], 781 782 + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], 783 784 + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 785 786 "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], 787 ··· 809 810 "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], 811 812 + "actor-typeahead": ["actor-typeahead@0.1.2", "", {}, "sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A=="], 813 814 "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 815 ··· 821 822 "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], 823 824 + "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=="], 825 826 "await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="], 827 ··· 829 830 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 831 832 + "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=="], 833 + 834 + "bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="], 835 836 "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 837 838 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 839 840 + "buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], 841 842 + "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=="], 843 844 "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], 845 846 + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 847 848 "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 849 ··· 879 880 "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 881 882 + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 883 884 + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], 885 886 + "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], 887 888 + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 889 890 "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 891 ··· 901 902 "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 903 904 + "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=="], 905 906 "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 907 ··· 931 932 "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], 933 934 + "exact-mirror": ["exact-mirror@0.2.5", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ=="], 935 936 + "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=="], 937 938 "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], 939 940 "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], 941 + 942 + "fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], 943 944 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 945 946 + "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=="], 947 948 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 949 950 + "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=="], 951 952 "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 953 ··· 971 972 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 973 974 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 975 976 "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 977 978 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 979 980 + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], 981 982 + "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=="], 983 984 "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 985 ··· 1113 1114 "pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="], 1115 1116 + "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], 1117 1118 + "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], 1119 1120 "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], 1121 1122 + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], 1123 1124 "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], 1125 ··· 1131 1132 "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=="], 1133 1134 + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], 1135 1136 "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], 1137 ··· 1139 1140 "rate-limiter-flexible": ["rate-limiter-flexible@2.4.2", "", {}, "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="], 1141 1142 + "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=="], 1143 1144 + "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], 1145 1146 + "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], 1147 1148 + "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=="], 1149 1150 "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=="], 1151 1152 "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], 1153 1154 + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], 1155 1156 "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], 1157 ··· 1171 1172 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 1173 1174 + "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=="], 1175 1176 "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=="], 1177 ··· 1193 1194 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 1195 1196 + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 1197 + 1198 + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], 1199 1200 "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=="], 1201 1202 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 1203 1204 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1205 + 1206 + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], 1207 1208 "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], 1209 ··· 1211 1212 "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 1213 1214 + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], 1215 1216 "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], 1217 1218 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], 1219 1220 "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], 1221 + 1222 + "tiered-storage": ["tiered-storage@1.0.3", "", { "dependencies": { "@aws-sdk/client-s3": "^3.500.0", "@aws-sdk/lib-storage": "^3.500.0", "hono": "^4.10.7", "mime-types": "^3.0.2", "tiny-lru": "^11.0.0" } }, "sha512-ntJCsWWYHSQWIDbObMfnJ8xw7cRSJCPzfbrjoY4hu0YIRizCthRWg8kCvJOgsgC+Upt259YHmfNwEr1wLv6Rxw=="], 1223 + 1224 + "tiny-lru": ["tiny-lru@11.4.5", "", {}, "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw=="], 1225 1226 "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 1227 ··· 1237 1238 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1239 1240 + "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=="], 1241 1242 "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], 1243 ··· 1263 1264 "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], 1265 1266 + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 1267 + 1268 "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], 1269 1270 "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], ··· 1289 1290 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1291 1292 + "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], 1293 1294 "@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=="], 1295 ··· 1297 1298 "@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1299 1300 "@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1301 1302 "@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1303 1304 "@atproto/lex-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1305 1306 "@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1307 1308 "@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1309 1310 "@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1311 1312 + "@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=="], 1313 1314 "@atproto/repo/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1315 1316 + "@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=="], 1317 1318 + "@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=="], 1319 1320 "@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1321 1322 + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], 1323 1324 + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], 1325 1326 + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], 1327 1328 "@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1329 1330 + "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1331 + 1332 + "@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=="], 1333 + 1334 + "@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=="], 1335 + 1336 + "@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=="], 1337 + 1338 + "@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=="], 1339 + 1340 + "@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=="], 1341 + 1342 + "@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=="], 1343 + 1344 + "@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=="], 1345 + 1346 + "@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=="], 1347 + 1348 + "@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=="], 1349 + 1350 + "@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=="], 1351 + 1352 + "@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=="], 1353 + 1354 + "@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=="], 1355 + 1356 + "@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=="], 1357 + 1358 + "@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=="], 1359 + 1360 + "@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=="], 1361 + 1362 + "@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=="], 1363 + 1364 + "@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=="], 1365 + 1366 + "@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=="], 1367 + 1368 + "@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=="], 1369 + 1370 + "@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=="], 1371 + 1372 + "@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=="], 1373 + 1374 + "@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=="], 1375 + 1376 + "@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=="], 1377 + 1378 + "@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=="], 1379 + 1380 + "@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=="], 1381 + 1382 + "@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=="], 1383 + 1384 + "@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=="], 1385 + 1386 + "@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=="], 1387 + 1388 + "@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=="], 1389 + 1390 + "@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=="], 1391 + 1392 + "@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=="], 1393 + 1394 + "@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=="], 1395 + 1396 + "@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=="], 1397 + 1398 + "@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=="], 1399 + 1400 + "@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=="], 1401 + 1402 + "@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=="], 1403 + 1404 + "@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=="], 1405 + 1406 + "@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=="], 1407 + 1408 + "@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=="], 1409 + 1410 + "@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=="], 1411 + 1412 + "@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=="], 1413 + 1414 + "@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=="], 1415 + 1416 + "@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=="], 1417 + 1418 + "@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=="], 1419 + 1420 + "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g=="], 1421 + 1422 + "@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=="], 1423 + 1424 + "@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=="], 1425 + 1426 + "@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=="], 1427 + 1428 + "@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=="], 1429 + 1430 + "@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=="], 1431 + 1432 + "@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=="], 1433 + 1434 + "@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=="], 1435 + 1436 + "@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1437 + 1438 + "@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=="], 1439 + 1440 + "@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=="], 1441 + 1442 + "@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=="], 1443 + 1444 + "@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=="], 1445 + 1446 + "@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=="], 1447 + 1448 + "@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=="], 1449 + 1450 + "@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=="], 1451 + 1452 + "@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=="], 1453 + 1454 + "@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=="], 1455 + 1456 + "@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=="], 1457 + 1458 "@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=="], 1459 1460 "@radix-ui/react-dialog/@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=="], ··· 1469 1470 "@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=="], 1471 1472 + "@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=="], 1473 1474 "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], 1475 ··· 1477 1478 "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1479 1480 + "@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 1481 1482 + "@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=="], 1483 1484 + "@wisp/observability/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 1485 1486 + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1487 1488 "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1489 ··· 1491 1492 "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 1493 1494 + "pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], 1495 1496 "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1497 1498 + "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=="], 1499 1500 "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1501 1502 + "send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 1503 + 1504 + "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=="], 1505 + 1506 + "tiered-storage/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 1507 + 1508 "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1509 1510 + "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=="], 1511 1512 "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 1513 ··· 1515 1516 "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=="], 1517 1518 + "@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=="], 1519 1520 + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], 1521 1522 + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], 1523 + 1524 + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], 1525 + 1526 + "@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=="], 1527 + 1528 + "@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=="], 1529 + 1530 + "@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=="], 1531 + 1532 + "@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=="], 1533 + 1534 + "@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=="], 1535 + 1536 + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1537 + 1538 + "@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=="], 1539 + 1540 + "@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=="], 1541 + 1542 + "@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=="], 1543 + 1544 + "@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=="], 1545 1546 + "@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=="], 1547 1548 + "@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1549 1550 + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1551 1552 + "@opentelemetry/sdk-metrics/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1553 1554 + "@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=="], 1555 1556 + "@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=="], 1557 1558 + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1559 1560 "@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1561 1562 + "pino-abstract-transport/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], 1563 1564 "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1565 1566 + "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1567 1568 + "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=="], 1569 1570 + "serve-static/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1571 1572 + "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 1573 1574 + "tiered-storage/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 1575 1576 + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="], 1577 1578 + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="], 1579 1580 + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="], 1581 + 1582 + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="], 1583 + 1584 + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="], 1585 1586 + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="], 1587 1588 + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="], 1589 1590 + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="], 1591 1592 + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="], 1593 1594 + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="], 1595 1596 + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="], 1597 1598 + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="], 1599 1600 + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="], 1601 1602 + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="], 1603 1604 + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="], 1605 1606 + "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=="], 1607 1608 + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="], 1609 1610 + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="], 1611 1612 + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="], 1613 1614 + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="], 1615 1616 + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="], 1617 1618 + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="], 1619 1620 + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="], 1621 1622 + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="], 1623 1624 + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="], 1625 1626 + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="], 1627 1628 "wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1629 1630 + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 1631 + 1632 + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 1633 + 1634 + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 1635 } 1636 }
+116 -401
claude.md
··· 1 - # Wisp.place - Codebase Overview 2 3 - **Project URL**: https://wisp.place 4 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. 6 7 - --- 8 9 - ## ๐Ÿ—๏ธ Architecture Overview 10 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 23 24 - --- 25 26 - ## ๐Ÿ“‚ Directory Structure 27 28 - ### `/src` - Main Backend Server 29 - **Purpose**: Core server handling OAuth, site management, custom domains, admin features 30 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) 37 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) 55 56 - ### `/lexicons` & `src/lexicons/` 57 - **Purpose**: AT Protocol Lexicon definitions for custom data types 58 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 160 ``` 161 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 - ``` 170 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 - ``` 183 184 - --- 185 186 - ## ๐Ÿ› ๏ธ Important Implementation Details 187 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 194 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 - --- 319 320 - ## ๐Ÿ” Notable Implementation Patterns 321 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 327 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 332 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) 338 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 359 360 - --- 361 362 - ## ๐Ÿ“‹ Environment Variables 363 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
··· 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 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 + } 13 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 + } 22 23 + interface Directory { 24 + $type?: 'place.wisp.fs#directory' 25 + type: 'directory' 26 + entries: Entry[] 27 + } 28 29 + interface Entry { 30 + $type?: 'place.wisp.fs#entry' 31 + name: string 32 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 33 + } 34 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 + } 41 42 + //place.wisp.subfs 43 + interface Main { 44 + $type: 'place.wisp.subfs' 45 + root: Directory 46 + fileCount?: number 47 + createdAt: string 48 + } 49 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 + } 58 59 + interface Directory { 60 + $type?: 'place.wisp.subfs#directory' 61 + type: 'directory' 62 + entries: Entry[] 63 + } 64 65 + interface Entry { 66 + $type?: 'place.wisp.subfs#entry' 67 + name: string 68 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 69 + } 70 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 + } 76 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 + } 87 88 + interface CustomHeader { 89 + $type?: 'place.wisp.settings#customHeader' 90 + name: string 91 + value: string 92 + path?: string // Optional glob pattern 93 + } 94 ``` 95 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 99 100 + The project is a monorepo. The package handler it uses for the typescript side is Bun. For the Rust cli, it is cargo. 101 102 + ### Typescript Bun Workspace Layout 103 104 + Bun workspaces: `packages/@wisp/*`, `apps/main-app`, `apps/hosting-service` 105 106 + There are two typescript apps 107 + **`apps/main-app`** - Main backend (Bun + Elysia) 108 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/ 114 115 + **`apps/hosting-service`** - CDN static file server (Node + Hono) 116 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 122 123 + ### Shared Packages (`packages/@wisp/*`) 124 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 133 134 + ### CLI 135 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 139 140 + ### Other Directories 141 142 + - **`docs/`** - Astro documentation site 143 + - **`binaries/`** - Compiled CLI binaries for distribution
+417 -504
cli/Cargo.lock
··· 3 version = 4 4 5 [[package]] 6 - name = "abnf" 7 - version = "0.13.0" 8 - source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" 10 - dependencies = [ 11 - "abnf-core", 12 - "nom", 13 - ] 14 - 15 - [[package]] 16 - name = "abnf-core" 17 - version = "0.5.0" 18 - source = "registry+https://github.com/rust-lang/crates.io-index" 19 - checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" 20 - dependencies = [ 21 - "nom", 22 - ] 23 - 24 - [[package]] 25 name = "addr2line" 26 version = "0.25.1" 27 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 71 dependencies = [ 72 "alloc-no-stdlib", 73 ] 74 75 [[package]] 76 name = "android_system_properties" ··· 139 140 [[package]] 141 name = "async-compression" 142 - version = "0.4.34" 143 source = "registry+https://github.com/rust-lang/crates.io-index" 144 - checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" 145 dependencies = [ 146 "compression-codecs", 147 "compression-core", ··· 158 dependencies = [ 159 "proc-macro2", 160 "quote", 161 - "syn 2.0.111", 162 ] 163 164 [[package]] ··· 175 176 [[package]] 177 name = "axum" 178 - version = "0.8.7" 179 source = "registry+https://github.com/rust-lang/crates.io-index" 180 - checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" 181 dependencies = [ 182 "axum-core", 183 "bytes", ··· 208 209 [[package]] 210 name = "axum-core" 211 - version = "0.5.5" 212 source = "registry+https://github.com/rust-lang/crates.io-index" 213 - checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" 214 dependencies = [ 215 "bytes", 216 "futures-core", ··· 237 "miniz_oxide", 238 "object", 239 "rustc-demangle", 240 - "windows-link 0.2.1", 241 ] 242 243 [[package]] ··· 285 286 [[package]] 287 name = "base64ct" 288 - version = "1.8.0" 289 source = "registry+https://github.com/rust-lang/crates.io-index" 290 - checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 291 292 [[package]] 293 name = "bitflags" ··· 326 "proc-macro2", 327 "quote", 328 "rustversion", 329 - "syn 2.0.111", 330 ] 331 332 [[package]] 333 name = "borsh" 334 - version = "1.5.7" 335 source = "registry+https://github.com/rust-lang/crates.io-index" 336 - checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" 337 dependencies = [ 338 "cfg_aliases", 339 ] ··· 370 ] 371 372 [[package]] 373 - name = "btree-range-map" 374 - version = "0.7.2" 375 - source = "registry+https://github.com/rust-lang/crates.io-index" 376 - checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" 377 - dependencies = [ 378 - "btree-slab", 379 - "cc-traits", 380 - "range-traits", 381 - "serde", 382 - "slab", 383 - ] 384 - 385 - [[package]] 386 - name = "btree-slab" 387 - version = "0.6.1" 388 - source = "registry+https://github.com/rust-lang/crates.io-index" 389 - checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" 390 - dependencies = [ 391 - "cc-traits", 392 - "slab", 393 - "smallvec", 394 - ] 395 - 396 - [[package]] 397 name = "buf_redux" 398 version = "0.8.4" 399 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 405 406 [[package]] 407 name = "bumpalo" 408 - version = "3.19.0" 409 source = "registry+https://github.com/rust-lang/crates.io-index" 410 - checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 411 412 [[package]] 413 name = "byteorder" ··· 435 436 [[package]] 437 name = "cc" 438 - version = "1.2.47" 439 source = "registry+https://github.com/rust-lang/crates.io-index" 440 - checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" 441 dependencies = [ 442 "find-msvc-tools", 443 "shlex", 444 ] 445 446 [[package]] 447 - name = "cc-traits" 448 - version = "2.0.0" 449 - source = "registry+https://github.com/rust-lang/crates.io-index" 450 - checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" 451 - dependencies = [ 452 - "slab", 453 - ] 454 - 455 - [[package]] 456 name = "cesu8" 457 version = "1.1.0" 458 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 481 "num-traits", 482 "serde", 483 "wasm-bindgen", 484 - "windows-link 0.2.1", 485 ] 486 487 [[package]] ··· 533 534 [[package]] 535 name = "clap" 536 - version = "4.5.53" 537 source = "registry+https://github.com/rust-lang/crates.io-index" 538 - checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" 539 dependencies = [ 540 "clap_builder", 541 "clap_derive", ··· 543 544 [[package]] 545 name = "clap_builder" 546 - version = "4.5.53" 547 source = "registry+https://github.com/rust-lang/crates.io-index" 548 - checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" 549 dependencies = [ 550 "anstream", 551 "anstyle", ··· 562 "heck 0.5.0", 563 "proc-macro2", 564 "quote", 565 - "syn 2.0.111", 566 ] 567 568 [[package]] ··· 572 checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 573 574 [[package]] 575 name = "colorchoice" 576 version = "1.0.4" 577 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 589 590 [[package]] 591 name = "compression-codecs" 592 - version = "0.4.33" 593 source = "registry+https://github.com/rust-lang/crates.io-index" 594 - checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" 595 dependencies = [ 596 "compression-core", 597 "flate2", ··· 603 version = "0.4.31" 604 source = "registry+https://github.com/rust-lang/crates.io-index" 605 checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" 606 607 [[package]] 608 name = "const-oid" ··· 678 dependencies = [ 679 "cfg-if", 680 ] 681 682 [[package]] 683 name = "crossbeam-channel" ··· 762 "proc-macro2", 763 "quote", 764 "strsim", 765 - "syn 2.0.111", 766 ] 767 768 [[package]] ··· 773 dependencies = [ 774 "darling_core", 775 "quote", 776 - "syn 2.0.111", 777 ] 778 779 [[package]] ··· 813 checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 814 dependencies = [ 815 "data-encoding", 816 - "syn 2.0.111", 817 ] 818 819 [[package]] ··· 844 checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 845 dependencies = [ 846 "powerfmt", 847 - "serde_core", 848 ] 849 850 [[package]] ··· 864 dependencies = [ 865 "proc-macro2", 866 "quote", 867 - "syn 2.0.111", 868 "unicode-xid", 869 ] 870 ··· 915 dependencies = [ 916 "proc-macro2", 917 "quote", 918 - "syn 2.0.111", 919 ] 920 921 [[package]] 922 - name = "dyn-clone" 923 - version = "1.0.20" 924 - source = "registry+https://github.com/rust-lang/crates.io-index" 925 - checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 926 - 927 - [[package]] 928 name = "ecdsa" 929 version = "0.16.9" 930 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 959 ] 960 961 [[package]] 962 name = "encoding_rs" 963 version = "0.8.35" 964 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 976 "heck 0.5.0", 977 "proc-macro2", 978 "quote", 979 - "syn 2.0.111", 980 ] 981 982 [[package]] ··· 1025 1026 [[package]] 1027 name = "find-msvc-tools" 1028 - version = "0.1.5" 1029 source = "registry+https://github.com/rust-lang/crates.io-index" 1030 - checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 1031 1032 [[package]] 1033 name = "flate2" ··· 1046 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 1047 1048 [[package]] 1049 name = "form_urlencoded" 1050 version = "1.2.2" 1051 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1146 dependencies = [ 1147 "proc-macro2", 1148 "quote", 1149 - "syn 2.0.111", 1150 ] 1151 1152 [[package]] ··· 1181 1182 [[package]] 1183 name = "generator" 1184 - version = "0.8.7" 1185 source = "registry+https://github.com/rust-lang/crates.io-index" 1186 - checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" 1187 dependencies = [ 1188 "cc", 1189 "cfg-if", 1190 "libc", 1191 "log", 1192 "rustversion", 1193 - "windows", 1194 ] 1195 1196 [[package]] ··· 1300 1301 [[package]] 1302 name = "h2" 1303 - version = "0.4.12" 1304 source = "registry+https://github.com/rust-lang/crates.io-index" 1305 - checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 1306 dependencies = [ 1307 "atomic-waker", 1308 "bytes", ··· 1310 "futures-core", 1311 "futures-sink", 1312 "http", 1313 - "indexmap 2.12.1", 1314 "slab", 1315 "tokio", 1316 "tokio-util", ··· 1329 ] 1330 1331 [[package]] 1332 - name = "hashbrown" 1333 - version = "0.12.3" 1334 source = "registry+https://github.com/rust-lang/crates.io-index" 1335 - checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 1336 1337 [[package]] 1338 name = "hashbrown" ··· 1342 1343 [[package]] 1344 name = "hashbrown" 1345 version = "0.16.1" 1346 source = "registry+https://github.com/rust-lang/crates.io-index" 1347 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 1348 1349 [[package]] 1350 name = "heck" 1351 version = "0.4.1" 1352 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1371 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1372 1373 [[package]] 1374 - name = "hex_fmt" 1375 - version = "0.3.0" 1376 - source = "registry+https://github.com/rust-lang/crates.io-index" 1377 - checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" 1378 - 1379 - [[package]] 1380 name = "hickory-proto" 1381 version = "0.24.4" 1382 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1441 "markup5ever", 1442 "proc-macro2", 1443 "quote", 1444 - "syn 2.0.111", 1445 ] 1446 1447 [[package]] ··· 1537 1538 [[package]] 1539 name = "hyper-util" 1540 - version = "0.1.18" 1541 source = "registry+https://github.com/rust-lang/crates.io-index" 1542 - checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" 1543 dependencies = [ 1544 "base64 0.22.1", 1545 "bytes", ··· 1573 "js-sys", 1574 "log", 1575 "wasm-bindgen", 1576 - "windows-core 0.62.2", 1577 ] 1578 1579 [[package]] ··· 1633 1634 [[package]] 1635 name = "icu_properties" 1636 - version = "2.1.1" 1637 source = "registry+https://github.com/rust-lang/crates.io-index" 1638 - checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" 1639 dependencies = [ 1640 "icu_collections", 1641 "icu_locale_core", ··· 1647 1648 [[package]] 1649 name = "icu_properties_data" 1650 - version = "2.1.1" 1651 source = "registry+https://github.com/rust-lang/crates.io-index" 1652 - checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" 1653 1654 [[package]] 1655 name = "icu_provider" ··· 1711 1712 [[package]] 1713 name = "indexmap" 1714 - version = "1.9.3" 1715 - source = "registry+https://github.com/rust-lang/crates.io-index" 1716 - checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 1717 - dependencies = [ 1718 - "autocfg", 1719 - "hashbrown 0.12.3", 1720 - "serde", 1721 - ] 1722 - 1723 - [[package]] 1724 - name = "indexmap" 1725 version = "2.12.1" 1726 source = "registry+https://github.com/rust-lang/crates.io-index" 1727 checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 1728 dependencies = [ 1729 "equivalent", 1730 "hashbrown 0.16.1", 1731 - "serde", 1732 - "serde_core", 1733 ] 1734 1735 [[package]] 1736 - name = "indoc" 1737 - version = "2.0.7" 1738 source = "registry+https://github.com/rust-lang/crates.io-index" 1739 - checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" 1740 dependencies = [ 1741 - "rustversion", 1742 ] 1743 1744 [[package]] ··· 1781 1782 [[package]] 1783 name = "iri-string" 1784 - version = "0.7.9" 1785 source = "registry+https://github.com/rust-lang/crates.io-index" 1786 - checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" 1787 dependencies = [ 1788 "memchr", 1789 "serde", ··· 1803 1804 [[package]] 1805 name = "itoa" 1806 - version = "1.0.15" 1807 source = "registry+https://github.com/rust-lang/crates.io-index" 1808 - checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1809 1810 [[package]] 1811 name = "jacquard" 1812 - version = "0.9.3" 1813 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1814 dependencies = [ 1815 "bytes", 1816 "getrandom 0.2.16", ··· 1839 1840 [[package]] 1841 name = "jacquard-api" 1842 - version = "0.9.2" 1843 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1844 dependencies = [ 1845 "bon", 1846 "bytes", ··· 1850 "miette", 1851 "rustversion", 1852 "serde", 1853 "serde_ipld_dagcbor", 1854 "thiserror 2.0.17", 1855 "unicode-segmentation", ··· 1857 1858 [[package]] 1859 name = "jacquard-common" 1860 - version = "0.9.2" 1861 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1862 dependencies = [ 1863 "base64 0.22.1", 1864 "bon", 1865 "bytes", 1866 "chrono", 1867 "ciborium", 1868 "cid", 1869 "futures", 1870 "getrandom 0.2.16", 1871 "getrandom 0.3.4", 1872 "http", 1873 "ipld-core", 1874 "k256", 1875 - "langtag", 1876 "miette", 1877 "multibase", 1878 "multihash", 1879 "n0-future 0.1.3", 1880 "ouroboros", 1881 "p256", 1882 "rand 0.9.2", 1883 "regex", 1884 "regex-lite", 1885 "reqwest", 1886 "serde", 1887 "serde_html_form", 1888 "serde_ipld_dagcbor", 1889 "serde_json", 1890 "signature", 1891 "smol_str", 1892 "thiserror 2.0.17", 1893 "tokio", 1894 "tokio-tungstenite-wasm", ··· 1899 1900 [[package]] 1901 name = "jacquard-derive" 1902 - version = "0.9.3" 1903 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1904 dependencies = [ 1905 "heck 0.5.0", 1906 "jacquard-lexicon", 1907 "proc-macro2", 1908 "quote", 1909 - "syn 2.0.111", 1910 ] 1911 1912 [[package]] 1913 name = "jacquard-identity" 1914 - version = "0.9.2" 1915 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1916 dependencies = [ 1917 "bon", 1918 "bytes", ··· 1922 "jacquard-common", 1923 "jacquard-lexicon", 1924 "miette", 1925 - "mini-moka", 1926 "percent-encoding", 1927 "reqwest", 1928 "serde", ··· 1937 1938 [[package]] 1939 name = "jacquard-lexicon" 1940 - version = "0.9.2" 1941 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1942 dependencies = [ 1943 "cid", 1944 "dashmap", ··· 1956 "serde_repr", 1957 "serde_with", 1958 "sha2", 1959 - "syn 2.0.111", 1960 "thiserror 2.0.17", 1961 "unicode-segmentation", 1962 ] 1963 1964 [[package]] 1965 name = "jacquard-oauth" 1966 - version = "0.9.2" 1967 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 1968 dependencies = [ 1969 "base64 0.22.1", 1970 "bytes", ··· 2052 2053 [[package]] 2054 name = "js-sys" 2055 - version = "0.3.82" 2056 source = "registry+https://github.com/rust-lang/crates.io-index" 2057 - checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" 2058 dependencies = [ 2059 "once_cell", 2060 "wasm-bindgen", ··· 2073 ] 2074 2075 [[package]] 2076 - name = "langtag" 2077 - version = "0.4.0" 2078 - source = "registry+https://github.com/rust-lang/crates.io-index" 2079 - checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" 2080 - dependencies = [ 2081 - "serde", 2082 - "static-regular-grammar", 2083 - "thiserror 1.0.69", 2084 - ] 2085 - 2086 - [[package]] 2087 name = "lazy_static" 2088 version = "1.5.0" 2089 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2094 2095 [[package]] 2096 name = "libc" 2097 - version = "0.2.177" 2098 source = "registry+https://github.com/rust-lang/crates.io-index" 2099 - checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 2100 2101 [[package]] 2102 name = "libm" ··· 2106 2107 [[package]] 2108 name = "libredox" 2109 - version = "0.1.10" 2110 source = "registry+https://github.com/rust-lang/crates.io-index" 2111 - checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" 2112 dependencies = [ 2113 "bitflags", 2114 "libc", 2115 - "redox_syscall", 2116 ] 2117 2118 [[package]] ··· 2144 2145 [[package]] 2146 name = "log" 2147 - version = "0.4.28" 2148 source = "registry+https://github.com/rust-lang/crates.io-index" 2149 - checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 2150 2151 [[package]] 2152 name = "loom" ··· 2183 checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 2184 2185 [[package]] 2186 name = "markup5ever" 2187 version = "0.12.1" 2188 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2267 dependencies = [ 2268 "proc-macro2", 2269 "quote", 2270 - "syn 2.0.111", 2271 ] 2272 2273 [[package]] ··· 2287 ] 2288 2289 [[package]] 2290 - name = "mini-moka" 2291 version = "0.10.99" 2292 - source = "git+https://tangled.org/nekomimi.pet/jacquard#e1b90160d4026e036ab5b797e56ddd7ae5c5537e" 2293 dependencies = [ 2294 "crossbeam-channel", 2295 "crossbeam-utils", ··· 2301 ] 2302 2303 [[package]] 2304 - name = "minimal-lexical" 2305 - version = "0.2.1" 2306 - source = "registry+https://github.com/rust-lang/crates.io-index" 2307 - checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 2308 - 2309 - [[package]] 2310 name = "miniz_oxide" 2311 version = "0.8.9" 2312 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2318 2319 [[package]] 2320 name = "mio" 2321 - version = "1.1.0" 2322 source = "registry+https://github.com/rust-lang/crates.io-index" 2323 - checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 2324 dependencies = [ 2325 "libc", 2326 "wasi", ··· 2369 ] 2370 2371 [[package]] 2372 name = "n0-future" 2373 version = "0.1.3" 2374 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2421 version = "1.0.6" 2422 source = "registry+https://github.com/rust-lang/crates.io-index" 2423 checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 2424 - 2425 - [[package]] 2426 - name = "nom" 2427 - version = "7.1.3" 2428 - source = "registry+https://github.com/rust-lang/crates.io-index" 2429 - checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2430 - dependencies = [ 2431 - "memchr", 2432 - "minimal-lexical", 2433 - ] 2434 2435 [[package]] 2436 name = "nu-ansi-term" ··· 2513 ] 2514 2515 [[package]] 2516 name = "objc2" 2517 version = "0.6.3" 2518 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2560 2561 [[package]] 2562 name = "openssl-probe" 2563 - version = "0.1.6" 2564 source = "registry+https://github.com/rust-lang/crates.io-index" 2565 - checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2566 2567 [[package]] 2568 name = "option-ext" ··· 2591 "proc-macro2", 2592 "proc-macro2-diagnostics", 2593 "quote", 2594 - "syn 2.0.111", 2595 ] 2596 2597 [[package]] ··· 2601 checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 2602 2603 [[package]] 2604 name = "p256" 2605 version = "0.13.2" 2606 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2646 dependencies = [ 2647 "cfg-if", 2648 "libc", 2649 - "redox_syscall", 2650 "smallvec", 2651 - "windows-link 0.2.1", 2652 ] 2653 2654 [[package]] ··· 2721 dependencies = [ 2722 "proc-macro2", 2723 "quote", 2724 - "syn 2.0.111", 2725 ] 2726 2727 [[package]] ··· 2755 dependencies = [ 2756 "der", 2757 "spki", 2758 ] 2759 2760 [[package]] ··· 2794 checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 2795 dependencies = [ 2796 "proc-macro2", 2797 - "syn 2.0.111", 2798 ] 2799 2800 [[package]] ··· 2807 ] 2808 2809 [[package]] 2810 - name = "proc-macro-error" 2811 - version = "1.0.4" 2812 - source = "registry+https://github.com/rust-lang/crates.io-index" 2813 - checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 2814 - dependencies = [ 2815 - "proc-macro-error-attr", 2816 - "proc-macro2", 2817 - "quote", 2818 - "syn 1.0.109", 2819 - "version_check", 2820 - ] 2821 - 2822 - [[package]] 2823 - name = "proc-macro-error-attr" 2824 - version = "1.0.4" 2825 - source = "registry+https://github.com/rust-lang/crates.io-index" 2826 - checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 2827 - dependencies = [ 2828 - "proc-macro2", 2829 - "quote", 2830 - "version_check", 2831 - ] 2832 - 2833 - [[package]] 2834 name = "proc-macro2" 2835 - version = "1.0.103" 2836 source = "registry+https://github.com/rust-lang/crates.io-index" 2837 - checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 2838 dependencies = [ 2839 "unicode-ident", 2840 ] ··· 2847 dependencies = [ 2848 "proc-macro2", 2849 "quote", 2850 - "syn 2.0.111", 2851 "version_check", 2852 "yansi", 2853 ] ··· 2915 2916 [[package]] 2917 name = "quote" 2918 - version = "1.0.42" 2919 source = "registry+https://github.com/rust-lang/crates.io-index" 2920 - checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 2921 dependencies = [ 2922 "proc-macro2", 2923 ] ··· 2988 ] 2989 2990 [[package]] 2991 - name = "range-traits" 2992 - version = "0.3.2" 2993 source = "registry+https://github.com/rust-lang/crates.io-index" 2994 - checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 2995 2996 [[package]] 2997 name = "redox_syscall" 2998 - version = "0.5.18" 2999 source = "registry+https://github.com/rust-lang/crates.io-index" 3000 - checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 3001 dependencies = [ 3002 "bitflags", 3003 ] ··· 3014 ] 3015 3016 [[package]] 3017 - name = "ref-cast" 3018 - version = "1.0.25" 3019 - source = "registry+https://github.com/rust-lang/crates.io-index" 3020 - checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" 3021 - dependencies = [ 3022 - "ref-cast-impl", 3023 - ] 3024 - 3025 - [[package]] 3026 - name = "ref-cast-impl" 3027 - version = "1.0.25" 3028 - source = "registry+https://github.com/rust-lang/crates.io-index" 3029 - checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" 3030 - dependencies = [ 3031 - "proc-macro2", 3032 - "quote", 3033 - "syn 2.0.111", 3034 - ] 3035 - 3036 - [[package]] 3037 name = "regex" 3038 version = "1.12.2" 3039 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3070 3071 [[package]] 3072 name = "reqwest" 3073 - version = "0.12.24" 3074 source = "registry+https://github.com/rust-lang/crates.io-index" 3075 - checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" 3076 dependencies = [ 3077 - "async-compression", 3078 "base64 0.22.1", 3079 "bytes", 3080 "encoding_rs", ··· 3169 3170 [[package]] 3171 name = "rsa" 3172 - version = "0.9.9" 3173 source = "registry+https://github.com/rust-lang/crates.io-index" 3174 - checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" 3175 dependencies = [ 3176 "const-oid", 3177 "digest", ··· 3198 version = "2.1.1" 3199 source = "registry+https://github.com/rust-lang/crates.io-index" 3200 checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 3201 3202 [[package]] 3203 name = "rustix" 3204 - version = "1.1.2" 3205 source = "registry+https://github.com/rust-lang/crates.io-index" 3206 - checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 3207 dependencies = [ 3208 "bitflags", 3209 "errno", ··· 3214 3215 [[package]] 3216 name = "rustls" 3217 - version = "0.23.35" 3218 source = "registry+https://github.com/rust-lang/crates.io-index" 3219 - checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 3220 dependencies = [ 3221 "once_cell", 3222 "ring", ··· 3228 3229 [[package]] 3230 name = "rustls-native-certs" 3231 - version = "0.8.2" 3232 source = "registry+https://github.com/rust-lang/crates.io-index" 3233 - checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" 3234 dependencies = [ 3235 "openssl-probe", 3236 "rustls-pki-types", ··· 3240 3241 [[package]] 3242 name = "rustls-pki-types" 3243 - version = "1.13.0" 3244 source = "registry+https://github.com/rust-lang/crates.io-index" 3245 - checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" 3246 dependencies = [ 3247 "web-time", 3248 "zeroize", ··· 3267 3268 [[package]] 3269 name = "ryu" 3270 - version = "1.0.20" 3271 source = "registry+https://github.com/rust-lang/crates.io-index" 3272 - checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 3273 3274 [[package]] 3275 name = "safemem" ··· 3296 ] 3297 3298 [[package]] 3299 - name = "schemars" 3300 - version = "0.9.0" 3301 - source = "registry+https://github.com/rust-lang/crates.io-index" 3302 - checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" 3303 - dependencies = [ 3304 - "dyn-clone", 3305 - "ref-cast", 3306 - "serde", 3307 - "serde_json", 3308 - ] 3309 - 3310 - [[package]] 3311 - name = "schemars" 3312 - version = "1.1.0" 3313 - source = "registry+https://github.com/rust-lang/crates.io-index" 3314 - checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" 3315 - dependencies = [ 3316 - "dyn-clone", 3317 - "ref-cast", 3318 - "serde", 3319 - "serde_json", 3320 - ] 3321 - 3322 - [[package]] 3323 name = "scoped-tls" 3324 version = "1.0.1" 3325 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3367 "core-foundation-sys", 3368 "libc", 3369 ] 3370 3371 [[package]] 3372 name = "send_wrapper" ··· 3411 dependencies = [ 3412 "proc-macro2", 3413 "quote", 3414 - "syn 2.0.111", 3415 ] 3416 3417 [[package]] 3418 name = "serde_html_form" 3419 - version = "0.2.8" 3420 source = "registry+https://github.com/rust-lang/crates.io-index" 3421 - checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 3422 dependencies = [ 3423 "form_urlencoded", 3424 - "indexmap 2.12.1", 3425 "itoa", 3426 - "ryu", 3427 "serde_core", 3428 ] 3429 ··· 3441 3442 [[package]] 3443 name = "serde_json" 3444 - version = "1.0.145" 3445 source = "registry+https://github.com/rust-lang/crates.io-index" 3446 - checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 3447 dependencies = [ 3448 "itoa", 3449 "memchr", 3450 - "ryu", 3451 "serde", 3452 "serde_core", 3453 ] 3454 3455 [[package]] ··· 3471 dependencies = [ 3472 "proc-macro2", 3473 "quote", 3474 - "syn 2.0.111", 3475 ] 3476 3477 [[package]] ··· 3488 3489 [[package]] 3490 name = "serde_with" 3491 - version = "3.16.0" 3492 source = "registry+https://github.com/rust-lang/crates.io-index" 3493 - checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" 3494 dependencies = [ 3495 "base64 0.22.1", 3496 "chrono", 3497 "hex", 3498 - "indexmap 1.9.3", 3499 - "indexmap 2.12.1", 3500 - "schemars 0.9.0", 3501 - "schemars 1.1.0", 3502 "serde_core", 3503 "serde_json", 3504 "serde_with_macros", ··· 3507 3508 [[package]] 3509 name = "serde_with_macros" 3510 - version = "3.16.0" 3511 source = "registry+https://github.com/rust-lang/crates.io-index" 3512 - checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" 3513 dependencies = [ 3514 "darling", 3515 "proc-macro2", 3516 "quote", 3517 - "syn 2.0.111", 3518 ] 3519 3520 [[package]] ··· 3571 3572 [[package]] 3573 name = "signal-hook-registry" 3574 - version = "1.4.7" 3575 source = "registry+https://github.com/rust-lang/crates.io-index" 3576 - checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" 3577 dependencies = [ 3578 "libc", 3579 ] 3580 ··· 3590 3591 [[package]] 3592 name = "simd-adler32" 3593 - version = "0.3.7" 3594 source = "registry+https://github.com/rust-lang/crates.io-index" 3595 - checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 3596 3597 [[package]] 3598 name = "siphasher" ··· 3647 version = "0.9.8" 3648 source = "registry+https://github.com/rust-lang/crates.io-index" 3649 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3650 3651 [[package]] 3652 name = "spin" ··· 3671 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 3672 3673 [[package]] 3674 - name = "static-regular-grammar" 3675 - version = "2.0.2" 3676 - source = "registry+https://github.com/rust-lang/crates.io-index" 3677 - checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" 3678 - dependencies = [ 3679 - "abnf", 3680 - "btree-range-map", 3681 - "ciborium", 3682 - "hex_fmt", 3683 - "indoc", 3684 - "proc-macro-error", 3685 - "proc-macro2", 3686 - "quote", 3687 - "serde", 3688 - "sha2", 3689 - "syn 2.0.111", 3690 - "thiserror 1.0.69", 3691 - ] 3692 - 3693 - [[package]] 3694 name = "static_assertions" 3695 version = "1.1.0" 3696 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3744 3745 [[package]] 3746 name = "supports-hyperlinks" 3747 - version = "3.1.0" 3748 source = "registry+https://github.com/rust-lang/crates.io-index" 3749 - checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" 3750 3751 [[package]] 3752 name = "supports-unicode" ··· 3767 3768 [[package]] 3769 name = "syn" 3770 - version = "2.0.111" 3771 source = "registry+https://github.com/rust-lang/crates.io-index" 3772 - checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 3773 dependencies = [ 3774 "proc-macro2", 3775 "quote", ··· 3793 dependencies = [ 3794 "proc-macro2", 3795 "quote", 3796 - "syn 2.0.111", 3797 ] 3798 3799 [[package]] ··· 3825 3826 [[package]] 3827 name = "tempfile" 3828 - version = "3.23.0" 3829 source = "registry+https://github.com/rust-lang/crates.io-index" 3830 - checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 3831 dependencies = [ 3832 "fastrand", 3833 "getrandom 0.3.4", ··· 3893 dependencies = [ 3894 "proc-macro2", 3895 "quote", 3896 - "syn 2.0.111", 3897 ] 3898 3899 [[package]] ··· 3904 dependencies = [ 3905 "proc-macro2", 3906 "quote", 3907 - "syn 2.0.111", 3908 ] 3909 3910 [[package]] ··· 3932 checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 3933 dependencies = [ 3934 "deranged", 3935 - "itoa", 3936 "libc", 3937 "num-conv", 3938 "num_threads", 3939 "powerfmt", 3940 "serde", 3941 "time-core", 3942 - "time-macros", 3943 ] 3944 3945 [[package]] ··· 3947 version = "0.1.6" 3948 source = "registry+https://github.com/rust-lang/crates.io-index" 3949 checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 3950 - 3951 - [[package]] 3952 - name = "time-macros" 3953 - version = "0.2.24" 3954 - source = "registry+https://github.com/rust-lang/crates.io-index" 3955 - checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 3956 - dependencies = [ 3957 - "num-conv", 3958 - "time-core", 3959 - ] 3960 3961 [[package]] 3962 name = "tiny_http" ··· 3997 3998 [[package]] 3999 name = "tokio" 4000 - version = "1.48.0" 4001 source = "registry+https://github.com/rust-lang/crates.io-index" 4002 - checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 4003 dependencies = [ 4004 "bytes", 4005 "libc", ··· 4020 dependencies = [ 4021 "proc-macro2", 4022 "quote", 4023 - "syn 2.0.111", 4024 ] 4025 4026 [[package]] ··· 4070 4071 [[package]] 4072 name = "tokio-util" 4073 - version = "0.7.17" 4074 source = "registry+https://github.com/rust-lang/crates.io-index" 4075 - checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 4076 dependencies = [ 4077 "bytes", 4078 "futures-core", ··· 4100 4101 [[package]] 4102 name = "tower-http" 4103 - version = "0.6.7" 4104 source = "registry+https://github.com/rust-lang/crates.io-index" 4105 - checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" 4106 dependencies = [ 4107 "async-compression", 4108 "bitflags", ··· 4141 4142 [[package]] 4143 name = "tracing" 4144 - version = "0.1.41" 4145 source = "registry+https://github.com/rust-lang/crates.io-index" 4146 - checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 4147 dependencies = [ 4148 "log", 4149 "pin-project-lite", ··· 4153 4154 [[package]] 4155 name = "tracing-attributes" 4156 - version = "0.1.30" 4157 source = "registry+https://github.com/rust-lang/crates.io-index" 4158 - checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 4159 dependencies = [ 4160 "proc-macro2", 4161 "quote", 4162 - "syn 2.0.111", 4163 ] 4164 4165 [[package]] 4166 name = "tracing-core" 4167 - version = "0.1.34" 4168 source = "registry+https://github.com/rust-lang/crates.io-index" 4169 - checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 4170 dependencies = [ 4171 "once_cell", 4172 "valuable", ··· 4185 4186 [[package]] 4187 name = "tracing-subscriber" 4188 - version = "0.3.20" 4189 source = "registry+https://github.com/rust-lang/crates.io-index" 4190 - checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 4191 dependencies = [ 4192 "matchers", 4193 "nu-ansi-term", ··· 4209 dependencies = [ 4210 "proc-macro2", 4211 "quote", 4212 - "syn 2.0.111", 4213 ] 4214 4215 [[package]] ··· 4261 4262 [[package]] 4263 name = "unicase" 4264 - version = "2.8.1" 4265 source = "registry+https://github.com/rust-lang/crates.io-index" 4266 - checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 4267 4268 [[package]] 4269 name = "unicode-ident" ··· 4315 4316 [[package]] 4317 name = "url" 4318 - version = "2.5.7" 4319 source = "registry+https://github.com/rust-lang/crates.io-index" 4320 - checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 4321 dependencies = [ 4322 "form_urlencoded", 4323 "idna", 4324 "percent-encoding", 4325 "serde", 4326 ] 4327 4328 [[package]] ··· 4397 4398 [[package]] 4399 name = "wasm-bindgen" 4400 - version = "0.2.105" 4401 source = "registry+https://github.com/rust-lang/crates.io-index" 4402 - checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" 4403 dependencies = [ 4404 "cfg-if", 4405 "once_cell", ··· 4410 4411 [[package]] 4412 name = "wasm-bindgen-futures" 4413 - version = "0.4.55" 4414 source = "registry+https://github.com/rust-lang/crates.io-index" 4415 - checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" 4416 dependencies = [ 4417 "cfg-if", 4418 "js-sys", ··· 4423 4424 [[package]] 4425 name = "wasm-bindgen-macro" 4426 - version = "0.2.105" 4427 source = "registry+https://github.com/rust-lang/crates.io-index" 4428 - checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" 4429 dependencies = [ 4430 "quote", 4431 "wasm-bindgen-macro-support", ··· 4433 4434 [[package]] 4435 name = "wasm-bindgen-macro-support" 4436 - version = "0.2.105" 4437 source = "registry+https://github.com/rust-lang/crates.io-index" 4438 - checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" 4439 dependencies = [ 4440 "bumpalo", 4441 "proc-macro2", 4442 "quote", 4443 - "syn 2.0.111", 4444 "wasm-bindgen-shared", 4445 ] 4446 4447 [[package]] 4448 name = "wasm-bindgen-shared" 4449 - version = "0.2.105" 4450 source = "registry+https://github.com/rust-lang/crates.io-index" 4451 - checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" 4452 dependencies = [ 4453 "unicode-ident", 4454 ] ··· 4468 4469 [[package]] 4470 name = "web-sys" 4471 - version = "0.3.82" 4472 source = "registry+https://github.com/rust-lang/crates.io-index" 4473 - checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" 4474 dependencies = [ 4475 "js-sys", 4476 "wasm-bindgen", ··· 4516 4517 [[package]] 4518 name = "webpki-roots" 4519 - version = "1.0.4" 4520 source = "registry+https://github.com/rust-lang/crates.io-index" 4521 - checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" 4522 dependencies = [ 4523 "rustls-pki-types", 4524 ] ··· 4539 ] 4540 4541 [[package]] 4542 - name = "windows" 4543 - version = "0.61.3" 4544 - source = "registry+https://github.com/rust-lang/crates.io-index" 4545 - checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 4546 - dependencies = [ 4547 - "windows-collections", 4548 - "windows-core 0.61.2", 4549 - "windows-future", 4550 - "windows-link 0.1.3", 4551 - "windows-numerics", 4552 - ] 4553 - 4554 - [[package]] 4555 - name = "windows-collections" 4556 - version = "0.2.0" 4557 - source = "registry+https://github.com/rust-lang/crates.io-index" 4558 - checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 4559 - dependencies = [ 4560 - "windows-core 0.61.2", 4561 - ] 4562 - 4563 - [[package]] 4564 - name = "windows-core" 4565 - version = "0.61.2" 4566 - source = "registry+https://github.com/rust-lang/crates.io-index" 4567 - checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 4568 - dependencies = [ 4569 - "windows-implement", 4570 - "windows-interface", 4571 - "windows-link 0.1.3", 4572 - "windows-result 0.3.4", 4573 - "windows-strings 0.4.2", 4574 - ] 4575 - 4576 - [[package]] 4577 name = "windows-core" 4578 version = "0.62.2" 4579 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4581 dependencies = [ 4582 "windows-implement", 4583 "windows-interface", 4584 - "windows-link 0.2.1", 4585 - "windows-result 0.4.1", 4586 - "windows-strings 0.5.1", 4587 - ] 4588 - 4589 - [[package]] 4590 - name = "windows-future" 4591 - version = "0.2.1" 4592 - source = "registry+https://github.com/rust-lang/crates.io-index" 4593 - checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 4594 - dependencies = [ 4595 - "windows-core 0.61.2", 4596 - "windows-link 0.1.3", 4597 - "windows-threading", 4598 ] 4599 4600 [[package]] ··· 4605 dependencies = [ 4606 "proc-macro2", 4607 "quote", 4608 - "syn 2.0.111", 4609 ] 4610 4611 [[package]] ··· 4616 dependencies = [ 4617 "proc-macro2", 4618 "quote", 4619 - "syn 2.0.111", 4620 ] 4621 4622 [[package]] 4623 name = "windows-link" 4624 - version = "0.1.3" 4625 - source = "registry+https://github.com/rust-lang/crates.io-index" 4626 - checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 4627 - 4628 - [[package]] 4629 - name = "windows-link" 4630 version = "0.2.1" 4631 source = "registry+https://github.com/rust-lang/crates.io-index" 4632 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 4633 4634 [[package]] 4635 - name = "windows-numerics" 4636 - version = "0.2.0" 4637 - source = "registry+https://github.com/rust-lang/crates.io-index" 4638 - checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 4639 - dependencies = [ 4640 - "windows-core 0.61.2", 4641 - "windows-link 0.1.3", 4642 - ] 4643 - 4644 - [[package]] 4645 name = "windows-registry" 4646 version = "0.6.1" 4647 source = "registry+https://github.com/rust-lang/crates.io-index" 4648 checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 4649 dependencies = [ 4650 - "windows-link 0.2.1", 4651 - "windows-result 0.4.1", 4652 - "windows-strings 0.5.1", 4653 - ] 4654 - 4655 - [[package]] 4656 - name = "windows-result" 4657 - version = "0.3.4" 4658 - source = "registry+https://github.com/rust-lang/crates.io-index" 4659 - checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 4660 - dependencies = [ 4661 - "windows-link 0.1.3", 4662 ] 4663 4664 [[package]] ··· 4667 source = "registry+https://github.com/rust-lang/crates.io-index" 4668 checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 4669 dependencies = [ 4670 - "windows-link 0.2.1", 4671 - ] 4672 - 4673 - [[package]] 4674 - name = "windows-strings" 4675 - version = "0.4.2" 4676 - source = "registry+https://github.com/rust-lang/crates.io-index" 4677 - checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 4678 - dependencies = [ 4679 - "windows-link 0.1.3", 4680 ] 4681 4682 [[package]] ··· 4685 source = "registry+https://github.com/rust-lang/crates.io-index" 4686 checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 4687 dependencies = [ 4688 - "windows-link 0.2.1", 4689 ] 4690 4691 [[package]] ··· 4717 4718 [[package]] 4719 name = "windows-sys" 4720 version = "0.60.2" 4721 source = "registry+https://github.com/rust-lang/crates.io-index" 4722 checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" ··· 4730 source = "registry+https://github.com/rust-lang/crates.io-index" 4731 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 4732 dependencies = [ 4733 - "windows-link 0.2.1", 4734 ] 4735 4736 [[package]] ··· 4785 source = "registry+https://github.com/rust-lang/crates.io-index" 4786 checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 4787 dependencies = [ 4788 - "windows-link 0.2.1", 4789 "windows_aarch64_gnullvm 0.53.1", 4790 "windows_aarch64_msvc 0.53.1", 4791 "windows_i686_gnu 0.53.1", ··· 4794 "windows_x86_64_gnu 0.53.1", 4795 "windows_x86_64_gnullvm 0.53.1", 4796 "windows_x86_64_msvc 0.53.1", 4797 - ] 4798 - 4799 - [[package]] 4800 - name = "windows-threading" 4801 - version = "0.1.0" 4802 - source = "registry+https://github.com/rust-lang/crates.io-index" 4803 - checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 4804 - dependencies = [ 4805 - "windows-link 0.1.3", 4806 ] 4807 4808 [[package]] ··· 5008 "futures", 5009 "globset", 5010 "ignore", 5011 "jacquard", 5012 "jacquard-api", 5013 "jacquard-common", ··· 5032 "tower-http", 5033 "url", 5034 "walkdir", 5035 ] 5036 5037 [[package]] ··· 5082 dependencies = [ 5083 "proc-macro2", 5084 "quote", 5085 - "syn 2.0.111", 5086 "synstructure", 5087 ] 5088 5089 [[package]] 5090 name = "zerocopy" 5091 - version = "0.8.30" 5092 source = "registry+https://github.com/rust-lang/crates.io-index" 5093 - checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" 5094 dependencies = [ 5095 "zerocopy-derive", 5096 ] 5097 5098 [[package]] 5099 name = "zerocopy-derive" 5100 - version = "0.8.30" 5101 source = "registry+https://github.com/rust-lang/crates.io-index" 5102 - checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" 5103 dependencies = [ 5104 "proc-macro2", 5105 "quote", 5106 - "syn 2.0.111", 5107 ] 5108 5109 [[package]] ··· 5123 dependencies = [ 5124 "proc-macro2", 5125 "quote", 5126 - "syn 2.0.111", 5127 "synstructure", 5128 ] 5129 ··· 5166 dependencies = [ 5167 "proc-macro2", 5168 "quote", 5169 - "syn 2.0.111", 5170 ]
··· 3 version = 4 4 5 [[package]] 6 name = "addr2line" 7 version = "0.25.1" 8 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 52 dependencies = [ 53 "alloc-no-stdlib", 54 ] 55 + 56 + [[package]] 57 + name = "allocator-api2" 58 + version = "0.2.21" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 61 62 [[package]] 63 name = "android_system_properties" ··· 126 127 [[package]] 128 name = "async-compression" 129 + version = "0.4.36" 130 source = "registry+https://github.com/rust-lang/crates.io-index" 131 + checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" 132 dependencies = [ 133 "compression-codecs", 134 "compression-core", ··· 145 dependencies = [ 146 "proc-macro2", 147 "quote", 148 + "syn 2.0.113", 149 + ] 150 + 151 + [[package]] 152 + name = "atomic-polyfill" 153 + version = "1.0.3" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 156 + dependencies = [ 157 + "critical-section", 158 ] 159 160 [[package]] ··· 171 172 [[package]] 173 name = "axum" 174 + version = "0.8.8" 175 source = "registry+https://github.com/rust-lang/crates.io-index" 176 + checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 177 dependencies = [ 178 "axum-core", 179 "bytes", ··· 204 205 [[package]] 206 name = "axum-core" 207 + version = "0.5.6" 208 source = "registry+https://github.com/rust-lang/crates.io-index" 209 + checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" 210 dependencies = [ 211 "bytes", 212 "futures-core", ··· 233 "miniz_oxide", 234 "object", 235 "rustc-demangle", 236 + "windows-link", 237 ] 238 239 [[package]] ··· 281 282 [[package]] 283 name = "base64ct" 284 + version = "1.8.2" 285 source = "registry+https://github.com/rust-lang/crates.io-index" 286 + checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" 287 288 [[package]] 289 name = "bitflags" ··· 322 "proc-macro2", 323 "quote", 324 "rustversion", 325 + "syn 2.0.113", 326 ] 327 328 [[package]] 329 name = "borsh" 330 + version = "1.6.0" 331 source = "registry+https://github.com/rust-lang/crates.io-index" 332 + checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" 333 dependencies = [ 334 "cfg_aliases", 335 ] ··· 366 ] 367 368 [[package]] 369 name = "buf_redux" 370 version = "0.8.4" 371 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 377 378 [[package]] 379 name = "bumpalo" 380 + version = "3.19.1" 381 source = "registry+https://github.com/rust-lang/crates.io-index" 382 + checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" 383 384 [[package]] 385 name = "byteorder" ··· 407 408 [[package]] 409 name = "cc" 410 + version = "1.2.51" 411 source = "registry+https://github.com/rust-lang/crates.io-index" 412 + checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" 413 dependencies = [ 414 "find-msvc-tools", 415 "shlex", 416 ] 417 418 [[package]] 419 name = "cesu8" 420 version = "1.1.0" 421 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 444 "num-traits", 445 "serde", 446 "wasm-bindgen", 447 + "windows-link", 448 ] 449 450 [[package]] ··· 496 497 [[package]] 498 name = "clap" 499 + version = "4.5.54" 500 source = "registry+https://github.com/rust-lang/crates.io-index" 501 + checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" 502 dependencies = [ 503 "clap_builder", 504 "clap_derive", ··· 506 507 [[package]] 508 name = "clap_builder" 509 + version = "4.5.54" 510 source = "registry+https://github.com/rust-lang/crates.io-index" 511 + checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" 512 dependencies = [ 513 "anstream", 514 "anstyle", ··· 525 "heck 0.5.0", 526 "proc-macro2", 527 "quote", 528 + "syn 2.0.113", 529 ] 530 531 [[package]] ··· 535 checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 536 537 [[package]] 538 + name = "cobs" 539 + version = "0.3.0" 540 + source = "registry+https://github.com/rust-lang/crates.io-index" 541 + checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" 542 + dependencies = [ 543 + "thiserror 2.0.17", 544 + ] 545 + 546 + [[package]] 547 name = "colorchoice" 548 version = "1.0.4" 549 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 561 562 [[package]] 563 name = "compression-codecs" 564 + version = "0.4.35" 565 source = "registry+https://github.com/rust-lang/crates.io-index" 566 + checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" 567 dependencies = [ 568 "compression-core", 569 "flate2", ··· 575 version = "0.4.31" 576 source = "registry+https://github.com/rust-lang/crates.io-index" 577 checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" 578 + 579 + [[package]] 580 + name = "console" 581 + version = "0.15.11" 582 + source = "registry+https://github.com/rust-lang/crates.io-index" 583 + checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 584 + dependencies = [ 585 + "encode_unicode", 586 + "libc", 587 + "once_cell", 588 + "unicode-width 0.2.2", 589 + "windows-sys 0.59.0", 590 + ] 591 592 [[package]] 593 name = "const-oid" ··· 663 dependencies = [ 664 "cfg-if", 665 ] 666 + 667 + [[package]] 668 + name = "critical-section" 669 + version = "1.2.0" 670 + source = "registry+https://github.com/rust-lang/crates.io-index" 671 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 672 673 [[package]] 674 name = "crossbeam-channel" ··· 753 "proc-macro2", 754 "quote", 755 "strsim", 756 + "syn 2.0.113", 757 ] 758 759 [[package]] ··· 764 dependencies = [ 765 "darling_core", 766 "quote", 767 + "syn 2.0.113", 768 ] 769 770 [[package]] ··· 804 checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 805 dependencies = [ 806 "data-encoding", 807 + "syn 2.0.113", 808 ] 809 810 [[package]] ··· 835 checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 836 dependencies = [ 837 "powerfmt", 838 ] 839 840 [[package]] ··· 854 dependencies = [ 855 "proc-macro2", 856 "quote", 857 + "syn 2.0.113", 858 "unicode-xid", 859 ] 860 ··· 905 dependencies = [ 906 "proc-macro2", 907 "quote", 908 + "syn 2.0.113", 909 ] 910 911 [[package]] 912 name = "ecdsa" 913 version = "0.16.9" 914 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 943 ] 944 945 [[package]] 946 + name = "embedded-io" 947 + version = "0.4.0" 948 + source = "registry+https://github.com/rust-lang/crates.io-index" 949 + checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" 950 + 951 + [[package]] 952 + name = "embedded-io" 953 + version = "0.6.1" 954 + source = "registry+https://github.com/rust-lang/crates.io-index" 955 + checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 956 + 957 + [[package]] 958 + name = "encode_unicode" 959 + version = "1.0.0" 960 + source = "registry+https://github.com/rust-lang/crates.io-index" 961 + checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 962 + 963 + [[package]] 964 name = "encoding_rs" 965 version = "0.8.35" 966 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 978 "heck 0.5.0", 979 "proc-macro2", 980 "quote", 981 + "syn 2.0.113", 982 ] 983 984 [[package]] ··· 1027 1028 [[package]] 1029 name = "find-msvc-tools" 1030 + version = "0.1.6" 1031 source = "registry+https://github.com/rust-lang/crates.io-index" 1032 + checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" 1033 1034 [[package]] 1035 name = "flate2" ··· 1048 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 1049 1050 [[package]] 1051 + name = "foldhash" 1052 + version = "0.1.5" 1053 + source = "registry+https://github.com/rust-lang/crates.io-index" 1054 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 1055 + 1056 + [[package]] 1057 name = "form_urlencoded" 1058 version = "1.2.2" 1059 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1154 dependencies = [ 1155 "proc-macro2", 1156 "quote", 1157 + "syn 2.0.113", 1158 ] 1159 1160 [[package]] ··· 1189 1190 [[package]] 1191 name = "generator" 1192 + version = "0.8.8" 1193 source = "registry+https://github.com/rust-lang/crates.io-index" 1194 + checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" 1195 dependencies = [ 1196 "cc", 1197 "cfg-if", 1198 "libc", 1199 "log", 1200 "rustversion", 1201 + "windows-link", 1202 + "windows-result", 1203 ] 1204 1205 [[package]] ··· 1309 1310 [[package]] 1311 name = "h2" 1312 + version = "0.4.13" 1313 source = "registry+https://github.com/rust-lang/crates.io-index" 1314 + checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" 1315 dependencies = [ 1316 "atomic-waker", 1317 "bytes", ··· 1319 "futures-core", 1320 "futures-sink", 1321 "http", 1322 + "indexmap", 1323 "slab", 1324 "tokio", 1325 "tokio-util", ··· 1338 ] 1339 1340 [[package]] 1341 + name = "hash32" 1342 + version = "0.2.1" 1343 source = "registry+https://github.com/rust-lang/crates.io-index" 1344 + checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 1345 + dependencies = [ 1346 + "byteorder", 1347 + ] 1348 1349 [[package]] 1350 name = "hashbrown" ··· 1354 1355 [[package]] 1356 name = "hashbrown" 1357 + version = "0.15.5" 1358 + source = "registry+https://github.com/rust-lang/crates.io-index" 1359 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 1360 + dependencies = [ 1361 + "allocator-api2", 1362 + "equivalent", 1363 + "foldhash", 1364 + ] 1365 + 1366 + [[package]] 1367 + name = "hashbrown" 1368 version = "0.16.1" 1369 source = "registry+https://github.com/rust-lang/crates.io-index" 1370 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 1371 1372 [[package]] 1373 + name = "heapless" 1374 + version = "0.7.17" 1375 + source = "registry+https://github.com/rust-lang/crates.io-index" 1376 + checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 1377 + dependencies = [ 1378 + "atomic-polyfill", 1379 + "hash32", 1380 + "rustc_version", 1381 + "serde", 1382 + "spin 0.9.8", 1383 + "stable_deref_trait", 1384 + ] 1385 + 1386 + [[package]] 1387 name = "heck" 1388 version = "0.4.1" 1389 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1408 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1409 1410 [[package]] 1411 name = "hickory-proto" 1412 version = "0.24.4" 1413 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1472 "markup5ever", 1473 "proc-macro2", 1474 "quote", 1475 + "syn 2.0.113", 1476 ] 1477 1478 [[package]] ··· 1568 1569 [[package]] 1570 name = "hyper-util" 1571 + version = "0.1.19" 1572 source = "registry+https://github.com/rust-lang/crates.io-index" 1573 + checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 1574 dependencies = [ 1575 "base64 0.22.1", 1576 "bytes", ··· 1604 "js-sys", 1605 "log", 1606 "wasm-bindgen", 1607 + "windows-core", 1608 ] 1609 1610 [[package]] ··· 1664 1665 [[package]] 1666 name = "icu_properties" 1667 + version = "2.1.2" 1668 source = "registry+https://github.com/rust-lang/crates.io-index" 1669 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 1670 dependencies = [ 1671 "icu_collections", 1672 "icu_locale_core", ··· 1678 1679 [[package]] 1680 name = "icu_properties_data" 1681 + version = "2.1.2" 1682 source = "registry+https://github.com/rust-lang/crates.io-index" 1683 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 1684 1685 [[package]] 1686 name = "icu_provider" ··· 1742 1743 [[package]] 1744 name = "indexmap" 1745 version = "2.12.1" 1746 source = "registry+https://github.com/rust-lang/crates.io-index" 1747 checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 1748 dependencies = [ 1749 "equivalent", 1750 "hashbrown 0.16.1", 1751 ] 1752 1753 [[package]] 1754 + name = "indicatif" 1755 + version = "0.17.11" 1756 source = "registry+https://github.com/rust-lang/crates.io-index" 1757 + checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 1758 dependencies = [ 1759 + "console", 1760 + "number_prefix", 1761 + "portable-atomic", 1762 + "unicode-width 0.2.2", 1763 + "web-time", 1764 ] 1765 1766 [[package]] ··· 1803 1804 [[package]] 1805 name = "iri-string" 1806 + version = "0.7.10" 1807 source = "registry+https://github.com/rust-lang/crates.io-index" 1808 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 1809 dependencies = [ 1810 "memchr", 1811 "serde", ··· 1825 1826 [[package]] 1827 name = "itoa" 1828 + version = "1.0.17" 1829 source = "registry+https://github.com/rust-lang/crates.io-index" 1830 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1831 1832 [[package]] 1833 name = "jacquard" 1834 + version = "0.9.5" 1835 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 1836 dependencies = [ 1837 "bytes", 1838 "getrandom 0.2.16", ··· 1861 1862 [[package]] 1863 name = "jacquard-api" 1864 + version = "0.9.5" 1865 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 1866 dependencies = [ 1867 "bon", 1868 "bytes", ··· 1872 "miette", 1873 "rustversion", 1874 "serde", 1875 + "serde_bytes", 1876 "serde_ipld_dagcbor", 1877 "thiserror 2.0.17", 1878 "unicode-segmentation", ··· 1880 1881 [[package]] 1882 name = "jacquard-common" 1883 + version = "0.9.5" 1884 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 1885 dependencies = [ 1886 "base64 0.22.1", 1887 "bon", 1888 "bytes", 1889 "chrono", 1890 "ciborium", 1891 + "ciborium-io", 1892 "cid", 1893 "futures", 1894 "getrandom 0.2.16", 1895 "getrandom 0.3.4", 1896 + "hashbrown 0.15.5", 1897 "http", 1898 "ipld-core", 1899 "k256", 1900 + "maitake-sync", 1901 "miette", 1902 "multibase", 1903 "multihash", 1904 "n0-future 0.1.3", 1905 "ouroboros", 1906 + "oxilangtag", 1907 "p256", 1908 + "postcard", 1909 "rand 0.9.2", 1910 "regex", 1911 + "regex-automata", 1912 "regex-lite", 1913 "reqwest", 1914 "serde", 1915 + "serde_bytes", 1916 "serde_html_form", 1917 "serde_ipld_dagcbor", 1918 "serde_json", 1919 "signature", 1920 "smol_str", 1921 + "spin 0.10.0", 1922 "thiserror 2.0.17", 1923 "tokio", 1924 "tokio-tungstenite-wasm", ··· 1929 1930 [[package]] 1931 name = "jacquard-derive" 1932 + version = "0.9.5" 1933 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 1934 dependencies = [ 1935 "heck 0.5.0", 1936 "jacquard-lexicon", 1937 "proc-macro2", 1938 "quote", 1939 + "syn 2.0.113", 1940 ] 1941 1942 [[package]] 1943 name = "jacquard-identity" 1944 + version = "0.9.5" 1945 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 1946 dependencies = [ 1947 "bon", 1948 "bytes", ··· 1952 "jacquard-common", 1953 "jacquard-lexicon", 1954 "miette", 1955 + "mini-moka-wasm", 1956 + "n0-future 0.1.3", 1957 "percent-encoding", 1958 "reqwest", 1959 "serde", ··· 1968 1969 [[package]] 1970 name = "jacquard-lexicon" 1971 + version = "0.9.5" 1972 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 1973 dependencies = [ 1974 "cid", 1975 "dashmap", ··· 1987 "serde_repr", 1988 "serde_with", 1989 "sha2", 1990 + "syn 2.0.113", 1991 "thiserror 2.0.17", 1992 "unicode-segmentation", 1993 ] 1994 1995 [[package]] 1996 name = "jacquard-oauth" 1997 + version = "0.9.6" 1998 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 1999 dependencies = [ 2000 "base64 0.22.1", 2001 "bytes", ··· 2083 2084 [[package]] 2085 name = "js-sys" 2086 + version = "0.3.83" 2087 source = "registry+https://github.com/rust-lang/crates.io-index" 2088 + checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" 2089 dependencies = [ 2090 "once_cell", 2091 "wasm-bindgen", ··· 2104 ] 2105 2106 [[package]] 2107 name = "lazy_static" 2108 version = "1.5.0" 2109 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2114 2115 [[package]] 2116 name = "libc" 2117 + version = "0.2.179" 2118 source = "registry+https://github.com/rust-lang/crates.io-index" 2119 + checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" 2120 2121 [[package]] 2122 name = "libm" ··· 2126 2127 [[package]] 2128 name = "libredox" 2129 + version = "0.1.12" 2130 source = "registry+https://github.com/rust-lang/crates.io-index" 2131 + checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 2132 dependencies = [ 2133 "bitflags", 2134 "libc", 2135 + "redox_syscall 0.7.0", 2136 ] 2137 2138 [[package]] ··· 2164 2165 [[package]] 2166 name = "log" 2167 + version = "0.4.29" 2168 source = "registry+https://github.com/rust-lang/crates.io-index" 2169 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 2170 2171 [[package]] 2172 name = "loom" ··· 2203 checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 2204 2205 [[package]] 2206 + name = "maitake-sync" 2207 + version = "0.1.2" 2208 + source = "registry+https://github.com/rust-lang/crates.io-index" 2209 + checksum = "6816ab14147f80234c675b80ed6dc4f440d8a1cefc158e766067aedb84c0bcd5" 2210 + dependencies = [ 2211 + "cordyceps", 2212 + "loom", 2213 + "mycelium-bitfield", 2214 + "pin-project", 2215 + "portable-atomic", 2216 + ] 2217 + 2218 + [[package]] 2219 name = "markup5ever" 2220 version = "0.12.1" 2221 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2300 dependencies = [ 2301 "proc-macro2", 2302 "quote", 2303 + "syn 2.0.113", 2304 ] 2305 2306 [[package]] ··· 2320 ] 2321 2322 [[package]] 2323 + name = "mini-moka-wasm" 2324 version = "0.10.99" 2325 + source = "git+https://tangled.org/nonbinary.computer/jacquard#5bcf7f8e87324b8e67fc273c678d0490c9c6d15b" 2326 dependencies = [ 2327 "crossbeam-channel", 2328 "crossbeam-utils", ··· 2334 ] 2335 2336 [[package]] 2337 name = "miniz_oxide" 2338 version = "0.8.9" 2339 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2345 2346 [[package]] 2347 name = "mio" 2348 + version = "1.1.1" 2349 source = "registry+https://github.com/rust-lang/crates.io-index" 2350 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 2351 dependencies = [ 2352 "libc", 2353 "wasi", ··· 2396 ] 2397 2398 [[package]] 2399 + name = "mycelium-bitfield" 2400 + version = "0.1.5" 2401 + source = "registry+https://github.com/rust-lang/crates.io-index" 2402 + checksum = "24e0cc5e2c585acbd15c5ce911dff71e1f4d5313f43345873311c4f5efd741cc" 2403 + 2404 + [[package]] 2405 name = "n0-future" 2406 version = "0.1.3" 2407 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2454 version = "1.0.6" 2455 source = "registry+https://github.com/rust-lang/crates.io-index" 2456 checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 2457 2458 [[package]] 2459 name = "nu-ansi-term" ··· 2536 ] 2537 2538 [[package]] 2539 + name = "number_prefix" 2540 + version = "0.4.0" 2541 + source = "registry+https://github.com/rust-lang/crates.io-index" 2542 + checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 2543 + 2544 + [[package]] 2545 name = "objc2" 2546 version = "0.6.3" 2547 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2589 2590 [[package]] 2591 name = "openssl-probe" 2592 + version = "0.2.0" 2593 source = "registry+https://github.com/rust-lang/crates.io-index" 2594 + checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" 2595 2596 [[package]] 2597 name = "option-ext" ··· 2620 "proc-macro2", 2621 "proc-macro2-diagnostics", 2622 "quote", 2623 + "syn 2.0.113", 2624 ] 2625 2626 [[package]] ··· 2630 checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 2631 2632 [[package]] 2633 + name = "oxilangtag" 2634 + version = "0.1.5" 2635 + source = "registry+https://github.com/rust-lang/crates.io-index" 2636 + checksum = "23f3f87617a86af77fa3691e6350483e7154c2ead9f1261b75130e21ca0f8acb" 2637 + dependencies = [ 2638 + "serde", 2639 + ] 2640 + 2641 + [[package]] 2642 name = "p256" 2643 version = "0.13.2" 2644 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2684 dependencies = [ 2685 "cfg-if", 2686 "libc", 2687 + "redox_syscall 0.5.18", 2688 "smallvec", 2689 + "windows-link", 2690 ] 2691 2692 [[package]] ··· 2759 dependencies = [ 2760 "proc-macro2", 2761 "quote", 2762 + "syn 2.0.113", 2763 ] 2764 2765 [[package]] ··· 2793 dependencies = [ 2794 "der", 2795 "spki", 2796 + ] 2797 + 2798 + [[package]] 2799 + name = "portable-atomic" 2800 + version = "1.13.0" 2801 + source = "registry+https://github.com/rust-lang/crates.io-index" 2802 + checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" 2803 + 2804 + [[package]] 2805 + name = "postcard" 2806 + version = "1.1.3" 2807 + source = "registry+https://github.com/rust-lang/crates.io-index" 2808 + checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" 2809 + dependencies = [ 2810 + "cobs", 2811 + "embedded-io 0.4.0", 2812 + "embedded-io 0.6.1", 2813 + "heapless", 2814 + "serde", 2815 ] 2816 2817 [[package]] ··· 2851 checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 2852 dependencies = [ 2853 "proc-macro2", 2854 + "syn 2.0.113", 2855 ] 2856 2857 [[package]] ··· 2864 ] 2865 2866 [[package]] 2867 name = "proc-macro2" 2868 + version = "1.0.105" 2869 source = "registry+https://github.com/rust-lang/crates.io-index" 2870 + checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" 2871 dependencies = [ 2872 "unicode-ident", 2873 ] ··· 2880 dependencies = [ 2881 "proc-macro2", 2882 "quote", 2883 + "syn 2.0.113", 2884 "version_check", 2885 "yansi", 2886 ] ··· 2948 2949 [[package]] 2950 name = "quote" 2951 + version = "1.0.43" 2952 source = "registry+https://github.com/rust-lang/crates.io-index" 2953 + checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" 2954 dependencies = [ 2955 "proc-macro2", 2956 ] ··· 3021 ] 3022 3023 [[package]] 3024 + name = "redox_syscall" 3025 + version = "0.5.18" 3026 source = "registry+https://github.com/rust-lang/crates.io-index" 3027 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 3028 + dependencies = [ 3029 + "bitflags", 3030 + ] 3031 3032 [[package]] 3033 name = "redox_syscall" 3034 + version = "0.7.0" 3035 source = "registry+https://github.com/rust-lang/crates.io-index" 3036 + checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" 3037 dependencies = [ 3038 "bitflags", 3039 ] ··· 3050 ] 3051 3052 [[package]] 3053 name = "regex" 3054 version = "1.12.2" 3055 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3086 3087 [[package]] 3088 name = "reqwest" 3089 + version = "0.12.28" 3090 source = "registry+https://github.com/rust-lang/crates.io-index" 3091 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 3092 dependencies = [ 3093 "base64 0.22.1", 3094 "bytes", 3095 "encoding_rs", ··· 3184 3185 [[package]] 3186 name = "rsa" 3187 + version = "0.9.10" 3188 source = "registry+https://github.com/rust-lang/crates.io-index" 3189 + checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" 3190 dependencies = [ 3191 "const-oid", 3192 "digest", ··· 3213 version = "2.1.1" 3214 source = "registry+https://github.com/rust-lang/crates.io-index" 3215 checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 3216 + 3217 + [[package]] 3218 + name = "rustc_version" 3219 + version = "0.4.1" 3220 + source = "registry+https://github.com/rust-lang/crates.io-index" 3221 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 3222 + dependencies = [ 3223 + "semver", 3224 + ] 3225 3226 [[package]] 3227 name = "rustix" 3228 + version = "1.1.3" 3229 source = "registry+https://github.com/rust-lang/crates.io-index" 3230 + checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 3231 dependencies = [ 3232 "bitflags", 3233 "errno", ··· 3238 3239 [[package]] 3240 name = "rustls" 3241 + version = "0.23.36" 3242 source = "registry+https://github.com/rust-lang/crates.io-index" 3243 + checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" 3244 dependencies = [ 3245 "once_cell", 3246 "ring", ··· 3252 3253 [[package]] 3254 name = "rustls-native-certs" 3255 + version = "0.8.3" 3256 source = "registry+https://github.com/rust-lang/crates.io-index" 3257 + checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" 3258 dependencies = [ 3259 "openssl-probe", 3260 "rustls-pki-types", ··· 3264 3265 [[package]] 3266 name = "rustls-pki-types" 3267 + version = "1.13.2" 3268 source = "registry+https://github.com/rust-lang/crates.io-index" 3269 + checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" 3270 dependencies = [ 3271 "web-time", 3272 "zeroize", ··· 3291 3292 [[package]] 3293 name = "ryu" 3294 + version = "1.0.22" 3295 source = "registry+https://github.com/rust-lang/crates.io-index" 3296 + checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 3297 3298 [[package]] 3299 name = "safemem" ··· 3320 ] 3321 3322 [[package]] 3323 name = "scoped-tls" 3324 version = "1.0.1" 3325 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3367 "core-foundation-sys", 3368 "libc", 3369 ] 3370 + 3371 + [[package]] 3372 + name = "semver" 3373 + version = "1.0.27" 3374 + source = "registry+https://github.com/rust-lang/crates.io-index" 3375 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 3376 3377 [[package]] 3378 name = "send_wrapper" ··· 3417 dependencies = [ 3418 "proc-macro2", 3419 "quote", 3420 + "syn 2.0.113", 3421 ] 3422 3423 [[package]] 3424 name = "serde_html_form" 3425 + version = "0.3.2" 3426 source = "registry+https://github.com/rust-lang/crates.io-index" 3427 + checksum = "2acf96b1d9364968fce46ebb548f1c0e1d7eceae27bdff73865d42e6c7369d94" 3428 dependencies = [ 3429 "form_urlencoded", 3430 + "indexmap", 3431 "itoa", 3432 "serde_core", 3433 ] 3434 ··· 3446 3447 [[package]] 3448 name = "serde_json" 3449 + version = "1.0.149" 3450 source = "registry+https://github.com/rust-lang/crates.io-index" 3451 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 3452 dependencies = [ 3453 "itoa", 3454 "memchr", 3455 "serde", 3456 "serde_core", 3457 + "zmij", 3458 ] 3459 3460 [[package]] ··· 3476 dependencies = [ 3477 "proc-macro2", 3478 "quote", 3479 + "syn 2.0.113", 3480 ] 3481 3482 [[package]] ··· 3493 3494 [[package]] 3495 name = "serde_with" 3496 + version = "3.16.1" 3497 source = "registry+https://github.com/rust-lang/crates.io-index" 3498 + checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" 3499 dependencies = [ 3500 "base64 0.22.1", 3501 "chrono", 3502 "hex", 3503 "serde_core", 3504 "serde_json", 3505 "serde_with_macros", ··· 3508 3509 [[package]] 3510 name = "serde_with_macros" 3511 + version = "3.16.1" 3512 source = "registry+https://github.com/rust-lang/crates.io-index" 3513 + checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" 3514 dependencies = [ 3515 "darling", 3516 "proc-macro2", 3517 "quote", 3518 + "syn 2.0.113", 3519 ] 3520 3521 [[package]] ··· 3572 3573 [[package]] 3574 name = "signal-hook-registry" 3575 + version = "1.4.8" 3576 source = "registry+https://github.com/rust-lang/crates.io-index" 3577 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 3578 dependencies = [ 3579 + "errno", 3580 "libc", 3581 ] 3582 ··· 3592 3593 [[package]] 3594 name = "simd-adler32" 3595 + version = "0.3.8" 3596 source = "registry+https://github.com/rust-lang/crates.io-index" 3597 + checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" 3598 3599 [[package]] 3600 name = "siphasher" ··· 3649 version = "0.9.8" 3650 source = "registry+https://github.com/rust-lang/crates.io-index" 3651 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3652 + dependencies = [ 3653 + "lock_api", 3654 + ] 3655 3656 [[package]] 3657 name = "spin" ··· 3676 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 3677 3678 [[package]] 3679 name = "static_assertions" 3680 version = "1.1.0" 3681 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3729 3730 [[package]] 3731 name = "supports-hyperlinks" 3732 + version = "3.2.0" 3733 source = "registry+https://github.com/rust-lang/crates.io-index" 3734 + checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" 3735 3736 [[package]] 3737 name = "supports-unicode" ··· 3752 3753 [[package]] 3754 name = "syn" 3755 + version = "2.0.113" 3756 source = "registry+https://github.com/rust-lang/crates.io-index" 3757 + checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" 3758 dependencies = [ 3759 "proc-macro2", 3760 "quote", ··· 3778 dependencies = [ 3779 "proc-macro2", 3780 "quote", 3781 + "syn 2.0.113", 3782 ] 3783 3784 [[package]] ··· 3810 3811 [[package]] 3812 name = "tempfile" 3813 + version = "3.24.0" 3814 source = "registry+https://github.com/rust-lang/crates.io-index" 3815 + checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" 3816 dependencies = [ 3817 "fastrand", 3818 "getrandom 0.3.4", ··· 3878 dependencies = [ 3879 "proc-macro2", 3880 "quote", 3881 + "syn 2.0.113", 3882 ] 3883 3884 [[package]] ··· 3889 dependencies = [ 3890 "proc-macro2", 3891 "quote", 3892 + "syn 2.0.113", 3893 ] 3894 3895 [[package]] ··· 3917 checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 3918 dependencies = [ 3919 "deranged", 3920 "libc", 3921 "num-conv", 3922 "num_threads", 3923 "powerfmt", 3924 "serde", 3925 "time-core", 3926 ] 3927 3928 [[package]] ··· 3930 version = "0.1.6" 3931 source = "registry+https://github.com/rust-lang/crates.io-index" 3932 checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 3933 3934 [[package]] 3935 name = "tiny_http" ··· 3970 3971 [[package]] 3972 name = "tokio" 3973 + version = "1.49.0" 3974 source = "registry+https://github.com/rust-lang/crates.io-index" 3975 + checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" 3976 dependencies = [ 3977 "bytes", 3978 "libc", ··· 3993 dependencies = [ 3994 "proc-macro2", 3995 "quote", 3996 + "syn 2.0.113", 3997 ] 3998 3999 [[package]] ··· 4043 4044 [[package]] 4045 name = "tokio-util" 4046 + version = "0.7.18" 4047 source = "registry+https://github.com/rust-lang/crates.io-index" 4048 + checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" 4049 dependencies = [ 4050 "bytes", 4051 "futures-core", ··· 4073 4074 [[package]] 4075 name = "tower-http" 4076 + version = "0.6.8" 4077 source = "registry+https://github.com/rust-lang/crates.io-index" 4078 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 4079 dependencies = [ 4080 "async-compression", 4081 "bitflags", ··· 4114 4115 [[package]] 4116 name = "tracing" 4117 + version = "0.1.44" 4118 source = "registry+https://github.com/rust-lang/crates.io-index" 4119 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 4120 dependencies = [ 4121 "log", 4122 "pin-project-lite", ··· 4126 4127 [[package]] 4128 name = "tracing-attributes" 4129 + version = "0.1.31" 4130 source = "registry+https://github.com/rust-lang/crates.io-index" 4131 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 4132 dependencies = [ 4133 "proc-macro2", 4134 "quote", 4135 + "syn 2.0.113", 4136 ] 4137 4138 [[package]] 4139 name = "tracing-core" 4140 + version = "0.1.36" 4141 source = "registry+https://github.com/rust-lang/crates.io-index" 4142 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 4143 dependencies = [ 4144 "once_cell", 4145 "valuable", ··· 4158 4159 [[package]] 4160 name = "tracing-subscriber" 4161 + version = "0.3.22" 4162 source = "registry+https://github.com/rust-lang/crates.io-index" 4163 + checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 4164 dependencies = [ 4165 "matchers", 4166 "nu-ansi-term", ··· 4182 dependencies = [ 4183 "proc-macro2", 4184 "quote", 4185 + "syn 2.0.113", 4186 ] 4187 4188 [[package]] ··· 4234 4235 [[package]] 4236 name = "unicase" 4237 + version = "2.9.0" 4238 source = "registry+https://github.com/rust-lang/crates.io-index" 4239 + checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" 4240 4241 [[package]] 4242 name = "unicode-ident" ··· 4288 4289 [[package]] 4290 name = "url" 4291 + version = "2.5.8" 4292 source = "registry+https://github.com/rust-lang/crates.io-index" 4293 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 4294 dependencies = [ 4295 "form_urlencoded", 4296 "idna", 4297 "percent-encoding", 4298 "serde", 4299 + "serde_derive", 4300 ] 4301 4302 [[package]] ··· 4371 4372 [[package]] 4373 name = "wasm-bindgen" 4374 + version = "0.2.106" 4375 source = "registry+https://github.com/rust-lang/crates.io-index" 4376 + checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 4377 dependencies = [ 4378 "cfg-if", 4379 "once_cell", ··· 4384 4385 [[package]] 4386 name = "wasm-bindgen-futures" 4387 + version = "0.4.56" 4388 source = "registry+https://github.com/rust-lang/crates.io-index" 4389 + checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" 4390 dependencies = [ 4391 "cfg-if", 4392 "js-sys", ··· 4397 4398 [[package]] 4399 name = "wasm-bindgen-macro" 4400 + version = "0.2.106" 4401 source = "registry+https://github.com/rust-lang/crates.io-index" 4402 + checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 4403 dependencies = [ 4404 "quote", 4405 "wasm-bindgen-macro-support", ··· 4407 4408 [[package]] 4409 name = "wasm-bindgen-macro-support" 4410 + version = "0.2.106" 4411 source = "registry+https://github.com/rust-lang/crates.io-index" 4412 + checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 4413 dependencies = [ 4414 "bumpalo", 4415 "proc-macro2", 4416 "quote", 4417 + "syn 2.0.113", 4418 "wasm-bindgen-shared", 4419 ] 4420 4421 [[package]] 4422 name = "wasm-bindgen-shared" 4423 + version = "0.2.106" 4424 source = "registry+https://github.com/rust-lang/crates.io-index" 4425 + checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 4426 dependencies = [ 4427 "unicode-ident", 4428 ] ··· 4442 4443 [[package]] 4444 name = "web-sys" 4445 + version = "0.3.83" 4446 source = "registry+https://github.com/rust-lang/crates.io-index" 4447 + checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" 4448 dependencies = [ 4449 "js-sys", 4450 "wasm-bindgen", ··· 4490 4491 [[package]] 4492 name = "webpki-roots" 4493 + version = "1.0.5" 4494 source = "registry+https://github.com/rust-lang/crates.io-index" 4495 + checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" 4496 dependencies = [ 4497 "rustls-pki-types", 4498 ] ··· 4513 ] 4514 4515 [[package]] 4516 name = "windows-core" 4517 version = "0.62.2" 4518 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4520 dependencies = [ 4521 "windows-implement", 4522 "windows-interface", 4523 + "windows-link", 4524 + "windows-result", 4525 + "windows-strings", 4526 ] 4527 4528 [[package]] ··· 4533 dependencies = [ 4534 "proc-macro2", 4535 "quote", 4536 + "syn 2.0.113", 4537 ] 4538 4539 [[package]] ··· 4544 dependencies = [ 4545 "proc-macro2", 4546 "quote", 4547 + "syn 2.0.113", 4548 ] 4549 4550 [[package]] 4551 name = "windows-link" 4552 version = "0.2.1" 4553 source = "registry+https://github.com/rust-lang/crates.io-index" 4554 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 4555 4556 [[package]] 4557 name = "windows-registry" 4558 version = "0.6.1" 4559 source = "registry+https://github.com/rust-lang/crates.io-index" 4560 checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 4561 dependencies = [ 4562 + "windows-link", 4563 + "windows-result", 4564 + "windows-strings", 4565 ] 4566 4567 [[package]] ··· 4570 source = "registry+https://github.com/rust-lang/crates.io-index" 4571 checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 4572 dependencies = [ 4573 + "windows-link", 4574 ] 4575 4576 [[package]] ··· 4579 source = "registry+https://github.com/rust-lang/crates.io-index" 4580 checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 4581 dependencies = [ 4582 + "windows-link", 4583 ] 4584 4585 [[package]] ··· 4611 4612 [[package]] 4613 name = "windows-sys" 4614 + version = "0.59.0" 4615 + source = "registry+https://github.com/rust-lang/crates.io-index" 4616 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 4617 + dependencies = [ 4618 + "windows-targets 0.52.6", 4619 + ] 4620 + 4621 + [[package]] 4622 + name = "windows-sys" 4623 version = "0.60.2" 4624 source = "registry+https://github.com/rust-lang/crates.io-index" 4625 checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" ··· 4633 source = "registry+https://github.com/rust-lang/crates.io-index" 4634 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 4635 dependencies = [ 4636 + "windows-link", 4637 ] 4638 4639 [[package]] ··· 4688 source = "registry+https://github.com/rust-lang/crates.io-index" 4689 checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 4690 dependencies = [ 4691 + "windows-link", 4692 "windows_aarch64_gnullvm 0.53.1", 4693 "windows_aarch64_msvc 0.53.1", 4694 "windows_i686_gnu 0.53.1", ··· 4697 "windows_x86_64_gnu 0.53.1", 4698 "windows_x86_64_gnullvm 0.53.1", 4699 "windows_x86_64_msvc 0.53.1", 4700 ] 4701 4702 [[package]] ··· 4902 "futures", 4903 "globset", 4904 "ignore", 4905 + "indicatif", 4906 "jacquard", 4907 "jacquard-api", 4908 "jacquard-common", ··· 4927 "tower-http", 4928 "url", 4929 "walkdir", 4930 + "wisp-lexicons", 4931 + ] 4932 + 4933 + [[package]] 4934 + name = "wisp-lexicons" 4935 + version = "0.1.0" 4936 + dependencies = [ 4937 + "jacquard-common", 4938 + "jacquard-derive", 4939 + "jacquard-lexicon", 4940 + "rustversion", 4941 + "serde", 4942 ] 4943 4944 [[package]] ··· 4989 dependencies = [ 4990 "proc-macro2", 4991 "quote", 4992 + "syn 2.0.113", 4993 "synstructure", 4994 ] 4995 4996 [[package]] 4997 name = "zerocopy" 4998 + version = "0.8.32" 4999 source = "registry+https://github.com/rust-lang/crates.io-index" 5000 + checksum = "1fabae64378cb18147bb18bca364e63bdbe72a0ffe4adf0addfec8aa166b2c56" 5001 dependencies = [ 5002 "zerocopy-derive", 5003 ] 5004 5005 [[package]] 5006 name = "zerocopy-derive" 5007 + version = "0.8.32" 5008 source = "registry+https://github.com/rust-lang/crates.io-index" 5009 + checksum = "c9c2d862265a8bb4471d87e033e730f536e2a285cc7cb05dbce09a2a97075f90" 5010 dependencies = [ 5011 "proc-macro2", 5012 "quote", 5013 + "syn 2.0.113", 5014 ] 5015 5016 [[package]] ··· 5030 dependencies = [ 5031 "proc-macro2", 5032 "quote", 5033 + "syn 2.0.113", 5034 "synstructure", 5035 ] 5036 ··· 5073 dependencies = [ 5074 "proc-macro2", 5075 "quote", 5076 + "syn 2.0.113", 5077 ] 5078 + 5079 + [[package]] 5080 + name = "zmij" 5081 + version = "1.0.12" 5082 + source = "registry+https://github.com/rust-lang/crates.io-index" 5083 + checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
+16 -7
cli/Cargo.toml
··· 8 place_wisp = [] 9 10 [dependencies] 11 - jacquard = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["loopback"] } 12 - jacquard-oauth = { git = "https://tangled.org/nekomimi.pet/jacquard" } 13 - jacquard-api = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["streaming"] } 14 - jacquard-common = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["websocket"] } 15 - jacquard-identity = { git = "https://tangled.org/nekomimi.pet/jacquard", features = ["dns"] } 16 - jacquard-derive = { git = "https://tangled.org/nekomimi.pet/jacquard" } 17 - jacquard-lexicon = { git = "https://tangled.org/nekomimi.pet/jacquard" } 18 clap = { version = "4.5.51", features = ["derive"] } 19 tokio = { version = "1.48", features = ["full"] } 20 miette = { version = "7.6.0", features = ["fancy"] } ··· 42 regex = "1.11" 43 ignore = "0.4" 44 globset = "0.4"
··· 8 place_wisp = [] 9 10 [dependencies] 11 + jacquard = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["loopback"] } 12 + jacquard-oauth = { git = "https://tangled.org/nonbinary.computer/jacquard" } 13 + jacquard-api = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["streaming"] } 14 + jacquard-common = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["websocket"] } 15 + jacquard-identity = { git = "https://tangled.org/nonbinary.computer/jacquard", features = ["dns"] } 16 + jacquard-derive = { git = "https://tangled.org/nonbinary.computer/jacquard" } 17 + jacquard-lexicon = { git = "https://tangled.org/nonbinary.computer/jacquard" } 18 + wisp-lexicons = { path = "crates/lexicons" } 19 + #jacquard = { path = "../../jacquard/crates/jacquard", features = ["loopback"] } 20 + #jacquard-oauth = { path = "../../jacquard/crates/jacquard-oauth" } 21 + #jacquard-api = { path = "../../jacquard/crates/jacquard-api", features = ["streaming"] } 22 + #jacquard-common = { path = "../../jacquard/crates/jacquard-common", features = ["websocket"] } 23 + #jacquard-identity = { path = "../../jacquard/crates/jacquard-identity", features = ["dns"] } 24 + #jacquard-derive = { path = "../../jacquard/crates/jacquard-derive" } 25 + #jacquard-lexicon = { path = "../../jacquard/crates/jacquard-lexicon" } 26 clap = { version = "4.5.51", features = ["derive"] } 27 tokio = { version = "1.48", features = ["full"] } 28 miette = { version = "7.6.0", features = ["fancy"] } ··· 50 regex = "1.11" 51 ignore = "0.4" 52 globset = "0.4" 53 + indicatif = "0.17"
+72 -2
cli/README.md
··· 32 33 ## Usage 34 35 ### Basic Deployment 36 37 Deploy the current directory: 38 39 ```bash 40 - wisp-cli nekomimi.ppet --path . --site my-site 41 ``` 42 43 Deploy a specific directory: ··· 46 wisp-cli alice.bsky.social --path ./dist/ --site my-site 47 ``` 48 49 ### Authentication Methods 50 51 #### OAuth (Recommended) ··· 79 80 ## Command-Line Options 81 82 ``` 83 - wisp-cli [OPTIONS] <INPUT> 84 85 Arguments: 86 <INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL ··· 90 -s, --site <SITE> Site name (defaults to directory name) 91 --store <STORE> Path to auth store file (only used with OAuth) [default: /tmp/wisp-oauth-session.json] 92 --password <PASSWORD> App Password for authentication (alternative to OAuth) 93 -h, --help Print help 94 -V, --version Print version 95 ``` 96 97 ## How It Works
··· 32 33 ## Usage 34 35 + ### Commands 36 + 37 + The CLI supports three main commands: 38 + - **deploy**: Upload a site to your PDS (default command) 39 + - **pull**: Download a site from a PDS to a local directory 40 + - **serve**: Serve a site locally with real-time firehose updates 41 + 42 ### Basic Deployment 43 44 Deploy the current directory: 45 46 ```bash 47 + wisp-cli nekomimi.pet --path . --site my-site 48 ``` 49 50 Deploy a specific directory: ··· 53 wisp-cli alice.bsky.social --path ./dist/ --site my-site 54 ``` 55 56 + Or use the explicit `deploy` subcommand: 57 + 58 + ```bash 59 + wisp-cli deploy alice.bsky.social --path ./dist/ --site my-site 60 + ``` 61 + 62 + ### Pull a Site 63 + 64 + Download a site from a PDS to a local directory: 65 + 66 + ```bash 67 + wisp-cli pull alice.bsky.social --site my-site --path ./downloaded-site 68 + ``` 69 + 70 + This will download all files from the site to the specified directory. 71 + 72 + ### Serve a Site Locally 73 + 74 + Serve a site locally with real-time updates from the firehose: 75 + 76 + ```bash 77 + wisp-cli serve alice.bsky.social --site my-site --path ./site --port 8080 78 + ``` 79 + 80 + This will: 81 + 1. Download the site to the specified path 82 + 2. Start a local server on the specified port (default: 8080) 83 + 3. Watch the firehose for updates and automatically reload files when changed 84 + 85 ### Authentication Methods 86 87 #### OAuth (Recommended) ··· 115 116 ## Command-Line Options 117 118 + ### Deploy Command 119 + 120 ``` 121 + wisp-cli [deploy] [OPTIONS] <INPUT> 122 123 Arguments: 124 <INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL ··· 128 -s, --site <SITE> Site name (defaults to directory name) 129 --store <STORE> Path to auth store file (only used with OAuth) [default: /tmp/wisp-oauth-session.json] 130 --password <PASSWORD> App Password for authentication (alternative to OAuth) 131 + --directory Enable directory listing mode for paths without index files 132 + --spa Enable SPA mode (serve index.html for all routes) 133 + -y, --yes Skip confirmation prompts (automatically accept warnings) 134 -h, --help Print help 135 -V, --version Print version 136 + ``` 137 + 138 + ### Pull Command 139 + 140 + ``` 141 + wisp-cli pull [OPTIONS] --site <SITE> <INPUT> 142 + 143 + Arguments: 144 + <INPUT> Handle (e.g., alice.bsky.social) or DID 145 + 146 + Options: 147 + -s, --site <SITE> Site name (record key) 148 + -p, --path <PATH> Output directory for the downloaded site [default: .] 149 + -h, --help Print help 150 + ``` 151 + 152 + ### Serve Command 153 + 154 + ``` 155 + wisp-cli serve [OPTIONS] --site <SITE> <INPUT> 156 + 157 + Arguments: 158 + <INPUT> Handle (e.g., alice.bsky.social) or DID 159 + 160 + Options: 161 + -s, --site <SITE> Site name (record key) 162 + -p, --path <PATH> Output directory for the site files [default: .] 163 + -P, --port <PORT> Port to serve on [default: 8080] 164 + -h, --help Print help 165 ``` 166 167 ## How It Works
+15
cli/crates/lexicons/Cargo.toml
···
··· 1 + [package] 2 + name = "wisp-lexicons" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [features] 7 + default = ["place_wisp"] 8 + place_wisp = [] 9 + 10 + [dependencies] 11 + jacquard-common = { git = "https://tangled.org/nonbinary.computer/jacquard" } 12 + jacquard-derive = { git = "https://tangled.org/nonbinary.computer/jacquard" } 13 + jacquard-lexicon = { git = "https://tangled.org/nonbinary.computer/jacquard" } 14 + serde = { version = "1.0", features = ["derive"] } 15 + rustversion = "1.0"
+43
cli/crates/lexicons/src/builder_types.rs
···
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + /// Marker type indicating a builder field has been set 7 + pub struct Set<T>(pub T); 8 + impl<T> Set<T> { 9 + /// Extract the inner value 10 + #[inline] 11 + pub fn into_inner(self) -> T { 12 + self.0 13 + } 14 + } 15 + 16 + /// Marker type indicating a builder field has not been set 17 + pub struct Unset; 18 + /// Trait indicating a builder field is set (has a value) 19 + #[rustversion::attr( 20 + since(1.78.0), 21 + diagnostic::on_unimplemented( 22 + message = "the field `{Self}` was not set, but this method requires it to be set", 23 + label = "the field `{Self}` was not set" 24 + ) 25 + )] 26 + pub trait IsSet: private::Sealed {} 27 + /// Trait indicating a builder field is unset (no value yet) 28 + #[rustversion::attr( 29 + since(1.78.0), 30 + diagnostic::on_unimplemented( 31 + message = "the field `{Self}` was already set, but this method requires it to be unset", 32 + label = "the field `{Self}` was already set" 33 + ) 34 + )] 35 + pub trait IsUnset: private::Sealed {} 36 + impl<T> IsSet for Set<T> {} 37 + impl IsUnset for Unset {} 38 + mod private { 39 + /// Sealed trait to prevent external implementations 40 + pub trait Sealed {} 41 + impl<T> Sealed for super::Set<T> {} 42 + impl Sealed for super::Unset {} 43 + }
+11
cli/crates/lexicons/src/lib.rs
···
··· 1 + extern crate alloc; 2 + 3 + // @generated by jacquard-lexicon. DO NOT EDIT. 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + pub mod builder_types; 9 + 10 + #[cfg(feature = "place_wisp")] 11 + pub mod place_wisp;
+1490
cli/crates/lexicons/src/place_wisp/fs.rs
···
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: place.wisp.fs 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + #[jacquard_derive::lexicon] 9 + #[derive( 10 + serde::Serialize, 11 + serde::Deserialize, 12 + Debug, 13 + Clone, 14 + PartialEq, 15 + Eq, 16 + jacquard_derive::IntoStatic 17 + )] 18 + #[serde(rename_all = "camelCase")] 19 + pub struct Directory<'a> { 20 + #[serde(borrow)] 21 + pub entries: Vec<crate::place_wisp::fs::Entry<'a>>, 22 + #[serde(borrow)] 23 + pub r#type: jacquard_common::CowStr<'a>, 24 + } 25 + 26 + pub mod directory_state { 27 + 28 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 29 + #[allow(unused)] 30 + use ::core::marker::PhantomData; 31 + mod sealed { 32 + pub trait Sealed {} 33 + } 34 + /// State trait tracking which required fields have been set 35 + pub trait State: sealed::Sealed { 36 + type Type; 37 + type Entries; 38 + } 39 + /// Empty state - all required fields are unset 40 + pub struct Empty(()); 41 + impl sealed::Sealed for Empty {} 42 + impl State for Empty { 43 + type Type = Unset; 44 + type Entries = Unset; 45 + } 46 + ///State transition - sets the `type` field to Set 47 + pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 48 + impl<S: State> sealed::Sealed for SetType<S> {} 49 + impl<S: State> State for SetType<S> { 50 + type Type = Set<members::r#type>; 51 + type Entries = S::Entries; 52 + } 53 + ///State transition - sets the `entries` field to Set 54 + pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>); 55 + impl<S: State> sealed::Sealed for SetEntries<S> {} 56 + impl<S: State> State for SetEntries<S> { 57 + type Type = S::Type; 58 + type Entries = Set<members::entries>; 59 + } 60 + /// Marker types for field names 61 + #[allow(non_camel_case_types)] 62 + pub mod members { 63 + ///Marker type for the `type` field 64 + pub struct r#type(()); 65 + ///Marker type for the `entries` field 66 + pub struct entries(()); 67 + } 68 + } 69 + 70 + /// Builder for constructing an instance of this type 71 + pub struct DirectoryBuilder<'a, S: directory_state::State> { 72 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 73 + __unsafe_private_named: ( 74 + ::core::option::Option<Vec<crate::place_wisp::fs::Entry<'a>>>, 75 + ::core::option::Option<jacquard_common::CowStr<'a>>, 76 + ), 77 + _phantom: ::core::marker::PhantomData<&'a ()>, 78 + } 79 + 80 + impl<'a> Directory<'a> { 81 + /// Create a new builder for this type 82 + pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> { 83 + DirectoryBuilder::new() 84 + } 85 + } 86 + 87 + impl<'a> DirectoryBuilder<'a, directory_state::Empty> { 88 + /// Create a new builder with all fields unset 89 + pub fn new() -> Self { 90 + DirectoryBuilder { 91 + _phantom_state: ::core::marker::PhantomData, 92 + __unsafe_private_named: (None, None), 93 + _phantom: ::core::marker::PhantomData, 94 + } 95 + } 96 + } 97 + 98 + impl<'a, S> DirectoryBuilder<'a, S> 99 + where 100 + S: directory_state::State, 101 + S::Entries: directory_state::IsUnset, 102 + { 103 + /// Set the `entries` field (required) 104 + pub fn entries( 105 + mut self, 106 + value: impl Into<Vec<crate::place_wisp::fs::Entry<'a>>>, 107 + ) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> { 108 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 109 + DirectoryBuilder { 110 + _phantom_state: ::core::marker::PhantomData, 111 + __unsafe_private_named: self.__unsafe_private_named, 112 + _phantom: ::core::marker::PhantomData, 113 + } 114 + } 115 + } 116 + 117 + impl<'a, S> DirectoryBuilder<'a, S> 118 + where 119 + S: directory_state::State, 120 + S::Type: directory_state::IsUnset, 121 + { 122 + /// Set the `type` field (required) 123 + pub fn r#type( 124 + mut self, 125 + value: impl Into<jacquard_common::CowStr<'a>>, 126 + ) -> DirectoryBuilder<'a, directory_state::SetType<S>> { 127 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 128 + DirectoryBuilder { 129 + _phantom_state: ::core::marker::PhantomData, 130 + __unsafe_private_named: self.__unsafe_private_named, 131 + _phantom: ::core::marker::PhantomData, 132 + } 133 + } 134 + } 135 + 136 + impl<'a, S> DirectoryBuilder<'a, S> 137 + where 138 + S: directory_state::State, 139 + S::Type: directory_state::IsSet, 140 + S::Entries: directory_state::IsSet, 141 + { 142 + /// Build the final struct 143 + pub fn build(self) -> Directory<'a> { 144 + Directory { 145 + entries: self.__unsafe_private_named.0.unwrap(), 146 + r#type: self.__unsafe_private_named.1.unwrap(), 147 + extra_data: Default::default(), 148 + } 149 + } 150 + /// Build the final struct with custom extra_data 151 + pub fn build_with_data( 152 + self, 153 + extra_data: std::collections::BTreeMap< 154 + jacquard_common::smol_str::SmolStr, 155 + jacquard_common::types::value::Data<'a>, 156 + >, 157 + ) -> Directory<'a> { 158 + Directory { 159 + entries: self.__unsafe_private_named.0.unwrap(), 160 + r#type: self.__unsafe_private_named.1.unwrap(), 161 + extra_data: Some(extra_data), 162 + } 163 + } 164 + } 165 + 166 + fn lexicon_doc_place_wisp_fs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 167 + ::jacquard_lexicon::lexicon::LexiconDoc { 168 + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 169 + id: ::jacquard_common::CowStr::new_static("place.wisp.fs"), 170 + revision: None, 171 + description: None, 172 + defs: { 173 + let mut map = ::std::collections::BTreeMap::new(); 174 + map.insert( 175 + ::jacquard_common::smol_str::SmolStr::new_static("directory"), 176 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 177 + description: None, 178 + required: Some( 179 + vec![ 180 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 181 + ::jacquard_common::smol_str::SmolStr::new_static("entries") 182 + ], 183 + ), 184 + nullable: None, 185 + properties: { 186 + #[allow(unused_mut)] 187 + let mut map = ::std::collections::BTreeMap::new(); 188 + map.insert( 189 + ::jacquard_common::smol_str::SmolStr::new_static("entries"), 190 + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 191 + description: None, 192 + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { 193 + description: None, 194 + r#ref: ::jacquard_common::CowStr::new_static("#entry"), 195 + }), 196 + min_length: None, 197 + max_length: Some(500usize), 198 + }), 199 + ); 200 + map.insert( 201 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 202 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 203 + description: None, 204 + format: None, 205 + default: None, 206 + min_length: None, 207 + max_length: None, 208 + min_graphemes: None, 209 + max_graphemes: None, 210 + r#enum: None, 211 + r#const: None, 212 + known_values: None, 213 + }), 214 + ); 215 + map 216 + }, 217 + }), 218 + ); 219 + map.insert( 220 + ::jacquard_common::smol_str::SmolStr::new_static("entry"), 221 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 222 + description: None, 223 + required: Some( 224 + vec![ 225 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 226 + ::jacquard_common::smol_str::SmolStr::new_static("node") 227 + ], 228 + ), 229 + nullable: None, 230 + properties: { 231 + #[allow(unused_mut)] 232 + let mut map = ::std::collections::BTreeMap::new(); 233 + map.insert( 234 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 235 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 236 + description: None, 237 + format: None, 238 + default: None, 239 + min_length: None, 240 + max_length: Some(255usize), 241 + min_graphemes: None, 242 + max_graphemes: None, 243 + r#enum: None, 244 + r#const: None, 245 + known_values: None, 246 + }), 247 + ); 248 + map.insert( 249 + ::jacquard_common::smol_str::SmolStr::new_static("node"), 250 + ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 251 + description: None, 252 + refs: vec![ 253 + ::jacquard_common::CowStr::new_static("#file"), 254 + ::jacquard_common::CowStr::new_static("#directory"), 255 + ::jacquard_common::CowStr::new_static("#subfs") 256 + ], 257 + closed: None, 258 + }), 259 + ); 260 + map 261 + }, 262 + }), 263 + ); 264 + map.insert( 265 + ::jacquard_common::smol_str::SmolStr::new_static("file"), 266 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 267 + description: None, 268 + required: Some( 269 + vec![ 270 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 271 + ::jacquard_common::smol_str::SmolStr::new_static("blob") 272 + ], 273 + ), 274 + nullable: None, 275 + properties: { 276 + #[allow(unused_mut)] 277 + let mut map = ::std::collections::BTreeMap::new(); 278 + map.insert( 279 + ::jacquard_common::smol_str::SmolStr::new_static("base64"), 280 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 281 + description: None, 282 + default: None, 283 + r#const: None, 284 + }), 285 + ); 286 + map.insert( 287 + ::jacquard_common::smol_str::SmolStr::new_static("blob"), 288 + ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob { 289 + description: None, 290 + accept: None, 291 + max_size: None, 292 + }), 293 + ); 294 + map.insert( 295 + ::jacquard_common::smol_str::SmolStr::new_static("encoding"), 296 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 297 + description: Some( 298 + ::jacquard_common::CowStr::new_static( 299 + "Content encoding (e.g., gzip for compressed files)", 300 + ), 301 + ), 302 + format: None, 303 + default: None, 304 + min_length: None, 305 + max_length: None, 306 + min_graphemes: None, 307 + max_graphemes: None, 308 + r#enum: None, 309 + r#const: None, 310 + known_values: None, 311 + }), 312 + ); 313 + map.insert( 314 + ::jacquard_common::smol_str::SmolStr::new_static("mimeType"), 315 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 316 + description: Some( 317 + ::jacquard_common::CowStr::new_static( 318 + "Original MIME type before compression", 319 + ), 320 + ), 321 + format: None, 322 + default: None, 323 + min_length: None, 324 + max_length: None, 325 + min_graphemes: None, 326 + max_graphemes: None, 327 + r#enum: None, 328 + r#const: None, 329 + known_values: None, 330 + }), 331 + ); 332 + map.insert( 333 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 334 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 335 + description: None, 336 + format: None, 337 + default: None, 338 + min_length: None, 339 + max_length: None, 340 + min_graphemes: None, 341 + max_graphemes: None, 342 + r#enum: None, 343 + r#const: None, 344 + known_values: None, 345 + }), 346 + ); 347 + map 348 + }, 349 + }), 350 + ); 351 + map.insert( 352 + ::jacquard_common::smol_str::SmolStr::new_static("main"), 353 + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { 354 + description: Some( 355 + ::jacquard_common::CowStr::new_static( 356 + "Virtual filesystem manifest for a Wisp site", 357 + ), 358 + ), 359 + key: None, 360 + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { 361 + description: None, 362 + required: Some( 363 + vec![ 364 + ::jacquard_common::smol_str::SmolStr::new_static("site"), 365 + ::jacquard_common::smol_str::SmolStr::new_static("root"), 366 + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") 367 + ], 368 + ), 369 + nullable: None, 370 + properties: { 371 + #[allow(unused_mut)] 372 + let mut map = ::std::collections::BTreeMap::new(); 373 + map.insert( 374 + ::jacquard_common::smol_str::SmolStr::new_static( 375 + "createdAt", 376 + ), 377 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 378 + description: None, 379 + format: Some( 380 + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, 381 + ), 382 + default: None, 383 + min_length: None, 384 + max_length: None, 385 + min_graphemes: None, 386 + max_graphemes: None, 387 + r#enum: None, 388 + r#const: None, 389 + known_values: None, 390 + }), 391 + ); 392 + map.insert( 393 + ::jacquard_common::smol_str::SmolStr::new_static( 394 + "fileCount", 395 + ), 396 + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { 397 + description: None, 398 + default: None, 399 + minimum: Some(0i64), 400 + maximum: Some(1000i64), 401 + r#enum: None, 402 + r#const: None, 403 + }), 404 + ); 405 + map.insert( 406 + ::jacquard_common::smol_str::SmolStr::new_static("root"), 407 + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { 408 + description: None, 409 + r#ref: ::jacquard_common::CowStr::new_static("#directory"), 410 + }), 411 + ); 412 + map.insert( 413 + ::jacquard_common::smol_str::SmolStr::new_static("site"), 414 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 415 + description: None, 416 + format: None, 417 + default: None, 418 + min_length: None, 419 + max_length: None, 420 + min_graphemes: None, 421 + max_graphemes: None, 422 + r#enum: None, 423 + r#const: None, 424 + known_values: None, 425 + }), 426 + ); 427 + map 428 + }, 429 + }), 430 + }), 431 + ); 432 + map.insert( 433 + ::jacquard_common::smol_str::SmolStr::new_static("subfs"), 434 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 435 + description: None, 436 + required: Some( 437 + vec![ 438 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 439 + ::jacquard_common::smol_str::SmolStr::new_static("subject") 440 + ], 441 + ), 442 + nullable: None, 443 + properties: { 444 + #[allow(unused_mut)] 445 + let mut map = ::std::collections::BTreeMap::new(); 446 + map.insert( 447 + ::jacquard_common::smol_str::SmolStr::new_static("flat"), 448 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 449 + description: None, 450 + default: None, 451 + r#const: None, 452 + }), 453 + ); 454 + map.insert( 455 + ::jacquard_common::smol_str::SmolStr::new_static("subject"), 456 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 457 + description: Some( 458 + ::jacquard_common::CowStr::new_static( 459 + "AT-URI pointing to a place.wisp.subfs record containing this subtree.", 460 + ), 461 + ), 462 + format: Some( 463 + ::jacquard_lexicon::lexicon::LexStringFormat::AtUri, 464 + ), 465 + default: None, 466 + min_length: None, 467 + max_length: None, 468 + min_graphemes: None, 469 + max_graphemes: None, 470 + r#enum: None, 471 + r#const: None, 472 + known_values: None, 473 + }), 474 + ); 475 + map.insert( 476 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 477 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 478 + description: None, 479 + format: None, 480 + default: None, 481 + min_length: None, 482 + max_length: None, 483 + min_graphemes: None, 484 + max_graphemes: None, 485 + r#enum: None, 486 + r#const: None, 487 + known_values: None, 488 + }), 489 + ); 490 + map 491 + }, 492 + }), 493 + ); 494 + map 495 + }, 496 + } 497 + } 498 + 499 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> { 500 + fn nsid() -> &'static str { 501 + "place.wisp.fs" 502 + } 503 + fn def_name() -> &'static str { 504 + "directory" 505 + } 506 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 507 + lexicon_doc_place_wisp_fs() 508 + } 509 + fn validate( 510 + &self, 511 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 512 + { 513 + let value = &self.entries; 514 + #[allow(unused_comparisons)] 515 + if value.len() > 500usize { 516 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 517 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 518 + "entries", 519 + ), 520 + max: 500usize, 521 + actual: value.len(), 522 + }); 523 + } 524 + } 525 + Ok(()) 526 + } 527 + } 528 + 529 + #[jacquard_derive::lexicon] 530 + #[derive( 531 + serde::Serialize, 532 + serde::Deserialize, 533 + Debug, 534 + Clone, 535 + PartialEq, 536 + Eq, 537 + jacquard_derive::IntoStatic 538 + )] 539 + #[serde(rename_all = "camelCase")] 540 + pub struct Entry<'a> { 541 + #[serde(borrow)] 542 + pub name: jacquard_common::CowStr<'a>, 543 + #[serde(borrow)] 544 + pub node: EntryNode<'a>, 545 + } 546 + 547 + pub mod entry_state { 548 + 549 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 550 + #[allow(unused)] 551 + use ::core::marker::PhantomData; 552 + mod sealed { 553 + pub trait Sealed {} 554 + } 555 + /// State trait tracking which required fields have been set 556 + pub trait State: sealed::Sealed { 557 + type Node; 558 + type Name; 559 + } 560 + /// Empty state - all required fields are unset 561 + pub struct Empty(()); 562 + impl sealed::Sealed for Empty {} 563 + impl State for Empty { 564 + type Node = Unset; 565 + type Name = Unset; 566 + } 567 + ///State transition - sets the `node` field to Set 568 + pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>); 569 + impl<S: State> sealed::Sealed for SetNode<S> {} 570 + impl<S: State> State for SetNode<S> { 571 + type Node = Set<members::node>; 572 + type Name = S::Name; 573 + } 574 + ///State transition - sets the `name` field to Set 575 + pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>); 576 + impl<S: State> sealed::Sealed for SetName<S> {} 577 + impl<S: State> State for SetName<S> { 578 + type Node = S::Node; 579 + type Name = Set<members::name>; 580 + } 581 + /// Marker types for field names 582 + #[allow(non_camel_case_types)] 583 + pub mod members { 584 + ///Marker type for the `node` field 585 + pub struct node(()); 586 + ///Marker type for the `name` field 587 + pub struct name(()); 588 + } 589 + } 590 + 591 + /// Builder for constructing an instance of this type 592 + pub struct EntryBuilder<'a, S: entry_state::State> { 593 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 594 + __unsafe_private_named: ( 595 + ::core::option::Option<jacquard_common::CowStr<'a>>, 596 + ::core::option::Option<EntryNode<'a>>, 597 + ), 598 + _phantom: ::core::marker::PhantomData<&'a ()>, 599 + } 600 + 601 + impl<'a> Entry<'a> { 602 + /// Create a new builder for this type 603 + pub fn new() -> EntryBuilder<'a, entry_state::Empty> { 604 + EntryBuilder::new() 605 + } 606 + } 607 + 608 + impl<'a> EntryBuilder<'a, entry_state::Empty> { 609 + /// Create a new builder with all fields unset 610 + pub fn new() -> Self { 611 + EntryBuilder { 612 + _phantom_state: ::core::marker::PhantomData, 613 + __unsafe_private_named: (None, None), 614 + _phantom: ::core::marker::PhantomData, 615 + } 616 + } 617 + } 618 + 619 + impl<'a, S> EntryBuilder<'a, S> 620 + where 621 + S: entry_state::State, 622 + S::Name: entry_state::IsUnset, 623 + { 624 + /// Set the `name` field (required) 625 + pub fn name( 626 + mut self, 627 + value: impl Into<jacquard_common::CowStr<'a>>, 628 + ) -> EntryBuilder<'a, entry_state::SetName<S>> { 629 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 630 + EntryBuilder { 631 + _phantom_state: ::core::marker::PhantomData, 632 + __unsafe_private_named: self.__unsafe_private_named, 633 + _phantom: ::core::marker::PhantomData, 634 + } 635 + } 636 + } 637 + 638 + impl<'a, S> EntryBuilder<'a, S> 639 + where 640 + S: entry_state::State, 641 + S::Node: entry_state::IsUnset, 642 + { 643 + /// Set the `node` field (required) 644 + pub fn node( 645 + mut self, 646 + value: impl Into<EntryNode<'a>>, 647 + ) -> EntryBuilder<'a, entry_state::SetNode<S>> { 648 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 649 + EntryBuilder { 650 + _phantom_state: ::core::marker::PhantomData, 651 + __unsafe_private_named: self.__unsafe_private_named, 652 + _phantom: ::core::marker::PhantomData, 653 + } 654 + } 655 + } 656 + 657 + impl<'a, S> EntryBuilder<'a, S> 658 + where 659 + S: entry_state::State, 660 + S::Node: entry_state::IsSet, 661 + S::Name: entry_state::IsSet, 662 + { 663 + /// Build the final struct 664 + pub fn build(self) -> Entry<'a> { 665 + Entry { 666 + name: self.__unsafe_private_named.0.unwrap(), 667 + node: self.__unsafe_private_named.1.unwrap(), 668 + extra_data: Default::default(), 669 + } 670 + } 671 + /// Build the final struct with custom extra_data 672 + pub fn build_with_data( 673 + self, 674 + extra_data: std::collections::BTreeMap< 675 + jacquard_common::smol_str::SmolStr, 676 + jacquard_common::types::value::Data<'a>, 677 + >, 678 + ) -> Entry<'a> { 679 + Entry { 680 + name: self.__unsafe_private_named.0.unwrap(), 681 + node: self.__unsafe_private_named.1.unwrap(), 682 + extra_data: Some(extra_data), 683 + } 684 + } 685 + } 686 + 687 + #[jacquard_derive::open_union] 688 + #[derive( 689 + serde::Serialize, 690 + serde::Deserialize, 691 + Debug, 692 + Clone, 693 + PartialEq, 694 + Eq, 695 + jacquard_derive::IntoStatic 696 + )] 697 + #[serde(tag = "$type")] 698 + #[serde(bound(deserialize = "'de: 'a"))] 699 + pub enum EntryNode<'a> { 700 + #[serde(rename = "place.wisp.fs#file")] 701 + File(Box<crate::place_wisp::fs::File<'a>>), 702 + #[serde(rename = "place.wisp.fs#directory")] 703 + Directory(Box<crate::place_wisp::fs::Directory<'a>>), 704 + #[serde(rename = "place.wisp.fs#subfs")] 705 + Subfs(Box<crate::place_wisp::fs::Subfs<'a>>), 706 + } 707 + 708 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> { 709 + fn nsid() -> &'static str { 710 + "place.wisp.fs" 711 + } 712 + fn def_name() -> &'static str { 713 + "entry" 714 + } 715 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 716 + lexicon_doc_place_wisp_fs() 717 + } 718 + fn validate( 719 + &self, 720 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 721 + { 722 + let value = &self.name; 723 + #[allow(unused_comparisons)] 724 + if <str>::len(value.as_ref()) > 255usize { 725 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 726 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 727 + "name", 728 + ), 729 + max: 255usize, 730 + actual: <str>::len(value.as_ref()), 731 + }); 732 + } 733 + } 734 + Ok(()) 735 + } 736 + } 737 + 738 + #[jacquard_derive::lexicon] 739 + #[derive( 740 + serde::Serialize, 741 + serde::Deserialize, 742 + Debug, 743 + Clone, 744 + PartialEq, 745 + Eq, 746 + jacquard_derive::IntoStatic 747 + )] 748 + #[serde(rename_all = "camelCase")] 749 + pub struct File<'a> { 750 + /// True if blob content is base64-encoded (used to bypass PDS content sniffing) 751 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 752 + pub base64: std::option::Option<bool>, 753 + /// Content blob ref 754 + #[serde(borrow)] 755 + pub blob: jacquard_common::types::blob::BlobRef<'a>, 756 + /// Content encoding (e.g., gzip for compressed files) 757 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 758 + #[serde(borrow)] 759 + pub encoding: std::option::Option<jacquard_common::CowStr<'a>>, 760 + /// Original MIME type before compression 761 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 762 + #[serde(borrow)] 763 + pub mime_type: std::option::Option<jacquard_common::CowStr<'a>>, 764 + #[serde(borrow)] 765 + pub r#type: jacquard_common::CowStr<'a>, 766 + } 767 + 768 + pub mod file_state { 769 + 770 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 771 + #[allow(unused)] 772 + use ::core::marker::PhantomData; 773 + mod sealed { 774 + pub trait Sealed {} 775 + } 776 + /// State trait tracking which required fields have been set 777 + pub trait State: sealed::Sealed { 778 + type Type; 779 + type Blob; 780 + } 781 + /// Empty state - all required fields are unset 782 + pub struct Empty(()); 783 + impl sealed::Sealed for Empty {} 784 + impl State for Empty { 785 + type Type = Unset; 786 + type Blob = Unset; 787 + } 788 + ///State transition - sets the `type` field to Set 789 + pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 790 + impl<S: State> sealed::Sealed for SetType<S> {} 791 + impl<S: State> State for SetType<S> { 792 + type Type = Set<members::r#type>; 793 + type Blob = S::Blob; 794 + } 795 + ///State transition - sets the `blob` field to Set 796 + pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>); 797 + impl<S: State> sealed::Sealed for SetBlob<S> {} 798 + impl<S: State> State for SetBlob<S> { 799 + type Type = S::Type; 800 + type Blob = Set<members::blob>; 801 + } 802 + /// Marker types for field names 803 + #[allow(non_camel_case_types)] 804 + pub mod members { 805 + ///Marker type for the `type` field 806 + pub struct r#type(()); 807 + ///Marker type for the `blob` field 808 + pub struct blob(()); 809 + } 810 + } 811 + 812 + /// Builder for constructing an instance of this type 813 + pub struct FileBuilder<'a, S: file_state::State> { 814 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 815 + __unsafe_private_named: ( 816 + ::core::option::Option<bool>, 817 + ::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 818 + ::core::option::Option<jacquard_common::CowStr<'a>>, 819 + ::core::option::Option<jacquard_common::CowStr<'a>>, 820 + ::core::option::Option<jacquard_common::CowStr<'a>>, 821 + ), 822 + _phantom: ::core::marker::PhantomData<&'a ()>, 823 + } 824 + 825 + impl<'a> File<'a> { 826 + /// Create a new builder for this type 827 + pub fn new() -> FileBuilder<'a, file_state::Empty> { 828 + FileBuilder::new() 829 + } 830 + } 831 + 832 + impl<'a> FileBuilder<'a, file_state::Empty> { 833 + /// Create a new builder with all fields unset 834 + pub fn new() -> Self { 835 + FileBuilder { 836 + _phantom_state: ::core::marker::PhantomData, 837 + __unsafe_private_named: (None, None, None, None, None), 838 + _phantom: ::core::marker::PhantomData, 839 + } 840 + } 841 + } 842 + 843 + impl<'a, S: file_state::State> FileBuilder<'a, S> { 844 + /// Set the `base64` field (optional) 845 + pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self { 846 + self.__unsafe_private_named.0 = value.into(); 847 + self 848 + } 849 + /// Set the `base64` field to an Option value (optional) 850 + pub fn maybe_base64(mut self, value: Option<bool>) -> Self { 851 + self.__unsafe_private_named.0 = value; 852 + self 853 + } 854 + } 855 + 856 + impl<'a, S> FileBuilder<'a, S> 857 + where 858 + S: file_state::State, 859 + S::Blob: file_state::IsUnset, 860 + { 861 + /// Set the `blob` field (required) 862 + pub fn blob( 863 + mut self, 864 + value: impl Into<jacquard_common::types::blob::BlobRef<'a>>, 865 + ) -> FileBuilder<'a, file_state::SetBlob<S>> { 866 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 867 + FileBuilder { 868 + _phantom_state: ::core::marker::PhantomData, 869 + __unsafe_private_named: self.__unsafe_private_named, 870 + _phantom: ::core::marker::PhantomData, 871 + } 872 + } 873 + } 874 + 875 + impl<'a, S: file_state::State> FileBuilder<'a, S> { 876 + /// Set the `encoding` field (optional) 877 + pub fn encoding( 878 + mut self, 879 + value: impl Into<Option<jacquard_common::CowStr<'a>>>, 880 + ) -> Self { 881 + self.__unsafe_private_named.2 = value.into(); 882 + self 883 + } 884 + /// Set the `encoding` field to an Option value (optional) 885 + pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 886 + self.__unsafe_private_named.2 = value; 887 + self 888 + } 889 + } 890 + 891 + impl<'a, S: file_state::State> FileBuilder<'a, S> { 892 + /// Set the `mimeType` field (optional) 893 + pub fn mime_type( 894 + mut self, 895 + value: impl Into<Option<jacquard_common::CowStr<'a>>>, 896 + ) -> Self { 897 + self.__unsafe_private_named.3 = value.into(); 898 + self 899 + } 900 + /// Set the `mimeType` field to an Option value (optional) 901 + pub fn maybe_mime_type( 902 + mut self, 903 + value: Option<jacquard_common::CowStr<'a>>, 904 + ) -> Self { 905 + self.__unsafe_private_named.3 = value; 906 + self 907 + } 908 + } 909 + 910 + impl<'a, S> FileBuilder<'a, S> 911 + where 912 + S: file_state::State, 913 + S::Type: file_state::IsUnset, 914 + { 915 + /// Set the `type` field (required) 916 + pub fn r#type( 917 + mut self, 918 + value: impl Into<jacquard_common::CowStr<'a>>, 919 + ) -> FileBuilder<'a, file_state::SetType<S>> { 920 + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); 921 + FileBuilder { 922 + _phantom_state: ::core::marker::PhantomData, 923 + __unsafe_private_named: self.__unsafe_private_named, 924 + _phantom: ::core::marker::PhantomData, 925 + } 926 + } 927 + } 928 + 929 + impl<'a, S> FileBuilder<'a, S> 930 + where 931 + S: file_state::State, 932 + S::Type: file_state::IsSet, 933 + S::Blob: file_state::IsSet, 934 + { 935 + /// Build the final struct 936 + pub fn build(self) -> File<'a> { 937 + File { 938 + base64: self.__unsafe_private_named.0, 939 + blob: self.__unsafe_private_named.1.unwrap(), 940 + encoding: self.__unsafe_private_named.2, 941 + mime_type: self.__unsafe_private_named.3, 942 + r#type: self.__unsafe_private_named.4.unwrap(), 943 + extra_data: Default::default(), 944 + } 945 + } 946 + /// Build the final struct with custom extra_data 947 + pub fn build_with_data( 948 + self, 949 + extra_data: std::collections::BTreeMap< 950 + jacquard_common::smol_str::SmolStr, 951 + jacquard_common::types::value::Data<'a>, 952 + >, 953 + ) -> File<'a> { 954 + File { 955 + base64: self.__unsafe_private_named.0, 956 + blob: self.__unsafe_private_named.1.unwrap(), 957 + encoding: self.__unsafe_private_named.2, 958 + mime_type: self.__unsafe_private_named.3, 959 + r#type: self.__unsafe_private_named.4.unwrap(), 960 + extra_data: Some(extra_data), 961 + } 962 + } 963 + } 964 + 965 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> { 966 + fn nsid() -> &'static str { 967 + "place.wisp.fs" 968 + } 969 + fn def_name() -> &'static str { 970 + "file" 971 + } 972 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 973 + lexicon_doc_place_wisp_fs() 974 + } 975 + fn validate( 976 + &self, 977 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 978 + Ok(()) 979 + } 980 + } 981 + 982 + /// Virtual filesystem manifest for a Wisp site 983 + #[jacquard_derive::lexicon] 984 + #[derive( 985 + serde::Serialize, 986 + serde::Deserialize, 987 + Debug, 988 + Clone, 989 + PartialEq, 990 + Eq, 991 + jacquard_derive::IntoStatic 992 + )] 993 + #[serde(rename_all = "camelCase")] 994 + pub struct Fs<'a> { 995 + pub created_at: jacquard_common::types::string::Datetime, 996 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 997 + pub file_count: std::option::Option<i64>, 998 + #[serde(borrow)] 999 + pub root: crate::place_wisp::fs::Directory<'a>, 1000 + #[serde(borrow)] 1001 + pub site: jacquard_common::CowStr<'a>, 1002 + } 1003 + 1004 + pub mod fs_state { 1005 + 1006 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 1007 + #[allow(unused)] 1008 + use ::core::marker::PhantomData; 1009 + mod sealed { 1010 + pub trait Sealed {} 1011 + } 1012 + /// State trait tracking which required fields have been set 1013 + pub trait State: sealed::Sealed { 1014 + type CreatedAt; 1015 + type Site; 1016 + type Root; 1017 + } 1018 + /// Empty state - all required fields are unset 1019 + pub struct Empty(()); 1020 + impl sealed::Sealed for Empty {} 1021 + impl State for Empty { 1022 + type CreatedAt = Unset; 1023 + type Site = Unset; 1024 + type Root = Unset; 1025 + } 1026 + ///State transition - sets the `created_at` field to Set 1027 + pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>); 1028 + impl<S: State> sealed::Sealed for SetCreatedAt<S> {} 1029 + impl<S: State> State for SetCreatedAt<S> { 1030 + type CreatedAt = Set<members::created_at>; 1031 + type Site = S::Site; 1032 + type Root = S::Root; 1033 + } 1034 + ///State transition - sets the `site` field to Set 1035 + pub struct SetSite<S: State = Empty>(PhantomData<fn() -> S>); 1036 + impl<S: State> sealed::Sealed for SetSite<S> {} 1037 + impl<S: State> State for SetSite<S> { 1038 + type CreatedAt = S::CreatedAt; 1039 + type Site = Set<members::site>; 1040 + type Root = S::Root; 1041 + } 1042 + ///State transition - sets the `root` field to Set 1043 + pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>); 1044 + impl<S: State> sealed::Sealed for SetRoot<S> {} 1045 + impl<S: State> State for SetRoot<S> { 1046 + type CreatedAt = S::CreatedAt; 1047 + type Site = S::Site; 1048 + type Root = Set<members::root>; 1049 + } 1050 + /// Marker types for field names 1051 + #[allow(non_camel_case_types)] 1052 + pub mod members { 1053 + ///Marker type for the `created_at` field 1054 + pub struct created_at(()); 1055 + ///Marker type for the `site` field 1056 + pub struct site(()); 1057 + ///Marker type for the `root` field 1058 + pub struct root(()); 1059 + } 1060 + } 1061 + 1062 + /// Builder for constructing an instance of this type 1063 + pub struct FsBuilder<'a, S: fs_state::State> { 1064 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1065 + __unsafe_private_named: ( 1066 + ::core::option::Option<jacquard_common::types::string::Datetime>, 1067 + ::core::option::Option<i64>, 1068 + ::core::option::Option<crate::place_wisp::fs::Directory<'a>>, 1069 + ::core::option::Option<jacquard_common::CowStr<'a>>, 1070 + ), 1071 + _phantom: ::core::marker::PhantomData<&'a ()>, 1072 + } 1073 + 1074 + impl<'a> Fs<'a> { 1075 + /// Create a new builder for this type 1076 + pub fn new() -> FsBuilder<'a, fs_state::Empty> { 1077 + FsBuilder::new() 1078 + } 1079 + } 1080 + 1081 + impl<'a> FsBuilder<'a, fs_state::Empty> { 1082 + /// Create a new builder with all fields unset 1083 + pub fn new() -> Self { 1084 + FsBuilder { 1085 + _phantom_state: ::core::marker::PhantomData, 1086 + __unsafe_private_named: (None, None, None, None), 1087 + _phantom: ::core::marker::PhantomData, 1088 + } 1089 + } 1090 + } 1091 + 1092 + impl<'a, S> FsBuilder<'a, S> 1093 + where 1094 + S: fs_state::State, 1095 + S::CreatedAt: fs_state::IsUnset, 1096 + { 1097 + /// Set the `createdAt` field (required) 1098 + pub fn created_at( 1099 + mut self, 1100 + value: impl Into<jacquard_common::types::string::Datetime>, 1101 + ) -> FsBuilder<'a, fs_state::SetCreatedAt<S>> { 1102 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 1103 + FsBuilder { 1104 + _phantom_state: ::core::marker::PhantomData, 1105 + __unsafe_private_named: self.__unsafe_private_named, 1106 + _phantom: ::core::marker::PhantomData, 1107 + } 1108 + } 1109 + } 1110 + 1111 + impl<'a, S: fs_state::State> FsBuilder<'a, S> { 1112 + /// Set the `fileCount` field (optional) 1113 + pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self { 1114 + self.__unsafe_private_named.1 = value.into(); 1115 + self 1116 + } 1117 + /// Set the `fileCount` field to an Option value (optional) 1118 + pub fn maybe_file_count(mut self, value: Option<i64>) -> Self { 1119 + self.__unsafe_private_named.1 = value; 1120 + self 1121 + } 1122 + } 1123 + 1124 + impl<'a, S> FsBuilder<'a, S> 1125 + where 1126 + S: fs_state::State, 1127 + S::Root: fs_state::IsUnset, 1128 + { 1129 + /// Set the `root` field (required) 1130 + pub fn root( 1131 + mut self, 1132 + value: impl Into<crate::place_wisp::fs::Directory<'a>>, 1133 + ) -> FsBuilder<'a, fs_state::SetRoot<S>> { 1134 + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); 1135 + FsBuilder { 1136 + _phantom_state: ::core::marker::PhantomData, 1137 + __unsafe_private_named: self.__unsafe_private_named, 1138 + _phantom: ::core::marker::PhantomData, 1139 + } 1140 + } 1141 + } 1142 + 1143 + impl<'a, S> FsBuilder<'a, S> 1144 + where 1145 + S: fs_state::State, 1146 + S::Site: fs_state::IsUnset, 1147 + { 1148 + /// Set the `site` field (required) 1149 + pub fn site( 1150 + mut self, 1151 + value: impl Into<jacquard_common::CowStr<'a>>, 1152 + ) -> FsBuilder<'a, fs_state::SetSite<S>> { 1153 + self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); 1154 + FsBuilder { 1155 + _phantom_state: ::core::marker::PhantomData, 1156 + __unsafe_private_named: self.__unsafe_private_named, 1157 + _phantom: ::core::marker::PhantomData, 1158 + } 1159 + } 1160 + } 1161 + 1162 + impl<'a, S> FsBuilder<'a, S> 1163 + where 1164 + S: fs_state::State, 1165 + S::CreatedAt: fs_state::IsSet, 1166 + S::Site: fs_state::IsSet, 1167 + S::Root: fs_state::IsSet, 1168 + { 1169 + /// Build the final struct 1170 + pub fn build(self) -> Fs<'a> { 1171 + Fs { 1172 + created_at: self.__unsafe_private_named.0.unwrap(), 1173 + file_count: self.__unsafe_private_named.1, 1174 + root: self.__unsafe_private_named.2.unwrap(), 1175 + site: self.__unsafe_private_named.3.unwrap(), 1176 + extra_data: Default::default(), 1177 + } 1178 + } 1179 + /// Build the final struct with custom extra_data 1180 + pub fn build_with_data( 1181 + self, 1182 + extra_data: std::collections::BTreeMap< 1183 + jacquard_common::smol_str::SmolStr, 1184 + jacquard_common::types::value::Data<'a>, 1185 + >, 1186 + ) -> Fs<'a> { 1187 + Fs { 1188 + created_at: self.__unsafe_private_named.0.unwrap(), 1189 + file_count: self.__unsafe_private_named.1, 1190 + root: self.__unsafe_private_named.2.unwrap(), 1191 + site: self.__unsafe_private_named.3.unwrap(), 1192 + extra_data: Some(extra_data), 1193 + } 1194 + } 1195 + } 1196 + 1197 + impl<'a> Fs<'a> { 1198 + pub fn uri( 1199 + uri: impl Into<jacquard_common::CowStr<'a>>, 1200 + ) -> Result< 1201 + jacquard_common::types::uri::RecordUri<'a, FsRecord>, 1202 + jacquard_common::types::uri::UriError, 1203 + > { 1204 + jacquard_common::types::uri::RecordUri::try_from_uri( 1205 + jacquard_common::types::string::AtUri::new_cow(uri.into())?, 1206 + ) 1207 + } 1208 + } 1209 + 1210 + /// Typed wrapper for GetRecord response with this collection's record type. 1211 + #[derive( 1212 + serde::Serialize, 1213 + serde::Deserialize, 1214 + Debug, 1215 + Clone, 1216 + PartialEq, 1217 + Eq, 1218 + jacquard_derive::IntoStatic 1219 + )] 1220 + #[serde(rename_all = "camelCase")] 1221 + pub struct FsGetRecordOutput<'a> { 1222 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1223 + #[serde(borrow)] 1224 + pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 1225 + #[serde(borrow)] 1226 + pub uri: jacquard_common::types::string::AtUri<'a>, 1227 + #[serde(borrow)] 1228 + pub value: Fs<'a>, 1229 + } 1230 + 1231 + impl From<FsGetRecordOutput<'_>> for Fs<'_> { 1232 + fn from(output: FsGetRecordOutput<'_>) -> Self { 1233 + use jacquard_common::IntoStatic; 1234 + output.value.into_static() 1235 + } 1236 + } 1237 + 1238 + impl jacquard_common::types::collection::Collection for Fs<'_> { 1239 + const NSID: &'static str = "place.wisp.fs"; 1240 + type Record = FsRecord; 1241 + } 1242 + 1243 + /// Marker type for deserializing records from this collection. 1244 + #[derive(Debug, serde::Serialize, serde::Deserialize)] 1245 + pub struct FsRecord; 1246 + impl jacquard_common::xrpc::XrpcResp for FsRecord { 1247 + const NSID: &'static str = "place.wisp.fs"; 1248 + const ENCODING: &'static str = "application/json"; 1249 + type Output<'de> = FsGetRecordOutput<'de>; 1250 + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; 1251 + } 1252 + 1253 + impl jacquard_common::types::collection::Collection for FsRecord { 1254 + const NSID: &'static str = "place.wisp.fs"; 1255 + type Record = FsRecord; 1256 + } 1257 + 1258 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Fs<'a> { 1259 + fn nsid() -> &'static str { 1260 + "place.wisp.fs" 1261 + } 1262 + fn def_name() -> &'static str { 1263 + "main" 1264 + } 1265 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1266 + lexicon_doc_place_wisp_fs() 1267 + } 1268 + fn validate( 1269 + &self, 1270 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1271 + if let Some(ref value) = self.file_count { 1272 + if *value > 1000i64 { 1273 + return Err(::jacquard_lexicon::validation::ConstraintError::Maximum { 1274 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1275 + "file_count", 1276 + ), 1277 + max: 1000i64, 1278 + actual: *value, 1279 + }); 1280 + } 1281 + } 1282 + if let Some(ref value) = self.file_count { 1283 + if *value < 0i64 { 1284 + return Err(::jacquard_lexicon::validation::ConstraintError::Minimum { 1285 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1286 + "file_count", 1287 + ), 1288 + min: 0i64, 1289 + actual: *value, 1290 + }); 1291 + } 1292 + } 1293 + Ok(()) 1294 + } 1295 + } 1296 + 1297 + #[jacquard_derive::lexicon] 1298 + #[derive( 1299 + serde::Serialize, 1300 + serde::Deserialize, 1301 + Debug, 1302 + Clone, 1303 + PartialEq, 1304 + Eq, 1305 + jacquard_derive::IntoStatic 1306 + )] 1307 + #[serde(rename_all = "camelCase")] 1308 + pub struct Subfs<'a> { 1309 + /// If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure. 1310 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1311 + pub flat: std::option::Option<bool>, 1312 + /// AT-URI pointing to a place.wisp.subfs record containing this subtree. 1313 + #[serde(borrow)] 1314 + pub subject: jacquard_common::types::string::AtUri<'a>, 1315 + #[serde(borrow)] 1316 + pub r#type: jacquard_common::CowStr<'a>, 1317 + } 1318 + 1319 + pub mod subfs_state { 1320 + 1321 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 1322 + #[allow(unused)] 1323 + use ::core::marker::PhantomData; 1324 + mod sealed { 1325 + pub trait Sealed {} 1326 + } 1327 + /// State trait tracking which required fields have been set 1328 + pub trait State: sealed::Sealed { 1329 + type Type; 1330 + type Subject; 1331 + } 1332 + /// Empty state - all required fields are unset 1333 + pub struct Empty(()); 1334 + impl sealed::Sealed for Empty {} 1335 + impl State for Empty { 1336 + type Type = Unset; 1337 + type Subject = Unset; 1338 + } 1339 + ///State transition - sets the `type` field to Set 1340 + pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 1341 + impl<S: State> sealed::Sealed for SetType<S> {} 1342 + impl<S: State> State for SetType<S> { 1343 + type Type = Set<members::r#type>; 1344 + type Subject = S::Subject; 1345 + } 1346 + ///State transition - sets the `subject` field to Set 1347 + pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>); 1348 + impl<S: State> sealed::Sealed for SetSubject<S> {} 1349 + impl<S: State> State for SetSubject<S> { 1350 + type Type = S::Type; 1351 + type Subject = Set<members::subject>; 1352 + } 1353 + /// Marker types for field names 1354 + #[allow(non_camel_case_types)] 1355 + pub mod members { 1356 + ///Marker type for the `type` field 1357 + pub struct r#type(()); 1358 + ///Marker type for the `subject` field 1359 + pub struct subject(()); 1360 + } 1361 + } 1362 + 1363 + /// Builder for constructing an instance of this type 1364 + pub struct SubfsBuilder<'a, S: subfs_state::State> { 1365 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1366 + __unsafe_private_named: ( 1367 + ::core::option::Option<bool>, 1368 + ::core::option::Option<jacquard_common::types::string::AtUri<'a>>, 1369 + ::core::option::Option<jacquard_common::CowStr<'a>>, 1370 + ), 1371 + _phantom: ::core::marker::PhantomData<&'a ()>, 1372 + } 1373 + 1374 + impl<'a> Subfs<'a> { 1375 + /// Create a new builder for this type 1376 + pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> { 1377 + SubfsBuilder::new() 1378 + } 1379 + } 1380 + 1381 + impl<'a> SubfsBuilder<'a, subfs_state::Empty> { 1382 + /// Create a new builder with all fields unset 1383 + pub fn new() -> Self { 1384 + SubfsBuilder { 1385 + _phantom_state: ::core::marker::PhantomData, 1386 + __unsafe_private_named: (None, None, None), 1387 + _phantom: ::core::marker::PhantomData, 1388 + } 1389 + } 1390 + } 1391 + 1392 + impl<'a, S: subfs_state::State> SubfsBuilder<'a, S> { 1393 + /// Set the `flat` field (optional) 1394 + pub fn flat(mut self, value: impl Into<Option<bool>>) -> Self { 1395 + self.__unsafe_private_named.0 = value.into(); 1396 + self 1397 + } 1398 + /// Set the `flat` field to an Option value (optional) 1399 + pub fn maybe_flat(mut self, value: Option<bool>) -> Self { 1400 + self.__unsafe_private_named.0 = value; 1401 + self 1402 + } 1403 + } 1404 + 1405 + impl<'a, S> SubfsBuilder<'a, S> 1406 + where 1407 + S: subfs_state::State, 1408 + S::Subject: subfs_state::IsUnset, 1409 + { 1410 + /// Set the `subject` field (required) 1411 + pub fn subject( 1412 + mut self, 1413 + value: impl Into<jacquard_common::types::string::AtUri<'a>>, 1414 + ) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> { 1415 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 1416 + SubfsBuilder { 1417 + _phantom_state: ::core::marker::PhantomData, 1418 + __unsafe_private_named: self.__unsafe_private_named, 1419 + _phantom: ::core::marker::PhantomData, 1420 + } 1421 + } 1422 + } 1423 + 1424 + impl<'a, S> SubfsBuilder<'a, S> 1425 + where 1426 + S: subfs_state::State, 1427 + S::Type: subfs_state::IsUnset, 1428 + { 1429 + /// Set the `type` field (required) 1430 + pub fn r#type( 1431 + mut self, 1432 + value: impl Into<jacquard_common::CowStr<'a>>, 1433 + ) -> SubfsBuilder<'a, subfs_state::SetType<S>> { 1434 + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); 1435 + SubfsBuilder { 1436 + _phantom_state: ::core::marker::PhantomData, 1437 + __unsafe_private_named: self.__unsafe_private_named, 1438 + _phantom: ::core::marker::PhantomData, 1439 + } 1440 + } 1441 + } 1442 + 1443 + impl<'a, S> SubfsBuilder<'a, S> 1444 + where 1445 + S: subfs_state::State, 1446 + S::Type: subfs_state::IsSet, 1447 + S::Subject: subfs_state::IsSet, 1448 + { 1449 + /// Build the final struct 1450 + pub fn build(self) -> Subfs<'a> { 1451 + Subfs { 1452 + flat: self.__unsafe_private_named.0, 1453 + subject: self.__unsafe_private_named.1.unwrap(), 1454 + r#type: self.__unsafe_private_named.2.unwrap(), 1455 + extra_data: Default::default(), 1456 + } 1457 + } 1458 + /// Build the final struct with custom extra_data 1459 + pub fn build_with_data( 1460 + self, 1461 + extra_data: std::collections::BTreeMap< 1462 + jacquard_common::smol_str::SmolStr, 1463 + jacquard_common::types::value::Data<'a>, 1464 + >, 1465 + ) -> Subfs<'a> { 1466 + Subfs { 1467 + flat: self.__unsafe_private_named.0, 1468 + subject: self.__unsafe_private_named.1.unwrap(), 1469 + r#type: self.__unsafe_private_named.2.unwrap(), 1470 + extra_data: Some(extra_data), 1471 + } 1472 + } 1473 + } 1474 + 1475 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> { 1476 + fn nsid() -> &'static str { 1477 + "place.wisp.fs" 1478 + } 1479 + fn def_name() -> &'static str { 1480 + "subfs" 1481 + } 1482 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1483 + lexicon_doc_place_wisp_fs() 1484 + } 1485 + fn validate( 1486 + &self, 1487 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1488 + Ok(()) 1489 + } 1490 + }
+653
cli/crates/lexicons/src/place_wisp/settings.rs
···
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: place.wisp.settings 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + /// Custom HTTP header configuration 9 + #[jacquard_derive::lexicon] 10 + #[derive( 11 + serde::Serialize, 12 + serde::Deserialize, 13 + Debug, 14 + Clone, 15 + PartialEq, 16 + Eq, 17 + jacquard_derive::IntoStatic, 18 + Default 19 + )] 20 + #[serde(rename_all = "camelCase")] 21 + pub struct CustomHeader<'a> { 22 + /// HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options') 23 + #[serde(borrow)] 24 + pub name: jacquard_common::CowStr<'a>, 25 + /// Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths. 26 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 27 + #[serde(borrow)] 28 + pub path: std::option::Option<jacquard_common::CowStr<'a>>, 29 + /// HTTP header value 30 + #[serde(borrow)] 31 + pub value: jacquard_common::CowStr<'a>, 32 + } 33 + 34 + fn lexicon_doc_place_wisp_settings() -> ::jacquard_lexicon::lexicon::LexiconDoc< 35 + 'static, 36 + > { 37 + ::jacquard_lexicon::lexicon::LexiconDoc { 38 + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 39 + id: ::jacquard_common::CowStr::new_static("place.wisp.settings"), 40 + revision: None, 41 + description: None, 42 + defs: { 43 + let mut map = ::std::collections::BTreeMap::new(); 44 + map.insert( 45 + ::jacquard_common::smol_str::SmolStr::new_static("customHeader"), 46 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 47 + description: Some( 48 + ::jacquard_common::CowStr::new_static( 49 + "Custom HTTP header configuration", 50 + ), 51 + ), 52 + required: Some( 53 + vec![ 54 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 55 + ::jacquard_common::smol_str::SmolStr::new_static("value") 56 + ], 57 + ), 58 + nullable: None, 59 + properties: { 60 + #[allow(unused_mut)] 61 + let mut map = ::std::collections::BTreeMap::new(); 62 + map.insert( 63 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 64 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 65 + description: Some( 66 + ::jacquard_common::CowStr::new_static( 67 + "HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')", 68 + ), 69 + ), 70 + format: None, 71 + default: None, 72 + min_length: None, 73 + max_length: Some(100usize), 74 + min_graphemes: None, 75 + max_graphemes: None, 76 + r#enum: None, 77 + r#const: None, 78 + known_values: None, 79 + }), 80 + ); 81 + map.insert( 82 + ::jacquard_common::smol_str::SmolStr::new_static("path"), 83 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 84 + description: Some( 85 + ::jacquard_common::CowStr::new_static( 86 + "Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.", 87 + ), 88 + ), 89 + format: None, 90 + default: None, 91 + min_length: None, 92 + max_length: Some(500usize), 93 + min_graphemes: None, 94 + max_graphemes: None, 95 + r#enum: None, 96 + r#const: None, 97 + known_values: None, 98 + }), 99 + ); 100 + map.insert( 101 + ::jacquard_common::smol_str::SmolStr::new_static("value"), 102 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 103 + description: Some( 104 + ::jacquard_common::CowStr::new_static("HTTP header value"), 105 + ), 106 + format: None, 107 + default: None, 108 + min_length: None, 109 + max_length: Some(1000usize), 110 + min_graphemes: None, 111 + max_graphemes: None, 112 + r#enum: None, 113 + r#const: None, 114 + known_values: None, 115 + }), 116 + ); 117 + map 118 + }, 119 + }), 120 + ); 121 + map.insert( 122 + ::jacquard_common::smol_str::SmolStr::new_static("main"), 123 + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { 124 + description: Some( 125 + ::jacquard_common::CowStr::new_static( 126 + "Configuration settings for a static site hosted on wisp.place", 127 + ), 128 + ), 129 + key: Some(::jacquard_common::CowStr::new_static("any")), 130 + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { 131 + description: None, 132 + required: None, 133 + nullable: None, 134 + properties: { 135 + #[allow(unused_mut)] 136 + let mut map = ::std::collections::BTreeMap::new(); 137 + map.insert( 138 + ::jacquard_common::smol_str::SmolStr::new_static( 139 + "cleanUrls", 140 + ), 141 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 142 + description: None, 143 + default: None, 144 + r#const: None, 145 + }), 146 + ); 147 + map.insert( 148 + ::jacquard_common::smol_str::SmolStr::new_static( 149 + "custom404", 150 + ), 151 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 152 + description: Some( 153 + ::jacquard_common::CowStr::new_static( 154 + "Custom 404 error page file path. Incompatible with directoryListing and spaMode.", 155 + ), 156 + ), 157 + format: None, 158 + default: None, 159 + min_length: None, 160 + max_length: Some(500usize), 161 + min_graphemes: None, 162 + max_graphemes: None, 163 + r#enum: None, 164 + r#const: None, 165 + known_values: None, 166 + }), 167 + ); 168 + map.insert( 169 + ::jacquard_common::smol_str::SmolStr::new_static( 170 + "directoryListing", 171 + ), 172 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 173 + description: None, 174 + default: None, 175 + r#const: None, 176 + }), 177 + ); 178 + map.insert( 179 + ::jacquard_common::smol_str::SmolStr::new_static("headers"), 180 + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 181 + description: Some( 182 + ::jacquard_common::CowStr::new_static( 183 + "Custom HTTP headers to set on responses", 184 + ), 185 + ), 186 + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { 187 + description: None, 188 + r#ref: ::jacquard_common::CowStr::new_static( 189 + "#customHeader", 190 + ), 191 + }), 192 + min_length: None, 193 + max_length: Some(50usize), 194 + }), 195 + ); 196 + map.insert( 197 + ::jacquard_common::smol_str::SmolStr::new_static( 198 + "indexFiles", 199 + ), 200 + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 201 + description: Some( 202 + ::jacquard_common::CowStr::new_static( 203 + "Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.", 204 + ), 205 + ), 206 + items: ::jacquard_lexicon::lexicon::LexArrayItem::String(::jacquard_lexicon::lexicon::LexString { 207 + description: None, 208 + format: None, 209 + default: None, 210 + min_length: None, 211 + max_length: Some(255usize), 212 + min_graphemes: None, 213 + max_graphemes: None, 214 + r#enum: None, 215 + r#const: None, 216 + known_values: None, 217 + }), 218 + min_length: None, 219 + max_length: Some(10usize), 220 + }), 221 + ); 222 + map.insert( 223 + ::jacquard_common::smol_str::SmolStr::new_static("spaMode"), 224 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 225 + description: Some( 226 + ::jacquard_common::CowStr::new_static( 227 + "File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.", 228 + ), 229 + ), 230 + format: None, 231 + default: None, 232 + min_length: None, 233 + max_length: Some(500usize), 234 + min_graphemes: None, 235 + max_graphemes: None, 236 + r#enum: None, 237 + r#const: None, 238 + known_values: None, 239 + }), 240 + ); 241 + map 242 + }, 243 + }), 244 + }), 245 + ); 246 + map 247 + }, 248 + } 249 + } 250 + 251 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for CustomHeader<'a> { 252 + fn nsid() -> &'static str { 253 + "place.wisp.settings" 254 + } 255 + fn def_name() -> &'static str { 256 + "customHeader" 257 + } 258 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 259 + lexicon_doc_place_wisp_settings() 260 + } 261 + fn validate( 262 + &self, 263 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 264 + { 265 + let value = &self.name; 266 + #[allow(unused_comparisons)] 267 + if <str>::len(value.as_ref()) > 100usize { 268 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 269 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 270 + "name", 271 + ), 272 + max: 100usize, 273 + actual: <str>::len(value.as_ref()), 274 + }); 275 + } 276 + } 277 + if let Some(ref value) = self.path { 278 + #[allow(unused_comparisons)] 279 + if <str>::len(value.as_ref()) > 500usize { 280 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 281 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 282 + "path", 283 + ), 284 + max: 500usize, 285 + actual: <str>::len(value.as_ref()), 286 + }); 287 + } 288 + } 289 + { 290 + let value = &self.value; 291 + #[allow(unused_comparisons)] 292 + if <str>::len(value.as_ref()) > 1000usize { 293 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 294 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 295 + "value", 296 + ), 297 + max: 1000usize, 298 + actual: <str>::len(value.as_ref()), 299 + }); 300 + } 301 + } 302 + Ok(()) 303 + } 304 + } 305 + 306 + /// Configuration settings for a static site hosted on wisp.place 307 + #[jacquard_derive::lexicon] 308 + #[derive( 309 + serde::Serialize, 310 + serde::Deserialize, 311 + Debug, 312 + Clone, 313 + PartialEq, 314 + Eq, 315 + jacquard_derive::IntoStatic 316 + )] 317 + #[serde(rename_all = "camelCase")] 318 + pub struct Settings<'a> { 319 + /// Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically. 320 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 321 + pub clean_urls: std::option::Option<bool>, 322 + /// Custom 404 error page file path. Incompatible with directoryListing and spaMode. 323 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 324 + #[serde(borrow)] 325 + pub custom404: std::option::Option<jacquard_common::CowStr<'a>>, 326 + /// Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode. 327 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 328 + pub directory_listing: std::option::Option<bool>, 329 + /// Custom HTTP headers to set on responses 330 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 331 + #[serde(borrow)] 332 + pub headers: std::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>, 333 + /// Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified. 334 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 335 + #[serde(borrow)] 336 + pub index_files: std::option::Option<Vec<jacquard_common::CowStr<'a>>>, 337 + /// File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404. 338 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 339 + #[serde(borrow)] 340 + pub spa_mode: std::option::Option<jacquard_common::CowStr<'a>>, 341 + } 342 + 343 + pub mod settings_state { 344 + 345 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 346 + #[allow(unused)] 347 + use ::core::marker::PhantomData; 348 + mod sealed { 349 + pub trait Sealed {} 350 + } 351 + /// State trait tracking which required fields have been set 352 + pub trait State: sealed::Sealed {} 353 + /// Empty state - all required fields are unset 354 + pub struct Empty(()); 355 + impl sealed::Sealed for Empty {} 356 + impl State for Empty {} 357 + /// Marker types for field names 358 + #[allow(non_camel_case_types)] 359 + pub mod members {} 360 + } 361 + 362 + /// Builder for constructing an instance of this type 363 + pub struct SettingsBuilder<'a, S: settings_state::State> { 364 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 365 + __unsafe_private_named: ( 366 + ::core::option::Option<bool>, 367 + ::core::option::Option<jacquard_common::CowStr<'a>>, 368 + ::core::option::Option<bool>, 369 + ::core::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>, 370 + ::core::option::Option<Vec<jacquard_common::CowStr<'a>>>, 371 + ::core::option::Option<jacquard_common::CowStr<'a>>, 372 + ), 373 + _phantom: ::core::marker::PhantomData<&'a ()>, 374 + } 375 + 376 + impl<'a> Settings<'a> { 377 + /// Create a new builder for this type 378 + pub fn new() -> SettingsBuilder<'a, settings_state::Empty> { 379 + SettingsBuilder::new() 380 + } 381 + } 382 + 383 + impl<'a> SettingsBuilder<'a, settings_state::Empty> { 384 + /// Create a new builder with all fields unset 385 + pub fn new() -> Self { 386 + SettingsBuilder { 387 + _phantom_state: ::core::marker::PhantomData, 388 + __unsafe_private_named: (None, None, None, None, None, None), 389 + _phantom: ::core::marker::PhantomData, 390 + } 391 + } 392 + } 393 + 394 + impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 395 + /// Set the `cleanUrls` field (optional) 396 + pub fn clean_urls(mut self, value: impl Into<Option<bool>>) -> Self { 397 + self.__unsafe_private_named.0 = value.into(); 398 + self 399 + } 400 + /// Set the `cleanUrls` field to an Option value (optional) 401 + pub fn maybe_clean_urls(mut self, value: Option<bool>) -> Self { 402 + self.__unsafe_private_named.0 = value; 403 + self 404 + } 405 + } 406 + 407 + impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 408 + /// Set the `custom404` field (optional) 409 + pub fn custom404( 410 + mut self, 411 + value: impl Into<Option<jacquard_common::CowStr<'a>>>, 412 + ) -> Self { 413 + self.__unsafe_private_named.1 = value.into(); 414 + self 415 + } 416 + /// Set the `custom404` field to an Option value (optional) 417 + pub fn maybe_custom404( 418 + mut self, 419 + value: Option<jacquard_common::CowStr<'a>>, 420 + ) -> Self { 421 + self.__unsafe_private_named.1 = value; 422 + self 423 + } 424 + } 425 + 426 + impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 427 + /// Set the `directoryListing` field (optional) 428 + pub fn directory_listing(mut self, value: impl Into<Option<bool>>) -> Self { 429 + self.__unsafe_private_named.2 = value.into(); 430 + self 431 + } 432 + /// Set the `directoryListing` field to an Option value (optional) 433 + pub fn maybe_directory_listing(mut self, value: Option<bool>) -> Self { 434 + self.__unsafe_private_named.2 = value; 435 + self 436 + } 437 + } 438 + 439 + impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 440 + /// Set the `headers` field (optional) 441 + pub fn headers( 442 + mut self, 443 + value: impl Into<Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>>, 444 + ) -> Self { 445 + self.__unsafe_private_named.3 = value.into(); 446 + self 447 + } 448 + /// Set the `headers` field to an Option value (optional) 449 + pub fn maybe_headers( 450 + mut self, 451 + value: Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>, 452 + ) -> Self { 453 + self.__unsafe_private_named.3 = value; 454 + self 455 + } 456 + } 457 + 458 + impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 459 + /// Set the `indexFiles` field (optional) 460 + pub fn index_files( 461 + mut self, 462 + value: impl Into<Option<Vec<jacquard_common::CowStr<'a>>>>, 463 + ) -> Self { 464 + self.__unsafe_private_named.4 = value.into(); 465 + self 466 + } 467 + /// Set the `indexFiles` field to an Option value (optional) 468 + pub fn maybe_index_files( 469 + mut self, 470 + value: Option<Vec<jacquard_common::CowStr<'a>>>, 471 + ) -> Self { 472 + self.__unsafe_private_named.4 = value; 473 + self 474 + } 475 + } 476 + 477 + impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 478 + /// Set the `spaMode` field (optional) 479 + pub fn spa_mode( 480 + mut self, 481 + value: impl Into<Option<jacquard_common::CowStr<'a>>>, 482 + ) -> Self { 483 + self.__unsafe_private_named.5 = value.into(); 484 + self 485 + } 486 + /// Set the `spaMode` field to an Option value (optional) 487 + pub fn maybe_spa_mode(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 488 + self.__unsafe_private_named.5 = value; 489 + self 490 + } 491 + } 492 + 493 + impl<'a, S> SettingsBuilder<'a, S> 494 + where 495 + S: settings_state::State, 496 + { 497 + /// Build the final struct 498 + pub fn build(self) -> Settings<'a> { 499 + Settings { 500 + clean_urls: self.__unsafe_private_named.0, 501 + custom404: self.__unsafe_private_named.1, 502 + directory_listing: self.__unsafe_private_named.2, 503 + headers: self.__unsafe_private_named.3, 504 + index_files: self.__unsafe_private_named.4, 505 + spa_mode: self.__unsafe_private_named.5, 506 + extra_data: Default::default(), 507 + } 508 + } 509 + /// Build the final struct with custom extra_data 510 + pub fn build_with_data( 511 + self, 512 + extra_data: std::collections::BTreeMap< 513 + jacquard_common::smol_str::SmolStr, 514 + jacquard_common::types::value::Data<'a>, 515 + >, 516 + ) -> Settings<'a> { 517 + Settings { 518 + clean_urls: self.__unsafe_private_named.0, 519 + custom404: self.__unsafe_private_named.1, 520 + directory_listing: self.__unsafe_private_named.2, 521 + headers: self.__unsafe_private_named.3, 522 + index_files: self.__unsafe_private_named.4, 523 + spa_mode: self.__unsafe_private_named.5, 524 + extra_data: Some(extra_data), 525 + } 526 + } 527 + } 528 + 529 + impl<'a> Settings<'a> { 530 + pub fn uri( 531 + uri: impl Into<jacquard_common::CowStr<'a>>, 532 + ) -> Result< 533 + jacquard_common::types::uri::RecordUri<'a, SettingsRecord>, 534 + jacquard_common::types::uri::UriError, 535 + > { 536 + jacquard_common::types::uri::RecordUri::try_from_uri( 537 + jacquard_common::types::string::AtUri::new_cow(uri.into())?, 538 + ) 539 + } 540 + } 541 + 542 + /// Typed wrapper for GetRecord response with this collection's record type. 543 + #[derive( 544 + serde::Serialize, 545 + serde::Deserialize, 546 + Debug, 547 + Clone, 548 + PartialEq, 549 + Eq, 550 + jacquard_derive::IntoStatic 551 + )] 552 + #[serde(rename_all = "camelCase")] 553 + pub struct SettingsGetRecordOutput<'a> { 554 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 555 + #[serde(borrow)] 556 + pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 557 + #[serde(borrow)] 558 + pub uri: jacquard_common::types::string::AtUri<'a>, 559 + #[serde(borrow)] 560 + pub value: Settings<'a>, 561 + } 562 + 563 + impl From<SettingsGetRecordOutput<'_>> for Settings<'_> { 564 + fn from(output: SettingsGetRecordOutput<'_>) -> Self { 565 + use jacquard_common::IntoStatic; 566 + output.value.into_static() 567 + } 568 + } 569 + 570 + impl jacquard_common::types::collection::Collection for Settings<'_> { 571 + const NSID: &'static str = "place.wisp.settings"; 572 + type Record = SettingsRecord; 573 + } 574 + 575 + /// Marker type for deserializing records from this collection. 576 + #[derive(Debug, serde::Serialize, serde::Deserialize)] 577 + pub struct SettingsRecord; 578 + impl jacquard_common::xrpc::XrpcResp for SettingsRecord { 579 + const NSID: &'static str = "place.wisp.settings"; 580 + const ENCODING: &'static str = "application/json"; 581 + type Output<'de> = SettingsGetRecordOutput<'de>; 582 + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; 583 + } 584 + 585 + impl jacquard_common::types::collection::Collection for SettingsRecord { 586 + const NSID: &'static str = "place.wisp.settings"; 587 + type Record = SettingsRecord; 588 + } 589 + 590 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Settings<'a> { 591 + fn nsid() -> &'static str { 592 + "place.wisp.settings" 593 + } 594 + fn def_name() -> &'static str { 595 + "main" 596 + } 597 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 598 + lexicon_doc_place_wisp_settings() 599 + } 600 + fn validate( 601 + &self, 602 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 603 + if let Some(ref value) = self.custom404 { 604 + #[allow(unused_comparisons)] 605 + if <str>::len(value.as_ref()) > 500usize { 606 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 607 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 608 + "custom404", 609 + ), 610 + max: 500usize, 611 + actual: <str>::len(value.as_ref()), 612 + }); 613 + } 614 + } 615 + if let Some(ref value) = self.headers { 616 + #[allow(unused_comparisons)] 617 + if value.len() > 50usize { 618 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 619 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 620 + "headers", 621 + ), 622 + max: 50usize, 623 + actual: value.len(), 624 + }); 625 + } 626 + } 627 + if let Some(ref value) = self.index_files { 628 + #[allow(unused_comparisons)] 629 + if value.len() > 10usize { 630 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 631 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 632 + "index_files", 633 + ), 634 + max: 10usize, 635 + actual: value.len(), 636 + }); 637 + } 638 + } 639 + if let Some(ref value) = self.spa_mode { 640 + #[allow(unused_comparisons)] 641 + if <str>::len(value.as_ref()) > 500usize { 642 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 643 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 644 + "spa_mode", 645 + ), 646 + max: 500usize, 647 + actual: <str>::len(value.as_ref()), 648 + }); 649 + } 650 + } 651 + Ok(()) 652 + } 653 + }
+1408
cli/crates/lexicons/src/place_wisp/subfs.rs
···
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: place.wisp.subfs 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + #[jacquard_derive::lexicon] 9 + #[derive( 10 + serde::Serialize, 11 + serde::Deserialize, 12 + Debug, 13 + Clone, 14 + PartialEq, 15 + Eq, 16 + jacquard_derive::IntoStatic 17 + )] 18 + #[serde(rename_all = "camelCase")] 19 + pub struct Directory<'a> { 20 + #[serde(borrow)] 21 + pub entries: Vec<crate::place_wisp::subfs::Entry<'a>>, 22 + #[serde(borrow)] 23 + pub r#type: jacquard_common::CowStr<'a>, 24 + } 25 + 26 + pub mod directory_state { 27 + 28 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 29 + #[allow(unused)] 30 + use ::core::marker::PhantomData; 31 + mod sealed { 32 + pub trait Sealed {} 33 + } 34 + /// State trait tracking which required fields have been set 35 + pub trait State: sealed::Sealed { 36 + type Type; 37 + type Entries; 38 + } 39 + /// Empty state - all required fields are unset 40 + pub struct Empty(()); 41 + impl sealed::Sealed for Empty {} 42 + impl State for Empty { 43 + type Type = Unset; 44 + type Entries = Unset; 45 + } 46 + ///State transition - sets the `type` field to Set 47 + pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 48 + impl<S: State> sealed::Sealed for SetType<S> {} 49 + impl<S: State> State for SetType<S> { 50 + type Type = Set<members::r#type>; 51 + type Entries = S::Entries; 52 + } 53 + ///State transition - sets the `entries` field to Set 54 + pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>); 55 + impl<S: State> sealed::Sealed for SetEntries<S> {} 56 + impl<S: State> State for SetEntries<S> { 57 + type Type = S::Type; 58 + type Entries = Set<members::entries>; 59 + } 60 + /// Marker types for field names 61 + #[allow(non_camel_case_types)] 62 + pub mod members { 63 + ///Marker type for the `type` field 64 + pub struct r#type(()); 65 + ///Marker type for the `entries` field 66 + pub struct entries(()); 67 + } 68 + } 69 + 70 + /// Builder for constructing an instance of this type 71 + pub struct DirectoryBuilder<'a, S: directory_state::State> { 72 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 73 + __unsafe_private_named: ( 74 + ::core::option::Option<Vec<crate::place_wisp::subfs::Entry<'a>>>, 75 + ::core::option::Option<jacquard_common::CowStr<'a>>, 76 + ), 77 + _phantom: ::core::marker::PhantomData<&'a ()>, 78 + } 79 + 80 + impl<'a> Directory<'a> { 81 + /// Create a new builder for this type 82 + pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> { 83 + DirectoryBuilder::new() 84 + } 85 + } 86 + 87 + impl<'a> DirectoryBuilder<'a, directory_state::Empty> { 88 + /// Create a new builder with all fields unset 89 + pub fn new() -> Self { 90 + DirectoryBuilder { 91 + _phantom_state: ::core::marker::PhantomData, 92 + __unsafe_private_named: (None, None), 93 + _phantom: ::core::marker::PhantomData, 94 + } 95 + } 96 + } 97 + 98 + impl<'a, S> DirectoryBuilder<'a, S> 99 + where 100 + S: directory_state::State, 101 + S::Entries: directory_state::IsUnset, 102 + { 103 + /// Set the `entries` field (required) 104 + pub fn entries( 105 + mut self, 106 + value: impl Into<Vec<crate::place_wisp::subfs::Entry<'a>>>, 107 + ) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> { 108 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 109 + DirectoryBuilder { 110 + _phantom_state: ::core::marker::PhantomData, 111 + __unsafe_private_named: self.__unsafe_private_named, 112 + _phantom: ::core::marker::PhantomData, 113 + } 114 + } 115 + } 116 + 117 + impl<'a, S> DirectoryBuilder<'a, S> 118 + where 119 + S: directory_state::State, 120 + S::Type: directory_state::IsUnset, 121 + { 122 + /// Set the `type` field (required) 123 + pub fn r#type( 124 + mut self, 125 + value: impl Into<jacquard_common::CowStr<'a>>, 126 + ) -> DirectoryBuilder<'a, directory_state::SetType<S>> { 127 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 128 + DirectoryBuilder { 129 + _phantom_state: ::core::marker::PhantomData, 130 + __unsafe_private_named: self.__unsafe_private_named, 131 + _phantom: ::core::marker::PhantomData, 132 + } 133 + } 134 + } 135 + 136 + impl<'a, S> DirectoryBuilder<'a, S> 137 + where 138 + S: directory_state::State, 139 + S::Type: directory_state::IsSet, 140 + S::Entries: directory_state::IsSet, 141 + { 142 + /// Build the final struct 143 + pub fn build(self) -> Directory<'a> { 144 + Directory { 145 + entries: self.__unsafe_private_named.0.unwrap(), 146 + r#type: self.__unsafe_private_named.1.unwrap(), 147 + extra_data: Default::default(), 148 + } 149 + } 150 + /// Build the final struct with custom extra_data 151 + pub fn build_with_data( 152 + self, 153 + extra_data: std::collections::BTreeMap< 154 + jacquard_common::smol_str::SmolStr, 155 + jacquard_common::types::value::Data<'a>, 156 + >, 157 + ) -> Directory<'a> { 158 + Directory { 159 + entries: self.__unsafe_private_named.0.unwrap(), 160 + r#type: self.__unsafe_private_named.1.unwrap(), 161 + extra_data: Some(extra_data), 162 + } 163 + } 164 + } 165 + 166 + fn lexicon_doc_place_wisp_subfs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 167 + ::jacquard_lexicon::lexicon::LexiconDoc { 168 + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 169 + id: ::jacquard_common::CowStr::new_static("place.wisp.subfs"), 170 + revision: None, 171 + description: None, 172 + defs: { 173 + let mut map = ::std::collections::BTreeMap::new(); 174 + map.insert( 175 + ::jacquard_common::smol_str::SmolStr::new_static("directory"), 176 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 177 + description: None, 178 + required: Some( 179 + vec![ 180 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 181 + ::jacquard_common::smol_str::SmolStr::new_static("entries") 182 + ], 183 + ), 184 + nullable: None, 185 + properties: { 186 + #[allow(unused_mut)] 187 + let mut map = ::std::collections::BTreeMap::new(); 188 + map.insert( 189 + ::jacquard_common::smol_str::SmolStr::new_static("entries"), 190 + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 191 + description: None, 192 + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { 193 + description: None, 194 + r#ref: ::jacquard_common::CowStr::new_static("#entry"), 195 + }), 196 + min_length: None, 197 + max_length: Some(500usize), 198 + }), 199 + ); 200 + map.insert( 201 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 202 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 203 + description: None, 204 + format: None, 205 + default: None, 206 + min_length: None, 207 + max_length: None, 208 + min_graphemes: None, 209 + max_graphemes: None, 210 + r#enum: None, 211 + r#const: None, 212 + known_values: None, 213 + }), 214 + ); 215 + map 216 + }, 217 + }), 218 + ); 219 + map.insert( 220 + ::jacquard_common::smol_str::SmolStr::new_static("entry"), 221 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 222 + description: None, 223 + required: Some( 224 + vec![ 225 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 226 + ::jacquard_common::smol_str::SmolStr::new_static("node") 227 + ], 228 + ), 229 + nullable: None, 230 + properties: { 231 + #[allow(unused_mut)] 232 + let mut map = ::std::collections::BTreeMap::new(); 233 + map.insert( 234 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 235 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 236 + description: None, 237 + format: None, 238 + default: None, 239 + min_length: None, 240 + max_length: Some(255usize), 241 + min_graphemes: None, 242 + max_graphemes: None, 243 + r#enum: None, 244 + r#const: None, 245 + known_values: None, 246 + }), 247 + ); 248 + map.insert( 249 + ::jacquard_common::smol_str::SmolStr::new_static("node"), 250 + ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 251 + description: None, 252 + refs: vec![ 253 + ::jacquard_common::CowStr::new_static("#file"), 254 + ::jacquard_common::CowStr::new_static("#directory"), 255 + ::jacquard_common::CowStr::new_static("#subfs") 256 + ], 257 + closed: None, 258 + }), 259 + ); 260 + map 261 + }, 262 + }), 263 + ); 264 + map.insert( 265 + ::jacquard_common::smol_str::SmolStr::new_static("file"), 266 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 267 + description: None, 268 + required: Some( 269 + vec![ 270 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 271 + ::jacquard_common::smol_str::SmolStr::new_static("blob") 272 + ], 273 + ), 274 + nullable: None, 275 + properties: { 276 + #[allow(unused_mut)] 277 + let mut map = ::std::collections::BTreeMap::new(); 278 + map.insert( 279 + ::jacquard_common::smol_str::SmolStr::new_static("base64"), 280 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 281 + description: None, 282 + default: None, 283 + r#const: None, 284 + }), 285 + ); 286 + map.insert( 287 + ::jacquard_common::smol_str::SmolStr::new_static("blob"), 288 + ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob { 289 + description: None, 290 + accept: None, 291 + max_size: None, 292 + }), 293 + ); 294 + map.insert( 295 + ::jacquard_common::smol_str::SmolStr::new_static("encoding"), 296 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 297 + description: Some( 298 + ::jacquard_common::CowStr::new_static( 299 + "Content encoding (e.g., gzip for compressed files)", 300 + ), 301 + ), 302 + format: None, 303 + default: None, 304 + min_length: None, 305 + max_length: None, 306 + min_graphemes: None, 307 + max_graphemes: None, 308 + r#enum: None, 309 + r#const: None, 310 + known_values: None, 311 + }), 312 + ); 313 + map.insert( 314 + ::jacquard_common::smol_str::SmolStr::new_static("mimeType"), 315 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 316 + description: Some( 317 + ::jacquard_common::CowStr::new_static( 318 + "Original MIME type before compression", 319 + ), 320 + ), 321 + format: None, 322 + default: None, 323 + min_length: None, 324 + max_length: None, 325 + min_graphemes: None, 326 + max_graphemes: None, 327 + r#enum: None, 328 + r#const: None, 329 + known_values: None, 330 + }), 331 + ); 332 + map.insert( 333 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 334 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 335 + description: None, 336 + format: None, 337 + default: None, 338 + min_length: None, 339 + max_length: None, 340 + min_graphemes: None, 341 + max_graphemes: None, 342 + r#enum: None, 343 + r#const: None, 344 + known_values: None, 345 + }), 346 + ); 347 + map 348 + }, 349 + }), 350 + ); 351 + map.insert( 352 + ::jacquard_common::smol_str::SmolStr::new_static("main"), 353 + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { 354 + description: Some( 355 + ::jacquard_common::CowStr::new_static( 356 + "Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.", 357 + ), 358 + ), 359 + key: None, 360 + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { 361 + description: None, 362 + required: Some( 363 + vec![ 364 + ::jacquard_common::smol_str::SmolStr::new_static("root"), 365 + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") 366 + ], 367 + ), 368 + nullable: None, 369 + properties: { 370 + #[allow(unused_mut)] 371 + let mut map = ::std::collections::BTreeMap::new(); 372 + map.insert( 373 + ::jacquard_common::smol_str::SmolStr::new_static( 374 + "createdAt", 375 + ), 376 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 377 + description: None, 378 + format: Some( 379 + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, 380 + ), 381 + default: None, 382 + min_length: None, 383 + max_length: None, 384 + min_graphemes: None, 385 + max_graphemes: None, 386 + r#enum: None, 387 + r#const: None, 388 + known_values: None, 389 + }), 390 + ); 391 + map.insert( 392 + ::jacquard_common::smol_str::SmolStr::new_static( 393 + "fileCount", 394 + ), 395 + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { 396 + description: None, 397 + default: None, 398 + minimum: Some(0i64), 399 + maximum: Some(1000i64), 400 + r#enum: None, 401 + r#const: None, 402 + }), 403 + ); 404 + map.insert( 405 + ::jacquard_common::smol_str::SmolStr::new_static("root"), 406 + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { 407 + description: None, 408 + r#ref: ::jacquard_common::CowStr::new_static("#directory"), 409 + }), 410 + ); 411 + map 412 + }, 413 + }), 414 + }), 415 + ); 416 + map.insert( 417 + ::jacquard_common::smol_str::SmolStr::new_static("subfs"), 418 + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 419 + description: None, 420 + required: Some( 421 + vec![ 422 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 423 + ::jacquard_common::smol_str::SmolStr::new_static("subject") 424 + ], 425 + ), 426 + nullable: None, 427 + properties: { 428 + #[allow(unused_mut)] 429 + let mut map = ::std::collections::BTreeMap::new(); 430 + map.insert( 431 + ::jacquard_common::smol_str::SmolStr::new_static("subject"), 432 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 433 + description: Some( 434 + ::jacquard_common::CowStr::new_static( 435 + "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.", 436 + ), 437 + ), 438 + format: Some( 439 + ::jacquard_lexicon::lexicon::LexStringFormat::AtUri, 440 + ), 441 + default: None, 442 + min_length: None, 443 + max_length: None, 444 + min_graphemes: None, 445 + max_graphemes: None, 446 + r#enum: None, 447 + r#const: None, 448 + known_values: None, 449 + }), 450 + ); 451 + map.insert( 452 + ::jacquard_common::smol_str::SmolStr::new_static("type"), 453 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 454 + description: None, 455 + format: None, 456 + default: None, 457 + min_length: None, 458 + max_length: None, 459 + min_graphemes: None, 460 + max_graphemes: None, 461 + r#enum: None, 462 + r#const: None, 463 + known_values: None, 464 + }), 465 + ); 466 + map 467 + }, 468 + }), 469 + ); 470 + map 471 + }, 472 + } 473 + } 474 + 475 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> { 476 + fn nsid() -> &'static str { 477 + "place.wisp.subfs" 478 + } 479 + fn def_name() -> &'static str { 480 + "directory" 481 + } 482 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 483 + lexicon_doc_place_wisp_subfs() 484 + } 485 + fn validate( 486 + &self, 487 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 488 + { 489 + let value = &self.entries; 490 + #[allow(unused_comparisons)] 491 + if value.len() > 500usize { 492 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 493 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 494 + "entries", 495 + ), 496 + max: 500usize, 497 + actual: value.len(), 498 + }); 499 + } 500 + } 501 + Ok(()) 502 + } 503 + } 504 + 505 + #[jacquard_derive::lexicon] 506 + #[derive( 507 + serde::Serialize, 508 + serde::Deserialize, 509 + Debug, 510 + Clone, 511 + PartialEq, 512 + Eq, 513 + jacquard_derive::IntoStatic 514 + )] 515 + #[serde(rename_all = "camelCase")] 516 + pub struct Entry<'a> { 517 + #[serde(borrow)] 518 + pub name: jacquard_common::CowStr<'a>, 519 + #[serde(borrow)] 520 + pub node: EntryNode<'a>, 521 + } 522 + 523 + pub mod entry_state { 524 + 525 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 526 + #[allow(unused)] 527 + use ::core::marker::PhantomData; 528 + mod sealed { 529 + pub trait Sealed {} 530 + } 531 + /// State trait tracking which required fields have been set 532 + pub trait State: sealed::Sealed { 533 + type Name; 534 + type Node; 535 + } 536 + /// Empty state - all required fields are unset 537 + pub struct Empty(()); 538 + impl sealed::Sealed for Empty {} 539 + impl State for Empty { 540 + type Name = Unset; 541 + type Node = Unset; 542 + } 543 + ///State transition - sets the `name` field to Set 544 + pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>); 545 + impl<S: State> sealed::Sealed for SetName<S> {} 546 + impl<S: State> State for SetName<S> { 547 + type Name = Set<members::name>; 548 + type Node = S::Node; 549 + } 550 + ///State transition - sets the `node` field to Set 551 + pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>); 552 + impl<S: State> sealed::Sealed for SetNode<S> {} 553 + impl<S: State> State for SetNode<S> { 554 + type Name = S::Name; 555 + type Node = Set<members::node>; 556 + } 557 + /// Marker types for field names 558 + #[allow(non_camel_case_types)] 559 + pub mod members { 560 + ///Marker type for the `name` field 561 + pub struct name(()); 562 + ///Marker type for the `node` field 563 + pub struct node(()); 564 + } 565 + } 566 + 567 + /// Builder for constructing an instance of this type 568 + pub struct EntryBuilder<'a, S: entry_state::State> { 569 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 570 + __unsafe_private_named: ( 571 + ::core::option::Option<jacquard_common::CowStr<'a>>, 572 + ::core::option::Option<EntryNode<'a>>, 573 + ), 574 + _phantom: ::core::marker::PhantomData<&'a ()>, 575 + } 576 + 577 + impl<'a> Entry<'a> { 578 + /// Create a new builder for this type 579 + pub fn new() -> EntryBuilder<'a, entry_state::Empty> { 580 + EntryBuilder::new() 581 + } 582 + } 583 + 584 + impl<'a> EntryBuilder<'a, entry_state::Empty> { 585 + /// Create a new builder with all fields unset 586 + pub fn new() -> Self { 587 + EntryBuilder { 588 + _phantom_state: ::core::marker::PhantomData, 589 + __unsafe_private_named: (None, None), 590 + _phantom: ::core::marker::PhantomData, 591 + } 592 + } 593 + } 594 + 595 + impl<'a, S> EntryBuilder<'a, S> 596 + where 597 + S: entry_state::State, 598 + S::Name: entry_state::IsUnset, 599 + { 600 + /// Set the `name` field (required) 601 + pub fn name( 602 + mut self, 603 + value: impl Into<jacquard_common::CowStr<'a>>, 604 + ) -> EntryBuilder<'a, entry_state::SetName<S>> { 605 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 606 + EntryBuilder { 607 + _phantom_state: ::core::marker::PhantomData, 608 + __unsafe_private_named: self.__unsafe_private_named, 609 + _phantom: ::core::marker::PhantomData, 610 + } 611 + } 612 + } 613 + 614 + impl<'a, S> EntryBuilder<'a, S> 615 + where 616 + S: entry_state::State, 617 + S::Node: entry_state::IsUnset, 618 + { 619 + /// Set the `node` field (required) 620 + pub fn node( 621 + mut self, 622 + value: impl Into<EntryNode<'a>>, 623 + ) -> EntryBuilder<'a, entry_state::SetNode<S>> { 624 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 625 + EntryBuilder { 626 + _phantom_state: ::core::marker::PhantomData, 627 + __unsafe_private_named: self.__unsafe_private_named, 628 + _phantom: ::core::marker::PhantomData, 629 + } 630 + } 631 + } 632 + 633 + impl<'a, S> EntryBuilder<'a, S> 634 + where 635 + S: entry_state::State, 636 + S::Name: entry_state::IsSet, 637 + S::Node: entry_state::IsSet, 638 + { 639 + /// Build the final struct 640 + pub fn build(self) -> Entry<'a> { 641 + Entry { 642 + name: self.__unsafe_private_named.0.unwrap(), 643 + node: self.__unsafe_private_named.1.unwrap(), 644 + extra_data: Default::default(), 645 + } 646 + } 647 + /// Build the final struct with custom extra_data 648 + pub fn build_with_data( 649 + self, 650 + extra_data: std::collections::BTreeMap< 651 + jacquard_common::smol_str::SmolStr, 652 + jacquard_common::types::value::Data<'a>, 653 + >, 654 + ) -> Entry<'a> { 655 + Entry { 656 + name: self.__unsafe_private_named.0.unwrap(), 657 + node: self.__unsafe_private_named.1.unwrap(), 658 + extra_data: Some(extra_data), 659 + } 660 + } 661 + } 662 + 663 + #[jacquard_derive::open_union] 664 + #[derive( 665 + serde::Serialize, 666 + serde::Deserialize, 667 + Debug, 668 + Clone, 669 + PartialEq, 670 + Eq, 671 + jacquard_derive::IntoStatic 672 + )] 673 + #[serde(tag = "$type")] 674 + #[serde(bound(deserialize = "'de: 'a"))] 675 + pub enum EntryNode<'a> { 676 + #[serde(rename = "place.wisp.subfs#file")] 677 + File(Box<crate::place_wisp::subfs::File<'a>>), 678 + #[serde(rename = "place.wisp.subfs#directory")] 679 + Directory(Box<crate::place_wisp::subfs::Directory<'a>>), 680 + #[serde(rename = "place.wisp.subfs#subfs")] 681 + Subfs(Box<crate::place_wisp::subfs::Subfs<'a>>), 682 + } 683 + 684 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> { 685 + fn nsid() -> &'static str { 686 + "place.wisp.subfs" 687 + } 688 + fn def_name() -> &'static str { 689 + "entry" 690 + } 691 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 692 + lexicon_doc_place_wisp_subfs() 693 + } 694 + fn validate( 695 + &self, 696 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 697 + { 698 + let value = &self.name; 699 + #[allow(unused_comparisons)] 700 + if <str>::len(value.as_ref()) > 255usize { 701 + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 702 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 703 + "name", 704 + ), 705 + max: 255usize, 706 + actual: <str>::len(value.as_ref()), 707 + }); 708 + } 709 + } 710 + Ok(()) 711 + } 712 + } 713 + 714 + #[jacquard_derive::lexicon] 715 + #[derive( 716 + serde::Serialize, 717 + serde::Deserialize, 718 + Debug, 719 + Clone, 720 + PartialEq, 721 + Eq, 722 + jacquard_derive::IntoStatic 723 + )] 724 + #[serde(rename_all = "camelCase")] 725 + pub struct File<'a> { 726 + /// True if blob content is base64-encoded (used to bypass PDS content sniffing) 727 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 728 + pub base64: std::option::Option<bool>, 729 + /// Content blob ref 730 + #[serde(borrow)] 731 + pub blob: jacquard_common::types::blob::BlobRef<'a>, 732 + /// Content encoding (e.g., gzip for compressed files) 733 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 734 + #[serde(borrow)] 735 + pub encoding: std::option::Option<jacquard_common::CowStr<'a>>, 736 + /// Original MIME type before compression 737 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 738 + #[serde(borrow)] 739 + pub mime_type: std::option::Option<jacquard_common::CowStr<'a>>, 740 + #[serde(borrow)] 741 + pub r#type: jacquard_common::CowStr<'a>, 742 + } 743 + 744 + pub mod file_state { 745 + 746 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 747 + #[allow(unused)] 748 + use ::core::marker::PhantomData; 749 + mod sealed { 750 + pub trait Sealed {} 751 + } 752 + /// State trait tracking which required fields have been set 753 + pub trait State: sealed::Sealed { 754 + type Blob; 755 + type Type; 756 + } 757 + /// Empty state - all required fields are unset 758 + pub struct Empty(()); 759 + impl sealed::Sealed for Empty {} 760 + impl State for Empty { 761 + type Blob = Unset; 762 + type Type = Unset; 763 + } 764 + ///State transition - sets the `blob` field to Set 765 + pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>); 766 + impl<S: State> sealed::Sealed for SetBlob<S> {} 767 + impl<S: State> State for SetBlob<S> { 768 + type Blob = Set<members::blob>; 769 + type Type = S::Type; 770 + } 771 + ///State transition - sets the `type` field to Set 772 + pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 773 + impl<S: State> sealed::Sealed for SetType<S> {} 774 + impl<S: State> State for SetType<S> { 775 + type Blob = S::Blob; 776 + type Type = Set<members::r#type>; 777 + } 778 + /// Marker types for field names 779 + #[allow(non_camel_case_types)] 780 + pub mod members { 781 + ///Marker type for the `blob` field 782 + pub struct blob(()); 783 + ///Marker type for the `type` field 784 + pub struct r#type(()); 785 + } 786 + } 787 + 788 + /// Builder for constructing an instance of this type 789 + pub struct FileBuilder<'a, S: file_state::State> { 790 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 791 + __unsafe_private_named: ( 792 + ::core::option::Option<bool>, 793 + ::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 794 + ::core::option::Option<jacquard_common::CowStr<'a>>, 795 + ::core::option::Option<jacquard_common::CowStr<'a>>, 796 + ::core::option::Option<jacquard_common::CowStr<'a>>, 797 + ), 798 + _phantom: ::core::marker::PhantomData<&'a ()>, 799 + } 800 + 801 + impl<'a> File<'a> { 802 + /// Create a new builder for this type 803 + pub fn new() -> FileBuilder<'a, file_state::Empty> { 804 + FileBuilder::new() 805 + } 806 + } 807 + 808 + impl<'a> FileBuilder<'a, file_state::Empty> { 809 + /// Create a new builder with all fields unset 810 + pub fn new() -> Self { 811 + FileBuilder { 812 + _phantom_state: ::core::marker::PhantomData, 813 + __unsafe_private_named: (None, None, None, None, None), 814 + _phantom: ::core::marker::PhantomData, 815 + } 816 + } 817 + } 818 + 819 + impl<'a, S: file_state::State> FileBuilder<'a, S> { 820 + /// Set the `base64` field (optional) 821 + pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self { 822 + self.__unsafe_private_named.0 = value.into(); 823 + self 824 + } 825 + /// Set the `base64` field to an Option value (optional) 826 + pub fn maybe_base64(mut self, value: Option<bool>) -> Self { 827 + self.__unsafe_private_named.0 = value; 828 + self 829 + } 830 + } 831 + 832 + impl<'a, S> FileBuilder<'a, S> 833 + where 834 + S: file_state::State, 835 + S::Blob: file_state::IsUnset, 836 + { 837 + /// Set the `blob` field (required) 838 + pub fn blob( 839 + mut self, 840 + value: impl Into<jacquard_common::types::blob::BlobRef<'a>>, 841 + ) -> FileBuilder<'a, file_state::SetBlob<S>> { 842 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 843 + FileBuilder { 844 + _phantom_state: ::core::marker::PhantomData, 845 + __unsafe_private_named: self.__unsafe_private_named, 846 + _phantom: ::core::marker::PhantomData, 847 + } 848 + } 849 + } 850 + 851 + impl<'a, S: file_state::State> FileBuilder<'a, S> { 852 + /// Set the `encoding` field (optional) 853 + pub fn encoding( 854 + mut self, 855 + value: impl Into<Option<jacquard_common::CowStr<'a>>>, 856 + ) -> Self { 857 + self.__unsafe_private_named.2 = value.into(); 858 + self 859 + } 860 + /// Set the `encoding` field to an Option value (optional) 861 + pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 862 + self.__unsafe_private_named.2 = value; 863 + self 864 + } 865 + } 866 + 867 + impl<'a, S: file_state::State> FileBuilder<'a, S> { 868 + /// Set the `mimeType` field (optional) 869 + pub fn mime_type( 870 + mut self, 871 + value: impl Into<Option<jacquard_common::CowStr<'a>>>, 872 + ) -> Self { 873 + self.__unsafe_private_named.3 = value.into(); 874 + self 875 + } 876 + /// Set the `mimeType` field to an Option value (optional) 877 + pub fn maybe_mime_type( 878 + mut self, 879 + value: Option<jacquard_common::CowStr<'a>>, 880 + ) -> Self { 881 + self.__unsafe_private_named.3 = value; 882 + self 883 + } 884 + } 885 + 886 + impl<'a, S> FileBuilder<'a, S> 887 + where 888 + S: file_state::State, 889 + S::Type: file_state::IsUnset, 890 + { 891 + /// Set the `type` field (required) 892 + pub fn r#type( 893 + mut self, 894 + value: impl Into<jacquard_common::CowStr<'a>>, 895 + ) -> FileBuilder<'a, file_state::SetType<S>> { 896 + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); 897 + FileBuilder { 898 + _phantom_state: ::core::marker::PhantomData, 899 + __unsafe_private_named: self.__unsafe_private_named, 900 + _phantom: ::core::marker::PhantomData, 901 + } 902 + } 903 + } 904 + 905 + impl<'a, S> FileBuilder<'a, S> 906 + where 907 + S: file_state::State, 908 + S::Blob: file_state::IsSet, 909 + S::Type: file_state::IsSet, 910 + { 911 + /// Build the final struct 912 + pub fn build(self) -> File<'a> { 913 + File { 914 + base64: self.__unsafe_private_named.0, 915 + blob: self.__unsafe_private_named.1.unwrap(), 916 + encoding: self.__unsafe_private_named.2, 917 + mime_type: self.__unsafe_private_named.3, 918 + r#type: self.__unsafe_private_named.4.unwrap(), 919 + extra_data: Default::default(), 920 + } 921 + } 922 + /// Build the final struct with custom extra_data 923 + pub fn build_with_data( 924 + self, 925 + extra_data: std::collections::BTreeMap< 926 + jacquard_common::smol_str::SmolStr, 927 + jacquard_common::types::value::Data<'a>, 928 + >, 929 + ) -> File<'a> { 930 + File { 931 + base64: self.__unsafe_private_named.0, 932 + blob: self.__unsafe_private_named.1.unwrap(), 933 + encoding: self.__unsafe_private_named.2, 934 + mime_type: self.__unsafe_private_named.3, 935 + r#type: self.__unsafe_private_named.4.unwrap(), 936 + extra_data: Some(extra_data), 937 + } 938 + } 939 + } 940 + 941 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> { 942 + fn nsid() -> &'static str { 943 + "place.wisp.subfs" 944 + } 945 + fn def_name() -> &'static str { 946 + "file" 947 + } 948 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 949 + lexicon_doc_place_wisp_subfs() 950 + } 951 + fn validate( 952 + &self, 953 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 954 + Ok(()) 955 + } 956 + } 957 + 958 + /// Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure. 959 + #[jacquard_derive::lexicon] 960 + #[derive( 961 + serde::Serialize, 962 + serde::Deserialize, 963 + Debug, 964 + Clone, 965 + PartialEq, 966 + Eq, 967 + jacquard_derive::IntoStatic 968 + )] 969 + #[serde(rename_all = "camelCase")] 970 + pub struct SubfsRecord<'a> { 971 + pub created_at: jacquard_common::types::string::Datetime, 972 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 973 + pub file_count: std::option::Option<i64>, 974 + #[serde(borrow)] 975 + pub root: crate::place_wisp::subfs::Directory<'a>, 976 + } 977 + 978 + pub mod subfs_record_state { 979 + 980 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 981 + #[allow(unused)] 982 + use ::core::marker::PhantomData; 983 + mod sealed { 984 + pub trait Sealed {} 985 + } 986 + /// State trait tracking which required fields have been set 987 + pub trait State: sealed::Sealed { 988 + type Root; 989 + type CreatedAt; 990 + } 991 + /// Empty state - all required fields are unset 992 + pub struct Empty(()); 993 + impl sealed::Sealed for Empty {} 994 + impl State for Empty { 995 + type Root = Unset; 996 + type CreatedAt = Unset; 997 + } 998 + ///State transition - sets the `root` field to Set 999 + pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>); 1000 + impl<S: State> sealed::Sealed for SetRoot<S> {} 1001 + impl<S: State> State for SetRoot<S> { 1002 + type Root = Set<members::root>; 1003 + type CreatedAt = S::CreatedAt; 1004 + } 1005 + ///State transition - sets the `created_at` field to Set 1006 + pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>); 1007 + impl<S: State> sealed::Sealed for SetCreatedAt<S> {} 1008 + impl<S: State> State for SetCreatedAt<S> { 1009 + type Root = S::Root; 1010 + type CreatedAt = Set<members::created_at>; 1011 + } 1012 + /// Marker types for field names 1013 + #[allow(non_camel_case_types)] 1014 + pub mod members { 1015 + ///Marker type for the `root` field 1016 + pub struct root(()); 1017 + ///Marker type for the `created_at` field 1018 + pub struct created_at(()); 1019 + } 1020 + } 1021 + 1022 + /// Builder for constructing an instance of this type 1023 + pub struct SubfsRecordBuilder<'a, S: subfs_record_state::State> { 1024 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1025 + __unsafe_private_named: ( 1026 + ::core::option::Option<jacquard_common::types::string::Datetime>, 1027 + ::core::option::Option<i64>, 1028 + ::core::option::Option<crate::place_wisp::subfs::Directory<'a>>, 1029 + ), 1030 + _phantom: ::core::marker::PhantomData<&'a ()>, 1031 + } 1032 + 1033 + impl<'a> SubfsRecord<'a> { 1034 + /// Create a new builder for this type 1035 + pub fn new() -> SubfsRecordBuilder<'a, subfs_record_state::Empty> { 1036 + SubfsRecordBuilder::new() 1037 + } 1038 + } 1039 + 1040 + impl<'a> SubfsRecordBuilder<'a, subfs_record_state::Empty> { 1041 + /// Create a new builder with all fields unset 1042 + pub fn new() -> Self { 1043 + SubfsRecordBuilder { 1044 + _phantom_state: ::core::marker::PhantomData, 1045 + __unsafe_private_named: (None, None, None), 1046 + _phantom: ::core::marker::PhantomData, 1047 + } 1048 + } 1049 + } 1050 + 1051 + impl<'a, S> SubfsRecordBuilder<'a, S> 1052 + where 1053 + S: subfs_record_state::State, 1054 + S::CreatedAt: subfs_record_state::IsUnset, 1055 + { 1056 + /// Set the `createdAt` field (required) 1057 + pub fn created_at( 1058 + mut self, 1059 + value: impl Into<jacquard_common::types::string::Datetime>, 1060 + ) -> SubfsRecordBuilder<'a, subfs_record_state::SetCreatedAt<S>> { 1061 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 1062 + SubfsRecordBuilder { 1063 + _phantom_state: ::core::marker::PhantomData, 1064 + __unsafe_private_named: self.__unsafe_private_named, 1065 + _phantom: ::core::marker::PhantomData, 1066 + } 1067 + } 1068 + } 1069 + 1070 + impl<'a, S: subfs_record_state::State> SubfsRecordBuilder<'a, S> { 1071 + /// Set the `fileCount` field (optional) 1072 + pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self { 1073 + self.__unsafe_private_named.1 = value.into(); 1074 + self 1075 + } 1076 + /// Set the `fileCount` field to an Option value (optional) 1077 + pub fn maybe_file_count(mut self, value: Option<i64>) -> Self { 1078 + self.__unsafe_private_named.1 = value; 1079 + self 1080 + } 1081 + } 1082 + 1083 + impl<'a, S> SubfsRecordBuilder<'a, S> 1084 + where 1085 + S: subfs_record_state::State, 1086 + S::Root: subfs_record_state::IsUnset, 1087 + { 1088 + /// Set the `root` field (required) 1089 + pub fn root( 1090 + mut self, 1091 + value: impl Into<crate::place_wisp::subfs::Directory<'a>>, 1092 + ) -> SubfsRecordBuilder<'a, subfs_record_state::SetRoot<S>> { 1093 + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); 1094 + SubfsRecordBuilder { 1095 + _phantom_state: ::core::marker::PhantomData, 1096 + __unsafe_private_named: self.__unsafe_private_named, 1097 + _phantom: ::core::marker::PhantomData, 1098 + } 1099 + } 1100 + } 1101 + 1102 + impl<'a, S> SubfsRecordBuilder<'a, S> 1103 + where 1104 + S: subfs_record_state::State, 1105 + S::Root: subfs_record_state::IsSet, 1106 + S::CreatedAt: subfs_record_state::IsSet, 1107 + { 1108 + /// Build the final struct 1109 + pub fn build(self) -> SubfsRecord<'a> { 1110 + SubfsRecord { 1111 + created_at: self.__unsafe_private_named.0.unwrap(), 1112 + file_count: self.__unsafe_private_named.1, 1113 + root: self.__unsafe_private_named.2.unwrap(), 1114 + extra_data: Default::default(), 1115 + } 1116 + } 1117 + /// Build the final struct with custom extra_data 1118 + pub fn build_with_data( 1119 + self, 1120 + extra_data: std::collections::BTreeMap< 1121 + jacquard_common::smol_str::SmolStr, 1122 + jacquard_common::types::value::Data<'a>, 1123 + >, 1124 + ) -> SubfsRecord<'a> { 1125 + SubfsRecord { 1126 + created_at: self.__unsafe_private_named.0.unwrap(), 1127 + file_count: self.__unsafe_private_named.1, 1128 + root: self.__unsafe_private_named.2.unwrap(), 1129 + extra_data: Some(extra_data), 1130 + } 1131 + } 1132 + } 1133 + 1134 + impl<'a> SubfsRecord<'a> { 1135 + pub fn uri( 1136 + uri: impl Into<jacquard_common::CowStr<'a>>, 1137 + ) -> Result< 1138 + jacquard_common::types::uri::RecordUri<'a, SubfsRecordRecord>, 1139 + jacquard_common::types::uri::UriError, 1140 + > { 1141 + jacquard_common::types::uri::RecordUri::try_from_uri( 1142 + jacquard_common::types::string::AtUri::new_cow(uri.into())?, 1143 + ) 1144 + } 1145 + } 1146 + 1147 + /// Typed wrapper for GetRecord response with this collection's record type. 1148 + #[derive( 1149 + serde::Serialize, 1150 + serde::Deserialize, 1151 + Debug, 1152 + Clone, 1153 + PartialEq, 1154 + Eq, 1155 + jacquard_derive::IntoStatic 1156 + )] 1157 + #[serde(rename_all = "camelCase")] 1158 + pub struct SubfsRecordGetRecordOutput<'a> { 1159 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1160 + #[serde(borrow)] 1161 + pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 1162 + #[serde(borrow)] 1163 + pub uri: jacquard_common::types::string::AtUri<'a>, 1164 + #[serde(borrow)] 1165 + pub value: SubfsRecord<'a>, 1166 + } 1167 + 1168 + impl From<SubfsRecordGetRecordOutput<'_>> for SubfsRecord<'_> { 1169 + fn from(output: SubfsRecordGetRecordOutput<'_>) -> Self { 1170 + use jacquard_common::IntoStatic; 1171 + output.value.into_static() 1172 + } 1173 + } 1174 + 1175 + impl jacquard_common::types::collection::Collection for SubfsRecord<'_> { 1176 + const NSID: &'static str = "place.wisp.subfs"; 1177 + type Record = SubfsRecordRecord; 1178 + } 1179 + 1180 + /// Marker type for deserializing records from this collection. 1181 + #[derive(Debug, serde::Serialize, serde::Deserialize)] 1182 + pub struct SubfsRecordRecord; 1183 + impl jacquard_common::xrpc::XrpcResp for SubfsRecordRecord { 1184 + const NSID: &'static str = "place.wisp.subfs"; 1185 + const ENCODING: &'static str = "application/json"; 1186 + type Output<'de> = SubfsRecordGetRecordOutput<'de>; 1187 + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; 1188 + } 1189 + 1190 + impl jacquard_common::types::collection::Collection for SubfsRecordRecord { 1191 + const NSID: &'static str = "place.wisp.subfs"; 1192 + type Record = SubfsRecordRecord; 1193 + } 1194 + 1195 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SubfsRecord<'a> { 1196 + fn nsid() -> &'static str { 1197 + "place.wisp.subfs" 1198 + } 1199 + fn def_name() -> &'static str { 1200 + "main" 1201 + } 1202 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1203 + lexicon_doc_place_wisp_subfs() 1204 + } 1205 + fn validate( 1206 + &self, 1207 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1208 + if let Some(ref value) = self.file_count { 1209 + if *value > 1000i64 { 1210 + return Err(::jacquard_lexicon::validation::ConstraintError::Maximum { 1211 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1212 + "file_count", 1213 + ), 1214 + max: 1000i64, 1215 + actual: *value, 1216 + }); 1217 + } 1218 + } 1219 + if let Some(ref value) = self.file_count { 1220 + if *value < 0i64 { 1221 + return Err(::jacquard_lexicon::validation::ConstraintError::Minimum { 1222 + path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1223 + "file_count", 1224 + ), 1225 + min: 0i64, 1226 + actual: *value, 1227 + }); 1228 + } 1229 + } 1230 + Ok(()) 1231 + } 1232 + } 1233 + 1234 + #[jacquard_derive::lexicon] 1235 + #[derive( 1236 + serde::Serialize, 1237 + serde::Deserialize, 1238 + Debug, 1239 + Clone, 1240 + PartialEq, 1241 + Eq, 1242 + jacquard_derive::IntoStatic 1243 + )] 1244 + #[serde(rename_all = "camelCase")] 1245 + pub struct Subfs<'a> { 1246 + /// AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures. 1247 + #[serde(borrow)] 1248 + pub subject: jacquard_common::types::string::AtUri<'a>, 1249 + #[serde(borrow)] 1250 + pub r#type: jacquard_common::CowStr<'a>, 1251 + } 1252 + 1253 + pub mod subfs_state { 1254 + 1255 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 1256 + #[allow(unused)] 1257 + use ::core::marker::PhantomData; 1258 + mod sealed { 1259 + pub trait Sealed {} 1260 + } 1261 + /// State trait tracking which required fields have been set 1262 + pub trait State: sealed::Sealed { 1263 + type Subject; 1264 + type Type; 1265 + } 1266 + /// Empty state - all required fields are unset 1267 + pub struct Empty(()); 1268 + impl sealed::Sealed for Empty {} 1269 + impl State for Empty { 1270 + type Subject = Unset; 1271 + type Type = Unset; 1272 + } 1273 + ///State transition - sets the `subject` field to Set 1274 + pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>); 1275 + impl<S: State> sealed::Sealed for SetSubject<S> {} 1276 + impl<S: State> State for SetSubject<S> { 1277 + type Subject = Set<members::subject>; 1278 + type Type = S::Type; 1279 + } 1280 + ///State transition - sets the `type` field to Set 1281 + pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 1282 + impl<S: State> sealed::Sealed for SetType<S> {} 1283 + impl<S: State> State for SetType<S> { 1284 + type Subject = S::Subject; 1285 + type Type = Set<members::r#type>; 1286 + } 1287 + /// Marker types for field names 1288 + #[allow(non_camel_case_types)] 1289 + pub mod members { 1290 + ///Marker type for the `subject` field 1291 + pub struct subject(()); 1292 + ///Marker type for the `type` field 1293 + pub struct r#type(()); 1294 + } 1295 + } 1296 + 1297 + /// Builder for constructing an instance of this type 1298 + pub struct SubfsBuilder<'a, S: subfs_state::State> { 1299 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1300 + __unsafe_private_named: ( 1301 + ::core::option::Option<jacquard_common::types::string::AtUri<'a>>, 1302 + ::core::option::Option<jacquard_common::CowStr<'a>>, 1303 + ), 1304 + _phantom: ::core::marker::PhantomData<&'a ()>, 1305 + } 1306 + 1307 + impl<'a> Subfs<'a> { 1308 + /// Create a new builder for this type 1309 + pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> { 1310 + SubfsBuilder::new() 1311 + } 1312 + } 1313 + 1314 + impl<'a> SubfsBuilder<'a, subfs_state::Empty> { 1315 + /// Create a new builder with all fields unset 1316 + pub fn new() -> Self { 1317 + SubfsBuilder { 1318 + _phantom_state: ::core::marker::PhantomData, 1319 + __unsafe_private_named: (None, None), 1320 + _phantom: ::core::marker::PhantomData, 1321 + } 1322 + } 1323 + } 1324 + 1325 + impl<'a, S> SubfsBuilder<'a, S> 1326 + where 1327 + S: subfs_state::State, 1328 + S::Subject: subfs_state::IsUnset, 1329 + { 1330 + /// Set the `subject` field (required) 1331 + pub fn subject( 1332 + mut self, 1333 + value: impl Into<jacquard_common::types::string::AtUri<'a>>, 1334 + ) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> { 1335 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 1336 + SubfsBuilder { 1337 + _phantom_state: ::core::marker::PhantomData, 1338 + __unsafe_private_named: self.__unsafe_private_named, 1339 + _phantom: ::core::marker::PhantomData, 1340 + } 1341 + } 1342 + } 1343 + 1344 + impl<'a, S> SubfsBuilder<'a, S> 1345 + where 1346 + S: subfs_state::State, 1347 + S::Type: subfs_state::IsUnset, 1348 + { 1349 + /// Set the `type` field (required) 1350 + pub fn r#type( 1351 + mut self, 1352 + value: impl Into<jacquard_common::CowStr<'a>>, 1353 + ) -> SubfsBuilder<'a, subfs_state::SetType<S>> { 1354 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 1355 + SubfsBuilder { 1356 + _phantom_state: ::core::marker::PhantomData, 1357 + __unsafe_private_named: self.__unsafe_private_named, 1358 + _phantom: ::core::marker::PhantomData, 1359 + } 1360 + } 1361 + } 1362 + 1363 + impl<'a, S> SubfsBuilder<'a, S> 1364 + where 1365 + S: subfs_state::State, 1366 + S::Subject: subfs_state::IsSet, 1367 + S::Type: subfs_state::IsSet, 1368 + { 1369 + /// Build the final struct 1370 + pub fn build(self) -> Subfs<'a> { 1371 + Subfs { 1372 + subject: self.__unsafe_private_named.0.unwrap(), 1373 + r#type: self.__unsafe_private_named.1.unwrap(), 1374 + extra_data: Default::default(), 1375 + } 1376 + } 1377 + /// Build the final struct with custom extra_data 1378 + pub fn build_with_data( 1379 + self, 1380 + extra_data: std::collections::BTreeMap< 1381 + jacquard_common::smol_str::SmolStr, 1382 + jacquard_common::types::value::Data<'a>, 1383 + >, 1384 + ) -> Subfs<'a> { 1385 + Subfs { 1386 + subject: self.__unsafe_private_named.0.unwrap(), 1387 + r#type: self.__unsafe_private_named.1.unwrap(), 1388 + extra_data: Some(extra_data), 1389 + } 1390 + } 1391 + } 1392 + 1393 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> { 1394 + fn nsid() -> &'static str { 1395 + "place.wisp.subfs" 1396 + } 1397 + fn def_name() -> &'static str { 1398 + "subfs" 1399 + } 1400 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1401 + lexicon_doc_place_wisp_subfs() 1402 + } 1403 + fn validate( 1404 + &self, 1405 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1406 + Ok(()) 1407 + } 1408 + }
+8
cli/crates/lexicons/src/place_wisp.rs
···
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + pub mod fs; 7 + pub mod settings; 8 + pub mod subfs;
+16
cli/default.nix
···
··· 1 + { 2 + rustPlatform, 3 + glibc, 4 + }: 5 + rustPlatform.buildRustPackage { 6 + name = "wisp-cli"; 7 + src = ./.; 8 + cargoLock = { 9 + lockFile = ./Cargo.lock; 10 + outputHashes = { 11 + "jacquard-0.9.5" = "sha256-75bas4VAYFcZAcBspSqS4vlJe8nmFn9ncTgeoT/OvnA="; 12 + }; 13 + }; 14 + buildInputs = [glibc.static]; 15 + RUSTFLAGS = ["-C" "target-feature=+crt-static"]; 16 + }
+96
cli/flake.lock
···
··· 1 + { 2 + "nodes": { 3 + "flake-utils": { 4 + "inputs": { 5 + "systems": "systems" 6 + }, 7 + "locked": { 8 + "lastModified": 1731533236, 9 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 + "owner": "numtide", 11 + "repo": "flake-utils", 12 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 + "type": "github" 14 + }, 15 + "original": { 16 + "owner": "numtide", 17 + "repo": "flake-utils", 18 + "type": "github" 19 + } 20 + }, 21 + "nixpkgs": { 22 + "locked": { 23 + "lastModified": 1767640445, 24 + "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", 25 + "owner": "NixOS", 26 + "repo": "nixpkgs", 27 + "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", 28 + "type": "github" 29 + }, 30 + "original": { 31 + "owner": "NixOS", 32 + "ref": "nixos-unstable", 33 + "repo": "nixpkgs", 34 + "type": "github" 35 + } 36 + }, 37 + "nixpkgs_2": { 38 + "locked": { 39 + "lastModified": 1744536153, 40 + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", 41 + "owner": "NixOS", 42 + "repo": "nixpkgs", 43 + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", 44 + "type": "github" 45 + }, 46 + "original": { 47 + "owner": "NixOS", 48 + "ref": "nixpkgs-unstable", 49 + "repo": "nixpkgs", 50 + "type": "github" 51 + } 52 + }, 53 + "root": { 54 + "inputs": { 55 + "flake-utils": "flake-utils", 56 + "nixpkgs": "nixpkgs", 57 + "rust-overlay": "rust-overlay" 58 + } 59 + }, 60 + "rust-overlay": { 61 + "inputs": { 62 + "nixpkgs": "nixpkgs_2" 63 + }, 64 + "locked": { 65 + "lastModified": 1767667566, 66 + "narHash": "sha256-COy+yxZGuhQRVD1r4bWVgeFt1GB+IB1k5WRpDKbLfI8=", 67 + "owner": "oxalica", 68 + "repo": "rust-overlay", 69 + "rev": "056ce5b125ab32ffe78c7d3e394d9da44733c95e", 70 + "type": "github" 71 + }, 72 + "original": { 73 + "owner": "oxalica", 74 + "repo": "rust-overlay", 75 + "type": "github" 76 + } 77 + }, 78 + "systems": { 79 + "locked": { 80 + "lastModified": 1681028828, 81 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 + "owner": "nix-systems", 83 + "repo": "default", 84 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 + "type": "github" 86 + }, 87 + "original": { 88 + "owner": "nix-systems", 89 + "repo": "default", 90 + "type": "github" 91 + } 92 + } 93 + }, 94 + "root": "root", 95 + "version": 7 96 + }
+136
cli/flake.nix
···
··· 1 + { 2 + inputs = { 3 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 + rust-overlay.url = "github:oxalica/rust-overlay"; 5 + flake-utils.url = "github:numtide/flake-utils"; 6 + }; 7 + 8 + outputs = { self, nixpkgs, rust-overlay, flake-utils }: 9 + flake-utils.lib.eachDefaultSystem (system: 10 + let 11 + overlays = [ (import rust-overlay) ]; 12 + pkgs = import nixpkgs { inherit system overlays; }; 13 + 14 + rustToolchain = pkgs.rust-bin.stable.latest.default.override { 15 + targets = [ 16 + "x86_64-unknown-linux-musl" 17 + "aarch64-unknown-linux-musl" 18 + "x86_64-apple-darwin" 19 + "aarch64-apple-darwin" 20 + ]; 21 + }; 22 + 23 + cargoLockConfig = { 24 + lockFile = ./Cargo.lock; 25 + outputHashes = { 26 + "jacquard-0.9.5" = "sha256-75bas4VAYFcZAcBspSqS4vlJe8nmFn9ncTgeoT/OvnA="; 27 + }; 28 + }; 29 + 30 + # Native build for current system 31 + native = pkgs.rustPlatform.buildRustPackage { 32 + pname = "wisp-cli"; 33 + version = "0.4.2"; 34 + src = ./.; 35 + cargoLock = cargoLockConfig; 36 + nativeBuildInputs = [ rustToolchain ]; 37 + }; 38 + 39 + # Cross-compilation targets (Linux from macOS needs zigbuild) 40 + linuxTargets = let 41 + zigbuildPkgs = pkgs; 42 + in { 43 + x86_64-linux = pkgs.stdenv.mkDerivation { 44 + pname = "wisp-cli-x86_64-linux"; 45 + version = "0.4.2"; 46 + src = ./.; 47 + 48 + nativeBuildInputs = [ 49 + rustToolchain 50 + pkgs.cargo-zigbuild 51 + pkgs.zig 52 + ]; 53 + 54 + buildPhase = '' 55 + export HOME=$(mktemp -d) 56 + cargo zigbuild --release --target x86_64-unknown-linux-musl 57 + ''; 58 + 59 + installPhase = '' 60 + mkdir -p $out/bin 61 + cp target/x86_64-unknown-linux-musl/release/wisp-cli $out/bin/ 62 + ''; 63 + 64 + # Skip Nix's cargo vendor - we use network 65 + dontConfigure = true; 66 + dontFixup = true; 67 + }; 68 + 69 + aarch64-linux = pkgs.stdenv.mkDerivation { 70 + pname = "wisp-cli-aarch64-linux"; 71 + version = "0.4.2"; 72 + src = ./.; 73 + 74 + nativeBuildInputs = [ 75 + rustToolchain 76 + pkgs.cargo-zigbuild 77 + pkgs.zig 78 + ]; 79 + 80 + buildPhase = '' 81 + export HOME=$(mktemp -d) 82 + cargo zigbuild --release --target aarch64-unknown-linux-musl 83 + ''; 84 + 85 + installPhase = '' 86 + mkdir -p $out/bin 87 + cp target/aarch64-unknown-linux-musl/release/wisp-cli $out/bin/ 88 + ''; 89 + 90 + dontConfigure = true; 91 + dontFixup = true; 92 + }; 93 + }; 94 + 95 + in { 96 + packages = { 97 + default = native; 98 + inherit native; 99 + 100 + # macOS universal binary 101 + macos-universal = pkgs.stdenv.mkDerivation { 102 + pname = "wisp-cli-macos-universal"; 103 + version = "0.4.2"; 104 + src = ./.; 105 + 106 + nativeBuildInputs = [ rustToolchain pkgs.darwin.lipo ]; 107 + 108 + buildPhase = '' 109 + export HOME=$(mktemp -d) 110 + cargo build --release --target aarch64-apple-darwin 111 + cargo build --release --target x86_64-apple-darwin 112 + ''; 113 + 114 + installPhase = '' 115 + mkdir -p $out/bin 116 + lipo -create \ 117 + target/aarch64-apple-darwin/release/wisp-cli \ 118 + target/x86_64-apple-darwin/release/wisp-cli \ 119 + -output $out/bin/wisp-cli 120 + ''; 121 + 122 + dontConfigure = true; 123 + dontFixup = true; 124 + }; 125 + } // (if pkgs.stdenv.isDarwin then linuxTargets else {}); 126 + 127 + devShells.default = pkgs.mkShell { 128 + buildInputs = [ 129 + rustToolchain 130 + pkgs.cargo-zigbuild 131 + pkgs.zig 132 + ]; 133 + }; 134 + } 135 + ); 136 + }
+1 -1
cli/src/blob_map.rs
··· 2 use jacquard_common::IntoStatic; 3 use std::collections::HashMap; 4 5 - use crate::place_wisp::fs::{Directory, EntryNode}; 6 7 /// Extract blob information from a directory tree 8 /// Returns a map of file paths to their blob refs and CIDs
··· 2 use jacquard_common::IntoStatic; 3 use std::collections::HashMap; 4 5 + use wisp_lexicons::place_wisp::fs::{Directory, EntryNode}; 6 7 /// Extract blob information from a directory tree 8 /// Returns a map of file paths to their blob refs and CIDs
-43
cli/src/builder_types.rs
··· 1 - // @generated by jacquard-lexicon. DO NOT EDIT. 2 - // 3 - // This file was automatically generated from Lexicon schemas. 4 - // Any manual changes will be overwritten on the next regeneration. 5 - 6 - /// Marker type indicating a builder field has been set 7 - pub struct Set<T>(pub T); 8 - impl<T> Set<T> { 9 - /// Extract the inner value 10 - #[inline] 11 - pub fn into_inner(self) -> T { 12 - self.0 13 - } 14 - } 15 - 16 - /// Marker type indicating a builder field has not been set 17 - pub struct Unset; 18 - /// Trait indicating a builder field is set (has a value) 19 - #[rustversion::attr( 20 - since(1.78.0), 21 - diagnostic::on_unimplemented( 22 - message = "the field `{Self}` was not set, but this method requires it to be set", 23 - label = "the field `{Self}` was not set" 24 - ) 25 - )] 26 - pub trait IsSet: private::Sealed {} 27 - /// Trait indicating a builder field is unset (no value yet) 28 - #[rustversion::attr( 29 - since(1.78.0), 30 - diagnostic::on_unimplemented( 31 - message = "the field `{Self}` was already set, but this method requires it to be unset", 32 - label = "the field `{Self}` was already set" 33 - ) 34 - )] 35 - pub trait IsUnset: private::Sealed {} 36 - impl<T> IsSet for Set<T> {} 37 - impl IsUnset for Unset {} 38 - mod private { 39 - /// Sealed trait to prevent external implementations 40 - pub trait Sealed {} 41 - impl<T> Sealed for super::Set<T> {} 42 - impl Sealed for super::Unset {} 43 - }
···
-9
cli/src/lib.rs
··· 1 - // @generated by jacquard-lexicon. DO NOT EDIT. 2 - // 3 - // This file was automatically generated from Lexicon schemas. 4 - // Any manual changes will be overwritten on the next regeneration. 5 - 6 - pub mod builder_types; 7 - 8 - #[cfg(feature = "place_wisp")] 9 - pub mod place_wisp;
···
+190 -54
cli/src/main.rs
··· 1 - mod builder_types; 2 - mod place_wisp; 3 mod cid; 4 mod blob_map; 5 mod metadata; ··· 26 use std::io::Write; 27 use base64::Engine; 28 use futures::stream::{self, StreamExt}; 29 30 - use place_wisp::fs::*; 31 - use place_wisp::settings::*; 32 33 #[derive(Parser, Debug)] 34 #[command(author, version, about = "wisp.place CLI tool")] 35 struct Args { 36 #[command(subcommand)] 37 command: Option<Commands>, 38 - 39 // Deploy arguments (when no subcommand is specified) 40 /// Handle (e.g., alice.bsky.social), DID, or PDS URL 41 - #[arg(global = true, conflicts_with = "command")] 42 input: Option<CowStr<'static>>, 43 44 /// Path to the directory containing your static site 45 - #[arg(short, long, global = true, conflicts_with = "command")] 46 path: Option<PathBuf>, 47 48 /// Site name (defaults to directory name) 49 - #[arg(short, long, global = true, conflicts_with = "command")] 50 site: Option<String>, 51 52 /// Path to auth store file 53 - #[arg(long, global = true, conflicts_with = "command")] 54 store: Option<String>, 55 56 /// App Password for authentication 57 - #[arg(long, global = true, conflicts_with = "command")] 58 password: Option<CowStr<'static>>, 59 60 /// Enable directory listing mode for paths without index files 61 - #[arg(long, global = true, conflicts_with = "command")] 62 directory: bool, 63 64 /// Enable SPA mode (serve index.html for all routes) 65 - #[arg(long, global = true, conflicts_with = "command")] 66 spa: bool, 67 } 68 69 #[derive(Subcommand, Debug)] ··· 96 /// Enable SPA mode (serve index.html for all routes) 97 #[arg(long)] 98 spa: bool, 99 }, 100 /// Pull a site from the PDS to a local directory 101 Pull { ··· 108 109 /// Output directory for the downloaded site 110 #[arg(short, long, default_value = ".")] 111 - output: PathBuf, 112 }, 113 /// Serve a site locally with real-time firehose updates 114 Serve { ··· 121 122 /// Output directory for the site files 123 #[arg(short, long, default_value = ".")] 124 - output: PathBuf, 125 126 /// Port to serve on 127 - #[arg(short, long, default_value = "8080")] 128 port: u16, 129 }, 130 } ··· 134 let args = Args::parse(); 135 136 let result = match args.command { 137 - Some(Commands::Deploy { input, path, site, store, password, directory, spa }) => { 138 // Dispatch to appropriate authentication method 139 if let Some(password) = password { 140 - run_with_app_password(input, password, path, site, directory, spa).await 141 } else { 142 - run_with_oauth(input, store, path, site, directory, spa).await 143 } 144 } 145 - Some(Commands::Pull { input, site, output }) => { 146 - pull::pull_site(input, CowStr::from(site), output).await 147 } 148 - Some(Commands::Serve { input, site, output, port }) => { 149 - serve::serve_site(input, CowStr::from(site), output, port).await 150 } 151 None => { 152 // Legacy mode: if input is provided, assume deploy command ··· 156 157 // Dispatch to appropriate authentication method 158 if let Some(password) = args.password { 159 - run_with_app_password(input, password, path, args.site, args.directory, args.spa).await 160 } else { 161 - run_with_oauth(input, store, path, args.site, args.directory, args.spa).await 162 } 163 } else { 164 // No command and no input, show help ··· 187 site: Option<String>, 188 directory: bool, 189 spa: bool, 190 ) -> miette::Result<()> { 191 let (session, auth) = 192 MemoryCredentialSession::authenticated(input, password, None, None).await?; 193 println!("Signed in as {}", auth.handle); 194 195 let agent: Agent<_> = Agent::from(session); 196 - deploy_site(&agent, path, site, directory, spa).await 197 } 198 199 /// Run deployment with OAuth authentication ··· 204 site: Option<String>, 205 directory: bool, 206 spa: bool, 207 ) -> miette::Result<()> { 208 use jacquard::oauth::scopes::Scope; 209 use jacquard::oauth::atproto::AtprotoClientMetadata; ··· 236 .await?; 237 238 let agent: Agent<_> = Agent::from(session); 239 - deploy_site(&agent, path, site, directory, spa).await 240 } 241 242 /// Deploy the site using the provided agent ··· 246 site: Option<String>, 247 directory_listing: bool, 248 spa_mode: bool, 249 ) -> miette::Result<()> { 250 // Verify the path exists 251 if !path.exists() { ··· 263 264 println!("Deploying site '{}'...", site_name); 265 266 // Try to fetch existing manifest for incremental updates 267 let (existing_blob_map, old_subfs_uris): (HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, Vec<(String, String)>) = { 268 use jacquard_common::types::string::AtUri; ··· 324 } 325 }; 326 327 - // Build directory tree with ignore patterns 328 - let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?; 329 - let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher).await?; 330 let uploaded_count = total_files - reused_count; 331 332 // Check if we need to split into subfs records 333 const MAX_MANIFEST_SIZE: usize = 140 * 1024; // 140KB (PDS limit is 150KB) ··· 378 let chunk_file_count = subfs_utils::count_files_in_directory(chunk); 379 let chunk_size = subfs_utils::estimate_directory_size(chunk); 380 381 - let chunk_manifest = crate::place_wisp::subfs::SubfsRecord::new() 382 .root(convert_fs_dir_to_subfs_dir(chunk.clone())) 383 .file_count(Some(chunk_file_count as i64)) 384 .created_at(Datetime::now()) ··· 401 // Each chunk reference MUST have flat: true to merge chunk contents 402 println!(" โ†’ Creating parent subfs with {} chunk references...", chunk_uris.len()); 403 use jacquard_common::CowStr; 404 - use crate::place_wisp::fs::{Subfs}; 405 406 // Convert to fs::Subfs (which has the 'flat' field) instead of subfs::Subfs 407 let parent_entries_fs: Vec<Entry> = chunk_uris.iter().enumerate().map(|(i, (uri, _))| { ··· 431 let parent_tid = Tid::now_0(); 432 let parent_rkey = parent_tid.to_string(); 433 434 - let parent_manifest = crate::place_wisp::subfs::SubfsRecord::new() 435 .root(parent_root_subfs) 436 .file_count(Some(largest_dir.file_count as i64)) 437 .created_at(Datetime::now()) ··· 450 let subfs_tid = Tid::now_0(); 451 let subfs_rkey = subfs_tid.to_string(); 452 453 - let subfs_manifest = crate::place_wisp::subfs::SubfsRecord::new() 454 .root(convert_fs_dir_to_subfs_dir(largest_dir.directory.clone())) 455 .file_count(Some(largest_dir.file_count as i64)) 456 .created_at(Datetime::now()) ··· 606 existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 607 current_path: String, 608 ignore_matcher: &'a ignore_patterns::IgnoreMatcher, 609 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>> 610 { 611 Box::pin(async move { ··· 653 } 654 } 655 656 - // Process files concurrently with a limit of 5 657 let file_results: Vec<(Entry<'static>, bool)> = stream::iter(file_tasks) 658 .map(|(name, path, full_path)| async move { 659 - let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs).await?; 660 let entry = Entry::new() 661 .name(CowStr::from(name)) 662 .node(EntryNode::File(Box::new(file_node))) 663 .build(); 664 Ok::<_, miette::Report>((entry, reused)) 665 }) 666 - .buffer_unordered(5) 667 .collect::<Vec<_>>() 668 .await 669 .into_iter() ··· 690 } else { 691 format!("{}/{}", current_path, name) 692 }; 693 - let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher).await?; 694 dir_entries.push(Entry::new() 695 .name(CowStr::from(name)) 696 .node(EntryNode::Directory(Box::new(subdir))) ··· 722 file_path: &Path, 723 file_path_key: &str, 724 existing_blobs: &HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 725 ) -> miette::Result<(File<'static>, bool)> 726 { 727 // Read file ··· 761 if let Some((existing_blob_ref, existing_cid)) = existing_blob { 762 if existing_cid == &file_cid { 763 // CIDs match - reuse existing blob 764 - println!(" โœ“ Reusing blob for {} (CID: {})", file_path_key, file_cid); 765 let mut file_builder = File::new() 766 .r#type(CowStr::from("file")) 767 .blob(existing_blob_ref.clone()) ··· 775 } 776 777 return Ok((file_builder.build(), true)); 778 - } else { 779 - // CID mismatch - file changed 780 - println!(" โ†’ File changed: {} (old CID: {}, new CID: {})", file_path_key, existing_cid, file_cid); 781 - } 782 - } else { 783 - // File not in existing blob map 784 - if file_path_key.starts_with("imgs/") { 785 - println!(" โ†’ New file (not in blob map): {}", file_path_key); 786 } 787 } 788 ··· 793 MimeType::new_static("application/octet-stream") 794 }; 795 796 - println!(" โ†‘ Uploading {} ({} bytes, CID: {})", file_path_key, upload_bytes.len(), file_cid); 797 let blob = agent.upload_blob(upload_bytes, mime_type).await?; 798 799 let mut file_builder = File::new() 800 .r#type(CowStr::from("file")) ··· 813 814 /// Convert fs::Directory to subfs::Directory 815 /// They have the same structure, but different types 816 - fn convert_fs_dir_to_subfs_dir(fs_dir: place_wisp::fs::Directory<'static>) -> place_wisp::subfs::Directory<'static> { 817 - use place_wisp::subfs::{Directory as SubfsDirectory, Entry as SubfsEntry, EntryNode as SubfsEntryNode, File as SubfsFile}; 818 819 let subfs_entries: Vec<SubfsEntry> = fs_dir.entries.into_iter().map(|entry| { 820 let node = match entry.node { 821 - place_wisp::fs::EntryNode::File(file) => { 822 SubfsEntryNode::File(Box::new(SubfsFile::new() 823 .r#type(file.r#type) 824 .blob(file.blob) ··· 827 .base64(file.base64) 828 .build())) 829 } 830 - place_wisp::fs::EntryNode::Directory(dir) => { 831 SubfsEntryNode::Directory(Box::new(convert_fs_dir_to_subfs_dir(*dir))) 832 } 833 - place_wisp::fs::EntryNode::Subfs(subfs) => { 834 // Nested subfs in the directory we're converting 835 // Note: subfs::Subfs doesn't have the 'flat' field - that's only in fs::Subfs 836 - SubfsEntryNode::Subfs(Box::new(place_wisp::subfs::Subfs::new() 837 .r#type(subfs.r#type) 838 .subject(subfs.subject) 839 .build())) 840 } 841 - place_wisp::fs::EntryNode::Unknown(unknown) => { 842 SubfsEntryNode::Unknown(unknown) 843 } 844 };
··· 1 mod cid; 2 mod blob_map; 3 mod metadata; ··· 24 use std::io::Write; 25 use base64::Engine; 26 use futures::stream::{self, StreamExt}; 27 + use indicatif::{ProgressBar, ProgressStyle, MultiProgress}; 28 29 + use wisp_lexicons::place_wisp::fs::*; 30 + use wisp_lexicons::place_wisp::settings::*; 31 + 32 + /// Maximum number of concurrent file uploads to the PDS 33 + const MAX_CONCURRENT_UPLOADS: usize = 2; 34 + 35 + /// Limits for caching on wisp.place (from @wisp/constants) 36 + const MAX_FILE_COUNT: usize = 1000; 37 + const MAX_SITE_SIZE: usize = 300 * 1024 * 1024; // 300MB 38 39 #[derive(Parser, Debug)] 40 #[command(author, version, about = "wisp.place CLI tool")] 41 struct Args { 42 #[command(subcommand)] 43 command: Option<Commands>, 44 + 45 // Deploy arguments (when no subcommand is specified) 46 /// Handle (e.g., alice.bsky.social), DID, or PDS URL 47 input: Option<CowStr<'static>>, 48 49 /// Path to the directory containing your static site 50 + #[arg(short, long)] 51 path: Option<PathBuf>, 52 53 /// Site name (defaults to directory name) 54 + #[arg(short, long)] 55 site: Option<String>, 56 57 /// Path to auth store file 58 + #[arg(long)] 59 store: Option<String>, 60 61 /// App Password for authentication 62 + #[arg(long)] 63 password: Option<CowStr<'static>>, 64 65 /// Enable directory listing mode for paths without index files 66 + #[arg(long)] 67 directory: bool, 68 69 /// Enable SPA mode (serve index.html for all routes) 70 + #[arg(long)] 71 spa: bool, 72 + 73 + /// Skip confirmation prompts (automatically accept warnings) 74 + #[arg(short = 'y', long)] 75 + yes: bool, 76 } 77 78 #[derive(Subcommand, Debug)] ··· 105 /// Enable SPA mode (serve index.html for all routes) 106 #[arg(long)] 107 spa: bool, 108 + 109 + /// Skip confirmation prompts (automatically accept warnings) 110 + #[arg(short = 'y', long)] 111 + yes: bool, 112 }, 113 /// Pull a site from the PDS to a local directory 114 Pull { ··· 121 122 /// Output directory for the downloaded site 123 #[arg(short, long, default_value = ".")] 124 + path: PathBuf, 125 }, 126 /// Serve a site locally with real-time firehose updates 127 Serve { ··· 134 135 /// Output directory for the site files 136 #[arg(short, long, default_value = ".")] 137 + path: PathBuf, 138 139 /// Port to serve on 140 + #[arg(short = 'P', long, default_value = "8080")] 141 port: u16, 142 }, 143 } ··· 147 let args = Args::parse(); 148 149 let result = match args.command { 150 + Some(Commands::Deploy { input, path, site, store, password, directory, spa, yes }) => { 151 // Dispatch to appropriate authentication method 152 if let Some(password) = password { 153 + run_with_app_password(input, password, path, site, directory, spa, yes).await 154 } else { 155 + run_with_oauth(input, store, path, site, directory, spa, yes).await 156 } 157 } 158 + Some(Commands::Pull { input, site, path }) => { 159 + pull::pull_site(input, CowStr::from(site), path).await 160 } 161 + Some(Commands::Serve { input, site, path, port }) => { 162 + serve::serve_site(input, CowStr::from(site), path, port).await 163 } 164 None => { 165 // Legacy mode: if input is provided, assume deploy command ··· 169 170 // Dispatch to appropriate authentication method 171 if let Some(password) = args.password { 172 + run_with_app_password(input, password, path, args.site, args.directory, args.spa, args.yes).await 173 } else { 174 + run_with_oauth(input, store, path, args.site, args.directory, args.spa, args.yes).await 175 } 176 } else { 177 // No command and no input, show help ··· 200 site: Option<String>, 201 directory: bool, 202 spa: bool, 203 + yes: bool, 204 ) -> miette::Result<()> { 205 let (session, auth) = 206 MemoryCredentialSession::authenticated(input, password, None, None).await?; 207 println!("Signed in as {}", auth.handle); 208 209 let agent: Agent<_> = Agent::from(session); 210 + deploy_site(&agent, path, site, directory, spa, yes).await 211 } 212 213 /// Run deployment with OAuth authentication ··· 218 site: Option<String>, 219 directory: bool, 220 spa: bool, 221 + yes: bool, 222 ) -> miette::Result<()> { 223 use jacquard::oauth::scopes::Scope; 224 use jacquard::oauth::atproto::AtprotoClientMetadata; ··· 251 .await?; 252 253 let agent: Agent<_> = Agent::from(session); 254 + deploy_site(&agent, path, site, directory, spa, yes).await 255 + } 256 + 257 + /// Scan directory to count files and calculate total size 258 + /// Returns (file_count, total_size_bytes) 259 + fn scan_directory_stats( 260 + dir_path: &Path, 261 + ignore_matcher: &ignore_patterns::IgnoreMatcher, 262 + current_path: String, 263 + ) -> miette::Result<(usize, u64)> { 264 + let mut file_count = 0; 265 + let mut total_size = 0u64; 266 + 267 + let dir_entries: Vec<_> = std::fs::read_dir(dir_path) 268 + .into_diagnostic()? 269 + .collect::<Result<Vec<_>, _>>() 270 + .into_diagnostic()?; 271 + 272 + for entry in dir_entries { 273 + let path = entry.path(); 274 + let name = entry.file_name(); 275 + let name_str = name.to_str() 276 + .ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))? 277 + .to_string(); 278 + 279 + let full_path = if current_path.is_empty() { 280 + name_str.clone() 281 + } else { 282 + format!("{}/{}", current_path, name_str) 283 + }; 284 + 285 + // Skip files/directories that match ignore patterns 286 + if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) { 287 + continue; 288 + } 289 + 290 + let metadata = entry.metadata().into_diagnostic()?; 291 + 292 + if metadata.is_file() { 293 + file_count += 1; 294 + total_size += metadata.len(); 295 + } else if metadata.is_dir() { 296 + let subdir_path = if current_path.is_empty() { 297 + name_str 298 + } else { 299 + format!("{}/{}", current_path, name_str) 300 + }; 301 + let (sub_count, sub_size) = scan_directory_stats(&path, ignore_matcher, subdir_path)?; 302 + file_count += sub_count; 303 + total_size += sub_size; 304 + } 305 + } 306 + 307 + Ok((file_count, total_size)) 308 } 309 310 /// Deploy the site using the provided agent ··· 314 site: Option<String>, 315 directory_listing: bool, 316 spa_mode: bool, 317 + skip_prompts: bool, 318 ) -> miette::Result<()> { 319 // Verify the path exists 320 if !path.exists() { ··· 332 333 println!("Deploying site '{}'...", site_name); 334 335 + // Scan directory to check file count and size 336 + let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?; 337 + let (file_count, total_size) = scan_directory_stats(&path, &ignore_matcher, String::new())?; 338 + 339 + let size_mb = total_size as f64 / (1024.0 * 1024.0); 340 + println!("Scanned: {} files, {:.1} MB total", file_count, size_mb); 341 + 342 + // Check if limits are exceeded 343 + let exceeds_file_count = file_count > MAX_FILE_COUNT; 344 + let exceeds_size = total_size > MAX_SITE_SIZE as u64; 345 + 346 + if exceeds_file_count || exceeds_size { 347 + println!("\nโš ๏ธ Warning: Your site exceeds wisp.place caching limits:"); 348 + 349 + if exceeds_file_count { 350 + println!(" โ€ข File count: {} (limit: {})", file_count, MAX_FILE_COUNT); 351 + } 352 + 353 + if exceeds_size { 354 + let size_mb = total_size as f64 / (1024.0 * 1024.0); 355 + let limit_mb = MAX_SITE_SIZE as f64 / (1024.0 * 1024.0); 356 + println!(" โ€ข Total size: {:.1} MB (limit: {:.0} MB)", size_mb, limit_mb); 357 + } 358 + 359 + println!("\nwisp.place will NOT serve your site if you proceed."); 360 + println!("Your site will be uploaded to your PDS, but will only be accessible via:"); 361 + println!(" โ€ข wisp-cli serve (local hosting)"); 362 + println!(" โ€ข Other hosting services with more generous limits"); 363 + 364 + if !skip_prompts { 365 + // Prompt for confirmation 366 + use std::io::{self, Write}; 367 + print!("\nDo you want to upload anyway? (y/N): "); 368 + io::stdout().flush().into_diagnostic()?; 369 + 370 + let mut input = String::new(); 371 + io::stdin().read_line(&mut input).into_diagnostic()?; 372 + let input = input.trim().to_lowercase(); 373 + 374 + if input != "y" && input != "yes" { 375 + println!("Upload cancelled."); 376 + return Ok(()); 377 + } 378 + } else { 379 + println!("\nSkipping confirmation (--yes flag set)."); 380 + } 381 + 382 + println!("\nProceeding with upload...\n"); 383 + } 384 + 385 // Try to fetch existing manifest for incremental updates 386 let (existing_blob_map, old_subfs_uris): (HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, Vec<(String, String)>) = { 387 use jacquard_common::types::string::AtUri; ··· 443 } 444 }; 445 446 + // Create progress tracking (spinner style since we don't know total count upfront) 447 + let multi_progress = MultiProgress::new(); 448 + let progress = multi_progress.add(ProgressBar::new_spinner()); 449 + progress.set_style( 450 + ProgressStyle::default_spinner() 451 + .template("[{elapsed_precise}] {spinner:.cyan} {pos} files {msg}") 452 + .into_diagnostic()? 453 + .tick_chars("โ โ ‚โ „โก€โข€โ  โ โ ˆ ") 454 + ); 455 + progress.set_message("Scanning files..."); 456 + progress.enable_steady_tick(std::time::Duration::from_millis(100)); 457 + 458 + let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher, &progress).await?; 459 let uploaded_count = total_files - reused_count; 460 + 461 + progress.finish_with_message(format!("โœ“ {} files ({} uploaded, {} reused)", total_files, uploaded_count, reused_count)); 462 463 // Check if we need to split into subfs records 464 const MAX_MANIFEST_SIZE: usize = 140 * 1024; // 140KB (PDS limit is 150KB) ··· 509 let chunk_file_count = subfs_utils::count_files_in_directory(chunk); 510 let chunk_size = subfs_utils::estimate_directory_size(chunk); 511 512 + let chunk_manifest = wisp_lexicons::place_wisp::subfs::SubfsRecord::new() 513 .root(convert_fs_dir_to_subfs_dir(chunk.clone())) 514 .file_count(Some(chunk_file_count as i64)) 515 .created_at(Datetime::now()) ··· 532 // Each chunk reference MUST have flat: true to merge chunk contents 533 println!(" โ†’ Creating parent subfs with {} chunk references...", chunk_uris.len()); 534 use jacquard_common::CowStr; 535 + use wisp_lexicons::place_wisp::fs::{Subfs}; 536 537 // Convert to fs::Subfs (which has the 'flat' field) instead of subfs::Subfs 538 let parent_entries_fs: Vec<Entry> = chunk_uris.iter().enumerate().map(|(i, (uri, _))| { ··· 562 let parent_tid = Tid::now_0(); 563 let parent_rkey = parent_tid.to_string(); 564 565 + let parent_manifest = wisp_lexicons::place_wisp::subfs::SubfsRecord::new() 566 .root(parent_root_subfs) 567 .file_count(Some(largest_dir.file_count as i64)) 568 .created_at(Datetime::now()) ··· 581 let subfs_tid = Tid::now_0(); 582 let subfs_rkey = subfs_tid.to_string(); 583 584 + let subfs_manifest = wisp_lexicons::place_wisp::subfs::SubfsRecord::new() 585 .root(convert_fs_dir_to_subfs_dir(largest_dir.directory.clone())) 586 .file_count(Some(largest_dir.file_count as i64)) 587 .created_at(Datetime::now()) ··· 737 existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 738 current_path: String, 739 ignore_matcher: &'a ignore_patterns::IgnoreMatcher, 740 + progress: &'a ProgressBar, 741 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>> 742 { 743 Box::pin(async move { ··· 785 } 786 } 787 788 + // Process files concurrently with a limit of 2 789 let file_results: Vec<(Entry<'static>, bool)> = stream::iter(file_tasks) 790 .map(|(name, path, full_path)| async move { 791 + let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs, progress).await?; 792 + progress.inc(1); 793 let entry = Entry::new() 794 .name(CowStr::from(name)) 795 .node(EntryNode::File(Box::new(file_node))) 796 .build(); 797 Ok::<_, miette::Report>((entry, reused)) 798 }) 799 + .buffer_unordered(MAX_CONCURRENT_UPLOADS) 800 .collect::<Vec<_>>() 801 .await 802 .into_iter() ··· 823 } else { 824 format!("{}/{}", current_path, name) 825 }; 826 + let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher, progress).await?; 827 dir_entries.push(Entry::new() 828 .name(CowStr::from(name)) 829 .node(EntryNode::Directory(Box::new(subdir))) ··· 855 file_path: &Path, 856 file_path_key: &str, 857 existing_blobs: &HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 858 + progress: &ProgressBar, 859 ) -> miette::Result<(File<'static>, bool)> 860 { 861 // Read file ··· 895 if let Some((existing_blob_ref, existing_cid)) = existing_blob { 896 if existing_cid == &file_cid { 897 // CIDs match - reuse existing blob 898 + progress.set_message(format!("โœ“ Reused {}", file_path_key)); 899 let mut file_builder = File::new() 900 .r#type(CowStr::from("file")) 901 .blob(existing_blob_ref.clone()) ··· 909 } 910 911 return Ok((file_builder.build(), true)); 912 } 913 } 914 ··· 919 MimeType::new_static("application/octet-stream") 920 }; 921 922 + // Format file size nicely 923 + let size_str = if upload_bytes.len() < 1024 { 924 + format!("{} B", upload_bytes.len()) 925 + } else if upload_bytes.len() < 1024 * 1024 { 926 + format!("{:.1} KB", upload_bytes.len() as f64 / 1024.0) 927 + } else { 928 + format!("{:.1} MB", upload_bytes.len() as f64 / (1024.0 * 1024.0)) 929 + }; 930 + 931 + progress.set_message(format!("โ†‘ Uploading {} ({})", file_path_key, size_str)); 932 let blob = agent.upload_blob(upload_bytes, mime_type).await?; 933 + progress.set_message(format!("โœ“ Uploaded {}", file_path_key)); 934 935 let mut file_builder = File::new() 936 .r#type(CowStr::from("file")) ··· 949 950 /// Convert fs::Directory to subfs::Directory 951 /// They have the same structure, but different types 952 + fn convert_fs_dir_to_subfs_dir(fs_dir: wisp_lexicons::place_wisp::fs::Directory<'static>) -> wisp_lexicons::place_wisp::subfs::Directory<'static> { 953 + use wisp_lexicons::place_wisp::subfs::{Directory as SubfsDirectory, Entry as SubfsEntry, EntryNode as SubfsEntryNode, File as SubfsFile}; 954 955 let subfs_entries: Vec<SubfsEntry> = fs_dir.entries.into_iter().map(|entry| { 956 let node = match entry.node { 957 + wisp_lexicons::place_wisp::fs::EntryNode::File(file) => { 958 SubfsEntryNode::File(Box::new(SubfsFile::new() 959 .r#type(file.r#type) 960 .blob(file.blob) ··· 963 .base64(file.base64) 964 .build())) 965 } 966 + wisp_lexicons::place_wisp::fs::EntryNode::Directory(dir) => { 967 SubfsEntryNode::Directory(Box::new(convert_fs_dir_to_subfs_dir(*dir))) 968 } 969 + wisp_lexicons::place_wisp::fs::EntryNode::Subfs(subfs) => { 970 // Nested subfs in the directory we're converting 971 // Note: subfs::Subfs doesn't have the 'flat' field - that's only in fs::Subfs 972 + SubfsEntryNode::Subfs(Box::new(wisp_lexicons::place_wisp::subfs::Subfs::new() 973 .r#type(subfs.r#type) 974 .subject(subfs.subject) 975 .build())) 976 } 977 + wisp_lexicons::place_wisp::fs::EntryNode::Unknown(unknown) => { 978 SubfsEntryNode::Unknown(unknown) 979 } 980 };
-9
cli/src/mod.rs
··· 1 - // @generated by jacquard-lexicon. DO NOT EDIT. 2 - // 3 - // This file was automatically generated from Lexicon schemas. 4 - // Any manual changes will be overwritten on the next regeneration. 5 - 6 - pub mod builder_types; 7 - 8 - #[cfg(feature = "place_wisp")] 9 - pub mod place_wisp;
···
-1490
cli/src/place_wisp/fs.rs
··· 1 - // @generated by jacquard-lexicon. DO NOT EDIT. 2 - // 3 - // Lexicon: place.wisp.fs 4 - // 5 - // This file was automatically generated from Lexicon schemas. 6 - // Any manual changes will be overwritten on the next regeneration. 7 - 8 - #[jacquard_derive::lexicon] 9 - #[derive( 10 - serde::Serialize, 11 - serde::Deserialize, 12 - Debug, 13 - Clone, 14 - PartialEq, 15 - Eq, 16 - jacquard_derive::IntoStatic 17 - )] 18 - #[serde(rename_all = "camelCase")] 19 - pub struct Directory<'a> { 20 - #[serde(borrow)] 21 - pub entries: Vec<crate::place_wisp::fs::Entry<'a>>, 22 - #[serde(borrow)] 23 - pub r#type: jacquard_common::CowStr<'a>, 24 - } 25 - 26 - pub mod directory_state { 27 - 28 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 29 - #[allow(unused)] 30 - use ::core::marker::PhantomData; 31 - mod sealed { 32 - pub trait Sealed {} 33 - } 34 - /// State trait tracking which required fields have been set 35 - pub trait State: sealed::Sealed { 36 - type Type; 37 - type Entries; 38 - } 39 - /// Empty state - all required fields are unset 40 - pub struct Empty(()); 41 - impl sealed::Sealed for Empty {} 42 - impl State for Empty { 43 - type Type = Unset; 44 - type Entries = Unset; 45 - } 46 - ///State transition - sets the `type` field to Set 47 - pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 48 - impl<S: State> sealed::Sealed for SetType<S> {} 49 - impl<S: State> State for SetType<S> { 50 - type Type = Set<members::r#type>; 51 - type Entries = S::Entries; 52 - } 53 - ///State transition - sets the `entries` field to Set 54 - pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>); 55 - impl<S: State> sealed::Sealed for SetEntries<S> {} 56 - impl<S: State> State for SetEntries<S> { 57 - type Type = S::Type; 58 - type Entries = Set<members::entries>; 59 - } 60 - /// Marker types for field names 61 - #[allow(non_camel_case_types)] 62 - pub mod members { 63 - ///Marker type for the `type` field 64 - pub struct r#type(()); 65 - ///Marker type for the `entries` field 66 - pub struct entries(()); 67 - } 68 - } 69 - 70 - /// Builder for constructing an instance of this type 71 - pub struct DirectoryBuilder<'a, S: directory_state::State> { 72 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 73 - __unsafe_private_named: ( 74 - ::core::option::Option<Vec<crate::place_wisp::fs::Entry<'a>>>, 75 - ::core::option::Option<jacquard_common::CowStr<'a>>, 76 - ), 77 - _phantom: ::core::marker::PhantomData<&'a ()>, 78 - } 79 - 80 - impl<'a> Directory<'a> { 81 - /// Create a new builder for this type 82 - pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> { 83 - DirectoryBuilder::new() 84 - } 85 - } 86 - 87 - impl<'a> DirectoryBuilder<'a, directory_state::Empty> { 88 - /// Create a new builder with all fields unset 89 - pub fn new() -> Self { 90 - DirectoryBuilder { 91 - _phantom_state: ::core::marker::PhantomData, 92 - __unsafe_private_named: (None, None), 93 - _phantom: ::core::marker::PhantomData, 94 - } 95 - } 96 - } 97 - 98 - impl<'a, S> DirectoryBuilder<'a, S> 99 - where 100 - S: directory_state::State, 101 - S::Entries: directory_state::IsUnset, 102 - { 103 - /// Set the `entries` field (required) 104 - pub fn entries( 105 - mut self, 106 - value: impl Into<Vec<crate::place_wisp::fs::Entry<'a>>>, 107 - ) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> { 108 - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 109 - DirectoryBuilder { 110 - _phantom_state: ::core::marker::PhantomData, 111 - __unsafe_private_named: self.__unsafe_private_named, 112 - _phantom: ::core::marker::PhantomData, 113 - } 114 - } 115 - } 116 - 117 - impl<'a, S> DirectoryBuilder<'a, S> 118 - where 119 - S: directory_state::State, 120 - S::Type: directory_state::IsUnset, 121 - { 122 - /// Set the `type` field (required) 123 - pub fn r#type( 124 - mut self, 125 - value: impl Into<jacquard_common::CowStr<'a>>, 126 - ) -> DirectoryBuilder<'a, directory_state::SetType<S>> { 127 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 128 - DirectoryBuilder { 129 - _phantom_state: ::core::marker::PhantomData, 130 - __unsafe_private_named: self.__unsafe_private_named, 131 - _phantom: ::core::marker::PhantomData, 132 - } 133 - } 134 - } 135 - 136 - impl<'a, S> DirectoryBuilder<'a, S> 137 - where 138 - S: directory_state::State, 139 - S::Type: directory_state::IsSet, 140 - S::Entries: directory_state::IsSet, 141 - { 142 - /// Build the final struct 143 - pub fn build(self) -> Directory<'a> { 144 - Directory { 145 - entries: self.__unsafe_private_named.0.unwrap(), 146 - r#type: self.__unsafe_private_named.1.unwrap(), 147 - extra_data: Default::default(), 148 - } 149 - } 150 - /// Build the final struct with custom extra_data 151 - pub fn build_with_data( 152 - self, 153 - extra_data: std::collections::BTreeMap< 154 - jacquard_common::smol_str::SmolStr, 155 - jacquard_common::types::value::Data<'a>, 156 - >, 157 - ) -> Directory<'a> { 158 - Directory { 159 - entries: self.__unsafe_private_named.0.unwrap(), 160 - r#type: self.__unsafe_private_named.1.unwrap(), 161 - extra_data: Some(extra_data), 162 - } 163 - } 164 - } 165 - 166 - fn lexicon_doc_place_wisp_fs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 167 - ::jacquard_lexicon::lexicon::LexiconDoc { 168 - lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 169 - id: ::jacquard_common::CowStr::new_static("place.wisp.fs"), 170 - revision: None, 171 - description: None, 172 - defs: { 173 - let mut map = ::std::collections::BTreeMap::new(); 174 - map.insert( 175 - ::jacquard_common::smol_str::SmolStr::new_static("directory"), 176 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 177 - description: None, 178 - required: Some( 179 - vec![ 180 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 181 - ::jacquard_common::smol_str::SmolStr::new_static("entries") 182 - ], 183 - ), 184 - nullable: None, 185 - properties: { 186 - #[allow(unused_mut)] 187 - let mut map = ::std::collections::BTreeMap::new(); 188 - map.insert( 189 - ::jacquard_common::smol_str::SmolStr::new_static("entries"), 190 - ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 191 - description: None, 192 - items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { 193 - description: None, 194 - r#ref: ::jacquard_common::CowStr::new_static("#entry"), 195 - }), 196 - min_length: None, 197 - max_length: Some(500usize), 198 - }), 199 - ); 200 - map.insert( 201 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 202 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 203 - description: None, 204 - format: None, 205 - default: None, 206 - min_length: None, 207 - max_length: None, 208 - min_graphemes: None, 209 - max_graphemes: None, 210 - r#enum: None, 211 - r#const: None, 212 - known_values: None, 213 - }), 214 - ); 215 - map 216 - }, 217 - }), 218 - ); 219 - map.insert( 220 - ::jacquard_common::smol_str::SmolStr::new_static("entry"), 221 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 222 - description: None, 223 - required: Some( 224 - vec![ 225 - ::jacquard_common::smol_str::SmolStr::new_static("name"), 226 - ::jacquard_common::smol_str::SmolStr::new_static("node") 227 - ], 228 - ), 229 - nullable: None, 230 - properties: { 231 - #[allow(unused_mut)] 232 - let mut map = ::std::collections::BTreeMap::new(); 233 - map.insert( 234 - ::jacquard_common::smol_str::SmolStr::new_static("name"), 235 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 236 - description: None, 237 - format: None, 238 - default: None, 239 - min_length: None, 240 - max_length: Some(255usize), 241 - min_graphemes: None, 242 - max_graphemes: None, 243 - r#enum: None, 244 - r#const: None, 245 - known_values: None, 246 - }), 247 - ); 248 - map.insert( 249 - ::jacquard_common::smol_str::SmolStr::new_static("node"), 250 - ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 251 - description: None, 252 - refs: vec![ 253 - ::jacquard_common::CowStr::new_static("#file"), 254 - ::jacquard_common::CowStr::new_static("#directory"), 255 - ::jacquard_common::CowStr::new_static("#subfs") 256 - ], 257 - closed: None, 258 - }), 259 - ); 260 - map 261 - }, 262 - }), 263 - ); 264 - map.insert( 265 - ::jacquard_common::smol_str::SmolStr::new_static("file"), 266 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 267 - description: None, 268 - required: Some( 269 - vec![ 270 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 271 - ::jacquard_common::smol_str::SmolStr::new_static("blob") 272 - ], 273 - ), 274 - nullable: None, 275 - properties: { 276 - #[allow(unused_mut)] 277 - let mut map = ::std::collections::BTreeMap::new(); 278 - map.insert( 279 - ::jacquard_common::smol_str::SmolStr::new_static("base64"), 280 - ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 281 - description: None, 282 - default: None, 283 - r#const: None, 284 - }), 285 - ); 286 - map.insert( 287 - ::jacquard_common::smol_str::SmolStr::new_static("blob"), 288 - ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob { 289 - description: None, 290 - accept: None, 291 - max_size: None, 292 - }), 293 - ); 294 - map.insert( 295 - ::jacquard_common::smol_str::SmolStr::new_static("encoding"), 296 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 297 - description: Some( 298 - ::jacquard_common::CowStr::new_static( 299 - "Content encoding (e.g., gzip for compressed files)", 300 - ), 301 - ), 302 - format: None, 303 - default: None, 304 - min_length: None, 305 - max_length: None, 306 - min_graphemes: None, 307 - max_graphemes: None, 308 - r#enum: None, 309 - r#const: None, 310 - known_values: None, 311 - }), 312 - ); 313 - map.insert( 314 - ::jacquard_common::smol_str::SmolStr::new_static("mimeType"), 315 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 316 - description: Some( 317 - ::jacquard_common::CowStr::new_static( 318 - "Original MIME type before compression", 319 - ), 320 - ), 321 - format: None, 322 - default: None, 323 - min_length: None, 324 - max_length: None, 325 - min_graphemes: None, 326 - max_graphemes: None, 327 - r#enum: None, 328 - r#const: None, 329 - known_values: None, 330 - }), 331 - ); 332 - map.insert( 333 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 334 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 335 - description: None, 336 - format: None, 337 - default: None, 338 - min_length: None, 339 - max_length: None, 340 - min_graphemes: None, 341 - max_graphemes: None, 342 - r#enum: None, 343 - r#const: None, 344 - known_values: None, 345 - }), 346 - ); 347 - map 348 - }, 349 - }), 350 - ); 351 - map.insert( 352 - ::jacquard_common::smol_str::SmolStr::new_static("main"), 353 - ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { 354 - description: Some( 355 - ::jacquard_common::CowStr::new_static( 356 - "Virtual filesystem manifest for a Wisp site", 357 - ), 358 - ), 359 - key: None, 360 - record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { 361 - description: None, 362 - required: Some( 363 - vec![ 364 - ::jacquard_common::smol_str::SmolStr::new_static("site"), 365 - ::jacquard_common::smol_str::SmolStr::new_static("root"), 366 - ::jacquard_common::smol_str::SmolStr::new_static("createdAt") 367 - ], 368 - ), 369 - nullable: None, 370 - properties: { 371 - #[allow(unused_mut)] 372 - let mut map = ::std::collections::BTreeMap::new(); 373 - map.insert( 374 - ::jacquard_common::smol_str::SmolStr::new_static( 375 - "createdAt", 376 - ), 377 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 378 - description: None, 379 - format: Some( 380 - ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, 381 - ), 382 - default: None, 383 - min_length: None, 384 - max_length: None, 385 - min_graphemes: None, 386 - max_graphemes: None, 387 - r#enum: None, 388 - r#const: None, 389 - known_values: None, 390 - }), 391 - ); 392 - map.insert( 393 - ::jacquard_common::smol_str::SmolStr::new_static( 394 - "fileCount", 395 - ), 396 - ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { 397 - description: None, 398 - default: None, 399 - minimum: Some(0i64), 400 - maximum: Some(1000i64), 401 - r#enum: None, 402 - r#const: None, 403 - }), 404 - ); 405 - map.insert( 406 - ::jacquard_common::smol_str::SmolStr::new_static("root"), 407 - ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { 408 - description: None, 409 - r#ref: ::jacquard_common::CowStr::new_static("#directory"), 410 - }), 411 - ); 412 - map.insert( 413 - ::jacquard_common::smol_str::SmolStr::new_static("site"), 414 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 415 - description: None, 416 - format: None, 417 - default: None, 418 - min_length: None, 419 - max_length: None, 420 - min_graphemes: None, 421 - max_graphemes: None, 422 - r#enum: None, 423 - r#const: None, 424 - known_values: None, 425 - }), 426 - ); 427 - map 428 - }, 429 - }), 430 - }), 431 - ); 432 - map.insert( 433 - ::jacquard_common::smol_str::SmolStr::new_static("subfs"), 434 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 435 - description: None, 436 - required: Some( 437 - vec![ 438 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 439 - ::jacquard_common::smol_str::SmolStr::new_static("subject") 440 - ], 441 - ), 442 - nullable: None, 443 - properties: { 444 - #[allow(unused_mut)] 445 - let mut map = ::std::collections::BTreeMap::new(); 446 - map.insert( 447 - ::jacquard_common::smol_str::SmolStr::new_static("flat"), 448 - ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 449 - description: None, 450 - default: None, 451 - r#const: None, 452 - }), 453 - ); 454 - map.insert( 455 - ::jacquard_common::smol_str::SmolStr::new_static("subject"), 456 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 457 - description: Some( 458 - ::jacquard_common::CowStr::new_static( 459 - "AT-URI pointing to a place.wisp.subfs record containing this subtree.", 460 - ), 461 - ), 462 - format: Some( 463 - ::jacquard_lexicon::lexicon::LexStringFormat::AtUri, 464 - ), 465 - default: None, 466 - min_length: None, 467 - max_length: None, 468 - min_graphemes: None, 469 - max_graphemes: None, 470 - r#enum: None, 471 - r#const: None, 472 - known_values: None, 473 - }), 474 - ); 475 - map.insert( 476 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 477 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 478 - description: None, 479 - format: None, 480 - default: None, 481 - min_length: None, 482 - max_length: None, 483 - min_graphemes: None, 484 - max_graphemes: None, 485 - r#enum: None, 486 - r#const: None, 487 - known_values: None, 488 - }), 489 - ); 490 - map 491 - }, 492 - }), 493 - ); 494 - map 495 - }, 496 - } 497 - } 498 - 499 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> { 500 - fn nsid() -> &'static str { 501 - "place.wisp.fs" 502 - } 503 - fn def_name() -> &'static str { 504 - "directory" 505 - } 506 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 507 - lexicon_doc_place_wisp_fs() 508 - } 509 - fn validate( 510 - &self, 511 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 512 - { 513 - let value = &self.entries; 514 - #[allow(unused_comparisons)] 515 - if value.len() > 500usize { 516 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 517 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 518 - "entries", 519 - ), 520 - max: 500usize, 521 - actual: value.len(), 522 - }); 523 - } 524 - } 525 - Ok(()) 526 - } 527 - } 528 - 529 - #[jacquard_derive::lexicon] 530 - #[derive( 531 - serde::Serialize, 532 - serde::Deserialize, 533 - Debug, 534 - Clone, 535 - PartialEq, 536 - Eq, 537 - jacquard_derive::IntoStatic 538 - )] 539 - #[serde(rename_all = "camelCase")] 540 - pub struct Entry<'a> { 541 - #[serde(borrow)] 542 - pub name: jacquard_common::CowStr<'a>, 543 - #[serde(borrow)] 544 - pub node: EntryNode<'a>, 545 - } 546 - 547 - pub mod entry_state { 548 - 549 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 550 - #[allow(unused)] 551 - use ::core::marker::PhantomData; 552 - mod sealed { 553 - pub trait Sealed {} 554 - } 555 - /// State trait tracking which required fields have been set 556 - pub trait State: sealed::Sealed { 557 - type Name; 558 - type Node; 559 - } 560 - /// Empty state - all required fields are unset 561 - pub struct Empty(()); 562 - impl sealed::Sealed for Empty {} 563 - impl State for Empty { 564 - type Name = Unset; 565 - type Node = Unset; 566 - } 567 - ///State transition - sets the `name` field to Set 568 - pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>); 569 - impl<S: State> sealed::Sealed for SetName<S> {} 570 - impl<S: State> State for SetName<S> { 571 - type Name = Set<members::name>; 572 - type Node = S::Node; 573 - } 574 - ///State transition - sets the `node` field to Set 575 - pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>); 576 - impl<S: State> sealed::Sealed for SetNode<S> {} 577 - impl<S: State> State for SetNode<S> { 578 - type Name = S::Name; 579 - type Node = Set<members::node>; 580 - } 581 - /// Marker types for field names 582 - #[allow(non_camel_case_types)] 583 - pub mod members { 584 - ///Marker type for the `name` field 585 - pub struct name(()); 586 - ///Marker type for the `node` field 587 - pub struct node(()); 588 - } 589 - } 590 - 591 - /// Builder for constructing an instance of this type 592 - pub struct EntryBuilder<'a, S: entry_state::State> { 593 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 594 - __unsafe_private_named: ( 595 - ::core::option::Option<jacquard_common::CowStr<'a>>, 596 - ::core::option::Option<EntryNode<'a>>, 597 - ), 598 - _phantom: ::core::marker::PhantomData<&'a ()>, 599 - } 600 - 601 - impl<'a> Entry<'a> { 602 - /// Create a new builder for this type 603 - pub fn new() -> EntryBuilder<'a, entry_state::Empty> { 604 - EntryBuilder::new() 605 - } 606 - } 607 - 608 - impl<'a> EntryBuilder<'a, entry_state::Empty> { 609 - /// Create a new builder with all fields unset 610 - pub fn new() -> Self { 611 - EntryBuilder { 612 - _phantom_state: ::core::marker::PhantomData, 613 - __unsafe_private_named: (None, None), 614 - _phantom: ::core::marker::PhantomData, 615 - } 616 - } 617 - } 618 - 619 - impl<'a, S> EntryBuilder<'a, S> 620 - where 621 - S: entry_state::State, 622 - S::Name: entry_state::IsUnset, 623 - { 624 - /// Set the `name` field (required) 625 - pub fn name( 626 - mut self, 627 - value: impl Into<jacquard_common::CowStr<'a>>, 628 - ) -> EntryBuilder<'a, entry_state::SetName<S>> { 629 - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 630 - EntryBuilder { 631 - _phantom_state: ::core::marker::PhantomData, 632 - __unsafe_private_named: self.__unsafe_private_named, 633 - _phantom: ::core::marker::PhantomData, 634 - } 635 - } 636 - } 637 - 638 - impl<'a, S> EntryBuilder<'a, S> 639 - where 640 - S: entry_state::State, 641 - S::Node: entry_state::IsUnset, 642 - { 643 - /// Set the `node` field (required) 644 - pub fn node( 645 - mut self, 646 - value: impl Into<EntryNode<'a>>, 647 - ) -> EntryBuilder<'a, entry_state::SetNode<S>> { 648 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 649 - EntryBuilder { 650 - _phantom_state: ::core::marker::PhantomData, 651 - __unsafe_private_named: self.__unsafe_private_named, 652 - _phantom: ::core::marker::PhantomData, 653 - } 654 - } 655 - } 656 - 657 - impl<'a, S> EntryBuilder<'a, S> 658 - where 659 - S: entry_state::State, 660 - S::Name: entry_state::IsSet, 661 - S::Node: entry_state::IsSet, 662 - { 663 - /// Build the final struct 664 - pub fn build(self) -> Entry<'a> { 665 - Entry { 666 - name: self.__unsafe_private_named.0.unwrap(), 667 - node: self.__unsafe_private_named.1.unwrap(), 668 - extra_data: Default::default(), 669 - } 670 - } 671 - /// Build the final struct with custom extra_data 672 - pub fn build_with_data( 673 - self, 674 - extra_data: std::collections::BTreeMap< 675 - jacquard_common::smol_str::SmolStr, 676 - jacquard_common::types::value::Data<'a>, 677 - >, 678 - ) -> Entry<'a> { 679 - Entry { 680 - name: self.__unsafe_private_named.0.unwrap(), 681 - node: self.__unsafe_private_named.1.unwrap(), 682 - extra_data: Some(extra_data), 683 - } 684 - } 685 - } 686 - 687 - #[jacquard_derive::open_union] 688 - #[derive( 689 - serde::Serialize, 690 - serde::Deserialize, 691 - Debug, 692 - Clone, 693 - PartialEq, 694 - Eq, 695 - jacquard_derive::IntoStatic 696 - )] 697 - #[serde(tag = "$type")] 698 - #[serde(bound(deserialize = "'de: 'a"))] 699 - pub enum EntryNode<'a> { 700 - #[serde(rename = "place.wisp.fs#file")] 701 - File(Box<crate::place_wisp::fs::File<'a>>), 702 - #[serde(rename = "place.wisp.fs#directory")] 703 - Directory(Box<crate::place_wisp::fs::Directory<'a>>), 704 - #[serde(rename = "place.wisp.fs#subfs")] 705 - Subfs(Box<crate::place_wisp::fs::Subfs<'a>>), 706 - } 707 - 708 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> { 709 - fn nsid() -> &'static str { 710 - "place.wisp.fs" 711 - } 712 - fn def_name() -> &'static str { 713 - "entry" 714 - } 715 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 716 - lexicon_doc_place_wisp_fs() 717 - } 718 - fn validate( 719 - &self, 720 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 721 - { 722 - let value = &self.name; 723 - #[allow(unused_comparisons)] 724 - if <str>::len(value.as_ref()) > 255usize { 725 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 726 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 727 - "name", 728 - ), 729 - max: 255usize, 730 - actual: <str>::len(value.as_ref()), 731 - }); 732 - } 733 - } 734 - Ok(()) 735 - } 736 - } 737 - 738 - #[jacquard_derive::lexicon] 739 - #[derive( 740 - serde::Serialize, 741 - serde::Deserialize, 742 - Debug, 743 - Clone, 744 - PartialEq, 745 - Eq, 746 - jacquard_derive::IntoStatic 747 - )] 748 - #[serde(rename_all = "camelCase")] 749 - pub struct File<'a> { 750 - /// True if blob content is base64-encoded (used to bypass PDS content sniffing) 751 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 752 - pub base64: Option<bool>, 753 - /// Content blob ref 754 - #[serde(borrow)] 755 - pub blob: jacquard_common::types::blob::BlobRef<'a>, 756 - /// Content encoding (e.g., gzip for compressed files) 757 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 758 - #[serde(borrow)] 759 - pub encoding: Option<jacquard_common::CowStr<'a>>, 760 - /// Original MIME type before compression 761 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 762 - #[serde(borrow)] 763 - pub mime_type: Option<jacquard_common::CowStr<'a>>, 764 - #[serde(borrow)] 765 - pub r#type: jacquard_common::CowStr<'a>, 766 - } 767 - 768 - pub mod file_state { 769 - 770 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 771 - #[allow(unused)] 772 - use ::core::marker::PhantomData; 773 - mod sealed { 774 - pub trait Sealed {} 775 - } 776 - /// State trait tracking which required fields have been set 777 - pub trait State: sealed::Sealed { 778 - type Type; 779 - type Blob; 780 - } 781 - /// Empty state - all required fields are unset 782 - pub struct Empty(()); 783 - impl sealed::Sealed for Empty {} 784 - impl State for Empty { 785 - type Type = Unset; 786 - type Blob = Unset; 787 - } 788 - ///State transition - sets the `type` field to Set 789 - pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 790 - impl<S: State> sealed::Sealed for SetType<S> {} 791 - impl<S: State> State for SetType<S> { 792 - type Type = Set<members::r#type>; 793 - type Blob = S::Blob; 794 - } 795 - ///State transition - sets the `blob` field to Set 796 - pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>); 797 - impl<S: State> sealed::Sealed for SetBlob<S> {} 798 - impl<S: State> State for SetBlob<S> { 799 - type Type = S::Type; 800 - type Blob = Set<members::blob>; 801 - } 802 - /// Marker types for field names 803 - #[allow(non_camel_case_types)] 804 - pub mod members { 805 - ///Marker type for the `type` field 806 - pub struct r#type(()); 807 - ///Marker type for the `blob` field 808 - pub struct blob(()); 809 - } 810 - } 811 - 812 - /// Builder for constructing an instance of this type 813 - pub struct FileBuilder<'a, S: file_state::State> { 814 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 815 - __unsafe_private_named: ( 816 - ::core::option::Option<bool>, 817 - ::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 818 - ::core::option::Option<jacquard_common::CowStr<'a>>, 819 - ::core::option::Option<jacquard_common::CowStr<'a>>, 820 - ::core::option::Option<jacquard_common::CowStr<'a>>, 821 - ), 822 - _phantom: ::core::marker::PhantomData<&'a ()>, 823 - } 824 - 825 - impl<'a> File<'a> { 826 - /// Create a new builder for this type 827 - pub fn new() -> FileBuilder<'a, file_state::Empty> { 828 - FileBuilder::new() 829 - } 830 - } 831 - 832 - impl<'a> FileBuilder<'a, file_state::Empty> { 833 - /// Create a new builder with all fields unset 834 - pub fn new() -> Self { 835 - FileBuilder { 836 - _phantom_state: ::core::marker::PhantomData, 837 - __unsafe_private_named: (None, None, None, None, None), 838 - _phantom: ::core::marker::PhantomData, 839 - } 840 - } 841 - } 842 - 843 - impl<'a, S: file_state::State> FileBuilder<'a, S> { 844 - /// Set the `base64` field (optional) 845 - pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self { 846 - self.__unsafe_private_named.0 = value.into(); 847 - self 848 - } 849 - /// Set the `base64` field to an Option value (optional) 850 - pub fn maybe_base64(mut self, value: Option<bool>) -> Self { 851 - self.__unsafe_private_named.0 = value; 852 - self 853 - } 854 - } 855 - 856 - impl<'a, S> FileBuilder<'a, S> 857 - where 858 - S: file_state::State, 859 - S::Blob: file_state::IsUnset, 860 - { 861 - /// Set the `blob` field (required) 862 - pub fn blob( 863 - mut self, 864 - value: impl Into<jacquard_common::types::blob::BlobRef<'a>>, 865 - ) -> FileBuilder<'a, file_state::SetBlob<S>> { 866 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 867 - FileBuilder { 868 - _phantom_state: ::core::marker::PhantomData, 869 - __unsafe_private_named: self.__unsafe_private_named, 870 - _phantom: ::core::marker::PhantomData, 871 - } 872 - } 873 - } 874 - 875 - impl<'a, S: file_state::State> FileBuilder<'a, S> { 876 - /// Set the `encoding` field (optional) 877 - pub fn encoding( 878 - mut self, 879 - value: impl Into<Option<jacquard_common::CowStr<'a>>>, 880 - ) -> Self { 881 - self.__unsafe_private_named.2 = value.into(); 882 - self 883 - } 884 - /// Set the `encoding` field to an Option value (optional) 885 - pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 886 - self.__unsafe_private_named.2 = value; 887 - self 888 - } 889 - } 890 - 891 - impl<'a, S: file_state::State> FileBuilder<'a, S> { 892 - /// Set the `mimeType` field (optional) 893 - pub fn mime_type( 894 - mut self, 895 - value: impl Into<Option<jacquard_common::CowStr<'a>>>, 896 - ) -> Self { 897 - self.__unsafe_private_named.3 = value.into(); 898 - self 899 - } 900 - /// Set the `mimeType` field to an Option value (optional) 901 - pub fn maybe_mime_type( 902 - mut self, 903 - value: Option<jacquard_common::CowStr<'a>>, 904 - ) -> Self { 905 - self.__unsafe_private_named.3 = value; 906 - self 907 - } 908 - } 909 - 910 - impl<'a, S> FileBuilder<'a, S> 911 - where 912 - S: file_state::State, 913 - S::Type: file_state::IsUnset, 914 - { 915 - /// Set the `type` field (required) 916 - pub fn r#type( 917 - mut self, 918 - value: impl Into<jacquard_common::CowStr<'a>>, 919 - ) -> FileBuilder<'a, file_state::SetType<S>> { 920 - self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); 921 - FileBuilder { 922 - _phantom_state: ::core::marker::PhantomData, 923 - __unsafe_private_named: self.__unsafe_private_named, 924 - _phantom: ::core::marker::PhantomData, 925 - } 926 - } 927 - } 928 - 929 - impl<'a, S> FileBuilder<'a, S> 930 - where 931 - S: file_state::State, 932 - S::Type: file_state::IsSet, 933 - S::Blob: file_state::IsSet, 934 - { 935 - /// Build the final struct 936 - pub fn build(self) -> File<'a> { 937 - File { 938 - base64: self.__unsafe_private_named.0, 939 - blob: self.__unsafe_private_named.1.unwrap(), 940 - encoding: self.__unsafe_private_named.2, 941 - mime_type: self.__unsafe_private_named.3, 942 - r#type: self.__unsafe_private_named.4.unwrap(), 943 - extra_data: Default::default(), 944 - } 945 - } 946 - /// Build the final struct with custom extra_data 947 - pub fn build_with_data( 948 - self, 949 - extra_data: std::collections::BTreeMap< 950 - jacquard_common::smol_str::SmolStr, 951 - jacquard_common::types::value::Data<'a>, 952 - >, 953 - ) -> File<'a> { 954 - File { 955 - base64: self.__unsafe_private_named.0, 956 - blob: self.__unsafe_private_named.1.unwrap(), 957 - encoding: self.__unsafe_private_named.2, 958 - mime_type: self.__unsafe_private_named.3, 959 - r#type: self.__unsafe_private_named.4.unwrap(), 960 - extra_data: Some(extra_data), 961 - } 962 - } 963 - } 964 - 965 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> { 966 - fn nsid() -> &'static str { 967 - "place.wisp.fs" 968 - } 969 - fn def_name() -> &'static str { 970 - "file" 971 - } 972 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 973 - lexicon_doc_place_wisp_fs() 974 - } 975 - fn validate( 976 - &self, 977 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 978 - Ok(()) 979 - } 980 - } 981 - 982 - /// Virtual filesystem manifest for a Wisp site 983 - #[jacquard_derive::lexicon] 984 - #[derive( 985 - serde::Serialize, 986 - serde::Deserialize, 987 - Debug, 988 - Clone, 989 - PartialEq, 990 - Eq, 991 - jacquard_derive::IntoStatic 992 - )] 993 - #[serde(rename_all = "camelCase")] 994 - pub struct Fs<'a> { 995 - pub created_at: jacquard_common::types::string::Datetime, 996 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 997 - pub file_count: Option<i64>, 998 - #[serde(borrow)] 999 - pub root: crate::place_wisp::fs::Directory<'a>, 1000 - #[serde(borrow)] 1001 - pub site: jacquard_common::CowStr<'a>, 1002 - } 1003 - 1004 - pub mod fs_state { 1005 - 1006 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 1007 - #[allow(unused)] 1008 - use ::core::marker::PhantomData; 1009 - mod sealed { 1010 - pub trait Sealed {} 1011 - } 1012 - /// State trait tracking which required fields have been set 1013 - pub trait State: sealed::Sealed { 1014 - type Site; 1015 - type Root; 1016 - type CreatedAt; 1017 - } 1018 - /// Empty state - all required fields are unset 1019 - pub struct Empty(()); 1020 - impl sealed::Sealed for Empty {} 1021 - impl State for Empty { 1022 - type Site = Unset; 1023 - type Root = Unset; 1024 - type CreatedAt = Unset; 1025 - } 1026 - ///State transition - sets the `site` field to Set 1027 - pub struct SetSite<S: State = Empty>(PhantomData<fn() -> S>); 1028 - impl<S: State> sealed::Sealed for SetSite<S> {} 1029 - impl<S: State> State for SetSite<S> { 1030 - type Site = Set<members::site>; 1031 - type Root = S::Root; 1032 - type CreatedAt = S::CreatedAt; 1033 - } 1034 - ///State transition - sets the `root` field to Set 1035 - pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>); 1036 - impl<S: State> sealed::Sealed for SetRoot<S> {} 1037 - impl<S: State> State for SetRoot<S> { 1038 - type Site = S::Site; 1039 - type Root = Set<members::root>; 1040 - type CreatedAt = S::CreatedAt; 1041 - } 1042 - ///State transition - sets the `created_at` field to Set 1043 - pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>); 1044 - impl<S: State> sealed::Sealed for SetCreatedAt<S> {} 1045 - impl<S: State> State for SetCreatedAt<S> { 1046 - type Site = S::Site; 1047 - type Root = S::Root; 1048 - type CreatedAt = Set<members::created_at>; 1049 - } 1050 - /// Marker types for field names 1051 - #[allow(non_camel_case_types)] 1052 - pub mod members { 1053 - ///Marker type for the `site` field 1054 - pub struct site(()); 1055 - ///Marker type for the `root` field 1056 - pub struct root(()); 1057 - ///Marker type for the `created_at` field 1058 - pub struct created_at(()); 1059 - } 1060 - } 1061 - 1062 - /// Builder for constructing an instance of this type 1063 - pub struct FsBuilder<'a, S: fs_state::State> { 1064 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1065 - __unsafe_private_named: ( 1066 - ::core::option::Option<jacquard_common::types::string::Datetime>, 1067 - ::core::option::Option<i64>, 1068 - ::core::option::Option<crate::place_wisp::fs::Directory<'a>>, 1069 - ::core::option::Option<jacquard_common::CowStr<'a>>, 1070 - ), 1071 - _phantom: ::core::marker::PhantomData<&'a ()>, 1072 - } 1073 - 1074 - impl<'a> Fs<'a> { 1075 - /// Create a new builder for this type 1076 - pub fn new() -> FsBuilder<'a, fs_state::Empty> { 1077 - FsBuilder::new() 1078 - } 1079 - } 1080 - 1081 - impl<'a> FsBuilder<'a, fs_state::Empty> { 1082 - /// Create a new builder with all fields unset 1083 - pub fn new() -> Self { 1084 - FsBuilder { 1085 - _phantom_state: ::core::marker::PhantomData, 1086 - __unsafe_private_named: (None, None, None, None), 1087 - _phantom: ::core::marker::PhantomData, 1088 - } 1089 - } 1090 - } 1091 - 1092 - impl<'a, S> FsBuilder<'a, S> 1093 - where 1094 - S: fs_state::State, 1095 - S::CreatedAt: fs_state::IsUnset, 1096 - { 1097 - /// Set the `createdAt` field (required) 1098 - pub fn created_at( 1099 - mut self, 1100 - value: impl Into<jacquard_common::types::string::Datetime>, 1101 - ) -> FsBuilder<'a, fs_state::SetCreatedAt<S>> { 1102 - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 1103 - FsBuilder { 1104 - _phantom_state: ::core::marker::PhantomData, 1105 - __unsafe_private_named: self.__unsafe_private_named, 1106 - _phantom: ::core::marker::PhantomData, 1107 - } 1108 - } 1109 - } 1110 - 1111 - impl<'a, S: fs_state::State> FsBuilder<'a, S> { 1112 - /// Set the `fileCount` field (optional) 1113 - pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self { 1114 - self.__unsafe_private_named.1 = value.into(); 1115 - self 1116 - } 1117 - /// Set the `fileCount` field to an Option value (optional) 1118 - pub fn maybe_file_count(mut self, value: Option<i64>) -> Self { 1119 - self.__unsafe_private_named.1 = value; 1120 - self 1121 - } 1122 - } 1123 - 1124 - impl<'a, S> FsBuilder<'a, S> 1125 - where 1126 - S: fs_state::State, 1127 - S::Root: fs_state::IsUnset, 1128 - { 1129 - /// Set the `root` field (required) 1130 - pub fn root( 1131 - mut self, 1132 - value: impl Into<crate::place_wisp::fs::Directory<'a>>, 1133 - ) -> FsBuilder<'a, fs_state::SetRoot<S>> { 1134 - self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); 1135 - FsBuilder { 1136 - _phantom_state: ::core::marker::PhantomData, 1137 - __unsafe_private_named: self.__unsafe_private_named, 1138 - _phantom: ::core::marker::PhantomData, 1139 - } 1140 - } 1141 - } 1142 - 1143 - impl<'a, S> FsBuilder<'a, S> 1144 - where 1145 - S: fs_state::State, 1146 - S::Site: fs_state::IsUnset, 1147 - { 1148 - /// Set the `site` field (required) 1149 - pub fn site( 1150 - mut self, 1151 - value: impl Into<jacquard_common::CowStr<'a>>, 1152 - ) -> FsBuilder<'a, fs_state::SetSite<S>> { 1153 - self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); 1154 - FsBuilder { 1155 - _phantom_state: ::core::marker::PhantomData, 1156 - __unsafe_private_named: self.__unsafe_private_named, 1157 - _phantom: ::core::marker::PhantomData, 1158 - } 1159 - } 1160 - } 1161 - 1162 - impl<'a, S> FsBuilder<'a, S> 1163 - where 1164 - S: fs_state::State, 1165 - S::Site: fs_state::IsSet, 1166 - S::Root: fs_state::IsSet, 1167 - S::CreatedAt: fs_state::IsSet, 1168 - { 1169 - /// Build the final struct 1170 - pub fn build(self) -> Fs<'a> { 1171 - Fs { 1172 - created_at: self.__unsafe_private_named.0.unwrap(), 1173 - file_count: self.__unsafe_private_named.1, 1174 - root: self.__unsafe_private_named.2.unwrap(), 1175 - site: self.__unsafe_private_named.3.unwrap(), 1176 - extra_data: Default::default(), 1177 - } 1178 - } 1179 - /// Build the final struct with custom extra_data 1180 - pub fn build_with_data( 1181 - self, 1182 - extra_data: std::collections::BTreeMap< 1183 - jacquard_common::smol_str::SmolStr, 1184 - jacquard_common::types::value::Data<'a>, 1185 - >, 1186 - ) -> Fs<'a> { 1187 - Fs { 1188 - created_at: self.__unsafe_private_named.0.unwrap(), 1189 - file_count: self.__unsafe_private_named.1, 1190 - root: self.__unsafe_private_named.2.unwrap(), 1191 - site: self.__unsafe_private_named.3.unwrap(), 1192 - extra_data: Some(extra_data), 1193 - } 1194 - } 1195 - } 1196 - 1197 - impl<'a> Fs<'a> { 1198 - pub fn uri( 1199 - uri: impl Into<jacquard_common::CowStr<'a>>, 1200 - ) -> Result< 1201 - jacquard_common::types::uri::RecordUri<'a, FsRecord>, 1202 - jacquard_common::types::uri::UriError, 1203 - > { 1204 - jacquard_common::types::uri::RecordUri::try_from_uri( 1205 - jacquard_common::types::string::AtUri::new_cow(uri.into())?, 1206 - ) 1207 - } 1208 - } 1209 - 1210 - /// Typed wrapper for GetRecord response with this collection's record type. 1211 - #[derive( 1212 - serde::Serialize, 1213 - serde::Deserialize, 1214 - Debug, 1215 - Clone, 1216 - PartialEq, 1217 - Eq, 1218 - jacquard_derive::IntoStatic 1219 - )] 1220 - #[serde(rename_all = "camelCase")] 1221 - pub struct FsGetRecordOutput<'a> { 1222 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 1223 - #[serde(borrow)] 1224 - pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 1225 - #[serde(borrow)] 1226 - pub uri: jacquard_common::types::string::AtUri<'a>, 1227 - #[serde(borrow)] 1228 - pub value: Fs<'a>, 1229 - } 1230 - 1231 - impl From<FsGetRecordOutput<'_>> for Fs<'_> { 1232 - fn from(output: FsGetRecordOutput<'_>) -> Self { 1233 - use jacquard_common::IntoStatic; 1234 - output.value.into_static() 1235 - } 1236 - } 1237 - 1238 - impl jacquard_common::types::collection::Collection for Fs<'_> { 1239 - const NSID: &'static str = "place.wisp.fs"; 1240 - type Record = FsRecord; 1241 - } 1242 - 1243 - /// Marker type for deserializing records from this collection. 1244 - #[derive(Debug, serde::Serialize, serde::Deserialize)] 1245 - pub struct FsRecord; 1246 - impl jacquard_common::xrpc::XrpcResp for FsRecord { 1247 - const NSID: &'static str = "place.wisp.fs"; 1248 - const ENCODING: &'static str = "application/json"; 1249 - type Output<'de> = FsGetRecordOutput<'de>; 1250 - type Err<'de> = jacquard_common::types::collection::RecordError<'de>; 1251 - } 1252 - 1253 - impl jacquard_common::types::collection::Collection for FsRecord { 1254 - const NSID: &'static str = "place.wisp.fs"; 1255 - type Record = FsRecord; 1256 - } 1257 - 1258 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Fs<'a> { 1259 - fn nsid() -> &'static str { 1260 - "place.wisp.fs" 1261 - } 1262 - fn def_name() -> &'static str { 1263 - "main" 1264 - } 1265 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1266 - lexicon_doc_place_wisp_fs() 1267 - } 1268 - fn validate( 1269 - &self, 1270 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1271 - if let Some(ref value) = self.file_count { 1272 - if *value > 1000i64 { 1273 - return Err(::jacquard_lexicon::validation::ConstraintError::Maximum { 1274 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1275 - "file_count", 1276 - ), 1277 - max: 1000i64, 1278 - actual: *value, 1279 - }); 1280 - } 1281 - } 1282 - if let Some(ref value) = self.file_count { 1283 - if *value < 0i64 { 1284 - return Err(::jacquard_lexicon::validation::ConstraintError::Minimum { 1285 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1286 - "file_count", 1287 - ), 1288 - min: 0i64, 1289 - actual: *value, 1290 - }); 1291 - } 1292 - } 1293 - Ok(()) 1294 - } 1295 - } 1296 - 1297 - #[jacquard_derive::lexicon] 1298 - #[derive( 1299 - serde::Serialize, 1300 - serde::Deserialize, 1301 - Debug, 1302 - Clone, 1303 - PartialEq, 1304 - Eq, 1305 - jacquard_derive::IntoStatic 1306 - )] 1307 - #[serde(rename_all = "camelCase")] 1308 - pub struct Subfs<'a> { 1309 - /// If true, the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false (default), the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure. 1310 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 1311 - pub flat: Option<bool>, 1312 - /// AT-URI pointing to a place.wisp.subfs record containing this subtree. 1313 - #[serde(borrow)] 1314 - pub subject: jacquard_common::types::string::AtUri<'a>, 1315 - #[serde(borrow)] 1316 - pub r#type: jacquard_common::CowStr<'a>, 1317 - } 1318 - 1319 - pub mod subfs_state { 1320 - 1321 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 1322 - #[allow(unused)] 1323 - use ::core::marker::PhantomData; 1324 - mod sealed { 1325 - pub trait Sealed {} 1326 - } 1327 - /// State trait tracking which required fields have been set 1328 - pub trait State: sealed::Sealed { 1329 - type Type; 1330 - type Subject; 1331 - } 1332 - /// Empty state - all required fields are unset 1333 - pub struct Empty(()); 1334 - impl sealed::Sealed for Empty {} 1335 - impl State for Empty { 1336 - type Type = Unset; 1337 - type Subject = Unset; 1338 - } 1339 - ///State transition - sets the `type` field to Set 1340 - pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 1341 - impl<S: State> sealed::Sealed for SetType<S> {} 1342 - impl<S: State> State for SetType<S> { 1343 - type Type = Set<members::r#type>; 1344 - type Subject = S::Subject; 1345 - } 1346 - ///State transition - sets the `subject` field to Set 1347 - pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>); 1348 - impl<S: State> sealed::Sealed for SetSubject<S> {} 1349 - impl<S: State> State for SetSubject<S> { 1350 - type Type = S::Type; 1351 - type Subject = Set<members::subject>; 1352 - } 1353 - /// Marker types for field names 1354 - #[allow(non_camel_case_types)] 1355 - pub mod members { 1356 - ///Marker type for the `type` field 1357 - pub struct r#type(()); 1358 - ///Marker type for the `subject` field 1359 - pub struct subject(()); 1360 - } 1361 - } 1362 - 1363 - /// Builder for constructing an instance of this type 1364 - pub struct SubfsBuilder<'a, S: subfs_state::State> { 1365 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1366 - __unsafe_private_named: ( 1367 - ::core::option::Option<bool>, 1368 - ::core::option::Option<jacquard_common::types::string::AtUri<'a>>, 1369 - ::core::option::Option<jacquard_common::CowStr<'a>>, 1370 - ), 1371 - _phantom: ::core::marker::PhantomData<&'a ()>, 1372 - } 1373 - 1374 - impl<'a> Subfs<'a> { 1375 - /// Create a new builder for this type 1376 - pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> { 1377 - SubfsBuilder::new() 1378 - } 1379 - } 1380 - 1381 - impl<'a> SubfsBuilder<'a, subfs_state::Empty> { 1382 - /// Create a new builder with all fields unset 1383 - pub fn new() -> Self { 1384 - SubfsBuilder { 1385 - _phantom_state: ::core::marker::PhantomData, 1386 - __unsafe_private_named: (None, None, None), 1387 - _phantom: ::core::marker::PhantomData, 1388 - } 1389 - } 1390 - } 1391 - 1392 - impl<'a, S: subfs_state::State> SubfsBuilder<'a, S> { 1393 - /// Set the `flat` field (optional) 1394 - pub fn flat(mut self, value: impl Into<Option<bool>>) -> Self { 1395 - self.__unsafe_private_named.0 = value.into(); 1396 - self 1397 - } 1398 - /// Set the `flat` field to an Option value (optional) 1399 - pub fn maybe_flat(mut self, value: Option<bool>) -> Self { 1400 - self.__unsafe_private_named.0 = value; 1401 - self 1402 - } 1403 - } 1404 - 1405 - impl<'a, S> SubfsBuilder<'a, S> 1406 - where 1407 - S: subfs_state::State, 1408 - S::Subject: subfs_state::IsUnset, 1409 - { 1410 - /// Set the `subject` field (required) 1411 - pub fn subject( 1412 - mut self, 1413 - value: impl Into<jacquard_common::types::string::AtUri<'a>>, 1414 - ) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> { 1415 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 1416 - SubfsBuilder { 1417 - _phantom_state: ::core::marker::PhantomData, 1418 - __unsafe_private_named: self.__unsafe_private_named, 1419 - _phantom: ::core::marker::PhantomData, 1420 - } 1421 - } 1422 - } 1423 - 1424 - impl<'a, S> SubfsBuilder<'a, S> 1425 - where 1426 - S: subfs_state::State, 1427 - S::Type: subfs_state::IsUnset, 1428 - { 1429 - /// Set the `type` field (required) 1430 - pub fn r#type( 1431 - mut self, 1432 - value: impl Into<jacquard_common::CowStr<'a>>, 1433 - ) -> SubfsBuilder<'a, subfs_state::SetType<S>> { 1434 - self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); 1435 - SubfsBuilder { 1436 - _phantom_state: ::core::marker::PhantomData, 1437 - __unsafe_private_named: self.__unsafe_private_named, 1438 - _phantom: ::core::marker::PhantomData, 1439 - } 1440 - } 1441 - } 1442 - 1443 - impl<'a, S> SubfsBuilder<'a, S> 1444 - where 1445 - S: subfs_state::State, 1446 - S::Type: subfs_state::IsSet, 1447 - S::Subject: subfs_state::IsSet, 1448 - { 1449 - /// Build the final struct 1450 - pub fn build(self) -> Subfs<'a> { 1451 - Subfs { 1452 - flat: self.__unsafe_private_named.0, 1453 - subject: self.__unsafe_private_named.1.unwrap(), 1454 - r#type: self.__unsafe_private_named.2.unwrap(), 1455 - extra_data: Default::default(), 1456 - } 1457 - } 1458 - /// Build the final struct with custom extra_data 1459 - pub fn build_with_data( 1460 - self, 1461 - extra_data: std::collections::BTreeMap< 1462 - jacquard_common::smol_str::SmolStr, 1463 - jacquard_common::types::value::Data<'a>, 1464 - >, 1465 - ) -> Subfs<'a> { 1466 - Subfs { 1467 - flat: self.__unsafe_private_named.0, 1468 - subject: self.__unsafe_private_named.1.unwrap(), 1469 - r#type: self.__unsafe_private_named.2.unwrap(), 1470 - extra_data: Some(extra_data), 1471 - } 1472 - } 1473 - } 1474 - 1475 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> { 1476 - fn nsid() -> &'static str { 1477 - "place.wisp.fs" 1478 - } 1479 - fn def_name() -> &'static str { 1480 - "subfs" 1481 - } 1482 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1483 - lexicon_doc_place_wisp_fs() 1484 - } 1485 - fn validate( 1486 - &self, 1487 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1488 - Ok(()) 1489 - } 1490 - }
···
-653
cli/src/place_wisp/settings.rs
··· 1 - // @generated by jacquard-lexicon. DO NOT EDIT. 2 - // 3 - // Lexicon: place.wisp.settings 4 - // 5 - // This file was automatically generated from Lexicon schemas. 6 - // Any manual changes will be overwritten on the next regeneration. 7 - 8 - /// Custom HTTP header configuration 9 - #[jacquard_derive::lexicon] 10 - #[derive( 11 - serde::Serialize, 12 - serde::Deserialize, 13 - Debug, 14 - Clone, 15 - PartialEq, 16 - Eq, 17 - jacquard_derive::IntoStatic, 18 - Default 19 - )] 20 - #[serde(rename_all = "camelCase")] 21 - pub struct CustomHeader<'a> { 22 - /// HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options') 23 - #[serde(borrow)] 24 - pub name: jacquard_common::CowStr<'a>, 25 - /// Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths. 26 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 27 - #[serde(borrow)] 28 - pub path: std::option::Option<jacquard_common::CowStr<'a>>, 29 - /// HTTP header value 30 - #[serde(borrow)] 31 - pub value: jacquard_common::CowStr<'a>, 32 - } 33 - 34 - fn lexicon_doc_place_wisp_settings() -> ::jacquard_lexicon::lexicon::LexiconDoc< 35 - 'static, 36 - > { 37 - ::jacquard_lexicon::lexicon::LexiconDoc { 38 - lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 39 - id: ::jacquard_common::CowStr::new_static("place.wisp.settings"), 40 - revision: None, 41 - description: None, 42 - defs: { 43 - let mut map = ::std::collections::BTreeMap::new(); 44 - map.insert( 45 - ::jacquard_common::smol_str::SmolStr::new_static("customHeader"), 46 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 47 - description: Some( 48 - ::jacquard_common::CowStr::new_static( 49 - "Custom HTTP header configuration", 50 - ), 51 - ), 52 - required: Some( 53 - vec![ 54 - ::jacquard_common::smol_str::SmolStr::new_static("name"), 55 - ::jacquard_common::smol_str::SmolStr::new_static("value") 56 - ], 57 - ), 58 - nullable: None, 59 - properties: { 60 - #[allow(unused_mut)] 61 - let mut map = ::std::collections::BTreeMap::new(); 62 - map.insert( 63 - ::jacquard_common::smol_str::SmolStr::new_static("name"), 64 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 65 - description: Some( 66 - ::jacquard_common::CowStr::new_static( 67 - "HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')", 68 - ), 69 - ), 70 - format: None, 71 - default: None, 72 - min_length: None, 73 - max_length: Some(100usize), 74 - min_graphemes: None, 75 - max_graphemes: None, 76 - r#enum: None, 77 - r#const: None, 78 - known_values: None, 79 - }), 80 - ); 81 - map.insert( 82 - ::jacquard_common::smol_str::SmolStr::new_static("path"), 83 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 84 - description: Some( 85 - ::jacquard_common::CowStr::new_static( 86 - "Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.", 87 - ), 88 - ), 89 - format: None, 90 - default: None, 91 - min_length: None, 92 - max_length: Some(500usize), 93 - min_graphemes: None, 94 - max_graphemes: None, 95 - r#enum: None, 96 - r#const: None, 97 - known_values: None, 98 - }), 99 - ); 100 - map.insert( 101 - ::jacquard_common::smol_str::SmolStr::new_static("value"), 102 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 103 - description: Some( 104 - ::jacquard_common::CowStr::new_static("HTTP header value"), 105 - ), 106 - format: None, 107 - default: None, 108 - min_length: None, 109 - max_length: Some(1000usize), 110 - min_graphemes: None, 111 - max_graphemes: None, 112 - r#enum: None, 113 - r#const: None, 114 - known_values: None, 115 - }), 116 - ); 117 - map 118 - }, 119 - }), 120 - ); 121 - map.insert( 122 - ::jacquard_common::smol_str::SmolStr::new_static("main"), 123 - ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { 124 - description: Some( 125 - ::jacquard_common::CowStr::new_static( 126 - "Configuration settings for a static site hosted on wisp.place", 127 - ), 128 - ), 129 - key: Some(::jacquard_common::CowStr::new_static("any")), 130 - record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { 131 - description: None, 132 - required: None, 133 - nullable: None, 134 - properties: { 135 - #[allow(unused_mut)] 136 - let mut map = ::std::collections::BTreeMap::new(); 137 - map.insert( 138 - ::jacquard_common::smol_str::SmolStr::new_static( 139 - "cleanUrls", 140 - ), 141 - ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 142 - description: None, 143 - default: None, 144 - r#const: None, 145 - }), 146 - ); 147 - map.insert( 148 - ::jacquard_common::smol_str::SmolStr::new_static( 149 - "custom404", 150 - ), 151 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 152 - description: Some( 153 - ::jacquard_common::CowStr::new_static( 154 - "Custom 404 error page file path. Incompatible with directoryListing and spaMode.", 155 - ), 156 - ), 157 - format: None, 158 - default: None, 159 - min_length: None, 160 - max_length: Some(500usize), 161 - min_graphemes: None, 162 - max_graphemes: None, 163 - r#enum: None, 164 - r#const: None, 165 - known_values: None, 166 - }), 167 - ); 168 - map.insert( 169 - ::jacquard_common::smol_str::SmolStr::new_static( 170 - "directoryListing", 171 - ), 172 - ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 173 - description: None, 174 - default: None, 175 - r#const: None, 176 - }), 177 - ); 178 - map.insert( 179 - ::jacquard_common::smol_str::SmolStr::new_static("headers"), 180 - ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 181 - description: Some( 182 - ::jacquard_common::CowStr::new_static( 183 - "Custom HTTP headers to set on responses", 184 - ), 185 - ), 186 - items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { 187 - description: None, 188 - r#ref: ::jacquard_common::CowStr::new_static( 189 - "#customHeader", 190 - ), 191 - }), 192 - min_length: None, 193 - max_length: Some(50usize), 194 - }), 195 - ); 196 - map.insert( 197 - ::jacquard_common::smol_str::SmolStr::new_static( 198 - "indexFiles", 199 - ), 200 - ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 201 - description: Some( 202 - ::jacquard_common::CowStr::new_static( 203 - "Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.", 204 - ), 205 - ), 206 - items: ::jacquard_lexicon::lexicon::LexArrayItem::String(::jacquard_lexicon::lexicon::LexString { 207 - description: None, 208 - format: None, 209 - default: None, 210 - min_length: None, 211 - max_length: Some(255usize), 212 - min_graphemes: None, 213 - max_graphemes: None, 214 - r#enum: None, 215 - r#const: None, 216 - known_values: None, 217 - }), 218 - min_length: None, 219 - max_length: Some(10usize), 220 - }), 221 - ); 222 - map.insert( 223 - ::jacquard_common::smol_str::SmolStr::new_static("spaMode"), 224 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 225 - description: Some( 226 - ::jacquard_common::CowStr::new_static( 227 - "File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.", 228 - ), 229 - ), 230 - format: None, 231 - default: None, 232 - min_length: None, 233 - max_length: Some(500usize), 234 - min_graphemes: None, 235 - max_graphemes: None, 236 - r#enum: None, 237 - r#const: None, 238 - known_values: None, 239 - }), 240 - ); 241 - map 242 - }, 243 - }), 244 - }), 245 - ); 246 - map 247 - }, 248 - } 249 - } 250 - 251 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for CustomHeader<'a> { 252 - fn nsid() -> &'static str { 253 - "place.wisp.settings" 254 - } 255 - fn def_name() -> &'static str { 256 - "customHeader" 257 - } 258 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 259 - lexicon_doc_place_wisp_settings() 260 - } 261 - fn validate( 262 - &self, 263 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 264 - { 265 - let value = &self.name; 266 - #[allow(unused_comparisons)] 267 - if <str>::len(value.as_ref()) > 100usize { 268 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 269 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 270 - "name", 271 - ), 272 - max: 100usize, 273 - actual: <str>::len(value.as_ref()), 274 - }); 275 - } 276 - } 277 - if let Some(ref value) = self.path { 278 - #[allow(unused_comparisons)] 279 - if <str>::len(value.as_ref()) > 500usize { 280 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 281 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 282 - "path", 283 - ), 284 - max: 500usize, 285 - actual: <str>::len(value.as_ref()), 286 - }); 287 - } 288 - } 289 - { 290 - let value = &self.value; 291 - #[allow(unused_comparisons)] 292 - if <str>::len(value.as_ref()) > 1000usize { 293 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 294 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 295 - "value", 296 - ), 297 - max: 1000usize, 298 - actual: <str>::len(value.as_ref()), 299 - }); 300 - } 301 - } 302 - Ok(()) 303 - } 304 - } 305 - 306 - /// Configuration settings for a static site hosted on wisp.place 307 - #[jacquard_derive::lexicon] 308 - #[derive( 309 - serde::Serialize, 310 - serde::Deserialize, 311 - Debug, 312 - Clone, 313 - PartialEq, 314 - Eq, 315 - jacquard_derive::IntoStatic 316 - )] 317 - #[serde(rename_all = "camelCase")] 318 - pub struct Settings<'a> { 319 - /// Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically. 320 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 321 - pub clean_urls: std::option::Option<bool>, 322 - /// Custom 404 error page file path. Incompatible with directoryListing and spaMode. 323 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 324 - #[serde(borrow)] 325 - pub custom404: std::option::Option<jacquard_common::CowStr<'a>>, 326 - /// Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode. 327 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 328 - pub directory_listing: std::option::Option<bool>, 329 - /// Custom HTTP headers to set on responses 330 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 331 - #[serde(borrow)] 332 - pub headers: std::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>, 333 - /// Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified. 334 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 335 - #[serde(borrow)] 336 - pub index_files: std::option::Option<Vec<jacquard_common::CowStr<'a>>>, 337 - /// File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404. 338 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 339 - #[serde(borrow)] 340 - pub spa_mode: std::option::Option<jacquard_common::CowStr<'a>>, 341 - } 342 - 343 - pub mod settings_state { 344 - 345 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 346 - #[allow(unused)] 347 - use ::core::marker::PhantomData; 348 - mod sealed { 349 - pub trait Sealed {} 350 - } 351 - /// State trait tracking which required fields have been set 352 - pub trait State: sealed::Sealed {} 353 - /// Empty state - all required fields are unset 354 - pub struct Empty(()); 355 - impl sealed::Sealed for Empty {} 356 - impl State for Empty {} 357 - /// Marker types for field names 358 - #[allow(non_camel_case_types)] 359 - pub mod members {} 360 - } 361 - 362 - /// Builder for constructing an instance of this type 363 - pub struct SettingsBuilder<'a, S: settings_state::State> { 364 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 365 - __unsafe_private_named: ( 366 - ::core::option::Option<bool>, 367 - ::core::option::Option<jacquard_common::CowStr<'a>>, 368 - ::core::option::Option<bool>, 369 - ::core::option::Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>, 370 - ::core::option::Option<Vec<jacquard_common::CowStr<'a>>>, 371 - ::core::option::Option<jacquard_common::CowStr<'a>>, 372 - ), 373 - _phantom: ::core::marker::PhantomData<&'a ()>, 374 - } 375 - 376 - impl<'a> Settings<'a> { 377 - /// Create a new builder for this type 378 - pub fn new() -> SettingsBuilder<'a, settings_state::Empty> { 379 - SettingsBuilder::new() 380 - } 381 - } 382 - 383 - impl<'a> SettingsBuilder<'a, settings_state::Empty> { 384 - /// Create a new builder with all fields unset 385 - pub fn new() -> Self { 386 - SettingsBuilder { 387 - _phantom_state: ::core::marker::PhantomData, 388 - __unsafe_private_named: (None, None, None, None, None, None), 389 - _phantom: ::core::marker::PhantomData, 390 - } 391 - } 392 - } 393 - 394 - impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 395 - /// Set the `cleanUrls` field (optional) 396 - pub fn clean_urls(mut self, value: impl Into<Option<bool>>) -> Self { 397 - self.__unsafe_private_named.0 = value.into(); 398 - self 399 - } 400 - /// Set the `cleanUrls` field to an Option value (optional) 401 - pub fn maybe_clean_urls(mut self, value: Option<bool>) -> Self { 402 - self.__unsafe_private_named.0 = value; 403 - self 404 - } 405 - } 406 - 407 - impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 408 - /// Set the `custom404` field (optional) 409 - pub fn custom404( 410 - mut self, 411 - value: impl Into<Option<jacquard_common::CowStr<'a>>>, 412 - ) -> Self { 413 - self.__unsafe_private_named.1 = value.into(); 414 - self 415 - } 416 - /// Set the `custom404` field to an Option value (optional) 417 - pub fn maybe_custom404( 418 - mut self, 419 - value: Option<jacquard_common::CowStr<'a>>, 420 - ) -> Self { 421 - self.__unsafe_private_named.1 = value; 422 - self 423 - } 424 - } 425 - 426 - impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 427 - /// Set the `directoryListing` field (optional) 428 - pub fn directory_listing(mut self, value: impl Into<Option<bool>>) -> Self { 429 - self.__unsafe_private_named.2 = value.into(); 430 - self 431 - } 432 - /// Set the `directoryListing` field to an Option value (optional) 433 - pub fn maybe_directory_listing(mut self, value: Option<bool>) -> Self { 434 - self.__unsafe_private_named.2 = value; 435 - self 436 - } 437 - } 438 - 439 - impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 440 - /// Set the `headers` field (optional) 441 - pub fn headers( 442 - mut self, 443 - value: impl Into<Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>>, 444 - ) -> Self { 445 - self.__unsafe_private_named.3 = value.into(); 446 - self 447 - } 448 - /// Set the `headers` field to an Option value (optional) 449 - pub fn maybe_headers( 450 - mut self, 451 - value: Option<Vec<crate::place_wisp::settings::CustomHeader<'a>>>, 452 - ) -> Self { 453 - self.__unsafe_private_named.3 = value; 454 - self 455 - } 456 - } 457 - 458 - impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 459 - /// Set the `indexFiles` field (optional) 460 - pub fn index_files( 461 - mut self, 462 - value: impl Into<Option<Vec<jacquard_common::CowStr<'a>>>>, 463 - ) -> Self { 464 - self.__unsafe_private_named.4 = value.into(); 465 - self 466 - } 467 - /// Set the `indexFiles` field to an Option value (optional) 468 - pub fn maybe_index_files( 469 - mut self, 470 - value: Option<Vec<jacquard_common::CowStr<'a>>>, 471 - ) -> Self { 472 - self.__unsafe_private_named.4 = value; 473 - self 474 - } 475 - } 476 - 477 - impl<'a, S: settings_state::State> SettingsBuilder<'a, S> { 478 - /// Set the `spaMode` field (optional) 479 - pub fn spa_mode( 480 - mut self, 481 - value: impl Into<Option<jacquard_common::CowStr<'a>>>, 482 - ) -> Self { 483 - self.__unsafe_private_named.5 = value.into(); 484 - self 485 - } 486 - /// Set the `spaMode` field to an Option value (optional) 487 - pub fn maybe_spa_mode(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 488 - self.__unsafe_private_named.5 = value; 489 - self 490 - } 491 - } 492 - 493 - impl<'a, S> SettingsBuilder<'a, S> 494 - where 495 - S: settings_state::State, 496 - { 497 - /// Build the final struct 498 - pub fn build(self) -> Settings<'a> { 499 - Settings { 500 - clean_urls: self.__unsafe_private_named.0, 501 - custom404: self.__unsafe_private_named.1, 502 - directory_listing: self.__unsafe_private_named.2, 503 - headers: self.__unsafe_private_named.3, 504 - index_files: self.__unsafe_private_named.4, 505 - spa_mode: self.__unsafe_private_named.5, 506 - extra_data: Default::default(), 507 - } 508 - } 509 - /// Build the final struct with custom extra_data 510 - pub fn build_with_data( 511 - self, 512 - extra_data: std::collections::BTreeMap< 513 - jacquard_common::smol_str::SmolStr, 514 - jacquard_common::types::value::Data<'a>, 515 - >, 516 - ) -> Settings<'a> { 517 - Settings { 518 - clean_urls: self.__unsafe_private_named.0, 519 - custom404: self.__unsafe_private_named.1, 520 - directory_listing: self.__unsafe_private_named.2, 521 - headers: self.__unsafe_private_named.3, 522 - index_files: self.__unsafe_private_named.4, 523 - spa_mode: self.__unsafe_private_named.5, 524 - extra_data: Some(extra_data), 525 - } 526 - } 527 - } 528 - 529 - impl<'a> Settings<'a> { 530 - pub fn uri( 531 - uri: impl Into<jacquard_common::CowStr<'a>>, 532 - ) -> Result< 533 - jacquard_common::types::uri::RecordUri<'a, SettingsRecord>, 534 - jacquard_common::types::uri::UriError, 535 - > { 536 - jacquard_common::types::uri::RecordUri::try_from_uri( 537 - jacquard_common::types::string::AtUri::new_cow(uri.into())?, 538 - ) 539 - } 540 - } 541 - 542 - /// Typed wrapper for GetRecord response with this collection's record type. 543 - #[derive( 544 - serde::Serialize, 545 - serde::Deserialize, 546 - Debug, 547 - Clone, 548 - PartialEq, 549 - Eq, 550 - jacquard_derive::IntoStatic 551 - )] 552 - #[serde(rename_all = "camelCase")] 553 - pub struct SettingsGetRecordOutput<'a> { 554 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 555 - #[serde(borrow)] 556 - pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 557 - #[serde(borrow)] 558 - pub uri: jacquard_common::types::string::AtUri<'a>, 559 - #[serde(borrow)] 560 - pub value: Settings<'a>, 561 - } 562 - 563 - impl From<SettingsGetRecordOutput<'_>> for Settings<'_> { 564 - fn from(output: SettingsGetRecordOutput<'_>) -> Self { 565 - use jacquard_common::IntoStatic; 566 - output.value.into_static() 567 - } 568 - } 569 - 570 - impl jacquard_common::types::collection::Collection for Settings<'_> { 571 - const NSID: &'static str = "place.wisp.settings"; 572 - type Record = SettingsRecord; 573 - } 574 - 575 - /// Marker type for deserializing records from this collection. 576 - #[derive(Debug, serde::Serialize, serde::Deserialize)] 577 - pub struct SettingsRecord; 578 - impl jacquard_common::xrpc::XrpcResp for SettingsRecord { 579 - const NSID: &'static str = "place.wisp.settings"; 580 - const ENCODING: &'static str = "application/json"; 581 - type Output<'de> = SettingsGetRecordOutput<'de>; 582 - type Err<'de> = jacquard_common::types::collection::RecordError<'de>; 583 - } 584 - 585 - impl jacquard_common::types::collection::Collection for SettingsRecord { 586 - const NSID: &'static str = "place.wisp.settings"; 587 - type Record = SettingsRecord; 588 - } 589 - 590 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Settings<'a> { 591 - fn nsid() -> &'static str { 592 - "place.wisp.settings" 593 - } 594 - fn def_name() -> &'static str { 595 - "main" 596 - } 597 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 598 - lexicon_doc_place_wisp_settings() 599 - } 600 - fn validate( 601 - &self, 602 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 603 - if let Some(ref value) = self.custom404 { 604 - #[allow(unused_comparisons)] 605 - if <str>::len(value.as_ref()) > 500usize { 606 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 607 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 608 - "custom404", 609 - ), 610 - max: 500usize, 611 - actual: <str>::len(value.as_ref()), 612 - }); 613 - } 614 - } 615 - if let Some(ref value) = self.headers { 616 - #[allow(unused_comparisons)] 617 - if value.len() > 50usize { 618 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 619 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 620 - "headers", 621 - ), 622 - max: 50usize, 623 - actual: value.len(), 624 - }); 625 - } 626 - } 627 - if let Some(ref value) = self.index_files { 628 - #[allow(unused_comparisons)] 629 - if value.len() > 10usize { 630 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 631 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 632 - "index_files", 633 - ), 634 - max: 10usize, 635 - actual: value.len(), 636 - }); 637 - } 638 - } 639 - if let Some(ref value) = self.spa_mode { 640 - #[allow(unused_comparisons)] 641 - if <str>::len(value.as_ref()) > 500usize { 642 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 643 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 644 - "spa_mode", 645 - ), 646 - max: 500usize, 647 - actual: <str>::len(value.as_ref()), 648 - }); 649 - } 650 - } 651 - Ok(()) 652 - } 653 - }
···
-1408
cli/src/place_wisp/subfs.rs
··· 1 - // @generated by jacquard-lexicon. DO NOT EDIT. 2 - // 3 - // Lexicon: place.wisp.subfs 4 - // 5 - // This file was automatically generated from Lexicon schemas. 6 - // Any manual changes will be overwritten on the next regeneration. 7 - 8 - #[jacquard_derive::lexicon] 9 - #[derive( 10 - serde::Serialize, 11 - serde::Deserialize, 12 - Debug, 13 - Clone, 14 - PartialEq, 15 - Eq, 16 - jacquard_derive::IntoStatic 17 - )] 18 - #[serde(rename_all = "camelCase")] 19 - pub struct Directory<'a> { 20 - #[serde(borrow)] 21 - pub entries: Vec<crate::place_wisp::subfs::Entry<'a>>, 22 - #[serde(borrow)] 23 - pub r#type: jacquard_common::CowStr<'a>, 24 - } 25 - 26 - pub mod directory_state { 27 - 28 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 29 - #[allow(unused)] 30 - use ::core::marker::PhantomData; 31 - mod sealed { 32 - pub trait Sealed {} 33 - } 34 - /// State trait tracking which required fields have been set 35 - pub trait State: sealed::Sealed { 36 - type Type; 37 - type Entries; 38 - } 39 - /// Empty state - all required fields are unset 40 - pub struct Empty(()); 41 - impl sealed::Sealed for Empty {} 42 - impl State for Empty { 43 - type Type = Unset; 44 - type Entries = Unset; 45 - } 46 - ///State transition - sets the `type` field to Set 47 - pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 48 - impl<S: State> sealed::Sealed for SetType<S> {} 49 - impl<S: State> State for SetType<S> { 50 - type Type = Set<members::r#type>; 51 - type Entries = S::Entries; 52 - } 53 - ///State transition - sets the `entries` field to Set 54 - pub struct SetEntries<S: State = Empty>(PhantomData<fn() -> S>); 55 - impl<S: State> sealed::Sealed for SetEntries<S> {} 56 - impl<S: State> State for SetEntries<S> { 57 - type Type = S::Type; 58 - type Entries = Set<members::entries>; 59 - } 60 - /// Marker types for field names 61 - #[allow(non_camel_case_types)] 62 - pub mod members { 63 - ///Marker type for the `type` field 64 - pub struct r#type(()); 65 - ///Marker type for the `entries` field 66 - pub struct entries(()); 67 - } 68 - } 69 - 70 - /// Builder for constructing an instance of this type 71 - pub struct DirectoryBuilder<'a, S: directory_state::State> { 72 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 73 - __unsafe_private_named: ( 74 - ::core::option::Option<Vec<crate::place_wisp::subfs::Entry<'a>>>, 75 - ::core::option::Option<jacquard_common::CowStr<'a>>, 76 - ), 77 - _phantom: ::core::marker::PhantomData<&'a ()>, 78 - } 79 - 80 - impl<'a> Directory<'a> { 81 - /// Create a new builder for this type 82 - pub fn new() -> DirectoryBuilder<'a, directory_state::Empty> { 83 - DirectoryBuilder::new() 84 - } 85 - } 86 - 87 - impl<'a> DirectoryBuilder<'a, directory_state::Empty> { 88 - /// Create a new builder with all fields unset 89 - pub fn new() -> Self { 90 - DirectoryBuilder { 91 - _phantom_state: ::core::marker::PhantomData, 92 - __unsafe_private_named: (None, None), 93 - _phantom: ::core::marker::PhantomData, 94 - } 95 - } 96 - } 97 - 98 - impl<'a, S> DirectoryBuilder<'a, S> 99 - where 100 - S: directory_state::State, 101 - S::Entries: directory_state::IsUnset, 102 - { 103 - /// Set the `entries` field (required) 104 - pub fn entries( 105 - mut self, 106 - value: impl Into<Vec<crate::place_wisp::subfs::Entry<'a>>>, 107 - ) -> DirectoryBuilder<'a, directory_state::SetEntries<S>> { 108 - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 109 - DirectoryBuilder { 110 - _phantom_state: ::core::marker::PhantomData, 111 - __unsafe_private_named: self.__unsafe_private_named, 112 - _phantom: ::core::marker::PhantomData, 113 - } 114 - } 115 - } 116 - 117 - impl<'a, S> DirectoryBuilder<'a, S> 118 - where 119 - S: directory_state::State, 120 - S::Type: directory_state::IsUnset, 121 - { 122 - /// Set the `type` field (required) 123 - pub fn r#type( 124 - mut self, 125 - value: impl Into<jacquard_common::CowStr<'a>>, 126 - ) -> DirectoryBuilder<'a, directory_state::SetType<S>> { 127 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 128 - DirectoryBuilder { 129 - _phantom_state: ::core::marker::PhantomData, 130 - __unsafe_private_named: self.__unsafe_private_named, 131 - _phantom: ::core::marker::PhantomData, 132 - } 133 - } 134 - } 135 - 136 - impl<'a, S> DirectoryBuilder<'a, S> 137 - where 138 - S: directory_state::State, 139 - S::Type: directory_state::IsSet, 140 - S::Entries: directory_state::IsSet, 141 - { 142 - /// Build the final struct 143 - pub fn build(self) -> Directory<'a> { 144 - Directory { 145 - entries: self.__unsafe_private_named.0.unwrap(), 146 - r#type: self.__unsafe_private_named.1.unwrap(), 147 - extra_data: Default::default(), 148 - } 149 - } 150 - /// Build the final struct with custom extra_data 151 - pub fn build_with_data( 152 - self, 153 - extra_data: std::collections::BTreeMap< 154 - jacquard_common::smol_str::SmolStr, 155 - jacquard_common::types::value::Data<'a>, 156 - >, 157 - ) -> Directory<'a> { 158 - Directory { 159 - entries: self.__unsafe_private_named.0.unwrap(), 160 - r#type: self.__unsafe_private_named.1.unwrap(), 161 - extra_data: Some(extra_data), 162 - } 163 - } 164 - } 165 - 166 - fn lexicon_doc_place_wisp_subfs() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 167 - ::jacquard_lexicon::lexicon::LexiconDoc { 168 - lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 169 - id: ::jacquard_common::CowStr::new_static("place.wisp.subfs"), 170 - revision: None, 171 - description: None, 172 - defs: { 173 - let mut map = ::std::collections::BTreeMap::new(); 174 - map.insert( 175 - ::jacquard_common::smol_str::SmolStr::new_static("directory"), 176 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 177 - description: None, 178 - required: Some( 179 - vec![ 180 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 181 - ::jacquard_common::smol_str::SmolStr::new_static("entries") 182 - ], 183 - ), 184 - nullable: None, 185 - properties: { 186 - #[allow(unused_mut)] 187 - let mut map = ::std::collections::BTreeMap::new(); 188 - map.insert( 189 - ::jacquard_common::smol_str::SmolStr::new_static("entries"), 190 - ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 191 - description: None, 192 - items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { 193 - description: None, 194 - r#ref: ::jacquard_common::CowStr::new_static("#entry"), 195 - }), 196 - min_length: None, 197 - max_length: Some(500usize), 198 - }), 199 - ); 200 - map.insert( 201 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 202 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 203 - description: None, 204 - format: None, 205 - default: None, 206 - min_length: None, 207 - max_length: None, 208 - min_graphemes: None, 209 - max_graphemes: None, 210 - r#enum: None, 211 - r#const: None, 212 - known_values: None, 213 - }), 214 - ); 215 - map 216 - }, 217 - }), 218 - ); 219 - map.insert( 220 - ::jacquard_common::smol_str::SmolStr::new_static("entry"), 221 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 222 - description: None, 223 - required: Some( 224 - vec![ 225 - ::jacquard_common::smol_str::SmolStr::new_static("name"), 226 - ::jacquard_common::smol_str::SmolStr::new_static("node") 227 - ], 228 - ), 229 - nullable: None, 230 - properties: { 231 - #[allow(unused_mut)] 232 - let mut map = ::std::collections::BTreeMap::new(); 233 - map.insert( 234 - ::jacquard_common::smol_str::SmolStr::new_static("name"), 235 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 236 - description: None, 237 - format: None, 238 - default: None, 239 - min_length: None, 240 - max_length: Some(255usize), 241 - min_graphemes: None, 242 - max_graphemes: None, 243 - r#enum: None, 244 - r#const: None, 245 - known_values: None, 246 - }), 247 - ); 248 - map.insert( 249 - ::jacquard_common::smol_str::SmolStr::new_static("node"), 250 - ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 251 - description: None, 252 - refs: vec![ 253 - ::jacquard_common::CowStr::new_static("#file"), 254 - ::jacquard_common::CowStr::new_static("#directory"), 255 - ::jacquard_common::CowStr::new_static("#subfs") 256 - ], 257 - closed: None, 258 - }), 259 - ); 260 - map 261 - }, 262 - }), 263 - ); 264 - map.insert( 265 - ::jacquard_common::smol_str::SmolStr::new_static("file"), 266 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 267 - description: None, 268 - required: Some( 269 - vec![ 270 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 271 - ::jacquard_common::smol_str::SmolStr::new_static("blob") 272 - ], 273 - ), 274 - nullable: None, 275 - properties: { 276 - #[allow(unused_mut)] 277 - let mut map = ::std::collections::BTreeMap::new(); 278 - map.insert( 279 - ::jacquard_common::smol_str::SmolStr::new_static("base64"), 280 - ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 281 - description: None, 282 - default: None, 283 - r#const: None, 284 - }), 285 - ); 286 - map.insert( 287 - ::jacquard_common::smol_str::SmolStr::new_static("blob"), 288 - ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob { 289 - description: None, 290 - accept: None, 291 - max_size: None, 292 - }), 293 - ); 294 - map.insert( 295 - ::jacquard_common::smol_str::SmolStr::new_static("encoding"), 296 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 297 - description: Some( 298 - ::jacquard_common::CowStr::new_static( 299 - "Content encoding (e.g., gzip for compressed files)", 300 - ), 301 - ), 302 - format: None, 303 - default: None, 304 - min_length: None, 305 - max_length: None, 306 - min_graphemes: None, 307 - max_graphemes: None, 308 - r#enum: None, 309 - r#const: None, 310 - known_values: None, 311 - }), 312 - ); 313 - map.insert( 314 - ::jacquard_common::smol_str::SmolStr::new_static("mimeType"), 315 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 316 - description: Some( 317 - ::jacquard_common::CowStr::new_static( 318 - "Original MIME type before compression", 319 - ), 320 - ), 321 - format: None, 322 - default: None, 323 - min_length: None, 324 - max_length: None, 325 - min_graphemes: None, 326 - max_graphemes: None, 327 - r#enum: None, 328 - r#const: None, 329 - known_values: None, 330 - }), 331 - ); 332 - map.insert( 333 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 334 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 335 - description: None, 336 - format: None, 337 - default: None, 338 - min_length: None, 339 - max_length: None, 340 - min_graphemes: None, 341 - max_graphemes: None, 342 - r#enum: None, 343 - r#const: None, 344 - known_values: None, 345 - }), 346 - ); 347 - map 348 - }, 349 - }), 350 - ); 351 - map.insert( 352 - ::jacquard_common::smol_str::SmolStr::new_static("main"), 353 - ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { 354 - description: Some( 355 - ::jacquard_common::CowStr::new_static( 356 - "Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.", 357 - ), 358 - ), 359 - key: None, 360 - record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { 361 - description: None, 362 - required: Some( 363 - vec![ 364 - ::jacquard_common::smol_str::SmolStr::new_static("root"), 365 - ::jacquard_common::smol_str::SmolStr::new_static("createdAt") 366 - ], 367 - ), 368 - nullable: None, 369 - properties: { 370 - #[allow(unused_mut)] 371 - let mut map = ::std::collections::BTreeMap::new(); 372 - map.insert( 373 - ::jacquard_common::smol_str::SmolStr::new_static( 374 - "createdAt", 375 - ), 376 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 377 - description: None, 378 - format: Some( 379 - ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, 380 - ), 381 - default: None, 382 - min_length: None, 383 - max_length: None, 384 - min_graphemes: None, 385 - max_graphemes: None, 386 - r#enum: None, 387 - r#const: None, 388 - known_values: None, 389 - }), 390 - ); 391 - map.insert( 392 - ::jacquard_common::smol_str::SmolStr::new_static( 393 - "fileCount", 394 - ), 395 - ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { 396 - description: None, 397 - default: None, 398 - minimum: Some(0i64), 399 - maximum: Some(1000i64), 400 - r#enum: None, 401 - r#const: None, 402 - }), 403 - ); 404 - map.insert( 405 - ::jacquard_common::smol_str::SmolStr::new_static("root"), 406 - ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { 407 - description: None, 408 - r#ref: ::jacquard_common::CowStr::new_static("#directory"), 409 - }), 410 - ); 411 - map 412 - }, 413 - }), 414 - }), 415 - ); 416 - map.insert( 417 - ::jacquard_common::smol_str::SmolStr::new_static("subfs"), 418 - ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { 419 - description: None, 420 - required: Some( 421 - vec![ 422 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 423 - ::jacquard_common::smol_str::SmolStr::new_static("subject") 424 - ], 425 - ), 426 - nullable: None, 427 - properties: { 428 - #[allow(unused_mut)] 429 - let mut map = ::std::collections::BTreeMap::new(); 430 - map.insert( 431 - ::jacquard_common::smol_str::SmolStr::new_static("subject"), 432 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 433 - description: Some( 434 - ::jacquard_common::CowStr::new_static( 435 - "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.", 436 - ), 437 - ), 438 - format: Some( 439 - ::jacquard_lexicon::lexicon::LexStringFormat::AtUri, 440 - ), 441 - default: None, 442 - min_length: None, 443 - max_length: None, 444 - min_graphemes: None, 445 - max_graphemes: None, 446 - r#enum: None, 447 - r#const: None, 448 - known_values: None, 449 - }), 450 - ); 451 - map.insert( 452 - ::jacquard_common::smol_str::SmolStr::new_static("type"), 453 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 454 - description: None, 455 - format: None, 456 - default: None, 457 - min_length: None, 458 - max_length: None, 459 - min_graphemes: None, 460 - max_graphemes: None, 461 - r#enum: None, 462 - r#const: None, 463 - known_values: None, 464 - }), 465 - ); 466 - map 467 - }, 468 - }), 469 - ); 470 - map 471 - }, 472 - } 473 - } 474 - 475 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Directory<'a> { 476 - fn nsid() -> &'static str { 477 - "place.wisp.subfs" 478 - } 479 - fn def_name() -> &'static str { 480 - "directory" 481 - } 482 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 483 - lexicon_doc_place_wisp_subfs() 484 - } 485 - fn validate( 486 - &self, 487 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 488 - { 489 - let value = &self.entries; 490 - #[allow(unused_comparisons)] 491 - if value.len() > 500usize { 492 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 493 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 494 - "entries", 495 - ), 496 - max: 500usize, 497 - actual: value.len(), 498 - }); 499 - } 500 - } 501 - Ok(()) 502 - } 503 - } 504 - 505 - #[jacquard_derive::lexicon] 506 - #[derive( 507 - serde::Serialize, 508 - serde::Deserialize, 509 - Debug, 510 - Clone, 511 - PartialEq, 512 - Eq, 513 - jacquard_derive::IntoStatic 514 - )] 515 - #[serde(rename_all = "camelCase")] 516 - pub struct Entry<'a> { 517 - #[serde(borrow)] 518 - pub name: jacquard_common::CowStr<'a>, 519 - #[serde(borrow)] 520 - pub node: EntryNode<'a>, 521 - } 522 - 523 - pub mod entry_state { 524 - 525 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 526 - #[allow(unused)] 527 - use ::core::marker::PhantomData; 528 - mod sealed { 529 - pub trait Sealed {} 530 - } 531 - /// State trait tracking which required fields have been set 532 - pub trait State: sealed::Sealed { 533 - type Name; 534 - type Node; 535 - } 536 - /// Empty state - all required fields are unset 537 - pub struct Empty(()); 538 - impl sealed::Sealed for Empty {} 539 - impl State for Empty { 540 - type Name = Unset; 541 - type Node = Unset; 542 - } 543 - ///State transition - sets the `name` field to Set 544 - pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>); 545 - impl<S: State> sealed::Sealed for SetName<S> {} 546 - impl<S: State> State for SetName<S> { 547 - type Name = Set<members::name>; 548 - type Node = S::Node; 549 - } 550 - ///State transition - sets the `node` field to Set 551 - pub struct SetNode<S: State = Empty>(PhantomData<fn() -> S>); 552 - impl<S: State> sealed::Sealed for SetNode<S> {} 553 - impl<S: State> State for SetNode<S> { 554 - type Name = S::Name; 555 - type Node = Set<members::node>; 556 - } 557 - /// Marker types for field names 558 - #[allow(non_camel_case_types)] 559 - pub mod members { 560 - ///Marker type for the `name` field 561 - pub struct name(()); 562 - ///Marker type for the `node` field 563 - pub struct node(()); 564 - } 565 - } 566 - 567 - /// Builder for constructing an instance of this type 568 - pub struct EntryBuilder<'a, S: entry_state::State> { 569 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 570 - __unsafe_private_named: ( 571 - ::core::option::Option<jacquard_common::CowStr<'a>>, 572 - ::core::option::Option<EntryNode<'a>>, 573 - ), 574 - _phantom: ::core::marker::PhantomData<&'a ()>, 575 - } 576 - 577 - impl<'a> Entry<'a> { 578 - /// Create a new builder for this type 579 - pub fn new() -> EntryBuilder<'a, entry_state::Empty> { 580 - EntryBuilder::new() 581 - } 582 - } 583 - 584 - impl<'a> EntryBuilder<'a, entry_state::Empty> { 585 - /// Create a new builder with all fields unset 586 - pub fn new() -> Self { 587 - EntryBuilder { 588 - _phantom_state: ::core::marker::PhantomData, 589 - __unsafe_private_named: (None, None), 590 - _phantom: ::core::marker::PhantomData, 591 - } 592 - } 593 - } 594 - 595 - impl<'a, S> EntryBuilder<'a, S> 596 - where 597 - S: entry_state::State, 598 - S::Name: entry_state::IsUnset, 599 - { 600 - /// Set the `name` field (required) 601 - pub fn name( 602 - mut self, 603 - value: impl Into<jacquard_common::CowStr<'a>>, 604 - ) -> EntryBuilder<'a, entry_state::SetName<S>> { 605 - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 606 - EntryBuilder { 607 - _phantom_state: ::core::marker::PhantomData, 608 - __unsafe_private_named: self.__unsafe_private_named, 609 - _phantom: ::core::marker::PhantomData, 610 - } 611 - } 612 - } 613 - 614 - impl<'a, S> EntryBuilder<'a, S> 615 - where 616 - S: entry_state::State, 617 - S::Node: entry_state::IsUnset, 618 - { 619 - /// Set the `node` field (required) 620 - pub fn node( 621 - mut self, 622 - value: impl Into<EntryNode<'a>>, 623 - ) -> EntryBuilder<'a, entry_state::SetNode<S>> { 624 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 625 - EntryBuilder { 626 - _phantom_state: ::core::marker::PhantomData, 627 - __unsafe_private_named: self.__unsafe_private_named, 628 - _phantom: ::core::marker::PhantomData, 629 - } 630 - } 631 - } 632 - 633 - impl<'a, S> EntryBuilder<'a, S> 634 - where 635 - S: entry_state::State, 636 - S::Name: entry_state::IsSet, 637 - S::Node: entry_state::IsSet, 638 - { 639 - /// Build the final struct 640 - pub fn build(self) -> Entry<'a> { 641 - Entry { 642 - name: self.__unsafe_private_named.0.unwrap(), 643 - node: self.__unsafe_private_named.1.unwrap(), 644 - extra_data: Default::default(), 645 - } 646 - } 647 - /// Build the final struct with custom extra_data 648 - pub fn build_with_data( 649 - self, 650 - extra_data: std::collections::BTreeMap< 651 - jacquard_common::smol_str::SmolStr, 652 - jacquard_common::types::value::Data<'a>, 653 - >, 654 - ) -> Entry<'a> { 655 - Entry { 656 - name: self.__unsafe_private_named.0.unwrap(), 657 - node: self.__unsafe_private_named.1.unwrap(), 658 - extra_data: Some(extra_data), 659 - } 660 - } 661 - } 662 - 663 - #[jacquard_derive::open_union] 664 - #[derive( 665 - serde::Serialize, 666 - serde::Deserialize, 667 - Debug, 668 - Clone, 669 - PartialEq, 670 - Eq, 671 - jacquard_derive::IntoStatic 672 - )] 673 - #[serde(tag = "$type")] 674 - #[serde(bound(deserialize = "'de: 'a"))] 675 - pub enum EntryNode<'a> { 676 - #[serde(rename = "place.wisp.subfs#file")] 677 - File(Box<crate::place_wisp::subfs::File<'a>>), 678 - #[serde(rename = "place.wisp.subfs#directory")] 679 - Directory(Box<crate::place_wisp::subfs::Directory<'a>>), 680 - #[serde(rename = "place.wisp.subfs#subfs")] 681 - Subfs(Box<crate::place_wisp::subfs::Subfs<'a>>), 682 - } 683 - 684 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> { 685 - fn nsid() -> &'static str { 686 - "place.wisp.subfs" 687 - } 688 - fn def_name() -> &'static str { 689 - "entry" 690 - } 691 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 692 - lexicon_doc_place_wisp_subfs() 693 - } 694 - fn validate( 695 - &self, 696 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 697 - { 698 - let value = &self.name; 699 - #[allow(unused_comparisons)] 700 - if <str>::len(value.as_ref()) > 255usize { 701 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 702 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 703 - "name", 704 - ), 705 - max: 255usize, 706 - actual: <str>::len(value.as_ref()), 707 - }); 708 - } 709 - } 710 - Ok(()) 711 - } 712 - } 713 - 714 - #[jacquard_derive::lexicon] 715 - #[derive( 716 - serde::Serialize, 717 - serde::Deserialize, 718 - Debug, 719 - Clone, 720 - PartialEq, 721 - Eq, 722 - jacquard_derive::IntoStatic 723 - )] 724 - #[serde(rename_all = "camelCase")] 725 - pub struct File<'a> { 726 - /// True if blob content is base64-encoded (used to bypass PDS content sniffing) 727 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 728 - pub base64: Option<bool>, 729 - /// Content blob ref 730 - #[serde(borrow)] 731 - pub blob: jacquard_common::types::blob::BlobRef<'a>, 732 - /// Content encoding (e.g., gzip for compressed files) 733 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 734 - #[serde(borrow)] 735 - pub encoding: Option<jacquard_common::CowStr<'a>>, 736 - /// Original MIME type before compression 737 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 738 - #[serde(borrow)] 739 - pub mime_type: Option<jacquard_common::CowStr<'a>>, 740 - #[serde(borrow)] 741 - pub r#type: jacquard_common::CowStr<'a>, 742 - } 743 - 744 - pub mod file_state { 745 - 746 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 747 - #[allow(unused)] 748 - use ::core::marker::PhantomData; 749 - mod sealed { 750 - pub trait Sealed {} 751 - } 752 - /// State trait tracking which required fields have been set 753 - pub trait State: sealed::Sealed { 754 - type Type; 755 - type Blob; 756 - } 757 - /// Empty state - all required fields are unset 758 - pub struct Empty(()); 759 - impl sealed::Sealed for Empty {} 760 - impl State for Empty { 761 - type Type = Unset; 762 - type Blob = Unset; 763 - } 764 - ///State transition - sets the `type` field to Set 765 - pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 766 - impl<S: State> sealed::Sealed for SetType<S> {} 767 - impl<S: State> State for SetType<S> { 768 - type Type = Set<members::r#type>; 769 - type Blob = S::Blob; 770 - } 771 - ///State transition - sets the `blob` field to Set 772 - pub struct SetBlob<S: State = Empty>(PhantomData<fn() -> S>); 773 - impl<S: State> sealed::Sealed for SetBlob<S> {} 774 - impl<S: State> State for SetBlob<S> { 775 - type Type = S::Type; 776 - type Blob = Set<members::blob>; 777 - } 778 - /// Marker types for field names 779 - #[allow(non_camel_case_types)] 780 - pub mod members { 781 - ///Marker type for the `type` field 782 - pub struct r#type(()); 783 - ///Marker type for the `blob` field 784 - pub struct blob(()); 785 - } 786 - } 787 - 788 - /// Builder for constructing an instance of this type 789 - pub struct FileBuilder<'a, S: file_state::State> { 790 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 791 - __unsafe_private_named: ( 792 - ::core::option::Option<bool>, 793 - ::core::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 794 - ::core::option::Option<jacquard_common::CowStr<'a>>, 795 - ::core::option::Option<jacquard_common::CowStr<'a>>, 796 - ::core::option::Option<jacquard_common::CowStr<'a>>, 797 - ), 798 - _phantom: ::core::marker::PhantomData<&'a ()>, 799 - } 800 - 801 - impl<'a> File<'a> { 802 - /// Create a new builder for this type 803 - pub fn new() -> FileBuilder<'a, file_state::Empty> { 804 - FileBuilder::new() 805 - } 806 - } 807 - 808 - impl<'a> FileBuilder<'a, file_state::Empty> { 809 - /// Create a new builder with all fields unset 810 - pub fn new() -> Self { 811 - FileBuilder { 812 - _phantom_state: ::core::marker::PhantomData, 813 - __unsafe_private_named: (None, None, None, None, None), 814 - _phantom: ::core::marker::PhantomData, 815 - } 816 - } 817 - } 818 - 819 - impl<'a, S: file_state::State> FileBuilder<'a, S> { 820 - /// Set the `base64` field (optional) 821 - pub fn base64(mut self, value: impl Into<Option<bool>>) -> Self { 822 - self.__unsafe_private_named.0 = value.into(); 823 - self 824 - } 825 - /// Set the `base64` field to an Option value (optional) 826 - pub fn maybe_base64(mut self, value: Option<bool>) -> Self { 827 - self.__unsafe_private_named.0 = value; 828 - self 829 - } 830 - } 831 - 832 - impl<'a, S> FileBuilder<'a, S> 833 - where 834 - S: file_state::State, 835 - S::Blob: file_state::IsUnset, 836 - { 837 - /// Set the `blob` field (required) 838 - pub fn blob( 839 - mut self, 840 - value: impl Into<jacquard_common::types::blob::BlobRef<'a>>, 841 - ) -> FileBuilder<'a, file_state::SetBlob<S>> { 842 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 843 - FileBuilder { 844 - _phantom_state: ::core::marker::PhantomData, 845 - __unsafe_private_named: self.__unsafe_private_named, 846 - _phantom: ::core::marker::PhantomData, 847 - } 848 - } 849 - } 850 - 851 - impl<'a, S: file_state::State> FileBuilder<'a, S> { 852 - /// Set the `encoding` field (optional) 853 - pub fn encoding( 854 - mut self, 855 - value: impl Into<Option<jacquard_common::CowStr<'a>>>, 856 - ) -> Self { 857 - self.__unsafe_private_named.2 = value.into(); 858 - self 859 - } 860 - /// Set the `encoding` field to an Option value (optional) 861 - pub fn maybe_encoding(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 862 - self.__unsafe_private_named.2 = value; 863 - self 864 - } 865 - } 866 - 867 - impl<'a, S: file_state::State> FileBuilder<'a, S> { 868 - /// Set the `mimeType` field (optional) 869 - pub fn mime_type( 870 - mut self, 871 - value: impl Into<Option<jacquard_common::CowStr<'a>>>, 872 - ) -> Self { 873 - self.__unsafe_private_named.3 = value.into(); 874 - self 875 - } 876 - /// Set the `mimeType` field to an Option value (optional) 877 - pub fn maybe_mime_type( 878 - mut self, 879 - value: Option<jacquard_common::CowStr<'a>>, 880 - ) -> Self { 881 - self.__unsafe_private_named.3 = value; 882 - self 883 - } 884 - } 885 - 886 - impl<'a, S> FileBuilder<'a, S> 887 - where 888 - S: file_state::State, 889 - S::Type: file_state::IsUnset, 890 - { 891 - /// Set the `type` field (required) 892 - pub fn r#type( 893 - mut self, 894 - value: impl Into<jacquard_common::CowStr<'a>>, 895 - ) -> FileBuilder<'a, file_state::SetType<S>> { 896 - self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); 897 - FileBuilder { 898 - _phantom_state: ::core::marker::PhantomData, 899 - __unsafe_private_named: self.__unsafe_private_named, 900 - _phantom: ::core::marker::PhantomData, 901 - } 902 - } 903 - } 904 - 905 - impl<'a, S> FileBuilder<'a, S> 906 - where 907 - S: file_state::State, 908 - S::Type: file_state::IsSet, 909 - S::Blob: file_state::IsSet, 910 - { 911 - /// Build the final struct 912 - pub fn build(self) -> File<'a> { 913 - File { 914 - base64: self.__unsafe_private_named.0, 915 - blob: self.__unsafe_private_named.1.unwrap(), 916 - encoding: self.__unsafe_private_named.2, 917 - mime_type: self.__unsafe_private_named.3, 918 - r#type: self.__unsafe_private_named.4.unwrap(), 919 - extra_data: Default::default(), 920 - } 921 - } 922 - /// Build the final struct with custom extra_data 923 - pub fn build_with_data( 924 - self, 925 - extra_data: std::collections::BTreeMap< 926 - jacquard_common::smol_str::SmolStr, 927 - jacquard_common::types::value::Data<'a>, 928 - >, 929 - ) -> File<'a> { 930 - File { 931 - base64: self.__unsafe_private_named.0, 932 - blob: self.__unsafe_private_named.1.unwrap(), 933 - encoding: self.__unsafe_private_named.2, 934 - mime_type: self.__unsafe_private_named.3, 935 - r#type: self.__unsafe_private_named.4.unwrap(), 936 - extra_data: Some(extra_data), 937 - } 938 - } 939 - } 940 - 941 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for File<'a> { 942 - fn nsid() -> &'static str { 943 - "place.wisp.subfs" 944 - } 945 - fn def_name() -> &'static str { 946 - "file" 947 - } 948 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 949 - lexicon_doc_place_wisp_subfs() 950 - } 951 - fn validate( 952 - &self, 953 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 954 - Ok(()) 955 - } 956 - } 957 - 958 - /// Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure. 959 - #[jacquard_derive::lexicon] 960 - #[derive( 961 - serde::Serialize, 962 - serde::Deserialize, 963 - Debug, 964 - Clone, 965 - PartialEq, 966 - Eq, 967 - jacquard_derive::IntoStatic 968 - )] 969 - #[serde(rename_all = "camelCase")] 970 - pub struct SubfsRecord<'a> { 971 - pub created_at: jacquard_common::types::string::Datetime, 972 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 973 - pub file_count: Option<i64>, 974 - #[serde(borrow)] 975 - pub root: crate::place_wisp::subfs::Directory<'a>, 976 - } 977 - 978 - pub mod subfs_record_state { 979 - 980 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 981 - #[allow(unused)] 982 - use ::core::marker::PhantomData; 983 - mod sealed { 984 - pub trait Sealed {} 985 - } 986 - /// State trait tracking which required fields have been set 987 - pub trait State: sealed::Sealed { 988 - type Root; 989 - type CreatedAt; 990 - } 991 - /// Empty state - all required fields are unset 992 - pub struct Empty(()); 993 - impl sealed::Sealed for Empty {} 994 - impl State for Empty { 995 - type Root = Unset; 996 - type CreatedAt = Unset; 997 - } 998 - ///State transition - sets the `root` field to Set 999 - pub struct SetRoot<S: State = Empty>(PhantomData<fn() -> S>); 1000 - impl<S: State> sealed::Sealed for SetRoot<S> {} 1001 - impl<S: State> State for SetRoot<S> { 1002 - type Root = Set<members::root>; 1003 - type CreatedAt = S::CreatedAt; 1004 - } 1005 - ///State transition - sets the `created_at` field to Set 1006 - pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>); 1007 - impl<S: State> sealed::Sealed for SetCreatedAt<S> {} 1008 - impl<S: State> State for SetCreatedAt<S> { 1009 - type Root = S::Root; 1010 - type CreatedAt = Set<members::created_at>; 1011 - } 1012 - /// Marker types for field names 1013 - #[allow(non_camel_case_types)] 1014 - pub mod members { 1015 - ///Marker type for the `root` field 1016 - pub struct root(()); 1017 - ///Marker type for the `created_at` field 1018 - pub struct created_at(()); 1019 - } 1020 - } 1021 - 1022 - /// Builder for constructing an instance of this type 1023 - pub struct SubfsRecordBuilder<'a, S: subfs_record_state::State> { 1024 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1025 - __unsafe_private_named: ( 1026 - ::core::option::Option<jacquard_common::types::string::Datetime>, 1027 - ::core::option::Option<i64>, 1028 - ::core::option::Option<crate::place_wisp::subfs::Directory<'a>>, 1029 - ), 1030 - _phantom: ::core::marker::PhantomData<&'a ()>, 1031 - } 1032 - 1033 - impl<'a> SubfsRecord<'a> { 1034 - /// Create a new builder for this type 1035 - pub fn new() -> SubfsRecordBuilder<'a, subfs_record_state::Empty> { 1036 - SubfsRecordBuilder::new() 1037 - } 1038 - } 1039 - 1040 - impl<'a> SubfsRecordBuilder<'a, subfs_record_state::Empty> { 1041 - /// Create a new builder with all fields unset 1042 - pub fn new() -> Self { 1043 - SubfsRecordBuilder { 1044 - _phantom_state: ::core::marker::PhantomData, 1045 - __unsafe_private_named: (None, None, None), 1046 - _phantom: ::core::marker::PhantomData, 1047 - } 1048 - } 1049 - } 1050 - 1051 - impl<'a, S> SubfsRecordBuilder<'a, S> 1052 - where 1053 - S: subfs_record_state::State, 1054 - S::CreatedAt: subfs_record_state::IsUnset, 1055 - { 1056 - /// Set the `createdAt` field (required) 1057 - pub fn created_at( 1058 - mut self, 1059 - value: impl Into<jacquard_common::types::string::Datetime>, 1060 - ) -> SubfsRecordBuilder<'a, subfs_record_state::SetCreatedAt<S>> { 1061 - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 1062 - SubfsRecordBuilder { 1063 - _phantom_state: ::core::marker::PhantomData, 1064 - __unsafe_private_named: self.__unsafe_private_named, 1065 - _phantom: ::core::marker::PhantomData, 1066 - } 1067 - } 1068 - } 1069 - 1070 - impl<'a, S: subfs_record_state::State> SubfsRecordBuilder<'a, S> { 1071 - /// Set the `fileCount` field (optional) 1072 - pub fn file_count(mut self, value: impl Into<Option<i64>>) -> Self { 1073 - self.__unsafe_private_named.1 = value.into(); 1074 - self 1075 - } 1076 - /// Set the `fileCount` field to an Option value (optional) 1077 - pub fn maybe_file_count(mut self, value: Option<i64>) -> Self { 1078 - self.__unsafe_private_named.1 = value; 1079 - self 1080 - } 1081 - } 1082 - 1083 - impl<'a, S> SubfsRecordBuilder<'a, S> 1084 - where 1085 - S: subfs_record_state::State, 1086 - S::Root: subfs_record_state::IsUnset, 1087 - { 1088 - /// Set the `root` field (required) 1089 - pub fn root( 1090 - mut self, 1091 - value: impl Into<crate::place_wisp::subfs::Directory<'a>>, 1092 - ) -> SubfsRecordBuilder<'a, subfs_record_state::SetRoot<S>> { 1093 - self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); 1094 - SubfsRecordBuilder { 1095 - _phantom_state: ::core::marker::PhantomData, 1096 - __unsafe_private_named: self.__unsafe_private_named, 1097 - _phantom: ::core::marker::PhantomData, 1098 - } 1099 - } 1100 - } 1101 - 1102 - impl<'a, S> SubfsRecordBuilder<'a, S> 1103 - where 1104 - S: subfs_record_state::State, 1105 - S::Root: subfs_record_state::IsSet, 1106 - S::CreatedAt: subfs_record_state::IsSet, 1107 - { 1108 - /// Build the final struct 1109 - pub fn build(self) -> SubfsRecord<'a> { 1110 - SubfsRecord { 1111 - created_at: self.__unsafe_private_named.0.unwrap(), 1112 - file_count: self.__unsafe_private_named.1, 1113 - root: self.__unsafe_private_named.2.unwrap(), 1114 - extra_data: Default::default(), 1115 - } 1116 - } 1117 - /// Build the final struct with custom extra_data 1118 - pub fn build_with_data( 1119 - self, 1120 - extra_data: std::collections::BTreeMap< 1121 - jacquard_common::smol_str::SmolStr, 1122 - jacquard_common::types::value::Data<'a>, 1123 - >, 1124 - ) -> SubfsRecord<'a> { 1125 - SubfsRecord { 1126 - created_at: self.__unsafe_private_named.0.unwrap(), 1127 - file_count: self.__unsafe_private_named.1, 1128 - root: self.__unsafe_private_named.2.unwrap(), 1129 - extra_data: Some(extra_data), 1130 - } 1131 - } 1132 - } 1133 - 1134 - impl<'a> SubfsRecord<'a> { 1135 - pub fn uri( 1136 - uri: impl Into<jacquard_common::CowStr<'a>>, 1137 - ) -> Result< 1138 - jacquard_common::types::uri::RecordUri<'a, SubfsRecordRecord>, 1139 - jacquard_common::types::uri::UriError, 1140 - > { 1141 - jacquard_common::types::uri::RecordUri::try_from_uri( 1142 - jacquard_common::types::string::AtUri::new_cow(uri.into())?, 1143 - ) 1144 - } 1145 - } 1146 - 1147 - /// Typed wrapper for GetRecord response with this collection's record type. 1148 - #[derive( 1149 - serde::Serialize, 1150 - serde::Deserialize, 1151 - Debug, 1152 - Clone, 1153 - PartialEq, 1154 - Eq, 1155 - jacquard_derive::IntoStatic 1156 - )] 1157 - #[serde(rename_all = "camelCase")] 1158 - pub struct SubfsRecordGetRecordOutput<'a> { 1159 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 1160 - #[serde(borrow)] 1161 - pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 1162 - #[serde(borrow)] 1163 - pub uri: jacquard_common::types::string::AtUri<'a>, 1164 - #[serde(borrow)] 1165 - pub value: SubfsRecord<'a>, 1166 - } 1167 - 1168 - impl From<SubfsRecordGetRecordOutput<'_>> for SubfsRecord<'_> { 1169 - fn from(output: SubfsRecordGetRecordOutput<'_>) -> Self { 1170 - use jacquard_common::IntoStatic; 1171 - output.value.into_static() 1172 - } 1173 - } 1174 - 1175 - impl jacquard_common::types::collection::Collection for SubfsRecord<'_> { 1176 - const NSID: &'static str = "place.wisp.subfs"; 1177 - type Record = SubfsRecordRecord; 1178 - } 1179 - 1180 - /// Marker type for deserializing records from this collection. 1181 - #[derive(Debug, serde::Serialize, serde::Deserialize)] 1182 - pub struct SubfsRecordRecord; 1183 - impl jacquard_common::xrpc::XrpcResp for SubfsRecordRecord { 1184 - const NSID: &'static str = "place.wisp.subfs"; 1185 - const ENCODING: &'static str = "application/json"; 1186 - type Output<'de> = SubfsRecordGetRecordOutput<'de>; 1187 - type Err<'de> = jacquard_common::types::collection::RecordError<'de>; 1188 - } 1189 - 1190 - impl jacquard_common::types::collection::Collection for SubfsRecordRecord { 1191 - const NSID: &'static str = "place.wisp.subfs"; 1192 - type Record = SubfsRecordRecord; 1193 - } 1194 - 1195 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SubfsRecord<'a> { 1196 - fn nsid() -> &'static str { 1197 - "place.wisp.subfs" 1198 - } 1199 - fn def_name() -> &'static str { 1200 - "main" 1201 - } 1202 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1203 - lexicon_doc_place_wisp_subfs() 1204 - } 1205 - fn validate( 1206 - &self, 1207 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1208 - if let Some(ref value) = self.file_count { 1209 - if *value > 1000i64 { 1210 - return Err(::jacquard_lexicon::validation::ConstraintError::Maximum { 1211 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1212 - "file_count", 1213 - ), 1214 - max: 1000i64, 1215 - actual: *value, 1216 - }); 1217 - } 1218 - } 1219 - if let Some(ref value) = self.file_count { 1220 - if *value < 0i64 { 1221 - return Err(::jacquard_lexicon::validation::ConstraintError::Minimum { 1222 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 1223 - "file_count", 1224 - ), 1225 - min: 0i64, 1226 - actual: *value, 1227 - }); 1228 - } 1229 - } 1230 - Ok(()) 1231 - } 1232 - } 1233 - 1234 - #[jacquard_derive::lexicon] 1235 - #[derive( 1236 - serde::Serialize, 1237 - serde::Deserialize, 1238 - Debug, 1239 - Clone, 1240 - PartialEq, 1241 - Eq, 1242 - jacquard_derive::IntoStatic 1243 - )] 1244 - #[serde(rename_all = "camelCase")] 1245 - pub struct Subfs<'a> { 1246 - /// AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures. 1247 - #[serde(borrow)] 1248 - pub subject: jacquard_common::types::string::AtUri<'a>, 1249 - #[serde(borrow)] 1250 - pub r#type: jacquard_common::CowStr<'a>, 1251 - } 1252 - 1253 - pub mod subfs_state { 1254 - 1255 - pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 1256 - #[allow(unused)] 1257 - use ::core::marker::PhantomData; 1258 - mod sealed { 1259 - pub trait Sealed {} 1260 - } 1261 - /// State trait tracking which required fields have been set 1262 - pub trait State: sealed::Sealed { 1263 - type Type; 1264 - type Subject; 1265 - } 1266 - /// Empty state - all required fields are unset 1267 - pub struct Empty(()); 1268 - impl sealed::Sealed for Empty {} 1269 - impl State for Empty { 1270 - type Type = Unset; 1271 - type Subject = Unset; 1272 - } 1273 - ///State transition - sets the `type` field to Set 1274 - pub struct SetType<S: State = Empty>(PhantomData<fn() -> S>); 1275 - impl<S: State> sealed::Sealed for SetType<S> {} 1276 - impl<S: State> State for SetType<S> { 1277 - type Type = Set<members::r#type>; 1278 - type Subject = S::Subject; 1279 - } 1280 - ///State transition - sets the `subject` field to Set 1281 - pub struct SetSubject<S: State = Empty>(PhantomData<fn() -> S>); 1282 - impl<S: State> sealed::Sealed for SetSubject<S> {} 1283 - impl<S: State> State for SetSubject<S> { 1284 - type Type = S::Type; 1285 - type Subject = Set<members::subject>; 1286 - } 1287 - /// Marker types for field names 1288 - #[allow(non_camel_case_types)] 1289 - pub mod members { 1290 - ///Marker type for the `type` field 1291 - pub struct r#type(()); 1292 - ///Marker type for the `subject` field 1293 - pub struct subject(()); 1294 - } 1295 - } 1296 - 1297 - /// Builder for constructing an instance of this type 1298 - pub struct SubfsBuilder<'a, S: subfs_state::State> { 1299 - _phantom_state: ::core::marker::PhantomData<fn() -> S>, 1300 - __unsafe_private_named: ( 1301 - ::core::option::Option<jacquard_common::types::string::AtUri<'a>>, 1302 - ::core::option::Option<jacquard_common::CowStr<'a>>, 1303 - ), 1304 - _phantom: ::core::marker::PhantomData<&'a ()>, 1305 - } 1306 - 1307 - impl<'a> Subfs<'a> { 1308 - /// Create a new builder for this type 1309 - pub fn new() -> SubfsBuilder<'a, subfs_state::Empty> { 1310 - SubfsBuilder::new() 1311 - } 1312 - } 1313 - 1314 - impl<'a> SubfsBuilder<'a, subfs_state::Empty> { 1315 - /// Create a new builder with all fields unset 1316 - pub fn new() -> Self { 1317 - SubfsBuilder { 1318 - _phantom_state: ::core::marker::PhantomData, 1319 - __unsafe_private_named: (None, None), 1320 - _phantom: ::core::marker::PhantomData, 1321 - } 1322 - } 1323 - } 1324 - 1325 - impl<'a, S> SubfsBuilder<'a, S> 1326 - where 1327 - S: subfs_state::State, 1328 - S::Subject: subfs_state::IsUnset, 1329 - { 1330 - /// Set the `subject` field (required) 1331 - pub fn subject( 1332 - mut self, 1333 - value: impl Into<jacquard_common::types::string::AtUri<'a>>, 1334 - ) -> SubfsBuilder<'a, subfs_state::SetSubject<S>> { 1335 - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 1336 - SubfsBuilder { 1337 - _phantom_state: ::core::marker::PhantomData, 1338 - __unsafe_private_named: self.__unsafe_private_named, 1339 - _phantom: ::core::marker::PhantomData, 1340 - } 1341 - } 1342 - } 1343 - 1344 - impl<'a, S> SubfsBuilder<'a, S> 1345 - where 1346 - S: subfs_state::State, 1347 - S::Type: subfs_state::IsUnset, 1348 - { 1349 - /// Set the `type` field (required) 1350 - pub fn r#type( 1351 - mut self, 1352 - value: impl Into<jacquard_common::CowStr<'a>>, 1353 - ) -> SubfsBuilder<'a, subfs_state::SetType<S>> { 1354 - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 1355 - SubfsBuilder { 1356 - _phantom_state: ::core::marker::PhantomData, 1357 - __unsafe_private_named: self.__unsafe_private_named, 1358 - _phantom: ::core::marker::PhantomData, 1359 - } 1360 - } 1361 - } 1362 - 1363 - impl<'a, S> SubfsBuilder<'a, S> 1364 - where 1365 - S: subfs_state::State, 1366 - S::Type: subfs_state::IsSet, 1367 - S::Subject: subfs_state::IsSet, 1368 - { 1369 - /// Build the final struct 1370 - pub fn build(self) -> Subfs<'a> { 1371 - Subfs { 1372 - subject: self.__unsafe_private_named.0.unwrap(), 1373 - r#type: self.__unsafe_private_named.1.unwrap(), 1374 - extra_data: Default::default(), 1375 - } 1376 - } 1377 - /// Build the final struct with custom extra_data 1378 - pub fn build_with_data( 1379 - self, 1380 - extra_data: std::collections::BTreeMap< 1381 - jacquard_common::smol_str::SmolStr, 1382 - jacquard_common::types::value::Data<'a>, 1383 - >, 1384 - ) -> Subfs<'a> { 1385 - Subfs { 1386 - subject: self.__unsafe_private_named.0.unwrap(), 1387 - r#type: self.__unsafe_private_named.1.unwrap(), 1388 - extra_data: Some(extra_data), 1389 - } 1390 - } 1391 - } 1392 - 1393 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subfs<'a> { 1394 - fn nsid() -> &'static str { 1395 - "place.wisp.subfs" 1396 - } 1397 - fn def_name() -> &'static str { 1398 - "subfs" 1399 - } 1400 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 1401 - lexicon_doc_place_wisp_subfs() 1402 - } 1403 - fn validate( 1404 - &self, 1405 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 1406 - Ok(()) 1407 - } 1408 - }
···
-8
cli/src/place_wisp.rs
··· 1 - // @generated by jacquard-lexicon. DO NOT EDIT. 2 - // 3 - // This file was automatically generated from Lexicon schemas. 4 - // Any manual changes will be overwritten on the next regeneration. 5 - 6 - pub mod fs; 7 - pub mod settings; 8 - pub mod subfs;
···
+12 -12
cli/src/pull.rs
··· 1 use crate::blob_map; 2 use crate::download; 3 use crate::metadata::SiteMetadata; 4 - use crate::place_wisp::fs::*; 5 use crate::subfs_utils; 6 use jacquard::CowStr; 7 use jacquard::prelude::IdentityResolver; ··· 410 ) -> miette::Result<Directory<'static>> { 411 use jacquard_common::IntoStatic; 412 use jacquard_common::types::value::from_data; 413 - use crate::place_wisp::subfs::SubfsRecord; 414 415 - let mut all_subfs_map: HashMap<String, crate::place_wisp::subfs::Directory> = HashMap::new(); 416 let mut to_fetch = subfs_utils::extract_subfs_uris(directory, String::new()); 417 418 if to_fetch.is_empty() { ··· 516 517 /// Extract subfs URIs from a subfs::Directory (helper for pull) 518 fn extract_subfs_uris_from_subfs_dir( 519 - directory: &crate::place_wisp::subfs::Directory, 520 current_path: String, 521 ) -> Vec<(String, String)> { 522 let mut uris = Vec::new(); ··· 529 }; 530 531 match &entry.node { 532 - crate::place_wisp::subfs::EntryNode::Subfs(subfs_node) => { 533 uris.push((subfs_node.subject.to_string(), full_path.clone())); 534 } 535 - crate::place_wisp::subfs::EntryNode::Directory(subdir) => { 536 let nested = extract_subfs_uris_from_subfs_dir(subdir, full_path); 537 uris.extend(nested); 538 } ··· 546 /// Recursively replace subfs nodes with their actual content 547 fn replace_subfs_with_content( 548 directory: Directory, 549 - subfs_map: &HashMap<String, crate::place_wisp::subfs::Directory>, 550 current_path: String, 551 ) -> Directory<'static> { 552 use jacquard_common::IntoStatic; ··· 628 } 629 630 /// Convert a subfs entry to a fs entry (they have the same structure but different types) 631 - fn convert_subfs_entry_to_fs(subfs_entry: crate::place_wisp::subfs::Entry<'static>) -> Entry<'static> { 632 use jacquard_common::IntoStatic; 633 634 let node = match subfs_entry.node { 635 - crate::place_wisp::subfs::EntryNode::File(file) => { 636 EntryNode::File(Box::new( 637 File::new() 638 .r#type(file.r#type.into_static()) ··· 643 .build() 644 )) 645 } 646 - crate::place_wisp::subfs::EntryNode::Directory(dir) => { 647 let converted_entries: Vec<Entry<'static>> = dir 648 .entries 649 .into_iter() ··· 657 .build() 658 )) 659 } 660 - crate::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => { 661 // Nested subfs should have been expanded already - if we get here, it means expansion failed 662 // Treat it like a directory reference that should have been expanded 663 eprintln!(" โš ๏ธ Warning: unexpanded nested subfs at path, treating as empty directory"); ··· 668 .build() 669 )) 670 } 671 - crate::place_wisp::subfs::EntryNode::Unknown(unknown) => { 672 EntryNode::Unknown(unknown) 673 } 674 };
··· 1 use crate::blob_map; 2 use crate::download; 3 use crate::metadata::SiteMetadata; 4 + use wisp_lexicons::place_wisp::fs::*; 5 use crate::subfs_utils; 6 use jacquard::CowStr; 7 use jacquard::prelude::IdentityResolver; ··· 410 ) -> miette::Result<Directory<'static>> { 411 use jacquard_common::IntoStatic; 412 use jacquard_common::types::value::from_data; 413 + use wisp_lexicons::place_wisp::subfs::SubfsRecord; 414 415 + let mut all_subfs_map: HashMap<String, wisp_lexicons::place_wisp::subfs::Directory> = HashMap::new(); 416 let mut to_fetch = subfs_utils::extract_subfs_uris(directory, String::new()); 417 418 if to_fetch.is_empty() { ··· 516 517 /// Extract subfs URIs from a subfs::Directory (helper for pull) 518 fn extract_subfs_uris_from_subfs_dir( 519 + directory: &wisp_lexicons::place_wisp::subfs::Directory, 520 current_path: String, 521 ) -> Vec<(String, String)> { 522 let mut uris = Vec::new(); ··· 529 }; 530 531 match &entry.node { 532 + wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(subfs_node) => { 533 uris.push((subfs_node.subject.to_string(), full_path.clone())); 534 } 535 + wisp_lexicons::place_wisp::subfs::EntryNode::Directory(subdir) => { 536 let nested = extract_subfs_uris_from_subfs_dir(subdir, full_path); 537 uris.extend(nested); 538 } ··· 546 /// Recursively replace subfs nodes with their actual content 547 fn replace_subfs_with_content( 548 directory: Directory, 549 + subfs_map: &HashMap<String, wisp_lexicons::place_wisp::subfs::Directory>, 550 current_path: String, 551 ) -> Directory<'static> { 552 use jacquard_common::IntoStatic; ··· 628 } 629 630 /// Convert a subfs entry to a fs entry (they have the same structure but different types) 631 + fn convert_subfs_entry_to_fs(subfs_entry: wisp_lexicons::place_wisp::subfs::Entry<'static>) -> Entry<'static> { 632 use jacquard_common::IntoStatic; 633 634 let node = match subfs_entry.node { 635 + wisp_lexicons::place_wisp::subfs::EntryNode::File(file) => { 636 EntryNode::File(Box::new( 637 File::new() 638 .r#type(file.r#type.into_static()) ··· 643 .build() 644 )) 645 } 646 + wisp_lexicons::place_wisp::subfs::EntryNode::Directory(dir) => { 647 let converted_entries: Vec<Entry<'static>> = dir 648 .entries 649 .into_iter() ··· 657 .build() 658 )) 659 } 660 + wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => { 661 // Nested subfs should have been expanded already - if we get here, it means expansion failed 662 // Treat it like a directory reference that should have been expanded 663 eprintln!(" โš ๏ธ Warning: unexpanded nested subfs at path, treating as empty directory"); ··· 668 .build() 669 )) 670 } 671 + wisp_lexicons::place_wisp::subfs::EntryNode::Unknown(unknown) => { 672 EntryNode::Unknown(unknown) 673 } 674 };
+1 -1
cli/src/serve.rs
··· 1 use crate::pull::pull_site; 2 use crate::redirects::{load_redirect_rules, match_redirect_rule, RedirectRule}; 3 - use crate::place_wisp::settings::Settings; 4 use axum::{ 5 Router, 6 extract::Request,
··· 1 use crate::pull::pull_site; 2 use crate::redirects::{load_redirect_rules, match_redirect_rule, RedirectRule}; 3 + use wisp_lexicons::place_wisp::settings::Settings; 4 use axum::{ 5 Router, 6 extract::Request,
+14 -14
cli/src/subfs_utils.rs
··· 6 use miette::IntoDiagnostic; 7 use std::collections::HashMap; 8 9 - use crate::place_wisp::fs::{Directory as FsDirectory, EntryNode as FsEntryNode}; 10 - use crate::place_wisp::subfs::SubfsRecord; 11 12 /// Extract all subfs URIs from a directory tree with their mount paths 13 pub fn extract_subfs_uris(directory: &FsDirectory, current_path: String) -> Vec<(String, String)> { ··· 145 146 /// Extract subfs URIs from a subfs::Directory 147 fn extract_subfs_uris_from_subfs_dir( 148 - directory: &crate::place_wisp::subfs::Directory, 149 current_path: String, 150 ) -> Vec<(String, String)> { 151 let mut uris = Vec::new(); 152 153 for entry in &directory.entries { 154 match &entry.node { 155 - crate::place_wisp::subfs::EntryNode::Subfs(subfs_node) => { 156 // Check if this is a chunk entry (chunk0, chunk1, etc.) 157 // Chunks should be flat-merged, so use the parent's path 158 let mount_path = if entry.name.starts_with("chunk") && ··· 171 172 uris.push((subfs_node.subject.to_string(), mount_path)); 173 } 174 - crate::place_wisp::subfs::EntryNode::Directory(subdir) => { 175 let full_path = if current_path.is_empty() { 176 entry.name.to_string() 177 } else { ··· 204 for (mount_path, subfs_record) in all_subfs { 205 // Check if this record only contains chunk subfs references (no files) 206 let only_has_chunks = subfs_record.root.entries.iter().all(|e| { 207 - matches!(&e.node, crate::place_wisp::subfs::EntryNode::Subfs(_)) && 208 e.name.starts_with("chunk") && 209 e.name.chars().skip(5).all(|c| c.is_ascii_digit()) 210 }); ··· 232 /// Extract blobs from a subfs directory (works with subfs::Directory) 233 /// Returns a map of file paths to their blob refs and CIDs 234 fn extract_subfs_blobs( 235 - directory: &crate::place_wisp::subfs::Directory, 236 current_path: String, 237 ) -> HashMap<String, (BlobRef<'static>, String)> { 238 let mut blob_map = HashMap::new(); ··· 245 }; 246 247 match &entry.node { 248 - crate::place_wisp::subfs::EntryNode::File(file_node) => { 249 let blob_ref = &file_node.blob; 250 let cid_string = blob_ref.blob().r#ref.to_string(); 251 blob_map.insert( ··· 253 (blob_ref.clone().into_static(), cid_string) 254 ); 255 } 256 - crate::place_wisp::subfs::EntryNode::Directory(subdir) => { 257 let sub_map = extract_subfs_blobs(subdir, full_path); 258 blob_map.extend(sub_map); 259 } 260 - crate::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => { 261 // Nested subfs - these should be resolved recursively in the main flow 262 // For now, we skip them (they'll be fetched separately) 263 eprintln!(" โš ๏ธ Found nested subfs at {}, skipping (should be fetched separately)", full_path); 264 } 265 - crate::place_wisp::subfs::EntryNode::Unknown(_) => { 266 // Skip unknown nodes 267 } 268 } ··· 352 flat: bool, 353 ) -> miette::Result<FsDirectory<'static>> { 354 use jacquard_common::CowStr; 355 - use crate::place_wisp::fs::{Entry, Subfs}; 356 357 let path_parts: Vec<&str> = target_path.split('/').collect(); 358 ··· 430 431 // Construct AT-URI and convert to RecordUri 432 let at_uri = AtUri::new(uri).into_diagnostic()?; 433 - let record_uri: RecordUri<'_, crate::place_wisp::subfs::SubfsRecordRecord> = RecordUri::try_from_uri(at_uri).into_diagnostic()?; 434 435 let rkey = record_uri.rkey() 436 .ok_or_else(|| miette::miette!("Invalid subfs URI: missing rkey"))? ··· 489 } 490 491 /// Estimate the JSON size of a single entry 492 - fn estimate_entry_size(entry: &crate::place_wisp::fs::Entry) -> usize { 493 match serde_json::to_string(entry) { 494 Ok(json) => json.len(), 495 Err(_) => 500, // Conservative estimate if serialization fails
··· 6 use miette::IntoDiagnostic; 7 use std::collections::HashMap; 8 9 + use wisp_lexicons::place_wisp::fs::{Directory as FsDirectory, EntryNode as FsEntryNode}; 10 + use wisp_lexicons::place_wisp::subfs::SubfsRecord; 11 12 /// Extract all subfs URIs from a directory tree with their mount paths 13 pub fn extract_subfs_uris(directory: &FsDirectory, current_path: String) -> Vec<(String, String)> { ··· 145 146 /// Extract subfs URIs from a subfs::Directory 147 fn extract_subfs_uris_from_subfs_dir( 148 + directory: &wisp_lexicons::place_wisp::subfs::Directory, 149 current_path: String, 150 ) -> Vec<(String, String)> { 151 let mut uris = Vec::new(); 152 153 for entry in &directory.entries { 154 match &entry.node { 155 + wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(subfs_node) => { 156 // Check if this is a chunk entry (chunk0, chunk1, etc.) 157 // Chunks should be flat-merged, so use the parent's path 158 let mount_path = if entry.name.starts_with("chunk") && ··· 171 172 uris.push((subfs_node.subject.to_string(), mount_path)); 173 } 174 + wisp_lexicons::place_wisp::subfs::EntryNode::Directory(subdir) => { 175 let full_path = if current_path.is_empty() { 176 entry.name.to_string() 177 } else { ··· 204 for (mount_path, subfs_record) in all_subfs { 205 // Check if this record only contains chunk subfs references (no files) 206 let only_has_chunks = subfs_record.root.entries.iter().all(|e| { 207 + matches!(&e.node, wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(_)) && 208 e.name.starts_with("chunk") && 209 e.name.chars().skip(5).all(|c| c.is_ascii_digit()) 210 }); ··· 232 /// Extract blobs from a subfs directory (works with subfs::Directory) 233 /// Returns a map of file paths to their blob refs and CIDs 234 fn extract_subfs_blobs( 235 + directory: &wisp_lexicons::place_wisp::subfs::Directory, 236 current_path: String, 237 ) -> HashMap<String, (BlobRef<'static>, String)> { 238 let mut blob_map = HashMap::new(); ··· 245 }; 246 247 match &entry.node { 248 + wisp_lexicons::place_wisp::subfs::EntryNode::File(file_node) => { 249 let blob_ref = &file_node.blob; 250 let cid_string = blob_ref.blob().r#ref.to_string(); 251 blob_map.insert( ··· 253 (blob_ref.clone().into_static(), cid_string) 254 ); 255 } 256 + wisp_lexicons::place_wisp::subfs::EntryNode::Directory(subdir) => { 257 let sub_map = extract_subfs_blobs(subdir, full_path); 258 blob_map.extend(sub_map); 259 } 260 + wisp_lexicons::place_wisp::subfs::EntryNode::Subfs(_nested_subfs) => { 261 // Nested subfs - these should be resolved recursively in the main flow 262 // For now, we skip them (they'll be fetched separately) 263 eprintln!(" โš ๏ธ Found nested subfs at {}, skipping (should be fetched separately)", full_path); 264 } 265 + wisp_lexicons::place_wisp::subfs::EntryNode::Unknown(_) => { 266 // Skip unknown nodes 267 } 268 } ··· 352 flat: bool, 353 ) -> miette::Result<FsDirectory<'static>> { 354 use jacquard_common::CowStr; 355 + use wisp_lexicons::place_wisp::fs::{Entry, Subfs}; 356 357 let path_parts: Vec<&str> = target_path.split('/').collect(); 358 ··· 430 431 // Construct AT-URI and convert to RecordUri 432 let at_uri = AtUri::new(uri).into_diagnostic()?; 433 + let record_uri: RecordUri<'_, wisp_lexicons::place_wisp::subfs::SubfsRecordRecord> = RecordUri::try_from_uri(at_uri).into_diagnostic()?; 434 435 let rkey = record_uri.rkey() 436 .ok_or_else(|| miette::miette!("Invalid subfs URI: missing rkey"))? ··· 489 } 490 491 /// Estimate the JSON size of a single entry 492 + fn estimate_entry_size(entry: &wisp_lexicons::place_wisp::fs::Entry) -> usize { 493 match serde_json::to_string(entry) { 494 Ok(json) => json.len(), 495 Err(_) => 500, // Conservative estimate if serialization fails
+21
docker-compose.yml
···
··· 1 + services: 2 + postgres: 3 + image: postgres:16-alpine 4 + container_name: wisp-postgres 5 + restart: unless-stopped 6 + environment: 7 + POSTGRES_USER: postgres 8 + POSTGRES_PASSWORD: postgres 9 + POSTGRES_DB: wisp 10 + ports: 11 + - "5432:5432" 12 + volumes: 13 + - postgres_data:/var/lib/postgresql/data 14 + healthcheck: 15 + test: ["CMD-SHELL", "pg_isready -U postgres"] 16 + interval: 5s 17 + timeout: 5s 18 + retries: 5 19 + 20 + volumes: 21 + postgres_data:
+4 -1
docs/astro.config.mjs
··· 7 integrations: [ 8 starlight({ 9 title: 'Wisp.place Docs', 10 - social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/tangled-org/wisp.place' }], 11 sidebar: [ 12 { 13 label: 'Getting Started', ··· 24 label: 'Guides', 25 items: [ 26 { label: 'Self-Hosting', slug: 'deployment' }, 27 { label: 'Redirects & Rewrites', slug: 'redirects' }, 28 ], 29 },
··· 7 integrations: [ 8 starlight({ 9 title: 'Wisp.place Docs', 10 + components: { 11 + SocialIcons: './src/components/SocialIcons.astro', 12 + }, 13 sidebar: [ 14 { 15 label: 'Getting Started', ··· 26 label: 'Guides', 27 items: [ 28 { label: 'Self-Hosting', slug: 'deployment' }, 29 + { label: 'Monitoring & Metrics', slug: 'monitoring' }, 30 { label: 'Redirects & Rewrites', slug: 'redirects' }, 31 ], 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>
+8 -11
docs/src/content/docs/cli.md
··· 71 72 engine: 'nixery' 73 74 - clone: 75 - skip: false 76 - depth: 1 77 - submodules: false 78 - 79 dependencies: 80 nixpkgs: 81 - nodejs ··· 141 # Pull a site to a specific directory 142 wisp-cli pull your-handle.bsky.social \ 143 --site my-site \ 144 - --output ./my-site 145 146 # Pull to current directory 147 wisp-cli pull your-handle.bsky.social \ ··· 204 205 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. 206 207 ## Incremental Updates 208 209 The CLI tracks file changes using CID-based content addressing to minimize upload times and bandwidth usage. On your first deploy, all files are uploaded to establish the initial site. For subsequent deploys, the CLI compares content-addressed CIDs to detect which files have actually changed, uploading only those that differ from the previous version. This makes fast iterations possible even for large sites, with deploys completing in seconds when only a few files have changed. ··· 236 ### Pull Command 237 238 ```bash 239 - wisp-cli pull [OPTIONS] <INPUT> 240 241 Arguments: 242 <INPUT> Handle or DID 243 244 Options: 245 -s, --site <SITE> Site name to download 246 - -o, --output <OUTPUT> Output directory [default: .] 247 -h, --help Print help 248 ``` 249 250 ### Serve Command 251 252 ```bash 253 - wisp-cli serve [OPTIONS] <INPUT> 254 255 Arguments: 256 <INPUT> Handle or DID 257 258 Options: 259 -s, --site <SITE> Site name to serve 260 - -o, --output <OUTPUT> Site files directory [default: .] 261 - -p, --port <PORT> Port to serve on [default: 8080] 262 --spa Enable SPA mode (serve index.html for all routes) 263 --directory Enable directory listing mode for paths without index files 264 -h, --help Print help
··· 71 72 engine: 'nixery' 73 74 dependencies: 75 nixpkgs: 76 - nodejs ··· 136 # Pull a site to a specific directory 137 wisp-cli pull your-handle.bsky.social \ 138 --site my-site \ 139 + --path ./my-site 140 141 # Pull to current directory 142 wisp-cli pull your-handle.bsky.social \ ··· 199 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). 203 + 204 ## Incremental Updates 205 206 The CLI tracks file changes using CID-based content addressing to minimize upload times and bandwidth usage. On your first deploy, all files are uploaded to establish the initial site. For subsequent deploys, the CLI compares content-addressed CIDs to detect which files have actually changed, uploading only those that differ from the previous version. This makes fast iterations possible even for large sites, with deploys completing in seconds when only a few files have changed. ··· 233 ### Pull Command 234 235 ```bash 236 + wisp-cli pull [OPTIONS] --site <SITE> <INPUT> 237 238 Arguments: 239 <INPUT> Handle or DID 240 241 Options: 242 -s, --site <SITE> Site name to download 243 + -p, --path <PATH> Output directory [default: .] 244 -h, --help Print help 245 ``` 246 247 ### Serve Command 248 249 ```bash 250 + wisp-cli serve [OPTIONS] --site <SITE> <INPUT> 251 252 Arguments: 253 <INPUT> Handle or DID 254 255 Options: 256 -s, --site <SITE> Site name to serve 257 + -p, --path <PATH> Site files directory [default: .] 258 + -P, --port <PORT> Port to serve on [default: 8080] 259 --spa Enable SPA mode (serve index.html for all routes) 260 --directory Enable directory listing mode for paths without index files 261 -h, --help Print help
+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 /* Increase base font size by 10% */ 6 font-size: 110%; 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); 22 --sl-color-bg-accent: oklch(0.88 0.01 35); 23 } 24 ··· 70 /* Sidebar active/hover state text contrast fix */ 71 .sidebar a[aria-current="page"], 72 .sidebar a[aria-current="page"] span { 73 - color: oklch(0.23 0.015 285) !important; 74 } 75 76 [data-theme="dark"] .sidebar a[aria-current="page"],
··· 5 /* Increase base font size by 10% */ 6 font-size: 110%; 7 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 --sl-color-bg-accent: oklch(0.88 0.01 35); 23 } 24 ··· 70 /* Sidebar active/hover state text contrast fix */ 71 .sidebar a[aria-current="page"], 72 .sidebar a[aria-current="page"] span { 73 + color: oklch(0.15 0.015 30) !important; 74 } 75 76 [data-theme="dark"] .sidebar a[aria-current="page"],
-318
flake.lock
··· 1 - { 2 - "nodes": { 3 - "crane": { 4 - "flake": false, 5 - "locked": { 6 - "lastModified": 1758758545, 7 - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", 8 - "owner": "ipetkov", 9 - "repo": "crane", 10 - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", 11 - "type": "github" 12 - }, 13 - "original": { 14 - "owner": "ipetkov", 15 - "ref": "v0.21.1", 16 - "repo": "crane", 17 - "type": "github" 18 - } 19 - }, 20 - "dream2nix": { 21 - "inputs": { 22 - "nixpkgs": [ 23 - "nci", 24 - "nixpkgs" 25 - ], 26 - "purescript-overlay": "purescript-overlay", 27 - "pyproject-nix": "pyproject-nix" 28 - }, 29 - "locked": { 30 - "lastModified": 1754978539, 31 - "narHash": "sha256-nrDovydywSKRbWim9Ynmgj8SBm8LK3DI2WuhIqzOHYI=", 32 - "owner": "nix-community", 33 - "repo": "dream2nix", 34 - "rev": "fbec3263cb4895ac86ee9506cdc4e6919a1a2214", 35 - "type": "github" 36 - }, 37 - "original": { 38 - "owner": "nix-community", 39 - "repo": "dream2nix", 40 - "type": "github" 41 - } 42 - }, 43 - "fenix": { 44 - "inputs": { 45 - "nixpkgs": [ 46 - "nixpkgs" 47 - ], 48 - "rust-analyzer-src": "rust-analyzer-src" 49 - }, 50 - "locked": { 51 - "lastModified": 1762584108, 52 - "narHash": "sha256-wZUW7dlXMXaRdvNbaADqhF8gg9bAfFiMV+iyFQiDv+Y=", 53 - "owner": "nix-community", 54 - "repo": "fenix", 55 - "rev": "32f3ad3b6c690061173e1ac16708874975ec6056", 56 - "type": "github" 57 - }, 58 - "original": { 59 - "owner": "nix-community", 60 - "repo": "fenix", 61 - "type": "github" 62 - } 63 - }, 64 - "flake-compat": { 65 - "flake": false, 66 - "locked": { 67 - "lastModified": 1696426674, 68 - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 69 - "owner": "edolstra", 70 - "repo": "flake-compat", 71 - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 72 - "type": "github" 73 - }, 74 - "original": { 75 - "owner": "edolstra", 76 - "repo": "flake-compat", 77 - "type": "github" 78 - } 79 - }, 80 - "mk-naked-shell": { 81 - "flake": false, 82 - "locked": { 83 - "lastModified": 1681286841, 84 - "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", 85 - "owner": "90-008", 86 - "repo": "mk-naked-shell", 87 - "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", 88 - "type": "github" 89 - }, 90 - "original": { 91 - "owner": "90-008", 92 - "repo": "mk-naked-shell", 93 - "type": "github" 94 - } 95 - }, 96 - "nci": { 97 - "inputs": { 98 - "crane": "crane", 99 - "dream2nix": "dream2nix", 100 - "mk-naked-shell": "mk-naked-shell", 101 - "nixpkgs": [ 102 - "nixpkgs" 103 - ], 104 - "parts": "parts", 105 - "rust-overlay": "rust-overlay", 106 - "treefmt": "treefmt" 107 - }, 108 - "locked": { 109 - "lastModified": 1762582646, 110 - "narHash": "sha256-MMzE4xccG+8qbLhdaZoeFDUKWUOn3B4lhp5dZmgukmM=", 111 - "owner": "90-008", 112 - "repo": "nix-cargo-integration", 113 - "rev": "0993c449377049fa8868a664e8290ac6658e0b9a", 114 - "type": "github" 115 - }, 116 - "original": { 117 - "owner": "90-008", 118 - "repo": "nix-cargo-integration", 119 - "type": "github" 120 - } 121 - }, 122 - "nixpkgs": { 123 - "locked": { 124 - "lastModified": 1762361079, 125 - "narHash": "sha256-lz718rr1BDpZBYk7+G8cE6wee3PiBUpn8aomG/vLLiY=", 126 - "owner": "nixos", 127 - "repo": "nixpkgs", 128 - "rev": "ffcdcf99d65c61956d882df249a9be53e5902ea5", 129 - "type": "github" 130 - }, 131 - "original": { 132 - "owner": "nixos", 133 - "ref": "nixpkgs-unstable", 134 - "repo": "nixpkgs", 135 - "type": "github" 136 - } 137 - }, 138 - "parts": { 139 - "inputs": { 140 - "nixpkgs-lib": [ 141 - "nci", 142 - "nixpkgs" 143 - ] 144 - }, 145 - "locked": { 146 - "lastModified": 1762440070, 147 - "narHash": "sha256-xxdepIcb39UJ94+YydGP221rjnpkDZUlykKuF54PsqI=", 148 - "owner": "hercules-ci", 149 - "repo": "flake-parts", 150 - "rev": "26d05891e14c88eb4a5d5bee659c0db5afb609d8", 151 - "type": "github" 152 - }, 153 - "original": { 154 - "owner": "hercules-ci", 155 - "repo": "flake-parts", 156 - "type": "github" 157 - } 158 - }, 159 - "parts_2": { 160 - "inputs": { 161 - "nixpkgs-lib": [ 162 - "nixpkgs" 163 - ] 164 - }, 165 - "locked": { 166 - "lastModified": 1762440070, 167 - "narHash": "sha256-xxdepIcb39UJ94+YydGP221rjnpkDZUlykKuF54PsqI=", 168 - "owner": "hercules-ci", 169 - "repo": "flake-parts", 170 - "rev": "26d05891e14c88eb4a5d5bee659c0db5afb609d8", 171 - "type": "github" 172 - }, 173 - "original": { 174 - "owner": "hercules-ci", 175 - "repo": "flake-parts", 176 - "type": "github" 177 - } 178 - }, 179 - "purescript-overlay": { 180 - "inputs": { 181 - "flake-compat": "flake-compat", 182 - "nixpkgs": [ 183 - "nci", 184 - "dream2nix", 185 - "nixpkgs" 186 - ], 187 - "slimlock": "slimlock" 188 - }, 189 - "locked": { 190 - "lastModified": 1728546539, 191 - "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", 192 - "owner": "thomashoneyman", 193 - "repo": "purescript-overlay", 194 - "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", 195 - "type": "github" 196 - }, 197 - "original": { 198 - "owner": "thomashoneyman", 199 - "repo": "purescript-overlay", 200 - "type": "github" 201 - } 202 - }, 203 - "pyproject-nix": { 204 - "inputs": { 205 - "nixpkgs": [ 206 - "nci", 207 - "dream2nix", 208 - "nixpkgs" 209 - ] 210 - }, 211 - "locked": { 212 - "lastModified": 1752481895, 213 - "narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=", 214 - "owner": "pyproject-nix", 215 - "repo": "pyproject.nix", 216 - "rev": "16ee295c25107a94e59a7fc7f2e5322851781162", 217 - "type": "github" 218 - }, 219 - "original": { 220 - "owner": "pyproject-nix", 221 - "repo": "pyproject.nix", 222 - "type": "github" 223 - } 224 - }, 225 - "root": { 226 - "inputs": { 227 - "fenix": "fenix", 228 - "nci": "nci", 229 - "nixpkgs": "nixpkgs", 230 - "parts": "parts_2" 231 - } 232 - }, 233 - "rust-analyzer-src": { 234 - "flake": false, 235 - "locked": { 236 - "lastModified": 1762438844, 237 - "narHash": "sha256-ApIKJf6CcMsV2nYBXhGF95BmZMO/QXPhgfSnkA/rVUo=", 238 - "owner": "rust-lang", 239 - "repo": "rust-analyzer", 240 - "rev": "4bf516ee5a960c1e2eee9fedd9b1c9e976a19c86", 241 - "type": "github" 242 - }, 243 - "original": { 244 - "owner": "rust-lang", 245 - "ref": "nightly", 246 - "repo": "rust-analyzer", 247 - "type": "github" 248 - } 249 - }, 250 - "rust-overlay": { 251 - "inputs": { 252 - "nixpkgs": [ 253 - "nci", 254 - "nixpkgs" 255 - ] 256 - }, 257 - "locked": { 258 - "lastModified": 1762569282, 259 - "narHash": "sha256-vINZAJpXQTZd5cfh06Rcw7hesH7sGSvi+Tn+HUieJn8=", 260 - "owner": "oxalica", 261 - "repo": "rust-overlay", 262 - "rev": "a35a6144b976f70827c2fe2f5c89d16d8f9179d8", 263 - "type": "github" 264 - }, 265 - "original": { 266 - "owner": "oxalica", 267 - "repo": "rust-overlay", 268 - "type": "github" 269 - } 270 - }, 271 - "slimlock": { 272 - "inputs": { 273 - "nixpkgs": [ 274 - "nci", 275 - "dream2nix", 276 - "purescript-overlay", 277 - "nixpkgs" 278 - ] 279 - }, 280 - "locked": { 281 - "lastModified": 1688756706, 282 - "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", 283 - "owner": "thomashoneyman", 284 - "repo": "slimlock", 285 - "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", 286 - "type": "github" 287 - }, 288 - "original": { 289 - "owner": "thomashoneyman", 290 - "repo": "slimlock", 291 - "type": "github" 292 - } 293 - }, 294 - "treefmt": { 295 - "inputs": { 296 - "nixpkgs": [ 297 - "nci", 298 - "nixpkgs" 299 - ] 300 - }, 301 - "locked": { 302 - "lastModified": 1762410071, 303 - "narHash": "sha256-aF5fvoZeoXNPxT0bejFUBXeUjXfHLSL7g+mjR/p5TEg=", 304 - "owner": "numtide", 305 - "repo": "treefmt-nix", 306 - "rev": "97a30861b13c3731a84e09405414398fbf3e109f", 307 - "type": "github" 308 - }, 309 - "original": { 310 - "owner": "numtide", 311 - "repo": "treefmt-nix", 312 - "type": "github" 313 - } 314 - } 315 - }, 316 - "root": "root", 317 - "version": 7 318 - }
···
-59
flake.nix
··· 1 - { 2 - inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 3 - inputs.nci.url = "github:90-008/nix-cargo-integration"; 4 - inputs.nci.inputs.nixpkgs.follows = "nixpkgs"; 5 - inputs.parts.url = "github:hercules-ci/flake-parts"; 6 - inputs.parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 7 - inputs.fenix = { 8 - url = "github:nix-community/fenix"; 9 - inputs.nixpkgs.follows = "nixpkgs"; 10 - }; 11 - 12 - outputs = inputs @ { 13 - parts, 14 - nci, 15 - ... 16 - }: 17 - parts.lib.mkFlake {inherit inputs;} { 18 - systems = ["x86_64-linux" "aarch64-darwin"]; 19 - imports = [ 20 - nci.flakeModule 21 - ./crates.nix 22 - ]; 23 - perSystem = { 24 - pkgs, 25 - config, 26 - ... 27 - }: let 28 - crateOutputs = config.nci.outputs."wisp-cli"; 29 - mkRenamedPackage = name: pkg: isWindows: pkgs.runCommand name {} '' 30 - mkdir -p $out/bin 31 - if [ -f ${pkg}/bin/wisp-cli.exe ]; then 32 - cp ${pkg}/bin/wisp-cli.exe $out/bin/${name} 33 - elif [ -f ${pkg}/bin/wisp-cli ]; then 34 - cp ${pkg}/bin/wisp-cli $out/bin/${name} 35 - else 36 - echo "Error: Could not find wisp-cli binary in ${pkg}/bin/" 37 - ls -la ${pkg}/bin/ || true 38 - exit 1 39 - fi 40 - ''; 41 - in { 42 - devShells.default = crateOutputs.devShell; 43 - packages.default = crateOutputs.packages.release; 44 - packages.wisp-cli-x86_64-linux = mkRenamedPackage "wisp-cli-x86_64-linux" crateOutputs.packages.release false; 45 - packages.wisp-cli-aarch64-linux = mkRenamedPackage "wisp-cli-aarch64-linux" crateOutputs.allTargets."aarch64-unknown-linux-gnu".packages.release false; 46 - packages.wisp-cli-x86_64-windows = mkRenamedPackage "wisp-cli-x86_64-windows.exe" crateOutputs.allTargets."x86_64-pc-windows-gnu".packages.release true; 47 - packages.wisp-cli-aarch64-darwin = mkRenamedPackage "wisp-cli-aarch64-darwin" crateOutputs.allTargets."aarch64-apple-darwin".packages.release false; 48 - packages.all = pkgs.symlinkJoin { 49 - name = "wisp-cli-all"; 50 - paths = [ 51 - config.packages.wisp-cli-x86_64-linux 52 - config.packages.wisp-cli-aarch64-linux 53 - config.packages.wisp-cli-x86_64-windows 54 - config.packages.wisp-cli-aarch64-darwin 55 - ]; 56 - }; 57 - }; 58 - }; 59 - }
···
+59
lexicons/fs.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.fs", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Virtual filesystem manifest for a Wisp site", 8 + "record": { 9 + "type": "object", 10 + "required": ["site", "root", "createdAt"], 11 + "properties": { 12 + "site": { "type": "string" }, 13 + "root": { "type": "ref", "ref": "#directory" }, 14 + "fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 }, 15 + "createdAt": { "type": "string", "format": "datetime" } 16 + } 17 + } 18 + }, 19 + "file": { 20 + "type": "object", 21 + "required": ["type", "blob"], 22 + "properties": { 23 + "type": { "type": "string", "const": "file" }, 24 + "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" }, 25 + "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" }, 26 + "mimeType": { "type": "string", "description": "Original MIME type before compression" }, 27 + "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } } 28 + }, 29 + "directory": { 30 + "type": "object", 31 + "required": ["type", "entries"], 32 + "properties": { 33 + "type": { "type": "string", "const": "directory" }, 34 + "entries": { 35 + "type": "array", 36 + "maxLength": 500, 37 + "items": { "type": "ref", "ref": "#entry" } 38 + } 39 + } 40 + }, 41 + "entry": { 42 + "type": "object", 43 + "required": ["name", "node"], 44 + "properties": { 45 + "name": { "type": "string", "maxLength": 255 }, 46 + "node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] } 47 + } 48 + }, 49 + "subfs": { 50 + "type": "object", 51 + "required": ["type", "subject"], 52 + "properties": { 53 + "type": { "type": "string", "const": "subfs" }, 54 + "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to a place.wisp.subfs record containing this subtree." }, 55 + "flat": { "type": "boolean", "description": "If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure." } 56 + } 57 + } 58 + } 59 + }
+76
lexicons/settings.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.settings", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Configuration settings for a static site hosted on wisp.place", 8 + "key": "any", 9 + "record": { 10 + "type": "object", 11 + "properties": { 12 + "directoryListing": { 13 + "type": "boolean", 14 + "description": "Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode.", 15 + "default": false 16 + }, 17 + "spaMode": { 18 + "type": "string", 19 + "description": "File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.", 20 + "maxLength": 500 21 + }, 22 + "custom404": { 23 + "type": "string", 24 + "description": "Custom 404 error page file path. Incompatible with directoryListing and spaMode.", 25 + "maxLength": 500 26 + }, 27 + "indexFiles": { 28 + "type": "array", 29 + "description": "Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.", 30 + "items": { 31 + "type": "string", 32 + "maxLength": 255 33 + }, 34 + "maxLength": 10 35 + }, 36 + "cleanUrls": { 37 + "type": "boolean", 38 + "description": "Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically.", 39 + "default": false 40 + }, 41 + "headers": { 42 + "type": "array", 43 + "description": "Custom HTTP headers to set on responses", 44 + "items": { 45 + "type": "ref", 46 + "ref": "#customHeader" 47 + }, 48 + "maxLength": 50 49 + } 50 + } 51 + } 52 + }, 53 + "customHeader": { 54 + "type": "object", 55 + "description": "Custom HTTP header configuration", 56 + "required": ["name", "value"], 57 + "properties": { 58 + "name": { 59 + "type": "string", 60 + "description": "HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')", 61 + "maxLength": 100 62 + }, 63 + "value": { 64 + "type": "string", 65 + "description": "HTTP header value", 66 + "maxLength": 1000 67 + }, 68 + "path": { 69 + "type": "string", 70 + "description": "Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.", 71 + "maxLength": 500 72 + } 73 + } 74 + } 75 + } 76 + }
+59
lexicons/subfs.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.subfs", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.", 8 + "record": { 9 + "type": "object", 10 + "required": ["root", "createdAt"], 11 + "properties": { 12 + "root": { "type": "ref", "ref": "#directory" }, 13 + "fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 }, 14 + "createdAt": { "type": "string", "format": "datetime" } 15 + } 16 + } 17 + }, 18 + "file": { 19 + "type": "object", 20 + "required": ["type", "blob"], 21 + "properties": { 22 + "type": { "type": "string", "const": "file" }, 23 + "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" }, 24 + "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" }, 25 + "mimeType": { "type": "string", "description": "Original MIME type before compression" }, 26 + "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } 27 + } 28 + }, 29 + "directory": { 30 + "type": "object", 31 + "required": ["type", "entries"], 32 + "properties": { 33 + "type": { "type": "string", "const": "directory" }, 34 + "entries": { 35 + "type": "array", 36 + "maxLength": 500, 37 + "items": { "type": "ref", "ref": "#entry" } 38 + } 39 + } 40 + }, 41 + "entry": { 42 + "type": "object", 43 + "required": ["name", "node"], 44 + "properties": { 45 + "name": { "type": "string", "maxLength": 255 }, 46 + "node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] } 47 + } 48 + }, 49 + "subfs": { 50 + "type": "object", 51 + "required": ["type", "subject"], 52 + "properties": { 53 + "type": { "type": "string", "const": "subfs" }, 54 + "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures." } 55 + } 56 + } 57 + } 58 + } 59 +
+13 -1
package.json
··· 9 ], 10 "dependencies": { 11 "@tailwindcss/cli": "^4.1.17", 12 "bun-plugin-tailwind": "^0.1.2", 13 "tailwindcss": "^4.1.17" 14 }, 15 "scripts": { ··· 19 "build": "cd apps/main-app && bun run build.ts", 20 "build:hosting": "cd apps/hosting-service && npm run build", 21 "build:all": "bun run build && npm run build:hosting", 22 "screenshot": "bun run apps/main-app/scripts/screenshot-sites.ts", 23 "hosting:dev": "cd apps/hosting-service && npm run dev", 24 - "hosting:start": "cd apps/hosting-service && npm run start" 25 } 26 }
··· 9 ], 10 "dependencies": { 11 "@tailwindcss/cli": "^4.1.17", 12 + "atproto-ui": "^0.12.0", 13 "bun-plugin-tailwind": "^0.1.2", 14 + "elysia": "^1.4.18", 15 "tailwindcss": "^4.1.17" 16 }, 17 "scripts": { ··· 21 "build": "cd apps/main-app && bun run build.ts", 22 "build:hosting": "cd apps/hosting-service && npm run build", 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", 25 "screenshot": "bun run apps/main-app/scripts/screenshot-sites.ts", 26 "hosting:dev": "cd apps/hosting-service && npm run dev", 27 + "hosting:start": "cd apps/hosting-service && npm run start", 28 + "codegen": "./scripts/codegen.sh" 29 + }, 30 + "trustedDependencies": [ 31 + "@parcel/watcher", 32 + "bun", 33 + "esbuild" 34 + ], 35 + "devDependencies": { 36 + "@types/bun": "^1.3.5" 37 } 38 }
+3
packages/@wisp/atproto-utils/package.json
··· 27 "@atproto/api": "^0.14.1", 28 "@wisp/lexicons": "workspace:*", 29 "multiformats": "^13.3.1" 30 } 31 }
··· 27 "@atproto/api": "^0.14.1", 28 "@wisp/lexicons": "workspace:*", 29 "multiformats": "^13.3.1" 30 + }, 31 + "devDependencies": { 32 + "@atproto/lexicon": "^0.5.2" 33 } 34 }
+1 -1
packages/@wisp/constants/src/index.ts
··· 14 15 // File size limits 16 export const MAX_SITE_SIZE = 300 * 1024 * 1024; // 300MB 17 - export const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB 18 export const MAX_FILE_COUNT = 1000; 19 20 // Cache configuration
··· 14 15 // File size limits 16 export const MAX_SITE_SIZE = 300 * 1024 * 1024; // 300MB 17 + export const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB 18 export const MAX_FILE_COUNT = 1000; 19 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 + })
-59
packages/@wisp/lexicons/lexicons/fs.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "place.wisp.fs", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "description": "Virtual filesystem manifest for a Wisp site", 8 - "record": { 9 - "type": "object", 10 - "required": ["site", "root", "createdAt"], 11 - "properties": { 12 - "site": { "type": "string" }, 13 - "root": { "type": "ref", "ref": "#directory" }, 14 - "fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 }, 15 - "createdAt": { "type": "string", "format": "datetime" } 16 - } 17 - } 18 - }, 19 - "file": { 20 - "type": "object", 21 - "required": ["type", "blob"], 22 - "properties": { 23 - "type": { "type": "string", "const": "file" }, 24 - "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" }, 25 - "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" }, 26 - "mimeType": { "type": "string", "description": "Original MIME type before compression" }, 27 - "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } } 28 - }, 29 - "directory": { 30 - "type": "object", 31 - "required": ["type", "entries"], 32 - "properties": { 33 - "type": { "type": "string", "const": "directory" }, 34 - "entries": { 35 - "type": "array", 36 - "maxLength": 500, 37 - "items": { "type": "ref", "ref": "#entry" } 38 - } 39 - } 40 - }, 41 - "entry": { 42 - "type": "object", 43 - "required": ["name", "node"], 44 - "properties": { 45 - "name": { "type": "string", "maxLength": 255 }, 46 - "node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] } 47 - } 48 - }, 49 - "subfs": { 50 - "type": "object", 51 - "required": ["type", "subject"], 52 - "properties": { 53 - "type": { "type": "string", "const": "subfs" }, 54 - "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to a place.wisp.subfs record containing this subtree." }, 55 - "flat": { "type": "boolean", "description": "If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure." } 56 - } 57 - } 58 - } 59 - }
···
-76
packages/@wisp/lexicons/lexicons/settings.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "place.wisp.settings", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "description": "Configuration settings for a static site hosted on wisp.place", 8 - "key": "any", 9 - "record": { 10 - "type": "object", 11 - "properties": { 12 - "directoryListing": { 13 - "type": "boolean", 14 - "description": "Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode.", 15 - "default": false 16 - }, 17 - "spaMode": { 18 - "type": "string", 19 - "description": "File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404.", 20 - "maxLength": 500 21 - }, 22 - "custom404": { 23 - "type": "string", 24 - "description": "Custom 404 error page file path. Incompatible with directoryListing and spaMode.", 25 - "maxLength": 500 26 - }, 27 - "indexFiles": { 28 - "type": "array", 29 - "description": "Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified.", 30 - "items": { 31 - "type": "string", 32 - "maxLength": 255 33 - }, 34 - "maxLength": 10 35 - }, 36 - "cleanUrls": { 37 - "type": "boolean", 38 - "description": "Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically.", 39 - "default": false 40 - }, 41 - "headers": { 42 - "type": "array", 43 - "description": "Custom HTTP headers to set on responses", 44 - "items": { 45 - "type": "ref", 46 - "ref": "#customHeader" 47 - }, 48 - "maxLength": 50 49 - } 50 - } 51 - } 52 - }, 53 - "customHeader": { 54 - "type": "object", 55 - "description": "Custom HTTP header configuration", 56 - "required": ["name", "value"], 57 - "properties": { 58 - "name": { 59 - "type": "string", 60 - "description": "HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options')", 61 - "maxLength": 100 62 - }, 63 - "value": { 64 - "type": "string", 65 - "description": "HTTP header value", 66 - "maxLength": 1000 67 - }, 68 - "path": { 69 - "type": "string", 70 - "description": "Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths.", 71 - "maxLength": 500 72 - } 73 - } 74 - } 75 - } 76 - }
···
-59
packages/@wisp/lexicons/lexicons/subfs.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "place.wisp.subfs", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "description": "Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.", 8 - "record": { 9 - "type": "object", 10 - "required": ["root", "createdAt"], 11 - "properties": { 12 - "root": { "type": "ref", "ref": "#directory" }, 13 - "fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 }, 14 - "createdAt": { "type": "string", "format": "datetime" } 15 - } 16 - } 17 - }, 18 - "file": { 19 - "type": "object", 20 - "required": ["type", "blob"], 21 - "properties": { 22 - "type": { "type": "string", "const": "file" }, 23 - "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" }, 24 - "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" }, 25 - "mimeType": { "type": "string", "description": "Original MIME type before compression" }, 26 - "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } 27 - } 28 - }, 29 - "directory": { 30 - "type": "object", 31 - "required": ["type", "entries"], 32 - "properties": { 33 - "type": { "type": "string", "const": "directory" }, 34 - "entries": { 35 - "type": "array", 36 - "maxLength": 500, 37 - "items": { "type": "ref", "ref": "#entry" } 38 - } 39 - } 40 - }, 41 - "entry": { 42 - "type": "object", 43 - "required": ["name", "node"], 44 - "properties": { 45 - "name": { "type": "string", "maxLength": 255 }, 46 - "node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] } 47 - } 48 - }, 49 - "subfs": { 50 - "type": "object", 51 - "required": ["type", "subject"], 52 - "properties": { 53 - "type": { "type": "string", "const": "subfs" }, 54 - "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures." } 55 - } 56 - } 57 - } 58 - } 59 -
···
+3 -2
packages/@wisp/lexicons/package.json
··· 32 } 33 }, 34 "scripts": { 35 - "codegen": "lex gen-server ./src ./lexicons" 36 }, 37 "dependencies": { 38 "@atproto/lexicon": "^0.5.1", 39 "@atproto/xrpc-server": "^0.9.5" 40 }, 41 "devDependencies": { 42 - "@atproto/lex-cli": "^0.9.5" 43 } 44 }
··· 32 } 33 }, 34 "scripts": { 35 + "codegen": "lex gen-server ./src ../../../lexicons/*.json" 36 }, 37 "dependencies": { 38 "@atproto/lexicon": "^0.5.1", 39 "@atproto/xrpc-server": "^0.9.5" 40 }, 41 "devDependencies": { 42 + "@atproto/lex-cli": "^0.9.5", 43 + "multiformats": "^13.4.1" 44 } 45 }
+1 -1
packages/@wisp/lexicons/src/index.ts
··· 9 type MethodConfigOrHandler, 10 createServer as createXrpcServer, 11 } from '@atproto/xrpc-server' 12 - import { schemas } from './lexicons' 13 14 export function createServer(options?: XrpcOptions): Server { 15 return new Server(options)
··· 9 type MethodConfigOrHandler, 10 createServer as createXrpcServer, 11 } from '@atproto/xrpc-server' 12 + import { schemas } from './lexicons.js' 13 14 export function createServer(options?: XrpcOptions): Server { 15 return new Server(options)
+1 -1
packages/@wisp/lexicons/src/lexicons.ts
··· 7 ValidationError, 8 type ValidationResult, 9 } from '@atproto/lexicon' 10 - import { type $Typed, is$typed, maybe$typed } from './util' 11 12 export const schemaDict = { 13 PlaceWispFs: {
··· 7 ValidationError, 8 type ValidationResult, 9 } from '@atproto/lexicon' 10 + import { type $Typed, is$typed, maybe$typed } from './util.js' 11 12 export const schemaDict = { 13 PlaceWispFs: {
+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 } 25 }, 26 "peerDependencies": { 27 - "hono": "^4.0.0" 28 }, 29 "peerDependenciesMeta": { 30 "hono": { 31 "optional": true 32 } 33 } 34 }
··· 24 } 25 }, 26 "peerDependencies": { 27 + "hono": "^4.10.7" 28 }, 29 "peerDependenciesMeta": { 30 "hono": { 31 "optional": true 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" 45 } 46 }
+12 -2
packages/@wisp/observability/src/core.ts
··· 3 * Framework-agnostic logging, error tracking, and metrics collection 4 */ 5 6 // ============================================================================ 7 // Types 8 // ============================================================================ ··· 128 logs.splice(MAX_LOGS) 129 } 130 131 // Also log to console for compatibility 132 const contextStr = context ? ` ${JSON.stringify(context)}` : '' 133 const traceStr = traceId ? ` [trace:${traceId}]` : '' ··· 163 }, 164 165 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') { 168 this.log('debug', message, service, context, traceId) 169 } 170 }, ··· 233 234 errors.set(key, entry) 235 236 // Rotate if needed 237 if (errors.size > MAX_ERRORS) { 238 const oldest = Array.from(errors.keys())[0] ··· 284 } 285 286 metrics.unshift(entry) 287 288 // Rotate if needed 289 if (metrics.length > MAX_METRICS) {
··· 3 * Framework-agnostic logging, error tracking, and metrics collection 4 */ 5 6 + import { lokiExporter, metricsExporter } from './exporters' 7 + 8 // ============================================================================ 9 // Types 10 // ============================================================================ ··· 130 logs.splice(MAX_LOGS) 131 } 132 133 + // Send to Loki exporter 134 + lokiExporter.pushLog(entry) 135 + 136 // Also log to console for compatibility 137 const contextStr = context ? ` ${JSON.stringify(context)}` : '' 138 const traceStr = traceId ? ` [trace:${traceId}]` : '' ··· 168 }, 169 170 debug(message: string, service: string, context?: Record<string, any>, traceId?: string) { 171 + if (process.env.NODE_ENV !== 'production') { 172 this.log('debug', message, service, context, traceId) 173 } 174 }, ··· 237 238 errors.set(key, entry) 239 240 + // Send to Loki exporter 241 + lokiExporter.pushError(entry) 242 + 243 // Rotate if needed 244 if (errors.size > MAX_ERRORS) { 245 const oldest = Array.from(errors.keys())[0] ··· 291 } 292 293 metrics.unshift(entry) 294 + 295 + // Send to Prometheus/OTLP exporter 296 + metricsExporter.recordMetric(entry) 297 298 // Rotate if needed 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 type { LogEntry, ErrorEntry, MetricEntry } from './core' 7 + import { metrics, type 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 // Export everything from core 7 export * from './core' 8 9 // Note: Middleware should be imported from specific subpaths: 10 // - import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 11 // - import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono'
··· 6 // Export everything from core 7 export * from './core' 8 9 + // Export Grafana integration 10 + export { 11 + initializeGrafanaExporters, 12 + shutdownGrafanaExporters, 13 + grafanaConfig, 14 + type GrafanaConfig 15 + } from './exporters' 16 + 17 // Note: Middleware should be imported from specific subpaths: 18 // - import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 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 const MAX_BLOB_SIZE = 500 * 1024 * 1024; // 500MB 29 const MAX_REDIRECTS = 10; 30 31 function isBlockedHost(hostname: string): boolean { 32 const lowerHost = hostname.toLowerCase(); 33 ··· 44 return false; 45 } 46 47 export async function safeFetch( 48 url: string, 49 - options?: RequestInit & { maxSize?: number; timeout?: number } 50 ): Promise<Response> { 51 const timeoutMs = options?.timeout ?? FETCH_TIMEOUT; 52 const maxSize = options?.maxSize ?? MAX_RESPONSE_SIZE; 53 54 - // Parse and validate URL 55 let parsedUrl: URL; 56 try { 57 parsedUrl = new URL(url); ··· 68 throw new Error(`Blocked host: ${hostname}`); 69 } 70 71 - const controller = new AbortController(); 72 - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); 73 74 - try { 75 - const response = await fetch(url, { 76 - ...options, 77 - signal: controller.signal, 78 - redirect: 'follow', 79 - }); 80 81 - const contentLength = response.headers.get('content-length'); 82 - if (contentLength && parseInt(contentLength, 10) > maxSize) { 83 - throw new Error(`Response too large: ${contentLength} bytes`); 84 } 85 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); 94 } 95 } 96 97 export async function safeFetchJson<T = any>( 98 url: string, 99 - options?: RequestInit & { maxSize?: number; timeout?: number } 100 ): Promise<T> { 101 const maxJsonSize = options?.maxSize ?? MAX_JSON_SIZE; 102 const response = await safeFetch(url, { ...options, maxSize: maxJsonSize }); ··· 142 143 export async function safeFetchBlob( 144 url: string, 145 - options?: RequestInit & { maxSize?: number; timeout?: number } 146 ): Promise<Uint8Array> { 147 const maxBlobSize = options?.maxSize ?? MAX_BLOB_SIZE; 148 const timeoutMs = options?.timeout ?? FETCH_TIMEOUT_BLOB;
··· 28 const MAX_BLOB_SIZE = 500 * 1024 * 1024; // 500MB 29 const MAX_REDIRECTS = 10; 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 + 36 function isBlockedHost(hostname: string): boolean { 37 const lowerHost = hostname.toLowerCase(); 38 ··· 49 return false; 50 } 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 + 139 export async function safeFetch( 140 url: string, 141 + options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean } 142 ): Promise<Response> { 143 + const shouldRetry = options?.retry !== false; // Default to true 144 const timeoutMs = options?.timeout ?? FETCH_TIMEOUT; 145 const maxSize = options?.maxSize ?? MAX_RESPONSE_SIZE; 146 147 + // Parse and validate URL (done once, outside retry loop) 148 let parsedUrl: URL; 149 try { 150 parsedUrl = new URL(url); ··· 161 throw new Error(`Blocked host: ${hostname}`); 162 } 163 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 + }); 178 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 + } 183 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); 192 } 193 + }; 194 195 + if (shouldRetry) { 196 + return withRetry(fetchFn, { context: `Fetch ${parsedUrl.hostname}` }); 197 + } else { 198 + return fetchFn(); 199 } 200 } 201 202 export async function safeFetchJson<T = any>( 203 url: string, 204 + options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean } 205 ): Promise<T> { 206 const maxJsonSize = options?.maxSize ?? MAX_JSON_SIZE; 207 const response = await safeFetch(url, { ...options, maxSize: maxJsonSize }); ··· 247 248 export async function safeFetchBlob( 249 url: string, 250 + options?: RequestInit & { maxSize?: number; timeout?: number; retry?: boolean } 251 ): Promise<Uint8Array> { 252 const maxBlobSize = options?.maxSize ?? MAX_BLOB_SIZE; 253 const timeoutMs = options?.timeout ?? FETCH_TIMEOUT_BLOB;
+28
scripts/codegen.sh
···
··· 1 + #!/bin/bash 2 + set -e 3 + 4 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 5 + ROOT_DIR="$(dirname "$SCRIPT_DIR")" 6 + 7 + # Parse arguments 8 + AUTO_ACCEPT="" 9 + if [[ "$1" == "-y" || "$1" == "--yes" ]]; then 10 + AUTO_ACCEPT="yes |" 11 + fi 12 + 13 + echo "=== Generating TypeScript lexicons ===" 14 + cd "$ROOT_DIR/packages/@wisp/lexicons" 15 + eval "$AUTO_ACCEPT npm run codegen" 16 + 17 + echo "=== Generating Rust lexicons ===" 18 + echo "Installing jacquard-lexgen..." 19 + cargo install jacquard-lexgen --version 0.9.5 2>/dev/null || true 20 + echo "Running jacquard-codegen..." 21 + echo " Input: $ROOT_DIR/lexicons" 22 + echo " Output: $ROOT_DIR/cli/crates/lexicons/src" 23 + jacquard-codegen -i "$ROOT_DIR/lexicons" -o "$ROOT_DIR/cli/crates/lexicons/src" 24 + 25 + # Add extern crate alloc for the macro to work 26 + sed -i '' '1s/^/extern crate alloc;\n\n/' "$ROOT_DIR/cli/crates/lexicons/src/lib.rs" 27 + 28 + echo "=== Done ==="
+1 -1
tsconfig.json
··· 33 // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 "types": [ 36 - "bun-types" 37 ] /* Specify type package names to be included without being referenced in a source file. */, 38 // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
··· 33 // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 "types": [ 36 + "bun" 37 ] /* Specify type package names to be included without being referenced in a source file. */, 38 // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */