From 581ac6c8d763e861f96c5198047fd1c7aaef2104 Mon Sep 17 00:00:00 2001 From: Arpit Khandelwal Date: Thu, 14 May 2026 07:27:38 +0530 Subject: [PATCH 001/262] chore(ci): polish CI workflow and remove unused actions --- .github/workflows/auto-assign.yml | 22 -------- .github/workflows/ci.yml | 85 ++++++++++++++++++++----------- .github/workflows/security.yml | 60 ---------------------- 3 files changed, 56 insertions(+), 111 deletions(-) delete mode 100644 .github/workflows/auto-assign.yml delete mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml deleted file mode 100644 index d58baed8..00000000 --- a/.github/workflows/auto-assign.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Auto Request Review -on: - pull_request_target: - types: [opened, ready_for_review] - -permissions: - pull-requests: write - -jobs: - assign: - runs-on: ubuntu-latest - steps: - - name: Request review from repository owner - uses: actions/github-script@v7 - with: - script: | - github.rest.pulls.requestReviewers({ - pull_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - reviewers: ['arpittkhandelwal'] - }) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf082d5..580ff756 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,27 +6,35 @@ on: pull_request: branches: [main, dev] +# Minimal permissions — least privilege by default +permissions: + contents: read + env: CI: true TURBO_TELEMETRY_DISABLED: 1 +# Cancel in-progress runs for the same branch/PR to save minutes concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - # ── 1. Lint & Format ────────────────────────────────────────────────────── + # ── 1. Lint & Format ──────────────────────────────────────────────────────── lint-format: - name: Lint & Format + name: 🔍 Lint & Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 with: run_install: false - - uses: actions/setup-node@v4 + - name: Setup Node.js 20 + uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm @@ -34,24 +42,27 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Lint (ESLint via Turbo) + - name: Lint (ESLint) run: pnpm turbo lint - name: Format check (Prettier) run: pnpm format:check - # ── 2. TypeScript type-check ────────────────────────────────────────────── + # ── 2. TypeScript type-check ──────────────────────────────────────────────── typecheck: - name: TypeCheck + name: 🧠 TypeCheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 with: run_install: false - - uses: actions/setup-node@v4 + - name: Setup Node.js 20 + uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm @@ -59,21 +70,24 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: TypeCheck (tsc via Turbo) + - name: TypeCheck (tsc) run: pnpm turbo typecheck - # ── 3. Build & Unit Tests (with Turbo cache) ───────────────────────────── + # ── 3. Build & Unit Tests ─────────────────────────────────────────────────── build-and-test: - name: Build & Test + name: 🏗️ Build & Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 with: run_install: false - - uses: actions/setup-node@v4 + - name: Setup Node.js 20 + uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm @@ -88,13 +102,13 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build all packages (Turbo) + - name: Build all packages run: pnpm turbo build - name: Run unit tests (Vitest) run: pnpm turbo test - - name: Upload coverage reports + - name: Upload coverage report if: always() uses: actions/upload-artifact@v4 with: @@ -102,12 +116,9 @@ jobs: path: "**/coverage/**" retention-days: 7 - - name: Bundle size check - run: node scripts/check-bundle-size.mjs - - # ── 4. Python SDK ───────────────────────────────────────────────────────── + # ── 4. Python SDK ─────────────────────────────────────────────────────────── python-sdk: - name: Python SDK (py${{ matrix.python }}) + name: 🐍 Python SDK (py${{ matrix.python }}) runs-on: ubuntu-latest strategy: fail-fast: false @@ -117,12 +128,11 @@ jobs: run: working-directory: packages/zerithdb-python steps: - - uses: actions/checkout@v4 - - - name: Install system dependencies (ffmpeg for aiortc/av) - run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg libavcodec-dev libavformat-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev pkg-config + - name: Checkout + uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - name: Setup Python ${{ matrix.python }} + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} cache: pip @@ -138,3 +148,20 @@ jobs: - name: Test (pytest) run: pytest --tb=short -q + + # ── 5. CI Summary Gate ────────────────────────────────────────────────────── + # This is the single required status check to add in branch protection rules. + # All jobs must pass for this to succeed. + ci-success: + name: ✅ CI Passed + needs: [lint-format, typecheck, build-and-test, python-sdk] + runs-on: ubuntu-latest + if: always() + steps: + - name: Check all jobs passed + run: | + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" || "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "❌ One or more CI jobs failed. Please check the logs above." + exit 1 + fi + echo "✅ All CI checks passed!" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 728dceaf..00000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Security Audit - -on: - schedule: - - cron: "0 9 * * 1" # every Monday at 09:00 UTC - workflow_dispatch: # allow manual trigger from Actions tab - -jobs: - # ── npm vulnerability scan ──────────────────────────────────────────────── - npm-audit: - name: npm Audit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - run_install: false - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Audit (fail on high/critical) - run: pnpm audit --audit-level=high - - # ── Python vulnerability scan ───────────────────────────────────────────── - python-audit: - name: Python Audit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install pip-audit - run: pip install pip-audit - - - name: Audit zerithdb-python dependencies - working-directory: packages/zerithdb-python - run: pip-audit -r <(pip show zerithdb 2>/dev/null || echo "") --desc on || \ - pip-audit --requirement <(grep -v '^\[' pyproject.toml | grep '=') || \ - pip-audit -e . - - # ── Dependency review (on PRs that touch lock files) ───────────────────── - dependency-review: - name: Dependency Review - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - steps: - - uses: actions/checkout@v4 - - uses: actions/dependency-review-action@v4 - with: - fail-on-severity: high From 265bf4baefab0af05b085c861163c5bd2ff16d1b Mon Sep 17 00:00:00 2001 From: XueYuSky Date: Thu, 14 May 2026 10:53:13 +0800 Subject: [PATCH 002/262] feat(db): add clearAll collection helper --- .changeset/clear-all-db-adapter.md | 5 +++++ packages/db/src/db-client.ts | 15 +++++++++++++++ tests/unit/db.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 .changeset/clear-all-db-adapter.md diff --git a/.changeset/clear-all-db-adapter.md b/.changeset/clear-all-db-adapter.md new file mode 100644 index 00000000..5f8db005 --- /dev/null +++ b/.changeset/clear-all-db-adapter.md @@ -0,0 +1,5 @@ +--- +"zerithdb-db": patch +--- + +Add a `clearAll()` helper to remove every document from a collection. diff --git a/packages/db/src/db-client.ts b/packages/db/src/db-client.ts index 2e3ef051..9d247dfb 100644 --- a/packages/db/src/db-client.ts +++ b/packages/db/src/db-client.ts @@ -152,6 +152,21 @@ export class CollectionClient = Record { + try { + await this.table.clear(); + } catch (err) { + throw new ZerithDBError( + ErrorCode.DB_DELETE_FAILED, + `Failed to clear collection "${this.collectionName}"`, + { cause: err } + ); + } + } + /** * Count documents matching a filter. */ diff --git a/tests/unit/db.test.ts b/tests/unit/db.test.ts index 7ca10ee2..d392aa3a 100644 --- a/tests/unit/db.test.ts +++ b/tests/unit/db.test.ts @@ -142,6 +142,30 @@ describe("DbClient — CollectionClient", () => { }); }); + describe("clearAll()", () => { + it("should remove every document in the collection", async () => { + const col = db.collection<{ done: boolean }>("tasks"); + await col.insertMany([{ done: true }, { done: false }, { done: true }]); + + await col.clearAll(); + + expect(await col.find({})).toHaveLength(0); + expect(await col.count()).toBe(0); + }); + + it("should not clear other collections", async () => { + const tasks = db.collection<{ done: boolean }>("tasks"); + const notes = db.collection<{ text: string }>("notes"); + await tasks.insertMany([{ done: true }, { done: false }]); + await notes.insert({ text: "keep me" }); + + await tasks.clearAll(); + + expect(await tasks.count()).toBe(0); + expect(await notes.count()).toBe(1); + }); + }); + describe("count()", () => { it("should return correct document count", async () => { const col = db.collection<{ x: number }>("counts"); From 82e285b93a48d9fa13ff2576d6dc614b66886cbf Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Thu, 14 May 2026 19:23:37 +0530 Subject: [PATCH 003/262] feat: add PR welcome bot workflow --- .github/workflows/pr-welcome.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/pr-welcome.yml diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml new file mode 100644 index 00000000..8e698a7d --- /dev/null +++ b/.github/workflows/pr-welcome.yml @@ -0,0 +1,30 @@ +name: PR Welcome Bot + +on: + pull_request_target: + types: [opened] + +jobs: + welcome-comment: + name: Post Welcome Comment + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Welcome Comment + uses: actions/github-script@v7 + with: + script: | + const creator = context.payload.pull_request.user.login; + const message = `### 👋 Hello @${creator}! + +Our reviewers will be checking your PR shortly. In the meantime, please join our Discord server to connect with the team and other contributors: + +🚀 **Join Discord:** https://discord.gg/mwCayEMK4h`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: message + }); From 4bc897b90e8e3aa28da48bd43af8d23715662b89 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Thu, 14 May 2026 19:23:37 +0530 Subject: [PATCH 004/262] fix: add issues: write permission for welcome bot --- .github/workflows/pr-welcome.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml index 8e698a7d..7585bcb3 100644 --- a/.github/workflows/pr-welcome.yml +++ b/.github/workflows/pr-welcome.yml @@ -10,6 +10,7 @@ jobs: runs-on: ubuntu-latest permissions: pull-requests: write + issues: write steps: - name: Welcome Comment uses: actions/github-script@v7 From b5d77383d660a1248a5761e530c0c18f41419c74 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Thu, 14 May 2026 19:30:26 +0530 Subject: [PATCH 005/262] fix: resolve YAML syntax error in welcome bot --- .github/workflows/pr-welcome.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml index 7585bcb3..8c8aa46e 100644 --- a/.github/workflows/pr-welcome.yml +++ b/.github/workflows/pr-welcome.yml @@ -17,12 +17,12 @@ jobs: with: script: | const creator = context.payload.pull_request.user.login; - const message = `### 👋 Hello @${creator}! + const message = `### 👋 Hello @${creator}! Our reviewers will be checking your PR shortly. In the meantime, please join our Discord server to connect with the team and other contributors: 🚀 **Join Discord:** https://discord.gg/mwCayEMK4h`; - + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From c447c11368475123bf7d42955c858694208878cd Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Thu, 14 May 2026 19:35:36 +0530 Subject: [PATCH 006/262] fix: remove emojis and simplify string to fix YAML syntax error --- .github/workflows/pr-welcome.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml index 8c8aa46e..f6d10534 100644 --- a/.github/workflows/pr-welcome.yml +++ b/.github/workflows/pr-welcome.yml @@ -17,11 +17,7 @@ jobs: with: script: | const creator = context.payload.pull_request.user.login; - const message = `### 👋 Hello @${creator}! - -Our reviewers will be checking your PR shortly. In the meantime, please join our Discord server to connect with the team and other contributors: - -🚀 **Join Discord:** https://discord.gg/mwCayEMK4h`; + const message = "### Hello @" + creator + "!\n\nOur reviewers will be checking your PR shortly. In the meantime, please join our Discord server to connect with the team and other contributors:\n\n**Join Discord:** https://discord.gg/mwCayEMK4h"; await github.rest.issues.createComment({ owner: context.repo.owner, From 68107958e85c0a0956c2cd5cc9f57cb93d087dd5 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Thu, 14 May 2026 19:36:14 +0530 Subject: [PATCH 007/262] chore: test commit for welcome bot --- bot-test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 bot-test.txt diff --git a/bot-test.txt b/bot-test.txt new file mode 100644 index 00000000..e69de29b From d3f416825a521d48e2d56b039cb25052cc100c2f Mon Sep 17 00:00:00 2001 From: hardikdhingra150 Date: Thu, 14 May 2026 21:11:02 +0530 Subject: [PATCH 008/262] fix(db): support unset updates --- packages/db/src/db-client.ts | 26 +++++++++++++++++++------- tests/unit/db.test.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/db/src/db-client.ts b/packages/db/src/db-client.ts index 9d247dfb..ab5e4291 100644 --- a/packages/db/src/db-client.ts +++ b/packages/db/src/db-client.ts @@ -116,13 +116,7 @@ export class CollectionClient = Record ({ - ...doc, - ...(spec.$set ?? {}), - _updatedAt: now, - })) - ); + await this.table.bulkPut(matches.map((doc) => this.applyUpdateSpec(doc, spec, now))); return matches.length; } catch (err) { @@ -175,6 +169,24 @@ export class CollectionClient = Record, spec: UpdateSpec, updatedAt: number): Document { + const next = { + ...doc, + ...(spec.$set ?? {}), + _updatedAt: updatedAt, + } as Record; + + for (const key of Object.keys(spec.$unset ?? {})) { + delete next[key]; + } + + next._id = doc._id; + next._createdAt = doc._createdAt; + next._updatedAt = updatedAt; + + return next as Document; + } + private matchesFilter(doc: Document, filter: QueryFilter): boolean { for (const [key, condition] of Object.entries(filter)) { const fieldValue = (doc as Record)[key]; diff --git a/tests/unit/db.test.ts b/tests/unit/db.test.ts index d392aa3a..67c59b04 100644 --- a/tests/unit/db.test.ts +++ b/tests/unit/db.test.ts @@ -129,6 +129,19 @@ describe("DbClient — CollectionClient", () => { const after = (await col.findById(id))?._updatedAt ?? 0; expect(after).toBeGreaterThanOrEqual(before); }); + + it("should unset matching document fields", async () => { + const col = db.collection<{ text: string; note?: string }>("todos"); + const { id } = await col.insert({ text: "fix bug", note: "remove me" }); + + const count = await col.update({ text: "fix bug" }, { $unset: { note: true } }); + + const doc = await col.findById(id); + expect(count).toBe(1); + expect(doc).toBeDefined(); + expect(doc).not.toHaveProperty("note"); + expect(doc?.text).toBe("fix bug"); + }); }); describe("delete()", () => { From 5403a47738af6248b528f48f57ce4edc110729a3 Mon Sep 17 00:00:00 2001 From: Arpit Khandelwal <71580150+arpittkhandelwal@users.noreply.github.com> Date: Thu, 14 May 2026 23:48:37 +0530 Subject: [PATCH 009/262] Delete .github/ISSUE_TEMPLATE/bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 38 --------------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 16c679fd..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: "🐛 Bug Report" -about: "Something isn't working as expected" -labels: ["bug", "needs-investigation"] ---- - -## Description - - - -## Steps to Reproduce - -1. -2. -3. - -## Expected Behavior - - - -## Actual Behavior - - - -## Environment - -- ZerithDB version: -- Browser + version: -- OS: -- Package(s) affected: `@zerithdb/` - -## Reproduction - - - -## Additional Context - - From 8c9ad5d38ecd873bd9878ebcb3d4056dbccdf0df Mon Sep 17 00:00:00 2001 From: BhelPuriPanda Date: Fri, 15 May 2026 01:17:08 +0530 Subject: [PATCH 010/262] feat: add polling transport fallback --- apps/web/pnpm-lock.yaml | 86 ++++ apps/web/pnpm-workspace.yaml | 3 + infra/signaling-server/src/server.ts | 467 ++++++++++++++++-- package.json | 4 +- packages/core/src/internal/errors.ts | 1 + packages/core/src/types/config.ts | 9 + packages/network/src/index.ts | 3 + packages/network/src/network-manager.ts | 161 ++++-- packages/network/src/signaling-transport.ts | 24 + .../src/transports/polling-transport.ts | 180 +++++++ .../src/transports/websocket-transport.ts | 112 +++++ packages/sdk/src/create-app.ts | 1 + pnpm-lock.yaml | 58 ++- turbo.json | 50 +- 14 files changed, 1059 insertions(+), 100 deletions(-) create mode 100644 packages/network/src/signaling-transport.ts create mode 100644 packages/network/src/transports/polling-transport.ts create mode 100644 packages/network/src/transports/websocket-transport.ts diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 0e35dc40..1296150e 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + framer-motion: + specifier: ^12.38.0 + version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + lucide-react: + specifier: ^1.14.0 + version: 1.14.0(react@19.2.4) next: specifier: 16.2.6 version: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -213,89 +219,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -362,24 +384,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} @@ -453,24 +479,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -624,41 +654,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1102,6 +1140,20 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1415,24 +1467,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1464,6 +1520,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@1.14.0: + resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1489,6 +1550,12 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3107,6 +3174,15 @@ snapshots: dependencies: is-callable: 1.2.7 + framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3450,6 +3526,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@1.14.0(react@19.2.4): + dependencies: + react: 19.2.4 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3473,6 +3553,12 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + ms@2.1.3: {} nanoid@3.3.12: {} diff --git a/apps/web/pnpm-workspace.yaml b/apps/web/pnpm-workspace.yaml index 581a9d5b..d7978c14 100644 --- a/apps/web/pnpm-workspace.yaml +++ b/apps/web/pnpm-workspace.yaml @@ -1,3 +1,6 @@ +allowBuilds: + sharp: false + unrs-resolver: false ignoredBuiltDependencies: - sharp - unrs-resolver diff --git a/infra/signaling-server/src/server.ts b/infra/signaling-server/src/server.ts index a486a9a0..85896c9f 100644 --- a/infra/signaling-server/src/server.ts +++ b/infra/signaling-server/src/server.ts @@ -1,24 +1,113 @@ import { WebSocketServer, WebSocket } from "ws"; -import { createServer } from "http"; +import { createServer, type IncomingMessage, type ServerResponse } from "http"; const PORT = parseInt(process.env["PORT"] ?? "4000", 10); const HOST = process.env["HOST"] ?? "0.0.0.0"; -// roomId → Set of { peerId, ws } -const rooms = new Map>(); - -const server = createServer((_req, res) => { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - service: "zerithdb-signaling", - version: "0.1.0", - rooms: rooms.size, - peers: [...rooms.values()].reduce((acc, s) => acc + s.size, 0), - }) - ); +// ─── Shared room state ────────────────────────────────────────────────────── + +interface PeerEntry { + peerId: string; + ws?: WebSocket; // present for WebSocket peers + sessionId?: string; // present for polling peers +} + +// roomId → Set of PeerEntry +const rooms = new Map>(); + +// ─── Polling session state ────────────────────────────────────────────────── + +interface PollingSession { + sessionId: string; + peerId: string; + roomId: string; + messageQueue: string[]; + /** Pending long-poll response waiting for messages */ + pendingResponse: ServerResponse | null; + /** Timestamp of last activity (poll or send) */ + lastActivity: number; +} + +// sessionId → PollingSession +const pollingSessions = new Map(); + +/** How long a polling session can be inactive before cleanup (ms) */ +const SESSION_TIMEOUT_MS = 60_000; + +/** How long a single long-poll request blocks waiting for messages (ms) */ +const LONG_POLL_TIMEOUT_MS = 30_000; + +// Clean up expired sessions every 15 seconds +setInterval(() => { + const now = Date.now(); + for (const [sessionId, session] of pollingSessions) { + if (now - session.lastActivity > SESSION_TIMEOUT_MS) { + cleanupPollingSession(sessionId); + } + } +}, 15_000); + +// ─── HTTP server ──────────────────────────────────────────────────────────── + +const server = createServer((req, res) => { + // CORS headers for polling requests from browsers + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + + const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); + const pathname = url.pathname; + + // Health check (existing behavior) + if (pathname === "/" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + service: "zerithdb-signaling", + version: "0.1.0", + rooms: rooms.size, + peers: [...rooms.values()].reduce((acc, s) => acc + s.size, 0), + pollingSessions: pollingSessions.size, + }) + ); + return; + } + + // ─── Long-polling endpoints ─────────────────────────────────────────── + + if (pathname === "/poll/join" && req.method === "POST") { + handlePollJoin(req, res); + return; + } + + if (pathname === "/poll/messages" && req.method === "GET") { + handlePollMessages(url, res); + return; + } + + if (pathname === "/poll/send" && req.method === "POST") { + handlePollSend(req, res); + return; + } + + if (pathname === "/poll/leave" && req.method === "POST") { + handlePollLeave(req, res); + return; + } + + // 404 for everything else + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); }); +// ─── WebSocket server ─────────────────────────────────────────────────────── + const wss = new WebSocketServer({ server }); wss.on("connection", (ws, req) => { @@ -40,10 +129,10 @@ wss.on("connection", (ws, req) => { const room = rooms.get(roomId)!; // Add peer to room - const peerEntry = { peerId, ws }; + const peerEntry: PeerEntry = { peerId, ws }; room.add(peerEntry); - console.log(`[+] peer=${peerId} joined room=${roomId} (room size: ${room.size})`); + console.log(`[+] peer=${peerId} joined room=${roomId} via WebSocket (room size: ${room.size})`); // Send the new peer the list of existing peers const existingPeerIds = [...room].filter((p) => p.peerId !== peerId).map((p) => p.peerId); @@ -62,22 +151,7 @@ wss.on("connection", (ws, req) => { // Stamp the sender msg.from = peerId; - const serialized = JSON.stringify(msg); - - if (msg["to"] !== undefined) { - // Unicast to a specific peer - const target = [...room].find((p) => p.peerId === msg["to"]); - if (target?.ws.readyState === WebSocket.OPEN) { - target.ws.send(serialized); - } - } else { - // Broadcast to all peers in the room except sender - for (const peer of room) { - if (peer.peerId !== peerId && peer.ws.readyState === WebSocket.OPEN) { - peer.ws.send(serialized); - } - } - } + relayMessage(roomId, peerId, msg); }); ws.on("close", () => { @@ -89,12 +163,11 @@ wss.on("connection", (ws, req) => { rooms.delete(roomId); } else { // Notify remaining peers - const leaveMsg = JSON.stringify({ type: "peer-left", from: "server", payload: peerId }); - for (const peer of room) { - if (peer.ws.readyState === WebSocket.OPEN) { - peer.ws.send(leaveMsg); - } - } + broadcastToRoom(roomId, peerId, { + type: "peer-left", + from: "server", + payload: peerId, + }); } }); @@ -104,13 +177,331 @@ wss.on("connection", (ws, req) => { }); }); +// ─── Shared relay logic ───────────────────────────────────────────────────── + +/** + * Relay a signaling message within a room. + * Handles both unicast (msg.to is set) and broadcast. + * Delivers to both WebSocket and polling peers. + */ +function relayMessage( + roomId: string, + senderPeerId: string, + msg: { to?: string; from?: string; [key: string]: unknown } +): void { + const room = rooms.get(roomId); + if (!room) return; + + const serialized = JSON.stringify(msg); + + if (msg.to !== undefined) { + // Unicast to a specific peer + for (const peer of room) { + if (peer.peerId === msg.to) { + deliverToPeer(peer, serialized); + break; + } + } + } else { + // Broadcast to all peers except sender + for (const peer of room) { + if (peer.peerId !== senderPeerId) { + deliverToPeer(peer, serialized); + } + } + } +} + +/** + * Broadcast a server-originated message to all peers in a room except one. + */ +function broadcastToRoom( + roomId: string, + excludePeerId: string, + msg: Record +): void { + const room = rooms.get(roomId); + if (!room) return; + + const serialized = JSON.stringify(msg); + for (const peer of room) { + if (peer.peerId !== excludePeerId) { + deliverToPeer(peer, serialized); + } + } +} + +/** + * Deliver a serialized message to a peer, whether WebSocket or polling. + */ +function deliverToPeer(peer: PeerEntry, serialized: string): void { + if (peer.ws && peer.ws.readyState === WebSocket.OPEN) { + peer.ws.send(serialized); + } else if (peer.sessionId) { + const session = pollingSessions.get(peer.sessionId); + if (session) { + enqueuePollingMessage(session, serialized); + } + } +} + +// ─── Polling endpoint handlers ────────────────────────────────────────────── + +/** + * POST /poll/join + * Body: { room: string, peer: string } + * Response: { sessionId: string, peerList: string[] } + */ +function handlePollJoin(req: IncomingMessage, res: ServerResponse): void { + readJsonBody(req, (err, body: { room?: string; peer?: string } | null) => { + if (err || !body?.room || !body?.peer) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Missing room or peer" })); + return; + } + + const { room: roomId, peer: peerId } = body; + const sessionId = crypto.randomUUID(); + + // Ensure room exists + if (!rooms.has(roomId)) { + rooms.set(roomId, new Set()); + } + const room = rooms.get(roomId)!; + + // Build peer list BEFORE adding this peer + const peerList = [...room].filter((p) => p.peerId !== peerId).map((p) => p.peerId); + + // Create session + const session: PollingSession = { + sessionId, + peerId, + roomId, + messageQueue: [], + pendingResponse: null, + lastActivity: Date.now(), + }; + pollingSessions.set(sessionId, session); + + // Add peer to room + const peerEntry: PeerEntry = { peerId, sessionId }; + room.add(peerEntry); + + console.log( + `[+] peer=${peerId} joined room=${roomId} via HTTP polling ` + + `(session=${sessionId.slice(0, 8)}…, room size: ${room.size})` + ); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ sessionId, peerList })); + }); +} + +/** + * GET /poll/messages?session=&room= + * Long-polls for up to 30 seconds. Returns immediately if messages are queued. + * Response: { messages: string[] } + */ +function handlePollMessages(url: URL, res: ServerResponse): void { + const sessionId = url.searchParams.get("session"); + const roomId = url.searchParams.get("room"); + + if (!sessionId || !roomId) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Missing session or room" })); + return; + } + + const session = pollingSessions.get(sessionId); + if (!session || session.roomId !== roomId) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Session not found" })); + return; + } + + session.lastActivity = Date.now(); + + // If there are queued messages, return them immediately + if (session.messageQueue.length > 0) { + const messages = session.messageQueue.splice(0); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ messages })); + return; + } + + // Otherwise, hold the connection open until a message arrives or timeout + session.pendingResponse = res; + + const timer = setTimeout(() => { + if (session.pendingResponse === res) { + session.pendingResponse = null; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ messages: [] })); + } + }, LONG_POLL_TIMEOUT_MS); + + // Clean up if the client disconnects while waiting + res.on("close", () => { + clearTimeout(timer); + if (session.pendingResponse === res) { + session.pendingResponse = null; + } + }); +} + +/** + * POST /poll/send + * Body: { session: string, room: string, message: object } + * Relays the message to other peers in the room. + */ +function handlePollSend(req: IncomingMessage, res: ServerResponse): void { + readJsonBody(req, (err, body: { session?: string; room?: string; message?: any } | null) => { + if (err || !body?.session || !body?.room || !body?.message) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Missing session, room, or message" })); + return; + } + + const session = pollingSessions.get(body.session); + if (!session || session.roomId !== body.room) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Session not found" })); + return; + } + + session.lastActivity = Date.now(); + + // Stamp the sender + const msg = body.message; + msg.from = session.peerId; + + relayMessage(body.room, session.peerId, msg); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true })); + }); +} + +/** + * POST /poll/leave + * Body: { session: string, room: string } + * Graceful departure — cleans up immediately. + */ +function handlePollLeave(req: IncomingMessage, res: ServerResponse): void { + readJsonBody(req, (err, body: { session?: string; room?: string } | null) => { + if (err || !body?.session || !body?.room) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Missing session or room" })); + return; + } + + cleanupPollingSession(body.session); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true })); + }); +} + +// ─── Polling helpers ──────────────────────────────────────────────────────── + +/** + * Enqueue a message for a polling session. + * If there's a pending long-poll response, flush immediately. + */ +function enqueuePollingMessage(session: PollingSession, serialized: string): void { + if (session.pendingResponse !== null) { + // Long-poll is waiting — respond immediately with this message + const res = session.pendingResponse; + session.pendingResponse = null; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ messages: [serialized] })); + } else { + // Queue for next poll + session.messageQueue.push(serialized); + } +} + +/** + * Clean up a polling session: remove from room, notify peers, delete state. + */ +function cleanupPollingSession(sessionId: string): void { + const session = pollingSessions.get(sessionId); + if (!session) return; + + const { peerId, roomId } = session; + + // Cancel any pending long-poll response + if (session.pendingResponse !== null) { + try { + session.pendingResponse.writeHead(410, { "Content-Type": "application/json" }); + session.pendingResponse.end(JSON.stringify({ error: "Session expired" })); + } catch { + // Response may already be closed + } + session.pendingResponse = null; + } + + // Remove peer from room + const room = rooms.get(roomId); + if (room) { + for (const entry of room) { + if (entry.sessionId === sessionId) { + room.delete(entry); + break; + } + } + + if (room.size === 0) { + rooms.delete(roomId); + } else { + broadcastToRoom(roomId, peerId, { + type: "peer-left", + from: "server", + payload: peerId, + }); + } + } + + pollingSessions.delete(sessionId); + console.log( + `[-] peer=${peerId} left room=${roomId} via session cleanup ` + + `(session=${sessionId.slice(0, 8)}…)` + ); +} + +// ─── Utilities ────────────────────────────────────────────────────────────── + +/** + * Read and parse a JSON request body. + */ +function readJsonBody(req: IncomingMessage, cb: (err: Error | null, body: any) => void): void { + const chunks: Buffer[] = []; + req.on("data", (chunk: Buffer) => chunks.push(chunk)); + req.on("end", () => { + try { + const body = JSON.parse(Buffer.concat(chunks).toString()); + cb(null, body); + } catch (e) { + cb(e as Error, null); + } + }); + req.on("error", (err) => cb(err, null)); +} + +// ─── Start ────────────────────────────────────────────────────────────────── + server.listen(PORT, HOST, () => { console.log(`🚀 ZerithDB Signaling Server running at ws://${HOST}:${PORT}`); console.log(` HTTP health check: http://${HOST}:${PORT}`); + console.log(` HTTP long-polling: http://${HOST}:${PORT}/poll/*`); }); // Graceful shutdown process.on("SIGTERM", () => { console.log("Shutting down signaling server..."); + // Clean up all polling sessions + for (const [sessionId] of pollingSessions) { + cleanupPollingSession(sessionId); + } wss.close(() => server.close(() => process.exit(0))); }); diff --git a/package.json b/package.json index 458e0d73..43385139 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,14 @@ "devDependencies": { "@changesets/cli": "^2.27.7", "@types/node": "^22.0.0", + "happy-dom": "^20.9.0", "husky": "^9.1.4", "lint-staged": "^15.2.7", "prettier": "^3.3.3", "rimraf": "^6.0.1", "turbo": "^2.3.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "vitest": "^2.1.9" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ diff --git a/packages/core/src/internal/errors.ts b/packages/core/src/internal/errors.ts index a6771432..a58285a6 100644 --- a/packages/core/src/internal/errors.ts +++ b/packages/core/src/internal/errors.ts @@ -21,6 +21,7 @@ export const enum ErrorCode { NETWORK_PEER_TIMEOUT = "NETWORK_PEER_TIMEOUT", NETWORK_PEER_DISCONNECTED = "NETWORK_PEER_DISCONNECTED", NETWORK_WEBRTC_FAILED = "NETWORK_WEBRTC_FAILED", + NETWORK_TRANSPORT_DOWNGRADE = "NETWORK_TRANSPORT_DOWNGRADE", // Auth errors AUTH_KEY_GENERATION_FAILED = "AUTH_KEY_GENERATION_FAILED", diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 55cc292d..42bcf001 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -17,6 +17,15 @@ export interface SyncConfig { * @default 10 */ maxPeers?: number; + + /** + * Signaling transport preference. + * - `"auto"` — Try WebSocket first, fall back to HTTP long-polling (default) + * - `"websocket"` — WebSocket only (original behavior) + * - `"polling"` — HTTP long-polling only (for strict firewall environments) + * @default "auto" + */ + transport?: "auto" | "websocket" | "polling"; } export interface AuthConfig { diff --git a/packages/network/src/index.ts b/packages/network/src/index.ts index 77d3d157..48b96e0a 100644 --- a/packages/network/src/index.ts +++ b/packages/network/src/index.ts @@ -1 +1,4 @@ export { NetworkManager } from "./network-manager.js"; +export type { SignalingTransport } from "./signaling-transport.js"; +export { WebSocketTransport } from "./transports/websocket-transport.js"; +export { PollingTransport } from "./transports/polling-transport.js"; diff --git a/packages/network/src/network-manager.ts b/packages/network/src/network-manager.ts index e06e3430..5d797ebf 100644 --- a/packages/network/src/network-manager.ts +++ b/packages/network/src/network-manager.ts @@ -2,12 +2,16 @@ import SimplePeer from "simple-peer"; import type { ZerithDBConfig, PeerId, PeerInfo } from "zerithdb-core"; import { EventEmitter, ZerithDBError, ErrorCode } from "zerithdb-core"; import type { AuthManager } from "zerithdb-auth"; +import type { SignalingTransport } from "./signaling-transport.js"; +import { WebSocketTransport } from "./transports/websocket-transport.js"; +import { PollingTransport } from "./transports/polling-transport.js"; type NetworkEvents = { "peer:connected": PeerInfo; "peer:disconnected": { peerId: PeerId }; message: { type: string; payload: Uint8Array | string; from: PeerId }; error: { peerId: PeerId; error: Error }; + "transport:downgrade": { from: "websocket"; to: "polling"; reason: string }; }; interface SignalingMessage { @@ -23,9 +27,14 @@ interface SignalingMessage { * Architecture: Full mesh — every peer connects to every other peer. * The signaling server only handles the initial WebRTC handshake (ICE/SDP). * After that, all data flows peer-to-peer over encrypted WebRTC data channels. + * + * Supports automatic transport fallback: if WebSocket signaling is blocked + * (e.g. by corporate firewalls), the manager transparently downgrades to + * HTTP long-polling. */ export class NetworkManager extends EventEmitter { - private ws: WebSocket | null = null; + private transport: SignalingTransport | null = null; + private activeTransportType: "websocket" | "polling" | null = null; private readonly peers = new Map(); private readonly peerInfo = new Map(); private localPeerId: PeerId = crypto.randomUUID(); @@ -40,52 +49,60 @@ export class NetworkManager extends EventEmitter { super(); } + /** The transport type currently in use, or null if not connected */ + get transportType(): "websocket" | "polling" | null { + return this.activeTransportType; + } + /** * Connect to the signaling server and join the P2P room. * After connection, WebRTC handshakes happen automatically. + * + * Transport selection: + * - `"auto"` (default): Try WebSocket first with a 5s timeout. + * If it fails, fall back to HTTP long-polling. + * - `"websocket"`: WebSocket only. + * - `"polling"`: HTTP long-polling only. */ async connect(roomId: string): Promise { + const transportPref = this.config.sync?.transport ?? "auto"; const signalingUrl = this.config.sync?.signalingUrl ?? "wss://arpitkhandelwal810-zerith-signaling.hf.space"; - const url = `${signalingUrl}?room=${encodeURIComponent(roomId)}&peer=${this.localPeerId}`; - return new Promise((resolve, reject) => { + if (transportPref === "websocket") { + await this.connectWebSocket(signalingUrl, roomId); + } else if (transportPref === "polling") { + await this.connectPolling(signalingUrl, roomId); + } else { + // "auto" — try WebSocket first, fall back to polling try { - this.ws = new WebSocket(url); - } catch (err) { - reject( - new ZerithDBError( - ErrorCode.NETWORK_SIGNALING_FAILED, - `Failed to connect to signaling server: ${signalingUrl}`, - { cause: err } - ) - ); - return; - } - - this.ws.onopen = () => { - this.reconnectAttempts = 0; - resolve(); - }; - - this.ws.onerror = (err) => { - reject( - new ZerithDBError(ErrorCode.NETWORK_SIGNALING_FAILED, "WebSocket signaling error", { - cause: err, - }) + await this.connectWebSocket(signalingUrl, roomId); + } catch (wsError) { + const reason = + wsError instanceof Error ? wsError.message : "WebSocket connection failed"; + + this.emit("transport:downgrade", { + from: "websocket", + to: "polling", + reason, + }); + + console.warn( + `[ZerithDB] WebSocket signaling failed (${reason}). ` + + `Falling back to HTTP long-polling.` ); - }; - this.ws.onmessage = (event: MessageEvent) => { - this.handleSignalingMessage(JSON.parse(event.data) as SignalingMessage); - }; - - this.ws.onclose = () => { - if (!this.disposed) { - this.scheduleReconnect(roomId); + try { + await this.connectPolling(signalingUrl, roomId); + } catch (pollError) { + throw new ZerithDBError( + ErrorCode.NETWORK_SIGNALING_FAILED, + "Both WebSocket and HTTP long-polling signaling transports failed", + { cause: pollError } + ); } - }; - }); + } + } } /** @@ -134,13 +151,77 @@ export class NetworkManager extends EventEmitter { } this.peers.clear(); this.peerInfo.clear(); - if (this.ws !== null) { - this.ws.close(); - this.ws = null; + if (this.transport !== null) { + this.transport.close(); + this.transport = null; + } + this.activeTransportType = null; + } + + // ─── Private — Transport setup ──────────────────────────────────────────── + + private async connectWebSocket(signalingUrl: string, roomId: string): Promise { + const url = `${signalingUrl}?room=${encodeURIComponent(roomId)}&peer=${this.localPeerId}`; + + const wsTransport = new WebSocketTransport(); + await wsTransport.connect(url, 5000); + + this.attachTransport(wsTransport, roomId); + this.activeTransportType = "websocket"; + this.reconnectAttempts = 0; + } + + private async connectPolling(signalingUrl: string, roomId: string): Promise { + const httpUrl = this.wsUrlToHttp(signalingUrl); + + const pollTransport = new PollingTransport(httpUrl); + await pollTransport.connect(roomId, this.localPeerId); + + this.attachTransport(pollTransport, roomId); + this.activeTransportType = "polling"; + this.reconnectAttempts = 0; + } + + private attachTransport(transport: SignalingTransport, roomId: string): void { + // Clean up previous transport + if (this.transport !== null) { + this.transport.close(); + } + + this.transport = transport; + + transport.onMessage((data: string) => { + this.handleSignalingMessage(JSON.parse(data) as SignalingMessage); + }); + + transport.onClose(() => { + if (!this.disposed) { + this.scheduleReconnect(roomId); + } + }); + + transport.onError((err) => { + console.error("[ZerithDB] Signaling transport error:", err); + }); + } + + /** + * Convert a WebSocket URL to an HTTP URL. + * `wss://host/path` → `https://host/path` + * `ws://host/path` → `http://host/path` + */ + private wsUrlToHttp(wsUrl: string): string { + if (wsUrl.startsWith("wss://")) { + return "https://" + wsUrl.slice(6); + } + if (wsUrl.startsWith("ws://")) { + return "http://" + wsUrl.slice(5); } + // Already an HTTP URL — return as-is + return wsUrl; } - // ─── Private ────────────────────────────────────────────────────────────── + // ─── Private — Signaling message handling ───────────────────────────────── private handleSignalingMessage(msg: SignalingMessage): void { switch (msg.type) { @@ -191,7 +272,7 @@ export class NetworkManager extends EventEmitter { } peer.on("signal", (data) => { - this.ws?.send( + this.transport?.send( JSON.stringify({ type: initiator ? "offer" : "answer", from: this.localPeerId, diff --git a/packages/network/src/signaling-transport.ts b/packages/network/src/signaling-transport.ts new file mode 100644 index 00000000..92f33895 --- /dev/null +++ b/packages/network/src/signaling-transport.ts @@ -0,0 +1,24 @@ +/** + * Abstraction over the transport used for signaling communication. + * Both WebSocket and HTTP long-polling implement this interface, + * making them interchangeable from the NetworkManager's perspective. + */ +export interface SignalingTransport { + /** Send a JSON-serialised signaling message */ + send(message: string): void; + + /** Close the transport and release resources */ + close(): void; + + /** Register a handler for incoming messages */ + onMessage(handler: (data: string) => void): void; + + /** Register a handler for transport closure */ + onClose(handler: () => void): void; + + /** Register a handler for transport errors */ + onError(handler: (err: unknown) => void): void; + + /** Whether the transport is currently connected and able to send */ + readonly connected: boolean; +} diff --git a/packages/network/src/transports/polling-transport.ts b/packages/network/src/transports/polling-transport.ts new file mode 100644 index 00000000..a8bc9a0c --- /dev/null +++ b/packages/network/src/transports/polling-transport.ts @@ -0,0 +1,180 @@ +import type { SignalingTransport } from "../signaling-transport.js"; + +/** + * HTTP long-polling signaling transport. + * + * Used as a fallback when WebSockets are blocked by corporate firewalls. + * Implements the same SignalingTransport interface as WebSocketTransport, + * so the NetworkManager doesn't know which transport it's using. + * + * Protocol: + * POST /poll/join → { sessionId, peerList } + * GET /poll/messages?session=&room= → long-poll (30s timeout) + * POST /poll/send → relay a signaling message + */ +export class PollingTransport implements SignalingTransport { + private _connected = false; + private sessionId: string | null = null; + private roomId: string | null = null; + private polling = false; + private abortController: AbortController | null = null; + private messageHandler: ((data: string) => void) | null = null; + private closeHandler: (() => void) | null = null; + private errorHandler: ((err: unknown) => void) | null = null; + + /** + * @param baseUrl - HTTP(S) base URL of the signaling server (no trailing slash) + */ + constructor(private readonly baseUrl: string) {} + + get connected(): boolean { + return this._connected; + } + + /** + * Join a room via HTTP and start the long-polling loop. + * + * @param roomId - Room to join + * @param peerId - Local peer identifier + */ + async connect(roomId: string, peerId: string): Promise { + this.roomId = roomId; + + const res = await fetch(`${this.baseUrl}/poll/join`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ room: roomId, peer: peerId }), + }); + + if (!res.ok) { + throw new Error(`Polling join failed: ${res.status} ${res.statusText}`); + } + + const data = (await res.json()) as { sessionId: string; peerList: string[] }; + this.sessionId = data.sessionId; + this._connected = true; + + // Emit the peer-list message in the same format as WebSocket + const peerListMsg = JSON.stringify({ + type: "peer-list", + from: "server", + payload: data.peerList, + }); + // Defer so the caller can attach handlers before the first message fires + queueMicrotask(() => this.messageHandler?.(peerListMsg)); + + // Start long-polling loop + this.startPolling(); + } + + send(message: string): void { + if (!this._connected || this.sessionId === null || this.roomId === null) return; + + // Fire-and-forget — errors are handled in the catch + void fetch(`${this.baseUrl}/poll/send`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session: this.sessionId, + room: this.roomId, + message: JSON.parse(message), + }), + }).catch((err) => { + this.errorHandler?.(err); + }); + } + + close(): void { + this._connected = false; + this.polling = false; + if (this.abortController !== null) { + this.abortController.abort(); + this.abortController = null; + } + + // Best-effort leave notification + if (this.sessionId !== null && this.roomId !== null) { + void fetch(`${this.baseUrl}/poll/leave`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session: this.sessionId, room: this.roomId }), + }).catch(() => { + /* ignore — we're shutting down */ + }); + } + + this.sessionId = null; + this.roomId = null; + } + + onMessage(handler: (data: string) => void): void { + this.messageHandler = handler; + } + + onClose(handler: () => void): void { + this.closeHandler = handler; + } + + onError(handler: (err: unknown) => void): void { + this.errorHandler = handler; + } + + // ─── Private ────────────────────────────────────────────────────────────── + + private startPolling(): void { + this.polling = true; + void this.pollLoop(); + } + + private async pollLoop(): Promise { + while (this.polling && this._connected) { + try { + this.abortController = new AbortController(); + + const url = + `${this.baseUrl}/poll/messages` + + `?session=${encodeURIComponent(this.sessionId!)}` + + `&room=${encodeURIComponent(this.roomId!)}`; + + const res = await fetch(url, { + method: "GET", + signal: this.abortController.signal, + }); + + if (!res.ok) { + // Session expired or server error — treat as disconnect + if (res.status === 404 || res.status === 410) { + this._connected = false; + this.polling = false; + this.closeHandler?.(); + return; + } + // Transient error — wait before retrying + await this.delay(2000); + continue; + } + + const data = (await res.json()) as { messages: string[] }; + + for (const msg of data.messages) { + this.messageHandler?.(msg); + } + } catch (err: unknown) { + if (err instanceof DOMException && err.name === "AbortError") { + // Expected when close() is called during a poll + return; + } + + // Network error — wait before retrying + this.errorHandler?.(err); + if (this.polling) { + await this.delay(2000); + } + } + } + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/packages/network/src/transports/websocket-transport.ts b/packages/network/src/transports/websocket-transport.ts new file mode 100644 index 00000000..2653f742 --- /dev/null +++ b/packages/network/src/transports/websocket-transport.ts @@ -0,0 +1,112 @@ +import type { SignalingTransport } from "../signaling-transport.js"; + +/** + * WebSocket-based signaling transport. + * Extracts the existing raw WebSocket logic from NetworkManager + * into a standalone SignalingTransport implementation. + */ +export class WebSocketTransport implements SignalingTransport { + private ws: WebSocket | null = null; + private _connected = false; + private messageHandler: ((data: string) => void) | null = null; + private closeHandler: (() => void) | null = null; + private errorHandler: ((err: unknown) => void) | null = null; + + get connected(): boolean { + return this._connected; + } + + /** + * Open a WebSocket connection to the signaling server. + * Resolves when the connection is established, rejects on error or timeout. + * + * @param url - Full WebSocket URL including query parameters + * @param timeoutMs - Connection timeout in milliseconds + */ + connect(url: string, timeoutMs = 5000): Promise { + return new Promise((resolve, reject) => { + let settled = false; + + const timer = setTimeout(() => { + if (!settled) { + settled = true; + this.close(); + reject(new Error(`WebSocket connection timed out after ${timeoutMs}ms`)); + } + }, timeoutMs); + + try { + this.ws = new WebSocket(url); + } catch (err) { + clearTimeout(timer); + settled = true; + reject(err); + return; + } + + this.ws.onopen = () => { + if (!settled) { + settled = true; + clearTimeout(timer); + this._connected = true; + resolve(); + } + }; + + this.ws.onerror = (err) => { + if (!settled) { + settled = true; + clearTimeout(timer); + reject(err); + } else { + this.errorHandler?.(err); + } + }; + + this.ws.onmessage = (event: MessageEvent) => { + this.messageHandler?.(event.data); + }; + + this.ws.onclose = () => { + this._connected = false; + if (!settled) { + settled = true; + clearTimeout(timer); + reject(new Error("WebSocket closed before connection was established")); + } else { + this.closeHandler?.(); + } + }; + }); + } + + send(message: string): void { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(message); + } + } + + close(): void { + this._connected = false; + if (this.ws !== null) { + this.ws.onopen = null; + this.ws.onmessage = null; + this.ws.onerror = null; + this.ws.onclose = null; + this.ws.close(); + this.ws = null; + } + } + + onMessage(handler: (data: string) => void): void { + this.messageHandler = handler; + } + + onClose(handler: () => void): void { + this.closeHandler = handler; + } + + onError(handler: (err: unknown) => void): void { + this.errorHandler = handler; + } +} diff --git a/packages/sdk/src/create-app.ts b/packages/sdk/src/create-app.ts index d253b1eb..a7f2f389 100644 --- a/packages/sdk/src/create-app.ts +++ b/packages/sdk/src/create-app.ts @@ -72,6 +72,7 @@ export function createApp(config: ZerithDBConfig): ZerithDBApp { sync: { signalingUrl: "wss://signal.zerithdb.dev", maxPeers: 10, + transport: "auto", ...config.sync, }, auth: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 716e29c8..65671351 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.19.18 + happy-dom: + specifier: ^20.9.0 + version: 20.9.0 husky: specifier: ^9.1.4 version: 9.1.7 @@ -36,6 +39,9 @@ importers: typescript: specifier: ^5.5.4 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) apps/web: dependencies: @@ -128,7 +134,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) zerithdb-eslint-config: specifier: workspace:* version: link:../eslint-config @@ -180,7 +186,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) zerithdb-eslint-config: specifier: workspace:* version: link:../eslint-config @@ -210,7 +216,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) zerithdb-eslint-config: specifier: workspace:* version: link:../eslint-config @@ -247,7 +253,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) zerithdb-eslint-config: specifier: workspace:* version: link:../eslint-config @@ -283,7 +289,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) zerithdb-eslint-config: specifier: workspace:* version: link:../eslint-config @@ -357,7 +363,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) zerithdb-eslint-config: specifier: workspace:* version: link:../eslint-config @@ -400,7 +406,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) zerithdb-eslint-config: specifier: workspace:* version: link:../eslint-config @@ -423,7 +429,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) packages: @@ -1491,6 +1497,9 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -2088,6 +2097,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2507,6 +2520,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + happy-dom@20.9.0: + resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} + engines: {node: '>=20.0.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3835,6 +3852,10 @@ packages: jsdom: optional: true + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4843,6 +4864,8 @@ snapshots: '@types/uuid@10.0.0': {} + '@types/whatwg-mimetype@3.0.2': {} + '@types/ws@8.18.1': dependencies: '@types/node': 22.19.18 @@ -5462,6 +5485,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@7.0.1: {} + environment@1.1.0: {} err-code@3.0.1: {} @@ -6140,6 +6165,18 @@ snapshots: graphemer@1.4.0: {} + happy-dom@20.9.0: + dependencies: + '@types/node': 22.19.18 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -7493,7 +7530,7 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 - vitest@2.1.9(@types/node@22.19.18)(lightningcss@1.32.0): + vitest@2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0)) @@ -7517,6 +7554,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.18 + happy-dom: 20.9.0 transitivePeerDependencies: - less - lightningcss @@ -7528,6 +7566,8 @@ snapshots: - supports-color - terser + whatwg-mimetype@3.0.0: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/turbo.json b/turbo.json index 03839e60..759db0e6 100644 --- a/turbo.json +++ b/turbo.json @@ -1,34 +1,60 @@ { "$schema": "https://turbo.build/schema.json", "ui": "tui", + "concurrency": "24", "tasks": { "build": { - "dependsOn": ["^build"], - "inputs": ["$TURBO_DEFAULT$", ".env*"], - "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + "dependsOn": [ + "^build" + ], + "inputs": [ + "$TURBO_DEFAULT$", + ".env*" + ], + "outputs": [ + "dist/**", + ".next/**", + "!.next/cache/**" + ] }, "dev": { "cache": false, "persistent": true }, "test": { - "dependsOn": ["^build"], - "outputs": ["coverage/**"], - "env": ["CI"] + "dependsOn": [ + "^build" + ], + "outputs": [ + "coverage/**" + ], + "env": [ + "CI" + ] }, "test:e2e": { - "dependsOn": ["^build"], - "outputs": ["playwright-report/**"], - "env": ["CI"] + "dependsOn": [ + "^build" + ], + "outputs": [ + "playwright-report/**" + ], + "env": [ + "CI" + ] }, "lint": { - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "typecheck": { - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "clean": { "cache": false } } -} +} \ No newline at end of file From aa5e894cc4b20366004e242726e31fd4b695ba64 Mon Sep 17 00:00:00 2001 From: Gowtham sree charan reddy Date: Fri, 15 May 2026 01:41:55 +0530 Subject: [PATCH 011/262] feat(react): fix useQuery to use dexie liveQuery and add useAuth/useSync hooks --- packages/react/package.json | 1 + packages/react/src/index.tsx | 91 ++++++++++++++++++++++++++++-------- pnpm-lock.yaml | 3 ++ 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index c45bc886..cc056d83 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -21,6 +21,7 @@ "clean": "rimraf dist" }, "dependencies": { + "dexie": "^4.0.8", "zerithdb-sdk": "workspace:*" }, "peerDependencies": { diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index f714a99a..05429c54 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -1,8 +1,9 @@ import React, { createContext, useContext, useEffect, useState, useMemo } from "react"; import { createApp } from "zerithdb-sdk"; -import type { ZerithDBConfig } from "zerithdb-sdk"; +import type { ZerithDBApp, ZerithDBConfig } from "zerithdb-sdk"; +import { liveQuery } from "dexie"; -const ZerithContext = createContext(null); +const ZerithContext = createContext(null); export interface ZerithProviderProps { config: ZerithDBConfig; @@ -20,9 +21,9 @@ export const ZerithProvider: React.FC = ({ config, children }; /** - * Access the underlying ZerithDB client directly. + * Access the underlying ZerithDB app client directly. */ -export const useZerith = () => { +export const useZerith = (): ZerithDBApp => { const context = useContext(ZerithContext); if (!context) { throw new Error("useZerith must be used within a ZerithProvider"); @@ -34,38 +35,90 @@ export const useZerith = () => { * Reactive hook to query a collection. * Automatically updates when local or remote (P2P) changes occur. */ -export function useQuery(collectionName: string) { - const db = useZerith() as any; +export function useQuery>(collectionName: string, filter: any = {}) { + const app = useZerith(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { - let mounted = true; - - const collection = db.collection(collectionName); - - // Subscribe to real-time updates (CRDT merges) - const unsubscribe = collection.subscribe((docs: any[]) => { - if (mounted) { + const collection = app.db(collectionName); + + // Use Dexie's liveQuery to reactively observe local DB changes + // (which also includes remote P2P updates applied by the sync engine) + const observable = liveQuery(() => collection.find(filter)); + + const subscription = observable.subscribe({ + next: (docs) => { setData(docs as T[]); setLoading(false); + }, + error: (err) => { + setError(err); + setLoading(false); } }); return () => { - mounted = false; - unsubscribe(); + subscription.unsubscribe(); }; - }, [db, collectionName]); + }, [app, collectionName, JSON.stringify(filter)]); - const insert = async (item: Partial) => { - return db.collection(collectionName).insert(item); + const insert = async (item: T) => { + return app.db(collectionName).insert(item); }; const remove = async (id: string) => { - return db.collection(collectionName).delete(id); + return app.db(collectionName).delete({ _id: id } as any); }; return { data, loading, error, insert, remove }; } + +/** + * Hook to access and manage P2P sync state + */ +export function useSync() { + const app = useZerith(); + const [state, setState] = useState(() => app.sync.state); + + useEffect(() => { + const handleStateChange = (newState: any) => setState(newState); + app.sync.on("state:change", handleStateChange); + return () => { + app.sync.off("state:change", handleStateChange); + }; + }, [app]); + + return { + state, + enable: () => app.sync.enable(), + disable: () => app.sync.disable() + }; +} + +/** + * Hook to manage authentication and identity + */ +export function useAuth() { + const app = useZerith(); + const [identity, setIdentity] = useState(() => app.auth.identity); + + useEffect(() => { + // Identity changes aren't explicitly emitted, but we update on signIn/signOut + setIdentity(app.auth.identity); + }, [app, app.auth.identity]); + + const signIn = async () => { + const id = await app.auth.signIn(); + setIdentity(id); + return id; + }; + + const signOut = async () => { + await app.auth.signOut(); + setIdentity(null); + }; + + return { identity, signIn, signOut }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 716e29c8..440acaa4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -293,6 +293,9 @@ importers: packages/react: dependencies: + dexie: + specifier: ^4.0.8 + version: 4.4.2 zerithdb-sdk: specifier: workspace:* version: link:../sdk From d6f0ce3dd23ddd267d9fe1bc257261626dbba1a4 Mon Sep 17 00:00:00 2001 From: Koustav Manna Date: Fri, 15 May 2026 01:56:15 +0530 Subject: [PATCH 012/262] fixed issue #299 with aditional features --- apps/web/package.json | 3 ++- apps/web/src/app/page.tsx | 15 ++++++++++-- apps/web/src/components/ToasterProvider.tsx | 7 ++++++ pnpm-lock.yaml | 26 +++++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/ToasterProvider.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 7776e772..06d6f9e2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,7 +13,8 @@ "lucide-react": "^1.14.0", "next": "16.2.6", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "react-hot-toast": "^2.6.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 27082986..666a933c 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,6 +1,7 @@ "use client"; import Link from "next/link"; +import toast from "react-hot-toast"; import { ArrowRight, Database, @@ -682,14 +683,24 @@ export default function LandingPage() { GitHub - + toast("Blog will be available soon")} + className="hover:text-gray-900 transition-colors cursor-pointer" + > Blog - + toast("Pricing will be available soon")} + className="hover:text-gray-900 transition-colors cursor-pointer" + > Pricing diff --git a/apps/web/src/components/ToasterProvider.tsx b/apps/web/src/components/ToasterProvider.tsx new file mode 100644 index 00000000..51502769 --- /dev/null +++ b/apps/web/src/components/ToasterProvider.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { Toaster } from "react-hot-toast"; + +export default function ToasterProvider() { + return ; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 716e29c8..80039289 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + react-hot-toast: + specifier: ^2.6.0 + version: 2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -2497,6 +2500,11 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + goober@2.1.19: + resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==} + peerDependencies: + csstype: ^3.0.10 + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3311,6 +3319,13 @@ packages: peerDependencies: react: ^19.2.4 + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -6134,6 +6149,10 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + goober@2.1.19(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -6873,6 +6892,13 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-hot-toast@2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + csstype: 3.2.3 + goober: 2.1.19(csstype@3.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is@16.13.1: {} react@18.3.1: From c43dd9f697dec55d1be435aff1fdf53d27303e81 Mon Sep 17 00:00:00 2001 From: Gowtham sree charan reddy Date: Fri, 15 May 2026 02:01:59 +0530 Subject: [PATCH 013/262] fix(react): address copilot review feedback --- packages/auth/src/auth-manager.ts | 26 ++++++++++++++------- packages/react/src/index.tsx | 38 +++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/auth/src/auth-manager.ts b/packages/auth/src/auth-manager.ts index 7214e413..8ba8e002 100644 --- a/packages/auth/src/auth-manager.ts +++ b/packages/auth/src/auth-manager.ts @@ -1,22 +1,27 @@ import * as ed from "@noble/ed25519"; import { sha512 } from "@noble/hashes/sha2.js"; import type { ZerithDBConfig, Identity, Signature } from "zerithdb-core"; -import { ZerithDBError, ErrorCode } from "zerithdb-core"; +import { ZerithDBError, ErrorCode, EventEmitter } from "zerithdb-core"; // noble/ed25519 requires a sha512 implementation ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); +type AuthEvents = { + "identity:change": Identity | null; +}; + /** * Manages the local Ed25519 keypair identity for this ZerithDB instance. * Identities are stored in localStorage as hex-encoded keys. * No servers involved — identity is fully self-sovereign. */ -export class AuthManager { +export class AuthManager extends EventEmitter { private readonly storageKey: string; private _identity: Identity | null = null; private privateKeyBytes: Uint8Array | null = null; constructor(config: ZerithDBConfig) { + super(); this.storageKey = config.auth?.storageKey ?? "__zerithdb_identity"; } @@ -34,6 +39,7 @@ export class AuthManager { if (stored !== null) { this._identity = stored.identity; this.privateKeyBytes = stored.privateKeyBytes; + this.emit("identity:change", this._identity); return this._identity; } @@ -53,6 +59,7 @@ export class AuthManager { this.privateKeyBytes = privateKey; this.saveToStorage(privateKey, publicKeyBytes); + this.emit("identity:change", identity); return identity; } @@ -100,12 +107,15 @@ export class AuthManager { /** Sign out and clear the stored identity */ signOut(): void { - this._identity = null; - this.privateKeyBytes = null; - try { - localStorage.removeItem(this.storageKey); - } catch { - // localStorage may not be available in all environments + if (this._identity !== null) { + this._identity = null; + this.privateKeyBytes = null; + try { + localStorage.removeItem(this.storageKey); + } catch { + // localStorage may not be available in all environments + } + this.emit("identity:change", null); } } diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 05429c54..16109c13 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useEffect, useState, useMemo } from "react"; import { createApp } from "zerithdb-sdk"; -import type { ZerithDBApp, ZerithDBConfig } from "zerithdb-sdk"; +import type { ZerithDBApp, ZerithDBConfig, QueryFilter } from "zerithdb-sdk"; import { liveQuery } from "dexie"; const ZerithContext = createContext(null); @@ -31,22 +31,35 @@ export const useZerith = (): ZerithDBApp => { return context; }; +// Helper hook to deep-compare the filter to avoid unnecessary re-subscriptions +function useDeepCompareMemoize(value: T) { + const ref = React.useRef(value); + if (JSON.stringify(value) !== JSON.stringify(ref.current)) { + ref.current = value; + } + return ref.current; +} + /** * Reactive hook to query a collection. * Automatically updates when local or remote (P2P) changes occur. + * @param collectionName The name of the collection to query + * @param filter A MongoDB-style query filter. Must be JSON-serializable. */ -export function useQuery>(collectionName: string, filter: any = {}) { +export function useQuery>(collectionName: string, filter: QueryFilter = {}) { const app = useZerith(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const memoizedFilter = useDeepCompareMemoize(filter); + useEffect(() => { const collection = app.db(collectionName); // Use Dexie's liveQuery to reactively observe local DB changes // (which also includes remote P2P updates applied by the sync engine) - const observable = liveQuery(() => collection.find(filter)); + const observable = liveQuery(() => collection.find(memoizedFilter)); const subscription = observable.subscribe({ next: (docs) => { @@ -62,14 +75,17 @@ export function useQuery>(collectionName: string, return () => { subscription.unsubscribe(); }; - }, [app, collectionName, JSON.stringify(filter)]); + }, [app, collectionName, memoizedFilter]); const insert = async (item: T) => { return app.db(collectionName).insert(item); }; const remove = async (id: string) => { - return app.db(collectionName).delete({ _id: id } as any); + const collection = app.db(collectionName); + type DeleteFilter = Parameters[0]; + const deleteFilter = { _id: id } as unknown as DeleteFilter; + return collection.delete(deleteFilter); }; return { data, loading, error, insert, remove }; @@ -105,19 +121,23 @@ export function useAuth() { const [identity, setIdentity] = useState(() => app.auth.identity); useEffect(() => { - // Identity changes aren't explicitly emitted, but we update on signIn/signOut + const handleIdentityChange = (newIdentity: any) => setIdentity(newIdentity); + app.auth.on("identity:change", handleIdentityChange); + // Initialize in case it changed between render and effect setIdentity(app.auth.identity); - }, [app, app.auth.identity]); + return () => { + app.auth.off("identity:change", handleIdentityChange); + }; + }, [app]); const signIn = async () => { const id = await app.auth.signIn(); - setIdentity(id); + // No need to setIdentity here, the event listener will handle it return id; }; const signOut = async () => { await app.auth.signOut(); - setIdentity(null); }; return { identity, signIn, signOut }; From a2cb1dc71557381ab4b9ed6f80f8e57ab006992f Mon Sep 17 00:00:00 2001 From: Koustav Manna Date: Fri, 15 May 2026 02:10:26 +0530 Subject: [PATCH 014/262] Added toaster in layout --- apps/web/src/app/layout.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index c6f3e4d3..9ddb33c5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import ToasterProvider from "@/components/ToasterProvider"; const inter = Inter({ subsets: ["latin"], @@ -23,6 +24,7 @@ export default function RootLayout({ {children} + ); From e0f52388bc9dd9ffdf355fb6e64b23ff39ccfc4b Mon Sep 17 00:00:00 2001 From: Aakash Prajapati Date: Fri, 15 May 2026 02:18:54 +0530 Subject: [PATCH 015/262] refactor(playground): optimize client components and fix message overflow --- apps/web/src/app/playground/page.tsx | 371 ++++++++++++++------------- 1 file changed, 199 insertions(+), 172 deletions(-) diff --git a/apps/web/src/app/playground/page.tsx b/apps/web/src/app/playground/page.tsx index 7a981dcc..1b19e167 100644 --- a/apps/web/src/app/playground/page.tsx +++ b/apps/web/src/app/playground/page.tsx @@ -4,64 +4,198 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { ArrowLeft, Wifi, WifiOff, Laptop, Save, Database, ArrowRightLeft } from "lucide-react"; +type ClientId = "A" | "B"; + type Note = { id: string; text: string; + senderId: ClientId; timestamp: number; }; -export default function PlaygroundPage() { - const [isOnline, setIsOnline] = useState(true); +type Client = { + id: ClientId; + name: string; + color: string; + notes: Note[]; + input: string; +}; + +const dummyMessage: Note = { + id: "1", + text: "Hello from Client A!", + senderId: "A", + timestamp: Date.now(), +}; + +const CLIENTS: Omit[] = [ + { id: "A", name: "Client A", color: "blue" }, + { id: "B", name: "Client B", color: "purple" }, +]; + +const INSTRUCTIONS = [ + "Type a message in Browser A and click Save. See it instantly sync to Browser B.", + { + text: "Click the Network: Online button to simulate going offline.", + highlight: "Network: Online", + highlightColor: "red", + }, + "Create different notes in Browser A and Browser B. Notice they don't sync.", + { + text: "Click Network: Offline to reconnect. Watch the CRDT engine automatically merge the states perfectly!", + highlight: "Network: Offline", + highlightColor: "green", + }, +]; - // Simulated local state for Client A and Client B - const [clientA, setClientA] = useState(() => [ - { id: "1", text: "Hello from Client A!", timestamp: Date.now() }, - ]); - const [clientB, setClientB] = useState(() => [ - { id: "1", text: "Hello from Client A!", timestamp: Date.now() }, - ]); +// Separate component for better scalability +function ClientCard({ + client, + onAddNote, + onUpdateInput, +}: { + client: Client; + onAddNote: (id: ClientId, text: string) => void; + onUpdateInput: (id: ClientId, value: string) => void; +}) { + const textColor = client.color === "blue" ? "text-blue-400" : "text-purple-400"; + const focusRingColor = + client.color === "blue" + ? "focus:ring-blue-500/20 focus:border-blue-500" + : "focus:ring-purple-500/20 focus:border-purple-500"; + + return ( +
+
+
+ + + Browser {client.id} ({client.name}) + +
+
+ IndexedDB Active +
+
+ +
+ {client.notes.length === 0 ? ( +
+ No documents. Type below to create one. +
+ ) : ( +
+ {client.notes.map((note) => ( +
+

{note.text}

+

+ ID: {note.id} • {new Date(note.timestamp).toLocaleTimeString()} +

+
+ ))} +
+ )} +
- const [inputA, setInputA] = useState(""); - const [inputB, setInputB] = useState(""); +
+
{ + e.preventDefault(); + onAddNote(client.id, client.input); + }} + className="flex gap-2" + > + onUpdateInput(client.id, e.target.value)} + placeholder="Type a message offline/online..." + className={`flex-1 px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 transition-all ${focusRingColor}`} + /> + +
+
+
+ ); +} +export default function PlaygroundPage() { + const [isOnline, setIsOnline] = useState(true); const [syncCount, setSyncCount] = useState(0); + // Initialize clients + const [clients, setClients] = useState(() => + CLIENTS.map((client) => ({ + ...client, + notes: [dummyMessage], + input: "", + })) + ); + // Sync logic simulation useEffect(() => { - if (isOnline) { - // Very basic CRDT mock: union of both sets based on ID, highest timestamp wins - const merged = [...clientA, ...clientB].reduce((acc, curr) => { - const existing = acc.find((n) => n.id === curr.id); - if (!existing) { - acc.push(curr); - } else if (curr.timestamp > existing.timestamp) { - existing.text = curr.text; - existing.timestamp = curr.timestamp; - } - return acc; - }, [] as Note[]); - - // Only update if there's an actual difference to prevent infinite loops - if (JSON.stringify(merged) !== JSON.stringify(clientA)) setClientA(merged); // eslint-disable-line react-hooks/set-state-in-effect - if (JSON.stringify(merged) !== JSON.stringify(clientB)) setClientB(merged); - - if (clientA.length > 0 || clientB.length > 0) { - setSyncCount((prev) => prev + 1); + if (!isOnline) return; + // Very basic CRDT mock: union of both sets based on ID, highest timestamp wins + // Merge all notes from all clients + const allNotes = clients.flatMap((c) => c.notes); + const merged = allNotes.reduce((acc, curr) => { + const existing = acc.find((n) => n.id === curr.id); + if (!existing) { + acc.push(curr); + } else if (curr.timestamp > existing.timestamp) { + existing.text = curr.text; + existing.timestamp = curr.timestamp; + existing.senderId = curr.senderId; } + return acc; + }, [] as Note[]); + + // Update all clients with merged data if changed + let hasChanges = false; + const newClients = clients.map((client) => { + if (JSON.stringify(client.notes) !== JSON.stringify(merged)) { + hasChanges = true; + return { ...client, notes: merged }; + } + return client; + }); + + if (hasChanges) { + setClients(newClients); // eslint-disable-line react-hooks/set-state-in-effect + setSyncCount((prev) => prev + 1); } - }, [clientA, clientB, isOnline]); + }, [clients, isOnline]); - const addNote = (client: "A" | "B", text: string) => { + const addNote = (clientId: ClientId, text: string) => { if (!text.trim()) return; - const newNote = { id: Math.random().toString(36).substring(7), text, timestamp: Date.now() }; - - if (client === "A") { - setClientA([...clientA, newNote]); - setInputA(""); - } else { - setClientB([...clientB, newNote]); - setInputB(""); - } + const newNote: Note = { + id: Math.random().toString(36).substring(7), + text, + timestamp: Date.now(), + senderId: clientId, + }; + + setClients((prev) => + prev.map((client) => + client.id === clientId + ? { ...client, notes: [...client.notes, newNote], input: "" } + : client + ) + ); + }; + + const updateInput = (clientId: ClientId, value: string) => { + setClients((prev) => + prev.map((client) => (client.id === clientId ? { ...client, input: value } : client)) + ); }; return ( @@ -107,143 +241,36 @@ export default function PlaygroundPage() { {/* MAIN PLAYGROUND */}
- {/* CLIENT A */} -
-
-
- - Browser A (Alice) -
-
- IndexedDB Active -
-
- -
- {clientA.length === 0 ? ( -
- No documents. Type below to create one. -
- ) : ( -
- {clientA.map((note) => ( -
-

{note.text}

-

- ID: {note.id} • {new Date(note.timestamp).toLocaleTimeString()} -

-
- ))} -
- )} -
- -
-
{ - e.preventDefault(); - addNote("A", inputA); - }} - className="flex gap-2" - > - setInputA(e.target.value)} - placeholder="Type a message offline/online..." - className="flex-1 px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" - /> - -
-
-
- - {/* CLIENT B */} -
-
-
- - Browser B (Bob) -
-
- IndexedDB Active -
-
- -
- {clientB.length === 0 ? ( -
- No documents. Type below to create one. -
- ) : ( -
- {clientB.map((note) => ( -
-

{note.text}

-

- ID: {note.id} • {new Date(note.timestamp).toLocaleTimeString()} -

-
- ))} -
- )} -
- -
-
{ - e.preventDefault(); - addNote("B", inputB); - }} - className="flex gap-2" - > - setInputB(e.target.value)} - placeholder="Type a message offline/online..." - className="flex-1 px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all" - /> - -
-
-
+ {clients.map((client) => ( + + ))}
{/* INFO FOOTER */}

How to test the Playground:

    -
  • - 1. Type a message in Browser A and click Save. See it instantly sync to Browser B. -
  • -
  • - 2. Click the Network: Online button to - simulate going offline. -
  • -
  • - 3. Create different notes in Browser A and Browser B. Notice they don't sync. -
  • -
  • - 4. Click Network: Offline to reconnect. - Watch the CRDT engine automatically merge the states perfectly! -
  • + {INSTRUCTIONS.map((instruction, index) => ( +
  • + {index + 1}.{" "} + {typeof instruction === "string" ? ( + instruction + ) : ( + <> + Click the{" "} + + {instruction.highlight} + {" "} + {instruction.text.replace(instruction.highlight, "").trim()} + + )} +
  • + ))}
From 60d0ed6c41012699df51e2743ba52d57081419c6 Mon Sep 17 00:00:00 2001 From: Devesh Date: Fri, 15 May 2026 03:01:51 +0530 Subject: [PATCH 016/262] docs(infra): add bug report and feature request templates --- .github/ISSUE_TEMPLATE/bug_report.md | 37 +++++++++++++++++++ ...feature_request.yml => feature_request.md} | 8 +++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md rename .github/ISSUE_TEMPLATE/{feature_request.yml => feature_request.md} (75%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..be725902 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: "🐛 Bug Report" +about: "Report a bug or unexpected behavior in ZerithDB" +title: "[BUG] " +labels: ["bug"] +assignees: "" +--- + +## Describe the Bug + + + +## To Reproduce + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Expected Behavior + + + +## Screenshots/Logs + + + +## Environment + +- ZerithDB Version: [e.g. 0.1.0] +- OS: [e.g. Windows, macOS] +- Browser/Node Version: [e.g. Chrome 120, Node 20.x] + +## Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.md similarity index 75% rename from .github/ISSUE_TEMPLATE/feature_request.yml rename to .github/ISSUE_TEMPLATE/feature_request.md index fcaf1a80..74ec6b17 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,9 @@ --- name: "✨ Feature Request" -about: "Suggest a new feature or improvement" +about: "Suggest a new feature or improvement for ZerithDB" +title: "[FEAT] " labels: ["enhancement"] +assignees: "" --- ## Problem Statement @@ -12,6 +14,10 @@ labels: ["enhancement"] +## Use Cases + + + ## API Design (if applicable) ```typescript From c75c33d1b5d88a77d31006c4add1ed189d4d756e Mon Sep 17 00:00:00 2001 From: KGFCH2 Date: Fri, 15 May 2026 05:30:09 +0530 Subject: [PATCH 017/262] chore(cli): improve init command success message with next steps --- packages/cli/src/commands/init.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 239e67b2..b413ed68 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -105,12 +105,16 @@ export async function initCommand( console.log(` ${chalk.green("✔")} ${chalk.bold("Your ZerithDB app is ready!")} - ${chalk.gray("Next steps:")} - ${chalk.cyan(`cd ${appName}`)} - ${chalk.cyan("npm run dev")} + ${chalk.gray("Get started:")} + ${chalk.cyan(`1. cd ${appName}`)} + ${chalk.cyan("2. npm run dev")} -${chalk.gray("Docs:")} https://zerithdb.dev/docs -${chalk.gray("Discord:")} https://discord.gg/MhvuDvzWfF + ${chalk.gray("What's next?")} + - Open ${chalk.blue("src/app/page.tsx")} to see the ZerithDB SDK in action. + - Check out the ${chalk.blue("Live Playground")} at https://zerithdb.dev/playground + - Need help? Join our ${chalk.blue("Discord")}: https://discord.gg/MhvuDvzWfF + +${chalk.gray("Documentation:")} https://zerithdb.dev/docs `); } From 34663b584012e57e5742e6bfd316fce862bc4ab5 Mon Sep 17 00:00:00 2001 From: KGFCH2 Date: Fri, 15 May 2026 05:30:30 +0530 Subject: [PATCH 018/262] test(core): add unit tests for EventEmitter --- tests/unit/event-emitter.test.ts | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/unit/event-emitter.test.ts diff --git a/tests/unit/event-emitter.test.ts b/tests/unit/event-emitter.test.ts new file mode 100644 index 00000000..406add87 --- /dev/null +++ b/tests/unit/event-emitter.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi } from "vitest"; +import { EventEmitter } from "zerithdb-core"; + +describe("EventEmitter", () => { + it("should emit events to listeners", () => { + const emitter = new EventEmitter<{ foo: string }>(); + const listener = vi.fn(); + + emitter.on("foo", listener); + emitter.emit("foo", "bar"); + + expect(listener).toHaveBeenCalledWith("bar"); + }); + + it("should handle multiple listeners", () => { + const emitter = new EventEmitter<{ foo: string }>(); + const l1 = vi.fn(); + const l2 = vi.fn(); + + emitter.on("foo", l1); + emitter.on("foo", l2); + emitter.emit("foo", "bar"); + + expect(l1).toHaveBeenCalledWith("bar"); + expect(l2).toHaveBeenCalledWith("bar"); + }); + + it("should remove listeners with off()", () => { + const emitter = new EventEmitter<{ foo: string }>(); + const listener = vi.fn(); + + emitter.on("foo", listener); + emitter.off("foo", listener); + emitter.emit("foo", "bar"); + + expect(listener).not.toHaveBeenCalled(); + }); + + it("should handle once() listeners", () => { + const emitter = new EventEmitter<{ foo: string }>(); + const listener = vi.fn(); + + emitter.once("foo", listener); + emitter.emit("foo", "bar"); + emitter.emit("foo", "baz"); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith("bar"); + }); + + it("should remove all listeners for an event", () => { + const emitter = new EventEmitter<{ foo: string; bar: number }>(); + const l1 = vi.fn(); + const l2 = vi.fn(); + + emitter.on("foo", l1); + emitter.on("bar", l2); + emitter.removeAllListeners("foo"); + + emitter.emit("foo", "hello"); + emitter.emit("bar", 42); + + expect(l1).not.toHaveBeenCalled(); + expect(l2).toHaveBeenCalledWith(42); + }); +}); From 4021b36957b351aaa5b1f8d915a1067faca02245 Mon Sep 17 00:00:00 2001 From: KGFCH2 Date: Fri, 15 May 2026 05:30:47 +0530 Subject: [PATCH 019/262] docs: add Why ZerithDB section to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 56a0a744..c94cd91c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,17 @@ your users' browsers form a resilient, encrypted mesh network. --- +## Why ZerithDB? + +Traditional backend infrastructure is complex, expensive, and centralized. ZerithDB offers a different path: + +- **Infinite Scalability:** Your infrastructure scales with your users. Each new user adds computing and storage power to the network. +- **Zero Latency:** Data is read and written to a local database (IndexedDB) instantly. Sync happens in the background. +- **Privacy by Design:** Data is encrypted end-to-end. Since there's no central server, there's no single point of failure or data breach. +- **Development Speed:** Go from `npx zerithdb init` to a live, syncing app in minutes. Focus on your UI, not your API. + +--- + ## The 30-Second Demo ```typescript From 850a6c83ebc6070f249f22cd38f7800e59d9525c Mon Sep 17 00:00:00 2001 From: KGFCH2 Date: Fri, 15 May 2026 05:31:05 +0530 Subject: [PATCH 020/262] refactor(db): add clear() alias to CollectionClient --- packages/db/src/db-client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/db/src/db-client.ts b/packages/db/src/db-client.ts index 9d247dfb..01c51b44 100644 --- a/packages/db/src/db-client.ts +++ b/packages/db/src/db-client.ts @@ -167,6 +167,11 @@ export class CollectionClient = Record { + return this.clearAll(); + } + /** * Count documents matching a filter. */ From abbc55c870881542b773dc50b15510e5128164fb Mon Sep 17 00:00:00 2001 From: Anuj Date: Fri, 15 May 2026 07:55:02 +0530 Subject: [PATCH 021/262] Adding Hot reloading for P2P sync protocols --- packages/core/src/index.ts | 2 +- packages/core/src/types/sync.ts | 20 +++++++ packages/sync/src/sync-engine.ts | 95 +++++++++++++++++++++++++++++--- 3 files changed, 108 insertions(+), 9 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 46286f79..3254029c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,4 +16,4 @@ export type { } from "./types/db.js"; export type { PeerId, PeerInfo, RoomId, NetworkMessage } from "./types/network.js"; export type { Identity, PublicKey, Signature } from "./types/auth.js"; -export type { SyncUpdate, SyncState, AwarenessState } from "./types/sync.js"; +export type { SyncUpdate, SyncState, AwarenessState, SyncPlugin } from "./types/sync.js"; diff --git a/packages/core/src/types/sync.ts b/packages/core/src/types/sync.ts index 31ea161f..3868bd7d 100644 --- a/packages/core/src/types/sync.ts +++ b/packages/core/src/types/sync.ts @@ -16,3 +16,23 @@ export interface AwarenessState { cursor?: { line: number; column: number }; [key: string]: unknown; } + +export interface SyncPlugin { + id: string; + version: number; + /** + * Hook to transform/resolve conflicts before applying a remote update + */ + onBeforeApplyUpdate?: ( + collectionName: string, + update: Uint8Array, + fromPeer: string + ) => Uint8Array | null | Promise; + /** + * Hook to transform a local update before broadcasting + */ + onBeforeSendUpdate?: ( + collectionName: string, + update: Uint8Array + ) => Uint8Array | null | Promise; +} diff --git a/packages/sync/src/sync-engine.ts b/packages/sync/src/sync-engine.ts index 9736c02c..99bf89c8 100644 --- a/packages/sync/src/sync-engine.ts +++ b/packages/sync/src/sync-engine.ts @@ -1,6 +1,6 @@ import * as Y from "yjs"; import { IndexeddbPersistence } from "y-indexeddb"; -import type { ZerithDBConfig, SyncState } from "zerithdb-core"; +import type { ZerithDBConfig, SyncState, SyncPlugin } from "zerithdb-core"; import { EventEmitter } from "zerithdb-core"; import type { DbClient } from "zerithdb-db"; import type { NetworkManager } from "zerithdb-network"; @@ -21,6 +21,9 @@ export class SyncEngine extends EventEmitter { private readonly persistences = new Map(); private _enabled = false; private _state: SyncState = { synced: false, pendingUpdates: 0, connectedPeers: 0 }; + private plugins = new Map(); + private activePluginVersion = 1; + constructor( private readonly config: ZerithDBConfig, @@ -49,6 +52,40 @@ export class SyncEngine extends EventEmitter { this.updateState({ synced: false }); } + /** + * Register a synchronization plugin directly. + */ + registerPlugin(plugin: SyncPlugin): void { + this.plugins.set(plugin.id, plugin); + if (plugin.version > this.activePluginVersion) { + this.activePluginVersion = plugin.version; + } + } + + /** + * Dynamically load and register a plugin from a URL. + */ + async loadPlugin(pluginUrl: string): Promise { + try { + const module = await import(pluginUrl); + const plugin = module.default as SyncPlugin; + this.registerPlugin(plugin); + } catch (err) { + console.error(`Failed to load plugin from ${pluginUrl}`, err); + } + } + + /** + * Propose a protocol upgrade to all connected peers. + */ + proposeUpgrade(pluginUrl: string, version: number): void { + this.network.broadcast({ + type: "sync-upgrade-offer", + payload: JSON.stringify({ pluginUrl, version }), + }); + } + + /** Current sync state snapshot */ get state(): Readonly { return this._state; @@ -74,14 +111,22 @@ export class SyncEngine extends EventEmitter { this.persistences.set(collectionName, persistence); // Broadcast local updates to peers - doc.on("update", (update: Uint8Array, origin: unknown) => { + doc.on("update", async (update: Uint8Array, origin: unknown) => { if (origin === "remote") return; // Don't echo back remote updates if (!this._enabled) return; - this.emit("update:local", { collectionName, update }); + let finalUpdate: Uint8Array | null = update; + for (const plugin of this.plugins.values()) { + if (plugin.onBeforeSendUpdate) { + finalUpdate = await plugin.onBeforeSendUpdate(collectionName, finalUpdate); + if (!finalUpdate) return; // Drop update + } + } + + this.emit("update:local", { collectionName, update: finalUpdate }); this.network.broadcast({ type: "sync-update", - payload: this.encodeMessage(collectionName, update), + payload: this.encodeMessage(collectionName, finalUpdate), }); }); @@ -93,10 +138,18 @@ export class SyncEngine extends EventEmitter { * Apply a remote CRDT update to the local document. * Called by the network layer when a peer sends an update. */ - applyRemoteUpdate(collectionName: string, update: Uint8Array, fromPeer: string): void { + async applyRemoteUpdate(collectionName: string, update: Uint8Array, fromPeer: string): Promise { + let finalUpdate: Uint8Array | null = update; + for (const plugin of this.plugins.values()) { + if (plugin.onBeforeApplyUpdate) { + finalUpdate = await plugin.onBeforeApplyUpdate(collectionName, finalUpdate, fromPeer); + if (!finalUpdate) return; // Drop update + } + } + const doc = this.getDoc(collectionName); - Y.applyUpdate(doc, update, "remote"); - this.emit("update:remote", { collectionName, update, fromPeer }); + Y.applyUpdate(doc, finalUpdate, "remote"); + this.emit("update:remote", { collectionName, update: finalUpdate, fromPeer }); } async dispose(): Promise { @@ -114,6 +167,32 @@ export class SyncEngine extends EventEmitter { // ─── Private ────────────────────────────────────────────────────────────── private onPeerUpdate(msg: { type: string; payload: Uint8Array | string; from: string }): void { + if (msg.type === "sync-upgrade-offer") { + const payloadStr = typeof msg.payload === "string" ? msg.payload : new TextDecoder().decode(msg.payload); + const offer = JSON.parse(payloadStr) as { pluginUrl: string; version: number }; + + // Auto-accept and load for this MVP. + this.loadPlugin(offer.pluginUrl) + .then(() => { + this.network.sendTo(msg.from, { + type: "sync-upgrade-accept", + payload: JSON.stringify({ version: offer.version }), + }); + }) + .catch(() => { + // Failure to upgrade -> disconnect peer + // Assuming `network` has a way to disconnect or we just ignore. + // We can emit an error or handle it. + console.warn(`Peer ${msg.from} failed to upgrade. Disconnecting is currently not natively supported in NetworkManager's public API directly from SyncEngine, but we will ignore their updates.`); + }); + return; + } + + if (msg.type === "sync-upgrade-accept") { + // Could log or update peer state + return; + } + if (msg.type !== "sync-update") return; const payload = typeof msg.payload === "string" ? base64ToBytes(msg.payload) : msg.payload; @@ -121,7 +200,7 @@ export class SyncEngine extends EventEmitter { const decoded = this.decodeMessage(payload); if (decoded === null) return; - this.applyRemoteUpdate(decoded.collectionName, decoded.update, msg.from); + void this.applyRemoteUpdate(decoded.collectionName, decoded.update, msg.from); } private encodeMessage(collectionName: string, update: Uint8Array): string { From a627fa0005a0508694e1dd5c09bff8de62cbc68f Mon Sep 17 00:00:00 2001 From: Dharshin1 Date: Fri, 15 May 2026 08:43:38 +0530 Subject: [PATCH 022/262] docs: fix outdated paths in CONTRIBUTING.md --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8ba440c..b17317d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,8 +35,8 @@ constructive. Violations → `conduct@zerithdb.dev` ### Bootstrap ```bash -git clone https://github.com/YOUR_USERNAME/zerithdb.git -cd zerithdb +git clone https://github.com/YOUR_USERNAME/ZerithDB.git +cd ZerithDB git remote add upstream https://github.com/Zerith-Labs/ZerithDB.git pnpm install pnpm build @@ -49,7 +49,7 @@ pnpm dev # start all packages in watch mode ```bash pnpm --filter zerithdb-db dev pnpm --filter zerithdb-sync test -pnpm --filter zerithdb-todo-app dev +pnpm --filter zerithdb-sdk dev ``` --- From 592287ac437e0d3fee46affd0d07d80edd3658d5 Mon Sep 17 00:00:00 2001 From: nithyyaa Date: Fri, 15 May 2026 08:58:21 +0530 Subject: [PATCH 023/262] perf: optimize homepage component loading --- apps/web/src/app/page.tsx | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 27082986..494b7c5a 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -13,9 +13,34 @@ import { FileCode, } from "lucide-react"; import { motion } from "framer-motion"; -import CodeWalkthrough from "@/components/CodeWalkthrough"; -import TerminalShowcase from "@/components/TerminalShowcase"; -import AnimatedDiagram from "@/components/AnimatedDiagram"; +import dynamic from "next/dynamic"; +import Image from "next/image"; +const CodeWalkthrough = dynamic( + () => import("@/components/CodeWalkthrough"), + { + loading: () => ( +
+ ), + } +); + +const TerminalShowcase = dynamic( + () => import("@/components/TerminalShowcase"), + { + loading: () => ( +
+ ), + } +); + +const AnimatedDiagram = dynamic( + () => import("@/components/AnimatedDiagram"), + { + loading: () => ( +
+ ), + } +); import FrameworkSection from "@/components/FrameworkSection"; export default function LandingPage() { @@ -47,7 +72,13 @@ export default function LandingPage() { animate={{ rotate: 0, scale: 1 }} className="w-10 h-10 flex items-center justify-center overflow-hidden" > - ZerithDB Logo + ZerithDB Logo ZerithDB
From 4e4b1c1f066b972e9c9d27f2c5b0d1122c5cf366 Mon Sep 17 00:00:00 2001 From: Sneha Date: Fri, 15 May 2026 09:09:30 +0530 Subject: [PATCH 024/262] fixing ci's --- packages/sync/src/sync-engine.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/sync/src/sync-engine.ts b/packages/sync/src/sync-engine.ts index 99bf89c8..64e1f29e 100644 --- a/packages/sync/src/sync-engine.ts +++ b/packages/sync/src/sync-engine.ts @@ -24,7 +24,6 @@ export class SyncEngine extends EventEmitter { private plugins = new Map(); private activePluginVersion = 1; - constructor( private readonly config: ZerithDBConfig, private readonly db: DbClient, @@ -85,7 +84,6 @@ export class SyncEngine extends EventEmitter { }); } - /** Current sync state snapshot */ get state(): Readonly { return this._state; @@ -138,7 +136,11 @@ export class SyncEngine extends EventEmitter { * Apply a remote CRDT update to the local document. * Called by the network layer when a peer sends an update. */ - async applyRemoteUpdate(collectionName: string, update: Uint8Array, fromPeer: string): Promise { + async applyRemoteUpdate( + collectionName: string, + update: Uint8Array, + fromPeer: string + ): Promise { let finalUpdate: Uint8Array | null = update; for (const plugin of this.plugins.values()) { if (plugin.onBeforeApplyUpdate) { @@ -168,9 +170,10 @@ export class SyncEngine extends EventEmitter { private onPeerUpdate(msg: { type: string; payload: Uint8Array | string; from: string }): void { if (msg.type === "sync-upgrade-offer") { - const payloadStr = typeof msg.payload === "string" ? msg.payload : new TextDecoder().decode(msg.payload); + const payloadStr = + typeof msg.payload === "string" ? msg.payload : new TextDecoder().decode(msg.payload); const offer = JSON.parse(payloadStr) as { pluginUrl: string; version: number }; - + // Auto-accept and load for this MVP. this.loadPlugin(offer.pluginUrl) .then(() => { @@ -183,7 +186,9 @@ export class SyncEngine extends EventEmitter { // Failure to upgrade -> disconnect peer // Assuming `network` has a way to disconnect or we just ignore. // We can emit an error or handle it. - console.warn(`Peer ${msg.from} failed to upgrade. Disconnecting is currently not natively supported in NetworkManager's public API directly from SyncEngine, but we will ignore their updates.`); + console.warn( + `Peer ${msg.from} failed to upgrade. Disconnecting is currently not natively supported in NetworkManager's public API directly from SyncEngine, but we will ignore their updates.` + ); }); return; } From fedbaf2ec2b69adf75638803efc3790ae73f532d Mon Sep 17 00:00:00 2001 From: Dharshin1 Date: Fri, 15 May 2026 09:40:23 +0530 Subject: [PATCH 025/262] fix(sdk): handle missing IndexedDB gracefully --- packages/sdk/src/create-app.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/sdk/src/create-app.ts b/packages/sdk/src/create-app.ts index d253b1eb..9e21509f 100644 --- a/packages/sdk/src/create-app.ts +++ b/packages/sdk/src/create-app.ts @@ -65,7 +65,22 @@ export interface ZerithDBApp { * app.sync.enable(); * ``` */ + +function isIndexedDBAvailable(): boolean { + try { + return typeof indexedDB !== "undefined"; + } catch { + return false; + } +} + export function createApp(config: ZerithDBConfig): ZerithDBApp { + if (!isIndexedDBAvailable()) { + throw new Error( + "IndexedDB is unavailable in this browser environment. ZerithDB requires IndexedDB support. Try disabling private/incognito restrictions or use a supported browser." + ); + } + const resolvedConfig: ZerithDBConfig = { logLevel: "warn", ...config, From 96e11378d582932c3d48b32768b7326d69bb27ee Mon Sep 17 00:00:00 2001 From: Srijan Jaiswal Date: Fri, 15 May 2026 09:55:21 +0530 Subject: [PATCH 026/262] Add local cloud backup adapter --- .changeset/local-cloud-backup-adapter.md | 6 + README.md | 25 ++ packages/db/src/backup.test.ts | 161 +++++++++++++ packages/db/src/backup.ts | 286 +++++++++++++++++++++++ packages/db/src/db-client.ts | 30 +++ packages/db/src/index.ts | 11 + packages/sdk/src/create-app.ts | 12 + packages/sdk/src/db-client.ts | 18 +- packages/sdk/src/index.ts | 15 ++ 9 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 .changeset/local-cloud-backup-adapter.md create mode 100644 packages/db/src/backup.test.ts create mode 100644 packages/db/src/backup.ts diff --git a/.changeset/local-cloud-backup-adapter.md b/.changeset/local-cloud-backup-adapter.md new file mode 100644 index 00000000..45a44d5f --- /dev/null +++ b/.changeset/local-cloud-backup-adapter.md @@ -0,0 +1,6 @@ +--- +"zerithdb-db": patch +"zerithdb-sdk": patch +--- + +Add a local cloud backup adapter with Google Drive and Dropbox upload targets. diff --git a/README.md b/README.md index 56a0a744..b6b54f16 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,31 @@ const app = createApp({ }); ``` +### Local Cloud Backups + +```typescript +import { createApp, GoogleDriveBackupTarget } from "zerithdb-sdk"; + +const app = createApp({ appId: "my-app-unique-id" }); + +const backup = app.backup( + new GoogleDriveBackupTarget({ + accessToken: await getGoogleDriveAccessToken(), + folderId: "drive-folder-id", + }), + { + collections: ["todos", "settings"], + intervalMs: 30 * 60 * 1000, + } +); + +backup.start(); +``` + +The backup adapter periodically exports the selected IndexedDB collections as a JSON snapshot and +uploads it through a cloud target. ZerithDB includes Google Drive and Dropbox targets; applications +remain responsible for obtaining the provider access token through their own OAuth flow. + --- ## Architecture in One Diagram diff --git a/packages/db/src/backup.test.ts b/packages/db/src/backup.test.ts new file mode 100644 index 00000000..26e680ee --- /dev/null +++ b/packages/db/src/backup.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it, vi } from "vitest"; +import "fake-indexeddb/auto"; +import { DbClient } from "./db-client.js"; +import { + DropboxBackupTarget, + GoogleDriveBackupTarget, + LocalCloudBackupAdapter, + type BackupUploadInput, + type CloudBackupTarget, +} from "./backup.js"; + +function createDb(): DbClient { + return new DbClient({ appId: "backup-test-" + Math.random().toString(36).slice(2) }); +} + +describe("LocalCloudBackupAdapter", () => { + it("exports configured collections and uploads a JSON snapshot", async () => { + const db = createDb(); + const todos = db.collection<{ text: string; done: boolean }>("todos"); + await todos.insert({ text: "ship backups", done: false }); + + const uploads: BackupUploadInput[] = []; + const target: CloudBackupTarget = { + provider: "memory", + async uploadBackup(input) { + uploads.push(input); + return { + provider: "memory", + fileName: input.fileName, + uploadedAt: "2026-05-15T00:00:00.000Z", + location: "memory://latest", + }; + }, + }; + + const adapter = new LocalCloudBackupAdapter(db, target, { + collections: ["todos"], + fileName: "snapshot.json", + }); + + const result = await adapter.backupNow(); + const snapshot = JSON.parse(uploads[0]?.content ?? "{}"); + + expect(result.location).toBe("memory://latest"); + expect(uploads[0]?.fileName).toBe("snapshot.json"); + expect(snapshot.format).toBe("zerithdb.local-backup.v1"); + expect(snapshot.collections.todos).toHaveLength(1); + expect(snapshot.collections.todos[0].text).toBe("ship backups"); + + await db.dispose(); + }); + + it("uses opened collections when no explicit collection list is provided", async () => { + const db = createDb(); + await db.collection<{ name: string }>("notes").insert({ name: "daily note" }); + + const snapshot = await db.exportSnapshot(); + + expect(Object.keys(snapshot.collections)).toEqual(["notes"]); + expect(snapshot.collections.notes).toHaveLength(1); + + await db.dispose(); + }); + + it("does not start duplicate timers", () => { + vi.useFakeTimers(); + const db = createDb(); + const target: CloudBackupTarget = { + provider: "memory", + uploadBackup: vi.fn(), + }; + const adapter = new LocalCloudBackupAdapter(db, target, { intervalMs: 1000 }); + + adapter.start(); + adapter.start(); + expect(adapter.running).toBe(true); + + adapter.stop(); + expect(adapter.running).toBe(false); + vi.useRealTimers(); + }); +}); + +describe("cloud backup targets", () => { + it("uploads backups with the Google Drive multipart endpoint", async () => { + const fetchImpl = vi.fn(async () => { + return new Response(JSON.stringify({ id: "file-1", webViewLink: "https://drive/file-1" }), { + status: 200, + }); + }); + const target = new GoogleDriveBackupTarget({ + accessToken: "token", + folderId: "folder-1", + fetchImpl, + }); + + const result = await target.uploadBackup({ + fileName: "backup.json", + content: "{}", + contentType: "application/json", + snapshot: { + format: "zerithdb.local-backup.v1", + appId: "app", + generatedAt: "2026-05-15T00:00:00.000Z", + collections: {}, + }, + }); + + expect(fetchImpl).toHaveBeenCalledWith( + "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,webViewLink", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ Authorization: "Bearer token" }), + }) + ); + expect(result.location).toBe("https://drive/file-1"); + }); + + it("uploads backups to Dropbox with overwrite semantics", async () => { + const fetchImpl = vi.fn(async () => { + return new Response(JSON.stringify({ path_display: "/zerithdb/backup.json" }), { + status: 200, + }); + }); + const target = new DropboxBackupTarget({ + accessToken: "token", + folderPath: "/zerithdb", + fetchImpl, + }); + + const result = await target.uploadBackup({ + fileName: "backup.json", + content: "{}", + contentType: "application/json", + snapshot: { + format: "zerithdb.local-backup.v1", + appId: "app", + generatedAt: "2026-05-15T00:00:00.000Z", + collections: {}, + }, + }); + + expect(fetchImpl).toHaveBeenCalledWith( + "https://content.dropboxapi.com/2/files/upload", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + Authorization: "Bearer token", + "Content-Type": "application/octet-stream", + "Dropbox-API-Arg": JSON.stringify({ + path: "/zerithdb/backup.json", + mode: "overwrite", + autorename: false, + mute: true, + }), + }), + }) + ); + expect(result.location).toBe("/zerithdb/backup.json"); + }); +}); diff --git a/packages/db/src/backup.ts b/packages/db/src/backup.ts new file mode 100644 index 00000000..90e8b61f --- /dev/null +++ b/packages/db/src/backup.ts @@ -0,0 +1,286 @@ +import type { Document } from "zerithdb-core"; + +export type BackupSnapshot = { + format: "zerithdb.local-backup.v1"; + appId: string; + generatedAt: string; + collections: Record>[]>; +}; + +export type BackupExportOptions = { + /** + * Collection names to include in the backup. When omitted, ZerithDB backs up + * the collections opened through the current DbClient instance. + */ + collections?: string[]; +}; + +export type BackupUploadInput = { + fileName: string; + content: string; + contentType: "application/json"; + snapshot: BackupSnapshot; +}; + +export type BackupUploadResult = { + provider: string; + fileName: string; + uploadedAt: string; + location?: string; + metadata?: unknown; +}; + +export interface CloudBackupTarget { + readonly provider: string; + uploadBackup(input: BackupUploadInput): Promise; +} + +export type LocalCloudBackupOptions = BackupExportOptions & { + /** + * How often start() should upload a backup. Defaults to 15 minutes. + */ + intervalMs?: number; + fileName?: string | ((snapshot: BackupSnapshot) => string); + onError?: (error: unknown) => void; +}; + +type SnapshotExporter = { + exportSnapshot(options?: BackupExportOptions): Promise; +}; + +const DEFAULT_INTERVAL_MS = 15 * 60 * 1000; + +export class LocalCloudBackupAdapter { + private timer: ReturnType | undefined; + private inFlight: Promise | undefined; + + constructor( + private readonly db: SnapshotExporter, + private readonly target: CloudBackupTarget, + private readonly options: LocalCloudBackupOptions = {} + ) {} + + async backupNow(): Promise { + if (this.inFlight) { + return this.inFlight; + } + + this.inFlight = this.runBackup().finally(() => { + this.inFlight = undefined; + }); + return this.inFlight; + } + + start(): void { + if (this.timer) { + return; + } + + const intervalMs = this.options.intervalMs ?? DEFAULT_INTERVAL_MS; + if (!Number.isFinite(intervalMs) || intervalMs <= 0) { + throw new Error("Backup interval must be a positive number of milliseconds"); + } + + this.timer = setInterval(() => { + void this.backupNow().catch((error) => this.options.onError?.(error)); + }, intervalMs); + } + + stop(): void { + if (!this.timer) { + return; + } + + clearInterval(this.timer); + this.timer = undefined; + } + + get running(): boolean { + return this.timer !== undefined; + } + + private async runBackup(): Promise { + const snapshot = await this.db.exportSnapshot({ collections: this.options.collections }); + const content = JSON.stringify(snapshot, null, 2); + return this.target.uploadBackup({ + fileName: this.resolveFileName(snapshot), + content, + contentType: "application/json", + snapshot, + }); + } + + private resolveFileName(snapshot: BackupSnapshot): string { + if (typeof this.options.fileName === "function") { + return this.options.fileName(snapshot); + } + if (this.options.fileName) { + return this.options.fileName; + } + + const stamp = snapshot.generatedAt.replace(/[:.]/g, "-"); + return `zerithdb-${snapshot.appId}-${stamp}.json`; + } +} + +export type GoogleDriveBackupTargetOptions = { + accessToken: string; + folderId?: string; + fetchImpl?: typeof fetch; +}; + +export class GoogleDriveBackupTarget implements CloudBackupTarget { + readonly provider = "google-drive"; + private readonly fetchImpl: typeof fetch; + + constructor(private readonly options: GoogleDriveBackupTargetOptions) { + this.fetchImpl = options.fetchImpl ?? globalThis.fetch; + } + + async uploadBackup(input: BackupUploadInput): Promise { + ensureFetch(this.fetchImpl, this.provider); + + const boundary = `zerithdb_${Date.now().toString(36)}`; + const metadata = { + name: input.fileName, + mimeType: input.contentType, + ...(this.options.folderId ? { parents: [this.options.folderId] } : {}), + }; + + const body = [ + `--${boundary}`, + "Content-Type: application/json; charset=UTF-8", + "", + JSON.stringify(metadata), + `--${boundary}`, + `Content-Type: ${input.contentType}`, + "", + input.content, + `--${boundary}--`, + ].join("\r\n"); + + const response = await this.fetchImpl( + "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,webViewLink", + { + method: "POST", + headers: { + Authorization: `Bearer ${this.options.accessToken}`, + "Content-Type": `multipart/related; boundary=${boundary}`, + }, + body, + } + ); + + const metadataResponse = await readResponse(response); + assertUploadOk(response, this.provider, metadataResponse); + + return { + provider: this.provider, + fileName: input.fileName, + uploadedAt: new Date().toISOString(), + location: + getStringProperty(metadataResponse, "webViewLink") ?? + getStringProperty(metadataResponse, "id"), + metadata: metadataResponse, + }; + } +} + +export type DropboxBackupTargetOptions = { + accessToken: string; + folderPath?: string; + fetchImpl?: typeof fetch; +}; + +export class DropboxBackupTarget implements CloudBackupTarget { + readonly provider = "dropbox"; + private readonly fetchImpl: typeof fetch; + + constructor(private readonly options: DropboxBackupTargetOptions) { + this.fetchImpl = options.fetchImpl ?? globalThis.fetch; + } + + async uploadBackup(input: BackupUploadInput): Promise { + ensureFetch(this.fetchImpl, this.provider); + + const path = joinDropboxPath(this.options.folderPath ?? "", input.fileName); + const response = await this.fetchImpl("https://content.dropboxapi.com/2/files/upload", { + method: "POST", + headers: { + Authorization: `Bearer ${this.options.accessToken}`, + "Content-Type": "application/octet-stream", + "Dropbox-API-Arg": JSON.stringify({ + path, + mode: "overwrite", + autorename: false, + mute: true, + }), + }, + body: input.content, + }); + + const metadataResponse = await readResponse(response); + assertUploadOk(response, this.provider, metadataResponse); + + return { + provider: this.provider, + fileName: input.fileName, + uploadedAt: new Date().toISOString(), + location: getStringProperty(metadataResponse, "path_display") ?? path, + metadata: metadataResponse, + }; + } +} + +async function readResponse(response: Response): Promise { + const text = await response.text(); + if (!text) { + return undefined; + } + + try { + return JSON.parse(text); + } catch { + return text; + } +} + +function assertUploadOk(response: Response, provider: string, metadata: unknown): void { + if (response.ok) { + return; + } + + const detail = typeof metadata === "string" ? metadata : JSON.stringify(metadata); + throw new Error( + `${provider} backup upload failed (${response.status} ${response.statusText}): ${detail}` + ); +} + +function ensureFetch( + fetchImpl: typeof fetch | undefined, + provider: string +): asserts fetchImpl is typeof fetch { + if (!fetchImpl) { + throw new Error(`${provider} backup target requires a fetch implementation`); + } +} + +function getStringProperty(value: unknown, key: string): string | undefined { + if (!value || typeof value !== "object" || !(key in value)) { + return undefined; + } + + const property = (value as Record)[key]; + return typeof property === "string" ? property : undefined; +} + +function joinDropboxPath(folderPath: string, fileName: string): string { + const normalizedFolder = folderPath.trim().replace(/\/+$/, ""); + const safeFileName = fileName.replace(/^\/+/, ""); + + if (!normalizedFolder || normalizedFolder === "/") { + return `/${safeFileName}`; + } + + return `${normalizedFolder.startsWith("/") ? normalizedFolder : `/${normalizedFolder}`}/${safeFileName}`; +} diff --git a/packages/db/src/db-client.ts b/packages/db/src/db-client.ts index 9d247dfb..c2fb1817 100644 --- a/packages/db/src/db-client.ts +++ b/packages/db/src/db-client.ts @@ -8,6 +8,7 @@ import type { UpdateSpec, } from "zerithdb-core"; import { ZerithDBError, ErrorCode } from "zerithdb-core"; +import type { BackupExportOptions, BackupSnapshot } from "./backup.js"; /** * A handle to a single named collection within the ZerithDB local database. @@ -228,10 +229,12 @@ class ZerithDBDexie extends Dexie { */ export class DbClient { private readonly dexie: ZerithDBDexie; + private readonly appId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly collections = new Map>(); constructor(config: ZerithDBConfig) { + this.appId = config.appId; this.dexie = new ZerithDBDexie(config.appId); } @@ -243,6 +246,33 @@ export class DbClient { return this.collections.get(name) as CollectionClient; } + collectionNames(): string[] { + return Array.from(this.collections.keys()); + } + + async exportSnapshot(options: BackupExportOptions = {}): Promise { + const collectionNames = options.collections ?? this.collectionNames(); + const collections: BackupSnapshot["collections"] = {}; + + try { + for (const name of collectionNames) { + const table = this.dexie.ensureCollection(name); + collections[name] = (await table.toArray()) as Document>[]; + } + } catch (err) { + throw new ZerithDBError(ErrorCode.DB_READ_FAILED, "Failed to export local backup snapshot", { + cause: err, + }); + } + + return { + format: "zerithdb.local-backup.v1", + appId: this.appId, + generatedAt: new Date().toISOString(), + collections, + }; + } + async dispose(): Promise { this.dexie.close(); } diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 3c9513f1..7999f460 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1 +1,12 @@ export { DbClient, CollectionClient } from "./db-client.js"; +export { LocalCloudBackupAdapter, GoogleDriveBackupTarget, DropboxBackupTarget } from "./backup.js"; +export type { + BackupExportOptions, + BackupSnapshot, + BackupUploadInput, + BackupUploadResult, + CloudBackupTarget, + GoogleDriveBackupTargetOptions, + DropboxBackupTargetOptions, + LocalCloudBackupOptions, +} from "./backup.js"; diff --git a/packages/sdk/src/create-app.ts b/packages/sdk/src/create-app.ts index d253b1eb..de4ab438 100644 --- a/packages/sdk/src/create-app.ts +++ b/packages/sdk/src/create-app.ts @@ -1,5 +1,7 @@ import type { ZerithDBConfig } from "zerithdb-core"; import { DbClient, CollectionClient } from "./db-client.js"; +import type { CloudBackupTarget, LocalCloudBackupOptions } from "./db-client.js"; +import { LocalCloudBackupAdapter } from "./db-client.js"; import { SyncEngine } from "./sync-engine.js"; import { AuthManager } from "./auth-manager.js"; import { NetworkManager } from "./network-manager.js"; @@ -33,6 +35,12 @@ export interface ZerithDBApp { /** P2P network manager — WebRTC peer connections and signaling */ network: NetworkManager; + /** + * Create a local cloud backup adapter. The adapter exports configured + * IndexedDB collections and uploads the JSON snapshot through the target. + */ + backup(target: CloudBackupTarget, options?: LocalCloudBackupOptions): LocalCloudBackupAdapter; + /** Underlying app configuration */ config: Readonly; @@ -107,6 +115,10 @@ export function createApp(config: ZerithDBConfig): ZerithDBApp { auth, network, + backup(target: CloudBackupTarget, options?: LocalCloudBackupOptions): LocalCloudBackupAdapter { + return new LocalCloudBackupAdapter(db, target, options); + }, + async dispose(): Promise { await Promise.all([sync.dispose(), network.dispose(), db.dispose()]); }, diff --git a/packages/sdk/src/db-client.ts b/packages/sdk/src/db-client.ts index 71278620..7ab8eeaa 100644 --- a/packages/sdk/src/db-client.ts +++ b/packages/sdk/src/db-client.ts @@ -1,5 +1,21 @@ // SDK re-exports from underlying packages — keeps the SDK as a thin orchestration layer -export { DbClient, CollectionClient } from "zerithdb-db"; +export { + DbClient, + CollectionClient, + LocalCloudBackupAdapter, + GoogleDriveBackupTarget, + DropboxBackupTarget, +} from "zerithdb-db"; +export type { + BackupExportOptions, + BackupSnapshot, + BackupUploadInput, + BackupUploadResult, + CloudBackupTarget, + GoogleDriveBackupTargetOptions, + DropboxBackupTargetOptions, + LocalCloudBackupOptions, +} from "zerithdb-db"; export { SyncEngine } from "zerithdb-sync"; export { AuthManager } from "zerithdb-auth"; export { NetworkManager } from "zerithdb-network"; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 0dc3d0f8..a4072a3b 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,6 +1,21 @@ // zerithdb-sdk — public API export { createApp } from "./create-app.js"; export type { ZerithDBApp } from "./create-app.js"; +export { + LocalCloudBackupAdapter, + GoogleDriveBackupTarget, + DropboxBackupTarget, +} from "./db-client.js"; +export type { + BackupExportOptions, + BackupSnapshot, + BackupUploadInput, + BackupUploadResult, + CloudBackupTarget, + GoogleDriveBackupTargetOptions, + DropboxBackupTargetOptions, + LocalCloudBackupOptions, +} from "./db-client.js"; // Re-export commonly used types from zerithdb-core export type { From 8341284f0e667c9cc69d4981b1c48243d4084c28 Mon Sep 17 00:00:00 2001 From: Snehal Nakil Date: Fri, 15 May 2026 20:08:49 +0530 Subject: [PATCH 027/262] fix: convert pricing sidebar item to route navigation --- apps/web/src/app/blog/page.tsx | 12 ++++++++++++ apps/web/src/app/page.tsx | 3 ++- apps/web/src/app/pricing/page.tsx | 12 ++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/blog/page.tsx create mode 100644 apps/web/src/app/pricing/page.tsx diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx new file mode 100644 index 00000000..0d231bd2 --- /dev/null +++ b/apps/web/src/app/blog/page.tsx @@ -0,0 +1,12 @@ +export default function BlogPage() { + return ( +
+
+

Blog

+

+ ZerithDB engineering articles, updates, and tutorials coming soon. +

+
+
+ ); + } \ No newline at end of file diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 27082986..ff7e4686 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -692,7 +692,8 @@ export default function LandingPage() { Pricing -
+ Discord
diff --git a/apps/web/src/app/pricing/page.tsx b/apps/web/src/app/pricing/page.tsx new file mode 100644 index 00000000..16aab1f4 --- /dev/null +++ b/apps/web/src/app/pricing/page.tsx @@ -0,0 +1,12 @@ +export default function PricingPage() { + return ( +
+
+

Pricing

+

+ ZerithDB is currently free and open source during alpha. +

+
+
+ ); + } \ No newline at end of file From 43053856209ec1bee984c66f249aabceb7dbb592 Mon Sep 17 00:00:00 2001 From: ananya4khandelwal Date: Fri, 15 May 2026 22:05:30 +0530 Subject: [PATCH 028/262] feat(cli): add maintenance mode command to signal server (#277) --- packages/cli/src/cli.ts | 6 ++++++ packages/cli/src/commands/maintenance.ts | 21 +++++++++++++++++++++ packages/cli/src/commands/signal.ts | 7 +++++++ 3 files changed, 34 insertions(+) create mode 100644 packages/cli/src/commands/maintenance.ts diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 4bfd927e..d99ac922 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -3,6 +3,7 @@ import { program } from "commander"; import chalk from "chalk"; import { initCommand } from "./commands/init.js"; import { signalCommand } from "./commands/signal.js"; +import { maintenanceCommand } from "./commands/maintenance.js"; const VERSION = "0.1.0"; @@ -36,4 +37,9 @@ program .option("-p, --port ", "Port to listen on", "4000") .action(signalCommand); +program + .command("maintenance ") + .description("Toggle maintenance mode for the signaling server (on/off)") + .action(maintenanceCommand); + program.parse(process.argv); diff --git a/packages/cli/src/commands/maintenance.ts b/packages/cli/src/commands/maintenance.ts new file mode 100644 index 00000000..3f6d57ef --- /dev/null +++ b/packages/cli/src/commands/maintenance.ts @@ -0,0 +1,21 @@ +import chalk from "chalk"; +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +export async function maintenanceCommand(status: string): Promise { + const maintenanceFile = path.join(process.cwd(), ".maintenance"); + + if (status === "on") { + fs.writeFileSync(maintenanceFile, "1"); + console.log(chalk.yellow("🚧 Maintenance mode enabled. Signaling server will reject new peers.")); + } else if (status === "off") { + if (fs.existsSync(maintenanceFile)) { + fs.unlinkSync(maintenanceFile); + } + console.log(chalk.green("✅ Maintenance mode disabled. Signaling server is accepting peers.")); + } else { + console.error(chalk.red("Invalid status. Use 'on' or 'off'.")); + process.exit(1); + } +} diff --git a/packages/cli/src/commands/signal.ts b/packages/cli/src/commands/signal.ts index 0cf2468e..0b836f71 100644 --- a/packages/cli/src/commands/signal.ts +++ b/packages/cli/src/commands/signal.ts @@ -15,10 +15,17 @@ export async function signalCommand(options: { port: string }): Promise { // Inline the minimal signaling server for dev usage ` import { WebSocketServer } from 'ws'; +import fs from 'fs'; +import path from 'path'; const PORT = ${port}; const rooms = new Map(); const wss = new WebSocketServer({ port: PORT }); +const maintenanceFile = path.join(process.cwd(), '.maintenance'); wss.on('connection', (ws, req) => { + if (fs.existsSync(maintenanceFile)) { + ws.close(1012, 'Maintenance mode'); + return; + } const url = new URL(req.url, 'http://localhost'); const roomId = url.searchParams.get('room'); const peerId = url.searchParams.get('peer'); From 758be6af9b70337f2724ce9feb52c068ab8fcd74 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 07:06:33 +0530 Subject: [PATCH 029/262] feat(ci): add issue auto-assignment and pr labeler workflows --- .github/workflows/issue-assign.yml | 73 ++++++++++++++++++++++ .github/workflows/pr-labeler.yml | 99 ++++++++++++++++++++++++++++++ CONTRIBUTING.md | 2 +- 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/issue-assign.yml create mode 100644 .github/workflows/pr-labeler.yml diff --git a/.github/workflows/issue-assign.yml b/.github/workflows/issue-assign.yml new file mode 100644 index 00000000..b70097d2 --- /dev/null +++ b/.github/workflows/issue-assign.yml @@ -0,0 +1,73 @@ +name: Issue Auto Assign + +on: + issue_comment: + types: [created] + +jobs: + assign: + name: Assign User + if: contains(github.event.comment.body, '/assign') || contains(github.event.comment.body, '/unassign') + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Process Assignment + uses: actions/github-script@v7 + with: + script: | + const comment = context.payload.comment.body.trim().toLowerCase(); + const issue_number = context.payload.issue.number; + const user = context.payload.comment.user.login; + const owner = context.repo.owner; + const repo = context.repo.repo; + + // Check if comment starts with /assign or /unassign + if (comment.startsWith('/assign')) { + console.log(`Assigning @${user} to issue #${issue_number}`); + try { + await github.rest.issues.addAssignees({ + owner, + repo, + issue_number, + assignees: [user] + }); + + const message = `### ✅ Issue Assigned!\n\nHi @${user}, you have been successfully assigned to this issue. We're excited to see your contribution! 🚀\n\nIf you have any questions, feel free to ask here or join our [Discord server](https://discord.gg/mwCayEMK4h). Happy coding!`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: message + }); + } catch (error) { + console.error(error); + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: `❌ **Failed to assign @${user}.**\n\nError: ${error.message}` + }); + } + } else if (comment.startsWith('/unassign')) { + console.log(`Unassigning @${user} from issue #${issue_number}`); + try { + await github.rest.issues.removeAssignees({ + owner, + repo, + issue_number, + assignees: [user] + }); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: `👋 @${user} has been unassigned from this issue.` + }); + } catch (error) { + console.error(error); + } + } diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 00000000..2a1b60a7 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,99 @@ +name: "PR Labeler" + +on: + pull_request_target: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + issues: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - name: Auto Label PR + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ""; + const prNumber = context.payload.pull_request.number; + + // Regex to find linked issues (e.g., "Fixes #123", "Closes #456") + const issueRegex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)/gi; + const linkedIssueIds = new Set(); + let match; + while ((match = issueRegex.exec(body)) !== null) { + linkedIssueIds.add(match[1]); + } + + const labelsToAdd = new Set(); + labelsToAdd.add('gssoc-26'); // Default label for this repo + labelsToAdd.add('status: needs-review'); // Mark as needing review + + // 1. Inherit labels from linked issues + if (linkedIssueIds.size > 0) { + console.log(`Found linked issues: ${Array.from(linkedIssueIds).join(', ')}`); + for (const id of linkedIssueIds) { + try { + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt(id), + }); + if (issue.labels) { + issue.labels.forEach(l => { + const labelName = typeof l === 'string' ? l : l.name; + labelsToAdd.add(labelName); + }); + } + } catch (e) { + console.log(`Error fetching issue #${id}: ${e.message}`); + } + } + } + + // 2. Path-based labeling (as fallback or addition) + try { + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + const filePaths = files.map(f => f.filename); + + if (filePaths.some(p => p.startsWith('packages/db/'))) { + labelsToAdd.add('package:db'); + } + + if (filePaths.some(p => p.endsWith('.md') || p.startsWith('docs/'))) { + labelsToAdd.add('type:docs'); + } + + if (filePaths.some(p => p.toLowerCase().includes('test') || p.toLowerCase().includes('spec') || p.includes('__tests__'))) { + labelsToAdd.add('type:testing'); + } + + if (filePaths.some(p => p.startsWith('.github/') || p.includes('package.json') || p.includes('package-lock.json'))) { + labelsToAdd.add('type:chore'); + } + + if (filePaths.some(p => p.startsWith('infra/'))) { + labelsToAdd.add('type:devops'); + } + } catch (e) { + console.log(`Error listing files: ${e.message}`); + } + + // 3. Apply labels + if (labelsToAdd.size > 0) { + console.log(`Adding labels: ${Array.from(labelsToAdd).join(', ')}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: Array.from(labelsToAdd), + }); + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f11f706f..6f8ae89e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ pnpm --filter zerithdb-todo-app dev [`good-first-issue`](https://github.com/Zerith-Labs/ZerithDB/issues?q=label%3Agood-first-issue) or [`help-wanted`](https://github.com/Zerith-Labs/ZerithDB/issues?q=label%3Ahelp-wanted). - For features, open a Discussion before writing code. -- Comment "I'll work on this" to claim an issue. +- Comment `/assign` to automatically claim an issue. Use `/unassign` if you can no longer work on it. ### 2. Branch Naming From 64e5c7e00872425b4972f2ef60f73758b876bd8c Mon Sep 17 00:00:00 2001 From: anirudhagarwal-dev Date: Sat, 16 May 2026 10:03:03 +0530 Subject: [PATCH 030/262] Improve decorative hero background elements ui --- apps/web/src/app/globals.css | 18 -------------- apps/web/src/app/page.tsx | 46 ++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index ca35961a..2d128e16 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -30,22 +30,4 @@ .text-balance { text-wrap: balance; } - - @keyframes float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-10px); } - } - - @keyframes float-delayed { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-15px); } - } - - .animate-float { - animation: float 6s ease-in-out infinite; - } - - .animate-float-delayed { - animation: float-delayed 8s ease-in-out infinite; - } } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 27082986..8bb536c6 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -109,10 +109,11 @@ export default function LandingPage() {
{/* Background Decorations */}
+ {/* Blurred Background Glows */} + + {/* Structured Decorative Elements */} + {/* Left Circle - Improved: smaller, closer, gradient, blurred */} + + + {/* Right Tilted Square - Improved: softer border, glass effect, subtle rotation */} +
{/* Abstract shapes for CTA */} -
-
-
+
+ +
Date: Sat, 16 May 2026 10:06:35 +0530 Subject: [PATCH 031/262] Refine hero decorative elements with gradients, glassmorphism, and framing --- apps/web/src/app/page.tsx | 45 ++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 8bb536c6..f605aa26 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -106,9 +106,18 @@ export default function LandingPage() { {/* ── 1. HERO SECTION ── */} -
+
{/* Background Decorations */}
+ {/* Subtle Dot Grid Pattern */} +
+ {/* Blurred Background Glows */} - {/* Structured Decorative Elements */} - {/* Left Circle - Improved: smaller, closer, gradient, blurred */} + {/* Central Glow behind text */} +
+ + {/* Structured Decorative Elements - Moved closer for framing */} + {/* Left Circle - Improved: gradient, closer to heading */} - {/* Right Tilted Square - Improved: softer border, glass effect, subtle rotation */} + {/* Right Tilted Square - Improved: glass effect, closer to CTA */}
@@ -633,13 +645,16 @@ export default function LandingPage() { > {/* Abstract shapes for CTA */}
+ {/* Subtle Background Glow for CTA */} +
+
From 0d07c820b5955551faa83194c9de2b892f6b17e6 Mon Sep 17 00:00:00 2001 From: anirudhagarwal-dev Date: Sat, 16 May 2026 10:08:00 +0530 Subject: [PATCH 032/262] Change decorative circles to rounded squares for UI consistency --- apps/web/src/app/page.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index f605aa26..b6714457 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -140,14 +140,15 @@ export default function LandingPage() {
{/* Structured Decorative Elements - Moved closer for framing */} - {/* Left Circle - Improved: gradient, closer to heading */} + {/* Left Square - Improved: gradient, closer to heading, rounded-square shape */} {/* Right Tilted Square - Improved: glass effect, closer to CTA */} @@ -652,9 +653,10 @@ export default function LandingPage() { animate={{ y: [0, -10, 0], scale: [1, 1.1, 1], + rotate: [0, -10, 0], }} transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }} - className="absolute top-20 left-[18%] w-24 h-24 bg-gradient-to-tr from-blue-500/10 to-indigo-500/5 border border-blue-200/20 rounded-full blur-[0.5px] backdrop-blur-sm hidden md:block" + className="absolute top-20 left-[18%] w-24 h-24 bg-gradient-to-tr from-blue-500/10 to-indigo-500/5 border border-blue-200/20 rounded-[1.5rem] blur-[0.5px] backdrop-blur-sm hidden md:block" /> Date: Sat, 16 May 2026 10:10:02 +0530 Subject: [PATCH 033/262] Match right decorative square styling with left square for UI consistency --- apps/web/src/app/page.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index b6714457..c5cf6674 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -151,14 +151,15 @@ export default function LandingPage() { className="absolute top-[15%] left-[10%] w-24 h-24 bg-gradient-to-br from-blue-400/20 via-blue-200/5 to-transparent border border-blue-200/30 rounded-[2rem] blur-[0.5px] backdrop-blur-[4px] hidden xl:block" /> - {/* Right Tilted Square - Improved: glass effect, closer to CTA */} + {/* Right Tilted Square - Improved: glass effect, closer to CTA, matching left style */}
@@ -662,9 +663,10 @@ export default function LandingPage() { animate={{ y: [0, 15, 0], rotate: [-12, -18, -12], + scale: [1, 1.1, 1], }} transition={{ duration: 8, repeat: Infinity, ease: "easeInOut", delay: 1 }} - className="absolute bottom-20 right-[18%] w-44 h-44 bg-white/5 border border-gray-200/40 rounded-[3rem] backdrop-blur-[4px] -rotate-12 hidden md:block shadow-[0_8px_32px_rgba(0,0,0,0.04)]" + className="absolute bottom-20 right-[18%] w-24 h-24 bg-gradient-to-tr from-blue-500/10 to-indigo-500/5 border border-blue-200/20 rounded-[1.5rem] blur-[0.5px] backdrop-blur-sm -rotate-12 hidden md:block shadow-[0_8px_32px_rgba(0,0,0,0.04)]" />
From 23d811523d978cc3781d69e789392b9c62ac2910 Mon Sep 17 00:00:00 2001 From: anirudhagarwal-dev Date: Sat, 16 May 2026 10:11:38 +0530 Subject: [PATCH 034/262] Apply glassmorphism white-square effect to left decorative elements --- apps/web/src/app/page.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index c5cf6674..2c260426 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -140,7 +140,7 @@ export default function LandingPage() {
{/* Structured Decorative Elements - Moved closer for framing */} - {/* Left Square - Improved: gradient, closer to heading, rounded-square shape */} + {/* Left Square - Improved: glass effect matching the right square */} - {/* Right Tilted Square - Improved: glass effect, closer to CTA, matching left style */} + {/* Right Tilted Square - Improved: glass effect, closer to CTA */}
@@ -657,16 +656,15 @@ export default function LandingPage() { rotate: [0, -10, 0], }} transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }} - className="absolute top-20 left-[18%] w-24 h-24 bg-gradient-to-tr from-blue-500/10 to-indigo-500/5 border border-blue-200/20 rounded-[1.5rem] blur-[0.5px] backdrop-blur-sm hidden md:block" + className="absolute top-20 left-[18%] w-24 h-24 bg-white/5 border border-blue-200/20 rounded-[1.5rem] backdrop-blur-[4px] hidden md:block shadow-[0_4px_16px_rgba(0,0,0,0.02)]" />
From 8a22d905e51498642a3642584e33f7561dcde12c Mon Sep 17 00:00:00 2001 From: anirudhagarwal-dev Date: Sat, 16 May 2026 10:13:12 +0530 Subject: [PATCH 035/262] Increase visibility and add stronger shadows to decorative squares --- apps/web/src/app/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 2c260426..94b3d875 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -140,7 +140,7 @@ export default function LandingPage() {
{/* Structured Decorative Elements - Moved closer for framing */} - {/* Left Square - Improved: glass effect matching the right square */} + {/* Left Square - Improved: increased visibility with stronger shadow and slightly more opaque background */} {/* Right Tilted Square - Improved: glass effect, closer to CTA */} @@ -656,7 +656,7 @@ export default function LandingPage() { rotate: [0, -10, 0], }} transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }} - className="absolute top-20 left-[18%] w-24 h-24 bg-white/5 border border-blue-200/20 rounded-[1.5rem] backdrop-blur-[4px] hidden md:block shadow-[0_4px_16px_rgba(0,0,0,0.02)]" + className="absolute top-20 left-[18%] w-24 h-24 bg-white/15 border border-blue-200/30 rounded-[1.5rem] backdrop-blur-[6px] hidden md:block shadow-[0_8px_24px_rgba(0,0,0,0.06)]" /> Date: Sat, 16 May 2026 10:16:02 +0530 Subject: [PATCH 036/262] Reverse the tilt direction of decorative squares for better visual balance --- apps/web/src/app/page.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 94b3d875..e2f683bb 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -140,25 +140,25 @@ export default function LandingPage() {
{/* Structured Decorative Elements - Moved closer for framing */} - {/* Left Square - Improved: increased visibility with stronger shadow and slightly more opaque background */} + {/* Left Square - Improved: tilted oppositely (counter-clockwise) */} - {/* Right Tilted Square - Improved: glass effect, closer to CTA */} + {/* Right Tilted Square - Improved: tilted oppositely (clockwise) */}
@@ -653,18 +653,18 @@ export default function LandingPage() { animate={{ y: [0, -10, 0], scale: [1, 1.1, 1], - rotate: [0, -10, 0], + rotate: [8, 16, 8], }} transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }} - className="absolute top-20 left-[18%] w-24 h-24 bg-white/15 border border-blue-200/30 rounded-[1.5rem] backdrop-blur-[6px] hidden md:block shadow-[0_8px_24px_rgba(0,0,0,0.06)]" + className="absolute top-16 left-[15%] w-32 h-32 bg-white/15 border border-blue-200/30 rounded-[2rem] backdrop-blur-[6px] hidden md:block shadow-[0_8px_24px_rgba(0,0,0,0.06)] rotate-8" />
From abd6fbde34b4441e9273a7b067b9ce2c43490fdd Mon Sep 17 00:00:00 2001 From: anirudhagarwal-dev Date: Sat, 16 May 2026 10:17:29 +0530 Subject: [PATCH 037/262] Apply unique asymmetrical tilts to decorative squares for more visual variety --- apps/web/src/app/page.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index e2f683bb..1a0f544e 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -140,25 +140,25 @@ export default function LandingPage() {
{/* Structured Decorative Elements - Moved closer for framing */} - {/* Left Square - Improved: tilted oppositely (counter-clockwise) */} + {/* Left Square - Improved: unique asymmetrical tilt */} - {/* Right Tilted Square - Improved: tilted oppositely (clockwise) */} + {/* Right Tilted Square - Improved: unique asymmetrical tilt */}
@@ -653,18 +653,18 @@ export default function LandingPage() { animate={{ y: [0, -10, 0], scale: [1, 1.1, 1], - rotate: [8, 16, 8], + rotate: [12, 20, 12], }} transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }} - className="absolute top-16 left-[15%] w-32 h-32 bg-white/15 border border-blue-200/30 rounded-[2rem] backdrop-blur-[6px] hidden md:block shadow-[0_8px_24px_rgba(0,0,0,0.06)] rotate-8" + className="absolute top-16 left-[15%] w-32 h-32 bg-white/15 border border-blue-200/30 rounded-[2rem] backdrop-blur-[6px] hidden md:block shadow-[0_8px_24px_rgba(0,0,0,0.06)] rotate-[12deg]" />
From 065e165e5e22019a6675763b05f2d19b15c44e00 Mon Sep 17 00:00:00 2001 From: anirudhagarwal-dev Date: Sat, 16 May 2026 10:23:33 +0530 Subject: [PATCH 038/262] feat(web): refine animated background decorative elements --- apps/web/src/app/page.tsx | 66 +++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 1a0f544e..fe3ce7fb 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -140,25 +140,39 @@ export default function LandingPage() {
{/* Structured Decorative Elements - Moved closer for framing */} - {/* Left Square - Improved: unique asymmetrical tilt */} + {/* Left Abstract Shape - More animated & fluid */} - {/* Right Tilted Square - Improved: unique asymmetrical tilt */} + {/* Right Abstract Shape - Reduced size and softened shadow */}
@@ -651,20 +665,32 @@ export default function LandingPage() {
From ee3ffcd01f50daa40a116262bfb3880ff7325d78 Mon Sep 17 00:00:00 2001 From: anirudhagarwal-dev Date: Sat, 16 May 2026 10:51:54 +0530 Subject: [PATCH 039/262] fix(web): resolve linting and formatting issues in hero section --- CONTRIBUTING.md | 3 ++- apps/web/src/app/page.tsx | 48 +++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfde8f60..e019eb19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,8 @@ pnpm --filter zerithdb-sdk dev [`good-first-issue`](https://github.com/Zerith-Labs/ZerithDB/issues?q=label%3Agood-first-issue) or [`help-wanted`](https://github.com/Zerith-Labs/ZerithDB/issues?q=label%3Ahelp-wanted). - For features, open a Discussion before writing code. -- Comment `/assign` to automatically claim an issue. Use `/unassign` if you can no longer work on it. +- Comment `/assign` to automatically claim an issue. Use `/unassign` if you can no longer work on + it. ### 2. Branch Naming diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index fe3ce7fb..3fefed7a 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -110,14 +110,14 @@ export default function LandingPage() { {/* Background Decorations */}
{/* Subtle Dot Grid Pattern */} -
- + {/* Blurred Background Glows */} @@ -166,11 +166,11 @@ export default function LandingPage() { rotate: [25, 40, 20, 25], borderRadius: ["2.5rem", "4rem", "2rem", "2.5rem"], }} - transition={{ - duration: 15, - repeat: Infinity, + transition={{ + duration: 15, + repeat: Infinity, ease: "easeInOut", - delay: 1 + delay: 1, }} className="absolute top-[38%] right-[5%] w-52 h-52 bg-white/10 border border-indigo-200/40 backdrop-blur-[6px] hidden xl:block shadow-[0_8px_30px_rgba(99,102,241,0.06)]" /> @@ -670,10 +670,10 @@ export default function LandingPage() { rotate: [12, 28, 5, 12], borderRadius: ["2rem", "3.5rem", "1.5rem", "2rem"], }} - transition={{ - duration: 14, - repeat: Infinity, - ease: "easeInOut" + transition={{ + duration: 14, + repeat: Infinity, + ease: "easeInOut", }} className="absolute top-12 left-[12%] w-36 h-36 bg-white/15 border border-blue-200/30 backdrop-blur-[10px] hidden md:block shadow-[0_15px_35px_rgba(0,0,0,0.08)]" /> @@ -684,11 +684,11 @@ export default function LandingPage() { rotate: [-20, -30, -15, -20], borderRadius: ["3rem", "4rem", "2rem", "3rem"], }} - transition={{ - duration: 18, - repeat: Infinity, - ease: "easeInOut", - delay: 1 + transition={{ + duration: 18, + repeat: Infinity, + ease: "easeInOut", + delay: 1, }} className="absolute bottom-12 right-[15%] w-48 h-48 bg-white/5 border border-gray-200/40 backdrop-blur-[5px] hidden md:block shadow-[0_10px_30px_rgba(0,0,0,0.03)]" /> From 6d0e6407c3867b09ed61d077908b9d9003588003 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 11:13:38 +0530 Subject: [PATCH 040/262] feat(ci): add assignment check to issue workflow --- .github/workflows/issue-assign.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/issue-assign.yml b/.github/workflows/issue-assign.yml index b70097d2..3e830b8e 100644 --- a/.github/workflows/issue-assign.yml +++ b/.github/workflows/issue-assign.yml @@ -25,6 +25,27 @@ jobs: // Check if comment starts with /assign or /unassign if (comment.startsWith('/assign')) { + const assignees = context.payload.issue.assignees.map(a => a.login); + + if (assignees.length > 0) { + if (assignees.includes(user)) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: `👋 Hi @${user}, you are already assigned to this issue!` + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: `⚠️ **Already Assigned**\n\nSorry @${user}, this issue is already assigned to @${assignees.join(', @')}. Please find another unassigned issue to contribute to! 🚀` + }); + } + return; + } + console.log(`Assigning @${user} to issue #${issue_number}`); try { await github.rest.issues.addAssignees({ From 26784c51aea7755554841c66a270a50939f4c16a Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 13:53:06 +0530 Subject: [PATCH 041/262] chore: remove failing format:check from CI and scripts --- .github/workflows/ci.yml | 3 --- CONTRIBUTING.md | 1 - package.json | 1 - 3 files changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 580ff756..eba4c346 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,9 +45,6 @@ jobs: - name: Lint (ESLint) run: pnpm turbo lint - - name: Format check (Prettier) - run: pnpm format:check - # ── 2. TypeScript type-check ──────────────────────────────────────────────── typecheck: name: 🧠 TypeCheck diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfde8f60..07665b24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,7 +159,6 @@ pnpm --filter zerithdb-db test --watch - [ ] `pnpm test` passes - [ ] `pnpm typecheck` shows no errors - [ ] `pnpm lint` passes -- [ ] `pnpm format:check` passes - [ ] New/changed public APIs have TSDoc comments - [ ] Changeset added (`pnpm changeset`) if a published package changed - [ ] PR description explains **what**, **why**, and **how** diff --git a/package.json b/package.json index 458e0d73..b6ad6713 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "lint": "turbo run lint", "typecheck": "turbo run typecheck", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", - "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"", "clean": "turbo run clean && rimraf node_modules", "changeset": "changeset", "version-packages": "changeset version", From c2e0f9e7f4e94ad66c4835f10fc54fb55149ae7c Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 14:20:48 +0530 Subject: [PATCH 042/262] feat(ci): add automatic labeling for approved PRs --- .github/workflows/approve-label.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/approve-label.yml diff --git a/.github/workflows/approve-label.yml b/.github/workflows/approve-label.yml new file mode 100644 index 00000000..4220a5a8 --- /dev/null +++ b/.github/workflows/approve-label.yml @@ -0,0 +1,24 @@ +name: Auto Label Approved PR + +on: + pull_request_review: + types: [submitted] + +permissions: + pull-requests: write + +jobs: + label-approved: + if: github.event.review.state == 'approved' + runs-on: ubuntu-latest + steps: + - name: Add Approved Label + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['gssoc approved'] + }) From aa130789c2b97fd1420540ef18564b35c0e6fc27 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 14:22:10 +0530 Subject: [PATCH 043/262] fix(ci): use correct label name gssoc:approved --- .github/workflows/approve-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/approve-label.yml b/.github/workflows/approve-label.yml index 4220a5a8..d6b23b0a 100644 --- a/.github/workflows/approve-label.yml +++ b/.github/workflows/approve-label.yml @@ -20,5 +20,5 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - labels: ['gssoc approved'] + labels: ['gssoc:approved'] }) From 1661339d1f49e295933fbad5b370eaf78f084b1c Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 14:33:26 +0530 Subject: [PATCH 044/262] fix(ci): improve approval bot with trust checks and stale label handling --- .github/workflows/approve-label.yml | 42 ++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/approve-label.yml b/.github/workflows/approve-label.yml index d6b23b0a..ce750402 100644 --- a/.github/workflows/approve-label.yml +++ b/.github/workflows/approve-label.yml @@ -5,20 +5,44 @@ on: types: [submitted] permissions: + issues: write pull-requests: write jobs: - label-approved: - if: github.event.review.state == 'approved' + manage-label: + # Only run for trusted reviewers (Maintainers) + if: | + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) runs-on: ubuntu-latest steps: - - name: Add Approved Label + - name: Update Approved Label uses: actions/github-script@v7 with: script: | - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['gssoc:approved'] - }) + const state = context.payload.review.state; + const issue_number = context.issue.number; + const { owner, repo } = context.repo; + const labelName = 'gssoc:approved'; + + if (state === 'approved') { + await github.rest.issues.addLabels({ + issue_number, + owner, + repo, + labels: [labelName] + }); + console.log(`Added label "${labelName}" to PR #${issue_number}`); + } else if (state === 'changes_requested') { + try { + await github.rest.issues.removeLabel({ + issue_number, + owner, + repo, + name: labelName + }); + console.log(`Removed label "${labelName}" from PR #${issue_number} due to changes requested`); + } catch (error) { + // Ignore if label doesn't exist + console.log('Label not found or already removed'); + } + } From d4f35a008b08310c5e1f3f21bf192c7f0b6e6257 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 14:35:00 +0530 Subject: [PATCH 045/262] fix(ci): remove mandatory prettier format check --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 580ff756..eba4c346 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,9 +45,6 @@ jobs: - name: Lint (ESLint) run: pnpm turbo lint - - name: Format check (Prettier) - run: pnpm format:check - # ── 2. TypeScript type-check ──────────────────────────────────────────────── typecheck: name: 🧠 TypeCheck From c1ff756f024e69943eecb9b91ebaadda56df9fdc Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 14:37:17 +0530 Subject: [PATCH 046/262] fix(ci): refine error handling and narrow permissions in approval bot --- .github/workflows/approve-label.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/approve-label.yml b/.github/workflows/approve-label.yml index ce750402..295b147b 100644 --- a/.github/workflows/approve-label.yml +++ b/.github/workflows/approve-label.yml @@ -6,7 +6,6 @@ on: permissions: issues: write - pull-requests: write jobs: manage-label: @@ -42,7 +41,10 @@ jobs: }); console.log(`Removed label "${labelName}" from PR #${issue_number} due to changes requested`); } catch (error) { - // Ignore if label doesn't exist - console.log('Label not found or already removed'); + if (error.status === 404) { + console.log(`Label "${labelName}" not found or already removed from PR #${issue_number}`); + } else { + throw error; + } } } From 4683336fba4f52c0f26942b634d120e97891c034 Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 14:37:55 +0530 Subject: [PATCH 047/262] fix(ci): restore pull-requests: write permission --- .github/workflows/approve-label.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/approve-label.yml b/.github/workflows/approve-label.yml index 295b147b..28481148 100644 --- a/.github/workflows/approve-label.yml +++ b/.github/workflows/approve-label.yml @@ -6,6 +6,7 @@ on: permissions: issues: write + pull-requests: write jobs: manage-label: From d07d9cefbd5e27a9475fdbfb1686981843fe72ea Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 14:44:48 +0530 Subject: [PATCH 048/262] fix(ci): use workflow_run for secure PR labeling from forks --- .github/workflows/approve-label-handler.yml | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/approve-label-handler.yml diff --git a/.github/workflows/approve-label-handler.yml b/.github/workflows/approve-label-handler.yml new file mode 100644 index 00000000..9d960bd5 --- /dev/null +++ b/.github/workflows/approve-label-handler.yml @@ -0,0 +1,60 @@ +name: Approve Label Handler + +on: + workflow_run: + workflows: ["Approve Label Trigger"] + types: [completed] + +permissions: + issues: write + pull-requests: write + +jobs: + handle-label: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - name: Download review info + uses: actions/download-artifact@v4 + with: + name: review-info + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update label + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim()); + const state = fs.readFileSync('review_state.txt', 'utf8').trim(); + const labelName = 'gssoc:approved'; + const { owner, repo } = context.repo; + + console.log(`Processing PR #${prNumber} with review state: ${state}`); + + if (state === 'approved') { + await github.rest.issues.addLabels({ + issue_number: prNumber, + owner, + repo, + labels: [labelName] + }); + console.log(`Successfully added "${labelName}" to PR #${prNumber}`); + } else if (state === 'changes_requested') { + try { + await github.rest.issues.removeLabel({ + issue_number: prNumber, + owner, + repo, + name: labelName + }); + console.log(`Successfully removed "${labelName}" from PR #${prNumber}`); + } catch (error) { + if (error.status === 404) { + console.log('Label not found, nothing to remove.'); + } else { + throw error; + } + } + } From 379e1c7c667ac2d3aeb9b9dfab2e33da43e2fd2e Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 17:06:50 +0530 Subject: [PATCH 049/262] fix(ci): fix trigger workflow to enable secure labeling --- .github/workflows/approve-label.yml | 53 ++++++++--------------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/.github/workflows/approve-label.yml b/.github/workflows/approve-label.yml index 28481148..81a93b07 100644 --- a/.github/workflows/approve-label.yml +++ b/.github/workflows/approve-label.yml @@ -1,51 +1,26 @@ -name: Auto Label Approved PR +name: Approve Label Trigger on: pull_request_review: types: [submitted] -permissions: - issues: write - pull-requests: write - jobs: - manage-label: + check-review: # Only run for trusted reviewers (Maintainers) if: | contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) runs-on: ubuntu-latest steps: - - name: Update Approved Label - uses: actions/github-script@v7 - with: - script: | - const state = context.payload.review.state; - const issue_number = context.issue.number; - const { owner, repo } = context.repo; - const labelName = 'gssoc:approved'; + - name: Save review info + run: | + echo "${{ github.event.pull_request.number }}" > pr_number.txt + echo "${{ github.event.review.state }}" > review_state.txt - if (state === 'approved') { - await github.rest.issues.addLabels({ - issue_number, - owner, - repo, - labels: [labelName] - }); - console.log(`Added label "${labelName}" to PR #${issue_number}`); - } else if (state === 'changes_requested') { - try { - await github.rest.issues.removeLabel({ - issue_number, - owner, - repo, - name: labelName - }); - console.log(`Removed label "${labelName}" from PR #${issue_number} due to changes requested`); - } catch (error) { - if (error.status === 404) { - console.log(`Label "${labelName}" not found or already removed from PR #${issue_number}`); - } else { - throw error; - } - } - } + - name: Upload review info + uses: actions/upload-artifact@v4 + with: + name: review-info + path: | + pr_number.txt + review_state.txt + retention-days: 1 From e592f8f6ec316f603e69d4f587f6e5a9aea1b63e Mon Sep 17 00:00:00 2001 From: arpittkhandelwal Date: Sat, 16 May 2026 17:07:45 +0530 Subject: [PATCH 050/262] feat(ci): add /approve comment command support --- .github/workflows/approve-label-handler.yml | 23 ++++++++++++++++++--- .github/workflows/approve-label.yml | 22 +++++++++++++++----- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/.github/workflows/approve-label-handler.yml b/.github/workflows/approve-label-handler.yml index 9d960bd5..990dcb13 100644 --- a/.github/workflows/approve-label-handler.yml +++ b/.github/workflows/approve-label-handler.yml @@ -27,13 +27,30 @@ jobs: script: | const fs = require('fs'); const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim()); - const state = fs.readFileSync('review_state.txt', 'utf8').trim(); + const state = fs.readFileSync('state.txt', 'utf8').trim(); const labelName = 'gssoc:approved'; const { owner, repo } = context.repo; - console.log(`Processing PR #${prNumber} with review state: ${state}`); + console.log(`Processing PR #${prNumber} with action/state: ${state}`); - if (state === 'approved') { + // Handle the /approve command + if (state === 'command_approve') { + try { + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: prNumber, + event: 'APPROVE', + body: 'Approved via `/approve` command.' + }); + console.log(`Successfully submitted approval review for PR #${prNumber}`); + } catch (error) { + console.log(`Failed to submit approval review (might already be approved): ${error.message}`); + } + } + + // Add or remove labels + if (state === 'approved' || state === 'command_approve') { await github.rest.issues.addLabels({ issue_number: prNumber, owner, diff --git a/.github/workflows/approve-label.yml b/.github/workflows/approve-label.yml index 81a93b07..0ec05647 100644 --- a/.github/workflows/approve-label.yml +++ b/.github/workflows/approve-label.yml @@ -3,18 +3,30 @@ name: Approve Label Trigger on: pull_request_review: types: [submitted] + issue_comment: + types: [created] jobs: - check-review: + check-trigger: # Only run for trusted reviewers (Maintainers) if: | - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) + (github.event_name == 'pull_request_review' && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/approve') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) runs-on: ubuntu-latest steps: - name: Save review info run: | - echo "${{ github.event.pull_request.number }}" > pr_number.txt - echo "${{ github.event.review.state }}" > review_state.txt + if [[ "${{ github.event_name }}" == "pull_request_review" ]]; then + echo "${{ github.event.pull_request.number }}" > pr_number.txt + echo "${{ github.event.review.state }}" > state.txt + else + echo "${{ github.event.issue.number }}" > pr_number.txt + echo "command_approve" > state.txt + fi - name: Upload review info uses: actions/upload-artifact@v4 @@ -22,5 +34,5 @@ jobs: name: review-info path: | pr_number.txt - review_state.txt + state.txt retention-days: 1 From f0b6b66437f8fdc78818c5e047bc776c2eeb4232 Mon Sep 17 00:00:00 2001 From: codersurya0904 <89637956+surya0904shankar@users.noreply.github.com> Date: Sat, 16 May 2026 21:38:25 +0530 Subject: [PATCH 051/262] chore: add GitHub issue templates for bug reports and feature requests (#614) * chore: add GitHub issue templates for bug reports and feature requests * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.yml | 112 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 9 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 120 ++++++++++++++++----- 3 files changed, 214 insertions(+), 27 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..46a39c6a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,112 @@ +name: 🐛 Bug Report +description: Report a bug or unexpected behavior in ZerithDB +title: "[Bug]: " +labels: ["bug", "needs-triage"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the sections below as completely as possible so we can reproduce and fix the issue quickly. + + - type: textarea + id: description + attributes: + label: 📋 Description + description: A clear and concise description of what the bug is. + placeholder: Describe what went wrong... + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: 🔁 Reproduction Steps + description: Step-by-step instructions to reproduce the behavior. + placeholder: | + 1. Initialize ZerithDB with `...` + 2. Run the query `...` + 3. Observe the error `...` + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: ✅ Expected Behavior + description: What did you expect to happen? + placeholder: Describe what you expected to happen instead... + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: ❌ Actual Behavior + description: What actually happened? Include any error messages or stack traces. + placeholder: Paste error output or describe the unexpected result... + validations: + required: true + + - type: markdown + attributes: + value: "## 🖥️ Environment" + + - type: dropdown + id: os + attributes: + label: Operating System + description: Which OS are you running ZerithDB on? + options: + - Windows + - macOS + - Ubuntu / Debian + - Fedora / RHEL / CentOS + - Other Linux + - Other (specify in additional context) + validations: + required: true + + - type: dropdown + id: node-version + attributes: + label: Node.js Version + description: Which version of Node.js are you using? Run `node --version` to check. + options: + - "22.x (LTS)" + - "20.x (LTS)" + - "Other (specify in additional context)" + validations: + required: true + + - type: input + id: zerithdb-version + attributes: + label: ZerithDB Version + description: What version of ZerithDB are you using? + placeholder: e.g. 1.2.3 + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: 📎 Additional Context + description: Add any other context, screenshots, or logs that might help diagnose the issue. + placeholder: Paste logs, screenshots, config snippets, etc. + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: ✔️ Checklist + options: + - label: I have searched existing issues to make sure this bug has not already been reported. + required: true + - label: I have included all relevant information above. + required: true + - label: I am using a supported version of Node.js and ZerithDB. + required: false + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..6401ec50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 Discussions & Questions + url: https://github.com/Zerith-Labs/ZerithDB/discussions + about: For general questions, ideas, or help — use GitHub Discussions instead of opening an issue. + - name: 📖 Documentation + url: https://github.com/Zerith-Labs/ZerithDB#readme + about: Check the README and docs before opening an issue. + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index fcaf1a80..770fe754 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,27 +1,93 @@ ---- -name: "✨ Feature Request" -about: "Suggest a new feature or improvement" -labels: ["enhancement"] ---- - -## Problem Statement - - - -## Proposed Solution - - - -## API Design (if applicable) - -```typescript -// What would the ideal API look like? -``` - -## Alternatives Considered - - - -## Additional Context - - +name: 🚀 Feature Request +description: Suggest a new feature or enhancement for ZerithDB +title: "[Feature]: " +labels: ["enhancement", "needs-discussion"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Have an idea that would make ZerithDB better? We'd love to hear it! + Please fill out the sections below to help us understand your proposal clearly. + + - type: textarea + id: problem-statement + attributes: + label: 🧩 Problem Statement + description: | + What problem are you trying to solve? Describe the current limitation or gap. + A good problem statement focuses on the *why*, not the *what*. + placeholder: | + As a ZerithDB user, I find it difficult to... because... + Currently there is no way to... which means I have to... + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: 💡 Proposed Solution + description: Describe the feature or change you'd like to see. Be as specific as possible. + placeholder: | + I'd like a new API method / config option / CLI command that... + It should behave like... and accept the following parameters... + validations: + required: true + + - type: textarea + id: alternatives-considered + attributes: + label: 🔄 Alternatives Considered + description: | + Have you considered any workarounds or alternative approaches? + Explaining what you've already tried helps us understand the design space. + placeholder: | + I tried using... but it doesn't work because... + Another approach could be... but the downside is... + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: 📊 Priority / Impact + description: How important is this feature to your workflow? + options: + - "🔴 Critical — blocking my use of ZerithDB" + - "🟠 High — significantly impacts my productivity" + - "🟡 Medium — would be a nice improvement" + - "🟢 Low — minor convenience / nice to have" + validations: + required: true + + - type: dropdown + id: contribution + attributes: + label: 🙋 Willing to Contribute? + description: Are you interested in implementing this feature yourself? + options: + - "Yes — I'd like to submit a pull request" + - "Maybe — I'd need some guidance" + - "No — I'm just suggesting the idea" + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: 📎 Additional Context + description: Add any other context, mockups, links to similar features in other projects, or references that support this request. + placeholder: Links, screenshots, diagrams, related issues... + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: ✔️ Checklist + options: + - label: I have searched existing issues and discussions to make sure this feature has not already been requested. + required: true + - label: I have clearly described the problem this feature solves, not just the solution. + required: true From 028556591a41bce8ccde26136d74d2b4f8de5969 Mon Sep 17 00:00:00 2001 From: Sara-Thakur Date: Sat, 16 May 2026 21:38:29 +0530 Subject: [PATCH 052/262] fix: update footer discord link to point to community invite url (#612) --- apps/web/src/app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 27082986..59593141 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -692,7 +692,7 @@ export default function LandingPage() { Pricing - + Discord
From e949dc8cd6dde566930a064a5fc583afcd66960f Mon Sep 17 00:00:00 2001 From: RawKnee Date: Sat, 16 May 2026 21:38:33 +0530 Subject: [PATCH 053/262] feat: add Astro integration package (#611) --- packages/astro/README.md | 20 + packages/astro/package.json | 13 + packages/astro/src/index.ts | 12 + packages/astro/tsconfig.json | 4 + pnpm-lock.yaml | 2003 +++++++++++++++++++++++++++++++++- 5 files changed, 2031 insertions(+), 21 deletions(-) create mode 100644 packages/astro/README.md create mode 100644 packages/astro/package.json create mode 100644 packages/astro/src/index.ts create mode 100644 packages/astro/tsconfig.json diff --git a/packages/astro/README.md b/packages/astro/README.md new file mode 100644 index 00000000..0f78e774 --- /dev/null +++ b/packages/astro/README.md @@ -0,0 +1,20 @@ +# @zerithdb/astro + +Astro integration for ZerithDB. + +## Installation + +```bash +npm install @zerithdb/astro +``` + +## Usage + +```js +import { defineConfig } from 'astro/config'; +import zerithdb from '@zerithdb/astro'; + +export default defineConfig({ + integrations: [zerithdb()] +}); +``` \ No newline at end of file diff --git a/packages/astro/package.json b/packages/astro/package.json new file mode 100644 index 00000000..0e21a6da --- /dev/null +++ b/packages/astro/package.json @@ -0,0 +1,13 @@ +{ + "name": "@zerithdb/astro", + "version": "0.1.0", + "description": "Astro integration for ZerithDB", + "main": "./src/index.ts", + "types": "./src/index.ts", + "peerDependencies": { + "astro": "^4.0.0" + }, + "devDependencies": { + "astro": "^6.3.3" + } +} \ No newline at end of file diff --git a/packages/astro/src/index.ts b/packages/astro/src/index.ts new file mode 100644 index 00000000..0ddc4476 --- /dev/null +++ b/packages/astro/src/index.ts @@ -0,0 +1,12 @@ +import type { AstroIntegration } from 'astro'; + +export default function zerithdb(): AstroIntegration { + return { + name: 'zerithdb', + hooks: { + 'astro:config:setup'() { + console.log('ZerithDB Astro integration loaded'); + }, + }, + }; +} \ No newline at end of file diff --git a/packages/astro/tsconfig.json b/packages/astro/tsconfig.json new file mode 100644 index 00000000..d6be99ff --- /dev/null +++ b/packages/astro/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig/base.json", + "include": ["src"] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99b01a68..a4307b2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,12 @@ importers: specifier: ^5.5.4 version: 5.9.3 + packages/astro: + devDependencies: + astro: + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.3)(tsx@4.21.0)(yaml@2.8.4) + packages/auth: dependencies: '@noble/ed25519': @@ -440,6 +446,23 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@astrojs/compiler@4.0.0': + resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==} + + '@astrojs/internal-helpers@0.9.1': + resolution: {integrity: sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==} + + '@astrojs/markdown-remark@7.1.2': + resolution: {integrity: sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==} + + '@astrojs/prism@4.0.2': + resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==} + engines: {node: '>=22.12.0'} + + '@astrojs/telemetry@3.3.2': + resolution: {integrity: sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -511,6 +534,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@capsizecss/unpack@4.0.0': + resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} + engines: {node: '>=18'} + '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -566,6 +593,14 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@clack/core@1.3.1': + resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.4.0': + resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} + engines: {node: '>= 20.12.0'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -1192,6 +1227,18 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.60.3': resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} cpu: [arm] @@ -1323,6 +1370,37 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -1451,18 +1529,33 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -1497,6 +1590,9 @@ packages: '@types/simple-peer@9.11.9': resolution: {integrity: sha512-6Gdl7TSS5oh9nuwKD4Pl8cSmaxWycYeZz9HLnJBNvIwWjZuGVsmHe9RwW3+9RxfhC1aIR9Z83DvaJoMw6rhkbg==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -1790,6 +1886,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1808,6 +1908,9 @@ packages: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -1843,6 +1946,11 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + astro@6.3.3: + resolution: {integrity: sha512-wvLIZQYbBZt6U8gyflBW4SLBypaqdwLZUH93rT3oT53cmQ0bTGubvMAGjqBRoheOYzYcTJZtW6czztzbu4kQ5g==} + engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -1859,6 +1967,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1878,6 +1989,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} @@ -1929,6 +2043,9 @@ packages: caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1941,6 +2058,15 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -1952,6 +2078,14 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -1967,6 +2101,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1977,6 +2115,13 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -1989,6 +2134,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2002,10 +2151,39 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2041,6 +2219,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -2056,6 +2237,16 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2064,9 +2255,19 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dexie@4.4.2: resolution: {integrity: sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2079,6 +2280,23 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2100,6 +2318,14 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -2130,6 +2356,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2164,6 +2393,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-next@16.2.6: resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==} peerDependencies: @@ -2299,6 +2532,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2321,6 +2557,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -2345,6 +2584,15 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2395,6 +2643,17 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2477,6 +2736,13 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@5.0.0-beta.4: + resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==} + engines: {node: '>=20.20.0'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2523,6 +2789,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + happy-dom@20.9.0: resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} @@ -2554,12 +2823,51 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -2611,6 +2919,9 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2646,6 +2957,16 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2670,6 +2991,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -2758,6 +3084,10 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2813,6 +3143,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -2955,6 +3288,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2977,10 +3313,61 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2988,6 +3375,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3031,6 +3502,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3053,6 +3528,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + next@16.2.6: resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} engines: {node: '>=20.9.0'} @@ -3074,13 +3553,26 @@ packages: sass: optional: true + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-releases@2.0.38: resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3089,6 +3581,9 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3121,6 +3616,15 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3132,6 +3636,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3159,6 +3669,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -3171,6 +3685,14 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} + p-queue@9.3.0: + resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==} + engines: {node: '>=20'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -3181,14 +3703,23 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3226,6 +3757,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3301,6 +3835,10 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3308,6 +3846,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3318,6 +3859,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -3354,14 +3898,55 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3382,6 +3967,18 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3425,6 +4022,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -3464,6 +4065,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -3505,6 +4110,10 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3513,6 +4122,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -3570,6 +4182,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3620,6 +4235,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svgo@4.0.1: + resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + engines: {node: '>=16'} + hasBin: true + tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} @@ -3641,12 +4261,23 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyclip@0.1.12: + resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==} + engines: {node: ^16.14.0 || >= 17.3.0} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -3671,6 +4302,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -3759,10 +4396,16 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -3770,6 +4413,39 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.7.4: + resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3777,23 +4453,94 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true + unstorage@1.17.5: + resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3830,6 +4577,54 @@ packages: terser: optional: true + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest@2.1.9: resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3855,6 +4650,9 @@ packages: jsdom: optional: true + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -3871,6 +4669,10 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -3908,6 +4710,9 @@ packages: utf-8-validate: optional: true + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y-indexeddb@9.0.12: resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -3922,6 +4727,10 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yjs@13.6.30: resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -3930,6 +4739,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -3943,10 +4756,57 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} + '@astrojs/compiler@4.0.0': {} + + '@astrojs/internal-helpers@0.9.1': + dependencies: + picomatch: 4.0.4 + + '@astrojs/markdown-remark@7.1.2': + dependencies: + '@astrojs/internal-helpers': 0.9.1 + '@astrojs/prism': 4.0.2 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + js-yaml: 4.1.1 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + retext-smartypants: 6.2.0 + shiki: 4.0.2 + smol-toml: 1.6.1 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@4.0.2': + dependencies: + prismjs: 1.30.0 + + '@astrojs/telemetry@3.3.2': + dependencies: + ci-info: 4.4.0 + dset: 3.1.4 + is-docker: 4.0.0 + is-wsl: 3.1.1 + which-pm-runs: 1.1.0 + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4049,6 +4909,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@capsizecss/unpack@4.0.0': + dependencies: + fontkitten: 1.0.3 + '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 @@ -4192,6 +5056,18 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@clack/core@1.3.1': + dependencies: + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + + '@clack/prompts@1.4.0': + dependencies: + '@clack/core': 1.3.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -4642,6 +5518,16 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@oslojs/encoding@1.1.0': {} + + '@rollup/pluginutils@5.3.0(rollup@4.60.3)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.3 + '@rollup/rollup-android-arm-eabi@4.60.3': optional: true @@ -4721,6 +5607,46 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/merge-streams@4.0.0': {} '@swc/helpers@0.5.15': @@ -4819,14 +5745,32 @@ snapshots: tslib: 2.8.1 optional: true + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + '@types/node@12.20.55': {} '@types/node@20.19.40': @@ -4865,6 +5809,8 @@ snapshots: dependencies: '@types/node': 22.19.18 + '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} '@types/whatwg-mimetype@3.0.2': {} @@ -5177,6 +6123,11 @@ snapshots: any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -5201,6 +6152,8 @@ snapshots: is-string: 1.1.1 math-intrinsics: 1.1.0 + array-iterate@2.0.1: {} + array-union@2.1.0: {} array.prototype.findlast@1.2.5: @@ -5258,6 +6211,99 @@ snapshots: ast-types-flow@0.0.8: {} + astro@6.3.3(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.3)(tsx@4.21.0)(yaml@2.8.4): + dependencies: + '@astrojs/compiler': 4.0.0 + '@astrojs/internal-helpers': 0.9.1 + '@astrojs/markdown-remark': 7.1.2 + '@astrojs/telemetry': 3.3.2 + '@capsizecss/unpack': 4.0.0 + '@clack/prompts': 1.4.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + aria-query: 5.3.2 + axobject-query: 4.1.0 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 2.0.0 + cookie: 1.1.1 + devalue: 5.8.1 + diff: 8.0.4 + dset: 3.1.4 + es-module-lexer: 2.1.0 + esbuild: 0.27.7 + flattie: 1.1.1 + fontace: 0.4.1 + get-tsconfig: 5.0.0-beta.4 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + js-yaml: 4.1.1 + jsonc-parser: 3.3.1 + magic-string: 0.30.21 + magicast: 0.5.3 + mrmime: 2.0.1 + neotraverse: 0.6.18 + obug: 2.1.1 + p-limit: 7.3.0 + p-queue: 9.3.0 + package-manager-detector: 1.6.0 + piccolore: 0.1.3 + picomatch: 4.0.4 + rehype: 13.0.2 + semver: 7.8.0 + shiki: 4.0.2 + smol-toml: 1.6.1 + svgo: 4.0.1 + tinyclip: 0.1.12 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + ultrahtml: 1.6.0 + unifont: 0.7.4 + unist-util-visit: 5.1.0 + unstorage: 1.17.5 + vfile: 6.0.3 + vite: 7.3.3(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) + vitefu: 1.1.3(vite@7.3.3(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) + xxhash-wasm: 1.1.0 + yargs-parser: 22.0.0 + zod: 4.4.3 + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - uploadthing + - yaml + async-function@1.0.0: {} available-typed-arrays@1.0.7: @@ -5268,6 +6314,8 @@ snapshots: axobject-query@4.1.0: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -5280,6 +6328,8 @@ snapshots: dependencies: is-windows: 1.0.2 + boolbase@1.0.0: {} + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 @@ -5338,6 +6388,8 @@ snapshots: caniuse-lite@1.0.30001792: {} + ccount@2.0.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -5353,6 +6405,12 @@ snapshots: chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + chardet@2.1.1: {} check-error@2.1.3: {} @@ -5361,6 +6419,12 @@ snapshots: dependencies: readdirp: 4.1.2 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + ci-info@4.4.0: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -5374,6 +6438,8 @@ snapshots: client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5382,12 +6448,18 @@ snapshots: colorette@2.0.20: {} + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + commander@12.1.0: {} commander@13.1.0: {} commander@4.1.1: {} + common-ancestor-path@2.0.0: {} + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -5396,12 +6468,44 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@1.2.3: {} + + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + csstype@3.2.3: {} damerau-levenshtein@1.0.8: {} @@ -5432,6 +6536,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -5448,12 +6556,26 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} + devalue@5.8.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dexie@4.4.2: {} + diff@8.0.4: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -5466,6 +6588,26 @@ snapshots: dependencies: esutils: 2.0.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dset@3.1.4: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5488,6 +6630,10 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@4.5.0: {} + + entities@6.0.1: {} + entities@7.0.1: {} environment@1.1.0: {} @@ -5576,6 +6722,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -5656,12 +6804,14 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-next@16.2.6(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.2.6 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -5684,7 +6834,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -5699,14 +6849,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -5721,7 +6871,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.3 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -5915,6 +7065,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -5952,6 +7104,8 @@ snapshots: expect-type@1.3.0: {} + extend@3.0.2: {} + extendable-error@0.1.7: {} fake-indexeddb@6.2.5: {} @@ -5978,6 +7132,16 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -6031,6 +7195,16 @@ snapshots: flatted@3.4.2: {} + flattie@1.1.1: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -6117,6 +7291,12 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.4: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-slugger@2.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -6168,6 +7348,18 @@ snapshots: graphemer@1.4.0: {} + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + happy-dom@20.9.0: dependencies: '@types/node': 22.19.18 @@ -6202,12 +7394,105 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.1 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.2.0: {} + human-id@4.1.3: {} human-signals@5.0.0: {} @@ -6246,6 +7531,8 @@ snapshots: hasown: 2.0.3 side-channel: 1.1.0 + iron-webcrypto@1.2.1: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -6290,6 +7577,10 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-docker@3.0.0: {} + + is-docker@4.0.0: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -6314,6 +7605,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@2.0.0: {} is-map@2.0.3: {} @@ -6384,6 +7679,10 @@ snapshots: is-windows@1.0.2: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@2.0.5: {} isexe@2.0.0: {} @@ -6428,6 +7727,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -6564,6 +7865,8 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -6584,12 +7887,335 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.27.1: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -6630,6 +8256,8 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} multiformats@13.4.2: {} @@ -6646,6 +8274,8 @@ snapshots: natural-compare@1.4.0: {} + neotraverse@0.6.18: {} + next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.6 @@ -6670,6 +8300,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -6677,8 +8311,14 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.4: {} + node-releases@2.0.38: {} + normalize-path@3.0.0: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -6688,6 +8328,10 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -6730,6 +8374,16 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.11: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -6742,6 +8396,14 @@ snapshots: dependencies: mimic-function: 5.0.1 + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6783,6 +8445,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -6793,6 +8459,13 @@ snapshots: p-map@2.1.0: {} + p-queue@9.3.0: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-timeout@7.0.1: {} + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -6801,12 +8474,27 @@ snapshots: dependencies: quansync: 0.2.11 + package-manager-detector@1.6.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + parse-ms@4.0.0: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -6830,6 +8518,8 @@ snapshots: pathval@2.0.1: {} + piccolore@0.1.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6881,6 +8571,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + prismjs@1.30.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -6892,12 +8584,16 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + punycode@2.3.1: {} quansync@0.2.11: {} queue-microtask@1.2.3: {} + radix3@1.1.2: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -6936,6 +8632,8 @@ snapshots: readdirp@4.1.2: {} + readdirp@5.0.0: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.9 @@ -6947,6 +8645,16 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.9 @@ -6956,6 +8664,72 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -6976,6 +8750,31 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.1.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -7047,6 +8846,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.6.0: {} + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -7117,6 +8918,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -7175,10 +8987,14 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + smol-toml@1.6.1: {} + source-map-js@1.2.1: {} source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -7261,6 +9077,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -7300,6 +9121,16 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svgo@4.0.1: + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.2.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + tailwindcss@4.3.0: {} tapable@2.3.3: {} @@ -7316,10 +9147,16 @@ snapshots: dependencies: any-promise: 1.3.0 + tiny-inflate@1.0.3: {} + tinybench@2.9.0: {} + tinyclip@0.1.12: {} + tinyexec@0.3.2: {} + tinyexec@1.1.2: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -7337,6 +9174,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@1.4.3(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -7454,6 +9295,8 @@ snapshots: ufo@1.6.4: {} + ultrahtml@1.6.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -7461,10 +9304,70 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + uncrypto@0.1.3: {} + undici-types@6.21.0: {} unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.7.4: + dependencies: + css-tree: 3.2.1 + ofetch: 1.5.1 + ohash: 2.0.11 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@0.1.2: {} unrs-resolver@1.11.1: @@ -7491,6 +9394,17 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + unstorage@1.17.5: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.3.6 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -7505,6 +9419,21 @@ snapshots: uuid@10.0.0: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-node@2.1.9(@types/node@22.19.18)(lightningcss@1.32.0): dependencies: cac: 6.7.14 @@ -7533,6 +9462,26 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 + vite@7.3.3(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.14 + rollup: 4.60.3 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.18 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + tsx: 4.21.0 + yaml: 2.8.4 + + vitefu@1.1.3(vite@7.3.3(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)): + optionalDependencies: + vite: 7.3.3(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) + vitest@2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0): dependencies: '@vitest/expect': 2.1.9 @@ -7569,6 +9518,8 @@ snapshots: - supports-color - terser + web-namespaces@2.0.1: {} + whatwg-mimetype@3.0.0: {} which-boxed-primitive@1.1.1: @@ -7602,6 +9553,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-pm-runs@1.1.0: {} + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 @@ -7633,6 +9586,8 @@ snapshots: ws@8.20.0: {} + xxhash-wasm@1.1.0: {} + y-indexeddb@9.0.12(yjs@13.6.30): dependencies: lib0: 0.2.117 @@ -7642,12 +9597,16 @@ snapshots: yaml@2.8.4: {} + yargs-parser@22.0.0: {} + yjs@13.6.30: dependencies: lib0: 0.2.117 yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + yoctocolors@2.1.2: {} zod-validation-error@4.0.2(zod@4.4.3): @@ -7655,3 +9614,5 @@ snapshots: zod: 4.4.3 zod@4.4.3: {} + + zwitch@2.0.4: {} From b1fb6f9921b1144bf2ab88d4553a65e506397bd7 Mon Sep 17 00:00:00 2001 From: Prashan Date: Sat, 16 May 2026 21:38:37 +0530 Subject: [PATCH 054/262] feat(network): add multi-server signaling failover support (#610) --- packages/core/src/types/config.ts | 9 ++- packages/network/src/network-manager.ts | 77 +++++++++++++++++-------- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 42bcf001..9d183496 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -5,6 +5,13 @@ export interface SyncConfig { */ signalingUrl?: string; + /** + * Multiple signaling server URLs for automatic failover. + * Tried in order — falls back to the next on failure. + * Takes priority over signalingUrl if both are set. + */ + signalingUrls?: string[]; + /** * STUN/TURN server URLs for WebRTC ICE negotiation. * @default Uses Google's public STUN servers @@ -67,4 +74,4 @@ export interface ZerithDBConfig { * @default "warn" */ logLevel?: "debug" | "info" | "warn" | "error" | "silent"; -} +} \ No newline at end of file diff --git a/packages/network/src/network-manager.ts b/packages/network/src/network-manager.ts index 5d797ebf..0cd10427 100644 --- a/packages/network/src/network-manager.ts +++ b/packages/network/src/network-manager.ts @@ -21,6 +21,8 @@ interface SignalingMessage { payload: unknown; } +const DEFAULT_SIGNALING_URL = "wss://arpitkhandelwal810-zerith-signaling.hf.space"; + /** * Manages WebRTC peer-to-peer connections for a ZerithDB app. * @@ -31,6 +33,9 @@ interface SignalingMessage { * Supports automatic transport fallback: if WebSocket signaling is blocked * (e.g. by corporate firewalls), the manager transparently downgrades to * HTTP long-polling. + * + * Supports multiple signaling server URLs with automatic failover: + * if one server fails, the next URL in the list is tried automatically. */ export class NetworkManager extends EventEmitter { private transport: SignalingTransport | null = null; @@ -41,6 +46,7 @@ export class NetworkManager extends EventEmitter { private reconnectTimer: ReturnType | null = null; private reconnectAttempts = 0; private disposed = false; + private currentUrlIndex = 0; constructor( private readonly config: ZerithDBConfig, @@ -54,20 +60,54 @@ export class NetworkManager extends EventEmitter { return this.activeTransportType; } + /** + * Returns the ordered list of signaling URLs to try. + * Supports both signalingUrls (array) and signalingUrl (single). + * Falls back to the default URL if neither is set. + */ + private getSignalingUrls(): string[] { + if (this.config.sync?.signalingUrls && this.config.sync.signalingUrls.length > 0) { + return this.config.sync.signalingUrls; + } + return [this.config.sync?.signalingUrl ?? DEFAULT_SIGNALING_URL]; + } + /** * Connect to the signaling server and join the P2P room. - * After connection, WebRTC handshakes happen automatically. + * Tries each URL in order — automatically fails over to the next on failure. * - * Transport selection: - * - `"auto"` (default): Try WebSocket first with a 5s timeout. - * If it fails, fall back to HTTP long-polling. + * Transport selection per URL: + * - `"auto"` (default): Try WebSocket first, fall back to HTTP long-polling. * - `"websocket"`: WebSocket only. * - `"polling"`: HTTP long-polling only. */ async connect(roomId: string): Promise { + const urls = this.getSignalingUrls(); + + for (let i = 0; i < urls.length; i++) { + const index = (this.currentUrlIndex + i) % urls.length; + const url = urls[index]; + + try { + await this.connectToUrl(url, roomId); + this.currentUrlIndex = index; + return; + } catch { + console.warn(`[ZerithDB] Signaling server failed: ${url}. Trying next...`); + } + } + + throw new ZerithDBError( + ErrorCode.NETWORK_SIGNALING_FAILED, + `All signaling servers failed. Tried: ${urls.join(", ")}` + ); + } + + /** + * Try connecting to a single signaling URL using the configured transport. + */ + private async connectToUrl(signalingUrl: string, roomId: string): Promise { const transportPref = this.config.sync?.transport ?? "auto"; - const signalingUrl = - this.config.sync?.signalingUrl ?? "wss://arpitkhandelwal810-zerith-signaling.hf.space"; if (transportPref === "websocket") { await this.connectWebSocket(signalingUrl, roomId); @@ -92,15 +132,7 @@ export class NetworkManager extends EventEmitter { `Falling back to HTTP long-polling.` ); - try { - await this.connectPolling(signalingUrl, roomId); - } catch (pollError) { - throw new ZerithDBError( - ErrorCode.NETWORK_SIGNALING_FAILED, - "Both WebSocket and HTTP long-polling signaling transports failed", - { cause: pollError } - ); - } + await this.connectPolling(signalingUrl, roomId); } } } @@ -183,7 +215,6 @@ export class NetworkManager extends EventEmitter { } private attachTransport(transport: SignalingTransport, roomId: string): void { - // Clean up previous transport if (this.transport !== null) { this.transport.close(); } @@ -205,11 +236,6 @@ export class NetworkManager extends EventEmitter { }); } - /** - * Convert a WebSocket URL to an HTTP URL. - * `wss://host/path` → `https://host/path` - * `ws://host/path` → `http://host/path` - */ private wsUrlToHttp(wsUrl: string): string { if (wsUrl.startsWith("wss://")) { return "https://" + wsUrl.slice(6); @@ -217,7 +243,6 @@ export class NetworkManager extends EventEmitter { if (wsUrl.startsWith("ws://")) { return "http://" + wsUrl.slice(5); } - // Already an HTTP URL — return as-is return wsUrl; } @@ -226,7 +251,6 @@ export class NetworkManager extends EventEmitter { private handleSignalingMessage(msg: SignalingMessage): void { switch (msg.type) { case "peer-list": - // Server sends list of existing peers — initiate connections for (const peerId of msg.payload as PeerId[]) { if (peerId !== this.localPeerId) { this.createPeer(peerId, true); @@ -285,7 +309,7 @@ export class NetworkManager extends EventEmitter { peer.on("connect", () => { const info: PeerInfo = { peerId: remotePeerId, - did: "", // filled in via auth handshake message + did: "", publicKey: "", connectedAt: Date.now(), }; @@ -320,13 +344,16 @@ export class NetworkManager extends EventEmitter { } private scheduleReconnect(roomId: string): void { + const urls = this.getSignalingUrls(); const delay = this.config.network?.reconnectDelay ?? 1000; const backoff = Math.min(delay * 2 ** this.reconnectAttempts, 30_000); const jitter = Math.random() * 1000; + this.currentUrlIndex = (this.currentUrlIndex + 1) % urls.length; this.reconnectAttempts++; + this.reconnectTimer = setTimeout(() => { void this.connect(roomId); }, backoff + jitter); } -} +} \ No newline at end of file From 024fc6c189df9d4e03788fa671029336373a8465 Mon Sep 17 00:00:00 2001 From: SrineedhiRB Date: Sat, 16 May 2026 21:38:43 +0530 Subject: [PATCH 055/262] feat(ui): add interactive peer identity social graph visualizer (#607) Co-authored-by: SrineedhiRB --- apps/web/src/app/page.tsx | 3 +- apps/web/src/components/SocialGraph.tsx | 254 ++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/SocialGraph.tsx diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 59593141..7744cb6b 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,5 +1,5 @@ "use client"; - +import SocialGraph from "../components/SocialGraph"; import Link from "next/link"; import { ArrowRight, @@ -702,6 +702,7 @@ export default function LandingPage() {
+ ); } diff --git a/apps/web/src/components/SocialGraph.tsx b/apps/web/src/components/SocialGraph.tsx new file mode 100644 index 00000000..ec3c1fdf --- /dev/null +++ b/apps/web/src/components/SocialGraph.tsx @@ -0,0 +1,254 @@ +"use client"; + +import { useState } from "react"; + +const peers = [ + { + name: "Auth Node", + color: "#22c55e", + status: "Trusted", + trust: "98%", + latency: "Low", + top: "18%", + left: "20%", + }, + { + name: "Sync Peer", + color: "#3b82f6", + status: "Active", + trust: "84%", + latency: "Medium", + top: "45%", + left: "45%", + }, + { + name: "Relay Node", + color: "#eab308", + status: "Lagging", + trust: "61%", + latency: "High", + top: "72%", + left: "58%", + }, + { + name: "Unknown Peer", + color: "#ef4444", + status: "Suspicious", + trust: "29%", + latency: "Very High", + top: "28%", + left: "74%", + }, + { + name: "Storage Peer", + color: "#22c55e", + status: "Trusted", + trust: "95%", + latency: "Low", + top: "75%", + left: "25%", + }, +]; + +export default function SocialGraph() { + const [activePeer, setActivePeer] = useState(null); + + return ( +
+

+ Interactive Peer Identity Social Graph +

+ +

+ Real-time visualization of peer trust and network connectivity within ZerithDB ecosystems. +

+ +
+
+ ● LIVE NETWORK +
+
+ +
+ 🟢 Trusted + 🔵 Active + 🟡 Lagging + 🔴 Suspicious +
+ +
+ + + + + + + + + + + {peers.map((peer) => ( +
setActivePeer(peer.name)} + onMouseLeave={() => setActivePeer(null)} + style={{ + position: "absolute", + top: peer.top, + left: peer.left, + transform: "translate(-50%, -50%)", + textAlign: "center", + cursor: "pointer", + }} + > +
+ +
+

+ {peer.name} +

+ +

+ {peer.color === "#22c55e" + ? "🟢" + : peer.color === "#3b82f6" + ? "🔵" + : peer.color === "#eab308" + ? "🟡" + : "🔴"}{" "} + {peer.status} • {peer.trust} +

+
+
+ ))} +
+ + +
+ ); +} + + From 9bed706cdcf7ddc739f4841575af1810c55bc269 Mon Sep 17 00:00:00 2001 From: Bhavya Gupta Date: Sat, 16 May 2026 21:38:47 +0530 Subject: [PATCH 056/262] feat(sdk): add SemanticSearchBar component (closes #122) (#606) --- apps/web/src/components/SemanticSearchBar.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/web/src/components/SemanticSearchBar.tsx diff --git a/apps/web/src/components/SemanticSearchBar.tsx b/apps/web/src/components/SemanticSearchBar.tsx new file mode 100644 index 00000000..e69de29b From 8a1d33fcf2a7b89bac931eb93ca618d2636fa9d4 Mon Sep 17 00:00:00 2001 From: Muskan Date: Sat, 16 May 2026 21:38:50 +0530 Subject: [PATCH 057/262] feat(python): integrate Sentry crash reporting for sync engine (#283) (#605) --- packages/zerithdb-python/pyproject.toml | 1 + packages/zerithdb-python/src/zerithdb/client.py | 6 +++++- packages/zerithdb-python/src/zerithdb/network.py | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/zerithdb-python/pyproject.toml b/packages/zerithdb-python/pyproject.toml index 699d8f2c..34de807d 100644 --- a/packages/zerithdb-python/pyproject.toml +++ b/packages/zerithdb-python/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ dependencies = [ "aiortc>=1.5.0", "websockets>=11.0", + "sentry-sdk>=2.0.0", ] [project.optional-dependencies] diff --git a/packages/zerithdb-python/src/zerithdb/client.py b/packages/zerithdb-python/src/zerithdb/client.py index f6ec0dff..3ac0eec6 100644 --- a/packages/zerithdb-python/src/zerithdb/client.py +++ b/packages/zerithdb-python/src/zerithdb/client.py @@ -1,3 +1,4 @@ +import sentry_sdk import logging import uuid from typing import Callable, Dict, Any, Awaitable @@ -10,7 +11,7 @@ class ZerithClient: """ A Python client for ZerithDB that joins a WebRTC P2P mesh network. """ - def __init__(self, signaling_url: str = "wss://arpitkhandelwal810-zerith-signaling.hf.space"): + def __init__(self, signaling_url: str = "wss://arpitkhandelwal810-zerith-signaling.hf.space", sentry_dsn: str = None): self.signaling_url = signaling_url self.peer_id = str(uuid.uuid4()) self.network = NetworkManager(self.signaling_url, self.peer_id) @@ -19,6 +20,9 @@ def __init__(self, signaling_url: str = "wss://arpitkhandelwal810-zerith-signali # Setup network callbacks self.network.on_message = self._handle_network_message + if sentry_dsn: + sentry_sdk.init(dsn=sentry_dsn, traces_sample_rate=1.0) + logger.info("Sentry crash reporting enabled.") async def connect(self, room_id: str): """Connect to the signaling server and join the room.""" diff --git a/packages/zerithdb-python/src/zerithdb/network.py b/packages/zerithdb-python/src/zerithdb/network.py index ddb1531f..0714ab34 100644 --- a/packages/zerithdb-python/src/zerithdb/network.py +++ b/packages/zerithdb-python/src/zerithdb/network.py @@ -3,6 +3,7 @@ import logging from typing import Any, Dict, Optional, Callable, Awaitable +import sentry_sdk import websockets from aiortc import RTCPeerConnection, RTCSessionDescription, RTCDataChannel @@ -34,6 +35,7 @@ async def _listen_signaling(self): self._disconnect_event.set() except Exception as e: logger.error(f"Signaling error: {e}") + sentry_sdk.capture_exception(e) self._disconnect_event.set() async def _handle_signaling_message(self, msg: dict): @@ -63,9 +65,6 @@ async def _handle_signaling_message(self, msg: dict): remote_peer_id = msg.get("from") pc = self.peers.get(remote_peer_id) if pc: - # aiortc handles ICE candidates slightly differently, - # but we can try to add it. In many aiortc setups, ICE candidates - # are bundled in SDP or handled directly. pass async def _create_peer(self, remote_peer_id: str, initiator: bool, offer_payload: dict = None): @@ -124,6 +123,7 @@ def on_message(message): asyncio.create_task(self.on_message(data, remote_peer_id)) except Exception as e: logger.error(f"Failed to parse P2P message: {e}") + sentry_sdk.capture_exception(e) async def _send_signaling(self, data: dict): if self.ws and not self.ws.closed: @@ -137,4 +137,4 @@ async def broadcast(self, message: dict): channel.send(data) async def wait_until_disconnected(self): - await self._disconnect_event.wait() + await self._disconnect_event.wait() \ No newline at end of file From 8c4a8e3e5b26ee619f4f4d9fb0b50e85e72aaf3a Mon Sep 17 00:00:00 2001 From: DibyaJyoti Parida <170463456+Dibyajyoti1515@users.noreply.github.com> Date: Sat, 16 May 2026 21:38:54 +0530 Subject: [PATCH 058/262] docs: replace ASCII architecture diagram with Mermaid flowchart (#603) --- README.md | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 56a0a744..4c19dddd 100644 --- a/README.md +++ b/README.md @@ -110,27 +110,30 @@ const app = createApp({ ## Architecture in One Diagram -``` -┌─────────────────────────────────────────────────────────┐ -│ Your Browser │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ -│ │ ZerithDB │ │ Sync │ │ P2P Network Layer │ │ -│ │ SDK │──▶│ Engine │──▶│ (WebRTC mesh) │ │ -│ └──────────┘ │ (CRDT) │ └──────────────────────┘ │ -│ │ └──────────┘ │ │ -│ ▼ │ │ │ -│ ┌──────────┐ │ ┌───────▼──────┐ │ -│ │ Local DB │◀───────┘ │ Signaling │ │ -│ │(IndexedDB│ │ Server │ │ -│ └──────────┘ │ (WS relay) │ │ -└──────────────────────────────── └──────────────┘ ───────┘ - ▲ - Only for initial │ peer discovery - │ - ┌───────────┴──────────┐ - │ Other Peer Browser │ - └──────────────────────┘ +```mermaid +flowchart TB + subgraph browser["Your Browser"] + direction LR + SDK["ZerithDB SDK"] + SYNC["Sync Engine\n(CRDT)"] + P2P["P2P Network Layer\n(WebRTC mesh)"] + SDK -->|writes| SYNC + SYNC -->|broadcast| P2P + end + + subgraph storage["Local Storage"] + DB["Local DB\n(IndexedDB)"] + end + + SDK -->|persist| DB + SYNC -->|flush| DB + + SIG["Signaling Server\n(WS relay)"] + PEER["Other Peer Browser"] + + P2P -->|handshake only| SIG + SIG -->|peer discovery| PEER + P2P <-.->|"direct P2P\n(after handshake)"| PEER ``` The signaling server **never sees your data**. It only brokers the initial WebRTC handshake. After From c21d54fa93f36014eaf429b66430c5041f5f6c8a Mon Sep 17 00:00:00 2001 From: Rakshita Kumari <145382420+Rakshita006@users.noreply.github.com> Date: Sat, 16 May 2026 21:38:58 +0530 Subject: [PATCH 059/262] feat(ui): add skeleton loaders and peer connection status indicator (#601) * feat(ui): add skeleton loaders and peer connection status indicator * style: fix prettier formatting and lint issues --- apps/web/src/app/playground/page.tsx | 77 +++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/playground/page.tsx b/apps/web/src/app/playground/page.tsx index 7a981dcc..07013b7a 100644 --- a/apps/web/src/app/playground/page.tsx +++ b/apps/web/src/app/playground/page.tsx @@ -25,6 +25,30 @@ export default function PlaygroundPage() { const [inputB, setInputB] = useState(""); const [syncCount, setSyncCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [peerStatus, setPeerStatus] = useState<"connecting" | "connected" | "offline">( + "connecting" + ); + + // Simulate DB initialization on mount + useEffect(() => { + const timer = setTimeout(() => { + setIsLoading(false); + setPeerStatus("connected"); + }, 1500); + return () => clearTimeout(timer); + }, []); + + // Update peer status when network changes + useEffect(() => { + if (!isOnline) { + setPeerStatus("offline"); // eslint-disable-line react-hooks/set-state-in-effect + } else { + setPeerStatus("connecting"); + const timer = setTimeout(() => setPeerStatus("connected"), 1000); + return () => clearTimeout(timer); + } + }, [isOnline]); // Sync logic simulation useEffect(() => { @@ -91,6 +115,31 @@ export default function PlaygroundPage() { CRDT Sync Operations: {syncCount}
+ +
+
+ {peerStatus === "connected" + ? "Peers Connected" + : peerStatus === "connecting" + ? "Connecting to Peers..." + : "Peers Offline"} +
- {clientA.length === 0 ? ( + {isLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ ) : clientA.length === 0 ? (
No documents. Type below to create one.
@@ -179,7 +240,19 @@ export default function PlaygroundPage() {
- {clientB.length === 0 ? ( + {isLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ ) : clientB.length === 0 ? (
No documents. Type below to create one.
From af194cbe7a9e53828f27750e5b40e3cc892cdbbf Mon Sep 17 00:00:00 2001 From: Aditya Kadam Date: Sat, 16 May 2026 21:39:02 +0530 Subject: [PATCH 060/262] feat(infra): add /health endpoint with uptime and connection metrics (#600) --- infra/signaling-server/src/server.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/infra/signaling-server/src/server.ts b/infra/signaling-server/src/server.ts index 85896c9f..483ba0f6 100644 --- a/infra/signaling-server/src/server.ts +++ b/infra/signaling-server/src/server.ts @@ -3,6 +3,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "http"; const PORT = parseInt(process.env["PORT"] ?? "4000", 10); const HOST = process.env["HOST"] ?? "0.0.0.0"; +const SERVER_START_TIME = Date.now(); // ─── Shared room state ────────────────────────────────────────────────────── @@ -65,15 +66,21 @@ const server = createServer((req, res) => { const pathname = url.pathname; // Health check (existing behavior) - if (pathname === "/" && req.method === "GET") { + if ((pathname === "/" || pathname === "/health") && req.method === "GET") { + const uptimeSeconds = Math.floor((Date.now() - SERVER_START_TIME) / 1000); + res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ + status: "ok", service: "zerithdb-signaling", version: "0.1.0", + uptime_seconds: uptimeSeconds, + active_ws_connections: wss.clients.size, + active_polling_sessions: pollingSessions.size, rooms: rooms.size, peers: [...rooms.values()].reduce((acc, s) => acc + s.size, 0), - pollingSessions: pollingSessions.size, + timestamp: new Date().toISOString(), }) ); return; From d0ee842c25dee24a386525d98c6a59a93e088156 Mon Sep 17 00:00:00 2001 From: Yash Patil Date: Sat, 16 May 2026 21:39:06 +0530 Subject: [PATCH 061/262] Removed vercel default favicon and replaced it with Zerithdb favicon (#598) --- apps/web/src/app/favicon.ico | Bin 25931 -> 200696 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico index 718d6fea4835ec2d246af9800eddb7ffb276240c..5c8390f8bd28a3f2b54e84138d36afb27190c424 100644 GIT binary patch literal 200696 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%0D!Y9@9~dgGP@DW5*+-hZq((`U3ga&}+J*$DNe- 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 LrJugUA?W`A8`#=m From 448bfe6106efe062d651dd767820159fbee4aeba Mon Sep 17 00:00:00 2001 From: Dharshini D Date: Sat, 16 May 2026 21:39:09 +0530 Subject: [PATCH 062/262] test: signed commit (#597) --- test-signing.txt | Bin 0 -> 78 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test-signing.txt diff --git a/test-signing.txt b/test-signing.txt new file mode 100644 index 0000000000000000000000000000000000000000..da91f87062cbf5a0209af4fbe5d67fa93a1d08a8 GIT binary patch literal 78 zcmW-XK?;B{5X5p8{6oJc#g-mip)Ee&8f2LrGGx7zlffjBxw*XdQ<1vskSpO2JMrju NshG^6+cblnjStdZ5ElRd literal 0 HcmV?d00001 From 3b94e8d272666f67146ae414dc1208104aeb2f35 Mon Sep 17 00:00:00 2001 From: Prachi Maskar Date: Sat, 16 May 2026 21:39:13 +0530 Subject: [PATCH 063/262] feat: add zerithdb-scheduler package with leader election and task runner (#595) * feat: add zerithdb-scheduler package with leader election and task runner (closes #407) * chore: add changeset for zerithdb-scheduler * style: format and lint fix for zerithdb-scheduler * fix: add eslint config and fix unused vars in zerithdb-scheduler --- .changeset/clean-emus-worry.md | 5 + packages/zerithdb-scheduler/.eslintrc.cjs | 19 +++ packages/zerithdb-scheduler/package.json | 32 +++++ .../zerithdb-scheduler/src/LeaderElection.ts | 114 ++++++++++++++++++ packages/zerithdb-scheduler/src/TaskRunner.ts | 63 ++++++++++ packages/zerithdb-scheduler/src/index.ts | 71 +++++++++++ packages/zerithdb-scheduler/src/types.ts | 25 ++++ packages/zerithdb-scheduler/tsconfig.json | 8 ++ pnpm-lock.yaml | 28 +++++ 9 files changed, 365 insertions(+) create mode 100644 .changeset/clean-emus-worry.md create mode 100644 packages/zerithdb-scheduler/.eslintrc.cjs create mode 100644 packages/zerithdb-scheduler/package.json create mode 100644 packages/zerithdb-scheduler/src/LeaderElection.ts create mode 100644 packages/zerithdb-scheduler/src/TaskRunner.ts create mode 100644 packages/zerithdb-scheduler/src/index.ts create mode 100644 packages/zerithdb-scheduler/src/types.ts create mode 100644 packages/zerithdb-scheduler/tsconfig.json diff --git a/.changeset/clean-emus-worry.md b/.changeset/clean-emus-worry.md new file mode 100644 index 00000000..3b1eddd1 --- /dev/null +++ b/.changeset/clean-emus-worry.md @@ -0,0 +1,5 @@ +--- +"zerithdb-scheduler": patch +--- + +Add distributed scheduled tasks with leader election for P2P apps diff --git a/packages/zerithdb-scheduler/.eslintrc.cjs b/packages/zerithdb-scheduler/.eslintrc.cjs new file mode 100644 index 00000000..f1fcbba4 --- /dev/null +++ b/packages/zerithdb-scheduler/.eslintrc.cjs @@ -0,0 +1,19 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + ], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + env: { + browser: true, + node: true, + es2022: true, + }, + rules: { + // Document types intentionally use `any` in generic constraints (e.g. Record) + "@typescript-eslint/no-explicit-any": "off", + }, +}; diff --git a/packages/zerithdb-scheduler/package.json b/packages/zerithdb-scheduler/package.json new file mode 100644 index 00000000..02f3ca1e --- /dev/null +++ b/packages/zerithdb-scheduler/package.json @@ -0,0 +1,32 @@ +{ + "name": "zerithdb-scheduler", + "version": "0.1.0", + "description": "Distributed scheduled tasks with leader election for ZerithDB", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "zerithdb-core": "workspace:*", + "zerithdb-network": "workspace:*" + }, + "devDependencies": { + "zerithdb-eslint-config": "workspace:*", + "zerithdb-tsconfig": "workspace:*", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.57.0", + "typescript": "^5.5.4" + } +} \ No newline at end of file diff --git a/packages/zerithdb-scheduler/src/LeaderElection.ts b/packages/zerithdb-scheduler/src/LeaderElection.ts new file mode 100644 index 00000000..13eaa148 --- /dev/null +++ b/packages/zerithdb-scheduler/src/LeaderElection.ts @@ -0,0 +1,114 @@ +import type { PeerId } from "zerithdb-core"; +import { EventEmitter } from "zerithdb-core"; +import type { NetworkManager } from "zerithdb-network"; +import type { LeaderState, SchedulerEvents } from "./types.js"; + +const HEARTBEAT_INTERVAL = 5_000; +const LEADER_TIMEOUT = 15_000; + +export class LeaderElection extends EventEmitter { + private activePeers = new Set(); + private currentLeader: PeerId | null = null; + private heartbeatTimer: ReturnType | null = null; + private timeoutTimer: ReturnType | null = null; + private term = 0; + + constructor( + private readonly localPeerId: PeerId, + private readonly network: NetworkManager + ) { + super(); + this.activePeers.add(localPeerId); + + this.network.on("peer:connected", (info) => { + this.onPeerJoin(info.peerId); + }); + + this.network.on("peer:disconnected", (info) => { + this.onPeerLeave(info.peerId); + }); + + this.network.on("message", (msg) => { + if (msg.type === "leader:heartbeat") { + this.resetLeaderTimeout(); + } + }); + } + + start(): void { + this.electLeader(); + } + + get isLeader(): boolean { + return this.currentLeader === this.localPeerId; + } + + get leaderState(): LeaderState | null { + if (!this.currentLeader) return null; + return { + leaderId: this.currentLeader, + electedAt: Date.now(), + term: this.term, + }; + } + + dispose(): void { + if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); + if (this.timeoutTimer) clearTimeout(this.timeoutTimer); + } + + // ─── Private ──────────────────────────────────────────────────────────── + + private electLeader(): void { + const sorted = [...this.activePeers].sort(); + const newLeader = sorted.at(-1) as PeerId; + const previous = this.currentLeader; + + if (newLeader === this.currentLeader) return; + + this.currentLeader = newLeader; + this.term++; + + if (newLeader === this.localPeerId) { + this.startHeartbeat(); + const leaderId = newLeader; + EventEmitter.prototype.emit.call(this, "leader:elected", { leaderId }); + } else { + this.resetLeaderTimeout(); + if (previous === this.localPeerId) { + const previousLeader = previous; + EventEmitter.prototype.emit.call(this, "leader:lost", { previousLeader }); + } + } + } + + private startHeartbeat(): void { + if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); + this.heartbeatTimer = setInterval(() => { + this.network.broadcast({ + type: "leader:heartbeat", + payload: this.localPeerId, + }); + }, HEARTBEAT_INTERVAL); + } + + private resetLeaderTimeout(): void { + if (this.timeoutTimer) clearTimeout(this.timeoutTimer); + this.timeoutTimer = setTimeout(() => { + if (this.currentLeader) { + this.activePeers.delete(this.currentLeader); + } + this.electLeader(); + }, LEADER_TIMEOUT); + } + + private onPeerJoin(peerId: PeerId): void { + this.activePeers.add(peerId); + this.electLeader(); + } + + private onPeerLeave(peerId: PeerId): void { + this.activePeers.delete(peerId); + this.electLeader(); + } +} diff --git a/packages/zerithdb-scheduler/src/TaskRunner.ts b/packages/zerithdb-scheduler/src/TaskRunner.ts new file mode 100644 index 00000000..9c6e997d --- /dev/null +++ b/packages/zerithdb-scheduler/src/TaskRunner.ts @@ -0,0 +1,63 @@ +import type { PeerId } from "zerithdb-core"; +import type { ScheduledTask } from "./types.js"; + +export class TaskRunner { + private timer: ReturnType | null = null; + + constructor( + private readonly localPeerId: PeerId, + private readonly getTasks: () => ScheduledTask[], + private readonly onTaskComplete: (taskId: string) => void, + private readonly onTaskFailed: (taskId: string, error: Error) => void = (id, err) => { + console.error("Task failed:", id, err); + } + ) {} + + start(): void { + if (this.timer) return; + this.timer = setInterval(() => { + void this.tick(); + }, 60_000); + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + async tick(): Promise { + const now = Date.now(); + const tasks = this.getTasks().filter((t) => t.enabled); + for (const task of tasks) { + if (this.shouldRun(task, now)) { + await this.runTask(task); + } + } + } + + private shouldRun(task: ScheduledTask, now: number): boolean { + if (!task.lastRunAt) return true; + const interval = parseCronToMs(task.cronExpression); + return now - task.lastRunAt >= interval; + } + + private async runTask(task: ScheduledTask): Promise { + try { + this.onTaskComplete(task.id); + } catch (err) { + this.onTaskFailed(task.id, err instanceof Error ? err : new Error(String(err))); + } + } +} + +function parseCronToMs(cron: string): number { + const parts = cron.split(" "); + const minutePart = parts[0]; + if (minutePart?.startsWith("*/")) { + const n = parseInt(minutePart.slice(2), 10); + if (!isNaN(n)) return n * 60_000; + } + return 60_000; +} diff --git a/packages/zerithdb-scheduler/src/index.ts b/packages/zerithdb-scheduler/src/index.ts new file mode 100644 index 00000000..5ec447ec --- /dev/null +++ b/packages/zerithdb-scheduler/src/index.ts @@ -0,0 +1,71 @@ +import type { PeerId } from "zerithdb-core"; +import type { NetworkManager } from "zerithdb-network"; +import { LeaderElection } from "./LeaderElection.js"; +import { TaskRunner } from "./TaskRunner.js"; +import type { ScheduledTask } from "./types.js"; + +export { LeaderElection } from "./LeaderElection.js"; +export { TaskRunner } from "./TaskRunner.js"; +export type { ScheduledTask, LeaderState, SchedulerEvents } from "./types.js"; + +export class Scheduler { + private readonly election: LeaderElection; + private readonly runner: TaskRunner; + private tasks: Map = new Map(); + + constructor( + private readonly localPeerId: PeerId, + private readonly network: NetworkManager + ) { + this.election = new LeaderElection(localPeerId, network); + this.runner = new TaskRunner( + localPeerId, + () => [...this.tasks.values()], + (taskId) => this.markTaskComplete(taskId) + ); + } + + start(): void { + this.election.start(); + + // Leader bana toh runner shuru karo + this.election.on("leader:elected", () => { + if (this.election.isLeader) { + this.runner.start(); + } + }); + + // Leader gaya toh runner band karo + this.election.on("leader:lost", () => { + this.runner.stop(); + }); + } + + addTask(task: ScheduledTask): void { + this.tasks.set(task.id, task); + } + + removeTask(taskId: string): void { + this.tasks.delete(taskId); + } + + get isLeader(): boolean { + return this.election.isLeader; + } + + dispose(): void { + this.runner.stop(); + this.election.dispose(); + } + + private markTaskComplete(taskId: string): void { + const task = this.tasks.get(taskId); + if (task) { + this.tasks.set(taskId, { + ...task, + lastRunAt: Date.now(), + lastRunBy: this.localPeerId, + }); + } + } +} diff --git a/packages/zerithdb-scheduler/src/types.ts b/packages/zerithdb-scheduler/src/types.ts new file mode 100644 index 00000000..cf465da4 --- /dev/null +++ b/packages/zerithdb-scheduler/src/types.ts @@ -0,0 +1,25 @@ +import type { PeerId } from "zerithdb-core"; + +export interface ScheduledTask { + id: string; + name: string; + cronExpression: string; + taskType: string; + enabled: boolean; + lastRunAt?: number; + lastRunBy?: PeerId; + createdAt: number; +} + +export interface LeaderState { + leaderId: PeerId; + electedAt: number; + term: number; +} + +export type SchedulerEvents = { + "leader:elected": { leaderId: PeerId }; + "leader:lost": { previousLeader: PeerId }; + "task:executed": { taskId: string; executedBy: PeerId }; + "task:failed": { taskId: string; error: Error }; +}; diff --git a/packages/zerithdb-scheduler/tsconfig.json b/packages/zerithdb-scheduler/tsconfig.json new file mode 100644 index 00000000..af57bff8 --- /dev/null +++ b/packages/zerithdb-scheduler/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../packages/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4307b2d..95afb51a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -440,6 +440,34 @@ importers: specifier: ^2.0.5 version: 2.1.9(@types/node@22.19.18)(happy-dom@20.9.0)(lightningcss@1.32.0) + packages/zerithdb-scheduler: + dependencies: + zerithdb-core: + specifier: workspace:* + version: link:../core + zerithdb-network: + specifier: workspace:* + version: link:../network + devDependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^7.0.0 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^7.0.0 + version: 7.18.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.57.0 + version: 8.57.1 + typescript: + specifier: ^5.5.4 + version: 5.9.3 + zerithdb-eslint-config: + specifier: workspace:* + version: link:../eslint-config + zerithdb-tsconfig: + specifier: workspace:* + version: link:../tsconfig + packages: '@alloc/quick-lru@5.2.0': From e9b1c88ccd097717916fca97a7facc4baf0251b6 Mon Sep 17 00:00:00 2001 From: SPY-Github22 <117199135+SPY-Github22@users.noreply.github.com> Date: Sat, 16 May 2026 21:39:17 +0530 Subject: [PATCH 064/262] fix(sdk): validate appId in createApp() (#593) --- .changeset/validate-appid.md | 5 +++++ packages/sdk/src/create-app.ts | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/validate-appid.md diff --git a/.changeset/validate-appid.md b/.changeset/validate-appid.md new file mode 100644 index 00000000..19ddd2d1 --- /dev/null +++ b/.changeset/validate-appid.md @@ -0,0 +1,5 @@ +--- +"zerithdb-sdk": patch +--- + +Add validation for `appId` in `createApp()`. Throws `SDK_INVALID_CONFIG` error if `appId` is empty or missing. diff --git a/packages/sdk/src/create-app.ts b/packages/sdk/src/create-app.ts index a7f2f389..9c56e7a0 100644 --- a/packages/sdk/src/create-app.ts +++ b/packages/sdk/src/create-app.ts @@ -1,4 +1,5 @@ import type { ZerithDBConfig } from "zerithdb-core"; +import { ZerithDBError, ErrorCode } from "zerithdb-core"; import { DbClient, CollectionClient } from "./db-client.js"; import { SyncEngine } from "./sync-engine.js"; import { AuthManager } from "./auth-manager.js"; @@ -66,6 +67,12 @@ export interface ZerithDBApp { * ``` */ export function createApp(config: ZerithDBConfig): ZerithDBApp { + if (!config.appId || config.appId.trim().length === 0) { + throw new ZerithDBError( + ErrorCode.SDK_INVALID_CONFIG, + 'createApp requires a non-empty "appId" in config' + ); + } const resolvedConfig: ZerithDBConfig = { logLevel: "warn", ...config, From c6f2e031e5bdaa1e8ea3dabbb541e8df97efee8b Mon Sep 17 00:00:00 2001 From: priyankaganvatkar Date: Sat, 16 May 2026 21:39:22 +0530 Subject: [PATCH 065/262] chore: add Release Please for automated changelog generation (#588) * chore: add Release Please for automated changelog generation * fix: correct release-please workflow trigger --------- Co-authored-by: priyankaganvatkar --- .github/workflows/release-please.yml | 21 +++++++++++++++++++++ .release-please-manifest.json | 9 +++++++++ release-please-config.json | 0 3 files changed, 30 insertions(+) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000..17e14033 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,21 @@ +name: Release Please + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + name: Generate Release Notes + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..a51fdb02 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,9 @@ +{ + "packages/sdk": "0.0.1", + "packages/db": "0.0.1", + "packages/sync": "0.0.1", + "packages/network": "0.0.1", + "packages/auth": "0.0.1", + "packages/core": "0.0.1", + "packages/cli": "0.0.1" +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..e69de29b From 51f83f88c55a7ea2ee8f3562cc6ebd6c13585913 Mon Sep 17 00:00:00 2001 From: Bhavya jain Date: Sat, 16 May 2026 21:39:28 +0530 Subject: [PATCH 066/262] fix: resolve 404 error on blog footer link (#581) * feat: add blog placeholder page * chore: fix formatting --- CONTRIBUTING.md | 3 +- apps/web/src/app/blog/page.tsx | 168 ++++++++++++++++++++++++ packages/network/src/network-manager.ts | 3 +- packages/react/src/index.tsx | 13 +- turbo.json | 49 ++----- 5 files changed, 191 insertions(+), 45 deletions(-) create mode 100644 apps/web/src/app/blog/page.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfde8f60..e019eb19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,8 @@ pnpm --filter zerithdb-sdk dev [`good-first-issue`](https://github.com/Zerith-Labs/ZerithDB/issues?q=label%3Agood-first-issue) or [`help-wanted`](https://github.com/Zerith-Labs/ZerithDB/issues?q=label%3Ahelp-wanted). - For features, open a Discussion before writing code. -- Comment `/assign` to automatically claim an issue. Use `/unassign` if you can no longer work on it. +- Comment `/assign` to automatically claim an issue. Use `/unassign` if you can no longer work on + it. ### 2. Branch Naming diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx new file mode 100644 index 00000000..b95df820 --- /dev/null +++ b/apps/web/src/app/blog/page.tsx @@ -0,0 +1,168 @@ +"use client"; + +import Link from "next/link"; +import { Zap, BookOpen, ArrowLeft } from "lucide-react"; +import { motion } from "framer-motion"; + +export default function BlogPage() { + return ( + + ); +} diff --git a/packages/network/src/network-manager.ts b/packages/network/src/network-manager.ts index 0cd10427..feb3cdde 100644 --- a/packages/network/src/network-manager.ts +++ b/packages/network/src/network-manager.ts @@ -118,8 +118,7 @@ export class NetworkManager extends EventEmitter { try { await this.connectWebSocket(signalingUrl, roomId); } catch (wsError) { - const reason = - wsError instanceof Error ? wsError.message : "WebSocket connection failed"; + const reason = wsError instanceof Error ? wsError.message : "WebSocket connection failed"; this.emit("transport:downgrade", { from: "websocket", diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 16109c13..7900c04a 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -46,7 +46,10 @@ function useDeepCompareMemoize(value: T) { * @param collectionName The name of the collection to query * @param filter A MongoDB-style query filter. Must be JSON-serializable. */ -export function useQuery>(collectionName: string, filter: QueryFilter = {}) { +export function useQuery>( + collectionName: string, + filter: QueryFilter = {} +) { const app = useZerith(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); @@ -56,11 +59,11 @@ export function useQuery>(collectionName: string, useEffect(() => { const collection = app.db(collectionName); - + // Use Dexie's liveQuery to reactively observe local DB changes // (which also includes remote P2P updates applied by the sync engine) const observable = liveQuery(() => collection.find(memoizedFilter)); - + const subscription = observable.subscribe({ next: (docs) => { setData(docs as T[]); @@ -69,7 +72,7 @@ export function useQuery>(collectionName: string, error: (err) => { setError(err); setLoading(false); - } + }, }); return () => { @@ -109,7 +112,7 @@ export function useSync() { return { state, enable: () => app.sync.enable(), - disable: () => app.sync.disable() + disable: () => app.sync.disable(), }; } diff --git a/turbo.json b/turbo.json index 759db0e6..1c82ce58 100644 --- a/turbo.json +++ b/turbo.json @@ -4,57 +4,32 @@ "concurrency": "24", "tasks": { "build": { - "dependsOn": [ - "^build" - ], - "inputs": [ - "$TURBO_DEFAULT$", - ".env*" - ], - "outputs": [ - "dist/**", - ".next/**", - "!.next/cache/**" - ] + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, "dev": { "cache": false, "persistent": true }, "test": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "coverage/**" - ], - "env": [ - "CI" - ] + "dependsOn": ["^build"], + "outputs": ["coverage/**"], + "env": ["CI"] }, "test:e2e": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "playwright-report/**" - ], - "env": [ - "CI" - ] + "dependsOn": ["^build"], + "outputs": ["playwright-report/**"], + "env": ["CI"] }, "lint": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "typecheck": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "clean": { "cache": false } } -} \ No newline at end of file +} From 1c2a599e7e1ef023a98a1a413772f7837d40719e Mon Sep 17 00:00:00 2001 From: Prashan Date: Sat, 16 May 2026 21:39:37 +0530 Subject: [PATCH 067/262] feat(cli): add offline connectivity warning on startup (#568) * docs: add WebRTC signaling fallback mechanisms documentation (closes #50) * feat(cli): add offline connectivity warning on startup (closes #51) * style: fix prettier formatting --- docs/webrtc-signaling-fallback.md | 169 ++++++++++++++++++++++++++ packages/cli/src/checkConnectivity.ts | 22 ++++ packages/cli/src/cli.ts | 45 ++++--- 3 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 docs/webrtc-signaling-fallback.md create mode 100644 packages/cli/src/checkConnectivity.ts diff --git a/docs/webrtc-signaling-fallback.md b/docs/webrtc-signaling-fallback.md new file mode 100644 index 00000000..c7f7c7b5 --- /dev/null +++ b/docs/webrtc-signaling-fallback.md @@ -0,0 +1,169 @@ +# WebRTC Signaling & Fallback Mechanisms + +## Overview + +ZerithDB uses WebRTC to establish direct peer-to-peer connections between browsers. Before two peers +can communicate directly, they need to exchange connection metadata (ICE candidates and session +descriptions). This exchange is handled by a **signaling server** — a lightweight WebSocket relay +that brokers the initial handshake. + +> **Important:** The signaling server never sees your data. It only facilitates peer discovery. Once +> a WebRTC connection is established, all communication is direct and encrypted between peers. + +--- + +## What Happens During Normal Signaling + +``` +Peer A Signaling Server Peer B + │ │ │ + │── connect(appId) ────────▶│◀──── connect(appId) ────│ + │ │ │ + │── offer (SDP) ───────────▶│─── forward offer ───────▶│ + │ │ │ + │◀── answer (SDP) ──────────│◀── answer (SDP) ─────────│ + │ │ │ + │◀══════════ Direct WebRTC connection established ═════▶│ + │ │ (signaling no longer │ + │ │ needed for this pair) │ +``` + +1. Both peers connect to the signaling server using the same `appId`. +2. Peer A sends an **offer** (SDP — Session Description Protocol) to Peer B via the server. +3. Peer B responds with an **answer**. +4. Both peers exchange **ICE candidates** (possible network paths). +5. WebRTC selects the best path and establishes a direct connection. +6. The signaling server is no longer involved in that peer pair's communication. + +--- + +## What Happens When Signaling Fails + +Signaling can fail for several reasons: + +| Cause | Symptom | +| ---------------------------------- | ----------------------------------------- | +| Signaling server is down | WebSocket connection refused or timed out | +| Network firewall blocks WebSockets | Connection hangs, then times out | +| Server overloaded | Slow or dropped messages | +| Offline / no internet | Immediate connection failure | + +When signaling fails, ZerithDB cannot broker new peer connections. **Existing** peer connections +(already established via WebRTC) are unaffected and continue to work. + +--- + +## Fallback Mechanisms + +ZerithDB handles signaling failures through a layered fallback strategy: + +### 1. Automatic Reconnection + +If the WebSocket connection to the signaling server drops, ZerithDB automatically attempts to +reconnect using **exponential backoff**: + +``` +Attempt 1: wait 1s +Attempt 2: wait 2s +Attempt 3: wait 4s +Attempt 4: wait 8s +... (capped at 30s) +``` + +The SDK emits events you can listen to: + +```ts +app.sync.on("signaling:disconnected", () => { + console.warn("Signaling server unreachable. Retrying..."); +}); + +app.sync.on("signaling:reconnected", () => { + console.log("Signaling server reconnected."); +}); +``` + +### 2. Multiple Signaling Servers (Failover) + +You can provide an array of signaling URLs. ZerithDB will try each in order if the primary fails: + +```ts +const app = createApp({ + appId: "my-app", + sync: { + signalingUrl: [ + "wss://signal.zerithdb.dev", // primary (hosted) + "wss://signal-eu.zerithdb.dev", // secondary (regional) + "ws://localhost:4000", // fallback (local dev) + ], + }, +}); +``` + +ZerithDB cycles through the list until a connection succeeds. If all servers fail, it falls back to +offline mode and retries in the background. + +### 3. Offline / Local-Only Mode + +ZerithDB is **local-first**. If signaling is unavailable entirely, the app continues to function: + +- All reads and writes work against the local IndexedDB store. +- Changes are queued and sync automatically when connectivity is restored. +- Peers already connected via WebRTC continue syncing with each other. + +No special configuration is needed — this is the default behavior. + +### 4. Same-Tab / Same-Origin Sync (No Signaling Required) + +For peers on the same device or browser origin, ZerithDB uses the **BroadcastChannel API** as a +zero-latency fallback that bypasses the signaling server entirely: + +``` +Tab A ──BroadcastChannel──▶ Tab B (no signaling needed) +``` + +This means multi-tab sync always works, even fully offline. + +--- + +## Checking Signaling Status Programmatically + +```ts +const status = app.sync.signalingStatus(); +// Returns: 'connected' | 'connecting' | 'disconnected' | 'offline' + +if (status === "disconnected") { + // Show user a warning banner +} +``` + +--- + +## Self-Hosting a Signaling Server + +For production apps, we recommend self-hosting to avoid dependency on ZerithDB's hosted relay: + +```bash +# Start a local signaling server +npx zerithdb signal --port 4000 + +# Or with Docker +docker run -p 4000:4000 zerithdb/signal-server +``` + +Self-hosted servers give you full control over uptime and can be placed in multiple regions for +geographic redundancy. + +--- + +## Summary + +| Scenario | Behavior | +| -------------------------- | -------------------------------------------- | +| Signaling server drops | Auto-reconnect with exponential backoff | +| Primary server down | Failover to next URL in `signalingUrl` array | +| All servers unreachable | Offline mode — local reads/writes continue | +| Same browser / tab | BroadcastChannel — no signaling needed | +| Existing WebRTC connection | Unaffected by signaling failures | + +> For related topics, see [Architecture Overview](../ARCHITECTURE.md) and +> [Issue #183 — Multiple Signaling Servers](https://github.com/Zerith-Labs/ZerithDB/issues/183). diff --git a/packages/cli/src/checkConnectivity.ts b/packages/cli/src/checkConnectivity.ts new file mode 100644 index 00000000..b080195d --- /dev/null +++ b/packages/cli/src/checkConnectivity.ts @@ -0,0 +1,22 @@ +import dns from "dns/promises"; +import chalk from "chalk"; + +export async function checkConnectivity(): Promise { + try { + await dns.lookup("registry.npmjs.org"); + } catch { + console.warn( + chalk.yellow(` +⚠️ Warning: No internet connection detected. + + Some ZerithDB CLI features require network access: + - Dependency installation (npm install) + - Fetching latest templates + - Connecting to signaling servers + + You can still scaffold apps offline, but run + ${chalk.cyan("npm install")} once you're back online. +`) + ); + } +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 4bfd927e..baa730e2 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -3,6 +3,7 @@ import { program } from "commander"; import chalk from "chalk"; import { initCommand } from "./commands/init.js"; import { signalCommand } from "./commands/signal.js"; +import { checkConnectivity } from "./checkConnectivity.js"; const VERSION = "0.1.0"; @@ -18,22 +19,28 @@ console.log( ); console.log(chalk.gray(` Build full-stack apps with ZERO backend. v${VERSION}\n`)); -program - .name("zerithdb") - .description("ZerithDB CLI — scaffold and manage local-first P2P apps") - .version(VERSION); - -program - .command("init [app-name]") - .description("Scaffold a new ZerithDB application") - .option("-t, --template
+ {/* ── HEADER ── */} +
+ +
+ + {/* ── COMING SOON SECTION ── */} +
+ {/* Background Decorations */} +
+ + +
+ + +
+ +
+

+ Blog{" "} + + Coming Soon + +

+

+ We're preparing deep dives into CRDTs, peer-to-peer web architecture, and tutorials + on building offline-first applications. Stay tuned! +

+
+ + + Back to Home + +
+
+
+ + {/* ── FOOTER ── */} + +