diff --git a/.env.example b/.env.example index 5539232..a9d9b4e 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,9 @@ VITE_SUPABASE_URL=https://your-project-id.supabase.co # This key has Row Level Security (RLS) enabled VITE_SUPABASE_ANON_KEY=your-anon-key-here +# Auth API (WebAuthn + NIP-98 server on Cloudflare Workers) +VITE_AUTH_API_URL=https://dreamlab-auth-api.solitary-paper-764d.workers.dev + # SECURITY NOTES: # - Never commit .env files with real credentials to git # - The ANON key is designed to be public but should still use RLS policies diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..383fd1b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,44 @@ +# Quality Engineering Standards (Agentic QE) + +## AQE MCP Server + +This project uses Agentic QE for AI-powered quality engineering. The AQE MCP server provides tools for test generation, coverage analysis, quality assessment, and learning. + +## Setup + +Always call `fleet_init` before using other AQE tools to initialize the QE fleet. + +## Available Tools + +### Test Generation +- `test_generate_enhanced` — AI-powered test generation with pattern recognition and anti-pattern detection +- Supports unit, integration, and e2e test types + +### Coverage Analysis +- `coverage_analyze_sublinear` — O(log n) coverage gap detection with ML-powered analysis +- Target: 80% statement coverage minimum, focus on risk-weighted coverage + +### Quality Assessment +- `quality_assess` — Quality gate evaluation with configurable thresholds +- Run before marking tasks complete + +### Security Scanning +- `security_scan_comprehensive` — SAST/DAST vulnerability scanning +- Run after changes to auth, security, or middleware code + +### Defect Prediction +- `defect_predict` — AI analysis of code complexity and change history + +### Learning & Memory +- `memory_store` — Store patterns and learnings for future reference +- `memory_query` — Query past patterns before starting work +- Always store successful patterns after task completion + +## Best Practices + +1. **Test Pyramid**: 70% unit, 20% integration, 10% e2e +2. **AAA Pattern**: Arrange-Act-Assert for clear test structure +3. **One assertion per test**: Test one behavior at a time +4. **Descriptive names**: `should_returnValue_when_condition` +5. **Mock at boundaries**: Only mock external dependencies +6. **Edge cases first**: Test boundary conditions, not just happy paths diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a2a7dee..538dad5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,106 +1,149 @@ name: Deploy to GitHub Pages +# Builds the React main site + Rust/WASM Leptos forum client and deploys +# both to GitHub Pages (dreamlab-ai.com). +# +# Layout: +# / → React marketing site +# /community/ → Leptos forum (Rust WASM) +# +# Rollback: revert the merge commit or reset main to `pre-rust-deploy` tag. + on: push: branches: - main workflow_dispatch: +env: + CARGO_TERM_COLOR: always + # Production API URLs — baked into WASM via option_env!() at compile time + VITE_RELAY_URL: 'wss://dreamlab-nostr-relay.solitary-paper-764d.workers.dev' + VITE_AUTH_API_URL: 'https://dreamlab-auth-api.solitary-paper-764d.workers.dev' + VITE_POD_API_URL: 'https://dreamlab-pod-api.solitary-paper-764d.workers.dev' + VITE_SEARCH_API_URL: 'https://dreamlab-search-api.solitary-paper-764d.workers.dev' + VITE_LINK_PREVIEW_API_URL: 'https://dreamlab-link-preview.solitary-paper-764d.workers.dev' + # Forum base path for leptos_router (sub-directory deployment) + FORUM_BASE: '/community' + jobs: build-and-deploy: + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' runs-on: ubuntu-latest permissions: contents: write steps: - name: Check out the code - uses: actions/checkout@v3 - # persist-credentials defaults to true, so we can drop the override. + uses: actions/checkout@v4 - - name: Use Node - uses: actions/setup-node@v3 + # ── React main site ──────────────────────────────────────────────── + - name: Set up Node.js + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: '20' + cache: 'npm' - - name: Install dependencies - run: npm install - - - name: Create .env file - run: | - echo "VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}" > .env - echo "VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}" >> .env + - name: Install Node dependencies + run: npm ci - - name: Prepare team data - run: | - mkdir -p public/data/team - if [ -d "src/data/team" ]; then - cp -rv src/data/team/* public/data/team/ - echo "Copied team data from src to public" - ls -la public/data/team/ - fi - - - name: Build + - name: Build React main site run: npm run build - - name: Create .nojekyll file - run: touch dist/.nojekyll + # ── Rust/WASM forum client ───────────────────────────────────────── + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown - - name: Copy 404.html + - name: Cache Cargo registry + build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + community-forum-rs/target + key: ${{ runner.os }}-cargo-${{ hashFiles('community-forum-rs/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install Trunk run: | - if [ -f "public/404.html" ]; then - cp public/404.html dist/404.html - echo "✅ 404.html copied to dist" - else - echo "❌ 404.html not found in public directory" - fi - - - name: Copy all data files to dist + TRUNK_VERSION="0.21.12" + curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \ + | tar xz -C /usr/local/bin + trunk --version + + - name: Build Leptos forum with Trunk + working-directory: community-forum-rs/crates/forum-client + run: trunk build --release --public-url /community/ + + # ── Combine outputs ──────────────────────────────────────────────── + - name: Merge React + Forum into dist/ run: | - echo "Copying all public/data to dist/data..." - mkdir -p dist/data - cp -rv public/data/* dist/data/ || echo "No data to copy" + # React build already in dist/ + # Copy forum output into dist/community/ + mkdir -p dist/community + cp -r community-forum-rs/dist/* dist/community/ - echo "Contents of dist/data:" - find dist/data -type f | head -20 + echo "=== React site ===" + ls dist/index.html dist/assets/ | head -5 - echo "Video files in dist/data/media/videos:" - ls -la dist/data/media/videos/ || echo "No video files found" + echo "=== Forum (community/) ===" + ls dist/community/ - echo "Thumbnail files in dist/data/media:" - ls -la dist/data/media/*-thumb.jpg || echo "No thumbnail files found" + - name: Inject runtime env config into forum + run: | + ENV_SCRIPT='' + sed -i "s||${ENV_SCRIPT}|" dist/community/index.html + echo "Injected window.__ENV__ into forum index.html" - - name: Verify build artifacts + - name: Create GitHub Pages SPA routing files run: | - echo "Verifying team data:" - ls -la dist/data/team/ || echo "dist/data/team directory not found!" - - echo "Verifying video files:" - if [ -d "dist/data/media/videos" ]; then - echo "✅ Videos directory exists" - ls -la dist/data/media/videos/ - else - echo "❌ Videos directory missing" - fi - - echo "Verifying thumbnail files:" - if ls dist/data/media/*-thumb.jpg 1> /dev/null 2>&1; then - echo "✅ Thumbnail files exist:" - ls -la dist/data/media/*-thumb.jpg - else - echo "❌ Thumbnail files missing" - fi - - if [ -f "dist/data/team/06.md" ]; then - echo "✅ 06.md exists in build output" - else - echo "❌ 06.md missing from build output" - fi - if [ -f "dist/data/team/06.png" ]; then - echo "✅ 06.png exists in build output" - else - echo "❌ 06.png missing from build output" - fi + # Smart 404.html: routes /community/* to the forum SPA, + # all other paths to the React SPA. + cat > dist/404.html << 'REDIRECT_EOF' + + Redirecting... + + + REDIRECT_EOF + + # Forum also needs its own 404.html for direct community/ sub-paths + cp dist/community/index.html dist/community/404.html + + # Prevent Jekyll processing + touch dist/.nojekyll + + echo "SPA routing files created" + + - name: Inject React SPA redirect pickup + run: | + # Add redirect pickup script to React index.html (before ) + PICKUP='' + sed -i "s||${PICKUP}|" dist/index.html + echo "Injected SPA redirect pickup into React index.html" + # ── Deploy ───────────────────────────────────────────────────────── - name: Deploy to gh-pages uses: peaceiris/actions-gh-pages@v3 with: @@ -109,4 +152,12 @@ jobs: publish_branch: gh-pages force_orphan: true cname: dreamlab-ai.com - commit_message: "Deploy from GitHub Actions ${{ github.sha }}" + commit_message: "Deploy React + Leptos forum from ${{ github.sha }}" + + - name: Deploy to Cloudflare Pages + if: vars.CLOUDFLARE_PAGES_ENABLED == 'true' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dist/ --project-name=dreamlab-ai --commit-dirty=true diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml new file mode 100644 index 0000000..baf0c95 --- /dev/null +++ b/.github/workflows/docs-update.yml @@ -0,0 +1,136 @@ +name: Documentation Auto-Update + +on: + schedule: + # Update documentation every Sunday at 22:00 UTC + - cron: '0 22 * * 0' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-timestamps: + name: Update Last Modified Timestamps + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Update timestamps + run: | + #!/bin/bash + set -e + + echo "Updating last_updated timestamps..." + + for file in $(find docs -name "*.md" ! -path "*/working/*"); do + # Get last git commit date for this file + last_commit_date=$(git log -1 --format="%ad" --date=short -- "$file" 2>/dev/null || echo "") + + if [[ -z "$last_commit_date" ]]; then + continue + fi + + # Check if file has front matter + if ! head -n 1 "$file" | grep -q "^---$"; then + continue + fi + + # Update last_updated field + if grep -q "^last_updated:" "$file"; then + current_date=$(grep "^last_updated:" "$file" | cut -d':' -f2- | xargs) + + if [[ "$current_date" != "$last_commit_date" ]]; then + sed -i "s/^last_updated:.*/last_updated: $last_commit_date/" "$file" + echo "Updated: $file ($current_date -> $last_commit_date)" + fi + fi + done + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + commit-message: 'docs: update last_modified timestamps' + title: '📅 Automated documentation timestamp update' + body: | + This PR updates `last_updated` timestamps in documentation front matter based on git commit history. + + Generated with [Claude Code](https://claude.com/claude-code) + + Co-Authored-By: Claude Opus 4.5 + branch: docs/auto-update-timestamps + delete-branch: true + labels: documentation, automated + + check-outdated: + name: Check for Outdated Documentation + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Find outdated docs + run: | + #!/bin/bash + set -e + + echo "# Outdated Documentation Report" > outdated.md + echo "" >> outdated.md + echo "Generated: $(date -u +%Y-%m-%d\ %H:%M:%S\ UTC)" >> outdated.md + echo "" >> outdated.md + + THRESHOLD_DAYS=90 + CURRENT_EPOCH=$(date +%s) + + while IFS= read -r file; do + if ! grep -q "^last_updated:" "$file"; then + continue + fi + + last_updated=$(grep "^last_updated:" "$file" | cut -d':' -f2- | xargs) + last_updated_epoch=$(date -d "$last_updated" +%s 2>/dev/null || echo 0) + + if [[ $last_updated_epoch -eq 0 ]]; then + continue + fi + + days_old=$(( (CURRENT_EPOCH - last_updated_epoch) / 86400 )) + + if [[ $days_old -gt $THRESHOLD_DAYS ]]; then + echo "- **$file**: Last updated $days_old days ago ($last_updated)" >> outdated.md + fi + done < <(find docs -name "*.md" ! -path "*/working/*") + + if [[ $(wc -l < outdated.md) -gt 4 ]]; then + echo "" >> outdated.md + echo "**Action Required**: These documents should be reviewed and updated." >> outdated.md + cat outdated.md + else + echo "No outdated documentation found." + fi + + - name: Create issue for outdated docs + if: hashFiles('outdated.md') != '' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const content = fs.readFileSync('outdated.md', 'utf8'); + + if (content.split('\n').length > 4) { + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '📚 Documentation review needed: outdated files detected', + body: content, + labels: ['documentation', 'maintenance', 'review-needed'] + }); + } diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..44dc071 --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,98 @@ +name: Rust CI + +on: + push: + branches: [rust-version] + pull_request: + branches: [rust-version, main] + +env: + CARGO_TERM_COLOR: always + MANIFEST: community-forum-rs/Cargo.toml + +jobs: + fmt: + name: Format + runs-on: ubuntu-latest + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --manifest-path ${{ env.MANIFEST }} --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + community-forum-rs/target + key: rust-${{ hashFiles('community-forum-rs/Cargo.lock') }} + restore-keys: rust- + - run: cargo clippy --manifest-path ${{ env.MANIFEST }} --all-targets -- -D warnings + + test-native: + name: Test (native) + runs-on: ubuntu-latest + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + community-forum-rs/target + key: rust-${{ hashFiles('community-forum-rs/Cargo.lock') }} + restore-keys: rust- + - run: cargo test --manifest-path ${{ env.MANIFEST }} --all + + test-wasm: + name: Test (wasm) + runs-on: ubuntu-latest + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + community-forum-rs/target + key: rust-${{ hashFiles('community-forum-rs/Cargo.lock') }} + restore-keys: rust- + - run: cargo install wasm-bindgen-cli --locked || true + - run: cargo test --manifest-path ${{ env.MANIFEST }} --target wasm32-unknown-unknown -p nostr-core + + check-wasm-client: + name: Check (wasm client) + runs-on: ubuntu-latest + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + community-forum-rs/target + key: rust-${{ hashFiles('community-forum-rs/Cargo.lock') }} + restore-keys: rust- + - run: cargo check --manifest-path ${{ env.MANIFEST }} --target wasm32-unknown-unknown -p forum-client diff --git a/.github/workflows/workers-deploy.yml b/.github/workflows/workers-deploy.yml new file mode 100644 index 0000000..421e99b --- /dev/null +++ b/.github/workflows/workers-deploy.yml @@ -0,0 +1,218 @@ +name: Deploy Cloudflare Workers + +# Deploys TypeScript Workers from workers/ and Rust Workers from community-forum-rs/crates/. +# Runs on DreamLab-AI/dreamlab-ai-website fork only. + +on: + push: + branches: [main] + paths: + - 'workers/**' + - 'community-forum-rs/crates/preview-worker/**' + - 'community-forum-rs/crates/pod-worker/**' + - 'community-forum-rs/crates/auth-worker/**' + - '.github/workflows/workers-deploy.yml' + workflow_dispatch: + +permissions: + contents: read + +env: + NODE_VERSION: '20' + CF_SUBDOMAIN: 'solitary-paper-764d' + +jobs: + # ─── TypeScript Workers ──────────────────────────────────────────── + deploy-ts: + name: Deploy TS Workers + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Deploy auth-api + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: '3' + command: deploy -c workers/auth-api/wrangler.toml + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Deploy pod-api + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: '3' + command: deploy -c workers/pod-api/wrangler.toml + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Deploy search-api + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: '3' + command: deploy -c workers/search-api/wrangler.toml + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Deploy nostr-relay + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: '3' + command: deploy -c workers/nostr-relay/wrangler.toml + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Deploy link-preview + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: '3' + command: deploy -c workers/link-preview-api/wrangler.toml + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + # ─── Rust Workers (worker-build + wasm) ──────────────────────────── + deploy-rust: + name: Deploy Rust Workers + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + runs-on: ubuntu-latest + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + include: + - crate: preview-worker + worker_name: dreamlab-link-preview + - crate: pod-worker + worker_name: dreamlab-pod-api + # auth-worker: uncomment when Tranche 3 is ready + # - crate: auth-worker + # worker_name: dreamlab-auth-api + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Cache Cargo registry + build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ~/.cargo/bin/worker-build + community-forum-rs/target + key: rust-deploy-${{ matrix.crate }}-${{ hashFiles('community-forum-rs/Cargo.lock') }} + restore-keys: | + rust-deploy-${{ matrix.crate }}- + rust-deploy- + + - name: Install worker-build + run: | + if ! command -v worker-build &>/dev/null; then + cargo install worker-build --locked + fi + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Build and deploy ${{ matrix.crate }} + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: '3' + command: deploy + workingDirectory: community-forum-rs/crates/${{ matrix.crate }} + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + # ─── Health checks (runs after both deploy jobs) ─────────────────── + health-check: + name: Health Checks + if: github.repository == 'DreamLab-AI/dreamlab-ai-website' + needs: [deploy-ts, deploy-rust] + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Wait for propagation + run: sleep 10 + + - name: Check all workers + run: | + PASS=0 + FAIL=0 + for worker in dreamlab-auth-api dreamlab-pod-api dreamlab-search-api dreamlab-nostr-relay dreamlab-link-preview; do + URL="https://${worker}.${{ env.CF_SUBDOMAIN }}.workers.dev/health" + if [ "$worker" = "dreamlab-search-api" ]; then + URL="https://${worker}.${{ env.CF_SUBDOMAIN }}.workers.dev/status" + fi + STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "$URL" || true) + echo "${worker}: HTTP ${STATUS}" + if [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 500 ]; then + echo " PASS" + PASS=$((PASS + 1)) + else + echo "::warning::${worker} health check returned HTTP ${STATUS}" + FAIL=$((FAIL + 1)) + fi + done + echo "" + echo "Results: ${PASS} passed, ${FAIL} warnings" + + - name: Deployment summary + run: | + cat >> $GITHUB_STEP_SUMMARY <`: +- `kind: 27235`, tags: `u` (URL), `method`, optional `payload` (SHA-256 of body) +- Schnorr-signed with privkey from auth store closure +- Server recomputes event ID from NIP-01 canonical form and verifies independently +- Payload hash uses raw body bytes (server captures raw body for payload hash verification) + +### Identity + +- Nostr pubkey (hex) is the primary identity +- `did:nostr:` for DID-based interop +- WebID at `https://pods.dreamlab-ai.com/{pubkey}/profile/card#me` (Solid/Linked Data) + +### Key files + +| File | Role | +|------|------| +| `community-forum/src/lib/auth/passkey.ts` | WebAuthn PRF ceremony, HKDF derivation | +| `community-forum/src/lib/auth/nip98-client.ts` | NIP-98 token creation + fetch wrapper | +| `community-forum/src/lib/stores/auth.ts` | Auth state, privkey closure, passkey hooks | +| `workers/auth-api/index.ts` | CF Worker: WebAuthn + NIP-98 + pod provisioning | +| `workers/pod-api/index.ts` | CF Worker: Solid pod storage on R2 | +| `workers/search-api/index.ts` | CF Worker: RuVector WASM vector search | +| `workers/link-preview-api/index.ts` | CF Worker: OG metadata proxy | + +## Claude Flow V3 Integration + +### MCP Server +Configured in `.mcp.json` — claude-flow MCP with v3 mode, hierarchical-mesh topology, max 15 agents, hybrid memory. + +### Hooks +7 hook types in `.claude/settings.json`: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop, PreCompact, SubagentStart, TeammateIdle, TaskCompleted. + +### Skills & Agents +- 29 skills in `.claude/skills/` +- 10 command groups in `.claude/commands/` +- 24 agent categories in `.claude/agents/` + +### Agentic QE v3 +14 domains: test-generation, test-execution, coverage-analysis, quality-assessment, defect-intelligence, requirements-validation, code-intelligence, security-compliance, contract-testing, visual-accessibility, chaos-resilience, learning-optimization, enterprise-integration, coordination. + +### CLI Quick Reference + +```bash +# Claude Flow +claude-flow daemon start # Start background workers +claude-flow memory init # Initialize memory database +claude-flow swarm init # Initialize a swarm +claude-flow doctor --fix # Diagnose and fix issues + +# Agentic QE +agentic-qe status # System status +agentic-qe health # Health check +agentic-qe domain list # List all domains +agentic-qe fleet spawn # Spawn multi-agent fleet +agentic-qe test generate # Generate tests +agentic-qe coverage # Coverage analysis +agentic-qe security # Security scanning +agentic-qe code analyze # Code intelligence +``` + +## Concurrency Rules + +- All independent operations MUST be concurrent/parallel in a single message +- Use Claude Code's Task tool for spawning agents +- Batch all related file reads/writes/edits in one message +- Batch all Bash commands in one message when independent +- Never continuously poll after spawning agents — wait for results + +## Swarm Configuration + +- Topology: hierarchical-mesh +- Max agents: 15 +- Memory backend: hybrid (HNSW enabled) +- Use `run_in_background: true` for all agent Task calls +- Put all agent Task calls in one message for parallel execution diff --git a/Marketing-Material/CHO.png b/Marketing-Material/CHO.png deleted file mode 100644 index caf1eff..0000000 Binary files a/Marketing-Material/CHO.png and /dev/null differ diff --git a/Marketing-Material/beamertheme-modern.sty b/Marketing-Material/beamertheme-modern.sty deleted file mode 100644 index 87d2c25..0000000 --- a/Marketing-Material/beamertheme-modern.sty +++ /dev/null @@ -1,135 +0,0 @@ -%====================================================================== -% Modern Beamer Theme for Professional Presentations -%====================================================================== -\ProvidesPackage{beamertheme-modern}[2025/01/01 Modern Beamer Theme] - -\mode - -%--------------------------------------------------------------------- -% Required Packages -%--------------------------------------------------------------------- -\RequirePackage{tikz} -\RequirePackage{xcolor} -\RequirePackage{calc} -\RequirePackage{etoolbox} -\RequirePackage{tcolorbox} - -%--------------------------------------------------------------------- -% Color Definitions -%--------------------------------------------------------------------- -\definecolor{modernPrimary}{RGB}{0,122,255} -\definecolor{modernAccent}{RGB}{0,184,212} -\definecolor{modernDark}{RGB}{25,42,86} -\definecolor{modernSuccess}{RGB}{76,175,80} -\definecolor{modernWarning}{RGB}{255,152,0} -\definecolor{modernDanger}{RGB}{244,67,54} -\definecolor{modernText}{RGB}{33,33,33} -\definecolor{modernTextLight}{RGB}{117,117,117} -\definecolor{modernBg}{RGB}{248,249,250} -\definecolor{modernGray}{RGB}{245,245,247} - -%--------------------------------------------------------------------- -% Beamer Color Theme -%--------------------------------------------------------------------- -\setbeamercolor{normal text}{fg=modernText,bg=white} -\setbeamercolor{alerted text}{fg=modernDanger} -\setbeamercolor{example text}{fg=modernSuccess} -\setbeamercolor{structure}{fg=modernPrimary} - -\setbeamercolor{palette primary}{fg=white,bg=modernPrimary} -\setbeamercolor{palette secondary}{fg=white,bg=modernAccent} -\setbeamercolor{palette tertiary}{fg=white,bg=modernDark} -\setbeamercolor{palette quaternary}{fg=white,bg=modernText} - -\setbeamercolor{title}{fg=modernDark,bg=white} -\setbeamercolor{subtitle}{fg=modernPrimary,bg=white} -\setbeamercolor{author}{fg=modernText,bg=white} -\setbeamercolor{institute}{fg=modernTextLight,bg=white} -\setbeamercolor{date}{fg=modernTextLight,bg=white} - -\setbeamercolor{frametitle}{fg=modernDark,bg=white} -\setbeamercolor{framesubtitle}{fg=modernTextLight,bg=white} - -\setbeamercolor{block title}{fg=white,bg=modernPrimary} -\setbeamercolor{block body}{fg=modernText,bg=modernBg} -\setbeamercolor{block title alerted}{fg=white,bg=modernDanger} -\setbeamercolor{block body alerted}{fg=modernText,bg=modernDanger!10} -\setbeamercolor{block title example}{fg=white,bg=modernSuccess} -\setbeamercolor{block body example}{fg=modernText,bg=modernSuccess!10} - -\setbeamercolor{item}{fg=modernPrimary} -\setbeamercolor{subitem}{fg=modernAccent} -\setbeamercolor{subsubitem}{fg=modernTextLight} - -\setbeamercolor{section in toc}{fg=modernDark} -\setbeamercolor{subsection in toc}{fg=modernText} - -\setbeamercolor{footline}{fg=modernTextLight,bg=modernBg} -\setbeamercolor{headline}{fg=white,bg=modernDark} - -%--------------------------------------------------------------------- -% Beamer Font Theme -%--------------------------------------------------------------------- -\setbeamerfont{title}{size=\Huge,series=\bfseries} -\setbeamerfont{subtitle}{size=\Large} -\setbeamerfont{author}{size=\large} -\setbeamerfont{institute}{size=\normalsize} -\setbeamerfont{date}{size=\normalsize} -\setbeamerfont{frametitle}{size=\Large,series=\bfseries} -\setbeamerfont{framesubtitle}{size=\normalsize} -\setbeamerfont{block title}{series=\bfseries} -\setbeamerfont{footline}{size=\scriptsize} - -%--------------------------------------------------------------------- -% Beamer Inner Theme -%--------------------------------------------------------------------- -\setbeamertemplate{blocks}[rounded][shadow=true] -\setbeamertemplate{items}[circle] -\setbeamertemplate{sections/subsections in toc}[circle] -\setbeamertemplate{title page}[default][colsep=-4bp,rounded=true] - -%--------------------------------------------------------------------- -% Beamer Outer Theme -%--------------------------------------------------------------------- -\setbeamertemplate{navigation symbols}{} -\setbeamertemplate{headline}{} - -\setbeamertemplate{footline}{ - \leavevmode% - \hbox{% - \begin{beamercolorbox}[wd=.333333\paperwidth,ht=2.25ex,dp=1ex,left]{footline}% - \hspace*{1ex}\insertshortauthor - \end{beamercolorbox}% - \begin{beamercolorbox}[wd=.333333\paperwidth,ht=2.25ex,dp=1ex,center]{footline}% - \insertshortdate - \end{beamercolorbox}% - \begin{beamercolorbox}[wd=.333333\paperwidth,ht=2.25ex,dp=1ex,right]{footline}% - \insertframenumber{} / \inserttotalframenumber\hspace*{1ex} - \end{beamercolorbox}}% - \vskip0pt% -} - -\setbeamertemplate{frametitle}{ - \nointerlineskip - \begin{beamercolorbox}[wd=\paperwidth,sep=0.3cm,leftskip=0.5cm,rightskip=0.5cm]{frametitle} - \usebeamerfont{frametitle}\insertframetitle - \ifx\insertframesubtitle\@empty\else - \\[0.2em] - \usebeamerfont{framesubtitle}\usebeamercolor[fg]{framesubtitle}\insertframesubtitle - \fi - \end{beamercolorbox} -} - -%--------------------------------------------------------------------- -% Custom Commands -%--------------------------------------------------------------------- -\newcommand{\modernframe}[2][]{ - \begin{frame}[#1] - \frametitle{#2} -} - -\newcommand{\endmodernframe}{ - \end{frame} -} - -\mode \ No newline at end of file diff --git a/Marketing-Material/learning-pathway-slides-modern.tex b/Marketing-Material/learning-pathway-slides-modern.tex deleted file mode 100644 index eca5509..0000000 --- a/Marketing-Material/learning-pathway-slides-modern.tex +++ /dev/null @@ -1,696 +0,0 @@ -%====================================================================== -% AI-Powered Knowledge Work – Modern Professional Presentation -%====================================================================== -\documentclass[aspectratio=169,14pt]{beamer} - -%--------------------------------------------------------------------- -% Modern Theme & Packages -%--------------------------------------------------------------------- -\usetheme{Boadilla} -\usecolortheme{whale} -\usefonttheme{professionalfonts} - -\usepackage[utf8]{inputenc} -\usepackage{graphicx} -\usepackage{booktabs} -\usepackage{tikz} -\usepackage{fontawesome5} -\usepackage{hyperref} -\usepackage{multicol} -\usepackage{xcolor} -\usepackage{tcolorbox} -\usepackage{pgfplots} -\pgfplotsset{compat=1.18} -\usepackage{soul} -\usepackage{ragged2e} -\usepackage{etoolbox} -\usepackage{microtype} - -% Modern fonts -\usepackage[sfdefault]{FiraSans} -\usepackage{FiraMono} -\usepackage[mathrm=sym]{unicode-math} - -%--------------------------------------------------------------------- -% Professional Color Palette -%--------------------------------------------------------------------- -\definecolor{primaryblue}{RGB}{0,123,255} -\definecolor{deepblue}{RGB}{0,90,200} -\definecolor{accentcyan}{RGB}{0,188,212} -\definecolor{successgreen}{RGB}{76,175,80} -\definecolor{warningorange}{RGB}{255,152,0} -\definecolor{modernpurple}{RGB}{103,58,183} -\definecolor{elegantgray}{RGB}{96,125,139} -\definecolor{lightbg}{RGB}{248,249,250} -\definecolor{darktext}{RGB}{33,37,41} -\definecolor{pgffillcolor}{RGB}{0,188,212} - -%--------------------------------------------------------------------- -% Modern Beamer Configuration -%--------------------------------------------------------------------- -\setbeamertemplate{navigation symbols}{} -\setbeamertemplate{footline}[frame number] -\setbeamercolor{normal text}{fg=darktext} -\setbeamercolor{structure}{fg=deepblue} -\setbeamercolor{frametitle}{fg=white,bg=deepblue} -\setbeamercolor{title}{fg=white} -\setbeamercolor{subtitle}{fg=accentcyan} -\setbeamercolor{block title}{fg=white,bg=primaryblue} -\setbeamercolor{block body}{fg=darktext,bg=lightbg} - -% Modern frame title -\setbeamertemplate{frametitle}{% - \nointerlineskip - \begin{beamercolorbox}[wd=\paperwidth,sep=0pt,leftskip=20pt,rightskip=20pt]{frametitle} - \vspace{15pt} - {\Large\bfseries\insertframetitle} - \vspace{15pt} - \end{beamercolorbox} -} - -% Rounded blocks -\tcbuselibrary{skins,breakable} -\newtcolorbox{modernblock}[2][]{% - colback=lightbg, - colframe=#2, - fonttitle=\bfseries, - coltitle=white, - colbacktitle=#2, - boxrule=0pt, - arc=4mm, - left=10pt,right=10pt,top=10pt,bottom=10pt, - title=#1 -} - -%--------------------------------------------------------------------- -% Advanced Background System -%--------------------------------------------------------------------- -\newcommand{\ModernBackground}[1]{% - \setbeamertemplate{background}{% - \begin{tikzpicture}[remember picture,overlay] - % Gradient overlay - \fill[white,opacity=0.95] (current page.south west) rectangle (current page.north east); - \shade[left color=primaryblue!10,right color=accentcyan!5,opacity=0.3] - (current page.south west) rectangle (current page.north east); - % Image with artistic overlay - \node[anchor=center,opacity=0.15] at (current page.center) { - \includegraphics[width=\paperwidth,height=\paperheight,keepaspectratio]{#1} - }; - % Modern geometric elements - \fill[primaryblue,opacity=0.05] ([xshift=-100pt,yshift=-50pt]current page.north east) - circle (200pt); - \fill[accentcyan,opacity=0.05] ([xshift=100pt,yshift=50pt]current page.south west) - circle (150pt); - \end{tikzpicture}} -} - -% Clean background for content slides -\newcommand{\ContentBackground}{% - \setbeamertemplate{background}{% - \begin{tikzpicture}[remember picture,overlay] - \fill[white] (current page.south west) rectangle (current page.north east); - \shade[top color=lightbg,bottom color=white,opacity=0.5] - (current page.south west) rectangle (current page.north east); - \end{tikzpicture}} -} - -%--------------------------------------------------------------------- -% Custom Commands -%--------------------------------------------------------------------- -\newcommand{\highlight}[1]{\textcolor{primaryblue}{\textbf{#1}}} -\newcommand{\stat}[2]{{\Huge\textbf{#1}}\\{\small #2}} -\newcommand{\impactitem}[2]{\item[\textcolor{#1}{\faCheckCircle}] #2} - -%--------------------------------------------------------------------- -% Document Metadata -%--------------------------------------------------------------------- -\title{\Huge\textbf{AI-Powered Knowledge Work}} -\subtitle{\Large Master the New Universal Workspace} -\author{\textbf{Dr. John O'Hare}} -\institute{DREAMLAB | Chief Hallucination Officer} -\date{2025 Workshop Series} - -%===================================================================== -% Document Body -%===================================================================== -\begin{document} - -% Title Slide with Modern Design -\ModernBackground{slide-1-backdrop.png} -\begin{frame}[plain] - \begin{tikzpicture}[remember picture,overlay] - % Dark overlay for text contrast - \fill[deepblue,opacity=0.9] (current page.south west) rectangle (current page.north east); - % Geometric accent - \fill[accentcyan,opacity=0.3] ([xshift=-100pt]current page.east) - -- ([yshift=100pt]current page.south east) - -- (current page.south east) - -- cycle; - \end{tikzpicture} - \centering - \vspace{2cm} - {\color{white}\Huge\textbf{AI-Powered Knowledge Work}}\\[0.5cm] - {\color{accentcyan}\Large Transform Your Professional Practice in 5 Days}\\[1cm] - {\color{white}\large\textbf{Dr. John O'Hare}}\\[0.2cm] - {\color{white!80}\small DREAMLAB | Chief Hallucination Officer}\\[1cm] - {\color{accentcyan}\faCalendar\quad March 17-21, 2025} -\end{frame} - -% The Revolution Slide -\ContentBackground -\begin{frame}[t] - \frametitle{The AI Revolution Has Two Classes} - - \begin{columns}[T] - \column{0.48\textwidth} - \begin{modernblock}[The 95\%]{warningorange} - \begin{itemize} - \item Copy-paste from ChatGPT - \item \$20/month for limitations - \item No version control - \item Manual repetitive tasks - \item Consumer-grade results - \end{itemize} - \end{modernblock} - - \column{0.48\textwidth} - \begin{modernblock}[The 5\%]{successgreen} - \begin{itemize} - \item Direct API access - \item Pay pennies per task - \item Full automation control - \item 10× productivity gains - \item Professional-grade output - \end{itemize} - \end{modernblock} - \end{columns} - - \vspace{0.5cm} - \centering - \begin{tcolorbox}[colback=primaryblue!10,colframe=primaryblue,boxrule=2pt,arc=3mm,width=0.8\textwidth] - \centering\large\textbf{Which side will you choose?} - \end{tcolorbox} -\end{frame} - -% Transformation Stats -\ModernBackground{slide-3-backdrop.png} -\begin{frame}[t] - \frametitle{Real Impact, Real Numbers} - - \begin{columns}[T] - \column{0.25\textwidth} - \centering - \begin{tcolorbox}[colback=primaryblue!20,colframe=primaryblue,arc=3mm,boxrule=0pt] - \centering - \stat{70\%}{Time saved on\\grant writing} - \end{tcolorbox} - - \column{0.25\textwidth} - \centering - \begin{tcolorbox}[colback=successgreen!20,colframe=successgreen,arc=3mm,boxrule=0pt] - \centering - \stat{5×}{Faster business\\documentation} - \end{tcolorbox} - - \column{0.25\textwidth} - \centering - \begin{tcolorbox}[colback=warningorange!20,colframe=warningorange,arc=3mm,boxrule=0pt] - \centering - \stat{10×}{Research\\throughput} - \end{tcolorbox} - - \column{0.25\textwidth} - \centering - \begin{tcolorbox}[colback=modernpurple!20,colframe=modernpurple,arc=3mm,boxrule=0pt] - \centering - \stat{£2M}{Funding won\\by alumni} - \end{tcolorbox} - \end{columns} - - \vspace{0.8cm} - \begin{center} - \large\textit{"The gap between AI users and AI masters is widening daily."} - \end{center} -\end{frame} - -% Who Is This For - Visual -\ContentBackground -\begin{frame}[t] - \frametitle{Perfect For Knowledge Workers} - - \begin{columns}[T] - \column{0.33\textwidth} - \centering - \textcolor{primaryblue}{\Huge\faUserGraduate}\\[0.3cm] - \textbf{Academics}\\ - \small Grant proposals\\Research papers\\Literature reviews - - \column{0.33\textwidth} - \centering - \textcolor{successgreen}{\Huge\faChartLine}\\[0.3cm] - \textbf{Business Leaders}\\ - \small Strategic documents\\Reports \& analysis\\Knowledge systems - - \column{0.33\textwidth} - \centering - \textcolor{warningorange}{\Huge\faLightbulb}\\[0.3cm] - \textbf{Consultants}\\ - \small Client deliverables\\Rapid prototyping\\Project automation - \end{columns} - - \vspace{0.8cm} - \begin{tcolorbox}[colback=lightbg,colframe=elegantgray,boxrule=1pt,arc=3mm] - \centering - \large\textbf{No coding required} – If you can use Word, you're ready - \end{tcolorbox} -\end{frame} - -% Journey Overview - Modern Visual -\ModernBackground{slide-5-backdrop.png} -\begin{frame}[t] - \frametitle{Your 5-Day Transformation Journey} - - \begin{tikzpicture}[remember picture,overlay] - % Timeline - \draw[line width=3pt,primaryblue] ([xshift=2cm,yshift=-1cm]current page.north west) -- ([xshift=-2cm,yshift=-1cm]current page.north east); - - % Day markers - \foreach \x/\day/\title/\color in { - 0.2/1/Universal Workspace/primaryblue, - 0.35/2/AI Partners/successgreen, - 0.5/3/Private Brain/warningorange, - 0.65/4/AI Teams/modernpurple, - 0.8/5/Publishing/accentcyan - }{ - \node[circle,fill=\color,text=white,minimum size=1cm,font=\bfseries] at ([xshift=\x*\paperwidth-0.5\paperwidth,yshift=-1cm]current page.north) {\day}; - \node[below,text width=2.5cm,align=center,font=\small] at ([xshift=\x*\paperwidth-0.5\paperwidth,yshift=-2cm]current page.north) {\title}; - } - \end{tikzpicture} - - \vspace{3cm} - \begin{columns}[T] - \column{0.5\textwidth} - \textbf{Start:} AI Consumer\\ - \small Limited to web interfaces - - \column{0.5\textwidth} - \raggedleft - \textbf{Finish:} AI Commander\\ - \small Complete automation mastery - \end{columns} -\end{frame} - -% Day 1 - Modern Layout -\ContentBackground -\begin{frame}[t] - \frametitle{Day 1: Your AI Command Center} - - \begin{columns}[T] - \column{0.5\textwidth} - \begin{modernblock}[Morning Session]{primaryblue} - \textbf{VS Code Transformation} - \begin{itemize} - \item AI-powered extensions - \item Container environments - \item Professional workspace - \item Version control mastery - \end{itemize} - \end{modernblock} - - \column{0.5\textwidth} - \begin{modernblock}[Afternoon Session]{accentcyan} - \textbf{Visual Project Management} - \begin{itemize} - \item Mermaid diagrams - \item Gantt charts - \item Process flows - \item Knowledge maps - \end{itemize} - \end{modernblock} - \end{columns} - - \vspace{0.5cm} - \centering - \begin{tcolorbox}[colback=successgreen!20,colframe=successgreen,boxrule=2pt,arc=3mm,width=0.9\textwidth] - \centering - \faCheckCircle\quad\textbf{Day 1 Achievement:} Professional AI workspace with visual project plan - \end{tcolorbox} -\end{frame} - -% Day 2 - Vibe Coding -\ModernBackground{slide-9-backdrop.png} -\begin{frame}[t] - \frametitle{Day 2: "Vibe Coding" Revolution} - - \begin{center} - \Large\textbf{Describe what you want → AI builds it perfectly} - \end{center} - - \vspace{0.5cm} - - \begin{columns}[T] - \column{0.33\textwidth} - \centering - \textcolor{primaryblue}{\Huge\faComments}\\[0.3cm] - \textbf{Natural Language}\\ - \small Just describe your vision - - \column{0.33\textwidth} - \centering - \textcolor{successgreen}{\Huge\faCode}\\[0.3cm] - \textbf{AI Creates}\\ - \small Complete implementations - - \column{0.33\textwidth} - \centering - \textcolor{warningorange}{\Huge\faGlobe}\\[0.3cm] - \textbf{Deploy Live}\\ - \small Online in minutes - \end{columns} - - \vspace{0.8cm} - \begin{modernblock}[Live Demo Examples]{deepblue} - \centering - Business website • Interactive dashboard • Documentation site • Portfolio - \end{modernblock} -\end{frame} - -% Day 3 - Private AI -\ContentBackground -\begin{frame}[t] - \frametitle{Day 3: Your Private AI Brain} - - \begin{tikzpicture}[remember picture,overlay] - % Security badge - \node[anchor=north east] at ([xshift=-20pt,yshift=-80pt]current page.north east) { - \textcolor{successgreen}{\Huge\faShieldAlt} - }; - \end{tikzpicture} - - \begin{columns}[T] - \column{0.5\textwidth} - \begin{modernblock}[100\% Private]{successgreen} - \begin{itemize} - \item Runs on YOUR laptop - \item No internet required - \item Complete data privacy - \item Zero subscription fees - \end{itemize} - \end{modernblock} - - \column{0.5\textwidth} - \begin{modernblock}[RAG System]{modernpurple} - \begin{itemize} - \item Reads all your documents - \item Instant answers - \item Exact citations - \item Your knowledge on tap - \end{itemize} - \end{modernblock} - \end{columns} - - \vspace{0.5cm} - \centering - \textit{"What did we decide in the March meeting?"} → \highlight{Instant answer with source} -\end{frame} - -% Day 4 - AI Teams Visual -\ModernBackground{slide-11-backdrop.png} -\begin{frame}[t] - \frametitle{Day 4: Deploy Your AI Workforce} - - \begin{center} - \begin{tikzpicture} - % Central node - \node[circle,fill=primaryblue,text=white,minimum size=2cm] (center) at (0,0) {\textbf{You}}; - - % Agent nodes - \node[circle,fill=successgreen,text=white,minimum size=1.5cm] (research) at (-3,2) {\faSearch}; - \node[circle,fill=warningorange,text=white,minimum size=1.5cm] (writer) at (3,2) {\faPen}; - \node[circle,fill=modernpurple,text=white,minimum size=1.5cm] (analyst) at (-3,-2) {\faChartBar}; - \node[circle,fill=accentcyan,text=white,minimum size=1.5cm] (auto) at (3,-2) {\faRobot}; - - % Connections - \draw[thick,->] (center) -- (research); - \draw[thick,->] (center) -- (writer); - \draw[thick,->] (center) -- (analyst); - \draw[thick,->] (center) -- (auto); - - % Labels - \node[below] at (research.south) {\small Research}; - \node[below] at (writer.south) {\small Writing}; - \node[above] at (analyst.north) {\small Analysis}; - \node[above] at (auto.north) {\small Automation}; - \end{tikzpicture} - \end{center} - - \vspace{0.5cm} - \begin{modernblock}[Autonomous Collaboration]{deepblue} - \centering - Agents work together while you sleep • Safety limits • Pennies per task - \end{modernblock} -\end{frame} - -% Day 5 - Professional Output -\ContentBackground -\begin{frame}[t] - \frametitle{Day 5: Professional Publishing Suite} - - \begin{columns}[T] - \column{0.25\textwidth} - \centering - \textcolor{primaryblue}{\Huge\faFileAlt}\\[0.2cm] - \textbf{LaTeX Papers}\\ - \small Academic quality\\Auto-citations - - \column{0.25\textwidth} - \centering - \textcolor{successgreen}{\Huge\faChartPie}\\[0.2cm] - \textbf{Reports}\\ - \small Interactive data\\Live dashboards - - \column{0.25\textwidth} - \centering - \textcolor{warningorange}{\Huge\faDesktop}\\[0.2cm] - \textbf{Websites}\\ - \small Client portals\\Documentation - - \column{0.25\textwidth} - \centering - \textcolor{modernpurple}{\Huge\faCogs}\\[0.2cm] - \textbf{Automation}\\ - \small Weekly updates\\Quality checks - \end{columns} - - \vspace{0.8cm} - \begin{tcolorbox}[colback=primaryblue!10,colframe=primaryblue,boxrule=2pt,arc=3mm] - \centering - \large\textbf{Complete system:} Knowledge base + AI agents + Professional outputs - \end{tcolorbox} -\end{frame} - -% Success Stories - Modern Cards -\ModernBackground{slide-13-backdrop.png} -\begin{frame}[t] - \frametitle{Proven Success Stories} - - \begin{columns}[T] - \column{0.5\textwidth} - \begin{tcolorbox}[colback=white,colframe=successgreen,boxrule=2pt,arc=3mm,drop shadow] - \textcolor{successgreen}{\faQuoteLeft}\\ - \textit{AI agents do the research, I do the strategy. Just won \textbf{£2M funding}.}\\[0.3cm] - \textbf{Dr. Rachel Morrison}\\ - \small University Research Director - \end{tcolorbox} - - \vspace{0.3cm} - - \begin{tcolorbox}[colback=white,colframe=primaryblue,boxrule=2pt,arc=3mm,drop shadow] - \textcolor{primaryblue}{\faQuoteLeft}\\ - \textit{10 years of company knowledge instantly accessible.}\\[0.3cm] - \textbf{Sandra Patel}\\ - \small Operations Director - \end{tcolorbox} - - \column{0.5\textwidth} - \begin{tcolorbox}[colback=white,colframe=warningorange,boxrule=2pt,arc=3mm,drop shadow] - \textcolor{warningorange}{\faQuoteLeft}\\ - \textit{Client reports that took days now take \textbf{hours}.}\\[0.3cm] - \textbf{James Liu}\\ - \small Management Consultant - \end{tcolorbox} - - \vspace{0.3cm} - - \begin{tcolorbox}[colback=white,colframe=modernpurple,boxrule=2pt,arc=3mm,drop shadow] - \textcolor{modernpurple}{\faQuoteLeft}\\ - \textit{Built our entire handbook site through conversation.}\\[0.3cm] - \textbf{Michael Chang}\\ - \small HR Director (Non-technical) - \end{tcolorbox} - \end{columns} -\end{frame} - -% Investment - Clear Value -\ContentBackground -\begin{frame}[t] - \frametitle{Investment in Your Future} - - \begin{columns}[T] - \column{0.5\textwidth} - \begin{modernblock}[Your Investment]{primaryblue} - \begin{itemize} - \item Standard: £2,995 - \item \highlight{Early Bird: £2,495} - \item Includes £200 API credits - \item Payment plans available - \item Team discount: 15\% (3+) - \end{itemize} - \end{modernblock} - - \column{0.5\textwidth} - \begin{modernblock}[Your Returns]{successgreen} - \begin{itemize} - \item 70\% faster grant writing - \item 5× documentation speed - \item 10× research throughput - \item ROI in first project - \item Lifetime skill advantage - \end{itemize} - \end{modernblock} - \end{columns} - - \vspace{0.5cm} - \begin{center} - \begin{tcolorbox}[colback=warningorange!20,colframe=warningorange,boxrule=2pt,arc=3mm,width=0.8\textwidth] - \centering - \large\textbf{100\% Satisfaction Guarantee}\\ - \small Full refund if not delighted after Day 1 - \end{tcolorbox} - \end{center} -\end{frame} - -% Instructor - Professional -\ModernBackground{slide-15-backdrop.png} -\begin{frame}[t] - \frametitle{Learn from the Best} - - \begin{columns}[T] - \column{0.4\textwidth} - \centering - \begin{tikzpicture} - \node[circle,draw=primaryblue,line width=3pt,minimum size=4cm,fill=white,drop shadow] at (0,0) { - \includegraphics[width=3.8cm]{CHO.png} - }; - \end{tikzpicture} - - \column{0.6\textwidth} - \textbf{\Large Dr. John O'Hare}\\[0.3cm] - \textcolor{elegantgray}{Chief Hallucination Officer, DREAMLAB}\\[0.5cm] - - \begin{itemize} - \impactitem{primaryblue}{25+ years at tech's edge} - \impactitem{successgreen}{VR pioneer → AI leader} - \impactitem{warningorange}{PhD collaborative tech} - \impactitem{modernpurple}{HP AI Lighthouse Partner} - \end{itemize} - - \vspace{0.3cm} - \textit{\small "This workshop distills decades of experience into skills you can use immediately."} - \end{columns} -\end{frame} - -% Why Choose Us - Differentiators -\ContentBackground -\begin{frame}[t] - \frametitle{Why This Workshop Is Different} - - \begin{columns}[T] - \column{0.33\textwidth} - \centering - \textcolor{primaryblue}{\Huge\faUsers}\\[0.3cm] - \textbf{Small Cohorts}\\ - \small Max 10 participants\\Personal attention\\Peer learning - - \column{0.33\textwidth} - \centering - \textcolor{successgreen}{\Huge\faProjectDiagram}\\[0.3cm] - \textbf{Real Projects}\\ - \small Work on YOUR tasks\\Immediate application\\Practical results - - \column{0.33\textwidth} - \centering - \textcolor{warningorange}{\Huge\faInfinity}\\[0.3cm] - \textbf{Lifetime Access}\\ - \small Alumni community\\Quarterly workshops\\Ongoing support - \end{columns} - - \vspace{0.8cm} - \begin{modernblock}[Unique Advantages]{deepblue} - \centering - Vendor-agnostic • Privacy-first • Cross-industry insights • Future-proof skills - \end{modernblock} -\end{frame} - -% Next Steps - Call to Action -\ModernBackground{slide-17-backdrop.png} -\begin{frame}[t] - \frametitle{Secure Your Transformation} - - \begin{modernblock}[Next Cohorts - Limited to 10 Seats]{primaryblue} - \Large - \begin{itemize} - \item \textbf{March 2025:} 17-21 March \textcolor{warningorange}{(3 seats left)} - \item \textbf{May 2025:} 12-16 May \textcolor{successgreen}{(Early bird open)} - \item \textbf{July 2025:} 14-18 July (Just announced) - \end{itemize} - \end{modernblock} - - \vspace{0.5cm} - - \begin{columns}[T] - \column{0.5\textwidth} - \textbf{Simple Process:} - \begin{enumerate} - \item Online application (5 min) - \item Screening call (15 min) - \item £500 deposit - \item Pre-course access - \item Join cohort Discord - \end{enumerate} - - \column{0.5\textwidth} - \centering - \vspace{0.5cm} - \begin{tcolorbox}[colback=accentcyan!20,colframe=accentcyan,boxrule=3pt,arc=3mm] - \centering - \Large\textbf{workshops@dreamlab.uk}\\[0.3cm] - \faPhone\quad +44 20 XXXX XXXX - \end{tcolorbox} - \end{columns} -\end{frame} - -% Final Slide - Powerful Close -\ModernBackground{slide-18-backdrop.png} -\begin{frame}[plain] - \begin{tikzpicture}[remember picture,overlay] - % Dramatic gradient - \shade[top color=deepblue,bottom color=primaryblue,opacity=0.95] - (current page.south west) rectangle (current page.north east); - \end{tikzpicture} - - \centering - \vspace{3cm} - {\color{white}\Huge\textbf{The Choice Is Clear}}\\[0.8cm] - {\color{white!80}\Large Remain limited by consumer AI tools...}\\[0.5cm] - {\color{white}\LARGE\textbf{OR}}\\[0.5cm] - {\color{accentcyan}\Large\textbf{Master professional AI that transforms how you work}}\\[1.5cm] - - \begin{tcolorbox}[colback=white,colframe=white,boxrule=0pt,arc=3mm,width=0.6\textwidth] - \centering - \Large\textbf{workshops@dreamlab.uk}\\[0.3cm] - \normalsize Join the 5\% who command AI with precision - \end{tcolorbox} -\end{frame} - -\end{document} \ No newline at end of file diff --git a/Marketing-Material/learning-pathway-slides.tex b/Marketing-Material/learning-pathway-slides.tex deleted file mode 100644 index 8318794..0000000 --- a/Marketing-Material/learning-pathway-slides.tex +++ /dev/null @@ -1,451 +0,0 @@ -%====================================================================== -% AI–Powered Knowledge Work – Complete, Spatially Tuned & Photo Included -%====================================================================== -\documentclass[aspectratio=169]{beamer} - -%--------------------------------------------------------------------- -% Theme & Packages -%--------------------------------------------------------------------- -\usetheme{CambridgeUS} -\usecolortheme{default} -\usepackage[utf8]{inputenc} -\usepackage{graphicx} -\usepackage{booktabs} -\usepackage{tikz} -\usepackage{fontawesome5} -\usepackage{hyperref} -\usepackage{multicol} -\usepackage{xcolor} - -%--------------------------------------------------------------------- -% Custom Colours -%--------------------------------------------------------------------- -\definecolor{aiblue}{RGB}{79,195,247} -\definecolor{darkblue}{RGB}{1,87,155} -\definecolor{successgreen}{RGB}{76,175,80} -\definecolor{warningorange}{RGB}{255,152,0} -\definecolor{textgray}{gray}{0.15} - -%--------------------------------------------------------------------- -% Beamer Tweaks -%--------------------------------------------------------------------- -\setbeamertemplate{headline}{} -\setbeamertemplate{footline}{} -\setbeamertemplate{navigation symbols}{} -\setbeamercolor{normal text}{fg=textgray,bg=white} -\setbeamercolor{frametitle}{fg=textgray} - -%--------------------------------------------------------------------- -% Background Helper -%--------------------------------------------------------------------- -\newcommand{\UseBackground}[1]{% - \setbeamertemplate{background}{% - \begin{tikzpicture}[remember picture,overlay] - \node at (current page.center) {\includegraphics[width=\paperwidth,height=\paperheight,keepaspectratio]{#1}}; - \fill[white,opacity=0.80] (current page.south west) rectangle (current page.north east); - \end{tikzpicture}} -} - -%--------------------------------------------------------------------- -% Document Metadata -%--------------------------------------------------------------------- -\title{AI-Powered Knowledge Work} -\subtitle{Master the New Universal Workspace} -\author{Dr. John O'Hare} -\institute{DREAMLAB | Chief Hallucination Officer} -\date{2025} - -%===================================================================== -% Document Body -%===================================================================== -\begin{document} - -% Slide 1 -\UseBackground{slide-1-backdrop.png} -\begin{frame}[plain]\titlepage\end{frame} - -% Slide 2 -\UseBackground{slide-2-backdrop.png} -\begin{frame}[t] - \frametitle{The AI Revolution in Knowledge Work} - \begin{block}{The Paradigm Shift} - The boundary between \textbf{"technical"} and \textbf{"non-technical"} work has - \textcolor{aiblue}{\textbf{dissolved}}. - \end{block} - \vspace{0.5em} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \textbf{Before:} - \begin{itemize}\itemsep4pt - \item Copy-paste from ChatGPT - \item Limited by web interfaces - \item No version control - \item Manual repetitive tasks - \end{itemize} - \column{0.48\textwidth} - \textbf{After This Workshop:} - \begin{itemize}\itemsep4pt - \item Direct AI API access - \item VS\,Code as AI command centre - \item Professional automation - \item 10× productivity gains - \end{itemize} - \end{columns} -\end{frame} - -% Slide 3 -\UseBackground{slide-3-backdrop.png} -\begin{frame}[t] - \frametitle{The Professional AI Divide} - \begin{center}\Large Two Classes of AI Users Are Emerging\end{center} - \vspace{1em} - \begin{itemize}\itemsep6pt - \item \textbf{The 95\%}: Limited to consumer tools, copy-paste workflows - \item \textbf{The 5\%}: Direct API access, automation, 10× productivity - \end{itemize} - \vspace{1em} - \begin{alertblock}{The Gap Is Widening Daily} - While most struggle with ChatGPT limits, a small group writes books in days, creates comprehensive documentation in hours, and builds autonomous knowledge systems. - \end{alertblock} -\end{frame} - -% Slide 4 -\UseBackground{slide-4-backdrop.png} -\begin{frame}[t] - \frametitle{Transform Your Professional Practice} - \begin{columns}[T,onlytextwidth] - \column{0.45\textwidth} - \textbf{Perfect For:} - \begin{itemize}\itemsep4pt - \item Academics \& Researchers - \item Business Leaders - \item Consultants - \item Government \& NGOs - \item Creative Professionals - \item Entrepreneurs - \end{itemize} - \column{0.45\textwidth} - \textbf{If You:} - \begin{itemize}\itemsep4pt - \item Work with documents daily - \item Feel limited by ChatGPT - \item Need privacy for sensitive data - \item Want to automate repetitive work - \item Are curious about AI’s potential - \end{itemize} - \end{columns} - \vspace{0.5em} - \begin{center}\small{\emph{No coding experience required – if you can use Word, you’re ready}}\end{center} -\end{frame} - -% Slide 5 -\UseBackground{slide-5-backdrop.png} -\begin{frame}[plain]\end{frame} - -% Slide 6 -\UseBackground{slide-6-backdrop.png} -\begin{frame}[t] - \frametitle{Your 5-Day Transformation Journey} - \begin{center} - \textbf{From AI Consumer to AI Commander in 5 Days} - \end{center} -\end{frame} - -% Slide 7 -\UseBackground{slide-7-backdrop.png} -\begin{frame}[t] - \frametitle{Programme Structure} - \footnotesize - \begin{tabular}{@{}p{0.6cm}p{3.0cm}p{5.5cm}p{3.0cm}@{}} - \toprule - \textbf{Day} & \textbf{Theme} & \textbf{Core Focus} & \textbf{Your Win}\\ - \midrule - 1 & \textcolor{aiblue}{Universal Workspace} & VS\,Code setup, containers, version control & Professional AI workspace \\ - 2 & \textcolor{aiblue}{AI Creative Partner} & Direct API access, \emph{vibe coding} & Live website via chat \\ - 3 & \textcolor{aiblue}{Private AI Brain} & Local models, RAG systems & Offline knowledge base \\ - 4 & \textcolor{aiblue}{AI Teams} & Specialised agents, orchestration & Automated workflows \\ - 5 & \textcolor{aiblue}{Pro Publishing} & LaTeX, websites, automation & Complete portfolio \\ - \bottomrule - \end{tabular} -\end{frame} - -% Slides 8–12 -% Day 1 -\UseBackground{slide-8-backdrop.png} -\begin{frame}[t] - \frametitle{Day\,1: Your Universal AI Workspace} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \textbf{Morning: Setup \& Foundations} - \begin{itemize}\itemsep4pt - \item Transform VS Code into AI command centre - \item Install game-changing extensions - \item Container setup for stability - \item Navigate like a pro - \end{itemize} - \column{0.48\textwidth} - \textbf{Afternoon: Visual Tools} - \begin{itemize}\itemsep4pt - \item Mermaid diagrams mastery - \item Version control for \textbf{ANY} document - \item Build your first repository - \item Track every change forever - \end{itemize} - \end{columns} - \vspace{0.5em} - \begin{alertblock}{Day\,1 Deliverable} - Professional workspace with visual project plan (Gantt chart) for \textbf{YOUR} real project - \end{alertblock} -\end{frame} - -% Day 2 -\UseBackground{slide-9-backdrop.png} -\begin{frame}[t] - \frametitle{Day\,2: AI as Your Creative Partner} - \begin{block}{The \emph{“Vibe Coding”} Revolution} - Describe what you want in plain English → AI builds it perfectly - \end{block} - \vspace{0.5em} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \textbf{Morning: Direct AI Access}\begin{itemize}\itemsep4pt - \item Connect OpenAI, Claude, Gemini - \item Compare model strengths - \item See real API costs (pennies!) - \item Generate business plans, papers - \end{itemize} - \column{0.48\textwidth} - \textbf{Afternoon: Build by Talking}\begin{itemize}\itemsep4pt - \item Create websites via chat - \item Interactive presentations - \item Data visualisations - \item Deploy live in minutes - \end{itemize} - \end{columns} - \vspace{0.5em} - \textcolor{successgreen}{\faCheckCircle} \textbf{Day\,2 Win:} Professional website live on the internet -\end{frame} - -% Day 3 -\UseBackground{slide-10-backdrop.png} -\begin{frame}[t] - \frametitle{Day\,3: Your Private AI Brain} - \begin{alertblock}{Complete Privacy + Total Recall} - AI that knows your entire document history and works 100\% offline - \end{alertblock} - \vspace{0.5em} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \textbf{Morning: Local AI Models}\begin{itemize}\itemsep4pt - \item Install AI on \textbf{YOUR} laptop - \item No internet required - \item Perfect for sensitive data - \item Multiple specialist models - \end{itemize} - \column{0.48\textwidth} - \textbf{Afternoon: RAG System}\begin{itemize}\itemsep4pt - \item Feed in all your documents - \item Instant answers with citations - \item \emph{“What was decided in March?”} - \item Your personal AI librarian - \end{itemize} - \end{columns} - \vspace{0.5em} - \begin{center}\small{\emph{Perfect for: Confidential business docs, proprietary research, client data}}\end{center} -\end{frame} - -% Day 4 -\UseBackground{slide-11-backdrop.png} -\begin{frame}[t] - \frametitle{Day\,4: AI Teams Working For You} - \begin{center}\Large Deploy Your AI Workforce\end{center} - \vspace{0.5em} - \begin{columns}[T,onlytextwidth] - \column{0.25\textwidth}\centering - \faSearch[regular]\quad \textbf{Research Agent}\\ Autonomous research \& fact-checking - \column{0.25\textwidth}\centering - \faFile[regular]\quad \textbf{Writer Agent}\\ Creates documents in your style - \column{0.25\textwidth}\centering - \faChartBar[regular]\quad \textbf{Analyst Agent}\\ Data processing \& insights - \column{0.25\textwidth}\centering - \faRobot[regular]\quad \textbf{Automator}\\ Handles workflows - \end{columns} - \vspace{0.5em} - \begin{block}{Afternoon: Orchestration \& Safety} - Make agents collaborate • Set spending limits • Quality checks • Cost control - \end{block} -\end{frame} - -% Day 5 -\UseBackground{slide-12-backdrop.png} -\begin{frame}[t] - \frametitle{Day\,5: Professional Publishing Suite} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \textbf{Morning: Quality \& Automation}\begin{itemize}\itemsep4pt - \item Automated quality checks - \item Weekly report workflows - \item Safety nets \& approvals - \item Full system integration - \end{itemize} - \column{0.48\textwidth} - \textbf{Afternoon: Pro Outputs}\begin{itemize}\itemsep4pt - \item LaTeX academic papers - \item Interactive business reports - \item Client microsites - \item Complete automation - \end{itemize} - \end{columns} - \vspace{0.5em} - \begin{alertblock}{Final Achievement} - Complete AI-powered workflow producing professional documents, websites, - and reports—all connected and automated - \end{alertblock} -\end{frame} - -% Slide 13 -\UseBackground{slide-13-backdrop.png} -\begin{frame}[t] - \frametitle{Real Results from Real Professionals} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \begin{block}{Academic Success} - “Used to spend weeks on grant proposals. Now my AI agents do research, I do strategy.\\ Just won £2M funding.”\\ - \emph{— Dr. Rachel Morrison, Research Director} - \end{block} - \vspace{0.5em} - \begin{block}{Business Transformation} - “Client reports that took days now take hours, with interactive diagrams.”\\ - \emph{— James Liu, Consultant} - \end{block} - \column{0.48\textwidth} - \begin{block}{Knowledge Management} - “10 years of company knowledge instantly accessible. Like having our history on tap.”\\ - \emph{— Sandra Patel, Ops Director} - \end{block} - \vspace{0.5em} - \begin{block}{Non-Technical Success} - “I’m not technical, but built our entire handbook site through conversation.”\\ - \emph{— Michael Chang, HR Director} - \end{block} - \end{columns} -\end{frame} - -% Slide 14 -\UseBackground{slide-14-backdrop.png} -\begin{frame}[t] - \frametitle{Investment \& Returns} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \textbf{Your Investment}\begin{itemize}\itemsep4pt - \item Standard: £2,995 - \item Early Bird: £2,495 (save £500) - \item Includes £200 API credits - \item Payment plans available - \item Team discounts: 15\% for 3+ - \end{itemize} - \column{0.48\textwidth} - \textbf{Documented Returns}\begin{itemize}\itemsep4pt - \item Grant writing: 70\% faster - \item Business docs: 5× speed - \item Research: 10× throughput - \item ROI in first project - \item Skills that compound daily - \end{itemize} - \end{columns} - \vspace{0.5em} - \begin{alertblock}{Satisfaction Guarantee} - Full refund if not completely satisfied after Day\,1 - \end{alertblock} -\end{frame} - -% Slide 15 -\UseBackground{slide-15-backdrop.png} -\begin{frame}[t] - \frametitle{Your Guide: Dr. John O'Hare} - \begin{columns}[T,onlytextwidth] - \column{0.45\textwidth}\centering - \includegraphics[width=0.6\textwidth]{CHO.png} - \column{0.55\textwidth} - \textbf{25+ Years at Tech’s Cutting Edge}\begin{itemize}\itemsep4pt - \item PhD in collaborative technologies - \item VR pioneer (1990s) → AI leader (today) - \item Associate Director R\&D, DREAMLAB - \item HP AI Lighthouse Partner - \item Published researcher - \item 15+ person team leadership - \end{itemize} - \end{columns} - \vspace{0.5em} - \begin{quote}\small - “The AI wave is different—faster, more profound, more accessible. This workshop distils decades of experience into skills you can use immediately.” - \end{quote} -\end{frame} - -% Slide 16 -\UseBackground{slide-16-backdrop.png} -\begin{frame}[t] - \frametitle{Why This Workshop Is Unique} - \begin{columns}[T,onlytextwidth] - \column{0.48\textwidth} - \textbf{Our Approach}\begin{itemize}\itemsep4pt - \item Work on YOUR real projects - \item Small cohorts (max 10) - \item Lifetime community access - \item Quarterly alumni workshops - \item 30-min mentorship included - \end{itemize} - \column{0.48\textwidth} - \textbf{Unique Features}\begin{itemize}\itemsep4pt - \item Vendor-agnostic training - \item Local + cloud options - \item Privacy-first approach - \item Cross-industry learning - \item Immediate application - \end{itemize} - \end{columns} - \vspace{0.5em} - \begin{block}{Not Just Tools—Transformation} - Learn principles that remain valuable as AI evolves, not just today’s tools. - \end{block} -\end{frame} - -% Slide 17 -\UseBackground{slide-17-backdrop.png} -\begin{frame}[t] - \frametitle{Secure Your Transformation} - \begin{alertblock}{Next Cohorts – Limited to 10 Participants} - \begin{itemize}\itemsep4pt - \item \textbf{March 2025}: 17–21 March (3 seats remaining) - \item \textbf{May 2025}: 12–16 May (Early bird available) - \item \textbf{July 2025}: 14–18 July (Just announced) - \end{itemize} - \end{alertblock} - \vspace{0.5em} - \textbf{How to Join:}\begin{enumerate}\itemsep4pt - \item Complete online application (5 minutes) - \item Brief screening call (15 minutes) - \item Secure with £500 deposit - \item Receive pre-course materials - \item Join cohort Discord - \end{enumerate} - \vspace{0.5em} - \begin{center}\Large\textcolor{aiblue}{\textbf{workshops@dreamlab.uk}}\\ - \small Stop using AI like everyone else. Start commanding it like the few who know how. - \end{center} -\end{frame} - -% Slide 18 -\UseBackground{slide-18-backdrop.png} -\begin{frame}[plain] - \vfill - \centering - {\Huge \textbf{The Choice Is Yours}}\\[1em] - Remain limited by consumer AI tools…\\[0.8em] - {\Large \textbf{OR}}\\[0.8em] - {\Large \textcolor{aiblue}{Gain professional-grade control that transforms how you work}}\\[2em] - {\LARGE workshops@dreamlab.uk} - \vfill -\end{frame} - -\end{document} diff --git a/Marketing-Material/slide-1-backdrop.png b/Marketing-Material/slide-1-backdrop.png deleted file mode 100644 index c0b50a6..0000000 Binary files a/Marketing-Material/slide-1-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-10-backdrop.png b/Marketing-Material/slide-10-backdrop.png deleted file mode 100644 index 710fec1..0000000 Binary files a/Marketing-Material/slide-10-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-11-backdrop.png b/Marketing-Material/slide-11-backdrop.png deleted file mode 100644 index 174fb61..0000000 Binary files a/Marketing-Material/slide-11-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-12-backdrop.png b/Marketing-Material/slide-12-backdrop.png deleted file mode 100644 index 174fb61..0000000 Binary files a/Marketing-Material/slide-12-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-13-backdrop.png b/Marketing-Material/slide-13-backdrop.png deleted file mode 100644 index b20467d..0000000 Binary files a/Marketing-Material/slide-13-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-14-backdrop.png b/Marketing-Material/slide-14-backdrop.png deleted file mode 100644 index 5241ca9..0000000 Binary files a/Marketing-Material/slide-14-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-15-backdrop.png b/Marketing-Material/slide-15-backdrop.png deleted file mode 100644 index f7ed526..0000000 Binary files a/Marketing-Material/slide-15-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-16-backdrop.png b/Marketing-Material/slide-16-backdrop.png deleted file mode 100644 index 145e6c8..0000000 Binary files a/Marketing-Material/slide-16-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-17-backdrop.png b/Marketing-Material/slide-17-backdrop.png deleted file mode 100644 index cbc1ae3..0000000 Binary files a/Marketing-Material/slide-17-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-18-backdrop.png b/Marketing-Material/slide-18-backdrop.png deleted file mode 100644 index c0b50a6..0000000 Binary files a/Marketing-Material/slide-18-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-2-backdrop.png b/Marketing-Material/slide-2-backdrop.png deleted file mode 100644 index 4cd9031..0000000 Binary files a/Marketing-Material/slide-2-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-3-backdrop.png b/Marketing-Material/slide-3-backdrop.png deleted file mode 100644 index ede94ea..0000000 Binary files a/Marketing-Material/slide-3-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-4-backdrop.png b/Marketing-Material/slide-4-backdrop.png deleted file mode 100644 index d28380f..0000000 Binary files a/Marketing-Material/slide-4-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-5-backdrop.png b/Marketing-Material/slide-5-backdrop.png deleted file mode 100644 index 9d63b1c..0000000 Binary files a/Marketing-Material/slide-5-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-6-backdrop.png b/Marketing-Material/slide-6-backdrop.png deleted file mode 100644 index 016513f..0000000 Binary files a/Marketing-Material/slide-6-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-7-backdrop.png b/Marketing-Material/slide-7-backdrop.png deleted file mode 100644 index 8de8df8..0000000 Binary files a/Marketing-Material/slide-7-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-8-backdrop.png b/Marketing-Material/slide-8-backdrop.png deleted file mode 100644 index 0e8ae1a..0000000 Binary files a/Marketing-Material/slide-8-backdrop.png and /dev/null differ diff --git a/Marketing-Material/slide-9-backdrop.png b/Marketing-Material/slide-9-backdrop.png deleted file mode 100644 index e4031c8..0000000 Binary files a/Marketing-Material/slide-9-backdrop.png and /dev/null differ diff --git a/README.md b/README.md index 8fbf970..c04703c 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,263 @@ -# DreamLab AI Consulting Ltd. Website +# DreamLab AI +**Premium AI training and consulting platform with a decentralized, end-to-end encrypted community forum.** -DreamLab AI Consulting Ltd. website landing page - A React + Vite application with ThreeJS integration. +[![Rust](https://img.shields.io/badge/Rust-1.77+-dea584?logo=rust)](https://www.rust-lang.org/) +[![Leptos 0.7](https://img.shields.io/badge/Leptos-0.7-ef3939)](https://leptos.dev/) +[![WASM](https://img.shields.io/badge/WebAssembly-654FF0?logo=webassembly&logoColor=white)](https://webassembly.org/) +[![Nostr](https://img.shields.io/badge/Nostr-Protocol-8B5CF6)](https://nostr.com/) +[![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare)](https://workers.cloudflare.com/) +[![React 18](https://img.shields.io/badge/React-18.3-61DAFB?logo=react)](https://react.dev/) +**Website**: [dreamlab-ai.com](https://dreamlab-ai.com) | **Repository**: [DreamLab-AI/dreamlab-ai-website](https://github.com/DreamLab-AI/dreamlab-ai-website) -## 🚀 Live Site +--- -Visit the live site at: [https://dreamlab-ai.com](https://dreamlab-ai.com) +## Architecture -## Edit Your Team Details +The platform consists of a React marketing site, a Rust/Leptos WASM community forum, and five Cloudflare Workers providing backend services. All communication is built on the Nostr protocol with end-to-end encryption. -Create or modify files in /public/data/team/ to add yourself +```mermaid +graph TB + subgraph "Browser" + REACT["React SPA
(Marketing Site)"] + LEPTOS["Leptos 0.7 CSR
(Community Forum)
WASM"] + end -## Manage Previous Work + subgraph "Cloudflare Workers" + direction TB + AUTH["auth-worker
(Rust WASM)"] + POD["pod-worker
(Rust WASM)"] + PREVIEW["preview-worker
(Rust WASM)"] + RELAY["nostr-relay
(TypeScript)"] + SEARCH["search-api
(TypeScript)"] + end -Previous work items are stored in `/public/data/showcase/manifest.json`. -The easiest way to update them is through [Pages CMS](https://app.pagescms.org), -which commits changes directly to this repository. Connect your GitHub account, -edit the entries in the "Showcase" collection, and your updates will be -published automatically. + subgraph "Cloudflare Storage" + D1[(D1
SQLite)] + KV[(KV
Key-Value)] + R2[(R2
Object Storage)] + DO[(Durable Objects
WebSocket State)] + end -## 🛠️ Technologies + LEPTOS -- "WebAuthn + NIP-98" --> AUTH + LEPTOS -- "Solid Pods" --> POD + LEPTOS -- "Link Previews" --> PREVIEW + LEPTOS -- "WebSocket
NIP-01" --> RELAY + LEPTOS -- "Vector Search" --> SEARCH -- React 18 -- TypeScript -- Vite -- Tailwind CSS -- ThreeJS (@react-three/fiber & @react-three/drei) -- shadcn/ui components -- React Router + AUTH --> D1 + AUTH --> KV + AUTH --> R2 + RELAY --> D1 + RELAY --> DO + POD --> R2 + POD --> KV + SEARCH --> R2 + SEARCH --> KV -## 📋 Development + style REACT fill:#61DAFB,color:#000 + style LEPTOS fill:#ef3939,color:#fff + style AUTH fill:#dea584,color:#000 + style POD fill:#dea584,color:#000 + style PREVIEW fill:#dea584,color:#000 + style RELAY fill:#3178C6,color:#fff + style SEARCH fill:#3178C6,color:#fff +``` + +## Features + +- **Passkey-first authentication** -- WebAuthn PRF derives a secp256k1 private key deterministically via HKDF. The key is never stored; it exists only in a Rust closure and is zeroized on page unload. +- **End-to-end encrypted DMs** -- NIP-59 Gift Wrap protocol (Rumor, Seal, Wrap) with NIP-44 ChaCha20-Poly1305 encryption. The relay and server never see plaintext. +- **Zone-based access control** -- Four access zones (Public Lobby, Cohort Channels, Staff Lounge, Admin Zone) enforced at both the relay and client layers. +- **Solid pods** -- User media stored in Cloudflare R2 with WAC (Web Access Control) ACL per pod, addressable via `did:nostr:{pubkey}`. +- **WASM vector search** -- RuVector WASM microkernel (42KB) with `.rvf` container format, running in a Cloudflare Worker at 490K vectors/sec. +- **Compile-time safety** -- All Rust crates enforce `#![deny(unsafe_code)]`. Zero `unsafe` blocks. NCC Group-audited cryptographic primitives. +- **3D visualizations** -- Three.js + React Three Fiber powering golden ratio Voronoi, 4D tesseract, and torus knot hero scenes on the marketing site. + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Marketing Site | React 18.3 + TypeScript 5.5 + Vite 5.4 | +| Styling | Tailwind CSS 3.4 + shadcn/ui (Radix UI) | +| 3D | Three.js 0.156 + React Three Fiber | +| Community Forum | **Rust / Leptos 0.7** (CSR, WASM, amber/gray theme) | +| Nostr Protocol | nostr-sdk 0.44 (Rust) + NDK 2.13 (legacy TS relay) | +| Auth | WebAuthn PRF via passkey-rs 0.3 + NIP-98 | +| Encryption | NIP-44 (ChaCha20-Poly1305) + NIP-59 Gift Wrap | +| Backend (Rust) | 3 Cloudflare Workers via `worker` 0.7.5 | +| Backend (TS) | 2 Cloudflare Workers (nostr-relay, search-api) | +| Storage | Cloudflare D1, KV, R2, Durable Objects | +| Hosting | GitHub Pages (static) + Cloudflare Workers (API) | +| WASM Search | RuVector microkernel + `.rvf` format | +| Crypto | k256, chacha20poly1305, hkdf, sha2 (NCC-audited) | + +## Quick Start ### Prerequisites -- Node.js (v18 or higher recommended) -- npm +```bash +# Rust toolchain + WASM target +rustup target add wasm32-unknown-unknown +cargo install trunk wasm-bindgen-cli worker-build wasm-opt + +# Node.js 20+ (for React site, Tailwind, TS Workers) +npm install -g wrangler +``` -### Setup and Run +### Clone and Build ```bash -# Install dependencies +git clone https://github.com/DreamLab-AI/dreamlab-ai-website.git +cd dreamlab-ai-website + +# Verify Rust workspace compiles (native + WASM) +cargo check --workspace +cargo check --workspace --target wasm32-unknown-unknown + +# Install Node dependencies (React site + Tailwind) npm install +``` -# Start development server +### Development Servers + +```bash +# React marketing site (http://localhost:5173) npm run dev -# Build for production -npm run build +# Leptos community forum (http://localhost:8080) +cd community-forum-rs && trunk serve + +# Cloudflare Workers (local dev with D1/KV/R2 simulators) +cd community-forum-rs/crates/auth-worker && worker-build --dev && wrangler dev +``` + +### Commands + +| Command | Description | +|---------|-------------| +| `npm run dev` | React marketing site with HMR | +| `npm run build` | Production build of React site | +| `npm run lint` | ESLint code quality checks | +| `trunk serve` | Leptos forum dev server with hot reload | +| `trunk build --release` | Production WASM build of forum | +| `cargo test --workspace` | Run all Rust tests (native) | +| `cargo test --workspace --target wasm32-unknown-unknown` | Run WASM tests | +| `cargo clippy --workspace -- -D warnings` | Lint all Rust code | -# Preview production build -npm run preview +## Project Structure + +``` +dreamlab-ai-website/ + src/ React SPA (13 lazy-loaded routes) + pages/ Route pages (Index, Team, Workshops, Contact, ...) + components/ 70+ React components (shadcn/ui primitives in ui/) + hooks/ Custom React hooks + lib/ Utilities, Supabase client + + community-forum-rs/ Rust/Leptos workspace (6 crates) + Cargo.toml Workspace root + Trunk.toml trunk build configuration + index.html Leptos SPA entry point + crates/ + nostr-core/ Shared crypto + protocol (NIP-01, NIP-44, NIP-59, NIP-98) + forum-client/ Leptos 0.7 CSR app (WASM, Tailwind, amber/gray theme) + auth-worker/ CF Worker (Rust) -- WebAuthn + NIP-98 + pod provisioning + pod-worker/ CF Worker (Rust) -- Solid pods on R2 with WAC ACL + preview-worker/ CF Worker (Rust) -- OG metadata / link preview + relay-worker/ CF Worker (Rust) -- Nostr relay stub + + workers/ TypeScript Cloudflare Workers + nostr-relay-api/ Nostr relay (D1 + Durable Objects, WebSocket) + search-api/ RuVector WASM vector search (.rvf format) + shared/ Shared modules (nip98.ts, types) + + wasm-voronoi/ Rust WASM for 3D Voronoi hero effect + public/data/ Runtime content (team profiles, workshops, media) + scripts/ Build and utility scripts + docs/ Full documentation suite (28 files) +``` + +## Documentation + +All documentation lives in the [`docs/`](docs/README.md) directory. Start there for the full navigation hub. + +| Document | Description | +|----------|-------------| +| [Documentation Hub](docs/README.md) | Central navigation for all project docs | +| [PRD: Rust Port v2.0.0](docs/prd-rust-port.md) | Accepted architecture baseline | +| [PRD: Rust Port v2.1.0](docs/prd-rust-port-v2.1.md) | Refined delivery plan with tranche-based execution | +| [Architecture Decision Records](docs/adr/README.md) | 19 ADRs tracking every major decision | +| [Domain-Driven Design](docs/ddd/README.md) | Domain model, bounded contexts, aggregates, events | +| [API Reference](docs/api/AUTH_API.md) | Auth, Pod, Relay, and Search API docs | +| [Security Overview](docs/security/SECURITY_OVERVIEW.md) | Compile-time safety, crypto stack, access control | +| [Authentication](docs/security/AUTHENTICATION.md) | Passkey PRF flow, NIP-98, session management | +| [Deployment](docs/deployment/README.md) | CI/CD pipelines, environments, DNS | +| [Getting Started](docs/developer/GETTING_STARTED.md) | Prerequisites, setup, local development | +| [Rust Style Guide](docs/developer/RUST_STYLE_GUIDE.md) | Coding standards, error handling, module patterns | +| [Benchmarks](docs/benchmarks/baseline-native.md) | nostr-core native performance baseline | +| [Feature Parity Matrix](docs/tranche-1/feature-parity-matrix.md) | SvelteKit-to-Rust migration tracking | +| [Route Parity Matrix](docs/tranche-1/route-parity-matrix.md) | Route-by-route migration status | + +## Deployment + +```mermaid +graph LR + subgraph "GitHub Actions" + PUSH["Push to main"] + DEPLOY_YML["deploy.yml"] + WORKERS_YML["workers-deploy.yml"] + end + + subgraph "Build Steps" + NPM["npm run build
(React)"] + TRUNK["trunk build --release
(Leptos WASM)"] + WASM_OPT["wasm-opt -Oz
(Size optimization)"] + WORKER_BUILD["worker-build --release
(3 Rust Workers)"] + WRANGLER["wrangler deploy
(5 Workers)"] + end + + subgraph "Hosting" + GH_PAGES["GitHub Pages
dreamlab-ai.com"] + CF_WORKERS["Cloudflare Workers
api. / pods. / search. / preview."] + end + + PUSH --> DEPLOY_YML + PUSH --> WORKERS_YML + DEPLOY_YML --> NPM --> TRUNK --> WASM_OPT --> GH_PAGES + WORKERS_YML --> WORKER_BUILD --> WRANGLER --> CF_WORKERS + + style GH_PAGES fill:#24292e,color:#fff + style CF_WORKERS fill:#F38020,color:#fff ``` -## 🌐 GitHub Pages Deployment +All workflows are guarded with `if: github.repository == 'DreamLab-AI/dreamlab-ai-website'`. + +| Target | Domain | Source | +|--------|--------|--------| +| React marketing site | `dreamlab-ai.com` | GitHub Pages (`gh-pages` branch) | +| Leptos forum client | `dreamlab-ai.com/community/` | GitHub Pages (WASM in `dist/community/`) | +| auth-worker | `api.dreamlab-ai.com` | Cloudflare Worker (Rust WASM) | +| pod-worker | `pods.dreamlab-ai.com` | Cloudflare Worker (Rust WASM) | +| preview-worker | `preview.dreamlab-ai.com` | Cloudflare Worker (Rust WASM) | +| nostr-relay | Cloudflare Worker route | Cloudflare Worker (TypeScript) | +| search-api | `search.dreamlab-ai.com` | Cloudflare Worker (TypeScript) | + +## Security Highlights + +- **Zero `unsafe`** -- All crates enforce `#![deny(unsafe_code)]` at the crate root +- **NCC Group-audited cryptography** -- `k256` (secp256k1/Schnorr), `chacha20poly1305` (NIP-44 AEAD) +- **Key never stored** -- WebAuthn PRF output fed through HKDF; private key lives only in a Rust `Option` closure, zeroized via the `zeroize` crate on page unload +- **Compile-time schema enforcement** -- All API boundaries use `serde` deserialization + `validator` runtime checks +- **SSRF protection** -- Link preview Worker blocks private/loopback/metadata IP ranges +- **Relay-level enforcement** -- Whitelist, rate limits (10 events/sec), connection limits (20/IP), size limits (64KB) +- **`cargo audit`** runs in CI on every push; `cargo clippy -- -D warnings` must pass with zero warnings -This project is configured to deploy automatically to GitHub Pages using GitHub Actions: +## Licence -1. Any push to the `main` branch triggers the deployment workflow -2. The GitHub Action will build the site and deploy it to the `gh-pages` branch -3. The site will be available at the custom domain: https://dreamlab-ai.com +Proprietary. Copyright 2024-2026 DreamLab AI Consulting Ltd. All rights reserved. -The custom domain is configured using a CNAME file in the repository. GitHub Pages will automatically use this for your domain configuration. +--- -You can also manually trigger the deployment from the Actions tab in the GitHub repository. \ No newline at end of file +*Last updated: 2026-03-08* diff --git a/brochure/README.md b/brochure/README.md deleted file mode 100644 index 84ee4f1..0000000 --- a/brochure/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# DreamLab AI Brochure System - -This directory contains the PDF brochure generation system for DreamLab AI. - -## Overview - -The brochure system provides a professional, downloadable PDF brochure for DreamLab AI's executive training services targeting the nuclear industry near Sellafield. - -## Key Files - -- `templates/dreamlab-brochure.html` - Complete HTML template with all DreamLab AI content -- `output/dreamlab-brochure-printable.html` - Static HTML version ready for printing -- `generate-pdf.js` - Core PDF generation logic using Puppeteer -- `generate-dreamlab-pdf.js` - Specific generator for DreamLab brochure -- `generate-static-html.js` - Creates printable HTML version - -## Features - -### Brochure Content (10 pages) -1. **Cover Page** - Professional design with gradient branding -2. **Table of Contents** - Easy navigation -3. **Executive Summary** - Key metrics and value proposition -4. **About DreamLab AI** - Mission, location, and team -5. **Technology Overview** - VR/AI training capabilities -6. **Training Programs** - Executive programs with pricing -7. **Premium Accommodation** - 5-bedroom luxury facility -8. **Market Opportunity** - Sellafield and UK nuclear market -9. **Investment Highlights** - Financial projections and growth -10. **Contact Information** - Full contact details - -### Design Elements -- Dark theme matching website branding -- Blue (#3B82F6) to purple (#A855F7) gradients -- Professional typography and layout -- Print-optimized CSS -- A4 format with proper margins - -## Usage - -### Option 1: Static HTML (Recommended) -1. The brochure is available at: `/public/dreamlab-brochure-printable.html` -2. Users click "Download DreamLab AI Brochure" button on the website -3. HTML opens in new tab with "Print as PDF" button -4. Users save as PDF using browser's print function - -### Option 2: Puppeteer Generation (Requires Dependencies) -```bash -# Install system dependencies first -sudo bash install-deps.sh - -# Generate PDF -node generate-dreamlab-pdf.js -``` - -### Option 3: API Endpoints (If Running Server) -```bash -# Start API server -node api-updated.js - -# Generate brochure -GET /generate-dreamlab - -# Download brochure -GET /download-brochure -``` - -## Integration - -The brochure is integrated into the React website via: -- `src/components/BrochureGenerator.tsx` - Download button component -- Public folder access for static HTML - -## Customization - -To update the brochure content: -1. Edit `templates/dreamlab-brochure.html` -2. Regenerate static version: `node generate-static-html.js` -3. Copy to public folder: `cp output/dreamlab-brochure-printable.html ../public/` - -## Key Information - -- **Target Market**: Sellafield Nuclear Site (11,000 employees) -- **Location**: Eskdale Green, Lake District -- **Programs**: £5,000-£12,500 executive training -- **Accommodation**: £350-£650 per night -- **Investment**: 68.5% IRR, £3M revenue target - -## Quality Assurance - -The brochure has been designed following best practices: -- Professional business design -- Clear information hierarchy -- Mobile-friendly viewing -- Print-optimized formatting -- Accessibility considerations -- Cross-browser compatibility \ No newline at end of file diff --git a/brochure/api-updated.js b/brochure/api-updated.js deleted file mode 100644 index 83e6f64..0000000 --- a/brochure/api-updated.js +++ /dev/null @@ -1,141 +0,0 @@ -// API endpoints for PDF generation -import express from 'express'; -import { generatePDF } from './generate-pdf.js'; -import { generateDreamLabBrochure } from './generate-dreamlab-pdf.js'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { readFile, access } from 'fs/promises'; -import { constants } from 'fs'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const app = express(); -app.use(express.json()); - -// Health check endpoint -app.get('/', (req, res) => { - res.json({ - message: 'DreamLab AI Brochure API', - endpoints: { - 'POST /generate': 'Generate PDF from template and data', - 'GET /generate-dreamlab': 'Generate the official DreamLab AI brochure', - 'GET /download/:filename': 'Download generated PDF', - 'GET /download-brochure': 'Download the official DreamLab AI brochure' - } - }); -}); - -// Generate PDF endpoint -app.post('/generate', async (req, res) => { - try { - const { templatePath, data, outputFilename } = req.body; - - if (!templatePath || !outputFilename) { - return res.status(400).json({ - success: false, - error: 'templatePath and outputFilename are required' - }); - } - - const outputPath = join(__dirname, 'output', outputFilename); - - await generatePDF({ - templatePath: join(__dirname, templatePath), - outputPath, - data: data || {}, - pdfOptions: { - format: 'A4', - printBackground: true, - displayHeaderFooter: false, - margin: { - top: '10mm', - right: '10mm', - bottom: '10mm', - left: '10mm' - } - } - }); - - res.json({ - success: true, - message: 'PDF generated successfully', - filename: outputFilename, - downloadUrl: `/download/${outputFilename}` - }); - } catch (error) { - res.status(500).json({ - success: false, - error: error.message - }); - } -}); - -// Generate DreamLab brochure endpoint -app.get('/generate-dreamlab', async (req, res) => { - try { - const outputPath = await generateDreamLabBrochure(); - res.json({ - success: true, - message: 'DreamLab AI brochure generated successfully', - filename: 'DreamLab-AI-Brochure.pdf', - downloadUrl: '/download-brochure' - }); - } catch (error) { - res.status(500).json({ - success: false, - error: error.message - }); - } -}); - -// Download DreamLab brochure endpoint -app.get('/download-brochure', async (req, res) => { - try { - const filepath = join(__dirname, 'output', 'DreamLab-AI-Brochure.pdf'); - - // Check if file exists - try { - await access(filepath, constants.F_OK); - } catch { - // Generate it if it doesn't exist - await generateDreamLabBrochure(); - } - - res.download(filepath, 'DreamLab-AI-Brochure.pdf'); - } catch (error) { - res.status(500).json({ - success: false, - error: 'Failed to download brochure' - }); - } -}); - -// Download endpoint -app.get('/download/:filename', async (req, res) => { - try { - const { filename } = req.params; - const filepath = join(__dirname, 'output', filename); - - // Security check - prevent directory traversal - if (filename.includes('..') || filename.includes('/')) { - return res.status(400).json({ - success: false, - error: 'Invalid filename' - }); - } - - res.download(filepath); - } catch (error) { - res.status(404).json({ - success: false, - error: 'File not found' - }); - } -}); - -// Start server -const PORT = process.env.PORT || 3001; -app.listen(PORT, () => { - console.log(`✅ Brochure API server running on http://localhost:${PORT}`); -}); \ No newline at end of file diff --git a/brochure/api.js b/brochure/api.js deleted file mode 100644 index 07687e5..0000000 --- a/brochure/api.js +++ /dev/null @@ -1,66 +0,0 @@ -import express from 'express'; -import { generateBrochure } from './generate-pdf.js'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const router = express.Router(); - -/** - * POST /api/brochure/generate - * Generate a PDF brochure with provided data - */ -router.post('/generate', async (req, res) => { - try { - const data = req.body; - - // Validate required fields - const requiredFields = ['companyName', 'tagline', 'introduction']; - const missingFields = requiredFields.filter(field => !data[field]); - - if (missingFields.length > 0) { - return res.status(400).json({ - error: 'Missing required fields', - missing: missingFields - }); - } - - // Generate PDF - const pdfPath = await generateBrochure(data); - - // Send PDF file - res.download(pdfPath, 'dreamlab-brochure.pdf', (err) => { - if (err) { - console.error('Error sending PDF:', err); - res.status(500).json({ error: 'Failed to send PDF' }); - } - }); - - } catch (error) { - console.error('Error generating brochure:', error); - res.status(500).json({ - error: 'Failed to generate brochure', - message: error.message - }); - } -}); - -/** - * GET /api/brochure/download/:filename - * Download a previously generated brochure - */ -router.get('/download/:filename', (req, res) => { - const { filename } = req.params; - const filePath = join(__dirname, 'output', filename); - - res.download(filePath, (err) => { - if (err) { - console.error('Error downloading file:', err); - res.status(404).json({ error: 'File not found' }); - } - }); -}); - -export default router; \ No newline at end of file diff --git a/brochure/content-structure.md b/brochure/content-structure.md deleted file mode 100644 index decd38d..0000000 --- a/brochure/content-structure.md +++ /dev/null @@ -1,172 +0,0 @@ -# DreamLab AI Brochure Content Structure - -## Page 1: Cover -- **Hero Visual**: Abstract neural network visualization with blue/purple gradients -- **Main Title**: DREAMLAB AI CONSULTING -- **Tagline**: Deep learning with no distractions -- **Subtext**: Transform Your Skills at the UK's Premier Tech Training Facility -- **Footer**: Website and email - -## Page 2: Table of Contents -- About DreamLab AI ........................ 3 -- Our Facility .............................. 4 -- Training Programs Overview ................ 5 -- Virtual Production & Engineering Viz ...... 6 -- Gaussian Splatting & Neural Rendering ..... 7 -- Telepresence & Remote Collaboration ....... 8 -- AI for Creative Production ................ 9 -- Agentic Engineering Systems .............. 10 -- The Residential Experience ............... 11 -- Industry Applications .................... 12 -- Success Stories & Testimonials ........... 13 -- Booking Your Training .................... 14 -- Contact Information .............. Back Cover - -## Page 3: About DreamLab AI -### Mission -"Empowering professionals to master emerging technologies through immersive, hands-on experiences at the intersection of creative vision and engineering precision." - -### Vision -"To be the global leader in convergent technology training, where creative industries and engineering disciplines unite to shape the future." - -### Our Unique Position -- UK's only facility combining virtual production with engineering simulation -- Residential immersion model for accelerated learning -- Cross-industry expertise from film to aerospace -- £2M+ of specialized equipment access -- Small cohorts ensuring personalized attention - -### Core Values -- **Innovation**: Pushing boundaries of what's possible -- **Excellence**: Industry-leading expertise and facilities -- **Convergence**: Uniting creative and technical disciplines -- **Sustainability**: Eco-conscious operations and future-focused training - -## Page 4: Our Facility -### State-of-the-Art Equipment -- **6m × 2.5m LED Volume**: Professional virtual production stage -- **Motion Capture Studio**: Vicon/OptiTrack systems -- **Gaussian Splatting Lab**: Multi-camera array setup -- **Compute Power**: High-end workstations with RTX 4090s -- **Holographic Displays**: Latest telepresence technology - -### Location Benefits -- **Cumbria, UK**: Inspiring natural setting near Cardiff -- **5-Bedroom Luxury Accommodation**: On-site residential facilities -- **Dedicated Training Wing**: Purpose-built learning spaces -- **Easy Access**: 2 hours from London, 45 mins from Bristol - -## Page 5: Training Programs Overview -### Our Approach -"Hands-on, project-based learning with real-world applications. No lectures, just doing." - -### Program Categories -1. **Creative Technology**: Virtual production, mocap, real-time rendering -2. **Engineering Visualization**: CAE/CFD data in game engines -3. **Emerging Tech**: Gaussian splatting, neural rendering, AI/ML -4. **Communication Systems**: Telepresence, XR collaboration -5. **Advanced Systems**: Agentic engineering, automation - -### Who Should Attend -- Creative professionals expanding technical skills -- Engineers seeking visualization expertise -- Studios adopting new pipelines -- R&D teams exploring emerging tech -- Freelancers future-proofing careers - -## Pages 6-10: Individual Training Programs -[Detailed program pages with modules, learning outcomes, and project examples] - -## Page 11: The Residential Experience -### Immersive Learning Environment -- **Focused Intensity**: No commute, no distractions -- **Peer Learning**: Network with industry professionals -- **24/7 Lab Access**: Work on projects anytime -- **Mentorship**: Expert instructors available throughout - -### Accommodation & Amenities -- Private bedrooms with workspace -- Fully equipped kitchen and dining -- Lounge and collaboration areas -- High-speed internet throughout -- Local catering options -- Evening social activities - -## Page 12: Industry Applications -### Creative Industries -- Film & television production -- Game development -- Advertising & commercials -- Live events & concerts -- Architectural visualization - -### Engineering Sectors -- Aerospace simulation -- Automotive design -- Energy & utilities -- Manufacturing -- Medical visualization - -### Enterprise Solutions -- Corporate training -- Product demonstrations -- Remote collaboration -- Digital twins -- Safety simulations - -## Page 13: Success Stories -### Client Testimonials -"DreamLab transformed how we approach virtual production. The hands-on training gave our team immediate, practical skills." - Studio Director - -"The engineering viz training opened new possibilities for client communication. ROI was immediate." - CAE Manager - -### Training Impact -- 95% report immediate skill application -- 87% implement new workflows within 30 days -- 92% recommend to colleagues -- Average 3.2x productivity improvement - -### Partner Organizations -[Logos of key clients and partners] - -## Page 14: Booking Your Training -### How to Book -1. **Choose Your Program**: Select from our course catalog -2. **Check Availability**: View upcoming dates online -3. **Reserve Your Spot**: £500 deposit secures booking -4. **Prepare**: Receive pre-course materials -5. **Transform**: Arrive ready to level up - -### Pricing -- Individual bookings: Standard rates -- Team bookings (3+): 10% discount -- Corporate packages: Custom pricing -- Early bird: 15% off 60+ days advance - -### What's Included -- All training and materials -- Accommodation and meals -- Equipment and software access -- Certificate of completion -- Post-training support - -## Back Cover: Contact Information -### Get Started Today -**Transform Your Skills at DreamLab AI** - -**Email**: info@dreamlab.ai -**Web**: www.dreamlab.ai -**Phone**: +44 (0) 29 XXXX XXXX - -**Address**: -DreamLab AI Consulting Ltd. -Cumbria, United Kingdom - -**Follow Us**: -LinkedIn: /dreamlab-ai-consulting -Bluesky: @thedreamlab.bsky.social -Instagram: @dreamlab.ai - -[QR Code for instant booking] - -"Where Vision Meets Precision" \ No newline at end of file diff --git a/brochure/design-specifications.md b/brochure/design-specifications.md deleted file mode 100644 index 1dd477a..0000000 --- a/brochure/design-specifications.md +++ /dev/null @@ -1,101 +0,0 @@ -# DreamLab AI PDF Brochure Design Specifications - -## Brand Overview -- **Primary Colors**: Dark theme with blue (#3B82F6) and purple (#A855F7) gradients -- **Background**: Dark (#0A0A0B) -- **Text**: Light foreground (#FAFAFA) on dark backgrounds -- **Accent**: Blue-purple gradient for highlights -- **Typography**: Modern sans-serif (system fonts) - -## Brochure Structure - -### 1. Cover Page -- **Hero Image**: Abstract visualization of interconnected technology nodes (neural network style) -- **Title**: "DREAMLAB AI" with gradient text effect -- **Tagline**: "Deep learning with no distractions" -- **Bottom**: Contact info and website URL - -### 2. Table of Contents -- Clean, minimalist design with page numbers -- Sections clearly labeled with icons - -### 3. About DreamLab (Pages 3-4) -- **Company Overview**: Mission, vision, unique position -- **Facility Features**: LED Volume, Motion Capture, Gaussian Splatting Lab -- **Location**: Cumbria, UK with map visualization -- **Team Expertise**: Key capabilities and industry experience - -### 4. Training Programs (Pages 5-10) -- **Program Cards**: Each program gets dedicated space - - Virtual Production & Engineering Viz - - Gaussian Splatting & Neural Rendering - - Telepresence & Remote Collaboration - - AI for Creative Production - - Agentic Engineering Systems -- **Format**: Duration, price, capacity, key modules -- **Visual Elements**: Icons for each technology area - -### 5. Residential Experience (Pages 11-12) -- **Accommodation**: 5-bedroom luxury facility -- **Training Environment**: Immersive, focused learning -- **Networking**: Small cohort benefits -- **Meal & Amenities**: Full residential package - -### 6. Industry Applications (Pages 13-14) -- **Creative Industries**: Film, gaming, VFX -- **Engineering**: CAE/CFD visualization -- **Enterprise**: Training and simulation -- **Research**: Academic partnerships - -### 7. Success Stories (Page 15) -- **Case Studies**: Brief testimonials and outcomes -- **Partner Logos**: Industry collaborations -- **Metrics**: Training impact numbers - -### 8. Contact & Booking (Back Cover) -- **Call to Action**: "Transform Your Skills Today" -- **Contact Methods**: Email, phone, website -- **Social Media**: LinkedIn, Bluesky, Instagram -- **QR Code**: Direct link to booking page - -## Design Guidelines - -### Layout Principles -- **Grid**: 12-column grid with generous margins -- **White Space**: 30-40% to maintain premium feel -- **Hierarchy**: Clear visual hierarchy with size and weight -- **Balance**: Mix of text, images, and graphics - -### Typography -- **Headings**: Bold, large (36-48pt for main, 24-30pt for sub) -- **Body Text**: Regular weight, 11-12pt -- **Captions**: Light weight, 9-10pt -- **Line Height**: 1.5-1.6 for readability - -### Visual Elements -- **Gradient Overlays**: Blue-purple for emphasis -- **Icons**: Minimalist line icons for features -- **Borders**: Subtle borders using muted colors -- **Cards**: Elevated with subtle shadows -- **Images**: High-contrast, tech-focused imagery - -### Color Usage -- **Primary Text**: #FAFAFA on dark backgrounds -- **Secondary Text**: #A1A1AA (muted foreground) -- **Backgrounds**: #0A0A0B (main), #18181B (cards) -- **Accents**: Blue (#3B82F6) to Purple (#A855F7) gradients -- **Success**: Green accents for positive metrics - -## Technical Specifications -- **Format**: A4 (210 x 297 mm) -- **Orientation**: Portrait -- **Bleed**: 3mm on all sides -- **Resolution**: 300 DPI for print -- **Color Mode**: CMYK for print, RGB for digital -- **File Formats**: PDF/X-4 for print, standard PDF for digital - -## Responsive Considerations -- **Digital Version**: Optimized for screen viewing -- **Interactive Elements**: Clickable links and navigation -- **File Size**: Under 10MB for easy sharing -- **Accessibility**: High contrast ratios, readable fonts \ No newline at end of file diff --git a/brochure/design-system.md b/brochure/design-system.md deleted file mode 100644 index 2c1aec0..0000000 --- a/brochure/design-system.md +++ /dev/null @@ -1,321 +0,0 @@ -# DreamLab AI Brochure Design System - -## Color Palette - -### Primary Colors -```css ---primary-blue: #3B82F6; /* Main blue accent */ ---primary-purple: #A855F7; /* Purple accent */ ---gradient-primary: linear-gradient(135deg, #3B82F6 0%, #A855F7 100%); -``` - -### Dark Theme Base -```css ---background-dark: #0A0A0B; /* Main background */ ---background-elevated: #18181B; /* Card backgrounds */ ---background-subtle: #27272A; /* Subtle elevation */ -``` - -### Text Colors -```css ---text-primary: #FAFAFA; /* Main text */ ---text-secondary: #E4E4E7; /* Body text */ ---text-muted: #A1A1AA; /* Muted/caption text */ ---text-disabled: #71717A; /* Disabled state */ -``` - -### Semantic Colors -```css ---success: #10B981; /* Success/positive */ ---warning: #F59E0B; /* Warning/caution */ ---error: #EF4444; /* Error/negative */ ---info: #3B82F6; /* Information */ -``` - -### Borders & Overlays -```css ---border-subtle: rgba(255, 255, 255, 0.08); ---border-default: rgba(255, 255, 255, 0.1); ---border-strong: rgba(255, 255, 255, 0.15); ---overlay-light: rgba(255, 255, 255, 0.05); ---overlay-medium: rgba(255, 255, 255, 0.1); -``` - -## Typography - -### Font Stack -```css -font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', - 'Helvetica Neue', 'Arial', sans-serif; -``` - -### Type Scale -```css ---text-xs: 12px; /* Captions, labels */ ---text-sm: 14px; /* Secondary text */ ---text-base: 16px; /* Body text */ ---text-lg: 18px; /* Large body */ ---text-xl: 20px; /* Section headers */ ---text-2xl: 24px; /* Sub-headings */ ---text-3xl: 32px; /* Page headings */ ---text-4xl: 40px; /* Major headings */ ---text-5xl: 48px; /* Hero text */ -``` - -### Font Weights -```css ---font-light: 300; ---font-regular: 400; ---font-medium: 500; ---font-semibold: 600; ---font-bold: 700; -``` - -### Line Heights -```css ---leading-tight: 1.25; ---leading-normal: 1.5; ---leading-relaxed: 1.6; ---leading-loose: 1.75; -``` - -## Spacing System - -### Base Unit: 4px -```css ---space-1: 4px; ---space-2: 8px; ---space-3: 12px; ---space-4: 16px; ---space-5: 20px; ---space-6: 24px; ---space-8: 32px; ---space-10: 40px; ---space-12: 48px; ---space-16: 64px; ---space-20: 80px; -``` - -### Page Margins (A4) -```css ---margin-top: 30mm; ---margin-bottom: 30mm; ---margin-left: 25mm; ---margin-right: 25mm; -``` - -## Component Styles - -### Cards -```css -.card { - background: var(--background-elevated); - border: 1px solid var(--border-default); - border-radius: 12px; - padding: var(--space-6); - backdrop-filter: blur(10px); -} - -.card-featured { - border-color: var(--primary-blue); - box-shadow: 0 0 20px rgba(59, 130, 246, 0.2); -} -``` - -### Buttons -```css -.button-primary { - background: var(--gradient-primary); - color: white; - padding: var(--space-3) var(--space-8); - border-radius: 8px; - font-weight: var(--font-semibold); -} - -.button-secondary { - background: transparent; - color: var(--text-primary); - border: 1px solid var(--border-strong); - padding: var(--space-3) var(--space-8); - border-radius: 8px; -} -``` - -### Icons -```css -.icon { - width: 24px; - height: 24px; - stroke-width: 2px; -} - -.icon-large { - width: 48px; - height: 48px; -} - -.icon-colored { - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} -``` - -### Badges -```css -.badge { - display: inline-flex; - padding: var(--space-1) var(--space-3); - background: var(--overlay-medium); - border: 1px solid var(--border-default); - border-radius: 16px; - font-size: var(--text-sm); - font-weight: var(--font-medium); -} -``` - -## Layout Grid - -### 12-Column Grid -```css -.grid { - display: grid; - grid-template-columns: repeat(12, 1fr); - gap: var(--space-5); -} - -.col-span-1 { grid-column: span 1; } -.col-span-2 { grid-column: span 2; } -.col-span-3 { grid-column: span 3; } -.col-span-4 { grid-column: span 4; } -.col-span-6 { grid-column: span 6; } -.col-span-8 { grid-column: span 8; } -.col-span-12 { grid-column: span 12; } -``` - -## Visual Effects - -### Gradients -```css -.gradient-text { - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.gradient-border { - border: 2px solid transparent; - background: linear-gradient(var(--background-dark), var(--background-dark)) padding-box, - var(--gradient-primary) border-box; -} -``` - -### Shadows -```css ---shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.5); ---shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); ---shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); ---shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.5); -``` - -### Blur Effects -```css -.blur-backdrop { - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -.blur-overlay { - background: rgba(10, 10, 11, 0.8); - backdrop-filter: blur(20px); -} -``` - -## Motion & Transitions - -### Timing Functions -```css ---ease-in: cubic-bezier(0.4, 0, 1, 1); ---ease-out: cubic-bezier(0, 0, 0.2, 1); ---ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); -``` - -### Durations -```css ---duration-fast: 150ms; ---duration-normal: 300ms; ---duration-slow: 500ms; -``` - -### Standard Transitions -```css -.transition-colors { - transition: color var(--duration-normal) var(--ease-in-out), - background-color var(--duration-normal) var(--ease-in-out), - border-color var(--duration-normal) var(--ease-in-out); -} - -.transition-transform { - transition: transform var(--duration-normal) var(--ease-out); -} -``` - -## Page Templates - -### Cover Page Layout -``` -┌─────────────────────────┐ -│ Neural Background │ -│ │ -│ Logo/Icon │ -│ │ -│ Main Title │ -│ (Gradient Text) │ -│ │ -│ Tagline │ -│ │ -│ Description Box │ -│ │ -│ │ -│ Footer Info │ -└─────────────────────────┘ -``` - -### Content Page Layout -``` -┌─────────────────────────┐ -│ Header with Icon │ -│ Page Title │ -├─────────────────────────┤ -│ │ -│ Main Content Area │ -│ - Sections │ -│ - Cards │ -│ - Lists │ -│ │ -├─────────────────────────┤ -│ CTA Section │ -│ Page Number │ -└─────────────────────────┘ -``` - -## Implementation Notes - -### Print Considerations -- Use CMYK color space for print -- Ensure 300 DPI for all images -- Add 3mm bleed on all sides -- Test dark backgrounds for print quality - -### Digital Optimization -- Compress images for web (under 200KB each) -- Use vector graphics where possible -- Ensure text remains selectable -- Add hyperlinks for digital version - -### Accessibility -- Maintain contrast ratio of 4.5:1 minimum -- Use semantic HTML structure -- Include alt text for images -- Ensure logical reading order \ No newline at end of file diff --git a/brochure/generate-dreamlab-pdf.js b/brochure/generate-dreamlab-pdf.js deleted file mode 100644 index 806e13b..0000000 --- a/brochure/generate-dreamlab-pdf.js +++ /dev/null @@ -1,52 +0,0 @@ -import { generatePDF } from './generate-pdf.js'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function generateDreamLabBrochure() { - console.log('🚀 Generating DreamLab AI Brochure...'); - - try { - const templatePath = join(__dirname, 'templates', 'dreamlab-brochure.html'); - const outputPath = join(__dirname, 'output', 'DreamLab-AI-Brochure.pdf'); - - // Generate the PDF with A4 settings - await generatePDF({ - templatePath, - outputPath, - data: {}, // No dynamic data needed for this static brochure - pdfOptions: { - format: 'A4', - printBackground: true, - displayHeaderFooter: false, - margin: { - top: '0mm', - right: '0mm', - bottom: '0mm', - left: '0mm' - }, - preferCSSPageSize: true - } - }); - - console.log('✅ Brochure generated successfully!'); - console.log(`📄 PDF saved to: ${outputPath}`); - - // Return the path for use in other scripts - return outputPath; - } catch (error) { - console.error('❌ Error generating brochure:', error); - throw error; - } -} - -// Run if called directly -if (import.meta.url === `file://${process.argv[1]}`) { - generateDreamLabBrochure() - .then(() => process.exit(0)) - .catch(() => process.exit(1)); -} - -export { generateDreamLabBrochure }; \ No newline at end of file diff --git a/brochure/generate-pdf.js b/brochure/generate-pdf.js deleted file mode 100644 index 67e7806..0000000 --- a/brochure/generate-pdf.js +++ /dev/null @@ -1,121 +0,0 @@ -import puppeteer from 'puppeteer'; -import { readFile, writeFile } from 'fs/promises'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -/** - * Generate a PDF from an HTML template - * @param {Object} options - Generation options - * @param {string} options.templatePath - Path to HTML template - * @param {string} options.outputPath - Path for output PDF - * @param {Object} options.data - Data to inject into template - * @param {Object} options.pdfOptions - Puppeteer PDF options - */ -export async function generatePDF({ - templatePath, - outputPath, - data = {}, - pdfOptions = {} -}) { - let browser; - - try { - // Launch Puppeteer - browser = await puppeteer.launch({ - headless: 'new', - args: ['--no-sandbox', '--disable-setuid-sandbox'] - }); - - const page = await browser.newPage(); - - // Read HTML template - let html = await readFile(templatePath, 'utf-8'); - - // Replace placeholders in template with data - Object.entries(data).forEach(([key, value]) => { - const placeholder = new RegExp(`{{${key}}}`, 'g'); - html = html.replace(placeholder, value); - }); - - // Set viewport for consistent rendering - await page.setViewport({ - width: 1200, - height: 1600, - deviceScaleFactor: 2 - }); - - // Load HTML content - await page.setContent(html, { - waitUntil: 'networkidle0', - timeout: 30000 - }); - - // Wait for any custom fonts or images to load - await page.evaluateHandle('document.fonts.ready'); - - // Generate PDF with default options - const defaultPdfOptions = { - format: 'A4', - printBackground: true, - displayHeaderFooter: false, - margin: { - top: '20mm', - right: '20mm', - bottom: '20mm', - left: '20mm' - }, - preferCSSPageSize: true - }; - - const finalPdfOptions = { ...defaultPdfOptions, ...pdfOptions }; - const pdfBuffer = await page.pdf(finalPdfOptions); - - // Save PDF - await writeFile(outputPath, pdfBuffer); - - console.log(`✅ PDF generated successfully: ${outputPath}`); - return outputPath; - - } catch (error) { - console.error('❌ Error generating PDF:', error); - throw error; - } finally { - if (browser) { - await browser.close(); - } - } -} - -/** - * Generate a brochure PDF with specific styling - */ -export async function generateBrochure(data) { - const templatePath = join(__dirname, 'templates', 'brochure-template.html'); - const outputPath = join(__dirname, 'output', `dreamlab-brochure-${Date.now()}.pdf`); - - return generatePDF({ - templatePath, - outputPath, - data, - pdfOptions: { - format: 'A4', - landscape: false, - printBackground: true, - margin: { - top: '15mm', - right: '15mm', - bottom: '15mm', - left: '15mm' - } - } - }); -} - -// Export for use in other modules -export default { - generatePDF, - generateBrochure -}; \ No newline at end of file diff --git a/brochure/generate-static-html.js b/brochure/generate-static-html.js deleted file mode 100644 index c510671..0000000 --- a/brochure/generate-static-html.js +++ /dev/null @@ -1,94 +0,0 @@ -import { readFile, writeFile } from 'fs/promises'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function generateStaticHTML() { - console.log('📄 Generating static HTML for DreamLab AI Brochure...'); - - try { - // Read the template - const templatePath = join(__dirname, 'templates', 'dreamlab-brochure.html'); - const html = await readFile(templatePath, 'utf-8'); - - // Add print-friendly CSS and instructions - const enhancedHTML = html.replace('', ` - - - `); - - // Add print button after body tag - const finalHTML = enhancedHTML.replace('', ` - `); - - // Save the enhanced HTML - const outputPath = join(__dirname, 'output', 'dreamlab-brochure-printable.html'); - await writeFile(outputPath, finalHTML); - - console.log('✅ Static HTML generated successfully!'); - console.log(`📄 HTML saved to: ${outputPath}`); - console.log('\n📌 To create a PDF:'); - console.log(' 1. Open the HTML file in a browser'); - console.log(' 2. Click the "Print as PDF" button'); - console.log(' 3. Save as PDF with background graphics enabled'); - - return outputPath; - } catch (error) { - console.error('❌ Error generating static HTML:', error); - throw error; - } -} - -// Run if called directly -if (import.meta.url === `file://${process.argv[1]}`) { - generateStaticHTML() - .then(() => process.exit(0)) - .catch(() => process.exit(1)); -} - -export { generateStaticHTML }; \ No newline at end of file diff --git a/brochure/install-deps.sh b/brochure/install-deps.sh deleted file mode 100644 index 3dc3daa..0000000 --- a/brochure/install-deps.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -# Install Puppeteer dependencies for Linux -echo "Installing Puppeteer dependencies..." - -# Update package list -sudo apt-get update - -# Install required dependencies -sudo apt-get install -y \ - libasound2 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libatspi2.0-0 \ - libc6 \ - libcairo2 \ - libcups2 \ - libdbus-1-3 \ - libdrm2 \ - libexpat1 \ - libgbm1 \ - libgcc1 \ - libglib2.0-0 \ - libgtk-3-0 \ - libnspr4 \ - libnss3 \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libstdc++6 \ - libx11-6 \ - libx11-xcb1 \ - libxcb-dri3-0 \ - libxcb1 \ - libxcomposite1 \ - libxcursor1 \ - libxdamage1 \ - libxext6 \ - libxfixes3 \ - libxi6 \ - libxkbcommon0 \ - libxrandr2 \ - libxrender1 \ - libxshmfence1 \ - libxss1 \ - libxtst6 \ - fonts-liberation \ - libappindicator3-1 \ - libnss3 \ - lsb-release \ - xdg-utils \ - wget - -echo "Dependencies installed successfully!" \ No newline at end of file diff --git a/brochure/output/dreamlab-brochure-printable.html b/brochure/output/dreamlab-brochure-printable.html deleted file mode 100644 index ba57c65..0000000 --- a/brochure/output/dreamlab-brochure-printable.html +++ /dev/null @@ -1,793 +0,0 @@ - - - - - - DreamLab AI - Professional Brochure - - - - - - - - -
-
- -

DREAMLAB AI

-

Deep learning with no distractions

-
-

Transform Nuclear Industry Leadership

-

Through Immersive AI & VR Training

-
-
- -
- - -
- -
-
- Executive Summary - 3 -
-
- About DreamLab AI - 4 -
-
- Our Technology - 5 -
-
- Training Programs - 6 -
-
- Premium Accommodation - 7 -
-
- Market Opportunity - 8 -
-
- Investment Highlights - 9 -
-
- Contact Information - 10 -
-
-
- - -
- -
-

- DreamLab AI is the UK's premier executive training facility, strategically positioned 30 minutes from Sellafield Nuclear Site in the stunning Lake District. We transform nuclear industry leaders through cutting-edge AI and VR immersive training experiences. -

- -
-
-
68.5%
-
IRR
-
-
-
£3M
-
Annual Revenue Target
-
-
-
51,000+
-
Target Market
-
-
- -

Strategic Advantages

-
    -
  • Only executive training facility near Sellafield (11,000 employees)
  • -
  • Unique nuclear + AI/VR specialization
  • -
  • Premium Lake District location near Scafell Pike
  • -
  • Dual revenue streams: training + luxury accommodation
  • -
  • First-mover advantage in £16.1B UK nuclear market
  • -
- -

Business Model

-

We offer intensive 2-5 day executive training programs combining:

-
    -
  • AI-powered nuclear simulations
  • -
  • VR safety and operations training
  • -
  • Leadership transformation workshops
  • -
  • 5-bedroom luxury accommodation (£350-650/night)
  • -
-
-
- - -
- -
-

Our Mission

-

Transform nuclear industry leadership through immersive AI and VR training, creating safer, more efficient operations while advancing the UK's clean energy future.

- -

Strategic Location

-
-
-

Eskdale Green, Lake District

-

Premium location combining natural beauty with proximity to Sellafield Nuclear Site

-
-
-

30 Minutes from Sellafield

-

Ideal for executive training without disrupting operations

-
-
-

Near Scafell Pike

-

Inspiring setting for leadership transformation

-
-
-

Excellent Transport Links

-

Easy access from Manchester, London, and Scotland

-
-
- -

Our Team

-

Led by industry veterans with combined expertise in:

-
    -
  • Nuclear operations and safety
  • -
  • AI and machine learning
  • -
  • Virtual reality training systems
  • -
  • Executive education and development
  • -
  • Hospitality and luxury experiences
  • -
-
-
- - -
- -
-

State-of-the-Art Training Facility

- -
-
-

6m × 2.5m LED Virtual Production Screen

-

Cinema-quality immersive environments for realistic nuclear facility simulations

-
-
-

24-Speaker Spatial Audio System

-

360-degree sound for enhanced realism and situational awareness training

-
-
-

Motion Control Robotics

-

Precise equipment simulation for hands-on operational training

-
-
-

6kW Solar Power System

-

Sustainable operations demonstrating clean energy commitment

-
-
- -

AI-Powered Training Platform

-
    -
  • Custom nuclear scenario simulations
  • -
  • Real-time performance analytics
  • -
  • Personalized learning pathways
  • -
  • Multi-user collaborative environments
  • -
  • Integration with Sellafield systems
  • -
- -

VR Training Capabilities

-

Our immersive VR system enables:

-
    -
  • Safe handling of hazardous materials
  • -
  • Emergency response procedures
  • -
  • Complex maintenance operations
  • -
  • Leadership crisis scenarios
  • -
  • Team coordination exercises
  • -
-
-
- - -
- -
-
-

Nuclear AI Leadership

-

Transform your leadership approach with AI-powered decision making and predictive analytics for nuclear operations.

-
    -
  • AI strategy for nuclear facilities
  • -
  • Predictive maintenance systems
  • -
  • Safety optimization algorithms
  • -
  • Data-driven decision frameworks
  • -
- 3 days - £7,500 per person -
- -
-

Advanced VR Operations Training

-

Master complex nuclear procedures in our state-of-the-art VR environment with zero safety risk.

-
    -
  • Hazardous material handling
  • -
  • Emergency response scenarios
  • -
  • Remote operations training
  • -
  • Multi-team coordination
  • -
- 2 days - £5,000 per person -
- -
-

Digital Transformation Bootcamp

-

Comprehensive program covering AI, VR, and digital innovation for nuclear industry leaders.

-
    -
  • Digital strategy development
  • -
  • Innovation lab sessions
  • -
  • Change management
  • -
  • ROI optimization
  • -
- 5 days - £12,500 per person -
-
-
- - -
- -
-

5-Bedroom Luxury Facility

-

Our exclusive Lake District property offers the perfect blend of comfort and focus for executive training.

- -
-
-

Executive Suites

-

5 premium private bedrooms with dedicated workspace areas

-
-
-

Professional Kitchen

-

Full catering facilities with private chef options

-
-
-

Wine Cellar & Bar

-

Premium selection for evening networking

-
-
-

Meeting Spaces

-

Multiple areas for breakout sessions and informal discussions

-
-
- -

Accommodation Packages

-
    -
  • Standard Room: £350 per night
  • -
  • Premium Suite: £450 per night
  • -
  • Master Suite: £650 per night
  • -
  • Full Facility Exclusive: £2,000 per night
  • -
- -

All packages include:

-
    -
  • Continental breakfast
  • -
  • High-speed WiFi throughout
  • -
  • Access to all facility amenities
  • -
  • Concierge services
  • -
  • Lake District activity coordination
  • -
-
-
- - -
- -
-

Sellafield Nuclear Site

-
-
-
11,000
-
Direct Employees
-
-
-
40,000
-
Supply Chain Workers
-
-
-
£3.5B
-
Annual Spending
-
-
- -

UK Nuclear Industry Growth

-
    -
  • Workforce expanding from 87,000 to 211,500 by 2050
  • -
  • £16.1B industry value growing rapidly
  • -
  • Major skills gap in AI and digital technologies
  • -
  • Government mandate for innovation and efficiency
  • -
  • Critical need for next-generation leadership
  • -
- -

Competitive Advantage

-

DreamLab AI is uniquely positioned as:

-
    -
  • The ONLY AI/VR training facility near Sellafield
  • -
  • First-mover in nuclear executive education
  • -
  • Premium positioning commands premium pricing
  • -
  • Dual revenue model ensures profitability
  • -
  • Scalable to other nuclear sites nationally
  • -
-
-
- - -
- -
-

Financial Projections

-
-
-
68.5%
-
Internal Rate of Return
-
-
-
£3M
-
Year 3 Revenue
-
-
-
42%
-
EBITDA Margin
-
-
- -

Growth Strategy

-
    -
  • Phase 1: Establish Sellafield executive training
  • -
  • Phase 2: Expand to supply chain companies
  • -
  • Phase 3: National nuclear site network
  • -
  • Phase 4: International nuclear markets
  • -
  • Phase 5: Adjacent industry expansion
  • -
- -

Investment Terms

-
    -
  • Seeking: £1.5M Series A
  • -
  • Valuation: £4.5M pre-money
  • -
  • Use of Funds: Facility upgrade, technology development, marketing
  • -
  • ROI Target: 5x in 5 years
  • -
  • Exit Strategy: Acquisition by training or nuclear services company
  • -
- -

Risk Mitigation

-
    -
  • Dual revenue streams reduce dependency
  • -
  • Government-backed nuclear expansion
  • -
  • Premium positioning protects margins
  • -
  • Exclusive location creates barriers to entry
  • -
  • Strong IP in training methodologies
  • -
-
-
- - -
- -
-

- Transform your nuclear leadership with AI-powered training at DreamLab AI. - Contact us to discuss your executive development needs. -

- -
-

Contact Information

-
- Email: info@dreamlab-ai.co.uk -
-
- Phone: +44 (0) 1946 123456 -
-
- Website: dreamlab-ai.co.uk -
-
- Address: Eskdale Green, Lake District, Cumbria, UK -
-
- -
-

Ready to Lead the Nuclear Future?

-

Book your executive training experience today.

-
-

dreamlab-ai.co.uk/book

-
-
- -
-

© 2024 DreamLab AI Consulting Ltd. All rights reserved.

-

Registered in England. Company No. 12345678

-
-
-
- - \ No newline at end of file diff --git a/brochure/templates/brochure-template.html b/brochure/templates/brochure-template.html deleted file mode 100644 index 56922bc..0000000 --- a/brochure/templates/brochure-template.html +++ /dev/null @@ -1,272 +0,0 @@ - - - - - - DreamLab AI - Brochure - - - - -
- -

{{companyName}}

-

{{tagline}}

-
-

{{date}}

-
-
- - -
-

Welcome to {{companyName}}

-
-

{{introduction}}

- -

Our Vision

-

{{vision}}

- -

Our Mission

-

{{mission}}

-
-
- - -
-

Our Services

-
-

{{servicesIntro}}

- -
- {{services}} -
-
-
- - -
-

Why Choose {{companyName}}?

-
- {{whyChooseUs}} -
- -

Our Process

-
- {{process}} -
-
- - -
-

Get In Touch

-
-

{{contactIntro}}

- -
-

Contact Information

-
-
-

Address

-

{{address}}

-
-
-

Email

-

{{email}}

-
-
-

Phone

-

{{phone}}

-
-
-

Website

-

{{website}}

-
-
-
-
- -
-

{{closingStatement}}

-
-
- - \ No newline at end of file diff --git a/brochure/templates/cover-mockup.html b/brochure/templates/cover-mockup.html deleted file mode 100644 index 8020693..0000000 --- a/brochure/templates/cover-mockup.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - DreamLab AI Brochure - Cover - - - -
-
- -
-
- -

DREAMLAB AI CONSULTING

- -

Where Creative Vision Meets Engineering Precision

- -
-

- Transform Your Skills at the UK's Premier - Technology Training Facility -

-
-
- - -
- - \ No newline at end of file diff --git a/brochure/templates/dreamlab-brochure.html b/brochure/templates/dreamlab-brochure.html deleted file mode 100644 index 7d6340c..0000000 --- a/brochure/templates/dreamlab-brochure.html +++ /dev/null @@ -1,746 +0,0 @@ - - - - - - DreamLab AI - Professional Brochure - - - - -
-
- -

DREAMLAB AI

-

Where AI Innovation Meets Sustainable Living

-
-

Transform Nuclear Industry Leadership

-

Through Immersive AI & VR Training

-
-
- -
- - -
- -
-
- Executive Summary - 3 -
-
- About DreamLab AI - 4 -
-
- Our Technology - 5 -
-
- Training Programs - 6 -
-
- Premium Accommodation - 7 -
-
- Market Opportunity - 8 -
-
- Investment Highlights - 9 -
-
- Contact Information - 10 -
-
-
- - -
- -
-

- DreamLab AI is the UK's premier executive training facility, strategically positioned 30 minutes from Sellafield Nuclear Site in the stunning Lake District. We transform nuclear industry leaders through cutting-edge AI and VR immersive training experiences. -

- -
-
-
68.5%
-
IRR
-
-
-
£3M
-
Annual Revenue Target
-
-
-
51,000+
-
Target Market
-
-
- -

Strategic Advantages

-
    -
  • Only executive training facility near Sellafield (11,000 employees)
  • -
  • Unique nuclear + AI/VR specialization
  • -
  • Premium Lake District location near Scafell Pike
  • -
  • Dual revenue streams: training + luxury accommodation
  • -
  • First-mover advantage in £16.1B UK nuclear market
  • -
- -

Business Model

-

We offer intensive 2-5 day executive training programs combining:

-
    -
  • AI-powered nuclear simulations
  • -
  • VR safety and operations training
  • -
  • Leadership transformation workshops
  • -
  • 5-bedroom luxury accommodation (£350-650/night)
  • -
-
-
- - -
- -
-

Our Mission

-

Transform nuclear industry leadership through immersive AI and VR training, creating safer, more efficient operations while advancing the UK's clean energy future.

- -

Strategic Location

-
-
-

Eskdale Green, Lake District

-

Premium location combining natural beauty with proximity to Sellafield Nuclear Site

-
-
-

30 Minutes from Sellafield

-

Ideal for executive training without disrupting operations

-
-
-

Near Scafell Pike

-

Inspiring setting for leadership transformation

-
-
-

Excellent Transport Links

-

Easy access from Manchester, London, and Scotland

-
-
- -

Our Team

-

Led by industry veterans with combined expertise in:

-
    -
  • Nuclear operations and safety
  • -
  • AI and machine learning
  • -
  • Virtual reality training systems
  • -
  • Executive education and development
  • -
  • Hospitality and luxury experiences
  • -
-
-
- - -
- -
-

State-of-the-Art Training Facility

- -
-
-

6m × 2.5m LED Virtual Production Screen

-

Cinema-quality immersive environments for realistic nuclear facility simulations

-
-
-

24-Speaker Spatial Audio System

-

360-degree sound for enhanced realism and situational awareness training

-
-
-

Motion Control Robotics

-

Precise equipment simulation for hands-on operational training

-
-
-

6kW Solar Power System

-

Sustainable operations demonstrating clean energy commitment

-
-
- -

AI-Powered Training Platform

-
    -
  • Custom nuclear scenario simulations
  • -
  • Real-time performance analytics
  • -
  • Personalized learning pathways
  • -
  • Multi-user collaborative environments
  • -
  • Integration with Sellafield systems
  • -
- -

VR Training Capabilities

-

Our immersive VR system enables:

-
    -
  • Safe handling of hazardous materials
  • -
  • Emergency response procedures
  • -
  • Complex maintenance operations
  • -
  • Leadership crisis scenarios
  • -
  • Team coordination exercises
  • -
-
-
- - -
- -
-
-

Nuclear AI Leadership

-

Transform your leadership approach with AI-powered decision making and predictive analytics for nuclear operations.

-
    -
  • AI strategy for nuclear facilities
  • -
  • Predictive maintenance systems
  • -
  • Safety optimization algorithms
  • -
  • Data-driven decision frameworks
  • -
- 3 days - £7,500 per person -
- -
-

Advanced VR Operations Training

-

Master complex nuclear procedures in our state-of-the-art VR environment with zero safety risk.

-
    -
  • Hazardous material handling
  • -
  • Emergency response scenarios
  • -
  • Remote operations training
  • -
  • Multi-team coordination
  • -
- 2 days - £5,000 per person -
- -
-

Digital Transformation Bootcamp

-

Comprehensive program covering AI, VR, and digital innovation for nuclear industry leaders.

-
    -
  • Digital strategy development
  • -
  • Innovation lab sessions
  • -
  • Change management
  • -
  • ROI optimization
  • -
- 5 days - £12,500 per person -
-
-
- - -
- -
-

5-Bedroom Luxury Facility

-

Our exclusive Lake District property offers the perfect blend of comfort and focus for executive training.

- -
-
-

Executive Suites

-

5 premium private bedrooms with dedicated workspace areas

-
-
-

Professional Kitchen

-

Full catering facilities with private chef options

-
-
-

Wine Cellar & Bar

-

Premium selection for evening networking

-
-
-

Meeting Spaces

-

Multiple areas for breakout sessions and informal discussions

-
-
- -

Accommodation Packages

-
    -
  • Standard Room: £350 per night
  • -
  • Premium Suite: £450 per night
  • -
  • Master Suite: £650 per night
  • -
  • Full Facility Exclusive: £2,000 per night
  • -
- -

All packages include:

-
    -
  • Continental breakfast
  • -
  • High-speed WiFi throughout
  • -
  • Access to all facility amenities
  • -
  • Concierge services
  • -
  • Lake District activity coordination
  • -
-
-
- - -
- -
-

Sellafield Nuclear Site

-
-
-
11,000
-
Direct Employees
-
-
-
40,000
-
Supply Chain Workers
-
-
-
£3.5B
-
Annual Spending
-
-
- -

UK Nuclear Industry Growth

-
    -
  • Workforce expanding from 87,000 to 211,500 by 2050
  • -
  • £16.1B industry value growing rapidly
  • -
  • Major skills gap in AI and digital technologies
  • -
  • Government mandate for innovation and efficiency
  • -
  • Critical need for next-generation leadership
  • -
- -

Competitive Advantage

-

DreamLab AI is uniquely positioned as:

-
    -
  • The ONLY AI/VR training facility near Sellafield
  • -
  • First-mover in nuclear executive education
  • -
  • Premium positioning commands premium pricing
  • -
  • Dual revenue model ensures profitability
  • -
  • Scalable to other nuclear sites nationally
  • -
-
-
- - -
- -
-

Financial Projections

-
-
-
68.5%
-
Internal Rate of Return
-
-
-
£3M
-
Year 3 Revenue
-
-
-
42%
-
EBITDA Margin
-
-
- -

Growth Strategy

-
    -
  • Phase 1: Establish Sellafield executive training
  • -
  • Phase 2: Expand to supply chain companies
  • -
  • Phase 3: National nuclear site network
  • -
  • Phase 4: International nuclear markets
  • -
  • Phase 5: Adjacent industry expansion
  • -
- -

Investment Terms

-
    -
  • Seeking: £1.5M Series A
  • -
  • Valuation: £4.5M pre-money
  • -
  • Use of Funds: Facility upgrade, technology development, marketing
  • -
  • ROI Target: 5x in 5 years
  • -
  • Exit Strategy: Acquisition by training or nuclear services company
  • -
- -

Risk Mitigation

-
    -
  • Dual revenue streams reduce dependency
  • -
  • Government-backed nuclear expansion
  • -
  • Premium positioning protects margins
  • -
  • Exclusive location creates barriers to entry
  • -
  • Strong IP in training methodologies
  • -
-
-
- - -
- -
-

- Transform your nuclear leadership with AI-powered training at DreamLab AI. - Contact us to discuss your executive development needs. -

- -
-

Contact Information

-
- Email: info@dreamlab-ai.co.uk -
-
- Phone: +44 (0) 1946 123456 -
-
- Website: dreamlab-ai.co.uk -
-
- Address: Eskdale Green, Lake District, Cumbria, UK -
-
- -
-

Ready to Lead the Nuclear Future?

-

Book your executive training experience today.

-
-

dreamlab-ai.co.uk/book

-
-
- -
-

© 2024 DreamLab AI Consulting Ltd. All rights reserved.

-

Registered in England. Company No. 12345678

-
-
-
- - \ No newline at end of file diff --git a/brochure/templates/program-page-mockup.html b/brochure/templates/program-page-mockup.html deleted file mode 100644 index d9fa069..0000000 --- a/brochure/templates/program-page-mockup.html +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - DreamLab AI - Training Program Page - - - -
- - -
- Master LED volume workflows and real-time visualization for engineering simulation data. - This intensive program bridges the gap between Hollywood virtual production techniques - and engineering visualization needs. -
- -
-

Course Modules

-
    -
  • -
    1
    -
    -

    LED Volume Operations & ICVFX

    -

    Hands-on experience with our 6m × 2.5m LED volume

    -
    -
  • -
  • -
    2
    -
    -

    Real-time Rendering for CAE/CFD Data

    -

    Transform engineering data into compelling visualizations

    -
    -
  • -
  • -
    3
    -
    -

    Unreal Engine for Engineering Viz

    -

    Advanced techniques for technical visualization

    -
    -
  • -
  • -
    4
    -
    -

    Motion Control & Camera Tracking

    -

    Professional camera systems and tracking workflows

    -
    -
  • -
  • -
    5
    -
    -

    Virtual Production Pipeline Integration

    -

    End-to-end workflow implementation

    -
    -
  • -
-
- -
-
-

You'll Master

-
    -
  • LED volume operation
  • -
  • Real-time compositing
  • -
  • Camera tracking systems
  • -
  • Color pipeline management
  • -
-
-
-

You'll Create

-
    -
  • Virtual production shots
  • -
  • Engineering visualizations
  • -
  • Interactive presentations
  • -
  • Digital twin demos
  • -
-
-
- - -
- - \ No newline at end of file diff --git a/brochure/test-generation.js b/brochure/test-generation.js deleted file mode 100644 index 3af55ba..0000000 --- a/brochure/test-generation.js +++ /dev/null @@ -1,71 +0,0 @@ -import { generateBrochure } from './generate-pdf.js'; - -// Test data for brochure generation -const testData = { - companyName: 'DreamLab AI', - tagline: 'Transforming Ideas into Digital Reality', - date: new Date().toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - }), - introduction: `DreamLab AI is a cutting-edge AI service laboratory that bridges the gap between innovative ideas and practical solutions. We specialize in crafting custom AI-powered applications, automation tools, and intelligent systems that transform how businesses operate in the digital age.`, - vision: `To be the leading partner for businesses seeking to harness the transformative power of artificial intelligence, making advanced AI technology accessible and practical for organizations of all sizes.`, - mission: `We empower businesses with custom AI solutions that drive innovation, efficiency, and growth. Through collaborative partnerships, we turn complex challenges into elegant, intelligent solutions that deliver real-world results.`, - servicesIntro: `Our comprehensive suite of AI services is designed to meet the diverse needs of modern businesses:`, - services: ` -
-

AI Consulting & Strategy

-

Expert guidance to identify AI opportunities and develop implementation roadmaps tailored to your business goals.

-
-
-

Custom AI Development

-

Bespoke AI solutions built from the ground up to address your specific challenges and requirements.

-
-
-

Machine Learning Models

-

Advanced ML models for prediction, classification, and optimization across various business domains.

-
-
-

Natural Language Processing

-

Intelligent text analysis, chatbots, and language understanding systems for enhanced communication.

-
- `, - whyChooseUs: ` -
    -
  • Expertise: Our team consists of AI researchers, engineers, and domain experts with proven track records.
  • -
  • Custom Solutions: We don't believe in one-size-fits-all. Every solution is tailored to your unique needs.
  • -
  • Ethical AI: We prioritize responsible AI development with transparency, fairness, and privacy at the core.
  • -
  • Partnership Approach: We work as an extension of your team, ensuring knowledge transfer and long-term success.
  • -
  • Cutting-Edge Technology: We stay at the forefront of AI research to bring you the latest innovations.
  • -
- `, - process: ` -
    -
  1. Discovery: We begin by understanding your business, challenges, and objectives.
  2. -
  3. Strategy: Our experts develop a comprehensive AI strategy aligned with your goals.
  4. -
  5. Development: We build and train custom AI models using state-of-the-art techniques.
  6. -
  7. Integration: Seamless integration with your existing systems and workflows.
  8. -
  9. Optimization: Continuous monitoring and improvement to ensure optimal performance.
  10. -
  11. Support: Ongoing support and maintenance to adapt to your evolving needs.
  12. -
- `, - contactIntro: `Ready to transform your business with AI? Let's start a conversation about how DreamLab AI can help you achieve your goals.`, - address: `Innovation Hub, Suite 300
1234 Tech Boulevard
San Francisco, CA 94105`, - email: `hello@dreamlab.ai`, - phone: `+1 (555) 123-4567`, - website: `www.dreamlab.ai`, - closingStatement: `Let's build the future together with AI.` -}; - -// Generate the brochure -console.log('🚀 Starting brochure generation...'); -generateBrochure(testData) - .then((outputPath) => { - console.log('✅ Brochure generated successfully!'); - console.log(`📄 Output file: ${outputPath}`); - }) - .catch((error) => { - console.error('❌ Failed to generate brochure:', error); - process.exit(1); - }); \ No newline at end of file diff --git a/brochure/wireframes.md b/brochure/wireframes.md deleted file mode 100644 index ac01250..0000000 --- a/brochure/wireframes.md +++ /dev/null @@ -1,178 +0,0 @@ -# DreamLab AI Brochure Wireframes - -## Cover Page Wireframe -``` -┌─────────────────────────────────────────┐ -│ │ -│ [Neural Network Visual] │ -│ (Full Bleed) │ -│ │ -│ │ -│ DREAMLAB AI CONSULTING │ -│ (Gradient Text 48pt) │ -│ │ -│ Where Creative Vision Meets │ -│ Engineering Precision │ -│ (Subtitle 18pt) │ -│ │ -│ │ -│ │ -│ ┌─────────────────────────────┐ │ -│ │ Transform Your Skills at │ │ -│ │ the UK's Premier Tech │ │ -│ │ Training Facility │ │ -│ └─────────────────────────────┘ │ -│ │ -│ www.dreamlab.ai | info@dreamlab.ai │ -└─────────────────────────────────────────┘ -``` - -## Table of Contents Wireframe -``` -┌─────────────────────────────────────────┐ -│ │ -│ TABLE OF CONTENTS │ -│ ───────────────── │ -│ │ -│ 🏢 About DreamLab .................. 3 │ -│ │ -│ 🎓 Training Programs ............... 5 │ -│ • Virtual Production │ -│ • Gaussian Splatting │ -│ • Telepresence & XR │ -│ • AI for Creative │ -│ • Agentic Engineering │ -│ │ -│ 🏠 Residential Experience ......... 11 │ -│ │ -│ 🏭 Industry Applications .......... 13 │ -│ │ -│ ⭐ Success Stories ................ 15 │ -│ │ -│ 📞 Contact & Booking ........ Back Cover│ -│ │ -└─────────────────────────────────────────┘ -``` - -## About DreamLab Spread (Pages 3-4) -``` -┌─────────────────────────┬─────────────────────────┐ -│ ABOUT DREAMLAB AI │ [Facility Image] │ -│ ───────────────── │ │ -│ │ │ -│ Mission Statement │ Key Features: │ -│ (3-4 lines) │ ┌─────────────────┐ │ -│ │ │ 6m LED Volume │ │ -│ Vision │ └─────────────────┘ │ -│ (2-3 lines) │ ┌─────────────────┐ │ -│ │ │ Motion Capture │ │ -│ ┌──────────────────┐ │ └─────────────────┘ │ -│ │ "Transform your │ │ ┌─────────────────┐ │ -│ │ creative and │ │ │ Gaussian Lab │ │ -│ │ technical │ │ └─────────────────┘ │ -│ │ capabilities" │ │ │ -│ └──────────────────┘ │ Location: │ -│ │ [Cubria Map Icon] │ -│ Industry Experience │ Cardiff Area │ -│ • Film & VFX │ │ -│ • Engineering Viz │ Team: 20+ experts │ -│ • Game Development │ Founded: 2024 │ -└─────────────────────────┴─────────────────────────┘ -``` - -## Training Program Template -``` -┌─────────────────────────────────────────┐ -│ │ -│ [Program Icon/Visual] │ -│ │ -│ PROGRAM TITLE │ -│ ───────────────────────────────── │ -│ │ -│ Duration: X Days | Price: £X,XXX │ -│ Capacity: X-X Participants │ -│ │ -│ Program Description │ -│ (2-3 lines explaining the program) │ -│ │ -│ Key Modules: │ -│ ✓ Module 1 with brief description │ -│ ✓ Module 2 with brief description │ -│ ✓ Module 3 with brief description │ -│ ✓ Module 4 with brief description │ -│ ✓ Module 5 with brief description │ -│ │ -│ What You'll Learn: │ -│ • Practical skill 1 │ -│ • Practical skill 2 │ -│ • Practical skill 3 │ -│ │ -│ [Book This Program] button │ -└─────────────────────────────────────────┘ -``` - -## Back Cover Design -``` -┌─────────────────────────────────────────┐ -│ │ -│ TRANSFORM YOUR SKILLS TODAY │ -│ ─────────────────────────── │ -│ │ -│ Ready to master the │ -│ technologies of tomorrow? │ -│ │ -│ ┌───────────────┐ │ -│ │ QR Code │ │ -│ │ for booking │ │ -│ └───────────────┘ │ -│ │ -│ 📧 info@dreamlab.ai │ -│ 🌐 www.dreamlab.ai │ -│ 📱 +44 (0) 29 XXXX XXXX │ -│ │ -│ ───────────────────────────── │ -│ │ -│ Follow us: │ -│ LinkedIn | Bluesky | Instagram │ -│ │ -│ DreamLab AI Consulting Ltd. │ -│ Cumbria, United Kingdom │ -│ │ -└─────────────────────────────────────────┘ -``` - -## Visual Style Notes - -### Icons -- Use minimalist line icons throughout -- Consistent stroke width (2px) -- Tech-focused iconography - -### Images -- High-contrast technical imagery -- Blue/purple color grading -- Futuristic but approachable - -### Layout Grid -``` -│←margin→│←──────── content area ────────→│←margin→│ -│ │ 1 2 3 4 5 6 7 8 9 10 11 12 │ │ -``` -- 12-column grid -- 20mm margins -- 5mm gutter between columns - -### Card Components -``` -┌─────────────────────┐ -│ [Icon] │ -│ Title │ -│ ─────── │ -│ Description │ -│ text here │ -└─────────────────────┘ -``` -- Rounded corners (8px) -- Subtle shadow -- Dark background -- Light text \ No newline at end of file diff --git a/community-forum-rs/.cargo/config.toml b/community-forum-rs/.cargo/config.toml new file mode 100644 index 0000000..15c152e --- /dev/null +++ b/community-forum-rs/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.wasm32-unknown-unknown] +rustflags = ["-C", "link-arg=--max-memory=268435456"] + +# Note: default target removed to allow native `cargo test` for non-WASM crates. +# Trunk automatically targets wasm32-unknown-unknown for the forum-client binary. +# Use `cargo build -p forum-client --target wasm32-unknown-unknown` for manual WASM builds. diff --git a/community-forum-rs/.gitignore b/community-forum-rs/.gitignore new file mode 100644 index 0000000..e8ec98d --- /dev/null +++ b/community-forum-rs/.gitignore @@ -0,0 +1,2 @@ +target/ +crates/nostr-core/pkg/ diff --git a/community-forum-rs/Cargo.lock b/community-forum-rs/Cargo.lock new file mode 100644 index 0000000..232420a --- /dev/null +++ b/community-forum-rs/Cargo.lock @@ -0,0 +1,4079 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "any_spawner" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41058deaa38c9d9dd933d6d238d825227cffa668e2839b52879f6619c63eee3b" +dependencies = [ + "futures", + "thiserror 2.0.18", + "wasm-bindgen-futures", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-utility" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" +dependencies = [ + "futures-util", + "gloo-timers", + "tokio", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-wsocket" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" +dependencies = [ + "async-utility", + "futures", + "futures-util", + "js-sys", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-tungstenite", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "atomic-destructor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" + +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "auth-worker" +version = "0.1.0" +dependencies = [ + "base64", + "getrandom 0.2.17", + "hex", + "js-sys", + "nostr-core", + "serde", + "serde_json", + "sha2", + "wasm-bindgen", + "worker", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" + +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "js-sys", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "codee" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "comrak" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f690706b5db081dccea6206d7f6d594bb9895599abea9d1a0539f13888781ae8" +dependencies = [ + "caseless", + "entities", + "memchr", + "slug", + "typed-arena", + "unicode_categories", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde_core", + "toml", + "winnow 0.7.15", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "either_of" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f86eef3a7e4b9c2107583dbbbe3d9535c4b800796faf1774b82ba22033da" +dependencies = [ + "paste", + "pin-project-lite", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "forum-client" +version = "0.1.0" +dependencies = [ + "base64", + "comrak", + "gloo", + "hex", + "js-sys", + "k256", + "leptos", + "leptos_meta", + "leptos_router", + "nostr-core", + "nostr-sdk", + "send_wrapper", + "serde", + "serde-wasm-bindgen", + "serde_json", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "zeroize", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net 0.5.0", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom 0.2.17", + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.4.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hydration_context" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35485b3dcbf7e044b8f28c73f04f13e7b509c2466fd10cb2a8a447e38f8a93a" +dependencies = [ + "futures", + "once_cell", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "leptos" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b8731cb00f3f0894058155410b95c8955b17273181d2bc72600ab84edd24f1" +dependencies = [ + "any_spawner", + "cfg-if", + "either_of", + "futures", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "reactive_graph", + "rustc-hash", + "send_wrapper", + "serde", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.18", + "throw_error", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_config" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bae3e0ead5a7a814c8340eef7cb8b6cba364125bd8174b15dc9fe1b3cab7e03" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.18", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89d4eb263bd5a9e7c49f780f17063f15aca56fd638c90b9dfd5f4739152e87d" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80219388501d99b246f43b6e7d08a28f327cdd34ba630a35654d917f3e1788e" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn", + "walkdir", +] + +[[package]] +name = "leptos_macro" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e621f8f5342b9bdc93bb263b839cee7405027a74560425a2dabea9de7952b1fd" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.7.1", + "html-escape", + "itertools 0.14.0", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "448a6387e9e2cccbb756f474a54e36a39557127a3b8e46744b6ef6372b50f575" +dependencies = [ + "futures", + "indexmap", + "leptos", + "once_cell", + "or_poisoned", + "send_wrapper", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4168ead6a9715daba953aa842795cb2ad81b6e011a15745bd3d1baf86f76de95" +dependencies = [ + "any_spawner", + "either_of", + "futures", + "gloo-net 0.6.0", + "js-sys", + "leptos", + "leptos_router_macro", + "once_cell", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "thiserror 2.0.18", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router_macro" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31197af38d209ffc5d9f89715381c415a1570176f8d23455fbe00d148e79640" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "leptos_server" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66985242812ec95e224fb48effe651ba02728beca92c461a9464c811a71aab11" +dependencies = [ + "any_spawner", + "base64", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "serde", + "serde_json", + "server_fn", + "tachys", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "negentropy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" + +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + +[[package]] +name = "nostr" +version = "0.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" +dependencies = [ + "base64", + "bech32", + "bip39", + "bitcoin_hashes", + "cbc", + "chacha20", + "chacha20poly1305", + "getrandom 0.2.17", + "hex", + "instant", + "scrypt", + "secp256k1", + "serde", + "serde_json", + "unicode-normalization", + "url", +] + +[[package]] +name = "nostr-core" +version = "0.1.0" +dependencies = [ + "base64", + "chacha20poly1305", + "criterion", + "getrandom 0.2.17", + "hex", + "hkdf", + "hmac", + "js-sys", + "k256", + "nostr", + "proptest", + "serde", + "serde-wasm-bindgen", + "serde_json", + "sha2", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-test", + "zeroize", +] + +[[package]] +name = "nostr-database" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" +dependencies = [ + "lru", + "nostr", + "tokio", +] + +[[package]] +name = "nostr-gossip" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +dependencies = [ + "nostr", +] + +[[package]] +name = "nostr-relay-pool" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +dependencies = [ + "async-utility", + "async-wsocket", + "atomic-destructor", + "hex", + "lru", + "negentropy", + "nostr", + "nostr-database", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-sdk" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" +dependencies = [ + "async-utility", + "nostr", + "nostr-database", + "nostr-gossip", + "nostr-relay-pool", + "tokio", + "tracing", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "oco_ref" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror 1.0.69", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "pod-worker" +version = "0.1.0" +dependencies = [ + "base64", + "hex", + "js-sys", + "nostr-core", + "serde", + "serde_json", + "sha2", + "worker", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "preview-worker" +version = "0.1.0" +dependencies = [ + "regex", + "serde", + "serde_json", + "worker", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reactive_graph" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a0ccddbc11a648bd09761801dac9e3f246ef7641130987d6120fced22515e6" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "or_poisoned", + "pin-project-lite", + "rustc-hash", + "send_wrapper", + "serde", + "slotmap", + "thiserror 2.0.18", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aadc7c19e3a360bf19cd595d2dc8b58ce67b9240b95a103fbc1317a8ff194237" +dependencies = [ + "guardian", + "itertools 0.14.0", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221095cb028dc51fbc2833743ea8b1a585da1a2af19b440b3528027495bf1f2d" +dependencies = [ + "convert_case 0.7.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "relay-worker" +version = "0.1.0" +dependencies = [ + "base64", + "hex", + "js-sys", + "nostr-core", + "serde", + "serde_json", + "sha2", + "wasm-bindgen", + "worker", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", + "syn_derive", + "thiserror 2.0.18", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d05a9e3fd8d7404985418db38c6617cc793a1a27f398d4fbc9dfe8e41b804e6" +dependencies = [ + "bytes", + "const_format", + "dashmap", + "futures", + "gloo-net 0.6.0", + "http 1.4.0", + "js-sys", + "once_cell", + "pin-project-lite", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.18", + "throw_error", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504b35e883267b3206317b46d02952ed7b8bf0e11b2e209e2eb453b609a5e052" +dependencies = [ + "const_format", + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb8b274f568c94226a8045668554aace8142a59b8bca5414ac5a79627c825568" +dependencies = [ + "server_fn_macro", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tachys" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66c3b70c32844a6f1e2943c72a33ebb777ad6acbeb20d1329d62e3a7806d6ec" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "dyn-clone", + "either_of", + "futures", + "html-escape", + "indexmap", + "itertools 0.14.0", + "js-sys", + "linear-map", + "next_tuple", + "oco_ref", + "once_cell", + "or_poisoned", + "parking_lot", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash", + "send_wrapper", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "throw_error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ef8bf264c6ae02a065a4a16553283f0656bd6266fc1fcb09fd2e6b5e91427b" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.15", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typed-builder" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "worker" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7267f3baa986254a8dace6f6a7c6ab88aef59f00c03aaad6749e048b5faaf6f6" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body", + "js-sys", + "matchit", + "pin-project", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_urlencoded", + "tokio", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", + "worker-macros", + "worker-sys", +] + +[[package]] +name = "worker-macros" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7410081121531ec2fa111ab17b911efc601d7b6d590c0a92b847874ebeff0030" +dependencies = [ + "async-trait", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-macro-support", + "worker-sys", +] + +[[package]] +name = "worker-sys" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4777582bf8a04174a034cb336f3702eb0e5cb444a67fdaa4fd44454ff7e2dd95" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/community-forum-rs/Cargo.toml b/community-forum-rs/Cargo.toml new file mode 100644 index 0000000..5847909 --- /dev/null +++ b/community-forum-rs/Cargo.toml @@ -0,0 +1,67 @@ +[workspace] +resolver = "2" +members = [ + "crates/nostr-core", + "crates/forum-client", + "crates/auth-worker", + "crates/pod-worker", + "crates/preview-worker", + "crates/relay-worker", +] + +[workspace.dependencies] +# Nostr protocol (0.44.x — alpha, pinned to minor) +nostr = "0.44" +nostr-sdk = "0.44" + +# UI framework (0.7.x — stay on 0.7 track, 0.8 is breaking) +leptos = "0.7" +leptos_router = "0.7" +leptos_meta = "0.7" + +# Cloudflare Workers +worker = "0.7" + +# WebAuthn with PRF support +passkey-types = "0.3" + +# Cryptography (NCC audited, pinned to latest patch) +k256 = { version = "0.13.4", features = ["schnorr", "ecdh"] } +chacha20poly1305 = "0.10.1" +hmac = "0.12.1" +hkdf = "0.12.4" +sha2 = "0.10.9" +getrandom = { version = "0.2", features = ["js"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# WASM bindings +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +web-sys = "0.3" +gloo = "0.11" + +# Markdown +comrak = { version = "0.38", default-features = false } + +# Validation +validator = { version = "0.20", features = ["derive"] } + +# Error handling +thiserror = "2" + +# Encoding +hex = "0.4" +base64 = "0.22" +zeroize = { version = "1.8", features = ["derive"] } + +# Testing +proptest = "1" +criterion = "0.5" +wasm-bindgen-test = "0.3" + +# Internal crates +nostr-core = { path = "crates/nostr-core" } diff --git a/community-forum-rs/README.md b/community-forum-rs/README.md new file mode 100644 index 0000000..f003fd7 --- /dev/null +++ b/community-forum-rs/README.md @@ -0,0 +1,88 @@ +# DreamLab Community Forum -- Rust Port + +Rust workspace for the DreamLab community forum, porting the SvelteKit forum and +selected Cloudflare Workers to Rust/WASM for performance, type safety, and shared +protocol logic. + +## Architecture + +Six crates in a Cargo workspace: + +| Crate | Type | Purpose | +|-------|------|---------| +| `nostr-core` | Library | Shared Nostr protocol: NIP-01/44/98, key management, event validation, WASM bridge | +| `preview-worker` | CF Worker | Link preview with SSRF protection, OG/meta parsing, Cache API integration | +| `pod-worker` | CF Worker | Solid pod storage on R2 with WAC ACL enforcement and NIP-98 auth | +| `auth-worker` | CF Worker | WebAuthn register/login (passkey-rs), NIP-98 verification, pod provisioning, cron keep-warm | +| `relay-worker` | CF Worker | NIP-01 WebSocket relay via Durable Objects, NIP-11/16/33 support, whitelist API | +| `forum-client` | Leptos App | Browser client (Leptos 0.7 + Trunk), passkey auth, channel browsing | + +## Crate Dependency Graph + +``` +forum-client + +-- nostr-core + +auth-worker + +-- nostr-core + +relay-worker + +-- nostr-core + +pod-worker + +-- nostr-core + +preview-worker + (standalone -- no internal deps) +``` + +All worker crates depend on the `worker` crate (0.7) for Cloudflare Workers bindings. +`nostr-core` compiles for both native (`x86_64`/`aarch64`) and `wasm32-unknown-unknown`. + +## Build + +```bash +# Build all crates (native) +cargo build + +# Run all workspace tests +cargo test --workspace + +# Build and serve the Leptos client (requires Trunk) +cd crates/forum-client && trunk serve +``` + +### Prerequisites + +- Rust stable (see `rust-toolchain.toml`) +- `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` +- Trunk (for forum-client): `cargo install trunk` +- wrangler (for worker deployment): `npm i -g wrangler` + +## Test Summary + +| Crate | Tests | Coverage | +|-------|-------|----------| +| nostr-core | 73 | NIP-01/44/98, keys, WASM bridge, property tests | +| preview-worker | 35 | SSRF protection, OG parsing, Cache API | +| pod-worker | 22 | R2 CRUD, WAC ACL, NIP-98 auth | +| auth-worker | -- | WebAuthn flows, NIP-98, pod provisioning | +| relay-worker | -- | WebSocket DO relay, NIP-11/16/33 | +| forum-client | -- | In progress (Tranche 4) | +| **Total** | **130** | | + +## Current Status + +- **Tranches 0-3**: COMPLETE +- **Tranche 4**: IN PROGRESS -- Leptos client (Slice A: auth shell, Slice B: channel browse) +- **Tranche 5**: NOT STARTED +- **Lines of Rust**: ~7,800 across 6 crates + +See the full delivery plan: [PRD v2.1](../docs/prd-rust-port-v2.1.md) + +## Scope Note + +The relay-worker was originally out of scope in the PRD but was ported during +Tranche 3. The Durable Object relay shared enough NIP-01/NIP-98 validation with +nostr-core that porting it was lower effort than maintaining a parallel TypeScript +implementation. diff --git a/community-forum-rs/benchmarks/js-vs-wasm/bench.mjs b/community-forum-rs/benchmarks/js-vs-wasm/bench.mjs new file mode 100644 index 0000000..6e34d51 --- /dev/null +++ b/community-forum-rs/benchmarks/js-vs-wasm/bench.mjs @@ -0,0 +1,453 @@ +/** + * DreamLab Crypto Benchmarks: JS vs WASM + * + * Compares pure-JavaScript crypto (noble/hashes, noble/curves, nostr-tools) + * against Rust-compiled WASM (@dreamlab/nostr-core-wasm) for the 7 core + * operations used in the DreamLab community forum. + * + * Usage: node --experimental-wasm-modules bench.mjs + */ + +import { performance } from 'node:perf_hooks'; +import { writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +// ── JS imports ────────────────────────────────────────────────────────────── + +import { sha256 } from '@noble/hashes/sha256'; +import { hkdf } from '@noble/hashes/hkdf'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import { schnorr, secp256k1 } from '@noble/curves/secp256k1'; +import { + generateSecretKey, + getPublicKey, + finalizeEvent, + getEventHash, +} from 'nostr-tools'; +import * as nip44js from 'nostr-tools/nip44'; + +// ── WASM imports ──────────────────────────────────────────────────────────── + +import { + derive_keypair_from_prf, + schnorr_sign, + compute_event_id, + nip44_encrypt as wasm_nip44_encrypt, + nip44_decrypt as wasm_nip44_decrypt, + create_nip98_token as wasm_create_nip98_token, +} from '@dreamlab/nostr-core-wasm'; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function hexToUint8(hex) { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes; +} + +function randomBytes(n) { + const buf = new Uint8Array(n); + for (let i = 0; i < n; i++) buf[i] = (Math.random() * 256) | 0; + return buf; +} + +function randomHex(n) { + return bytesToHex(randomBytes(n)); +} + +/** Compute statistics from an array of durations in ms. */ +function computeStats(durations) { + const sorted = [...durations].sort((a, b) => a - b); + const n = sorted.length; + const sum = sorted.reduce((a, b) => a + b, 0); + const mean = sum / n; + const median = n % 2 === 0 + ? (sorted[n / 2 - 1] + sorted[n / 2]) / 2 + : sorted[Math.floor(n / 2)]; + const p95 = sorted[Math.floor(n * 0.95)]; + const p99 = sorted[Math.floor(n * 0.99)]; + const opsPerSec = 1000 / mean; + return { mean, median, p95, p99, opsPerSec, n }; +} + +/** Run a function `iterations` times and collect per-call durations. */ +function benchmark(fn, iterations) { + // Warmup: 10% of iterations or at least 10 + const warmup = Math.max(10, Math.floor(iterations * 0.1)); + for (let i = 0; i < warmup; i++) fn(); + + const durations = new Array(iterations); + for (let i = 0; i < iterations; i++) { + const start = performance.now(); + fn(); + durations[i] = performance.now() - start; + } + return computeStats(durations); +} + +/** Run an async function `iterations` times sequentially. */ +async function benchmarkAsync(fn, iterations) { + const warmup = Math.max(10, Math.floor(iterations * 0.1)); + for (let i = 0; i < warmup; i++) await fn(); + + const durations = new Array(iterations); + for (let i = 0; i < iterations; i++) { + const start = performance.now(); + await fn(); + durations[i] = performance.now() - start; + } + return computeStats(durations); +} + +function formatMs(ms) { + if (ms < 0.001) return `${(ms * 1_000_000).toFixed(0)} ns`; + if (ms < 1) return `${(ms * 1000).toFixed(1)} us`; + return `${ms.toFixed(3)} ms`; +} + +function formatOps(ops) { + if (ops >= 1_000_000) return `${(ops / 1_000_000).toFixed(2)}M`; + if (ops >= 1_000) return `${(ops / 1_000).toFixed(1)}K`; + return `${ops.toFixed(0)}`; +} + +// ── Pre-generate shared test data ─────────────────────────────────────────── + +const PRF_OUTPUT = randomBytes(32); +const SECRET_KEY = generateSecretKey(); +const PUBLIC_KEY_HEX = getPublicKey(SECRET_KEY); +const PUBLIC_KEY_BYTES = hexToUint8(PUBLIC_KEY_HEX); + +// Second keypair for NIP-44 +const SECRET_KEY_2 = generateSecretKey(); +const PUBLIC_KEY_2_HEX = getPublicKey(SECRET_KEY_2); +const PUBLIC_KEY_2_BYTES = hexToUint8(PUBLIC_KEY_2_HEX); + +// Pre-compute conversation key for JS NIP-44 (used in "cached" variants) +const JS_CONV_KEY = nip44js.v2.utils.getConversationKey(SECRET_KEY, PUBLIC_KEY_2_HEX); +const JS_CONV_KEY_DECRYPT = nip44js.v2.utils.getConversationKey(SECRET_KEY_2, PUBLIC_KEY_HEX); + +const MSG_HASH = sha256(new TextEncoder().encode('benchmark message')); +const TIMESTAMP = Math.floor(Date.now() / 1000); +const TAGS_JSON = JSON.stringify([['p', PUBLIC_KEY_HEX], ['e', randomHex(32)]]); + +const PLAINTEXT_1KB = 'A'.repeat(1024); +const PLAINTEXT_10KB = 'B'.repeat(10240); + +// Pre-encrypt for decrypt benchmarks +const JS_CIPHERTEXT_1KB = nip44js.v2.encrypt(PLAINTEXT_1KB, JS_CONV_KEY); +const JS_CIPHERTEXT_10KB = nip44js.v2.encrypt(PLAINTEXT_10KB, JS_CONV_KEY); +const WASM_CIPHERTEXT_1KB = wasm_nip44_encrypt(SECRET_KEY, PUBLIC_KEY_2_BYTES, PLAINTEXT_1KB); +const WASM_CIPHERTEXT_10KB = wasm_nip44_encrypt(SECRET_KEY, PUBLIC_KEY_2_BYTES, PLAINTEXT_10KB); + +// NIP-98 test data +const NIP98_URL = 'https://api.dreamlab-ai.com/pods/abc123/media/upload'; +const NIP98_METHOD = 'POST'; +const NIP98_BODY = new TextEncoder().encode('{"file":"test.jpg","size":12345}'); + +// ── Benchmark definitions ─────────────────────────────────────────────────── + +const results = []; + +async function runAll() { + console.log('='.repeat(76)); + console.log(' DreamLab Crypto Benchmarks: JS vs WASM'); + console.log(' ' + new Date().toISOString()); + console.log(' Node.js ' + process.version); + console.log('='.repeat(76)); + console.log(); + + // ── 1. HKDF-PRF Key Derivation (1000 iterations) ─────────────────────── + + console.log('[1/7] HKDF-PRF Key Derivation (1000 iterations)'); + + const jsHkdf = await benchmarkAsync(async () => { + // Match passkey.ts: Web Crypto HKDF with SHA-256, empty salt, "nostr-secp256k1-v1" info + // Since Web Crypto is async in Node, we use @noble/hashes HKDF which is the sync equivalent + const derived = hkdf(sha256, PRF_OUTPUT, new Uint8Array(0), 'nostr-secp256k1-v1', 32); + // Derive public key to match full operation + const pk = getPublicKey(derived); + return pk; + }, 1000); + + const wasmHkdf = benchmark(() => { + const kp = derive_keypair_from_prf(PRF_OUTPUT); + return kp.publicKey; + }, 1000); + + const hkdfSpeedup = jsHkdf.mean / wasmHkdf.mean; + results.push({ + name: 'HKDF-PRF Key Derivation', + iterations: 1000, + js: jsHkdf, + wasm: wasmHkdf, + speedup: hkdfSpeedup, + }); + printResult('HKDF-PRF Key Derivation', jsHkdf, wasmHkdf, hkdfSpeedup); + + // ── 2. Schnorr Sign (1000 iterations) ─────────────────────────────────── + + console.log('[2/7] Schnorr Sign (1000 iterations)'); + + const jsSchnorr = benchmark(() => { + schnorr.sign(MSG_HASH, SECRET_KEY); + }, 1000); + + const wasmSchnorr = benchmark(() => { + schnorr_sign(SECRET_KEY, MSG_HASH); + }, 1000); + + const schnorrSpeedup = jsSchnorr.mean / wasmSchnorr.mean; + results.push({ + name: 'Schnorr Sign', + iterations: 1000, + js: jsSchnorr, + wasm: wasmSchnorr, + speedup: schnorrSpeedup, + }); + printResult('Schnorr Sign', jsSchnorr, wasmSchnorr, schnorrSpeedup); + + // ── 3. NIP-44 Encrypt 1KB (500 iterations) ───────────────────────────── + // Full encrypt: ECDH conversation key + ChaCha20-Poly1305 + HMAC + base64 + // JS side includes getConversationKey to match WASM which computes ECDH internally. + + console.log('[3/7] NIP-44 Encrypt 1KB (500 iterations)'); + + const jsEnc1k = benchmark(() => { + const ck = nip44js.v2.utils.getConversationKey(SECRET_KEY, PUBLIC_KEY_2_HEX); + nip44js.v2.encrypt(PLAINTEXT_1KB, ck); + }, 500); + + const wasmEnc1k = benchmark(() => { + wasm_nip44_encrypt(SECRET_KEY, PUBLIC_KEY_2_BYTES, PLAINTEXT_1KB); + }, 500); + + const enc1kSpeedup = jsEnc1k.mean / wasmEnc1k.mean; + results.push({ + name: 'NIP-44 Encrypt 1KB', + iterations: 500, + js: jsEnc1k, + wasm: wasmEnc1k, + speedup: enc1kSpeedup, + }); + printResult('NIP-44 Encrypt 1KB', jsEnc1k, wasmEnc1k, enc1kSpeedup); + + // ── 4. NIP-44 Encrypt 10KB (200 iterations) ──────────────────────────── + + console.log('[4/7] NIP-44 Encrypt 10KB (200 iterations)'); + + const jsEnc10k = benchmark(() => { + const ck = nip44js.v2.utils.getConversationKey(SECRET_KEY, PUBLIC_KEY_2_HEX); + nip44js.v2.encrypt(PLAINTEXT_10KB, ck); + }, 200); + + const wasmEnc10k = benchmark(() => { + wasm_nip44_encrypt(SECRET_KEY, PUBLIC_KEY_2_BYTES, PLAINTEXT_10KB); + }, 200); + + const enc10kSpeedup = jsEnc10k.mean / wasmEnc10k.mean; + results.push({ + name: 'NIP-44 Encrypt 10KB', + iterations: 200, + js: jsEnc10k, + wasm: wasmEnc10k, + speedup: enc10kSpeedup, + }); + printResult('NIP-44 Encrypt 10KB', jsEnc10k, wasmEnc10k, enc10kSpeedup); + + // ── 5. NIP-44 Decrypt 1KB (500 iterations) ───────────────────────────── + // Full decrypt: ECDH conversation key + HMAC verify + ChaCha20-Poly1305 + unpad + // JS side includes getConversationKey to match WASM which computes ECDH internally. + + console.log('[5/7] NIP-44 Decrypt 1KB (500 iterations)'); + + const jsDec1k = benchmark(() => { + const ck = nip44js.v2.utils.getConversationKey(SECRET_KEY_2, PUBLIC_KEY_HEX); + nip44js.v2.decrypt(JS_CIPHERTEXT_1KB, ck); + }, 500); + + const wasmDec1k = benchmark(() => { + wasm_nip44_decrypt(SECRET_KEY_2, PUBLIC_KEY_BYTES, WASM_CIPHERTEXT_1KB); + }, 500); + + const dec1kSpeedup = jsDec1k.mean / wasmDec1k.mean; + results.push({ + name: 'NIP-44 Decrypt 1KB', + iterations: 500, + js: jsDec1k, + wasm: wasmDec1k, + speedup: dec1kSpeedup, + }); + printResult('NIP-44 Decrypt 1KB', jsDec1k, wasmDec1k, dec1kSpeedup); + + // ── 6. Event ID Computation (2000 iterations) ────────────────────────── + + console.log('[6/7] Event ID Computation (2000 iterations)'); + + // JS: SHA-256 of NIP-01 canonical JSON [0, pubkey, created_at, kind, tags, content] + const canonicalPrefix = `[0,"${PUBLIC_KEY_HEX}",${TIMESTAMP},1,${TAGS_JSON},"benchmark event content"]`; + + const jsEventId = benchmark(() => { + const hash = sha256(new TextEncoder().encode(canonicalPrefix)); + bytesToHex(hash); + }, 2000); + + const wasmEventId = benchmark(() => { + compute_event_id(PUBLIC_KEY_HEX, TIMESTAMP, 1, TAGS_JSON, 'benchmark event content'); + }, 2000); + + const eventIdSpeedup = jsEventId.mean / wasmEventId.mean; + results.push({ + name: 'Event ID Computation', + iterations: 2000, + js: jsEventId, + wasm: wasmEventId, + speedup: eventIdSpeedup, + }); + printResult('Event ID Computation', jsEventId, wasmEventId, eventIdSpeedup); + + // ── 7. NIP-98 Token Creation (500 iterations) ────────────────────────── + + console.log('[7/7] NIP-98 Token Creation (500 iterations)'); + + // JS: Full NIP-98 flow (build kind:27235 event, SHA-256 body hash, finalize+sign, base64) + const jsNip98 = benchmark(() => { + const bodyHash = bytesToHex(sha256(NIP98_BODY)); + const event = { + kind: 27235, + tags: [ + ['u', NIP98_URL], + ['method', NIP98_METHOD], + ['payload', bodyHash], + ], + created_at: TIMESTAMP, + content: '', + }; + const signed = finalizeEvent(event, SECRET_KEY); + // Base64 encode + const json = JSON.stringify(signed); + return Buffer.from(json).toString('base64'); + }, 500); + + // WASM: create_nip98_token uses std::time::SystemTime::now() which panics in WASM. + // We benchmark the equivalent operation decomposed: event_id + schnorr_sign + // This gives a fair comparison of the crypto work without the WASM time syscall issue. + let wasmNip98; + let nip98WasmNote = ''; + try { + wasmNip98 = benchmark(() => { + wasm_create_nip98_token(SECRET_KEY, NIP98_URL, NIP98_METHOD, NIP98_BODY); + }, 500); + } catch { + // Expected: WASM create_nip98_token panics because std::time::SystemTime::now() + // is not available in WASM. Measure constituent operations instead. + nip98WasmNote = ' (composite: hash + event_id + sign)'; + wasmNip98 = benchmark(() => { + // 1. SHA-256 body hash (done in WASM internally) + const bodyHash = sha256(NIP98_BODY); + // 2. Compute event ID (the core NIP-01 canonical JSON + SHA-256) + const tagsJson = JSON.stringify([ + ['u', NIP98_URL], + ['method', NIP98_METHOD], + ['payload', bytesToHex(bodyHash)], + ]); + const eventId = compute_event_id(PUBLIC_KEY_HEX, TIMESTAMP, 27235, tagsJson, ''); + // 3. Schnorr sign the event ID + const idBytes = hexToUint8(eventId); + schnorr_sign(SECRET_KEY, idBytes); + }, 500); + } + + const nip98Speedup = jsNip98.mean / wasmNip98.mean; + results.push({ + name: 'NIP-98 Token Creation' + nip98WasmNote, + iterations: 500, + js: jsNip98, + wasm: wasmNip98, + speedup: nip98Speedup, + note: nip98WasmNote ? 'WASM create_nip98_token panics (SystemTime unavailable); measured composite of hash + event_id + sign instead.' : '', + }); + printResult('NIP-98 Token Creation', jsNip98, wasmNip98, nip98Speedup, nip98WasmNote); + + // ── Summary ──────────────────────────────────────────────────────────── + + console.log(); + console.log('='.repeat(76)); + console.log(' SUMMARY'); + console.log('='.repeat(76)); + console.log(); + + const avgSpeedup = results.reduce((s, r) => s + r.speedup, 0) / results.length; + const minSpeedup = Math.min(...results.map(r => r.speedup)); + const maxSpeedup = Math.max(...results.map(r => r.speedup)); + + console.log(` Average speedup: ${avgSpeedup.toFixed(2)}x`); + console.log(` Min speedup: ${minSpeedup.toFixed(2)}x`); + console.log(` Max speedup: ${maxSpeedup.toFixed(2)}x`); + console.log(); + + // Go/No-Go assessment + const cryptoOps = results.filter(r => + r.name.includes('HKDF') || r.name.includes('Schnorr') || r.name.includes('NIP-44') || r.name.includes('Event ID') + ); + const cryptoAvg = cryptoOps.reduce((s, r) => s + r.speedup, 0) / cryptoOps.length; + const allAbove2x = cryptoOps.every(r => r.speedup >= 2.0); + const allAbove3x = cryptoOps.every(r => r.speedup >= 3.0); + + console.log(' Go/No-Go Assessment (PRD criteria):'); + console.log(` Crypto average speedup: ${cryptoAvg.toFixed(2)}x`); + console.log(` All crypto ops >= 2x: ${allAbove2x ? 'YES' : 'NO'}`); + console.log(` All crypto ops >= 3x: ${allAbove3x ? 'YES (preferred)' : 'NO'}`); + + if (allAbove3x) { + console.log(' Verdict: GO -- exceeds preferred 3x threshold'); + } else if (allAbove2x) { + console.log(' Verdict: GO -- meets minimum 2x threshold'); + } else { + console.log(' Verdict: CONDITIONAL -- some ops below 2x minimum; evaluate reliability wins'); + } + + console.log(); + + // Write JSON results for report.html consumption + const __dirname = dirname(fileURLToPath(import.meta.url)); + const jsonPath = join(__dirname, 'results.json'); + writeFileSync(jsonPath, JSON.stringify({ + timestamp: new Date().toISOString(), + node: process.version, + platform: `${process.platform} ${process.arch}`, + results: results.map(r => ({ + name: r.name, + iterations: r.iterations, + js: { mean: r.js.mean, median: r.js.median, p95: r.js.p95, p99: r.js.p99, opsPerSec: r.js.opsPerSec }, + wasm: { mean: r.wasm.mean, median: r.wasm.median, p95: r.wasm.p95, p99: r.wasm.p99, opsPerSec: r.wasm.opsPerSec }, + speedup: r.speedup, + note: r.note || '', + })), + summary: { + avgSpeedup, + minSpeedup, + maxSpeedup, + cryptoAvgSpeedup: cryptoAvg, + allAbove2x, + allAbove3x, + verdict: allAbove3x ? 'GO (preferred)' : allAbove2x ? 'GO (minimum)' : 'CONDITIONAL', + }, + }, null, 2)); + console.log(` Results written to: ${jsonPath}`); +} + +function printResult(name, js, wasm, speedup, note = '') { + const tag = speedup >= 3 ? '\x1b[32m' : speedup >= 2 ? '\x1b[33m' : '\x1b[31m'; + const reset = '\x1b[0m'; + + console.log(` JS mean: ${formatMs(js.mean).padStart(10)} median: ${formatMs(js.median).padStart(10)} p95: ${formatMs(js.p95).padStart(10)} ops/s: ${formatOps(js.opsPerSec).padStart(8)}`); + console.log(` WASM mean: ${formatMs(wasm.mean).padStart(10)} median: ${formatMs(wasm.median).padStart(10)} p95: ${formatMs(wasm.p95).padStart(10)} ops/s: ${formatOps(wasm.opsPerSec).padStart(8)}`); + console.log(` ${tag}Speedup: ${speedup.toFixed(2)}x${reset}${note}`); + console.log(); +} + +await runAll(); diff --git a/community-forum-rs/benchmarks/js-vs-wasm/package-lock.json b/community-forum-rs/benchmarks/js-vs-wasm/package-lock.json new file mode 100644 index 0000000..eb40454 --- /dev/null +++ b/community-forum-rs/benchmarks/js-vs-wasm/package-lock.json @@ -0,0 +1,196 @@ +{ + "name": "dreamlab-crypto-benchmarks", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dreamlab-crypto-benchmarks", + "version": "1.0.0", + "dependencies": { + "@dreamlab/nostr-core-wasm": "file:../../crates/nostr-core/pkg", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "nostr-tools": "^2.7.0" + } + }, + "../../crates/nostr-core/pkg": { + "name": "@dreamlab/nostr-core-wasm", + "version": "0.1.0" + }, + "node_modules/@dreamlab/nostr-core-wasm": { + "resolved": "../../crates/nostr-core/pkg", + "link": true + }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nostr-tools": { + "version": "2.23.3", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.23.3.tgz", + "integrity": "sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "2.1.1", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0", + "@scure/bip32": "2.0.1", + "@scure/bip39": "2.0.1", + "nostr-wasm": "0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/nostr-tools/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nostr-tools/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "license": "MIT" + } + } +} diff --git a/community-forum-rs/benchmarks/js-vs-wasm/package.json b/community-forum-rs/benchmarks/js-vs-wasm/package.json new file mode 100644 index 0000000..0b0e2f2 --- /dev/null +++ b/community-forum-rs/benchmarks/js-vs-wasm/package.json @@ -0,0 +1,17 @@ +{ + "name": "dreamlab-crypto-benchmarks", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "JS vs WASM benchmark comparison for DreamLab Nostr crypto primitives", + "scripts": { + "bench": "node --experimental-wasm-modules bench.mjs", + "bench:html": "open report.html" + }, + "dependencies": { + "@dreamlab/nostr-core-wasm": "file:../../crates/nostr-core/pkg", + "@noble/hashes": "^1.4.0", + "@noble/curves": "^1.4.0", + "nostr-tools": "^2.7.0" + } +} diff --git a/community-forum-rs/benchmarks/js-vs-wasm/report.html b/community-forum-rs/benchmarks/js-vs-wasm/report.html new file mode 100644 index 0000000..b8a5098 --- /dev/null +++ b/community-forum-rs/benchmarks/js-vs-wasm/report.html @@ -0,0 +1,309 @@ + + + + + + DreamLab Crypto Benchmarks: JS vs WASM + + + +

DreamLab Crypto Benchmarks

+

JavaScript (noble/nostr-tools) vs Rust WASM (@dreamlab/nostr-core-wasm)

+ +
Loading results from results.json...
+
+ + + + + diff --git a/community-forum-rs/benchmarks/js-vs-wasm/results.json b/community-forum-rs/benchmarks/js-vs-wasm/results.json new file mode 100644 index 0000000..fa612f0 --- /dev/null +++ b/community-forum-rs/benchmarks/js-vs-wasm/results.json @@ -0,0 +1,156 @@ +{ + "timestamp": "2026-03-08T13:25:30.337Z", + "node": "v23.11.1", + "platform": "linux x64", + "results": [ + { + "name": "HKDF-PRF Key Derivation", + "iterations": 1000, + "js": { + "mean": 0.40337561200000066, + "median": 0.36434250000002066, + "p95": 0.6775260000000003, + "p99": 0.7761229999999841, + "opsPerSec": 2479.0789781311773 + }, + "wasm": { + "mean": 0.5996006479999972, + "median": 0.5921760000000518, + "p95": 0.6005800000000363, + "p99": 0.7547289999999975, + "opsPerSec": 1667.7767166122285 + }, + "speedup": 0.6727404537428093, + "note": "" + }, + { + "name": "Schnorr Sign", + "iterations": 1000, + "js": { + "mean": 3.1427435630000105, + "median": 3.096620499999972, + "p95": 3.417048999999679, + "p99": 3.642877000000226, + "opsPerSec": 318.1933173845774 + }, + "wasm": { + "mean": 0.9249100459999936, + "median": 0.8780424999999923, + "p95": 1.2814309999994293, + "p99": 1.818956000000071, + "opsPerSec": 1081.1862238114418 + }, + "speedup": 3.397891045287697, + "note": "" + }, + { + "name": "NIP-44 Encrypt 1KB", + "iterations": 500, + "js": { + "mean": 4.171434730000004, + "median": 4.092044500000156, + "p95": 4.43981199999962, + "p99": 4.939150000000154, + "opsPerSec": 239.72567347350034 + }, + "wasm": { + "mean": 0.18967216199999348, + "median": 0.1834284999999909, + "p95": 0.2013249999999971, + "p99": 0.35066900000128953, + "opsPerSec": 5272.254976457928 + }, + "speedup": 21.992867514211955, + "note": "" + }, + { + "name": "NIP-44 Encrypt 10KB", + "iterations": 200, + "js": { + "mean": 5.447860019999971, + "median": 5.280790999999226, + "p95": 6.261545999999726, + "p99": 7.161276000000726, + "opsPerSec": 183.5583139671062 + }, + "wasm": { + "mean": 0.3121281399999316, + "median": 0.3041350000012244, + "p95": 0.3142329999991489, + "p99": 0.5817470000001776, + "opsPerSec": 3203.8123829534215 + }, + "speedup": 17.45392139267278, + "note": "" + }, + { + "name": "NIP-44 Decrypt 1KB", + "iterations": 500, + "js": { + "mean": 4.305267775999975, + "median": 4.051661000000422, + "p95": 6.521061999999802, + "p99": 7.434919999999693, + "opsPerSec": 232.2735894790498 + }, + "wasm": { + "mean": 0.2645533720000021, + "median": 0.18806599999970786, + "p95": 0.38928499999929045, + "p99": 0.42245999999977357, + "opsPerSec": 3779.955600036699 + }, + "speedup": 16.27372103954865, + "note": "" + }, + { + "name": "Event ID Computation", + "iterations": 2000, + "js": { + "mean": 0.005434624500002428, + "median": 0.004364999998870189, + "p95": 0.009332999999969616, + "p99": 0.01282300000093528, + "opsPerSec": 184005.3530836497 + }, + "wasm": { + "mean": 0.009134422499996617, + "median": 0.00922299999911047, + "p95": 0.012493000000176835, + "p99": 0.014869000000544474, + "opsPerSec": 109475.99588264835 + }, + "speedup": 0.5949609293860056, + "note": "" + }, + { + "name": "NIP-98 Token Creation (composite: hash + event_id + sign)", + "iterations": 500, + "js": { + "mean": 3.760555538000033, + "median": 3.4732279999998354, + "p95": 6.154351999999562, + "p99": 6.497789000000921, + "opsPerSec": 265.91815754217725 + }, + "wasm": { + "mean": 0.9010594259999998, + "median": 0.8949865000004138, + "p95": 0.912027999998827, + "p99": 1.072479000000385, + "opsPerSec": 1109.8047155882039 + }, + "speedup": 4.173482269303772, + "note": "WASM create_nip98_token panics (SystemTime unavailable); measured composite of hash + event_id + sign instead." + } + ], + "summary": { + "avgSpeedup": 9.222797806307668, + "minSpeedup": 0.5949609293860056, + "maxSpeedup": 21.992867514211955, + "cryptoAvgSpeedup": 10.064350395808317, + "allAbove2x": false, + "allAbove3x": false, + "verdict": "CONDITIONAL" + } +} \ No newline at end of file diff --git a/community-forum-rs/crates/auth-worker/Cargo.toml b/community-forum-rs/crates/auth-worker/Cargo.toml new file mode 100644 index 0000000..40f16ba --- /dev/null +++ b/community-forum-rs/crates/auth-worker/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "auth-worker" +version = "0.1.0" +edition = "2021" +description = "DreamLab auth Worker: WebAuthn + NIP-98 + pod provisioning" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +nostr-core = { workspace = true } +worker = { workspace = true, features = ["d1"] } +serde = { workspace = true } +serde_json = { workspace = true } +base64 = { workspace = true } +getrandom = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +js-sys = { workspace = true } +wasm-bindgen = { workspace = true } diff --git a/community-forum-rs/crates/auth-worker/src/auth.rs b/community-forum-rs/crates/auth-worker/src/auth.rs new file mode 100644 index 0000000..0acd8e6 --- /dev/null +++ b/community-forum-rs/crates/auth-worker/src/auth.rs @@ -0,0 +1,43 @@ +//! NIP-98 verification wrapper for the auth worker. +//! +//! Delegates to `nostr_core::verify_nip98_token_at` for cryptographic +//! verification and adds the worker-specific glue: extracting the +//! `Authorization` header, reading the request body for payload hash +//! verification, and obtaining the current timestamp via `Date.now()`. + +use nostr_core::nip98::{Nip98Error, Nip98Token}; +use sha2::{Digest, Sha256}; +use worker::js_sys; + +/// Verify the NIP-98 `Authorization` header from an incoming request. +/// +/// Returns `Ok(token)` on success or `Err` if verification fails. +/// +/// # Arguments +/// * `auth_header` - The raw `Authorization` header value +/// * `expected_url` - The canonical URL the token should authorize +/// * `expected_method` - The HTTP method the token should authorize +/// * `body` - Optional request body bytes for payload hash verification +pub fn verify_nip98( + auth_header: &str, + expected_url: &str, + expected_method: &str, + body: Option<&[u8]>, +) -> Result { + let now = js_now_secs(); + nostr_core::verify_nip98_token_at(auth_header, expected_url, expected_method, body, now) +} + +/// Compute the SHA-256 hex digest of a byte slice. +#[allow(dead_code)] +pub fn sha256_hex(data: &[u8]) -> String { + hex::encode(Sha256::digest(data)) +} + +/// Get the current Unix timestamp in seconds from the JS runtime. +/// +/// Workers do not have access to `std::time::SystemTime`, so we call +/// `Date.now()` via `js_sys` and convert milliseconds to seconds. +fn js_now_secs() -> u64 { + (js_sys::Date::now() / 1000.0) as u64 +} diff --git a/community-forum-rs/crates/auth-worker/src/lib.rs b/community-forum-rs/crates/auth-worker/src/lib.rs new file mode 100644 index 0000000..9ec87f8 --- /dev/null +++ b/community-forum-rs/crates/auth-worker/src/lib.rs @@ -0,0 +1,205 @@ +//! DreamLab auth-api Worker (Rust) +//! +//! WebAuthn registration/authentication + NIP-98 verification + pod provisioning. +//! Port of `workers/auth-api/index.ts` (510 lines). +//! +//! ## Architecture +//! +//! - `lib.rs` -- Router, CORS, entry point +//! - `webauthn.rs` -- WebAuthn registration + authentication handlers +//! - `pod.rs` -- Pod provisioning and profile retrieval +//! - `auth.rs` -- NIP-98 verification wrapper + +mod auth; +mod pod; +mod webauthn; + +use worker::*; + +/// Build CORS headers from the `EXPECTED_ORIGIN` env var. +fn cors_headers(env: &Env) -> Headers { + let origin = env + .var("EXPECTED_ORIGIN") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "https://dreamlab-ai.com".to_string()); + + let headers = Headers::new(); + headers.set("Access-Control-Allow-Origin", &origin).ok(); + headers + .set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .ok(); + headers + .set("Access-Control-Allow-Headers", "Content-Type, Authorization") + .ok(); + headers.set("Access-Control-Max-Age", "86400").ok(); + headers +} + +/// Create a JSON response with CORS headers. +fn json_response(env: &Env, body: &serde_json::Value, status: u16) -> Result { + let json_str = serde_json::to_string(body).map_err(|e| Error::RustError(e.to_string()))?; + let cors = cors_headers(env); + let resp = Response::ok(json_str)?.with_status(status).with_headers(cors); + resp.headers().set("Content-Type", "application/json").ok(); + Ok(resp) +} + +/// Attach CORS headers to an existing response. +fn with_cors(resp: Response, env: &Env) -> Response { + let cors = cors_headers(env); + let result = resp.with_headers(cors); + result +} + +#[event(fetch)] +async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { + // CORS preflight + if req.method() == Method::Options { + return Ok(Response::empty()?.with_status(204).with_headers(cors_headers(&env))); + } + + let url = req.url()?; + let path = url.path(); + let method = req.method(); + + let result = route(req, &env, &path, &method).await; + + match result { + Ok(resp) => Ok(with_cors(resp, &env)), + Err(e) => { + console_error!("Worker error: {e}"); + // Check if it's a JSON parse error + let msg = e.to_string(); + if msg.contains("JSON") || msg.contains("json") { + json_response(&env, &serde_json::json!({ "error": "Invalid JSON body" }), 400) + } else { + json_response( + &env, + &serde_json::json!({ "error": "Internal server error" }), + 500, + ) + } + } + } +} + +/// Route an incoming request to the appropriate handler. +async fn route(req: Request, env: &Env, path: &str, method: &Method) -> Result { + // Health check + if path == "/health" { + return json_response( + env, + &serde_json::json!({ + "status": "ok", + "service": "auth-api", + "runtime": "workers-rs" + }), + 200, + ); + } + + // WebAuthn Registration -- Generate options + if path == "/auth/register/options" && *method == Method::Post { + return webauthn::register_options(req, env).await; + } + + // WebAuthn Registration -- Verify + if path == "/auth/register/verify" && *method == Method::Post { + return webauthn::register_verify(req, env).await; + } + + // WebAuthn Authentication -- Generate options + if path == "/auth/login/options" && *method == Method::Post { + return webauthn::login_options(req, env).await; + } + + // WebAuthn Authentication -- Verify + if path == "/auth/login/verify" && *method == Method::Post { + return webauthn::login_verify(req, env).await; + } + + // Credential lookup (for discoverable login) + if path == "/auth/lookup" && *method == Method::Post { + return webauthn::credential_lookup(req, env).await; + } + + // NIP-98 protected endpoints + if path.starts_with("/api/") { + let auth_header = match req.headers().get("Authorization").ok().flatten() { + Some(h) => h, + None => { + return json_response( + env, + &serde_json::json!({ "error": "Authorization required" }), + 401, + ) + } + }; + + let expected_origin = env + .var("EXPECTED_ORIGIN") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "https://dreamlab-ai.com".to_string()); + let request_url = format!("{expected_origin}{path}"); + + // Read body for payload hash verification + let body_bytes: Option> = if *method == Method::Post || *method == Method::Put { + // For GET requests on /api/profile, there's no body + None + } else { + None + }; + + let result = auth::verify_nip98( + &auth_header, + &request_url, + method_str(method), + body_bytes.as_deref(), + ); + + match result { + Ok(token) => { + // Route authenticated requests + if path == "/api/profile" && *method == Method::Get { + let cors = cors_headers(env); + return pod::handle_profile(&token.pubkey, env, cors).await; + } + } + Err(_) => { + return json_response( + env, + &serde_json::json!({ "error": "Invalid NIP-98 token" }), + 401, + ) + } + } + } + + json_response(env, &serde_json::json!({ "error": "Not found" }), 404) +} + +/// Map a `worker::Method` enum to its string name. +fn method_str(m: &Method) -> &'static str { + match m { + Method::Get => "GET", + Method::Head => "HEAD", + Method::Post => "POST", + Method::Put => "PUT", + Method::Delete => "DELETE", + Method::Options => "OPTIONS", + Method::Patch => "PATCH", + Method::Connect => "CONNECT", + Method::Trace => "TRACE", + _ => "GET", + } +} + +/// Cron keep-warm: prevents cold starts by pinging D1. +#[event(scheduled)] +async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) -> () { + let db = match env.d1("DB") { + Ok(db) => db, + Err(_) => return, + }; + let _ = db.prepare("SELECT 1").first::(None).await; +} diff --git a/community-forum-rs/crates/auth-worker/src/pod.rs b/community-forum-rs/crates/auth-worker/src/pod.rs new file mode 100644 index 0000000..bf3cf8f --- /dev/null +++ b/community-forum-rs/crates/auth-worker/src/pod.rs @@ -0,0 +1,138 @@ +//! Pod provisioning and profile retrieval. +//! +//! Creates per-user Solid pods in R2 with WAC ACL metadata in KV, +//! and serves profile cards from R2. + +use serde_json::json; +use worker::*; + +/// Pod base URL template. The pubkey is appended as a path segment. +const POD_BASE_URL: &str = "https://pods.dreamlab-ai.com"; + +/// Provision a new Solid pod for the given pubkey. +/// +/// Creates: +/// - ACL document in KV (`acl:{pubkey}`) with owner + public read rules +/// - Profile card in R2 (`pods/{pubkey}/profile/card`) as JSON-LD +/// - Pod metadata in KV (`meta:{pubkey}`) +/// +/// Returns the WebID and pod URL on success. +pub async fn provision_pod(pubkey: &str, env: &Env) -> Result { + let did = format!("did:nostr:{pubkey}"); + + let default_acl = json!({ + "@context": { + "acl": "http://www.w3.org/ns/auth/acl#", + "foaf": "http://xmlns.com/foaf/0.1/" + }, + "@graph": [ + { + "@id": "#owner", + "@type": "acl:Authorization", + "acl:agent": { "@id": did }, + "acl:accessTo": { "@id": "./" }, + "acl:default": { "@id": "./" }, + "acl:mode": [ + { "@id": "acl:Read" }, + { "@id": "acl:Write" }, + { "@id": "acl:Control" } + ] + }, + { + "@id": "#public", + "@type": "acl:Authorization", + "acl:agentClass": { "@id": "foaf:Agent" }, + "acl:accessTo": { "@id": "./profile/" }, + "acl:mode": [{ "@id": "acl:Read" }] + }, + { + "@id": "#media-public", + "@type": "acl:Authorization", + "acl:agentClass": { "@id": "foaf:Agent" }, + "acl:accessTo": { "@id": "./media/public/" }, + "acl:mode": [{ "@id": "acl:Read" }] + } + ] + }); + + let profile_card = json!({ + "@context": { + "foaf": "http://xmlns.com/foaf/0.1/" + }, + "@id": did, + "@type": "foaf:Person" + }); + + let acl_json = + serde_json::to_string(&default_acl).map_err(|e| Error::RustError(e.to_string()))?; + let profile_json = + serde_json::to_string(&profile_card).map_err(|e| Error::RustError(e.to_string()))?; + let now_ms = (js_sys::Date::now()) as u64; + let meta_json = serde_json::to_string(&json!({ + "created": now_ms, + "storageUsed": 0 + })) + .map_err(|e| Error::RustError(e.to_string()))?; + + // Write ACL to KV + let kv = env.kv("POD_META")?; + kv.put(&format!("acl:{pubkey}"), acl_json)? + .execute() + .await?; + + // Write profile card to R2 + let bucket = env.bucket("PODS")?; + let r2_key = format!("pods/{pubkey}/profile/card"); + bucket + .put(&r2_key, profile_json) + .http_metadata(HttpMetadata { + content_type: Some("application/ld+json".to_string()), + ..Default::default() + }) + .execute() + .await?; + + // Write metadata to KV + kv.put(&format!("meta:{pubkey}"), meta_json)? + .execute() + .await?; + + Ok(PodInfo { + web_id: format!("{POD_BASE_URL}/{pubkey}/profile/card#me"), + pod_url: format!("{POD_BASE_URL}/{pubkey}/"), + }) +} + +/// Handle GET /api/profile: return the authenticated user's profile card from R2. +pub async fn handle_profile(pubkey: &str, env: &Env, cors: Headers) -> Result { + let bucket = env.bucket("PODS")?; + let r2_key = format!("pods/{pubkey}/profile/card"); + + let object = match bucket.get(&r2_key).execute().await? { + Some(obj) => obj, + None => { + let body = serde_json::json!({ "error": "Profile not found" }); + let json_str = + serde_json::to_string(&body).map_err(|e| Error::RustError(e.to_string()))?; + let resp = Response::ok(json_str)?.with_status(404).with_headers(cors); + resp.headers().set("Content-Type", "application/json").ok(); + return Ok(resp); + } + }; + + let body = object + .body() + .ok_or_else(|| Error::RustError("R2 object has no body".to_string()))?; + let bytes = body.bytes().await?; + let resp = Response::from_bytes(bytes)?.with_headers(cors); + resp.headers() + .set("Content-Type", "application/ld+json") + .ok(); + Ok(resp) +} + +/// Pod provisioning result. +pub struct PodInfo { + pub web_id: String, + pub pod_url: String, +} diff --git a/community-forum-rs/crates/auth-worker/src/webauthn.rs b/community-forum-rs/crates/auth-worker/src/webauthn.rs new file mode 100644 index 0000000..df5c94e --- /dev/null +++ b/community-forum-rs/crates/auth-worker/src/webauthn.rs @@ -0,0 +1,657 @@ +//! WebAuthn registration and authentication handlers. +//! +//! Implements the server-side WebAuthn ceremony for passkey registration +//! and login, with PRF-derived Nostr keys. Mirrors the TypeScript +//! implementation in `workers/auth-api/index.ts`. + +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use wasm_bindgen::JsValue; +use worker::*; + +use crate::auth; +use crate::pod; + +// --------------------------------------------------------------------------- +// Utility functions +// --------------------------------------------------------------------------- + +/// Encode bytes as unpadded base64url (RFC 4648 section 5). +fn array_to_base64url(bytes: &[u8]) -> String { + URL_SAFE_NO_PAD.encode(bytes) +} + +/// Decode an unpadded base64url string to bytes. +fn base64url_decode(input: &str) -> std::result::Result, base64::DecodeError> { + URL_SAFE_NO_PAD.decode(input) +} + +/// Constant-time comparison of two byte slices. +fn constant_time_equal(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +/// Current time in milliseconds from the JS runtime. +fn js_now_ms() -> u64 { + js_sys::Date::now() as u64 +} + +/// Validate that a string is exactly 64 hex characters (Nostr pubkey). +fn is_valid_pubkey(s: &str) -> bool { + s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()) +} + +/// Convert a u64 to JsValue (as f64 since JS has no u64). +fn js_u64(v: u64) -> JsValue { + JsValue::from_f64(v as f64) +} + +/// Convert an i32 to JsValue. +fn js_i32(v: i32) -> JsValue { + JsValue::from_f64(v as f64) +} + +/// Convert a u32 to JsValue. +fn js_u32(v: u32) -> JsValue { + JsValue::from_f64(v as f64) +} + +/// Convert a string to JsValue. +fn js_str(s: &str) -> JsValue { + JsValue::from_str(s) +} + +// --------------------------------------------------------------------------- +// Request/response types +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +struct RegisterOptionsBody { + #[serde(rename = "displayName")] + display_name: Option, +} + +#[derive(Deserialize)] +struct RegisterVerifyBody { + pubkey: Option, + response: Option, + #[serde(rename = "credentialId")] + credential_id_flat: Option, + #[serde(rename = "publicKey")] + public_key_flat: Option, + #[serde(rename = "prfSalt")] + prf_salt: Option, +} + +#[derive(Deserialize)] +struct CredentialResponse { + id: Option, + response: Option, +} + +#[derive(Deserialize)] +struct InnerAttestationResponse { + #[serde(rename = "attestationObject")] + attestation_object: Option, +} + +#[derive(Deserialize)] +struct LoginOptionsBody { + pubkey: Option, +} + +#[derive(Deserialize)] +struct LoginVerifyBody { + pubkey: Option, + response: Option, +} + +#[derive(Deserialize)] +struct AssertionData { + id: Option, + response: Option, +} + +#[derive(Deserialize)] +struct InnerAssertionResponse { + #[serde(rename = "clientDataJSON")] + client_data_json: Option, + #[serde(rename = "authenticatorData")] + authenticator_data: Option, +} + +#[derive(Deserialize)] +struct ClientData { + #[serde(rename = "type")] + ceremony_type: Option, + challenge: Option, + origin: Option, +} + +#[derive(Deserialize)] +struct CredentialLookupBody { + #[serde(rename = "credentialId")] + credential_id: Option, +} + +// --------------------------------------------------------------------------- +// D1 row types +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +struct ChallengeRow { + challenge: String, +} + +#[derive(Deserialize)] +struct CredentialRow { + credential_id: Option, + prf_salt: Option, +} + +#[derive(Deserialize)] +struct StoredCredential { + credential_id: Option, + #[allow(dead_code)] + public_key: Option, + counter: Option, +} + +#[derive(Deserialize)] +struct CheckRow { + #[allow(dead_code)] + ok: Option, +} + +#[derive(Deserialize)] +struct PubkeyRow { + pubkey: Option, +} + +// --------------------------------------------------------------------------- +// JSON error helper +// --------------------------------------------------------------------------- + +fn json_err(message: &str, status: u16) -> Result { + let body = serde_json::json!({ "error": message }); + let json_str = serde_json::to_string(&body).map_err(|e| Error::RustError(e.to_string()))?; + let resp = Response::ok(json_str)?.with_status(status); + resp.headers().set("Content-Type", "application/json").ok(); + Ok(resp) +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + +/// POST /auth/register/options +/// +/// Generate a WebAuthn PublicKeyCredentialCreationOptions with a +/// server-controlled PRF salt and a random challenge. +pub async fn register_options(mut req: Request, env: &Env) -> Result { + let body: RegisterOptionsBody = req.json().await.unwrap_or(RegisterOptionsBody { + display_name: None, + }); + let display_name = body + .display_name + .filter(|s| !s.trim().is_empty()) + .unwrap_or_else(|| "Nostr User".to_string()); + + // Generate 32-byte challenge + let mut challenge_bytes = [0u8; 32]; + getrandom::getrandom(&mut challenge_bytes) + .map_err(|e| Error::RustError(format!("RNG failed: {e}")))?; + let challenge_b64 = array_to_base64url(&challenge_bytes); + + // Server-controlled PRF salt + let mut prf_salt_bytes = [0u8; 32]; + getrandom::getrandom(&mut prf_salt_bytes) + .map_err(|e| Error::RustError(format!("RNG failed: {e}")))?; + let prf_salt_b64 = array_to_base64url(&prf_salt_bytes); + + // Temporary user ID for the WebAuthn ceremony + let mut temp_user_id = [0u8; 16]; + getrandom::getrandom(&mut temp_user_id) + .map_err(|e| Error::RustError(format!("RNG failed: {e}")))?; + let temp_user_id_b64 = array_to_base64url(&temp_user_id); + + // Clean expired challenges and store the new one + let now_ms = js_now_ms(); + let five_min_ago = now_ms.saturating_sub(5 * 60 * 1000); + + let db = env.d1("DB")?; + let delete_stmt = db + .prepare("DELETE FROM challenges WHERE created_at < ?1") + .bind(&[js_u64(five_min_ago)])?; + let insert_stmt = db + .prepare("INSERT INTO challenges (pubkey, challenge, created_at) VALUES (?1, ?2, ?3)") + .bind(&[ + js_str(&challenge_b64), + js_str(&challenge_b64), + js_u64(now_ms), + ])?; + db.batch(vec![delete_stmt, insert_stmt]).await?; + + let rp_name = env + .var("RP_NAME") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "DreamLab AI".to_string()); + let rp_id = env + .var("RP_ID") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "dreamlab-ai.com".to_string()); + + let response_body = serde_json::json!({ + "options": { + "rp": { "name": rp_name, "id": rp_id }, + "user": { + "id": temp_user_id_b64, + "name": display_name, + "displayName": display_name + }, + "challenge": challenge_b64, + "pubKeyCredParams": [ + { "alg": -7, "type": "public-key" }, + { "alg": -257, "type": "public-key" } + ], + "timeout": 60000, + "authenticatorSelection": { + "authenticatorAttachment": "platform", + "residentKey": "required", + "userVerification": "required" + }, + "excludeCredentials": [] + }, + "prfSalt": prf_salt_b64 + }); + + Ok(Response::from_json(&response_body)?) +} + +/// POST /auth/register/verify +/// +/// Verify a WebAuthn registration response, store the credential in D1, +/// provision a Solid pod, and return the user's DID/WebID/podUrl. +pub async fn register_verify(mut req: Request, env: &Env) -> Result { + let body: RegisterVerifyBody = req + .json() + .await + .map_err(|_| Error::RustError("Invalid JSON body".to_string()))?; + + let pubkey = match &body.pubkey { + Some(pk) if is_valid_pubkey(pk) => pk.to_lowercase(), + _ => return json_err("Invalid pubkey", 400), + }; + + // Verify a non-expired challenge exists + let now_ms = js_now_ms(); + let five_min_ago = now_ms.saturating_sub(5 * 60 * 1000); + + let db = env.d1("DB")?; + let challenge_row = db + .prepare("SELECT challenge FROM challenges WHERE created_at > ?1 LIMIT 1") + .bind(&[js_u64(five_min_ago)])? + .first::(None) + .await?; + + let challenge_row = match challenge_row { + Some(row) => row, + None => return json_err("No pending challenge or challenge expired", 400), + }; + + // Extract credential data -- accept both nested and flat formats + let credential_id = body + .response + .as_ref() + .and_then(|r| r.id.clone()) + .or(body.credential_id_flat.clone()); + + let attestation = body + .response + .as_ref() + .and_then(|r| r.response.as_ref()) + .and_then(|r| r.attestation_object.clone()) + .or(body.public_key_flat.clone()); + + let credential_id = match credential_id { + Some(id) => id, + None => return json_err("Missing credential data", 400), + }; + + let public_key = attestation.unwrap_or_else(|| credential_id.clone()); + let prf_salt_val: JsValue = match &body.prf_salt { + Some(s) => js_str(s), + None => JsValue::NULL, + }; + + // Store credential in D1 + db.prepare( + "INSERT INTO webauthn_credentials (pubkey, credential_id, public_key, counter, prf_salt, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + ) + .bind(&[ + js_str(&pubkey), + js_str(&credential_id), + js_str(&public_key), + js_i32(0), + prf_salt_val, + js_u64(now_ms), + ])? + .run() + .await?; + + // Provision pod + let pod_info = pod::provision_pod(&pubkey, env).await?; + + // Clean up used challenge + db.prepare("DELETE FROM challenges WHERE challenge = ?1") + .bind(&[js_str(&challenge_row.challenge)])? + .run() + .await?; + + let response_body = serde_json::json!({ + "verified": true, + "pubkey": pubkey, + "didNostr": format!("did:nostr:{pubkey}"), + "webId": pod_info.web_id, + "podUrl": pod_info.pod_url + }); + + Ok(Response::from_json(&response_body)?) +} + +/// POST /auth/login/options +/// +/// Generate a WebAuthn PublicKeyCredentialRequestOptions. If a pubkey is +/// provided, include the stored credential ID and PRF salt in the response. +pub async fn login_options(mut req: Request, env: &Env) -> Result { + let body: LoginOptionsBody = req.json().await.unwrap_or(LoginOptionsBody { pubkey: None }); + + // Generate 32-byte challenge + let mut challenge_bytes = [0u8; 32]; + getrandom::getrandom(&mut challenge_bytes) + .map_err(|e| Error::RustError(format!("RNG failed: {e}")))?; + let challenge_b64 = array_to_base64url(&challenge_bytes); + + let mut prf_salt: Option = None; + let mut allow_credentials: Vec = Vec::new(); + + let db = env.d1("DB")?; + + if let Some(ref pubkey) = body.pubkey { + let cred = db + .prepare( + "SELECT credential_id, prf_salt FROM webauthn_credentials WHERE pubkey = ?1 LIMIT 1", + ) + .bind(&[js_str(pubkey)])? + .first::(None) + .await?; + + match cred { + None => { + return json_err( + "No passkey registered for this account. Use private key login or create a new passkey.", + 404, + ); + } + Some(cred) => { + prf_salt = cred.prf_salt; + if let Some(ref cid) = cred.credential_id { + allow_credentials.push(serde_json::json!({ + "id": cid, + "type": "public-key" + })); + } + } + } + } + + // Store challenge (supports discoverable credential flows without pubkey) + let challenge_pubkey = body + .pubkey + .clone() + .unwrap_or_else(|| "__discoverable__".to_string()); + let now_ms = js_now_ms(); + let five_min_ago = now_ms.saturating_sub(5 * 60 * 1000); + + let delete_stmt = db + .prepare("DELETE FROM challenges WHERE created_at < ?1") + .bind(&[js_u64(five_min_ago)])?; + let insert_stmt = db + .prepare("INSERT INTO challenges (pubkey, challenge, created_at) VALUES (?1, ?2, ?3)") + .bind(&[ + js_str(&challenge_pubkey), + js_str(&challenge_b64), + js_u64(now_ms), + ])?; + db.batch(vec![delete_stmt, insert_stmt]).await?; + + let rp_id = env + .var("RP_ID") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "dreamlab-ai.com".to_string()); + + let response_body = serde_json::json!({ + "options": { + "challenge": challenge_b64, + "rpId": rp_id, + "timeout": 60000, + "userVerification": "required", + "allowCredentials": allow_credentials + }, + "prfSalt": prf_salt + }); + + Ok(Response::from_json(&response_body)?) +} + +/// POST /auth/login/verify +/// +/// The most complex handler: verifies NIP-98, looks up the stored credential, +/// validates clientDataJSON and authenticatorData, checks the signature +/// counter, and returns the verified pubkey. +pub async fn login_verify(mut req: Request, env: &Env) -> Result { + // Read raw body bytes -- needed for both JSON parsing and NIP-98 payload hash + let raw_body = req + .bytes() + .await + .map_err(|_| Error::RustError("Failed to read request body".to_string()))?; + + let body: LoginVerifyBody = serde_json::from_slice(&raw_body) + .map_err(|_| Error::RustError("Invalid JSON body".to_string()))?; + + let pubkey = match &body.pubkey { + Some(pk) if is_valid_pubkey(pk) => pk.to_lowercase(), + _ => return json_err("Invalid pubkey", 400), + }; + + // Step 1: Verify NIP-98 Authorization header + let auth_header = match req.headers().get("Authorization").ok().flatten() { + Some(h) => h, + None => return json_err("NIP-98 Authorization required", 401), + }; + + let request_url = req.url().map(|u| u.to_string()).unwrap_or_default(); + + let nip98_result = + match auth::verify_nip98(&auth_header, &request_url, "POST", Some(&raw_body)) { + Ok(token) => token, + Err(_) => return json_err("Invalid NIP-98 token", 401), + }; + + if nip98_result.pubkey != pubkey { + return json_err("NIP-98 pubkey mismatch", 401); + } + + // Step 2: Look up stored credential + let db = env.d1("DB")?; + let cred = db + .prepare( + "SELECT credential_id, public_key, counter FROM webauthn_credentials WHERE pubkey = ?1 LIMIT 1", + ) + .bind(&[js_str(&pubkey)])? + .first::(None) + .await?; + + let cred = match cred { + Some(c) => c, + None => return json_err("No registered credential", 400), + }; + + // Step 3: Extract assertion response and verify credential ID + let assertion_data = match &body.response { + Some(a) => a, + None => return json_err("Missing assertion response", 400), + }; + let inner_response = match &assertion_data.response { + Some(r) => r, + None => return json_err("Missing assertion response", 400), + }; + + if assertion_data.id.as_deref() != cred.credential_id.as_deref() { + return json_err("Credential mismatch", 400); + } + + // Step 4: Verify clientDataJSON + let client_data_b64 = match &inner_response.client_data_json { + Some(s) if !s.is_empty() => s.clone(), + _ => return json_err("Missing clientDataJSON", 400), + }; + + let client_data_bytes = match base64url_decode(&client_data_b64) { + Ok(b) => b, + Err(_) => return json_err("Invalid clientDataJSON", 400), + }; + + let client_data: ClientData = match serde_json::from_slice(&client_data_bytes) { + Ok(cd) => cd, + Err(_) => return json_err("Invalid clientDataJSON", 400), + }; + + if client_data.ceremony_type.as_deref() != Some("webauthn.get") { + return json_err("Invalid ceremony type", 400); + } + + let expected_origin = env + .var("EXPECTED_ORIGIN") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "https://dreamlab-ai.com".to_string()); + + if client_data.origin.as_deref() != Some(&expected_origin) { + return json_err("Origin mismatch", 400); + } + + // Verify challenge was issued by this server and hasn't expired + let now_ms = js_now_ms(); + let five_min_ago = now_ms.saturating_sub(5 * 60 * 1000); + let challenge_str = client_data.challenge.unwrap_or_default(); + + let challenge_check: Option = db + .prepare("SELECT 1 as ok FROM challenges WHERE challenge = ?1 AND created_at > ?2") + .bind(&[js_str(&challenge_str), js_u64(five_min_ago)])? + .first::(None) + .await?; + + if challenge_check.is_none() { + return json_err("Invalid or expired challenge", 400); + } + + // Step 5: Verify authenticatorData + let auth_data_b64 = match &inner_response.authenticator_data { + Some(s) if !s.is_empty() => s.clone(), + _ => return json_err("Missing authenticatorData", 400), + }; + + let auth_data = match base64url_decode(&auth_data_b64) { + Ok(b) => b, + Err(_) => return json_err("Invalid authenticatorData", 400), + }; + + if auth_data.len() < 37 { + return json_err("authenticatorData too short", 400); + } + + // First 32 bytes = SHA-256(rpId) + let rp_id = env + .var("RP_ID") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "dreamlab-ai.com".to_string()); + let rp_id_hash = Sha256::digest(rp_id.as_bytes()); + + if !constant_time_equal(&rp_id_hash, &auth_data[..32]) { + return json_err("RP ID mismatch", 400); + } + + // Byte 32 = flags: bit 0 (UP), bit 2 (UV) + let flags = auth_data[32]; + if flags & 0x01 == 0 { + return json_err("User presence not verified", 400); + } + if flags & 0x04 == 0 { + return json_err("User verification not performed", 400); + } + + // Bytes 33-36 = sign counter (big-endian u32) + let sign_count = + u32::from_be_bytes([auth_data[33], auth_data[34], auth_data[35], auth_data[36]]); + let stored_counter = cred.counter.unwrap_or(0) as u32; + + // signCount 0 means authenticator doesn't support counters -- skip check + if sign_count != 0 && sign_count <= stored_counter { + return json_err("Credential replay detected", 400); + } + + // Step 6: All checks passed -- update counter and consume challenge + let update_stmt = db + .prepare("UPDATE webauthn_credentials SET counter = ?1 WHERE pubkey = ?2") + .bind(&[js_u32(sign_count), js_str(&pubkey)])?; + let delete_stmt = db + .prepare("DELETE FROM challenges WHERE challenge = ?1") + .bind(&[js_str(&challenge_str)])?; + db.batch(vec![update_stmt, delete_stmt]).await?; + + let response_body = serde_json::json!({ + "verified": true, + "pubkey": pubkey, + "didNostr": format!("did:nostr:{pubkey}") + }); + + Ok(Response::from_json(&response_body)?) +} + +/// POST /auth/lookup +/// +/// Look up a pubkey by credential ID (for discoverable credential flows). +pub async fn credential_lookup(mut req: Request, env: &Env) -> Result { + let body: CredentialLookupBody = req + .json() + .await + .map_err(|_| Error::RustError("Invalid JSON body".to_string()))?; + + let credential_id = match &body.credential_id { + Some(id) if !id.is_empty() => id.clone(), + _ => return json_err("Missing credentialId", 400), + }; + + let db = env.d1("DB")?; + let cred = db + .prepare("SELECT pubkey FROM webauthn_credentials WHERE credential_id = ?1 LIMIT 1") + .bind(&[js_str(&credential_id)])? + .first::(None) + .await?; + + match cred { + Some(row) => { + let resp = serde_json::json!({ "pubkey": row.pubkey }); + Ok(Response::from_json(&resp)?) + } + None => json_err("Credential not found", 404), + } +} diff --git a/community-forum-rs/crates/auth-worker/wrangler.toml b/community-forum-rs/crates/auth-worker/wrangler.toml new file mode 100644 index 0000000..a2f1a32 --- /dev/null +++ b/community-forum-rs/crates/auth-worker/wrangler.toml @@ -0,0 +1,28 @@ +name = "dreamlab-auth-api" +main = "build/worker/shim.mjs" +compatibility_date = "2024-09-02" + +[build] +command = "worker-build --release" + +[[d1_databases]] +binding = "DB" +database_name = "dreamlab-auth" +database_id = "" # Set via wrangler or GitHub secret + +[[kv_namespaces]] +binding = "SESSIONS" +id = "" # Set via wrangler or GitHub secret + +[[kv_namespaces]] +binding = "POD_META" +id = "" # Set via wrangler or GitHub secret + +[[r2_buckets]] +binding = "PODS" +bucket_name = "dreamlab-pods" + +[vars] +RP_ID = "dreamlab-ai.com" +RP_NAME = "DreamLab Community" +EXPECTED_ORIGIN = "https://dreamlab-ai.com" diff --git a/community-forum-rs/crates/forum-client/Cargo.toml b/community-forum-rs/crates/forum-client/Cargo.toml new file mode 100644 index 0000000..7c50f06 --- /dev/null +++ b/community-forum-rs/crates/forum-client/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "forum-client" +version = "0.1.0" +edition = "2021" +description = "DreamLab community forum Leptos CSR application" + +[[bin]] +name = "forum-client" +path = "src/main.rs" + +[dependencies] +nostr-core = { workspace = true } +nostr-sdk = { workspace = true } +leptos = { workspace = true } +leptos_router = { workspace = true } +leptos_meta = { workspace = true } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +js-sys = { workspace = true } +web-sys = { workspace = true, features = [ + "WebSocket", + "MessageEvent", + "CloseEvent", + "ErrorEvent", + "BinaryType", + "Window", + "Document", + "HtmlElement", + "HtmlInputElement", + "HtmlCanvasElement", + "CanvasRenderingContext2d", + "Element", + "ScrollBehavior", + "ScrollIntoViewOptions", + "EventTarget", + "Location", + "Navigator", + "CredentialsContainer", + "PublicKeyCredential", + "PublicKeyCredentialCreationOptions", + "PublicKeyCredentialRequestOptions", + "AuthenticatorResponse", + "AuthenticatorAttestationResponse", + "AuthenticatorAssertionResponse", + "CredentialCreationOptions", + "CredentialRequestOptions", + "AuthenticationExtensionsClientInputs", + "AuthenticationExtensionsClientOutputs", + "Storage", + "HtmlDocument", + "Headers", + "RequestInit", + "Request", + "Response", + "PageTransitionEvent", + "DomRect", + "MediaQueryList", +] } +gloo = { workspace = true } +comrak = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +hex = { workspace = true } +base64 = { workspace = true } +zeroize = { workspace = true } +thiserror = { workspace = true } +k256 = { workspace = true } +send_wrapper = "0.6" +serde-wasm-bindgen = "0.6" diff --git a/community-forum-rs/crates/forum-client/Trunk.toml b/community-forum-rs/crates/forum-client/Trunk.toml new file mode 100644 index 0000000..9628b00 --- /dev/null +++ b/community-forum-rs/crates/forum-client/Trunk.toml @@ -0,0 +1,10 @@ +[build] +target = "index.html" +dist = "../../dist" + +[watch] +watch = ["src", "../nostr-core/src"] + +[serve] +address = "127.0.0.1" +port = 8080 diff --git a/community-forum-rs/crates/forum-client/index.html b/community-forum-rs/crates/forum-client/index.html new file mode 100644 index 0000000..e5f8fef --- /dev/null +++ b/community-forum-rs/crates/forum-client/index.html @@ -0,0 +1,79 @@ + + + + + + DreamLab Forum + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

DreamLab

+

Initializing…

+
+ + + + diff --git a/community-forum-rs/crates/forum-client/src/admin/channel_form.rs b/community-forum-rs/crates/forum-client/src/admin/channel_form.rs new file mode 100644 index 0000000..bcc14ed --- /dev/null +++ b/community-forum-rs/crates/forum-client/src/admin/channel_form.rs @@ -0,0 +1,205 @@ +//! Channel creation form component for the admin panel. +//! +//! Provides a form with name, description, and section dropdown. Validates that +//! the name is at least 3 characters before enabling submission. + +use leptos::prelude::*; + +use crate::utils::capitalize; + +/// Predefined channel sections matching the forum's zone/section model. +const SECTIONS: &[&str] = &[ + "general", + "announcements", + "introductions", + "music", + "events", + "tech", + "random", + "support", +]; + +/// Data submitted from the channel creation form. +#[derive(Clone, Debug)] +pub struct ChannelFormData { + pub name: String, + pub description: String, + pub section: String, +} + +/// Channel creation form. Calls `on_submit` with the validated form data. +#[component] +pub fn ChannelForm(on_submit: F) -> impl IntoView +where + F: Fn(ChannelFormData) + 'static, +{ + let name = RwSignal::new(String::new()); + let description = RwSignal::new(String::new()); + let section = RwSignal::new(SECTIONS[0].to_string()); + let validation_error = RwSignal::new(Option::::None); + let is_submitting = RwSignal::new(false); + + let is_valid = Memo::new(move |_| { + let n = name.get(); + n.trim().len() >= 3 + }); + + let on_name_input = move |ev: leptos::ev::Event| { + let target = event_target_value(&ev); + name.set(target); + validation_error.set(None); + }; + + let on_desc_input = move |ev: leptos::ev::Event| { + let target = event_target_value(&ev); + description.set(target); + }; + + let on_section_change = move |ev: leptos::ev::Event| { + let target = event_target_value(&ev); + section.set(target); + }; + + let on_form_submit = move |ev: leptos::ev::SubmitEvent| { + ev.prevent_default(); + + let n = name.get_untracked(); + if n.trim().len() < 3 { + validation_error.set(Some("Channel name must be at least 3 characters".into())); + return; + } + + is_submitting.set(true); + on_submit(ChannelFormData { + name: n.trim().to_string(), + description: description.get_untracked().trim().to_string(), + section: section.get_untracked(), + }); + is_submitting.set(false); + + // Reset form + name.set(String::new()); + description.set(String::new()); + section.set(SECTIONS[0].to_string()); + }; + + view! { +
+

+ {plus_circle_icon()} + "Create Channel" +

+ + // Name input +
+ + +
+ {move || { + validation_error.get().map(|msg| view! { +

{msg}

+ }) + }} + + {move || format!("{}/64", name.get().len())} + +
+
+ + // Description input +
+ + +
+ +
+ + + + {$SECTION_CONFIG[formSection]?.description || ''} + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ +
+ + +
+ + {/if} + + +
+ {#if isSectionLoading && channels.length === 0} +
+ +
+ {:else if channels.length === 0} +
+

No channels yet. Create your first channel above.

+
+ {:else} +
+ {#each channels as channel (channel.id)} +
+
+

{channel.name}

+ {#if channel.description} +

{channel.description}

+ {/if} +
+ + {$SECTION_CONFIG[channel.section]?.icon || '👋'} {$SECTION_CONFIG[channel.section]?.name || 'Guest Area'} + + + {channel.visibility} + + {#if channel.encrypted} + Encrypted + {/if} + {#each channel.cohorts as cohort} + {cohort} + {/each} +
+
+ Created {formatTimestamp(channel.createdAt)} +
+
+
+ {/each} +
+ {/if} +
+ + diff --git a/community-forum/src/lib/components/admin/PendingUserApproval.svelte b/community-forum/src/lib/components/admin/PendingUserApproval.svelte new file mode 100644 index 0000000..a984874 --- /dev/null +++ b/community-forum/src/lib/components/admin/PendingUserApproval.svelte @@ -0,0 +1,337 @@ + + +
+
+
+

+ + + + Pending User Registrations + {#if pendingUserRegistrations.length > 0} + {pendingUserRegistrations.length} + {/if} +

+ +
+ +

+ Assign zone access when approving new users. Click a user to configure their cohort membership. +

+ + {#if isLoading && pendingUserRegistrations.length === 0} +
+ +

Loading user registrations...

+
+ {:else if pendingUserRegistrations.length === 0} +
+ + + +

No pending user registrations

+

New users signing up will appear here for approval

+
+ {:else} +
+ {#each pendingUserRegistrations as registration (registration.id)} + {@const isExpanded = expandedUsers.has(registration.id)} + {@const userCohorts = selectedCohorts[registration.id] || []} + +
+ +
toggleExpanded(registration.id)} + on:keydown={(e) => e.key === 'Enter' && toggleExpanded(registration.id)} + role="button" + tabindex="0" + > +
+
+ +
+ {formatRelativeTime(registration.createdAt)} +
+
+ +
+ {#if userCohorts.length > 0} +
+ {#each userCohorts.slice(0, 3) as cohort} + {cohort} + {/each} + {#if userCohorts.length > 3} + +{userCohorts.length - 3} + {/if} +
+ {/if} + + + +
+
+ + {#if registration.message} +

"{registration.message}"

+ {/if} +
+ + + {#if isExpanded} +
+ +
+ +
+ + + + +
+
+ + + {#if zoneCohorts.length > 0} +
+ +
+ {#each zoneCohorts as cohort (cohort.id)} + {@const zones = getZonesForCohort(cohort.id)} + {@const isSelected = userCohorts.includes(cohort.id)} + + {/each} +
+
+ {/if} + + + {#if adminCohorts.length > 0} +
+ +
+ {#each adminCohorts as cohort (cohort.id)} + {@const isSelected = userCohorts.includes(cohort.id)} + + {/each} +
+
+ {/if} + + +
+
+ {#if userCohorts.length > 0} + Selected: {userCohorts.join(', ')} + {:else} + No cohorts selected - user will have minimal access + {/if} +
+ +
+ + +
+
+
+ {/if} +
+ {/each} +
+ {/if} +
+
diff --git a/community-forum/src/lib/components/admin/QuickActions.svelte b/community-forum/src/lib/components/admin/QuickActions.svelte new file mode 100644 index 0000000..7427a6f --- /dev/null +++ b/community-forum/src/lib/components/admin/QuickActions.svelte @@ -0,0 +1,50 @@ + + + diff --git a/community-forum/src/lib/components/admin/RelaySettings.svelte b/community-forum/src/lib/components/admin/RelaySettings.svelte new file mode 100644 index 0000000..415b2d2 --- /dev/null +++ b/community-forum/src/lib/components/admin/RelaySettings.svelte @@ -0,0 +1,69 @@ + + +
+
+

Relay Settings

+

+ Control whether the app connects only to your private relay or connects to other community servers. +

+ +
+ +
+ +
+
Active Relays:
+
+ {#each getRelayUrls() as relay} + {relay} + {/each} +
+
+ + {#if !isPrivateMode} +
+ + + + Federated mode connects to public relays. Messages will be visible on other community servers. +
+ {/if} +
+
diff --git a/community-forum/src/lib/components/admin/SectionRequests.svelte b/community-forum/src/lib/components/admin/SectionRequests.svelte new file mode 100644 index 0000000..066ac74 --- /dev/null +++ b/community-forum/src/lib/components/admin/SectionRequests.svelte @@ -0,0 +1,130 @@ + + +
+
+
+

+ Pending Section Access Requests + {#if pendingRequests.length > 0} + {pendingRequests.length} + {/if} +

+ +
+ + {#if isSectionLoading && pendingRequests.length === 0} +
+ +

Loading pending requests...

+
+ {:else if pendingRequests.length === 0} +
+ + + +

No pending section access requests

+

New users requesting section access will appear here

+
+ {:else} +
+ + + + + + + + + + + + {#each pendingRequests as request (request.id)} + + + + + + + + {/each} + +
UserSectionMessageRequestedActions
+ + + + {$SECTION_CONFIG[request.section]?.icon || ''} {$SECTION_CONFIG[request.section]?.name || request.section} + + + {#if request.message} + {request.message} + {:else} + No message + {/if} + + {formatRelativeTime(request.requestedAt)} + +
+ + +
+
+
+ {/if} +
+
diff --git a/community-forum/src/lib/components/admin/UserCohortManager.svelte b/community-forum/src/lib/components/admin/UserCohortManager.svelte new file mode 100644 index 0000000..30269ac --- /dev/null +++ b/community-forum/src/lib/components/admin/UserCohortManager.svelte @@ -0,0 +1,261 @@ + + +
+
+

+ + + + Manage User Access +

+ +
+ {userPubkey.slice(0, 8)}...{userPubkey.slice(-8)} + {#if userName} + ({userName}) + {/if} +
+ + +
+ +
+ + +
+ +
+ + + + +
+
+ +
Detailed Cohort Selection
+ + + {#if adminCohorts.length > 0} +
+ +
+ {#each adminCohorts as cohort (cohort.id)} + + {/each} +
+
+ {/if} + + + {#if zoneCohorts.length > 0} +
+ +
+ {#each zoneCohorts as cohort (cohort.id)} + {@const zones = getZonesForCohort(cohort.id)} + + {/each} +
+
+ {/if} + + + {#if otherCohorts.length > 0} +
+ +
+ {#each otherCohorts as cohort (cohort.id)} + + {/each} +
+
+ {/if} + + +
+ + + +
+
Selected Cohorts ({selectedCohorts.length})
+
+ {#if selectedCohorts.length > 0} + {selectedCohorts.join(', ')} + {:else} + No cohorts selected - user will have minimal access + {/if} +
+
+
+ + +
+ + +
+
+
diff --git a/community-forum/src/lib/components/admin/UserList.svelte b/community-forum/src/lib/components/admin/UserList.svelte new file mode 100644 index 0000000..be03ad8 --- /dev/null +++ b/community-forum/src/lib/components/admin/UserList.svelte @@ -0,0 +1,310 @@ + + +
+ +
+
+

User Management

+

+ {filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} + {#if filterCohort || searchQuery || showBannedOnly} + (filtered) + {/if} +

+
+ +
+ +
+
+ + +
+
+
+
+ +
+ + + + + + +
+
+
+ + +
+
+ + + + + + + + + + + + + + {#if $adminStore.loading.users} + + + + {:else if filteredUsers.length === 0} + + + + {:else} + {#each filteredUsers as user (user.pubkey)} + + + + + + + + + + {/each} + {/if} + +
UserCohortsChannelsJoinedLast SeenStatusActions
+ +
+ No users found +
+ + +
+ {#each user.cohorts as cohort} + {cohort} + {/each} + {#if user.cohorts.length === 0} + None + {/if} +
+
+ {user.channels.length} + + {formatTimestamp(user.joinedAt)} + + {#if user.lastSeen} + {formatRelativeTime(user.lastSeen)} + {:else} + Never + {/if} + + {#if user.isBanned} + Banned + {:else} + Active + {/if} + + +
+
+
+ + + + + + + + + + + {#if $adminStore.error} +
+ + + + {$adminStore.error} +
+ {/if} +
diff --git a/community-forum/src/lib/components/admin/UserManagement.svelte b/community-forum/src/lib/components/admin/UserManagement.svelte new file mode 100644 index 0000000..f7f81d3 --- /dev/null +++ b/community-forum/src/lib/components/admin/UserManagement.svelte @@ -0,0 +1,689 @@ + + +
+
+
+
+

+ + + + User Management +

+

+ {total} registered user{total !== 1 ? 's' : ''} +

+
+ + +
+ + {#if error} +
+ + + + {error} + +
+ {/if} + + {#if successMessage} +
+ + + + {successMessage} +
+ {/if} + + +
+
+ +
+ + +
+ + + {#if someSelected} +
+ + {selectedUsers.size} user{selectedUsers.size !== 1 ? 's' : ''} selected + + +
+ + + + + + + + {#if batchProcessing} + + {/if} +
+ {/if} + + +
+ + + + + + + + + + + {#if loading && users.length === 0} + + + + {:else if filteredUsers.length === 0} + + + + {:else} + {#each filteredUsers as user (user.pubkey)} + {@const userZones = getUserZones(user)} + {@const isPending = pendingUpdates.has(user.pubkey)} + {@const isSelected = selectedUsers.has(user.pubkey)} + + + + + + + {/each} + {/if} + +
+ + UserAddedZone Access (multi-select)
+ +
+ No users found +
+ toggleUserSelection(user.pubkey)} + /> + +
+ + {user.displayName || 'Anonymous'} + + + {truncatePubkey(user.pubkey)} + + {#if user.cohorts.length > 0} +
+ {#each user.cohorts.filter(c => !ZONE_COHORTS.some(z => z.id === c)) as cohort} + {cohort} + {/each} +
+ {/if} +
+
+ {formatDate(user.addedAt)} + +
+ {#each ZONE_COHORTS as zone} + {@const isSelected = hasZone(user, zone.id)} + + {/each} + {#if isPending} + + {/if} +
+ {#if userZones.length === 0} +
No zones assigned
+ {/if} +
+
+ + + {#if totalPages > 1} +
+ + + + + Page {currentPage} of {totalPages} + + + + +
+ {/if} + + +
+ +
+
+
+ + +{#if batchZonesModal} + +{/if} diff --git a/community-forum/src/lib/components/admin/UserRegistrations.svelte b/community-forum/src/lib/components/admin/UserRegistrations.svelte new file mode 100644 index 0000000..3555a24 --- /dev/null +++ b/community-forum/src/lib/components/admin/UserRegistrations.svelte @@ -0,0 +1,131 @@ + + +
+
+
+

+ Pending User Registrations + {#if pendingUserRegistrations.length > 0} + {pendingUserRegistrations.length} + {/if} +

+ +
+ + {#if isSectionLoading && pendingUserRegistrations.length === 0} +
+ +

Loading user registrations...

+
+ {:else if pendingUserRegistrations.length === 0} +
+ + + +

No pending user registrations

+

New users signing up will appear here for approval

+
+ {:else} +
+ + + + + + + + + + + {#each pendingUserRegistrations as registration (registration.id)} + + + + + + + {/each} + +
UserMessageRequestedActions
+
+ + {#if registration.displayName} + + Signed up as: {registration.displayName} + + {/if} +
+
+ {#if registration.message} + {registration.message} + {:else} + No message + {/if} + + {formatRelativeTime(registration.createdAt)} + +
+ + +
+
+
+ {/if} +
+
diff --git a/community-forum/src/lib/components/auth/AuthFlow.svelte b/community-forum/src/lib/components/auth/AuthFlow.svelte new file mode 100644 index 0000000..f150b12 --- /dev/null +++ b/community-forum/src/lib/components/auth/AuthFlow.svelte @@ -0,0 +1,97 @@ + + +{#if currentStep === 'signup'} + +{:else if currentStep === 'login'} + { currentStep = 'signup'; }} + /> +{:else if currentStep === 'pending-approval' && currentPublicKey} + +{/if} diff --git a/community-forum/src/lib/components/auth/FastSignup.svelte b/community-forum/src/lib/components/auth/FastSignup.svelte new file mode 100644 index 0000000..6463168 --- /dev/null +++ b/community-forum/src/lib/components/auth/FastSignup.svelte @@ -0,0 +1,130 @@ + + +
+
+
+

Quick Access

+

+ Browse without creating a full account +

+ +
+ + + +
+

Limited Access

+

This creates a temporary read-only account.

+
+
+ +
+

+ + + + What you CAN do: +

+
    +
  • Browse all public content
  • +
  • View posts and discussions
  • +
  • Explore the community
  • +
+
+ +
+

+ + + + What you CANNOT do: +

+
    +
  • Post new content
  • +
  • React to posts
  • +
  • Reply or comment
  • +
  • Follow other users
  • +
+
+ +
+ + + + Complete signup in Profile to get full access +
+ + {#if error} +
+ + + + {error} +
+ {/if} + +
+ +
+ +
OR
+ + +
+
+
diff --git a/community-forum/src/lib/components/auth/Login.svelte b/community-forum/src/lib/components/auth/Login.svelte new file mode 100644 index 0000000..b4210be --- /dev/null +++ b/community-forum/src/lib/components/auth/Login.svelte @@ -0,0 +1,267 @@ + + +
+
+
+

Log In

+ + +
+ +
+ + + {#if hasExtension} +
+ +

+ Use a compatible browser extension (Alby, nos2x, etc.) +

+
+ {/if} + + +
+ + Advanced: sign in with private key + +
+
+ + + + Enter your private key to access your account +
+ +
+ + e.key === 'Enter' && handleNsecRestore()} + /> + +
+ +
+ + {#if rememberMe} +
+ + Your private key will be stored locally. Anyone with access to this browser can use your account. +
+ {/if} +
+ + +
+
+ + + {#if validationError || $authStore.error} +
+ + + + {validationError || $authStore.error} +
+ {/if} + +
OR
+ + +
+
+
diff --git a/community-forum/src/lib/components/auth/MnemonicDisplay.svelte b/community-forum/src/lib/components/auth/MnemonicDisplay.svelte new file mode 100644 index 0000000..0cf0693 --- /dev/null +++ b/community-forum/src/lib/components/auth/MnemonicDisplay.svelte @@ -0,0 +1,239 @@ + + +
+
+
+
+

Your Recovery Phrase

+ +
+ + +
+
+
+ + + +
+

CRITICAL: Your Private Key

+
    +
  • + + This is the ONLY way to recover your account +
  • +
  • + + Anyone with this phrase can access your account +
  • +
  • + + Nostr-BBS University cannot recover lost recovery phrases +
  • +
  • + + Write it on paper and store it in a secure location +
  • +
+ +
+ +
+
+
+
+
+ + {#if !showMnemonic} + +
+ +
+ {:else} + +
+
+
+ + + + Private - Keep Secret +
+ +
+ +
+
+ {#each words as word, i} +
+ {i + 1}. + {word} +
+ {/each} +
+
+ +
+ + + + {#if clipboardWarning} +
+ + + + Clipboard will be cleared in 10 seconds for security +
+ {/if} + + + {#if clipboardCleared} +
+ + + + Clipboard cleared for security +
+ {/if} +
+ +
+ +
+
+ {/if} + + {#if showMnemonic} +
+ +
+ {/if} +
+
+
+ + diff --git a/community-forum/src/lib/components/auth/NicknameSetup.svelte b/community-forum/src/lib/components/auth/NicknameSetup.svelte new file mode 100644 index 0000000..21c004a --- /dev/null +++ b/community-forum/src/lib/components/auth/NicknameSetup.svelte @@ -0,0 +1,151 @@ + + +
+
+
+

Choose Your Nickname

+ +

+ Pick a display name that others will see. You can change this later in your profile settings. +

+ +
+ + + +
+

This is how you'll appear to others

+

Your nickname will be shown instead of your public key throughout the system.

+
+
+ +
+ + + +
+

Nickname required for permissions

+

You must set a nickname to appear on the admin permissions list. Without a nickname, admins cannot grant you additional access to other areas.

+
+
+ +
+ + e.key === 'Enter' && isValidNickname && handleContinue()} + /> + {#if nicknameError} + {nicknameError} + {:else} + 2-50 characters, letters, numbers, and symbols allowed + {/if} +
+ + {#if error} +
+ + + + {error} +
+ {/if} + +
+ +
+ +

+ You can add an avatar and update your profile anytime in Settings. +

+
+
+
diff --git a/community-forum/src/lib/components/auth/NsecBackup.svelte b/community-forum/src/lib/components/auth/NsecBackup.svelte new file mode 100644 index 0000000..c454ea2 --- /dev/null +++ b/community-forum/src/lib/components/auth/NsecBackup.svelte @@ -0,0 +1,112 @@ + + +
+
+
+

Store this backup securely

+ + +
+ + + +
+

One-time backup opportunity

+

Your private key is derived from your passkey. This download is the only copy. If you lose your passkey AND this file, you cannot recover your account.

+
+
+ + +
+

Your public key

+

{npub}

+
+ + +
+ + + {#if hasDownloaded} + + {/if} + + +
+
+
+
diff --git a/community-forum/src/lib/components/auth/PendingApproval.svelte b/community-forum/src/lib/components/auth/PendingApproval.svelte new file mode 100644 index 0000000..4bfa8bb --- /dev/null +++ b/community-forum/src/lib/components/auth/PendingApproval.svelte @@ -0,0 +1,152 @@ + + +
+
+
+
+
+
+ + + +
+
+
+
+ +

+ Pending Admin Approval{loadingText()} +

+ +
+ + + +
+

Your account has been created!

+

An administrator needs to approve your access before you can join. This usually takes a few minutes.

+
+
+ +
+

Your Public Key:

+
+ {npub} +
+ +
+ +
+

+ + Waiting for admin approval +

+

+ + + + Your keys are saved locally +

+

+ + + + Keep your backup key safe +

+
+ +
+ +

+ You can close this page and check back later. Your account will be ready once approved. +

+
+
+
+ + diff --git a/community-forum/src/lib/components/auth/README.md b/community-forum/src/lib/components/auth/README.md new file mode 100644 index 0000000..8d4e50d --- /dev/null +++ b/community-forum/src/lib/components/auth/README.md @@ -0,0 +1,166 @@ +# Authentication Components + +[Back to Main README](../../../../README.md) + +Complete Svelte authentication flow for Fairfield with Tailwind CSS and DaisyUI styling. + +## Components + +### 1. Signup.svelte +Entry point for new users. Generates a Nostr keypair on button click. + +**Features:** +- "Create Account" button +- Loading state during key generation +- Error display +- Link to login for existing users + +**Events:** +- `on:next` - Emitted with `{ publicKey, privateKey }` to proceed to backup +- `on:login` - Emitted when user wants to switch to login + +### 2. NsecBackup.svelte +Forces user to backup their nsec (private key) before continuing. + +**Props:** +- `publicKey: string` - User's public key (hex) +- `privateKey: string` - User's private key (hex) + +**Features:** +- Displays nsec-encoded private key +- Copy button with success feedback +- Download button to save as text file +- "I've backed up my key" checkbox (required) +- Continue button (disabled until backup confirmed) + +**Events:** +- `on:continue` - Emitted when user confirms they've backed up + +### 3. Login.svelte +Restore account using nsec or hex private key. + +**Features:** +- Single password input for private key +- Supports nsec1... format or 64-character hex +- Validation with error feedback +- Enter key to submit + +**Events:** +- `on:success` - Emitted with `{ publicKey, privateKey }` +- `on:signup` - Emitted when user wants to create new account + +### 4. FastSignup.svelte +One-click "quick browse" signup with read-only access. + +**Features:** +- Instantly generates keypair +- Clear warning about read-only limitations +- Option to complete full signup later +- Marks account as 'incomplete' + +**Events:** +- `on:success` - Emitted with `{ publicKey, privateKey }` + +### 5. PendingApproval.svelte +Waiting screen for admin approval. + +**Props:** +- `publicKey: string` - User's public key (hex) + +**Features:** +- Animated loading indicator with pulsing clock +- Rotating spinner border +- npub-encoded public key display +- Copy pubkey button +- Auto-polls whitelist status +- Status checklist + +**Events:** +- `on:approved` - Emitted when user is approved + +### 6. AuthFlow.svelte +Orchestrator component for complete authentication flows. + +**Flow Steps:** +1. **Signup** → NsecBackup → PendingApproval +2. **Login** → PendingApproval + +## Usage + +### Individual Components + +```svelte + + + { + publicKey = e.detail.publicKey; + privateKey = e.detail.privateKey; + // Show NsecBackup next + }} + on:login={() => { + // Switch to login view + }} +/> +``` + +### Complete Flow + +```svelte + + + +``` + +## State Management + +Uses `$lib/stores/auth.ts`: + +```typescript +interface AuthState { + isAuthenticated: boolean; + publicKey: string | null; + privateKey: string | null; + accountStatus: 'incomplete' | 'complete'; + nsecBackedUp: boolean; + isPending: boolean; + error: string | null; +} +``` + +### Account Status + +- **incomplete**: Fast signup, read-only access +- **complete**: Full signup with nsec backup confirmed + +## Dependencies + +Required packages (already in package.json): +- `nostr-tools` - Nostr key operations (secp256k1, nip19) +- `@noble/hashes` - Cryptographic utilities +- `tailwindcss` + `daisyui` - Styling + +## Styling + +All components use: +- DaisyUI component classes (btn, card, alert, etc.) +- Tailwind utility classes for layout +- Mobile-responsive design with breakpoints +- Dark mode compatible (DaisyUI theme system) + +## Security Features + +- Client-side key generation (no server transmission) +- Direct secp256k1 keypair generation (no mnemonic) +- nsec format for Nostr-native key backup +- Forced backup confirmation before account completion +- localStorage for key persistence with secure context validation +- Copy-to-clipboard for secure backup +- Visual warnings about key security diff --git a/community-forum/src/lib/components/auth/Signup.svelte b/community-forum/src/lib/components/auth/Signup.svelte new file mode 100644 index 0000000..f38b40e --- /dev/null +++ b/community-forum/src/lib/components/auth/Signup.svelte @@ -0,0 +1,281 @@ + + + + +
+
+
+

Welcome to {appName}

+ + {#if step === 'idle'} +
+ + + + Your private key is derived from your passkey — no password needed +
+ + {#if prfUnsupported} +
+ + + +
+

Passkey not fully supported

+

+ Your browser or authenticator does not support the secure key feature required for passkey-based accounts. + Try Create account with local key below, or use an existing account on the login page. +

+
+
+ {/if} + + {#if $authStore.error && !prfUnsupported} +
+ + + + {$authStore.error} +
+ {/if} + +
+ + e.key === 'Enter' && handleCreateAccount()} + /> +
+ +
+ +
+ +
OR
+ +
+ +

+ Generates a standard key pair. Your private key will be cached in this browser. +

+
+ +
+ +
+ + {:else if step === 'registering'} +
+ +

Creating your secure account...

+
+ + {:else if step === 'download' || step === 'publishing'} +
+ + + +
+

Save your backup key

+

You'll need it if you lose access to your passkey. This download is only available now.

+
+
+ +
+

Your public key

+

{npub}

+
+ +
+ + + +
+ {/if} +
+
+
diff --git a/community-forum/src/lib/components/auth/SignupGateway.svelte b/community-forum/src/lib/components/auth/SignupGateway.svelte new file mode 100644 index 0000000..3af3d3a --- /dev/null +++ b/community-forum/src/lib/components/auth/SignupGateway.svelte @@ -0,0 +1,72 @@ + + +
+
+
+

Create Your Account

+ +

+ Choose how you'd like to set up your account +

+ +
+ + + + + +
+ +
What's the difference?
+ +
+

Quick Start: Uses a simple password system. Easy to use but less secure if you lose access.

+

Secure Setup: Creates a 12-word recovery phrase you can use to restore your account anywhere.

+
+ + +
+
+
diff --git a/community-forum/src/lib/components/auth/SimpleLogin.svelte b/community-forum/src/lib/components/auth/SimpleLogin.svelte new file mode 100644 index 0000000..cd30b56 --- /dev/null +++ b/community-forum/src/lib/components/auth/SimpleLogin.svelte @@ -0,0 +1,163 @@ + + +
+
+
+

Quick Login

+ +
+ + + +
+

Basic Security Mode

+

For better account protection, consider upgrading to recovery phrase login.

+
+
+ +
+ + e.key === 'Enter' && handleLogin()} + /> + + The password you received when you created your account + +
+ + {#if validationError || $authStore.error} +
+ + + + {validationError || $authStore.error} +
+ {/if} + + +
+ +
+ +
+ +
+ +
More secure options
+ + + +
+

+ Upgrade your security: You can migrate to a more secure login method anytime: +

+
    +
  • 12-word recovery phrase (works on any device)
  • +
  • Browser extension like Alby or nos2x
  • +
+
+
+
+
diff --git a/community-forum/src/lib/components/auth/SimpleSignup.svelte b/community-forum/src/lib/components/auth/SimpleSignup.svelte new file mode 100644 index 0000000..7e25d67 --- /dev/null +++ b/community-forum/src/lib/components/auth/SimpleSignup.svelte @@ -0,0 +1,273 @@ + + +
+
+
+ {#if currentStep === 'nickname'} +

Choose Your Nickname

+ +

+ This is how others will see you in the community. +

+ +
+ + e.key === 'Enter' && isValidNickname && handleNicknameSubmit()} + /> + {#if nicknameError} + {nicknameError} + {:else} + 2-50 characters + {/if} +
+ + {#if error} +
+ + + + {error} +
+ {/if} + +
+ + + +
+ + {:else if currentStep === 'password'} +

Your Password

+ +
+ + + +
+

Important: Save this password!

+

This is the ONLY way to access your account. Store it somewhere safe.

+
+
+ +
+ +
+ + +
+ + This is your unique password. Use it with your nickname "{nickname}" to login. + +
+ +
+

+ To login later:
+ Username: {nickname}
+ Password: your copied password +

+
+ + {#if error} +
+ + + + {error} +
+ {/if} + +
+ + + {#if !hasCopied} +

+ Please copy your password first +

+ {/if} +
+ +
Later upgrade
+ +

+ You can upgrade to a more secure 12-word recovery phrase anytime in Settings. +

+ {/if} +
+
+
diff --git a/community-forum/src/lib/components/auth/index.ts b/community-forum/src/lib/components/auth/index.ts new file mode 100644 index 0000000..6fa873a --- /dev/null +++ b/community-forum/src/lib/components/auth/index.ts @@ -0,0 +1,6 @@ +export { default as Signup } from './Signup.svelte'; +export { default as Login } from './Login.svelte'; +export { default as NsecBackup } from './NsecBackup.svelte'; +export { default as FastSignup } from './FastSignup.svelte'; +export { default as PendingApproval } from './PendingApproval.svelte'; +export { default as AuthFlow } from './AuthFlow.svelte'; diff --git a/community-forum/src/lib/components/calendar/AvailabilityBlockRenderer.svelte b/community-forum/src/lib/components/calendar/AvailabilityBlockRenderer.svelte new file mode 100644 index 0000000..39d9dd0 --- /dev/null +++ b/community-forum/src/lib/components/calendar/AvailabilityBlockRenderer.svelte @@ -0,0 +1,326 @@ + + +{#if blocks.length === 0} + +{:else if variant === 'dot'} + +
+ {#each visibleBlocks.slice(0, maxBlocks) as block} +
+{:else if variant === 'bar'} + +
+
+ {#if showLabel} + + {getStatusMessage(mostRestrictiveType)} + + {/if} +
+{:else if variant === 'full'} + +
+ {#each visibleBlocks as block} + + {/each} +
+{/if} + + diff --git a/community-forum/src/lib/components/calendar/CalendarSheet.svelte b/community-forum/src/lib/components/calendar/CalendarSheet.svelte new file mode 100644 index 0000000..80e2760 --- /dev/null +++ b/community-forum/src/lib/components/calendar/CalendarSheet.svelte @@ -0,0 +1,395 @@ + + +{#if isOpen} + + {#if isExpanded} +
e.key === 'Escape' && handleBackdropClick()} + role="button" + tabindex="0" + aria-label="Close calendar" + /> + {/if} + + +
+ +
+
+
+ + +
e.key === 'Enter' && handleCollapsedClick()} + role="button" + tabindex="0" + > +
+
+ 📅 + + {todayEventCount} {todayEventCount === 1 ? 'event' : 'events'} today + +
+ {#if isCollapsed} + + + + {/if} +
+
+ + + {#if isExpanded} +
+ +
+ +
+ + +
+ + + {#if showFilters} +
+ +
+ Sections +
+ {#each sections as section} + + {/each} +
+
+ + + {#if filters.sections.length > 0 || filters.categories.length > 0 || filters.venueTypes.length > 0} + + {/if} +
+ {/if} +
+ + +
+
+

+ {$calendarStore.selectedDate ? 'Events on ' + formatEventDate($calendarStore.selectedDate.getTime() / 1000) : 'Upcoming Events'} +

+ +
+ + {#if displayedEvents.length === 0} +
+ + + +

No events found

+
+ {:else} +
+ {#each displayedEvents as event (event.id)} +
+ + + {#if selectedEventId === event.id} +
+ {#if event.description} +

{event.description}

+ {/if} + {#if event.end} +

+ Ends: {formatEventTime(event.end)} +

+ {/if} + {#if event.tags && event.tags.length > 0} +
+ {#each event.tags as tag} + {tag} + {/each} +
+ {/if} +
+ {/if} +
+ {/each} +
+ {/if} +
+
+ {/if} +
+{/if} + + diff --git a/community-forum/src/lib/components/calendar/CalendarSidebar.svelte b/community-forum/src/lib/components/calendar/CalendarSidebar.svelte new file mode 100644 index 0000000..d01e49f --- /dev/null +++ b/community-forum/src/lib/components/calendar/CalendarSidebar.svelte @@ -0,0 +1,712 @@ + + + + + diff --git a/community-forum/src/lib/components/calendar/CreateEventModal.svelte b/community-forum/src/lib/components/calendar/CreateEventModal.svelte new file mode 100644 index 0000000..dfb4c36 --- /dev/null +++ b/community-forum/src/lib/components/calendar/CreateEventModal.svelte @@ -0,0 +1,708 @@ + + + + +
+
    +
  • Basic Info
  • +
  • Date & Time
  • +
  • Location
  • +
  • Visibility
  • +
+
+ + + {#if currentStep === 1} +
+
+ + + {#if validationErrors.title} +
+ {validationErrors.title} +
+ {/if} +
+ +
+ + +
+ + Tap send button or Enter + {#if hasDraft && messageText.trim()} + + + + + Draft saved + + {/if} +
+
+ + + + + +
+ + {#if !canSend && $userMemberStatus !== 'pending'} +
+ + You must be a member to send messages in this channel. +
+ {/if} +
+ + diff --git a/community-forum/src/lib/components/chat/MessageItem.svelte b/community-forum/src/lib/components/chat/MessageItem.svelte new file mode 100644 index 0000000..0fdafca --- /dev/null +++ b/community-forum/src/lib/components/chat/MessageItem.svelte @@ -0,0 +1,370 @@ + + +
+ {#if !isCurrentUser} +
+ +
+ {/if} + +
+
+ {#if isCurrentUser} + You + {:else} + + {/if} + {#if $isPinned} +
+ + + +
+ {/if} + + {formatTime(message.createdAt)} + {#if isOwnMessage && message.sendStatus} + + {#if message.sendStatus === 'sending'} + + + + {:else if message.sendStatus === 'sent'} + + + + {:else if message.sendStatus === 'delivered'} + + + + {/if} + + {/if} + +
+ +
+ {#if message.isEncrypted && !isDecrypted} +
+ + + + Encrypted message +
+ {:else} +

+ {/if} + + {#if message.isEncrypted && isDecrypted} +
+ + + + Encrypted +
+ {/if} +
+ + + {#if hasMedia} +
+ {#each mediaUrls as media} + {#if media.type === 'link'} + + {:else} + + {/if} + {/each} +
+ {/if} + +
+ {#if $isAdmin} + + {/if} + + + + {#if canDelete} + + {/if} + + +
+ + showReactionPicker = false} + /> +
+
+ + + +
+
diff --git a/community-forum/src/lib/components/chat/MessageList.svelte b/community-forum/src/lib/components/chat/MessageList.svelte new file mode 100644 index 0000000..74eef32 --- /dev/null +++ b/community-forum/src/lib/components/chat/MessageList.svelte @@ -0,0 +1,182 @@ + + +
+ {#if isLoadingMore} +
+ +
+ {/if} + + {#if messages.length === 0} +
+
+ {#if isFiltering} + + + +

No messages found

+

Try adjusting your search or filters

+ {:else} + + + +

No messages yet

+

Be the first to start the conversation

+ {/if} +
+
+ {:else} + {#if isFiltering} +
+ Showing {messages.length} of {allMessages.length} messages +
+ {/if} + {#each messages as message (message.id)} + + {/each} + + {#if mutedMessageCount > 0} +
+ +
+ {/if} + {/if} + + {#if !autoScroll} + + {/if} +
diff --git a/community-forum/src/lib/components/chat/MuteButton.svelte b/community-forum/src/lib/components/chat/MuteButton.svelte new file mode 100644 index 0000000..027ee66 --- /dev/null +++ b/community-forum/src/lib/components/chat/MuteButton.svelte @@ -0,0 +1,114 @@ + + +{#if compact} + +{:else} + +{/if} + +{#if showConfirmDialog} + +{/if} + + diff --git a/community-forum/src/lib/components/chat/PinnedMessages.svelte b/community-forum/src/lib/components/chat/PinnedMessages.svelte new file mode 100644 index 0000000..12f2adc --- /dev/null +++ b/community-forum/src/lib/components/chat/PinnedMessages.svelte @@ -0,0 +1,151 @@ + + +{#if hasPinnedMessages} +
+
+
+ +
+ + + + + Pinned Messages ({pinnedMessages.length}) + +
+ +
+
+ {#each pinnedMessages as message (message.id)} +
handleScrollTo(message.id)} + on:keydown={(e) => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), handleScrollTo(message.id))} + role="button" + tabindex="0" + aria-label="Jump to pinned message" + > +
+
+ + + +
+ +
+
+ + {getAuthorName(message.authorPubkey)} + + + {formatTime(message.createdAt)} + +
+ +

+ {#if message.isEncrypted && message.decryptedContent} + {truncateContent(message.decryptedContent)} + {:else if !message.isEncrypted} + {truncateContent(message.content)} + {:else} + Encrypted message + {/if} +

+
+ + {#if $isAdmin} + + {/if} +
+
+ {/each} +
+
+
+
+
+{/if} + + diff --git a/community-forum/src/lib/components/chat/QuotedMessage.svelte b/community-forum/src/lib/components/chat/QuotedMessage.svelte new file mode 100644 index 0000000..71838fc --- /dev/null +++ b/community-forum/src/lib/components/chat/QuotedMessage.svelte @@ -0,0 +1,128 @@ + + +
(e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), handleClick())} + role="button" + tabindex="0" + aria-label="Jump to quoted message" +> +
+
+ + + +
+ +
+
+ + {authorDisplayName} + + {#if showTimestamp} + + {formatTime(message.createdAt)} + + {/if} +
+ +
+ {#if compact} +

{truncateContent(displayContent, 150)}

+ {:else} +

{displayContent}

+ {/if} +
+ + {#if message.isEncrypted} +
+ + + + Encrypted +
+ {/if} +
+
+
+ + diff --git a/community-forum/src/lib/components/chat/ReactionBar.svelte b/community-forum/src/lib/components/chat/ReactionBar.svelte new file mode 100644 index 0000000..4fcafc4 --- /dev/null +++ b/community-forum/src/lib/components/chat/ReactionBar.svelte @@ -0,0 +1,96 @@ + + +{#if $reactions.totalCount > 0} +
+ {#each Array.from($reactions.reactions.values()) as reaction} + + {/each} +
+{/if} + + diff --git a/community-forum/src/lib/components/chat/ReactionPicker.svelte b/community-forum/src/lib/components/chat/ReactionPicker.svelte new file mode 100644 index 0000000..69afe11 --- /dev/null +++ b/community-forum/src/lib/components/chat/ReactionPicker.svelte @@ -0,0 +1,99 @@ + + +{#if show} + + + +
+{/if} + + diff --git a/community-forum/src/lib/components/chat/SectionCard.svelte b/community-forum/src/lib/components/chat/SectionCard.svelte new file mode 100644 index 0000000..71cb0ac --- /dev/null +++ b/community-forum/src/lib/components/chat/SectionCard.svelte @@ -0,0 +1,155 @@ + + +
+
+ +
+
{config.icon}
+
+

{config.name}

+

{config.description}

+
+ {#if isPending} +
Pending
+ {:else if isApproved} +
Approved
+ {/if} +
+ + + {#if config?.showStats && stats} +
+
+
{stats.channelCount}
+
Channels
+
+
+
{stats.memberCount}
+
Members
+
+
+
{formatLastActivity(stats.lastActivity)}
+
Last Activity
+
+
+ {/if} + + + {#if config?.access?.requiresApproval && !isApproved} +
+ + + + + {#if isPending} + Your access request is pending approval + {:else} + Request access to join this section + {/if} + +
+ {/if} + + +
+ {#if hasCalendarAccess} + + {/if} + + {#if needsApproval} + + {:else if isApproved} + + {:else if !config?.access?.requiresApproval} + + {/if} +
+
+
diff --git a/community-forum/src/lib/components/chat/index.ts b/community-forum/src/lib/components/chat/index.ts new file mode 100644 index 0000000..28beaac --- /dev/null +++ b/community-forum/src/lib/components/chat/index.ts @@ -0,0 +1,6 @@ +export { default as ChannelList } from './ChannelList.svelte'; +export { default as ChannelHeader } from './ChannelHeader.svelte'; +export { default as MessageList } from './MessageList.svelte'; +export { default as MessageItem } from './MessageItem.svelte'; +export { default as MessageInput } from './MessageInput.svelte'; +export { default as JoinRequestButton } from './JoinRequestButton.svelte'; diff --git a/community-forum/src/lib/components/dm/ConversationList.svelte b/community-forum/src/lib/components/dm/ConversationList.svelte new file mode 100644 index 0000000..aa730b9 --- /dev/null +++ b/community-forum/src/lib/components/dm/ConversationList.svelte @@ -0,0 +1,149 @@ + + +
+ +
+

Messages

+
+ + +
+ {#if $sortedConversations.length === 0} +
+ + + +

No conversations yet

+

Start a new message to begin

+
+ {:else} +
+ {#each $sortedConversations as conversation (conversation.pubkey)} + + {/each} +
+ {/if} +
+
+ + diff --git a/community-forum/src/lib/components/dm/DMView.svelte b/community-forum/src/lib/components/dm/DMView.svelte new file mode 100644 index 0000000..d513663 --- /dev/null +++ b/community-forum/src/lib/components/dm/DMView.svelte @@ -0,0 +1,412 @@ + + +
+ {#if $currentConversation} + +
+ + + + +
+
+ {#if $currentConversation.avatar} + {$currentConversation.name} + {:else} + + {getAvatarPlaceholder($currentConversation.name)} + + {/if} +
+
+ +
+

+ {$currentConversation.name} +

+

+ {$currentConversation.pubkey} +

+
+ + + +
+ + +
+ {#if $dmStore.messages.length === 0} +
+

No messages yet

+

Send a message to start the conversation

+
+ {:else} + {#each $dmStore.messages as message (message.id)} +
+
+
+ + {message.isSent ? 'ME' : getAvatarPlaceholder($currentConversation.name)} + +
+
+
+ +
+
+ {message.content} +
+
+ {/each} + {/if} +
+ + +
+
+
+ +
+ +
+ +
+{/if} diff --git a/community-forum/src/routes/[category]/[section]/[forumId]/+page.svelte b/community-forum/src/routes/[category]/[section]/[forumId]/+page.svelte new file mode 100644 index 0000000..1c0336b --- /dev/null +++ b/community-forum/src/routes/[category]/[section]/[forumId]/+page.svelte @@ -0,0 +1,68 @@ + + + + Forum - {appConfig.name} + + +
+ {#if loading} +
+ +
+ {:else if error} + +
+ {error} + Back to Section +
+ {/if} +
diff --git a/community-forum/src/routes/[category]/[section]/calendar/+page.svelte b/community-forum/src/routes/[category]/[section]/calendar/+page.svelte new file mode 100644 index 0000000..a70013e --- /dev/null +++ b/community-forum/src/routes/[category]/[section]/calendar/+page.svelte @@ -0,0 +1,324 @@ + + + + {section?.name || 'Section'} Calendar - {appConfig.name} + + +
+ + +
+
+
+

+ {#if section} + {section.icon} + {section.name} Calendar + {:else} + Section Calendar + {/if} +

+

+ {#if section} + {section.description} + {/if} +

+ {#if accessLevel === 'availability'} +
Availability Only - Join section for details
+ {/if} +
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+

Section Calendars

+
+ {#each categories as cat} +
{cat.icon} {cat.name}
+ {#each cat.sections || [] as sec} + + {/each} + {/each} +
+
+
+ +
+
+

Event Stats

+
+
+ Total Events + {events.length} +
+
+ Upcoming + + {events.filter((e) => e.start > Date.now() / 1000).length} + +
+
+
+
+ +
+
+

Access Level

+
+ {#if accessLevel === 'full'} +
Full Access
+ {:else if accessLevel === 'availability'} +
Availability Only
+ {:else} +
No Access
+ {/if} +
+ {#if section?.calendar?.canCreate && accessLevel === 'full'} +

+ You can create events in this section +

+ {/if} +
+
+
+ + +
+ {#if loading} +
+ +
+ {:else if error} +
+ {error} +
+ {:else if viewMode === 'calendar'} + + {:else} +
+ {#if displayEvents.length === 0} +
+
+

No events in this section

+
+
+ {:else} + {#each displayEvents as event (event.id)} +
handleEventClick(new CustomEvent('click', { detail: event }))} + on:keydown={(e) => e.key === 'Enter' && handleEventClick(new CustomEvent('click', { detail: event }))} + role="button" + tabindex="0" + > +
+
+
+

{event.title}

+ {#if accessLevel === 'full' && event.description} +

+ {event.description} +

+ {/if} +
+
+
+ {new Date(event.start * 1000).toLocaleDateString()} +
+
+ {new Date(event.start * 1000).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} +
+
+
+ {#if accessLevel === 'full' && event.location} +
+ + + + + {event.location} +
+ {/if} +
+
+ {/each} + {/if} +
+ {/if} +
+
+
diff --git a/community-forum/src/routes/[section]/calendar/+page.svelte b/community-forum/src/routes/[section]/calendar/+page.svelte new file mode 100644 index 0000000..5fc1652 --- /dev/null +++ b/community-forum/src/routes/[section]/calendar/+page.svelte @@ -0,0 +1,403 @@ + + + + {section?.name || 'Section'} Calendar - {appConfig.name} + + +
+ +
+
+
+

+ {#if section} + {section.icon} + {section.name} Calendar + {:else} + Section Calendar + {/if} +

+

+ {#if section} + {section.description} + {/if} +

+ {#if accessLevel === 'availability'} +
Availability Only - Join section for details
+ {/if} +
+ +
+ + {#if canCreateEvent} + + {/if} + + +
+ + +
+
+
+
+ +
+ +
+
+
+

Section Calendars

+
+ {#each sections as sec} + + {/each} +
+
+
+ + +
+
+

Event Stats

+
+
+ Total Events + {events.length} +
+
+ Upcoming + + {events.filter((e) => e.start > Date.now() / 1000).length} + +
+
+
+
+ + +
+
+

Access Level

+
+ {#if accessLevel === 'full'} +
Full Access
+ {:else if accessLevel === 'availability'} +
Availability Only
+ {:else} +
No Access
+ {/if} +
+ {#if canCreateEvent} +

+ Click a day or use the Create Event button to add events +

+ {/if} +
+
+
+ + +
+ {#if loading} +
+ +
+ {:else if error} +
+ {error} +
+ {:else if viewMode === 'calendar'} + + {:else} +
+ {#if displayEvents.length === 0} +
+
+

No events in this section

+
+
+ {:else} + {#each displayEvents as event (event.id)} +
handleEventClick(new CustomEvent('click', { detail: event }))} + on:keydown={(e) => e.key === 'Enter' && handleEventClick(new CustomEvent('click', { detail: event }))} + role="button" + tabindex="0" + > +
+
+
+

{event.title}

+ {#if accessLevel === 'full' && event.description} +

+ {event.description} +

+ {/if} +
+
+
+ {new Date(event.start * 1000).toLocaleDateString()} +
+
+ {new Date(event.start * 1000).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} +
+
+
+ {#if accessLevel === 'full' && event.location} +
+ + + + + {event.location} +
+ {/if} +
+
+ {/each} + {/if} +
+ {/if} +
+
+
+ + +{#if canCreateEvent} + +{/if} diff --git a/community-forum/src/routes/admin/+page.svelte b/community-forum/src/routes/admin/+page.svelte new file mode 100644 index 0000000..c2c6212 --- /dev/null +++ b/community-forum/src/routes/admin/+page.svelte @@ -0,0 +1,777 @@ + + + + Admin Dashboard - {appConfig.name} + + +
+
+

Admin Dashboard

+

System overview and management

+
+ + {relayStatus} + + Relay Status +
+
+ + {#if $authStore.isNip07 && !authStore.getPrivkey()} +
+ + + + NIP-07 login detected. Admin operations will use your browser extension to sign NIP-98 auth tokens — you may see a signing prompt for each action. +
+ {/if} + + {#if error} +
+ + + + {error} + +
+ {/if} + + {#if successMessage} +
+ + + + {successMessage} + +
+ {/if} + + +
+ + + + +
+ + + {#if activeTab === 'overview'} + + + + {:else if activeTab === 'channels'} + + {:else if activeTab === 'users'} + handleApproveUserRegistration(e.detail)} + on:reject={(e) => handleRejectUserRegistration(e.detail)} + on:refresh={loadUserRegistrations} + /> + + {:else if activeTab === 'access'} + handleApproveRequest(e.detail)} + on:deny={(e) => handleDenyRequest(e.detail)} + on:refresh={loadPendingRequests} + /> + handleApproveChannelJoin(e.detail)} + on:reject={(e) => handleRejectChannelJoin(e.detail)} + on:refresh={loadChannelJoinRequests} + /> + {/if} +
diff --git a/community-forum/src/routes/admin/+page.ts b/community-forum/src/routes/admin/+page.ts new file mode 100644 index 0000000..be9814d --- /dev/null +++ b/community-forum/src/routes/admin/+page.ts @@ -0,0 +1,10 @@ +import type { PageLoad } from './$types'; + +// Disable prerendering - admin dashboard should not be statically generated +// This prevents leaking admin layout/menu structure to unauthenticated users +export const prerender = false; + +export const load: PageLoad = async () => { + // Auth and admin check handled in component + return { stats: null }; +}; diff --git a/community-forum/src/routes/admin/calendar/+page.svelte b/community-forum/src/routes/admin/calendar/+page.svelte new file mode 100644 index 0000000..c3b12d5 --- /dev/null +++ b/community-forum/src/routes/admin/calendar/+page.svelte @@ -0,0 +1,323 @@ + + + + Admin Calendar - {appConfig.name} + + +
+ +
+ + +
+
+

Admin Calendar

+

Combined view of all channel events

+
+ +
+
+
Total Events
+
{events.length}
+
+
+
Channels
+
{channels.length}
+
+
+
This Week
+
{upcomingEvents.length}
+
+
+
+
+ + {#if loading} +
+ +
+ {:else if error} +
+ {error} +
+ {:else} +
+ +
+ +
+ + +
+ +
+
+

Upcoming This Week

+ {#if upcomingEvents.length === 0} +

No upcoming events

+ {:else} +
+ {#each upcomingEvents as event} + + {/each} +
+ {/if} +
+
+ + +
+
+

Events by Channel

+
+ {#each Object.entries(eventsByChannel) as [channelId, channelEvents]} +
+ {getChannelName(channelId)} + {channelEvents.length} +
+ {/each} +
+
+
+ + + +
+
+ {/if} +
+ + +{#if selectedEvent} + +{/if} diff --git a/community-forum/src/routes/admin/stats/+page.svelte b/community-forum/src/routes/admin/stats/+page.svelte new file mode 100644 index 0000000..fc101ea --- /dev/null +++ b/community-forum/src/routes/admin/stats/+page.svelte @@ -0,0 +1,721 @@ + + + + Platform Statistics - Admin + + +
+ + + {#if isLoading} +
+
+

Loading platform statistics...

+
+ {:else if error} +
+

{error}

+ +
+ {:else if platformStats} +
+ +
+
+
+ + + + +
+
+
{formatNumber(platformStats.totalChannels)}
+
Total Channels
+
+ {platformStats.activeChannels} active in last 7 days +
+
+
+ +
+
+ + + +
+
+
{formatNumber(platformStats.totalMessages)}
+
Total Messages
+
+ {formatNumber(calculateTotalActivity())} including deleted +
+
+
+ +
+
+ + + + + + +
+
+
{formatNumber(platformStats.totalUsers)}
+
Total Users
+
+ {getAverageMembersPerChannel().toFixed(1)} avg per channel +
+
+
+ +
+ +
+
+ {platformStats.totalChannels === 0 ? '0' : ((platformStats.activeChannels / platformStats.totalChannels) * 100).toFixed(0)}% +
+
Engagement Rate
+
+ Based on 7-day activity +
+
+
+
+ + +
+
+

Channel Comparison

+
+ + + +
+
+ +
+
+
Channel
+
Messages
+
Members
+
Avg/Day
+
Peak Hour
+
Top Poster
+
+ + {#each sortedChannels as stats} +
+
+
{stats.channelId.slice(0, 16)}...
+
+
+ {formatNumber(stats.messageCount)} +
+
+ {formatNumber(stats.uniquePosters)} +
+
+ {stats.avgMessagesPerDay.toFixed(1)} +
+
+ {stats.peakHour}:00 +
+
+ {#if stats.topPosters[0]} +
+ {#if stats.topPosters[0].avatar} + + {/if} + + {stats.topPosters[0].name || `${stats.topPosters[0].pubkey.slice(0, 8)}...`} + + ({stats.topPosters[0].messageCount}) +
+ {:else} + N/A + {/if} +
+
+ {/each} +
+
+ + + {#if sortedChannels.length > 0 && sortedChannels[0].activityByDay.length > 0} +
+

Platform Activity Trend

+ +
+ {/if} + + + +
+ {/if} +
+ + diff --git a/community-forum/src/routes/api/proxy/+server.ts b/community-forum/src/routes/api/proxy/+server.ts new file mode 100644 index 0000000..518a374 --- /dev/null +++ b/community-forum/src/routes/api/proxy/+server.ts @@ -0,0 +1,639 @@ +/** + * Link Preview Proxy Endpoint + * + * Proxies requests to external URLs to bypass CORS and fetch OpenGraph metadata. + * Special handling for Twitter/X to use their oEmbed API for rich previews. + * + * Features: + * - Server-side caching to reduce load on external services + * - Twitter/X oEmbed support for rich tweet previews + * - Shared cache across all users on the node + * + * Security: + * - URL allowlist for trusted domains only + * - HTTPS-only scheme validation + * - Private/internal IP blocking + * - Rate limiting structure (TODO: implement with Redis) + */ + +import type { RequestHandler } from './$types'; +import { dev } from '$app/environment'; + +const TWITTER_OEMBED_URL = 'https://publish.twitter.com/oembed'; +const TIMEOUT_MS = 10000; + +// ============================================================================= +// SECURITY: URL Deny-list Configuration (SSRF Protection) +// ============================================================================= + +/** + * Deny-list approach: Block known dangerous domains instead of maintaining + * a fragile allow-list. This allows previews for any legitimate public site + * while blocking known malicious or problematic domains. + * + * Combined with private IP blocking (below), this provides SSRF protection + * without breaking previews for valid but unlisted domains. + */ +const BLOCKED_DOMAINS = new Set([ + // Localhost variations + 'localhost', + 'localhost.localdomain', + '127.0.0.1', + '0.0.0.0', + '[::1]', + + // Cloud metadata endpoints (SSRF targets) + 'metadata.google.internal', + 'metadata.gcp.internal', + 'instance-data', + '169.254.169.254', + + // Internal/corporate patterns + 'internal', + 'intranet', + 'corp', + 'private', + + // Known malware/phishing domains (examples - extend as needed) + 'bit.ly', // URL shorteners often used for phishing - consider allowing with caution +]); + +/** + * Private/internal IP ranges that must be blocked to prevent SSRF attacks. + * Includes IPv4 and IPv6 private ranges. + */ +const BLOCKED_IP_PATTERNS = [ + // IPv4 loopback + /^127\./, + // IPv4 private ranges (RFC 1918) + /^10\./, + /^192\.168\./, + /^172\.(1[6-9]|2[0-9]|3[0-1])\./, + // IPv4 link-local + /^169\.254\./, + // IPv4 localhost alternatives + /^0\./, + // IPv6 loopback + /^::1$/, + /^0:0:0:0:0:0:0:1$/, + // IPv6 private ranges + /^fc[0-9a-f]{2}:/i, + /^fd[0-9a-f]{2}:/i, + // IPv6 link-local + /^fe80:/i, + // AWS metadata endpoint + /^169\.254\.169\.254$/, + // Cloud metadata endpoints (GCP, Azure) + /^metadata\./i, +]; + +const BLOCKED_HOSTNAMES = new Set([ + 'localhost', + 'localhost.localdomain', + 'local', + 'internal', + 'intranet', + 'metadata', + 'metadata.google.internal', + '169.254.169.254', +]); + +// ============================================================================= +// RATE LIMITING STRUCTURE (Future Implementation) +// ============================================================================= + +/** + * Rate limiting configuration - to be implemented with Redis or similar. + * + * TODO: Implement rate limiting with the following structure: + * + * interface RateLimitConfig { + * windowMs: number; // Time window in milliseconds + * maxRequests: number; // Max requests per window + * keyGenerator: (ip: string, url: string) => string; + * } + * + * const RATE_LIMITS = { + * perIP: { windowMs: 60000, maxRequests: 30 }, // 30 req/min per IP + * perDomain: { windowMs: 60000, maxRequests: 100 }, // 100 req/min per domain + * global: { windowMs: 60000, maxRequests: 1000 }, // 1000 req/min global + * }; + * + * Implementation notes: + * - Use Redis INCR with EXPIRE for atomic rate limiting + * - Return 429 Too Many Requests when limit exceeded + * - Include Retry-After header with remaining window time + * - Consider using sliding window algorithm for smoother limiting + */ + +// ============================================================================= +// Cache Implementation +// ============================================================================= + +interface CacheEntry { + data: Record; + timestamp: number; + hits: number; +} + +const previewCache = new Map(); +const CACHE_MAX_AGE = 10 * 24 * 60 * 60 * 1000; // 10 days +const TWITTER_CACHE_MAX_AGE = 24 * 60 * 60 * 1000; // 1 day for Twitter (more dynamic content) +const MAX_CACHE_SIZE = 1000; + +/** + * Get cached preview if available and not expired + */ +function getCachedPreview(url: string, isTwitter: boolean): Record | null { + const entry = previewCache.get(url); + if (!entry) return null; + + const maxAge = isTwitter ? TWITTER_CACHE_MAX_AGE : CACHE_MAX_AGE; + if (Date.now() - entry.timestamp > maxAge) { + previewCache.delete(url); + return null; + } + + // Increment hit counter for analytics + entry.hits++; + return entry.data; +} + +/** + * Store preview in cache + */ +function setCachedPreview(url: string, data: Record): void { + // Enforce cache size limit (LRU-style: remove oldest entries) + if (previewCache.size >= MAX_CACHE_SIZE) { + // Remove oldest 10% of entries + const entries = Array.from(previewCache.entries()); + entries.sort((a, b) => a[1].timestamp - b[1].timestamp); + const toRemove = Math.ceil(MAX_CACHE_SIZE * 0.1); + for (let i = 0; i < toRemove; i++) { + previewCache.delete(entries[i][0]); + } + } + + previewCache.set(url, { + data, + timestamp: Date.now(), + hits: 0, + }); +} + +/** + * Get cache statistics + */ +function getCacheStats(): { size: number; totalHits: number } { + let totalHits = 0; + for (const entry of previewCache.values()) { + totalHits += entry.hits; + } + return { size: previewCache.size, totalHits }; +} + +// ============================================================================= +// Security Validation Functions +// ============================================================================= + +/** + * Check if a domain is blocked (deny-list approach) + * Returns true if the domain should be BLOCKED + */ +function isDomainBlocked(hostname: string): boolean { + const normalizedHost = hostname.toLowerCase(); + + // Check exact match against blocked domains + if (BLOCKED_DOMAINS.has(normalizedHost)) { + return true; + } + + // Check if it's a subdomain of a blocked domain + for (const blockedDomain of BLOCKED_DOMAINS) { + if (normalizedHost.endsWith(`.${blockedDomain}`)) { + return true; + } + } + + return false; +} + +/** + * Check if a hostname or IP is a private/internal address (SSRF protection) + */ +function isPrivateHost(hostname: string): boolean { + const normalizedHost = hostname.toLowerCase(); + + // Check blocked hostnames + if (BLOCKED_HOSTNAMES.has(normalizedHost)) { + return true; + } + + // Check blocked IP patterns (private ranges, localhost, metadata endpoints) + for (const pattern of BLOCKED_IP_PATTERNS) { + if (pattern.test(normalizedHost)) { + return true; + } + } + + return false; +} + +/** + * Validate URL for SSRF protection + * Uses deny-list approach: block known dangerous patterns, allow everything else + * Returns an error message if validation fails, null if valid + */ +function validateUrlSecurity(urlString: string): string | null { + let parsed: URL; + + try { + parsed = new URL(urlString); + } catch { + return 'Invalid URL format'; + } + + // 1. Validate scheme - HTTPS only (HTTP is insecure) + if (parsed.protocol !== 'https:') { + return 'Only HTTPS URLs are allowed'; + } + + // 2. Block private/internal addresses (SSRF protection) + if (isPrivateHost(parsed.hostname)) { + return 'Access to internal/private addresses is not allowed'; + } + + // 3. Check domain deny-list (known malicious/problematic domains) + if (isDomainBlocked(parsed.hostname)) { + return `Domain "${parsed.hostname}" is blocked`; + } + + // 4. Block URLs with credentials (prevent credential leakage) + if (parsed.username || parsed.password) { + return 'URLs with credentials are not allowed'; + } + + // 5. Block non-standard ports (only allow 443 for HTTPS) + // This prevents port scanning via SSRF + if (parsed.port && parsed.port !== '443') { + return 'Non-standard ports are not allowed'; + } + + // 6. Validate hostname has a TLD (basic sanity check) + if (!parsed.hostname.includes('.') && !parsed.hostname.match(/^\d+\.\d+\.\d+\.\d+$/)) { + return 'Invalid hostname: must be a fully qualified domain'; + } + + return null; +} + +// ============================================================================= +// URL Type Detection +// ============================================================================= + +/** + * Detect if URL is Twitter/X + */ +function isTwitterUrl(url: string): boolean { + try { + const parsed = new URL(url); + return ['twitter.com', 'x.com', 'www.twitter.com', 'www.x.com', 'mobile.twitter.com', 'mobile.x.com'].includes(parsed.hostname); + } catch { + return false; + } +} + +// ============================================================================= +// DNS Resolution for SSRF Protection +// ============================================================================= + +/** + * Resolve hostname via DNS-over-HTTPS and validate resolved IPs against SSRF blocklist. + * Prevents DNS rebinding attacks where a hostname resolves to a private IP. + * Throws if any resolved IP is in a blocked range. + */ +async function resolveAndValidateHost(hostname: string): Promise { + try { + const dohUrl = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(hostname)}&type=A`; + const response = await fetch(dohUrl, { + headers: { 'Accept': 'application/dns-json' }, + signal: AbortSignal.timeout(3000), + }); + + if (!response.ok) return; // DNS resolution failure — let fetch handle it + + const data = await response.json() as { Answer?: Array<{ type: number; data: string }> }; + const answers = data.Answer || []; + + for (const record of answers) { + if (record.type === 1 || record.type === 28) { // A or AAAA + if (isPrivateHost(record.data)) { + throw new Error(`DNS resolved to private IP: ${record.data}`); + } + } + } + } catch (error) { + if (error instanceof Error && error.message.includes('private IP')) { + throw error; + } + // DNS resolution failures are non-fatal — the fetch will fail naturally + } +} + +// ============================================================================= +// Data Fetching Functions +// ============================================================================= + +/** + * Fetch Twitter oEmbed data + */ +async function fetchTwitterEmbed(url: string): Promise { + const oembedUrl = new URL(TWITTER_OEMBED_URL); + oembedUrl.searchParams.set('url', url); + oembedUrl.searchParams.set('omit_script', 'true'); + oembedUrl.searchParams.set('dnt', 'true'); // Do not track + oembedUrl.searchParams.set('theme', 'dark'); // Request dark theme + + const response = await fetch(oembedUrl.toString(), { + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Nostr-BBS/1.0', + }, + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + + if (!response.ok) { + throw new Error(`Twitter oEmbed failed: ${response.status}`); + } + + const data = await response.json(); + + return new Response(JSON.stringify({ + type: 'twitter', + url: url, + html: data.html, + author_name: data.author_name, + author_url: data.author_url, + provider_name: data.provider_name || 'X', + width: data.width, + }), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=3600', // Cache for 1 hour + }, + }); +} + +/** + * Fetch generic OpenGraph metadata. + * Uses manual redirect following with DNS re-validation at each hop to prevent + * SSRF via redirect chains that land on private IPs. + */ +async function fetchOpenGraphData(url: string, redirectCount = 0): Promise { + if (redirectCount > 3) { + throw new Error('Too many redirects'); + } + + const response = await fetch(url, { + headers: { + 'Accept': 'text/html,application/xhtml+xml', + 'User-Agent': 'Nostr-BBS/1.0 (Link Preview Bot)', + }, + signal: AbortSignal.timeout(TIMEOUT_MS), + redirect: 'manual', + }); + + // Handle redirects with DNS re-validation + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get('Location'); + if (!location) throw new Error('Redirect without Location header'); + + const redirectUrl = new URL(location, url).href; + const securityError = validateUrlSecurity(redirectUrl); + if (securityError) throw new Error(`Redirect blocked: ${securityError}`); + + await resolveAndValidateHost(new URL(redirectUrl).hostname); + return fetchOpenGraphData(redirectUrl, redirectCount + 1); + } + + if (!response.ok) { + throw new Error(`Failed to fetch URL: ${response.status}`); + } + + const html = await response.text(); + + // Parse OpenGraph tags + const preview = parseOpenGraphTags(html, url); + + return new Response(JSON.stringify({ + type: 'opengraph', + ...preview, + }), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=86400', // Cache for 24 hours + }, + }); +} + +/** + * Parse OpenGraph tags from HTML + */ +function parseOpenGraphTags(html: string, url: string): Record { + const domain = new URL(url).hostname.replace(/^www\./, ''); + + const preview: Record = { + url, + domain, + favicon: `https://www.google.com/s2/favicons?domain=${domain}&sz=32`, + }; + + // Helper to extract meta content + const extractMeta = (pattern: RegExp): string | undefined => { + const match = html.match(pattern); + return match ? decodeHtmlEntities(match[1]) : undefined; + }; + + // Extract og:title (try multiple patterns) + preview.title = extractMeta(/([^<]+)<\/title>/i); + + // Extract og:description + preview.description = extractMeta(/ = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + ''': "'", + ' ': ' ', + }; + + let decoded = text; + for (const [entity, char] of Object.entries(entities)) { + decoded = decoded.replace(new RegExp(entity, 'gi'), char); + } + + // Handle numeric entities + decoded = decoded.replace(/&#(\d+);/g, (_, num) => { + const code = parseInt(num, 10); + return code > 0 && code < 0x10FFFF ? String.fromCodePoint(code) : ''; + }); + + decoded = decoded.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => { + const code = parseInt(hex, 16); + return code > 0 && code < 0x10FFFF ? String.fromCodePoint(code) : ''; + }); + + return decoded; +} + +/** + * Resolve relative URLs + */ +function resolveUrl(relativeUrl: string, baseUrl: string): string { + try { + return new URL(relativeUrl, baseUrl).href; + } catch { + return relativeUrl; + } +} + +// ============================================================================= +// Request Handler +// ============================================================================= + +export const GET: RequestHandler = async ({ url, getClientAddress }) => { + const targetUrl = url.searchParams.get('url'); + const statsOnly = url.searchParams.get('stats') === 'true'; + + // Return cache stats if requested (useful for monitoring) + if (statsOnly) { + return new Response(JSON.stringify(getCacheStats()), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (!targetUrl) { + return new Response(JSON.stringify({ error: 'Missing url parameter' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // ========================================================================== + // SECURITY VALIDATION + // ========================================================================== + + const securityError = validateUrlSecurity(targetUrl); + if (securityError) { + return new Response(JSON.stringify({ error: securityError }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // ========================================================================== + // RATE LIMITING (TODO: Implement with Redis) + // ========================================================================== + + // TODO: Add rate limiting check here + // const clientIP = getClientAddress(); + // const rateLimitResult = await checkRateLimit(clientIP, targetUrl); + // if (rateLimitResult.exceeded) { + // return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), { + // status: 429, + // headers: { + // 'Content-Type': 'application/json', + // 'Retry-After': String(rateLimitResult.retryAfter), + // }, + // }); + // } + + // DNS resolution check for SSRF protection — validates resolved IPs + // are not in private/internal ranges before allowing the fetch. + const targetHostname = new URL(targetUrl).hostname; + await resolveAndValidateHost(targetHostname); + + const isTwitter = isTwitterUrl(targetUrl); + + // Check server-side cache first + const cached = getCachedPreview(targetUrl, isTwitter); + if (cached) { + return new Response(JSON.stringify({ ...cached, cached: true }), { + headers: { + 'Content-Type': 'application/json', + 'X-Cache': 'HIT', + 'Cache-Control': isTwitter ? 'public, max-age=3600' : 'public, max-age=86400', + }, + }); + } + + try { + let data: Record; + + // Special handling for Twitter/X + if (isTwitter) { + const response = await fetchTwitterEmbed(targetUrl); + data = await response.json(); + } else { + const response = await fetchOpenGraphData(targetUrl); + data = await response.json(); + } + + // Store in server cache + setCachedPreview(targetUrl, data); + + return new Response(JSON.stringify({ ...data, cached: false }), { + headers: { + 'Content-Type': 'application/json', + 'X-Cache': 'MISS', + 'Cache-Control': isTwitter ? 'public, max-age=3600' : 'public, max-age=86400', + }, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return new Response(JSON.stringify({ error: message }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +}; diff --git a/community-forum/src/routes/chat/+layout.svelte b/community-forum/src/routes/chat/+layout.svelte new file mode 100644 index 0000000..7b3804b --- /dev/null +++ b/community-forum/src/routes/chat/+layout.svelte @@ -0,0 +1,19 @@ + + +
+ +
+ +
+
+ + diff --git a/community-forum/src/routes/chat/+page.svelte b/community-forum/src/routes/chat/+page.svelte new file mode 100644 index 0000000..46d42ad --- /dev/null +++ b/community-forum/src/routes/chat/+page.svelte @@ -0,0 +1,348 @@ + + + + {pageTitle} - {appConfig.name} + + +
+ +
+ +
+ +
+ +
+
+
+
+

{pageTitle}

+

{pageDescription}

+
+
+ + +
+
+
+ + + {#if showSearch} +
+
+
+

Semantic Search

+ +
+

+ Search by meaning, not just keywords. Find messages that are semantically similar to your query. +

+ +
+
+ {/if} + + {#if loading} +
+ +
+ {:else if error} +
+
+
+ {#if error.includes('timed out') || error.includes('unavailable')} + 🔌 + {:else if error.includes('Authentication')} + 🔒 + {:else} + ⚠️ + {/if} +
+

Connection Issue

+

{error}

+
+ {#if retryCount < MAX_RETRIES} + + {:else} +

Max retries reached. Please refresh the page.

+ {/if} + Back to Home +
+ {#if retryCount > 0} +

Retry attempt {retryCount} of {MAX_RETRIES}

+ {/if} +
+
+ {:else if channels.length === 0} +
+ +
+ {#if activeSection} + View All Channels + {/if} + {#if canCreateInAnySection} + + + + + Create Channel + + {/if} +
+
+ {:else} +
+ {#each channels as channel (channel.id)} + + {/each} +
+ {/if} +
+ + + +
+
diff --git a/community-forum/src/routes/chat/+page.ts b/community-forum/src/routes/chat/+page.ts new file mode 100644 index 0000000..d1086ad --- /dev/null +++ b/community-forum/src/routes/chat/+page.ts @@ -0,0 +1,10 @@ +import type { PageLoad } from './$types'; +import { browser } from '$app/environment'; + +// Allow prerendering - data is loaded in component +export const prerender = true; + +export const load: PageLoad = async () => { + // All data loading happens in the component to avoid SSR/prerender issues + return { channels: [] }; +}; diff --git a/community-forum/src/routes/chat/[channelId]/+page.svelte b/community-forum/src/routes/chat/[channelId]/+page.svelte new file mode 100644 index 0000000..d823a0e --- /dev/null +++ b/community-forum/src/routes/chat/[channelId]/+page.svelte @@ -0,0 +1,511 @@ + + + + {channel?.name || 'Channel'} - {appConfig.name} + + +{#if loading} +
+
+
+{:else if error && !channel} +
+ +
+ {error} +
+
+{:else if channel} +
+
+
+
+ +
+

{channel.name}

+ {#if channel.description} +

{channel.description}

+ {/if} +
+ {channel.visibility} + {#if channel.encrypted} + Encrypted + {/if} + {messageCount} messages + +
+
+
+ + + + + + {#if error} +
+
+ {error} + +
+
+ {/if} + +
+
+ {#if messages.length === 0} +
+

No messages yet. Start the conversation!

+
+ {:else} +
+ {#each formattedMessages as message (message.id)} + + {/each} +
+ {/if} +
+ + {#if showScrollButton} + + {/if} +
+ +
+
+
+ + +
+
+
+
+{/if} + + diff --git a/community-forum/src/routes/chat/[channelId]/+page.ts b/community-forum/src/routes/chat/[channelId]/+page.ts new file mode 100644 index 0000000..da74c11 --- /dev/null +++ b/community-forum/src/routes/chat/[channelId]/+page.ts @@ -0,0 +1,9 @@ +import type { PageLoad } from './$types'; + +// Dynamic routes can't be prerendered +export const prerender = false; + +export const load: PageLoad = async ({ params }) => { + // Data loading happens in component to avoid SSR issues + return { channelId: params.channelId, channel: null, messages: [] }; +}; diff --git a/community-forum/src/routes/dm/+page.svelte b/community-forum/src/routes/dm/+page.svelte new file mode 100644 index 0000000..c95946d --- /dev/null +++ b/community-forum/src/routes/dm/+page.svelte @@ -0,0 +1,205 @@ + + + + Direct Messages - {appConfig.name} + + +
+
+
+

Direct Messages

+

Private encrypted conversations

+
+ +
+ + {#if error} +
+ {error} + +
+ {/if} + + {#if loading} +
+
+

Loading conversations...

+
+ {:else if conversations.length === 0} +
+
+ + + +

No conversations yet

+

+ Start a private, encrypted conversation by sending a message to someone using their public ID. +

+ +
+
+ {:else} +
+ {#each conversations as conv (conv.pubkey)} + + {/each} +
+ {/if} +
+ + diff --git a/community-forum/src/routes/dm/+page.ts b/community-forum/src/routes/dm/+page.ts new file mode 100644 index 0000000..6cb9f33 --- /dev/null +++ b/community-forum/src/routes/dm/+page.ts @@ -0,0 +1,9 @@ +import type { PageLoad } from './$types'; + +// Allow prerendering - data is loaded in component +export const prerender = true; + +export const load: PageLoad = async () => { + // All data loading happens in the component to avoid SSR/prerender issues + return { conversations: [] }; +}; diff --git a/community-forum/src/routes/dm/[pubkey]/+page.svelte b/community-forum/src/routes/dm/[pubkey]/+page.svelte new file mode 100644 index 0000000..993b76f --- /dev/null +++ b/community-forum/src/routes/dm/[pubkey]/+page.svelte @@ -0,0 +1,217 @@ + + + + DM with {recipientDisplayName} - {appConfig.name} + + +{#if loading} +
+
+
+{:else} +
+
+
+ +
+
+
+ {recipientDisplayName.charAt(0).toUpperCase()} +
+
+
+

{recipientDisplayName}

+

{formatPubkey(recipientPubkey)}

+
+
+
+
+ +
+
+ {#if messages.length === 0} +
+

No messages yet. Start the conversation!

+
+ {:else} +
+ {#each messages as message (message.id)} +
+
+ {message.content} +
+ +
+ {/each} +
+ {/if} +
+
+ +
+
+ {#if $isReadOnly} +
+ Messaging is disabled in read-only mode. + Complete signup + to send messages. +
+ {:else} +
+ + +
+ {/if} +
+
+
+{/if} diff --git a/community-forum/src/routes/dm/[pubkey]/+page.ts b/community-forum/src/routes/dm/[pubkey]/+page.ts new file mode 100644 index 0000000..a499189 --- /dev/null +++ b/community-forum/src/routes/dm/[pubkey]/+page.ts @@ -0,0 +1,9 @@ +import type { PageLoad } from './$types'; + +// Dynamic routes can't be prerendered +export const prerender = false; + +export const load: PageLoad = async ({ params }) => { + // Data loading happens in component to avoid SSR issues + return { recipientPubkey: params.pubkey, messages: [] }; +}; diff --git a/community-forum/src/routes/events/+layout.svelte b/community-forum/src/routes/events/+layout.svelte new file mode 100644 index 0000000..a2f359e --- /dev/null +++ b/community-forum/src/routes/events/+layout.svelte @@ -0,0 +1,90 @@ + + +
+ +
+ +
+ + + {#if !isMobile} +
+ +
+ {/if} + + + {#if isMobile} + + {/if} +
+ + +{#if !isMobile && !sidebarExpanded} + +{/if} + + diff --git a/community-forum/src/routes/events/+page.svelte b/community-forum/src/routes/events/+page.svelte new file mode 100644 index 0000000..1f4a280 --- /dev/null +++ b/community-forum/src/routes/events/+page.svelte @@ -0,0 +1,294 @@ + + + + Events - {appConfig.name} + + +
+ +
+
+
+

Events

+

+ {#if selectedChannel} + Events for {selectedChannel.name} + {:else} + All community events + {/if} +

+
+ +
+ +
+ + +
+ + + {#if selectedChannelId} + + {/if} +
+
+
+ +
+ +
+
+
+

Filter by Channel

+
+ + {#each channels as channel} + + {/each} +
+
+
+ + +
+
+

Event Stats

+
+
+ Total Events + {events.length} +
+
+ This Month + {filteredEvents.length} +
+
+ Upcoming + + {filteredEvents.filter((e) => e.start > Date.now() / 1000).length} + +
+
+
+
+
+ + +
+ {#if loading} +
+ +
+ {:else if error} +
+ {error} +
+ {:else if viewMode === 'calendar'} + + {:else} +
+ {#if filteredEvents.length === 0} +
+
+

No events found

+ {#if selectedChannelId} + + {/if} +
+
+ {:else} + {#each filteredEvents as event (event.id)} + + {/each} + {/if} +
+ {/if} +
+
+
+ + +{#if selectedChannelId && selectedChannel} + +{/if} diff --git a/community-forum/src/routes/forums/+page.svelte b/community-forum/src/routes/forums/+page.svelte new file mode 100644 index 0000000..894ddd4 --- /dev/null +++ b/community-forum/src/routes/forums/+page.svelte @@ -0,0 +1,87 @@ + + + + Forums - {appConfig.name} + + +
+ +
+ +
+ +
+ +
+
+

Forums

+

Browse discussion categories

+
+ + {#if loading} +
+ +
+ {:else if categories.length === 0} +
+
+
📁
+

No categories available

+

Check back later for new discussion areas.

+
+
+ {:else} +
+ {#each categories as category (category.id)} + + {/each} +
+ {/if} +
+ + +
+ + + + +
+
+
diff --git a/community-forum/src/routes/login/+page.svelte b/community-forum/src/routes/login/+page.svelte new file mode 100644 index 0000000..0763018 --- /dev/null +++ b/community-forum/src/routes/login/+page.svelte @@ -0,0 +1,19 @@ + + + + Login - DreamLab Community + + +
+
+ +
+
diff --git a/community-forum/src/routes/pending/+page.svelte b/community-forum/src/routes/pending/+page.svelte new file mode 100644 index 0000000..8774bbf --- /dev/null +++ b/community-forum/src/routes/pending/+page.svelte @@ -0,0 +1,130 @@ + + + + Welcome - {appConfig.name} + + +
+ + + + +
+
+ +
+
+
👋
+ +

Welcome to {appName}!

+ +

+ Your account has been created successfully. + You're just one step away from joining our community. +

+ +
+ + +
+ +
+

Awaiting Admin Approval

+

An administrator will review your access shortly

+
+
+
+
+ + +
+
+
+

+ + + + What happens next? +

+
    +
  • Admin reviews your request
  • +
  • You'll be assigned to community zones
  • +
  • Page auto-refreshes when approved
  • +
+
+
+ +
+
+

+ + + + Community Zones +

+
    +
  • Family - Family events & activities
  • +
  • DreamLab - Creative projects
  • +
  • Minimoonoir - Special interest group
  • +
+
+
+
+ + +
+

This page will automatically redirect once you're approved.

+

Questions? Contact an administrator.

+
+
+
+
diff --git a/community-forum/src/routes/settings/muted/+page.svelte b/community-forum/src/routes/settings/muted/+page.svelte new file mode 100644 index 0000000..4edf9a3 --- /dev/null +++ b/community-forum/src/routes/settings/muted/+page.svelte @@ -0,0 +1,182 @@ + + +
+
+
+ +

Muted Users

+
+ +
+
+

+ {mutedUsers.length} {mutedUsers.length === 1 ? 'user' : 'users'} muted +

+

+ You won't see messages, DMs, or notifications from muted users +

+
+ + {#if mutedUsers.length > 0} + + {/if} +
+
+ + {#if mutedUsers.length === 0} +
+ + + +

No muted users

+

+ When you mute someone, they'll appear here +

+
+ {:else} +
+ {#each mutedUsers as user (user.pubkey)} +
+
+
+
+
+ {getDisplayName(user.pubkey)} +
+
+ +
+
+ + {getDisplayName(user.pubkey)} + + Muted +
+ +
+ {formatPubkey(user.pubkey)} + · Muted {formatDate(user.mutedAt)} +
+ + {#if user.reason} +
+ Reason: {user.reason} +
+ {/if} + +
+ {getNpub(user.pubkey)} + +
+
+ +
+ +
+
+
+
+ {/each} +
+ {/if} +
diff --git a/community-forum/src/routes/setup/+page.svelte b/community-forum/src/routes/setup/+page.svelte new file mode 100644 index 0000000..d484f66 --- /dev/null +++ b/community-forum/src/routes/setup/+page.svelte @@ -0,0 +1,534 @@ + + +
+ +
+
+
+ {#each steps as step, i} +
+
+ {#if isStepComplete(step.id, $currentSetupStep)} + + + + {:else} + {step.icon} + {/if} +
+ +
+ {#if i < steps.length - 1} +
+ {/if} + {/each} +
+
+
+ + +
+ {#if $currentSetupStep === 'welcome'} +
+
🌱
+

Instance Setup

+

+ Configure your instance with sections, roles, and permissions. You can upload an existing + configuration or create one step by step. +

+ +
+ + +
+
+ {:else if $currentSetupStep === 'upload-config'} +
+

Configuration

+

+ Upload an existing YAML configuration file or create one manually. +

+ + +
+
📄
+

Drop your YAML config file here

+

or

+ + +
+ + +
+ + Paste YAML directly + +
+