diff --git a/.changeset/backfill-all-rename.md b/.changeset/backfill-all-rename.md new file mode 100644 index 0000000..9718563 --- /dev/null +++ b/.changeset/backfill-all-rename.md @@ -0,0 +1,31 @@ +--- +"@atmo-dev/contrail": minor +--- + +rename `contrail.sync()` → `contrail.backfillAll()` and emit progress via `config.logger` by default. + +the method previously returned `{ discovered, backfilled }` but emitted no output, so callers had to wire up their own `onProgress`. it now logs discovery + throttled backfill progress + final summary through `config.logger` (defaults to `console`). supplying `onProgress` still takes over, and passing a no-op logger silences it. + +also renames the internal `backfillAll` function (in `src/core/backfill.ts`) to `backfillPending` to reduce confusion with the new public method. not publicly exported, so no user-facing impact. + +adds a `contrail` CLI bin with a `backfill` subcommand so workers deploys don't need a local script file at all: + +```json +"scripts": { + "backfill": "contrail backfill --config src/config.ts", + "backfill:remote": "contrail backfill --config src/config.ts --remote" +} +``` + +auto-detects `contrail.config.ts`, `app/config.ts`, or `src/lib/contrail/config.ts`; loads TS configs via `jiti` (no tsx hook required). flags: `--config`, `--remote`, `--binding `, `--concurrency `. + +the underlying helper is also exported at `@atmo-dev/contrail/workers` for embedded use: + +```ts +import { backfillAll } from "@atmo-dev/contrail/workers"; +await backfillAll({ config, remote: true }); +``` + +`wrangler` is an optional peer dep — only imported at runtime when the cli/helper is called. + +breaking: `contrail.sync()` is gone; rename callsites to `contrail.backfillAll()`. signature and return shape unchanged. diff --git a/.changeset/jiti-config-loader.md b/.changeset/jiti-config-loader.md new file mode 100644 index 0000000..494bd26 --- /dev/null +++ b/.changeset/jiti-config-loader.md @@ -0,0 +1,5 @@ +--- +"@atmo-dev/contrail-lexicons": patch +--- + +fix `contrail-lex --config ` with plain TS files. previously broke with `ERR_UNKNOWN_FILE_EXTENSION` under plain node — bare `contrail-lex` invocation couldn't load TS configs. now uses `jiti` to handle TS + ESM + CJS transparently, so invocations like `contrail-lex all --config src/config.ts` work without needing tsx/ts-node preregistered. diff --git a/.changeset/lexicons-publish-cmd.md b/.changeset/lexicons-publish-cmd.md new file mode 100644 index 0000000..5efbcbf --- /dev/null +++ b/.changeset/lexicons-publish-cmd.md @@ -0,0 +1,13 @@ +--- +"@atmo-dev/contrail-lexicons": minor +--- + +add `contrail-lex publish` subcommand. wraps `publishLexicons` so you can push lexicon JSON to a PDS without writing a script: + +```bash +contrail-lex publish +# or via env: +LEXICON_ACCOUNT_IDENTIFIER=you.bsky.social LEXICON_ACCOUNT_PASSWORD=xxxx contrail-lex publish +``` + +supports `--generated-dir` (default `lexicons-generated`), `--skip-confirm` (for CI), and `--dry-run` (print what would be published + the DNS records needed, no writes, credentials not required). diff --git a/.changeset/refresh-command.md b/.changeset/refresh-command.md new file mode 100644 index 0000000..a429382 --- /dev/null +++ b/.changeset/refresh-command.md @@ -0,0 +1,25 @@ +--- +"@atmo-dev/contrail": minor +--- + +add `refresh` — a "catch-up" CLI + method that reconciles every known DID's PDS against the DB and reports what was missed. + +unlike `backfillAll`, it ignores the `backfills` state table and sweeps fresh. useful after jetstream outages or after leaving a dev deployment idle for days. + +each record in each configured collection is classified as: + +- **missing** — PDS has it, DB doesn't +- **stale update** — DB has it with a different CID, *and* the DB row was written before the ignore window (default 60s, configurable) +- **in sync** — same CID, or DB row is within the ignore window + +```bash +pnpm contrail refresh # totals +pnpm contrail refresh --by-collection # + per-nsid breakdown +pnpm contrail refresh --ignore-window 30 +``` + +programmatic: `contrail.refresh({ ignoreWindowMs, concurrency })` returns per-collection stats + totals. also exported from `@atmo-dev/contrail/workers` as `refresh()` for wrangler-backed deployments. + +safe to run repeatedly — each pass converges toward zero. not a replacement for `ingest` / `runPersistent` (walks every user's history, which is expensive); use for after-outage reconciliation or dev-idle catch-up. + +also extends `ExistingRecordInfo` with an `indexed_at: number | null` field so callers using `lookupExistingRecords` can inspect per-row freshness without a second query. diff --git a/README.md b/README.md index 28adadf..40634f9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ # Contrail -> **Pre-alpha.** Expect breaking changes. +> **pre-alpha.** Expect breaking changes. -A library for building AT Protocol appviews. Declare your collections, get automatic Jetstream ingestion, typed XRPC endpoints, optional permissioned spaces for private records, group-controlled communities, and a client-side reactive sync layer. Runs on Cloudflare Workers + D1, Node.js + PostgreSQL, or SvelteKit. +a library for easily creating (serverless) atproto backends/appviews. + +- declare collections +- get automatic jetstream backfill and ingestion, typed XRPC endpoints +- optional: permissioned spaces and group-controlled communities + +mostly tested on cloudflare workers with d1 but should run in any node env too (+ has adapters for node:sqlite and postgres for the db). ## Install @@ -12,39 +18,82 @@ pnpm add @atmo-dev/contrail ## Minimal example +a complete cloudflare worker that indexes public calendar events from the atproto network and serves them over a typed XRPC endpoint. two files + config. a runnable version lives in [`apps/cloudflare-workers`](./apps/cloudflare-workers) — clone, deploy, `pnpm contrail backfill --remote`, done. + +**`src/contrail.config.ts`** — picked up automatically by the `contrail` CLI: + ```ts -import { Contrail } from "@atmo-dev/contrail"; +import type { ContrailConfig } from "@atmo-dev/contrail"; -const contrail = new Contrail({ +export const config: ContrailConfig = { namespace: "com.example", - db, // D1, node:sqlite, or @atmo-dev/contrail/postgres collections: { event: { - collection: "community.lexicon.calendar.event", - queryable: { startsAt: { type: "range" } }, - searchable: ["name", "description"], + collection: "community.lexicon.calendar.event", // NSID to index + queryable: { startsAt: { type: "range" } }, // ?startsAtMin=... + searchable: ["name", "description"], // ?search=... }, }, -}); - -await contrail.init(); -await contrail.ingest(); // pulls from Jetstream +}; ``` -Mount the XRPC handler in any fetch-style framework: +**`src/worker.ts`** — serves XRPC requests, pulls new records every minute: ```ts +import { Contrail } from "@atmo-dev/contrail"; import { createHandler } from "@atmo-dev/contrail/server"; -export default { fetch: createHandler(contrail) }; +import { config } from "./contrail.config"; + +const contrail = new Contrail(config); +const handle = createHandler(contrail); +let ready = false; + +export default { + async fetch(req: Request, env: { DB: D1Database }) { + if (!ready) { await contrail.init(env.DB); ready = true; } // create tables once + return handle(req, env.DB); // xrpc routes + }, + async scheduled(_ev: ScheduledEvent, env: { DB: D1Database }, ctx: ExecutionContext) { + ctx.waitUntil(contrail.ingest({}, env.DB)); // pull new records + }, +}; +``` + +and a d1 binding + cron in `wrangler.jsonc`: + +```jsonc +{ + "main": "src/worker.ts", + "d1_databases": [{ "binding": "DB", "database_name": "contrail", "database_id": "..." }], + "triggers": { "crons": ["*/1 * * * *"] } +} +``` + +then: + +```bash +npx wrangler d1 create contrail # copy the id into wrangler.jsonc +pnpm wrangler deploy # deploy the worker +pnpm contrail backfill --remote # one-shot historical backfill ``` +the worker keeps itself fresh from now on via the cron. hit: + +``` +GET https://.workers.dev/xrpc/com.example.event.listRecords?startsAtMin=2026-01-01&limit=10 +``` + +returns every `community.lexicon.calendar.event` record published anywhere on atproto that matches, as JSON. that's it — no PDS setup, no lexicon publishing, no relay configuration. everything scales from there: add filters, add full-text search, add more collections, turn on [spaces](./docs/03-spaces.md) for private records, mount the handler in sveltekit instead, swap the adapter for postgres. + +**not using workers?** same library, different `db`. see [adapters](./docs/01-indexing.md#adapters) for node:sqlite and postgres. + ## Docs - [Indexing](./docs/01-indexing.md) — the core: collections, queries, ingestion, adapters -- [Spaces](./docs/02-spaces.md) — permissioned records stored by the appview -- [Communities](./docs/03-communities.md) — group-controlled atproto DIDs -- [Sync](./docs/04-sync.md) — reactive client-side store over `watchRecords` -- [Lexicons](./docs/05-lexicons.md) — `contrail-lex` CLI and codegen +- [Lexicons](./docs/02-lexicons.md) — `contrail-lex` CLI, codegen, publishing +- [Spaces](./docs/03-spaces.md) — permissioned records stored by the appview +- [Communities](./docs/04-communities.md) — group-controlled atproto DIDs +- [Sync](./docs/05-sync.md) — reactive client-side store over `watchRecords` - [Examples](./docs/06-examples.md) — reference deployments in the repo ## Packages diff --git a/apps/cloudflare-workers/README.md b/apps/cloudflare-workers/README.md index 38bb787..ccedbef 100644 --- a/apps/cloudflare-workers/README.md +++ b/apps/cloudflare-workers/README.md @@ -1,65 +1,43 @@ -# Contrail — Cloudflare Workers Example +# cloudflare-workers -A complete example of using Contrail to index AT Protocol calendar events and RSVPs on Cloudflare Workers + D1. +minimal, runnable contrail deployment — cloudflare workers + d1, one collection (`community.lexicon.calendar.event`), no spaces / communities / realtime. mirrors the setup shown in the [root README](../../README.md) exactly. -## Setup +## layout -```bash -# Copy this folder to a new project -cp -r examples/cloudflare-workers my-contrail-app -cd my-contrail-app - -# Install dependencies -npm install - -# Create a D1 database -npx wrangler d1 create contrail +``` +src/contrail.config.ts — shared config (collections, queryables, searchables) +src/worker.ts — fetch handler + scheduled ingest +wrangler.jsonc — d1 binding + cron ``` -Copy the `database_id` from the output into `wrangler.jsonc`. - -> **Note:** The `contrail` dependency installs from GitHub. This may take a minute on first install since it also pulls in the AT Protocol client libraries. - -## Configure - -Edit `config.ts` to define your collections, queryable fields, relations, and references. See the [Contrail README](https://github.com/flo-bit/contrail) for all options. +backfills run via the `contrail` cli from the library (see `package.json` scripts) — no script file needed. -## Develop +## setup ```bash -# Discover users from relays and backfill their records -npm run sync - -# Start the dev server (ingests from Jetstream every minute) -npm run dev +pnpm install +npx wrangler d1 create contrail # copy database_id into wrangler.jsonc +pnpm deploy # deploy the worker +pnpm contrail backfill --remote # discover + backfill historical events ``` -Your XRPC API is now available at `http://localhost:8787`: +then hit: ``` -# List events sorted by RSVP count -/xrpc/rsvp.atmo.event.listRecords?sort=rsvpsCount - -# Upcoming events with 10+ going RSVPs -/xrpc/rsvp.atmo.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10 - -# Single event with hydrated RSVPs and profiles -/xrpc/rsvp.atmo.event.getRecord?uri=at://...&hydrateRsvps=10&profiles=true - -# Search events -/xrpc/rsvp.atmo.event.listRecords?search=meetup - -# RSVPs for a specific event -/xrpc/rsvp.atmo.rsvp.listRecords?subjectUri=at://... +GET https://.workers.dev/xrpc/com.example.event.listRecords?startsAtMin=2026-01-01&limit=10 ``` -## Deploy +## local dev ```bash -npm run deploy - -# Sync against production D1 -npm run sync:remote +pnpm dev # wrangler dev, cron fires every minute +pnpm contrail backfill # backfill against the local D1 created by wrangler ``` -Ingestion runs automatically via cron (`*/1 * * * *`). +## extending + +- **add a collection:** append to `collections` in `src/contrail.config.ts`; redeploy; `pnpm contrail backfill --remote` to backfill the new one. +- **add full-text search:** `searchable: ["field1", "field2"]`, redeploy, no backfill needed (fts indexes repopulate on ingest). +- **add relations / references:** see [indexing docs](../../docs/01-indexing.md). +- **private records:** see [spaces docs](../../docs/03-spaces.md). +- **group-controlled DIDs:** see [communities docs](../../docs/04-communities.md). diff --git a/apps/cloudflare-workers/generate.ts b/apps/cloudflare-workers/generate.ts deleted file mode 100644 index 61361dd..0000000 --- a/apps/cloudflare-workers/generate.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; -import { config } from "./config"; -import { generateLexicons } from "@atmo-dev/contrail-lexicons"; - -const ROOT_DIR = dirname(fileURLToPath(import.meta.url)); - -generateLexicons({ - config, - rootDir: ROOT_DIR, - outputDir: join(ROOT_DIR, "lexicons-generated"), - writeRuntimeFiles: true, -}); diff --git a/apps/cloudflare-workers/package.json b/apps/cloudflare-workers/package.json index cffa716..48b55a0 100644 --- a/apps/cloudflare-workers/package.json +++ b/apps/cloudflare-workers/package.json @@ -5,21 +5,15 @@ "type": "module", "scripts": { "dev": "wrangler dev --test-scheduled", - "deploy": "wrangler deploy", - "generate": "tsx generate.ts", - "generate:pull": "tsx generate.ts && lex-cli pull && tsx generate.ts && lex-cli pull && lex-cli generate", - "sync": "tsx sync.ts", - "sync:remote": "tsx sync.ts --remote" + "deploy": "wrangler deploy" }, "dependencies": { "@atmo-dev/contrail": "workspace:*" }, "devDependencies": { - "@atcute/lex-cli": "^2.5.3", - "@atcute/lexicon-doc": "^2.1.2", + "@atcute/lex-cli": "^2.8.1", "@atmo-dev/contrail-lexicons": "workspace:*", "@cloudflare/workers-types": "^4.20250124.0", - "tsx": "^4.21.0", "typescript": "^5.7.3", "wrangler": "^4.63.0" } diff --git a/apps/cloudflare-workers/src/contrail.config.ts b/apps/cloudflare-workers/src/contrail.config.ts new file mode 100644 index 0000000..b6802e1 --- /dev/null +++ b/apps/cloudflare-workers/src/contrail.config.ts @@ -0,0 +1,12 @@ +import type { ContrailConfig } from "@atmo-dev/contrail"; + +export const config: ContrailConfig = { + namespace: "com.example", + collections: { + event: { + collection: "community.lexicon.calendar.event", // NSID to index + queryable: { startsAt: { type: "range" } }, // ?startsAtMin=... + searchable: ["name", "description"], // ?search=... + }, + }, +}; diff --git a/apps/cloudflare-workers/src/worker.ts b/apps/cloudflare-workers/src/worker.ts new file mode 100644 index 0000000..8d9a216 --- /dev/null +++ b/apps/cloudflare-workers/src/worker.ts @@ -0,0 +1,17 @@ +import { Contrail } from "@atmo-dev/contrail"; +import { createHandler } from "@atmo-dev/contrail/server"; +import { config } from "./contrail.config"; + +const contrail = new Contrail(config); +const handle = createHandler(contrail); +let ready = false; + +export default { + async fetch(req: Request, env: { DB: D1Database }) { + if (!ready) { await contrail.init(env.DB); ready = true; } // create tables once + return handle(req, env.DB); // xrpc routes + }, + async scheduled(_ev: ScheduledEvent, env: { DB: D1Database }, ctx: ExecutionContext) { + ctx.waitUntil(contrail.ingest({}, env.DB)); // pull new records + }, +}; diff --git a/apps/cloudflare-workers/sync.ts b/apps/cloudflare-workers/sync.ts deleted file mode 100644 index 09bacd0..0000000 --- a/apps/cloudflare-workers/sync.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Discover users from relays and backfill their records from PDS. - * - * Usage: - * npx tsx sync.ts # local D1 - * npx tsx sync.ts --remote # prod D1 - */ -import { Contrail } from "@atmo-dev/contrail"; -import { config } from "./config"; -import { getPlatformProxy } from "wrangler"; - -function elapsed(start: number): string { - const ms = Date.now() - start; - if (ms < 1000) return `${ms}ms`; - if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; - const mins = Math.floor(ms / 60_000); - const secs = ((ms % 60_000) / 1000).toFixed(0); - return `${mins}m ${secs}s`; -} - -async function main() { - const remote = process.argv.includes("--remote"); - const syncStart = Date.now(); - - console.log(`=== Sync (${remote ? "remote/prod" : "local"} D1) ===\n`); - - const { env, dispose } = await getPlatformProxy<{ DB: D1Database }>({ - environment: remote ? "production" : undefined, - }); - - const contrail = new Contrail({ ...config, db: env.DB }); - - try { - await contrail.init(); - - console.log("--- Discovery ---"); - const discoveryStart = Date.now(); - const discovered = await contrail.discover(); - console.log(` Done: ${discovered.length} users in ${elapsed(discoveryStart)}\n`); - - console.log("--- Backfill ---"); - const backfillStart = Date.now(); - const total = await contrail.backfill({ - concurrency: 100, - onProgress: ({ records, usersComplete, usersTotal, usersFailed }) => { - const secs = (Date.now() - backfillStart) / 1000; - const rate = secs > 0 ? Math.round(records / secs) : 0; - const failStr = usersFailed > 0 ? ` | ${usersFailed} failed` : ""; - process.stdout.write( - `\r ${records} records | ${usersComplete}/${usersTotal} users | ${rate}/s | ${elapsed(backfillStart)}${failStr} ` - ); - }, - }); - process.stdout.write("\n"); - console.log(` Done: ${total} records in ${elapsed(backfillStart)}\n`); - - console.log(`=== Finished in ${elapsed(syncStart)} ===`); - console.log(` Discovered: ${discovered.length} users`); - console.log(` Backfilled: ${total} records`); - } finally { - await dispose(); - } -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/apps/cloudflare-workers/worker.ts b/apps/cloudflare-workers/worker.ts deleted file mode 100644 index b046d44..0000000 --- a/apps/cloudflare-workers/worker.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Contrail } from "@atmo-dev/contrail"; -import { createHandler } from "@atmo-dev/contrail/server"; -import { config } from "./config"; - -const contrail = new Contrail(config); -const handle = createHandler(contrail); - -let initialized = false; - -export default { - async fetch(request: Request, env: { DB: D1Database }) { - if (!initialized) { - await contrail.init(env.DB); - initialized = true; - } - return handle(request, env.DB); - }, - - async scheduled( - _event: ScheduledEvent, - env: { DB: D1Database }, - ctx: ExecutionContext - ) { - if (!initialized) { - await contrail.init(env.DB); - initialized = true; - } - ctx.waitUntil(contrail.ingest({}, env.DB)); - }, -}; diff --git a/apps/cloudflare-workers/wrangler.jsonc b/apps/cloudflare-workers/wrangler.jsonc index 0b403a5..230f9fe 100644 --- a/apps/cloudflare-workers/wrangler.jsonc +++ b/apps/cloudflare-workers/wrangler.jsonc @@ -1,6 +1,6 @@ { "name": "contrail", - "main": "worker.ts", + "main": "src/worker.ts", "compatibility_date": "2025-12-25", "observability": { "enabled": true diff --git a/apps/group-chat/README.md b/apps/group-chat/README.md index 07f807c..e1b1169 100644 --- a/apps/group-chat/README.md +++ b/apps/group-chat/README.md @@ -21,7 +21,7 @@ pnpm dev ``` Re-run `pnpm generate` whenever you add/remove collections or toggle contrail -modules in `src/lib/contrail/config.ts`: the OAuth scope list is derived from +modules in `src/lib/contrail.config.ts`: the OAuth scope list is derived from it, so the consent screen only asks for permissions you actually use. Dev mode uses a loopback OAuth client — no Cloudflare setup needed. The realtime Durable Object runs locally via miniflare. diff --git a/apps/group-chat/lexicons/tools/atmo/chat/channel.json b/apps/group-chat/lexicons/custom/tools/atmo/chat/channel.json similarity index 100% rename from apps/group-chat/lexicons/tools/atmo/chat/channel.json rename to apps/group-chat/lexicons/custom/tools/atmo/chat/channel.json diff --git a/apps/group-chat/lexicons/tools/atmo/chat/message.json b/apps/group-chat/lexicons/custom/tools/atmo/chat/message.json similarity index 100% rename from apps/group-chat/lexicons/tools/atmo/chat/message.json rename to apps/group-chat/lexicons/custom/tools/atmo/chat/message.json diff --git a/apps/group-chat/lexicons/tools/atmo/chat/server.json b/apps/group-chat/lexicons/custom/tools/atmo/chat/server.json similarity index 100% rename from apps/group-chat/lexicons/tools/atmo/chat/server.json rename to apps/group-chat/lexicons/custom/tools/atmo/chat/server.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/channel/getRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/channel/getRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/channel/listRecords.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/channel/listRecords.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/channel/watchRecords.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/channel/watchRecords.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/adopt.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/adopt.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/defs.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/defs.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/defs.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/defs.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/delete.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/delete.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/delete.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/delete.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/deleteRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/deleteRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/getHealth.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/getHealth.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/list.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/list.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/list.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/list.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/mint.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/mint.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/mint.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/mint.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/putRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/putRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/reauth.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/reauth.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/create.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/create.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/delete.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/delete.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/deleteRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/deleteRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/grant.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/grant.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/listMembers.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/listMembers.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/putRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/putRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/resync.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/resync.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/revoke.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/revoke.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/setAccessLevel.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/community/space/setAccessLevel.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/getCursor.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/getCursor.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/getCursor.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/getCursor.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/getOverview.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/getOverview.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/getOverview.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/getOverview.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/getProfile.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/getProfile.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/getProfile.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/getProfile.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/create.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/invite/create.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/invite/create.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/invite/create.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/invite/defs.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/invite/defs.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/list.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/invite/list.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/invite/list.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/invite/list.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/invite/redeem.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/invite/redeem.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/invite/revoke.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/invite/revoke.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/message/getRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/message/getRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/message/listRecords.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/message/listRecords.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/message/watchRecords.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/message/watchRecords.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/notifyOfUpdate.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/notifyOfUpdate.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/permissionSet.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/permissionSet.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/realtime/subscribe.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/realtime/subscribe.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/realtime/ticket.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/realtime/ticket.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/server/getRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/server/getRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/server/listRecords.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/server/listRecords.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/server/watchRecords.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/server/watchRecords.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/addMember.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/addMember.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/createSpace.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/createSpace.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/defs.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/defs.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/defs.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/defs.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/deleteRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/deleteRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/getBlob.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/getBlob.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/getRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/getRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/getSpace.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/getSpace.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/leaveSpace.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/leaveSpace.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/listBlobs.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/listBlobs.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/listMembers.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/listMembers.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/listRecords.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/listRecords.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/listSpaces.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/listSpaces.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/putRecord.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/putRecord.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/removeMember.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/removeMember.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/space/uploadBlob.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/space/uploadBlob.json diff --git a/apps/group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json b/apps/group-chat/lexicons/generated/tools/atmo/chat/spaceExt/whoami.json similarity index 100% rename from apps/group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json rename to apps/group-chat/lexicons/generated/tools/atmo/chat/spaceExt/whoami.json diff --git a/apps/group-chat/lexicons-pulled/README.md b/apps/group-chat/lexicons/pulled/README.md similarity index 100% rename from apps/group-chat/lexicons-pulled/README.md rename to apps/group-chat/lexicons/pulled/README.md diff --git a/apps/group-chat/lexicons-pulled/app/bsky/actor/profile.json b/apps/group-chat/lexicons/pulled/app/bsky/actor/profile.json similarity index 100% rename from apps/group-chat/lexicons-pulled/app/bsky/actor/profile.json rename to apps/group-chat/lexicons/pulled/app/bsky/actor/profile.json diff --git a/apps/group-chat/scripts/generate.ts b/apps/group-chat/scripts/generate.ts index 205c0f9..c29853a 100644 --- a/apps/group-chat/scripts/generate.ts +++ b/apps/group-chat/scripts/generate.ts @@ -1,7 +1,7 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { writeFileSync } from 'fs'; -import { baseConfig } from '../src/lib/contrail/config'; +import { baseConfig } from '../src/lib/contrail.config'; import { generateLexicons, extractXrpcMethods } from '@atmo-dev/contrail-lexicons'; const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); @@ -19,7 +19,7 @@ const configForGen = { const generated = generateLexicons({ config: configForGen, rootDir: ROOT_DIR, - outputDir: join(ROOT_DIR, 'lexicons-generated'), + outputDir: join(ROOT_DIR, 'lexicons', 'generated'), writeRuntimeFiles: true }); diff --git a/apps/group-chat/scripts/sync.ts b/apps/group-chat/scripts/sync.ts index c946f86..c1f672f 100644 --- a/apps/group-chat/scripts/sync.ts +++ b/apps/group-chat/scripts/sync.ts @@ -6,7 +6,7 @@ * pnpm sync:remote # prod D1 */ import { Contrail } from '@atmo-dev/contrail'; -import { config } from '../src/lib/contrail/config'; +import { config } from '../src/lib/contrail.config'; import { getPlatformProxy } from 'wrangler'; function elapsed(start: number): string { diff --git a/apps/group-chat/src/lib/contrail/config.ts b/apps/group-chat/src/lib/contrail.config.ts similarity index 100% rename from apps/group-chat/src/lib/contrail/config.ts rename to apps/group-chat/src/lib/contrail.config.ts diff --git a/apps/group-chat/src/lib/contrail/index.ts b/apps/group-chat/src/lib/contrail/index.ts index 555d348..a75157a 100644 --- a/apps/group-chat/src/lib/contrail/index.ts +++ b/apps/group-chat/src/lib/contrail/index.ts @@ -13,7 +13,7 @@ import { import { createHandler, createServerClient } from '@atmo-dev/contrail/server'; import type { Client } from '@atcute/client'; import { dev } from '$app/environment'; -import { baseConfig } from './config'; +import { baseConfig } from '../contrail.config'; type Env = App.Platform['env']; diff --git a/apps/postgres/config.ts b/apps/postgres/config.ts deleted file mode 100644 index a7169da..0000000 --- a/apps/postgres/config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ContrailConfig } from "@atmo-dev/contrail"; - -export const config: ContrailConfig = { - namespace: "rsvp.atmo", - 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/cloudflare-workers/config.ts b/apps/postgres/contrail.config.ts similarity index 100% rename from apps/cloudflare-workers/config.ts rename to apps/postgres/contrail.config.ts diff --git a/apps/postgres/ingest.ts b/apps/postgres/ingest.ts index 1acc13e..0f0cca4 100644 --- a/apps/postgres/ingest.ts +++ b/apps/postgres/ingest.ts @@ -10,7 +10,7 @@ import pg from "pg"; import { Contrail } from "@atmo-dev/contrail"; import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; -import { config } from "./config"; +import { config } from "./contrail.config"; const DATABASE_URL = process.env.DATABASE_URL ?? diff --git a/apps/postgres/serve.ts b/apps/postgres/serve.ts index ce48513..27bfcac 100644 --- a/apps/postgres/serve.ts +++ b/apps/postgres/serve.ts @@ -9,7 +9,7 @@ import { createServer } from "node:http"; import { Contrail } from "@atmo-dev/contrail"; import { createHandler } from "@atmo-dev/contrail/server"; import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; -import { config } from "./config"; +import { config } from "./contrail.config"; const DATABASE_URL = process.env.DATABASE_URL ?? diff --git a/apps/postgres/sync.ts b/apps/postgres/sync.ts index eecbdb8..4efe47d 100644 --- a/apps/postgres/sync.ts +++ b/apps/postgres/sync.ts @@ -10,7 +10,7 @@ import pg from "pg"; import { Contrail } from "@atmo-dev/contrail"; import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; -import { config } from "./config"; +import { config } from "./contrail.config"; const DATABASE_URL = process.env.DATABASE_URL ?? diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/create.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/create.json deleted file mode 100644 index 9b4d211..0000000 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/create.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "lexicon": 1, - "id": "rsvp.atmo.community.invite.create", - "defs": { - "main": { - "type": "procedure", - "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "spaceUri", - "accessLevel" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "accessLevel": { - "type": "ref", - "ref": "rsvp.atmo.community.defs#accessLevel" - }, - "expiresAt": { - "type": "integer", - "description": "Unix ms. Omit for no expiry." - }, - "maxUses": { - "type": "integer", - "minimum": 1, - "description": "Omit for unlimited redemptions." - }, - "note": { - "type": "string", - "maxLength": 500 - } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "token", - "tokenHash", - "spaceUri", - "accessLevel", - "createdAt" - ], - "properties": { - "token": { - "type": "string", - "description": "Raw token. Shown once — cannot be retrieved later." - }, - "tokenHash": { - "type": "string" - }, - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "accessLevel": { - "type": "ref", - "ref": "rsvp.atmo.community.defs#accessLevel" - }, - "expiresAt": { - "type": "integer" - }, - "maxUses": { - "type": "integer" - }, - "createdAt": { - "type": "integer" - } - } - } - }, - "errors": [ - { - "name": "InvalidRequest" - }, - { - "name": "Forbidden" - }, - { - "name": "NotFound" - } - ] - } - } -} diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/list.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/list.json deleted file mode 100644 index 7bd0010..0000000 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/list.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "lexicon": 1, - "id": "rsvp.atmo.community.invite.list", - "defs": { - "main": { - "type": "query", - "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", - "parameters": { - "type": "params", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "includeRevoked": { - "type": "boolean", - "default": false - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "invites" - ], - "properties": { - "invites": { - "type": "array", - "items": { - "type": "ref", - "ref": "rsvp.atmo.community.defs#inviteView" - } - } - } - } - }, - "errors": [ - { - "name": "Forbidden" - }, - { - "name": "NotFound" - } - ] - } - } -} diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/revoke.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/revoke.json deleted file mode 100644 index c7aa335..0000000 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/revoke.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "lexicon": 1, - "id": "rsvp.atmo.community.invite.revoke", - "defs": { - "main": { - "type": "procedure", - "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "tokenHash" - ], - "properties": { - "tokenHash": { - "type": "string" - } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "ok" - ], - "properties": { - "ok": { - "type": "boolean" - } - } - } - }, - "errors": [ - { - "name": "NotFound" - }, - { - "name": "Forbidden" - } - ] - } - } -} diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/whoami.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/whoami.json deleted file mode 100644 index 524345d..0000000 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/whoami.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "lexicon": 1, - "id": "rsvp.atmo.community.space.whoami", - "defs": { - "main": { - "type": "query", - "description": "Report the JWT issuer's effective access level in a space (resolving delegated memberships).", - "parameters": { - "type": "params", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "accessLevel": { - "type": "ref", - "ref": "rsvp.atmo.community.defs#accessLevel", - "description": "Null when caller has no resolvable access." - } - } - } - } - } - } -} diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json deleted file mode 100644 index b0fa07e..0000000 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "lexicon": 1, - "id": "rsvp.atmo.spaceExt.invite.redeem", - "defs": { - "main": { - "type": "procedure", - "description": "Redeem an invite token. The JWT issuer becomes a member of the space.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" - } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - } - } - } - }, - "errors": [ - { - "name": "InvalidInvite" - } - ] - } - } -} diff --git a/apps/rsvp-atmo/lexicons/README.md b/apps/rsvp-atmo/lexicons/README.md deleted file mode 100644 index 8534000..0000000 --- a/apps/rsvp-atmo/lexicons/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Custom lexicons - -Place your own hand-authored lexicon JSON files here. - -## Folder conventions - -- **`lexicons/`** (this folder) — Your own custom lexicons. Hand-authored. -- **`lexicons-pulled/`** — Downloaded via `lex-cli pull` from the atproto registry. - Generated from the `pull.sources` list in `lex.config.js`. -- **`lexicons-generated/`** — Auto-generated by `pnpm generate` from `app/config.ts`. - Don't edit by hand. -- **`lexicon-templates/{spaces,community,realtime}/`** — Shipped library templates; - the generator instantiates them under `.{space,community,realtime}.*` into - `lexicons-generated/`. `spaces/ext/` also emits `.spaceExt.*` (contrail - extras — invites, whoami — outside the permissioned-data spec). - -All three (`lexicons/`, `lexicons-pulled/`, `lexicons-generated/`) are picked up by -`lex-cli generate` (see `lex.config.js` `files` glob). diff --git a/apps/rsvp-atmo/lexicons/custom/README.md b/apps/rsvp-atmo/lexicons/custom/README.md new file mode 100644 index 0000000..dc7779b --- /dev/null +++ b/apps/rsvp-atmo/lexicons/custom/README.md @@ -0,0 +1,13 @@ +# Custom lexicons + +Place your own hand-authored lexicon JSON files here. + +## Folder conventions + +- **`lexicons/custom/`** (this folder) — Your own hand-authored lexicons. +- **`lexicons/pulled/`** — Downloaded via `lex-cli pull` from the atproto registry. + Generated from the `pull.sources` list in `lex.config.js`. +- **`lexicons/generated/`** — Auto-generated by `pnpm generate` from your Contrail config. + Don't edit by hand. + +All three are picked up by `lex-cli generate` (see `lex.config.js` `files` glob). diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/adopt.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/adopt.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/adopt.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/adopt.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/defs.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/defs.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/defs.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/defs.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/delete.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/delete.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/delete.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/delete.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/deleteRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/deleteRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/deleteRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/deleteRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/getHealth.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/getHealth.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/getHealth.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/getHealth.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/list.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/list.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/list.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/list.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/mint.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/mint.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/mint.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/mint.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/putRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/putRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/putRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/putRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/reauth.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/reauth.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/reauth.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/reauth.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/create.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/create.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/create.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/create.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/delete.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/delete.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/delete.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/delete.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/deleteRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/deleteRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/grant.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/grant.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/grant.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/grant.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/listMembers.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/listMembers.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/listMembers.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/listMembers.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/putRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/putRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/putRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/putRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/resync.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/resync.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/resync.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/resync.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/revoke.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/revoke.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/revoke.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/revoke.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/setAccessLevel.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/community/space/setAccessLevel.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/getRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/event/getRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/getRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/event/getRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/listRecords.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/event/listRecords.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/listRecords.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/event/listRecords.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getCursor.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getCursor.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getCursor.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getCursor.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getOverview.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getOverview.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getOverview.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getOverview.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getProfile.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getProfile.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getProfile.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/getProfile.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/create.json similarity index 59% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/create.json index b604074..9858929 100644 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json +++ b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/create.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "rsvp.atmo.spaceExt.invite.create", + "id": "rsvp.atmo.invite.create", "defs": { "main": { "type": "procedure", - "description": "Create an invite for a space. Caller must be the space owner. Returns the raw token once; only the hash is stored.", + "description": "Create an invite for a space. The service dispatches on space ownership: user-owned spaces take `kind` (default `join`); community-owned spaces take `accessLevel`. Exactly one of `kind` / `accessLevel` must be set. Returns the raw token once; only the hash is stored.", "input": { "encoding": "application/json", "schema": { @@ -24,8 +24,17 @@ "read", "read-join" ], - "default": "join", - "description": "join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." + "description": "For user-owned spaces. join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." + }, + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "For community-owned spaces. The access level granted on redemption — the creator's own level caps what they can grant." }, "expiresAt": { "type": "integer", @@ -33,8 +42,7 @@ }, "maxUses": { "type": "integer", - "minimum": 1, - "description": "Caps join redemptions only — read-token reads are unlimited. Omit for unlimited joins." + "minimum": 1 }, "note": { "type": "string", @@ -58,7 +66,7 @@ }, "invite": { "type": "ref", - "ref": "rsvp.atmo.space.defs#inviteView" + "ref": "rsvp.atmo.invite.defs#inviteView" } } } @@ -69,6 +77,9 @@ }, { "name": "Forbidden" + }, + { + "name": "InvalidRequest" } ] } diff --git a/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/defs.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/defs.json new file mode 100644 index 0000000..050b3a2 --- /dev/null +++ b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/defs.json @@ -0,0 +1,73 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.invite.defs", + "defs": { + "inviteView": { + "type": "object", + "description": "An invite row as exposed to clients. Either `kind` (user-owned space) or `accessLevel` (community-owned space) is set, never both.", + "required": [ + "tokenHash", + "spaceUri", + "createdBy", + "createdAt", + "usedCount" + ], + "properties": { + "tokenHash": { + "type": "string", + "description": "Stable identifier for list/revoke operations." + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "kind": { + "type": "string", + "knownValues": [ + "join", + "read", + "read-join" + ], + "description": "Set for user-owned spaces. Absent for community-owned." + }, + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "Set for community-owned spaces. Absent for user-owned." + }, + "createdBy": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer", + "description": "Unix ms." + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms. Omitted for no expiry." + }, + "maxUses": { + "type": "integer", + "minimum": 1 + }, + "usedCount": { + "type": "integer" + }, + "revokedAt": { + "type": "integer", + "description": "Unix ms. Omitted if not revoked." + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + } + } +} diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/list.json similarity index 81% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/list.json index 304be52..2e4f6f7 100644 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json +++ b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/list.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "rsvp.atmo.spaceExt.invite.list", + "id": "rsvp.atmo.invite.list", "defs": { "main": { "type": "query", - "description": "List invites for a space. Owner only.", + "description": "List invites for a space. User-owned spaces: owner-only. Community-owned spaces: manager+.", "parameters": { "type": "params", "required": [ @@ -33,7 +33,7 @@ "type": "array", "items": { "type": "ref", - "ref": "rsvp.atmo.space.defs#inviteView" + "ref": "rsvp.atmo.invite.defs#inviteView" } } } diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/redeem.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/redeem.json similarity index 58% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/redeem.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/redeem.json index 0995e9b..6968ec1 100644 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/redeem.json +++ b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/redeem.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "rsvp.atmo.community.invite.redeem", + "id": "rsvp.atmo.invite.redeem", "defs": { "main": { "type": "procedure", - "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", + "description": "Redeem an invite token. User-owned spaces: caller becomes a member. Community-owned spaces: caller is granted the invite's access level.", "input": { "encoding": "application/json", "schema": { @@ -24,32 +24,32 @@ "schema": { "type": "object", "required": [ - "spaceUri", - "accessLevel", - "communityDid" + "spaceUri" ], "properties": { "spaceUri": { "type": "string", "format": "at-uri" }, + "kind": { + "type": "string", + "description": "Set for user-owned spaces — echoes the invite kind consumed." + }, "accessLevel": { - "type": "ref", - "ref": "rsvp.atmo.community.defs#accessLevel" + "type": "string", + "description": "Set for community-owned spaces — the level granted." }, "communityDid": { "type": "string", - "format": "did" + "format": "did", + "description": "Set for community-owned spaces." } } } }, "errors": [ { - "name": "InvalidRequest" - }, - { - "name": "InvalidToken" + "name": "InvalidInvite" }, { "name": "NotFound" diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/revoke.json similarity index 70% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/revoke.json index 40c4293..b5c97d5 100644 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json +++ b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/invite/revoke.json @@ -1,22 +1,22 @@ { "lexicon": 1, - "id": "rsvp.atmo.spaceExt.invite.revoke", + "id": "rsvp.atmo.invite.revoke", "defs": { "main": { "type": "procedure", - "description": "Revoke an invite. Owner only. Invites are identified by their tokenHash (visible in list output).", + "description": "Revoke an invite by tokenHash. User-owned spaces: owner-only. Community-owned spaces: invite creator OR manager+ on the target space.", "input": { "encoding": "application/json", "schema": { "type": "object", "required": [ - "spaceUri", "tokenHash" ], "properties": { "spaceUri": { "type": "string", - "format": "at-uri" + "format": "at-uri", + "description": "Optional — ownership is inferred from the invite row; required for user-owned spaces for a sanity check." }, "tokenHash": { "type": "string" diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/notifyOfUpdate.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/notifyOfUpdate.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/notifyOfUpdate.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/notifyOfUpdate.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/permissionSet.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/permissionSet.json similarity index 83% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/permissionSet.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/permissionSet.json index 5fae345..30572a6 100644 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/permissionSet.json +++ b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/permissionSet.json @@ -16,10 +16,6 @@ "rsvp.atmo.community.delete", "rsvp.atmo.community.deleteRecord", "rsvp.atmo.community.getHealth", - "rsvp.atmo.community.invite.create", - "rsvp.atmo.community.invite.list", - "rsvp.atmo.community.invite.redeem", - "rsvp.atmo.community.invite.revoke", "rsvp.atmo.community.list", "rsvp.atmo.community.mint", "rsvp.atmo.community.putRecord", @@ -33,12 +29,15 @@ "rsvp.atmo.community.space.resync", "rsvp.atmo.community.space.revoke", "rsvp.atmo.community.space.setAccessLevel", - "rsvp.atmo.community.space.whoami", "rsvp.atmo.event.getRecord", "rsvp.atmo.event.listRecords", "rsvp.atmo.getCursor", "rsvp.atmo.getOverview", "rsvp.atmo.getProfile", + "rsvp.atmo.invite.create", + "rsvp.atmo.invite.list", + "rsvp.atmo.invite.redeem", + "rsvp.atmo.invite.revoke", "rsvp.atmo.notifyOfUpdate", "rsvp.atmo.rsvp.getRecord", "rsvp.atmo.rsvp.listRecords", @@ -56,10 +55,6 @@ "rsvp.atmo.space.putRecord", "rsvp.atmo.space.removeMember", "rsvp.atmo.space.uploadBlob", - "rsvp.atmo.spaceExt.invite.create", - "rsvp.atmo.spaceExt.invite.list", - "rsvp.atmo.spaceExt.invite.redeem", - "rsvp.atmo.spaceExt.invite.revoke", "rsvp.atmo.spaceExt.whoami" ] } diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/getRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/rsvp/getRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/getRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/rsvp/getRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/listRecords.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/rsvp/listRecords.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/listRecords.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/rsvp/listRecords.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/addMember.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/addMember.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/addMember.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/addMember.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/createSpace.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/createSpace.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/createSpace.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/createSpace.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/defs.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/defs.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/defs.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/defs.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/deleteRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/deleteRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/deleteRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/deleteRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getBlob.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getBlob.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getBlob.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getBlob.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getSpace.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getSpace.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getSpace.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/getSpace.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/leaveSpace.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/leaveSpace.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/leaveSpace.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/leaveSpace.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listBlobs.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listBlobs.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listBlobs.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listBlobs.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listMembers.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listMembers.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listMembers.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listMembers.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listRecords.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listRecords.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listRecords.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listRecords.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listSpaces.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listSpaces.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listSpaces.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/listSpaces.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/putRecord.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/putRecord.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/putRecord.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/putRecord.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/removeMember.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/removeMember.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/removeMember.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/removeMember.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/uploadBlob.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/uploadBlob.json similarity index 100% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/uploadBlob.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/space/uploadBlob.json diff --git a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/whoami.json b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/spaceExt/whoami.json similarity index 57% rename from apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/whoami.json rename to apps/rsvp-atmo/lexicons/generated/rsvp/atmo/spaceExt/whoami.json index 0f5d078..236d190 100644 --- a/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/whoami.json +++ b/apps/rsvp-atmo/lexicons/generated/rsvp/atmo/spaceExt/whoami.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Report the caller's relationship to a space: whether they are the owner and/or a member. Useful for clients to avoid a listMembers roundtrip.", + "description": "Caller's relationship to a space. For user-owned spaces membership is binary (`isMember`). For community-owned spaces `accessLevel` is also returned, resolved through the community's access-level ladder (including delegated grants).", "parameters": { "type": "params", "required": [ @@ -31,6 +31,16 @@ }, "isMember": { "type": "boolean" + }, + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "Only set for community-owned spaces. Null when the caller has no resolvable access." } } } diff --git a/apps/rsvp-atmo/lexicons-pulled/README.md b/apps/rsvp-atmo/lexicons/pulled/README.md similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/README.md rename to apps/rsvp-atmo/lexicons/pulled/README.md diff --git a/apps/rsvp-atmo/lexicons-pulled/app/bsky/actor/profile.json b/apps/rsvp-atmo/lexicons/pulled/app/bsky/actor/profile.json similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/app/bsky/actor/profile.json rename to apps/rsvp-atmo/lexicons/pulled/app/bsky/actor/profile.json diff --git a/apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/event.json b/apps/rsvp-atmo/lexicons/pulled/community/lexicon/calendar/event.json similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/event.json rename to apps/rsvp-atmo/lexicons/pulled/community/lexicon/calendar/event.json diff --git a/apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/rsvp.json b/apps/rsvp-atmo/lexicons/pulled/community/lexicon/calendar/rsvp.json similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/rsvp.json rename to apps/rsvp-atmo/lexicons/pulled/community/lexicon/calendar/rsvp.json diff --git a/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/address.json b/apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/address.json similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/address.json rename to apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/address.json diff --git a/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/fsq.json b/apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/fsq.json similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/fsq.json rename to apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/fsq.json diff --git a/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/geo.json b/apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/geo.json similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/geo.json rename to apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/geo.json diff --git a/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/hthree.json b/apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/hthree.json similarity index 100% rename from apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/hthree.json rename to apps/rsvp-atmo/lexicons/pulled/community/lexicon/location/hthree.json diff --git a/apps/rsvp-atmo/package.json b/apps/rsvp-atmo/package.json index 8503756..7c57671 100644 --- a/apps/rsvp-atmo/package.json +++ b/apps/rsvp-atmo/package.json @@ -10,7 +10,7 @@ "deploy": "wrangler deploy", "clean": "tsx scripts/clean.ts", "generate": "tsx src/generate.ts", - "generate:pull": "contrail-lex all --config src/config.ts", + "generate:pull": "contrail-lex all", "publish-lexicons": "tsx scripts/publish-lexicons.ts", "typecheck": "tsc --noEmit", "ingest": "curl -s http://localhost:8787/__scheduled?cron=*/1+*+*+*+*", diff --git a/apps/rsvp-atmo/scripts/clean.ts b/apps/rsvp-atmo/scripts/clean.ts index 144384d..7345b4c 100644 --- a/apps/rsvp-atmo/scripts/clean.ts +++ b/apps/rsvp-atmo/scripts/clean.ts @@ -13,8 +13,8 @@ const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const targets = [ ".wrangler", "lex.config.js", - "lexicons-pulled", - "lexicons-generated", + "lexicons/pulled", + "lexicons/generated", "src/lexicon-types", ]; diff --git a/apps/rsvp-atmo/scripts/publish-lexicons.ts b/apps/rsvp-atmo/scripts/publish-lexicons.ts index f5909f9..88a7a5d 100644 --- a/apps/rsvp-atmo/scripts/publish-lexicons.ts +++ b/apps/rsvp-atmo/scripts/publish-lexicons.ts @@ -1,6 +1,6 @@ /** * CLI wrapper for `publishLexicons` — publishes contrail's own generated - * lexicons (under `lexicons-generated/`) to the logged-in account's PDS. + * lexicons (under `lexicons/generated/`) to the logged-in account's PDS. * * Usage: * LEXICON_ACCOUNT_IDENTIFIER=you.bsky.social \ @@ -30,7 +30,7 @@ async function main(): Promise { } await publishLexicons({ - generatedDir: join(ROOT, "lexicons-generated"), + generatedDir: join(ROOT, "lexicons", "generated"), identifier, password, }); diff --git a/apps/rsvp-atmo/src/config.ts b/apps/rsvp-atmo/src/contrail.config.ts similarity index 100% rename from apps/rsvp-atmo/src/config.ts rename to apps/rsvp-atmo/src/contrail.config.ts diff --git a/apps/rsvp-atmo/src/generate.ts b/apps/rsvp-atmo/src/generate.ts index 2239922..f06f1c7 100644 --- a/apps/rsvp-atmo/src/generate.ts +++ b/apps/rsvp-atmo/src/generate.ts @@ -5,7 +5,7 @@ */ import { join, dirname } from "path"; import { fileURLToPath } from "url"; -import { config } from "./config"; +import { config } from "./contrail.config"; import { generateLexicons } from "@atmo-dev/contrail-lexicons"; const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), ".."); @@ -13,6 +13,6 @@ const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), ".."); generateLexicons({ config, rootDir: ROOT_DIR, - outputDir: join(ROOT_DIR, "lexicons-generated"), + outputDir: join(ROOT_DIR, "lexicons", "generated"), writeRuntimeFiles: true, }); diff --git a/apps/rsvp-atmo/src/sync.ts b/apps/rsvp-atmo/src/sync.ts index bf25bc9..6fda0d9 100644 --- a/apps/rsvp-atmo/src/sync.ts +++ b/apps/rsvp-atmo/src/sync.ts @@ -6,7 +6,7 @@ * pnpm sync --remote # prod D1 */ import { Contrail } from "@atmo-dev/contrail"; -import { config } from "./config"; +import { config } from "./contrail.config"; import { getPlatformProxy } from "wrangler"; function elapsed(start: number): string { diff --git a/apps/rsvp-atmo/src/worker.ts b/apps/rsvp-atmo/src/worker.ts index b046d44..c0322c6 100644 --- a/apps/rsvp-atmo/src/worker.ts +++ b/apps/rsvp-atmo/src/worker.ts @@ -1,6 +1,6 @@ import { Contrail } from "@atmo-dev/contrail"; import { createHandler } from "@atmo-dev/contrail/server"; -import { config } from "./config"; +import { config } from "./contrail.config"; const contrail = new Contrail(config); const handle = createHandler(contrail); diff --git a/apps/sveltekit-cloudflare-workers/README.md b/apps/sveltekit-cloudflare-workers/README.md index 4917318..50917ff 100644 --- a/apps/sveltekit-cloudflare-workers/README.md +++ b/apps/sveltekit-cloudflare-workers/README.md @@ -13,7 +13,7 @@ Dev mode uses a loopback OAuth client — no keys or Cloudflare setup needed. ## Config -Define which AT Protocol collections to index in `src/lib/contrail/config.ts`: +Define which AT Protocol collections to index in `src/lib/contrail.config.ts`: ```ts import type { ContrailConfig } from '@atmo-dev/contrail'; diff --git a/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getCursor.json b/apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/getCursor.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getCursor.json rename to apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/getCursor.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getOverview.json b/apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/getOverview.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getOverview.json rename to apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/getOverview.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json b/apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/getProfile.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json rename to apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/getProfile.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/notifyOfUpdate.json b/apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/notifyOfUpdate.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/notifyOfUpdate.json rename to apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/notifyOfUpdate.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json b/apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/permissionSet.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json rename to apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/permissionSet.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json b/apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/status/getRecord.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json rename to apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/status/getRecord.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json b/apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/status/listRecords.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json rename to apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/status/listRecords.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-pulled/README.md b/apps/sveltekit-cloudflare-workers/lexicons/pulled/README.md similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-pulled/README.md rename to apps/sveltekit-cloudflare-workers/lexicons/pulled/README.md diff --git a/apps/sveltekit-cloudflare-workers/lexicons-pulled/app/bsky/actor/profile.json b/apps/sveltekit-cloudflare-workers/lexicons/pulled/app/bsky/actor/profile.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-pulled/app/bsky/actor/profile.json rename to apps/sveltekit-cloudflare-workers/lexicons/pulled/app/bsky/actor/profile.json diff --git a/apps/sveltekit-cloudflare-workers/lexicons-pulled/xyz/statusphere/status.json b/apps/sveltekit-cloudflare-workers/lexicons/pulled/xyz/statusphere/status.json similarity index 100% rename from apps/sveltekit-cloudflare-workers/lexicons-pulled/xyz/statusphere/status.json rename to apps/sveltekit-cloudflare-workers/lexicons/pulled/xyz/statusphere/status.json diff --git a/apps/sveltekit-cloudflare-workers/scripts/generate.ts b/apps/sveltekit-cloudflare-workers/scripts/generate.ts index 6dd6fde..b951669 100644 --- a/apps/sveltekit-cloudflare-workers/scripts/generate.ts +++ b/apps/sveltekit-cloudflare-workers/scripts/generate.ts @@ -1,6 +1,6 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import { config } from '../src/lib/contrail/config'; +import { config } from '../src/lib/contrail.config'; import { generateLexicons } from '@atmo-dev/contrail-lexicons'; const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); @@ -8,6 +8,6 @@ const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); generateLexicons({ config, rootDir: ROOT_DIR, - outputDir: join(ROOT_DIR, 'lexicons-generated'), + outputDir: join(ROOT_DIR, 'lexicons', 'generated'), writeRuntimeFiles: true }); diff --git a/apps/sveltekit-cloudflare-workers/scripts/sync.ts b/apps/sveltekit-cloudflare-workers/scripts/sync.ts index c946f86..c1f672f 100644 --- a/apps/sveltekit-cloudflare-workers/scripts/sync.ts +++ b/apps/sveltekit-cloudflare-workers/scripts/sync.ts @@ -6,7 +6,7 @@ * pnpm sync:remote # prod D1 */ import { Contrail } from '@atmo-dev/contrail'; -import { config } from '../src/lib/contrail/config'; +import { config } from '../src/lib/contrail.config'; import { getPlatformProxy } from 'wrangler'; function elapsed(start: number): string { diff --git a/apps/sveltekit-cloudflare-workers/src/lib/contrail/config.ts b/apps/sveltekit-cloudflare-workers/src/lib/contrail.config.ts similarity index 100% rename from apps/sveltekit-cloudflare-workers/src/lib/contrail/config.ts rename to apps/sveltekit-cloudflare-workers/src/lib/contrail.config.ts diff --git a/apps/sveltekit-cloudflare-workers/src/lib/contrail/index.ts b/apps/sveltekit-cloudflare-workers/src/lib/contrail/index.ts index 7aaf747..29f4474 100644 --- a/apps/sveltekit-cloudflare-workers/src/lib/contrail/index.ts +++ b/apps/sveltekit-cloudflare-workers/src/lib/contrail/index.ts @@ -1,7 +1,7 @@ import { Contrail } from '@atmo-dev/contrail'; import { createHandler, createServerClient } from '@atmo-dev/contrail/server'; import type { Client } from '@atcute/client'; -import { config } from './config'; +import { config } from '../contrail.config'; export const contrail = new Contrail(config); diff --git a/docs/01-indexing.md b/docs/01-indexing.md index 9f22c70..0a811e8 100644 --- a/docs/01-indexing.md +++ b/docs/01-indexing.md @@ -4,24 +4,33 @@ Contrail's core job: mirror atproto records into your DB and expose them via XRP ## Collection shape +A realistic two-collection example: events and RSVPs. RSVPs point at events via `subject.uri`; events expose per-status RSVP counts. + ```ts collections: { event: { collection: "community.lexicon.calendar.event", // full NSID queryable: { - mode: {}, // ?mode=online - startsAt: { type: "range" }, // ?startsAtMin=...&startsAtMax=... + mode: {}, // ?mode=online + startsAt: { type: "range" }, // ?startsAtMin=...&startsAtMax=... }, searchable: ["name", "description"], // FTS5 / tsvector relations: { rsvps: { - collection: "rsvp", - groupBy: "status", - groups: { going: "community.lexicon.calendar.rsvp#going" }, + collection: "rsvp", // short name of the child collection + groupBy: "status", // field on the child record + groups: { + going: "community.lexicon.calendar.rsvp#going", + interested: "community.lexicon.calendar.rsvp#interested", + }, }, }, + }, + rsvp: { + collection: "community.lexicon.calendar.rsvp", + queryable: { status: {} }, references: { - event: { collection: "event", field: "subject.uri" }, + event: { collection: "event", field: "subject.uri" }, // RSVP's field → event's URI }, }, } @@ -29,45 +38,182 @@ collections: { - **queryable** — string equality or range, exposed as query params. - **searchable** — FTS5 on D1/Postgres. Not available on `node:sqlite`. -- **relations** — materialized many-to-one counts (`rsvpsGoingCount`). -- **references** — forward lookups. Hydrate inline with `?hydrateEvent=true`. +- **relations** — many-to-one with materialized counts. The `event` collection gains `rsvpsCount`, `rsvpsGoingCount`, `rsvpsInterestedCount` columns — filter (`?rsvpsGoingCountMin=10`) and sort (`?sort=rsvpsGoingCount`) on them. Hydrate inline with `?hydrateRsvps=5`. +- **references** — forward lookups from child → parent. `?hydrateEvent=true` on an RSVP query embeds the referenced event record. + +## Backfill (historical data) + +Run once at setup to pull every record that exists today. + +```ts +await contrail.backfillAll({ concurrency: 100 }); // discover + backfill, logs progress +``` + +Under the hood this is two steps you can call separately if you want finer control: + +```ts +await contrail.discover(); // walk relays, register DIDs +await contrail.backfill({ concurrency: 100 }); // fetch history for registered DIDs +``` + +`backfill()` picks up where it left off across runs — safe to re-run. + +### Workers CLI + +For Cloudflare Workers deploys, `@atmo-dev/contrail` ships a `contrail` bin that handles the `wrangler.getPlatformProxy` dance — no script file, no package.json alias needed: + +```bash +pnpm contrail backfill # local D1 (wrangler dev's bindings) +pnpm contrail backfill --remote # production D1 +``` + +Auto-detects configs at `contrail.config.ts`, `src/contrail.config.ts`, `src/lib/contrail.config.ts`, or `app/contrail.config.ts` (first match wins). Override with `--config `. Other flags: `--binding ` (default `DB`), `--concurrency ` (default 100). + +If you'd rather embed backfill inside your own script, `@atmo-dev/contrail/workers` exports the same logic as a function: + +```ts +import { backfillAll } from "@atmo-dev/contrail/workers"; +import { config } from "../src/contrail.config"; + +await backfillAll({ config, remote: process.argv.includes("--remote") }); +``` + +For node/postgres deploys, skip both — you already have a `db` in hand; just `await contrail.backfillAll({}, db)` directly. + +## Ingestion (ongoing new records) + +After the initial `backfillAll()`, keep the index fresh with new records as they're published. Pick the mode that matches your runtime. + +### Cron-driven (cloudflare workers) + +Workers can't hold long-lived connections, so run one catch-up cycle per cron fire: + +```ts +// wrangler.jsonc: "triggers": { "crons": ["*/1 * * * *"] } +async scheduled(_ev, env, ctx) { + ctx.waitUntil(contrail.ingest({}, env.DB)); +} +``` -## Ingestion +`ingest()` connects to Jetstream, streams events since the saved cursor, stops when caught up. Running every minute is fine — the next fire resumes where this one left off. Each cycle is bounded, so it can't blow past the Worker time limit. -Three ways records land in the DB: +### Persistent (node / any long-lived server) + +If your runtime can keep a socket open, skip the cron entirely: ```ts -await contrail.ingest(); // one Jetstream cycle, then stops -await contrail.runPersistent(); // long-lived connection, auto-reconnect -await contrail.notify(uri); // immediate PDS fetch for one record +const ac = new AbortController(); +await contrail.runPersistent({ + batchSize: 50, // flush every N events (default: 50) + flushIntervalMs: 5000, // or every N ms, whichever first + signal: ac.signal, +}); +// ac.abort() flushes the current batch and saves the cursor before returning ``` -Call `notify()` after your app writes to a PDS and needs the change reflected now. Jetstream catches the same event later; the duplicate is detected by CID. +One process, one socket, auto-reconnect on drops. Lower latency than cron mode (events land within seconds instead of up-to-a-minute), but needs a runtime that can run indefinitely. -## Discovery + backfill +### Immediate (`notify()`) + +Use this when your own app writes to a user's PDS and needs the change indexed *now* — waiting for the next cron / Jetstream flush is too slow: ```ts -await contrail.discover(); // find users from relays -await contrail.backfill({ concurrency: 100 }); // pull their history -await contrail.sync(); // both +await contrail.notify(uri); // one record +await contrail.notify([u1, u2, u3]); // batch, up to 25 +``` + +Fetches directly from the user's PDS and indexes synchronously. When Jetstream later delivers the same event, the duplicate is detected by CID and skipped. + +### Which one do I use? + +| | backfillAll | ingest | runPersistent | notify | +|---|---|---|---|---| +| when | once, at setup | every cron fire | start once, runs forever | per-write, on demand | +| runtime | local script | cloudflare workers | node / long-lived server | anywhere | +| scope | all historical records | events since last cursor | events since last cursor, live | specific URIs | +| latency | — | ~minute | ~seconds | immediate | + +Typical combos: +- **workers app:** `backfillAll()` once + `ingest()` on cron + optional `notify()` for self-writes +- **node server:** `backfillAll()` once + `runPersistent()` forever + optional `notify()` for self-writes + +## Refresh (catch-up after outages / dev idle) + +When Jetstream drops events — you went offline for a few days in dev, there was an outage, or you just want reassurance nothing was lost — `refresh` walks every known DID's PDS and reconciles against your DB: + +```bash +pnpm contrail refresh # totals only +pnpm contrail refresh --by-collection # totals + per-collection breakdown +pnpm contrail refresh --ignore-window 30 # grace seconds (default: 60) ``` +Each record is classified as: + +- **missing** — PDS has it, DB doesn't. Inserted. +- **stale update** — DB has it with a different CID, *and* the DB row is older than the ignore window. Upserted. +- **in sync** — same CID, or DB row is within the ignore window (jetstream probably just hadn't caught up yet). + +The ignore window is there so a refresh run seconds after a normal jetstream cycle doesn't double-count records that are about to sync anyway. Records inside the window are still written if they differ; they just don't show up in the stats. + +Report shape (`--by-collection`): + +``` +by collection: + community.lexicon.calendar.event + 3 missing, 1 stale updates, 842 in sync + community.lexicon.calendar.rsvp + 12 missing, 0 stale updates, 4108 in sync + +total: + 15 missing, 1 stale updates, 4950 in sync + 234 users scanned, 1 failed in 85.3s + (ignore window: 60s) +``` + +Safe to run repeatedly — each pass converges toward zero missing / stale. Programmatic equivalent: `contrail.refresh({ ignoreWindowMs, concurrency })` returns the same structure. + +Refresh is **not** a replacement for `ingest`/`runPersistent` — it walks every user's full history, which is expensive. Use it after outages or during dev idle, not as a continuous freshness mechanism. + ## Querying +### HTTP (what most callers use) + +Every config field gets a predictable URL param: + +``` +/xrpc/com.example.event.listRecords?mode=online&startsAtMin=2026-01-01&rsvpsGoingCountMin=10&sort=startsAt&order=asc&hydrateRsvps=5 +/xrpc/com.example.event.getRecord?uri=at://did:plc:.../...&hydrateRsvps=5 +``` + +| Config produces | URL param | +|---|---| +| `queryable: { field: {} }` | `?field=value` (equality) | +| `queryable: { field: { type: "range" } }` | `?fieldMin=…`, `?fieldMax=…` | +| `relations: { rel: {...} }` | `?relCountMin=N`, `?sort=relCount`, `?hydrateRel=N` | +| `relations: { rel: { groups: { going } } }` | `?relGoingCountMin=N`, `?sort=relGoingCount` | +| `references: { ref: {...} }` | `?hydrateRef=true` | + +Dotted field names become camelCase params — `queryable: { "subject.uri": {} }` → `?subjectUri=…`. + +### Programmatic + ```ts const { records, cursor } = await contrail.query("event", { filters: { mode: "online" }, - sort: { field: "startsAt", direction: "asc" }, + rangeFilters: { startsAt: { min: "2026-01-01" } }, + countFilters: { rsvp: 10 }, // keyed by child collection short name + sort: { recordField: "startsAt", direction: "asc" }, limit: 20, }); ``` -Or via HTTP once the handler is mounted: +The programmatic shape doesn't use the URL param names — keys are the underlying field/collection identifiers: -``` -/xrpc/com.example.event.listRecords?mode=online&sort=startsAt&limit=20 -/xrpc/com.example.event.getRecord?uri=at://did:plc:.../... -``` +- `filters` / `rangeFilters` — keyed by the field name from your config (`startsAt`, `subject.uri`), not the camelCased URL param. +- `countFilters` — keyed by the target collection's short name for totals, or by the full `nsid#group` token for group counts. E.g., `{ rsvp: 10 }` for "at least 10 RSVPs total," or `{ "community.lexicon.calendar.rsvp#going": 10 }` for "at least 10 going." +- `sort` — `{ recordField, direction }` for field sorts, `{ countType, direction }` for count sorts (where `countType` is the same collection-short-name or `nsid#group` as above). + +For count filters / sorts, the HTTP side is nicer than the programmatic side — consider going through `createHandler` + `fetch` even for in-process calls if you want the friendly names. ## Adapters @@ -91,6 +237,6 @@ const db = createPostgresDatabase(pool); | `jetstreams` | Bluesky | Jetstream URLs | | `relays` | Bluesky | Relay URLs for discovery | | `notify` | off | `true` opens `notifyOfUpdate`; a string requires `Bearer` | -| `spaces` | — | See [Spaces](./02-spaces.md) | -| `community` | — | See [Communities](./03-communities.md) | -| `realtime` | — | See [Sync](./04-sync.md) | +| `spaces` | — | See [Spaces](./03-spaces.md) | +| `community` | — | See [Communities](./04-communities.md) | +| `realtime` | — | See [Sync](./05-sync.md) | diff --git a/docs/02-lexicons.md b/docs/02-lexicons.md new file mode 100644 index 0000000..09cd550 --- /dev/null +++ b/docs/02-lexicons.md @@ -0,0 +1,80 @@ +# Lexicons + +Contrail emits lexicon JSON for every XRPC method it exposes. Your app needs those files for two things: **generating typed TypeScript clients** and **publishing to a PDS** so other apps can discover your schemas. The `contrail-lex` CLI handles both. + +```bash +pnpm add -D @atmo-dev/contrail-lexicons @atcute/lex-cli +``` + +`@atcute/lex-cli` is a peer dep — you pin the version. + +## CLI + +```bash +contrail-lex generate # emit lexicon JSON from your Contrail config +contrail-lex pull # wraps `lex-cli pull` (fetch external lexicons) +contrail-lex types # wraps `lex-cli generate` (JSON → TS types) +contrail-lex all # generate → pull → generate → pull → types +contrail-lex all --no-types # same, skip the type step +contrail-lex publish # publish lexicons to your PDS (add --dry-run to preview) +``` + +Config is auto-detected at `contrail.config.ts`, `src/contrail.config.ts`, `src/lib/contrail.config.ts`, or `app/contrail.config.ts`. Override with `--config `. + +## What lands where + +Running `contrail-lex all` organises everything under one `lexicons/` directory: + +``` +lexicons/ + custom/ # hand-authored lexicons you write (optional) + generated/ # JSON emitted from your Contrail config + pulled/ # external NSIDs fetched by lex-cli pull +lex.config.js # regenerated each run (gitignore) +src/lexicon-types/ # TS types from lex-cli generate +``` + +`lexicons/custom/`, `lexicons/generated/`, and `lexicons/pulled/` should be **committed** — that way CI doesn't need network access. `lex.config.js` and `src/lexicon-types/` are regenerated on demand and safe to gitignore. + +## Publishing + +Once your lexicons are generated and committed, publish them as `com.atproto.lexicon.schema` records on your PDS so other apps can resolve them: + +```bash +contrail-lex publish + +# or via env vars (nicer for CI): +LEXICON_ACCOUNT_IDENTIFIER=you.bsky.social \ +LEXICON_ACCOUNT_PASSWORD=xxxx-xxxx-xxxx-xxxx \ + contrail-lex publish +``` + +Writes each lexicon to `at:///com.atproto.lexicon.schema/`. You'll also need DNS TXT records for your NSID authorities — the command prints the exact records you need and won't proceed until you confirm (override with `--skip-confirm` in CI). + +Preview first with `--dry-run`: + +```bash +contrail-lex publish --dry-run +``` + +Walks your `lexicons/generated/` dir, prints the NSIDs it would publish and the exact TXT records you'd need. Doesn't log in or write anything. Credentials aren't required for dry-run, so it's safe to run without secrets configured — useful for sanity-checking what a release would push. + +**One-off flow:** generate once, commit, `contrail-lex publish --dry-run` to preview, `contrail-lex publish` once per version bump. You don't need to re-publish unless your lexicon JSON changes. + +## Programmatic API + +If you need to call the generator from code — e.g. to derive the XRPC method list for an OAuth permission set: + +```ts +import { generateLexicons, extractXrpcMethods } from "@atmo-dev/contrail-lexicons"; + +const generated = generateLexicons({ + config, + rootDir: process.cwd(), + outputDir: "lexicons/generated", +}); + +const methods = extractXrpcMethods(generated); // every query + procedure NSID +``` + +`publishLexicons` is also exported for custom flows, but the CLI covers the common case. diff --git a/docs/02-spaces.md b/docs/03-spaces.md similarity index 100% rename from docs/02-spaces.md rename to docs/03-spaces.md diff --git a/docs/03-communities.md b/docs/04-communities.md similarity index 97% rename from docs/03-communities.md rename to docs/04-communities.md index c07d517..fbfcfa5 100644 --- a/docs/03-communities.md +++ b/docs/04-communities.md @@ -1,6 +1,6 @@ # Communities -Group-controlled atproto DIDs. A community is a DID whose signing/rotation keys are held by the appview on behalf of multiple members, with tiered access levels. Built on top of [spaces](./02-spaces.md). +Group-controlled atproto DIDs. A community is a DID whose signing/rotation keys are held by the appview on behalf of multiple members, with tiered access levels. Built on top of [spaces](./03-spaces.md). ## When to use this diff --git a/docs/05-lexicons.md b/docs/05-lexicons.md deleted file mode 100644 index 32fba96..0000000 --- a/docs/05-lexicons.md +++ /dev/null @@ -1,91 +0,0 @@ -# Lexicons - -Contrail emits lexicon JSON for every XRPC method it exposes. Your app needs those JSON files for two things: publishing to your PDS (so other apps can discover them) and generating TypeScript types for typed clients. The `contrail-lex` CLI handles both. - -```bash -pnpm add -D @atmo-dev/contrail-lexicons @atcute/lex-cli -``` - -`@atcute/lex-cli` is a peer dep — you pin the version. - -## CLI - -```bash -contrail-lex generate # emit lexicon JSON from your Contrail config -contrail-lex pull # wraps `lex-cli pull` (fetches external lexicons) -contrail-lex types # wraps `lex-cli generate` (JSON → TS types) -contrail-lex all # generate → pull → generate → pull → types -contrail-lex all --no-types # skip the type step -``` - -The CLI auto-detects your config at `contrail.config.ts`, `app/config.ts`, or `src/lib/contrail/config.ts`. Override with `--config `. - -## What lands where - -Running `contrail-lex all` against a config with `namespace: "com.example"` writes: - -``` -lexicons-generated/ # JSON lexicons emitted from your config -lexicons-pulled/ # external NSIDs fetched by lex-cli pull -lex.config.js # regenerated each run (add to .gitignore) -src/lexicon-types/ # TS types emitted by lex-cli generate -``` - -`lexicons-generated/` and `lexicons-pulled/` should be **committed** — that way CI doesn't need network access and consumers of your repo can pull the JSON as a lexicon source. `lex.config.js` and `src/lexicon-types/` are generated on demand and safe to gitignore. - -## Publishing to a PDS - -Once committed, publish the lexicons to an atproto account so other apps can resolve them: - -```ts -import { publishLexicons } from "@atmo-dev/contrail-lexicons"; - -await publishLexicons({ - generatedDir: "lexicons-generated", - identifier: process.env.LEXICON_ACCOUNT_IDENTIFIER, - password: process.env.LEXICON_ACCOUNT_PASSWORD, -}); -``` - -Writes each lexicon as a `com.atproto.lexicon.schema` record under `at:///com.atproto.lexicon.schema/`. You'll also need DNS TXT records — publishing prints them for you. - -## Programmatic API - -```ts -import { generateLexicons, extractXrpcMethods } from "@atmo-dev/contrail-lexicons"; - -const generated = generateLexicons({ - config, - rootDir: process.cwd(), - outputDir: "lexicons-generated", -}); - -const methods = extractXrpcMethods(generated); // NSIDs of every query + procedure -``` - -Handy for emitting the method list for your OAuth permission set. - -## Consuming a third-party contrail instance - -Someone else runs a contrail deployment; you want typed client access to their XRPCs. Point `lex-cli pull` at their git repo: - -```js -// lex.config.js -import { defineLexiconConfig } from "@atcute/lex-cli"; - -export default defineLexiconConfig({ - outdir: "src/lexicon-types/", - imports: ["@atcute/atproto"], - files: ["lexicons/**/*.json"], - pull: { - outdir: "lexicons/", - sources: [{ - type: "git", - remote: "https://github.com/them/their-contrail.git", - pattern: ["lexicons-generated/**/*.json"], - }], - }, -}); -``` - -Then `npx lex-cli pull && npx lex-cli generate`, and their endpoints are typed in your client. diff --git a/docs/04-sync.md b/docs/05-sync.md similarity index 100% rename from docs/04-sync.md rename to docs/05-sync.md diff --git a/packages/contrail/README.md b/packages/contrail/README.md index 4e95bd6..6754a8d 100644 --- a/packages/contrail/README.md +++ b/packages/contrail/README.md @@ -104,8 +104,8 @@ await contrail.discover(); // Backfill their records from PDS await contrail.backfill({ concurrency: 100 }); -// Or both in one call -await contrail.sync({ concurrency: 100 }); +// Or both in one call — logs progress via config.logger +await contrail.backfillAll({ concurrency: 100 }); ``` ### Notify of immediate updates diff --git a/packages/contrail/package.json b/packages/contrail/package.json index de960fa..7253821 100644 --- a/packages/contrail/package.json +++ b/packages/contrail/package.json @@ -26,8 +26,19 @@ "./postgres": { "types": "./dist/adapters/postgres.d.ts", "import": "./dist/adapters/postgres.js" + }, + "./workers": { + "types": "./dist/workers/backfill.d.ts", + "import": "./dist/workers/backfill.js" + }, + "./cli-config": { + "types": "./dist/cli-config.d.ts", + "import": "./dist/cli-config.js" } }, + "bin": { + "contrail": "./dist/cli.js" + }, "repository": { "type": "git", "url": "https://github.com/flo-bit/contrail.git", @@ -57,7 +68,8 @@ "@atcute/jetstream": "^1.0.2", "@atcute/lexicons": "^1.2.9", "@atcute/xrpc-server": "^0.1.12", - "hono": "^4.12.8" + "hono": "^4.12.8", + "jiti": "^2.4.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250124.0", @@ -66,14 +78,19 @@ "pg": "^8.20.0", "tsup": "^8.5.0", "typescript": "^5.7.3", - "vitest": "^4.1.0" + "vitest": "^4.1.0", + "wrangler": "^4.63.0" }, "peerDependencies": { - "pg": "^8.0.0" + "pg": "^8.0.0", + "wrangler": "^4.0.0" }, "peerDependenciesMeta": { "pg": { "optional": true + }, + "wrangler": { + "optional": true } }, "license": "MIT" diff --git a/packages/contrail/src/cli-config.ts b/packages/contrail/src/cli-config.ts new file mode 100644 index 0000000..6482319 --- /dev/null +++ b/packages/contrail/src/cli-config.ts @@ -0,0 +1,58 @@ +/** + * Shared config-file discovery + loading for the `contrail` and + * `contrail-lex` CLIs. Exported so downstream CLIs can use the same + * auto-detect convention and fall back to the same list of locations. + * + * Not for runtime use by regular contrail apps — they pass `config` + * directly to `new Contrail(...)`. This module exists only to support + * CLI tooling that has to discover + load a TS/JS config file off disk. + */ +import { existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { createJiti } from "jiti"; +import type { ContrailConfig } from "./core/types.js"; + +/** Auto-detect locations (first match wins). Keep this list small and stable — + * every CLI in the ecosystem walks it, so additions are cross-cutting. */ +export const CONFIG_CANDIDATES: readonly string[] = [ + "contrail.config.ts", + "contrail.config.js", + "src/contrail.config.ts", + "src/contrail.config.js", + "src/lib/contrail.config.ts", + "src/lib/contrail.config.js", + "app/contrail.config.ts", + "app/contrail.config.js", +]; + +/** Human-readable summary for CLI error messages. */ +export const CONFIG_CANDIDATES_MESSAGE = + "contrail.config.ts | src/contrail.config.ts | src/lib/contrail.config.ts | app/contrail.config.ts"; + +/** Find a config file by walking the auto-detect list relative to `root`. + * When `explicit` is passed, resolves that (relative to `root`) and checks + * existence — no auto-detection. Returns `null` if nothing found. */ +export function findConfigFile(root: string, explicit?: string): string | null { + if (explicit) { + const p = resolve(root, explicit); + return existsSync(p) ? p : null; + } + for (const c of CONFIG_CANDIDATES) { + const p = join(root, c); + if (existsSync(p)) return p; + } + return null; +} + +/** Load a config file via jiti — handles TS + ESM + CJS transparently, + * no tsx/ts-node hook required. Accepts either a named export `config` or + * a default export. Throws if neither is present. */ +export async function loadConfig(path: string): Promise { + const jiti = createJiti(import.meta.url, { interopDefault: true }); + const mod = (await jiti.import(path)) as { config?: unknown; default?: unknown }; + const config = mod.config ?? mod.default; + if (!config || typeof config !== "object") { + throw new Error(`Config at ${path} did not export a \`config\` object`); + } + return config as T; +} diff --git a/packages/contrail/src/cli.ts b/packages/contrail/src/cli.ts new file mode 100644 index 0000000..4f98dfb --- /dev/null +++ b/packages/contrail/src/cli.ts @@ -0,0 +1,170 @@ +#!/usr/bin/env node +/** + * contrail — CLI for one-off operations against a contrail deployment. + * + * Usage: + * contrail backfill [--remote] [--binding DB] [--config path] + * contrail refresh [--remote] [--ignore-window 60s] [--by-collection] + * + * Config auto-detects at contrail.config.ts, src/contrail.config.ts, + * src/lib/contrail.config.ts, or app/contrail.config.ts (first match wins). + */ +import { findConfigFile, loadConfig, CONFIG_CANDIDATES_MESSAGE } from "./cli-config.js"; +import { backfillAll, refresh } from "./workers/backfill.js"; +import type { CollectionStats, RefreshResult } from "./core/refresh.js"; + +type Subcommand = "backfill" | "refresh" | "help"; + +const USAGE = `contrail [options] + +Subcommands: + backfill One-time bulk load from each known DID's PDS (resumable) + refresh Fresh sweep: reconcile PDS vs DB, report missing + stale + help Print this message + +Options (backfill): + --config Path to Contrail config file (TS or JS). + --root Project root for auto-detection. Default: CWD. + --remote Use production D1 bindings. + --binding D1 binding name in wrangler.jsonc. Default: "DB". + --concurrency Passed to contrail.backfillAll(). Default: 100. + +Options (refresh): + --config Path to Contrail config file (TS or JS). + --root Project root for auto-detection. Default: CWD. + --remote Use production D1 bindings. + --binding D1 binding name in wrangler.jsonc. Default: "DB". + --concurrency Passed to contrail.refresh(). Default: 50. + --ignore-window Seconds of grace for stale-update detection. Default: 60. + --by-collection Print per-collection stats, not just totals. +`; + +interface Args { + cmd: Subcommand; + config?: string; + root: string; + remote: boolean; + binding: string; + concurrency?: number; + ignoreWindowMs?: number; + byCollection: boolean; +} + +function parseArgs(argv: string[]): Args { + const args = argv.slice(2); + const cmd = (args.shift() ?? "help") as Subcommand; + let config: string | undefined; + let root = process.cwd(); + let remote = false; + let binding = "DB"; + let concurrency: number | undefined; + let ignoreWindowMs: number | undefined; + let byCollection = false; + for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === "--config") config = args[++i]; + else if (a === "--root") root = args[++i]; + else if (a === "--remote") remote = true; + else if (a === "--binding") binding = args[++i]; + else if (a === "--concurrency") concurrency = parseInt(args[++i], 10); + else if (a === "--ignore-window") ignoreWindowMs = parseInt(args[++i], 10) * 1000; + else if (a === "--by-collection") byCollection = true; + else if (a === "-h" || a === "--help") + return { + cmd: "help", + config, + root, + remote, + binding, + concurrency, + ignoreWindowMs, + byCollection, + }; + } + return { cmd, config, root, remote, binding, concurrency, ignoreWindowMs, byCollection }; +} + +function resolveConfigPath(opts: Args): string | null { + const p = findConfigFile(opts.root, opts.config); + if (!p) { + console.error( + "Could not find a Contrail config. Pass --config or place one at\n" + + ` ${CONFIG_CANDIDATES_MESSAGE}` + ); + } + return p; +} + +function formatStats(s: CollectionStats): string { + return `${s.missing} missing, ${s.staleUpdates} stale updates, ${s.inSync} in sync`; +} + +function printRefreshReport(result: RefreshResult, byCollection: boolean): void { + console.log(""); + if (byCollection) { + console.log("by collection:"); + const entries = Object.entries(result.byCollection).sort(([a], [b]) => + a.localeCompare(b) + ); + for (const [nsid, stats] of entries) { + if (stats.missing === 0 && stats.staleUpdates === 0 && stats.inSync === 0) continue; + console.log(` ${nsid}`); + console.log(` ${formatStats(stats)}`); + } + console.log(""); + } + console.log("total:"); + console.log(` ${formatStats(result.total)}`); + console.log(` ${result.usersScanned} users scanned` + + (result.usersFailed ? `, ${result.usersFailed} failed` : "") + + ` in ${(result.elapsedMs / 1000).toFixed(1)}s`); + console.log(` (ignore window: ${(result.ignoreWindowMs / 1000).toFixed(0)}s)`); +} + +async function main(): Promise { + const opts = parseArgs(process.argv); + + if (opts.cmd === "help") { + process.stdout.write(USAGE); + return 0; + } + + if (opts.cmd === "backfill") { + const configPath = resolveConfigPath(opts); + if (!configPath) return 1; + const config = await loadConfig(configPath); + await backfillAll({ + config, + remote: opts.remote, + binding: opts.binding, + concurrency: opts.concurrency ?? 100, + }); + return 0; + } + + if (opts.cmd === "refresh") { + const configPath = resolveConfigPath(opts); + if (!configPath) return 1; + const config = await loadConfig(configPath); + const result = await refresh({ + config, + remote: opts.remote, + binding: opts.binding, + concurrency: opts.concurrency ?? 50, + ignoreWindowMs: opts.ignoreWindowMs, + }); + printRefreshReport(result, opts.byCollection); + return 0; + } + + console.error(USAGE); + return 1; +} + +main().then( + (code) => process.exit(code), + (err) => { + console.error(err); + process.exit(1); + } +); diff --git a/packages/contrail/src/contrail.ts b/packages/contrail/src/contrail.ts index 38f04c1..5f0fb72 100644 --- a/packages/contrail/src/contrail.ts +++ b/packages/contrail/src/contrail.ts @@ -5,8 +5,10 @@ import { queryRecords } from "./core/db/records"; import type { QueryOptions, SortOption } from "./core/db/records"; import { runIngestCycle, createIngestState } from "./core/jetstream"; import type { IngestState } from "./core/jetstream"; -import { discoverDIDs, backfillAll } from "./core/backfill"; +import { discoverDIDs, backfillPending } from "./core/backfill"; import type { BackfillAllOptions, BackfillProgress } from "./core/backfill"; +import { refresh as runRefresh } from "./core/refresh"; +import type { RefreshOptions, RefreshResult } from "./core/refresh"; import { processNotifyUris } from "./core/router/notify"; import type { NotifyResult } from "./core/router/notify"; import { runPersistent as runPersistentIngestion } from "./core/persistent"; @@ -114,20 +116,94 @@ export class Contrail { options?: BackfillAllOptions, db?: Database ): Promise { - return backfillAll(this.getDb(db), this.config, options); + return backfillPending(this.getDb(db), this.config, options); } - /** Discover + backfill in one call. */ - async sync( + /** Discover every DID with records in the configured collections, then + * backfill their history. Logs progress via `config.logger` — supply + * `onProgress` to take over output, or pass a no-op logger in the config + * to silence the defaults. */ + async backfillAll( options?: BackfillAllOptions, db?: Database ): Promise<{ discovered: number; backfilled: number }> { const d = this.getDb(db); + const logger = this.config.logger; + const startedAt = Date.now(); + + logger?.log?.("discovering users…"); const discovered = await this.discover(d); - const backfilled = await this.backfill(options, d); + logger?.log?.(` discovered ${discovered.length} users`); + + // Wrap the call with a throttled default progress logger when the + // caller hasn't supplied their own. Throttle at 2s so we don't spam in + // fast/local runs; final summary always prints. + let effective = options; + if (!options?.onProgress) { + let lastLogAt = 0; + effective = { + ...options, + onProgress: ({ records, usersComplete, usersTotal, usersFailed }) => { + const now = Date.now(); + if (now - lastLogAt < 2_000) return; + lastLogAt = now; + const failStr = usersFailed > 0 ? `, ${usersFailed} failed` : ""; + logger?.log?.( + ` ${records} records | ${usersComplete}/${usersTotal} users${failStr}` + ); + }, + }; + } + + logger?.log?.("backfilling…"); + const backfilled = await this.backfill(effective, d); + const elapsedS = ((Date.now() - startedAt) / 1000).toFixed(1); + logger?.log?.( + ` done: ${backfilled} records across ${discovered.length} users in ${elapsedS}s` + ); return { discovered: discovered.length, backfilled }; } + /** Fresh sweep: re-walk every known DID's PDS, compare each record against + * our DB, count anything missing or stale (outside the ignore window). + * Apply the deltas. Returns stats per-collection + totals. + * + * Progress logs via `config.logger` unless `onProgress` is supplied. */ + async refresh( + options?: RefreshOptions, + db?: Database + ): Promise { + const d = this.getDb(db); + const logger = this.config.logger; + + let effective = options; + if (!options?.onProgress) { + let lastLogAt = 0; + effective = { + ...options, + onProgress: ({ usersComplete, usersTotal, usersFailed, recordsScanned }) => { + const now = Date.now(); + if (now - lastLogAt < 2_000) return; + lastLogAt = now; + const failStr = usersFailed > 0 ? `, ${usersFailed} failed` : ""; + logger?.log?.( + ` ${recordsScanned} records scanned | ${usersComplete}/${usersTotal} users${failStr}` + ); + }, + }; + } + + logger?.log?.("refreshing…"); + const result = await runRefresh(d, this.config, effective); + const elapsedS = (result.elapsedMs / 1000).toFixed(1); + logger?.log?.( + ` done: ${result.total.missing} missing, ${result.total.staleUpdates} stale updates ` + + `across ${result.usersScanned} users in ${elapsedS}s` + + (result.usersFailed > 0 ? ` (${result.usersFailed} failed)` : "") + ); + return result; + } + /** Immediately fetch and index specific records from their PDS. */ async notify( uris: string | string[], diff --git a/packages/contrail/src/core/backfill.ts b/packages/contrail/src/core/backfill.ts index d938f68..3d23dc1 100644 --- a/packages/contrail/src/core/backfill.ts +++ b/packages/contrail/src/core/backfill.ts @@ -219,7 +219,7 @@ export interface BackfillAllOptions { onProgress?: (progress: BackfillProgress) => void; } -export async function backfillAll( +export async function backfillPending( db: Database, config: ContrailConfig, options?: BackfillAllOptions diff --git a/packages/contrail/src/core/db/records.ts b/packages/contrail/src/core/db/records.ts index b7ea175..7ac7d8a 100644 --- a/packages/contrail/src/core/db/records.ts +++ b/packages/contrail/src/core/db/records.ts @@ -332,6 +332,9 @@ export async function saveCursor( export interface ExistingRecordInfo { cid: string | null; record: string | null; + /** When the row was last written to our DB (microseconds). Populated + * whenever `lookupExistingRecords` runs, regardless of `includeRecord`. */ + indexed_at: number | null; } /** @@ -358,7 +361,7 @@ export async function lookupExistingRecords( byShort.set(short, uris); } - const selectCols = includeRecord ? "uri, cid, record" : "uri, cid"; + const selectCols = includeRecord ? "uri, cid, record, indexed_at" : "uri, cid, indexed_at"; for (const [short, uris] of byShort) { const table = recordsTableName(short); for (let i = 0; i < uris.length; i += 50) { @@ -367,11 +370,17 @@ export async function lookupExistingRecords( const rows = await db .prepare(`SELECT ${selectCols} FROM ${table} WHERE uri IN (${placeholders})`) .bind(...chunk) - .all<{ uri: string; cid: string | null; record?: string | null }>(); + .all<{ + uri: string; + cid: string | null; + record?: string | null; + indexed_at: number | null; + }>(); for (const row of rows.results ?? []) { result.set(row.uri, { cid: row.cid, record: includeRecord ? (row.record ?? null) : null, + indexed_at: row.indexed_at ?? null, }); } } diff --git a/packages/contrail/src/core/refresh.ts b/packages/contrail/src/core/refresh.ts new file mode 100644 index 0000000..aacedaa --- /dev/null +++ b/packages/contrail/src/core/refresh.ts @@ -0,0 +1,266 @@ +/** + * Fresh refresh: re-walk every known DID's PDS for every configured collection + * and reconcile against what's in our DB. Unlike `backfillPending`, this + * ignores the `backfills` state machine — it's a "check what we might have + * missed" pass, not a resumable bulk load. + * + * Two categories of delta are counted: + * - missing — the PDS has a record we don't + * - staleUpdates — we have the same URI but a different CID, *and* our + * copy's `indexed_at` is older than `ignoreWindowMs` + * + * The ignore window exists because Jetstream can run ~seconds behind the + * PDS; without the window, "in-sync but racy" writes would show up as + * misses every run. Records inside the window are still applied (they + * might be legit updates), just not counted toward stats. + * + * Typical uses: + * - dev: "I ran backfillAll on Monday, haven't touched it for a week, + * how much did jetstream miss?" + * - prod: "we had jetstream outage yesterday, what did we drop?" + */ +import { type Did, type Nsid } from "@atcute/lexicons"; +import { isDid, isNsid } from "@atcute/lexicons/syntax"; + +import type { Client } from "@atcute/client"; +import type { ContrailConfig, Database, IngestEvent } from "./types.js"; +import { applyEvents, lookupExistingRecords } from "./db/records.js"; +import { getClient } from "./client.js"; + +const PAGE_SIZE = 100; +const REQUEST_TIMEOUT_MS = 10_000; + +async function withTimeout(fn: () => Promise, ms: number): Promise { + return Promise.race([ + fn(), + new Promise((_, rej) => + setTimeout(() => rej(new Error(`timeout after ${ms}ms`)), ms) + ), + ]); +} + +export interface CollectionStats { + /** Record exists on PDS but was absent from our DB. */ + missing: number; + /** Record exists in our DB with a different CID than the PDS, and our + * copy was written before the ignore window. */ + staleUpdates: number; + /** Record is present and matches (same CID, or within ignore window). */ + inSync: number; +} + +export interface RefreshProgress { + usersComplete: number; + usersTotal: number; + usersFailed: number; + recordsScanned: number; +} + +export interface RefreshResult { + /** Per-NSID stats. */ + byCollection: Record; + /** Sum across every NSID. */ + total: CollectionStats; + usersScanned: number; + usersFailed: number; + /** Effective ignore window used for classification, in ms. */ + ignoreWindowMs: number; + /** Wall-clock runtime, in ms. */ + elapsedMs: number; +} + +export interface RefreshOptions { + /** How many DIDs to fan out against in parallel. Default: 50. */ + concurrency?: number; + /** Records whose local `indexed_at` is within this window of `now` are + * still upserted but excluded from `staleUpdates` counts — guards + * against jetstream being briefly behind the PDS. Default: 60_000 ms. */ + ignoreWindowMs?: number; + /** Override which NSIDs to walk. Default: every `config.collections[*].collection`. */ + nsids?: string[]; + /** Optional progress callback (fires per completed DID). */ + onProgress?: (p: RefreshProgress) => void; + /** Max attempts per listRecords request. Default: 3. */ + maxRetries?: number; + /** Per-request timeout in ms. Default: 10000. */ + requestTimeout?: number; +} + +function emptyStats(): CollectionStats { + return { missing: 0, staleUpdates: 0, inSync: 0 }; +} + +export async function refresh( + db: Database, + config: ContrailConfig, + options?: RefreshOptions +): Promise { + const concurrency = options?.concurrency ?? 50; + const ignoreWindowMs = options?.ignoreWindowMs ?? 60_000; + const requestTimeout = options?.requestTimeout ?? REQUEST_TIMEOUT_MS; + const maxRetries = options?.maxRetries ?? 3; + const startedAt = Date.now(); + + // Default to every configured collection NSID. Profiles are already + // included because `resolveConfig` adds them to `config.collections`. + const nsids = + options?.nsids ?? + Object.values(config.collections).map((c) => c.collection); + + const byCollection: Record = {}; + for (const nsid of nsids) byCollection[nsid] = emptyStats(); + const total: CollectionStats = emptyStats(); + + // Known DIDs = every author we've ever written for. `backfills` is a + // superset (it also includes failed/pending users that we never got + // records from), which is actually what we want — if we tried and + // failed before, we might succeed now. + const didRows = await db + .prepare("SELECT DISTINCT did FROM backfills") + .all<{ did: string }>(); + const dids = (didRows.results ?? []) + .map((r) => r.did) + .filter((d) => isDid(d)); + + const usersTotal = dids.length; + let usersComplete = 0; + let usersFailed = 0; + let recordsScanned = 0; + + const ignoreBeforeUs = (Date.now() - ignoreWindowMs) * 1000; + + const processDid = async (did: string): Promise => { + let client: Client; + try { + client = await withTimeout( + () => getClient(did as Did, db), + requestTimeout + ); + } catch { + usersFailed++; + return; + } + + for (const nsid of nsids) { + if (!isNsid(nsid)) continue; + let cursor: string | undefined; + while (true) { + let pageRecords: Array<{ uri: string; cid: string; value: unknown }>; + let nextCursor: string | undefined; + try { + // Retry listRecords: transient PDS failures are expected during refresh + let attempt = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const res = await withTimeout( + () => + client.get("com.atproto.repo.listRecords", { + params: { + repo: did as Did, + collection: nsid as Nsid, + limit: PAGE_SIZE, + cursor, + }, + }), + requestTimeout + ); + if (!res.ok) { + // 400s on a collection the user doesn't have are fine; stop + // paging this collection for this user. + pageRecords = []; + nextCursor = undefined; + break; + } + pageRecords = res.data.records; + nextCursor = res.data.cursor ?? undefined; + break; + } catch (err) { + if (attempt >= maxRetries) throw err; + attempt++; + await new Promise((r) => setTimeout(r, 500 * 2 ** attempt)); + } + } + } catch { + // Give up on this collection for this user; keep going. + break; + } + + if (pageRecords.length === 0) break; + + const now = Date.now(); + const events: IngestEvent[] = pageRecords.map((r) => ({ + uri: r.uri, + did, + collection: nsid, + rkey: r.uri.split("/").pop()!, + operation: "create" as const, + cid: r.cid, + record: JSON.stringify(r.value), + time_us: now * 1000, + indexed_at: now * 1000, + })); + + const existing = await lookupExistingRecords( + db, + events.map((e) => ({ uri: e.uri, collection: e.collection })), + false, + config + ); + + for (const ev of events) { + const ex = existing.get(ev.uri); + if (!ex) { + byCollection[nsid].missing++; + total.missing++; + } else if (ex.cid !== ev.cid) { + const inWindow = + ex.indexed_at !== null && ex.indexed_at >= ignoreBeforeUs; + if (inWindow) { + byCollection[nsid].inSync++; + total.inSync++; + } else { + byCollection[nsid].staleUpdates++; + total.staleUpdates++; + } + } else { + byCollection[nsid].inSync++; + total.inSync++; + } + } + + // Upsert everything — even records "inside the ignore window" + // might genuinely have a new CID; we just don't count them as a + // miss-signal. Skip feed fanout since this is a catch-up, not a + // user-visible write. + await applyEvents(db, events, config, { skipFeedFanout: true }); + recordsScanned += events.length; + + cursor = nextCursor; + if (!cursor) break; + } + } + + usersComplete++; + options?.onProgress?.({ + usersComplete, + usersTotal, + usersFailed, + recordsScanned, + }); + }; + + for (let i = 0; i < dids.length; i += concurrency) { + const batch = dids.slice(i, i + concurrency); + await Promise.allSettled(batch.map(processDid)); + } + + return { + byCollection, + total, + usersScanned: usersComplete, + usersFailed, + ignoreWindowMs, + elapsedMs: Date.now() - startedAt, + }; +} diff --git a/packages/contrail/src/index.ts b/packages/contrail/src/index.ts index feff577..6665862 100644 --- a/packages/contrail/src/index.ts +++ b/packages/contrail/src/index.ts @@ -26,6 +26,12 @@ export { resolveConfig, validateConfig } from "./core/types"; export type { QueryOptions, SortOption } from "./core/db/records"; export type { BackfillProgress, BackfillAllOptions } from "./core/backfill"; export type { NotifyResult } from "./core/router/notify"; +export type { + RefreshOptions, + RefreshResult, + RefreshProgress, + CollectionStats, +} from "./core/refresh"; export { runPersistent } from "./core/persistent"; export type { PersistentIngestOptions } from "./core/persistent"; diff --git a/packages/contrail/src/workers/backfill.ts b/packages/contrail/src/workers/backfill.ts new file mode 100644 index 0000000..5f0ae6c --- /dev/null +++ b/packages/contrail/src/workers/backfill.ts @@ -0,0 +1,97 @@ +/** + * Wrangler-backed helpers for Cloudflare Workers deployments: + * + * - `backfillAll` — one-time bulk load from scratch (uses the `backfills` + * state table to resume across runs). + * - `refresh` — reconcile every known DID's PDS against our DB, report + * what's missing or stale. Use after outages or long idle periods. + * + * Both dynamically import `wrangler` (optional peer dep) and wire it to + * the user's D1 binding, then dispose the proxy on exit. + */ +import { Contrail } from "../contrail.js"; +import type { ContrailConfig, Database } from "../core/types.js"; +import type { BackfillAllOptions } from "../core/backfill.js"; +import type { RefreshOptions, RefreshResult } from "../core/refresh.js"; + +interface WranglerCommon { + config: ContrailConfig; + /** Use production D1 bindings. Equivalent to + * `getPlatformProxy({ environment: "production" })`. */ + remote?: boolean; + /** Name of the D1 binding in wrangler.jsonc. Default: `"DB"`. */ + binding?: string; +} + +export interface BackfillAllViaWranglerOptions extends WranglerCommon { + /** Passed through to `contrail.backfillAll()`. Default: 100. */ + concurrency?: number; + /** Override the built-in progress logging. */ + onProgress?: BackfillAllOptions["onProgress"]; +} + +export interface RefreshViaWranglerOptions extends WranglerCommon { + /** Passed through to `contrail.refresh()`. Default: 50. */ + concurrency?: number; + /** Ignore-window for stale-update classification, in ms. Default: 60_000. */ + ignoreWindowMs?: number; + /** Override the built-in progress logging. */ + onProgress?: RefreshOptions["onProgress"]; +} + +async function withWrangler( + opts: WranglerCommon, + fn: (contrail: Contrail, db: Database) => Promise +): Promise { + // Dynamic import so wrangler is only required when these helpers are + // actually called — keeps the main package usable in runtimes without it. + const { getPlatformProxy } = await import("wrangler"); + const binding = opts.binding ?? "DB"; + + const { env, dispose } = await getPlatformProxy({ + environment: opts.remote ? "production" : undefined, + }); + + const db = (env as Record)[binding] as Database | undefined; + if (!db) { + await dispose(); + throw new Error( + `No binding named "${binding}" in wrangler env. Add a d1_databases ` + + `entry to wrangler.jsonc, or pass { binding: "..." }.` + ); + } + + try { + const contrail = new Contrail(opts.config); + await contrail.init(db); + return await fn(contrail, db); + } finally { + await dispose(); + } +} + +export async function backfillAll( + opts: BackfillAllViaWranglerOptions +): Promise<{ discovered: number; backfilled: number }> { + return withWrangler(opts, (contrail, db) => + contrail.backfillAll( + { concurrency: opts.concurrency ?? 100, onProgress: opts.onProgress }, + db + ) + ); +} + +export async function refresh( + opts: RefreshViaWranglerOptions +): Promise { + return withWrangler(opts, (contrail, db) => + contrail.refresh( + { + concurrency: opts.concurrency ?? 50, + ignoreWindowMs: opts.ignoreWindowMs, + onProgress: opts.onProgress, + }, + db + ) + ); +} diff --git a/packages/contrail/tsup.config.ts b/packages/contrail/tsup.config.ts index e1ce7fa..fc11602 100644 --- a/packages/contrail/tsup.config.ts +++ b/packages/contrail/tsup.config.ts @@ -6,11 +6,14 @@ export default defineConfig({ "src/server.ts", "src/adapters/sqlite.ts", "src/adapters/postgres.ts", + "src/workers/backfill.ts", + "src/cli.ts", + "src/cli-config.ts", ], format: ["esm"], dts: true, sourcemap: true, clean: true, tsconfig: "tsconfig.build.json", - external: ["node:sqlite", "pg"], + external: ["node:sqlite", "pg", "wrangler"], }); diff --git a/packages/lexicons/package.json b/packages/lexicons/package.json index 547d36f..ab4f65f 100644 --- a/packages/lexicons/package.json +++ b/packages/lexicons/package.json @@ -46,10 +46,10 @@ "test": "vitest run" }, "dependencies": { - "@atcute/lex-cli": "^2.5.3" + "@atcute/lex-cli": "^2.5.3", + "@atmo-dev/contrail": "workspace:*" }, "devDependencies": { - "@atmo-dev/contrail": "workspace:*", "@types/node": "^25.5.0", "tsup": "^8.5.0", "typescript": "^5.7.3", diff --git a/packages/lexicons/src/cli.ts b/packages/lexicons/src/cli.ts index fb29109..7c42bff 100644 --- a/packages/lexicons/src/cli.ts +++ b/packages/lexicons/src/cli.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node /** - * contrail-lex — CLI for generating lexicons from a Contrail config and - * (optionally) typegenning them via @atcute/lex-cli. + * contrail-lex — CLI for generating, typegenning, and publishing lexicons. * * Usage from a consumer project: * @@ -9,18 +8,24 @@ * contrail-lex pull # wraps `lex-cli pull` * contrail-lex types # wraps `lex-cli generate` * contrail-lex all [--no-types] [--config ...] # generate → pull → types + * contrail-lex publish [handle] [password] # publish lexicons to a PDS * - * Looks for a user config via --config , else ./contrail.config.ts, - * ./app/config.ts, ./src/lib/contrail/config.ts (first match wins). The - * config file must default-export or named-export `config: ContrailConfig`. + * Config auto-detects at ./contrail.config.ts, ./app/config.ts, or + * ./src/lib/contrail/config.ts (first match wins); override with --config. + * The file must default-export or named-export `config: ContrailConfig`. */ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { resolve, join } from "node:path"; -import { pathToFileURL } from "node:url"; +import { + findConfigFile, + loadConfig, + CONFIG_CANDIDATES_MESSAGE, +} from "@atmo-dev/contrail/cli-config"; import { generateLexicons } from "./generate.js"; +import { publishLexicons } from "./publish.js"; -type Subcommand = "generate" | "pull" | "types" | "all" | "help"; +type Subcommand = "generate" | "pull" | "types" | "all" | "publish" | "help"; const USAGE = `contrail-lex [options] @@ -29,14 +34,20 @@ Subcommands: pull Pull external lexicons (wraps \`lex-cli pull\`) types Generate TS types from lexicon JSON (wraps \`lex-cli generate\`) all generate → pull → generate → pull → types (full pipeline) + publish Publish lexicon JSON as com.atproto.lexicon.schema records on a PDS help Print this message Options: - --config Path to Contrail config file (TS or JS, default export - or named \`config\`). Default: auto-detect. - --root Project root (where lexicons/ and node_modules/ live). - Default: CWD. - --no-types In \`all\`, skip the final type-generation step. + --config Path to Contrail config file. Default: auto-detect. + --root Project root (where lexicons/ and node_modules/ live). Default: CWD. + --no-types In \`all\`, skip the final type-generation step. + --generated-dir For \`publish\`: dir of JSON to publish. Default: lexicons/generated. + --skip-confirm For \`publish\`: skip the "do you control these zones?" prompt. + --dry-run For \`publish\`: print what would be published + the DNS records needed, no writes. + +Environment variables (for \`publish\`): + LEXICON_ACCOUNT_IDENTIFIER handle or DID (falls back to positional arg 1) + LEXICON_ACCOUNT_PASSWORD app password (falls back to positional arg 2) `; function parseArgs(argv: string[]): { @@ -44,54 +55,35 @@ function parseArgs(argv: string[]): { config?: string; root: string; withTypes: boolean; + generatedDir: string; + skipConfirm: boolean; + dryRun: boolean; + positional: string[]; } { const args = argv.slice(2); const cmd = (args.shift() ?? "help") as Subcommand; let config: string | undefined; let root = process.cwd(); let withTypes = true; + let generatedDir = "lexicons/generated"; + let skipConfirm = false; + let dryRun = false; + const positional: string[] = []; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === "--config") config = args[++i]; else if (a === "--root") root = args[++i]; else if (a === "--no-types") withTypes = false; - else if (a === "-h" || a === "--help") return { cmd: "help", root, withTypes: true }; + else if (a === "--generated-dir") generatedDir = args[++i]; + else if (a === "--skip-confirm") skipConfirm = true; + else if (a === "--dry-run") dryRun = true; + else if (a === "-h" || a === "--help") + return { cmd: "help", root, withTypes: true, generatedDir, skipConfirm, dryRun, positional }; + else positional.push(a); } - return { cmd, config, root, withTypes }; + return { cmd, config, root, withTypes, generatedDir, skipConfirm, dryRun, positional }; } -function findConfigFile(root: string, explicit?: string): string | null { - if (explicit) { - const p = resolve(root, explicit); - return existsSync(p) ? p : null; - } - const candidates = [ - "contrail.config.ts", - "contrail.config.js", - "app/config.ts", - "src/lib/contrail/config.ts", - ]; - for (const c of candidates) { - const p = join(root, c); - if (existsSync(p)) return p; - } - return null; -} - -async function loadConfig(path: string): Promise { - // TS files are loaded via `tsx` if available; otherwise fall back to the - // Node loader (works for .js). We use dynamic import. - if (path.endsWith(".ts") || path.endsWith(".mts")) { - // `tsx` registers a hook when invoked via `tsx `. If this CLI is - // run through tsx (e.g. `npx tsx contrail-lex ...`), the import below - // will just work. Otherwise we spawn a tsx child below — but dynamic - // import is the preferred path for a programmatic config load. - const mod = await import(pathToFileURL(path).href); - return mod.config ?? mod.default; - } - const mod = await import(pathToFileURL(path).href); - return mod.config ?? mod.default; -} function runLexCli(args: string[], cwd: string): number { const result = spawnSync("npx", ["lex-cli", ...args], { @@ -104,20 +96,55 @@ function runLexCli(args: string[], cwd: string): number { async function cmdGenerate(configPath: string, root: string): Promise { const config = await loadConfig(configPath); - if (!config || typeof config !== "object") { - throw new Error(`Config at ${configPath} did not export a \`config\` object`); - } generateLexicons({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: config as any, + config, rootDir: root, - outputDir: join(root, "lexicons-generated"), + outputDir: join(root, "lexicons", "generated"), writeRuntimeFiles: true, }); } +async function cmdPublish( + root: string, + generatedDir: string, + positional: string[], + skipConfirm: boolean, + dryRun: boolean +): Promise { + const identifier = positional[0] ?? process.env.LEXICON_ACCOUNT_IDENTIFIER; + const password = positional[1] ?? process.env.LEXICON_ACCOUNT_PASSWORD; + if (!dryRun && (!identifier || !password)) { + console.error( + "missing credentials. pass positional or set\n" + + "LEXICON_ACCOUNT_IDENTIFIER and LEXICON_ACCOUNT_PASSWORD.\n" + + "(not required with --dry-run)" + ); + return 1; + } + const dir = resolve(root, generatedDir); + if (!existsSync(dir)) { + console.error(`generated lexicons dir not found: ${dir}`); + return 1; + } + const result = await publishLexicons({ + generatedDir: dir, + identifier, + password, + skipConfirm, + dryRun, + }); + if (!dryRun) { + console.log( + `published ${result.published} lexicon(s)` + + (result.failed.length ? `, ${result.failed.length} failed` : "") + ); + } + return result.failed.length ? 1 : 0; +} + async function main(): Promise { - const { cmd, config, root, withTypes } = parseArgs(process.argv); + const { cmd, config, root, withTypes, generatedDir, skipConfirm, dryRun, positional } = + parseArgs(process.argv); if (cmd === "help") { process.stdout.write(USAGE); @@ -126,12 +153,14 @@ async function main(): Promise { if (cmd === "pull") return runLexCli(["pull"], root); if (cmd === "types") return runLexCli(["generate"], root); + if (cmd === "publish") + return cmdPublish(root, generatedDir, positional, skipConfirm, dryRun); const configPath = findConfigFile(root, config); if (!configPath) { console.error( "Could not find a Contrail config. Pass --config or place one at\n" + - " contrail.config.ts | app/config.ts | src/lib/contrail/config.ts" + ` ${CONFIG_CANDIDATES_MESSAGE}` ); return 1; } diff --git a/packages/lexicons/src/generate.ts b/packages/lexicons/src/generate.ts index 276798d..d4e6f48 100644 --- a/packages/lexicons/src/generate.ts +++ b/packages/lexicons/src/generate.ts @@ -123,8 +123,8 @@ interface ReferenceDef { export function generateLexicons(options: GenerateOptions): Record { const { config, rootDir, outputDir, quiet } = options; const lexiconDirs = options.lexiconDirs ?? [ - join(rootDir, "lexicons"), - join(rootDir, "lexicons-pulled"), + join(rootDir, "lexicons", "custom"), + join(rootDir, "lexicons", "pulled"), ]; const log = quiet ? () => {} : console.log; @@ -1035,7 +1035,7 @@ export function generateLexicons(options: GenerateOptions): Record/com.atproto.lexicon.schema/`. * * Exposed as a library function so downstream deployments can wrap it with a - * one-line script pointing at their own `lexicons-generated/` directory. + * one-line script pointing at their own `lexicons/generated/` directory. */ import { readFileSync, readdirSync, statSync } from "node:fs"; @@ -110,12 +110,15 @@ export function nsidRootDomain(nsid: string): string { export interface PublishOptions { /** Directory to walk for `*.json` lexicon files. */ generatedDir: string; - /** PDS account handle or DID. */ - identifier: string; - /** App password (or equivalent on a non-bsky PDS). */ - password: string; + /** PDS account handle or DID. Not required when `dryRun` is true. */ + identifier?: string; + /** App password (or equivalent on a non-bsky PDS). Not required when `dryRun` is true. */ + password?: string; /** Skip the interactive "do you control these zones?" prompt. */ skipConfirm?: boolean; + /** Print what would be published + the DNS records that would be required, + * then return without logging in or writing anything to the PDS. */ + dryRun?: boolean; } export async function publishLexicons(opts: PublishOptions): Promise<{ @@ -139,7 +142,14 @@ export async function publishLexicons(opts: PublishOptions): Promise<{ } const sortedAuthorities = [...authorities].sort(); - console.log(`About to publish ${lexicons.length} lexicons from ${opts.generatedDir}.\n`); + const header = opts.dryRun + ? `[dry-run] Would publish ${lexicons.length} lexicons from ${opts.generatedDir}:` + : `About to publish ${lexicons.length} lexicons from ${opts.generatedDir}.`; + console.log(`${header}\n`); + if (opts.dryRun) { + for (const lex of lexicons) console.log(` ${lex.id}`); + console.log(""); + } console.log( `⚠ Do you control ${rootDomains.size === 1 ? "the DNS zone" : "these DNS zones"} below?` ); @@ -154,6 +164,29 @@ export async function publishLexicons(opts: PublishOptions): Promise<{ " requires a valid TXT record.\n" ); + if (opts.dryRun) { + console.log("─────────────────────────────────────────────────"); + console.log("DNS TXT records that would be needed for NSID resolution"); + console.log("─────────────────────────────────────────────────"); + console.log( + "Atproto NSID resolution does NOT walk up — each distinct authority\n" + + "needs its own TXT record. One record per unique authority below.\n" + + "(DID would be filled in at publish time, once logged in.)\n" + ); + for (const authority of sortedAuthorities) { + console.log(` host: _lexicon.${authority}`); + console.log(` value: did=`); + console.log(""); + } + console.log(`Total: ${sortedAuthorities.length} DNS TXT record(s) to add.`); + console.log("\n[dry-run] No PDS login or writes performed."); + return { published: 0, failed: [], authorities: sortedAuthorities, session: null as never }; + } + + if (!opts.identifier || !opts.password) { + throw new Error("`identifier` and `password` are required when `dryRun` is false"); + } + if (!opts.skipConfirm) { const rl = createInterface({ input, output }); const answer = (await rl.question("Continue? [y/N] ")).trim().toLowerCase(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89a1941..acce499 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,20 +25,14 @@ importers: version: link:../../packages/contrail devDependencies: '@atcute/lex-cli': - specifier: ^2.5.3 + specifier: ^2.8.1 version: 2.8.1 - '@atcute/lexicon-doc': - specifier: ^2.1.2 - version: 2.2.0 '@atmo-dev/contrail-lexicons': specifier: workspace:* version: link:../../packages/lexicons '@cloudflare/workers-types': specifier: ^4.20250124.0 version: 4.20260424.1 - tsx: - specifier: ^4.21.0 - version: 4.21.0 typescript: specifier: ^5.7.3 version: 5.9.3 @@ -62,13 +56,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 +105,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 +162,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 +239,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 +288,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 +345,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) @@ -385,6 +379,9 @@ importers: hono: specifier: ^4.12.8 version: 4.12.15 + jiti: + specifier: ^2.4.0 + version: 2.6.1 devDependencies: '@cloudflare/workers-types': specifier: ^4.20250124.0 @@ -400,41 +397,44 @@ 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)) + wrangler: + specifier: ^4.63.0 + version: 4.84.1(@cloudflare/workers-types@4.20260424.1) packages/lexicons: dependencies: '@atcute/lex-cli': specifier: ^2.5.3 version: 2.8.1 - devDependencies: '@atmo-dev/contrail': specifier: workspace:* version: link:../contrail + devDependencies: '@types/node': specifier: ^25.5.0 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 +1105,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 +1365,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 +1462,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 +1656,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 +2590,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 +4195,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 +4219,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 +4244,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 +4267,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 +4283,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 +4659,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 +4682,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 +4765,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 +5123,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 +5194,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 +5928,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 +6149,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 +6294,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 +6384,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 +6395,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 +6475,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 +6488,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 +6513,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