From 2cd5ea3b7b9085d12ec409d2e25d62646c9ce386 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Fri, 24 Apr 2026 17:44:23 -0400 Subject: [PATCH 1/2] add contrail-e2e app with end-to-end tests against a local ATProto stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New app under apps/contrail-e2e that brings up a full sealed local ATProto network (PDS, PLC, Jetstream, TAP, Postgres, maildev) via docker-compose and runs three end-to-end test files against it: - health.test.ts — service health checks - ingest-roundtrip.test.ts — publish → index roundtrip, verifying a calendar event and an RSVP land in the DB and that the grouped rsvpsGoingCount is populated via an in-process XRPC handler - cursor-resume.test.ts — regression for runPersistent's durable cursor. Publishes A, stops the ingester, publishes B + updates B + deletes A, restarts, and asserts the saved cursor replays the gap. The basic roundtrip can't catch a broken cursor because events always arrive while the ingester is live. Each test stands up its own runPersistent and XRPC handler in-process against an isolated pg schema (search_path pinned per test), so the suite works with just pnpm stack:up and doesn't fight a dogfooding ingester writing to public. --- apps/contrail-e2e/.env.example | 29 +++ apps/contrail-e2e/.gitignore | 2 + apps/contrail-e2e/README.md | 90 ++++++++ apps/contrail-e2e/config.ts | 51 +++++ apps/contrail-e2e/docker-compose.yml | 55 +++++ apps/contrail-e2e/init-db/01-databases.sql | 5 + apps/contrail-e2e/package.json | 23 ++ apps/contrail-e2e/scripts/down.sh | 9 + apps/contrail-e2e/scripts/up.sh | 39 ++++ apps/contrail-e2e/tests/cursor-resume.test.ts | 192 +++++++++++++++++ apps/contrail-e2e/tests/health.test.ts | 58 +++++ apps/contrail-e2e/tests/helpers.ts | 107 ++++++++++ .../tests/ingest-roundtrip.test.ts | 150 +++++++++++++ apps/contrail-e2e/tsconfig.json | 13 ++ apps/contrail-e2e/vitest.config.ts | 11 + pnpm-lock.yaml | 198 ++++++++++++------ 16 files changed, 966 insertions(+), 66 deletions(-) create mode 100644 apps/contrail-e2e/.env.example create mode 100644 apps/contrail-e2e/.gitignore create mode 100644 apps/contrail-e2e/README.md create mode 100644 apps/contrail-e2e/config.ts create mode 100644 apps/contrail-e2e/docker-compose.yml create mode 100644 apps/contrail-e2e/init-db/01-databases.sql create mode 100644 apps/contrail-e2e/package.json create mode 100755 apps/contrail-e2e/scripts/down.sh create mode 100755 apps/contrail-e2e/scripts/up.sh create mode 100644 apps/contrail-e2e/tests/cursor-resume.test.ts create mode 100644 apps/contrail-e2e/tests/health.test.ts create mode 100644 apps/contrail-e2e/tests/helpers.ts create mode 100644 apps/contrail-e2e/tests/ingest-roundtrip.test.ts create mode 100644 apps/contrail-e2e/tsconfig.json create mode 100644 apps/contrail-e2e/vitest.config.ts diff --git a/apps/contrail-e2e/.env.example b/apps/contrail-e2e/.env.example new file mode 100644 index 0000000..03bf68a --- /dev/null +++ b/apps/contrail-e2e/.env.example @@ -0,0 +1,29 @@ +# Ports (override to avoid conflicts with other local services) +DEVNET_PDS_PORT=4000 +DEVNET_PLC_PORT=2582 +DEVNET_JETSTREAM_PORT=6008 +DEVNET_JETSTREAM_METRICS_PORT=6009 +DEVNET_TAP_PORT=2480 +PG_PORT=5433 +MAILDEV_WEB_PORT=1080 +MAILDEV_SMTP_PORT=1025 + +# Devnet database (PLC server) — uses the shared postgres service +DEVNET_DB_HOST=postgres +DEVNET_DB_PORT=5432 +DEVNET_DB_USER=postgres +DEVNET_DB_PASSWORD=postgres +DEVNET_DB_NAME=plc + +# Devnet SMTP (PDS email verification) — uses the shared maildev service +DEVNET_SMTP_URL=smtp://maildev:1025 + +# Devnet handle config +DEVNET_PDS_HOSTNAME=devnet.test +DEVNET_PDS_ADMIN_PASSWORD=devnet-admin-password +DEVNET_HANDLE_DOMAIN=.devnet.test +DEVNET_SEED_ACCOUNTS=true + +# Contrail runtime (consumed by the e2e suite) +DATABASE_URL=postgresql://postgres:postgres@localhost:5433/contrail +JETSTREAM_URL=ws://localhost:6008/subscribe diff --git a/apps/contrail-e2e/.gitignore b/apps/contrail-e2e/.gitignore new file mode 100644 index 0000000..6ed48a9 --- /dev/null +++ b/apps/contrail-e2e/.gitignore @@ -0,0 +1,2 @@ +.env +node_modules diff --git a/apps/contrail-e2e/README.md b/apps/contrail-e2e/README.md new file mode 100644 index 0000000..ac0a789 --- /dev/null +++ b/apps/contrail-e2e/README.md @@ -0,0 +1,90 @@ +# contrail-e2e — end-to-end tests against a local ATProto stack + +End-to-end test suite for Contrail against a sealed local ATProto stack. +Nothing touches the public ATProto — writes go to a local PDS, the local +Jetstream reads that PDS's firehose, and each test stands up its own +in-process Contrail ingester + XRPC handler to consume it. + +## What's inside + +| Service | Port | Purpose | +|---------|------|---------| +| PDS | 4000 | Personal Data Server (user repos) | +| PLC | 2582 | DID registry | +| Jetstream | 6008 | Firehose relay in JSON | +| TAP | 2480 | Sync/backfill relay | +| PostgreSQL | 5433 | Shared by PLC + Contrail | +| Maildev | 1080 | PDS email verification catcher | + +## Prerequisites + +Clone `atproto-devnet` as a sibling of this contrail repo: + +``` +some-workspace/ +├── contrail/ (this repo) +└── atproto-devnet/ +``` + +```bash +git clone https://github.com/OpenMeet-Team/atproto-devnet.git /path/to/atproto-devnet +``` + +## Running the tests + +```bash +# From the monorepo root (one-time build of @atmo-dev/contrail) +pnpm install && pnpm build + +# Bring the devnet stack up (creates .env from .env.example on first run) +cd apps/contrail-e2e +pnpm stack:up + +# Run the suite +pnpm test:e2e # once +pnpm test:e2e:watch # re-run on change +``` + +> **Note:** `up` and `down` are reserved by pnpm (aliases for `update`), so +> the stack scripts are prefixed `stack:` to disambiguate. The test scripts +> are named `test:e2e` so the monorepo-wide `pnpm test` skips them in CI +> (they need the live docker stack). + +## Tests + +- `tests/health.test.ts` — service health checks (PLC, PDS, TAP, Jetstream) +- `tests/ingest-roundtrip.test.ts` — publish → index roundtrip. Creates a + fresh PDS account, publishes a calendar event and an RSVP, and verifies + both the record and its `rsvpsGoingCount` via an in-process XRPC handler. +- `tests/cursor-resume.test.ts` — regression for `runPersistent`'s durable + cursor. Starts the ingester, publishes A, stops the ingester, publishes + B + updates B + deletes A, restarts, and verifies the saved cursor + replays the gap. + +Each test spins up its own `runPersistent` in-process against an isolated +postgres schema, so tests don't interfere with each other or with any +dogfooding ingester running in another terminal. + +## Teardown + +```bash +pnpm stack:down # stops containers AND wipes volumes for a clean slate +``` + +## Namespace + +These tests use `rsvp.atmo` as the Contrail namespace — same as +[atmo-events](https://github.com/flo-bit/atmo-events), so the fixtures are +compatible with that frontend. If you're running your own namespace, edit +`config.ts`. + +## Why not use `apps/postgres/`? + +`apps/postgres/` connects Contrail to the public Bluesky Jetstream — useful +for indexing real events, unusable for writing test fixtures. This suite +gives a sealed environment where the tests control every record. + +## Overrides + +Copy `.env.example` to `.env` and edit. All ports, database credentials, and +devnet hostnames are overridable. diff --git a/apps/contrail-e2e/config.ts b/apps/contrail-e2e/config.ts new file mode 100644 index 0000000..0c94c29 --- /dev/null +++ b/apps/contrail-e2e/config.ts @@ -0,0 +1,51 @@ +import type { ContrailConfig } from "@atmo-dev/contrail"; + +// Namespace matches atmo.rsvp so the same atmo-events frontend can point at +// this devnet-backed Contrail without patching its hardcoded namespace. +// See https://github.com/flo-bit/atmo-events/issues/26 for making this +// configurable upstream. +export const config: ContrailConfig = { + namespace: "rsvp.atmo", + jetstreams: [ + process.env.JETSTREAM_URL ?? "ws://localhost:6008/subscribe", + ], + collections: { + event: { + collection: "community.lexicon.calendar.event", + queryable: { + mode: {}, + name: {}, + status: {}, + startsAt: { type: "range" }, + endsAt: { type: "range" }, + createdAt: { type: "range" }, + }, + searchable: ["name", "description"], + relations: { + rsvps: { + collection: "rsvp", + groupBy: "status", + count: true, + groups: { + interested: "community.lexicon.calendar.rsvp#interested", + going: "community.lexicon.calendar.rsvp#going", + notgoing: "community.lexicon.calendar.rsvp#notgoing", + }, + }, + }, + }, + rsvp: { + collection: "community.lexicon.calendar.rsvp", + queryable: { + status: {}, + "subject.uri": {}, + }, + references: { + event: { + collection: "event", + field: "subject.uri", + }, + }, + }, + }, +}; diff --git a/apps/contrail-e2e/docker-compose.yml b/apps/contrail-e2e/docker-compose.yml new file mode 100644 index 0000000..78d1668 --- /dev/null +++ b/apps/contrail-e2e/docker-compose.yml @@ -0,0 +1,55 @@ +# contrail-e2e: Contrail + atproto-devnet for local testing. +# +# Brings up a full local ATProto network (PDS, PLC, Jetstream, TAP) alongside +# the PostgreSQL instance Contrail indexes into. No traffic leaves your laptop. +# +# Prerequisites: +# - atproto-devnet cloned as a sibling of this contrail repo: +# some-workspace/ +# ├── contrail/ (this repo) +# └── atproto-devnet/ +# - Copy .env.example to .env first (scripts/up.sh does this automatically) +# +# Usage: +# pnpm up # bring everything up, wait for health +# pnpm down # stop + wipe volumes (clean slate) +# pnpm smoke # verify services are healthy + +include: + - path: ../../../atproto-devnet/docker-compose.yml + +services: + # PostgreSQL — shared by PLC (devnet) and Contrail (this example). + # NOTE: included services like PLC can't be modified via `include:` merge, + # so scripts/up.sh brings postgres up first, then the rest. + postgres: + image: postgres:17 + restart: unless-stopped + environment: + POSTGRES_USER: ${DEVNET_DB_USER:-postgres} + POSTGRES_PASSWORD: ${DEVNET_DB_PASSWORD:-postgres} + ports: + - "${PG_PORT:-5433}:5432" + volumes: + - pgdata:/var/lib/postgresql/data + - ./init-db:/docker-entrypoint-initdb.d:ro + networks: + - atproto-devnet + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DEVNET_DB_USER:-postgres}"] + interval: 3s + timeout: 3s + retries: 20 + + # Maildev — catches PDS email verification messages + maildev: + image: maildev/maildev:latest + restart: unless-stopped + ports: + - "${MAILDEV_WEB_PORT:-1080}:1080" + - "${MAILDEV_SMTP_PORT:-1025}:1025" + networks: + - atproto-devnet + +volumes: + pgdata: diff --git a/apps/contrail-e2e/init-db/01-databases.sql b/apps/contrail-e2e/init-db/01-databases.sql new file mode 100644 index 0000000..ae5a095 --- /dev/null +++ b/apps/contrail-e2e/init-db/01-databases.sql @@ -0,0 +1,5 @@ +-- Create both databases used by this example: +-- plc — DID registry state for the devnet PLC server +-- contrail — Contrail's own indexed records +CREATE DATABASE plc; +CREATE DATABASE contrail; diff --git a/apps/contrail-e2e/package.json b/apps/contrail-e2e/package.json new file mode 100644 index 0000000..2e42c0b --- /dev/null +++ b/apps/contrail-e2e/package.json @@ -0,0 +1,23 @@ +{ + "name": "contrail-e2e", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "stack:up": "./scripts/up.sh", + "stack:down": "./scripts/down.sh", + "test:e2e": "vitest run", + "test:e2e:watch": "vitest" + }, + "dependencies": { + "@atmo-dev/contrail": "workspace:*", + "pg": "^8.20.0" + }, + "devDependencies": { + "@atcute/atproto": "^3.1.10", + "@atcute/client": "^4.2.1", + "@types/pg": "^8.20.0", + "typescript": "^5.9.3", + "vitest": "^4.1.0" + } +} diff --git a/apps/contrail-e2e/scripts/down.sh b/apps/contrail-e2e/scripts/down.sh new file mode 100755 index 0000000..ae74736 --- /dev/null +++ b/apps/contrail-e2e/scripts/down.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +echo "→ Tearing down (removes volumes — clean slate)..." +docker compose down -v + +echo "✓ Down. All state wiped." diff --git a/apps/contrail-e2e/scripts/up.sh b/apps/contrail-e2e/scripts/up.sh new file mode 100755 index 0000000..cf5341a --- /dev/null +++ b/apps/contrail-e2e/scripts/up.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +if [ ! -f .env ]; then + echo "→ First run: copying .env.example to .env" + cp .env.example .env +fi + +DEVNET_COMPOSE="../../../atproto-devnet/docker-compose.yml" +if [ ! -f "$DEVNET_COMPOSE" ]; then + echo "Error: atproto-devnet not found at $DEVNET_COMPOSE" + echo "" + echo "Clone it as a sibling of the contrail repo:" + echo " git clone https://github.com/OpenMeet-Team/atproto-devnet.git ../../../../atproto-devnet" + exit 1 +fi + +echo "→ Bringing up postgres (PLC depends on it)..." +docker compose up -d --wait postgres maildev + +echo "→ Bringing up devnet (PLC, PDS, Jetstream, TAP, init)..." +docker compose up -d --wait + +# shellcheck disable=SC1091 +source .env + +echo "" +echo "Services:" +echo " PDS: http://localhost:${DEVNET_PDS_PORT:-4000}" +echo " PLC: http://localhost:${DEVNET_PLC_PORT:-2582}" +echo " Jetstream: ws://localhost:${DEVNET_JETSTREAM_PORT:-6008}" +echo " TAP: http://localhost:${DEVNET_TAP_PORT:-2480}" +echo " Postgres: localhost:${PG_PORT:-5433}" +echo " MailDev UI: http://localhost:${MAILDEV_WEB_PORT:-1080}" +echo "" +echo "Next:" +echo " pnpm test # run the e2e suite (ingester + XRPC handler run in-process)" diff --git a/apps/contrail-e2e/tests/cursor-resume.test.ts b/apps/contrail-e2e/tests/cursor-resume.test.ts new file mode 100644 index 0000000..1b01066 --- /dev/null +++ b/apps/contrail-e2e/tests/cursor-resume.test.ts @@ -0,0 +1,192 @@ +/** + * End-to-end: the ingester must resume from its persisted cursor after a + * restart, capturing events that arrived on the firehose while it was down. + * + * Flow: + * 1. Start runPersistent in-process against an isolated pg schema. + * 2. Publish event A → wait for it to be indexed → abort the ingester and + * await the promise. The finally-block's final flush must persist the + * cursor or this test fails at step 5. + * 3. While the ingester is DOWN: publish event B, update B, delete A. + * 4. Restart runPersistent against the SAME schema (same cursor row). + * 5. Assert: A is gone, B is indexed with its updated name. Those three + * commits can only be seen by replaying from the saved cursor — Jetstream + * is "live tail" without it, and the events already happened. + * + * Why this matters: runPersistent's whole reason to exist is durable state. + * A silently broken cursor would pass the basic roundtrip test because there's + * nothing to replay — every event arrives while the ingester is live. This + * test is the only one that exercises the replay path. + * + * Requires `pnpm stack:up` only. Ingester runs in-process. + */ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import pg from "pg"; +import { CredentialManager, Client } from "@atcute/client"; +import "@atcute/atproto"; +import { Contrail } from "@atmo-dev/contrail"; +import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; +import { config as baseConfig } from "../config"; +import { + createTestAccount, + createIsolatedSchema, + waitFor, + PDS_URL, + type TestAccount, +} from "./helpers"; + +const EVENT_NSID = "community.lexicon.calendar.event"; + +describe("cursor resume (ingester stops, events pile up, ingester restarts)", () => { + let account: TestAccount; + let client: Client; + let pool: pg.Pool; + let cleanupSchema: () => Promise; + + beforeAll(async () => { + account = await createTestAccount(); + const creds = new CredentialManager({ service: PDS_URL }); + await creds.login({ identifier: account.handle, password: account.password }); + client = new Client({ handler: creds }); + + const iso = await createIsolatedSchema("test_cursor"); + pool = iso.pool; + cleanupSchema = iso.cleanup; + }); + + afterAll(async () => { + await cleanupSchema?.(); + }); + + async function queryRecord(uri: string): Promise<{ record: any } | undefined> { + const res = await pool.query(`SELECT uri, record FROM records_event WHERE uri = $1`, [uri]); + if (res.rows.length === 0) return undefined; + const row = res.rows[0]; + return { record: typeof row.record === "string" ? JSON.parse(row.record) : row.record }; + } + + async function getCursor(): Promise { + const res = await pool.query(`SELECT time_us FROM cursor WHERE id = 1`); + if (res.rows.length === 0) return null; + return Number(res.rows[0].time_us); + } + + it("resumes from saved cursor and picks up commits issued while down", async () => { + const db = createPostgresDatabase(pool); + // Go through the Contrail wrapper so the config is resolved; raw + // runPersistent expects a resolved config. + const contrail = new Contrail({ ...baseConfig, db }); + await contrail.init(); + const runOpts = { batchSize: 50, flushIntervalMs: 500 }; + + // ---- Phase 1: start ingester, publish A, verify indexed ---- + const c1 = new AbortController(); + const ingest1 = contrail.runPersistent({ ...runOpts, signal: c1.signal }); + + const nameA = `A-${Date.now()}`; + const aRes = await client.post("com.atproto.repo.createRecord", { + input: { + repo: account.did, + collection: EVENT_NSID, + record: { + $type: EVENT_NSID, + name: nameA, + createdAt: new Date().toISOString(), + startsAt: new Date(Date.now() + 60 * 60_000).toISOString(), + mode: `${EVENT_NSID}#inperson`, + status: `${EVENT_NSID}#scheduled`, + }, + }, + }); + expect(aRes.ok).toBe(true); + if (!aRes.ok) throw new Error("unreachable"); + const uriA = aRes.data.uri; + + await waitFor(() => queryRecord(uriA), { label: `A indexed (${uriA})` }); + const cursorAfterA = await getCursor(); + expect(cursorAfterA, "cursor must be persisted after first flush").not.toBeNull(); + + // ---- Phase 2: stop ingester ---- + c1.abort(); + await ingest1; + + const cursorAfterStop = await getCursor(); + expect(cursorAfterStop).toBe(cursorAfterA); + + // ---- Phase 3: mutate while ingester is DOWN ---- + const nameB = `B-${Date.now()}`; + const bRes = await client.post("com.atproto.repo.createRecord", { + input: { + repo: account.did, + collection: EVENT_NSID, + record: { + $type: EVENT_NSID, + name: nameB, + createdAt: new Date().toISOString(), + startsAt: new Date(Date.now() + 2 * 60 * 60_000).toISOString(), + mode: `${EVENT_NSID}#inperson`, + status: `${EVENT_NSID}#scheduled`, + }, + }, + }); + expect(bRes.ok).toBe(true); + if (!bRes.ok) throw new Error("unreachable"); + const uriB = bRes.data.uri; + const rkeyB = uriB.split("/").pop()!; + const rkeyA = uriA.split("/").pop()!; + + const nameBUpdated = `${nameB}-renamed`; + const putRes = await client.post("com.atproto.repo.putRecord", { + input: { + repo: account.did, + collection: EVENT_NSID, + rkey: rkeyB, + record: { + $type: EVENT_NSID, + name: nameBUpdated, + createdAt: new Date().toISOString(), + startsAt: new Date(Date.now() + 2 * 60 * 60_000).toISOString(), + mode: `${EVENT_NSID}#inperson`, + status: `${EVENT_NSID}#scheduled`, + }, + }, + }); + expect(putRes.ok).toBe(true); + + const delRes = await client.post("com.atproto.repo.deleteRecord", { + input: { repo: account.did, collection: EVENT_NSID, rkey: rkeyA }, + }); + expect(delRes.ok).toBe(true); + + // Confirm the DB didn't move while the ingester was down. + expect(await queryRecord(uriA), "A must still be indexed — nobody is consuming").toBeDefined(); + expect(await queryRecord(uriB), "B should not be indexed yet — ingester is down").toBeUndefined(); + + // ---- Phase 4: restart ingester, assert replay ---- + const c2 = new AbortController(); + const ingest2 = contrail.runPersistent({ ...runOpts, signal: c2.signal }); + + try { + const indexedB = await waitFor( + async () => { + const r = await queryRecord(uriB); + return r && r.record.name === nameBUpdated ? r : undefined; + }, + { label: `B replayed with updated name` }, + ); + expect(indexedB.record.name).toBe(nameBUpdated); + + await waitFor( + async () => ((await queryRecord(uriA)) === undefined ? true : undefined), + { label: `A deletion replayed` }, + ); + + const cursorAfterReplay = await getCursor(); + expect(cursorAfterReplay).not.toBeNull(); + expect(cursorAfterReplay!).toBeGreaterThan(cursorAfterStop!); + } finally { + c2.abort(); + await ingest2; + } + }, 60_000); +}); diff --git a/apps/contrail-e2e/tests/health.test.ts b/apps/contrail-e2e/tests/health.test.ts new file mode 100644 index 0000000..c980e27 --- /dev/null +++ b/apps/contrail-e2e/tests/health.test.ts @@ -0,0 +1,58 @@ +/** + * Health checks for the running devnet stack. + * + * Assumes `pnpm stack:up` has completed. Reads ports from the environment + * with fallbacks matching `.env.example` so tests work out of the box. + * + * The Contrail handler is covered by ingest-roundtrip.test.ts in-process — + * this file only verifies the external devnet services are reachable. + */ +import { describe, it, expect } from "vitest"; +import net from "node:net"; + +const PDS_PORT = Number(process.env.DEVNET_PDS_PORT ?? 4000); +const PLC_PORT = Number(process.env.DEVNET_PLC_PORT ?? 2582); +const TAP_PORT = Number(process.env.DEVNET_TAP_PORT ?? 2480); +const JS_PORT = Number(process.env.DEVNET_JETSTREAM_PORT ?? 6008); + +async function get(url: string): Promise { + return fetch(url); +} + +function tcpConnect(port: number, host = "127.0.0.1"): Promise { + return new Promise((resolve, reject) => { + const socket = new net.Socket(); + socket.setTimeout(3000); + socket.once("connect", () => { + socket.destroy(); + resolve(); + }); + socket.once("timeout", () => { + socket.destroy(); + reject(new Error(`TCP connect to ${host}:${port} timed out`)); + }); + socket.once("error", reject); + socket.connect(port, host); + }); +} + +describe("devnet services", () => { + it("PLC responds to /_health", async () => { + const res = await get(`http://localhost:${PLC_PORT}/_health`); + expect(res.ok).toBe(true); + }); + + it("PDS responds to /xrpc/_health", async () => { + const res = await get(`http://localhost:${PDS_PORT}/xrpc/_health`); + expect(res.ok).toBe(true); + }); + + it("TAP responds to /health", async () => { + const res = await get(`http://localhost:${TAP_PORT}/health`); + expect(res.ok).toBe(true); + }); + + it("Jetstream accepts TCP connections", async () => { + await expect(tcpConnect(JS_PORT)).resolves.toBeUndefined(); + }); +}); diff --git a/apps/contrail-e2e/tests/helpers.ts b/apps/contrail-e2e/tests/helpers.ts new file mode 100644 index 0000000..f3965ef --- /dev/null +++ b/apps/contrail-e2e/tests/helpers.ts @@ -0,0 +1,107 @@ +/** + * Shared test infrastructure for the contrail-e2e suite. + * + * The ingester and XRPC handler run in-process against an isolated pg schema + * per test — no external processes required, and no collision with a + * dogfooding ingester the developer might have running in another terminal. + */ +import pg from "pg"; + +export type Did = `did:${string}:${string}`; + +export const PDS_PORT = Number(process.env.DEVNET_PDS_PORT ?? 4000); +export const PDS_URL = `http://localhost:${PDS_PORT}`; +export const HANDLE_DOMAIN = process.env.DEVNET_HANDLE_DOMAIN ?? ".devnet.test"; +export const PDS_ADMIN_PASSWORD = process.env.DEVNET_PDS_ADMIN_PASSWORD ?? "devnet-admin-password"; +export const DATABASE_URL = + process.env.DATABASE_URL ?? "postgresql://postgres:postgres@localhost:5433/contrail"; + +export type TestAccount = { handle: string; password: string; did: Did }; + +export async function createTestAccount(): Promise { + const inviteRes = await fetch(`${PDS_URL}/xrpc/com.atproto.server.createInviteCode`, { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Basic ${Buffer.from(`admin:${PDS_ADMIN_PASSWORD}`).toString("base64")}`, + }, + body: JSON.stringify({ useCount: 1 }), + }); + if (!inviteRes.ok) { + throw new Error(`createInviteCode → ${inviteRes.status}: ${await inviteRes.text()}`); + } + const { code: inviteCode } = (await inviteRes.json()) as { code: string }; + + const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; + const handle = `test-${suffix}${HANDLE_DOMAIN}`; + const password = `pw-${suffix}`; + + const accountRes = await fetch(`${PDS_URL}/xrpc/com.atproto.server.createAccount`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ handle, email: `${suffix}@devnet.test`, password, inviteCode }), + }); + if (!accountRes.ok) { + throw new Error(`createAccount → ${accountRes.status}: ${await accountRes.text()}`); + } + const { did } = (await accountRes.json()) as { did: string }; + return { handle, password, did: did as Did }; +} + +/** + * Create a fresh pg schema and return a Pool pinned to it via search_path. + * Call the returned `cleanup` in afterAll to drop the schema. + */ +export async function createIsolatedSchema( + prefix = "test", +): Promise<{ pool: pg.Pool; schemaName: string; cleanup: () => Promise }> { + const schemaName = `${prefix}_${Date.now()}${Math.floor(Math.random() * 1000)}`; + const bootstrap = new pg.Pool({ connectionString: DATABASE_URL }); + await bootstrap.query(`CREATE SCHEMA "${schemaName}"`); + await bootstrap.end(); + + const pool = new pg.Pool({ + connectionString: DATABASE_URL, + options: `-c search_path="${schemaName}"`, + }); + + const cleanup = async () => { + await pool.end(); + const c = new pg.Pool({ connectionString: DATABASE_URL }); + await c.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`); + await c.end(); + }; + + return { pool, schemaName, cleanup }; +} + +/** + * Poll `fn` until it returns a defined value, or time out. + * + * Thrown errors count as "not yet" (last is included in the timeout message) + * so transient 404s during startup don't abort. Use `label` to say what was + * missing. Defaults (15s / 250ms) cover Contrail's 500ms test-mode flush plus + * firehose propagation with plenty of headroom. + */ +export async function waitFor( + fn: () => Promise, + { timeoutMs = 15_000, intervalMs = 250, label }: { timeoutMs?: number; intervalMs?: number; label: string }, +): Promise { + const deadline = Date.now() + timeoutMs; + let lastErr: unknown; + let attempts = 0; + while (Date.now() < deadline) { + attempts++; + try { + const v = await fn(); + if (v !== undefined) return v; + } catch (err) { + lastErr = err; + } + await new Promise((r) => setTimeout(r, intervalMs)); + } + throw new Error( + `waitFor(${label}) timed out after ${timeoutMs}ms (${attempts} attempts)` + + (lastErr ? `: ${(lastErr as Error).message}` : ""), + ); +} diff --git a/apps/contrail-e2e/tests/ingest-roundtrip.test.ts b/apps/contrail-e2e/tests/ingest-roundtrip.test.ts new file mode 100644 index 0000000..b597245 --- /dev/null +++ b/apps/contrail-e2e/tests/ingest-roundtrip.test.ts @@ -0,0 +1,150 @@ +/** + * End-to-end: publish a record via the devnet PDS, verify Contrail indexes it + * via Jetstream. + * + * Flow: + * 1. Stand up an in-process Contrail against an isolated pg schema — no + * collision with a dogfooding `pnpm ingest` and no external processes + * required. + * 2. Create a fresh PDS account + auth (unique handle per run). + * 3. Publish records through `com.atproto.repo.createRecord` on the PDS. + * 4. Poll the in-process XRPC handler's `rsvp.atmo.event.getRecord` until + * the record appears. Flush cadence is 500ms in tests, so publish → + * indexed is bounded by firehose propagation plus one flush. + * 5. Assert indexed fields match what we published. + * + * Requires `pnpm stack:up` only. The ingester and XRPC handler run in-process. + */ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import pg from "pg"; +import { CredentialManager, Client } from "@atcute/client"; +import "@atcute/atproto"; +import { Contrail } from "@atmo-dev/contrail"; +import { createHandler } from "@atmo-dev/contrail/server"; +import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; +import { config as baseConfig } from "../config"; +import { + createTestAccount, + createIsolatedSchema, + waitFor, + PDS_URL, + type TestAccount, +} from "./helpers"; + +const EVENT_NSID = "community.lexicon.calendar.event"; +const RSVP_NSID = "community.lexicon.calendar.rsvp"; + +describe("ingest roundtrip (devnet PDS → Jetstream → Contrail)", () => { + let account: TestAccount; + let client: Client; + let pool: pg.Pool; + let cleanupSchema: () => Promise; + let handle: (req: Request) => Promise; + let ingestController: AbortController; + let ingestPromise: Promise; + + let eventUri: string; + let eventCid: string; + const eventName = `devnet roundtrip ${Date.now()}`; + const startsAt = new Date(Date.now() + 60 * 60_000).toISOString(); + + beforeAll(async () => { + // PDS auth + account = await createTestAccount(); + const creds = new CredentialManager({ service: PDS_URL }); + await creds.login({ identifier: account.handle, password: account.password }); + client = new Client({ handler: creds }); + + // Isolated schema + in-process Contrail + const iso = await createIsolatedSchema("test_roundtrip"); + pool = iso.pool; + cleanupSchema = iso.cleanup; + const db = createPostgresDatabase(pool); + const contrail = new Contrail({ ...baseConfig, db }); + await contrail.init(); + handle = createHandler(contrail); + + // In-process ingester via Contrail so the config is resolved (raw + // runPersistent expects a resolved config — grouped counts silently + // don't update otherwise). + ingestController = new AbortController(); + ingestPromise = contrail.runPersistent({ + batchSize: 50, + flushIntervalMs: 500, + signal: ingestController.signal, + }); + }); + + afterAll(async () => { + ingestController?.abort(); + await ingestPromise?.catch(() => {}); + await cleanupSchema?.(); + }); + + async function getIndexedRecord(uri: string): Promise { + const url = `http://test/xrpc/rsvp.atmo.event.getRecord?uri=${encodeURIComponent(uri)}`; + const res = await handle(new Request(url)); + if (res.status === 404) return undefined; + if (!res.ok) throw new Error(`getRecord ${uri} → ${res.status}: ${await res.text()}`); + // Contrail's getRecord returns the formatted row — uri/did/cid/time_us/record + // plus flattened relation counts (e.g. rsvpsGoingCount). + return await res.json(); + } + + it("indexes a community.lexicon.calendar.event published to devnet PDS", async () => { + const res = await client.post("com.atproto.repo.createRecord", { + input: { + repo: account.did, + collection: EVENT_NSID, + record: { + $type: EVENT_NSID, + name: eventName, + createdAt: new Date().toISOString(), + startsAt, + mode: `${EVENT_NSID}#inperson`, + status: `${EVENT_NSID}#scheduled`, + }, + }, + }); + expect(res.ok, `createRecord failed: ${JSON.stringify(res.data)}`).toBe(true); + if (!res.ok) throw new Error("unreachable"); + eventUri = res.data.uri; + eventCid = res.data.cid; + expect(eventUri).toMatch(new RegExp(`^at://${account.did}/${EVENT_NSID}/`)); + + const indexed = await waitFor(() => getIndexedRecord(eventUri), { label: `event ${eventUri}` }); + + expect(indexed.uri).toBe(eventUri); + expect(indexed.did).toBe(account.did); + expect(indexed.record.name).toBe(eventName); + expect(indexed.record.startsAt).toBe(startsAt); + }); + + it("increments rsvpsGoingCount when an RSVP is published", async () => { + expect(eventUri, "event must be published by the previous test").toBeTruthy(); + + const res = await client.post("com.atproto.repo.createRecord", { + input: { + repo: account.did, + collection: RSVP_NSID, + record: { + $type: RSVP_NSID, + subject: { uri: eventUri, cid: eventCid }, + status: `${RSVP_NSID}#going`, + createdAt: new Date().toISOString(), + }, + }, + }); + expect(res.ok, `createRecord failed: ${JSON.stringify(res.data)}`).toBe(true); + + const indexed = await waitFor( + async () => { + const r = await getIndexedRecord(eventUri); + return r && r.rsvpsGoingCount >= 1 ? r : undefined; + }, + { label: `rsvpsGoingCount for ${eventUri}` }, + ); + + expect(indexed.rsvpsGoingCount).toBe(1); + }); +}); diff --git a/apps/contrail-e2e/tsconfig.json b/apps/contrail-e2e/tsconfig.json new file mode 100644 index 0000000..9b7d9df --- /dev/null +++ b/apps/contrail-e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true + }, + "include": ["."] +} diff --git a/apps/contrail-e2e/vitest.config.ts b/apps/contrail-e2e/vitest.config.ts new file mode 100644 index 0000000..164b0c7 --- /dev/null +++ b/apps/contrail-e2e/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + // Live-stack tests mutate a shared devnet — can't run in parallel. + fileParallelism: false, + // Network round-trips can be slow on a cold stack. + testTimeout: 30_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89a1941..7a51e70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,31 @@ importers: specifier: ^4.63.0 version: 4.84.1(@cloudflare/workers-types@4.20260424.1) + apps/contrail-e2e: + dependencies: + '@atmo-dev/contrail': + specifier: workspace:* + version: link:../../packages/contrail + pg: + specifier: ^8.20.0 + version: 8.20.0 + devDependencies: + '@atcute/atproto': + specifier: ^3.1.10 + version: 3.1.11 + '@atcute/client': + specifier: ^4.2.1 + version: 4.2.1 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + apps/group-chat: dependencies: '@atcute/jetstream': @@ -62,13 +87,13 @@ importers: version: link:../../packages/sync '@foxui/core': specifier: ^0.9.1 - version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@foxui/social': specifier: ^0.8.10 - version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@foxui/time': specifier: ^0.8.5 - version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) valibot: specifier: ^1.3.1 version: 1.3.1(typescript@6.0.3) @@ -111,22 +136,22 @@ importers: version: 10.0.1(eslint@10.2.1(jiti@2.6.1)) '@sveltejs/adapter-cloudflare': specifier: ^7.2.8 - version: 7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1)) + version: 7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1)) '@sveltejs/kit': specifier: ^2.55.0 - version: 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@tailwindcss/forms': specifier: ^0.5.11 version: 0.5.11(tailwindcss@4.2.4) '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) bits-ui: specifier: ^2.16.4 - version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) eslint: specifier: ^10.1.0 version: 10.2.1(jiti@2.6.1) @@ -168,7 +193,7 @@ importers: version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) vite: specifier: ^8.0.3 - version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) wrangler: specifier: ^4.78.0 version: 4.84.1(@cloudflare/workers-types@4.20260424.1) @@ -245,13 +270,13 @@ importers: version: link:../../packages/lexicons '@foxui/core': specifier: ^0.9.1 - version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@foxui/social': specifier: ^0.8.10 - version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@foxui/time': specifier: ^0.8.5 - version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) valibot: specifier: ^1.3.1 version: 1.3.1(typescript@6.0.3) @@ -294,22 +319,22 @@ importers: version: 10.0.1(eslint@10.2.1(jiti@2.6.1)) '@sveltejs/adapter-cloudflare': specifier: ^7.2.8 - version: 7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1)) + version: 7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1)) '@sveltejs/kit': specifier: ^2.55.0 - version: 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@tailwindcss/forms': specifier: ^0.5.11 version: 0.5.11(tailwindcss@4.2.4) '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) bits-ui: specifier: ^2.16.4 - version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) eslint: specifier: ^10.1.0 version: 10.2.1(jiti@2.6.1) @@ -351,7 +376,7 @@ importers: version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) vite: specifier: ^8.0.3 - version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) wrangler: specifier: ^4.78.0 version: 4.84.1(@cloudflare/workers-types@4.20260424.1) @@ -400,13 +425,13 @@ importers: version: 8.20.0 tsup: specifier: ^8.5.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3) typescript: specifier: ^5.7.3 version: 5.9.3 vitest: specifier: ^4.1.0 - version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) packages/lexicons: dependencies: @@ -422,19 +447,19 @@ importers: version: 25.6.0 tsup: specifier: ^8.5.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3) typescript: specifier: ^5.7.3 version: 5.9.3 vitest: specifier: ^4.1.0 - version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) packages/sync: devDependencies: tsup: specifier: ^8.5.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -1105,89 +1130,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==} @@ -1349,36 +1390,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} @@ -1440,66 +1487,79 @@ packages: resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.2': resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.2': resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.2': resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.2': resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.2': resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.2': resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.2': resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.2': resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.2': resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.2': resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.2': resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.2': resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.2': resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} @@ -1621,24 +1681,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.4': resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.4': resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.4': resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.4': resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} @@ -2551,24 +2615,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==} @@ -4152,14 +4220,14 @@ snapshots: '@fontsource-variable/geist@5.2.8': {} - '@foxui/core@0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/core@0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: '@fontsource-variable/geist': 5.2.8 '@fontsource-variable/geist-mono': 5.2.7 '@jis3r/icons': 2.7.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) '@lucide/svelte': 1.9.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) '@number-flow/svelte': 0.4.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) clsx: 2.1.1 dompurify: 3.4.1 mode-watcher: 1.1.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) @@ -4176,15 +4244,15 @@ snapshots: - react-dom - vue - '@foxui/social@0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/social@0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: '@atcute/bluesky': 3.3.3 '@atcute/bluesky-richtext-segmenter': 3.0.0 - '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) - '@foxui/text': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) - '@foxui/time': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/text': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/time': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@use-gesture/vanilla': 10.3.1 - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) emoji-picker-element: 1.29.1 hls.js: 1.6.16 is-emoji-supported: 0.0.5 @@ -4201,10 +4269,10 @@ snapshots: - react-dom - vue - '@foxui/text@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/text@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: '@floating-ui/dom': 1.7.6 - '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/extension-code-block-lowlight': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(highlight.js@11.11.1)(lowlight@3.3.0) @@ -4224,7 +4292,7 @@ snapshots: '@tiptap/pm': 3.22.4 '@tiptap/starter-kit': 3.22.4 '@tiptap/suggestion': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) lowlight: 3.3.0 state: link:@tiptap/pm/state svelte: 5.55.5(@typescript-eslint/types@8.59.0) @@ -4240,11 +4308,11 @@ snapshots: - react-dom - vue - '@foxui/time@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/time@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: - '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@number-flow/svelte': 0.4.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) svelte: 5.55.5(@typescript-eslint/types@8.59.0) tailwindcss: 4.2.4 transitivePeerDependencies: @@ -4616,18 +4684,18 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1))': + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1))': dependencies: '@cloudflare/workers-types': 4.20260424.1 - '@sveltejs/kit': 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + '@sveltejs/kit': 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) worktop: 0.8.0-next.18 wrangler: 4.84.1(@cloudflare/workers-types@4.20260424.1) - '@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': + '@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -4639,18 +4707,18 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.55.5(@typescript-eslint/types@8.59.0) - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) optionalDependencies: typescript: 6.0.3 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 svelte: 5.55.5(@typescript-eslint/types@8.59.0) - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) - vitefu: 1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) + vitefu: 1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@swc/helpers@0.5.21': dependencies: @@ -4722,12 +4790,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 - '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': + '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 tailwindcss: 4.2.4 - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) '@tiptap/core@3.22.4(@tiptap/pm@3.22.4)': dependencies: @@ -5080,13 +5148,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) '@vitest/pretty-format@4.1.5': dependencies: @@ -5151,15 +5219,15 @@ snapshots: dependencies: is-windows: 1.0.2 - bits-ui@2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): + bits-ui@2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: '@floating-ui/core': 1.7.5 '@floating-ui/dom': 1.7.6 '@internationalized/date': 3.12.1 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + runed: 0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) svelte: 5.55.5(@typescript-eslint/types@8.59.0) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -5885,14 +5953,13 @@ snapshots: optionalDependencies: postcss: 8.5.10 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@1.10.3): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.10 tsx: 4.21.0 - yaml: 1.10.3 postcss-safe-parser@7.0.1(postcss@8.5.10): dependencies: @@ -6107,14 +6174,14 @@ snapshots: esm-env: 1.2.2 svelte: 5.55.5(@typescript-eslint/types@8.59.0) - runed@0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): + runed@0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.55.5(@typescript-eslint/types@8.59.0) optionalDependencies: - '@sveltejs/kit': 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + '@sveltejs/kit': 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) sade@1.8.1: dependencies: @@ -6252,10 +6319,10 @@ snapshots: '@tiptap/pm': 3.22.4 svelte: 5.55.5(@typescript-eslint/types@8.59.0) - svelte-toolbelt@0.10.6(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + runed: 0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) style-to-object: 1.0.14 svelte: 5.55.5(@typescript-eslint/types@8.59.0) transitivePeerDependencies: @@ -6342,7 +6409,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -6353,7 +6420,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@1.10.3) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0) resolve-from: 5.0.0 rollup: 4.60.2 source-map: 0.7.6 @@ -6433,7 +6500,7 @@ snapshots: optionalDependencies: typescript: 6.0.3 - vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3): + vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -6446,16 +6513,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 - yaml: 1.10.3 - vitefu@1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)): + vitefu@1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)): optionalDependencies: - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) - vitest@4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)): + vitest@4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -6472,7 +6538,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 -- 2.51.2 From 4ef9cf70f0935ac1a79b46ac3fc3524e153c0f52 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Fri, 24 Apr 2026 15:24:33 -0400 Subject: [PATCH 2/2] fix: resolve config defensively in raw runPersistent export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exported runPersistent(db, config, options) from @atmo-dev/contrail silently skipped grouped relation count updates when passed an unresolved ContrailConfig. Internals (applyEvents' buildBatchCountStatements, query planning, etc.) read `config._resolved?.relations[...]?.groups` — if `_resolved` was missing, the grouped-counts loop never ran, so columns like count_rsvp_going stayed at 0 while total count_rsvp updated normally. The Contrail class constructor resolves the config, so callers going through `new Contrail(...).runPersistent()` were fine. Only the raw export was affected — a real footgun for library consumers. Fix: mirror what Contrail's constructor does and defensively call resolveConfig when `_resolved` is absent. resolveConfig is idempotent, so this is a no-op for already-resolved configs. Regression test in packages/contrail/tests/persistent.test.ts passes a raw ContrailConfig with a grouped relation and asserts count_rsvp_going = 1. Confirmed to fail without this patch. Reverts the Contrail.runPersistent() workaround in the postgres-devnet e2e tests — they now call the raw runPersistent export directly, same as any external library consumer would. --- apps/contrail-e2e/tests/cursor-resume.test.ts | 10 +- .../tests/ingest-roundtrip.test.ts | 7 +- packages/contrail/src/core/persistent.ts | 11 ++- packages/contrail/tests/persistent.test.ts | 95 ++++++++++++++++++- 4 files changed, 107 insertions(+), 16 deletions(-) diff --git a/apps/contrail-e2e/tests/cursor-resume.test.ts b/apps/contrail-e2e/tests/cursor-resume.test.ts index 1b01066..9161f5a 100644 --- a/apps/contrail-e2e/tests/cursor-resume.test.ts +++ b/apps/contrail-e2e/tests/cursor-resume.test.ts @@ -24,7 +24,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import pg from "pg"; import { CredentialManager, Client } from "@atcute/client"; import "@atcute/atproto"; -import { Contrail } from "@atmo-dev/contrail"; +import { runPersistent } from "@atmo-dev/contrail"; import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; import { config as baseConfig } from "../config"; import { @@ -73,15 +73,11 @@ describe("cursor resume (ingester stops, events pile up, ingester restarts)", () it("resumes from saved cursor and picks up commits issued while down", async () => { const db = createPostgresDatabase(pool); - // Go through the Contrail wrapper so the config is resolved; raw - // runPersistent expects a resolved config. - const contrail = new Contrail({ ...baseConfig, db }); - await contrail.init(); const runOpts = { batchSize: 50, flushIntervalMs: 500 }; // ---- Phase 1: start ingester, publish A, verify indexed ---- const c1 = new AbortController(); - const ingest1 = contrail.runPersistent({ ...runOpts, signal: c1.signal }); + const ingest1 = runPersistent(db, baseConfig, { ...runOpts, signal: c1.signal }); const nameA = `A-${Date.now()}`; const aRes = await client.post("com.atproto.repo.createRecord", { @@ -164,7 +160,7 @@ describe("cursor resume (ingester stops, events pile up, ingester restarts)", () // ---- Phase 4: restart ingester, assert replay ---- const c2 = new AbortController(); - const ingest2 = contrail.runPersistent({ ...runOpts, signal: c2.signal }); + const ingest2 = runPersistent(db, baseConfig, { ...runOpts, signal: c2.signal }); try { const indexedB = await waitFor( diff --git a/apps/contrail-e2e/tests/ingest-roundtrip.test.ts b/apps/contrail-e2e/tests/ingest-roundtrip.test.ts index b597245..e9b67ce 100644 --- a/apps/contrail-e2e/tests/ingest-roundtrip.test.ts +++ b/apps/contrail-e2e/tests/ingest-roundtrip.test.ts @@ -19,7 +19,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import pg from "pg"; import { CredentialManager, Client } from "@atcute/client"; import "@atcute/atproto"; -import { Contrail } from "@atmo-dev/contrail"; +import { Contrail, runPersistent } from "@atmo-dev/contrail"; import { createHandler } from "@atmo-dev/contrail/server"; import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; import { config as baseConfig } from "../config"; @@ -64,11 +64,8 @@ describe("ingest roundtrip (devnet PDS → Jetstream → Contrail)", () => { await contrail.init(); handle = createHandler(contrail); - // In-process ingester via Contrail so the config is resolved (raw - // runPersistent expects a resolved config — grouped counts silently - // don't update otherwise). ingestController = new AbortController(); - ingestPromise = contrail.runPersistent({ + ingestPromise = runPersistent(db, baseConfig, { batchSize: 50, flushIntervalMs: 500, signal: ingestController.signal, diff --git a/packages/contrail/src/core/persistent.ts b/packages/contrail/src/core/persistent.ts index 4dade65..c1e1c18 100644 --- a/packages/contrail/src/core/persistent.ts +++ b/packages/contrail/src/core/persistent.ts @@ -1,6 +1,6 @@ import type { JetstreamSubscription } from "@atcute/jetstream"; -import type { ContrailConfig, IngestEvent, Database, Logger } from "./types"; -import { getCollectionNsids, getDependentNsids, DEFAULT_FEED_MAX_ITEMS } from "./types"; +import type { ContrailConfig, IngestEvent, Database, Logger, ResolvedContrailConfig } from "./types"; +import { getCollectionNsids, getDependentNsids, DEFAULT_FEED_MAX_ITEMS, resolveConfig } from "./types"; import { initSchema, getLastCursor, saveCursor, applyEvents, pruneFeedItems } from "./db"; import { refreshStaleIdentities } from "./identity"; import { createIngestState } from "./jetstream"; @@ -29,6 +29,13 @@ export async function runPersistent( config: ContrailConfig, options?: PersistentIngestOptions, ): Promise { + // Internals (applyEvents, count updates, query planning) read `_resolved` + // and silently skip features when it's missing. The Contrail class resolves + // in its constructor; callers using this raw export must also get a resolved + // config, so do it defensively here. resolveConfig is idempotent. + if (!(config as ResolvedContrailConfig)._resolved) { + config = resolveConfig(config); + } const log = getLogger(config, options); const batchSize = options?.batchSize ?? 50; const flushIntervalMs = options?.flushIntervalMs ?? 5_000; diff --git a/packages/contrail/tests/persistent.test.ts b/packages/contrail/tests/persistent.test.ts index 5fba923..f248cf0 100644 --- a/packages/contrail/tests/persistent.test.ts +++ b/packages/contrail/tests/persistent.test.ts @@ -1,8 +1,9 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { Database } from "../src/core/types"; -import { createTestDbWithSchema, TEST_CONFIG } from "./helpers"; +import type { ContrailConfig, Database } from "../src/core/types"; +import { createTestDb, createTestDbWithSchema, TEST_CONFIG } from "./helpers"; import { runPersistent } from "../src/core/persistent"; import { getLastCursor, queryRecords } from "../src/core/db/records"; +import { initSchema } from "../src/core/db/schema"; // Mock identity resolution to avoid network calls in tests vi.mock("../src/core/identity", () => ({ @@ -201,6 +202,96 @@ describe("runPersistent", () => { await promise; }); + it("resolves config internally when given an unresolved ContrailConfig", async () => { + // Regression test for the silent grouped-count bug: passing a raw + // ContrailConfig (no `_resolved`) used to let total counts update while + // grouped count columns stayed at 0. runPersistent must defensively + // resolve so consumers of the raw export get the same behavior as those + // going through the Contrail class wrapper. + const rawConfig: ContrailConfig = { + namespace: "com.example", + collections: { + event: { + collection: "community.lexicon.calendar.event", + relations: { + rsvps: { + collection: "rsvp", + groupBy: "status", + groups: { + going: "community.lexicon.calendar.rsvp#going", + notgoing: "community.lexicon.calendar.rsvp#notgoing", + }, + }, + }, + }, + rsvp: { + collection: "community.lexicon.calendar.rsvp", + references: { + event: { collection: "event", field: "subject.uri" }, + }, + }, + }, + }; + // Sanity: the raw object must NOT have _resolved — that's the whole point. + expect((rawConfig as any)._resolved).toBeUndefined(); + + const freshDb = createTestDb(); + await initSchema(freshDb, rawConfig); + + const eventUri = "at://did:plc:alice/community.lexicon.calendar.event/evt1"; + const events = [ + { + kind: "commit" as const, + did: "did:plc:alice", + time_us: 9000, + commit: { + collection: "community.lexicon.calendar.event", + operation: "create", + rkey: "evt1", + cid: "cidE", + record: { name: "E", startsAt: "2026-04-01T10:00:00Z", mode: "online" }, + }, + }, + { + kind: "commit" as const, + did: "did:plc:alice", + time_us: 9001, + commit: { + collection: "community.lexicon.calendar.rsvp", + operation: "create", + rkey: "r1", + cid: "cidR", + record: { + subject: { uri: eventUri, cid: "cidE" }, + status: "community.lexicon.calendar.rsvp#going", + }, + }, + }, + ]; + + const controller = new AbortController(); + const promise = runPersistent(freshDb, rawConfig, { + batchSize: 100, + flushIntervalMs: 50, + signal: controller.signal, + createSubscription: () => mockSubscription(events) as any, + }); + await new Promise((r) => setTimeout(r, 300)); + controller.abort(); + await promise; + + // Query the raw count columns directly — using queryRecords would hide + // the bug since it hydrates via the resolved config anyway. + const row = await freshDb + .prepare(`SELECT count_rsvp, count_rsvp_going FROM records_event WHERE uri = ?`) + .bind(eventUri) + .first<{ count_rsvp: number; count_rsvp_going: number }>(); + expect(row).not.toBeNull(); + expect(row!.count_rsvp).toBe(1); + // This is the assertion that would fail before the fix. + expect(row!.count_rsvp_going).toBe(1); + }); + it("skips non-commit events", async () => { const events = [ { kind: "identity" as const, did: "did:plc:someone", time_us: 4000 }, -- 2.51.2